gammapy-plugin 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,5 @@
1
+ """Top-level package for Gammapy Plugin."""
2
+
3
+ from .gammapy_like import GammapyLike
4
+
5
+ __all__ = ["GammapyLike"]
@@ -0,0 +1,24 @@
1
+ # file generated by vcs-versioning
2
+ # don't change, don't track in version control
3
+ from __future__ import annotations
4
+
5
+ __all__ = [
6
+ "__version__",
7
+ "__version_tuple__",
8
+ "version",
9
+ "version_tuple",
10
+ "__commit_id__",
11
+ "commit_id",
12
+ ]
13
+
14
+ version: str
15
+ __version__: str
16
+ __version_tuple__: tuple[int | str, ...]
17
+ version_tuple: tuple[int | str, ...]
18
+ commit_id: str | None
19
+ __commit_id__: str | None
20
+
21
+ __version__ = version = '0.0.1'
22
+ __version_tuple__ = version_tuple = (0, 0, 1)
23
+
24
+ __commit_id__ = commit_id = 'g71c123a'
@@ -0,0 +1,276 @@
1
+ import logging
2
+
3
+ from astromodels.core.model import Model
4
+ from astromodels.sources import ExtendedSource, PointSource, Source
5
+ from gammapy.modeling.models import SkyModel
6
+
7
+ from gammapy_plugin.models import (
8
+ PointSourceModelConverted,
9
+ SpatialModelConverted,
10
+ SpectralModelConverted,
11
+ )
12
+ from gammapy_plugin.utils.gammapy_parser import (
13
+ parameter_to_gammapy_dict,
14
+ )
15
+
16
+ __all__ = ["AstromodelConverter", "SourceConverter"]
17
+
18
+ log = logging.getLogger(__name__)
19
+
20
+
21
+ class AstromodelConverter:
22
+ """Class for analyizing an astromodel model and converting all the
23
+ individual sources such that it can be used with gammapy.
24
+
25
+ Every Source in the Model will get its own Gammapy skymodel. The
26
+ evaluation happens via the astromodel definition.
27
+ """
28
+
29
+ def __init__(
30
+ self, model: Model, frame: str | None = None, convert_ps: bool = True
31
+ ) -> None:
32
+ """
33
+ :param model: the astromodel model
34
+ :type model: astromodels.core.model.Model
35
+ :param frame: geometry frame for gammapy, defaults to ICRS, optional
36
+ :type frame: str
37
+ :param convert_ps: convert PointSources to a PointSpatialModel, optional
38
+ :type convert_ps: bool
39
+ """
40
+ assert isinstance(model, Model), (
41
+ "AstromodelConverter needs an astromodels Model"
42
+ )
43
+ self._astromodel_model = model
44
+ if frame is not None:
45
+ self._frame = frame
46
+ else:
47
+ log.info("No frame passed - will use ICRS")
48
+ self._frame = "icrs"
49
+ self._convert_ps = convert_ps
50
+ self._converted_sources = {}
51
+ self._gammapy_models = []
52
+ self._convert_extendend_sources()
53
+ self._convert_point_sources()
54
+ self._create_gammapy_models_list()
55
+
56
+ def _convert_extendend_sources(self) -> None:
57
+ """Converts all extended sources into individual gammapy skymodels."""
58
+ for (
59
+ source_name,
60
+ source_instance,
61
+ ) in self._astromodel_model.extended_sources.items():
62
+ self._converted_sources[source_name] = SourceConverter(
63
+ source_instance, frame=self._frame, converter=self
64
+ )
65
+
66
+ def _convert_point_sources(self) -> None:
67
+ """Converts all point sources into individual skymodels."""
68
+ for (
69
+ source_name,
70
+ source_instance,
71
+ ) in self._astromodel_model.point_sources.items():
72
+ log.debug(f"Converting pointsource {source_name}")
73
+ self._converted_sources[source_name] = SourceConverter(
74
+ source_instance, converter=self, convert_spatial=self._convert_ps
75
+ )
76
+
77
+ def _create_gammapy_models_list(self) -> None:
78
+ """Creates a list with all SkyModels fro the converted sources."""
79
+ for source in self._converted_sources.values():
80
+ self._gammapy_models.append(source.skymodel)
81
+
82
+ def _update_parameters(self) -> None:
83
+ """Update all the parameters in the SkyModels with the values from the
84
+ astromodels model using the underlying SourceConverters."""
85
+ for source in self._converted_sources.values():
86
+ source._update_parameters()
87
+
88
+ def update(self) -> None:
89
+ """Update all parameters of all SkyModels witht the current values from the
90
+ astromodels model. Public method for _update_parameters"""
91
+ self._update_parameters()
92
+
93
+ @property
94
+ def gammapy_models(self) -> list[SkyModel]:
95
+ """
96
+ Returns all the gammapy skymodels for that model.
97
+
98
+ :return: list with all the SkyModels
99
+ :rtype: list[SkyModel]
100
+ """
101
+ return self._gammapy_models
102
+
103
+ @property
104
+ def model(self) -> Model:
105
+ """
106
+ Return the corresponding astromodels model
107
+
108
+ :return: the astromodels model
109
+ :rtype: astromodels.core.model.Model
110
+ """
111
+
112
+ return self._astromodel_model
113
+
114
+ @property
115
+ def converted_sources(self) -> dict:
116
+ """
117
+ Returns dictionary with all the converted sources.
118
+
119
+ :return: the converted sources dict
120
+ :rtype: dict
121
+ """
122
+ return self._converted_sources
123
+
124
+
125
+ class SourceConverter:
126
+ """Takes a astromodels source and converts it to a SkyModel"""
127
+
128
+ def __init__(
129
+ self, source: Source, converter: AstromodelConverter, **kwargs
130
+ ) -> None:
131
+ """
132
+ Initialize the SourceConverter.
133
+ :param source: The astromodels source
134
+ :type source: astromodels.core.model.Model
135
+ :param converter: The used AstromodelConverter instance
136
+ :type converter: AstromodelConverter
137
+ :param kwargs: Additional keyword arguments, optional
138
+ :type kwrags: dict
139
+ """
140
+
141
+ self._conv_spatial = kwargs.get("convert_spatial", True)
142
+ self._converter = converter
143
+ self._frame = self._converter._frame
144
+ self._source = source
145
+
146
+ # init everything to None
147
+ self._spatial_model = None
148
+ self._spectral_model = None
149
+ self._temporal_model = None
150
+ self._gammapy_model = None
151
+ self._parameter_dict = None
152
+
153
+ # start with converting the spatial model
154
+ if self._conv_spatial:
155
+ log.debug("Converting the spatial Model")
156
+ self._convert_spatial_model()
157
+
158
+ log.debug("Converting the spectral Model")
159
+ # converting the spectral model
160
+ self._convert_spectral_model()
161
+
162
+ self._create_skymodel()
163
+
164
+ def _gather_mappings(self) -> None:
165
+ """
166
+ Gather all the parameter mappings from astromodels to gammapy for the individual
167
+ spectral and spatial models
168
+ """
169
+ self._mapping = None
170
+ self._mapping_free = None
171
+ for comp in [self._spectral_model, self._spatial_model]:
172
+ if comp is not None:
173
+ if self._mapping is None:
174
+ self._mapping = comp.mapping
175
+ else:
176
+ self._mapping.update(comp.mapping)
177
+ if self._mapping_free is None:
178
+ self._mapping_free = comp.mapping_free
179
+ else:
180
+ self._mapping_free.update(comp.mapping_free)
181
+
182
+ def _update_parameters(self) -> None:
183
+ """Update the skymodel parameters during the sampling process using the
184
+ parameter dict."""
185
+ # update the parameter dict for this skymodel
186
+ for k, v in self._mapping_free.items():
187
+ self.skymodel.parameters[v].update_from_dict(
188
+ parameter_to_gammapy_dict(self._converter.model[k])
189
+ )
190
+
191
+ def _convert_spectral_model(self) -> None:
192
+ """Convert the spectral model of the source."""
193
+ if len(self._source.components.keys()) > 1:
194
+ self._spectral_model = SpectralModelConverted(
195
+ [x.shape for x in self._source.components.values()],
196
+ )
197
+
198
+ else:
199
+ self._spectral_model = SpectralModelConverted(
200
+ self._source.components[
201
+ next(iter(self._source.components.keys()))
202
+ ].shape,
203
+ )
204
+
205
+ def _convert_spatial_model(self) -> None:
206
+ """Convert the spatial model of the source."""
207
+ if isinstance(self._source, ExtendedSource):
208
+ ps = False
209
+ elif isinstance(self._source, PointSource):
210
+ position = self._source.position
211
+ ps = True
212
+ if ps:
213
+ self._spatial_model = PointSourceModelConverted(
214
+ sky_position=position,
215
+ frame=self._frame,
216
+ )
217
+ else:
218
+ self._spatial_model = SpatialModelConverted(
219
+ self._source.spatial_shape, frame=self._frame
220
+ )
221
+
222
+ def _create_skymodel(self) -> None:
223
+ """Create the skymodel instance out of the individual components."""
224
+ self._skymodel = SkyModel(
225
+ name=self._source.name,
226
+ spectral_model=self._spectral_model,
227
+ spatial_model=self._spatial_model,
228
+ temporal_model=self._temporal_model,
229
+ )
230
+ self._gather_mappings()
231
+
232
+ def update(self) -> None:
233
+ """
234
+ This invokes the updating of all parameters of the converted model in the
235
+ gammapy SkyModels
236
+ """
237
+ self._update_parameters()
238
+
239
+ @property
240
+ def skymodel(self) -> SkyModel:
241
+ """Returns the Gammapy skymodel for this source.
242
+
243
+ :returns: the created gammapy SkyModel
244
+ :rtype: gammapy.modeling.models.SkyModel
245
+
246
+ """
247
+ return self._skymodel
248
+
249
+ @property
250
+ def astromodels_source(self) -> Source:
251
+ """Returns the original astromodel source.
252
+
253
+ :returns: the converted astromodels Source
254
+ :rtype: astromodels.sources.Source
255
+ """
256
+
257
+ return self._source
258
+
259
+
260
+ class GammapyConverter: # pragma: no cover
261
+ """Takes a gammapy SkyModel and transforms it into a astromodels Model."""
262
+
263
+ def __init__(self, model: SkyModel) -> None:
264
+ assert isinstance(model, SkyModel), "You must provide a gammapy.model.SkyModel"
265
+ self._sky_model = model
266
+ raise NotImplementedError("Sorry this is not yet a feature :(")
267
+
268
+ def _parse_sources(self):
269
+ spectral_model = self._sky_model.spectral_model # noqa: F841
270
+ spatial_model = self._sky_model.spatial_model # noqa: F841
271
+
272
+ def _create_astromodel(self):
273
+ # load the sources
274
+ # load the
275
+ sources = []
276
+ self._astromodel = Model(*sources)
@@ -0,0 +1,325 @@
1
+ import logging
2
+ from pathlib import Path
3
+ from typing import TYPE_CHECKING
4
+
5
+ import gammapy
6
+ import numpy as np
7
+ from astromodels.core.model import Model
8
+ from astromodels.functions.priors import Truncated_gaussian
9
+ from gammapy.datasets import Dataset, Datasets
10
+ from gammapy.modeling.models import DatasetModels, ModelBase, Models
11
+ from packaging.version import Version
12
+ from threeML.plugin_prototype import PluginPrototype
13
+
14
+ from gammapy_plugin.converter import AstromodelConverter
15
+ from gammapy_plugin.io.plot_model import plot_model
16
+ from gammapy_plugin.utils.gammapy_parser import (
17
+ parameter_to_gammapy_dict,
18
+ parse_gammapy_model,
19
+ )
20
+
21
+ if TYPE_CHECKING:
22
+ from threeML.io.plotting.data_residual_plot import ResidualPlot
23
+
24
+
25
+ __all__ = ["GammapyLike"]
26
+
27
+ log = logging.getLogger(__name__)
28
+
29
+ __instrument_name = "gammapy"
30
+
31
+ gammapy_version = Version(gammapy.__version__)
32
+
33
+
34
+ class GammapyLike(PluginPrototype):
35
+ """A plugin for including Gammapy datasets."""
36
+
37
+ def __new__(cls, *args, **kwargs) -> PluginPrototype:
38
+ instance = object.__new__(cls)
39
+ return instance
40
+
41
+ def __init__(self, name: str, **kwargs) -> None:
42
+ """
43
+ Initialize the GammapyLike plugin.
44
+
45
+ :param name: Name of the plugin
46
+ :type name: str
47
+ """
48
+ nuisance_parameters = kwargs.get("nuisance_parameters", {})
49
+ super().__init__(name, nuisance_parameters=nuisance_parameters)
50
+ self._frame: str = kwargs.get("frame", "icrs")
51
+ self._sources: list[str] | None = kwargs.get("sources", None)
52
+ self._nuisance_mapping = {}
53
+ self._background_models: dict = kwargs.get("background_models", {})
54
+ self._background_models_mapper: dict = {}
55
+ self._nuisance_parameters_dicts: dict = {}
56
+ if len(self._background_models.keys()) > 0:
57
+ self._parse_background_models()
58
+
59
+ def set_datasets(
60
+ self,
61
+ datasets: Dataset | Datasets | list[Dataset] | Path | str,
62
+ mode: str = "individual",
63
+ stacked_name: str = "stacked",
64
+ ) -> None:
65
+ """Set the Gammapy Dataset.
66
+
67
+ :param datasets: list of Gammapy datasets or a single Dataset object
68
+ :param mode: individual or stacked - defaults to individual, stacked
69
+ stacks the passed datasets
70
+ :param stacked_name: name of the stacked datasset if mode is stacked
71
+ """
72
+ if mode not in [
73
+ "individual",
74
+ "stacked",
75
+ ]:
76
+ raise ValueError("mode needs to be individual or stacked")
77
+ if isinstance(datasets, (str, Path)):
78
+ log.warning(
79
+ "You have provided a path to a file - we will assume this is a Datasets"
80
+ " file. If not this will likely fail"
81
+ )
82
+ datasets = Datasets.read(datasets)
83
+ self._datasets = datasets
84
+ if mode == "stacked":
85
+ self._datasets = Datasets(
86
+ self._datasets.stack_reduce(name=stacked_name)
87
+ )
88
+
89
+ if isinstance(datasets, list):
90
+ self._datasets = Datasets()
91
+ for d in datasets:
92
+ self._datasets.append(d)
93
+ if mode == "stacked":
94
+ self._datasets = Datasets(
95
+ self._datasets.stack_reduce(name=stacked_name)
96
+ )
97
+
98
+ elif isinstance(datasets, Datasets):
99
+ self._datasets = datasets
100
+ if mode == "stacked":
101
+ self._datasets = Datasets(
102
+ self._datasets.stack_reduce(name=stacked_name)
103
+ )
104
+ elif isinstance(datasets, Dataset):
105
+ self._datasets = Datasets(datasets)
106
+ if mode == "stacked":
107
+ log.info("Only using a single dataset - can not stack that")
108
+ else:
109
+ msg = "datasets has to be list of Dataset,"
110
+ msg += " a single Datasets or Dataset object"
111
+ raise TypeError(msg)
112
+
113
+ def set_sources(self, sources: list[str] | str | None = None) -> None:
114
+ """
115
+ Set the sources to be used by this plugin - No need to specify bkg models
116
+
117
+ :param sources: Source(s) to be used in the analysis defaults to all
118
+ :type sources: list of str or str
119
+ """
120
+
121
+ if isinstance(sources, list):
122
+ self._sources = sources
123
+ elif isinstance(sources, str):
124
+ self._sources = [sources]
125
+ elif sources is None:
126
+ self._sources = None
127
+ else:
128
+ raise ValueError("")
129
+
130
+ def set_model(
131
+ self,
132
+ likelihood_model: Model,
133
+ converted_model: AstromodelConverter = None,
134
+ ) -> None:
135
+ """
136
+ Set the model to be used in the joint minimization.
137
+
138
+ :param likelihood_model: astromodels model
139
+ :param converted_model: converted astromodels
140
+ :type likelihood_model: Model
141
+ :type converted_model: AstromodelConverter
142
+ """
143
+
144
+ if self._sources is None:
145
+ log.debug(
146
+ "If you want to specify sources for this Plugin you MUST do so before"
147
+ )
148
+ else:
149
+ log.debug(f"Will use {self._sources} for this plugin")
150
+
151
+ self._likelihood_model: Model = likelihood_model
152
+ if converted_model is not None:
153
+ self._likelihood_model_converted: AstromodelConverter = converted_model
154
+ elif hasattr(self, "_likelihood_model_converted"):
155
+ pass
156
+ else:
157
+ self._likelihood_model_converted: AstromodelConverter = AstromodelConverter(
158
+ model=self._likelihood_model, frame=self._frame
159
+ )
160
+
161
+ self._update_gammapy_model_list()
162
+ self._assign_models()
163
+ self._set_get_log_like()
164
+
165
+ def _update_gammapy_model_list(self) -> Models:
166
+ """
167
+ Update the list of gammapy models.
168
+ """
169
+
170
+ # this will only be run after setting the model
171
+ if hasattr(self, "_likelihood_model_converted"):
172
+ if self._sources is not None:
173
+ tmp2 = [
174
+ x
175
+ for x in self._likelihood_model_converted.gammapy_models
176
+ if x.name in self._sources
177
+ ]
178
+
179
+ tmp = [*tmp2]
180
+ else:
181
+ tmp2 = [x for x in self._likelihood_model_converted.gammapy_models]
182
+ tmp = [*tmp2]
183
+ else:
184
+ tmp = []
185
+ tmp2 = []
186
+ self._global_models = Models(tmp2)
187
+ if hasattr(self, "_background_models"):
188
+ for m in list(self._background_models.values()):
189
+ tmp.append(m)
190
+ self._gammapy_model = Models(tmp)
191
+
192
+ def _assign_models(self):
193
+ """
194
+ Assign the gammapy models to the datasets
195
+ """
196
+ for d in self._datasets:
197
+ d.models = []
198
+ tmp = list(self._global_models).copy()
199
+ if d.name in self._background_models_mapper:
200
+ tmp.append(
201
+ self._background_models[self._background_models_mapper[d.name]]
202
+ )
203
+ d.models = Models(tmp)
204
+
205
+ def set_background_models(
206
+ self, bkg_model: ModelBase | list | Models | DatasetModels
207
+ ) -> None:
208
+ """
209
+ Set the gammapy background models (e.g. FoVBackgroundModel)
210
+
211
+ :param bkg_model: Background model(s) :type bkg_model: ModelBase or list of
212
+ ModelBase or Models or DatasetModels.
213
+ """
214
+ if isinstance(bkg_model, ModelBase):
215
+ bkg_model = [bkg_model]
216
+ else:
217
+ if not isinstance(bkg_model, (list, Models, DatasetModels)):
218
+ raise TypeError(
219
+ "either pass a singular gammapy model or a list of models"
220
+ )
221
+ for b in bkg_model:
222
+ self._background_models[b.name] = b
223
+ self._background_models_mapper[b.datasets_names[0]] = b.name
224
+ self._parse_background_models()
225
+ self._update_gammapy_model_list()
226
+ self._assign_models()
227
+
228
+ def _parse_background_models(self):
229
+ """
230
+ Parse the background models and link the gammapy parameters to
231
+ nuissance parameters of this plugin and set the prior.
232
+ """
233
+ # TODO: way of manually specifying the priors
234
+ for bkg in self._background_models.values():
235
+ bkg_paras = parse_gammapy_model(bkg, self._name)
236
+ for k, v in bkg_paras.items():
237
+ para_path = k.split(".")
238
+ self._nuisance_mapping[k] = para_path
239
+ self._nuisance_parameters[k] = v
240
+ if v.is_normalization and v.free:
241
+ self._nuisance_parameters[k].prior = Truncated_gaussian(
242
+ mu=1.0, sigma=0.1, lower_bound=0.2, upper_bound=1.8
243
+ )
244
+ self._nuisance_parameters_dicts[k] = parameter_to_gammapy_dict(v)
245
+
246
+ def _update_background_models(self):
247
+ """
248
+ Update the background models with the current values of the nuisance
249
+ parameters.
250
+ """
251
+ # TODO: rewrite this to directly update
252
+ for k, v in self._nuisance_parameters.items():
253
+ self._nuisance_parameters_dicts[k]["value"] = v.value
254
+ p = self._nuisance_mapping[k]
255
+ # TODO find an elegant way to not hardcode this
256
+ self._background_models[p[1]].parameters[p[2]].update_from_dict(
257
+ self._nuisance_parameters_dicts[k]
258
+ )
259
+
260
+ def _set_get_log_like(self):
261
+ if hasattr(self._datasets, "_stat_sum_likelihood"):
262
+ func = self._datasets._stat_sum_likelihood
263
+ elif hasattr(self._datasets, "stat_sum_likelihood"):
264
+ func = self._datasets.stat_sum_likelihood
265
+ else:
266
+ raise AttributeError(
267
+ "gammapy.Datasets has neither _stat_sum_likelihood nor "
268
+ "stat_sum_likelihood - something went fundamentally wrong!"
269
+ )
270
+
271
+ self.log_like_func = func
272
+
273
+ def get_log_like(self) -> float:
274
+ """
275
+ Return the value of the log-likelihood with the current values for
276
+ the parameters stored in the model instance.
277
+ """
278
+ self._likelihood_model_converted._update_parameters()
279
+ self._update_background_models()
280
+ return -0.5 * self.log_like_func()
281
+
282
+ def inner_fit(self):
283
+ return self.get_log_like()
284
+
285
+ def get_number_of_data_points(self) -> np.int64:
286
+ """
287
+ Return the number of data points in the datasets.
288
+ """
289
+ return np.sum([np.prod(d.counts.data.shape) for d in self._datasets])
290
+
291
+ @property
292
+ def datasets(self) -> Datasets:
293
+ """Gammapy datasets of the plugin."""
294
+ return self._datasets
295
+
296
+ @property
297
+ def model(self) -> Model:
298
+ """Astromodels model of the plugin."""
299
+ return self._likelihood_model
300
+
301
+ @property
302
+ def astromodel_converter(self) -> AstromodelConverter:
303
+ """AstromodelConverter object used for this plugin."""
304
+ return self._likelihood_model_converted
305
+
306
+ @property
307
+ def gammapy_model(self) -> Models:
308
+ """List of all the Gammapy SkyModels."""
309
+
310
+ if not hasattr(self, "_gammapy_model"):
311
+ self._update_gammapy_model_list()
312
+ return self._gammapy_model
313
+
314
+ @property
315
+ def frame(self) -> str:
316
+ """Coordinate Frame of the plugin."""
317
+ return self._frame
318
+
319
+ def display_model(
320
+ self,
321
+ *args,
322
+ **kwargs,
323
+ ) -> "ResidualPlot":
324
+
325
+ return plot_model(self._datasets, *args, **kwargs)
File without changes