Passed
Pull Request — main (#157)
by Chaitanya
04:40 queued 02:37
created

asgardpy.base.reduction   A

Complexity

Total Complexity 36

Size/Duplication

Total Lines 594
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 254
dl 0
loc 594
rs 9.52
c 0
b 0
f 0
wmc 36
1
"""
2
Classes containing the Dataset Reduction config parameters for the high-level
3
interface and also various functions to support in Dataset Reduction and
4
creating of the appropriate DL4 dataset.
5
"""
6
7
from enum import Enum
8
9
import numpy as np
10
from astropy import units as u
11
from astropy.coordinates import SkyCoord
12
from gammapy.catalog import CATALOG_REGISTRY
13
from gammapy.data import DataStore
14
from gammapy.datasets import MapDataset, SpectrumDataset
15
from gammapy.makers import (
16
    DatasetsMaker,
17
    FoVBackgroundMaker,
18
    MapDatasetMaker,
19
    ReflectedRegionsBackgroundMaker,
20
    ReflectedRegionsFinder,
21
    RingBackgroundMaker,
22
    SafeMaskMaker,
23
    SpectrumDatasetMaker,
24
    WobbleRegionsFinder,
25
)
26
from gammapy.maps import Map
27
from gammapy.utils.scripts import make_path
28
from regions import CircleAnnulusSkyRegion, CircleSkyRegion
29
30
from asgardpy.base.base import AngleType, BaseConfig, PathType, TimeIntervalsType
31
from asgardpy.base.geom import SkyPositionConfig, get_energy_axis
32
33
__all__ = [
34
    "BackgroundConfig",
35
    "BackgroundMethodEnum",
36
    "BackgroundRegionFinderMethodEnum",
37
    "ExclusionRegionsConfig",
38
    "generate_dl4_dataset",
39
    "get_bkg_maker",
40
    "get_dataset_maker",
41
    "get_dataset_reference",
42
    "get_exclusion_region_mask",
43
    "get_filtered_observations",
44
    "get_safe_mask_maker",
45
    "MapSelectionEnum",
46
    "ObservationsConfig",
47
    "ReductionTypeEnum",
48
    "ReflectedRegionFinderConfig",
49
    "RegionsConfig",
50
    "RequiredHDUEnum",
51
    "SafeMaskConfig",
52
    "SafeMaskMethodsEnum",
53
    "WobbleRegionsFinderConfig",
54
]
55
56
57
# Basic Components to define the various Dataset Reduction Maker Config
58
class ReductionTypeEnum(str, Enum):
59
    """
60
    Config section for list of DL3 Dataset Reduction methods used in Gammapy.
61
    """
62
63
    spectrum = "1d"
64
    cube = "3d"
65
66
67
class RequiredHDUEnum(str, Enum):
68
    """
69
    Config section for list of HDU objects required for reading the DL3 file
70
    into a Dataset object.
71
    """
72
73
    aeff = "aeff"
74
    bkg = "bkg"
75
    edisp = "edisp"
76
    psf = "psf"
77
    rad_max = "rad_max"
78
    point_like = "point-like"
79
    full_enclosure = "full-enclosure"
80
81
82
class ObservationsConfig(BaseConfig):
83
    """
84
    Config section for getting main information for creating an Observations
85
    object.
86
    """
87
88
    obs_ids: list[int] = []
89
    obs_file: PathType = PathType("None")
90
    obs_time: list[TimeIntervalsType] = []
91
    obs_cone: SkyPositionConfig = SkyPositionConfig()
92
    required_irfs: list[RequiredHDUEnum] = [RequiredHDUEnum.aeff]
93
94
95
class BackgroundMethodEnum(str, Enum):
96
    """
97
    Config section for list of methods for creating Background Reduction
98
    Makers object.
99
    """
100
101
    reflected = "reflected"
102
    fov = "fov_background"
103
    ring = "ring"
104
105
106
class BackgroundRegionFinderMethodEnum(str, Enum):
107
    """
108
    Config section for list of background region finder methods for creating
109
    the Background Reduction Makers object.
110
    """
111
112
    reflected = "reflected"
113
    wobble = "wobble"
114
115
116
class ReflectedRegionFinderConfig(BaseConfig):
117
    """
118
    Config section for getting main information for creating a
119
    ReflectedRegionsFinder object.
120
    """
121
122
    angle_increment: AngleType = 0.01 * u.deg
123
    min_distance: AngleType = 0.1 * u.deg
124
    min_distance_input: AngleType = 0.1 * u.deg
125
    max_region_number: int = 10000
126
    binsz: AngleType = 0.05 * u.deg
127
128
129
class WobbleRegionsFinderConfig(BaseConfig):
130
    """
131
    Config section for getting main information for creating a
132
    WobbleRegionsFinder object.
133
    """
134
135
    n_off_regions: int = 1
136
    binsz: AngleType = 0.05 * u.deg
137
138
139
class RegionsConfig(BaseConfig):
140
    """
141
    Config section for getting main information for creating a Regions object.
142
    """
143
144
    type: str = ""
145
    name: str = ""
146
    position: SkyPositionConfig = SkyPositionConfig()
147
    parameters: dict = {}
148
149
150
class ExclusionRegionsConfig(BaseConfig):
151
    """
152
    Config section for getting information on exclusion regions for creating
153
    an Exclusion Mask object.
154
    """
155
156
    target_source: bool = True
157
    regions: list[RegionsConfig] = []
158
    exclusion_file: PathType = PathType("None")
159
160
161
class SafeMaskMethodsEnum(str, Enum):
162
    """
163
    Config section for list of methods for creating Safe Mask Reduction Makers
164
    object.
165
    """
166
167
    aeff_default = "aeff-default"
168
    aeff_max = "aeff-max"
169
    edisp_bias = "edisp-bias"
170
    offset_max = "offset-max"
171
    bkg_peak = "bkg-peak"
172
    custom_mask = "custom-mask"
173
174
175
class MapSelectionEnum(str, Enum):
176
    """
177
    Config section for list of methods for creating a Dataset Maker object.
178
    """
179
180
    counts = "counts"
181
    exposure = "exposure"
182
    background = "background"
183
    psf = "psf"
184
    edisp = "edisp"
185
186
187
# Dataset Reduction Makers config
188
class BackgroundConfig(BaseConfig):
189
    """
190
    Config section for getting main information for creating a Background
191
    Reduction Makers object.
192
    """
193
194
    method: BackgroundMethodEnum = BackgroundMethodEnum.reflected
195
    region_finder_method: BackgroundRegionFinderMethodEnum = BackgroundRegionFinderMethodEnum.wobble
196
    parameters: dict = {}
197
    exclusion: ExclusionRegionsConfig = ExclusionRegionsConfig()
198
199
200
class SafeMaskConfig(BaseConfig):
201
    """
202
    Config section for getting main information for creating a Safe Mask Makers
203
    object.
204
    """
205
206
    methods: list[SafeMaskMethodsEnum] = []
207
    parameters: dict = {}
208
209
210
def get_filtered_observations(dl3_path, obs_config, log):
211
    """
212
    From the path of the DL3 index files, create gammapy Observations object and
213
    apply any observation filters provided in the obs_config object to return
214
    the selected Observations object.
215
216
    Parameters
217
    ----------
218
    dl3_path: `pathlib.Path`
219
        Path to the DL3 index files, to create `gammapy.data.DataStore` object.
220
    obs_config: `asgardpy.base.reduction.ObservationsConfig`
221
        Config information for creating the `gammapy.data.Observations` object.
222
    log: `logging()`
223
        Common log file.
224
225
    Return
226
    ------
227
    observations: `gammapy.data.Observations`
228
        Selected list of Observation object
229
    """
230
    datastore = DataStore.from_dir(dl3_path)
231
232
    obs_time = obs_config.obs_time
233
    obs_list = obs_config.obs_ids
234
    obs_cone = obs_config.obs_cone
235
    filtered_obs_ids = []
236
237
    # In case the obs_table is not sorted.
238
    obs_table = datastore.obs_table.group_by("OBS_ID")
239
240
    # Use the given list of Observation IDs to select Observations
241
    if len(obs_list) > 0:
242
        # if len(obs_list) > 2:
243
        # list of observation ids to be included
244
        filtered_obs_ids = obs_list
245
        # else:  # Find another way to make the distinction between list and range
246
        # the list has a min and max value to use this method
247
        #    id_select = {
248
        #        "type": "par_box",
249
        #        "variable": "OBS_ID",
250
        #        "value_range": obs_list,
251
        #    }
252
        #    obs_table = obs_table.select_observations(id_select)
253
254
    # Filter the Observations using the Time interval range provided
255
    if len(obs_time) != 0:
256
        for i, intervals in enumerate(obs_time):
257
            gti_select = {
258
                "type": "time_box",
259
                "time_range": [intervals["start"], intervals["stop"]],
260
            }
261
262
            if i == 0:
263
                obs_table_interval = obs_table.select_observations(gti_select)
264
            else:
265
                for row in obs_table.select_observations(gti_select):
266
                    obs_table_interval.add_row(row)
267
        obs_table = obs_table_interval
268
269
    # For 3D Dataset, use a sky region to select Observations
270
    if obs_cone.lon != 0 * u.deg:
271
        cone_select = {
272
            "type": "sky_circle",
273
            "frame": obs_cone.frame,
274
            "lon": obs_cone.lon,
275
            "lat": obs_cone.lat,
276
            "radius": obs_cone.radius,
277
            "border": 0 * u.deg,  # Default?
278
        }
279
        obs_table = obs_table.select_observations(cone_select)
280
281
    if len(filtered_obs_ids) > 0:
282
        filtered_obs_ids = np.intersect1d(filtered_obs_ids, obs_table["OBS_ID"].data)
283
    else:
284
        filtered_obs_ids = obs_table["OBS_ID"].data
285
286
    obs_ids_str = " ".join(map(str, filtered_obs_ids))
287
    log.info("Observation ID list selected: %s", obs_ids_str)
288
289
    # IRFs selection
290
    irfs_selected = obs_config.required_irfs
291
    observations = datastore.get_observations(filtered_obs_ids, required_irf=irfs_selected)
292
293
    return observations
294
295
296
def get_dataset_reference(tag, geom, geom_config, name=None):
297
    """
298
    Create a base Dataset object to fill up with the appropriate {1, 3}D type
299
    of DL3 data to generate the reduced DL4 dataset, using the given base
300
    geometry and relevant axes details.
301
302
    Parameters
303
    ----------
304
    tag: str
305
        Determining either the {1, 3}d Dataset type.
306
    geom: 'gammapy.maps.RegionGeom' or `gammapy.maps.WcsGeom`
307
        Appropriate Base geometry objects for {1, 3}D type of DL4 Datasets.
308
    geom_config: `asgardpy.base.geom.GeomConfig`
309
        Config information on creating the Base Geometry of the DL4 dataset.
310
    name: str
311
        Name for the dataset.
312
313
    Return
314
    ------
315
    dataset_reference: `gammapy.dataset.SpectrumDataset` or
316
        `gammapy.dataset.MapDataset`
317
        Appropriate Dataset reference for {1, 3}D type of DL4 Datasets.
318
    """
319
    dataset_reference = None
320
321
    if tag == "1d":
322
        for axes_ in geom_config.axes:
323
            if axes_.name == "energy_true":
324
                energy_axis = get_energy_axis(axes_)
325
                dataset_reference = SpectrumDataset.create(
326
                    geom=geom,
327
                    name=name,
328
                    energy_axis_true=energy_axis,
329
                )
330
    else:  # For tag == "3d"
331
        binsize_irf = geom_config.wcs.binsize_irf.to_value("deg")
332
        dataset_reference = MapDataset.create(
333
            geom=geom,
334
            name=name,
335
            binsz_irf=binsize_irf,
336
        )
337
338
    return dataset_reference
339
340
341
def get_dataset_maker(tag, dataset_config):
342
    """
343
    Create a Dataset Maker object to support creating an appropriate {1, 3}D
344
    type of DL4 Dataset along with other reduction makers.
345
346
    Parameters
347
    ----------
348
    tag: str
349
        Determining either the {1, 3}d Dataset type.
350
    dataset_config: `asgardpy.data.dataset_1d.Dataset1DInfoConfig` or
351
        `asgardpy.data.dataset_3d.Dataset3DInfoConfig`
352
        Config information on creating appropriate {1, 3}d DL4 dataset type.
353
354
    Return
355
    ------
356
    dataset_maker: `gammapy.makers.SpectrumDatasetMaker` or
357
        `gammapy.makers.MapDatasetMaker`
358
        Appropriate Dataset Maker for {1, 3}D type of DL4 Datasets.
359
    """
360
    if tag == "1d":
361
        dataset_maker = SpectrumDatasetMaker(
362
            containment_correction=dataset_config.containment_correction,
363
            selection=dataset_config.map_selection,
364
        )
365
    else:  # for tag == "3d"
366
        dataset_maker = MapDatasetMaker(selection=dataset_config.map_selection)
367
368
    return dataset_maker
369
370
371
def get_safe_mask_maker(safe_config):
372
    """
373
    Generate Safe mask reduction maker as per the given config information.
374
375
    Parameters
376
    ---------
377
    safe_config: `asgardpy.base.reduction.SafeMaskConfig`
378
        Config information to create `gammapy.makers.SafeMaskMaker` object.
379
380
    Return
381
    ------
382
    safe_maker: `gammapy.makers.SafeMaskMaker`
383
        Gammapy Dataset Reduction Maker, for safe data range mask.
384
    """
385
    pars = safe_config.parameters
386
387
    if len(safe_config.methods) != 0:
388
        if "custom-mask" not in safe_config.methods:
389
            safe_maker = SafeMaskMaker(methods=safe_config.methods, **pars)
390
        else:
391
            safe_maker = None
392
    else:
393
        safe_maker = None
394
395
    return safe_maker
396
397
398
def get_exclusion_region_mask(
399
    exclusion_params,
400
    excluded_geom,
401
    exclusion_regions,
402
    config_target,
403
    geom_config,
404
    log,
405
):
406
    """
407
    Generate from a given parameters, base geometry for exclusion mask, list
408
    of exclusion regions, config information on the target source and the base
409
    geometry for the exclusion mask, a background exclusion region mask.
410
    # Create exclusion mask either by given regions, catalog or from a file.
411
412
    Parameters
413
    ----------
414
    exclusion_params: `asgardpy.base.reduction.ExclusionRegionsConfig`
415
        Config information on the list of Exclusion Regions
416
    excluded_geom: 'gammapy.maps.RegionGeom' or `gammapy.maps.WcsGeom`
417
        Appropriate Base geometry objects for exclusion regions for {1, 3}D
418
        type of DL4 Datasets.
419
    exclusion_regions: list of `gammapy.maps.WcsMap`
420
        Existing list of excluded regions.
421
    config_target: `asgardpy.config.generator.AsgardpyConfig.target`
422
        Config information on the target source
423
    geom_config: `asgardpy.base.geom.GeomConfig`
424
        Config information on creating the Base Geometry of the DL4 dataset.
425
    log: `logging()`
426
        Common log file.
427
    Return
428
    ------
429
    exclusion_mask: `gammapy.maps.WcsNDMap`
430
        Boolean region mask for the exclusion regions
431
    """
432
    exclusion_mask = None
433
434
    if len(exclusion_params.regions) != 0:
435
        # Fetch information from config
436
        for region in exclusion_params.regions:
437
            if region.name == "":
438
                # Using the sky position information without the source name.
439
                coord = region.position
440
                center_ex = SkyCoord(u.Quantity(coord.lon), u.Quantity(coord.lat), frame=coord.frame).icrs
441
            else:
442
                # Using Sesame name resolver
443
                center_ex = SkyCoord.from_name(region.name)
444
445
            if region.type == "CircleAnnulusSkyRegion":
446
                excluded_region = CircleAnnulusSkyRegion(
447
                    center=center_ex,
448
                    inner_radius=u.Quantity(region.parameters["rad_0"]),
449
                    outer_radius=u.Quantity(region.parameters["rad_1"]),
450
                )
451
            elif region.type == "CircleSkyRegion":
452
                excluded_region = CircleSkyRegion(
453
                    center=center_ex, radius=u.Quantity(region.parameters["region_radius"])
454
                )
455
            else:
456
                log.error(f"Unknown type of region passed {region.type}")
457
            exclusion_regions.append(excluded_region)
458
459
    elif exclusion_params.exclusion_file.is_file():
460
        path = make_path(exclusion_params.exclusion_file)
461
462
        exclusion_mask = Map.read(path)
463
        exclusion_mask.data = exclusion_mask.data.astype(bool)
464
465
    # Check if a catalog data is given with exclusion radius
466
    if config_target.use_catalog.exclusion_radius != 0 * u.deg:
467
        catalog = CATALOG_REGISTRY.get_cls(config_target.use_catalog.name)()
468
469
        # Only use source positions from the Catalog within the base geometry
470
        inside_geom = excluded_geom.to_image().contains(catalog.positions)
471
472
        idx_list = np.nonzero(inside_geom)[0]
473
        for i in idx_list:
474
            exclusion_regions.append(
475
                CircleSkyRegion(
476
                    center=catalog[i].position,
477
                    radius=config_target.use_catalog.exclusion_radius,
478
                )
479
            )
480
481
    # Apply the exclusion regions mask on the base geometry to get the final
482
    # boolean mask
483
    if len(exclusion_regions) > 0:
484
        exclusion_mask = ~excluded_geom.region_mask(exclusion_regions)
485
486
    return exclusion_mask
487
488
489
def get_bkg_maker(bkg_config, exclusion_mask):
490
    """
491
    Generate Background reduction maker by including a boolean exclusion mask,
492
    with methods to find background normalization using the information
493
    provided in the config for using specific methods.
494
495
    Parameters
496
    ----------
497
    bkg_config: `asgardpy.base.reduction.BackgroundConfig`
498
        Config information for evaluating a particular Background
499
        normalization maker for dataset reduction.
500
    exclusion_mask: `gammapy.maps.WcsNDMap`
501
        Boolean region mask for the exclusion regions
502
503
    Return
504
    ------
505
    bkg_maker: `gammapy.makers.background()`
506
        Appropriate gammapy Background Maker objects as per the config.
507
    """
508
    match bkg_config.method:
509
        case "reflected":
510
            match bkg_config.region_finder_method:
511
                case "wobble":
512
                    region_finder = WobbleRegionsFinder(**bkg_config.parameters)
513
                case "reflected":
514
                    region_finder = ReflectedRegionsFinder(**bkg_config.parameters)
515
516
            bkg_maker = ReflectedRegionsBackgroundMaker(region_finder=region_finder, exclusion_mask=exclusion_mask)
517
518
        case "fov_background":
519
            bkg_maker = FoVBackgroundMaker(exclusion_mask=exclusion_mask, **bkg_config.parameters)
520
        case "ring":
521
            bkg_maker = RingBackgroundMaker(exclusion_mask=exclusion_mask, **bkg_config.parameters)
522
        case _:
523
            bkg_maker = None
524
525
    return bkg_maker
526
527
528
def generate_dl4_dataset(
529
    tag,
530
    observations,
531
    dataset_reference,
532
    dataset_maker,
533
    bkg_maker,
534
    safe_maker,
535
    n_jobs,
536
    parallel_backend,
537
):
538
    """
539
    From the given Observations, Dataset reference and various Makers,
540
    use the multiprocessing method with DatasetsMaker, create the appropriate
541
    DL4 Dataset for {1, 3}D type of DL3 data.
542
543
    Parameters
544
    ----------
545
    tag: str
546
        Determining either the {1, 3}d Dataset type.
547
    observations: `gammapy.data.Observations`
548
        Selected list of Observation object
549
    dataset_reference: `gammapy.dataset.SpectrumDataset` or
550
        `gammapy.dataset.MapDataset`
551
        Appropriate Dataset reference for {1, 3}D type of DL4 Datasets.
552
    dataset_maker: `gammapy.makers.MapDatasetMaker` or
553
        `gammapy.makers.SpectrumDatasetMaker`
554
        Appropriate gammapy object to bin the Observations Map data or 1D
555
        spectrum extraction data for {1, 3}D type of DL4 Datasets.
556
    bkg_maker: `gammapy.makers.background()`
557
        Appropriate gammapy Background Maker objects as per the config.
558
    safe_maker: `gammapy.makers.SafeMaskMaker`
559
        Gammapy Dataset Reduction Maker, for safe data range mask.
560
    n_jobs: int
561
        Number of parallel processing jobs for `gammapy.makers.DatasetsMaker`
562
    parallel_backend: str
563
        Name of the parallel backend used for the parallel processing. By
564
        default "multiprocessing" is used for now.
565
566
    Return
567
    ------
568
    datasets: `gammapy.datasets.Datasets`
569
        A Datasets object containing appropriate `gammapy.dataset.MapDataset`
570
        or `gammapy.dataset.SpectrumDataset` for {1, 3}D type of DL3 dataset.
571
    """
572
    if safe_maker:
573
        makers = [dataset_maker, safe_maker, bkg_maker]
574
    else:
575
        makers = [dataset_maker, bkg_maker]
576
577
    if tag == "1d":
578
        datasets_maker = DatasetsMaker(
579
            makers,
580
            stack_datasets=False,
581
            n_jobs=n_jobs,
582
            parallel_backend=parallel_backend,
583
        )
584
    else:
585
        datasets_maker = DatasetsMaker(
586
            makers,
587
            stack_datasets=False,  # Add to the config for 3D dataset?
588
            n_jobs=n_jobs,
589
            parallel_backend=parallel_backend,
590
            cutout_mode="trim",  # Add to the config for 3D dataset?
591
            # cutout_width=2*offset_max  # As used in the API, from geom.selection
592
        )
593
594
    datasets = datasets_maker.run(dataset_reference, observations)
595
596
    return datasets
597