From 77b3a1d9f5cdd26380f06ff31a47e1dc14cbce81 Mon Sep 17 00:00:00 2001 From: Aswathi Balagopal Date: Mon, 5 May 2025 19:35:20 -0500 Subject: [PATCH 1/5] Changes made to data handlers, sob_terms and time_profile to make custom time profile work --- build/lib/mla/__init__.py | 14 + build/lib/mla/analysis.py | 99 +++ build/lib/mla/configurable.py | 23 + build/lib/mla/core.py | 9 + build/lib/mla/data_handlers.py | 402 ++++++++++ build/lib/mla/minimizers.py | 120 +++ build/lib/mla/params.py | 67 ++ build/lib/mla/sob_terms.py | 355 +++++++++ build/lib/mla/sources.py | 96 +++ build/lib/mla/test_statistics.py | 206 ++++++ build/lib/mla/threeml/IceCubeLike.py | 978 +++++++++++++++++++++++++ build/lib/mla/threeml/__init__.py | 6 + build/lib/mla/threeml/data_handlers.py | 179 +++++ build/lib/mla/threeml/profilellh.py | 100 +++ build/lib/mla/threeml/sob_terms.py | 504 +++++++++++++ build/lib/mla/threeml/spectral.py | 157 ++++ build/lib/mla/time_profiles.py | 632 ++++++++++++++++ build/lib/mla/trial_generators.py | 115 +++ build/lib/mla/utility_functions.py | 255 +++++++ dist/mla-1.4.0-py3.12.egg | Bin 0 -> 109273 bytes mla/data_handlers.py | 22 +- mla/threeml/IceCubeLike.py | 8 +- mla/threeml/sob_terms.py | 19 +- mla/time_profiles.py | 25 +- 24 files changed, 4365 insertions(+), 26 deletions(-) create mode 100644 build/lib/mla/__init__.py create mode 100644 build/lib/mla/analysis.py create mode 100644 build/lib/mla/configurable.py create mode 100644 build/lib/mla/core.py create mode 100644 build/lib/mla/data_handlers.py create mode 100644 build/lib/mla/minimizers.py create mode 100644 build/lib/mla/params.py create mode 100644 build/lib/mla/sob_terms.py create mode 100644 build/lib/mla/sources.py create mode 100644 build/lib/mla/test_statistics.py create mode 100644 build/lib/mla/threeml/IceCubeLike.py create mode 100644 build/lib/mla/threeml/__init__.py create mode 100644 build/lib/mla/threeml/data_handlers.py create mode 100644 build/lib/mla/threeml/profilellh.py create mode 100644 build/lib/mla/threeml/sob_terms.py create mode 100644 build/lib/mla/threeml/spectral.py create mode 100644 build/lib/mla/time_profiles.py create mode 100644 build/lib/mla/trial_generators.py create mode 100644 build/lib/mla/utility_functions.py create mode 100644 dist/mla-1.4.0-py3.12.egg diff --git a/build/lib/mla/__init__.py b/build/lib/mla/__init__.py new file mode 100644 index 00000000..b67284b6 --- /dev/null +++ b/build/lib/mla/__init__.py @@ -0,0 +1,14 @@ +"""__init__.py""" +# flake8: noqa +from .analysis import * +from .core import * +from .configurable import * +from .data_handlers import * +from .minimizers import * +from .params import * +from .sob_terms import * +from .sources import * +from .test_statistics import * +from .time_profiles import * +from .trial_generators import * +from .utility_functions import * diff --git a/build/lib/mla/analysis.py b/build/lib/mla/analysis.py new file mode 100644 index 00000000..423bad84 --- /dev/null +++ b/build/lib/mla/analysis.py @@ -0,0 +1,99 @@ +"""Docstring""" + +__author__ = 'John Evans and Jason Fan' +__copyright__ = 'Copyright 2024' +__credits__ = ['John Evans', 'Jason Fan', 'Michael Larson'] +__license__ = 'Apache License 2.0' +__version__ = '1.4.1' +__maintainer__ = 'Jason Fan' +__email__ = 'klfan@terpmail.umd.edu' +__status__ = 'Development' +from typing import List, Tuple, Type +from dataclasses import dataclass, field + +from .core import generate_default_config +from .data_handlers import DataHandler +from .minimizers import Minimizer +from .params import Params +from .sob_terms import SoBTermFactory +from .sources import PointSource +from .test_statistics import LLHTestStatisticFactory +from .trial_generators import SingleSourceTrialGenerator + + +@dataclass +class SingleSourceLLHAnalysis: + """Docstring""" + config: dict + minimizer_class: Type[Minimizer] + sob_term_factories: List[SoBTermFactory] + data_handler_source: Tuple[DataHandler, PointSource] + _sob_term_factories: List[SoBTermFactory] = field(init=False, repr=False) + _data_handler_source: Tuple[DataHandler, PointSource] = field(init=False, repr=False) + _trial_generator: SingleSourceTrialGenerator = field(init=False, repr=False) + _test_statistic_factory: LLHTestStatisticFactory = field(init=False, repr=False) + + def produce_and_minimize( + self, + params: Params, + fitting_params: List[str], + n_signal: float = 0, + ) -> tuple: + """Docstring""" + trial = self._trial_generator(n_signal=n_signal) + test_statistic = self._test_statistic_factory(params, trial) + minimizer = self.minimizer_class( + self.config[self.minimizer_class.__name__], test_statistic) + return minimizer(fitting_params) + + def generate_params(self) -> Params: + """Docstring""" + return self._test_statistic_factory.generate_params() + + @property + def test_statistic_factory(self) -> LLHTestStatisticFactory: + """Docstring""" + return self._test_statistic_factory + + @property + def trial_generator(self) -> SingleSourceTrialGenerator: + """Docstring""" + return self._trial_generator + + @property + def sob_term_factories(self) -> List[SoBTermFactory]: + """Docstring""" + return self._sob_term_factories + + @sob_term_factories.setter + def sob_term_factories(self, sob_term_factories: List[SoBTermFactory]) -> None: + """Docstring""" + self._sob_term_factories = sob_term_factories + self._test_statistic_factory = LLHTestStatisticFactory( # pylint: disable=too-many-function-args + self.config['LLHTestStatisticFactory'], + self._sob_term_factories, + ) + + @property + def data_handler_source(self) -> Tuple[DataHandler, PointSource]: + """Docstring""" + return self._data_handler_source + + @data_handler_source.setter + def data_handler_source( + self, data_handler_source: Tuple[DataHandler, PointSource]) -> None: + """Docstring""" + self._data_handler_source = data_handler_source + self._trial_generator = SingleSourceTrialGenerator( + self.config['SingleSourceTrialGenerator'], + *self._data_handler_source, + ) + + @classmethod + def generate_default_config(cls, minimizer_class: Type[Minimizer]) -> dict: + """Docstring""" + return generate_default_config([ + minimizer_class, + SingleSourceTrialGenerator, + LLHTestStatisticFactory, + ]) diff --git a/build/lib/mla/configurable.py b/build/lib/mla/configurable.py new file mode 100644 index 00000000..2db30e66 --- /dev/null +++ b/build/lib/mla/configurable.py @@ -0,0 +1,23 @@ +"""Docstring""" + +__author__ = 'John Evans and Jason Fan' +__copyright__ = 'Copyright 2024' +__credits__ = ['John Evans', 'Jason Fan', 'Michael Larson'] +__license__ = 'Apache License 2.0' +__version__ = '1.4.1' +__maintainer__ = 'Jason Fan' +__email__ = 'klfan@terpmail.umd.edu' +__status__ = 'Development' + +import dataclasses + + +@dataclasses.dataclass +class Configurable: + """Docstring""" + config: dict + + @classmethod + def generate_config(cls) -> dict: + """Docstring""" + return {} diff --git a/build/lib/mla/core.py b/build/lib/mla/core.py new file mode 100644 index 00000000..d79be033 --- /dev/null +++ b/build/lib/mla/core.py @@ -0,0 +1,9 @@ +"""Docstring""" + +from .configurable import Configurable + + +def generate_default_config(classes: list) -> dict: + """Docstring""" + return { + c.__name__: c.generate_config() for c in classes if issubclass(c, Configurable)} diff --git a/build/lib/mla/data_handlers.py b/build/lib/mla/data_handlers.py new file mode 100644 index 00000000..8fa2b31a --- /dev/null +++ b/build/lib/mla/data_handlers.py @@ -0,0 +1,402 @@ +"""Docstring""" + +__author__ = 'John Evans and Jason Fan' +__copyright__ = 'Copyright 2024' +__credits__ = ['John Evans', 'Jason Fan', 'Michael Larson'] +__license__ = 'Apache License 2.0' +__version__ = '1.4.1' +__maintainer__ = 'Jason Fan' +__email__ = 'klfan@terpmail.umd.edu' +__status__ = 'Development' + +from typing import Tuple + +import abc +import copy +from dataclasses import dataclass +from dataclasses import field + +import numpy as np +import numpy.lib.recfunctions as rf +from scipy.interpolate import UnivariateSpline as Spline + +from . import configurable +from .time_profiles import GenericProfile + + +@dataclass +class DataHandler(configurable.Configurable): + """Docstring""" + _n_background: float = field(init=False, repr=False) + __metaclass__ = abc.ABCMeta + + @abc.abstractmethod + def sample_background(self, n: int, rng: np.random.Generator) -> np.ndarray: + """Docstring""" + + @abc.abstractmethod + def sample_signal(self, n: int, rng: np.random.Generator) -> np.ndarray: + """Docstring""" + + @abc.abstractmethod + def calculate_n_signal(self, time_integrated_flux: float) -> float: + """Docstring""" + + @abc.abstractmethod + def evaluate_background_sindec_pdf(self, events: np.ndarray) -> np.ndarray: + """Docstring""" + + @abc.abstractmethod + def build_background_sindec_logenergy_histogram(self, bins: np.ndarray) -> np.ndarray: + """Docstring""" + + @abc.abstractmethod + def build_signal_sindec_logenergy_histogram( + self, gamma: float, bins: np.ndarray) -> np.ndarray: + """Docstring""" + + @property + @abc.abstractmethod + def n_background(self) -> float: + """Docstring""" + + +@dataclass +class NuSourcesDataHandler(DataHandler): + """Docstring""" + sim: np.ndarray + data_grl: Tuple[np.ndarray, np.ndarray] + + _sim: np.ndarray = field(init=False, repr=False) + _full_sim: np.ndarray = field(init=False, repr=False) + _data: np.ndarray = field(init=False, repr=False) + _grl: np.ndarray = field(init=False, repr=False) + _n_background: float = field(init=False, repr=False) + _grl_rates: np.ndarray = field(init=False, repr=False) + _dec_spline: Spline = field(init=False, repr=False) + _livetime: float = field(init=False, repr=False) + _sin_dec_bins: np.ndarray = field(init=False, repr=False) + + def sample_background(self, n: int, rng: np.random.Generator) -> np.ndarray: + """Docstring""" + return rng.choice(self._data, n) + + def sample_signal(self, n: int, rng: np.random.Generator) -> np.ndarray: + """Docstring""" + return rng.choice( + self.sim, + n, + p=self.sim['weight'] / self.sim['weight'].sum(), + replace=False, + ) + + def calculate_n_signal(self, time_integrated_flux: float) -> float: + """Docstring""" + return self.sim['weight'].sum() * time_integrated_flux + + def evaluate_background_sindec_pdf(self, events: np.ndarray) -> np.ndarray: + """Calculates the background probability of events based on their dec. + + Args: + events: An array of events including their declination. + + Returns: + The value for the background space pdf for the given events decs. + """ + return (1 / (2 * np.pi)) * self._dec_spline(events['sindec']) + + def build_background_sindec_logenergy_histogram(self, bins: np.ndarray) -> np.ndarray: + """Docstring""" + return np.histogram2d( + self._data['sindec'], + self._data['logE'], + bins=bins, + density=True, + )[0] + + def build_mcbackground_sindec_logenergy_histogram( + self, + bins: np.ndarray, + mcbkgname: str, + ) -> np.ndarray: + """Docstring""" + return np.histogram2d( + self._full_sim['sindec'], + self._full_sim['logE'], + bins=bins, + weights=self._full_sim[mcbkgname], + density=True, + )[0] + + def build_signal_sindec_logenergy_histogram( + self, gamma: float, bins: np.ndarray) -> np.ndarray: + """Docstring""" + return np.histogram2d( + self.full_sim['sindec'], + self.full_sim['logE'], + bins=bins, + weights=self.full_sim['ow'] * self.full_sim['trueE']**gamma, + density=True, + )[0] + + @property + def sim(self) -> np.ndarray: + """Docstring""" + return self._sim + + @property + def full_sim(self) -> np.ndarray: + """Docstring""" + return self._full_sim + + @sim.setter + def sim(self, sim: np.ndarray) -> None: + """Docstring""" + self._full_sim = sim.copy() + + if 'sindec' not in self._full_sim.dtype.names: + self._full_sim = rf.append_fields( + self._full_sim, + 'sindec', + np.sin(self._full_sim['dec']), + usemask=False, + ) + + if 'weight' not in self._full_sim.dtype.names: + self._full_sim = rf.append_fields( + self._full_sim, 'weight', + np.zeros(len(self._full_sim)), + dtypes=np.float32 + ) + + self._full_sim['weight'] = self._full_sim['ow'] * ( + self._full_sim['trueE'] / self.config['normalization_energy (GeV)'] + )**self.config['assumed_gamma'] + + self._cut_sim_dec() + + def _cut_sim_dec(self) -> None: + """Docstring""" + if ( + self.config['dec_bandwidth (rad)'] is not None + ) and ( + self.config['dec_cut_location'] is not None + ): + sindec_dist = np.abs( + self.config['dec_cut_location'] - self._full_sim['trueDec']) + close = sindec_dist < self.config['dec_bandwidth (rad)'] + self._sim = self._full_sim[close].copy() + + self._sim['ow'] /= 2 * np.pi * (np.min([np.sin( + self.config['dec_cut_location'] + self.config['dec_bandwidth (rad)'] + ), 1]) - np.max([np.sin( + self.config['dec_cut_location'] - self.config['dec_bandwidth (rad)'] + ), -1])) + self._sim['weight'] /= 2 * np.pi * (np.min([np.sin( + self.config['dec_cut_location'] + self.config['dec_bandwidth (rad)'] + ), 1]) - np.max([np.sin( + self.config['dec_cut_location'] - self.config['dec_bandwidth (rad)'] + ), -1])) + else: + self._sim = self._full_sim + + @property + def data_grl(self) -> Tuple[np.ndarray, np.ndarray]: + """Docstring""" + return self._data, self._grl + + @data_grl.setter + def data_grl(self, data_grl: Tuple[np.ndarray, np.ndarray]) -> None: + """Docstring""" + self._sin_dec_bins = np.linspace(-1, 1, 1 + self.config['sin_dec_bins']) + self._data = data_grl[0].copy() + self._grl = data_grl[1].copy() + if 'sindec' not in self._data.dtype.names: + self._data = rf.append_fields( + self._data, + 'sindec', + np.sin(self._data['dec']), + usemask=False, + ) + + min_mjd = np.min(self._data['time']) + max_mjd = np.max(self._data['time']) + self._grl = self._grl[ + (self._grl['start'] < max_mjd) & (self._grl['stop'] > min_mjd)] + + self._livetime = self._grl['livetime'].sum() + self._n_background = self._grl['events'].sum() + self._grl_rates = self._grl['events'] / self._grl['livetime'] + + hist, bins = np.histogram( + self._data['sindec'], bins=self._sin_dec_bins, density=True) + bin_centers = bins[:-1] + np.diff(bins) / 2 + + self._dec_spline = Spline(bin_centers, hist, **self.config['dec_spline_kwargs']) + + @property + def livetime(self) -> float: + """Docstring""" + return self._livetime + + @property + def n_background(self) -> float: + """Docstring""" + return self._n_background + + @classmethod + def generate_config(cls) -> dict: + """Docstring""" + config = super().generate_config() + config['normalization_energy (GeV)'] = 100e3 + config['assumed_gamma'] = -2 + config['dec_cut_location'] = None + config['dec_bandwidth (rad)'] = None + config['sin_dec_bins'] = 50 + config['dec_spline_kwargs'] = { + 's': 0, + 'k': 2, + 'ext': 3, + } + return config + + +@dataclass +class TimeDependentNuSourcesDataHandler(NuSourcesDataHandler): + """Docstring""" + background_time_profile: GenericProfile + signal_time_profile: GenericProfile + + _background_time_profile: GenericProfile = field(init=False, repr=False) + _signal_time_profile: GenericProfile = field(init=False, repr=False) + + @property + def background_time_profile(self) -> GenericProfile: + """Docstring""" + return self._background_time_profile + + @background_time_profile.setter + def background_time_profile(self, profile: GenericProfile) -> None: + """Docstring""" + # Find the runs contianed in the background time window + start, stop = profile.range + return_stop_contained = True + + if self.config['outside_time_profile (days)'] is not None: + stop = start + start -= self.config['outside_time_profile (days)'] + return_stop_contained = False + + background_run_mask = self._contained_run_mask( + start, + stop, + return_stop_contained=return_stop_contained, + ) + + if not np.any(background_run_mask): + print('ERROR: No runs found in GRL for calculation of ' + 'background rates!') + raise RuntimeError + + background_grl = self._grl[background_run_mask] + self._n_background = background_grl['events'].sum() + self._n_background /= background_grl['livetime'].sum() + self._n_background *= self._contained_livetime(*profile.range, background_grl) + self._background_time_profile = copy.deepcopy(profile) + + @property + def signal_time_profile(self) -> GenericProfile: + """Docstring""" + return self._signal_time_profile + + @signal_time_profile.setter + def signal_time_profile(self, profile: GenericProfile) -> None: + """Docstring""" + self._signal_time_profile = copy.deepcopy(profile) + + def _contained_run_mask( + self, + start: float, + stop: float, + return_stop_contained: bool = True, + ) -> np.ndarray: + """Docstring""" + fully_contained = ( + self._grl['start'] >= start + ) & (self._grl['stop'] < stop) + + start_contained = ( + self._grl['start'] < start + ) & (self._grl['stop'] > start) + + if not return_stop_contained: + return fully_contained | start_contained + + stop_contained = ( + self._grl['start'] < stop + ) & (self._grl['stop'] > stop) + + return fully_contained | start_contained | stop_contained + + def contained_livetime(self, start: float, stop: float) -> float: + """Docstring""" + contained_runs = self._grl[self._contained_run_mask(start, stop)] + return self._contained_livetime(start, stop, contained_runs) + + @staticmethod + def _contained_livetime( + start: float, + stop: float, + contained_runs: np.ndarray, + ) -> float: + """Docstring""" + runs_before_start = contained_runs[contained_runs['start'] < start] + runs_after_stop = contained_runs[contained_runs['stop'] > stop] + contained_livetime = contained_runs['livetime'].sum() + + if len(runs_before_start) == 1: + contained_livetime -= start - runs_before_start['start'][0] + + if len(runs_after_stop) == 1: + contained_livetime -= runs_after_stop['stop'][0] - stop + + return contained_livetime + + def sample_background(self, n: int, rng: np.random.Generator) -> np.ndarray: + """Docstring""" + events = super().sample_background(n, rng) + return self._randomize_times(events, self._background_time_profile) + + def sample_signal(self, n: int, rng: np.random.Generator) -> np.ndarray: + """Docstring""" + events = super().sample_signal(n, rng) + return self._randomize_times(events, self._signal_time_profile) + + def _randomize_times( + self, + events: np.ndarray, + time_profile: GenericProfile, + ) -> np.ndarray: + grl_start_cdf = time_profile.cdf(self._grl['start']) + grl_stop_cdf = time_profile.cdf(self._grl['stop']) + valid = np.logical_and(grl_start_cdf < 1, grl_stop_cdf > 0) + rates = grl_stop_cdf[valid] - grl_start_cdf[valid] + + runs = np.random.choice( + self._grl[valid], + size=len(events), + replace=True, + p=rates / rates.sum(), + ) + + events['time'] = time_profile.inverse_transform_sample( + runs['start'], runs['stop']) + + return events + + @classmethod + def generate_config(cls) -> dict: + """Docstring""" + config = super().generate_config() + config['outside_time_profile (days)'] = None + return config diff --git a/build/lib/mla/minimizers.py b/build/lib/mla/minimizers.py new file mode 100644 index 00000000..f91f973c --- /dev/null +++ b/build/lib/mla/minimizers.py @@ -0,0 +1,120 @@ +""" +Top-level analysis code, and functions that are generic enough to not belong +in any class. +""" + +__author__ = 'John Evans and Jason Fan' +__copyright__ = 'Copyright 2024' +__credits__ = ['John Evans', 'Jason Fan', 'Michael Larson'] +__license__ = 'Apache License 2.0' +__version__ = '1.4.1' +__maintainer__ = 'Jason Fan' +__email__ = 'klfan@terpmail.umd.edu' +__status__ = 'Development' + +from typing import List, Optional, Tuple + +import abc +import dataclasses + +import numpy as np +import scipy.optimize + +from . import configurable +from .test_statistics import LLHTestStatistic + + +@dataclasses.dataclass +class Minimizer(configurable.Configurable): + """Docstring""" + test_statistic: LLHTestStatistic + + __metaclass__ = abc.ABCMeta + + @abc.abstractmethod + def __call__( + self, fitting_params: Optional[List[str]] = None) -> Tuple[float, np.ndarray]: + """Docstring""" + + +@dataclasses.dataclass +class GridSearchMinimizer(Minimizer): + """Docstring""" + def __call__( + self, fitting_params: Optional[List[str]] = None) -> Tuple[float, np.ndarray]: + """Docstring""" + if fitting_params is None: + fitting_key_idx_map = self.test_statistic.params.key_idx_map + fitting_bounds = self.test_statistic.params.bounds + else: + fitting_key_idx_map = { + key: val for key, val in self.test_statistic.params.key_idx_map.items() + if key in fitting_params + } + fitting_bounds = { + key: val for key, val in self.test_statistic.params.bounds.items() + if key in fitting_params + } + + if self.test_statistic.n_kept == 0: + return 0, np.array([(0,)], dtype=[('ns', np.float64)]) + + grid = [ + np.linspace(lo, hi, self.config['gridsearch_points']) + for lo, hi in fitting_bounds.values() + ] + + points = np.array(np.meshgrid(*grid)).T + + grid_ts_values = np.array([ + self._eval_test_statistic(point, fitting_key_idx_map) + for point in points + ]) + + return self._minimize( + points[grid_ts_values.argmin()], fitting_key_idx_map, fitting_bounds) + + def _eval_test_statistic(self, point: np.ndarray, fitting_key_idx_map: dict) -> float: + """Docstring""" + return self.test_statistic(self._param_values(point, fitting_key_idx_map)) + + def _param_values(self, point: np.ndarray, fitting_key_idx_map: dict) -> np.ndarray: + """Docstring""" + param_values = self.test_statistic.params.value_array.copy() + + for i, j in enumerate(fitting_key_idx_map.values()): + param_values[j] = point[i] + + return param_values + + def _minimize( + self, + point: np.ndarray, + fitting_key_idx_map: dict, + fitting_bounds: dict, + ) -> Tuple[float, np.ndarray]: + """Docstring""" + result = scipy.optimize.minimize( + self._eval_test_statistic, + x0=point, + args=(fitting_key_idx_map,), + bounds=fitting_bounds.values(), + method=self.config['scipy_minimize_method'], + ) + + best_ts_value = -result.fun + best_param_values = self._param_values(result.x, fitting_key_idx_map) + + if 'ns' not in fitting_key_idx_map: + idx = self.test_statistic.params.key_idx_map['ns'] + best_param_values[idx] = self.test_statistic.best_ns + + return best_ts_value, best_param_values + + @classmethod + def generate_config(cls) -> dict: + """Docstring""" + config = super().generate_config() + config['gridsearch_points'] = 5 + config['scipy_minimize_method'] = 'L-BFGS-B' + return config diff --git a/build/lib/mla/params.py b/build/lib/mla/params.py new file mode 100644 index 00000000..ad7fadf0 --- /dev/null +++ b/build/lib/mla/params.py @@ -0,0 +1,67 @@ +"""Docstring""" + +__author__ = 'John Evans and Jason Fan' +__copyright__ = 'Copyright 2024' +__credits__ = ['John Evans', 'Jason Fan', 'Michael Larson'] +__license__ = 'Apache License 2.0' +__version__ = '1.4.1' +__maintainer__ = 'Jason Fan' +__email__ = 'klfan@terpmail.umd.edu' +__status__ = 'Development' + +from typing import List, Optional + +import dataclasses +import numpy as np +import numpy.lib.recfunctions as rf + + +@dataclasses.dataclass +class Params: + """Docstring""" + value_array: np.ndarray + key_idx_map: dict + bounds: dict + + def __contains__(self, item: str) -> bool: + """Docstring""" + return item in self.key_idx_map + + def __getitem__(self, key: str): + """Docstring""" + return self.value_array[self.key_idx_map[key]] + + @property + def names(self) -> List[str]: + """Docstring""" + return [*self.key_idx_map] + + @classmethod + def from_dict(cls, value_dict: dict, bounds: Optional[dict] = None) -> 'Params': + """Docstring""" + value_array = np.array(list(value_dict.values())) + key_idx_map = {key: i for i, key in enumerate(value_dict)} + return cls._build_params(value_array, key_idx_map, bounds) + + @classmethod + def from_array( + cls, + named_value_array: np.ndarray, + bounds: Optional[dict] = None, + ) -> 'Params': + """Docstring""" + value_array = rf.structured_to_unstructured(named_value_array, copy=True)[0] + key_idx_map = {name: i for i, name in enumerate(named_value_array.dtype.names)} + return cls._build_params(value_array, key_idx_map, bounds) + + @classmethod + def _build_params( + cls, + value_array: np.ndarray, + key_idx_map: dict, + bounds: Optional[dict], + ) -> 'Params': + """Docstring""" + if bounds is None: + bounds = {key: (-np.inf, np.inf) for key in key_idx_map} + return cls(value_array, key_idx_map, bounds) diff --git a/build/lib/mla/sob_terms.py b/build/lib/mla/sob_terms.py new file mode 100644 index 00000000..5a022e2f --- /dev/null +++ b/build/lib/mla/sob_terms.py @@ -0,0 +1,355 @@ +"""Docstring""" + +__author__ = 'John Evans and Jason Fan' +__copyright__ = 'Copyright 2024' +__credits__ = ['John Evans', 'Jason Fan', 'Michael Larson'] +__license__ = 'Apache License 2.0' +__version__ = '1.4.1' +__maintainer__ = 'Jason Fan' +__email__ = 'klfan@terpmail.umd.edu' +__status__ = 'Development' + +from typing import List + +import abc +import copy +import dataclasses +import warnings + +import numpy as np +from scipy.interpolate import UnivariateSpline as Spline + +from . import configurable +from .params import Params +from .sources import PointSource +from .data_handlers import DataHandler +from .time_profiles import GenericProfile + + +@dataclasses.dataclass +class SoBTerm: + """Docstring""" + __metaclass__ = abc.ABCMeta + name: str + _params: Params + _sob: np.ndarray + + @property + @abc.abstractmethod + def params(self) -> Params: + """Docstring""" + + @params.setter + @abc.abstractmethod + def params(self, params: Params) -> None: + """Docstring""" + + @property + @abc.abstractmethod + def sob(self) -> np.ndarray: + """Docstring""" + + +@dataclasses.dataclass +class SoBTermFactory(configurable.Configurable): + """Docstring""" + __metaclass__ = abc.ABCMeta + + @abc.abstractmethod + def __call__(self, params: Params, events: np.ndarray) -> SoBTerm: + """Docstring""" + + @abc.abstractmethod + def calculate_drop_mask(self, events: np.ndarray) -> np.ndarray: + """Docstring""" + + @abc.abstractmethod + def generate_params(self) -> tuple: + """Docstring""" + + @classmethod + def generate_config(cls) -> dict: + """Docstring""" + return {'name': cls.__name__.replace('Factory', '')} + + +@dataclasses.dataclass +class SpatialTerm(SoBTerm): + """Docstring""" + + @property + def params(self) -> Params: + """Docstring""" + return self._params + + @params.setter + def params(self, params: Params) -> None: + """Docstring""" + self._params = params + + @property + def sob(self) -> np.ndarray: + """Docstring""" + return self._sob + + +@dataclasses.dataclass +class SpatialTermFactory(SoBTermFactory): + """Docstring""" + data_handler: DataHandler + source: PointSource + + def __call__(self, params: Params, events: np.ndarray) -> SoBTerm: + """Docstring""" + sob_spatial = self.source.spatial_pdf(events) + sob_spatial /= self.data_handler.evaluate_background_sindec_pdf(events) + return SpatialTerm( + name=self.config['name'], + _params=params, + _sob=sob_spatial, + ) + + def calculate_drop_mask(self, events: np.ndarray) -> np.ndarray: + """Docstring""" + return self.source.spatial_pdf(events) != 0 + + def generate_params(self) -> tuple: + return {}, {} + + +@dataclasses.dataclass +class TimeTerm(SoBTerm): + """Docstring""" + _times: np.ndarray + _signal_time_profile: GenericProfile + + @property + def params(self) -> Params: + """Docstring""" + return self._params + + @params.setter + def params(self, params: Params) -> None: + """Docstring""" + self._signal_time_profile.params = params + self._params = params + + @property + def sob(self) -> np.ndarray: + """Docstring""" + return self._sob * self._signal_time_profile.pdf(self._times) + + +@dataclasses.dataclass +class TimeTermFactory(SoBTermFactory): + """Docstring""" + background_time_profile: GenericProfile + signal_time_profile: GenericProfile + + def __call__(self, params: Params, events: np.ndarray) -> SoBTerm: + """Docstring""" + times = np.empty(len(events), dtype=events['time'].dtype) + times[:] = events['time'][:] + signal_time_profile = copy.deepcopy(self.signal_time_profile) + signal_time_profile.params = params + sob_bg = 1 / self.background_time_profile.pdf(times) + + if np.logical_not(np.all(np.isfinite(sob_bg))): + warnings.warn( + 'Warning, events outside background time profile', + RuntimeWarning + ) + + return TimeTerm( + name=self.config['name'], + _params=params, + _sob=sob_bg, + _times=times, + _signal_time_profile=signal_time_profile, + ) + + def calculate_drop_mask(self, events: np.ndarray) -> np.ndarray: + """Docstring""" + return 1 / self.background_time_profile.pdf(events['time']) != 0 + + def generate_params(self) -> tuple: + return self.signal_time_profile.params, self.signal_time_profile.param_bounds + + +@dataclasses.dataclass +class SplineMapEnergyTerm(SoBTerm): + """Docstring""" + gamma: float + _splines: List[Spline] + _event_spline_idxs: np.ndarray + + @property + def params(self) -> Params: + """Docstring""" + return self._params + + @params.setter + def params(self, params: Params) -> None: + """Docstring""" + if 'gamma' in params: + self.gamma = params['gamma'] + self._params = params + + @property + def sob(self) -> np.ndarray: + """Docstring""" + spline_evals = np.exp([spline(self.gamma) for spline in self._splines]) + return spline_evals[self._event_spline_idxs] + + +@dataclasses.dataclass +class SplineMapEnergyTermFactory(SoBTermFactory): + """Docstring""" + data_handler: DataHandler + _sin_dec_bins: np.ndarray = dataclasses.field(init=False, repr=False) + _log_energy_bins: np.ndarray = dataclasses.field(init=False, repr=False) + _gamma_bins: np.ndarray = dataclasses.field(init=False, repr=False) + _spline_map: List[List[Spline]] = dataclasses.field(init=False, repr=False) + + def __post_init__(self) -> None: + """Docstring""" + if self.config['list_sin_dec_bins'] is None: + self._sin_dec_bins = np.linspace( + -1, 1, 1 + self.config['sin_dec_bins'] + ) + else: + self._sin_dec_bins = self.config['list_sin_dec_bins'] + if self.config['list_log_energy_bins'] is None: + self._log_energy_bins = np.linspace( + *self.config['log_energy_bounds'], + 1 + self.config['log_energy_bins'] + ) + else: + self._log_energy_bins = self.config['list_log_energy_bins'] + + self._gamma_bins = np.linspace( + *self.config['gamma_bounds'], 1 + self.config['gamma_bins']) + self._spline_map = self._init_spline_map() + + def __call__(self, params: Params, events: np.ndarray) -> SoBTerm: + """Docstring""" + sin_dec_idx = np.searchsorted(self._sin_dec_bins[:-1], events['sindec']) + log_energy_idx = np.searchsorted(self._log_energy_bins[:-1], events['logE']) + + spline_idxs, event_spline_idxs = np.unique( + [sin_dec_idx - 1, log_energy_idx - 1], + return_inverse=True, + axis=1 + ) + + splines = [self._spline_map[i][j] for i, j in spline_idxs.T] + + return SplineMapEnergyTerm( + name=self.config['name'], + _params=params, + _sob=np.empty(1), + gamma=self.config['initial_gamma'], + _splines=splines, + _event_spline_idxs=event_spline_idxs, + ) + + def calculate_drop_mask(self, events: np.ndarray) -> np.ndarray: + """Docstring""" + return np.ones(len(events), dtype=bool) + + def generate_params(self) -> tuple: + return {'gamma': -2}, {'gamma': (-4, -1)} + + def _init_sob_map( + self, + gamma: float, + bins: np.ndarray, + bin_centers: np.ndarray, + bg_h: np.ndarray, + ) -> np.ndarray: + """Creates sob histogram for a given spectral index (gamma). + + Args: + gamma: The gamma value to use to weight the signal. + + Returns: + An array of signal-over-background values binned in sin(dec) and + log(energy) for a given gamma. + """ + sig_h = self.data_handler.build_signal_sindec_logenergy_histogram(gamma, bins) + + # Normalize histogram by dec band + sig_h /= np.sum(sig_h, axis=1)[:, None] + + # div-0 okay here + with np.errstate(divide='ignore', invalid='ignore'): + ratio = sig_h / bg_h + + for i in range(ratio.shape[0]): + # Pick out the values we want to use. + # We explicitly want to avoid NaNs and infinities + good = np.isfinite(ratio[i]) & (ratio[i] > 0) + good_bins, good_vals = bin_centers[good], ratio[i][good] + + # Do a linear interpolation across the energy range + spline = Spline(good_bins, good_vals, **self.config['energy_spline_kwargs']) + + # And store the interpolated values + ratio[i] = spline(bin_centers) + return ratio + + def _init_spline_map(self) -> List[List[Spline]]: + """Builds a 3D hist of sob vs. sin(dec), log(energy), and gamma, then + returns splines of sob vs. gamma. + + Returns: A Nested spline list of shape (sin_dec_bins, log_energy_bins). + """ + bins = np.array([self._sin_dec_bins, self._log_energy_bins]) + bin_centers = bins[1, :-1] + np.diff(bins[1]) / 2 + bg_h = self.data_handler.build_background_sindec_logenergy_histogram(bins) + + # Normalize histogram by dec band + bg_h /= np.sum(bg_h, axis=1)[:, None] + if self.config['backgroundSOBoption'] == 1: + bg_h[bg_h <= 0] = np.min(bg_h[bg_h > 0]) + elif self.config['backgroundSOBoption'] == 0: + pass + + sob_maps = np.array([ + self._init_sob_map(gamma, bins, bin_centers, bg_h) + for gamma in self._gamma_bins + ]) + + transposed_log_sob_maps = np.log(sob_maps.transpose(1, 2, 0)) + + splines = [[ + Spline(self._gamma_bins, log_ratios, **self.config['sob_spline_kwargs']) + for log_ratios in dec_bin + ] for dec_bin in transposed_log_sob_maps] + + return splines + + @classmethod + def generate_config(cls): + """Docstring""" + config = super().generate_config() + config['initial_gamma'] = -2 + config['sin_dec_bins'] = 50 + config['log_energy_bins'] = 50 + config['log_energy_bounds'] = (1, 8) + config['list_sin_dec_bins'] = None + config['list_log_energy_bins'] = None + config['gamma_bins'] = 50 + config['gamma_bounds'] = (-4.25, -0.5) + config['backgroundSOBoption'] = 0 + config['sob_spline_kwargs'] = { + 'k': 3, + 's': 0, + 'ext': 'raise', + } + config['energy_spline_kwargs'] = { + 'k': 1, + 's': 0, + 'ext': 3, + } + return config diff --git a/build/lib/mla/sources.py b/build/lib/mla/sources.py new file mode 100644 index 00000000..ee937d76 --- /dev/null +++ b/build/lib/mla/sources.py @@ -0,0 +1,96 @@ +""" +Docstring +""" + +__author__ = 'John Evans and Jason Fan' +__copyright__ = 'Copyright 2024' +__credits__ = ['John Evans', 'Jason Fan', 'Michael Larson'] +__license__ = 'Apache License 2.0' +__version__ = '1.4.1' +__maintainer__ = 'Jason Fan' +__email__ = 'klfan@terpmail.umd.edu' +__status__ = 'Development' + +import dataclasses + +from . import configurable +from . import utility_functions as uf + +import numpy as np + + +@dataclasses.dataclass +class PointSource(configurable.Configurable): + """Stores a source object name and location""" + def sample(self, size: int = 1) -> tuple: + """Sample locations. + + Args: + size: number of points to sample + """ + return (np.ones(size) * self.config['ra'], np.ones(size) * self.config['dec']) + + def spatial_pdf(self, events: np.ndarray) -> np.ndarray: + """calculates the signal probability of events. + + gives a gaussian probability based on their angular distance from the + source object. + + args: + source: + events: an array of events including their positional data. + + returns: + the value for the signal spatial pdf for the given events angular + distances. + """ + sigma2 = events['angErr']**2 + self.sigma**2 + dist = uf.angular_distance(events['ra'], events['dec'], *self.location) + norm = 1 / (2 * np.pi * sigma2) + return norm * np.exp(-dist**2 / (2 * sigma2)) + + @property + def location(self) -> tuple: + """return location of the source""" + return (self.config['ra'], self.config['dec']) + + @property + def sigma(self) -> float: + """return 0 for point source""" + return 0 + + @classmethod + def generate_config(cls) -> dict: + """Docstring""" + config = super().generate_config() + config['name'] = 'source_name' + config['ra'] = np.nan + config['dec'] = np.nan + return config + + +@dataclasses.dataclass +class GaussianExtendedSource(PointSource): + """Gaussian Extended Source""" + def sample(self, size: int = 1) -> np.ndarray: + """Sample locations. + + Args: + size: number of points to sample + """ + mean = self.location + x = np.random.normal(mean[0], self.sigma, size=size) + y = np.random.normal(mean[1], self.sigma, size=size) + return np.array([x, y]) + + @property + def sigma(self) -> float: + """return sigma for GaussianExtendedSource""" + return self.config['sigma'] + + @classmethod + def generate_config(cls) -> dict: + """Docstring""" + config = super().generate_config() + config['sigma'] = np.deg2rad(1) + return config diff --git a/build/lib/mla/test_statistics.py b/build/lib/mla/test_statistics.py new file mode 100644 index 00000000..174aba92 --- /dev/null +++ b/build/lib/mla/test_statistics.py @@ -0,0 +1,206 @@ +"""Docstring""" + +__author__ = 'John Evans and Jason Fan' +__copyright__ = 'Copyright 2024' +__credits__ = ['John Evans', 'Jason Fan', 'Michael Larson'] +__license__ = 'Apache License 2.0' +__version__ = '1.4.1' +__maintainer__ = 'Jason Fan' +__email__ = 'klfan@terpmail.umd.edu' +__status__ = 'Development' + +from typing import Dict, List, Optional + +import dataclasses +import numpy as np + +from . import configurable +from .sob_terms import SoBTerm, SoBTermFactory +from .params import Params + + +@dataclasses.dataclass +class LLHTestStatistic(): + """Docstring""" + sob_terms: Dict[str, SoBTerm] + _n_events: int + _n_kept: int + _events: np.ndarray + _params: Params + _newton_precision: float + _newton_iterations: int + _best_ts: float = dataclasses.field(init=False, default=0) + _best_ns: float = dataclasses.field(init=False, default=0) + + def __call__( + self, + param_values: Optional[np.ndarray] = None, + fitting_ns: bool = False, + ) -> float: + """Evaluates the test-statistic for the given events and parameters + + Calculates the test-statistic using a given event model, n_signal, and + gamma. This function does not attempt to fit n_signal or gamma. + + Returns: + The overall test-statistic value for the given events and + parameters. + """ + if param_values is not None: + self.params.value_array = param_values + self._update_term_params() + + if self._n_events == 0: + return 0 + + sob = self._calculate_sob() + + if fitting_ns: + ns_ratio = self._params['ns'] / self._n_events + else: + ns_ratio = self._newton_ns_ratio(sob) + + llh = np.sign(ns_ratio) * np.log(np.abs(ns_ratio) * (sob - 1) + 1) + drop_term = np.sign(ns_ratio) * np.log(1 - np.abs(ns_ratio)) + ts = -2 * (llh.sum() + self.n_dropped * drop_term) + + if ts < self._best_ts: + self._best_ts = ts + self._best_ns = ns_ratio * self._n_events + + return ts + + def _calculate_sob(self) -> np.ndarray: + """Docstring""" + sob = np.ones(self.n_kept) + for _, term in self.sob_terms.items(): + sob *= term.sob.reshape((-1,)) + return sob + + def _newton_ns_ratio(self, sob: np.ndarray) -> float: + """Docstring + + Args: + sob: + + Returns: + + """ + precision = self._newton_precision + 1 + eps = 1e-5 + k = 1 / (sob - 1) + x = [0.] * self._newton_iterations + + for i in range(self._newton_iterations - 1): + # get next iteration and clamp + inv_terms = x[i] + k + inv_terms[inv_terms == 0] = eps + terms = 1 / inv_terms + drop_term = 1 / (x[i] - 1) + d1 = np.sum(terms) + self.n_dropped * drop_term + d2 = np.sum(terms**2) + self.n_dropped * drop_term**2 + x[i + 1] = min(1 - eps, max(0, x[i] + d1 / d2)) + + if x[i] == x[i + 1] or ( + x[i] < x[i + 1] and x[i + 1] <= x[i] * precision + ) or (x[i + 1] < x[i] and x[i] <= x[i + 1] * precision): + break + return x[i + 1] + + @property + def params(self) -> Params: + """Docstring""" + return self._params + + @params.setter + def params(self, params: Params) -> None: + """Docstring""" + if params == self._params: + return + if 'ns' in params: + params.bounds['ns'] = (0, min(params.bounds['ns'], self.n_kept)) + self._params = params + self._update_term_params() + self._best_ns = self._best_ts = 0 + + def _update_term_params(self) -> None: + """Docstring""" + for _, term in self.sob_terms.items(): + term.params = self.params + + @property + def best_ns(self) -> float: + """Docstring""" + return self._best_ns + + @property + def best_ts(self) -> float: + """Docstring""" + return self._best_ts + + @property + def n_events(self) -> int: + """Docstring""" + return self._n_events + + @property + def n_kept(self) -> int: + """Docstring""" + return self._n_kept + + @property + def n_dropped(self) -> int: + """Docstring""" + return self._n_events - self._n_kept + + +@dataclasses.dataclass +class LLHTestStatisticFactory(configurable.Configurable): + """Docstring""" + sob_term_factories: List[SoBTermFactory] + + def __call__(self, params: Params, events: np.ndarray) -> LLHTestStatistic: + """Docstring""" + drop_mask = np.logical_and.reduce(np.array([ + term_factory.calculate_drop_mask(events) + for term_factory in self.sob_term_factories + ])) + + n_kept = drop_mask.sum() + pruned_events = np.empty(n_kept, dtype=events.dtype) + pruned_events[:] = events[drop_mask] + + sob_terms = { + term_factory.config['name']: term_factory(params, pruned_events) + for term_factory in self.sob_term_factories + } + + return LLHTestStatistic( + sob_terms=sob_terms, + _n_events=len(events), + _n_kept=n_kept, + _events=pruned_events, + _params=params, + _newton_precision=self.config['newton_precision'], + _newton_iterations=self.config['newton_iterations'], + ) + + def generate_params(self) -> Params: + """Docstring""" + param_values = {'ns': 0} + param_bounds = {'ns': (0, np.inf)} + + for term in self.sob_term_factories: + vals, bounds = term.generate_params() + param_values = dict(param_values, **vals) + param_bounds = dict(param_bounds, **bounds) + + return Params.from_dict(param_values, param_bounds) + + @classmethod + def generate_config(cls) -> dict: + """Docstring""" + config = super().generate_config() + config['newton_precision'] = 0 + config['newton_iterations'] = 20 + return config diff --git a/build/lib/mla/threeml/IceCubeLike.py b/build/lib/mla/threeml/IceCubeLike.py new file mode 100644 index 00000000..a6862502 --- /dev/null +++ b/build/lib/mla/threeml/IceCubeLike.py @@ -0,0 +1,978 @@ +"""Docstring""" + +__author__ = 'John Evans and Jason Fan' +__copyright__ = 'Copyright 2024' +__credits__ = ['John Evans', 'Jason Fan', 'Michael Larson'] +__license__ = 'Apache License 2.0' +__version__ = '1.4.1' +__maintainer__ = 'Jason Fan' +__email__ = 'klfan@terpmail.umd.edu' +__status__ = 'Development' + +from __future__ import print_function +from __future__ import division +from past.utils import old_div +import collections +import scipy +import numpy as np +import numpy.lib.recfunctions as rf +from astromodels import Gaussian_on_sphere +from astromodels.core.sky_direction import SkyDirection +from astromodels.core.spectral_component import SpectralComponent +from astromodels.core.tree import Node +from astromodels.core.units import get_units +from astromodels.sources.source import Source, SourceType +from astromodels.utils.pretty_list import dict_to_list +from astromodels.core.memoization import use_astromodels_memoization +from astromodels import PointSource, ExtendedSource +import astropy.units as u +from threeML.plugin_prototype import PluginPrototype +from mla.threeml import data_handlers +from mla.threeml import sob_terms +from mla import sob_terms as sob_terms_base +from mla import test_statistics +from mla.params import Params +from mla import analysis +from mla import sources +from mla import minimizers +from mla import trial_generators +from mla.utility_functions import newton_method, newton_method_multidataset + +__all__ = ["NeutrinoPointSource"] +r"""This IceCube plugin is currently under develop by Kwok Lung Fan""" + + +class NeutrinoPointSource(PointSource,Node): + """ + Class for NeutrinoPointSource. It is inherited from astromodels PointSource class. + """ + + def __init__( + self, + source_name, + ra=None, + dec=None, + spectral_shape=None, + l=None, + b=None, + components=None, + sky_position=None, + energy_unit=u.GeV, + ): + """Constructor for NeutrinoPointSource + + More info ... + + Args: + source_name:Name of the source + ra: right ascension in degree + dec: declination in degree + spectral_shape: Shape of the spectrum.Check 3ML example for more detail. + l: galactic longitude in degree + b: galactic in degree + components: Spectral Component.Check 3ML example for more detail. + sky_position: sky position + energy_unit: Unit of the energy + """ + # Check that we have all the required information + + # (the '^' operator acts as XOR on booleans) + + # Check that we have one and only one specification of the position + + assert ( + (ra is not None and dec is not None) + ^ (l is not None and b is not None) + ^ (sky_position is not None) + ), "You have to provide one and only one specification for the position" + + # Gather the position + + if not isinstance(sky_position, SkyDirection): + + if (ra is not None) and (dec is not None): + + # Check that ra and dec are actually numbers + + try: + + ra = float(ra) + dec = float(dec) + + except (TypeError, ValueError): + + raise AssertionError( + "RA and Dec must be numbers. If you are confused by this message," + " you are likely using the constructor in the wrong way. Check" + " the documentation." + ) + + sky_position = SkyDirection(ra=ra, dec=dec) + + else: + + sky_position = SkyDirection(l=l, b=b) + + self._sky_position = sky_position + + # Now gather the component(s) + + # We need either a single component, or a list of components, but not both + # (that's the ^ symbol) + + assert (spectral_shape is not None) ^ (components is not None), ( + "You have to provide either a single " + "component, or a list of components " + "(but not both)." + ) + + if spectral_shape is not None: + + components = [SpectralComponent("main", spectral_shape)] + + Source.__init__(self, components, src_type=SourceType.POINT_SOURCE) + + # A source is also a Node in the tree + + Node.__init__(self, source_name) + + # Add the position as a child node, with an explicit name + + self._add_child(self._sky_position) + + # Add a node called 'spectrum' + + spectrum_node = Node("spectrum") + spectrum_node._add_children(list(self._components.values())) + + self._add_child(spectrum_node) + + # Now set the units + # Now sets the units of the parameters for the energy domain + + current_units = get_units() + + # Components in this case have energy as x and differential flux as y + + x_unit = energy_unit + y_unit = (energy_unit * current_units.area * current_units.time) ** (-1) + + # Now set the units of the components + for component in list(self._components.values()): + + component.shape.set_units(x_unit, y_unit) + + def __call__(self, x, tag=None): + """ + Overwrite the function so it always return 0. + It is because it should not produce any EM signal. + """ + if isinstance(x, u.Quantity): + if isinstance(x, (float, int)): + return 0 * (u.keV ** -1 * u.cm ** -2 * u.second ** -1) + return np.zeros((len(x))) * ( + u.keV ** -1 * u.cm ** -2 * u.second ** -1 + ) # It is zero so the unit doesn't matter + else: + if isinstance(x, (float, int)): + return 0 + return np.zeros((len(x))) + + def call(self, x, tag=None): + """ + Calling the spectrum + + Args: + x: Energy + + return + differential flux + """ + if tag is None: + + # No integration nor time-varying or whatever-varying + + if isinstance(x, u.Quantity): + + # Slow version with units + + results = [ + component.shape(x) for component in list(self.components.values()) + ] + + # We need to sum like this (slower) because using + # np.sum will not preserve the units (thanks astropy.units) + + return sum(results) + + else: + + # Fast version without units, where x is supposed to be in the same + # units as currently defined in units.get_units() + + results = [ + component.shape(x) for component in list(self.components.values()) + ] + + return np.sum(results, 0) + + else: + + # Time-varying or energy-varying or whatever-varying + + integration_variable, a, b = tag + + if b is None: + + # Evaluate in a, do not integrate + + with use_astromodels_memoization(False): + + integration_variable.value = a + + res = self.__call__(x, tag=None) + + return res + + else: + + # Integrate between a and b + + integrals = np.zeros(len(x)) + + # TODO: implement an integration scheme avoiding the for loop + + with use_astromodels_memoization(False): + + reentrant_call = self.__call__ + + for i, e in enumerate(x): + + def integral(y): + + integration_variable.value = y + + return reentrant_call(e, tag=None) + + # Now integrate + integrals[i] = scipy.integrate.quad( + integral, a, b, epsrel=1e-5 + )[0] + + return old_div(integrals, (b - a)) + + +class NeutrinoExtendedSource(ExtendedSource): + def __init__( + self, source_name, spatial_shape, spectral_shape=None, components=None + ): + + # Check that we have all the required information + # and set the units + + current_u = get_units() + + if isinstance(spatial_shape, Gaussian_on_sphere): + + # Now gather the component(s) + + # We need either a single component, or a list of components, but not both + # (that's the ^ symbol) + + assert (spectral_shape is not None) ^ (components is not None), ( + "You have to provide either a single " + "component, or a list of components " + "(but not both)." + ) + + if spectral_shape is not None: + + components = [SpectralComponent("main", spectral_shape)] + + # Components in this case have energy as x and differential flux as y + + diff_flux_units = (current_u.energy * current_u.area * current_u.time) ** ( + -1 + ) + + # Now set the units of the components + for component in components: + + component.shape.set_units(current_u.energy, diff_flux_units) + + # Set the units of the brightness + spatial_shape.set_units( + current_u.angle, current_u.angle, current_u.angle ** (-2) + ) + + else: + + print("Only support Gaussian_on_sphere") + + raise RuntimeError() + + # Here we have a list of components + + Source.__init__(self, components, SourceType.EXTENDED_SOURCE) + + # A source is also a Node in the tree + + Node.__init__(self, source_name) + + # Add the spatial shape as a child node, with an explicit name + self._spatial_shape = spatial_shape + self._add_child(self._spatial_shape) + + # Add the same node also with the name of the function + # self._add_child(self._shape, self._shape.__name__) + + # Add a node called 'spectrum' + + spectrum_node = Node("spectrum") + spectrum_node._add_children(list(self._components.values())) + + self._add_child(spectrum_node) + + @property + def spatial_shape(self): + """ + A generic name for the spatial shape. + :return: the spatial shape instance + """ + + return self._spatial_shape + + def get_spatially_integrated_flux(self, energies): + + """ + Returns total flux of source at the given energy + :param energies: energies (array or float) + :return: differential flux at given energy + """ + + if not isinstance(energies, np.ndarray): + energies = np.array(energies, ndmin=1) + + # Get the differential flux from the spectral components + + results = [ + self.spatial_shape.get_total_spatial_integral(energies) + * component.shape(energies) + for component in self.components.values() + ] + + if isinstance(energies, u.Quantity): + + # Slow version with units + + # We need to sum like this (slower) because using + # np.sum will not preserve the units (thanks astropy.units) + + differential_flux = sum(results) + + else: + + # Fast version without units, where x is supposed to be in the + # same units as currently defined in units.get_units() + + differential_flux = np.sum(results, 0) + + return differential_flux + + def __call__(self, lon, lat, energies): + """ + Returns brightness of source at the given position and energy + :param lon: longitude (array or float) + :param lat: latitude (array or float) + :param energies: energies (array or float) + :return: differential flux at given position and energy + """ + + lat = np.array(lat, ndmin=1) + lon = np.array(lon, ndmin=1) + energies = np.array(energies, ndmin=1) + if isinstance(self.spatial_shape, Gaussian_on_sphere): + if isinstance(energies, u.Quantity): + + # Slow version with units + + # We need to sum like this (slower) because + # using np.sum will not preserve the units (thanks astropy.units) + + result = np.zeros((lat.shape[0], energies.shape[0])) * ( + u.keV ** -1 * u.cm ** -2 * u.second ** -1 * u.degree ** -2 + ) + + else: + + # Fast version without units, where x is supposed to be in the + # same units as currently defined in units.get_units() + + result = np.zeros((lat.shape[0], energies.shape[0])) + + return np.squeeze(result) + + def call(self, energies): + """Returns total flux of source at the given energy""" + return self.get_spatially_integrated_flux(energies) + + @property + def has_free_parameters(self): + """ + Returns True or False whether there is any parameter in this source + :return: + """ + + for component in list(self._components.values()): + + for par in list(component.shape.parameters.values()): + + if par.free: + + return True + + for par in list(self.spatial_shape.parameters.values()): + + if par.free: + + return True + + return False + + @property + def free_parameters(self): + """ + Returns a dictionary of free parameters for this source + We use the parameter path as the key because it's + guaranteed to be unique, unlike the parameter name. + :return: + """ + free_parameters = collections.OrderedDict() + + for component in list(self._components.values()): + + for par in list(component.shape.parameters.values()): + + if par.free: + + free_parameters[par.path] = par + + for par in list(self.spatial_shape.parameters.values()): + + if par.free: + + free_parameters[par.path] = par + + return free_parameters + + @property + def parameters(self): + """ + Returns a dictionary of all parameters for this source. + We use the parameter path as the key because it's + guaranteed to be unique, unlike the parameter name. + :return: + """ + all_parameters = collections.OrderedDict() + + for component in list(self._components.values()): + + for par in list(component.shape.parameters.values()): + + all_parameters[par.path] = par + + for par in list(self.spatial_shape.parameters.values()): + + all_parameters[par.path] = par + + return all_parameters + + def _repr__base(self, rich_output=False): + """ + Representation of the object + :param rich_output: if True, generates HTML, otherwise text + :return: the representation + """ + + # Make a dictionary which will then be transformed in a list + + repr_dict = collections.OrderedDict() + + key = "%s (extended source)" % self.name + + repr_dict[key] = collections.OrderedDict() + repr_dict[key]["shape"] = self._spatial_shape.to_dict(minimal=True) + repr_dict[key]["spectrum"] = collections.OrderedDict() + + for component_name, component in list(self.components.items()): + repr_dict[key]["spectrum"][component_name] = component.to_dict(minimal=True) + + return dict_to_list(repr_dict, rich_output) + + def get_boundaries(self): + """ + Returns the boundaries for this extended source + :return: a tuple of tuples ((min. lon, max. lon), (min lat, max lat)) + """ + return self._spatial_shape.get_boundaries() + + +class Spectrum(object): + r""" + A class that converter a astromodels model + instance to a spectrum object with __call__ method. + """ + + def __init__(self, likelihood_model_instance, A=1): + r"""Constructor of the class""" + self.model = likelihood_model_instance + self.norm = A + for source_name, source in likelihood_model_instance.point_sources.items(): + if isinstance(source, NeutrinoPointSource): + self.neutrinosource = source_name + self.point = True + for source_name, source in likelihood_model_instance.extended_sources.items(): + if isinstance(source, NeutrinoExtendedSource): + self.neutrinosource = source_name + self.point = False + + def __call__(self, energy, **kwargs): + r"""Evaluate spectrum at E""" + if self.point: + return ( + self.model.point_sources[self.neutrinosource].call(energy) * self.norm + ) + else: + return ( + self.model.extended_sources[self.neutrinosource].call(energy) + * self.norm + ) + + def validate(self): + pass + + def __str__(self): + r"""String representation of class""" + return "SpectrumConverter class doesn't support string representation now" + + def copy(self): + r"""Return copy of this class""" + c = type(self).__new__(type(self)) + c.__dict__.update(self.__dict__) + return c + + +class IceCubeLike(PluginPrototype): + def __init__( + self, + name: str, + data: np.ndarray, + data_handlers: data_handlers.ThreeMLDataHandler, + llh: test_statistics.LLHTestStatisticFactory, + source: sources.PointSource = None, + livetime: float = None, + fix_flux_norm: bool = False, + verbose: bool = False, + **kwargs + ): + r"""Constructor of the class. + Args: + name: name for the plugin + data: data of experiment + data_handlers: mla.threeml.data_handlers ThreeMLDataHandler object + llh: test_statistics.LLHTestStatistic object. Used to evaluate the ts + source: injection location(only when need injection) + livetime: livetime in days(calculated using livetime within time profile if None) + fix_flux_norm: only fit the spectrum shape + verbose: print the output or not + + """ + nuisance_parameters = {} + super(IceCubeLike, self).__init__(name, nuisance_parameters) + self.parameter = kwargs + self.fix_flux_norm = fix_flux_norm + self.fit_ns = 0 + self.fit_likelihood = 0 + if livetime is None: + for term in llh.sob_term_factories: + if isinstance(term, sob_terms_base.TimeTermFactory): + self.livetime = ( + data_handlers.contained_livetime( + term.signal_time_profile.range[0], + term.signal_time_profile.range[1], + ) + * 3600 + * 24 + ) + else: + self.livetime = livetime + if source is None: + config = sources.PointSource.generate_config() + config["ra"] = 0 + config["dec"] = 0 + source = sources.PointSource(config=config) + self.injected_source = source + trial_config = trial_generators.SingleSourceTrialGenerator.generate_config() + self.trial_generator = trial_generators.SingleSourceTrialGenerator( + trial_config, data_handlers, source + ) + analysis_config = analysis.SingleSourceLLHAnalysis.generate_default_config( + minimizer_class=minimizers.GridSearchMinimizer + ) + self.analysis = analysis.SingleSourceLLHAnalysis( + config=analysis_config, + minimizer_class=minimizers.GridSearchMinimizer, + sob_term_factories=llh.sob_term_factories, + data_handler_source=(data_handlers, source), + ) + + self.sob_term_factories = llh.sob_term_factories + for term in llh.sob_term_factories: + if isinstance(term, sob_terms_base.SpatialTermFactory): + self.spatial_sob_factory = term + if isinstance(term, sob_terms.ThreeMLBaseEnergyTermFactory): + self.energy_sob_factory = term + self.verbose = verbose + self._data = data + self._ra = np.rad2deg(source.config["ra"]) + self._dec = np.rad2deg(source.config["dec"]) + self._sigma = np.rad2deg(source.sigma) + self.test_statistic = self.analysis.test_statistic_factory( + Params.from_dict({"ns": 0}), data + ) + for key in self.test_statistic.sob_terms.keys(): + if isinstance( + self.test_statistic.sob_terms[key], sob_terms.ThreeMLPSEnergyTerm + ): + self.energyname = key + return + + def set_model(self, likelihood_model_instance): + r"""Setting up the model""" + if likelihood_model_instance is None: + + return + + for source_name, source in likelihood_model_instance.point_sources.items(): + if isinstance(source, NeutrinoPointSource): + self.source_name = source_name + ra = source.position.get_ra() + dec = source.position.get_dec() + if self._ra == ra and self._dec == dec: + self.llh_model = likelihood_model_instance + self.energy_sob_factory.spectrum = Spectrum( + likelihood_model_instance + ) + self.test_statistic = self.analysis.test_statistic_factory( + Params.from_dict({"ns": 0}), self._data + ) + else: + self._ra = ra + self._dec = dec + config = sources.PointSource.generate_config() + config["ra"] = np.deg2rad(ra) + config["dec"] = np.deg2rad(dec) + mlasource = sources.PointSource(config=config) + self.analysis.data_handler_source = ( + self.analysis.data_handler_source[0], + mlasource, + ) + self.spatial_sob_factory.source = mlasource + self.llh_model = likelihood_model_instance + self.energy_sob_factory.source = mlasource + self.energy_sob_factory.spectrum = Spectrum( + likelihood_model_instance + ) + self.test_statistic = self.analysis.test_statistic_factory( + Params.from_dict({"ns": 0}), self._data + ) + for source_name, source in likelihood_model_instance.extended_sources.items(): + if isinstance(source, NeutrinoExtendedSource): + self.source_name = source_name + ra = source.spatial_shape.lon0.value + dec = source.spatial_shape.lat0.value + sigma = source.spatial_shape.sigma.value + if self._ra == ra and self._dec == dec and self._sigma == sigma: + self.llh_model = likelihood_model_instance + self.energy_sob_factory.spectrum = Spectrum( + likelihood_model_instance + ) + self.test_statistic = self.analysis.test_statistic_factory( + Params.from_dict({"ns": 0}), self._data + ) + else: + self._ra = ra + self._dec = dec + self._sigma = sigma + config = sources.GaussianExtendedSource.generate_config() + config["ra"] = np.deg2rad(ra) + config["dec"] = np.deg2rad(dec) + config["sigma"] = np.deg2rad(sigma) + mlasource = sources.GaussianExtendedSource(config=config) + self.analysis.data_handler_source = ( + self.analysis.data_handler_source[0], + mlasource, + ) + self.spatial_sob_factory.source = mlasource + self.llh_model = likelihood_model_instance + self.energy_sob_factory.source = mlasource + self.energy_sob_factory.spectrum = Spectrum( + likelihood_model_instance + ) + self.test_statistic = self.analysis.test_statistic_factory( + Params.from_dict({"ns": 0}), self._data + ) + + if self.source_name is None: + print("No point sources in the model") + return + + def inject_background_and_signal(self, **kwargs) -> None: + """docstring""" + self._data = self.trial_generator(**kwargs) + return + + def update_data(self, data) -> None: + """docstring""" + self._data = data + return + + def update_injection(self, source: sources.PointSource): + """docstring""" + self.trial_generator.source = source + return + + def update_model(self): + """docstring""" + spectrum = Spectrum(self.llh_model) + self.energy_sob_factory.spectrum = spectrum + self.test_statistic.sob_terms[self.energyname].update_sob_hist( + self.energy_sob_factory + ) + return + + def get_ns(self): + """docstring""" + ns = self.energy_sob_factory.get_ns() * self.livetime * 3600 * 24 + return ns + + def get_log_like(self, verbose=None): + """docstring""" + if verbose is None: + verbose = self.verbose + self.update_model() + if self.fix_flux_norm: + llh = self.test_statistic() # doesn't matter here + if verbose: + ns = self.test_statistic.best_ns + print(ns, llh) + else: + ns = self.get_ns() + if ns > self.test_statistic.n_events: + if verbose: + print(ns, 0) + return 0 + llh = self.test_statistic([ns], fitting_ns=True) + self.fit_ns = ns + self.fit_likelihood = llh + if verbose: + print(ns, llh / 2) + + return -llh / 2 + + def get_number_of_data_points(self): + """docstring""" + return self.test_statistic.n_events + + def get_current_fit_ns(self): + """docstring""" + return self.fit_ns + + def inner_fit(self): + return self.get_log_like() + + @property + def data(self) -> np.ndarray: + """Getter for data.""" + return self._data + + @property + def ra(self) -> float: + """Getter for ra.""" + return self._ra + + @property + def dec(self) -> float: + """Getter for ra.""" + return self._dec + + +class icecube_analysis(PluginPrototype): + """Docstring""" + + def __init__( + self, listoficecubelike, newton_flux_norm=False, name="combine", verbose=False + ): + """Docstring""" + nuisance_parameters = {} + super(icecube_analysis, self).__init__(name, nuisance_parameters) + self.listoficecubelike = listoficecubelike + self.livetime_ratio = [] # livetime ratio between sample + self.effA_ratio = [] + self.totallivetime = [] + self._p = [] + self.mc_index = [] + self.dataset_ratio = [] + self.dataset_weight = [] + self.totaln = 0 + for icecube in listoficecubelike: + self.totaln += len(icecube.data) + self.init_mc_array() + self.newton_flux_norm = newton_flux_norm + self.verbose = verbose + self.current_fit_ns = 0 + + def get_log_like(self, verbose=None): + if self.newton_flux_norm: + sob = [] + n_drop = [] + fraction = [] + self.totaln = 0 + dataset_weight = [] + for icecubeobject in self.listoficecubelike: + self.totaln += len(icecubeobject.data) + icecubeobject.update_model() + dataset_weight.append(icecubeobject.get_ns()) + dataset_weight = np.array(dataset_weight) + self.dataset_weight = dataset_weight / dataset_weight.sum() + + for i, icecubeobject in enumerate(self.listoficecubelike): + sob.append(icecubeobject.test_statistic._calculate_sob()) + n_drop.append(icecubeobject.test_statistic.n_dropped) + fraction.append( + self.totaln * self.dataset_weight[i] / len(icecubeobject.data) + ) + fraction = np.array(fraction) + # fraction = fraction/fraction.sum() + ns_ratio = newton_method_multidataset(sob, n_drop, fraction) + llh = 0 + for i, icecubeobject in enumerate(self.listoficecubelike): + templlh = np.sign(ns_ratio) * np.log( + np.abs(ns_ratio) * fraction[i] * (sob[i] - 1) + 1 + ) + drop_term = np.sign(ns_ratio) * np.log( + 1 - np.abs(ns_ratio) * fraction[i] + ) + llh += templlh.sum() + n_drop[i] * drop_term + self.current_fit_ns = ns_ratio * self.totaln + if self.verbose: + print(self.current_fit_ns, llh) + + else: + llh = 0 + ns = 0 + for icecubeobject in self.listoficecubelike: + icecubeobject.update_model() + llh += icecubeobject.get_log_like() + ns += icecubeobject.get_current_fit_ns() + self.current_fit_ns = ns + if self.verbose: + print(self.current_fit_ns, llh) + return llh + + def get_current_fit_ns(self): + return self.current_fit_ns + + def set_model(self, likelihood_model_instance): + for icecubeobject in self.listoficecubelike: + icecubeobject.set_model(likelihood_model_instance) + return + + def inner_fit(self): + return self.get_log_like() + + def init_mc_array(self): + """Docstring""" + for i, sample in enumerate(self.listoficecubelike): + self.livetime_ratio.append(sample.livetime) + self.effA_ratio.append( + sample.analysis.data_handler_source[0].sim["weight"].sum() + ) + self.livetime_ratio = np.array(self.livetime_ratio) + self.totallivetime = self.livetime_ratio.sum() + self.dataset_ratio = self.livetime_ratio * self.effA_ratio + self.dataset_ratio = self.dataset_ratio / self.dataset_ratio.sum() + self.livetime_ratio /= np.sum(self.livetime_ratio) + self.effA_ratio /= np.sum(self.effA_ratio) + + for i, sample in enumerate(self.listoficecubelike): + sim = sample.analysis.data_handler_source[0].sim + mc_array = rf.append_fields( + np.empty(len(sim["weight"])), + "p", + sim["weight"] / sim["weight"].sum() * self.livetime_ratio[i], + usemask=False, + ) + mc_array = rf.append_fields( + mc_array, + "index", + np.arange(len(mc_array)), + usemask=False, + ) + mc_array = rf.append_fields( + mc_array, + "sample", + np.ones(len(sim["weight"])) * i, + usemask=False, + ) + self.mc_index.append(mc_array) + + self.mc_index = np.array(self.mc_index, dtype=object) + + def injection(self, n_signal=0, flux_norm=None, poisson=False): + """docstring""" + self.totaln = 0 + if flux_norm is not None: + for i, icecubeobject in enumerate(self.listoficecubelike): + time_intergrated = flux_norm * icecubeobject.livetime + icecubeobject.trial_generator.config["fixed_ns"] = False + tempdata = icecubeobject.trial_generator(time_intergrated) + self.listoficecubelike[i].update_data(tempdata) + else: + if poisson: + ratio_injection = self.dataset_ratio * n_signal + for i, icecubeobject in enumerate(self.listoficecubelike): + icecubeobject.trial_generator.config["fixed_ns"] = True + injection_signal = np.random.poisson(ratio_injection[i]) + tempdata = icecubeobject.trial_generator(injection_signal) + self.listoficecubelike[i].update_data(tempdata) + else: + print("No fix number injection implemented") + self.totaln = 0 + for icecube in self.listoficecubelike: + self.totaln += len(icecube.data) + + def cal_injection_ns(self, flux_norm): + """Docstring""" + ns = 0 + for icecubeobject in self.listoficecubelike: + time_intergrated = flux_norm * icecubeobject.livetime * 3600 * 24 + tempns = ( + time_intergrated + * icecubeobject.analysis.data_handler_source[0].sim["weight"].sum() + ) + ns = ns + tempns + return ns + + def cal_injection_fluxnorm(self, ns): + """Docstring""" + totalweight = 0 + for icecubeobject in self.listoficecubelike: + tempweight = ( + icecubeobject.analysis.data_handler_source[0].sim["weight"].sum() + * icecubeobject.livetime + * 3600 + * 24 + ) + totalweight = totalweight + tempweight + return ns / totalweight diff --git a/build/lib/mla/threeml/__init__.py b/build/lib/mla/threeml/__init__.py new file mode 100644 index 00000000..9f44addd --- /dev/null +++ b/build/lib/mla/threeml/__init__.py @@ -0,0 +1,6 @@ +"""Docstring""" +# flake8: noqa +from . import spectral +from . import data_handlers +from . import sob_terms +from . import IceCubeLike diff --git a/build/lib/mla/threeml/data_handlers.py b/build/lib/mla/threeml/data_handlers.py new file mode 100644 index 00000000..8d65bd1c --- /dev/null +++ b/build/lib/mla/threeml/data_handlers.py @@ -0,0 +1,179 @@ +"""Docstring""" + +__author__ = 'John Evans and Jason Fan' +__copyright__ = 'Copyright 2024' +__credits__ = ['John Evans', 'Jason Fan', 'Michael Larson'] +__license__ = 'Apache License 2.0' +__version__ = '1.4.1' +__maintainer__ = 'Jason Fan' +__email__ = 'klfan@terpmail.umd.edu' +__status__ = 'Development' + +import dataclasses + +import numpy as np +import numpy.lib.recfunctions as rf + +from .. import data_handlers +from . import spectral + + +@dataclasses.dataclass +class ThreeMLDataHandler(data_handlers.NuSourcesDataHandler): + """ + Inheritance class from NuSourcesDataHandler. + For time independent 3ML analysis. + + Additional init argument: + injection_spectrum: spectral.BaseSpectrum(in keV by default) + """ + + injection_spectrum: spectral.BaseSpectrum + _injection_spectrum: spectral.BaseSpectrum = dataclasses.field( + init=False, repr=False, default=spectral.PowerLaw(1e9, 1e-22, -2) + ) + _flux_unit_conversion: float = dataclasses.field( + init=False, repr=False, default=1e6 + ) + _reduced_reco_sim: np.ndarray = dataclasses.field(init=False, repr=False) + + def __post_init__(self) -> None: + """Docstring""" + self._reduced_reco_sim = self.cut_reconstructed_sim( + self.config["dec_cut_location"], self.config["reco_sampling_width"] + ) + self._flux_unit_conversion = self.config["flux_unit_conversion"] + + def build_signal_energy_histogram( + self, spectrum: spectral.BaseSpectrum, bins: np.ndarray, scale: float + ) -> np.ndarray: + """ + Building the signal energy histogram. + Only used when using MC instead of IRF to build signal energy histogram. + + Args: + spectrum: signal spectrum + bins: 2d bins in sindec and logE + """ + return np.histogram2d( + self.reduced_reco_sim["sindec"], + self.reduced_reco_sim["logE"], + bins=bins, + weights=self.reduced_reco_sim["ow"] + * spectrum(self.reduced_reco_sim["trueE"] * scale), + density=True, + )[0] + + def cut_reconstructed_sim(self, dec: float, sampling_width: float) -> np.ndarray: + """ + Cutting the MC based on reconstructed dec. + Only use when using MC instead of IRF to build signal energy histogram. + + Args: + dec: declination of the source + sampling_width: size of the sampling band in reconstruction dec. + """ + dec_dist = np.abs(dec - self._full_sim["dec"]) + close = dec_dist < sampling_width + return self._full_sim[close].copy() + + @property + def reduced_reco_sim(self) -> np.ndarray: + """ + Return the reduced sim based on reconstructed dec. + This is the return output of cut_reconstructed_sim. + """ + return self._reduced_reco_sim + + @reduced_reco_sim.setter + def reduced_reco_sim(self, reduced_reco_sim: np.ndarray) -> None: + """ + setting the reduced sim based on reconstructed dec directly. + + Args: + reduced_reco_sim: reduced sim based on reconstructed dec + """ + self._reduced_reco_sim = reduced_reco_sim.copy() + + @property + def injection_spectrum(self) -> spectral.BaseSpectrum: + """ + Getting the injection spectrum + """ + return self._injection_spectrum + + @injection_spectrum.setter + def injection_spectrum(self, inject_spectrum: spectral.BaseSpectrum) -> None: + """ + Setting the injection spectrum + + Args: + inject_spectrum: spectrum used for injection + """ + if isinstance(inject_spectrum, property): + # initial value not specified, use default + inject_spectrum = ThreeMLDataHandler._injection_spectrum + self._injection_spectrum = inject_spectrum + if "weight" not in self._full_sim.dtype.names: + self._full_sim = rf.append_fields( + self._full_sim, + "weight", + np.zeros(len(self._full_sim)), + dtypes=np.float32, + ) + + self._full_sim["weight"] = ( + self._full_sim["ow"] + * (inject_spectrum(self._full_sim["trueE"] * self._flux_unit_conversion)) + * self._flux_unit_conversion + ) + + self._cut_sim_dec() + + @property + def sim(self) -> np.ndarray: + """Docstring""" + return self._sim + + @sim.setter + def sim(self, sim: np.ndarray) -> None: + """Docstring""" + self._full_sim = sim.copy() + + if "sindec" not in self._full_sim.dtype.names: + self._full_sim = rf.append_fields( + self._full_sim, + "sindec", + np.sin(self._full_sim["dec"]), + usemask=False, + ) + if "weight" not in self._full_sim.dtype.names: + self._full_sim = rf.append_fields( + self._full_sim, + "weight", + np.zeros(len(self._full_sim)), + dtypes=np.float32, + ) + + self._cut_sim_dec() + + @classmethod + def generate_config(cls): + """Docstring""" + config = super().generate_config() + config["reco_sampling_width"] = np.deg2rad(5) + config["flux_unit_conversion"] = 1e6 + return config + + +@dataclasses.dataclass +class ThreeMLTimeDepDataHandler( + data_handlers.TimeDependentNuSourcesDataHandler, ThreeMLDataHandler +): + """Docstring""" + + @classmethod + def generate_config(cls): + """Docstring""" + config = super().generate_config() + return config diff --git a/build/lib/mla/threeml/profilellh.py b/build/lib/mla/threeml/profilellh.py new file mode 100644 index 00000000..3bfe3862 --- /dev/null +++ b/build/lib/mla/threeml/profilellh.py @@ -0,0 +1,100 @@ +"""Docstring""" + +__author__ = 'John Evans and Jason Fan' +__copyright__ = 'Copyright 2024' +__credits__ = ['John Evans', 'Jason Fan', 'Michael Larson'] +__license__ = 'Apache License 2.0' +__version__ = '1.4.1' +__maintainer__ = 'Jason Fan' +__email__ = 'klfan@terpmail.umd.edu' +__status__ = 'Development' + +from threeML.plugin_prototype import PluginPrototype +from scipy.interpolate import RegularGridInterpolator +import pandas as pd +from typing import Optional +from astromodels import Model +import numpy as np + + +class ProfileLLHLike(PluginPrototype): + """Docstring""" + def __init__( + self, + name: str, + df: Optional[pd.DataFrame] = None, + spline: Optional[callable] = None, + fill_value: Optional[float] = 1e30, + ): + nuisance_parameters = {} + + """ + A generic plugin for profile likelihood. + Give either a pandas dataframe in format of parameter1,2...,-llh + or a spline which return -llh. + + :param name: + :type name: str + :param df: + :type df: pandas dataframe + :param spline: + :type spline: callable + :returns: + """ + + super().__init__(name, nuisance_parameters) + if spline is not None: + self.spline = spline + self.df = None + else: + self.df = df + self.par_name = list(df.columns) + self.par_name.pop() + listofpoint = [] + shape = [] + for n in self.par_name: + points = np.unique(df[n]) + listofpoint.append(points) + shape.append(points.shape[0]) + llh = np.reshape(df["llh"].values, shape) + self.spline = RegularGridInterpolator( + listofpoint, llh, bounds_error=False, fill_value=fill_value + ) + + @property + def likelihood_model(self) -> Model: + + return self._likelihood_model + + def set_model(self, likelihood_model_instance: Model) -> None: + """ + Set the model to be used in the joint minimization. + Must be a LikelihoodModel instance. + :param likelihood_model_instance: instance of Model + :type likelihood_model_instance: astromodels.Model + """ + + self._likelihood_model = likelihood_model_instance + + def get_log_like(self) -> float: + """ + Return the value of the log-likelihood with the current values for the + parameters + """ + current_value = [] + for name in self.par_name: + value = self._likelihood_model.parameters[name].value + current_value.append(value) + llh = -self.spline(current_value)[0] + return llh + + def inner_fit(self) -> float: + """ + This is used for the profile likelihood. Keeping fixed all parameters in the + LikelihoodModel, this method minimize the logLike over the remaining nuisance + parameters, i.e., the parameters belonging only to the model for this + particular detector. If there are no nuisance parameters, simply return the + logLike value. + """ + + return self.get_log_like() diff --git a/build/lib/mla/threeml/sob_terms.py b/build/lib/mla/threeml/sob_terms.py new file mode 100644 index 00000000..ff7ce5a9 --- /dev/null +++ b/build/lib/mla/threeml/sob_terms.py @@ -0,0 +1,504 @@ +"""Docstring""" + +__author__ = 'John Evans and Jason Fan' +__copyright__ = 'Copyright 2024' +__credits__ = ['John Evans', 'Jason Fan', 'Michael Larson'] +__license__ = 'Apache License 2.0' +__version__ = '1.4.1' +__maintainer__ = 'Jason Fan' +__email__ = 'klfan@terpmail.umd.edu' +__status__ = 'Development' + +import dataclasses + +import numpy as np +from scipy.interpolate import UnivariateSpline as Spline +from .. import sob_terms +from .. import sources +from .. import params as par +from . import spectral +from . import data_handlers + +PSTrackv4_sin_dec_bin = np.unique( + np.concatenate( + [ + np.linspace(-1, -0.93, 4 + 1), + np.linspace(-0.93, -0.3, 10 + 1), + np.linspace(-0.3, 0.05, 9 + 1), + np.linspace(0.05, 1, 18 + 1), + ] + ) +) + +PSTrackv4_log_energy_bins = np.arange(1, 9.5 + 0.01, 0.125) + + +@dataclasses.dataclass +class ThreeMLPSEnergyTerm(sob_terms.SoBTerm): + """ + Energy term for 3ML. Constructs only from 3ML energy term factory + """ + + _energysobhist: np.ndarray + _sin_dec_idx: np.ndarray + _log_energy_idx: np.ndarray + + def update_sob_hist(self, factory: sob_terms.SoBTermFactory) -> None: + """ + Updating the signal-over-background energy histogram. + + Args: + factory: energy term factory + """ + self._energysobhist = factory.cal_sob_map() + + @property + def params(self) -> par.Params: + """Docstring""" + return self._params + + @params.setter + def params(self, params: par.Params) -> None: + """Docstring""" + self._params = params + + @property + def sob(self) -> np.ndarray: + """Docstring""" + return self._energysobhist[self._sin_dec_idx, self._log_energy_idx] + + +@dataclasses.dataclass +class ThreeMLBaseEnergyTermFactory(sob_terms.SoBTermFactory): + """Docstring""" + + pass + + +@dataclasses.dataclass +class ThreeMLPSEnergyTermFactory(ThreeMLBaseEnergyTermFactory): + """ + This is the class for using MC directly to build the Energy terms. + We sugguest using the IRF for Energy term factory due to speed. + + Args: + data_handler: 3ML data handler + source: 3ML source object + spectrum: signal spectrum + """ + + data_handler: data_handlers.ThreeMLDataHandler + source: sources.PointSource + spectrum: spectral.BaseSpectrum + _source: sources.PointSource = dataclasses.field(init=False, repr=False) + _spectrum: spectral.BaseSpectrum = dataclasses.field( + init=False, repr=False, default=spectral.PowerLaw(1e3, 1e-14, -2) + ) + _bg_sob: np.ndarray = dataclasses.field(init=False, repr=False) + _sin_dec_bins: np.ndarray = dataclasses.field(init=False, repr=False) + _log_energy_bins: np.ndarray = dataclasses.field(init=False, repr=False) + _bins: np.ndarray = dataclasses.field(init=False, repr=False) + _ow_hist: np.ndarray = dataclasses.field(init=False, repr=False) + _ow_ebin: np.ndarray = dataclasses.field(init=False, repr=False) + _unit_scale: float = dataclasses.field(init=False, repr=False) + + def __post_init__(self) -> None: + """Docstring""" + if self.config["list_sin_dec_bins"] is None: + self._sin_dec_bins = np.linspace(-1, 1, 1 + self.config["sin_dec_bins"]) + else: + self._sin_dec_bins = self.config["list_sin_dec_bins"] + if self.config["list_log_energy_bins"] is None: + self._log_energy_bins = np.linspace( + *self.config["log_energy_bounds"], 1 + self.config["log_energy_bins"] + ) + else: + self._log_energy_bins = self.config["list_log_energy_bins"] + self.data_handler.reduced_reco_sim = self.data_handler.cut_reconstructed_sim( + self.source.location[1], + self.data_handler.config["reco_sampling_width"], + ) + self._unit_scale = self.config["Energy_convesion(ToGeV)"] + self._bins = np.array([self._sin_dec_bins, self._log_energy_bins], dtype=object) + self._init_bg_sob_map() + self._build_ow_hist() + + def __call__(self, params: par.Params, events: np.ndarray) -> sob_terms.SoBTerm: + """Docstring""" + # Get the bin that each event belongs to + sin_dec_idx = np.searchsorted(self._sin_dec_bins[:-1], events["sindec"]) - 1 + + log_energy_idx = np.searchsorted(self._log_energy_bins[:-1], events["logE"]) - 1 + + return ThreeMLPSEnergyTerm( + name=self.config["name"], + _params=params, + _sob=np.empty(1), + _sin_dec_idx=sin_dec_idx, + _log_energy_idx=log_energy_idx, + _energysobhist=self.cal_sob_map(), + ) + + def _build_ow_hist(self) -> np.ndarray: + """Docstring""" + self._ow_hist, self._ow_ebin = np.histogram( + np.log10(self.data_handler.sim["trueE"]), + bins=200, + weights=self.data_handler.sim["ow"], + ) + self._ow_ebin = 10**self._ow_ebin[:-1] * self._unit_scale + + def get_ns(self) -> float: + """Docstring""" + return (self.spectrum(self._ow_ebin) * self._ow_hist).sum() * self._unit_scale + + def _init_bg_sob_map(self) -> np.ndarray: + """Docstring""" + if self.config["mc_bkgweight"] is None: + bg_h = self.data_handler.build_background_sindec_logenergy_histogram( + self._bins + ) + else: + bg_h = self.data_handler.build_mcbackground_sindec_logenergy_histogram( + self._bins, self.config["mc_bkgweight"] + ) + print("using mc background") + # Normalize histogram by dec band + bg_h /= np.sum(bg_h, axis=1)[:, None] + if self.config["backgroundSOBoption"] == 1: + bg_h[bg_h <= 0] = np.min(bg_h[bg_h > 0]) + elif self.config["backgroundSOBoption"] == 0: + pass + self._bg_sob = bg_h + + @property + def source(self) -> sources.PointSource: + """Docstring""" + return self._source + + @source.setter + def source(self, source: sources.PointSource) -> None: + """Docstring""" + self._source = source + self.data_handler.reduced_reco_sim = self.data_handler.cut_reconstructed_sim( + self.source.location[1], + self.data_handler.config["reco_sampling_width"], + ) + + def cal_sob_map(self) -> np.ndarray: + """Creates sob histogram for a given spectrum. + Returns: + An array of signal-over-background values binned in sin(dec) and + log(energy) for a given gamma. + """ + sig_h = self.data_handler.build_signal_energy_histogram( + self.spectrum, self._bins, self._unit_scale + ) + bin_centers = self._log_energy_bins[:-1] + np.diff(self._log_energy_bins) / 2 + # Normalize histogram by dec band + sig_h /= np.sum(sig_h, axis=1)[:, None] + + # div-0 okay here + with np.errstate(divide="ignore", invalid="ignore"): + ratio = sig_h / self._bg_sob + + for i in range(ratio.shape[0]): + # Pick out the values we want to use. + # We explicitly want to avoid NaNs and infinities + good = np.isfinite(ratio[i]) & (ratio[i] > 0) + good_bins, good_vals = bin_centers[good], ratio[i][good] + if len(good_bins) > 1: + # Do a linear interpolation across the energy range + spline = Spline( + good_bins, good_vals, **self.config["energy_spline_kwargs"] + ) + + # And store the interpolated values + ratio[i] = spline(bin_centers) + elif len(good_bins) == 1: + ratio[i] = good_vals + else: + ratio[i] = 0 + return ratio + + def calculate_drop_mask(self, events: np.ndarray) -> np.ndarray: + """Docstring""" + return np.ones(len(events), dtype=bool) + + @property + def spectrum(self) -> spectral.BaseSpectrum: + """Docstring""" + return self._spectrum + + @spectrum.setter + def spectrum(self, spectrum: spectral.BaseSpectrum) -> None: + """Docstring""" + if isinstance(spectrum, property): + # initial value not specified, use default + spectrum = ThreeMLPSEnergyTermFactory._spectrum + self._spectrum = spectrum + + @classmethod + def generate_config(cls): + """Docstring""" + config = super().generate_config() + config["sin_dec_bins"] = 68 + config["log_energy_bins"] = 42 + config["log_energy_bounds"] = (1, 8) + config["energy_spline_kwargs"] = { + "k": 1, + "s": 0, + "ext": 3, + } + config["backgroundSOBoption"] = 0 + config["mc_bkgweight"] = None + config["list_sin_dec_bins"] = PSTrackv4_sin_dec_bin + config["list_log_energy_bins"] = PSTrackv4_log_energy_bins + return config + + +@dataclasses.dataclass +class ThreeMLPSIRFEnergyTermFactory(ThreeMLPSEnergyTermFactory): + """Docstring""" + + data_handler: data_handlers.ThreeMLDataHandler + source: sources.PointSource + spectrum: spectral.BaseSpectrum + _source: sources.PointSource = dataclasses.field(init=False, repr=False) + _spectrum: spectral.BaseSpectrum = dataclasses.field( + init=False, repr=False, default=spectral.PowerLaw(1e3, 1e-14, -2) + ) + _bg_sob: np.ndarray = dataclasses.field(init=False, repr=False) + _sin_dec_bins: np.ndarray = dataclasses.field( + init=False, repr=False, default=PSTrackv4_sin_dec_bin + ) + _log_energy_bins: np.ndarray = dataclasses.field(init=False, repr=False) + _bins: np.ndarray = dataclasses.field(init=False, repr=False) + _trueebin: np.ndarray = dataclasses.field(init=False, repr=False) + _irf: np.ndarray = dataclasses.field(init=False, repr=False) + _sindec_bounds: np.ndarray = dataclasses.field(init=False, repr=False) + _ntrueebin: int = dataclasses.field(init=False, repr=False) + _unit_scale: float = dataclasses.field(init=False, repr=False) + + def __post_init__(self) -> None: + """Docstring""" + if self.config["list_sin_dec_bins"] is None: + self._sin_dec_bins = np.linspace(-1, 1, 1 + self.config["sin_dec_bins"]) + else: + self._sin_dec_bins = self.config["list_sin_dec_bins"] + if self.config["list_log_energy_bins"] is None: + self._log_energy_bins = np.linspace( + *self.config["log_energy_bounds"], 1 + self.config["log_energy_bins"] + ) + else: + self._log_energy_bins = self.config["list_log_energy_bins"] + lower_sindec = np.maximum( + np.sin( + self.source.location[1] + - self.data_handler.config["reco_sampling_width"] + ), + -0.99, + ) + upper_sindec = np.minimum( + np.sin( + self.source.location[1] + + self.data_handler.config["reco_sampling_width"] + ), + 1, + ) + lower_sindec_index = np.searchsorted(self._sin_dec_bins, lower_sindec) - 1 + uppper_sindec_index = np.searchsorted(self._sin_dec_bins, upper_sindec) + self._sindec_bounds = np.array([lower_sindec_index, uppper_sindec_index]) + self._bins = np.array([self._sin_dec_bins, self._log_energy_bins], dtype=object) + self._truelogebin = self.config["list_truelogebin"] + self._unit_scale = self.config["Energy_convesion(ToGeV)"] + self._init_bg_sob_map() + self._build_ow_hist() + self._init_irf() + + def __call__(self, params: par.Params, events: np.ndarray) -> sob_terms.SoBTerm: + """Docstring""" + # Get the bin that each event belongs to + sin_dec_idx = np.searchsorted(self._sin_dec_bins[:-1], events["sindec"]) - 1 + + log_energy_idx = np.searchsorted(self._log_energy_bins[:-1], events["logE"]) - 1 + + return ThreeMLPSEnergyTerm( + name=self.config["name"], + _params=params, + _sob=np.empty(1), + _sin_dec_idx=sin_dec_idx, + _log_energy_idx=log_energy_idx, + _energysobhist=self.cal_sob_map(), + ) + + def _init_bg_sob_map(self) -> None: + """Docstring""" + if self.config["mc_bkgweight"] is None: + bg_h = self.data_handler.build_background_sindec_logenergy_histogram( + self._bins + ) + else: + bg_h = self.data_handler.build_mcbackground_sindec_logenergy_histogram( + self._bins, self.config["mc_bkgweight"] + ) + print("using mc background") + # Normalize histogram by dec band + bg_h /= np.sum(bg_h, axis=1)[:, None] + if self.config["backgroundSOBoption"] == 1: + bg_h[bg_h <= 0] = np.min(bg_h[bg_h > 0]) + elif self.config["backgroundSOBoption"] == 0: + pass + self._bg_sob = bg_h + + def _init_irf(self) -> None: + """Docstring""" + self._irf = np.zeros( + ( + len(self._sin_dec_bins) - 1, + len(self._log_energy_bins) - 1, + len(self._truelogebin) - 1, + ) + ) + self._trueebin = 10 ** (self._truelogebin[:-1]) + sindec_idx = ( + np.digitize(np.sin(self.data_handler.full_sim["dec"]), self._sin_dec_bins) + - 1 + ) + + for i in range(len(self._sin_dec_bins) - 1): + events_dec = self.data_handler.full_sim[(sindec_idx == i)] + loge_idx = np.digitize(events_dec["logE"], self._log_energy_bins) - 1 + + for j in range(len(self._log_energy_bins) - 1): + events = events_dec[(loge_idx == j)] + + # Don't bother if we don't find events. + if events["ow"].sum() == 0: + continue + + # True bins are in log(trueE) to ensure they're well spaced. + self._irf[i, j], _ = np.histogram( + np.log10(events["trueE"]), + bins=self._truelogebin, + weights=events["ow"], + ) + + # Have to pick an "energy" to assign to the bin. That's complicated, since + # you'd (in principle) want the flux-weighted average energy, but we don't + # have a flux function here. Instead, try just using the minimum energy of + # the bin? Should be fine for small enough bins. + # self._trueebin[i,j] = np.exp(bins[:-1] + (bins[1] - bins[0])) + # emean[i,j] = np.average(events['trueE'], weights=events['ow']) + + def build_sig_h(self, spectrum: spectral.BaseSpectrum) -> np.ndarray: + """Docstring""" + sig = np.zeros(self._bg_sob.shape) + flux = spectrum(self._trueebin * self._unit_scale) # converting unit + sig[self._sindec_bounds[0]:self._sindec_bounds[1], :] = np.dot( + self._irf[self._sindec_bounds[0]:self._sindec_bounds[1], :, :], flux + ) + sig /= np.sum(sig, axis=1)[:, None] + return sig + + @property + def source(self) -> sources.PointSource: + """Docstring""" + return self._source + + @source.setter + def source(self, source: sources.PointSource) -> None: + """Docstring""" + self._source = source + lower_sindec = np.maximum( + np.sin( + self.source.location[1] + - self.data_handler.config["reco_sampling_width"] + ), + -0.99, + ) + upper_sindec = np.minimum( + np.sin( + self.source.location[1] + + self.data_handler.config["reco_sampling_width"] + ), + 1, + ) + lower_sindec_index = np.searchsorted(self._sin_dec_bins, lower_sindec) - 1 + uppper_sindec_index = np.searchsorted(self._sin_dec_bins, upper_sindec) + self._sindec_bounds = np.array([lower_sindec_index, uppper_sindec_index]) + + def cal_sob_map(self) -> np.ndarray: + """Creates sob histogram for a given spectrum. + + Returns: + An array of signal-over-background values binned in sin(dec) and + log(energy) for a given gamma. + """ + sig_h = self.build_sig_h(self.spectrum) + + bin_spline = self._log_energy_bins[:-1] + # Normalize histogram by dec band + + # div-0 okay here + with np.errstate(divide="ignore", invalid="ignore"): + ratio = sig_h / self._bg_sob + + for i in range(ratio.shape[0]): + # Pick out the values we want to use. + # We explicitly want to avoid NaNs and infinities + good = np.isfinite(ratio[i]) & (ratio[i] > 0) + good_bins, good_vals = bin_spline[good], ratio[i][good] + if len(good_bins) > 1: + # Do a linear interpolation across the energy range + spline = Spline( + good_bins, good_vals, **self.config["energy_spline_kwargs"] + ) + + # And store the interpolated values + ratio[i] = spline(bin_spline) + elif len(good_bins) == 1: + ratio[i] = good_vals + else: + ratio[i] = 0 + + return ratio + + def calculate_drop_mask(self, events: np.ndarray) -> np.ndarray: + """Docstring""" + return np.ones(len(events), dtype=bool) + + @property + def spectrum(self) -> spectral.BaseSpectrum: + """Docstring""" + return self._spectrum + + @spectrum.setter + def spectrum(self, spectrum: spectral.BaseSpectrum) -> None: + """Docstring""" + if isinstance(spectrum, property): + # initial value not specified, use default + spectrum = ThreeMLPSEnergyTermFactory._spectrum + self._spectrum = spectrum + + @classmethod + def generate_config(cls): + """Docstring""" + config = super().generate_config() + config["sin_dec_bins"] = 68 + config["log_energy_bins"] = 42 + config["log_energy_bounds"] = (1, 8) + config["energy_spline_kwargs"] = { + "k": 1, + "s": 0, + "ext": 3, + } + config["backgroundSOBoption"] = 0 + config["mc_bkgweight"] = None + config["list_sin_dec_bins"] = PSTrackv4_sin_dec_bin + config["list_log_energy_bins"] = PSTrackv4_log_energy_bins + config["list_truelogebin"] = np.arange( + 2, 9.01 + 0.01, 0.01 + ) + config["Energy_convesion(ToGeV)"] = 1e6 # GeV to keV + return config diff --git a/build/lib/mla/threeml/spectral.py b/build/lib/mla/threeml/spectral.py new file mode 100644 index 00000000..986ec3a5 --- /dev/null +++ b/build/lib/mla/threeml/spectral.py @@ -0,0 +1,157 @@ +""" +The classes in this file are example time profiles that can be used in the +analysis classes. There is also GenericProfile, an abstract parent class to +inherit from to create other time profiles. +""" + + +__author__ = 'John Evans and Jason Fan' +__copyright__ = 'Copyright 2024' +__credits__ = ['John Evans', 'Jason Fan', 'Michael Larson'] +__license__ = 'Apache License 2.0' +__version__ = '1.4.1' +__maintainer__ = 'Jason Fan' +__email__ = 'klfan@terpmail.umd.edu' +__status__ = 'Development' + + +from typing import Optional, Union + +import abc +import numpy as np + + +class BaseSpectrum: + """A generic base class to standardize the methods for the Spectrum. + + Any callable function will work. + + Attributes: + """ + + __metaclass__ = abc.ABCMeta + + @abc.abstractmethod + def __init__(self) -> None: + """Initializes the Spectrum. + """ + + @abc.abstractmethod + def __call__(self, energy: Union[np.ndarray, float], + **kwargs) -> np.ndarray: + """return the differential flux at given energy(s). + + Args: + energy: An array of energy + + Returns: + A numpy array of differential flux. + """ + + @abc.abstractmethod + def __str__(self) -> None: + """String representation + """ + return 'Base spectrum' + + +class PowerLaw(BaseSpectrum): + """Spectrum class for PowerLaw. + + Use this to produce PowerLaw spectrum. + + Attributes: + E0 (float): pivot energy + A (float): Flux Norm + gamma(float): Spectral index + Ecut(float): Cut-off energy + """ + + def __init__(self, energy_0: float, flux_norm: float, gamma: float, + energy_cut: Optional[float] = None) -> None: + """ Constructor of PowerLaw object. + + Args: + energy_0: Normalize Energy + flux_norm: Flux Normalization + gamma: Spectral index + energy_cut: Cut-off energy + """ + + super().__init__() + self.energy_0 = energy_0 + self.flux_norm = flux_norm + self.gamma = gamma + self.energy_cut = energy_cut + + def __call__(self, energy: Union[np.ndarray, float], + **kwargs) -> np.ndarray: + """Evaluate spectrum at energy E according to + + dN/dE = A (E / E0)^gamma + + where A has units of events / (GeV cm^2 s). We treat + the 'events' in the numerator as implicit and say the + units are [GeV^-1 cm^-2 s^-1]. Specifying Ecut provides + an optional spectral cutoff according to + + dN/dE = A (E / E0)^gamma * exp( -E/Ecut ) + + Args: + energy : Evaluation energy [GeV] + + Returns: + np.ndarray of differential flux + """ + flux_norm = kwargs.pop('flux_norm', self.flux_norm) + energy_0 = kwargs.pop('energy_0', self.energy_0) + energy_cut = kwargs.pop('energy_cut', self.energy_cut) + gamma = kwargs.pop('gamma', self.gamma) + + flux = flux_norm * (energy / energy_0)**(gamma) + + # apply optional exponential cutoff + if energy_cut is not None: + flux *= np.exp(-energy / self.energy_cut) + + return flux + + def __str__(self) -> None: + """String representation + """ + return 'PowerLaw' + + +class CustomSpectrum(BaseSpectrum): + '''Custom spectrum using callable object + ''' + def __init__(self, spectrum): + """Constructor of CustomSpectrum object. + + Constructor + + Args: + spectrum: Any callable object + + """ + + super().__init__() + self.spectrum = spectrum + + def __call__(self, energy: Union[np.ndarray, float], + **kwargs) -> np.ndarray: + """Evaluate spectrum at energy E + + Constructor + + Args: + energy : Evaluation energy + + Returns: + np.ndarray of differential flux + """ + return self.spectrum(energy) + + def __str__(self): + r"""String representation of class""" + return 'CustomSpectrum' diff --git a/build/lib/mla/time_profiles.py b/build/lib/mla/time_profiles.py new file mode 100644 index 00000000..06fb3931 --- /dev/null +++ b/build/lib/mla/time_profiles.py @@ -0,0 +1,632 @@ +""" +The classes in this file are example time profiles that can be used in the +analysis classes. There is also GenericProfile, an abstract parent class to +inherit from to create other time profiles. +""" + +__author__ = 'John Evans and Jason Fan' +__copyright__ = 'Copyright 2024' +__credits__ = ['John Evans', 'Jason Fan', 'Michael Larson'] +__license__ = 'Apache License 2.0' +__version__ = '1.4.1' +__maintainer__ = 'Jason Fan' +__email__ = 'klfan@terpmail.umd.edu' +__status__ = 'Development' + +from typing import Callable, ClassVar, Dict, List, Optional, Tuple + +import abc +import dataclasses + +import numpy as np +import scipy.stats + +from . import configurable +from .params import Params + + +@dataclasses.dataclass +class GenericProfile(configurable.Configurable): + """A generic base class to standardize the methods for the time profiles. + + While I'm only currently using scipy-based + probability distributions, you can write your own if you + want. Just be sure to define these methods and ensure that + the PDF is normalized! + + Attributes: + exposure (float): + range (Tuple[Optional[float], Optional[float]]): The range of allowed + times for for events injected using this time profile. + default_params (Dict[str, float]): A dictionary of fitting parameters + for this time profile. + param_dtype (List[Tuple[str, str]]): The numpy dytpe for the fitting + parameters. + """ + + __metaclass__ = abc.ABCMeta + + @abc.abstractmethod + def pdf(self, times: np.ndarray) -> np.ndarray: + """Get the probability amplitude given a time for this time profile. + + Args: + times: An array of event times to get the probability amplitude for. + + Returns: + A numpy array of probability amplitudes at the given times. + """ + + @abc.abstractmethod + def logpdf(self, times: np.ndarray) -> np.ndarray: + """Get the log(probability) given a time for this time profile. + + Args: + times: An array of times to get the log(probability) of. + + Returns: + A numpy array of log(probability) at the given times. + """ + + @abc.abstractmethod + def random(self, size: int) -> np.ndarray: + """Get random times sampled from the pdf of this time profile. + + Args: + size: The number of times to return. + + Returns: + An array of times. + """ + + @abc.abstractmethod + def x0(self, times: np.ndarray) -> Tuple: + """Gets a tuple of initial guess to use when fitting parameters. + + The guesses are arrived at by simple approximations using the given + times. + + Args: + times: An array of times to use to approximate the fitting + parameters of this time profile. + + Returns: + A tuple of approximate parameters. + """ + + @abc.abstractmethod + def bounds( + self, + time_profile: 'GenericProfile', + ) -> List[Tuple[Optional[float], Optional[float]]]: + """Get a list of tuples of bounds for the parameters of this profile. + + Uses another time profile to constrain the bounds. This is usually + needed to constrain the bounds of a signal time profile given a + background time profile. + + Args: + time_profile: Another time profile to constrain from. + + Returns: + A list of tuples of bounds for fitting the parameters of this time + profile. + """ + + @abc.abstractmethod + def cdf(self, times: np.ndarray) -> np.ndarray: + """Docstring""" + + @abc.abstractmethod + def inverse_transform_sample( + self, start_times: np.ndarray, stop_times: np.ndarray) -> np.ndarray: + """Docstring""" + + @property + @abc.abstractmethod + def params(self) -> dict: + """Docstring""" + + @params.setter + @abc.abstractmethod + def params(self, params: Params) -> None: + """Docstring""" + + @property + @abc.abstractmethod + def param_bounds(self) -> dict: + """Docstring""" + + @property + @abc.abstractmethod + def exposure(self) -> float: + """Docstring""" + + @property + @abc.abstractmethod + def range(self) -> Tuple[float, float]: + """Gets the maximum and minimum values for the times in this profile. + """ + + @property + @abc.abstractmethod + def default_params(self) -> Dict[str, float]: + """Returns the initial parameters formatted for ts calculation output. + """ + + @property + @abc.abstractmethod + def param_dtype(self) -> np.dtype: + """Returns the parameter names and datatypes formatted for numpy dtypes. + """ + + +@dataclasses.dataclass +class GaussProfile(GenericProfile): + """Time profile class for a gaussian distribution. + + Use this to produce gaussian-distributed times for your source. + + Attributes: + mean (float): The center of the distribution. + sigma (float): The spread of the distribution. + scipy_dist(scipy.stats.rv_continuous): + exposure (float): + range (Tuple[Optional[float], Optional[float]]): The range of allowed + times for for events injected using this time profile. + default_params (Dict[str, float]): A dictionary of fitting parameters + for this time profile. + param_dtype (List[Tuple[str, str]]): The numpy dytpe for the fitting + parameters. + """ + scipy_dist: scipy.stats.distributions.rv_frozen = dataclasses.field(init=False) + _mean: float = dataclasses.field(init=False, repr=False) + _sigma: float = dataclasses.field(init=False, repr=False) + _param_dtype: ClassVar[np.dtype] = np.dtype( + [('mean', np.float32), ('sigma', np.float32)]) + + def __post_init__(self) -> None: + """Initializes the time profile.""" + self._mean = self.config['mean'] + self._sigma = self.config['sigma'] + self.scipy_dist = scipy.stats.norm(self._mean, self._sigma) + + def pdf(self, times: np.ndarray) -> np.ndarray: + """Calculates the probability for each time. + + Args: + times: A numpy list of times to evaluate. + + Returns: + A numpy array of probability amplitudes at the given times. + """ + return self.scipy_dist.pdf(times) + + def logpdf(self, times: np.ndarray) -> np.ndarray: + """Calculates the log(probability) for each time. + + Args: + times: A numpy list of times to evaluate. + + Returns: + A numpy array of log(probability) at the given times. + """ + return self.scipy_dist.logpdf(times) + + def random(self, size: int = 1) -> np.ndarray: + """Returns random values following the gaussian distribution. + + Args: + size: The number of random values to return. + + Returns: + An array of times. + """ + return self.scipy_dist.rvs(size=size) + + def x0(self, times: np.ndarray) -> tuple: + """Returns good guesses for mean and sigma based on given times. + + Args: + times: A numpy list of times to evaluate. + + Returns: + A tuple of mean and sigma guesses. + """ + x0_mean = np.average(times) + x0_sigma = np.std(times) + return x0_mean, x0_sigma + + def bounds(self, time_profile: GenericProfile) -> List[tuple]: + """Returns good bounds for this time profile given another time profile. + + Limits the mean to be within the range of the other profile and limits + the sigma to be >= 0 and <= the width of the other profile. + + Args: + time_profile: Another time profile to use to define the parameter + bounds of this time profile. + + Returns: + A list of tuples of bounds for fitting the parameters in this time + profile. + """ + if np.nan in time_profile.range: + return [time_profile.range, (0, np.nan)] + + diff = time_profile.range[1] - time_profile.range[0] + return [time_profile.range, (0, diff)] + + def cdf(self, times: np.ndarray) -> np.ndarray: + """Docstring""" + return self.scipy_dist.cdf(times) + + def inverse_transform_sample( + self, start_times: np.ndarray, stop_times: np.ndarray) -> np.ndarray: + """Docstring""" + start_cdfs = self.cdf(start_times) + stop_cdfs = self.cdf(stop_times) + cdfs = np.random.uniform(start_cdfs, stop_cdfs) + return self.scipy_dist.ppf(cdfs) + + @property + def params(self) -> dict: + """Docstring""" + return {'mean': self._mean, 'sigma': self._sigma} + + @params.setter + def params(self, params: Params) -> None: + """Docstring""" + update = False + + if 'mean' in params: + self._mean = params['mean'] + update = True + if 'sigma' in params: + self._sigma = params['sigma'] + update = True + + if update: + self.scipy_dist = scipy.stats.norm(self._mean, self._sigma) + + @property + def param_bounds(self) -> dict: + return {'mean': self.range, 'sigma': (0, np.inf)} + + @property + def exposure(self) -> float: + return np.sqrt(2 * np.pi * self._sigma**2) + + @property + def range(self) -> Tuple[float, float]: + return -np.inf, np.inf + + @property + def param_dtype(self) -> np.dtype: + return self._param_dtype + + @classmethod + def generate_config(cls) -> dict: + """Docstring""" + config = super().generate_config() + config['mean'] = np.nan + config['sigma'] = np.nan + return config + + +@dataclasses.dataclass +class UniformProfile(GenericProfile): + """Time profile class for a uniform distribution. + + Use this for background or if you want to assume a steady signal from + your source. + + Attributes: + exposure (float): + range (Tuple[Optional[float], Optional[float]]): The range of allowed + times for for events injected using this time profile. + default_params (Dict[str, float]): A dictionary of fitting parameters + for this time profile. + param_dtype (List[Tuple[str, str]]): The numpy dytpe for the fitting + parameters. + """ + _range: Tuple[float, float] = dataclasses.field(init=False, repr=False) + _param_dtype: ClassVar[np.dtype] = np.dtype( + [('start', np.float32), ('length', np.float32)]) + + def __post_init__(self) -> None: + """Constructs the time profile.""" + self._range = (self.config['start'], self.config['start'] + self.config['length']) + + def pdf(self, times: np.ndarray) -> np.ndarray: + """Calculates the probability for each time. + + Args: + times: A numpy list of times to evaluate. + + Returns: + A numpy array of probability amplitudes at the given times. + """ + output = np.zeros_like(times) + output[ + (times >= self.range[0]) & (times < self.range[1]) + ] = 1 / (self.range[1] - self.range[0]) + return output + + def logpdf(self, times: np.ndarray) -> np.ndarray: + """Calculates the log(probability) for each time. + + Args: + times: A numpy list of times to evaluate. + + Returns: + A numpy array of log(probability) at the given times. + """ + return np.log(self.pdf(times)) + + def random(self, size: int = 1) -> np.ndarray: + """Returns random values following the uniform distribution. + + Args: + size: The number of random values to return. + + Returns: + An array of times. + """ + return np.random.uniform(*self.range, size) + + def x0(self, times: np.ndarray) -> Tuple[float, float]: + """Returns good guesses for start and stop based on given times. + + Args: + times: A numpy list of times to evaluate. + + Returns: + A tuple of start and stop guesses. + """ + x0_start = np.min(times) + x0_end = np.max(times) + return x0_start, x0_end + + def bounds(self, time_profile: GenericProfile + ) -> List[Tuple[Optional[float], Optional[float]]]: + """Given some other profile, returns allowable ranges for parameters. + + Args: + time_profile: Another time profile used to get the limits of start + and length. + + Returns: + A list of tuples of bounds for fitting. + """ + diff = time_profile.range[1] - time_profile.range[0] + return [time_profile.range, (0, diff)] + + def cdf(self, times: np.ndarray) -> np.ndarray: + """Docstring""" + return np.clip((times - self.range[0]) / (self.range[1] - self.range[0]), 0, 1) + + def inverse_transform_sample( + self, start_times: np.ndarray, stop_times: np.ndarray) -> np.ndarray: + """Docstring""" + return np.random.uniform( + np.maximum(start_times, self.range[0]), + np.minimum(stop_times, self.range[1]), + ) + + @property + def params(self) -> dict: + """Docstring""" + return {'start': self._range[0], 'length': self._range[1] - self._range[0]} + + @params.setter + def params(self, params: Params) -> None: + """Docstring""" + if 'start' in params: + self._range = ( + params['start'], params['start'] + self._range[1] - self._range[0]) + if 'length' in params: + self._range = (self._range[0], self._range[0] + params['length']) + + @property + def param_bounds(self) -> dict: + return {'start': (-np.inf, np.inf), 'length': (0, np.inf)} + + @property + def exposure(self) -> float: + return self._range[1] - self._range[0] + + @property + def range(self) -> Tuple[float, float]: + return self._range + + @property + def param_dtype(self) -> np.dtype: + return self._param_dtype + + @classmethod + def generate_config(cls) -> dict: + """Docstring""" + config = super().generate_config() + config['start'] = np.nan + config['length'] = np.nan + return config + + +@dataclasses.dataclass +class CustomProfile(GenericProfile): + """Time profile class for a custom binned distribution. + + This time profile uses a binned pdf defined between 0 and 1. Normalization + is handled internally and not required beforehand. + + Attributes: + pdf (Callable[[np.array, Tuple[float, float]], np.array]): The + distribution function. This function needs to accept an array of bin + centers and a time window as a tuple, and it needs to return an + array of probability densities at the given bin centers. + dist (scipy.stats.rv_histogram): The histogrammed version of the + distribution function. + exposure (float): + range (Tuple[Optional[float], Optional[float]]): The range of allowed + times for for events injected using this time profile. + default_params (Dict[str, float]): A dictionary of fitting parameters + for this time profile. + param_dtype (List[Tuple[str, str]]): The numpy dytpe for the fitting + parameters. + """ + dist: Callable[[np.ndarray, Tuple[float, float]], np.ndarray] + _dist: scipy.stats.rv_histogram = dataclasses.field(init=False, repr=False) + _offset: float = dataclasses.field(init=False, repr=False) + _exposure: float = dataclasses.field(init=False, repr=False) + _param_dtype: ClassVar[np.dtype] = np.dtype([('offset', np.float32)]) + + @property + def dist(self) -> scipy.stats.rv_histogram: + """Docstring""" + return self._dist + + @dist.setter + def dist( + self, + dist: Callable[[np.ndarray, Tuple[float, float]], np.ndarray], + ) -> None: + """Constructs the time profile. + + Args: + dist: + """ + self._offset = self.config['offset'] + + if isinstance(self.config['bins'], int): + bin_edges = np.linspace(*self.config['range'], self.config['bins']) + else: + span = self.config['range'][1] - self.config['range'][0] + bin_edges = span * np.array(self.config['bins']) + + bin_widths = np.diff(bin_edges) + bin_centers = bin_edges[:-1] + bin_widths + hist = dist(bin_centers, tuple(self.config['range'])) + + area_under_hist = np.sum(hist * bin_widths) + hist *= 1 / area_under_hist + self._exposure = 1 / np.max(hist) + hist *= bin_widths + + self._dist = scipy.stats.rv_histogram((hist, bin_edges)) + + def pdf(self, times: np.ndarray) -> np.ndarray: + """Calculates the probability density for each time. + + Args: + times: An array of times to evaluate. + + Returns: + An array of probability densities at the given times. + """ + return self.dist.pdf(times + self.offset) + + def logpdf(self, times: np.ndarray) -> np.ndarray: + """Calculates the log(probability) for each time. + + Args: + times: An array of times to evaluate. + + Returns: + An array of the log(probability density) for the given times. + """ + return self.dist.logpdf(times + self.offset) + + def random(self, size: int = 1) -> np.ndarray: + """Returns random values following the uniform distribution. + + Args: + size: The number of random values to return. + + Returns: + An array of random values sampled from the histogram distribution. + """ + return self.dist.rvs(size=size) + self.offset + + def x0(self, times: np.ndarray) -> Tuple[float, float]: + """Gives a guess of the parameters of this type of time profile. + + Args: + times: An array of times to use to guess the parameters. + + Returns: + The guessed start and end times of the distribution that generated + the given times. + """ + x0_start = np.min(times) + x0_end = np.max(times) + return x0_start, x0_end + + def bounds(self, time_profile: GenericProfile + ) -> List[Tuple[Optional[float], Optional[float]]]: + """Given some other profile, returns allowable ranges for parameters. + + Args: + time_profile: Another time profile to use to get the bounds. + + Returns: + The fitting bounds for the parameters of this time profile. + """ + return [time_profile.range, time_profile.range] + + def cdf(self, times: np.ndarray) -> np.ndarray: + """Docstring""" + return self.dist.cdf(times) + + def inverse_transform_sample( + self, start_times: np.ndarray, stop_times: np.ndarray) -> np.ndarray: + """Docstring""" + start_cdfs = self.cdf(start_times) + stop_cdfs = self.cdf(stop_times) + cdfs = np.random.uniform(start_cdfs, stop_cdfs) + return self.dist.ppf(cdfs) + + @property + def params(self) -> dict: + """Docstring""" + return {'offset': self.offset} + + @params.setter + def params(self, params: Params) -> None: + """Docstring""" + if 'offset' in params: + self.offset = params['offset'] + + @property + def param_bounds(self) -> dict: + return {'offset': (-np.inf, np.inf)} + + @property + def exposure(self) -> float: + return self._exposure + + @property + def offset(self) -> float: + return self._offset + + @offset.setter + def offset(self, offset: float) -> None: + self._offset = offset + + @property + def range(self) -> Tuple[Optional[float], Optional[float]]: + return ( + self.config['range'][0] + self.offset, self.config['range'][1] + self.offset) + + @property + def param_dtype(self) -> np.dtype: + return self._param_dtype + + @classmethod + def generate_config(cls) -> dict: + """Docstring""" + config = super().generate_config() + config['range'] = (np.nan, np.nan) + config['bins'] = 100 + config['offset'] = 0 + return config diff --git a/build/lib/mla/trial_generators.py b/build/lib/mla/trial_generators.py new file mode 100644 index 00000000..10939a00 --- /dev/null +++ b/build/lib/mla/trial_generators.py @@ -0,0 +1,115 @@ +"""Docstring""" + +__author__ = 'John Evans and Jason Fan' +__copyright__ = 'Copyright 2024' +__credits__ = ['John Evans', 'Jason Fan', 'Michael Larson'] +__license__ = 'Apache License 2.0' +__version__ = '1.4.1' +__maintainer__ = 'Jason Fan' +__email__ = 'klfan@terpmail.umd.edu' +__status__ = 'Development' + +import dataclasses + +import numpy as np +import numpy.lib.recfunctions as rf + +from . import utility_functions as uf +from . import configurable + +from .data_handlers import DataHandler +from .sources import PointSource + + +@dataclasses.dataclass +class SingleSourceTrialGenerator(configurable.Configurable): + """Docstring""" + + data_handler: DataHandler + source: PointSource + _source: PointSource = dataclasses.field(init=False, repr=False) + + def __call__(self, n_signal: float = 0) -> np.ndarray: + """Produces a single trial of background+signal events based on inputs. + + Args: + n_signal: flux norm if not fixed_ns + + Returns: + An array of combined signal and background events. + """ + rng = np.random.default_rng(self.config["random_seed"]) + n_background = rng.poisson(self.data_handler.n_background) + if not self.config["fixed_ns"]: + n_signal = rng.poisson(self.data_handler.calculate_n_signal(n_signal)) + + background = self.data_handler.sample_background(n_background, rng) + background["ra"] = rng.uniform(0, 2 * np.pi, len(background)) + + if n_signal > 0: + signal = self.data_handler.sample_signal(int(n_signal), rng) + signal = self._rotate_signal(signal) + else: + signal = np.empty(0, dtype=background.dtype) + + # Because we want to return the entire event and not just the + # number of events, we need to do some numpy magic. Specifically, + # we need to remove the fields in the simulated events that are + # not present in the data events. These include the true direction, + # energy, and 'oneweight'. + signal = rf.drop_fields( + signal, [n for n in signal.dtype.names if n not in background.dtype.names] + ) + + # Combine the signal background events and time-sort them. + # Use recfunctions.stack_arrays to prevent numpy from scrambling entry order + if background.dtype == signal.dtype: + return np.concatenate([background, signal]) + else: + return rf.stack_arrays( + [background, signal], autoconvert=True, usemask=False + ) + + def _rotate_signal(self, signal: np.ndarray) -> np.ndarray: + """Docstring""" + ra, dec = self.source.sample(len(signal)) + + signal["ra"], signal["dec"] = uf.rotate( + signal["trueRa"], + signal["trueDec"], + ra, + dec, + signal["ra"], + signal["dec"], + ) + + signal["trueRa"], signal["trueDec"] = uf.rotate( + signal["trueRa"], + signal["trueDec"], + ra, + dec, + signal["trueRa"], + signal["trueDec"], + ) + + signal["sindec"] = np.sin(signal["dec"]) + return signal + + @property + def source(self) -> PointSource: + """Docstring""" + return self._source + + @source.setter + def source(self, source: PointSource) -> None: + """Docstring""" + self.data_handler.dec_cut_location = source.config["dec"] + self._source = source + + @classmethod + def generate_config(cls) -> dict: + """Docstring""" + config = super().generate_config() + config["random_seed"] = None + config["fixed_ns"] = False + return config diff --git a/build/lib/mla/utility_functions.py b/build/lib/mla/utility_functions.py new file mode 100644 index 00000000..a4eb7f4e --- /dev/null +++ b/build/lib/mla/utility_functions.py @@ -0,0 +1,255 @@ +""" +Math functions needed for this package +""" + +__author__ = 'John Evans and Jason Fan' +__copyright__ = 'Copyright 2024' +__credits__ = ['John Evans', 'Jason Fan', 'Michael Larson'] +__license__ = 'Apache License 2.0' +__version__ = '1.4.1' +__maintainer__ = 'Jason Fan' +__email__ = 'klfan@terpmail.umd.edu' +__status__ = 'Development' + +import numpy as np + + +def ra_to_rad(hrs: float, mins: float, secs: float) -> float: + """Converts right ascension to radians. + + Args: + hrs: Hours. + mins: Minutes. + secs: Seconds. + + Returns: + Radian representation of right ascension. + """ + return (hrs * 15 + mins / 4 + secs / 240) * np.pi / 180 + + +def dec_to_rad(sign: int, deg: float, mins: float, secs: float) -> float: + """Converts declination to radians. + + Args: + sign: A positive integer for a positive sign, a negative integer for a + negative sign. + deg: Degrees. + mins: Minutes. + secs: Seconds. + + Returns: + Radian representation of declination. + """ + return sign / np.abs(sign) * (deg + mins / 60 + secs / 3600) * np.pi / 180 + + +def cross_matrix(mat: np.ndarray) -> np.ndarray: + """Calculate cross product matrix. + A[ij] = x_i * y_j - y_i * x_j + Args: + mat: A 2D array to take the cross product of. + Returns: + The cross matrix. + """ + skv = np.roll(np.roll(np.diag(mat.ravel()), 1, 1), -1, 0) + return skv - skv.T + + +def rotate( + ra1: float, + dec1: float, + ra2: float, + dec2: float, + ra3: float, + dec3: float, +) -> tuple: + """Rotation matrix for rotation of (ra1, dec1) onto (ra2, dec2). + + The rotation is performed on (ra3, dec3). + + Args: + ra1: The right ascension of the point to be rotated from. + dec1: The declination of the point to be rotated from. + ra2: the right ascension of the point to be rotated onto. + dec2: the declination of the point to be rotated onto. + ra3: the right ascension of the point that will actually be rotated. + dec3: the declination of the point that will actually be rotated. + + Returns: + The rotated ra3 and dec3. + + Raises: + IndexError: Arguments must all have the same dimension. + """ + ra1 = np.atleast_1d(ra1) + dec1 = np.atleast_1d(dec1) + ra2 = np.atleast_1d(ra2) + dec2 = np.atleast_1d(dec2) + ra3 = np.atleast_1d(ra3) + dec3 = np.atleast_1d(dec3) + + if not len(ra1) == len(dec1) == len(ra2) == len(dec2) == len(ra3) == len(dec3): + raise IndexError("Arguments must all have the same dimension.") + + cos_alpha = np.cos(ra2 - ra1) * np.cos(dec1) * np.cos(dec2) + np.sin(dec1) * np.sin( + dec2 + ) + + # correct rounding errors + cos_alpha[cos_alpha > 1] = 1 + cos_alpha[cos_alpha < -1] = -1 + + alpha = np.arccos(cos_alpha) + vec1 = np.vstack( + [np.cos(ra1) * np.cos(dec1), np.sin(ra1) * np.cos(dec1), np.sin(dec1)] + ).T + vec2 = np.vstack( + [np.cos(ra2) * np.cos(dec2), np.sin(ra2) * np.cos(dec2), np.sin(dec2)] + ).T + vec3 = np.vstack( + [np.cos(ra3) * np.cos(dec3), np.sin(ra3) * np.cos(dec3), np.sin(dec3)] + ).T + nvec = np.cross(vec1, vec2) + norm = np.sqrt(np.sum(nvec ** 2, axis=1)) + nvec[norm > 0] /= norm[np.newaxis, norm > 0].T + + one = np.diagflat(np.ones(3)) + ntn = np.array([np.outer(nv, nv) for nv in nvec]) + nx = np.array([cross_matrix(nv) for nv in nvec]) + + r = np.array( + [ + (1.0 - np.cos(a)) * ntn_i + np.cos(a) * one + np.sin(a) * nx_i + for a, ntn_i, nx_i in zip(alpha, ntn, nx) + ] + ) + vec = np.array([np.dot(r_i, vec_i.T) for r_i, vec_i in zip(r, vec3)]) + + r_a = np.arctan2(vec[:, 1], vec[:, 0]) + dec = np.arcsin(vec[:, 2]) + + r_a += np.where(r_a < 0.0, 2.0 * np.pi, 0.0) + + return r_a, dec + + +def angular_distance(src_ra: float, src_dec: float, r_a: float, dec: float) -> float: + """Computes angular distance between source and location. + + Args: + src_ra: The right ascension of the first point (radians). + src_dec: The declination of the first point (radians). + r_a: The right ascension of the second point (radians). + dec: The declination of the second point (radians). + + Returns: + The distance, in radians, between the two points. + """ + sin_dec = np.sin(dec) + + cos_dec = np.sqrt(1.0 - sin_dec ** 2) + + cos_dist = (np.cos(src_ra - r_a) * np.cos(src_dec) * cos_dec) + np.sin( + src_dec + ) * sin_dec + # handle possible floating precision errors + cos_dist = np.clip(cos_dist, -1, 1) + + return np.arccos(cos_dist) + + +def newton_method(sob: np.ndarray, n_drop: float) -> float: + """Docstring + + Args: + sob: + n_drop: + Returns: + + """ + newton_precision = 0 + newton_iterations = 20 + precision = newton_precision + 1 + eps = 1e-5 + k = 1 / (sob - 1) + x = [1.0 / n_drop] * newton_iterations + + for i in range(newton_iterations - 1): + # get next iteration and clamp + inv_terms = x[i] + k + inv_terms[inv_terms == 0] = eps + terms = 1 / inv_terms + drop_term = 1 / (x[i] - 1) + d1 = np.sum(terms) + n_drop * drop_term + d2 = np.sum(terms ** 2) + n_drop * drop_term ** 2 + x[i + 1] = min(1 - eps, max(0, x[i] + d1 / d2)) + + if ( + x[i] == x[i + 1] + or (x[i] < x[i + 1] and x[i + 1] <= x[i] * precision) + or (x[i + 1] < x[i] and x[i] <= x[i + 1] * precision) + ): + break + return x[i + 1] + + +def newton_method_multidataset( + sob: list, n_drop: np.ndarray, fraction: np.ndarray +) -> float: + """Docstring + + Args: + sob: + n_drop: + Returns: + + """ + newton_precision = 0 + newton_iterations = 20 + precision = newton_precision + 1 + eps = 1e-5 + k = [] + for i in range(len(sob)): + k.append(1 / (sob[i] - 1)) + x = [0.0] * newton_iterations + for i in range(newton_iterations - 1): + # get next iteration and clamp + d1 = 0 + d2 = 0 + for j in range(len(sob)): + inv_terms = x[i] * fraction[j] + k[j] + inv_terms[inv_terms == 0] = eps + terms = fraction[j] / inv_terms + drop_term = fraction[j] / (x[i] * fraction[j] - 1) + d1 += np.sum(terms) + n_drop[j] * drop_term + d2 += np.sum(terms ** 2) + n_drop[j] * drop_term ** 2 + x[i + 1] = min(1 - eps, max(0, x[i] + d1 / d2)) + + if ( + x[i] == x[i + 1] + or (x[i] < x[i + 1] and x[i + 1] <= x[i] * precision) + or (x[i + 1] < x[i] and x[i] <= x[i + 1] * precision) + ): + break + return x[i + 1] + + +def trimsim(sim: np.ndarray, fraction: float, scaleow: bool = True) -> np.ndarray: + """Keep only fraction of the simulation + + Args: + sim: simulation. + fraction: Fraction of sim to keep(will round to int). + scaleow: whether to scale the ow. + + Returns: + Trimmed sim + """ + simsize = len(sim) + n_keep = int(fraction * simsize) + sim = np.random.choice(sim, n_keep) + if scaleow: + sim["ow"] = sim["ow"] * (simsize / float(n_keep)) + + return sim diff --git a/dist/mla-1.4.0-py3.12.egg b/dist/mla-1.4.0-py3.12.egg new file mode 100644 index 0000000000000000000000000000000000000000..0a7b164026abea0b14ef08a86c52d2291fc66094 GIT binary patch literal 109273 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

+sNw*S-ShNDxa3=w&B$m6=RBtWjKwr}8r;`MWJ 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 NU@lpE0nC1&{|6kSusZ+% literal 0 HcmV?d00001 diff --git a/mla/data_handlers.py b/mla/data_handlers.py index 8fa2b31a..c1377142 100644 --- a/mla/data_handlers.py +++ b/mla/data_handlers.py @@ -369,24 +369,28 @@ def sample_background(self, n: int, rng: np.random.Generator) -> np.ndarray: def sample_signal(self, n: int, rng: np.random.Generator) -> np.ndarray: """Docstring""" - events = super().sample_signal(n, rng) - return self._randomize_times(events, self._signal_time_profile) + self.events = super().sample_signal(n, rng) + #print('in data handlers events', self.events.dtype) + #print('signal time profile pdf',self._signal_time_profile.pdf(self._grl['start'])) + #print(self._randomize_times(self.events, self._signal_time_profile)) + return self._randomize_times(self.events, self._signal_time_profile) def _randomize_times( self, events: np.ndarray, time_profile: GenericProfile, ) -> np.ndarray: - grl_start_cdf = time_profile.cdf(self._grl['start']) - grl_stop_cdf = time_profile.cdf(self._grl['stop']) - valid = np.logical_and(grl_start_cdf < 1, grl_stop_cdf > 0) - rates = grl_stop_cdf[valid] - grl_start_cdf[valid] - + self.grl_start_cdf = time_profile.cdf(self._grl['start']) + self.grl_stop_cdf = time_profile.cdf(self._grl['stop']) + self.valid = np.logical_and(self.grl_start_cdf <= 1, self.grl_stop_cdf >= 0) + self.rates = self.grl_stop_cdf[self.valid] - self.grl_start_cdf[self.valid] + #print(self.rates, self.rates.sum()) + #print(len(events)) runs = np.random.choice( - self._grl[valid], + self._grl[self.valid], size=len(events), replace=True, - p=rates / rates.sum(), + p=self.rates / self.rates.sum(), ) events['time'] = time_profile.inverse_transform_sample( diff --git a/mla/threeml/IceCubeLike.py b/mla/threeml/IceCubeLike.py index a6862502..8c7439ce 100644 --- a/mla/threeml/IceCubeLike.py +++ b/mla/threeml/IceCubeLike.py @@ -1,3 +1,5 @@ +from __future__ import print_function +from __future__ import division """Docstring""" __author__ = 'John Evans and Jason Fan' @@ -9,8 +11,8 @@ __email__ = 'klfan@terpmail.umd.edu' __status__ = 'Development' -from __future__ import print_function -from __future__ import division +#from __future__ import print_function +#from __future__ import division from past.utils import old_div import collections import scipy @@ -943,7 +945,9 @@ def injection(self, n_signal=0, flux_norm=None, poisson=False): ratio_injection = self.dataset_ratio * n_signal for i, icecubeobject in enumerate(self.listoficecubelike): icecubeobject.trial_generator.config["fixed_ns"] = True + #print('in IceCubelike test injection', n_signal) injection_signal = np.random.poisson(ratio_injection[i]) + #print(injection_signal) tempdata = icecubeobject.trial_generator(injection_signal) self.listoficecubelike[i].update_data(tempdata) else: diff --git a/mla/threeml/sob_terms.py b/mla/threeml/sob_terms.py index ff7ce5a9..166575b3 100644 --- a/mla/threeml/sob_terms.py +++ b/mla/threeml/sob_terms.py @@ -268,9 +268,10 @@ class ThreeMLPSIRFEnergyTermFactory(ThreeMLPSEnergyTermFactory): _spectrum: spectral.BaseSpectrum = dataclasses.field( init=False, repr=False, default=spectral.PowerLaw(1e3, 1e-14, -2) ) + _bg_sob: np.ndarray = dataclasses.field(init=False, repr=False) _sin_dec_bins: np.ndarray = dataclasses.field( - init=False, repr=False, default=PSTrackv4_sin_dec_bin + init=False, repr=False, default_factory=lambda: PSTrackv4_sin_dec_bin.copy() ) _log_energy_bins: np.ndarray = dataclasses.field(init=False, repr=False) _bins: np.ndarray = dataclasses.field(init=False, repr=False) @@ -282,10 +283,13 @@ class ThreeMLPSIRFEnergyTermFactory(ThreeMLPSEnergyTermFactory): def __post_init__(self) -> None: """Docstring""" + print("Calling __post_init__") # or use logging + #self._source = self.config.get("source", None) if self.config["list_sin_dec_bins"] is None: self._sin_dec_bins = np.linspace(-1, 1, 1 + self.config["sin_dec_bins"]) else: self._sin_dec_bins = self.config["list_sin_dec_bins"] + if self.config["list_log_energy_bins"] is None: self._log_energy_bins = np.linspace( *self.config["log_energy_bounds"], 1 + self.config["log_energy_bins"] @@ -308,6 +312,7 @@ def __post_init__(self) -> None: ) lower_sindec_index = np.searchsorted(self._sin_dec_bins, lower_sindec) - 1 uppper_sindec_index = np.searchsorted(self._sin_dec_bins, upper_sindec) + #print(lower_sindec_index,uppper_sindec_index) self._sindec_bounds = np.array([lower_sindec_index, uppper_sindec_index]) self._bins = np.array([self._sin_dec_bins, self._log_energy_bins], dtype=object) self._truelogebin = self.config["list_truelogebin"] @@ -412,21 +417,23 @@ def source(self, source: sources.PointSource) -> None: self._source = source lower_sindec = np.maximum( np.sin( - self.source.location[1] + self._source.location[1] - self.data_handler.config["reco_sampling_width"] ), -0.99, ) upper_sindec = np.minimum( np.sin( - self.source.location[1] + self._source.location[1] + self.data_handler.config["reco_sampling_width"] ), 1, ) - lower_sindec_index = np.searchsorted(self._sin_dec_bins, lower_sindec) - 1 - uppper_sindec_index = np.searchsorted(self._sin_dec_bins, upper_sindec) - self._sindec_bounds = np.array([lower_sindec_index, uppper_sindec_index]) + #print(self) + #print(self._sin_dec_bins) + #lower_sindec_index = np.searchsorted(self._sin_dec_bins, lower_sindec) - 1 + #uppper_sindec_index = np.searchsorted(self._sin_dec_bins, upper_sindec) + #self._sindec_bounds = np.array([lower_sindec_index, uppper_sindec_index]) def cal_sob_map(self) -> np.ndarray: """Creates sob histogram for a given spectrum. diff --git a/mla/time_profiles.py b/mla/time_profiles.py index 06fb3931..009126a7 100644 --- a/mla/time_profiles.py +++ b/mla/time_profiles.py @@ -499,21 +499,28 @@ def dist( self._offset = self.config['offset'] if isinstance(self.config['bins'], int): - bin_edges = np.linspace(*self.config['range'], self.config['bins']) + bin_edges = np.linspace(*self.config['range'], self.config['bins']+1) + #print(bin_edges, *self.config['range']) else: span = self.config['range'][1] - self.config['range'][0] - bin_edges = span * np.array(self.config['bins']) - + bin_edges = np.array(self.config['bins']) #*span + #print('span, bin edges',span, bin_edges) + bin_widths = np.diff(bin_edges) - bin_centers = bin_edges[:-1] + bin_widths - hist = dist(bin_centers, tuple(self.config['range'])) - + bin_centers = bin_edges[:-1] + bin_widths/2 + + hist,_ = np.histogram(bin_centers,bins =bin_edges) + hist = hist.astype(float) + #print('hist, bin_widths', hist, bin_widths) area_under_hist = np.sum(hist * bin_widths) - hist *= 1 / area_under_hist + #print('area', area_under_hist) + hist *= 1. / area_under_hist + #print('normed hist, bin centers', hist, bin_centers) self._exposure = 1 / np.max(hist) hist *= bin_widths - - self._dist = scipy.stats.rv_histogram((hist, bin_edges)) + #print('final hist', hist) + #print('setting dist', scipy.stats.rv_histogram((hist,bin_edges)).pdf(bin_centers)) + self._dist = scipy.stats.rv_histogram((hist,bin_edges)) def pdf(self, times: np.ndarray) -> np.ndarray: """Calculates the probability density for each time. From 3bdbd81496f0cb8d78365d24ff2609d26721ada3 Mon Sep 17 00:00:00 2001 From: Aswathi Balagopal Date: Tue, 16 Sep 2025 13:23:19 -0500 Subject: [PATCH 2/5] remove build and dist --- .gitignore | 3 + build/lib/mla/__init__.py | 14 - build/lib/mla/analysis.py | 99 --- build/lib/mla/configurable.py | 23 - build/lib/mla/core.py | 9 - build/lib/mla/data_handlers.py | 402 ---------- build/lib/mla/minimizers.py | 120 --- build/lib/mla/params.py | 67 -- build/lib/mla/sob_terms.py | 355 --------- build/lib/mla/sources.py | 96 --- build/lib/mla/test_statistics.py | 206 ------ build/lib/mla/threeml/IceCubeLike.py | 978 ------------------------- build/lib/mla/threeml/__init__.py | 6 - build/lib/mla/threeml/data_handlers.py | 179 ----- build/lib/mla/threeml/profilellh.py | 100 --- build/lib/mla/threeml/sob_terms.py | 504 ------------- build/lib/mla/threeml/spectral.py | 157 ---- build/lib/mla/time_profiles.py | 632 ---------------- build/lib/mla/trial_generators.py | 115 --- build/lib/mla/utility_functions.py | 255 ------- dist/mla-1.4.0-py3.12.egg | Bin 109273 -> 0 bytes 21 files changed, 3 insertions(+), 4317 deletions(-) delete mode 100644 build/lib/mla/__init__.py delete mode 100644 build/lib/mla/analysis.py delete mode 100644 build/lib/mla/configurable.py delete mode 100644 build/lib/mla/core.py delete mode 100644 build/lib/mla/data_handlers.py delete mode 100644 build/lib/mla/minimizers.py delete mode 100644 build/lib/mla/params.py delete mode 100644 build/lib/mla/sob_terms.py delete mode 100644 build/lib/mla/sources.py delete mode 100644 build/lib/mla/test_statistics.py delete mode 100644 build/lib/mla/threeml/IceCubeLike.py delete mode 100644 build/lib/mla/threeml/__init__.py delete mode 100644 build/lib/mla/threeml/data_handlers.py delete mode 100644 build/lib/mla/threeml/profilellh.py delete mode 100644 build/lib/mla/threeml/sob_terms.py delete mode 100644 build/lib/mla/threeml/spectral.py delete mode 100644 build/lib/mla/time_profiles.py delete mode 100644 build/lib/mla/trial_generators.py delete mode 100644 build/lib/mla/utility_functions.py delete mode 100644 dist/mla-1.4.0-py3.12.egg diff --git a/.gitignore b/.gitignore index b68aa375..63265039 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,6 @@ Pipfile **/.venv **/.vim pyvenv.cfg + +build/ +dist/ diff --git a/build/lib/mla/__init__.py b/build/lib/mla/__init__.py deleted file mode 100644 index b67284b6..00000000 --- a/build/lib/mla/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -"""__init__.py""" -# flake8: noqa -from .analysis import * -from .core import * -from .configurable import * -from .data_handlers import * -from .minimizers import * -from .params import * -from .sob_terms import * -from .sources import * -from .test_statistics import * -from .time_profiles import * -from .trial_generators import * -from .utility_functions import * diff --git a/build/lib/mla/analysis.py b/build/lib/mla/analysis.py deleted file mode 100644 index 423bad84..00000000 --- a/build/lib/mla/analysis.py +++ /dev/null @@ -1,99 +0,0 @@ -"""Docstring""" - -__author__ = 'John Evans and Jason Fan' -__copyright__ = 'Copyright 2024' -__credits__ = ['John Evans', 'Jason Fan', 'Michael Larson'] -__license__ = 'Apache License 2.0' -__version__ = '1.4.1' -__maintainer__ = 'Jason Fan' -__email__ = 'klfan@terpmail.umd.edu' -__status__ = 'Development' -from typing import List, Tuple, Type -from dataclasses import dataclass, field - -from .core import generate_default_config -from .data_handlers import DataHandler -from .minimizers import Minimizer -from .params import Params -from .sob_terms import SoBTermFactory -from .sources import PointSource -from .test_statistics import LLHTestStatisticFactory -from .trial_generators import SingleSourceTrialGenerator - - -@dataclass -class SingleSourceLLHAnalysis: - """Docstring""" - config: dict - minimizer_class: Type[Minimizer] - sob_term_factories: List[SoBTermFactory] - data_handler_source: Tuple[DataHandler, PointSource] - _sob_term_factories: List[SoBTermFactory] = field(init=False, repr=False) - _data_handler_source: Tuple[DataHandler, PointSource] = field(init=False, repr=False) - _trial_generator: SingleSourceTrialGenerator = field(init=False, repr=False) - _test_statistic_factory: LLHTestStatisticFactory = field(init=False, repr=False) - - def produce_and_minimize( - self, - params: Params, - fitting_params: List[str], - n_signal: float = 0, - ) -> tuple: - """Docstring""" - trial = self._trial_generator(n_signal=n_signal) - test_statistic = self._test_statistic_factory(params, trial) - minimizer = self.minimizer_class( - self.config[self.minimizer_class.__name__], test_statistic) - return minimizer(fitting_params) - - def generate_params(self) -> Params: - """Docstring""" - return self._test_statistic_factory.generate_params() - - @property - def test_statistic_factory(self) -> LLHTestStatisticFactory: - """Docstring""" - return self._test_statistic_factory - - @property - def trial_generator(self) -> SingleSourceTrialGenerator: - """Docstring""" - return self._trial_generator - - @property - def sob_term_factories(self) -> List[SoBTermFactory]: - """Docstring""" - return self._sob_term_factories - - @sob_term_factories.setter - def sob_term_factories(self, sob_term_factories: List[SoBTermFactory]) -> None: - """Docstring""" - self._sob_term_factories = sob_term_factories - self._test_statistic_factory = LLHTestStatisticFactory( # pylint: disable=too-many-function-args - self.config['LLHTestStatisticFactory'], - self._sob_term_factories, - ) - - @property - def data_handler_source(self) -> Tuple[DataHandler, PointSource]: - """Docstring""" - return self._data_handler_source - - @data_handler_source.setter - def data_handler_source( - self, data_handler_source: Tuple[DataHandler, PointSource]) -> None: - """Docstring""" - self._data_handler_source = data_handler_source - self._trial_generator = SingleSourceTrialGenerator( - self.config['SingleSourceTrialGenerator'], - *self._data_handler_source, - ) - - @classmethod - def generate_default_config(cls, minimizer_class: Type[Minimizer]) -> dict: - """Docstring""" - return generate_default_config([ - minimizer_class, - SingleSourceTrialGenerator, - LLHTestStatisticFactory, - ]) diff --git a/build/lib/mla/configurable.py b/build/lib/mla/configurable.py deleted file mode 100644 index 2db30e66..00000000 --- a/build/lib/mla/configurable.py +++ /dev/null @@ -1,23 +0,0 @@ -"""Docstring""" - -__author__ = 'John Evans and Jason Fan' -__copyright__ = 'Copyright 2024' -__credits__ = ['John Evans', 'Jason Fan', 'Michael Larson'] -__license__ = 'Apache License 2.0' -__version__ = '1.4.1' -__maintainer__ = 'Jason Fan' -__email__ = 'klfan@terpmail.umd.edu' -__status__ = 'Development' - -import dataclasses - - -@dataclasses.dataclass -class Configurable: - """Docstring""" - config: dict - - @classmethod - def generate_config(cls) -> dict: - """Docstring""" - return {} diff --git a/build/lib/mla/core.py b/build/lib/mla/core.py deleted file mode 100644 index d79be033..00000000 --- a/build/lib/mla/core.py +++ /dev/null @@ -1,9 +0,0 @@ -"""Docstring""" - -from .configurable import Configurable - - -def generate_default_config(classes: list) -> dict: - """Docstring""" - return { - c.__name__: c.generate_config() for c in classes if issubclass(c, Configurable)} diff --git a/build/lib/mla/data_handlers.py b/build/lib/mla/data_handlers.py deleted file mode 100644 index 8fa2b31a..00000000 --- a/build/lib/mla/data_handlers.py +++ /dev/null @@ -1,402 +0,0 @@ -"""Docstring""" - -__author__ = 'John Evans and Jason Fan' -__copyright__ = 'Copyright 2024' -__credits__ = ['John Evans', 'Jason Fan', 'Michael Larson'] -__license__ = 'Apache License 2.0' -__version__ = '1.4.1' -__maintainer__ = 'Jason Fan' -__email__ = 'klfan@terpmail.umd.edu' -__status__ = 'Development' - -from typing import Tuple - -import abc -import copy -from dataclasses import dataclass -from dataclasses import field - -import numpy as np -import numpy.lib.recfunctions as rf -from scipy.interpolate import UnivariateSpline as Spline - -from . import configurable -from .time_profiles import GenericProfile - - -@dataclass -class DataHandler(configurable.Configurable): - """Docstring""" - _n_background: float = field(init=False, repr=False) - __metaclass__ = abc.ABCMeta - - @abc.abstractmethod - def sample_background(self, n: int, rng: np.random.Generator) -> np.ndarray: - """Docstring""" - - @abc.abstractmethod - def sample_signal(self, n: int, rng: np.random.Generator) -> np.ndarray: - """Docstring""" - - @abc.abstractmethod - def calculate_n_signal(self, time_integrated_flux: float) -> float: - """Docstring""" - - @abc.abstractmethod - def evaluate_background_sindec_pdf(self, events: np.ndarray) -> np.ndarray: - """Docstring""" - - @abc.abstractmethod - def build_background_sindec_logenergy_histogram(self, bins: np.ndarray) -> np.ndarray: - """Docstring""" - - @abc.abstractmethod - def build_signal_sindec_logenergy_histogram( - self, gamma: float, bins: np.ndarray) -> np.ndarray: - """Docstring""" - - @property - @abc.abstractmethod - def n_background(self) -> float: - """Docstring""" - - -@dataclass -class NuSourcesDataHandler(DataHandler): - """Docstring""" - sim: np.ndarray - data_grl: Tuple[np.ndarray, np.ndarray] - - _sim: np.ndarray = field(init=False, repr=False) - _full_sim: np.ndarray = field(init=False, repr=False) - _data: np.ndarray = field(init=False, repr=False) - _grl: np.ndarray = field(init=False, repr=False) - _n_background: float = field(init=False, repr=False) - _grl_rates: np.ndarray = field(init=False, repr=False) - _dec_spline: Spline = field(init=False, repr=False) - _livetime: float = field(init=False, repr=False) - _sin_dec_bins: np.ndarray = field(init=False, repr=False) - - def sample_background(self, n: int, rng: np.random.Generator) -> np.ndarray: - """Docstring""" - return rng.choice(self._data, n) - - def sample_signal(self, n: int, rng: np.random.Generator) -> np.ndarray: - """Docstring""" - return rng.choice( - self.sim, - n, - p=self.sim['weight'] / self.sim['weight'].sum(), - replace=False, - ) - - def calculate_n_signal(self, time_integrated_flux: float) -> float: - """Docstring""" - return self.sim['weight'].sum() * time_integrated_flux - - def evaluate_background_sindec_pdf(self, events: np.ndarray) -> np.ndarray: - """Calculates the background probability of events based on their dec. - - Args: - events: An array of events including their declination. - - Returns: - The value for the background space pdf for the given events decs. - """ - return (1 / (2 * np.pi)) * self._dec_spline(events['sindec']) - - def build_background_sindec_logenergy_histogram(self, bins: np.ndarray) -> np.ndarray: - """Docstring""" - return np.histogram2d( - self._data['sindec'], - self._data['logE'], - bins=bins, - density=True, - )[0] - - def build_mcbackground_sindec_logenergy_histogram( - self, - bins: np.ndarray, - mcbkgname: str, - ) -> np.ndarray: - """Docstring""" - return np.histogram2d( - self._full_sim['sindec'], - self._full_sim['logE'], - bins=bins, - weights=self._full_sim[mcbkgname], - density=True, - )[0] - - def build_signal_sindec_logenergy_histogram( - self, gamma: float, bins: np.ndarray) -> np.ndarray: - """Docstring""" - return np.histogram2d( - self.full_sim['sindec'], - self.full_sim['logE'], - bins=bins, - weights=self.full_sim['ow'] * self.full_sim['trueE']**gamma, - density=True, - )[0] - - @property - def sim(self) -> np.ndarray: - """Docstring""" - return self._sim - - @property - def full_sim(self) -> np.ndarray: - """Docstring""" - return self._full_sim - - @sim.setter - def sim(self, sim: np.ndarray) -> None: - """Docstring""" - self._full_sim = sim.copy() - - if 'sindec' not in self._full_sim.dtype.names: - self._full_sim = rf.append_fields( - self._full_sim, - 'sindec', - np.sin(self._full_sim['dec']), - usemask=False, - ) - - if 'weight' not in self._full_sim.dtype.names: - self._full_sim = rf.append_fields( - self._full_sim, 'weight', - np.zeros(len(self._full_sim)), - dtypes=np.float32 - ) - - self._full_sim['weight'] = self._full_sim['ow'] * ( - self._full_sim['trueE'] / self.config['normalization_energy (GeV)'] - )**self.config['assumed_gamma'] - - self._cut_sim_dec() - - def _cut_sim_dec(self) -> None: - """Docstring""" - if ( - self.config['dec_bandwidth (rad)'] is not None - ) and ( - self.config['dec_cut_location'] is not None - ): - sindec_dist = np.abs( - self.config['dec_cut_location'] - self._full_sim['trueDec']) - close = sindec_dist < self.config['dec_bandwidth (rad)'] - self._sim = self._full_sim[close].copy() - - self._sim['ow'] /= 2 * np.pi * (np.min([np.sin( - self.config['dec_cut_location'] + self.config['dec_bandwidth (rad)'] - ), 1]) - np.max([np.sin( - self.config['dec_cut_location'] - self.config['dec_bandwidth (rad)'] - ), -1])) - self._sim['weight'] /= 2 * np.pi * (np.min([np.sin( - self.config['dec_cut_location'] + self.config['dec_bandwidth (rad)'] - ), 1]) - np.max([np.sin( - self.config['dec_cut_location'] - self.config['dec_bandwidth (rad)'] - ), -1])) - else: - self._sim = self._full_sim - - @property - def data_grl(self) -> Tuple[np.ndarray, np.ndarray]: - """Docstring""" - return self._data, self._grl - - @data_grl.setter - def data_grl(self, data_grl: Tuple[np.ndarray, np.ndarray]) -> None: - """Docstring""" - self._sin_dec_bins = np.linspace(-1, 1, 1 + self.config['sin_dec_bins']) - self._data = data_grl[0].copy() - self._grl = data_grl[1].copy() - if 'sindec' not in self._data.dtype.names: - self._data = rf.append_fields( - self._data, - 'sindec', - np.sin(self._data['dec']), - usemask=False, - ) - - min_mjd = np.min(self._data['time']) - max_mjd = np.max(self._data['time']) - self._grl = self._grl[ - (self._grl['start'] < max_mjd) & (self._grl['stop'] > min_mjd)] - - self._livetime = self._grl['livetime'].sum() - self._n_background = self._grl['events'].sum() - self._grl_rates = self._grl['events'] / self._grl['livetime'] - - hist, bins = np.histogram( - self._data['sindec'], bins=self._sin_dec_bins, density=True) - bin_centers = bins[:-1] + np.diff(bins) / 2 - - self._dec_spline = Spline(bin_centers, hist, **self.config['dec_spline_kwargs']) - - @property - def livetime(self) -> float: - """Docstring""" - return self._livetime - - @property - def n_background(self) -> float: - """Docstring""" - return self._n_background - - @classmethod - def generate_config(cls) -> dict: - """Docstring""" - config = super().generate_config() - config['normalization_energy (GeV)'] = 100e3 - config['assumed_gamma'] = -2 - config['dec_cut_location'] = None - config['dec_bandwidth (rad)'] = None - config['sin_dec_bins'] = 50 - config['dec_spline_kwargs'] = { - 's': 0, - 'k': 2, - 'ext': 3, - } - return config - - -@dataclass -class TimeDependentNuSourcesDataHandler(NuSourcesDataHandler): - """Docstring""" - background_time_profile: GenericProfile - signal_time_profile: GenericProfile - - _background_time_profile: GenericProfile = field(init=False, repr=False) - _signal_time_profile: GenericProfile = field(init=False, repr=False) - - @property - def background_time_profile(self) -> GenericProfile: - """Docstring""" - return self._background_time_profile - - @background_time_profile.setter - def background_time_profile(self, profile: GenericProfile) -> None: - """Docstring""" - # Find the runs contianed in the background time window - start, stop = profile.range - return_stop_contained = True - - if self.config['outside_time_profile (days)'] is not None: - stop = start - start -= self.config['outside_time_profile (days)'] - return_stop_contained = False - - background_run_mask = self._contained_run_mask( - start, - stop, - return_stop_contained=return_stop_contained, - ) - - if not np.any(background_run_mask): - print('ERROR: No runs found in GRL for calculation of ' - 'background rates!') - raise RuntimeError - - background_grl = self._grl[background_run_mask] - self._n_background = background_grl['events'].sum() - self._n_background /= background_grl['livetime'].sum() - self._n_background *= self._contained_livetime(*profile.range, background_grl) - self._background_time_profile = copy.deepcopy(profile) - - @property - def signal_time_profile(self) -> GenericProfile: - """Docstring""" - return self._signal_time_profile - - @signal_time_profile.setter - def signal_time_profile(self, profile: GenericProfile) -> None: - """Docstring""" - self._signal_time_profile = copy.deepcopy(profile) - - def _contained_run_mask( - self, - start: float, - stop: float, - return_stop_contained: bool = True, - ) -> np.ndarray: - """Docstring""" - fully_contained = ( - self._grl['start'] >= start - ) & (self._grl['stop'] < stop) - - start_contained = ( - self._grl['start'] < start - ) & (self._grl['stop'] > start) - - if not return_stop_contained: - return fully_contained | start_contained - - stop_contained = ( - self._grl['start'] < stop - ) & (self._grl['stop'] > stop) - - return fully_contained | start_contained | stop_contained - - def contained_livetime(self, start: float, stop: float) -> float: - """Docstring""" - contained_runs = self._grl[self._contained_run_mask(start, stop)] - return self._contained_livetime(start, stop, contained_runs) - - @staticmethod - def _contained_livetime( - start: float, - stop: float, - contained_runs: np.ndarray, - ) -> float: - """Docstring""" - runs_before_start = contained_runs[contained_runs['start'] < start] - runs_after_stop = contained_runs[contained_runs['stop'] > stop] - contained_livetime = contained_runs['livetime'].sum() - - if len(runs_before_start) == 1: - contained_livetime -= start - runs_before_start['start'][0] - - if len(runs_after_stop) == 1: - contained_livetime -= runs_after_stop['stop'][0] - stop - - return contained_livetime - - def sample_background(self, n: int, rng: np.random.Generator) -> np.ndarray: - """Docstring""" - events = super().sample_background(n, rng) - return self._randomize_times(events, self._background_time_profile) - - def sample_signal(self, n: int, rng: np.random.Generator) -> np.ndarray: - """Docstring""" - events = super().sample_signal(n, rng) - return self._randomize_times(events, self._signal_time_profile) - - def _randomize_times( - self, - events: np.ndarray, - time_profile: GenericProfile, - ) -> np.ndarray: - grl_start_cdf = time_profile.cdf(self._grl['start']) - grl_stop_cdf = time_profile.cdf(self._grl['stop']) - valid = np.logical_and(grl_start_cdf < 1, grl_stop_cdf > 0) - rates = grl_stop_cdf[valid] - grl_start_cdf[valid] - - runs = np.random.choice( - self._grl[valid], - size=len(events), - replace=True, - p=rates / rates.sum(), - ) - - events['time'] = time_profile.inverse_transform_sample( - runs['start'], runs['stop']) - - return events - - @classmethod - def generate_config(cls) -> dict: - """Docstring""" - config = super().generate_config() - config['outside_time_profile (days)'] = None - return config diff --git a/build/lib/mla/minimizers.py b/build/lib/mla/minimizers.py deleted file mode 100644 index f91f973c..00000000 --- a/build/lib/mla/minimizers.py +++ /dev/null @@ -1,120 +0,0 @@ -""" -Top-level analysis code, and functions that are generic enough to not belong -in any class. -""" - -__author__ = 'John Evans and Jason Fan' -__copyright__ = 'Copyright 2024' -__credits__ = ['John Evans', 'Jason Fan', 'Michael Larson'] -__license__ = 'Apache License 2.0' -__version__ = '1.4.1' -__maintainer__ = 'Jason Fan' -__email__ = 'klfan@terpmail.umd.edu' -__status__ = 'Development' - -from typing import List, Optional, Tuple - -import abc -import dataclasses - -import numpy as np -import scipy.optimize - -from . import configurable -from .test_statistics import LLHTestStatistic - - -@dataclasses.dataclass -class Minimizer(configurable.Configurable): - """Docstring""" - test_statistic: LLHTestStatistic - - __metaclass__ = abc.ABCMeta - - @abc.abstractmethod - def __call__( - self, fitting_params: Optional[List[str]] = None) -> Tuple[float, np.ndarray]: - """Docstring""" - - -@dataclasses.dataclass -class GridSearchMinimizer(Minimizer): - """Docstring""" - def __call__( - self, fitting_params: Optional[List[str]] = None) -> Tuple[float, np.ndarray]: - """Docstring""" - if fitting_params is None: - fitting_key_idx_map = self.test_statistic.params.key_idx_map - fitting_bounds = self.test_statistic.params.bounds - else: - fitting_key_idx_map = { - key: val for key, val in self.test_statistic.params.key_idx_map.items() - if key in fitting_params - } - fitting_bounds = { - key: val for key, val in self.test_statistic.params.bounds.items() - if key in fitting_params - } - - if self.test_statistic.n_kept == 0: - return 0, np.array([(0,)], dtype=[('ns', np.float64)]) - - grid = [ - np.linspace(lo, hi, self.config['gridsearch_points']) - for lo, hi in fitting_bounds.values() - ] - - points = np.array(np.meshgrid(*grid)).T - - grid_ts_values = np.array([ - self._eval_test_statistic(point, fitting_key_idx_map) - for point in points - ]) - - return self._minimize( - points[grid_ts_values.argmin()], fitting_key_idx_map, fitting_bounds) - - def _eval_test_statistic(self, point: np.ndarray, fitting_key_idx_map: dict) -> float: - """Docstring""" - return self.test_statistic(self._param_values(point, fitting_key_idx_map)) - - def _param_values(self, point: np.ndarray, fitting_key_idx_map: dict) -> np.ndarray: - """Docstring""" - param_values = self.test_statistic.params.value_array.copy() - - for i, j in enumerate(fitting_key_idx_map.values()): - param_values[j] = point[i] - - return param_values - - def _minimize( - self, - point: np.ndarray, - fitting_key_idx_map: dict, - fitting_bounds: dict, - ) -> Tuple[float, np.ndarray]: - """Docstring""" - result = scipy.optimize.minimize( - self._eval_test_statistic, - x0=point, - args=(fitting_key_idx_map,), - bounds=fitting_bounds.values(), - method=self.config['scipy_minimize_method'], - ) - - best_ts_value = -result.fun - best_param_values = self._param_values(result.x, fitting_key_idx_map) - - if 'ns' not in fitting_key_idx_map: - idx = self.test_statistic.params.key_idx_map['ns'] - best_param_values[idx] = self.test_statistic.best_ns - - return best_ts_value, best_param_values - - @classmethod - def generate_config(cls) -> dict: - """Docstring""" - config = super().generate_config() - config['gridsearch_points'] = 5 - config['scipy_minimize_method'] = 'L-BFGS-B' - return config diff --git a/build/lib/mla/params.py b/build/lib/mla/params.py deleted file mode 100644 index ad7fadf0..00000000 --- a/build/lib/mla/params.py +++ /dev/null @@ -1,67 +0,0 @@ -"""Docstring""" - -__author__ = 'John Evans and Jason Fan' -__copyright__ = 'Copyright 2024' -__credits__ = ['John Evans', 'Jason Fan', 'Michael Larson'] -__license__ = 'Apache License 2.0' -__version__ = '1.4.1' -__maintainer__ = 'Jason Fan' -__email__ = 'klfan@terpmail.umd.edu' -__status__ = 'Development' - -from typing import List, Optional - -import dataclasses -import numpy as np -import numpy.lib.recfunctions as rf - - -@dataclasses.dataclass -class Params: - """Docstring""" - value_array: np.ndarray - key_idx_map: dict - bounds: dict - - def __contains__(self, item: str) -> bool: - """Docstring""" - return item in self.key_idx_map - - def __getitem__(self, key: str): - """Docstring""" - return self.value_array[self.key_idx_map[key]] - - @property - def names(self) -> List[str]: - """Docstring""" - return [*self.key_idx_map] - - @classmethod - def from_dict(cls, value_dict: dict, bounds: Optional[dict] = None) -> 'Params': - """Docstring""" - value_array = np.array(list(value_dict.values())) - key_idx_map = {key: i for i, key in enumerate(value_dict)} - return cls._build_params(value_array, key_idx_map, bounds) - - @classmethod - def from_array( - cls, - named_value_array: np.ndarray, - bounds: Optional[dict] = None, - ) -> 'Params': - """Docstring""" - value_array = rf.structured_to_unstructured(named_value_array, copy=True)[0] - key_idx_map = {name: i for i, name in enumerate(named_value_array.dtype.names)} - return cls._build_params(value_array, key_idx_map, bounds) - - @classmethod - def _build_params( - cls, - value_array: np.ndarray, - key_idx_map: dict, - bounds: Optional[dict], - ) -> 'Params': - """Docstring""" - if bounds is None: - bounds = {key: (-np.inf, np.inf) for key in key_idx_map} - return cls(value_array, key_idx_map, bounds) diff --git a/build/lib/mla/sob_terms.py b/build/lib/mla/sob_terms.py deleted file mode 100644 index 5a022e2f..00000000 --- a/build/lib/mla/sob_terms.py +++ /dev/null @@ -1,355 +0,0 @@ -"""Docstring""" - -__author__ = 'John Evans and Jason Fan' -__copyright__ = 'Copyright 2024' -__credits__ = ['John Evans', 'Jason Fan', 'Michael Larson'] -__license__ = 'Apache License 2.0' -__version__ = '1.4.1' -__maintainer__ = 'Jason Fan' -__email__ = 'klfan@terpmail.umd.edu' -__status__ = 'Development' - -from typing import List - -import abc -import copy -import dataclasses -import warnings - -import numpy as np -from scipy.interpolate import UnivariateSpline as Spline - -from . import configurable -from .params import Params -from .sources import PointSource -from .data_handlers import DataHandler -from .time_profiles import GenericProfile - - -@dataclasses.dataclass -class SoBTerm: - """Docstring""" - __metaclass__ = abc.ABCMeta - name: str - _params: Params - _sob: np.ndarray - - @property - @abc.abstractmethod - def params(self) -> Params: - """Docstring""" - - @params.setter - @abc.abstractmethod - def params(self, params: Params) -> None: - """Docstring""" - - @property - @abc.abstractmethod - def sob(self) -> np.ndarray: - """Docstring""" - - -@dataclasses.dataclass -class SoBTermFactory(configurable.Configurable): - """Docstring""" - __metaclass__ = abc.ABCMeta - - @abc.abstractmethod - def __call__(self, params: Params, events: np.ndarray) -> SoBTerm: - """Docstring""" - - @abc.abstractmethod - def calculate_drop_mask(self, events: np.ndarray) -> np.ndarray: - """Docstring""" - - @abc.abstractmethod - def generate_params(self) -> tuple: - """Docstring""" - - @classmethod - def generate_config(cls) -> dict: - """Docstring""" - return {'name': cls.__name__.replace('Factory', '')} - - -@dataclasses.dataclass -class SpatialTerm(SoBTerm): - """Docstring""" - - @property - def params(self) -> Params: - """Docstring""" - return self._params - - @params.setter - def params(self, params: Params) -> None: - """Docstring""" - self._params = params - - @property - def sob(self) -> np.ndarray: - """Docstring""" - return self._sob - - -@dataclasses.dataclass -class SpatialTermFactory(SoBTermFactory): - """Docstring""" - data_handler: DataHandler - source: PointSource - - def __call__(self, params: Params, events: np.ndarray) -> SoBTerm: - """Docstring""" - sob_spatial = self.source.spatial_pdf(events) - sob_spatial /= self.data_handler.evaluate_background_sindec_pdf(events) - return SpatialTerm( - name=self.config['name'], - _params=params, - _sob=sob_spatial, - ) - - def calculate_drop_mask(self, events: np.ndarray) -> np.ndarray: - """Docstring""" - return self.source.spatial_pdf(events) != 0 - - def generate_params(self) -> tuple: - return {}, {} - - -@dataclasses.dataclass -class TimeTerm(SoBTerm): - """Docstring""" - _times: np.ndarray - _signal_time_profile: GenericProfile - - @property - def params(self) -> Params: - """Docstring""" - return self._params - - @params.setter - def params(self, params: Params) -> None: - """Docstring""" - self._signal_time_profile.params = params - self._params = params - - @property - def sob(self) -> np.ndarray: - """Docstring""" - return self._sob * self._signal_time_profile.pdf(self._times) - - -@dataclasses.dataclass -class TimeTermFactory(SoBTermFactory): - """Docstring""" - background_time_profile: GenericProfile - signal_time_profile: GenericProfile - - def __call__(self, params: Params, events: np.ndarray) -> SoBTerm: - """Docstring""" - times = np.empty(len(events), dtype=events['time'].dtype) - times[:] = events['time'][:] - signal_time_profile = copy.deepcopy(self.signal_time_profile) - signal_time_profile.params = params - sob_bg = 1 / self.background_time_profile.pdf(times) - - if np.logical_not(np.all(np.isfinite(sob_bg))): - warnings.warn( - 'Warning, events outside background time profile', - RuntimeWarning - ) - - return TimeTerm( - name=self.config['name'], - _params=params, - _sob=sob_bg, - _times=times, - _signal_time_profile=signal_time_profile, - ) - - def calculate_drop_mask(self, events: np.ndarray) -> np.ndarray: - """Docstring""" - return 1 / self.background_time_profile.pdf(events['time']) != 0 - - def generate_params(self) -> tuple: - return self.signal_time_profile.params, self.signal_time_profile.param_bounds - - -@dataclasses.dataclass -class SplineMapEnergyTerm(SoBTerm): - """Docstring""" - gamma: float - _splines: List[Spline] - _event_spline_idxs: np.ndarray - - @property - def params(self) -> Params: - """Docstring""" - return self._params - - @params.setter - def params(self, params: Params) -> None: - """Docstring""" - if 'gamma' in params: - self.gamma = params['gamma'] - self._params = params - - @property - def sob(self) -> np.ndarray: - """Docstring""" - spline_evals = np.exp([spline(self.gamma) for spline in self._splines]) - return spline_evals[self._event_spline_idxs] - - -@dataclasses.dataclass -class SplineMapEnergyTermFactory(SoBTermFactory): - """Docstring""" - data_handler: DataHandler - _sin_dec_bins: np.ndarray = dataclasses.field(init=False, repr=False) - _log_energy_bins: np.ndarray = dataclasses.field(init=False, repr=False) - _gamma_bins: np.ndarray = dataclasses.field(init=False, repr=False) - _spline_map: List[List[Spline]] = dataclasses.field(init=False, repr=False) - - def __post_init__(self) -> None: - """Docstring""" - if self.config['list_sin_dec_bins'] is None: - self._sin_dec_bins = np.linspace( - -1, 1, 1 + self.config['sin_dec_bins'] - ) - else: - self._sin_dec_bins = self.config['list_sin_dec_bins'] - if self.config['list_log_energy_bins'] is None: - self._log_energy_bins = np.linspace( - *self.config['log_energy_bounds'], - 1 + self.config['log_energy_bins'] - ) - else: - self._log_energy_bins = self.config['list_log_energy_bins'] - - self._gamma_bins = np.linspace( - *self.config['gamma_bounds'], 1 + self.config['gamma_bins']) - self._spline_map = self._init_spline_map() - - def __call__(self, params: Params, events: np.ndarray) -> SoBTerm: - """Docstring""" - sin_dec_idx = np.searchsorted(self._sin_dec_bins[:-1], events['sindec']) - log_energy_idx = np.searchsorted(self._log_energy_bins[:-1], events['logE']) - - spline_idxs, event_spline_idxs = np.unique( - [sin_dec_idx - 1, log_energy_idx - 1], - return_inverse=True, - axis=1 - ) - - splines = [self._spline_map[i][j] for i, j in spline_idxs.T] - - return SplineMapEnergyTerm( - name=self.config['name'], - _params=params, - _sob=np.empty(1), - gamma=self.config['initial_gamma'], - _splines=splines, - _event_spline_idxs=event_spline_idxs, - ) - - def calculate_drop_mask(self, events: np.ndarray) -> np.ndarray: - """Docstring""" - return np.ones(len(events), dtype=bool) - - def generate_params(self) -> tuple: - return {'gamma': -2}, {'gamma': (-4, -1)} - - def _init_sob_map( - self, - gamma: float, - bins: np.ndarray, - bin_centers: np.ndarray, - bg_h: np.ndarray, - ) -> np.ndarray: - """Creates sob histogram for a given spectral index (gamma). - - Args: - gamma: The gamma value to use to weight the signal. - - Returns: - An array of signal-over-background values binned in sin(dec) and - log(energy) for a given gamma. - """ - sig_h = self.data_handler.build_signal_sindec_logenergy_histogram(gamma, bins) - - # Normalize histogram by dec band - sig_h /= np.sum(sig_h, axis=1)[:, None] - - # div-0 okay here - with np.errstate(divide='ignore', invalid='ignore'): - ratio = sig_h / bg_h - - for i in range(ratio.shape[0]): - # Pick out the values we want to use. - # We explicitly want to avoid NaNs and infinities - good = np.isfinite(ratio[i]) & (ratio[i] > 0) - good_bins, good_vals = bin_centers[good], ratio[i][good] - - # Do a linear interpolation across the energy range - spline = Spline(good_bins, good_vals, **self.config['energy_spline_kwargs']) - - # And store the interpolated values - ratio[i] = spline(bin_centers) - return ratio - - def _init_spline_map(self) -> List[List[Spline]]: - """Builds a 3D hist of sob vs. sin(dec), log(energy), and gamma, then - returns splines of sob vs. gamma. - - Returns: A Nested spline list of shape (sin_dec_bins, log_energy_bins). - """ - bins = np.array([self._sin_dec_bins, self._log_energy_bins]) - bin_centers = bins[1, :-1] + np.diff(bins[1]) / 2 - bg_h = self.data_handler.build_background_sindec_logenergy_histogram(bins) - - # Normalize histogram by dec band - bg_h /= np.sum(bg_h, axis=1)[:, None] - if self.config['backgroundSOBoption'] == 1: - bg_h[bg_h <= 0] = np.min(bg_h[bg_h > 0]) - elif self.config['backgroundSOBoption'] == 0: - pass - - sob_maps = np.array([ - self._init_sob_map(gamma, bins, bin_centers, bg_h) - for gamma in self._gamma_bins - ]) - - transposed_log_sob_maps = np.log(sob_maps.transpose(1, 2, 0)) - - splines = [[ - Spline(self._gamma_bins, log_ratios, **self.config['sob_spline_kwargs']) - for log_ratios in dec_bin - ] for dec_bin in transposed_log_sob_maps] - - return splines - - @classmethod - def generate_config(cls): - """Docstring""" - config = super().generate_config() - config['initial_gamma'] = -2 - config['sin_dec_bins'] = 50 - config['log_energy_bins'] = 50 - config['log_energy_bounds'] = (1, 8) - config['list_sin_dec_bins'] = None - config['list_log_energy_bins'] = None - config['gamma_bins'] = 50 - config['gamma_bounds'] = (-4.25, -0.5) - config['backgroundSOBoption'] = 0 - config['sob_spline_kwargs'] = { - 'k': 3, - 's': 0, - 'ext': 'raise', - } - config['energy_spline_kwargs'] = { - 'k': 1, - 's': 0, - 'ext': 3, - } - return config diff --git a/build/lib/mla/sources.py b/build/lib/mla/sources.py deleted file mode 100644 index ee937d76..00000000 --- a/build/lib/mla/sources.py +++ /dev/null @@ -1,96 +0,0 @@ -""" -Docstring -""" - -__author__ = 'John Evans and Jason Fan' -__copyright__ = 'Copyright 2024' -__credits__ = ['John Evans', 'Jason Fan', 'Michael Larson'] -__license__ = 'Apache License 2.0' -__version__ = '1.4.1' -__maintainer__ = 'Jason Fan' -__email__ = 'klfan@terpmail.umd.edu' -__status__ = 'Development' - -import dataclasses - -from . import configurable -from . import utility_functions as uf - -import numpy as np - - -@dataclasses.dataclass -class PointSource(configurable.Configurable): - """Stores a source object name and location""" - def sample(self, size: int = 1) -> tuple: - """Sample locations. - - Args: - size: number of points to sample - """ - return (np.ones(size) * self.config['ra'], np.ones(size) * self.config['dec']) - - def spatial_pdf(self, events: np.ndarray) -> np.ndarray: - """calculates the signal probability of events. - - gives a gaussian probability based on their angular distance from the - source object. - - args: - source: - events: an array of events including their positional data. - - returns: - the value for the signal spatial pdf for the given events angular - distances. - """ - sigma2 = events['angErr']**2 + self.sigma**2 - dist = uf.angular_distance(events['ra'], events['dec'], *self.location) - norm = 1 / (2 * np.pi * sigma2) - return norm * np.exp(-dist**2 / (2 * sigma2)) - - @property - def location(self) -> tuple: - """return location of the source""" - return (self.config['ra'], self.config['dec']) - - @property - def sigma(self) -> float: - """return 0 for point source""" - return 0 - - @classmethod - def generate_config(cls) -> dict: - """Docstring""" - config = super().generate_config() - config['name'] = 'source_name' - config['ra'] = np.nan - config['dec'] = np.nan - return config - - -@dataclasses.dataclass -class GaussianExtendedSource(PointSource): - """Gaussian Extended Source""" - def sample(self, size: int = 1) -> np.ndarray: - """Sample locations. - - Args: - size: number of points to sample - """ - mean = self.location - x = np.random.normal(mean[0], self.sigma, size=size) - y = np.random.normal(mean[1], self.sigma, size=size) - return np.array([x, y]) - - @property - def sigma(self) -> float: - """return sigma for GaussianExtendedSource""" - return self.config['sigma'] - - @classmethod - def generate_config(cls) -> dict: - """Docstring""" - config = super().generate_config() - config['sigma'] = np.deg2rad(1) - return config diff --git a/build/lib/mla/test_statistics.py b/build/lib/mla/test_statistics.py deleted file mode 100644 index 174aba92..00000000 --- a/build/lib/mla/test_statistics.py +++ /dev/null @@ -1,206 +0,0 @@ -"""Docstring""" - -__author__ = 'John Evans and Jason Fan' -__copyright__ = 'Copyright 2024' -__credits__ = ['John Evans', 'Jason Fan', 'Michael Larson'] -__license__ = 'Apache License 2.0' -__version__ = '1.4.1' -__maintainer__ = 'Jason Fan' -__email__ = 'klfan@terpmail.umd.edu' -__status__ = 'Development' - -from typing import Dict, List, Optional - -import dataclasses -import numpy as np - -from . import configurable -from .sob_terms import SoBTerm, SoBTermFactory -from .params import Params - - -@dataclasses.dataclass -class LLHTestStatistic(): - """Docstring""" - sob_terms: Dict[str, SoBTerm] - _n_events: int - _n_kept: int - _events: np.ndarray - _params: Params - _newton_precision: float - _newton_iterations: int - _best_ts: float = dataclasses.field(init=False, default=0) - _best_ns: float = dataclasses.field(init=False, default=0) - - def __call__( - self, - param_values: Optional[np.ndarray] = None, - fitting_ns: bool = False, - ) -> float: - """Evaluates the test-statistic for the given events and parameters - - Calculates the test-statistic using a given event model, n_signal, and - gamma. This function does not attempt to fit n_signal or gamma. - - Returns: - The overall test-statistic value for the given events and - parameters. - """ - if param_values is not None: - self.params.value_array = param_values - self._update_term_params() - - if self._n_events == 0: - return 0 - - sob = self._calculate_sob() - - if fitting_ns: - ns_ratio = self._params['ns'] / self._n_events - else: - ns_ratio = self._newton_ns_ratio(sob) - - llh = np.sign(ns_ratio) * np.log(np.abs(ns_ratio) * (sob - 1) + 1) - drop_term = np.sign(ns_ratio) * np.log(1 - np.abs(ns_ratio)) - ts = -2 * (llh.sum() + self.n_dropped * drop_term) - - if ts < self._best_ts: - self._best_ts = ts - self._best_ns = ns_ratio * self._n_events - - return ts - - def _calculate_sob(self) -> np.ndarray: - """Docstring""" - sob = np.ones(self.n_kept) - for _, term in self.sob_terms.items(): - sob *= term.sob.reshape((-1,)) - return sob - - def _newton_ns_ratio(self, sob: np.ndarray) -> float: - """Docstring - - Args: - sob: - - Returns: - - """ - precision = self._newton_precision + 1 - eps = 1e-5 - k = 1 / (sob - 1) - x = [0.] * self._newton_iterations - - for i in range(self._newton_iterations - 1): - # get next iteration and clamp - inv_terms = x[i] + k - inv_terms[inv_terms == 0] = eps - terms = 1 / inv_terms - drop_term = 1 / (x[i] - 1) - d1 = np.sum(terms) + self.n_dropped * drop_term - d2 = np.sum(terms**2) + self.n_dropped * drop_term**2 - x[i + 1] = min(1 - eps, max(0, x[i] + d1 / d2)) - - if x[i] == x[i + 1] or ( - x[i] < x[i + 1] and x[i + 1] <= x[i] * precision - ) or (x[i + 1] < x[i] and x[i] <= x[i + 1] * precision): - break - return x[i + 1] - - @property - def params(self) -> Params: - """Docstring""" - return self._params - - @params.setter - def params(self, params: Params) -> None: - """Docstring""" - if params == self._params: - return - if 'ns' in params: - params.bounds['ns'] = (0, min(params.bounds['ns'], self.n_kept)) - self._params = params - self._update_term_params() - self._best_ns = self._best_ts = 0 - - def _update_term_params(self) -> None: - """Docstring""" - for _, term in self.sob_terms.items(): - term.params = self.params - - @property - def best_ns(self) -> float: - """Docstring""" - return self._best_ns - - @property - def best_ts(self) -> float: - """Docstring""" - return self._best_ts - - @property - def n_events(self) -> int: - """Docstring""" - return self._n_events - - @property - def n_kept(self) -> int: - """Docstring""" - return self._n_kept - - @property - def n_dropped(self) -> int: - """Docstring""" - return self._n_events - self._n_kept - - -@dataclasses.dataclass -class LLHTestStatisticFactory(configurable.Configurable): - """Docstring""" - sob_term_factories: List[SoBTermFactory] - - def __call__(self, params: Params, events: np.ndarray) -> LLHTestStatistic: - """Docstring""" - drop_mask = np.logical_and.reduce(np.array([ - term_factory.calculate_drop_mask(events) - for term_factory in self.sob_term_factories - ])) - - n_kept = drop_mask.sum() - pruned_events = np.empty(n_kept, dtype=events.dtype) - pruned_events[:] = events[drop_mask] - - sob_terms = { - term_factory.config['name']: term_factory(params, pruned_events) - for term_factory in self.sob_term_factories - } - - return LLHTestStatistic( - sob_terms=sob_terms, - _n_events=len(events), - _n_kept=n_kept, - _events=pruned_events, - _params=params, - _newton_precision=self.config['newton_precision'], - _newton_iterations=self.config['newton_iterations'], - ) - - def generate_params(self) -> Params: - """Docstring""" - param_values = {'ns': 0} - param_bounds = {'ns': (0, np.inf)} - - for term in self.sob_term_factories: - vals, bounds = term.generate_params() - param_values = dict(param_values, **vals) - param_bounds = dict(param_bounds, **bounds) - - return Params.from_dict(param_values, param_bounds) - - @classmethod - def generate_config(cls) -> dict: - """Docstring""" - config = super().generate_config() - config['newton_precision'] = 0 - config['newton_iterations'] = 20 - return config diff --git a/build/lib/mla/threeml/IceCubeLike.py b/build/lib/mla/threeml/IceCubeLike.py deleted file mode 100644 index a6862502..00000000 --- a/build/lib/mla/threeml/IceCubeLike.py +++ /dev/null @@ -1,978 +0,0 @@ -"""Docstring""" - -__author__ = 'John Evans and Jason Fan' -__copyright__ = 'Copyright 2024' -__credits__ = ['John Evans', 'Jason Fan', 'Michael Larson'] -__license__ = 'Apache License 2.0' -__version__ = '1.4.1' -__maintainer__ = 'Jason Fan' -__email__ = 'klfan@terpmail.umd.edu' -__status__ = 'Development' - -from __future__ import print_function -from __future__ import division -from past.utils import old_div -import collections -import scipy -import numpy as np -import numpy.lib.recfunctions as rf -from astromodels import Gaussian_on_sphere -from astromodels.core.sky_direction import SkyDirection -from astromodels.core.spectral_component import SpectralComponent -from astromodels.core.tree import Node -from astromodels.core.units import get_units -from astromodels.sources.source import Source, SourceType -from astromodels.utils.pretty_list import dict_to_list -from astromodels.core.memoization import use_astromodels_memoization -from astromodels import PointSource, ExtendedSource -import astropy.units as u -from threeML.plugin_prototype import PluginPrototype -from mla.threeml import data_handlers -from mla.threeml import sob_terms -from mla import sob_terms as sob_terms_base -from mla import test_statistics -from mla.params import Params -from mla import analysis -from mla import sources -from mla import minimizers -from mla import trial_generators -from mla.utility_functions import newton_method, newton_method_multidataset - -__all__ = ["NeutrinoPointSource"] -r"""This IceCube plugin is currently under develop by Kwok Lung Fan""" - - -class NeutrinoPointSource(PointSource,Node): - """ - Class for NeutrinoPointSource. It is inherited from astromodels PointSource class. - """ - - def __init__( - self, - source_name, - ra=None, - dec=None, - spectral_shape=None, - l=None, - b=None, - components=None, - sky_position=None, - energy_unit=u.GeV, - ): - """Constructor for NeutrinoPointSource - - More info ... - - Args: - source_name:Name of the source - ra: right ascension in degree - dec: declination in degree - spectral_shape: Shape of the spectrum.Check 3ML example for more detail. - l: galactic longitude in degree - b: galactic in degree - components: Spectral Component.Check 3ML example for more detail. - sky_position: sky position - energy_unit: Unit of the energy - """ - # Check that we have all the required information - - # (the '^' operator acts as XOR on booleans) - - # Check that we have one and only one specification of the position - - assert ( - (ra is not None and dec is not None) - ^ (l is not None and b is not None) - ^ (sky_position is not None) - ), "You have to provide one and only one specification for the position" - - # Gather the position - - if not isinstance(sky_position, SkyDirection): - - if (ra is not None) and (dec is not None): - - # Check that ra and dec are actually numbers - - try: - - ra = float(ra) - dec = float(dec) - - except (TypeError, ValueError): - - raise AssertionError( - "RA and Dec must be numbers. If you are confused by this message," - " you are likely using the constructor in the wrong way. Check" - " the documentation." - ) - - sky_position = SkyDirection(ra=ra, dec=dec) - - else: - - sky_position = SkyDirection(l=l, b=b) - - self._sky_position = sky_position - - # Now gather the component(s) - - # We need either a single component, or a list of components, but not both - # (that's the ^ symbol) - - assert (spectral_shape is not None) ^ (components is not None), ( - "You have to provide either a single " - "component, or a list of components " - "(but not both)." - ) - - if spectral_shape is not None: - - components = [SpectralComponent("main", spectral_shape)] - - Source.__init__(self, components, src_type=SourceType.POINT_SOURCE) - - # A source is also a Node in the tree - - Node.__init__(self, source_name) - - # Add the position as a child node, with an explicit name - - self._add_child(self._sky_position) - - # Add a node called 'spectrum' - - spectrum_node = Node("spectrum") - spectrum_node._add_children(list(self._components.values())) - - self._add_child(spectrum_node) - - # Now set the units - # Now sets the units of the parameters for the energy domain - - current_units = get_units() - - # Components in this case have energy as x and differential flux as y - - x_unit = energy_unit - y_unit = (energy_unit * current_units.area * current_units.time) ** (-1) - - # Now set the units of the components - for component in list(self._components.values()): - - component.shape.set_units(x_unit, y_unit) - - def __call__(self, x, tag=None): - """ - Overwrite the function so it always return 0. - It is because it should not produce any EM signal. - """ - if isinstance(x, u.Quantity): - if isinstance(x, (float, int)): - return 0 * (u.keV ** -1 * u.cm ** -2 * u.second ** -1) - return np.zeros((len(x))) * ( - u.keV ** -1 * u.cm ** -2 * u.second ** -1 - ) # It is zero so the unit doesn't matter - else: - if isinstance(x, (float, int)): - return 0 - return np.zeros((len(x))) - - def call(self, x, tag=None): - """ - Calling the spectrum - - Args: - x: Energy - - return - differential flux - """ - if tag is None: - - # No integration nor time-varying or whatever-varying - - if isinstance(x, u.Quantity): - - # Slow version with units - - results = [ - component.shape(x) for component in list(self.components.values()) - ] - - # We need to sum like this (slower) because using - # np.sum will not preserve the units (thanks astropy.units) - - return sum(results) - - else: - - # Fast version without units, where x is supposed to be in the same - # units as currently defined in units.get_units() - - results = [ - component.shape(x) for component in list(self.components.values()) - ] - - return np.sum(results, 0) - - else: - - # Time-varying or energy-varying or whatever-varying - - integration_variable, a, b = tag - - if b is None: - - # Evaluate in a, do not integrate - - with use_astromodels_memoization(False): - - integration_variable.value = a - - res = self.__call__(x, tag=None) - - return res - - else: - - # Integrate between a and b - - integrals = np.zeros(len(x)) - - # TODO: implement an integration scheme avoiding the for loop - - with use_astromodels_memoization(False): - - reentrant_call = self.__call__ - - for i, e in enumerate(x): - - def integral(y): - - integration_variable.value = y - - return reentrant_call(e, tag=None) - - # Now integrate - integrals[i] = scipy.integrate.quad( - integral, a, b, epsrel=1e-5 - )[0] - - return old_div(integrals, (b - a)) - - -class NeutrinoExtendedSource(ExtendedSource): - def __init__( - self, source_name, spatial_shape, spectral_shape=None, components=None - ): - - # Check that we have all the required information - # and set the units - - current_u = get_units() - - if isinstance(spatial_shape, Gaussian_on_sphere): - - # Now gather the component(s) - - # We need either a single component, or a list of components, but not both - # (that's the ^ symbol) - - assert (spectral_shape is not None) ^ (components is not None), ( - "You have to provide either a single " - "component, or a list of components " - "(but not both)." - ) - - if spectral_shape is not None: - - components = [SpectralComponent("main", spectral_shape)] - - # Components in this case have energy as x and differential flux as y - - diff_flux_units = (current_u.energy * current_u.area * current_u.time) ** ( - -1 - ) - - # Now set the units of the components - for component in components: - - component.shape.set_units(current_u.energy, diff_flux_units) - - # Set the units of the brightness - spatial_shape.set_units( - current_u.angle, current_u.angle, current_u.angle ** (-2) - ) - - else: - - print("Only support Gaussian_on_sphere") - - raise RuntimeError() - - # Here we have a list of components - - Source.__init__(self, components, SourceType.EXTENDED_SOURCE) - - # A source is also a Node in the tree - - Node.__init__(self, source_name) - - # Add the spatial shape as a child node, with an explicit name - self._spatial_shape = spatial_shape - self._add_child(self._spatial_shape) - - # Add the same node also with the name of the function - # self._add_child(self._shape, self._shape.__name__) - - # Add a node called 'spectrum' - - spectrum_node = Node("spectrum") - spectrum_node._add_children(list(self._components.values())) - - self._add_child(spectrum_node) - - @property - def spatial_shape(self): - """ - A generic name for the spatial shape. - :return: the spatial shape instance - """ - - return self._spatial_shape - - def get_spatially_integrated_flux(self, energies): - - """ - Returns total flux of source at the given energy - :param energies: energies (array or float) - :return: differential flux at given energy - """ - - if not isinstance(energies, np.ndarray): - energies = np.array(energies, ndmin=1) - - # Get the differential flux from the spectral components - - results = [ - self.spatial_shape.get_total_spatial_integral(energies) - * component.shape(energies) - for component in self.components.values() - ] - - if isinstance(energies, u.Quantity): - - # Slow version with units - - # We need to sum like this (slower) because using - # np.sum will not preserve the units (thanks astropy.units) - - differential_flux = sum(results) - - else: - - # Fast version without units, where x is supposed to be in the - # same units as currently defined in units.get_units() - - differential_flux = np.sum(results, 0) - - return differential_flux - - def __call__(self, lon, lat, energies): - """ - Returns brightness of source at the given position and energy - :param lon: longitude (array or float) - :param lat: latitude (array or float) - :param energies: energies (array or float) - :return: differential flux at given position and energy - """ - - lat = np.array(lat, ndmin=1) - lon = np.array(lon, ndmin=1) - energies = np.array(energies, ndmin=1) - if isinstance(self.spatial_shape, Gaussian_on_sphere): - if isinstance(energies, u.Quantity): - - # Slow version with units - - # We need to sum like this (slower) because - # using np.sum will not preserve the units (thanks astropy.units) - - result = np.zeros((lat.shape[0], energies.shape[0])) * ( - u.keV ** -1 * u.cm ** -2 * u.second ** -1 * u.degree ** -2 - ) - - else: - - # Fast version without units, where x is supposed to be in the - # same units as currently defined in units.get_units() - - result = np.zeros((lat.shape[0], energies.shape[0])) - - return np.squeeze(result) - - def call(self, energies): - """Returns total flux of source at the given energy""" - return self.get_spatially_integrated_flux(energies) - - @property - def has_free_parameters(self): - """ - Returns True or False whether there is any parameter in this source - :return: - """ - - for component in list(self._components.values()): - - for par in list(component.shape.parameters.values()): - - if par.free: - - return True - - for par in list(self.spatial_shape.parameters.values()): - - if par.free: - - return True - - return False - - @property - def free_parameters(self): - """ - Returns a dictionary of free parameters for this source - We use the parameter path as the key because it's - guaranteed to be unique, unlike the parameter name. - :return: - """ - free_parameters = collections.OrderedDict() - - for component in list(self._components.values()): - - for par in list(component.shape.parameters.values()): - - if par.free: - - free_parameters[par.path] = par - - for par in list(self.spatial_shape.parameters.values()): - - if par.free: - - free_parameters[par.path] = par - - return free_parameters - - @property - def parameters(self): - """ - Returns a dictionary of all parameters for this source. - We use the parameter path as the key because it's - guaranteed to be unique, unlike the parameter name. - :return: - """ - all_parameters = collections.OrderedDict() - - for component in list(self._components.values()): - - for par in list(component.shape.parameters.values()): - - all_parameters[par.path] = par - - for par in list(self.spatial_shape.parameters.values()): - - all_parameters[par.path] = par - - return all_parameters - - def _repr__base(self, rich_output=False): - """ - Representation of the object - :param rich_output: if True, generates HTML, otherwise text - :return: the representation - """ - - # Make a dictionary which will then be transformed in a list - - repr_dict = collections.OrderedDict() - - key = "%s (extended source)" % self.name - - repr_dict[key] = collections.OrderedDict() - repr_dict[key]["shape"] = self._spatial_shape.to_dict(minimal=True) - repr_dict[key]["spectrum"] = collections.OrderedDict() - - for component_name, component in list(self.components.items()): - repr_dict[key]["spectrum"][component_name] = component.to_dict(minimal=True) - - return dict_to_list(repr_dict, rich_output) - - def get_boundaries(self): - """ - Returns the boundaries for this extended source - :return: a tuple of tuples ((min. lon, max. lon), (min lat, max lat)) - """ - return self._spatial_shape.get_boundaries() - - -class Spectrum(object): - r""" - A class that converter a astromodels model - instance to a spectrum object with __call__ method. - """ - - def __init__(self, likelihood_model_instance, A=1): - r"""Constructor of the class""" - self.model = likelihood_model_instance - self.norm = A - for source_name, source in likelihood_model_instance.point_sources.items(): - if isinstance(source, NeutrinoPointSource): - self.neutrinosource = source_name - self.point = True - for source_name, source in likelihood_model_instance.extended_sources.items(): - if isinstance(source, NeutrinoExtendedSource): - self.neutrinosource = source_name - self.point = False - - def __call__(self, energy, **kwargs): - r"""Evaluate spectrum at E""" - if self.point: - return ( - self.model.point_sources[self.neutrinosource].call(energy) * self.norm - ) - else: - return ( - self.model.extended_sources[self.neutrinosource].call(energy) - * self.norm - ) - - def validate(self): - pass - - def __str__(self): - r"""String representation of class""" - return "SpectrumConverter class doesn't support string representation now" - - def copy(self): - r"""Return copy of this class""" - c = type(self).__new__(type(self)) - c.__dict__.update(self.__dict__) - return c - - -class IceCubeLike(PluginPrototype): - def __init__( - self, - name: str, - data: np.ndarray, - data_handlers: data_handlers.ThreeMLDataHandler, - llh: test_statistics.LLHTestStatisticFactory, - source: sources.PointSource = None, - livetime: float = None, - fix_flux_norm: bool = False, - verbose: bool = False, - **kwargs - ): - r"""Constructor of the class. - Args: - name: name for the plugin - data: data of experiment - data_handlers: mla.threeml.data_handlers ThreeMLDataHandler object - llh: test_statistics.LLHTestStatistic object. Used to evaluate the ts - source: injection location(only when need injection) - livetime: livetime in days(calculated using livetime within time profile if None) - fix_flux_norm: only fit the spectrum shape - verbose: print the output or not - - """ - nuisance_parameters = {} - super(IceCubeLike, self).__init__(name, nuisance_parameters) - self.parameter = kwargs - self.fix_flux_norm = fix_flux_norm - self.fit_ns = 0 - self.fit_likelihood = 0 - if livetime is None: - for term in llh.sob_term_factories: - if isinstance(term, sob_terms_base.TimeTermFactory): - self.livetime = ( - data_handlers.contained_livetime( - term.signal_time_profile.range[0], - term.signal_time_profile.range[1], - ) - * 3600 - * 24 - ) - else: - self.livetime = livetime - if source is None: - config = sources.PointSource.generate_config() - config["ra"] = 0 - config["dec"] = 0 - source = sources.PointSource(config=config) - self.injected_source = source - trial_config = trial_generators.SingleSourceTrialGenerator.generate_config() - self.trial_generator = trial_generators.SingleSourceTrialGenerator( - trial_config, data_handlers, source - ) - analysis_config = analysis.SingleSourceLLHAnalysis.generate_default_config( - minimizer_class=minimizers.GridSearchMinimizer - ) - self.analysis = analysis.SingleSourceLLHAnalysis( - config=analysis_config, - minimizer_class=minimizers.GridSearchMinimizer, - sob_term_factories=llh.sob_term_factories, - data_handler_source=(data_handlers, source), - ) - - self.sob_term_factories = llh.sob_term_factories - for term in llh.sob_term_factories: - if isinstance(term, sob_terms_base.SpatialTermFactory): - self.spatial_sob_factory = term - if isinstance(term, sob_terms.ThreeMLBaseEnergyTermFactory): - self.energy_sob_factory = term - self.verbose = verbose - self._data = data - self._ra = np.rad2deg(source.config["ra"]) - self._dec = np.rad2deg(source.config["dec"]) - self._sigma = np.rad2deg(source.sigma) - self.test_statistic = self.analysis.test_statistic_factory( - Params.from_dict({"ns": 0}), data - ) - for key in self.test_statistic.sob_terms.keys(): - if isinstance( - self.test_statistic.sob_terms[key], sob_terms.ThreeMLPSEnergyTerm - ): - self.energyname = key - return - - def set_model(self, likelihood_model_instance): - r"""Setting up the model""" - if likelihood_model_instance is None: - - return - - for source_name, source in likelihood_model_instance.point_sources.items(): - if isinstance(source, NeutrinoPointSource): - self.source_name = source_name - ra = source.position.get_ra() - dec = source.position.get_dec() - if self._ra == ra and self._dec == dec: - self.llh_model = likelihood_model_instance - self.energy_sob_factory.spectrum = Spectrum( - likelihood_model_instance - ) - self.test_statistic = self.analysis.test_statistic_factory( - Params.from_dict({"ns": 0}), self._data - ) - else: - self._ra = ra - self._dec = dec - config = sources.PointSource.generate_config() - config["ra"] = np.deg2rad(ra) - config["dec"] = np.deg2rad(dec) - mlasource = sources.PointSource(config=config) - self.analysis.data_handler_source = ( - self.analysis.data_handler_source[0], - mlasource, - ) - self.spatial_sob_factory.source = mlasource - self.llh_model = likelihood_model_instance - self.energy_sob_factory.source = mlasource - self.energy_sob_factory.spectrum = Spectrum( - likelihood_model_instance - ) - self.test_statistic = self.analysis.test_statistic_factory( - Params.from_dict({"ns": 0}), self._data - ) - for source_name, source in likelihood_model_instance.extended_sources.items(): - if isinstance(source, NeutrinoExtendedSource): - self.source_name = source_name - ra = source.spatial_shape.lon0.value - dec = source.spatial_shape.lat0.value - sigma = source.spatial_shape.sigma.value - if self._ra == ra and self._dec == dec and self._sigma == sigma: - self.llh_model = likelihood_model_instance - self.energy_sob_factory.spectrum = Spectrum( - likelihood_model_instance - ) - self.test_statistic = self.analysis.test_statistic_factory( - Params.from_dict({"ns": 0}), self._data - ) - else: - self._ra = ra - self._dec = dec - self._sigma = sigma - config = sources.GaussianExtendedSource.generate_config() - config["ra"] = np.deg2rad(ra) - config["dec"] = np.deg2rad(dec) - config["sigma"] = np.deg2rad(sigma) - mlasource = sources.GaussianExtendedSource(config=config) - self.analysis.data_handler_source = ( - self.analysis.data_handler_source[0], - mlasource, - ) - self.spatial_sob_factory.source = mlasource - self.llh_model = likelihood_model_instance - self.energy_sob_factory.source = mlasource - self.energy_sob_factory.spectrum = Spectrum( - likelihood_model_instance - ) - self.test_statistic = self.analysis.test_statistic_factory( - Params.from_dict({"ns": 0}), self._data - ) - - if self.source_name is None: - print("No point sources in the model") - return - - def inject_background_and_signal(self, **kwargs) -> None: - """docstring""" - self._data = self.trial_generator(**kwargs) - return - - def update_data(self, data) -> None: - """docstring""" - self._data = data - return - - def update_injection(self, source: sources.PointSource): - """docstring""" - self.trial_generator.source = source - return - - def update_model(self): - """docstring""" - spectrum = Spectrum(self.llh_model) - self.energy_sob_factory.spectrum = spectrum - self.test_statistic.sob_terms[self.energyname].update_sob_hist( - self.energy_sob_factory - ) - return - - def get_ns(self): - """docstring""" - ns = self.energy_sob_factory.get_ns() * self.livetime * 3600 * 24 - return ns - - def get_log_like(self, verbose=None): - """docstring""" - if verbose is None: - verbose = self.verbose - self.update_model() - if self.fix_flux_norm: - llh = self.test_statistic() # doesn't matter here - if verbose: - ns = self.test_statistic.best_ns - print(ns, llh) - else: - ns = self.get_ns() - if ns > self.test_statistic.n_events: - if verbose: - print(ns, 0) - return 0 - llh = self.test_statistic([ns], fitting_ns=True) - self.fit_ns = ns - self.fit_likelihood = llh - if verbose: - print(ns, llh / 2) - - return -llh / 2 - - def get_number_of_data_points(self): - """docstring""" - return self.test_statistic.n_events - - def get_current_fit_ns(self): - """docstring""" - return self.fit_ns - - def inner_fit(self): - return self.get_log_like() - - @property - def data(self) -> np.ndarray: - """Getter for data.""" - return self._data - - @property - def ra(self) -> float: - """Getter for ra.""" - return self._ra - - @property - def dec(self) -> float: - """Getter for ra.""" - return self._dec - - -class icecube_analysis(PluginPrototype): - """Docstring""" - - def __init__( - self, listoficecubelike, newton_flux_norm=False, name="combine", verbose=False - ): - """Docstring""" - nuisance_parameters = {} - super(icecube_analysis, self).__init__(name, nuisance_parameters) - self.listoficecubelike = listoficecubelike - self.livetime_ratio = [] # livetime ratio between sample - self.effA_ratio = [] - self.totallivetime = [] - self._p = [] - self.mc_index = [] - self.dataset_ratio = [] - self.dataset_weight = [] - self.totaln = 0 - for icecube in listoficecubelike: - self.totaln += len(icecube.data) - self.init_mc_array() - self.newton_flux_norm = newton_flux_norm - self.verbose = verbose - self.current_fit_ns = 0 - - def get_log_like(self, verbose=None): - if self.newton_flux_norm: - sob = [] - n_drop = [] - fraction = [] - self.totaln = 0 - dataset_weight = [] - for icecubeobject in self.listoficecubelike: - self.totaln += len(icecubeobject.data) - icecubeobject.update_model() - dataset_weight.append(icecubeobject.get_ns()) - dataset_weight = np.array(dataset_weight) - self.dataset_weight = dataset_weight / dataset_weight.sum() - - for i, icecubeobject in enumerate(self.listoficecubelike): - sob.append(icecubeobject.test_statistic._calculate_sob()) - n_drop.append(icecubeobject.test_statistic.n_dropped) - fraction.append( - self.totaln * self.dataset_weight[i] / len(icecubeobject.data) - ) - fraction = np.array(fraction) - # fraction = fraction/fraction.sum() - ns_ratio = newton_method_multidataset(sob, n_drop, fraction) - llh = 0 - for i, icecubeobject in enumerate(self.listoficecubelike): - templlh = np.sign(ns_ratio) * np.log( - np.abs(ns_ratio) * fraction[i] * (sob[i] - 1) + 1 - ) - drop_term = np.sign(ns_ratio) * np.log( - 1 - np.abs(ns_ratio) * fraction[i] - ) - llh += templlh.sum() + n_drop[i] * drop_term - self.current_fit_ns = ns_ratio * self.totaln - if self.verbose: - print(self.current_fit_ns, llh) - - else: - llh = 0 - ns = 0 - for icecubeobject in self.listoficecubelike: - icecubeobject.update_model() - llh += icecubeobject.get_log_like() - ns += icecubeobject.get_current_fit_ns() - self.current_fit_ns = ns - if self.verbose: - print(self.current_fit_ns, llh) - return llh - - def get_current_fit_ns(self): - return self.current_fit_ns - - def set_model(self, likelihood_model_instance): - for icecubeobject in self.listoficecubelike: - icecubeobject.set_model(likelihood_model_instance) - return - - def inner_fit(self): - return self.get_log_like() - - def init_mc_array(self): - """Docstring""" - for i, sample in enumerate(self.listoficecubelike): - self.livetime_ratio.append(sample.livetime) - self.effA_ratio.append( - sample.analysis.data_handler_source[0].sim["weight"].sum() - ) - self.livetime_ratio = np.array(self.livetime_ratio) - self.totallivetime = self.livetime_ratio.sum() - self.dataset_ratio = self.livetime_ratio * self.effA_ratio - self.dataset_ratio = self.dataset_ratio / self.dataset_ratio.sum() - self.livetime_ratio /= np.sum(self.livetime_ratio) - self.effA_ratio /= np.sum(self.effA_ratio) - - for i, sample in enumerate(self.listoficecubelike): - sim = sample.analysis.data_handler_source[0].sim - mc_array = rf.append_fields( - np.empty(len(sim["weight"])), - "p", - sim["weight"] / sim["weight"].sum() * self.livetime_ratio[i], - usemask=False, - ) - mc_array = rf.append_fields( - mc_array, - "index", - np.arange(len(mc_array)), - usemask=False, - ) - mc_array = rf.append_fields( - mc_array, - "sample", - np.ones(len(sim["weight"])) * i, - usemask=False, - ) - self.mc_index.append(mc_array) - - self.mc_index = np.array(self.mc_index, dtype=object) - - def injection(self, n_signal=0, flux_norm=None, poisson=False): - """docstring""" - self.totaln = 0 - if flux_norm is not None: - for i, icecubeobject in enumerate(self.listoficecubelike): - time_intergrated = flux_norm * icecubeobject.livetime - icecubeobject.trial_generator.config["fixed_ns"] = False - tempdata = icecubeobject.trial_generator(time_intergrated) - self.listoficecubelike[i].update_data(tempdata) - else: - if poisson: - ratio_injection = self.dataset_ratio * n_signal - for i, icecubeobject in enumerate(self.listoficecubelike): - icecubeobject.trial_generator.config["fixed_ns"] = True - injection_signal = np.random.poisson(ratio_injection[i]) - tempdata = icecubeobject.trial_generator(injection_signal) - self.listoficecubelike[i].update_data(tempdata) - else: - print("No fix number injection implemented") - self.totaln = 0 - for icecube in self.listoficecubelike: - self.totaln += len(icecube.data) - - def cal_injection_ns(self, flux_norm): - """Docstring""" - ns = 0 - for icecubeobject in self.listoficecubelike: - time_intergrated = flux_norm * icecubeobject.livetime * 3600 * 24 - tempns = ( - time_intergrated - * icecubeobject.analysis.data_handler_source[0].sim["weight"].sum() - ) - ns = ns + tempns - return ns - - def cal_injection_fluxnorm(self, ns): - """Docstring""" - totalweight = 0 - for icecubeobject in self.listoficecubelike: - tempweight = ( - icecubeobject.analysis.data_handler_source[0].sim["weight"].sum() - * icecubeobject.livetime - * 3600 - * 24 - ) - totalweight = totalweight + tempweight - return ns / totalweight diff --git a/build/lib/mla/threeml/__init__.py b/build/lib/mla/threeml/__init__.py deleted file mode 100644 index 9f44addd..00000000 --- a/build/lib/mla/threeml/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -"""Docstring""" -# flake8: noqa -from . import spectral -from . import data_handlers -from . import sob_terms -from . import IceCubeLike diff --git a/build/lib/mla/threeml/data_handlers.py b/build/lib/mla/threeml/data_handlers.py deleted file mode 100644 index 8d65bd1c..00000000 --- a/build/lib/mla/threeml/data_handlers.py +++ /dev/null @@ -1,179 +0,0 @@ -"""Docstring""" - -__author__ = 'John Evans and Jason Fan' -__copyright__ = 'Copyright 2024' -__credits__ = ['John Evans', 'Jason Fan', 'Michael Larson'] -__license__ = 'Apache License 2.0' -__version__ = '1.4.1' -__maintainer__ = 'Jason Fan' -__email__ = 'klfan@terpmail.umd.edu' -__status__ = 'Development' - -import dataclasses - -import numpy as np -import numpy.lib.recfunctions as rf - -from .. import data_handlers -from . import spectral - - -@dataclasses.dataclass -class ThreeMLDataHandler(data_handlers.NuSourcesDataHandler): - """ - Inheritance class from NuSourcesDataHandler. - For time independent 3ML analysis. - - Additional init argument: - injection_spectrum: spectral.BaseSpectrum(in keV by default) - """ - - injection_spectrum: spectral.BaseSpectrum - _injection_spectrum: spectral.BaseSpectrum = dataclasses.field( - init=False, repr=False, default=spectral.PowerLaw(1e9, 1e-22, -2) - ) - _flux_unit_conversion: float = dataclasses.field( - init=False, repr=False, default=1e6 - ) - _reduced_reco_sim: np.ndarray = dataclasses.field(init=False, repr=False) - - def __post_init__(self) -> None: - """Docstring""" - self._reduced_reco_sim = self.cut_reconstructed_sim( - self.config["dec_cut_location"], self.config["reco_sampling_width"] - ) - self._flux_unit_conversion = self.config["flux_unit_conversion"] - - def build_signal_energy_histogram( - self, spectrum: spectral.BaseSpectrum, bins: np.ndarray, scale: float - ) -> np.ndarray: - """ - Building the signal energy histogram. - Only used when using MC instead of IRF to build signal energy histogram. - - Args: - spectrum: signal spectrum - bins: 2d bins in sindec and logE - """ - return np.histogram2d( - self.reduced_reco_sim["sindec"], - self.reduced_reco_sim["logE"], - bins=bins, - weights=self.reduced_reco_sim["ow"] - * spectrum(self.reduced_reco_sim["trueE"] * scale), - density=True, - )[0] - - def cut_reconstructed_sim(self, dec: float, sampling_width: float) -> np.ndarray: - """ - Cutting the MC based on reconstructed dec. - Only use when using MC instead of IRF to build signal energy histogram. - - Args: - dec: declination of the source - sampling_width: size of the sampling band in reconstruction dec. - """ - dec_dist = np.abs(dec - self._full_sim["dec"]) - close = dec_dist < sampling_width - return self._full_sim[close].copy() - - @property - def reduced_reco_sim(self) -> np.ndarray: - """ - Return the reduced sim based on reconstructed dec. - This is the return output of cut_reconstructed_sim. - """ - return self._reduced_reco_sim - - @reduced_reco_sim.setter - def reduced_reco_sim(self, reduced_reco_sim: np.ndarray) -> None: - """ - setting the reduced sim based on reconstructed dec directly. - - Args: - reduced_reco_sim: reduced sim based on reconstructed dec - """ - self._reduced_reco_sim = reduced_reco_sim.copy() - - @property - def injection_spectrum(self) -> spectral.BaseSpectrum: - """ - Getting the injection spectrum - """ - return self._injection_spectrum - - @injection_spectrum.setter - def injection_spectrum(self, inject_spectrum: spectral.BaseSpectrum) -> None: - """ - Setting the injection spectrum - - Args: - inject_spectrum: spectrum used for injection - """ - if isinstance(inject_spectrum, property): - # initial value not specified, use default - inject_spectrum = ThreeMLDataHandler._injection_spectrum - self._injection_spectrum = inject_spectrum - if "weight" not in self._full_sim.dtype.names: - self._full_sim = rf.append_fields( - self._full_sim, - "weight", - np.zeros(len(self._full_sim)), - dtypes=np.float32, - ) - - self._full_sim["weight"] = ( - self._full_sim["ow"] - * (inject_spectrum(self._full_sim["trueE"] * self._flux_unit_conversion)) - * self._flux_unit_conversion - ) - - self._cut_sim_dec() - - @property - def sim(self) -> np.ndarray: - """Docstring""" - return self._sim - - @sim.setter - def sim(self, sim: np.ndarray) -> None: - """Docstring""" - self._full_sim = sim.copy() - - if "sindec" not in self._full_sim.dtype.names: - self._full_sim = rf.append_fields( - self._full_sim, - "sindec", - np.sin(self._full_sim["dec"]), - usemask=False, - ) - if "weight" not in self._full_sim.dtype.names: - self._full_sim = rf.append_fields( - self._full_sim, - "weight", - np.zeros(len(self._full_sim)), - dtypes=np.float32, - ) - - self._cut_sim_dec() - - @classmethod - def generate_config(cls): - """Docstring""" - config = super().generate_config() - config["reco_sampling_width"] = np.deg2rad(5) - config["flux_unit_conversion"] = 1e6 - return config - - -@dataclasses.dataclass -class ThreeMLTimeDepDataHandler( - data_handlers.TimeDependentNuSourcesDataHandler, ThreeMLDataHandler -): - """Docstring""" - - @classmethod - def generate_config(cls): - """Docstring""" - config = super().generate_config() - return config diff --git a/build/lib/mla/threeml/profilellh.py b/build/lib/mla/threeml/profilellh.py deleted file mode 100644 index 3bfe3862..00000000 --- a/build/lib/mla/threeml/profilellh.py +++ /dev/null @@ -1,100 +0,0 @@ -"""Docstring""" - -__author__ = 'John Evans and Jason Fan' -__copyright__ = 'Copyright 2024' -__credits__ = ['John Evans', 'Jason Fan', 'Michael Larson'] -__license__ = 'Apache License 2.0' -__version__ = '1.4.1' -__maintainer__ = 'Jason Fan' -__email__ = 'klfan@terpmail.umd.edu' -__status__ = 'Development' - -from threeML.plugin_prototype import PluginPrototype -from scipy.interpolate import RegularGridInterpolator -import pandas as pd -from typing import Optional -from astromodels import Model -import numpy as np - - -class ProfileLLHLike(PluginPrototype): - """Docstring""" - def __init__( - self, - name: str, - df: Optional[pd.DataFrame] = None, - spline: Optional[callable] = None, - fill_value: Optional[float] = 1e30, - ): - nuisance_parameters = {} - - """ - A generic plugin for profile likelihood. - Give either a pandas dataframe in format of parameter1,2...,-llh - or a spline which return -llh. - - :param name: - :type name: str - :param df: - :type df: pandas dataframe - :param spline: - :type spline: callable - :returns: - """ - - super().__init__(name, nuisance_parameters) - if spline is not None: - self.spline = spline - self.df = None - else: - self.df = df - self.par_name = list(df.columns) - self.par_name.pop() - listofpoint = [] - shape = [] - for n in self.par_name: - points = np.unique(df[n]) - listofpoint.append(points) - shape.append(points.shape[0]) - llh = np.reshape(df["llh"].values, shape) - self.spline = RegularGridInterpolator( - listofpoint, llh, bounds_error=False, fill_value=fill_value - ) - - @property - def likelihood_model(self) -> Model: - - return self._likelihood_model - - def set_model(self, likelihood_model_instance: Model) -> None: - """ - Set the model to be used in the joint minimization. - Must be a LikelihoodModel instance. - :param likelihood_model_instance: instance of Model - :type likelihood_model_instance: astromodels.Model - """ - - self._likelihood_model = likelihood_model_instance - - def get_log_like(self) -> float: - """ - Return the value of the log-likelihood with the current values for the - parameters - """ - current_value = [] - for name in self.par_name: - value = self._likelihood_model.parameters[name].value - current_value.append(value) - llh = -self.spline(current_value)[0] - return llh - - def inner_fit(self) -> float: - """ - This is used for the profile likelihood. Keeping fixed all parameters in the - LikelihoodModel, this method minimize the logLike over the remaining nuisance - parameters, i.e., the parameters belonging only to the model for this - particular detector. If there are no nuisance parameters, simply return the - logLike value. - """ - - return self.get_log_like() diff --git a/build/lib/mla/threeml/sob_terms.py b/build/lib/mla/threeml/sob_terms.py deleted file mode 100644 index ff7ce5a9..00000000 --- a/build/lib/mla/threeml/sob_terms.py +++ /dev/null @@ -1,504 +0,0 @@ -"""Docstring""" - -__author__ = 'John Evans and Jason Fan' -__copyright__ = 'Copyright 2024' -__credits__ = ['John Evans', 'Jason Fan', 'Michael Larson'] -__license__ = 'Apache License 2.0' -__version__ = '1.4.1' -__maintainer__ = 'Jason Fan' -__email__ = 'klfan@terpmail.umd.edu' -__status__ = 'Development' - -import dataclasses - -import numpy as np -from scipy.interpolate import UnivariateSpline as Spline -from .. import sob_terms -from .. import sources -from .. import params as par -from . import spectral -from . import data_handlers - -PSTrackv4_sin_dec_bin = np.unique( - np.concatenate( - [ - np.linspace(-1, -0.93, 4 + 1), - np.linspace(-0.93, -0.3, 10 + 1), - np.linspace(-0.3, 0.05, 9 + 1), - np.linspace(0.05, 1, 18 + 1), - ] - ) -) - -PSTrackv4_log_energy_bins = np.arange(1, 9.5 + 0.01, 0.125) - - -@dataclasses.dataclass -class ThreeMLPSEnergyTerm(sob_terms.SoBTerm): - """ - Energy term for 3ML. Constructs only from 3ML energy term factory - """ - - _energysobhist: np.ndarray - _sin_dec_idx: np.ndarray - _log_energy_idx: np.ndarray - - def update_sob_hist(self, factory: sob_terms.SoBTermFactory) -> None: - """ - Updating the signal-over-background energy histogram. - - Args: - factory: energy term factory - """ - self._energysobhist = factory.cal_sob_map() - - @property - def params(self) -> par.Params: - """Docstring""" - return self._params - - @params.setter - def params(self, params: par.Params) -> None: - """Docstring""" - self._params = params - - @property - def sob(self) -> np.ndarray: - """Docstring""" - return self._energysobhist[self._sin_dec_idx, self._log_energy_idx] - - -@dataclasses.dataclass -class ThreeMLBaseEnergyTermFactory(sob_terms.SoBTermFactory): - """Docstring""" - - pass - - -@dataclasses.dataclass -class ThreeMLPSEnergyTermFactory(ThreeMLBaseEnergyTermFactory): - """ - This is the class for using MC directly to build the Energy terms. - We sugguest using the IRF for Energy term factory due to speed. - - Args: - data_handler: 3ML data handler - source: 3ML source object - spectrum: signal spectrum - """ - - data_handler: data_handlers.ThreeMLDataHandler - source: sources.PointSource - spectrum: spectral.BaseSpectrum - _source: sources.PointSource = dataclasses.field(init=False, repr=False) - _spectrum: spectral.BaseSpectrum = dataclasses.field( - init=False, repr=False, default=spectral.PowerLaw(1e3, 1e-14, -2) - ) - _bg_sob: np.ndarray = dataclasses.field(init=False, repr=False) - _sin_dec_bins: np.ndarray = dataclasses.field(init=False, repr=False) - _log_energy_bins: np.ndarray = dataclasses.field(init=False, repr=False) - _bins: np.ndarray = dataclasses.field(init=False, repr=False) - _ow_hist: np.ndarray = dataclasses.field(init=False, repr=False) - _ow_ebin: np.ndarray = dataclasses.field(init=False, repr=False) - _unit_scale: float = dataclasses.field(init=False, repr=False) - - def __post_init__(self) -> None: - """Docstring""" - if self.config["list_sin_dec_bins"] is None: - self._sin_dec_bins = np.linspace(-1, 1, 1 + self.config["sin_dec_bins"]) - else: - self._sin_dec_bins = self.config["list_sin_dec_bins"] - if self.config["list_log_energy_bins"] is None: - self._log_energy_bins = np.linspace( - *self.config["log_energy_bounds"], 1 + self.config["log_energy_bins"] - ) - else: - self._log_energy_bins = self.config["list_log_energy_bins"] - self.data_handler.reduced_reco_sim = self.data_handler.cut_reconstructed_sim( - self.source.location[1], - self.data_handler.config["reco_sampling_width"], - ) - self._unit_scale = self.config["Energy_convesion(ToGeV)"] - self._bins = np.array([self._sin_dec_bins, self._log_energy_bins], dtype=object) - self._init_bg_sob_map() - self._build_ow_hist() - - def __call__(self, params: par.Params, events: np.ndarray) -> sob_terms.SoBTerm: - """Docstring""" - # Get the bin that each event belongs to - sin_dec_idx = np.searchsorted(self._sin_dec_bins[:-1], events["sindec"]) - 1 - - log_energy_idx = np.searchsorted(self._log_energy_bins[:-1], events["logE"]) - 1 - - return ThreeMLPSEnergyTerm( - name=self.config["name"], - _params=params, - _sob=np.empty(1), - _sin_dec_idx=sin_dec_idx, - _log_energy_idx=log_energy_idx, - _energysobhist=self.cal_sob_map(), - ) - - def _build_ow_hist(self) -> np.ndarray: - """Docstring""" - self._ow_hist, self._ow_ebin = np.histogram( - np.log10(self.data_handler.sim["trueE"]), - bins=200, - weights=self.data_handler.sim["ow"], - ) - self._ow_ebin = 10**self._ow_ebin[:-1] * self._unit_scale - - def get_ns(self) -> float: - """Docstring""" - return (self.spectrum(self._ow_ebin) * self._ow_hist).sum() * self._unit_scale - - def _init_bg_sob_map(self) -> np.ndarray: - """Docstring""" - if self.config["mc_bkgweight"] is None: - bg_h = self.data_handler.build_background_sindec_logenergy_histogram( - self._bins - ) - else: - bg_h = self.data_handler.build_mcbackground_sindec_logenergy_histogram( - self._bins, self.config["mc_bkgweight"] - ) - print("using mc background") - # Normalize histogram by dec band - bg_h /= np.sum(bg_h, axis=1)[:, None] - if self.config["backgroundSOBoption"] == 1: - bg_h[bg_h <= 0] = np.min(bg_h[bg_h > 0]) - elif self.config["backgroundSOBoption"] == 0: - pass - self._bg_sob = bg_h - - @property - def source(self) -> sources.PointSource: - """Docstring""" - return self._source - - @source.setter - def source(self, source: sources.PointSource) -> None: - """Docstring""" - self._source = source - self.data_handler.reduced_reco_sim = self.data_handler.cut_reconstructed_sim( - self.source.location[1], - self.data_handler.config["reco_sampling_width"], - ) - - def cal_sob_map(self) -> np.ndarray: - """Creates sob histogram for a given spectrum. - Returns: - An array of signal-over-background values binned in sin(dec) and - log(energy) for a given gamma. - """ - sig_h = self.data_handler.build_signal_energy_histogram( - self.spectrum, self._bins, self._unit_scale - ) - bin_centers = self._log_energy_bins[:-1] + np.diff(self._log_energy_bins) / 2 - # Normalize histogram by dec band - sig_h /= np.sum(sig_h, axis=1)[:, None] - - # div-0 okay here - with np.errstate(divide="ignore", invalid="ignore"): - ratio = sig_h / self._bg_sob - - for i in range(ratio.shape[0]): - # Pick out the values we want to use. - # We explicitly want to avoid NaNs and infinities - good = np.isfinite(ratio[i]) & (ratio[i] > 0) - good_bins, good_vals = bin_centers[good], ratio[i][good] - if len(good_bins) > 1: - # Do a linear interpolation across the energy range - spline = Spline( - good_bins, good_vals, **self.config["energy_spline_kwargs"] - ) - - # And store the interpolated values - ratio[i] = spline(bin_centers) - elif len(good_bins) == 1: - ratio[i] = good_vals - else: - ratio[i] = 0 - return ratio - - def calculate_drop_mask(self, events: np.ndarray) -> np.ndarray: - """Docstring""" - return np.ones(len(events), dtype=bool) - - @property - def spectrum(self) -> spectral.BaseSpectrum: - """Docstring""" - return self._spectrum - - @spectrum.setter - def spectrum(self, spectrum: spectral.BaseSpectrum) -> None: - """Docstring""" - if isinstance(spectrum, property): - # initial value not specified, use default - spectrum = ThreeMLPSEnergyTermFactory._spectrum - self._spectrum = spectrum - - @classmethod - def generate_config(cls): - """Docstring""" - config = super().generate_config() - config["sin_dec_bins"] = 68 - config["log_energy_bins"] = 42 - config["log_energy_bounds"] = (1, 8) - config["energy_spline_kwargs"] = { - "k": 1, - "s": 0, - "ext": 3, - } - config["backgroundSOBoption"] = 0 - config["mc_bkgweight"] = None - config["list_sin_dec_bins"] = PSTrackv4_sin_dec_bin - config["list_log_energy_bins"] = PSTrackv4_log_energy_bins - return config - - -@dataclasses.dataclass -class ThreeMLPSIRFEnergyTermFactory(ThreeMLPSEnergyTermFactory): - """Docstring""" - - data_handler: data_handlers.ThreeMLDataHandler - source: sources.PointSource - spectrum: spectral.BaseSpectrum - _source: sources.PointSource = dataclasses.field(init=False, repr=False) - _spectrum: spectral.BaseSpectrum = dataclasses.field( - init=False, repr=False, default=spectral.PowerLaw(1e3, 1e-14, -2) - ) - _bg_sob: np.ndarray = dataclasses.field(init=False, repr=False) - _sin_dec_bins: np.ndarray = dataclasses.field( - init=False, repr=False, default=PSTrackv4_sin_dec_bin - ) - _log_energy_bins: np.ndarray = dataclasses.field(init=False, repr=False) - _bins: np.ndarray = dataclasses.field(init=False, repr=False) - _trueebin: np.ndarray = dataclasses.field(init=False, repr=False) - _irf: np.ndarray = dataclasses.field(init=False, repr=False) - _sindec_bounds: np.ndarray = dataclasses.field(init=False, repr=False) - _ntrueebin: int = dataclasses.field(init=False, repr=False) - _unit_scale: float = dataclasses.field(init=False, repr=False) - - def __post_init__(self) -> None: - """Docstring""" - if self.config["list_sin_dec_bins"] is None: - self._sin_dec_bins = np.linspace(-1, 1, 1 + self.config["sin_dec_bins"]) - else: - self._sin_dec_bins = self.config["list_sin_dec_bins"] - if self.config["list_log_energy_bins"] is None: - self._log_energy_bins = np.linspace( - *self.config["log_energy_bounds"], 1 + self.config["log_energy_bins"] - ) - else: - self._log_energy_bins = self.config["list_log_energy_bins"] - lower_sindec = np.maximum( - np.sin( - self.source.location[1] - - self.data_handler.config["reco_sampling_width"] - ), - -0.99, - ) - upper_sindec = np.minimum( - np.sin( - self.source.location[1] - + self.data_handler.config["reco_sampling_width"] - ), - 1, - ) - lower_sindec_index = np.searchsorted(self._sin_dec_bins, lower_sindec) - 1 - uppper_sindec_index = np.searchsorted(self._sin_dec_bins, upper_sindec) - self._sindec_bounds = np.array([lower_sindec_index, uppper_sindec_index]) - self._bins = np.array([self._sin_dec_bins, self._log_energy_bins], dtype=object) - self._truelogebin = self.config["list_truelogebin"] - self._unit_scale = self.config["Energy_convesion(ToGeV)"] - self._init_bg_sob_map() - self._build_ow_hist() - self._init_irf() - - def __call__(self, params: par.Params, events: np.ndarray) -> sob_terms.SoBTerm: - """Docstring""" - # Get the bin that each event belongs to - sin_dec_idx = np.searchsorted(self._sin_dec_bins[:-1], events["sindec"]) - 1 - - log_energy_idx = np.searchsorted(self._log_energy_bins[:-1], events["logE"]) - 1 - - return ThreeMLPSEnergyTerm( - name=self.config["name"], - _params=params, - _sob=np.empty(1), - _sin_dec_idx=sin_dec_idx, - _log_energy_idx=log_energy_idx, - _energysobhist=self.cal_sob_map(), - ) - - def _init_bg_sob_map(self) -> None: - """Docstring""" - if self.config["mc_bkgweight"] is None: - bg_h = self.data_handler.build_background_sindec_logenergy_histogram( - self._bins - ) - else: - bg_h = self.data_handler.build_mcbackground_sindec_logenergy_histogram( - self._bins, self.config["mc_bkgweight"] - ) - print("using mc background") - # Normalize histogram by dec band - bg_h /= np.sum(bg_h, axis=1)[:, None] - if self.config["backgroundSOBoption"] == 1: - bg_h[bg_h <= 0] = np.min(bg_h[bg_h > 0]) - elif self.config["backgroundSOBoption"] == 0: - pass - self._bg_sob = bg_h - - def _init_irf(self) -> None: - """Docstring""" - self._irf = np.zeros( - ( - len(self._sin_dec_bins) - 1, - len(self._log_energy_bins) - 1, - len(self._truelogebin) - 1, - ) - ) - self._trueebin = 10 ** (self._truelogebin[:-1]) - sindec_idx = ( - np.digitize(np.sin(self.data_handler.full_sim["dec"]), self._sin_dec_bins) - - 1 - ) - - for i in range(len(self._sin_dec_bins) - 1): - events_dec = self.data_handler.full_sim[(sindec_idx == i)] - loge_idx = np.digitize(events_dec["logE"], self._log_energy_bins) - 1 - - for j in range(len(self._log_energy_bins) - 1): - events = events_dec[(loge_idx == j)] - - # Don't bother if we don't find events. - if events["ow"].sum() == 0: - continue - - # True bins are in log(trueE) to ensure they're well spaced. - self._irf[i, j], _ = np.histogram( - np.log10(events["trueE"]), - bins=self._truelogebin, - weights=events["ow"], - ) - - # Have to pick an "energy" to assign to the bin. That's complicated, since - # you'd (in principle) want the flux-weighted average energy, but we don't - # have a flux function here. Instead, try just using the minimum energy of - # the bin? Should be fine for small enough bins. - # self._trueebin[i,j] = np.exp(bins[:-1] + (bins[1] - bins[0])) - # emean[i,j] = np.average(events['trueE'], weights=events['ow']) - - def build_sig_h(self, spectrum: spectral.BaseSpectrum) -> np.ndarray: - """Docstring""" - sig = np.zeros(self._bg_sob.shape) - flux = spectrum(self._trueebin * self._unit_scale) # converting unit - sig[self._sindec_bounds[0]:self._sindec_bounds[1], :] = np.dot( - self._irf[self._sindec_bounds[0]:self._sindec_bounds[1], :, :], flux - ) - sig /= np.sum(sig, axis=1)[:, None] - return sig - - @property - def source(self) -> sources.PointSource: - """Docstring""" - return self._source - - @source.setter - def source(self, source: sources.PointSource) -> None: - """Docstring""" - self._source = source - lower_sindec = np.maximum( - np.sin( - self.source.location[1] - - self.data_handler.config["reco_sampling_width"] - ), - -0.99, - ) - upper_sindec = np.minimum( - np.sin( - self.source.location[1] - + self.data_handler.config["reco_sampling_width"] - ), - 1, - ) - lower_sindec_index = np.searchsorted(self._sin_dec_bins, lower_sindec) - 1 - uppper_sindec_index = np.searchsorted(self._sin_dec_bins, upper_sindec) - self._sindec_bounds = np.array([lower_sindec_index, uppper_sindec_index]) - - def cal_sob_map(self) -> np.ndarray: - """Creates sob histogram for a given spectrum. - - Returns: - An array of signal-over-background values binned in sin(dec) and - log(energy) for a given gamma. - """ - sig_h = self.build_sig_h(self.spectrum) - - bin_spline = self._log_energy_bins[:-1] - # Normalize histogram by dec band - - # div-0 okay here - with np.errstate(divide="ignore", invalid="ignore"): - ratio = sig_h / self._bg_sob - - for i in range(ratio.shape[0]): - # Pick out the values we want to use. - # We explicitly want to avoid NaNs and infinities - good = np.isfinite(ratio[i]) & (ratio[i] > 0) - good_bins, good_vals = bin_spline[good], ratio[i][good] - if len(good_bins) > 1: - # Do a linear interpolation across the energy range - spline = Spline( - good_bins, good_vals, **self.config["energy_spline_kwargs"] - ) - - # And store the interpolated values - ratio[i] = spline(bin_spline) - elif len(good_bins) == 1: - ratio[i] = good_vals - else: - ratio[i] = 0 - - return ratio - - def calculate_drop_mask(self, events: np.ndarray) -> np.ndarray: - """Docstring""" - return np.ones(len(events), dtype=bool) - - @property - def spectrum(self) -> spectral.BaseSpectrum: - """Docstring""" - return self._spectrum - - @spectrum.setter - def spectrum(self, spectrum: spectral.BaseSpectrum) -> None: - """Docstring""" - if isinstance(spectrum, property): - # initial value not specified, use default - spectrum = ThreeMLPSEnergyTermFactory._spectrum - self._spectrum = spectrum - - @classmethod - def generate_config(cls): - """Docstring""" - config = super().generate_config() - config["sin_dec_bins"] = 68 - config["log_energy_bins"] = 42 - config["log_energy_bounds"] = (1, 8) - config["energy_spline_kwargs"] = { - "k": 1, - "s": 0, - "ext": 3, - } - config["backgroundSOBoption"] = 0 - config["mc_bkgweight"] = None - config["list_sin_dec_bins"] = PSTrackv4_sin_dec_bin - config["list_log_energy_bins"] = PSTrackv4_log_energy_bins - config["list_truelogebin"] = np.arange( - 2, 9.01 + 0.01, 0.01 - ) - config["Energy_convesion(ToGeV)"] = 1e6 # GeV to keV - return config diff --git a/build/lib/mla/threeml/spectral.py b/build/lib/mla/threeml/spectral.py deleted file mode 100644 index 986ec3a5..00000000 --- a/build/lib/mla/threeml/spectral.py +++ /dev/null @@ -1,157 +0,0 @@ -""" -The classes in this file are example time profiles that can be used in the -analysis classes. There is also GenericProfile, an abstract parent class to -inherit from to create other time profiles. -""" - - -__author__ = 'John Evans and Jason Fan' -__copyright__ = 'Copyright 2024' -__credits__ = ['John Evans', 'Jason Fan', 'Michael Larson'] -__license__ = 'Apache License 2.0' -__version__ = '1.4.1' -__maintainer__ = 'Jason Fan' -__email__ = 'klfan@terpmail.umd.edu' -__status__ = 'Development' - - -from typing import Optional, Union - -import abc -import numpy as np - - -class BaseSpectrum: - """A generic base class to standardize the methods for the Spectrum. - - Any callable function will work. - - Attributes: - """ - - __metaclass__ = abc.ABCMeta - - @abc.abstractmethod - def __init__(self) -> None: - """Initializes the Spectrum. - """ - - @abc.abstractmethod - def __call__(self, energy: Union[np.ndarray, float], - **kwargs) -> np.ndarray: - """return the differential flux at given energy(s). - - Args: - energy: An array of energy - - Returns: - A numpy array of differential flux. - """ - - @abc.abstractmethod - def __str__(self) -> None: - """String representation - """ - return 'Base spectrum' - - -class PowerLaw(BaseSpectrum): - """Spectrum class for PowerLaw. - - Use this to produce PowerLaw spectrum. - - Attributes: - E0 (float): pivot energy - A (float): Flux Norm - gamma(float): Spectral index - Ecut(float): Cut-off energy - """ - - def __init__(self, energy_0: float, flux_norm: float, gamma: float, - energy_cut: Optional[float] = None) -> None: - """ Constructor of PowerLaw object. - - Args: - energy_0: Normalize Energy - flux_norm: Flux Normalization - gamma: Spectral index - energy_cut: Cut-off energy - """ - - super().__init__() - self.energy_0 = energy_0 - self.flux_norm = flux_norm - self.gamma = gamma - self.energy_cut = energy_cut - - def __call__(self, energy: Union[np.ndarray, float], - **kwargs) -> np.ndarray: - """Evaluate spectrum at energy E according to - - dN/dE = A (E / E0)^gamma - - where A has units of events / (GeV cm^2 s). We treat - the 'events' in the numerator as implicit and say the - units are [GeV^-1 cm^-2 s^-1]. Specifying Ecut provides - an optional spectral cutoff according to - - dN/dE = A (E / E0)^gamma * exp( -E/Ecut ) - - Args: - energy : Evaluation energy [GeV] - - Returns: - np.ndarray of differential flux - """ - flux_norm = kwargs.pop('flux_norm', self.flux_norm) - energy_0 = kwargs.pop('energy_0', self.energy_0) - energy_cut = kwargs.pop('energy_cut', self.energy_cut) - gamma = kwargs.pop('gamma', self.gamma) - - flux = flux_norm * (energy / energy_0)**(gamma) - - # apply optional exponential cutoff - if energy_cut is not None: - flux *= np.exp(-energy / self.energy_cut) - - return flux - - def __str__(self) -> None: - """String representation - """ - return 'PowerLaw' - - -class CustomSpectrum(BaseSpectrum): - '''Custom spectrum using callable object - ''' - def __init__(self, spectrum): - """Constructor of CustomSpectrum object. - - Constructor - - Args: - spectrum: Any callable object - - """ - - super().__init__() - self.spectrum = spectrum - - def __call__(self, energy: Union[np.ndarray, float], - **kwargs) -> np.ndarray: - """Evaluate spectrum at energy E - - Constructor - - Args: - energy : Evaluation energy - - Returns: - np.ndarray of differential flux - """ - return self.spectrum(energy) - - def __str__(self): - r"""String representation of class""" - return 'CustomSpectrum' diff --git a/build/lib/mla/time_profiles.py b/build/lib/mla/time_profiles.py deleted file mode 100644 index 06fb3931..00000000 --- a/build/lib/mla/time_profiles.py +++ /dev/null @@ -1,632 +0,0 @@ -""" -The classes in this file are example time profiles that can be used in the -analysis classes. There is also GenericProfile, an abstract parent class to -inherit from to create other time profiles. -""" - -__author__ = 'John Evans and Jason Fan' -__copyright__ = 'Copyright 2024' -__credits__ = ['John Evans', 'Jason Fan', 'Michael Larson'] -__license__ = 'Apache License 2.0' -__version__ = '1.4.1' -__maintainer__ = 'Jason Fan' -__email__ = 'klfan@terpmail.umd.edu' -__status__ = 'Development' - -from typing import Callable, ClassVar, Dict, List, Optional, Tuple - -import abc -import dataclasses - -import numpy as np -import scipy.stats - -from . import configurable -from .params import Params - - -@dataclasses.dataclass -class GenericProfile(configurable.Configurable): - """A generic base class to standardize the methods for the time profiles. - - While I'm only currently using scipy-based - probability distributions, you can write your own if you - want. Just be sure to define these methods and ensure that - the PDF is normalized! - - Attributes: - exposure (float): - range (Tuple[Optional[float], Optional[float]]): The range of allowed - times for for events injected using this time profile. - default_params (Dict[str, float]): A dictionary of fitting parameters - for this time profile. - param_dtype (List[Tuple[str, str]]): The numpy dytpe for the fitting - parameters. - """ - - __metaclass__ = abc.ABCMeta - - @abc.abstractmethod - def pdf(self, times: np.ndarray) -> np.ndarray: - """Get the probability amplitude given a time for this time profile. - - Args: - times: An array of event times to get the probability amplitude for. - - Returns: - A numpy array of probability amplitudes at the given times. - """ - - @abc.abstractmethod - def logpdf(self, times: np.ndarray) -> np.ndarray: - """Get the log(probability) given a time for this time profile. - - Args: - times: An array of times to get the log(probability) of. - - Returns: - A numpy array of log(probability) at the given times. - """ - - @abc.abstractmethod - def random(self, size: int) -> np.ndarray: - """Get random times sampled from the pdf of this time profile. - - Args: - size: The number of times to return. - - Returns: - An array of times. - """ - - @abc.abstractmethod - def x0(self, times: np.ndarray) -> Tuple: - """Gets a tuple of initial guess to use when fitting parameters. - - The guesses are arrived at by simple approximations using the given - times. - - Args: - times: An array of times to use to approximate the fitting - parameters of this time profile. - - Returns: - A tuple of approximate parameters. - """ - - @abc.abstractmethod - def bounds( - self, - time_profile: 'GenericProfile', - ) -> List[Tuple[Optional[float], Optional[float]]]: - """Get a list of tuples of bounds for the parameters of this profile. - - Uses another time profile to constrain the bounds. This is usually - needed to constrain the bounds of a signal time profile given a - background time profile. - - Args: - time_profile: Another time profile to constrain from. - - Returns: - A list of tuples of bounds for fitting the parameters of this time - profile. - """ - - @abc.abstractmethod - def cdf(self, times: np.ndarray) -> np.ndarray: - """Docstring""" - - @abc.abstractmethod - def inverse_transform_sample( - self, start_times: np.ndarray, stop_times: np.ndarray) -> np.ndarray: - """Docstring""" - - @property - @abc.abstractmethod - def params(self) -> dict: - """Docstring""" - - @params.setter - @abc.abstractmethod - def params(self, params: Params) -> None: - """Docstring""" - - @property - @abc.abstractmethod - def param_bounds(self) -> dict: - """Docstring""" - - @property - @abc.abstractmethod - def exposure(self) -> float: - """Docstring""" - - @property - @abc.abstractmethod - def range(self) -> Tuple[float, float]: - """Gets the maximum and minimum values for the times in this profile. - """ - - @property - @abc.abstractmethod - def default_params(self) -> Dict[str, float]: - """Returns the initial parameters formatted for ts calculation output. - """ - - @property - @abc.abstractmethod - def param_dtype(self) -> np.dtype: - """Returns the parameter names and datatypes formatted for numpy dtypes. - """ - - -@dataclasses.dataclass -class GaussProfile(GenericProfile): - """Time profile class for a gaussian distribution. - - Use this to produce gaussian-distributed times for your source. - - Attributes: - mean (float): The center of the distribution. - sigma (float): The spread of the distribution. - scipy_dist(scipy.stats.rv_continuous): - exposure (float): - range (Tuple[Optional[float], Optional[float]]): The range of allowed - times for for events injected using this time profile. - default_params (Dict[str, float]): A dictionary of fitting parameters - for this time profile. - param_dtype (List[Tuple[str, str]]): The numpy dytpe for the fitting - parameters. - """ - scipy_dist: scipy.stats.distributions.rv_frozen = dataclasses.field(init=False) - _mean: float = dataclasses.field(init=False, repr=False) - _sigma: float = dataclasses.field(init=False, repr=False) - _param_dtype: ClassVar[np.dtype] = np.dtype( - [('mean', np.float32), ('sigma', np.float32)]) - - def __post_init__(self) -> None: - """Initializes the time profile.""" - self._mean = self.config['mean'] - self._sigma = self.config['sigma'] - self.scipy_dist = scipy.stats.norm(self._mean, self._sigma) - - def pdf(self, times: np.ndarray) -> np.ndarray: - """Calculates the probability for each time. - - Args: - times: A numpy list of times to evaluate. - - Returns: - A numpy array of probability amplitudes at the given times. - """ - return self.scipy_dist.pdf(times) - - def logpdf(self, times: np.ndarray) -> np.ndarray: - """Calculates the log(probability) for each time. - - Args: - times: A numpy list of times to evaluate. - - Returns: - A numpy array of log(probability) at the given times. - """ - return self.scipy_dist.logpdf(times) - - def random(self, size: int = 1) -> np.ndarray: - """Returns random values following the gaussian distribution. - - Args: - size: The number of random values to return. - - Returns: - An array of times. - """ - return self.scipy_dist.rvs(size=size) - - def x0(self, times: np.ndarray) -> tuple: - """Returns good guesses for mean and sigma based on given times. - - Args: - times: A numpy list of times to evaluate. - - Returns: - A tuple of mean and sigma guesses. - """ - x0_mean = np.average(times) - x0_sigma = np.std(times) - return x0_mean, x0_sigma - - def bounds(self, time_profile: GenericProfile) -> List[tuple]: - """Returns good bounds for this time profile given another time profile. - - Limits the mean to be within the range of the other profile and limits - the sigma to be >= 0 and <= the width of the other profile. - - Args: - time_profile: Another time profile to use to define the parameter - bounds of this time profile. - - Returns: - A list of tuples of bounds for fitting the parameters in this time - profile. - """ - if np.nan in time_profile.range: - return [time_profile.range, (0, np.nan)] - - diff = time_profile.range[1] - time_profile.range[0] - return [time_profile.range, (0, diff)] - - def cdf(self, times: np.ndarray) -> np.ndarray: - """Docstring""" - return self.scipy_dist.cdf(times) - - def inverse_transform_sample( - self, start_times: np.ndarray, stop_times: np.ndarray) -> np.ndarray: - """Docstring""" - start_cdfs = self.cdf(start_times) - stop_cdfs = self.cdf(stop_times) - cdfs = np.random.uniform(start_cdfs, stop_cdfs) - return self.scipy_dist.ppf(cdfs) - - @property - def params(self) -> dict: - """Docstring""" - return {'mean': self._mean, 'sigma': self._sigma} - - @params.setter - def params(self, params: Params) -> None: - """Docstring""" - update = False - - if 'mean' in params: - self._mean = params['mean'] - update = True - if 'sigma' in params: - self._sigma = params['sigma'] - update = True - - if update: - self.scipy_dist = scipy.stats.norm(self._mean, self._sigma) - - @property - def param_bounds(self) -> dict: - return {'mean': self.range, 'sigma': (0, np.inf)} - - @property - def exposure(self) -> float: - return np.sqrt(2 * np.pi * self._sigma**2) - - @property - def range(self) -> Tuple[float, float]: - return -np.inf, np.inf - - @property - def param_dtype(self) -> np.dtype: - return self._param_dtype - - @classmethod - def generate_config(cls) -> dict: - """Docstring""" - config = super().generate_config() - config['mean'] = np.nan - config['sigma'] = np.nan - return config - - -@dataclasses.dataclass -class UniformProfile(GenericProfile): - """Time profile class for a uniform distribution. - - Use this for background or if you want to assume a steady signal from - your source. - - Attributes: - exposure (float): - range (Tuple[Optional[float], Optional[float]]): The range of allowed - times for for events injected using this time profile. - default_params (Dict[str, float]): A dictionary of fitting parameters - for this time profile. - param_dtype (List[Tuple[str, str]]): The numpy dytpe for the fitting - parameters. - """ - _range: Tuple[float, float] = dataclasses.field(init=False, repr=False) - _param_dtype: ClassVar[np.dtype] = np.dtype( - [('start', np.float32), ('length', np.float32)]) - - def __post_init__(self) -> None: - """Constructs the time profile.""" - self._range = (self.config['start'], self.config['start'] + self.config['length']) - - def pdf(self, times: np.ndarray) -> np.ndarray: - """Calculates the probability for each time. - - Args: - times: A numpy list of times to evaluate. - - Returns: - A numpy array of probability amplitudes at the given times. - """ - output = np.zeros_like(times) - output[ - (times >= self.range[0]) & (times < self.range[1]) - ] = 1 / (self.range[1] - self.range[0]) - return output - - def logpdf(self, times: np.ndarray) -> np.ndarray: - """Calculates the log(probability) for each time. - - Args: - times: A numpy list of times to evaluate. - - Returns: - A numpy array of log(probability) at the given times. - """ - return np.log(self.pdf(times)) - - def random(self, size: int = 1) -> np.ndarray: - """Returns random values following the uniform distribution. - - Args: - size: The number of random values to return. - - Returns: - An array of times. - """ - return np.random.uniform(*self.range, size) - - def x0(self, times: np.ndarray) -> Tuple[float, float]: - """Returns good guesses for start and stop based on given times. - - Args: - times: A numpy list of times to evaluate. - - Returns: - A tuple of start and stop guesses. - """ - x0_start = np.min(times) - x0_end = np.max(times) - return x0_start, x0_end - - def bounds(self, time_profile: GenericProfile - ) -> List[Tuple[Optional[float], Optional[float]]]: - """Given some other profile, returns allowable ranges for parameters. - - Args: - time_profile: Another time profile used to get the limits of start - and length. - - Returns: - A list of tuples of bounds for fitting. - """ - diff = time_profile.range[1] - time_profile.range[0] - return [time_profile.range, (0, diff)] - - def cdf(self, times: np.ndarray) -> np.ndarray: - """Docstring""" - return np.clip((times - self.range[0]) / (self.range[1] - self.range[0]), 0, 1) - - def inverse_transform_sample( - self, start_times: np.ndarray, stop_times: np.ndarray) -> np.ndarray: - """Docstring""" - return np.random.uniform( - np.maximum(start_times, self.range[0]), - np.minimum(stop_times, self.range[1]), - ) - - @property - def params(self) -> dict: - """Docstring""" - return {'start': self._range[0], 'length': self._range[1] - self._range[0]} - - @params.setter - def params(self, params: Params) -> None: - """Docstring""" - if 'start' in params: - self._range = ( - params['start'], params['start'] + self._range[1] - self._range[0]) - if 'length' in params: - self._range = (self._range[0], self._range[0] + params['length']) - - @property - def param_bounds(self) -> dict: - return {'start': (-np.inf, np.inf), 'length': (0, np.inf)} - - @property - def exposure(self) -> float: - return self._range[1] - self._range[0] - - @property - def range(self) -> Tuple[float, float]: - return self._range - - @property - def param_dtype(self) -> np.dtype: - return self._param_dtype - - @classmethod - def generate_config(cls) -> dict: - """Docstring""" - config = super().generate_config() - config['start'] = np.nan - config['length'] = np.nan - return config - - -@dataclasses.dataclass -class CustomProfile(GenericProfile): - """Time profile class for a custom binned distribution. - - This time profile uses a binned pdf defined between 0 and 1. Normalization - is handled internally and not required beforehand. - - Attributes: - pdf (Callable[[np.array, Tuple[float, float]], np.array]): The - distribution function. This function needs to accept an array of bin - centers and a time window as a tuple, and it needs to return an - array of probability densities at the given bin centers. - dist (scipy.stats.rv_histogram): The histogrammed version of the - distribution function. - exposure (float): - range (Tuple[Optional[float], Optional[float]]): The range of allowed - times for for events injected using this time profile. - default_params (Dict[str, float]): A dictionary of fitting parameters - for this time profile. - param_dtype (List[Tuple[str, str]]): The numpy dytpe for the fitting - parameters. - """ - dist: Callable[[np.ndarray, Tuple[float, float]], np.ndarray] - _dist: scipy.stats.rv_histogram = dataclasses.field(init=False, repr=False) - _offset: float = dataclasses.field(init=False, repr=False) - _exposure: float = dataclasses.field(init=False, repr=False) - _param_dtype: ClassVar[np.dtype] = np.dtype([('offset', np.float32)]) - - @property - def dist(self) -> scipy.stats.rv_histogram: - """Docstring""" - return self._dist - - @dist.setter - def dist( - self, - dist: Callable[[np.ndarray, Tuple[float, float]], np.ndarray], - ) -> None: - """Constructs the time profile. - - Args: - dist: - """ - self._offset = self.config['offset'] - - if isinstance(self.config['bins'], int): - bin_edges = np.linspace(*self.config['range'], self.config['bins']) - else: - span = self.config['range'][1] - self.config['range'][0] - bin_edges = span * np.array(self.config['bins']) - - bin_widths = np.diff(bin_edges) - bin_centers = bin_edges[:-1] + bin_widths - hist = dist(bin_centers, tuple(self.config['range'])) - - area_under_hist = np.sum(hist * bin_widths) - hist *= 1 / area_under_hist - self._exposure = 1 / np.max(hist) - hist *= bin_widths - - self._dist = scipy.stats.rv_histogram((hist, bin_edges)) - - def pdf(self, times: np.ndarray) -> np.ndarray: - """Calculates the probability density for each time. - - Args: - times: An array of times to evaluate. - - Returns: - An array of probability densities at the given times. - """ - return self.dist.pdf(times + self.offset) - - def logpdf(self, times: np.ndarray) -> np.ndarray: - """Calculates the log(probability) for each time. - - Args: - times: An array of times to evaluate. - - Returns: - An array of the log(probability density) for the given times. - """ - return self.dist.logpdf(times + self.offset) - - def random(self, size: int = 1) -> np.ndarray: - """Returns random values following the uniform distribution. - - Args: - size: The number of random values to return. - - Returns: - An array of random values sampled from the histogram distribution. - """ - return self.dist.rvs(size=size) + self.offset - - def x0(self, times: np.ndarray) -> Tuple[float, float]: - """Gives a guess of the parameters of this type of time profile. - - Args: - times: An array of times to use to guess the parameters. - - Returns: - The guessed start and end times of the distribution that generated - the given times. - """ - x0_start = np.min(times) - x0_end = np.max(times) - return x0_start, x0_end - - def bounds(self, time_profile: GenericProfile - ) -> List[Tuple[Optional[float], Optional[float]]]: - """Given some other profile, returns allowable ranges for parameters. - - Args: - time_profile: Another time profile to use to get the bounds. - - Returns: - The fitting bounds for the parameters of this time profile. - """ - return [time_profile.range, time_profile.range] - - def cdf(self, times: np.ndarray) -> np.ndarray: - """Docstring""" - return self.dist.cdf(times) - - def inverse_transform_sample( - self, start_times: np.ndarray, stop_times: np.ndarray) -> np.ndarray: - """Docstring""" - start_cdfs = self.cdf(start_times) - stop_cdfs = self.cdf(stop_times) - cdfs = np.random.uniform(start_cdfs, stop_cdfs) - return self.dist.ppf(cdfs) - - @property - def params(self) -> dict: - """Docstring""" - return {'offset': self.offset} - - @params.setter - def params(self, params: Params) -> None: - """Docstring""" - if 'offset' in params: - self.offset = params['offset'] - - @property - def param_bounds(self) -> dict: - return {'offset': (-np.inf, np.inf)} - - @property - def exposure(self) -> float: - return self._exposure - - @property - def offset(self) -> float: - return self._offset - - @offset.setter - def offset(self, offset: float) -> None: - self._offset = offset - - @property - def range(self) -> Tuple[Optional[float], Optional[float]]: - return ( - self.config['range'][0] + self.offset, self.config['range'][1] + self.offset) - - @property - def param_dtype(self) -> np.dtype: - return self._param_dtype - - @classmethod - def generate_config(cls) -> dict: - """Docstring""" - config = super().generate_config() - config['range'] = (np.nan, np.nan) - config['bins'] = 100 - config['offset'] = 0 - return config diff --git a/build/lib/mla/trial_generators.py b/build/lib/mla/trial_generators.py deleted file mode 100644 index 10939a00..00000000 --- a/build/lib/mla/trial_generators.py +++ /dev/null @@ -1,115 +0,0 @@ -"""Docstring""" - -__author__ = 'John Evans and Jason Fan' -__copyright__ = 'Copyright 2024' -__credits__ = ['John Evans', 'Jason Fan', 'Michael Larson'] -__license__ = 'Apache License 2.0' -__version__ = '1.4.1' -__maintainer__ = 'Jason Fan' -__email__ = 'klfan@terpmail.umd.edu' -__status__ = 'Development' - -import dataclasses - -import numpy as np -import numpy.lib.recfunctions as rf - -from . import utility_functions as uf -from . import configurable - -from .data_handlers import DataHandler -from .sources import PointSource - - -@dataclasses.dataclass -class SingleSourceTrialGenerator(configurable.Configurable): - """Docstring""" - - data_handler: DataHandler - source: PointSource - _source: PointSource = dataclasses.field(init=False, repr=False) - - def __call__(self, n_signal: float = 0) -> np.ndarray: - """Produces a single trial of background+signal events based on inputs. - - Args: - n_signal: flux norm if not fixed_ns - - Returns: - An array of combined signal and background events. - """ - rng = np.random.default_rng(self.config["random_seed"]) - n_background = rng.poisson(self.data_handler.n_background) - if not self.config["fixed_ns"]: - n_signal = rng.poisson(self.data_handler.calculate_n_signal(n_signal)) - - background = self.data_handler.sample_background(n_background, rng) - background["ra"] = rng.uniform(0, 2 * np.pi, len(background)) - - if n_signal > 0: - signal = self.data_handler.sample_signal(int(n_signal), rng) - signal = self._rotate_signal(signal) - else: - signal = np.empty(0, dtype=background.dtype) - - # Because we want to return the entire event and not just the - # number of events, we need to do some numpy magic. Specifically, - # we need to remove the fields in the simulated events that are - # not present in the data events. These include the true direction, - # energy, and 'oneweight'. - signal = rf.drop_fields( - signal, [n for n in signal.dtype.names if n not in background.dtype.names] - ) - - # Combine the signal background events and time-sort them. - # Use recfunctions.stack_arrays to prevent numpy from scrambling entry order - if background.dtype == signal.dtype: - return np.concatenate([background, signal]) - else: - return rf.stack_arrays( - [background, signal], autoconvert=True, usemask=False - ) - - def _rotate_signal(self, signal: np.ndarray) -> np.ndarray: - """Docstring""" - ra, dec = self.source.sample(len(signal)) - - signal["ra"], signal["dec"] = uf.rotate( - signal["trueRa"], - signal["trueDec"], - ra, - dec, - signal["ra"], - signal["dec"], - ) - - signal["trueRa"], signal["trueDec"] = uf.rotate( - signal["trueRa"], - signal["trueDec"], - ra, - dec, - signal["trueRa"], - signal["trueDec"], - ) - - signal["sindec"] = np.sin(signal["dec"]) - return signal - - @property - def source(self) -> PointSource: - """Docstring""" - return self._source - - @source.setter - def source(self, source: PointSource) -> None: - """Docstring""" - self.data_handler.dec_cut_location = source.config["dec"] - self._source = source - - @classmethod - def generate_config(cls) -> dict: - """Docstring""" - config = super().generate_config() - config["random_seed"] = None - config["fixed_ns"] = False - return config diff --git a/build/lib/mla/utility_functions.py b/build/lib/mla/utility_functions.py deleted file mode 100644 index a4eb7f4e..00000000 --- a/build/lib/mla/utility_functions.py +++ /dev/null @@ -1,255 +0,0 @@ -""" -Math functions needed for this package -""" - -__author__ = 'John Evans and Jason Fan' -__copyright__ = 'Copyright 2024' -__credits__ = ['John Evans', 'Jason Fan', 'Michael Larson'] -__license__ = 'Apache License 2.0' -__version__ = '1.4.1' -__maintainer__ = 'Jason Fan' -__email__ = 'klfan@terpmail.umd.edu' -__status__ = 'Development' - -import numpy as np - - -def ra_to_rad(hrs: float, mins: float, secs: float) -> float: - """Converts right ascension to radians. - - Args: - hrs: Hours. - mins: Minutes. - secs: Seconds. - - Returns: - Radian representation of right ascension. - """ - return (hrs * 15 + mins / 4 + secs / 240) * np.pi / 180 - - -def dec_to_rad(sign: int, deg: float, mins: float, secs: float) -> float: - """Converts declination to radians. - - Args: - sign: A positive integer for a positive sign, a negative integer for a - negative sign. - deg: Degrees. - mins: Minutes. - secs: Seconds. - - Returns: - Radian representation of declination. - """ - return sign / np.abs(sign) * (deg + mins / 60 + secs / 3600) * np.pi / 180 - - -def cross_matrix(mat: np.ndarray) -> np.ndarray: - """Calculate cross product matrix. - A[ij] = x_i * y_j - y_i * x_j - Args: - mat: A 2D array to take the cross product of. - Returns: - The cross matrix. - """ - skv = np.roll(np.roll(np.diag(mat.ravel()), 1, 1), -1, 0) - return skv - skv.T - - -def rotate( - ra1: float, - dec1: float, - ra2: float, - dec2: float, - ra3: float, - dec3: float, -) -> tuple: - """Rotation matrix for rotation of (ra1, dec1) onto (ra2, dec2). - - The rotation is performed on (ra3, dec3). - - Args: - ra1: The right ascension of the point to be rotated from. - dec1: The declination of the point to be rotated from. - ra2: the right ascension of the point to be rotated onto. - dec2: the declination of the point to be rotated onto. - ra3: the right ascension of the point that will actually be rotated. - dec3: the declination of the point that will actually be rotated. - - Returns: - The rotated ra3 and dec3. - - Raises: - IndexError: Arguments must all have the same dimension. - """ - ra1 = np.atleast_1d(ra1) - dec1 = np.atleast_1d(dec1) - ra2 = np.atleast_1d(ra2) - dec2 = np.atleast_1d(dec2) - ra3 = np.atleast_1d(ra3) - dec3 = np.atleast_1d(dec3) - - if not len(ra1) == len(dec1) == len(ra2) == len(dec2) == len(ra3) == len(dec3): - raise IndexError("Arguments must all have the same dimension.") - - cos_alpha = np.cos(ra2 - ra1) * np.cos(dec1) * np.cos(dec2) + np.sin(dec1) * np.sin( - dec2 - ) - - # correct rounding errors - cos_alpha[cos_alpha > 1] = 1 - cos_alpha[cos_alpha < -1] = -1 - - alpha = np.arccos(cos_alpha) - vec1 = np.vstack( - [np.cos(ra1) * np.cos(dec1), np.sin(ra1) * np.cos(dec1), np.sin(dec1)] - ).T - vec2 = np.vstack( - [np.cos(ra2) * np.cos(dec2), np.sin(ra2) * np.cos(dec2), np.sin(dec2)] - ).T - vec3 = np.vstack( - [np.cos(ra3) * np.cos(dec3), np.sin(ra3) * np.cos(dec3), np.sin(dec3)] - ).T - nvec = np.cross(vec1, vec2) - norm = np.sqrt(np.sum(nvec ** 2, axis=1)) - nvec[norm > 0] /= norm[np.newaxis, norm > 0].T - - one = np.diagflat(np.ones(3)) - ntn = np.array([np.outer(nv, nv) for nv in nvec]) - nx = np.array([cross_matrix(nv) for nv in nvec]) - - r = np.array( - [ - (1.0 - np.cos(a)) * ntn_i + np.cos(a) * one + np.sin(a) * nx_i - for a, ntn_i, nx_i in zip(alpha, ntn, nx) - ] - ) - vec = np.array([np.dot(r_i, vec_i.T) for r_i, vec_i in zip(r, vec3)]) - - r_a = np.arctan2(vec[:, 1], vec[:, 0]) - dec = np.arcsin(vec[:, 2]) - - r_a += np.where(r_a < 0.0, 2.0 * np.pi, 0.0) - - return r_a, dec - - -def angular_distance(src_ra: float, src_dec: float, r_a: float, dec: float) -> float: - """Computes angular distance between source and location. - - Args: - src_ra: The right ascension of the first point (radians). - src_dec: The declination of the first point (radians). - r_a: The right ascension of the second point (radians). - dec: The declination of the second point (radians). - - Returns: - The distance, in radians, between the two points. - """ - sin_dec = np.sin(dec) - - cos_dec = np.sqrt(1.0 - sin_dec ** 2) - - cos_dist = (np.cos(src_ra - r_a) * np.cos(src_dec) * cos_dec) + np.sin( - src_dec - ) * sin_dec - # handle possible floating precision errors - cos_dist = np.clip(cos_dist, -1, 1) - - return np.arccos(cos_dist) - - -def newton_method(sob: np.ndarray, n_drop: float) -> float: - """Docstring - - Args: - sob: - n_drop: - Returns: - - """ - newton_precision = 0 - newton_iterations = 20 - precision = newton_precision + 1 - eps = 1e-5 - k = 1 / (sob - 1) - x = [1.0 / n_drop] * newton_iterations - - for i in range(newton_iterations - 1): - # get next iteration and clamp - inv_terms = x[i] + k - inv_terms[inv_terms == 0] = eps - terms = 1 / inv_terms - drop_term = 1 / (x[i] - 1) - d1 = np.sum(terms) + n_drop * drop_term - d2 = np.sum(terms ** 2) + n_drop * drop_term ** 2 - x[i + 1] = min(1 - eps, max(0, x[i] + d1 / d2)) - - if ( - x[i] == x[i + 1] - or (x[i] < x[i + 1] and x[i + 1] <= x[i] * precision) - or (x[i + 1] < x[i] and x[i] <= x[i + 1] * precision) - ): - break - return x[i + 1] - - -def newton_method_multidataset( - sob: list, n_drop: np.ndarray, fraction: np.ndarray -) -> float: - """Docstring - - Args: - sob: - n_drop: - Returns: - - """ - newton_precision = 0 - newton_iterations = 20 - precision = newton_precision + 1 - eps = 1e-5 - k = [] - for i in range(len(sob)): - k.append(1 / (sob[i] - 1)) - x = [0.0] * newton_iterations - for i in range(newton_iterations - 1): - # get next iteration and clamp - d1 = 0 - d2 = 0 - for j in range(len(sob)): - inv_terms = x[i] * fraction[j] + k[j] - inv_terms[inv_terms == 0] = eps - terms = fraction[j] / inv_terms - drop_term = fraction[j] / (x[i] * fraction[j] - 1) - d1 += np.sum(terms) + n_drop[j] * drop_term - d2 += np.sum(terms ** 2) + n_drop[j] * drop_term ** 2 - x[i + 1] = min(1 - eps, max(0, x[i] + d1 / d2)) - - if ( - x[i] == x[i + 1] - or (x[i] < x[i + 1] and x[i + 1] <= x[i] * precision) - or (x[i + 1] < x[i] and x[i] <= x[i + 1] * precision) - ): - break - return x[i + 1] - - -def trimsim(sim: np.ndarray, fraction: float, scaleow: bool = True) -> np.ndarray: - """Keep only fraction of the simulation - - Args: - sim: simulation. - fraction: Fraction of sim to keep(will round to int). - scaleow: whether to scale the ow. - - Returns: - Trimmed sim - """ - simsize = len(sim) - n_keep = int(fraction * simsize) - sim = np.random.choice(sim, n_keep) - if scaleow: - sim["ow"] = sim["ow"] * (simsize / float(n_keep)) - - return sim diff --git a/dist/mla-1.4.0-py3.12.egg b/dist/mla-1.4.0-py3.12.egg deleted file mode 100644 index 0a7b164026abea0b14ef08a86c52d2291fc66094..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 109273 zcmZ5{b8xNAmvwGz+qP}nwr!g?wr$&Xa%0<0Zfsj$-rsyRGw=M-bxxhC-B0&AyH~GX zy&rigAYfzw003}+3zY=TX1YYfC_n%J77zdcgr8qU#KdSMWJTrZR%3E*GFEjF==eeg4R$T>5UY!b9SNiu@% zq%|hys3A=k2SdTFX&_~FaV$>a!HOsjThxx1)qgSV>_KSDr|;YL)<*<=ADR=*a6S}$ zgATH}!JZX$<7<9JX9-AeZ^@dO`%4qpd0*b?p7B^LP66JtkYpRJg_Zj!nMWjGc0AM3O}!ztXq(88*V zi|W!#g!2I;SM5ExxikYa1w`WIkkZ{wIN!_~Cu?3-^hu z;O)wuNHsFnIdL0QK0W&_1b~L^z(mP;c9<80I+T;6i5yuHD&N zb!8>|id>Ccw=Fei2yH36k5_CYAc(hEEBoV|r2 zz@_IxP~{7+bh_8BAP4B9eF4x8T!;kDNr=q^P*FTS&0JaZHAMY|99#t$*Cv`i73%J zyF1&&Psk3?!;3tBrjqXbmgGkXE>NwiE@zSu!{I}ZMc6x6DCU&FE;q*V7V|-6rKOB+s@0t#Q{z9SHFPMc`jc^ID1rxjE z0J?(y9=(KX!Oz#H4sE|K>)KP__hv1YcS*lT%_j7@){{&9v=54{9r%xF;+H?>VIj9* z?PZ#=0Vi#`TH1#|wkOQ0nv0jr!F*L+qf$JRBU(;sZ{&oUD7#F2Y`L_ba+#ASffh$k zrulyVMlBI^g%Rintsme3aQ-)H#wPYAw#Fv5Mjm?B7PeMS|G>8o2=MRdg4CgM*q_Q+Zn0(>NKJn*3|9pT8qC4fmw)l760H^fOnupHCZW13EoD z3tJ0kJv~}`51W{J*nWCwVYePaNGu_Z6Vs1f_I{@p4$W&D|Pug>};>y^+oPYU+(g`N#c86=oQ9x7@WQE29ySiss{VR znQ#kpb+UCA5Hc>L0YsVAaxQ1F%Zpu4z(synn7 z!2dz}p9z>hD<;SM{8=e5|1$vtTLWtkCkv;a2~?}f*zL2zcb}_4Zw6)wA~f{n0GExn zSYT3j4FTRx(Sz48H-sw|@y7tR_4dG*!%3wen$2MYS{*qYO=V(8RxX_>n59I?hb~#7 z07uNBwas@P8G%Bz0kl(A!%|&S41#`9EN%}#BN!4^0CwF`N zqGUwVE{;)-WHUPed(JS4|a;U@eE@DL)-n2w{afTN)3%!l@ z`Hm{{I>*bE>jB`aniVa#LS=&@Z;oI1@ABPnV>rU~=F|1DA$ zD2(T~q5pM;F96T|&;fe&4c@)R?WdrfQ&lGc08p19?PYY2Nfs9wm6eYc*B%yreqMv- zaLu1BUuDk%-V}}%vZYQB$9s)^|Chwq+|}$eays|{Y^~V>|52$KRZ{+Kuy_a@T6Orl z=XTCFWW*(&tXpc^d_DP>7}8>jpJa;Y$yQzuoI1aWv#!}_+qlNcY4d$YO96Gk*BG7T ze9fpn9dNun*z&8670__=8yc!qH1Pm%gq||tmG^W%)il6BC|*r z<`XrxCu8q0%+v`TJ%Y0#n4FS*;)YCKQbvo3TEv5ncueP(Qada6?5bc{Ku`m8YhC4cYrSb zU7qZU4jFSlu72nvNS9b@$hegrF^)M72Az}ZKH%f#XOh6rmg2$5zpW8`8)m8Vt___C zEpfU%C)}ABv%d~SJT1GQfb#vI=$bBy-u@JHYGB=QQi~l^?AR(Rlh}3`nplr=fQn@K z{lwPyzi9TKrF#uNLW%doI}bk%>Hl+&k)5rng_(<^fuXg@&+>JNn~?-!fDgU)h%lg2 zm@TH^K}nLV3{}kKCyp9x!kUy#F>b;C+c`n19H|dWaDSMo_r)yJ7|kkm=WU%KN?~N5 zL`!P43;RIp3LQ&!06hxpnnpB$)Sx=N%I;0!qEW~dP)JS|1LL5HUB2Krz( z4|8H>$5jO2CC$;`t1=917tXYF68g(VtgaRs_0l?mPhp(bQRV2eAzQ9VY$hP*^dn$` zN-36C`3o0z)GD7@x(df@B>{_!0%}~{uDaE^06sKFy-BeL&k)g`+#KjgT7#mwhkPTq znUcwx5bwi5`UTaz7yuxH5095Ys|tnPP6VZa_L4^eP3Kn(tUYl_kYLH18tZ{oTQ{8z z%SSIzNxb;*;LU4s&#!+|VS%&+SK<$jCqE79zwvbZ2S=S40my!OWZ`Fyp*R(&bs*i8&h$XL`VP0+!V{@IMncJQo@um6K^JBK{qHcX675VaW+TY?!!=kZ_)CXkd4N1p$9#LUn(gCCmdPa60!8M z7mc_%F6p42Y~oTV5I-)~T{R~!S$GJ_~{3;XGP|QeA*vUQ^%(O2Q6- zb`R%oz?^=kctS!^8l<&&Z<&H;rgCJR*d@Na#|asoDW>s)k%Dt4cQ*@$Y3=a>_BB|fZl5F=0$JSJk*zdg+`w%lOY%E1r&gc7{{CD5iwaOUy&%I3U~rM)C3VjG?UO}&E3$M+k9M5qaS`Qvqs+OdkDv8 z9c-Hmtt!?kwE3&RPP=v>qrnTS1mVV+vV5dmBvv{fkQyH0b_@P!p8>98v}{w&0b5J_ ztWJ`dcz_&a8Pe0s7xuZ2=m&PJ&YDH(so*&w9>G#5-Z=nyqF!{Z1aUNkL6R&UXL-n9 z{@xhVX_C2%q>5uu7c7MiL^P+oH!pJ3Myn99F?Fh^;rgP>GRD-G)n?B-W*&FVl~j|O zJq~z46XH+5m~}ZDlO%lQe@LUTO5o3=uUt)v&6p(`nX?V08`f-eFPnyjES(0O1THf{ zs`P0qS3!(iuDd|^Dv&u@_;!mL$OsaW<)CM)P>3v)efT*MuxYejKV?rzxB>We6|`ax zu?LeymAGVF-ck&c#XA=ivPw$74A+u^^!exRN<#L$jLaudCMXs5Q{ZIjA|e5$_VZbC zT>W!DfJqu8{{4l>?jmnp+r}qR`P4yFV-*pG*oO+nuEN<(O}>b+J#O<)Z!SzHr}t{i zKn0Su#k_wQ4`R3P#z@9jtM!d6%Zwraw+lGdY zfdq?lWo%u>Ex14h2Wa;0&TlEP3KJ4>6bNdPBO)7BU%D!cUNdCA$@xQ&dP0=)G0`^c z2EMWUd%!$tz+00j#`ao=qJV))1fni19B};Eqp<)2t#`Fg3?O3I7P=4 z>Lt?j!hMf43K9&2vt3~;7u{Tk3ZN)jQnt(F(=uy8i!tD32}Aw&2k=B`cJWLb?VgM; z*Capes^v>sqmxXb!@#B(O2Gk{Od#2oK0G-UqxOLFOGMfvaFn+X_i41)U2lhO9opjy zChV~mQQw$5`%-SvqYu?f>m#Nc~*8QWs^7Toh{ z4{IXBX-B=4Amt#^B|fG`pky&k87gZbwUfqXbRXWf)fU(9E{6kfFn4_7zm(0~mu=L0 zaNB^YU|R0W;a%7&r?$Jt^w6B%?a+kR;4+F;w)e(<3aJAW(bqSqvVQ2-pc~MS!W?6-$pE#Ri4mimiw6Ps(X$QtimhGohVAq6&^nuD* z=$Szl6wDJ*MPl~M;^VCunD_P=uLmjH^8-F~gZ4fy!MRa!T|H=qZEYAd8RVJu8{D|I zT4t!7r`U!s3roZTGx7K#ejEgdVfrv?wjoeg0W6#D^dB0aR_=`a-}~2f23#(=5An(` zD5@AZN*o^T%plzM6UI~H7hbo`egri_b5U-vH49LQMCG_%Xm|J<`!yoNd+bEV@aeRf zNJ7pbo>Y^QTD@?b#D5`8FB}B19=zOz%uB#lyYdBJbBM$KO0!^ZPim<(%LSQUEX3ui z^lAH3M#8(@m04v0dZ|h*VkWacjL=k!m*4F|6J}jZVN!^P;4~9V&g!~{%Gds4OfYZn zLJN~>3zOUZyE|i%uVXDguui`{E6l>@<8U?irnaX(xFnAA9bh3C6F&9qO}{sb<#cNyyT23gd(L>N_Zo~pkH0SKHzcUp7k z=I=GS9|jJMhgspe&vG0kx4v|JMCy#*V%yN`7|t_m%+Z!=bfBc!d0$WUkUi3l#6uMu zE9Xkez+c6;8NnE3`=sFeB#;bKH*Wuva zONZBR7LB>8P~E01SqMcwSXJ^!%Y^n>rD}rbaVCXkEChe*;S1nPk&~v7NW%cfK0AYq z*u~tAJKv}KYDbeiErZzekdMkVBOxKOItG22U8#iyq+yL~K&(v&R+j)o|}-Oxj0 z!RK#dmo}4^xm@!piZ#biNDwl~4xJ94gY*ubuOY#m^2~pI>3?+!vq)qi%pzPo>-A##Yh?4IDWyjD;3(*WBlSKpQDmsn$M_$F7o{m$ zDA2sl_OW;1uOt6H0x_yo!!cgv>c(IC%~+WoPsP*iuHVuIl)0vnZOZHR&c-~(b*jgX z$GM|20?H!N9O7Gka)ypvlp0m;pe*hD8ve?$Z~SexEJ&2>GqwqZb}jweJ}_H$9!Ymw zomC3i5*?toH|N_Ask-!ij(;mS1V{#x^-W-%f|0-f8Syz$xo|=ZKY^r3W zOWbhfQ$$WX`O7gm<+N`{@^&2;7TeS>WKrg_uP^L2I=3JGCZg5h7Jv#5JP6m z^YR;m?UN;vZ_H$&%@nWfa2Qdi8y0zk0)m>EQsjsY!E5x{^2$Qd=w|H@OWDHNwo*``16i^ zNKZi4e|02*RnHYS=)s>6IC!M7S7a5-7`1*AghJ7QTYw@MmO!#>(rl#DnZ2#Pc-J5M zzvE-L9NnIQQQW;9ZmmT3Gp6QejLe~R`BKc#bJoq0iQtVUWjor^HC*YsI|sVx8BXFq zP9!Q z2}Krg10Ebse!Ngk?X@6^gx!HM`G*~-gqA9H37n=fxP~^(X=pRmj44yM4wr4n=A0>0 z5djJ)-$I&V`p`RFJ7n)ohQr?!u+NhgAh~IjP?B0Upj69PxmtYKDfT`dpR6qE2mPH7 z?hO8vSmd$dFiE*0od)E4{u-q!?bPJS1tN#^LcMHNTlm}unA(XG;JrfUL8|SC5nUu| z_3KRe+(?jcWNDJaIS}dRT5Ak0=W`kAMES?S9$_-mOn41vcd%~c&8G^l4dk4aUN?aV zmM{fu>z?lL(`+|O?)fN*BK34mM;cV=PA9M=D4t9JPI;4si1CIZ)Ga#lHo^||Bd#I} zo3iqw?s`R3-bN{pS0w-dY1u*OR$Qb&il#JJw)_|c?j3S7E$s4rezQ60Si1#}8VbaFZugfP! z8{(})1dZJpB>8gC>RIzNE$q~(=?}~5HnVRANqOqAwDF}t!(nLC8;6yzXm+v?aM~1| zxSyi;@wBoi(Islt&QrW?ZPNir=(ufn>Fozz&NNW1H$Anv|itKtg z>5U#W73ZFt92_u{duEa%{G~KuNeWRYDkpP{u}D>KE9E@Z&OtQ+aakEfof&18CBmVN zE5{cTrRd=W@%x0sW2aEp1NE1&w8t%eck8Z$9$1=q)Q)E?FlTKwIbqiLzIw+yHh(-F z+Px`4+Uyd9w0U9wLeTH|(u>c>eP3JS67k^8tm)@?S5KQfsTqXZ#)9@2kH%`nNbm2e zp^d}mcYxmcWdpAEf*|$HyuL$<`2qdg4T{uQgWrY#04SvZ0D%AhN$g~2sON0r_@6x9 z!nBCpXnXAR8#2GK??kBOUUkhxSh~Pv=4`C$X4m6ZrD#6GC)Z z%^5r5=K^P(-{0?~Q*?50xJ@V)bQHXgjEo#3mY9{b#gIb`>w6Ejq3BvCq6CT1{GyG?lF@xgHYZAm@@h9&_=ClQ*gv*43{%QiL8EX?NqQAJ*3p}I` zb}81GtC=E9yFyBc6T698UXyI^2dI#g%x6loW+mgpq2zPKH5h95V-X%08|uw+R^uf* zoE<|;B%z9fQ(Ml${7stGX4X2bse=6tJUd>8Kv52hkS{5yiXUzj^Ic*b%PxTER|Y9q zziymU^&5eiaTaFG4Z9l3I+b!zfx$!*g?8cQkh52$@d3o?=}hb{D)=qs z*iss`j-~;GH+9UH+sd+O0>AV8}2CE{U31ZlcmzvD|0#f248WJ}q6l`?g@qk>@ z8o^9|qNq-*3NM7_(5~K`&!CK9=%fF0exiER|6mS4&nj@_MqlI9IEEj<#?1iW_)CaG zRr{xB>Ne)iNWa8&u?PZ{}%irw7HCH zNRn;*+jxf)^?|i!fC+p|bBugi&i<@sm!?8jVe)NQvcIkTnI)CtQd}dIpn9b;n{wp$&2@I{ zsQfJc_)?hzU=7o>C{8>n?w>5_%e#!x!d(5iucQMXsr9atCvqz=y<*AF77tqO&WL0M zMoX_9_xQj>0Sx`)Ff`RT$3vl>qfc|JhF>D{g>s^9m0H%&enNV1ru&d3QD`Vg3ON&l z^*j+R%SI6vhePI0g9STEo_FsC7?vz?C(9XBlzRtClvLL1K$%%B2wO?`AQ)gJk8R`VhL&!u-0-q~SD~--)38jwLos7YDr1FA3qTmu6F1FtR z*QN-6tMK6cm-(t>`Jl#=PB(Go;WgFPzBqXAIqV80ypJ}_(P6#DsaDkVRJawcvziOE z8%7zs$_j~V(uoq&*~pWdoy%^sJHV^KJ!>_gXgxZYa}5Sn09`L^gVG9MN$Qztna5*k ztWetY(v*|Ty6Uf%Ws*E4X3c@NMti7Bw!3M`1j9#?YjnXJ>rkpv=+^)v;(B=^0_*TN z>f^G-_-K4GuPQ>U(u6`lWtTRP9cXt2tiNj9*D_VAwXs^(xB@zQhH=oYg5Mo|Z|^mm zhF%}8qn{PgTZQf-eb6k{MsCt;xcB8wT}#|S4G2T&vJ!%y(53!wyPE{6b(?G|L?`AI zCehmGZK=RxsKYg;D_n}kj~I*FW2n@vD&-923|eK`=g8@~yEywKl%sxiMqiJ=7_<7< zQ8vYIW|MYxPS2lbmuvOg!5^vX93{+*`Lcg?ih`{g#$+aA5w?1|9&7s)4Eu^c5f9Xh zIgYbG3F14)LME`pZJPj#CD5C|5GBHPrqt6$j{m}*!+yu)K-5y+JG5RZd_d|VJ8kZ$ zp6%23aSz-BRj>deJ-d8?0qUuSOhDAY#uRQcBMx*s$$kX_5`6T5>?^=Zk1{FgV@9LZ z>><$rxluFM-KCfe=#$;r61+E$<(HMDMe#-vlo{xf^PKNB{>_gfF_`~JE{6`1$@?U z5~6*7bI)#}Dbzp#jQ}e5&|t=hM-hd{d_rt*x1KX-a?bmaN-3_|Gs0uK&_YdiUJ|ax z3?pBpR6`CpfoQ{YOC|x234cySkY>G zysyVB70UQ7ltG5c&S24a9ZOZeSc4YEWm<^Gjo?yxt@e?mpPpSbRL*1zoJ_O}D$aM} z_vOrV?K4D9I9V4xouLvbTT3Qb(P%FHi~F1xbR306hJdG?*4G&R`oX--&(?fJ>Tz~F88vW(K zJ*`ukKR@olv*Flb^>uW%EV6Hqe`~?+#YLC=p8}8QpWDHI%4+`c zTAb`$9F2YoY3x0wRJ0=ZS&@C8YuOKmWHVft401sgP1mwfQ)rNn7=WD&akwlGi3r9F z=JoW5_5N;l1S_6!L)SlKUvs%lnKm9o(q^|NoEx)dl1M1%pTn)Af>`5!R=`2miW!5A z0`?RvK=TomN+=|)XH`|f^#uCj@)~q&`b7aq33{T{wjxzHL!72tK%CY#aN|?Fge+Ug zx=ARIYanu+mGNOv!vXJfFsUC`^7|2=7ahUgCHYO}_FDud@JN;r1e8xK<|%Kz;#COx z-WtoWPi%-D5U&F`;`(fOWc%h5QP^M_=8}e6N44Mrds3rtiqT3?R$`6j_foQ`HQ3Tz zq3Gx|>r$S|i7Yvu7Q?o~SVug_OkDVBo94SoYd5c)yc&A;S#jT>eY}(MNfbaru*EX^!K)vWJfb3N~R`V z8&&+M?)R?IcCkUGrnH;Xi&j`3YUeTk0ATfy67<`L2ZBd< zg*A$&<1QSq>~a)j)RNccNJ1z$*SLhDrAHJ?dA*A)p&t6hTs**(zq}@YjaD#fkTY&O zaai^t+%dO~vTH#=YkBPGL?eN&aoAoq4(M31W$;0V+mJb#kdML;Lz=kDVY?y}1V@1- z1f@;s>1iUYsPevwI0Rh3&m@|)&UlC{#o+tRvot9sR5Xbl9RX}vm` zlI$wK)?v3HLzyv<%!<;0nsqJ-HR0Cm)qQ+yvrT{6pV+u+1B8s>(b<5x_E#4+F?Zp) z&M{u#%Uv70O{~YMH^VAc<4+6gEdk!A%fP;iU{Lcin<}A;K(BijvmD&%nQE!I!$!X3 z`IGC{#t4(x!0**mm0H9VU7@eMA#!D;L=icWV>MMuG?ZX!I*<`CTgq9CK2U9|QPca6 z3|n82tr7#hlFl1S=P%UTT~Jg;(9AaKI6GUTJKNfKcdo_f0fmJ`kmTeDDZpp?`gSuA@eV)J5N&pZzk@wu@=k0?wL6;(4}= z8?N7a|BpK{LzZJV`BU3wkM>`sZT~E0XA>u9Jtt=aXA3813!{G)b+x)w>^>`E&$}9O z7uW=D^Iz5mG^3`p3u#b+*-YTEO=zLK;szOJ5lG_0CWJ{py|^TD#S|~nSYBr9hO^4& zz?_+5JmK=Dq7UZW8MDR${Ayk(4dl3izhVwYH=n`wAVF$3L<#`|V*O~11>_=5$q&xU zF%Lf9D6HpGtttG5BFzmeodA~c{f7LK2Df~gIS`EQgo+S7t1}S+bBOJNV4pfd<4paD zT!AtztT$*$5s+8nxQ%v=VosV!<1G2uUQmL`^`uGd_hzodAK9X7X^mCYmC%*T@Dwu6 z=S{laf1nuW7DpcqS-B(?0()4Ka(^#J$6@l$Dwf^|C(G^~iz@X&WKMNVM3^fw79dxA zg|)gkffW2^!_i$9N)Es~El)o9Q`f2ZSKd6S`OLx>>H9ONLG~REPog}y%k=;z-Z<}b z$TxG!^cs+d@!P}ndthM!E?53^hj~ufLX&jj5*k3CR}$LkXlYHe(@j#Uc5GV_x$6&< zV|5FE`&DdC{al}bTwi9Cny{p!%dzaIO>LXe@J{ot2vL0z>ADs>qkyxs1dF7hHl8F; z-h6Pp;VA)JU_1^*17&{J*$0sUq*B?9xMs=yBcuMYU+~tQntMGiVH8t%MC~hdDg)}qX?cUDU$r|<_HX+9MOtePr>c&s_hg(q7eSf z8^RvMbOfHPZRr3oNFv?4#fj&PG_T!rPHeL%GLt_prCPDAcwakU?WVrV>b>Jt;kTZ;ch$^Ju&SZ*^ql#UW90ya%bFS7F;3ohNY-|;!pJzg-WD{Zy@*c zaus=%$@l7%&0QYIA|yf^nQ#X$3uI;k+zEVentcVg>w^Wb(W_To6|0#|lYP zdA~-q+B-DiFLv!s&H!}2nDpej!^t-1BHQW>Epr%U_g+wNg;*BkY^@sp6_)CU?5~tI zTuL!-H73Ge+gemHn`Xgk5sXb`N7)~<)KmDiFM|LFd*}UbKioc>j^;`?ayi%cwCGs% zaX-9jj2p5t=ujemY8BYtIdhT9HQ8CM!?eaqIn8TG7WCV9nIjNngKkVSAU|_lWgzN^riw2Xd~+8Ud{X;Z zM)cL;m1BE`49sBGn2=2yT#3Tno(p}1bf}zdzO|rOw!aZsl`4>P=F@tfvcv8*zGt(?7x7d*n&`RV#G;a zULAkzR4EFz#fF}8@s9kP>7Kxq+hPMhv0}pjt%S;#iI?#%8i@T2dRZSh!DI^wb%UL6 z#e<;UIc3!&9qm*N>BjYcHeH)$UO%a8bApNlM<3pFbs}4;;gvt*lHxF}MdUve(qGN+ z$`=;3OC#k1e4RFr@KHT4ZLcVN(^CVO)YBWbj zJC0i*dXXshIknUjD-8Nngb^3t0rTxG7&Cexfk$TUAf40d^k#%;?{DlHu;u~)WMyY> z{aJ7EM^R|H<&@93i5iXaDH<-!FY+W}1Zj_Z*>vJIex8X=r`gF)#dog{Zb}(WjHFo{ zU&ZSD9uFIf>q;;bQLWPVgu+nLY*4Y0t|nZKz${(rHqOW(gZP^RG%;mo?Jd_9v-hrP z!St+5Q$EL8$gy8mXQ{$!E7wl41|K~ogXZh`9-SViJ(_@gK{WNH-|0|TO{x5IJ;L{v zk2q?OffjJ0@h*bcQ$jDYqwcP!fxpz?g8OX9k(nvJiy4kBt`O#B2KQ|yZ3ScVHODpH z3?cqqP+pSWNCH}F5B)bpMAS(UjnmrGy>Jb7={~6cW8KsWNmtPX{FDItIf>JY)3IV zB4pb#1+e5isy|~(0d`Q+G&CF2mdz2TO_^!Yscr9fz?Ax~I~U$8;Ih}yu^{Njx2y|| zhumOvtg!AMax##?@p>G=TL<%yfhU^5w|G+8K~GF`ws+uVoK+Wmr2{f8=Rf;%ElELU z1^Pu|z8ZoHsV?nAdnO#(rT&zjv*?~uAncu=%-ff%b zO6XQ^CGx`-lVC(i4kZ{A`Z`LTElUyopCNbgZD`KJ&X}2LV+1m&+zCeaeI@?JW5Jr zh_EMPOb|=gu%|mACfkb#9AHsX0xK^Z=~=*$r7fC54g6E;BUq^B!q!4i-!Z!V;S%j8 zCqdXy>sWpp*)jT+NarY2W*`r>7z|NNnYUEpWZ)rdIVn1WwCAnfXv9CHHZw=dMn+XH z*}*=I0LKsbHm(f%)}+&3InLRl&dxB!+_gLyNX=@+X8$#<4*h1V+k+hfhPjs8pqiD$nlapWQcgLw9W<) zk}q<0pYmaiusN|#mK_dm708# z$9c3CC`T$$cCM7`k3DldREioSgxne2>aJ@}-HI6ne#OD_t$jet0298`Iz< zb*$AGd)XCh!N<8WHNS6_(^@pL=R>|!gyb6Y#s}{-!!=8M=g=#EOG&ybeHuC@Dc0zS zw&nBaWT1~Olbo_DB^MEM%}(-zguk-F@F%Kola%jf+-fdbKDBd>^rp&ZyNy`m66!dB ztuqLvN!`Xfw!s=SvYR!HmXJzn`w2mcn6>P(>9Vz!aLnQZN>Hs?L`^^MF8l8t!7#Vo z+!{fw$WU9PehxisntS7>+mS!d)S4kk&nP5cogZ4y2UQT42hE4ikx8uUv#2xnHYz0f z6j}c6AM(ZjbBN2J30HF&I0T5M`UCf@>;i7_QoGoCOalR2$l4$C1yyTgM_A7{{;=I; z8km|Lf)M|OxFRfLcjf3q?QRJ*pKOsSM#%>Rl=%gIN|`u80-yKcVdYgy`keuRlpGMS zv>_<^{W13OUqGBjk2dVEHGf#NFB@}b5U@w*jkQg~%_F{epzhmfMsAX8f=c@}U{@u# z%gYsx)<4MTmw!Eoz|ErBl{Uj`1m>b2_v;%sP+(cx;{gC)$6PhG=>*z6C_AplY+Mc$ z!|%Fw z_Z31Af%EH+xK$bVywS7H_FH#sGIcwoi6k(I3&&`9!{^QNv#vWBB;Mq>4W; zvG{hHrEf<$dI8E0_*-1cDrC+=$2FlwcmPDWGGihp3HKOt!9w5mYQ!ug*jwYN6AU|? zQXXmBGZaPYdtz|NHkxeoMjdi!$I|O+)9fv+zE7C!?7KxUO?KCn_S<iVak**3ddkX~bY?5m8&V!vHShvy|VJlVF#R z$%STm&VSh?E^-8&7Dnt^H*cD8YVOm0>#1aRp>%p=d8f zKib0uxIK~Ky1IdTeSE!wMVFV`1V<7?c#(?G6Dx7==05PN$=3NoC(nF%8-#rLe$B2k z2xsszi?~tnoAU92MLTT=B`~s%2@S2*?dcf#WLm<@Wa;Vbx^+=tk}@mBApdODFnL30 z;3f@hj=BDDiv=LW5BDO!S3QK38DH0|>Vgh`4yB}?9MfUd44BebA!l0GA zO3AxOv8+hH7ur%)WRoKiH}{2 zhIe04Y)`R^Q9h~J9!i`Cim=(t1B>miA50u(3MrZaq&p_Hi#d}EnRqb5q@bXtG96&i zS-CTKQNsbcu;j29JmomkbGr9xv>ZX^=o}wF&Y2NKEYCpTlUl*WFTr>vjWz0#ESr|_ zSe)s(Kpi;2m>Xw5ciUM(Ltk&uS%iMwwksYOMP}aZ@AFyc+P_WuON-TtpDM>U9mstq zz-n~JqiYyzoE@v_<}nUI=yHqKCwb94ZwPrj3U<+UnckFX!$)5C^h(;=v_v-;de2f^ z-Y}<50PJB!c?IwBNu+Z)lZT*Mw-R>O!82}d&rZv8!y3opE^ot^yaQ;_tliA-@$^x6 z8y0ZwX~6^UN$!)ska!7Dk?6np{up#6UI80+?QkQ8w^t{l!hsnoTEkKeb4(52Ej>5Dw_6mQy4=?vWzb{+(VPQ>9T)e&%0-OT5dBxDY>i%$oGO88Z$ z3(L00Ey`aB*hZa{_lmX9gY;;Li*`ALGlBMUO%^b{4#WuQIt9sZ5*U7miG?y`6I)19 zMX!MQlyn<*4b0}uHyp2S%0ze#Gdb8@m9#6}A^4hfm|TeFID15^c3>PvSS-^yt3`RO z{jxP?%>Ih6fl<<|BB8(uTHsD)267Xby#&QIsF<CT* zO$z39;v^@#(^mN0H8R+)&e_*qGxW4=#sT{bnMmPbI9;jKVdb(pMh5!>F*wz$n-apv zm1nS+ehr8FwR<-rfqv8@35SA7Q*5nQ-TqFtw}}tz^_Lo~1f8wbSZO_2R4s}skvIcN zOJ|R27%3xt9eJcpUt5K^8204ml3^A)Pi3N6XUii0?cH_hrY?6M zv>EgKu&O!CA{>lO`&#;u-DY)))7e+1!}gHosJ2F$aihUP)B1vz_)TM$v%zY`xk{^% zpMj7h|3D~ejyqo zr()A$W{0m}%Y>oLW`p#n#2))8qsJWD#F{hsQL$j6J z+qBNm)~rL2>*C9$&nESsL5gQJ4N=;p)o;E?x_B>&cZTV247qP*ch6lYSOvFdlh_+T zy?5-MKU}(>QIp*Y@ZK+17_gMSyag{|L_Z}4>~ktW_wsFY5SU(|OTL!FVvHuj2`8 zGKqw_D*x1bU%Ey6cJ^dOBSYD;k!^w%wG^>%oE;wS7Ul}s!=(BEv3q~;{rk*VSzA*2 z`BQ8v{c~>ou}%MT0pjdvVPLIiW@2mNXy9!3pSPoiRHb4!SmAsA^QP;iDgkR`7OH(5 z^F|Z+JfZmff+4}|!Ki5}CZxCZUq8JY0;z^>3C}ZJ)MfjleKtE<)JQF&tkwwE$f1IY zj1aJ1wiXM0pb0lqxtk3lGO#S-uKj8FuEVCP4AHbN{UzJ$#lq?+0qJz|hZMUT{& zYf||M%oHL;mRlqTW5Z^7QdNKE06Z@$e!VeyzsIxDp-~=iGY%0#%HuDwe=-z{Q)OIPc`|>(}+j> zs(p5o!&wX?xP}kFjsH`6uxXt!vH*s-i;*D))>=!YngxfRyrYk*`XfKRJorgi3f*#v zR=sD59bcGnDkpK#d@Ij=g6ErPA|ivS50abGn?B;Bl(@3uY-y0l^+Tul`$l3;)@b?D z8fB&T|FQLsL7oK9zW3PLv27bWwr$(CZQJ(Dj(=m@wrv|by!(IR#&gd(ab9$ENAiGr}I7#`=Uy{T45TE z=!E@_({(*WboX@c>am?op(WUAhou^Vt2QW`+;zkyjz#l6KD$G6m^nkBmspbaB;tDd z!7QY;Nkrh$o{~Y|Xhwjg;pFh(OqReyIvn&{#1WoNyD)T^fFePc@$#>`=I!=heBZL? zA$0;HZUtI|hlQ&8itI;XkMU>0r6?h*V3BSmq%y6lA;>U=Yl9@y1M#w7+}|xt!Ll(y z$E32Oq%AxayBZs%V+}c1&M0O1~Qbx4sd!sAyw@^;U-{&I3@jX+jR2@igE}@h}<_^#B7#$>v#ek-2 zaxIriVPivo=6UbAjf{Dm(RE04Wegv|)j;OIo*|(^)PdnjwH)L!^lV*@ za%<4K0**;FEl)Y&dIn+7&OCe+qF)yG#pMn1CcSZV<*pRR+VJnI7I$Mp|Ku>Gjm3BQ^j(9F>FBU(C8*`ofmNd>n2+X44 zC^0>n2xi-6Ng?pkfB!rJye}9tGrdjDA?o<{NRmZ~f$dt5c;L!3pfE@8dnO&Qh*MU_ zcZB+RHM^a?is6snK1^a~!?hxBko71*|M>Q-Q|_R6HC3^3hid0nE;# z^vGzObo!7>gUE&-FRq`-)mMw2xvyk_6v1^;pA0h(aiFCIZ2@NlEbVswO)Hf@$6|=yayc~V-5$ssPA4W!dI*FJjU^o-oGMXJ%kqu_2 zB?-OdbG#mC2;9q5TH3ZAx6rhQ1rsaxbQ)y}8M3>DPiyXs>@QT>jv8me^uee-XE)x) z7y}E3?m}cd?y>(8KO7g?i=%>eJ$=EPZ>Wco$Ue1%@H~ydV@Onm9NLZ*>!B%WDzIew z3?dU;Ay%vy$E`ri#;i&kk07->Ser0Ks^BzF&uV7)Yw#)g!<8Qd2s3_^U&>L)l$8^a zSvJjys!u|J_EhFA7H|0r4MubSObh58!&J#waJ{{S71{5H z9!=eXdZh|QOff7_C-+Ee^nlHuR5VZ%9vOps-YqR82S|dw73wf<4Zn<0MqtHhY6SQC zx6LhXhEZA%`N!@yNO~C%uA3hk@skWR9nQ~N&Uwo(xYVVsI}~J~tzrP$z=my+bUcrKoPtq}f-~Mg!-%nGHOJQ&6ngoI>rX;p>TVsTeYTOzgcAKKYO(``a z7*(VPa8%_{|5iP$HXwSM%#G77U<%77QiOI!6?-$i%sWeWBdRddBE&;i1Gh|PWUgT$5R zuO#)yJIY-aqaqBj0->SU7nrN_ud0Rks0+7Cb!{LOhXtJ3c0p8b9P^*d?qbQzk}?^1 z$8_-HeTm$qq5ybD^*@hhtWsOU7+t1qop1igfWxxuEV2Ftt}!`saJ*xYup zBmM(5#)$OEzfDD54Q7I5tOh`o>lG*rX9KV4)0wswl$_^zOHJ!wt_Em65SS}b%^*^$ zMQ4Wb6A~`@QeBApUG)>q8vs)8nKn7 z;hmcus#40m`lA+RC!(94c$_6;kO{=?+^sj{NMdWO$=B}#>ux) zjJY}PRyJVXh&|lWfhoGCH&&v(inJ9P#adHY+f6)2c?ZRGbaKd%;w1ybA2!qJJVPf-BC#69j4Gd(lY29lP;dR2+U`nj(~3ogmg*P#D>Q9OT7ut9t8BiF z#5l8m*Hu(UGuSMo3g-Mf$&~qHNS?dljEn_^L)%};(y2HUKMx}#eGHjWuGO$eIFcHG zO2bQY0f2S1TC=K&*ZLKb0NGUZ(PIus5R|9LQw+7hw1&Bf!;pxD-x86Rfv1QgEC)}0 zyN0)j{AhxqUIFJK$;s9LK9NBLgQx>idF4EM=ep_e*uA!Ya<45aSW@Bq zwht7=Q!qJw<+DK`-NXe$4Ti<8*ujy;%O<37PZhOgOWSCkA?$lt+@zrnHgHxfs!7B- z!L#Jp_*FG6K*E`<>T>-@)NF&2{v{Zij$ku1eJS3Z!3JN-`VRYK8`MsL>DANj8_WD5rwha`3UbaUeXmy7p6~>L|Dh=GTbcLjhaYRoyj9kLHlE3 z+8P8~oi_rRHiRhT!lG@KJ-I@{XbIl*GzsgaE*vcLQnAlte-_smtTis$6hNh!BrrZR z&jU?A25sIG<0}-Lox2HmNNPEXC2Vk~Oyzhqu1d_=rRDQ-m*2373Mcij62Bq?vvX&s zEKG)adaX#n>ug>Rz^5E*s)ikOHp>t@v}qyJS`$JXUZ-2jHF~+Ub$5T_S82<>RsdfR zmY5uEJG?xMQALa5fhomj?pD5eT=!|4_~{^SJ!ZRAm+vIamAB6V7mhVr2=RS1*&I2s zs@KcEpK0q}v57FO*!8H}?pb%C&-mGO-?`-$yH|Q- zUg+vtQ5>kqX#hEX+z*-MDCP(h?lIPxTq{83DEMo7hb-gi;}`4`d-`#D0W9 zJ&VJ5I+w!m&s7jx$(R%%C`OOkVu5I$P(2Y8`!DZm(lSel`o@7*Ndb0544q)EOk?ql%KMkHVP-awUf%?^8}7F zn##0g@|TJjij|!2i!3F4dHTFV3a%xEyKP z#YzDt$}aUMaFwpF;kmjHiGP4VE^P$letzgrYo^oQz1=xIa*7lm(O zRtdo7M`a!_q=+4v;OTppv{fKT3fJgWm%hZ}8hrQNsOKY& zulMV9lOjkJ*W@paJwJTlgmegkv`gnF2v=gLw6}G~wyfal8Vnz_idJ-{7U+yZbMmUj zG|xaown=*FvhAwOc7@g`CY5nh-{oV9ru`CCv-Zcds?)LU z9s!4x5kdLSZ-MD3c$Y#HcM{?zq4Imu2YR$cW6(={Tk)c(x+dgBhNy#7sG~$Y0;NS@ z_#(*o)Zq#bX_V!OA|@d^BO=ZW%wjqHG<-b$YVgVZB;fe+wI;Y{9wg z@3Vsw1QA4K@0G2ELTv8te>jP5LC)Pk0$Zg}ijG#h!0< z&TE4ckBDx=e)%TMTgW5vP-8`#(nro+-#fIpobuyYt>yy{{5UOdCAqxQT;j9&`2o>M z_NdZN9_zKlqW1#0ct<@=w-#czlv&)y(7(*&4`j9)5S3S(k;ZRl4N>=cZ!tSm`GpSd zA0D*qClDO4@sx5uUe>SJX8g;tW|tB`&eah~Odakb%c(@W7v*d0GtaW9d{P;Z$2aMA zoch9QTqVc3MDp_-sq!r(S h+jHms`KT?(8u^ATse7G5#H2?PJ^NoaE0n&L6xSXk zgEI^yu+xzO7Pk%d&A`mlQj-#1fbo>xNJUK&)l>o2rN@T9} zP?C2;4$356L}2BZ?S9>^5R!N&c%qk;@jK~tXO7QmoT%f3q3QVItu}&D=%wBA=oUdx zFCS6FR}Ep@J19ehF`nQQF8smIRTzOHv~wPg5Mi%CG3PzyhZJ{|vz*O8riqLQ{;2os zL3!@mIfaoyP^i;WTb0PJs^|%IC*FzdjNRk9Er%+|?2H;i7cjDQ_U0(9D=68t(dZ?S zG}XT>l|S8Pcs?Y5nbJsR^AA+ekqHU=1Et%$;uZHaz9F>}P>4wrbcjO=mf9W^TlpRC z_@WbfpaO@Y>>yGH6O_N2GV5)IyEmbT8MKhPfYrDg85G(eqvbOjJgkrq12(B3ju#gZ zs!K@T9jRQl-p1T(WqM7qUkoonOe>22L$tG z=x(1b(W*rL#&=|L-a48Yc4otM{%*Gxf&ch8?G+3x;_TGz)NU0or*jQGoNajvJjLTc zt;6;HL8*81`vf;4`fd&E`SoY++4k$jI=K1UvF`qMI0OCF-p5}^2E97i%J;oY-Tn8$ zboATf+e1i(nw8MR_p`0q`i+>tL}mT(VxxUWr+O-}VIdYM=HgA7UPKP>GfJ@cMOi6( zF#(a^5eqwv40>HG9zY)VMl4`Kk>s<{EYFpzc|dwOTu^T;wpu-Wj)4mg|L(Hc9gQyT zQ{!|B?<(1RqUYzlj#IxzvwRd{e0n;RT~2y;!33F{2zUz#CU|BdrcJCh_h&ZU8LmVw z7wfqHl2JFzrAg31o4pJ4M~-PfvIwnk30MSdMdBt&Kj3&GB#rxGGg^$t5xTsRn5LY3J1|ZPX}hU)b+V z98Q{)#cvPN(0ggHX_Luz3*&*O^D^Bd_AGaG)BFuofVIbYLzI9&O3_LjYvM+8Lr!la ziW7Ni!BUTA`;@mK#cI@E<3PgKh4vTZ2shyaMCJ<*^`-yZm$3mc>{ZZ@z~r@mi%-J> z0+`^7;sLQju4%pyy`X!Gsn4&TV1wH0yWaV zFKSJ}9|i@bUs;i&C51TfcVYA*@?O}eU+;}?{^?G9tu_eIo&C4f_Z$#{;ep2C?L9kC z7dQwo65U(`{+TS{h<$1Xjin46P9)I~5qX5?s^}QRx~q?gbA&e}-y1$d($Q(cg9IwPXBUiO0 zU5g`%SN~Lt%1LI&t{uXMC5vS;e#(n{2o#!F3|WZ4xCU_~SG>yp3O1V6-Z_qlxJO|3Ma+8_r^~WZ6ywN5 zJxe(Fxw3!cRk|A-fC>vo{iK*BKFxuDQSq7D%piGY$M}(RxZOB5@fdkUfWT+g_P~`;Mhg5+);Pt211x??V zPXgI^mt3n}WZSkhi2s)s{LeZVcj@$9_!mHW{#y&A|LZ#VM}GXD z_OHlfjVg%gyL+>>qQ#6(Z(E=WcRU6e5~ASQStkk*~qZaTPAJ9A|Of+0~@ zfD~b1XlPt{#GtWejUuvSDLQ8-DZ-k1{J)sp0p`-c(14+$t;=Fkfnaez?S zk3Z>O@=4#iGPc+xt1@FT#1Jh&7t<~+jHKEiyA3T!+kLYSqBQ*KSLw=+pZS*MhE=RZ z>b^v*MYGqFn|aJ^bqzR#o$VZ*L-fQAu#Qq_?W7Fv((lXk;3}c5B_4*C@P)@I6++|6 zPZh5&nRBf_N{kVg2lr1;{q26}VnvG>g>svUnC8}x*6HZo!_jzHV7Z1z?jA)ZA3Ggf zo)u(AeX~U_)sIlZ5gWn$@kv(vrp_b=Zc?eUZD_=|2|l3(Y|@->;; zD^uYsS9Z2;btcCJQt97Ug`8*D0pb*CYthQmS7nzOe@-Jqyz?yYB1OMoCyErx!y*&P zk5TIy^nEg{fH*7Y5K$6EAEK;)DcOtVLC0)~--9acIkUOhUC)oNalw0#`K47xfj|F? zR_;GGA>TTkD?tPVG@=XyMEAeg#D6Dh{tx50_R)60Sxq%?FYcW82qu*PAruU<7eb|q zEYeG%T}OUyInzi>#eBhrEzCxxL?`}1|Qd<}N5E%ar`?{n ztslPgPe_6!imVvk`zuw0M0Vb+H<{D}$3DKT$gUTs^pcovUd^9I5=t$KQ|j(n9lA%* zK|U5)wU^*x;`YJcU%Nf_kh-4(##Z(C`uL%&3T&6QNj=ruT!ydQ1~WPwS&|7iTZUVH zXm+$KoB_fZ+MaR5TOJJMKI2t_F|7qZN`5wCfBaGM_HJeWS3bS1PR&QVpz6H}rwbpY zNEZX-un13Dt-s}O%-;+i`Uv468bWsnh_F|{o@FyislZa7BJyG1Y^+7 zv|+?Tcpefi9a_02gcM>!1u7pxVcakAkBQ1ae-ZeC@6o51fH&@-0u^E`X{ZE84$jB; zOHvm`aW-rePs&9YdnB;{={IKfy8b+jG{bxg-J5sxl&ljts{0)my z>>=SJ79$zr7!-x>m{QGYx&=8l>Wid#wy)R^9cB!up&&&fq=eW&0oIvv;ska`kX;#Y zl4E3F!oE@Az<@EqNiUIuNC}>d6x%=nM)YWk;Bg0@G$=3=-8&tLm0p*-IVvKo6uLkG z6-t8JzyKTgJZaKl14=Bkq|?K$YS_^vX0+@yO(x94L&fy~xkm$L`iXZ;0@UHX7^w0b zsA`R?FmZk!_CaHsy?)u~drLu?nTSoADbTR63SzBP$WLjXY|;pR5rRyP74@1H1bbbmi<$+% za=*v?XX7vNKmSJN=XyweK`zb4^0O4Z|4@FvKKV=AtK=Kyh^lK#Lf-CtZz{KzIe3lI zpFOciq-!h1G!Ff$Zf*azq9Sn`8@u6bCNr|J>#1C(z;}Gimt`c z*L_TV{d!ju^xXbo>N%^ru}j{xS}k~QORc!pX*lhsI*riJ%~lo&tqLo=!!7uTchU3a z*YikyeJkYa7un4(c&|(Cu`Sd}F4n;S+8Ja2iv4zNajoDJPu*Icv{^NAjrRWbyU;&q z0vUyL*L_)s>fSU`y2O9cI%JlDdqlZzSlLKIqI#9YM3)#hm5a1fJ}Qx<2!7lNc#I8< zTqzMsiW#_qXi)(&|4d8*LO@JztPE(32D-#^GE9KfM-hlgt0jOkctU_(N0oYL&?`$?G23Nix*33nB@j#5ZLsY-V(jNRX$n5}%NYXuC=@)vD&m zdJa3Jl4giR`>XlM#;fq-qzot9sXwTOfA-l}Q-&A2n>nlZ6S;VP=2TLBn!U{?s*+w2 zU3dcw=j7jZ+ayFq5L=$_D-N_d^NWBic@dz8T1L{Ju(udl9w;Mz2O}xUCIQ8)*5OQo zY~=U?*Thpsu0Lt^$!F>gbqby=62&&%&F3Gi|zX?5S-==PAg*l5^l7Qfg~KHH$t zd_~8rO$w~{PV{HF!ymM!+ti9xr<2v&GBDW&Z?Xk{d_}qb`IzH6-R9%* zUqgtAyys$*6h%MK*dz*1YE7yoEnG(IjPIBekbgoK1j+|tr)GBAxF?Ko)FqUiBhUWM z4!o{KqfebX9ZqDq8IMC0z3L3FS#T7P+t|?pH;tzUA&W7Kr!;?t$mGcR3I4Zzwc?$0 zBR$gAD#B3XLWwBEQF3)b(E+9s<_dv4v9MTIh1nd8u7XCGoyfK}{M#WHMpBRRzmZb= zCT~SZ${oj{3Pu$mvS49f++Cved?$;Mkb4Ylc|aL=i7$Q5N&(z>VUEL2~Ff3+C~ufz|2R#lxZH&>l~fpSxM8J(n=-BRyG zCU!I{Z0LA16c7e%Vgn|AHbz(I2EWm5s2D|rPw8cXvZ|(Uf4&G(&o)Z2XvO+M7@H|X ze6k56SlnHX|Muzd6Mbt_D#=DcNh|Wiu~4I9-|M7i8Y3<8V|%enPxr&0++i7(0EHh~ zpyy{mdv`O(A2uGsc9J=_ChbsVa^Lgl5#$rhUMJ!PGnSLD*wJPh%F)qIQEBHXn~Z9* zWaltvOZ!i)msq7-uv0y4?qqp&N5R7lRs5tKEQe3HHj7XBI>0(ca&?Od8M=YKQ1ro*CwIUUU@YSTOJp?V@ z3)x(gVci(A?PzP}Mn22c%S=7mtPn_U8Bb0X*4?^Aou1k1WxBR~fS&G8e; ztA{Nm$MS4J1i@K(Ib}LFLn*D;rdO9Pc zj;E}(XDI`7Ipb>icswk2ljcxqN75NQtGMGuSIzp0jd9Vwz}n%MnT{c?UsNSTR(6+= zn!&kFOJzNLzmru=$1iy(X53s28t1nj6kfaV#Jh@61Oy63{hZtcZGKKBI`V;M1r|Bf zfwMa;y;yLf19oIG@KjaTmw0F+-#W4ksghzKBB6U<+br=S(VZ`WID`$Dl-b^u{8$Pn zk#^AHqEHMXA%P1>G;4FS=~dLKS2;nu_5b0LIan zSG&3KNF&g)CZKD6J1=_%XXa;bX5m(wAQKy1%5N5u98(QqS z!b;%@7VSLzhZodp;^uEy)i*y~Z$El=Ki!>%R(d=M@~uC6;$dR6UAbEOI3%MnbmKv3 z!iDT>^SptChc&nB@jbi_f(`Ipxa!EuljombGz~B->il{6*8WKED6Q3! zrgiv0^M&&U0nwv)OH|-rioB zr*e<01i)VN;@jeEXAU%-)kg`CuQQ)WS3Y(%E*|%y^Liclud`O6u+nM95#}U5{cml1 zEqe`Ts&THiE8Mi4LGV)vx2vzix#|1$L+8)@UclWnVwnAHTl3+_)OIide!KJ0C?x-H zr`xf21jrlsqt{+?`255xb2XPGgiDg_?jZ$|XBsfJwL44W$LGdJbAN~TlXV>P{Dr3J z_c1kfBTD%}7(q5Je3qwzLnc9Q90HW4()(F`Dy%^AfUUnl?Yynmp!aSUY8NYc1j+;r zuWLU-UnyVEA1U-NDGi(OYqlRzYqp?Q9ZYVB)7NAy`wu{hjmEpYdcGy9)!BnMXGj~w z%V4Ev=&0ZSc@ z{!SIm9sujNuQ0lS!nD*Dq!&JwZn1Em;_4R)CXN@9(tD=YKk(# z1-0{0y2USNw@%day+WX9CZ{aI>=i`%Wh#tY`p9AQ#hzD|r$*q!#h{KAV(=*JwK|w2 z5v-C(wXQ6h#i>{|5b~~(R%~l|AcGHZjMUb~B$zn+5vf%Ixk*)7fj1ZI&1%aKMU~IB zvg?NSfpe1jX3TK*r;@E5b$YQq852=eptOc{dS=NzQe4@E6crA1i)y@kPr!e9nT5fy zQ@5}>>wv2~)l*5-+3Ossn9;P=af675Els?|X2i)8;i4oniNS^C<6VJ?CziHl#f23q_s}GO*As%q{uhzep6hU; zLqRu`5RxN%(K36`7!9&GJn=X@#g74OYp1;*r*Z7*R~ULaMCfCET^@GEA=NC8&@3-$ zz-LzhPr@_0Hczx*r@K0KSBco(FtNUx@4m+K^Bs2=pW~+Dtw(3{oL-GjA7R7`!s7NZ z^sR6bbfA-Tntxp7jOuS)R3-@ZD>Ov-ik=3>1jBZo6zd9E*{y&H=17oZvBZND7oOc=0%P+wR3~ zl8)9puCNk$NWf?1WZ>O5Wq0BAZ!(VXb&B|zw^+7zNo{#)+Y!yrUu_BTy4ZfwHkK2l z>0hrC-(PqzJ&Q=PUszAm8HNpVbqxLwfYHDBz!u}s{&s>e?tQjS&gU%`H{2rMHQVA%^ z83{w0%smU9>@bceiv0;4f51DX5S4I`*qcp+x2#dw-w%voFiQ9z z7%5?7rULsxgWX`6f`GRkqdf7PbNAmS73t$4GI|NF1EnRL@j~XNK z?DX67;~fg5fo-WaP+${|DW;|eFyz;}K$e|slAQ=!Kfo73qkFFr+)Es@9MY~|p&hcC zEp4WxlP9*I$gS;b>$#?lyGd;?M)8Qu+@!UQYrR-W+sLcRS7#G|b$7DbYeik(hT5LB zJ&)@0j~Im=Do0&15<~k5Bha3Bp+us?9$-f>2hsgn?)?U7#afZOn`rMn;86?&Cf+j6 z8kHQZnyPJ>(P8|}-imsop~=_5Ls5^elA#a_(YT2Y5hYX#RQm`IiH13%;;t$35ECV& z#I!IE`VD>y=t*)`@Z&qxEX44nh;O8`C@CQ!Qe>ztqG3@}(5?vc^ZEy%C*-~GZ(bNt zI&7KMt;QRE-%<mJ&x&`_B_#j!^g4I2Ak7n*~V;` zr?Jr*y!NGqxbPb9THM(v!WN7(UIK33cD(4y@EJUH1oQB^*qJ!=Gdu!oc71}M_l9!k zM9tryb1YuRL%85ET>|lbS9Y`AJxgzM-Io*N?PVa{(%_dK?BEVvywTigN{HPII$UfV zKE5t~O#;0NMN}h@iH)d-+BjU&OnkUpogm}L<>oO+q*dlI*1;yvF~16qihWj|Xhw?9J} zSkjSXxYK03C7c5mVU-$8eZ6X+#)AEIcWOcUM6tp8>V&#h#2PeUy&WfV`meJe{3Mhk zp&+`7y{8O{(=wMiP@|@i7G_$`pXPT#S#ByV%v)1W zsbQ1w-soO;$eKKF`;puf4?DR@-WYv1m3=pYy8aY*dIciwnH*y}#&TV`J*j%w-xg0h z7CpD%mB#*&0ZEV~`Gp*;wnM4XoirA&76ma%xkww*Ou4B=a+H5)6w%A5#QBM6Wl&I; zc?03DQhB@h_S6-Z)dXCZVBXL&oE#WRQWSLEbTumRQvqyJTq;o-od$bcfq4ZhTb|4? zBiQbF7I}xVD6IB+%G!a2J&h*lQ}Tg^s;lfsJ~;L$v|YOO0&fJf(CpLMVz~PLsrRd@ z7fwN?xG`D`tSU};8#Riy2tumrw`%vYLL|GiYfS_Cf%ER;~TM@j61=FA3?54oKquyz)4w3Wz_hM_yDm zjV7Bpr|gFrSibEWZEiV9!j6L0d$Ftv7l1elhDDrqk6r5tg>+ZSk|MKQ7nZ&QOE5_q zw2+={!C5Dw*)lBOCqGoHJ54UT#zs}#Ac}cTV zQ_4ysRVdSlK+wY9VrI+*IxnV9>F!mSf1QM~S`Jr*$uhlf)<6%h70OnE;&O;Um=K+nL98YB5uM4^f(PUDTK1cY$q ztC|fN*dXJ89EC0Gff!Mv3(%hej``V1SQd$ORG9PShJlU;i!d=7WZqpx8FfL6jItz% zia{Bm7RPQsNmA+d2=;)1DGt~?KEe7GuEoLO*Z)l9ppq=B1=h8#XB#wLHY5j z98crLZ~RW*bd$2>jy~xqTGzYUQe8l~?o@;7*v>fBawu3GUXT7uzAL<0CO_p$7G!hg zE0mDdBY(O1aMB`9k63a2w>CG)T75<9362&&nTI=#*PC=UFUXuhZHeoV`ShFYR4;<= zy|iU}()OdI#W%F&3%T`2{m+5V<_tLgVX{59xBidvq&)54mPW*#FVjuUNbV1snJ>wo z6GS?9TIuA_@2`TuS0B@_*HJB8EFG?MJo$XmSP~;|G>)5e6~haw?+Rtc?JwFuy9-|} zDh7R)r}KKlD_|bK7`FC_4qcS>J2bHt9I=%Ou%zcVyZjDUtIE1LUKo@>)o)jA(vlWd zHL;I6`B=o+1dN?O=Sj5Tw}J53v{Pq1>rlwgs1hYcRXtNCF3{w{<{3@+YGYvzmE{99 zYkdrjFjH{RL?13|f3ynI;zi)3_Sr8nW8sOX%+$EjY=p~HRl{8AZfj0{|NO-&*9~s$ z0b2a#@DSWY!`LY?v73CBIN=+fa#sX29pfgIo-zqHKbhKA?eM8ID>A>dTH+ea}n%0oT)wiB*kZrD%ekEO(^1LcNnDXs;@8Cl*XDmg#8K z{VrM{k9|KibQh8htS~tlqCQA?F1Sf4zQ` z1O_AOr9;R`)KEO;RGr=E+jP!K^?)LFknR2&_hq#XW_b@eT5rBur}2D2=V3VFdMzin z-Z`Pm`0Z5py|APmGM&5I^!lx6`Hy?5PV~_FSYg7K#nI|(1u*MXWo$le`C3?le{P|D zmdf?MahJ&bxS=SW5l;Gx6aijkwzq6;z9D^9V-?I83qNpit?qnz-h^ja<-TxdRx*r; zic@TZxc70n&x&q^BC*stxOd&|C5Bj zFt^>*X3L~ffinZJ0CkgYx!GDDW|<78DCtK5-yG+02V|9h^%CI&rccm*38%192Gf_? zdYVlbPzhI;N!j0IN9%@m6jJ}eO@xR?wGn^ZnqP+10%+2eS?rR%Nk4engBU>+JEic; zZ?YxMrk|UHzGXEi{>JZIvdn`J)68TijWI~~E*m(bZyb-(C@sa5&;0XfwdE?ps9s#4 zE-I5ZUXSHAp|&mNTkObI_7}h>Y)RFP<@;A-E}cqnZl8ulLL&1U#DgZO^jff5rM%$UcGB1dhF)o2aCg4; zq7wsg_x|WurEmEQc8m!q-D+|Hjh`s{UO%Dc4LY+OGbY4SL3DqCE;i1_PIZC$(Rq#V zeBXTU^fF_PyB^jrFta_;;hC2EgUtM=2vp`;x|4KwC&*cUMakwy%kCyQ>jV895#Ak6 zy8BOO13k!II=4B!vi6_1$+ynqZDvd|w!hMzPqfodo7T%|4!S7zNL{foN24=1YyS+eC^V?_)agPr z`L_1>99{lkwtC%YDOdK-t-J0N_>{BN-&8xY-lTO57S%33pdy)l+hEHzIf?RzYaj7b zF-#9fz1IKK7X2ms&6LT95ihPX!$!*Z4Kk|Lm#!tW<{t_cy1~aXlpg4Epc{&Cu-kul zyV~B8mI4WB9ERAOs;iBEr;n!V3*K?dp)Gdv%7cV&=gB4CNX-~IT5ewdRX22L zIPt!-a1m0L#IVrWp(ubJnU5lSS(6e%qE%Y=TtLxaBF z@fuZWaUCLlh^E|7`yAD5anoZ&skNmZ^CQFQFOB&FY52}Ie{VhTs9p5!kF4uibh$co z<4?ZTOYTFKRv;r^fv~{U>e=om@aG?&<+sG!Z$wjSLAP_vNUNChv9i!^8nCX((gfOA zW1vwg0M%}BRn>UjyZMl8iD1A(O_C54 zO+oSZ+KL)OJvwE*l)jNMC@Hv7Q~YKTCHt8$rz#Ghk+Q&G5&c7~F2>F+KZ=E9qG?=J zs|1Ymbc)}6<}bspf96lV4<9}d;a~0EiO)!aJ!6ZH2FSy2WgrhI<`MyNr4X62F}SfM zePk-BGUcYJ63k551^~@*QadU|rzzj*%p(=wt)%C}PD(n^I-R#uPs%L*7gaAWgp!~) zT9R zq_6|55@K1&(L^mu)3ie|qFVHrH{uDjCAhi@M@il2a_6L4ioef0959nspH(zTe>OL} ziO#{<{8W`zztmxd*<{CQaqgXyPs_lpa{XIgVBI$daHPC6h1%a-B^O~YygXVcOVbrU zJCely#2uU_C0|ahAE`W4i-Gic&2DccmFTf2s7*;r(Q8gjN|L72&hn%+C`^$ozk$&R z6wtnR6ycz|S4D`XIH5b2t4tMU=Oeu_8G0^)yGo9yuv#*OgAKsw^?twOmEgdJnNW5V zmMmURoM>5i85BbcMkNr5_QW!Y5@9njbAbXyOuuu}r5+$2C+yTB$&$U+3Nt|_^s6A zGy2F#a(o;m8c>8Yo>P>jHmY8(|4TnJOCWPfgpWe@NW1HN{2*&M(nxFKSj3UnXUqj4 zD&55X?YlM_!$WQdWB3_GeVjqHfPs;BtrCOLt};}}__BN1H`CLDx@Br2&2 ztML%@C%Q;fGTL&IjobYgf|+!ZBFfW5f;~xe{^9?yb0el3+ux3+SPE(%p zYeQ^pI$P}xV|LG^CyWd{=b2?G;!mwfhVW!QQ$0S=nC-jFQA}!lYFESG(z zGe6%-Qrl#K*53n3(sYi+y=ENw4>kF5i2z^!;L3lM+Tdi1!>SJ80Y?kc4G_Ard4TSm z$dDbzrCn7Rp}QMH+e=kmtQ9JD>DE`jOH@u+&ryEIUAJ96`{FidPT0sQ!bZoY$oGB z;1aMpdJoj%*{lZWS6{cB%R*&}N6&6d2z6KTs-g<_7TImnxkO;|y9}Mbwy^-Tw!Xwu zrdnVu<>K*)ocQY(^U_$hPxko3?=X}z$UiTC(xo?W!Djx92kvrfEt7_h8wJNw_ItLF zt+bkiO(60WZj)=e!pIfZ#Ss@7T({3DflHC%h!3i0S!X)+x~I{$kHub|_7Tr7tMFUJ zU1t{O^>zL;#8_R+esbZAc{g;+;c(m9e(l)f^?0uH#JjpTpG#(fmfQW|{#?{W(A6z{kAw-P{e-WstaETV&U zuP|$X0Yj?1L@UaOIMq|`hJglWy)wn%mFtwW;_ZM{;w0@cj)>K`wMUVK}`5{-q( zn7UE5Ufg)N$a+X!bS}DKOfo)-h!6&@k8_PSCeJ%(8j|VwqaPj8GsAn%5am5FupUS{ zjoZ6rc0$xZna5s78i%gzL~2-?$DT9ut;eKiKBHS5FNZn&pCt%gRtViH1RW*_`px!L zBYhfm3=?VnVmd9xAZq8`WXg?l7~71#_4dANA5r=KFJEk&Rh<<~8)1*81QVkM2?2^X zJ^cvFy%5&xFmisl_YAM$KenlT*v`AOif)#wEuWYoaJY^0vE3f`P;x?U#J2ygFqwV} zIZub4V!ETgnNF(_BY)NphKy^Db3Z(d5$}^Xf0I3`mTBBMSbII`dK_8AwWhmPkM`Q< zZSm1@yXtnop0sxGvZm9y-s1VrG#y_FZ-T=-hJke17}$!7c=M z@7Zf81MQX9<~r~Y{bL&D*t%8>$2xlUJ9}2DrRhESyxw}Z>Bsi5E5pNNB`#=>W1RPF zWAk|pIQU5++oH%2Y&3^WKTj+4e@#DgT8wz0*@$Ymk!kD&OwvH+@$`? z(jdSUd*pn)0rCPHvriDxaT@Eb2iaF;j1+6{Yl*<#>bc&(X#n!{6aO8{01}o}_IBg$ z`0^1Wf7#gV64Kp--J-p3vktcDXfRJ(>pO?suGsbMo$c!F9$6_jq<=-5pgx^aV^wB{ z*L7++-Y1;av`?dKa$`Ef4TLqe0Boi^C~0C%YPr%%8&hR48|LWLCZ|=uAx?W3M}{fR zeN5x4^Pb$i$$_4yCahwLKpbh9Sg}Gu-i}rDd0C@xxnW<4#H|w(l~!BpaZ!oMtr^p4 zGNe;6d^EALVPn;G_G#?&kIkuxm&F=h<{Do6#rHKDSj%l=tzHzy9HR75yW}EkmONB+-5;N*i!`@rGcH1JA z)oKuun#mYVbvNjmi_>{(wo15V2&TLf?*r%>kt#W@#vD@{n@!0@0DX^qq;SsQN{zZ! zo*744BKJq~pneZ{C7ZD?1znO7b`W~)8|?KOrnEinUTp3e@H23O^}n4MyUs*rnw-wc zB$29E?{}OhN)esK)~18AzrI)jzW=CSG%t!7Zob#RzZCfb4G}_`g3t2gnOW9uz_Z-8 zA%(Gb<1|hVMGp@2jckPt5a?~RE3@1NBRkd}_|;Y**d|n0O%UJquOGYDyD!V$ut09# z_~&rMc{N^V&-NGBRI9JyH%jfjJ+t{~*-Si!uy(UO+y-D{7Wz#mZqhv5)?j77truW* z%XQ2nAs^$+9gW&}9aeE#weI0%nkn#Dkl~RBgAJ2ycX317(+-0+S*&niyLGL56w&)?t5Pws zHsPX_SBJ_{Dy(Q@8CrLTR#3CEnOak@{S1|JP;3lRr$d9a1^TD*cY(G0J3~yhVd0MP z+avOJzrwXGE$AC1nIe*t8g_SHEnxm652K&`hqjz2b$)xYv%>+9qV&gO1W5#vt=ydGaiQ@_gHkVOqL&b=k_M zXGZ*Ve?AAmYYQd~1=|Koi@fY7U(0XS1!z{o_1A#O{EwR6 zjiyzp+va~e;@cRz*EBRrWF0M)O;s;rY(&l{w-;6c0N_Dih=6od0Yq8x@Z1cZ7cur1 zQHHMF`VS0t9yniM-Dqn!tUNzB-~RWx;P&0~?VWEJQJG(#zg}VpJ{SY}{0+Z>KEL7j zWzfZSvrZVpvP%jxPP})jAi$U@E&?)oycwC61`bSY*XuMvXX|M%K=3X}Wv${TU;d}n z^1m9}XTL0zJAaIpaX(5+vj1fk^?wbQ|1pDVRQc~%w$J&h3b3b&BCvBT&T9a=&zAkSGIA6QuMuLcmXFg)}^u%n$VQ1}*H z0|YXaq%n3bodbnOK^3sI7nMnJ;)x`k2PM!|$E$n&wf8o=?eX)h86<$fb5FOv8y4UN zrp;HhX+sCnqurAWg6;$q(Z&Iz_NFvLN^{Pn;sjRF77pS8uxtws;7CM37xXAaT;_|Zz_xQDG>h-Pl0ExQwJSXV^9N04 zxG4Fz<_~3!!?c8+=2ue!n^k6)8HWh>XR*GjVZxw7kVd_u&D+cDz9D8A)|$>c&r0J> zdorhKtk+w-KvurCOg1&<5srjzowNZri8qgDLpjcPv~i3)6rj8;<26&?cb&9BiJ{V~ zNvlWQe_Y2coF>+*R*9hL-4)$h^x;4%dEa9#Dc%yzn%H;sVv2di&s|QB0c8dZw#_Ze zj~2K~fgV63A3LsMNNmB5}r4;0S&?`qtxTe@wMFvD9?pSkE~ zi~;O%T7}buBKEoC0}(-HI^R=Z7YA%C4)ehqn+fEnDJrXWXHc1mxF&KBlhJpR5WGWp{!g{mzU}J7bWzQck$7Vo))v*ph z&7b^s%ea}7VeZ$jvf-a@%Mr4Ou>_m=F_=^FET=#3rx)qca7TmMqF})s@46ss{mg}t zxh_pCqZbPrPH37QgCQH*L?pIJERw;)k3eypu&yldk4-V5vEG3y!vkuqLs?%eEMx_%>jVnts zcs68+^IIY3OT?JNI*dEzAliA>qH6Z?tRB4ko{g$pSCGX^r9Y)m_kLOTxaxeUI#w8@ z>@An8m|6Y^2MDN&bv>xQg+L#2plXR2N$Cm+e5l(9qyI|pcqc+!3DwHWSa0>YQ!IPR zt0`%zBJMn``;=uHq_X$$NjRselE&oiItXgV!35z_+~fz8qrE6ifwPT{aNA<+5%m$s zuw?(WsI<6c0x)2|UkgDU8`O4YR6N%PtfC?rV({%8zViBdw%Q%39r4>OF*5r1qjrCQ zDT?P1WJ`)Q+fcslMjx{uZ*h*33$LCb%kb6Onl38SRWu8V$Jc?_W%t;(M$FkZ1M`;e zSKA*S2Ob|@+T7od#h@Sw{{3dJ)(;y#i&6|CdJD>eE~f7Y-yrL(KrlIBS+=OjPjAvL zmi(KitTmVU5#QYVm}#o@uFU@A85jMf4QTw1TeP&e0yMv$UYRiN?GGs)DfKPi%_hSY;@WwJKfCMj56%!KUKJz{o_|>+|<{LitMRm zHXT|h+s~}G?I*1b7Gv#DJ0(6MNy(y?LQkaiM)eSe8;DZQSslZ6fXCtVH)RpGYTft%AcGqoSq4NvZ}gv zFb42KNv=C;85v(ozRJWp$B%|p`O?13JRYlbwLc#pI#1uah@%$bJiZsQ*v~%|bq5m0 zO_%zfOP2Qyovkm8zXe96MT-6Mge|PrTC9~APBG1`)#_+Br^+PX27BMzk)y~o?;{lP!TubB% zu;RK^N=%Z_x2^=M^lmu5-`&1=z8prGb{oe*rtPXk(=P@Of#-uW8!Nmq+NRFQ{5o-x z($7kU4(qJ&rmjWLu{1}N`KzvBnm}+K zw1T&r%QV&OHum9F`ino!$HO0AH!!k}TeT3Hi~{gQMtGP7!vOf|0Tm?pDMp=YScONp5!u`a-L#ZU@$y0N~?U^~|q1W++H6$6w@ z3gv$THjpDMmMn<<`?RPOgS#GeD-_ot=BwyNbPrpqHfrQX!H$@#1@#RZ=L0(4H~2tT zuM}R07Hz;xQJgh!fp1V=zXPlQSV#lMZLIC%^ZP&2jz98;dsRFDp!Eln@goKPZwTuD zMCSZIJMe$j0AAj9*kZ}L)8ydWIiN4JKdvZ2{6q@*y5&0K(R_;Wc+#53#-VHkpji+| zRLB`-0@Ase?54^|e~iocj&)jv)~eK}%fSV8jQJ#`wc~ zt4XzhEpL`B)i&8-S9>a48shH41K&K&lJ^Gtl5ha!K9l|JYL+;hTDOpL8z=jnZUcOB zoFN)xW#yYuj$LAzU=Pd=WOdYRbu!;6k&~POXu8wJetWQ*E4d{5dkKDTwOJ@TECb+% zJ6dg$$f0Eig2kGcZaD@}pgq(+7-OGXxW@{l_;)1Wnpd0hF0Q53V!BWx18Q z-jcT0XSB@xul-qBn*U~hOGj=*lIB4n<%6{fK~HLsBRxWa2cchzekyUx31L&M0Rb}j zoSmTw{|*hsM1~|&GBPt+F9xGC6MHU2VLOa#k@zQbBgfIkji`bDDU{np59?q;k}0Dt z76&ykhzpe#5i_VeJz*ZsKCYKv#S4qPT{WXYLlaq`FacY|3=^I|m1awqF_|+UAmm<(GrV!9CVR*5gHw;PKY%geY-l28HyojBEp+-2u4`Shq|YvPFbSj zlzaSP3Fvw5@h#~z*45ix_$b4kPN98T{*!#UwLFzf+q8~42-_}-8Lv* zfq^kYf}`a!m;?{)g5|OZnIy2^XAFou$dvF~JZH>i1@4xaLDI^A{wDAO7QEgYpUi4y zR(;O^dGjqK^1QO(xzTol$@YDc1B)w<;t9ft%F#YMmgGrDMYZC^$zIvJ(@F}rStbSN zdKScRMCBxIeSkF%G)xW^kroMZM6p8Lh?CxD!>2|+%DM3^b@)4fR?GO78Vg#6xJbPh zD0JHGh?-Id)223iGtlJjH#=2t8h;Z^R3%mGR9o5kT8=pnZGnvEFIku{oL`$ScfDqx z5q<4w3Rpc-$jAJlDqD3hwPp(HaRQ;JZ8x}n__89kl*I;=voG$bhFdT0iBI|sc;c-a z05tR#kcfrzru8O;d%80BsoH&gz$3Yt+X!YsEUfPa-u#Gas4#QHX7IN`u!>Rprw1A^X(>Hf1k3^zduwGiK8;o zH${FX**vmm@{`~@9MAJd+yb5DE)cdeZt*+m%s{0mQ9bu2KT&-Gs5Gw6{yoMP`RO4% z=eJ>|!9b2x4;!9z8Bs;97daNGOEU5XnXPb4wynM69$et(ZXrk;kTON9juI2L#i{PU zqAq6bvxgazlLydR8Fg;hyI^IE`|_!T4g~q^76vgcEi>k!Ot2g+e<7=|ASSci-yoWz z^1nz`1vsMp8h02qKuDbCr!?Sy-pgM|i*!8;6H0!HmBu3$6rSx)`u$X$zMU(WyTOJU z_E?kX^Ps0dhGL%0xP-v*=Bhn_Hu+s8hQB_}M6mo!S>tXYu2e_(n_^0-T_W{wB=h;d zyqpx%zSTMbCttleK*ZkwYs<)M^=$bMfcQPx$S+%qk-KD$2sW3MHmImt{+W@7VC&e! zOMB$JkAH?^$aQ0l8HG!r<7~esMTt|;*u_mmE^w7E&qOOKu|vDJjFx|;h2Xq4wY^s> za#BHvng{q4M?IH+7H7zBSE}0riRwWkoB1Au;y9-)d~7;GGM)@k2=7J^Et;VTRo?I{ z?dI1>#@YYEnCXh=ejPQl09H?rGrN!!9FZ>67nUw2#P1g#X7HDAxHN&ILh{k}SMoAo8IrFGnDwW6FM2YZwB zLs8fnc(-D-Q`wt3s>0{c9KR|KYImm_4P(hU`GU(^A83cRcRMQLmtEbR@p;cQQro{DWF-=u!N@sMn zokiX0sm4iS>J!Z7TNJFsQU%7Rnnc{6BW&RfQ|MRF<%?4@kd*TcrKWO?afzpmt`C!~ zYFyB~T>GjVl3>tdT&|#-hGCJ;e%4&LZ}3rfsi$OiG|{Gf6Ii%YuApNim>+m_ty!+0 zAx%XXt5eyFxQSdp1POELP5`S?l^y85u7w9Ae zb{UC$`IQ!Z{|4B|ow+biLf(RP!3?(l6*WoqF{D_UFk&K0Hu`o^929$$Y}ifyNo2r? z2v=q|C(fSq?7lG%m@Pg$i7gQ7PZZRmAaNl{=FIjrS*f0!7?>!tn3E2p&5W3wy-+B! z&YW_5J7aLE={2kHV!rx~_WPYv*m-(HJ7&Q z;nu^X$G5<)T0jggLP#~?%*l?P#Bzq9#&qU`tOK)!bSrGb7mm!+Xy(4HjWy!W-k4@W zlrzB!xo|~7*x{#@YD#bV`Qg^VP`DegD9}#N^z!;W=t)$D8I>fZFKep1m& z{9&`vT~Zdj_s)_90jPx<0?~rYY;s^cqFnS4qSn~gXL1o!NSs{x{Z!(|e`n{SJ>W!( zoy_Zj?Xt?;Z30T=Wh(sWYRs+KQp09V%DbVfR@pjD--oheN6&U`t#P4ZFXzO-3pT+! z)zFBTIH|<8Sv#w`>KzFN%5E(h*sl9CeYAHmL&yUj3JF23Iilm9MuHNnmV=)%r)lg*GQuQ>$ z*DzjIfr@Hx1n|0KG|w+178XPbCB9B!u4 z=BkN*wr6dzd+F8nx)pBUm#AugSIg|Qu%lK4u6AAg_TNoBN^7hnmsnITc=UGGht}LM zTI=4`?H5lIS@f)M;xZsb#UKjverR1Q4}}LlOw&F_jnjD9oF55S%bvE+Ukmpt>)6)g z7!le5gG(UKRS#tW<=I%`%{Bu>e)w*WiG*lAq*Mu4!P%z$KN3%OqTKhbPH( zcD!r%!Dcs8hsVt(v&c2$sQbkhb^QG*@bd*3KUYUufL<;vwj7q54`eC~2I@$@t?O#= zuDB6sSh88Ui7?*)Wkn|^K$v{Ou~efQT3T3&(_wKPKXFg6OLs=E-jy3izDlx ze0KwVs(>^p$$jE?u^e45C&Dm7fl5Wo31P9?3q(FSKb45ncG#oY>LTm1gM4KCd&N7O zDeE=JaHXSDUmvH{OqOnLJ_xb-y*vZ7!}8<{jR%(yxz8u0mmC@gg-ERX1Z0;*fHrK3 zBBqz!aabY{*-;%rRGq-d1R>fQ<*jjp9T^8&++E&=5NQxd`L)(!C!;B(U!HCpuWr0Q z!sZN;mm1qbw_oUn0aPG@#!UPjs`5agGaEunpY>8NUleaqe{cy1Xa!eu zIIdvM{X~js{v+y018WZGcQnyjR-D0ypTj-;Rxh{a!sj$|Y(K9C;B?_1jD|bA6$1*( zeKrToCHv_=SCn*738P^du8RPZF3LZG$3JHwbi9B~xS?B?mg~>(dyU*{GF+SBqsM7n za{YN3u8qFtBZ17nUm2M;vTJucSV%T}5Q39+aXMNlb)xdSyNHzYkmlnJJuxtKxFUhX z6eK+HKco)o_%RY9o~xRTTG4+Jb3UaOMl@Vqv#_UKN5{g3%!Mlf*zNUP6D$hFvHmPi z&yk{fGm!JFVy8dFR7~=dHJdH1F$mX%?xvf8;}>$2hvCjG*=(%qpMCG_7wo5yP<-!) z;LLieW3I#HgUJubEgEdMiXo7mQg6eU=EtS-6kd_wO6X+!l6V7`%_pw=TMS*VTd*fo zqxClLkbSdNby-8j=8`$9GMgpDkWJx1L5DIU4V;)Ex80lq&uuXgJk}N> z@n06rkC8yUf~FBe%myli(bCVX=#5{_iQS_6AAfFjE2mb4m_wlbaO0g#d)vMPs__oa zleDv23mj``;JbWIp7M&W#jPQVdU{#IFI)~ne2j%8I$kICpA1Tjt}EnTrlPaPZ?dRet1GYK z-(U(>k=^;!?qFtjjEvL zUg9%@gV4QE*mF?;H8z|zcN0Gt}FWa1-&N7WMZ(!Rp^C-cs1!@mZ* zgL^8?lOyDRv4AUC%sWqU&?Wb`k%q92@VHk5fiF*na-CPYGZn;=ln6&!WDii#Rt(o- zMfx|2O=TKhYiy}3%YqdyIA67$Qpp{DMcPRLWpm6^%$G^;aiG866Dqh+43iO$K9JCd zc1VlWkcu28M&U}pO6nOP!Ph?Kk;7WooW3mOLpI4A6bN3_r6SewR6;jT{3%SRk-lNF z*FidwptT|yNZP`l%PQxR?6a&Z5FO3kUc$PTaA_YCP)ft&EP1Y-Wwl90WRlc@x&3i6e9Q8~oDw#(Aq(7*_Z)oW3I($kt z)}?e(gT~cCyN(N*srAIz#{YW7$IzTQ#>EeE>6&*aDYCNF!AT9HDNfOi<9bH70y%7= zHEur63)|9@dC-YKICck~SGPwS>LF zBe&DKI+kt5w=`vskoP{dYrGOLcAo8$G85F2xfCQl_IHKPlDN;0?OBYICR-$sT)gPt|L zS^9qY_PQO!AiUfEj-olCo!Q~YLRMX?g_?FIBy*Zje4r*IQ`MVI=6bdL5D zW_|tJ5qupN8@~UGl-FE-eVgB!*v>s=_U%U;IKs$@RRXORs))e~5=*i0Bw0ZWBnnYCqdfJ zvH|}!1je7dGmx8z1f?e*v?3!E9`?VDM2rMS8I|D0MIC-sQwc5h7eCd7d7|X%*fyAL z*pVkBXaVRYs5uCHt!u96Ep)sd=4&UjO-l_1zF{(Fk#vK zC!BU2SMVieP9(HB(wphmCYTVO9xwFSq4FF)G}Gr&=ot8j8_Ke(Kz6 z7$3~6%=#MW7Rd{1R z`U4%+7dYBCIE41koWlXrd76y5PuD;|)`&@ttw6zVMG_4x8Z_}8!}|O}Q!Zo#I({mW z;8!Q7aCH$2A>2cEVyZS`ONL}M-RPpSLsp^|`S!NT%TZANxj}Z>zn+ViIMZIfYh9z{ z=q=ahuGg`&zOPBQDy}79XrC(wt=dL&1U>JP{gC^x*B79voEKwmut$tjmKE9 z?4FH*zKZ)AI=Bhn=z?d|B&Qp9%q?l!5Kd%C*A;ke=8Q`Dz) zW4=2x$pc*VWwGE|M`yAtBRgX($7=-G_kC})h_dWdlUj(6`|+x*FD_zkhxF(7>#pof z?cgBwU{CbNZEH23chejw&pNfwj>Xuz&$d8nJ5^}&^exj@N7(Nr;m`aq&LwjVQ?!e8 zBJ1KVIxe=M*03{qHeeCc+0oxAq3%l4Mg_)p*YnGtORTJ>v1*5h0QywxN z1bv?eO(w`USU+_QIP7=(DeOpa*tdKwjc zGVeh?qyVbq8eEyh4{R&B8?zf&dK33j7wBRZwpcp&r$uls2J*(+2)Qgu`HqqpMh)%W z98;yL<<|OXqoLUl>2OASYH}7!+Au|#F+*mxeYjZ$OjX&SE2YYbA>usI2kGwgz4XBT4Tdv9QJCaa#qv`#ih>seKcRm#Q-EneNV&&Y=NG{O7Y zd`Ak|l+UUNkce6lea0p?JKo26jP16Ep`yckYHe3Kv}Rm2(n9O)*pKZIO?w443eEu1 zWAjn>x6gZ#;|O=GG^+QQ5ipnRtZ_Q$O;>C0`;762InvQa%{`Z6^WR?eQgv54_TO`D z*8x*I=eO&(Mfc;HsvE9vTQBc+2yyq(aURN2(e? zz0*8+!VjNe(eA(*xClRK1FFy=F#dRuH51CuDq8LiF?JkjrgE8vD+_JeiW7Lvelm_Y z7iC#%2558U9!2Q|UrTuNt{i9f?@^XC5bb0*zPS8H6*?CUI%ciTp*>8)g$ zW6VOU>0w`I@p~4zO=kK%O~X_(`YFoc)^Kulq6dD02fk+KjnDk&Rz3Tl$=4$Sg{1iQ z67d}Eg5MKglG_D;0xqiXz*jYY!_;Bmf~__$G=M{z=mu?$vHZ4sqnc1DhB_sFzBrjM z8w~WZ@7a#5t3qRbGoD?mJV}95v#PIRuCjD_IA@vd1x;?EGfoNF9+B7#u*?mBQi2Cr z>J)v$T5u<0YlE4inBt`T2}Nh}?TE^v(?&7itwHYygaqpf3L!r|>2E z_YNe8Jt57Bgov9cuGWkxD=J^fg~VStTFjT4|8)S<-4?|Rz{brQJ=MHh6~K4g9vRL= zHS{mDgnVeI1Qp~O-Ej1_?281tKhjhVRFdIT^9qq_!ov;4_tr{*HyY`d&}UWWeK#`H zwau#Lw>;I}yM1x;tL9u&bp&PtoK#?jf#jU)qB6vciVZvG$gf3r%)DDR@X$uP&e^>WH z3xv0?M)sg4!z^_~SHy9rete9if1pj>e?Y{5t`mGlxkzAfj6q)LfE=F2Mi<0q@2}(@ zIlR)-BBB-n)VIPh7Qd?W9aLA)_Tcw_a90f^TB9eS0Dxo^0D$~|{W|UJU})_4pE%wk zHCZdvWlZ0D*Y9f&48s7!bRZnPJaq#@X@3c5;Pm)n8EW#vH5}3D_pR#TtgO|pH2cda z+E=}@b0KJzZ#Q}B+9@zO4X6dlQAW-p1^O+A9;{iRAdj7ZY7!?3;An&5|Mh4pf`T>P}ceTO;)W9GQfcEZi_U^r)2zC|e=m{9< zi74p_Iq3`Jcmw-h8Nzc*@kYhuUQm0}k8#g1!5y~Gl#)$nByr{^6%RcF5*YRl z2iP4?epv)5MubGKjDpBsVr*QzFYh!vOO7b--A7*dEV72tWOT>a6S2HSLfEldSSF%XmBv|0-AG+`KW8VAIdp(T{ zV2I`v4_V;v8XVqO#RGDF6*mWW;ACnp8*HP)eEGrdsknIOwog%diwpBXyRn2+3KRJ< zxe1yYShEgS_!fF*;|Q1 zb(cTOu%=IotINX+wX^#zN?bZiyY|y)D%DKKV_V1NY49b79ltGdSbrWP(3pLKPD|DX z0GJNihOFg5&U+l>ClA-SEvXNl z)MVE?@3Bw$FH$wc*g!Y#jpSdOqY}8kr{^27K=cGMF)qb3__jnqdgs}|5xbq~ASvH? zf$EsFb^sLoojWHpL&*lR`h#sZXJxEY{m zBT?sbfm!qU3l2_VDOiYd<$EOKQjH}orPh-(adrK;K~n5(w(k?UtN4Zd0(;oQSd5&t)uz^!>y5n1|WGa);nZ0 z^tzMQCFwD-iXsAe3Tq|eog`+1(6_Bdl4z=wg6XbNH}Q1yvBd5B14C? zBhtEv@DtO#Y0$_PqTh$%!4hN119yq(8&c{9U9=WpXceF9cXko~a>(|9rG}OWMlQ-i zGEP6Ec1=7=^|wo_m2DAfXj$e9SYLgpyy=~D)3|1SwR1MO9Jy>`%w>6>mpyk~e0prF zzJDcq)R5T}dQ@k>|Mt~xBdgM~7@6D>nq1It>pWjhs2EH#z-67%re5@QVD`ShtMWVQ znr-(c&nQbc-%Th)0Lbld?P)>Iz0lG}wUg6HU7C`z7P+%Fv`xeKyTBFs4P;K;tT;^5 zMjLfti>&2CyX(c<&R(2df-=VbAV;1(A;$4Tt{87`iy>6Al)v)DjY%gxXpiL>XIgW+lM zeSD{ToL|$m5-g`242T=sll9!T_c{`%TlACC3d?Pjl*h>&oZfI$7jrn}o+$~HMN9;g zwFQ{1RO_kpkz`5mLne$!%6)HTpt*2;sO7jL(H=eG0?Oc=oRvJ*&uSU|Hg{ z*gIXC!4wwNuBS~yT)|q+4KEQAkZbbLXO(x~(f3G|um$NApgrZxsp3jXHcOcQ?A+AP zEJI`bXddMi-WWIa%$OG25b8yn_1REi>o+yZ_;!NrenD|49K=%$*=H>7%YTO<%w7<16{2y+W0lEo3_D` z&*#yZ4>emFwZJ63gW!KaYao7+pZ38>VsL|3&>7BjFwiHz=tZUh)Z<7f6o}q~_Vb4B z#7%myihjsXeiL=n^H&hg^be%TgaS2%5ZbkodAknvbnMScF-EP*o6DOKUoCIm=};my zkwXHq*GWN#LFsOM*&Pn%uXCL*dd$3PzTLIoYawt>h91^$O`Ul79oT$|`#OQ7NnK3s zJeso0(~F1gWhu-3^w*VhUWhnjzScn~1mmg*?{I;vKuf}H>%%uhAt!T<1+cs@=v2>4 zziTTtwLEIPIzH?_m6)CFetX?;Mjd@(n6bFT&TC^sti636G!rigf;!NIA{Be9khJE` zybq%6&O!Qx!YVGhx&#PKGd!0K-5~w;F0?dk;bU<7JZUV5!vDTnOxJpUb+D*_L*ybg zQam8>#w!@7Z0q=w!VZi!>V_B}xF)g%8%fxJ9e3Su$tjiMD4TS3-+T94G8Izt*-y+% zSi$TtT3j++x9RO8pZh?0H_rtP@2=wCiL!pJcF2Q&@T3 z6Y>Xjez}dbG7Kp3WK##m=KFBJXIk&ToV1YgW?^}9p2>kU_tvCz6c z@QK!R8%C2BznK}LM#2bzlxIJs>Jg8u*jlE|hu5$!v5u=Dhw*8jRo=>PsGM7@JI%nh z6n_g^5P$~ouzieMHj1WxEKl99%@cWa6dMOG#-=zP{*=v#+Ng&e0|#Q>hF`1Vur#9h ze<%2Qd$xP25%`NqL3r(B&Mh6hV_tEVT_4wg+#&^K*l9B{opwRArV zWM-}%dGfKP7ymP`U|6JTdHrLNBmGG$(El$Lp_8$rldhwazLUA5leyvlR~c4u{bavY zU0ij%;3eYn#U&R4>Wo(tjbDofBrYD`2+5;~EtebF+l}|LqTND}NgXJrBRi6r?mUn% zFoFKi4w}h$)6mcZs|muY0cliXP|S$T#(=#QK+T78%gEEl=ilI?fLXKQ%h+GrQH&LK zm^Xi<4s$+ddtS93|NbD483qFYyhFON(jY-@C`;c~$DX5p?!grZ6bcXq3N#AJ2~6+^ zLnI);B_KrB%dgo7ehJ0xM1nH|rHyI2lw@bT3FVqB^AGgzpOYy!WIV5DGOyU6Y-ypT zoEsEC-Ao3Q5jINHsvc43ws%^w6$KK}-yO_z5LQ0q4q&~08Nlat0@=YQ6B&~GP-Kq` z02}OkB~RnyeL>Mlqj=*scNb@b)L29^f_Og8u~nYY<%o)Yj8P}B*0CcK#gP7fui@Fr z#z0asCX%XF2m=0bxXgU{Xr(4g8DzR7p`ar+4DK#I6Qu-%oc!gW6cJmR984bFAmj~R zni=B#?Hyh+H0yN6jchQ2991BF2u;xu!_G&ma(QtFo#6M69*aRM@5CemJ^es?OjvqWrNaP!9vQ;w3uoKhx( z)%GzdFEx2Zkzq5cA88yBB!K{bEP?Oe1wUjnKO`&4I!pk;3ZXmfCayoOLJ~O^ALp=t zqd&$)Y_D3gUI^RA0JMu)&4e>f|1i1RkcOoxv9jk$QzF4`zvJrnU?ezCe}D;xA4(7W z3ChPKr^tH%oFL#0jLkT}FGD&X$%dItq;Dw0i3l>_@irA4F2iOnjEoTU)1OLiuK29Cb4I{gGtV+G^{r6jAk_=4xC?jgx) zWWIAHD747MY~UTpa|tj*Y1%DhB)^*`4IYxAX8-|`VrZA+`zv#zq@*>~FUQ57AX7E( zr#U--x#2{$H?;)4Lg6~)H75@(2MpJ;lsUdhR3!>#A-bY(N&ZQOiVCdE90M>93y4h@ z<)$@6GzxzG-UV0KPR1V?M~AnHRY|M0yTmycR$@Re6PgV+k=0bXDAxuvr2PSyN&pHe z`W%uRtE*5wlX3495M^m7z+W6d;_(M&acAO>~#0Bitrv6F6=1 z+O<+N+khh_6E0y?$C!$8keaG!(2)9B1A1?&yN>@bYfF*Rm^(Pw_!r1BZT3$qmBMi)2(yqwf5z+>JZUuQ7b+hOb|Oz2 zYbFsy(Z)a^fYfLec`m1=Ki8NrAwD0{zb{Q-v{k=fo>0t~sUR3dW&r1h<}eNq_}kc6 zgMc7g%xw3~AU!UrOGf)gXWjKO=(zRx{TYUs_`M$U$0I|k8dtQz&S=cR(U1l^EW^%s zx%E!7@CWdB;ztt>C{pPWRoUc@*bHg#Td$eydO~WiA7QJk)#h4%L|X5>dfMdW_2lv0 z6>{~gWo)^Et90jfwl(c-S1j;3wxJ3xa9wnFw&zC}P65Y*qeVE~O);6S4k@tQ)%m^A z?o^A&TblJbv2N#Sv)73YbyJfiNb?kiW;=Qk z#PYT7vSyCZ^*L|%(FT8UI!XCOYHi;b-yVrZ+0M`iS2SqktaE0}VRhJB^`1JR5kPAT zst20{z7--Zwr$(CZL?$B>e#lE9ox2@bZp=J z_rBb}&Uu)%9=@tot5((g=9n4-DXz422%!14M2lHEAOcV35EP3f2NZBMOl^1P5Ve(+pS2c+iY>K8GV!;pb6>#kNSmg_VLVUnN?UsEr}EW7dd zVQLK~E7je4(jS%TD7h&2xo-XtcF`2D8+r&O7cgC`8~t*ao}(Jw`fIQGlqng}nTb0)Ni;hm+R^s8h;!G8sgD4S;r8XE zWNrJ;!|Dx!R0R7qPw&ycxD0swAR4v3K?D%2=7m67FNcHCVv)eH+vPp$WCSj@@5!^Y zI2gkX{Avv0HT&YhibE*BUba&}O#OAPK%Wq!7D^$jjV~Hw_>|_lFJ;6ZJPqHaC?1fR zg-+Cevw=KPX!rU#N7SBRm@39RPSH+kNBs@QEcqul7GO>vR?$41tRB(N6L=X)OMO0` zt#*Sop>RI`_$srsW0{Y+&`#cye}+C$z+!Da zPU)72U!NTO43!6IIlaopXn_QjS($sQ81GP0$ssskMmme1)lRV64$mda;!`!i9FvNW zOVQzv$Wmnugq!?ju0oTLC9OmG6Dbrodtz`7ztXc&)L}Q#eUtZnErLhbZg1Q*3`cTx zY)R3&@U69UUNKZXc^}u^KG}VqO$t6)Rd9hfrP}4>@Z?<0t;QI($`shui9mE5g`LK% zh{fOx^4zG1X&2ae+}=;~G+Byjm~lQrvtSii8mMTCX= zJ!O?sKz;aS9Wa+`;inEWWyiwTuWOqwEoPZUYqaIL25x3*rixSL%gi;o-cVpY9z$ql zzDDBwVuW$MY}8}GQhAodhkAy?T%t+1Y*tglx%O!u0_uX^XR1BSJZb%=!)#buO)qYB z@o84sC1ZHDb`ZflsGQ1@gel#!U{Iw!u_dzKuXWfc&ZmFX&|c%VI2)NBlPE+tEKvrd zr3FFBC&Ei*|95w<)f!gTbcSi%L5=i98pG(AJx$8-uQ5}Fp-95U$>^}dHvtZEgWiY( zcrxm@lyF@nJlahH1Q|z}bc`A_+W`u1`mTva91LkPDx=Cn%Rx`mq&;nFJgim|q^u(J za8ObvJh2Is*N=YE-w}l3#Wh`Fv^53ev6b`vj+(5oBglWJ+jEno{-gqS=z6YBhVpA1 zE}eO~*-GCye>;hB)t=mJ%1)3%Ik%ozc%OR>+FSYC(un5&M*5X=DOFb2%6+g(!g`XONl3bK2|@qb{ibKpN`gZ-mmYzoZ_}QMP5RDE*C``LdsNgQ6*$ygg zR4HvFp!EHZox1)xd3|z>hjnwZ>#bZ$IV@ecZ9tee+8=b|? z(nOLnmx_K6`%K9MB76y5+W{Io%LyGbS__rfyMroHOybdWNi461tg5*OPK;z%PNl(| z7T3RHrN&)DcS3z*SEkmKWpnIsjRJ@Xvu2kR(T@w-TS}XvE=-TJlxO2Bl`NNWE&0r8 zb<^^aO2JGWiWZV~lAw!;?A=vjnY3mVE-tXUxNBQ$^^-On+73tCE zmCR=Rh9;-rY@JO#Jg}FGzymSIloVmDnqH!jv?>HlelT3FNIMr%2h^)9(W_i$7#fZU zVP!*FC?e}~0QX_6Es8R1v4Z_iS6D;TEMh)EWj@!e<$emeg&u`21vv1a`?l`SW*_pL z*Ht&IAn>>z@L`m1H*L*m=KGa+L|-hfZWS$mrC)pn>{}D?mNo>EL|8gnOd;lGXzR7}3ss_@Jrghdy#g}nLN_A^W}U17_qU$= zo-L;tx<0rDW3ryMwB4Ott{l4dJM><$VX6`B*>$?V@}tk(K0YG>iw6nuC|`_@uO5$h zoa=IUMJ>dntmE_E)}Kv&e#RMT1T!D2x@5saHi^A(e&cUrOQto0nz{frkbCF?<#M`Y zaRiIpxhn);i3xZWd&GVWXfnHt5`V=TX(Q_kD9XHq6cjyS5m3)9#asiQk~zaMW$W!Q ze8NP8%Gg|HX)+Wh9*5#W?G&Lahs?5Jh04Aown;}1H|P>FwU;RB-ITDZ{OaQRPh^Vo z%ZsFkvc)f0g}4br;<=3D<6`D!bc;zn2Cs{kn??KBp=t;stKKfWpHG%xgXY7U&fU11 zdSSSKaTdUj+WEp=sp~!O+fX!Pc}rkY^kOSl?oZ-ZdlPV&9|#|ys$Mvq^@a&Mk|>^I zsySnL3l4;eU(|$M=rc%Eu3?o9mmoK6X+kxV>A!6Kv=*a0eu%%$p)8#B;fgcz5is2z z1mUaT1f25kB?*u))jPxFfDZ3&#O;rfY{4ki+WB(zW+3|p{)o&l(c1**t?}b?<1Kx7 zL4D#*s%|V+t*ZKDwHt}mdBJ&ZWnV@u!ZT+!{FxoY4@#Bz>C)uiRU}fJfC4f4rI|ybpF}@)k;SN`gPb2JwhWRoH zMK>P#R*HH4cD4(Tp|5^lL=2;^COMtGzh(ZT&BNR1e_8Yg&B>af5kz~UPVvXeJ4u3p zVB~$OpMfCx_^od^z%(UTHA3v>Q+{n)jhls$hh1v~OdS?(zt??8$H}HYtqj|Si0?U!UHnD+F14qoZR;o4zop(wIe{HL z{+(@s@os;h?@t;MEPNg-VR16SFOh80a32!CYAj^@ei9^lakUU{30!=^mkG@`gszVf zaCZKn>z-)@bf$N{|E5fw;gEshZT%w-(@f!h4t=5}zSi^FFsV8Eh7H%OtI2QW@#yW+RE{lsl|0w2%1I z9Lyv2h3V9Ser*aphcFOgPah)9*p)VM@WNoF0x;*`1hFlH8o6T+K!URm1c7xbEx$BP zv{p>WT21e-0kWP>D;SXd3P8vHd^o9N`f(%GcWtJ^hZi0H!y$AB z60bLrH=)FPfxK7i5C1DQ_eFun(D1ctPBPtH@(pWFG~MyV?k!};H1F;;2kBqlW8vT$ z!~8ilj@{cf@c_DGoLx#il-J5fm4azZJ(}0byTveD`IUfErMk{f^)JzQziCi&wxDMCWolL~j^#ijjFuLb;3YbEeUGRYJKB znEJs>ZAc^`XNHF=8PTAyr}1c6@J?dfxSY|9bJ%Basfb=^BWCcXyF0+_Lya6ae}DQti)=$Dd?+ux(`Rv{O^pd| z1|&8RWxRkE4$?PgCheQhWwh~$M{0wl&_{M`V|BDNXLkhb5_$BA-4srPo;Y|J)Ld(T z@aSrwPbu}V>KY;1GQh#jar-1Ly(NBN6^x;maz@BEfto==nSeu~WLB%0#2wjyLsb(Z zk9nlELn#MX%FheK2R&jG?leFy58CD`zeV4S802cI1(i4aT8xlMF+HS$DOv<`i@&AL zB@n-rC6u%YXJ_CKE~g(@P{fxYF7$Y- zFL`%KTrMHDt@C#fw5$`eX%V*Ayo(#S$L(QBpE%2^cwx;ax82b`2}R(;HWaYObqz@H z=eTM}9R0{fdXdNOp~%Vt+^PqquILL3Z{L~fa*fyjt1z-}bFX#_i2k)p!3vMGtBjj% zXrw5PZc@Zg&dCCZ+*PfeIG7MIH|&)_~TD=slHN8`kE0v|HO80ZKPJdH4CNemmt^Rmf{@${NnGG3@flr8t5(i9TZq2s+Uf`ev1olL{k^XD3W^a<2O;QxYT zj4|)UV@AE}AGJW*alJPf?a*myCb zAA@icToS6^CcqU?s3gmL{+j!N6chD`v3Br*Gg8Qp`O`!+ATvb>Bsu{R-U>1R{=9wy zZS#jfVb$&k`GL6)u9(9$NLmR3@6KFYagjWB<@m}M0E5aA6s~2obQf{0{_boF!&Q_w zY^9z8*w^>4>jfFc5j6ff<#aexSWYM6#f`+Frf_ z10zPp&P;x>+=ivvtVJ2N$3!*EIh7c+iGTh)WhPTo^)gaK6nK`?5aJ!#KSiCKj17dE zDWv0rm{g~Z&Uto_DykK^#g}ZOegX+mn7}4snNH+z$Pk-IiMQXGe{jQ9$dwu5KQRz; ziz8TclDQY`gb+8R2MW>AVC7B=e*oNj(8fcbMt6I(9zb-MVygr>`$juos=VS#5P!pA`8410my>(Z(CbFZ|mUX+FZ%f zCUa;=HGguDZfeH>4N?#L6i)y*Y~ zWj_~1wftcke*SCy3!RLkY!VJ*zspY@wWPp`m{`Ezv3aI?R4#C{)aiLFy1l9mHieFr ztM(VA71D1}COeU%6z;G^WYJH4ptwTCcsmKg)lxDMw}u&HPr7_arULX0Qu9M_;N`c81H05f>LAu*J&2c?XV z2{O!GqY7v}v~V3L>H1GRubq90XL2DDiF)!&F=d7I!N6C1Atl%h2JJPl3o^dKUwfoC zC>Ms->9YC2o2jx@CAMtekrmZ+@L-D(dDbBgHK%2!t5Nvxp>XjoJH2{e7(G0VUsgmB z(+dbojz+IUZgNvO1E--|+bxZDUHARuHjXD-z~BUntSG0LcB`BgvtMNU(nG)yJWEZo z{fS2OIaP+K(93LSr9pNk)jr*H0MqnTR*nyWhXDfhvyuyux&nVgro!m{H+1`8J40|t-9c&44hJTsr z+U`cpj&#*9U%N@klM^jrBOH#{z7HfgJzVR!PL)=0uqGIuZa075q1__-{zTf>g?V#~ zsnQc0{hSbl-)2#Nn-*%E5JWga_09I2z9n*Tywea^byqRKWE#J6*D*l1zrF_vqCugG zEZH*pQ&|1UOI8Yi-D(fVP9_&i&**2F$~dS6S#~%3PlOzg9E?)rgs|kV;@BkJ;N_yu z{6=NAb3jfYV(ZYG%-Ih8cd(xUy>#Le1(s(v*7$GtsRa%pmRZ^|0*etBcmy!ldVD6~ zDGqRQQQVb@^FK*1th}N;+wA6muE0ET;E>E*!>tUP1>SE?cZd^c#m-?r8fy@(p&XXH zMd)@9h%4whZz);JXweFSMu=e59~8hz__avEH~ybd3N%+2A?|b$Y!@g35oRzHZ@sG8 zmI1#KyoY5yr#1ajy+XcV&{wjd++%kJYPY0cy?fhjEq@Q2^ko2%fd}VE+Bg#+ZC`@SwQ*YRG6mGM=Bz} zDc7Q9?b}uhOMkKd$+CtAx$bY}z2_t!{YwoKq4wb)hX1X0PE6|YHF^^B`Da`7XgLp)qvM5GWv&5z?3RUX^Aly?!e| zka!fjdH!&Sb%?>32!CUkfOP1?8t8v5=0Y>0Bi)qr(F^#q4m^6?Xyf+cSgmM%L%Ls& zndZ50YnigsdAO&_{v1STtOJvhO@pgP3lrlJFNlQyGjm*y7|*Qt_${Ng9rhz-#GO(O^r!eR zo*z1EFwh*-oD~HC9ld=-JdVaOF{Ek@6~R zxFRQgt*tn&^@bG16w;ekyPpse@El!~5N%`Y5SGtzki|wP=p{l(1e4UzZ+_VRK*nXk z+q4M5Q!c~jpNVr&ejg-Jw`Ts5t>a=kzmPbc<2L>0aM;84s)OAoc3F{d(C>cIj2{@=rJ`5`!6u6T-zA@iq zCWFHyqy66-_rFc>Q)BBLb{E+mK21QivMUwrE-RORmO8PD%MI!uTk%h^bU1p>g_dH9 zQ}LnIM=ENeduQUVml2=M_oJm_?5$F5?x)qihRcFfOoQZTabEC(K)xAkJ z*KW-p@Ne~2;H*e!l`C6)G)jJOf_8XiAa)KqO(Lw)8s;<*iStcycvddcY#+xpAV(C= znR70hOCCn72gBpij#|YQG6k(u(stTL2KDbqHTZBw6RrDepqaFhka@YqqJ=cG=Jul* z5G;b7hyke}yyQjW2(IC}7SBnQXUsL>fQ-xiO%6ix5RSE6ZQ0%64ars>H`)o^w2n^2 z?VQ@0MV2iO*T+?kZAl%y{C6j{hl?^Tr3zYAhdPPrJu9ke9!O6bu@oYzYG6`T4Se>j z*S2{0mRa@s%|VC#S&{s-R(}0jdg>UZ$xAz02$54Lf$D2T{Rh1fNrq+~ito+F8(JzY zWp#;Sb4h)g`PB7n$BtMj2)9SpUYW%$B>`uNgihV0>Y8KLo&`_#lvf1 zEeX`;(D?6u)_%@F&bmoBwY<*C)1CS38Os@KiAyid19dHO)r)K-F*O{PrU4NJCX@Z7 zSVt{nFAoolS6jWD!hSHr1$aH4$`p2>!B= zaBj_$z@QhE1mX`Wl_F_Q?IaV#VB99Leh-DAQlA9EV??r{Fba#5b%^Wud#tPkyb?TJ zS@GI$yU}638Ol1LS8?=#KTdOKktUiBVk|gFbK=Sk*|2YwUj2PRMBOWLNVGVOATKF1 zAMplU_I?6}hO9#Rcvr4*7q02F7h7sJ+gY6sH}g6=j@%|?NyIKM^j5m1X|WYCU8`F| z6r1w|Y2`7|Vr=vaPSwe9&!D6Z(n+A?M(omAz@a#G6KlYu18izZY&R0hGq{#CtoGJA z7Ie-#S#BmNdAvth8M3 z;t@!G?a4_HVc99n4{%8@F??`pcxGpidLRbSpBLxD=E|`r%s?k)O{AVM6Ak9`OLxST zaFKy^TY3l?rt@pnG?Kul3`EwxCBJ=8zE@2NveCQcN{M!ogmQWO5X+s}F@8A58mQUv zW?s)#eyUP_BCE_{W7&3@?m$`^Sky|+@zT6}oOmLx47Y00wyJ&={%N>}7<^GEMZAR% z8xgACwjBV+J{lGpuLiiMk)Kv6O4t7t86M|v%MY{=tk9LnH z*EdXIBa0^2FNV8hw+!+XSlU)vw(2CT6uKMiF-{oBJdhH)TI}J0Ot5@Aps%$Nm8x&c(2lIBQuCp}w zHZ@C?U1cx*zCpnrdF(+fmrrzAsh5c z{GDj^Se`ChdR{!Hm1q*la^;o6UG^BCWV1~4d$r|w=rkSJme=8R{6xv36Z>_nl%_FJDvZL0U=`I&KBLP-r_Iyt)Wy1i?Bj0MJwj0Fi!}mrk@nRRrs}V!RXF{+qef*Wurvh z#L#9pe_L%z!TxnVkC{$PVR%RNyufxB|j{1QF9=pjQVAB8_% ztQ|ma%NZM!%7GawYK*PVPb-En_EnvqHPG*>?~F8FS32*b__=1XdGAl->I4o=+sLJq zeM1|L-7r?hRVT-rsblP1Es7R4E;Bh;C?DtJof2M-WS6@D$72TLwdUA`)XWuoL{ICI zHq*e+4N^Aq@QBMN3QMNYy!!@7mD&8Dd9HJ+UMNWl~`oGT%`sc{G|Q}ok%AexL?Ipqe#L@aM@%HqRW{1#h0?@*5}&RMeSS$ zlj`9SW9jW&*&5RuyQ@pan>^D2W;fS@6^cBi>@8;K`5R5D3QYam7&$m{s?)OMC|aoL zCa+*`K5v0pPgF_3z=*pUHPrHr>^ejhTNR!Wgw*QloQ<&7Foz*xWwiX>D0F(w;{Nzq z!X1`A+4H;BfLSG6#$pX4X_d)~ik9lBYqufO;UmNC$jVkv#pZ0n=Q3VXI_q2o(=LO? zu*F@717;4XPptE9{%Y`>?u}pVnuZSy!wjDeJt~Xh0bNxjI%aMnZddpJ zk4aTNJZTTHHyIKs>S&Yu_Z~O`*~JvlaV22c>q31Agh(E7yR0sy;_k6Ou6TOaNvXZ& zmC|E_Y4W|SqRfIA%*!Dg0NTPmz}aP2NZ=*+8na)fsuZCeeOf1~Z) z245by>t4R#4K8D5oM{G$*KD6&V5?J}par)2R(wa=Gpr0P zc^IgZ-hw|7I&}_|QdZ?sC!Bl;$THo>tML3C#{-6meq_RYBw zvaigotTu7)4#pbpQ7PA?j)ue+RMUSXCMVPOffl!}#%#-Fh0y+7JI3Q=vy|*imqOEM z_EPPP%~}uKKa2{t<`ol!zs)OfJu2Tb*Ritg?(qHNdpq`?HyHo)M0cSFtam=?y@`FK zgpj4uk}Kh!{zg|$P=fKKE3qVM%RY(=mOi1S+%vZz-tXZWZX*094H#f5rMEsKL$ur% ztSwucJl$T^PoY9ju5|xoSsv*0UL20)JbC|V04rN#3a!O(v4V73D%KESEjWx$(C|

+sNw*S-ShNDxa3=w&B$m6=RBtWjKwr}8r;`MWJ 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 NU@lpE0nC1&{|6kSusZ+% From 25cb2572a39217b1548d10a426e199bcbd7cd7ea Mon Sep 17 00:00:00 2001 From: Aswathi Balagopal Date: Tue, 16 Sep 2025 13:29:28 -0500 Subject: [PATCH 3/5] ignore build and dist, add self along with par_name --- .gitignore | 3 +++ mla/threeml/profilellh.py | 13 +++++-------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index 63265039..c569ea6b 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,6 @@ pyvenv.cfg build/ dist/ + +build/ +dist/ diff --git a/mla/threeml/profilellh.py b/mla/threeml/profilellh.py index 3bfe3862..833f067f 100644 --- a/mla/threeml/profilellh.py +++ b/mla/threeml/profilellh.py @@ -45,18 +45,15 @@ def __init__( super().__init__(name, nuisance_parameters) if spline is not None: self.spline = spline - self.df = None + self.df = df#None else: self.df = df self.par_name = list(df.columns) self.par_name.pop() - listofpoint = [] - shape = [] - for n in self.par_name: - points = np.unique(df[n]) - listofpoint.append(points) - shape.append(points.shape[0]) - llh = np.reshape(df["llh"].values, shape) + listofpoint = [np.unique(df[n]) for n in self.par_name] + shape = [len(points) for points in listofpoint] + sort_idx = np.lexsort([df[p].values for p in reversed(self.par_name)]) + llh = np.reshape(df["llh"].values[sort_idx], shape) self.spline = RegularGridInterpolator( listofpoint, llh, bounds_error=False, fill_value=fill_value ) From 7d83d4eb4fa3da0c61fc11dc92c1e24b19419f5a Mon Sep 17 00:00:00 2001 From: Aswathi Balagopal Date: Tue, 16 Sep 2025 16:58:53 -0500 Subject: [PATCH 4/5] fixing linting issues --- mla/analysis.py | 20 +++-- mla/core.py | 3 +- mla/data_handlers.py | 56 ++++++++---- mla/minimizers.py | 32 +++++-- mla/params.py | 12 ++- mla/sob_terms.py | 25 ++++-- mla/sources.py | 8 +- mla/threeml/IceCubeLike.py | 144 +++++++++++++++++------------ mla/threeml/data_handlers.py | 12 ++- mla/threeml/sob_terms.py | 169 ++++++++++++++++++++--------------- mla/time_profiles.py | 71 +++++++++------ mla/trial_generators.py | 11 ++- mla/utility_functions.py | 17 ++-- 13 files changed, 359 insertions(+), 221 deletions(-) diff --git a/mla/analysis.py b/mla/analysis.py index 423bad84..1ae94e15 100644 --- a/mla/analysis.py +++ b/mla/analysis.py @@ -29,9 +29,12 @@ class SingleSourceLLHAnalysis: sob_term_factories: List[SoBTermFactory] data_handler_source: Tuple[DataHandler, PointSource] _sob_term_factories: List[SoBTermFactory] = field(init=False, repr=False) - _data_handler_source: Tuple[DataHandler, PointSource] = field(init=False, repr=False) - _trial_generator: SingleSourceTrialGenerator = field(init=False, repr=False) - _test_statistic_factory: LLHTestStatisticFactory = field(init=False, repr=False) + _data_handler_source: Tuple[DataHandler, + PointSource] = field(init=False, repr=False) + _trial_generator: SingleSourceTrialGenerator = field( + init=False, repr=False) + _test_statistic_factory: LLHTestStatisticFactory = field( + init=False, repr=False) def produce_and_minimize( self, @@ -66,10 +69,12 @@ def sob_term_factories(self) -> List[SoBTermFactory]: return self._sob_term_factories @sob_term_factories.setter - def sob_term_factories(self, sob_term_factories: List[SoBTermFactory]) -> None: + def sob_term_factories( + self, + sob_term_factories: List[SoBTermFactory]) -> None: """Docstring""" self._sob_term_factories = sob_term_factories - self._test_statistic_factory = LLHTestStatisticFactory( # pylint: disable=too-many-function-args + self._test_statistic_factory = LLHTestStatisticFactory( self.config['LLHTestStatisticFactory'], self._sob_term_factories, ) @@ -80,8 +85,9 @@ def data_handler_source(self) -> Tuple[DataHandler, PointSource]: return self._data_handler_source @data_handler_source.setter - def data_handler_source( - self, data_handler_source: Tuple[DataHandler, PointSource]) -> None: + def data_handler_source(self, + data_handler_source: Tuple[DataHandler, + PointSource]) -> None: """Docstring""" self._data_handler_source = data_handler_source self._trial_generator = SingleSourceTrialGenerator( diff --git a/mla/core.py b/mla/core.py index d79be033..13508ffa 100644 --- a/mla/core.py +++ b/mla/core.py @@ -6,4 +6,5 @@ def generate_default_config(classes: list) -> dict: """Docstring""" return { - c.__name__: c.generate_config() for c in classes if issubclass(c, Configurable)} + c.__name__: c.generate_config() + for c in classes if issubclass(c, Configurable)} diff --git a/mla/data_handlers.py b/mla/data_handlers.py index c1377142..e4a7cb96 100644 --- a/mla/data_handlers.py +++ b/mla/data_handlers.py @@ -31,7 +31,10 @@ class DataHandler(configurable.Configurable): __metaclass__ = abc.ABCMeta @abc.abstractmethod - def sample_background(self, n: int, rng: np.random.Generator) -> np.ndarray: + def sample_background( + self, + n: int, + rng: np.random.Generator) -> np.ndarray: """Docstring""" @abc.abstractmethod @@ -47,7 +50,8 @@ def evaluate_background_sindec_pdf(self, events: np.ndarray) -> np.ndarray: """Docstring""" @abc.abstractmethod - def build_background_sindec_logenergy_histogram(self, bins: np.ndarray) -> np.ndarray: + def build_background_sindec_logenergy_histogram( + self, bins: np.ndarray) -> np.ndarray: """Docstring""" @abc.abstractmethod @@ -77,7 +81,10 @@ class NuSourcesDataHandler(DataHandler): _livetime: float = field(init=False, repr=False) _sin_dec_bins: np.ndarray = field(init=False, repr=False) - def sample_background(self, n: int, rng: np.random.Generator) -> np.ndarray: + def sample_background( + self, + n: int, + rng: np.random.Generator) -> np.ndarray: """Docstring""" return rng.choice(self._data, n) @@ -105,7 +112,8 @@ def evaluate_background_sindec_pdf(self, events: np.ndarray) -> np.ndarray: """ return (1 / (2 * np.pi)) * self._dec_spline(events['sindec']) - def build_background_sindec_logenergy_histogram(self, bins: np.ndarray) -> np.ndarray: + def build_background_sindec_logenergy_histogram( + self, bins: np.ndarray) -> np.ndarray: """Docstring""" return np.histogram2d( self._data['sindec'], @@ -188,14 +196,18 @@ def _cut_sim_dec(self) -> None: self._sim = self._full_sim[close].copy() self._sim['ow'] /= 2 * np.pi * (np.min([np.sin( - self.config['dec_cut_location'] + self.config['dec_bandwidth (rad)'] + self.config['dec_cut_location'] + + self.config['dec_bandwidth (rad)'] ), 1]) - np.max([np.sin( - self.config['dec_cut_location'] - self.config['dec_bandwidth (rad)'] + self.config['dec_cut_location'] - + self.config['dec_bandwidth (rad)'] ), -1])) self._sim['weight'] /= 2 * np.pi * (np.min([np.sin( - self.config['dec_cut_location'] + self.config['dec_bandwidth (rad)'] + self.config['dec_cut_location'] + + self.config['dec_bandwidth (rad)'] ), 1]) - np.max([np.sin( - self.config['dec_cut_location'] - self.config['dec_bandwidth (rad)'] + self.config['dec_cut_location'] - + self.config['dec_bandwidth (rad)'] ), -1])) else: self._sim = self._full_sim @@ -208,7 +220,8 @@ def data_grl(self) -> Tuple[np.ndarray, np.ndarray]: @data_grl.setter def data_grl(self, data_grl: Tuple[np.ndarray, np.ndarray]) -> None: """Docstring""" - self._sin_dec_bins = np.linspace(-1, 1, 1 + self.config['sin_dec_bins']) + self._sin_dec_bins = np.linspace(-1, 1, + 1 + self.config['sin_dec_bins']) self._data = data_grl[0].copy() self._grl = data_grl[1].copy() if 'sindec' not in self._data.dtype.names: @@ -232,7 +245,8 @@ def data_grl(self, data_grl: Tuple[np.ndarray, np.ndarray]) -> None: self._data['sindec'], bins=self._sin_dec_bins, density=True) bin_centers = bins[:-1] + np.diff(bins) / 2 - self._dec_spline = Spline(bin_centers, hist, **self.config['dec_spline_kwargs']) + self._dec_spline = Spline( + bin_centers, hist, **self.config['dec_spline_kwargs']) @property def livetime(self) -> float: @@ -301,7 +315,8 @@ def background_time_profile(self, profile: GenericProfile) -> None: background_grl = self._grl[background_run_mask] self._n_background = background_grl['events'].sum() self._n_background /= background_grl['livetime'].sum() - self._n_background *= self._contained_livetime(*profile.range, background_grl) + self._n_background *= self._contained_livetime( + *profile.range, background_grl) self._background_time_profile = copy.deepcopy(profile) @property @@ -362,7 +377,10 @@ def _contained_livetime( return contained_livetime - def sample_background(self, n: int, rng: np.random.Generator) -> np.ndarray: + def sample_background( + self, + n: int, + rng: np.random.Generator) -> np.ndarray: """Docstring""" events = super().sample_background(n, rng) return self._randomize_times(events, self._background_time_profile) @@ -370,9 +388,6 @@ def sample_background(self, n: int, rng: np.random.Generator) -> np.ndarray: def sample_signal(self, n: int, rng: np.random.Generator) -> np.ndarray: """Docstring""" self.events = super().sample_signal(n, rng) - #print('in data handlers events', self.events.dtype) - #print('signal time profile pdf',self._signal_time_profile.pdf(self._grl['start'])) - #print(self._randomize_times(self.events, self._signal_time_profile)) return self._randomize_times(self.events, self._signal_time_profile) def _randomize_times( @@ -382,10 +397,13 @@ def _randomize_times( ) -> np.ndarray: self.grl_start_cdf = time_profile.cdf(self._grl['start']) self.grl_stop_cdf = time_profile.cdf(self._grl['stop']) - self.valid = np.logical_and(self.grl_start_cdf <= 1, self.grl_stop_cdf >= 0) - self.rates = self.grl_stop_cdf[self.valid] - self.grl_start_cdf[self.valid] - #print(self.rates, self.rates.sum()) - #print(len(events)) + self.valid = np.logical_and( + self.grl_start_cdf <= 1, + self.grl_stop_cdf >= 0) + self.rates = self.grl_stop_cdf[self.valid] - \ + self.grl_start_cdf[self.valid] + # print(self.rates, self.rates.sum()) + # print(len(events)) runs = np.random.choice( self._grl[self.valid], size=len(events), diff --git a/mla/minimizers.py b/mla/minimizers.py index f91f973c..2ffdb14d 100644 --- a/mla/minimizers.py +++ b/mla/minimizers.py @@ -33,26 +33,31 @@ class Minimizer(configurable.Configurable): @abc.abstractmethod def __call__( - self, fitting_params: Optional[List[str]] = None) -> Tuple[float, np.ndarray]: + self, fitting_params: Optional[List[str]] = None + ) -> Tuple[float, np.ndarray]: """Docstring""" @dataclasses.dataclass class GridSearchMinimizer(Minimizer): """Docstring""" + def __call__( - self, fitting_params: Optional[List[str]] = None) -> Tuple[float, np.ndarray]: + self, fitting_params: Optional[List[str]] = None + ) -> Tuple[float, np.ndarray]: """Docstring""" if fitting_params is None: fitting_key_idx_map = self.test_statistic.params.key_idx_map fitting_bounds = self.test_statistic.params.bounds else: fitting_key_idx_map = { - key: val for key, val in self.test_statistic.params.key_idx_map.items() + key: val + for key, val in self.test_statistic.params.key_idx_map.items() if key in fitting_params } fitting_bounds = { - key: val for key, val in self.test_statistic.params.bounds.items() + key: val + for key, val in self.test_statistic.params.bounds.items() if key in fitting_params } @@ -72,13 +77,22 @@ def __call__( ]) return self._minimize( - points[grid_ts_values.argmin()], fitting_key_idx_map, fitting_bounds) + points[grid_ts_values.argmin()], + fitting_key_idx_map, fitting_bounds) - def _eval_test_statistic(self, point: np.ndarray, fitting_key_idx_map: dict) -> float: + def _eval_test_statistic( + self, + point: np.ndarray, + fitting_key_idx_map: dict) -> float: """Docstring""" - return self.test_statistic(self._param_values(point, fitting_key_idx_map)) - - def _param_values(self, point: np.ndarray, fitting_key_idx_map: dict) -> np.ndarray: + return self.test_statistic( + self._param_values( + point, fitting_key_idx_map)) + + def _param_values( + self, + point: np.ndarray, + fitting_key_idx_map: dict) -> np.ndarray: """Docstring""" param_values = self.test_statistic.params.value_array.copy() diff --git a/mla/params.py b/mla/params.py index ad7fadf0..c349ca6e 100644 --- a/mla/params.py +++ b/mla/params.py @@ -37,7 +37,10 @@ def names(self) -> List[str]: return [*self.key_idx_map] @classmethod - def from_dict(cls, value_dict: dict, bounds: Optional[dict] = None) -> 'Params': + def from_dict( + cls, + value_dict: dict, + bounds: Optional[dict] = None) -> 'Params': """Docstring""" value_array = np.array(list(value_dict.values())) key_idx_map = {key: i for i, key in enumerate(value_dict)} @@ -50,8 +53,11 @@ def from_array( bounds: Optional[dict] = None, ) -> 'Params': """Docstring""" - value_array = rf.structured_to_unstructured(named_value_array, copy=True)[0] - key_idx_map = {name: i for i, name in enumerate(named_value_array.dtype.names)} + value_array = rf.structured_to_unstructured( + named_value_array, copy=True)[0] + key_idx_map = { + name: i for i, name in enumerate( + named_value_array.dtype.names)} return cls._build_params(value_array, key_idx_map, bounds) @classmethod diff --git a/mla/sob_terms.py b/mla/sob_terms.py index 5a022e2f..09a8dd5a 100644 --- a/mla/sob_terms.py +++ b/mla/sob_terms.py @@ -173,7 +173,10 @@ def calculate_drop_mask(self, events: np.ndarray) -> np.ndarray: return 1 / self.background_time_profile.pdf(events['time']) != 0 def generate_params(self) -> tuple: - return self.signal_time_profile.params, self.signal_time_profile.param_bounds + return ( + self.signal_time_profile.params, + self.signal_time_profile.param_bounds + ) @dataclasses.dataclass @@ -233,8 +236,10 @@ def __post_init__(self) -> None: def __call__(self, params: Params, events: np.ndarray) -> SoBTerm: """Docstring""" - sin_dec_idx = np.searchsorted(self._sin_dec_bins[:-1], events['sindec']) - log_energy_idx = np.searchsorted(self._log_energy_bins[:-1], events['logE']) + sin_dec_idx = np.searchsorted( + self._sin_dec_bins[:-1], events['sindec']) + log_energy_idx = np.searchsorted( + self._log_energy_bins[:-1], events['logE']) spline_idxs, event_spline_idxs = np.unique( [sin_dec_idx - 1, log_energy_idx - 1], @@ -276,7 +281,8 @@ def _init_sob_map( An array of signal-over-background values binned in sin(dec) and log(energy) for a given gamma. """ - sig_h = self.data_handler.build_signal_sindec_logenergy_histogram(gamma, bins) + sig_h = self.data_handler.build_signal_sindec_logenergy_histogram( + gamma, bins) # Normalize histogram by dec band sig_h /= np.sum(sig_h, axis=1)[:, None] @@ -292,7 +298,10 @@ def _init_sob_map( good_bins, good_vals = bin_centers[good], ratio[i][good] # Do a linear interpolation across the energy range - spline = Spline(good_bins, good_vals, **self.config['energy_spline_kwargs']) + spline = Spline( + good_bins, + good_vals, + **self.config['energy_spline_kwargs']) # And store the interpolated values ratio[i] = spline(bin_centers) @@ -306,7 +315,8 @@ def _init_spline_map(self) -> List[List[Spline]]: """ bins = np.array([self._sin_dec_bins, self._log_energy_bins]) bin_centers = bins[1, :-1] + np.diff(bins[1]) / 2 - bg_h = self.data_handler.build_background_sindec_logenergy_histogram(bins) + bg_h = self.data_handler.build_background_sindec_logenergy_histogram( + bins) # Normalize histogram by dec band bg_h /= np.sum(bg_h, axis=1)[:, None] @@ -323,7 +333,8 @@ def _init_spline_map(self) -> List[List[Spline]]: transposed_log_sob_maps = np.log(sob_maps.transpose(1, 2, 0)) splines = [[ - Spline(self._gamma_bins, log_ratios, **self.config['sob_spline_kwargs']) + Spline(self._gamma_bins, log_ratios, + **self.config['sob_spline_kwargs']) for log_ratios in dec_bin ] for dec_bin in transposed_log_sob_maps] diff --git a/mla/sources.py b/mla/sources.py index ee937d76..686c74fb 100644 --- a/mla/sources.py +++ b/mla/sources.py @@ -22,13 +22,18 @@ @dataclasses.dataclass class PointSource(configurable.Configurable): """Stores a source object name and location""" + def sample(self, size: int = 1) -> tuple: """Sample locations. Args: size: number of points to sample """ - return (np.ones(size) * self.config['ra'], np.ones(size) * self.config['dec']) + return ( + np.ones(size) * + self.config['ra'], + np.ones(size) * + self.config['dec']) def spatial_pdf(self, events: np.ndarray) -> np.ndarray: """calculates the signal probability of events. @@ -72,6 +77,7 @@ def generate_config(cls) -> dict: @dataclasses.dataclass class GaussianExtendedSource(PointSource): """Gaussian Extended Source""" + def sample(self, size: int = 1) -> np.ndarray: """Sample locations. diff --git a/mla/threeml/IceCubeLike.py b/mla/threeml/IceCubeLike.py index 8c7439ce..0fc40acf 100644 --- a/mla/threeml/IceCubeLike.py +++ b/mla/threeml/IceCubeLike.py @@ -11,8 +11,8 @@ __email__ = 'klfan@terpmail.umd.edu' __status__ = 'Development' -#from __future__ import print_function -#from __future__ import division +# from __future__ import print_function +# from __future__ import division from past.utils import old_div import collections import scipy @@ -38,15 +38,16 @@ from mla import sources from mla import minimizers from mla import trial_generators -from mla.utility_functions import newton_method, newton_method_multidataset +from mla.utility_functions import newton_method_multidataset __all__ = ["NeutrinoPointSource"] r"""This IceCube plugin is currently under develop by Kwok Lung Fan""" -class NeutrinoPointSource(PointSource,Node): +class NeutrinoPointSource(PointSource, Node): """ - Class for NeutrinoPointSource. It is inherited from astromodels PointSource class. + Class for NeutrinoPointSource. + It is inherited from astromodels PointSource class. """ def __init__( @@ -69,10 +70,12 @@ def __init__( source_name:Name of the source ra: right ascension in degree dec: declination in degree - spectral_shape: Shape of the spectrum.Check 3ML example for more detail. + spectral_shape: + Shape of the spectrum.Check 3ML example for more detail. l: galactic longitude in degree b: galactic in degree - components: Spectral Component.Check 3ML example for more detail. + components: + Spectral Component.Check 3ML example for more detail. sky_position: sky position energy_unit: Unit of the energy """ @@ -86,7 +89,8 @@ def __init__( (ra is not None and dec is not None) ^ (l is not None and b is not None) ^ (sky_position is not None) - ), "You have to provide one and only one specification for the position" + ), "You have to provide one and only one \ + specification for the position" # Gather the position @@ -104,10 +108,10 @@ def __init__( except (TypeError, ValueError): raise AssertionError( - "RA and Dec must be numbers. If you are confused by this message," - " you are likely using the constructor in the wrong way. Check" - " the documentation." - ) + "RA and Dec must be numbers. " + "If you are confused by this message, " + "you are likely using the constructor in the wrong way" + ". Check the documentation.") sky_position = SkyDirection(ra=ra, dec=dec) @@ -119,7 +123,8 @@ def __init__( # Now gather the component(s) - # We need either a single component, or a list of components, but not both + # We need either a single component, + # or a list of components, but not both # (that's the ^ symbol) assert (spectral_shape is not None) ^ (components is not None), ( @@ -157,7 +162,8 @@ def __init__( # Components in this case have energy as x and differential flux as y x_unit = energy_unit - y_unit = (energy_unit * current_units.area * current_units.time) ** (-1) + y_unit = (energy_unit * current_units.area * + current_units.time) ** (-1) # Now set the units of the components for component in list(self._components.values()): @@ -199,8 +205,8 @@ def call(self, x, tag=None): # Slow version with units results = [ - component.shape(x) for component in list(self.components.values()) - ] + component.shape(x) for component in list( + self.components.values())] # We need to sum like this (slower) because using # np.sum will not preserve the units (thanks astropy.units) @@ -209,12 +215,13 @@ def call(self, x, tag=None): else: - # Fast version without units, where x is supposed to be in the same + # Fast version without units, + # where x is supposed to be in the same # units as currently defined in units.get_units() results = [ - component.shape(x) for component in list(self.components.values()) - ] + component.shape(x) for component in list( + self.components.values())] return np.sum(results, 0) @@ -278,7 +285,8 @@ def __init__( # Now gather the component(s) - # We need either a single component, or a list of components, but not both + # We need either a single component, + # or a list of components, but not both # (that's the ^ symbol) assert (spectral_shape is not None) ^ (components is not None), ( @@ -291,11 +299,11 @@ def __init__( components = [SpectralComponent("main", spectral_shape)] - # Components in this case have energy as x and differential flux as y + # Components in this case have energy as x and differential flux as + # y - diff_flux_units = (current_u.energy * current_u.area * current_u.time) ** ( - -1 - ) + diff_flux_units = (current_u.energy * + current_u.area * current_u.time) ** (-1) # Now set the units of the components for component in components: @@ -345,7 +353,6 @@ def spatial_shape(self): return self._spatial_shape def get_spatially_integrated_flux(self, energies): - """ Returns total flux of source at the given energy :param energies: energies (array or float) @@ -399,7 +406,8 @@ def __call__(self, lon, lat, energies): # Slow version with units # We need to sum like this (slower) because - # using np.sum will not preserve the units (thanks astropy.units) + # using np.sum will not preserve the units (thanks + # astropy.units) result = np.zeros((lat.shape[0], energies.shape[0])) * ( u.keV ** -1 * u.cm ** -2 * u.second ** -1 * u.degree ** -2 @@ -507,7 +515,8 @@ def _repr__base(self, rich_output=False): repr_dict[key]["spectrum"] = collections.OrderedDict() for component_name, component in list(self.components.items()): - repr_dict[key]["spectrum"][component_name] = component.to_dict(minimal=True) + repr_dict[key]["spectrum"][component_name] = component.to_dict( + minimal=True) return dict_to_list(repr_dict, rich_output) @@ -529,11 +538,15 @@ def __init__(self, likelihood_model_instance, A=1): r"""Constructor of the class""" self.model = likelihood_model_instance self.norm = A - for source_name, source in likelihood_model_instance.point_sources.items(): + for source_name, source in ( + likelihood_model_instance.point_sources.items() + ): if isinstance(source, NeutrinoPointSource): self.neutrinosource = source_name self.point = True - for source_name, source in likelihood_model_instance.extended_sources.items(): + for source_name, source in ( + likelihood_model_instance.extended_sources.items() + ): if isinstance(source, NeutrinoExtendedSource): self.neutrinosource = source_name self.point = False @@ -541,9 +554,8 @@ def __init__(self, likelihood_model_instance, A=1): def __call__(self, energy, **kwargs): r"""Evaluate spectrum at E""" if self.point: - return ( - self.model.point_sources[self.neutrinosource].call(energy) * self.norm - ) + return (self.model.point_sources[self.neutrinosource].call( + energy) * self.norm) else: return ( self.model.extended_sources[self.neutrinosource].call(energy) @@ -555,7 +567,8 @@ def validate(self): def __str__(self): r"""String representation of class""" - return "SpectrumConverter class doesn't support string representation now" + return "SpectrumConverter class doesn't \ + support string representation now" def copy(self): r"""Return copy of this class""" @@ -582,9 +595,11 @@ def __init__( name: name for the plugin data: data of experiment data_handlers: mla.threeml.data_handlers ThreeMLDataHandler object - llh: test_statistics.LLHTestStatistic object. Used to evaluate the ts + llh: test_statistics.LLHTestStatistic object. + Used to evaluate the ts source: injection location(only when need injection) - livetime: livetime in days(calculated using livetime within time profile if None) + livetime: livetime in days(calculated using + livetime within time profile if None) fix_flux_norm: only fit the spectrum shape verbose: print the output or not @@ -614,13 +629,14 @@ def __init__( config["dec"] = 0 source = sources.PointSource(config=config) self.injected_source = source - trial_config = trial_generators.SingleSourceTrialGenerator.generate_config() + trial_config = \ + trial_generators.SingleSourceTrialGenerator.generate_config() self.trial_generator = trial_generators.SingleSourceTrialGenerator( trial_config, data_handlers, source ) - analysis_config = analysis.SingleSourceLLHAnalysis.generate_default_config( - minimizer_class=minimizers.GridSearchMinimizer - ) + analysis_config = \ + analysis.SingleSourceLLHAnalysis.generate_default_config( + minimizer_class=minimizers.GridSearchMinimizer) self.analysis = analysis.SingleSourceLLHAnalysis( config=analysis_config, minimizer_class=minimizers.GridSearchMinimizer, @@ -644,8 +660,8 @@ def __init__( ) for key in self.test_statistic.sob_terms.keys(): if isinstance( - self.test_statistic.sob_terms[key], sob_terms.ThreeMLPSEnergyTerm - ): + self.test_statistic.sob_terms[key], + sob_terms.ThreeMLPSEnergyTerm): self.energyname = key return @@ -655,7 +671,9 @@ def set_model(self, likelihood_model_instance): return - for source_name, source in likelihood_model_instance.point_sources.items(): + for source_name, source in ( + likelihood_model_instance.point_sources.items() + ): if isinstance(source, NeutrinoPointSource): self.source_name = source_name ra = source.position.get_ra() @@ -688,13 +706,16 @@ def set_model(self, likelihood_model_instance): self.test_statistic = self.analysis.test_statistic_factory( Params.from_dict({"ns": 0}), self._data ) - for source_name, source in likelihood_model_instance.extended_sources.items(): + for source_name, source in ( + likelihood_model_instance.extended_sources.items() + ): if isinstance(source, NeutrinoExtendedSource): self.source_name = source_name ra = source.spatial_shape.lon0.value dec = source.spatial_shape.lat0.value sigma = source.spatial_shape.sigma.value - if self._ra == ra and self._dec == dec and self._sigma == sigma: + if (self._ra == ra and self._dec == dec + and self._sigma == sigma): self.llh_model = likelihood_model_instance self.energy_sob_factory.spectrum = Spectrum( likelihood_model_instance @@ -813,8 +834,11 @@ class icecube_analysis(PluginPrototype): """Docstring""" def __init__( - self, listoficecubelike, newton_flux_norm=False, name="combine", verbose=False - ): + self, + listoficecubelike, + newton_flux_norm=False, + name="combine", + verbose=False): """Docstring""" nuisance_parameters = {} super(icecube_analysis, self).__init__(name, nuisance_parameters) @@ -851,9 +875,9 @@ def get_log_like(self, verbose=None): for i, icecubeobject in enumerate(self.listoficecubelike): sob.append(icecubeobject.test_statistic._calculate_sob()) n_drop.append(icecubeobject.test_statistic.n_dropped) - fraction.append( - self.totaln * self.dataset_weight[i] / len(icecubeobject.data) - ) + fraction.append(self.totaln * + self.dataset_weight[i] / + len(icecubeobject.data)) fraction = np.array(fraction) # fraction = fraction/fraction.sum() ns_ratio = newton_method_multidataset(sob, n_drop, fraction) @@ -945,9 +969,9 @@ def injection(self, n_signal=0, flux_norm=None, poisson=False): ratio_injection = self.dataset_ratio * n_signal for i, icecubeobject in enumerate(self.listoficecubelike): icecubeobject.trial_generator.config["fixed_ns"] = True - #print('in IceCubelike test injection', n_signal) + # print('in IceCubelike test injection', n_signal) injection_signal = np.random.poisson(ratio_injection[i]) - #print(injection_signal) + # print(injection_signal) tempdata = icecubeobject.trial_generator(injection_signal) self.listoficecubelike[i].update_data(tempdata) else: @@ -962,9 +986,10 @@ def cal_injection_ns(self, flux_norm): for icecubeobject in self.listoficecubelike: time_intergrated = flux_norm * icecubeobject.livetime * 3600 * 24 tempns = ( - time_intergrated - * icecubeobject.analysis.data_handler_source[0].sim["weight"].sum() - ) + time_intergrated * + icecubeobject.analysis + .data_handler_source[0] + .sim["weight"].sum()) ns = ns + tempns return ns @@ -973,10 +998,11 @@ def cal_injection_fluxnorm(self, ns): totalweight = 0 for icecubeobject in self.listoficecubelike: tempweight = ( - icecubeobject.analysis.data_handler_source[0].sim["weight"].sum() - * icecubeobject.livetime - * 3600 - * 24 - ) + icecubeobject.analysis + .data_handler_source[0] + .sim["weight"].sum() * + icecubeobject.livetime * + 3600 * + 24) totalweight = totalweight + tempweight return ns / totalweight diff --git a/mla/threeml/data_handlers.py b/mla/threeml/data_handlers.py index 8d65bd1c..ff008dfd 100644 --- a/mla/threeml/data_handlers.py +++ b/mla/threeml/data_handlers.py @@ -49,7 +49,8 @@ def build_signal_energy_histogram( ) -> np.ndarray: """ Building the signal energy histogram. - Only used when using MC instead of IRF to build signal energy histogram. + Only used when using MC instead of IRF + to build signal energy histogram. Args: spectrum: signal spectrum @@ -64,7 +65,8 @@ def build_signal_energy_histogram( density=True, )[0] - def cut_reconstructed_sim(self, dec: float, sampling_width: float) -> np.ndarray: + def cut_reconstructed_sim( + self, dec: float, sampling_width: float) -> np.ndarray: """ Cutting the MC based on reconstructed dec. Only use when using MC instead of IRF to build signal energy histogram. @@ -103,7 +105,8 @@ def injection_spectrum(self) -> spectral.BaseSpectrum: return self._injection_spectrum @injection_spectrum.setter - def injection_spectrum(self, inject_spectrum: spectral.BaseSpectrum) -> None: + def injection_spectrum( + self, inject_spectrum: spectral.BaseSpectrum) -> None: """ Setting the injection spectrum @@ -124,7 +127,8 @@ def injection_spectrum(self, inject_spectrum: spectral.BaseSpectrum) -> None: self._full_sim["weight"] = ( self._full_sim["ow"] - * (inject_spectrum(self._full_sim["trueE"] * self._flux_unit_conversion)) + * (inject_spectrum(self._full_sim["trueE"] + * self._flux_unit_conversion)) * self._flux_unit_conversion ) diff --git a/mla/threeml/sob_terms.py b/mla/threeml/sob_terms.py index 166575b3..6b45b735 100644 --- a/mla/threeml/sob_terms.py +++ b/mla/threeml/sob_terms.py @@ -105,30 +105,38 @@ class ThreeMLPSEnergyTermFactory(ThreeMLBaseEnergyTermFactory): def __post_init__(self) -> None: """Docstring""" if self.config["list_sin_dec_bins"] is None: - self._sin_dec_bins = np.linspace(-1, 1, 1 + self.config["sin_dec_bins"]) + self._sin_dec_bins = np.linspace( + -1, 1, 1 + self.config["sin_dec_bins"]) else: self._sin_dec_bins = self.config["list_sin_dec_bins"] if self.config["list_log_energy_bins"] is None: self._log_energy_bins = np.linspace( - *self.config["log_energy_bounds"], 1 + self.config["log_energy_bins"] - ) + *self.config["log_energy_bounds"], + 1 + self.config["log_energy_bins"]) else: self._log_energy_bins = self.config["list_log_energy_bins"] - self.data_handler.reduced_reco_sim = self.data_handler.cut_reconstructed_sim( - self.source.location[1], - self.data_handler.config["reco_sampling_width"], - ) + self.data_handler.reduced_reco_sim = \ + self.data_handler.cut_reconstructed_sim( + self.source.location[1], + self.data_handler.config["reco_sampling_width"], + ) self._unit_scale = self.config["Energy_convesion(ToGeV)"] - self._bins = np.array([self._sin_dec_bins, self._log_energy_bins], dtype=object) + self._bins = np.array( + [self._sin_dec_bins, self._log_energy_bins], dtype=object) self._init_bg_sob_map() self._build_ow_hist() - def __call__(self, params: par.Params, events: np.ndarray) -> sob_terms.SoBTerm: + def __call__( + self, + params: par.Params, + events: np.ndarray) -> sob_terms.SoBTerm: """Docstring""" # Get the bin that each event belongs to - sin_dec_idx = np.searchsorted(self._sin_dec_bins[:-1], events["sindec"]) - 1 + sin_dec_idx = np.searchsorted( + self._sin_dec_bins[:-1], events["sindec"]) - 1 - log_energy_idx = np.searchsorted(self._log_energy_bins[:-1], events["logE"]) - 1 + log_energy_idx = np.searchsorted( + self._log_energy_bins[:-1], events["logE"]) - 1 return ThreeMLPSEnergyTerm( name=self.config["name"], @@ -150,18 +158,19 @@ def _build_ow_hist(self) -> np.ndarray: def get_ns(self) -> float: """Docstring""" - return (self.spectrum(self._ow_ebin) * self._ow_hist).sum() * self._unit_scale + return (self.spectrum(self._ow_ebin) * + self._ow_hist).sum() * self._unit_scale def _init_bg_sob_map(self) -> np.ndarray: """Docstring""" if self.config["mc_bkgweight"] is None: - bg_h = self.data_handler.build_background_sindec_logenergy_histogram( - self._bins - ) + bg_h = \ + self.data_handler.build_background_sindec_logenergy_histogram( + self._bins) else: - bg_h = self.data_handler.build_mcbackground_sindec_logenergy_histogram( - self._bins, self.config["mc_bkgweight"] - ) + bg_h = \ + self.data_handler.build_mcbackground_sindec_logenergy_histogram( + self._bins, self.config["mc_bkgweight"]) print("using mc background") # Normalize histogram by dec band bg_h /= np.sum(bg_h, axis=1)[:, None] @@ -180,10 +189,11 @@ def source(self) -> sources.PointSource: def source(self, source: sources.PointSource) -> None: """Docstring""" self._source = source - self.data_handler.reduced_reco_sim = self.data_handler.cut_reconstructed_sim( - self.source.location[1], - self.data_handler.config["reco_sampling_width"], - ) + self.data_handler.reduced_reco_sim = \ + self.data_handler.cut_reconstructed_sim( + self.source.location[1], + self.data_handler.config["reco_sampling_width"], + ) def cal_sob_map(self) -> np.ndarray: """Creates sob histogram for a given spectrum. @@ -194,7 +204,8 @@ def cal_sob_map(self) -> np.ndarray: sig_h = self.data_handler.build_signal_energy_histogram( self.spectrum, self._bins, self._unit_scale ) - bin_centers = self._log_energy_bins[:-1] + np.diff(self._log_energy_bins) / 2 + bin_centers = self._log_energy_bins[:-1] + \ + np.diff(self._log_energy_bins) / 2 # Normalize histogram by dec band sig_h /= np.sum(sig_h, axis=1)[:, None] @@ -268,11 +279,11 @@ class ThreeMLPSIRFEnergyTermFactory(ThreeMLPSEnergyTermFactory): _spectrum: spectral.BaseSpectrum = dataclasses.field( init=False, repr=False, default=spectral.PowerLaw(1e3, 1e-14, -2) ) - + _bg_sob: np.ndarray = dataclasses.field(init=False, repr=False) _sin_dec_bins: np.ndarray = dataclasses.field( - init=False, repr=False, default_factory=lambda: PSTrackv4_sin_dec_bin.copy() - ) + init=False, repr=False, + default_factory=lambda: PSTrackv4_sin_dec_bin.copy()) _log_energy_bins: np.ndarray = dataclasses.field(init=False, repr=False) _bins: np.ndarray = dataclasses.field(init=False, repr=False) _trueebin: np.ndarray = dataclasses.field(init=False, repr=False) @@ -284,16 +295,17 @@ class ThreeMLPSIRFEnergyTermFactory(ThreeMLPSEnergyTermFactory): def __post_init__(self) -> None: """Docstring""" print("Calling __post_init__") # or use logging - #self._source = self.config.get("source", None) + # self._source = self.config.get("source", None) if self.config["list_sin_dec_bins"] is None: - self._sin_dec_bins = np.linspace(-1, 1, 1 + self.config["sin_dec_bins"]) + self._sin_dec_bins = np.linspace( + -1, 1, 1 + self.config["sin_dec_bins"]) else: self._sin_dec_bins = self.config["list_sin_dec_bins"] - + if self.config["list_log_energy_bins"] is None: self._log_energy_bins = np.linspace( - *self.config["log_energy_bounds"], 1 + self.config["log_energy_bins"] - ) + *self.config["log_energy_bounds"], + 1 + self.config["log_energy_bins"]) else: self._log_energy_bins = self.config["list_log_energy_bins"] lower_sindec = np.maximum( @@ -310,23 +322,31 @@ def __post_init__(self) -> None: ), 1, ) - lower_sindec_index = np.searchsorted(self._sin_dec_bins, lower_sindec) - 1 + lower_sindec_index = np.searchsorted( + self._sin_dec_bins, lower_sindec) - 1 uppper_sindec_index = np.searchsorted(self._sin_dec_bins, upper_sindec) - #print(lower_sindec_index,uppper_sindec_index) - self._sindec_bounds = np.array([lower_sindec_index, uppper_sindec_index]) - self._bins = np.array([self._sin_dec_bins, self._log_energy_bins], dtype=object) + # print(lower_sindec_index,uppper_sindec_index) + self._sindec_bounds = np.array( + [lower_sindec_index, uppper_sindec_index]) + self._bins = np.array( + [self._sin_dec_bins, self._log_energy_bins], dtype=object) self._truelogebin = self.config["list_truelogebin"] self._unit_scale = self.config["Energy_convesion(ToGeV)"] self._init_bg_sob_map() self._build_ow_hist() self._init_irf() - def __call__(self, params: par.Params, events: np.ndarray) -> sob_terms.SoBTerm: + def __call__( + self, + params: par.Params, + events: np.ndarray) -> sob_terms.SoBTerm: """Docstring""" # Get the bin that each event belongs to - sin_dec_idx = np.searchsorted(self._sin_dec_bins[:-1], events["sindec"]) - 1 + sin_dec_idx = np.searchsorted( + self._sin_dec_bins[:-1], events["sindec"]) - 1 - log_energy_idx = np.searchsorted(self._log_energy_bins[:-1], events["logE"]) - 1 + log_energy_idx = np.searchsorted( + self._log_energy_bins[:-1], events["logE"]) - 1 return ThreeMLPSEnergyTerm( name=self.config["name"], @@ -340,13 +360,13 @@ def __call__(self, params: par.Params, events: np.ndarray) -> sob_terms.SoBTerm: def _init_bg_sob_map(self) -> None: """Docstring""" if self.config["mc_bkgweight"] is None: - bg_h = self.data_handler.build_background_sindec_logenergy_histogram( - self._bins - ) + bg_h = \ + self.data_handler.build_background_sindec_logenergy_histogram( + self._bins) else: - bg_h = self.data_handler.build_mcbackground_sindec_logenergy_histogram( - self._bins, self.config["mc_bkgweight"] - ) + bg_h = \ + self.data_handler.build_mcbackground_sindec_logenergy_histogram( + self._bins, self.config["mc_bkgweight"]) print("using mc background") # Normalize histogram by dec band bg_h /= np.sum(bg_h, axis=1)[:, None] @@ -367,13 +387,17 @@ def _init_irf(self) -> None: ) self._trueebin = 10 ** (self._truelogebin[:-1]) sindec_idx = ( - np.digitize(np.sin(self.data_handler.full_sim["dec"]), self._sin_dec_bins) - - 1 - ) + np.digitize( + np.sin( + self.data_handler.full_sim["dec"]), + self._sin_dec_bins) - + 1) for i in range(len(self._sin_dec_bins) - 1): events_dec = self.data_handler.full_sim[(sindec_idx == i)] - loge_idx = np.digitize(events_dec["logE"], self._log_energy_bins) - 1 + loge_idx = np.digitize( + events_dec["logE"], + self._log_energy_bins) - 1 for j in range(len(self._log_energy_bins) - 1): events = events_dec[(loge_idx == j)] @@ -389,19 +413,23 @@ def _init_irf(self) -> None: weights=events["ow"], ) - # Have to pick an "energy" to assign to the bin. That's complicated, since - # you'd (in principle) want the flux-weighted average energy, but we don't - # have a flux function here. Instead, try just using the minimum energy of + # Have to pick an "energy" to assign to the bin. + # That's complicated, since + # you'd (in principle) want the flux-weighted average energy, + # but we don't have a flux function here. Instead, try just + # using the minimum energy of # the bin? Should be fine for small enough bins. # self._trueebin[i,j] = np.exp(bins[:-1] + (bins[1] - bins[0])) - # emean[i,j] = np.average(events['trueE'], weights=events['ow']) + # emean[i,j] = np.average(events['trueE'], + # weights=events['ow']) def build_sig_h(self, spectrum: spectral.BaseSpectrum) -> np.ndarray: """Docstring""" sig = np.zeros(self._bg_sob.shape) flux = spectrum(self._trueebin * self._unit_scale) # converting unit sig[self._sindec_bounds[0]:self._sindec_bounds[1], :] = np.dot( - self._irf[self._sindec_bounds[0]:self._sindec_bounds[1], :, :], flux + self._irf[self._sindec_bounds[0]:self._sindec_bounds[1], :, :], + flux ) sig /= np.sum(sig, axis=1)[:, None] return sig @@ -415,25 +443,22 @@ def source(self) -> sources.PointSource: def source(self, source: sources.PointSource) -> None: """Docstring""" self._source = source - lower_sindec = np.maximum( - np.sin( - self._source.location[1] - - self.data_handler.config["reco_sampling_width"] - ), - -0.99, - ) - upper_sindec = np.minimum( - np.sin( - self._source.location[1] - + self.data_handler.config["reco_sampling_width"] - ), - 1, - ) - #print(self) - #print(self._sin_dec_bins) - #lower_sindec_index = np.searchsorted(self._sin_dec_bins, lower_sindec) - 1 - #uppper_sindec_index = np.searchsorted(self._sin_dec_bins, upper_sindec) - #self._sindec_bounds = np.array([lower_sindec_index, uppper_sindec_index]) + # flake8 says that this variable is not used. Kept it under the comment + # section if we end up having to use it + # lower_sindec = np.maximum( + # np.sin( + # self._source.location[1] + # - self.data_handler.config["reco_sampling_width"] + # ), + # -0.99, + # ) + # upper_sindec = np.minimum( + # np.sin( + # self._source.location[1] + # + self.data_handler.config["reco_sampling_width"] + # ), + # 1, + # ) def cal_sob_map(self) -> np.ndarray: """Creates sob histogram for a given spectrum. diff --git a/mla/time_profiles.py b/mla/time_profiles.py index 009126a7..f641ada0 100644 --- a/mla/time_profiles.py +++ b/mla/time_profiles.py @@ -51,7 +51,7 @@ def pdf(self, times: np.ndarray) -> np.ndarray: """Get the probability amplitude given a time for this time profile. Args: - times: An array of event times to get the probability amplitude for. + times: An array of event times to get the probability amplitude. Returns: A numpy array of probability amplitudes at the given times. @@ -119,7 +119,10 @@ def cdf(self, times: np.ndarray) -> np.ndarray: @abc.abstractmethod def inverse_transform_sample( - self, start_times: np.ndarray, stop_times: np.ndarray) -> np.ndarray: + self, + start_times: np.ndarray, + stop_times: np.ndarray + ) -> np.ndarray: """Docstring""" @property @@ -157,7 +160,8 @@ def default_params(self) -> Dict[str, float]: @property @abc.abstractmethod def param_dtype(self) -> np.dtype: - """Returns the parameter names and datatypes formatted for numpy dtypes. + """Returns the parameter names and datatypes + formatted for numpy dtypes. """ @@ -179,7 +183,8 @@ class GaussProfile(GenericProfile): param_dtype (List[Tuple[str, str]]): The numpy dytpe for the fitting parameters. """ - scipy_dist: scipy.stats.distributions.rv_frozen = dataclasses.field(init=False) + scipy_dist: scipy.stats.distributions.rv_frozen = dataclasses.field( + init=False) _mean: float = dataclasses.field(init=False, repr=False) _sigma: float = dataclasses.field(init=False, repr=False) _param_dtype: ClassVar[np.dtype] = np.dtype( @@ -238,7 +243,8 @@ def x0(self, times: np.ndarray) -> tuple: return x0_mean, x0_sigma def bounds(self, time_profile: GenericProfile) -> List[tuple]: - """Returns good bounds for this time profile given another time profile. + """Returns good bounds for this time profile + given another time profile. Limits the mean to be within the range of the other profile and limits the sigma to be >= 0 and <= the width of the other profile. @@ -262,7 +268,10 @@ def cdf(self, times: np.ndarray) -> np.ndarray: return self.scipy_dist.cdf(times) def inverse_transform_sample( - self, start_times: np.ndarray, stop_times: np.ndarray) -> np.ndarray: + self, + start_times: np.ndarray, + stop_times: np.ndarray + ) -> np.ndarray: """Docstring""" start_cdfs = self.cdf(start_times) stop_cdfs = self.cdf(stop_times) @@ -336,7 +345,9 @@ class UniformProfile(GenericProfile): def __post_init__(self) -> None: """Constructs the time profile.""" - self._range = (self.config['start'], self.config['start'] + self.config['length']) + self._range = ( + self.config['start'], + self.config['start'] + self.config['length']) def pdf(self, times: np.ndarray) -> np.ndarray: """Calculates the probability for each time. @@ -404,10 +415,13 @@ def bounds(self, time_profile: GenericProfile def cdf(self, times: np.ndarray) -> np.ndarray: """Docstring""" - return np.clip((times - self.range[0]) / (self.range[1] - self.range[0]), 0, 1) + return np.clip( + (times - self.range[0]) / (self.range[1] - self.range[0]), 0, 1) def inverse_transform_sample( - self, start_times: np.ndarray, stop_times: np.ndarray) -> np.ndarray: + self, + start_times: np.ndarray, + stop_times: np.ndarray) -> np.ndarray: """Docstring""" return np.random.uniform( np.maximum(start_times, self.range[0]), @@ -417,14 +431,17 @@ def inverse_transform_sample( @property def params(self) -> dict: """Docstring""" - return {'start': self._range[0], 'length': self._range[1] - self._range[0]} + return { + 'start': self._range[0], + 'length': self._range[1] - self._range[0]} @params.setter def params(self, params: Params) -> None: """Docstring""" if 'start' in params: self._range = ( - params['start'], params['start'] + self._range[1] - self._range[0]) + params['start'], + params['start'] + self._range[1] - self._range[0]) if 'length' in params: self._range = (self._range[0], self._range[0] + params['length']) @@ -462,7 +479,8 @@ class CustomProfile(GenericProfile): Attributes: pdf (Callable[[np.array, Tuple[float, float]], np.array]): The - distribution function. This function needs to accept an array of bin + distribution function. + This function needs to accept an array of bin centers and a time window as a tuple, and it needs to return an array of probability densities at the given bin centers. dist (scipy.stats.rv_histogram): The histogrammed version of the @@ -499,28 +517,20 @@ def dist( self._offset = self.config['offset'] if isinstance(self.config['bins'], int): - bin_edges = np.linspace(*self.config['range'], self.config['bins']+1) - #print(bin_edges, *self.config['range']) + bin_edges = np.linspace(*self.config['range'], + self.config['bins'] + 1) else: - span = self.config['range'][1] - self.config['range'][0] - bin_edges = np.array(self.config['bins']) #*span - #print('span, bin edges',span, bin_edges) - + bin_edges = np.array(self.config['bins']) bin_widths = np.diff(bin_edges) - bin_centers = bin_edges[:-1] + bin_widths/2 - - hist,_ = np.histogram(bin_centers,bins =bin_edges) + bin_centers = bin_edges[:-1] + bin_widths / 2 + + hist, _ = np.histogram(bin_centers, bins=bin_edges) hist = hist.astype(float) - #print('hist, bin_widths', hist, bin_widths) area_under_hist = np.sum(hist * bin_widths) - #print('area', area_under_hist) hist *= 1. / area_under_hist - #print('normed hist, bin centers', hist, bin_centers) self._exposure = 1 / np.max(hist) hist *= bin_widths - #print('final hist', hist) - #print('setting dist', scipy.stats.rv_histogram((hist,bin_edges)).pdf(bin_centers)) - self._dist = scipy.stats.rv_histogram((hist,bin_edges)) + self._dist = scipy.stats.rv_histogram((hist, bin_edges)) def pdf(self, times: np.ndarray) -> np.ndarray: """Calculates the probability density for each time. @@ -586,7 +596,9 @@ def cdf(self, times: np.ndarray) -> np.ndarray: return self.dist.cdf(times) def inverse_transform_sample( - self, start_times: np.ndarray, stop_times: np.ndarray) -> np.ndarray: + self, + start_times: np.ndarray, + stop_times: np.ndarray) -> np.ndarray: """Docstring""" start_cdfs = self.cdf(start_times) stop_cdfs = self.cdf(stop_times) @@ -623,7 +635,8 @@ def offset(self, offset: float) -> None: @property def range(self) -> Tuple[Optional[float], Optional[float]]: return ( - self.config['range'][0] + self.offset, self.config['range'][1] + self.offset) + self.config['range'][0] + self.offset, + self.config['range'][1] + self.offset) @property def param_dtype(self) -> np.dtype: diff --git a/mla/trial_generators.py b/mla/trial_generators.py index 10939a00..629a1187 100644 --- a/mla/trial_generators.py +++ b/mla/trial_generators.py @@ -41,7 +41,8 @@ def __call__(self, n_signal: float = 0) -> np.ndarray: rng = np.random.default_rng(self.config["random_seed"]) n_background = rng.poisson(self.data_handler.n_background) if not self.config["fixed_ns"]: - n_signal = rng.poisson(self.data_handler.calculate_n_signal(n_signal)) + n_signal = rng.poisson( + self.data_handler.calculate_n_signal(n_signal)) background = self.data_handler.sample_background(n_background, rng) background["ra"] = rng.uniform(0, 2 * np.pi, len(background)) @@ -58,11 +59,13 @@ def __call__(self, n_signal: float = 0) -> np.ndarray: # not present in the data events. These include the true direction, # energy, and 'oneweight'. signal = rf.drop_fields( - signal, [n for n in signal.dtype.names if n not in background.dtype.names] - ) + signal, [ + n for n in signal.dtype.names if ( + n not in background.dtype.names)]) # Combine the signal background events and time-sort them. - # Use recfunctions.stack_arrays to prevent numpy from scrambling entry order + # Use recfunctions.stack_arrays to prevent numpy from scrambling entry + # order if background.dtype == signal.dtype: return np.concatenate([background, signal]) else: diff --git a/mla/utility_functions.py b/mla/utility_functions.py index a4eb7f4e..e2951ff1 100644 --- a/mla/utility_functions.py +++ b/mla/utility_functions.py @@ -89,12 +89,12 @@ def rotate( ra3 = np.atleast_1d(ra3) dec3 = np.atleast_1d(dec3) - if not len(ra1) == len(dec1) == len(ra2) == len(dec2) == len(ra3) == len(dec3): + if not len(ra1) == len(dec1) == len( + ra2) == len(dec2) == len(ra3) == len(dec3): raise IndexError("Arguments must all have the same dimension.") - cos_alpha = np.cos(ra2 - ra1) * np.cos(dec1) * np.cos(dec2) + np.sin(dec1) * np.sin( - dec2 - ) + cos_alpha = np.cos(ra2 - ra1) * np.cos(dec1) * \ + np.cos(dec2) + np.sin(dec1) * np.sin(dec2) # correct rounding errors cos_alpha[cos_alpha > 1] = 1 @@ -134,7 +134,11 @@ def rotate( return r_a, dec -def angular_distance(src_ra: float, src_dec: float, r_a: float, dec: float) -> float: +def angular_distance( + src_ra: float, + src_dec: float, + r_a: float, + dec: float) -> float: """Computes angular distance between source and location. Args: @@ -235,7 +239,8 @@ def newton_method_multidataset( return x[i + 1] -def trimsim(sim: np.ndarray, fraction: float, scaleow: bool = True) -> np.ndarray: +def trimsim(sim: np.ndarray, fraction: float, + scaleow: bool = True) -> np.ndarray: """Keep only fraction of the simulation Args: From 69dfc5c64acedb1dbba4cb289662c7c1ce5ae280 Mon Sep 17 00:00:00 2001 From: Aswathi Balagopal Date: Tue, 16 Sep 2025 18:01:08 -0500 Subject: [PATCH 5/5] more linting errors --- mla/threeml/data_handlers.py | 16 ++++++++-------- mla/threeml/profilellh.py | 33 +++++++++++++++++++-------------- mla/threeml/sob_terms.py | 16 ++++++++-------- 3 files changed, 35 insertions(+), 30 deletions(-) diff --git a/mla/threeml/data_handlers.py b/mla/threeml/data_handlers.py index ff008dfd..dcb20214 100644 --- a/mla/threeml/data_handlers.py +++ b/mla/threeml/data_handlers.py @@ -1,13 +1,13 @@ """Docstring""" -__author__ = 'John Evans and Jason Fan' -__copyright__ = 'Copyright 2024' -__credits__ = ['John Evans', 'Jason Fan', 'Michael Larson'] -__license__ = 'Apache License 2.0' -__version__ = '1.4.1' -__maintainer__ = 'Jason Fan' -__email__ = 'klfan@terpmail.umd.edu' -__status__ = 'Development' +__author__ = "John Evans and Jason Fan" +__copyright__ = "Copyright 2024" +__credits__ = ["John Evans", "Jason Fan", "Michael Larson"] +__license__ = "Apache License 2.0" +__version__ = "1.4.1" +__maintainer__ = "Jason Fan" +__email__ = "klfan@terpmail.umd.edu" +__status__ = "Development" import dataclasses diff --git a/mla/threeml/profilellh.py b/mla/threeml/profilellh.py index 833f067f..5af5e10b 100644 --- a/mla/threeml/profilellh.py +++ b/mla/threeml/profilellh.py @@ -1,13 +1,13 @@ """Docstring""" -__author__ = 'John Evans and Jason Fan' -__copyright__ = 'Copyright 2024' -__credits__ = ['John Evans', 'Jason Fan', 'Michael Larson'] -__license__ = 'Apache License 2.0' -__version__ = '1.4.1' -__maintainer__ = 'Jason Fan' -__email__ = 'klfan@terpmail.umd.edu' -__status__ = 'Development' +__author__ = "John Evans and Jason Fan" +__copyright__ = "Copyright 2024" +__credits__ = ["John Evans", "Jason Fan", "Michael Larson"] +__license__ = "Apache License 2.0" +__version__ = "1.4.1" +__maintainer__ = "Jason Fan" +__email__ = "klfan@terpmail.umd.edu" +__status__ = "Development" from threeML.plugin_prototype import PluginPrototype from scipy.interpolate import RegularGridInterpolator @@ -45,14 +45,15 @@ def __init__( super().__init__(name, nuisance_parameters) if spline is not None: self.spline = spline - self.df = df#None + self.df = df # None else: self.df = df self.par_name = list(df.columns) self.par_name.pop() listofpoint = [np.unique(df[n]) for n in self.par_name] shape = [len(points) for points in listofpoint] - sort_idx = np.lexsort([df[p].values for p in reversed(self.par_name)]) + sort_idx = np.lexsort( + [df[p].values for p in reversed(self.par_name)]) llh = np.reshape(df["llh"].values[sort_idx], shape) self.spline = RegularGridInterpolator( listofpoint, llh, bounds_error=False, fill_value=fill_value @@ -87,10 +88,14 @@ def get_log_like(self) -> float: def inner_fit(self) -> float: """ - This is used for the profile likelihood. Keeping fixed all parameters in the - LikelihoodModel, this method minimize the logLike over the remaining nuisance - parameters, i.e., the parameters belonging only to the model for this - particular detector. If there are no nuisance parameters, simply return the + This is used for the profile likelihood. + Keeping fixed all parameters in the + LikelihoodModel, this method minimize the logLike + over the remaining nuisance + parameters, i.e., the parameters belonging + only to the model for this + particular detector. If there are no nuisance parameters, + simply return the logLike value. """ diff --git a/mla/threeml/sob_terms.py b/mla/threeml/sob_terms.py index 6b45b735..581c733a 100644 --- a/mla/threeml/sob_terms.py +++ b/mla/threeml/sob_terms.py @@ -1,13 +1,13 @@ """Docstring""" -__author__ = 'John Evans and Jason Fan' -__copyright__ = 'Copyright 2024' -__credits__ = ['John Evans', 'Jason Fan', 'Michael Larson'] -__license__ = 'Apache License 2.0' -__version__ = '1.4.1' -__maintainer__ = 'Jason Fan' -__email__ = 'klfan@terpmail.umd.edu' -__status__ = 'Development' +__author__ = "John Evans and Jason Fan" +__copyright__ = "Copyright 2024" +__credits__ = ["John Evans", "Jason Fan", "Michael Larson"] +__license__ = "Apache License 2.0" +__version__ = "1.4.1" +__maintainer__ = "Jason Fan" +__email__ = "klfan@terpmail.umd.edu" +__status__ = "Development" import dataclasses