Source code for soxs.instrument_registry

import json
import os
from copy import deepcopy

from soxs.utils import PoochHandle, mylog, parse_value

# The Instrument Registry


[docs] class InstrumentRegistry: def __init__(self): self.registry = {} def __getitem__(self, key): return self.registry[key] def __setitem__(self, key, value): self.registry[key] = value def keys(self): return self.registry.keys() def items(self): return self.registry.items() def __contains__(self, item): return item in self.registry def get(self, key, default=None): return self.registry.get(key, default)
[docs] def fetch_files(self, key, loc=None): """ A handy method to fetch ARF, RMF, background, and PSF files to a location of one's choice. Files are only actually downloaded if they are not present already. Parameters ---------- key : string The instrument specification to download the files for. loc : string, optional The path to download the files to. If not specified, it will download them to the current working directory. """ inst_spec = self[key] if loc is None: loc = os.getcwd() dog = PoochHandle(cache_dir=loc) log_msg = f'Downloading %s "%s" for instrument "{key}".' fns = [inst_spec["arf"], inst_spec["rmf"]] logs = ["ARF", "RMF"] if inst_spec["bkgnd"] is not None: bkgnd = inst_spec["bkgnd"][0] if isinstance(bkgnd, list): for b in inst_spec["bkgnd"]: fns.append(b[0]) else: fns.append(bkgnd) logs.append("instrumental background model") if inst_spec["psf"] is not None: if "image" in inst_spec["psf"][0]: fns.append(inst_spec["psf"][1]) logs.append("PSF model") for fn, log in zip(fns, logs): mylog.info(log_msg, log, fn) dog.fetch(fn)
instrument_registry = InstrumentRegistry() # Lynx High-Definition X-ray Imager (HDXI) instrument_registry["lynx_hdxi"] = { "name": "lynx_hdxi", "arf": "xrs_hdxi_3x10.arf", "rmf": "xrs_hdxi.rmf", "bkgnd": ["lynx_hdxi_particle_bkgnd.pha", 1.0], "fov": 22.0, "num_pixels": 4096, "aimpt_coords": [0.0, 0.0], "chips": [["Box", 0, 0, 4096, 4096]], "focal_length": 10.0, "dither": True, "psf": ["image", "chandra_psf.fits", 6], "imaging": True, "grating": False, } # Lynx Micro-calorimeter instrument_registry["lynx_lxm"] = { "name": "lynx_lxm", "arf": "xrs_mucal_3x10_3.0eV.arf", "rmf": "xrs_mucal_3.0eV.rmf", "bkgnd": ["lynx_lxm_particle_bkgnd.pha", 1.0], "fov": 5.0, "num_pixels": 300, "aimpt_coords": [0.0, 0.0], "chips": [["Box", 0, 0, 300, 300]], "focal_length": 10.0, "dither": True, "psf": ["image", "chandra_psf.fits", 6], "imaging": True, "grating": False, } instrument_registry["lynx_lxm_enh"] = { "name": "lynx_lxm_enh", "arf": "xrs_mucal_3x10_1.5eV.arf", "rmf": "xrs_mucal_1.5eV.rmf", "bkgnd": ["lynx_lxm_enh_particle_bkgnd.pha", 1.0], "fov": 1.0, "num_pixels": 120, "aimpt_coords": [0.0, 0.0], "chips": [["Box", 0, 0, 120, 120]], "focal_length": 10.0, "dither": True, "psf": ["image", "chandra_psf.fits", 6], "imaging": True, "grating": False, } instrument_registry["lynx_lxm_ultra"] = { "name": "lynx_lxm_ultra", "arf": "xrs_mucal_3x10_0.3eV.arf", "rmf": "xrs_mucal_0.3eV.rmf", "bkgnd": ["lynx_lxm_ultra_particle_bkgnd.pha", 1.0], "fov": 1.0, "num_pixels": 60, "aimpt_coords": [0.0, 0.0], "chips": [["Box", 0, 0, 60, 60]], "focal_length": 10.0, "dither": True, "psf": ["image", "chandra_psf.fits", 6], "imaging": True, "grating": False, } # Lynx Gratings (for spectra only) instrument_registry["lynx_xgs"] = { "name": "lynx_xgs", "arf": "xrs_cat.arf", "rmf": "xrs_cat.rmf", "bkgnd": None, "focal_length": 10.0, "imaging": False, "grating": True, } # Athena WFI instrument_registry["athena_wfi"] = { "name": "athena_wfi", "arf": "athena_sixte_wfi_wo_filter_v20190122.arf", "rmf": "athena_wfi_sixte_v20150504.rmf", "bkgnd": ["sixte_wfi_particle_bkg_20190829.pha", 79552.92570677], "fov": 40.147153, "num_pixels": 1078, "aimpt_coords": [53.69, -53.69], "chips": [ ["Box", -283, -283, 512, 512], ["Box", 283, -283, 512, 512], ["Box", -283, 283, 512, 512], ["Box", 283, 283, 512, 512], ], "focal_length": 12.0, "dither": True, "psf": ["multi_image", "athena_psf_15row.fits"], "imaging": True, "grating": False, } # Athena XIFU instrument_registry["athena_xifu"] = { "name": "athena_xifu", "arf": "sixte_xifu_cc_baselineconf_20180821.arf", "rmf": "XIFU_CC_BASELINECONF_2018_10_10.rmf", "bkgnd": ["xifu_nxb_20181209.pha", 79552.92570677], "fov": 5.991992621478149, "num_pixels": 84, "aimpt_coords": [0.0, 0.0], "chips": [["Polygon", [-33, 0, 33, 33, 0, -33], [20, 38, 20, -20, -38, -20]]], "focal_length": 12.0, "dither": True, "psf": ["multi_image", "athena_psf_15row.fits"], "imaging": True, "grating": False, } # Chandra ACIS-I, Cycle 0 and 20 for cycle in [0, 22]: name = f"chandra_acisi_cy{cycle}" instrument_registry[name] = { "name": name, "arf": f"acisi_aimpt_cy{cycle}.arf", "rmf": f"acisi_aimpt_cy{cycle}.rmf", "bkgnd": [f"chandra_acisi_cy{cycle}_particle_bkgnd.pha", 1.0], "fov": 20.008, "num_pixels": 2440, "aimpt_coords": [86.0, 57.0], "chips": [ ["Box", -523, -523, 1024, 1024], ["Box", 523, -523, 1024, 1024], ["Box", -523, 523, 1024, 1024], ["Box", 523, 523, 1024, 1024], ], "psf": ["multi_image", "chandra_psf.fits"], "focal_length": 10.0, "dither": True, "imaging": True, "grating": False, } # Chandra ACIS-S, Cycle 0 and 22 for cycle in [0, 22]: name = f"chandra_aciss_cy{cycle}" instrument_registry[name] = { "name": name, "arf": f"aciss_aimpt_cy{cycle}.arf", "rmf": f"aciss_aimpt_cy{cycle}.rmf", "bkgnd": [ [f"chandra_aciss_cy{cycle}_fi_particle_bkgnd.pha", 1.0], [f"chandra_aciss_cy{cycle}_bi_particle_bkgnd.pha", 1.0], [f"chandra_aciss_cy{cycle}_fi_particle_bkgnd.pha", 1.0], [f"chandra_aciss_cy{cycle}_bi_particle_bkgnd.pha", 1.0], [f"chandra_aciss_cy{cycle}_fi_particle_bkgnd.pha", 1.0], [f"chandra_aciss_cy{cycle}_fi_particle_bkgnd.pha", 1.0], ], "fov": 50.02, "num_pixels": 6100, "aimpt_coords": [206.0, 0.0], "chips": [ ["Box", -2605, 0, 1024, 1024], ["Box", -1563, 0, 1024, 1024], ["Box", -521, 0, 1024, 1024], ["Box", 521, 0, 1024, 1024], ["Box", 1563, 0, 1024, 1024], ["Box", 2605, 0, 1024, 1024], ], "psf": ["multi_image", "chandra_psf.fits"], "focal_length": 10.0, "dither": True, "imaging": True, "grating": False, } # Chandra ACIS-S, Cycle 0 and 22 HETG (for spectra only) orders = {"p1": 1, "m1": -1} for energy in ["meg", "heg"]: for order in ["p1", "m1"]: for cycle in [0, 22]: name = f"chandra_aciss_{energy}_{order}_cy{cycle}" resp_name = f"chandra_aciss_{energy}{orders[order]}_cy{cycle}" instrument_registry[name] = { "name": name, "arf": f"{resp_name}.garf", "rmf": f"{resp_name}.grmf", "bkgnd": None, "focal_length": 10.0, "imaging": False, "grating": True, } # XRISM Resolve for res in ["Hp_5eV", "Mp_6eV", "Lp_18eV"]: for filt in ["", "fwBe_", "fwND_"]: instrument_registry[f"xrism_resolve_{filt}{res}"] = { "name": f"xrism_resolve_{filt}{res}", "arf": f"rsl_pointsource_{filt}GVclosed.arf", "rmf": f"rsl_{res}.rmf", "bkgnd": None, "num_pixels": 6, "fov": 3.0, "aimpt_coords": [0.0, 0.0], "chips": [["Box", 0, 0, 6, 6]], "focal_length": 5.6, "dither": False, "psf": ["multi_image", "sxs_psfimage_20140618.fits"], "imaging": True, "grating": False, } instrument_registry["xrism_resolve"] = deepcopy( instrument_registry["xrism_resolve_Hp_5eV"] ) instrument_registry["xrism_resolve_old"] = { "name": "xrism_resolve", "arf": "resolve_pnt_heasim_noGV_20190701.arf", "rmf": "resolve_h5ev_2019a.rmf", "bkgnd": ["resolve_h5ev_2019a_rslnxb.pha", 9.130329009932256], "num_pixels": 6, "fov": 3.06450576, "aimpt_coords": [0.0, 0.0], "chips": [["Box", 0, 0, 6, 6]], "focal_length": 5.6, "dither": False, "psf": ["multi_image", "sxs_psfimage_20140618.fits"], "imaging": True, "grating": False, } instrument_registry["xrism_resolve_withGV_old"] = deepcopy( instrument_registry["xrism_resolve"] ) instrument_registry["xrism_resolve_withGV_old"][ "arf" ] = "resolve_pnt_heasim_withGV_20190701.arf" # XRISM Xtend instrument_registry["xrism_xtend"] = { "name": "xrism_xtend", "arf": "xtd_pointsource.arf", "rmf": "xtd_sim_v20201009.rmf", "bkgnd": ["xtend_sim_nxb_v20201009_r5arcmin.pha", 78.53981633974483], "num_pixels": 1296, "fov": 38.18845555660526, "aimpt_coords": [-244.0, -244.0], "chips": [ ["Box", -327, 327, 640, 640], ["Box", -327, -327, 640, 640], ["Box", 327, 327, 640, 640], ["Box", 327, -327, 640, 640], ], "focal_length": 5.6, "dither": False, "psf": ["eef", "eef_from_sxi_psfimage_20140618.fits", 1], "imaging": True, "grating": False, } # AXIS instrument_registry["axis"] = { "name": "axis", "arf": "axis_onaxis_20221116.arf", "rmf": "axis_ccd_20221101.rmf", "bkgnd": ["axis_nxb_FOV_10Msec_20221215.pha", 697.06], "num_pixels": 2952, "fov": 27.06194257961904, "aimpt_coords": [-109, 109], "chips": [ ["Box", -756, -756, 1440, 1440], ["Box", -756, 756, 1440, 1440], ["Box", 756, -756, 1440, 1440], ["Box", 756, 756, 1440, 1440], ], "focal_length": 9.0, "dither": False, "psf": ["multi_eef", "AXIS_EEF_2022-02-16.fits", 2], "imaging": True, "grating": False, } # STAR-X instrument_registry["star-x"] = { "name": "star-x", "arf": "starx_2020-11-26_fov_avg.arf", "rmf": "starx.rmf", "bkgnd": None, "num_pixels": 3600, "fov": 60.0, "aimpt_coords": [0.0, 0.0], "chips": [["Box", 0, 0, 3600, 3600]], "focal_length": 4.5, "dither": True, "psf": ["gaussian", 3.0], "imaging": True, "grating": False, } # LEM instrument_registry["lem_outer_array"] = { "name": "lem_outer_array", "arf": "lem_300522.arf", "rmf": "lem_2.5ev_110422.rmf", "bkgnd": ["lem_2.5eV_171222_fov_bkg.pi", 900.0], "num_pixels": 128, "fov": 32.0, "aimpt_coords": [0.0, 0.0], "chips": [["Box", 0, 0, 128, 128]], "focal_length": 4.0, "dither": True, "psf": ["gaussian", 10.0], "imaging": True, "grating": False, } instrument_registry["lem_inner_array"] = { "name": "lem_inner_array", "arf": "lem_300522.arf", "rmf": "lem_1.3ev_110422.rmf", "bkgnd": ["lem_1.3eV_171222_fov_bkg.pi", 900.0], "num_pixels": 28, "fov": 7.0, "aimpt_coords": [0.0, 0.0], "chips": [["Box", 0, 0, 28, 28]], "focal_length": 4.0, "dither": True, "psf": ["gaussian", 10.0], "imaging": True, "grating": False, } instrument_registry["lem_2eV"] = { "name": "lem_2eV", "arf": "lem_300522.arf", "rmf": "lem_2ev_110422.rmf", "bkgnd": ["lem_2eV_171222_fov_bkg.pi", 900.0], "num_pixels": 128, "fov": 32.0, "aimpt_coords": [0.0, 0.0], "chips": [["Box", 0, 0, 128, 128]], "focal_length": 4.0, "dither": True, "psf": ["gaussian", 10.0], "imaging": True, "grating": False, } instrument_registry["lem_0.9eV"] = { "name": "lem_0.9eV", "arf": "lem_300522.arf", "rmf": "lem_09ev_110422.rmf", "bkgnd": ["lem_09eV_171222_fov_bkg.pi", 900.0], "num_pixels": 128, "fov": 32.0, "aimpt_coords": [0.0, 0.0], "chips": [["Box", 0, 0, 128, 128]], "focal_length": 4.0, "dither": True, "psf": ["gaussian", 10.0], "imaging": True, "grating": False, } def add_instrument_to_registry(inst_spec): """ Add an instrument specification to the registry, contained in either a dictionary or a JSON file. The *inst_spec* must have the structure as shown below. The order is not important. If you use a JSON file, the structure is the same, but the file cannot include comments, and use "null" instead of "None", and "true" or "false" instead of "True" or "False". For the "chips" entry, "None" means no chips and the detector field of view is a single square. If you want to have multiple chips, they must be specified in a format described in the online documentation. >>> { ... "name": "lynx_hdxi", # The short name of the instrument ... "arf": "xrs_hdxi_3x10.arf", # The file containing the ARF ... "rmf": "xrs_hdxi.rmf", # The file containing the RMF ... "bkgnd": ["lynx_hdxi_particle_bkgnd.pha", 1.0], # The name of the particle background file and the area of extraction ... "fov": 20.0, # The field of view in arcminutes ... "focal_length": 10.0, # The focal length in meters ... "num_pixels": 4096, # The number of pixels on a side in the FOV ... "dither": True, # Whether to dither the instrument ... "psf": ["image", "chandra_psf.fits", 6], # The type of PSF and associated parameters ... "chips": [["Box", 0, 0, 4096, 4096]], # The specification for the chips ... "aimpt_coords": [0.0, 0.0], # The detector coordinates of the aimpoint ... "imaging": True # Whether this is an imaging instrument ... "grating": False # Whether this is a grating instrument ... } """ if isinstance(inst_spec, dict): inst = inst_spec elif os.path.exists(inst_spec): with open(inst_spec, "r") as f: inst = json.load(f) name = inst["name"] if name in instrument_registry: raise KeyError( f"The instrument with name {name} is already in the " f"registry! Assign a different name!" ) # Catch older JSON files which don't distinguish between imagings # and non-imagings if "imaging" not in inst: mylog.warning( "Instrument specifications must now include an 'imaging' " "item, which determines whether or not this instrument " "specification supports imaging. Default is True." ) inst["imaging"] = True if "grating" not in inst: mylog.warning( "Instrument specifications must now include an 'grating' " "item, which determines whether or not this instrument " "specification corresponds to a gratings instrument. " "Default is False." ) inst["grating"] = False if inst["grating"] and inst["imaging"]: raise RuntimeError( "Currently, gratings instrument specifications cannot " "have 'imaging' == True!" ) if inst["imaging"]: default_set = { "name", "arf", "rmf", "bkgnd", "fov", "chips", "aimpt_coords", "focal_length", "num_pixels", "dither", "psf", "imaging", "grating", } else: default_set = { "name", "arf", "rmf", "bkgnd", "focal_length", "imaging", "grating", } if inst["imaging"]: if inst["psf"] is not None and not isinstance(inst["psf"], list): raise RuntimeError( "The 'psf' option in the instrument needs to be " "a two-element list specifying the PSF type and " "additional arguments. See the SOXS documentation " "for details." ) if inst["bkgnd"] is not None and not isinstance(inst["bkgnd"], list): raise RuntimeError( "The 'bkgnd' option in the instrument needs to be " "either a two-element list specifying the background " "file and its area in square arcminutes, or a list of " "such two-element lists if the background is different " "on different chips. See the SOXS documentation for " "details." ) if "dep_name" in inst: mylog.warning( "The 'dep_name' option is no longer supported. Dropping it " "from the instrument specification." ) inst.pop("dep_name") my_keys = set(inst.keys()) if my_keys != default_set: missing = default_set.difference(my_keys) raise RuntimeError( f"One or more items is missing from the instrument " f"specification!\nItems needed: {missing}" ) instrument_registry[name] = inst mylog.debug( "The %s instrument specification has been added " "to the instrument registry.", name, ) return name def get_instrument_from_registry(name): """ Returns a copy of the instrument specification corresponding to *name*. """ if name not in instrument_registry: raise KeyError(f"Instrument '{name}' not in registry!") return deepcopy(instrument_registry[name]) def show_instrument_registry(): """ Print the contents of the instrument registry. """ for name, spec in instrument_registry.items(): print(f"Instrument: {name}") for k, v in spec.items(): print(f" {k}: {v}") def write_instrument_json(inst_name, filename): """ Write an instrument specification to a JSON file. Useful if one would like to create a new specification by editing an existing one. Parameters ---------- inst_name : string The instrument specification to write. filename : string The filename to write to. """ inst_dict = instrument_registry[inst_name] with open(filename, "w") as f: json.dump(inst_dict, f, indent=4)
[docs] def make_simple_instrument( base_inst, new_inst, fov, num_pixels, no_bkgnd=False, no_psf=False, no_dither=False ): """ Using an existing imaging instrument specification, make a simple square instrument given a field of view and a resolution. Parameters ---------- base_inst : string The name for the instrument specification to base the new one on. new_inst : string The name for the new instrument specification. fov : float, (value, unit) tuple, or :class:`~astropy.units.Quantity` The field of view in arcminutes. num_pixels : integer The number of pixels on a side. no_bkgnd : boolean, optional Set this new instrument to have no particle background. Default: False no_psf : boolean, optional Set this new instrument to have no spatial PSF. Default: False no_dither : boolean, optional Set this new instrument to have no dithering. Default: False """ sq_inst = get_instrument_from_registry(base_inst) if sq_inst["imaging"] is False: raise RuntimeError( "make_simple_instrument only works with imaging instruments!" ) sq_inst["name"] = new_inst sq_inst["chips"] = [["Box", 0, 0, num_pixels, num_pixels]] sq_inst["fov"] = parse_value(fov, "arcmin") sq_inst["num_pixels"] = num_pixels sq_inst["aimpt_coords"] = [0.0, 0.0] if no_bkgnd: sq_inst["bkgnd"] = None elif base_inst.startswith("aciss"): # Special-case ACIS-S to use the BI background on S3 sq_inst["bkgnd"] = "aciss" if no_psf: sq_inst["psf"] = None if sq_inst["dither"]: sq_inst["dither"] = not no_dither add_instrument_to_registry(sq_inst)