diff --git a/.gitignore b/.gitignore index e4aac3a..bd9ed0b 100644 --- a/.gitignore +++ b/.gitignore @@ -7,9 +7,11 @@ __pycache__/ # Python response_tools_py.egg-info/ +build/ # data directories response-information/attenuation-data/ response-information/detector-response-data/ response-information/effective-area-data/ -response-information/quantum-efficiency-data/ \ No newline at end of file +response-information/quantum-efficiency-data/ +response-information/atmospheric-data/ \ No newline at end of file diff --git a/assets/response-tools-py-figs/att-figs/atmospheric-transmissions.png b/assets/response-tools-py-figs/att-figs/atmospheric-transmissions.png new file mode 100644 index 0000000..34bbeea Binary files /dev/null and b/assets/response-tools-py-figs/att-figs/atmospheric-transmissions.png differ diff --git a/assets/response-tools-py-figs/att-figs/model-transmissions.png b/assets/response-tools-py-figs/att-figs/model-transmissions.png index bda1fce..094ca29 100644 Binary files a/assets/response-tools-py-figs/att-figs/model-transmissions.png and b/assets/response-tools-py-figs/att-figs/model-transmissions.png differ diff --git a/assets/response-tools-py-figs/att-figs/transmissions.png b/assets/response-tools-py-figs/att-figs/transmissions.png index 8970b3e..136abee 100644 Binary files a/assets/response-tools-py-figs/att-figs/transmissions.png and b/assets/response-tools-py-figs/att-figs/transmissions.png differ diff --git a/assets/response-tools-py-figs/det-resp-figs/cdte-response-matrix.png b/assets/response-tools-py-figs/det-resp-figs/cdte-response-matrix.png index a350328..20007c5 100644 Binary files a/assets/response-tools-py-figs/det-resp-figs/cdte-response-matrix.png and b/assets/response-tools-py-figs/det-resp-figs/cdte-response-matrix.png differ diff --git a/assets/response-tools-py-figs/det-resp-figs/cmos-response-matrices.png b/assets/response-tools-py-figs/det-resp-figs/cmos-response-matrices.png index a7a2cbf..871acc4 100644 Binary files a/assets/response-tools-py-figs/det-resp-figs/cmos-response-matrices.png and b/assets/response-tools-py-figs/det-resp-figs/cmos-response-matrices.png differ diff --git a/assets/response-tools-py-figs/response-figs/response-chain.png b/assets/response-tools-py-figs/response-figs/response-chain.png new file mode 100644 index 0000000..8650292 Binary files /dev/null and b/assets/response-tools-py-figs/response-figs/response-chain.png differ diff --git a/assets/response-tools-py-figs/response-figs/response-hit-combinations.png b/assets/response-tools-py-figs/response-figs/response-hit-combinations.png new file mode 100644 index 0000000..fa187a8 Binary files /dev/null and b/assets/response-tools-py-figs/response-figs/response-hit-combinations.png differ diff --git a/response-information/atmospheric-data/README.md b/response-information/atmospheric-data/README.md new file mode 100644 index 0000000..9775858 --- /dev/null +++ b/response-information/atmospheric-data/README.md @@ -0,0 +1,3 @@ +# Detector quantum efficiency data files + +Only really here so this folder is tracked by `git`. diff --git a/response-tools-py/examples/README.md b/response-tools-py/examples/README.md index 91fcd85..82e351f 100644 --- a/response-tools-py/examples/README.md +++ b/response-tools-py/examples/README.md @@ -1,3 +1,13 @@ # `response-tools-py` Examples This module contains functions to handle the visualization of the various response data products. + +## 1. Data handling examples + +- [using_the_functions.py](./functions_and_outputs.py) + - Shows how to use the functions and their outputs in the package. + +## 2. Plotting examples + +- [plot_arf_rmf_drm.py](./plot_arf_rmf_drm.py) + - Shows how to obtain and plot the ARF, RMF, and DRM for Telescope 2. diff --git a/response-tools-py/examples/functions_and_outputs.py b/response-tools-py/examples/functions_and_outputs.py new file mode 100644 index 0000000..7e40302 --- /dev/null +++ b/response-tools-py/examples/functions_and_outputs.py @@ -0,0 +1,231 @@ +""" +Function & Outputs +------------------ + +Script showing a quick example on how to use the functions. + +The chosen telescope for the example is Telescope 2 with photon path: +- Thermal blanket -> Marshall 10-shell X-7 -> Al (0.015") -> CdTe4 + +Mainly a quick example on unit aware objects and how to access the +returned dataclass objects. + +This example shows the use of nice high level functions that are tied to +FOXSI-4 telescopes. +- responses +- telescope_parts + +If you're looking for access to response data of the +individual components with more freedom then you'll likely be interested +in the longer named moduels in the package like: +- attenuation +- detector_response +- effective_area +- quantum_efficiency + +THIS SCRIPT IS AN EXAMPLE, DO NOT USE DIRECTLY. +----------------------------------------------- +If you would like to run the contents of this script and play around +with it then either use this file and be aware of not adding/commiting +any changes you make and/or just make a copy of this file, put it +somewhere else on your computer, and play around with the copy. +""" + +import numpy as np + +""" +For higher level user engagement, the following two modules are the ones +likely to use. The `telescope_parts` are well-named functions tied to +FOXSI positions: E.g., `foxsi4_position2_optics` will return FOXSI-4's +optical information for Position/Telescope 2. + +The `responses` module will contains functions that combine the relevant +`telescope_parts` functions into one to return higher level products +such as the telescope's Ancillary Response Function (ARF), +Redistribution Matrix Function (RMF), and/or Detector Response Matrix +(DRM). +""" + +import response_tools_py.responses as responses +import response_tools_py.telescope_parts as telescope_parts + +""" +Let's look at the `foxsi4_position2_optics` function since we mentioned +it earlier. To see the documentation for this, and any function, we can +always run: + +``` +>>> help(telescope_parts.foxsi4_position2_optics) +Help on function foxsi4_position2_optics in module +response_tools_py.telescope_parts: + +foxsi4_position2_optics(mid_energies, off_axis_angle) + Position 2 MSFC heritage X-7 optic effective areas. + + Parameters + ---------- + mid_energies : `astropy.units.quantity.Quantity` + The energies at which the position 2 optics is required. If + `numpy.nan<foxsi4_position2_optics', + 'mid_energies': , + 'off_axis_angle': , + 'effective_areas': , + 'optic_id': 'X-7', + 'model': False} +``` + +Note: there is a method called `print_contents()` you can use on the +function output that might format the contents a little nicer than the +above you may wish to use. + +Each field can be accessed with the displayed name. For excample, to get +the effective areas of the optics, simply: +""" + +print(pos2_optics.effective_areas) + +""" +Notice that these are also unit-aware, help you see at a glance you're +working with a product that you might expect. + +The `telescope_parts.foxsi4_position2_optics` function is helpful but an +even higher level exists that will allow a user to specify a FOXSI-4 +telescope to obtain the Ancillary Response Function (ARF), +Redistribution Matrix Function (RMF), and/or Detector Response Matrix +(DRM). + +First, we can get the RMF for a telescope, say, Telescope 2: +""" + +pos2_rmf = responses.foxsi4_telescope2_rmf(region=0) + +""" +The `region` input refers to the different pitch regions across the CdTe +detectors. IF you would rather specificy by passing the pitch (that is +unit aware) then practice running `help` on the function and check the +documentation. + +The RMF defined the input and output energy axes for the detector so we +might as well access the RMF input energies for those energies we want +the ARF values for: +""" + +mid_energies = (pos2_rmf.input_energy_edges[:-1]\ + +pos2_rmf.input_energy_edges[1:])/2 +pos2_arf = responses.foxsi4_telescope2_arf(mid_energies=mid_energies, + off_axis_angle=0< Marshall 10-shell X-7 -> Al (0.015") -> CdTe4 + +THIS SCRIPT IS AN EXAMPLE, DO NOT USE DIRECTLY. +----------------------------------------------- +If you would like to run the contents of this script and play around +with it then either use this file and be aware of not adding/commiting +any changes you make and/or just make a copy of this file, put it +somewhere else on your computer, and play around with the copy. +""" + +import astropy.units as u +from matplotlib.colors import LogNorm +import matplotlib.gridspec as gridspec +import matplotlib.pyplot as plt +import numpy as np + +import response_tools_py.responses as responses + +fig = plt.figure(figsize=(18, 5)) +gs = gridspec.GridSpec(1, 3) + +# set up the ARF with the RMF information then make the DRM +off_axis_angle = 0 << u.arcmin +pos_rmf = responses.foxsi4_telescope2_rmf(region=0) +mid_energies = (pos_rmf.input_energy_edges[:-1]\ + +pos_rmf.input_energy_edges[1:])/2 +pos_arf = responses.foxsi4_telescope2_arf(mid_energies=mid_energies, + off_axis_angle=off_axis_angle) +pos_drm = responses.foxsi4_telescope_response(pos_arf, pos_rmf) + +# the ARF info +gs_ax0 = fig.add_subplot(gs[0, 0]) +gs_ax0.plot(pos_arf.mid_energies, pos_arf.response) +gs_ax0.set_xlabel(f"Photon Energy [{pos_arf.mid_energies.unit:latex}]") +gs_ax0.set_ylabel(f"Response [{pos_arf.response.unit:latex}]") +gs_ax0.set_title(f"Telescope 2: ARF") + +# the RMF info +gs_ax1 = fig.add_subplot(gs[0, 1]) +r = gs_ax1.imshow(pos_rmf.response.value, + origin="lower", + norm=LogNorm(vmin=0.001), + extent=[np.min(pos_rmf.output_energy_edges.value), + np.max(pos_rmf.output_energy_edges.value), + np.min(pos_rmf.input_energy_edges.value), + np.max(pos_rmf.input_energy_edges.value)] + ) +cbar = plt.colorbar(r) +cbar.ax.set_ylabel(f"Response [{pos_rmf.response.unit:latex}]") +gs_ax1.set_xlabel(f"Count Energy [{pos_rmf.output_energy_edges.unit:latex}]") +gs_ax1.set_ylabel(f"Photon Energy [{pos_rmf.input_energy_edges.unit:latex}]") +gs_ax1.set_title(f"Telescope 2: RMF") + +# the DRM info +gs_ax2 = fig.add_subplot(gs[0, 2]) +r = gs_ax2.imshow(pos_drm.response.value, + origin="lower", + norm=LogNorm(vmin=0.001), + extent=[np.min(pos_drm.output_energy_edges.value), + np.max(pos_drm.output_energy_edges.value), + np.min(pos_drm.input_energy_edges.value), + np.max(pos_drm.input_energy_edges.value)] + ) +cbar = plt.colorbar(r) +cbar.ax.set_ylabel(f"Response [{pos_drm.response.unit:latex}]") +gs_ax2.set_xlabel(f"Count Energy [{pos_drm.output_energy_edges.unit:latex}]") +gs_ax2.set_ylabel(f"Photon Energy [{pos_drm.input_energy_edges.unit:latex}]") +gs_ax2.set_title(f"Telescope 2: DRM") + +plt.tight_layout() +plt.show() \ No newline at end of file diff --git a/response-tools-py/response_tools_py/attenuation.py b/response-tools-py/response_tools_py/attenuation.py index 307c070..85b5c7c 100644 --- a/response-tools-py/response_tools_py/attenuation.py +++ b/response-tools-py/response_tools_py/attenuation.py @@ -1,8 +1,11 @@ """Code to load different attenuators. """ +from dataclasses import dataclass import logging import os import pathlib +import sys +import warnings from astropy.io import fits import astropy.units as u @@ -10,131 +13,537 @@ import pandas import scipy +from response_tools_py.util import BaseOutput, native_resolution + ATT_PATH = os.path.join(pathlib.Path(__file__).parent, "..", "..", "response-information", "attenuation-data") +ATM_PATH = os.path.join(pathlib.Path(__file__).parent, "..", "..", "response-information", "atmospheric-data") +ASSETS_PATH = os.path.join(pathlib.Path(__file__).parent, "..", "..", "assets", "response-tools-py-figs", "att-figs") + +@dataclass +class AttOutput(BaseOutput): + """Class for keeping track of attenuation response values.""" + # numbers + transmissions: u.Quantity + # bookkeeping + attenuation_type: str + model: bool + # any other fields needed can be added here + # can even add with a default so the input is not required for every other instance + mid_energies: u.Quantity = np.nan<eV + Need an array of times included + -> 10,284 entries and t=0 is index `0` while t=100 is index `2000` + -> final time is 100/2000 * 10284 = 514.2 + + Parameters + ---------- + mid_energies : `astropy.units.quantity.Quantity` + The energies at which the transmission is required. If + `numpy.nan<0 else energy_inds + # energy_inds = np.insert(energy_inds, 1, energy_inds[-1]+1) if energy_inds[-1]<(len(energy_inds)-1) else energy_inds + + times = native_times[time_inds] + transmissions = transmission[:,time_inds] + + tave_transmissions = np.mean(transmissions, axis=1) + + return AttOutput(filename=_f, + function_path=f"{sys._getframe().f_code.co_name}", + mid_energies=mid_energies< 60< 80< 100< Region 0 + 80< Region 1 + 100< Region 2 + + side : `str` + Define the side on the detector the user requires the response + from. Must be in ["pt", "merged"]. + Default: "merged" + + event_type : `str` + Define the type of event trigger being considered in the + response. Must be in ["1hit", "2hit", ("all", "mix")]. + Note: \"all\" and \"mix\" are the same but some from different + naming conventions on the merged and individual detector sides. + This will be fixed at some point in the future. + Default: "all" + + file : `str` + The file for the RMF. + + Returns + ------- + : `DetectorResponseOutput` + An object containing all the redistribution matrix information. + See accessible information using `.contents` on the output. + """ + + have_region, have_pitch = (region is not None), (pitch is not None) + pitch2region = {60< X10/FM2 + Position 3 -> X09/FM1 + Position 6 -> X11/FM3 + Default: None + + use_model : `bool` + Defines whether to use the measured values for the optic (False) + or the modelled values (True). + Default: False + + file : `str` or `None` + Gives the ability to provide custom files for the information. + Default: None + + Returns + ------- + : `EffAreaOutput` + An object containing the energies for each effective area, the + effective areas, and more. See accessible information using + `.contents` on the output. """ + if off_axis_angle is not None: + logging.warning(f"The `off_axis_angle` input for MSFC high-resolution optics ({sys._getframe().f_code.co_name}) is not yet implemented.") # msfc_hi_res effective areas _f = os.path.join(EFF_PATH, "FOXSI4_Module_MSFC_HiRes_EA_with_models_v1.txt") if file is None else file e, f1, f2, f3, f1_m, f2_m, f3_m = np.loadtxt(_f).T @@ -78,19 +179,33 @@ def eff_area_msfc_hi_res(mid_energies, file=None, position=None, use_model=False fm2 <<= u.cm**2 fm3 <<= u.cm**2 if position==0: - return mid_energies, np.interp(mid_energies.value, e.value, fm2.value, left=fm2.value[0], right=0) << u.cm**2 + ea_vals, opt_id = fm2.value, "X10/FM2" elif position==3: - return mid_energies, np.interp(mid_energies.value, e.value, fm1.value, left=fm1.value[0], right=0) << u.cm**2 + ea_vals, opt_id = fm1.value, "X09/FM1" elif position==6: - return mid_energies, np.interp(mid_energies.value, e.value, fm3.value, left=fm3.value[0], right=0) << u.cm**2 + ea_vals, opt_id = fm3.value, "X11/FM3" else: - logging.warning("`position` must be 0 (X10/FM2), 3 (X09/FM1), or 6 (X11/FM3).") + logging.warning(f"The `position` in {sys._getframe().f_code.co_name} must be 0 (X10/FM2), 3 (X09/FM1), or 6 (X11/FM3).") + return + + return EffAreaOutput(filename=_f, + function_path=f"{sys._getframe().f_code.co_name}", + mid_energies=mid_energies, + off_axis_angle="N/A", + effective_areas=np.interp(mid_energies.value, + e.value, + ea_vals, + left=ea_vals[0], + right=0) << u.cm**2, + optic_id=opt_id, + model=use_model, + ) @u.quantity_input(mid_energies=u.keV) -def eff_area_msfc(mid_energies, file=None): - """Return early MSCF hi-res effective areas interpolated to the given energies.""" +def _eff_area_msfc(mid_energies, file=None): + """Early MSCF hi-res eff. areas interpolated to given energies.""" # msfc_hi_res effective areas - logging.warning("Caution: This might not be the function you are looking for, please see `eff_area_msfc_hi_res`.") + logging.warning(f"Caution: This might not be the function ({sys._getframe().f_code.co_name}) you are looking for, please see `eff_area_msfc_hi_res`.") logging.warning("This current function loads in some very early numbers for the new FOXSI-4 MSFC optics.") _f = os.path.join(EFF_PATH, "3Inner_EA_EPDL97_14AA.csv") if file is None else file msfc_hi_res = pandas.read_csv(_f).to_numpy()[:,1:] # remove the first column that only indexes @@ -99,65 +214,224 @@ def eff_area_msfc(mid_energies, file=None): msfc_hi_res_effa = msfc_hi_res_effas08 + msfc_hi_res_effas10 mid_energies = native_resolution(native_x=msfc_hi_res_es, input_x=mid_energies) - return mid_energies, np.interp(mid_energies.value, msfc_hi_res_es.value, msfc_hi_res_effa.value, left=msfc_hi_res_effa.value[0], right=0) << u.cm**2 + + return EffAreaOutput(filename=_f, + function_path=f"{sys._getframe().f_code.co_name}", + mid_energies=mid_energies, + off_axis_angle="N/A", + effective_areas=np.interp(mid_energies.value, + msfc_hi_res_es.value, + msfc_hi_res_effa.value, + left=msfc_hi_res_effa.value[0], + right=0) << u.cm**2, + optic_id="Early-MSFC-EAs", + model=True, + ) @u.quantity_input(mid_energies=u.keV) -def eff_area_nagoya(mid_energies, file=None): - """Return early Nagoya SXR hi-res effective areas interpolated to the given energies.""" +def _eff_area_nagoya(mid_energies, file=None): + """Early Nagoya SXR hi-res eff. areas interpolated to energies.""" # nagoya sxr effective areas - logging.warning("Caution: This might not be the function you are looking for and has other effects included than just optics.") + logging.warning(f"Caution: This might not be the function ({sys._getframe().f_code.co_name}) you are looking for and has other effects included than just optics.") logging.warning("This current function loads in some very early numbers for the new FOXSI-4 Nagoya SXR optics.") _f = os.path.join(EFF_PATH, "effective-area_raytracing_soft-xray-optic_on-axis.txt") if file is None else file nagoya_sxr = np.loadtxt(_f) nagoya_sxr_es, nagoya_sxr_effa = nagoya_sxr[:,0] << u.keV, nagoya_sxr[:,1]/100 << u.cm**2 mid_energies = native_resolution(native_x=nagoya_sxr_es, input_x=mid_energies) - return mid_energies, np.interp(mid_energies.value, nagoya_sxr_es.value, nagoya_sxr_effa.value, left=0, right=0) << u.cm**2 -@u.quantity_input(mid_energies=u.keV) -def eff_area_nagoya_hxt(mid_energies, file=None): - """Return Nagoya HXR hi-res effective areas (measured) interpolated to the given energies.""" - # nagoya sxr effective areas - _f = os.path.join(EFF_PATH, "nagoya_hxt_onaxis_measurement_v1.txt") if file is None else file - nagoya_sxr = np.loadtxt(_f) - # energy, energy_err, area, area_err - nagoya_sxr_es, _, nagoya_sxr_effa, _ = nagoya_sxr[:,0] << u.keV, nagoya_sxr[:,1] << u.keV, nagoya_sxr[:,2] << u.cm**2, nagoya_sxr[:,3] << u.cm**2 + return EffAreaOutput(filename=_f, + function_path=f"{sys._getframe().f_code.co_name}", + mid_energies=mid_energies, + off_axis_angle="N/A", + effective_areas=np.interp(mid_energies.value, + nagoya_sxr_es.value, + nagoya_sxr_effa.value, + left=0, + right=0) << u.cm**2, + optic_id="Early-Nagoya-SXR-EAs", + model=True, + ) + +@u.quantity_input(mid_energies=u.keV, off_axis_angle=u.arcmin) +def eff_area_nagoya_hxt(mid_energies, off_axis_angle=None, use_model=False, file=None): + """Nagoya HXR hi-res effective areas interpolated to given energies. + + Parameters + ---------- + mid_energies : `astropy.units.quantity.Quantity` + The energies at which the transmission is required. If + `numpy.nan< Position 0 -> X10/FM2 + Telescope 1 -> Position 1 -> Nagoya-SXR + Default: None + + file : `str` or `None` + Gives the ability to provide custom files for the information. + Default: None + + Returns + ------- + : `EffAreaOutput` + An object containing the energies for each effective area, the + effective areas, and more. See accessible information using + `.contents` on the output. """ - if telescope is None: - logging.warning("`telescope` input in `eff_area_cmos()` must be 0 or 1.") + if (telescope is None) or (telescope not in [0,1]): + logging.warning(f"The `telescope` input in {sys._getframe().f_code.co_name} must be 0 or 1.") return _f = os.path.join(EFF_PATH, f"foxsi4_telescope-{telescope}_BASIC_mirror_effective_area_v1.fits") if file is None else file with fits.open(_f) as hdul: es, effas = hdul[2].data << u.keV, hdul[1].data << u.cm**2 mid_energies = native_resolution(native_x=es, input_x=mid_energies) - return mid_energies, np.interp(mid_energies.value, es.value, effas.value, left=0, right=0) << u.cm**2 + + position_alias = {0:"CMOS-X10/FM2", 1:"CMOS-Nagoya-SXT"} + + return EffAreaOutput(filename=_f, + function_path=f"{sys._getframe().f_code.co_name}", + mid_energies=mid_energies, + off_axis_angle="N/A", + effective_areas=np.interp(mid_energies.value, + es.value, + effas.value, + left=0, + right=0) << u.cm**2, + optic_id=position_alias[telescope], + model=True, + ) @u.quantity_input(mid_energies=u.keV) -def eff_area_cmos_telescope(mid_energies, file=None, telescope=None): +def eff_area_cmos_telescope(mid_energies, telescope=None, file=None): """Return full telescope(?) with CMOS effective areas interpolated to the given energies. @@ -167,13 +441,39 @@ def eff_area_cmos_telescope(mid_energies, file=None, telescope=None): **Note** Will come back to this when there are off-axis angles and do 2D interp. Currently only on-axis angle so just interp across energies. + + Parameters + ---------- + mid_energies : `astropy.units.quantity.Quantity` + The energies at which the transmission is required. If + `numpy.nan< Position 0 -> X10/FM2 + Telescope 1 -> Position 1 -> Nagoya-SXR + Default: None + + file : `str` or `None` + Gives the ability to provide custom files for the information. + Default: None + + Returns + ------- + : `EffAreaOutput` + An object containing the energies for each effective area, the + effective areas, and more. See accessible information using + `.contents` on the output. """ - if telescope is None: - logging.warning("`telescope` input in `eff_area_cmos_telescope()` must be 0 or 1.") + if (telescope is None) or (telescope not in [0,1]): + logging.warning(f"The `telescope` input in {sys._getframe().f_code.co_name} must be 0 or 1.") return # not tracking this combined response product - logging.warning("Caution: This output will include a combined response from various elements.") + logging.warning(f"Caution: The {sys._getframe().f_code.co_name} output will include a combined response from various elements.") logging.warning("If you care about what elements are included then proceed carefully.") logging.warning("For current file, see PR#11 in the `cmos-tools` repository.") @@ -182,7 +482,21 @@ def eff_area_cmos_telescope(mid_energies, file=None, telescope=None): # _ is the off-axis angle but it's just [0] at the minute ea_energies, _, effas = hdul[2].data << u.keV, hdul[3].data << u.arcsec, hdul[1].data << u.cm**2 mid_energies = native_resolution(native_x=ea_energies, input_x=mid_energies) - return mid_energies, np.interp(mid_energies.value, ea_energies.value, effas.value, left=0, right=0) << u.cm**2 + + position_alias = {0:"Telescope-CMOS-X10/FM2", 1:"Telescope-CMOS-Nagoya-SXT"} + + return EffAreaOutput(filename=_f, + function_path=f"{sys._getframe().f_code.co_name}", + mid_energies=mid_energies, + off_axis_angle="N/A", + effective_areas=np.interp(mid_energies.value, + ea_energies.value, + effas.value, + left=0, + right=0) << u.cm**2, + optic_id=position_alias[telescope], + model=True, + ) def asset_cmos_plot(save_asset=False): """Plot the CMOS data to visually check.""" @@ -193,29 +507,29 @@ def asset_cmos_plot(save_asset=False): gs = gridspec.GridSpec(1, 2) gs_ax0 = fig.add_subplot(gs[0, 0]) - _, a0 = eff_area_cmos(mid_energies, file=None, telescope=0) - _, a1 = eff_area_cmos(mid_energies, file=None, telescope=1) - _, msfcp0 = eff_area_msfc_hi_res(mid_energies, file=None, position=0) - _, msfcp0m = eff_area_msfc_hi_res(mid_energies, file=None, position=0, use_model=True) - _, nag = eff_area_nagoya_sxt(mid_energies) - gs_ax0.plot(mid_energies, a0, label="CMOS telescope 0, position 0") - gs_ax0.plot(mid_energies, a1, label="CMOS telescope 1, position 1") - gs_ax0.plot(mid_energies, msfcp0, label="MSFC (meas.) position 0") - gs_ax0.plot(mid_energies, msfcp0m, label="MSFC (mod.) position 0") - gs_ax0.plot(mid_energies, nag, label="Nagoya (meas.) position 1") + a0 = eff_area_cmos(mid_energies, file=None, telescope=0) + a1 = eff_area_cmos(mid_energies, file=None, telescope=1) + msfcp0 = eff_area_msfc_hi_res(mid_energies, file=None, position=0) + msfcp0m = eff_area_msfc_hi_res(mid_energies, file=None, position=0, use_model=True) + nag_sxt = eff_area_nagoya_sxt(mid_energies) + gs_ax0.plot(mid_energies, a0.effective_areas, label="CMOS telescope 0, position 0") + gs_ax0.plot(mid_energies, a1.effective_areas, label="CMOS telescope 1, position 1") + gs_ax0.plot(mid_energies, msfcp0.effective_areas, label="MSFC (meas.) position 0") + gs_ax0.plot(mid_energies, msfcp0m.effective_areas, label="MSFC (mod.) position 0") + gs_ax0.plot(mid_energies, nag_sxt.effective_areas, label="Nagoya (meas.) position 1") gs_ax0.set_title("CMOS Optics") - gs_ax0.set_ylabel(f"Effective Area [{a0.unit:latex}]") + gs_ax0.set_ylabel(f"Effective Area [{a0.effective_areas.unit:latex}]") gs_ax0.set_xlabel(f"Energy [{mid_energies.unit:latex}]") plt.legend() # CMOS full telescope ones gs_ax1 = fig.add_subplot(gs[0, 1]) - _, a0ft = eff_area_cmos_telescope(mid_energies, file=None, telescope=0) - _, a1ft = eff_area_cmos_telescope(mid_energies, file=None, telescope=1) - gs_ax1.plot(mid_energies, a0ft, label="CMOS telescope 0, position 0") - gs_ax1.plot(mid_energies, a1ft, label="CMOS telescope 1, position 1") + a0ft = eff_area_cmos_telescope(mid_energies, file=None, telescope=0) + a1ft = eff_area_cmos_telescope(mid_energies, file=None, telescope=1) + gs_ax1.plot(mid_energies, a0ft.effective_areas, label="CMOS telescope 0, position 0") + gs_ax1.plot(mid_energies, a1ft.effective_areas, label="CMOS telescope 1, position 1") gs_ax1.set_title("CMOS Telescope?") - gs_ax1.set_ylabel(f"Effective Area [{a0ft.unit:latex}]") + gs_ax1.set_ylabel(f"Effective Area [{a0ft.effective_areas.unit:latex}]") gs_ax1.set_xlabel(f"Energy [{mid_energies.unit:latex}]") plt.legend() @@ -225,7 +539,7 @@ def asset_cmos_plot(save_asset=False): plt.savefig(os.path.join(ASSETS_PATH,"cmos-sxr-optics-resp.png"), dpi=200, bbox_inches="tight") plt.show() -def asset_cmos_sxr(save_asset=False): +def asset_cmos_files(save_asset=False): """Plot the CMOS data to visually check.""" mid_energies = np.linspace(0, 20, 1000)<=0 else 2 - _, _, efs = eff_area_msfc_10shell(ea_energies, off_axis=oaa<=0 else 2 - _, _, efs = eff_area_msfc_10shell(ea_energies, off_axis=oaa< Position 0 + Telescope 1 -> Position 1 + Default: None + + file : `str` or `None` + Gives the ability to provide custom files for the information. + Default: None + + Returns + ------- + : `QuantumEffOutput` + An object containing the energies for each quantum efficency, + the quantum efficencies, and more. See accessible information + using `.contents` on the output. """ - if telescope is None: - logging.warning("`telescope` input in `qe_cmos()` must be 0 or 1.") + if (telescope is None) or (telescope not in [0,1]): + logging.warning(f"The `telescope` input in {sys._getframe().f_code.co_name} must be 0 or 1.") return _f = os.path.join(Q_PATH, f"foxsi4_telescope-{telescope}_BASIC_sensor_quantum_efficiency_v1.fits") if file is None else file with fits.open(_f) as hdul: es, qe = hdul[2].data << u.keV, hdul[1].data << u.dimensionless_unscaled - return np.interp(mid_energies.value, es.value, qe.value, left=0, right=0) << u.dimensionless_unscaled - -if __name__=="__main__": - import matplotlib.pyplot as plt + mid_energies = native_resolution(native_x=es, input_x=mid_energies) - SAVE_ASSETS = False - assets_dir = os.path.join(pathlib.Path(__file__).parent, "..", "..", "assets", "response-tools-py-figs", "quantum-eff-figs") - pathlib.Path(assets_dir).mkdir(parents=True, exist_ok=True) + return QuantumEffOutput(filename=_f, + function_path=f"{sys._getframe().f_code.co_name}", + mid_energies=mid_energies, + quantum_efficiency=np.interp(mid_energies.value, + es.value, + qe.value, + left=0, + right=0) << u.dimensionless_unscaled, + detector="CMOS{telescope}-Quantum-Efficiency" + ) +def asset_qe(save_asset=False): mid_energies = np.linspace(0, 20, 1000)< 60< 80< 100< Region 0 + 80< Region 1 + 100< Region 2 + + _side : `str` + Define the side on the detector the user requires the response + from. Must be in ["pt", "merged"]. + Default: "merged" + + _event_type : `str` + Define the type of event trigger being considered in the + response. Must be in ["1hit", "2hit", ("all", "mix")]. + Note: \"all\" and \"mix\" are the same but some from different + naming conventions on the merged and individual detector sides. + This will be fixed at some point in the future. + Default: "all" + + Returns + ------- + : `responses.Response2DOutput` + An object containing all the redistribution matrix information. + See accessible information using `.contents` on the output. + """ + + rmf = tp.foxsi4_position2_detector_response(region=region, + pitch=pitch, + _side=_side, + _event_type=_event_type) + func_name = sys._getframe().f_code.co_name + rmf.update_function_path(func_name) + + return Response2DOutput(filename="No-File", + function_path=func_name, + input_energy_edges=rmf.input_energy_edges, + output_energy_edges=rmf.output_energy_edges, + response=rmf.detector_response, + response_type="RMF", + telescope="foxsi4-2", + elements=(rmf, + ), + ) + +# telescope 3 +@u.quantity_input(mid_energies=u.keV, off_axis_angle=u.arcmin) +def foxsi4_telescope3_arf(mid_energies, off_axis_angle=None): + """The Ancillary Response Function (ARF) for Telescope 3. + + **DOES NOT** include atmospheric attenuation from flight. + + Parameters + ---------- + mid_energies : `astropy.units.quantity.Quantity` + The energies at which the Telescope 3 componented are calculated. + If `numpy.nan< 60< 80< 100< Region 0 + 80< Region 1 + 100< Region 2 + + _side : `str` + Define the side on the detector the user requires the response + from. Must be in ["pt", "merged"]. + Default: "merged" + + _event_type : `str` + Define the type of event trigger being considered in the + response. Must be in ["1hit", "2hit", ("all", "mix")]. + Note: \"all\" and \"mix\" are the same but some from different + naming conventions on the merged and individual detector sides. + This will be fixed at some point in the future. + Default: "all" + + Returns + ------- + : `responses.Response2DOutput` + An object containing all the redistribution matrix information. + See accessible information using `.contents` on the output. + """ + + rmf = tp.foxsi4_position3_detector_response(region=region, + pitch=pitch, + _side=_side, + _event_type=_event_type) + func_name = sys._getframe().f_code.co_name + rmf.update_function_path(func_name) + + return Response2DOutput(filename="No-File", + function_path=func_name, + input_energy_edges=rmf.input_energy_edges, + output_energy_edges=rmf.output_energy_edges, + response=rmf.detector_response, + response_type="RMF", + telescope="foxsi4-3", + elements=(rmf, + ), + ) + +# telescope 4 +@u.quantity_input(mid_energies=u.keV, off_axis_angle=u.arcmin) +def foxsi4_telescope4_arf(mid_energies, off_axis_angle=None): + """The Ancillary Response Function (ARF) for Telescope 4. + + **DOES NOT** include atmospheric attenuation from flight. + + Parameters + ---------- + mid_energies : `astropy.units.quantity.Quantity` + The energies at which the Telescope 4 componented are calculated. + If `numpy.nan< 60< 80< 100< Region 0 + 80< Region 1 + 100< Region 2 + + _side : `str` + Define the side on the detector the user requires the response + from. Must be in ["pt", "merged"]. + Default: "merged" + + _event_type : `str` + Define the type of event trigger being considered in the + response. Must be in ["1hit", "2hit", ("all", "mix")]. + Note: \"all\" and \"mix\" are the same but some from different + naming conventions on the merged and individual detector sides. + This will be fixed at some point in the future. + Default: "all" + + Returns + ------- + : `responses.Response2DOutput` + An object containing all the redistribution matrix information. + See accessible information using `.contents` on the output. + """ + + rmf = tp.foxsi4_position4_detector_response(region=region, + pitch=pitch, + _side=_side, + _event_type=_event_type) + func_name = sys._getframe().f_code.co_name + rmf.update_function_path(func_name) + + return Response2DOutput(filename="No-File", + function_path=func_name, + input_energy_edges=rmf.input_energy_edges, + output_energy_edges=rmf.output_energy_edges, + response=rmf.detector_response, + response_type="RMF", + telescope="foxsi4-4", + elements=(rmf, + ), + ) + +# telescope 5 +@u.quantity_input(mid_energies=u.keV, off_axis_angle=u.arcmin) +def foxsi4_telescope5_arf(mid_energies, off_axis_angle): + """The Ancillary Response Function (ARF) for Telescope 5. + + **DOES NOT** include atmospheric attenuation from flight. + + Parameters + ---------- + mid_energies : `astropy.units.quantity.Quantity` + The energies at which the Telescope 5 componented are calculated. + If `numpy.nan< 60< 80< 100< Region 0 + 80< Region 1 + 100< Region 2 + + _side : `str` + Define the side on the detector the user requires the response + from. Must be in ["pt", "merged"]. + Default: "merged" + + _event_type : `str` + Define the type of event trigger being considered in the + response. Must be in ["1hit", "2hit", ("all", "mix")]. + Note: \"all\" and \"mix\" are the same but some from different + naming conventions on the merged and individual detector sides. + This will be fixed at some point in the future. + Default: "all" + + Returns + ------- + : `responses.Response2DOutput` + An object containing all the redistribution matrix information. + See accessible information using `.contents` on the output. + """ + + rmf = tp.foxsi4_position5_detector_response(region=region, + pitch=pitch, + _side=_side, + _event_type=_event_type) + func_name = sys._getframe().f_code.co_name + rmf.update_function_path(func_name) + + return Response2DOutput(filename="No-File", + function_path=func_name, + input_energy_edges=rmf.input_energy_edges, + output_energy_edges=rmf.output_energy_edges, + response=rmf.detector_response, + response_type="RMF", + telescope="foxsi4-5", + elements=(rmf, + ), + ) + +def asset_response_chain_plot(save_asset=False): + """Plot the response chain data to visually check.""" + pos2arffunc = {2:foxsi4_telescope2_arf, + 3:foxsi4_telescope3_arf, + 4:foxsi4_telescope4_arf, + 5:foxsi4_telescope5_arf, + } + pos2rmffunc = {2:foxsi4_telescope2_rmf, + 3:foxsi4_telescope3_rmf, + 4:foxsi4_telescope4_rmf, + 5:foxsi4_telescope5_rmf, + } + + fig = plt.figure(figsize=(11, 10)) + positions = list(pos2rmffunc.keys()) + gs = gridspec.GridSpec(len(positions), 3) + + for c, key in enumerate(positions): + off_axis_angle = 0 << u.arcmin + pos_rmf = pos2rmffunc[key](region=0) + mid_energies = (pos_rmf.input_energy_edges[:-1]+pos_rmf.input_energy_edges[1:])/2 + pos_arf = pos2arffunc[key](mid_energies=mid_energies, off_axis_angle=off_axis_angle) + pos_drm = foxsi4_telescope_response(pos_arf, pos_rmf) + + gs_ax0 = fig.add_subplot(gs[c, 0]) + gs_ax0.plot(pos_arf.mid_energies, pos_arf.response) + gs_ax0.set_xlabel(f"Photon Energy [{pos_arf.mid_energies.unit:latex}]") + gs_ax0.set_ylabel(f"Response [{pos_arf.response.unit:latex}]") + gs_ax0.set_title(f"Pos. {key}: ARF") + + gs_ax1 = fig.add_subplot(gs[c, 1]) + r = gs_ax1.imshow(pos_rmf.response.value, + origin="lower", + norm=LogNorm(vmin=0.001), + extent=[np.min(pos_rmf.output_energy_edges.value), + np.max(pos_rmf.output_energy_edges.value), + np.min(pos_rmf.input_energy_edges.value), + np.max(pos_rmf.input_energy_edges.value)] + ) + cbar = plt.colorbar(r) + cbar.ax.set_ylabel(f"Response [{pos_rmf.response.unit:latex}]") + gs_ax1.set_xlabel(f"Count Energy [{pos_rmf.output_energy_edges.unit:latex}]") + gs_ax1.set_ylabel(f"Photon Energy [{pos_rmf.input_energy_edges.unit:latex}]") + gs_ax1.set_title(f"Pos. {key}: RMF") + + gs_ax2 = fig.add_subplot(gs[c, 2]) + r = gs_ax2.imshow(pos_drm.response.value, + origin="lower", + norm=LogNorm(vmin=0.001), + extent=[np.min(pos_drm.output_energy_edges.value), + np.max(pos_drm.output_energy_edges.value), + np.min(pos_drm.input_energy_edges.value), + np.max(pos_drm.input_energy_edges.value)] + ) + cbar = plt.colorbar(r) + cbar.ax.set_ylabel(f"Response [{pos_drm.response.unit:latex}]") + gs_ax2.set_xlabel(f"Count Energy [{pos_drm.output_energy_edges.unit:latex}]") + gs_ax2.set_ylabel(f"Photon Energy [{pos_drm.input_energy_edges.unit:latex}]") + gs_ax2.set_title(f"Pos. {key}: DRM") + plt.tight_layout() + if save_asset: + pathlib.Path(ASSETS_PATH).mkdir(parents=True, exist_ok=True) + plt.savefig(os.path.join(ASSETS_PATH,"response-chain.png"), dpi=200, bbox_inches="tight") + plt.show() + +def asset_response_hit_combination_plot(save_asset=False): + """Look at different combinations of the 1hit and 2hit responses.""" + p5_rmf1 = foxsi4_telescope5_rmf(region=0, _event_type="1hit") + p5_rmf2 = foxsi4_telescope5_rmf(region=0, _event_type="2hit") + + fig = plt.figure(figsize=(18, 4.9)) + gs = gridspec.GridSpec(1, 3) + + gs_ax0 = fig.add_subplot(gs[0, 0]) + h1f, h2f = 0.6, 0.4 + m = h1f*p5_rmf1.response + h2f*p5_rmf2.response + r = gs_ax0.imshow(m.value, + origin="lower", + norm=LogNorm(vmin=0.001), + extent=[np.min(p5_rmf1.output_energy_edges.value), + np.max(p5_rmf1.output_energy_edges.value), + np.min(p5_rmf1.input_energy_edges.value), + np.max(p5_rmf1.input_energy_edges.value)] + ) + cbar = plt.colorbar(r) + cbar.ax.set_ylabel(f"Response [{m.unit:latex}]") + gs_ax0.set_xlabel(f"Count Energy [{p5_rmf1.output_energy_edges.unit:latex}]") + gs_ax0.set_ylabel(f"Photon Energy [{p5_rmf1.input_energy_edges.unit:latex}]") + gs_ax0.set_title(f"Pos. 5: RMF-({h1f}*1hit+{h2f}*2hit)") + + gs_ax1 = fig.add_subplot(gs[0, 1]) + h1f, h2f = 0.1, 0.9 + m = h1f*p5_rmf1.response + h2f*p5_rmf2.response + r = gs_ax1.imshow(m.value, + origin="lower", + norm=LogNorm(vmin=0.001), + extent=[np.min(p5_rmf1.output_energy_edges.value), + np.max(p5_rmf1.output_energy_edges.value), + np.min(p5_rmf1.input_energy_edges.value), + np.max(p5_rmf1.input_energy_edges.value)] + ) + cbar = plt.colorbar(r) + cbar.ax.set_ylabel(f"Response [{m.unit:latex}]") + gs_ax1.set_xlabel(f"Count Energy [{p5_rmf1.output_energy_edges.unit:latex}]") + gs_ax1.set_ylabel(f"Photon Energy [{p5_rmf1.input_energy_edges.unit:latex}]") + gs_ax1.set_title(f"Pos. 5: RMF-({h1f}*1hit+{h2f}*2hit)") + + gs_ax2 = fig.add_subplot(gs[0, 2]) + h1f, h2f = 0.9, 0.1 + m = h1f*p5_rmf1.response + h2f*p5_rmf2.response + r = gs_ax2.imshow(m.value, + origin="lower", + norm=LogNorm(vmin=0.001), + extent=[np.min(p5_rmf1.output_energy_edges.value), + np.max(p5_rmf1.output_energy_edges.value), + np.min(p5_rmf1.input_energy_edges.value), + np.max(p5_rmf1.input_energy_edges.value)] + ) + cbar = plt.colorbar(r) + cbar.ax.set_ylabel(f"Response [{m.unit:latex}]") + gs_ax2.set_xlabel(f"Count Energy [{p5_rmf1.output_energy_edges.unit:latex}]") + gs_ax2.set_ylabel(f"Photon Energy [{p5_rmf1.input_energy_edges.unit:latex}]") + gs_ax2.set_title(f"Pos. 5: RMF-({h1f}*1hit+{h2f}*2hit)") + plt.tight_layout() + if save_asset: + pathlib.Path(ASSETS_PATH).mkdir(parents=True, exist_ok=True) + plt.savefig(os.path.join(ASSETS_PATH,"response-hit-combinations.png"), dpi=200, bbox_inches="tight") + plt.show() + +if __name__=="__main__": + from matplotlib.colors import LogNorm + import matplotlib.gridspec as gridspec + import matplotlib.pyplot as plt + + save_asset = False + + asset_response_chain_plot(save_asset=save_asset) + asset_response_hit_combination_plot(save_asset=save_asset) \ No newline at end of file diff --git a/response-tools-py/response_tools_py/telescope_parts.py b/response-tools-py/response_tools_py/telescope_parts.py new file mode 100644 index 0000000..fb17526 --- /dev/null +++ b/response-tools-py/response_tools_py/telescope_parts.py @@ -0,0 +1,604 @@ +"""Wrappers for position aliases for more specific functions.""" + +import logging +import sys + +import astropy.units as u + +from response_tools_py.attenuation import (att_thermal_blanket, + att_pixelated, + att_al_mylar, + att_uniform_al_cdte + ) +from response_tools_py.detector_response import (cdte_det_resp, + cmos_det_resp, + ) +from response_tools_py.effective_area import (eff_area_msfc_10shell, + eff_area_msfc_hi_res, + eff_area_nagoya_hxt, + ) + +# position 2 +@u.quantity_input(mid_energies=u.keV) +def foxsi4_position2_thermal_blanket(mid_energies): + """Position 2 thermal blanket transmissions. + + Parameters + ---------- + mid_energies : `astropy.units.quantity.Quantity` + The energies at which the thermal blanketing transmission is + required. If `numpy.nan< 60< 80< 100< Region 0 + 80< Region 1 + 100< Region 2 + + _side : `str` + Define the side on the detector the user requires the response + from. Must be in ["pt", "merged"]. + Default: "merged" + + _event_type : `str` + Define the type of event trigger being considered in the + response. Must be in ["1hit", "2hit", ("all", "mix")]. + Note: \"all\" and \"mix\" are the same but some from different + naming conventions on the merged and individual detector sides. + This will be fixed at some point in the future. + Default: "all" + + Returns + ------- + : `detector_response.DetectorResponseOutput` + An object containing all the redistribution matrix information. + See accessible information using `.contents` on the output. + """ + pos2_det = 4 + r = cdte_det_resp(cdte=pos2_det, + region=region, + pitch=pitch, + side=_side, + event_type=_event_type) + if r is None: + return + r.update_function_path(sys._getframe().f_code.co_name) + r.detector = f"CdTe{pos2_det}-Detector-Response" + return r + +# position 3 +@u.quantity_input(mid_energies=u.keV) +def foxsi4_position3_thermal_blanket(mid_energies): + """Position 3 thermal blanket transmissions. + + Parameters + ---------- + mid_energies : `astropy.units.quantity.Quantity` + The energies at which the thermal blanketing transmission is + required. If `numpy.nan< 60< 80< 100< Region 0 + 80< Region 1 + 100< Region 2 + + _side : `str` + Define the side on the detector the user requires the response + from. Must be in ["pt", "merged"]. + Default: "merged" + + _event_type : `str` + Define the type of event trigger being considered in the + response. Must be in ["1hit", "2hit", ("all", "mix")]. + Note: \"all\" and \"mix\" are the same but some from different + naming conventions on the merged and individual detector sides. + This will be fixed at some point in the future. + Default: "all" + + Returns + ------- + : `detector_response.DetectorResponseOutput` + An object containing all the redistribution matrix information. + See accessible information using `.contents` on the output. + """ + pos3_det = 2 + r = cdte_det_resp(cdte=pos3_det, + region=region, + pitch=pitch, + side=_side, + event_type=_event_type) + if r is None: + return + r.update_function_path(sys._getframe().f_code.co_name) + r.detector = f"CdTe{pos3_det}-Detector-Response" + return r + +# position 4 +@u.quantity_input(mid_energies=u.keV) +def foxsi4_position4_thermal_blanket(mid_energies): + """Position 4 thermal blanket transmissions. + + Parameters + ---------- + mid_energies : `astropy.units.quantity.Quantity` + The energies at which the thermal blanketing transmission is + required. If `numpy.nan< 60< 80< 100< Region 0 + 80< Region 1 + 100< Region 2 + + _side : `str` + Define the side on the detector the user requires the response + from. Must be in ["pt", "merged"]. + Default: "merged" + + _event_type : `str` + Define the type of event trigger being considered in the + response. Must be in ["1hit", "2hit", ("all", "mix")]. + Note: \"all\" and \"mix\" are the same but some from different + naming conventions on the merged and individual detector sides. + This will be fixed at some point in the future. + Default: "all" + + Returns + ------- + : `detector_response.DetectorResponseOutput` + An object containing all the redistribution matrix information. + See accessible information using `.contents` on the output. + """ + pos4_det = 3 + r = cdte_det_resp(cdte=pos4_det, + region=region, + pitch=pitch, + side=_side, + event_type=_event_type) + if r is None: + return + r.update_function_path(sys._getframe().f_code.co_name) + r.detector = f"CdTe{pos4_det}-Detector-Response" + return r + +# position 5 +@u.quantity_input(mid_energies=u.keV) +def foxsi4_position5_thermal_blanket(mid_energies): + """Position 5 thermal blanket transmissions. + + Parameters + ---------- + mid_energies : `astropy.units.quantity.Quantity` + The energies at which the thermal blanketing transmission is + required. If `numpy.nan< 60< 80< 100< Region 0 + 80< Region 1 + 100< Region 2 + + _side : `str` + Define the side on the detector the user requires the response + from. Must be in ["pt", "merged"]. + Default: "merged" + + _event_type : `str` + Define the type of event trigger being considered in the + response. Must be in ["1hit", "2hit", ("all", "mix")]. + Note: \"all\" and \"mix\" are the same but some from different + naming conventions on the merged and individual detector sides. + This will be fixed at some point in the future. + Default: "all" + + Returns + ------- + : `detector_response.DetectorResponseOutput` + An object containing all the redistribution matrix information. + See accessible information using `.contents` on the output. + """ + pos5_det = 1 + r = cdte_det_resp(cdte=pos5_det, + region=region, + pitch=pitch, + side=_side, + event_type=_event_type) + if r is None: + return + r.update_function_path(sys._getframe().f_code.co_name) + r.detector = f"CdTe{pos5_det}-Detector-Response" + return r + +if __name__=="__main__": + # mid_energies = [4.5, 5.5, 6.5, 7.5, 8.5, 9.5, 11. , 13. , 15. , 17. , 19. , 22.5, 27.5] << u.keV + # p2_tb = position2_thermal_blanket(mid_energies) + p2_dr_reg = position2_detector_response(region=0) + p2_dr_pitch = position2_detector_response(pitch=60<{new_function_name}" + + def __getitem__(self, index:str): + """Allow the field names to be passed like indices too. + + Example + ----------- + >>> ex = BaseOutput(filename="foo", function="bar") + >>> ex.filename + "foo" + >>> ex["filename"] + "foo" + """ + return (self.contents | {"fields":self.fields})[index] + diff --git a/response-tools-py/setup.py b/response-tools-py/setup.py index 143ed6c..eaabd7f 100644 --- a/response-tools-py/setup.py +++ b/response-tools-py/setup.py @@ -16,5 +16,5 @@ "pandas", ], packages=setuptools.find_packages(), - zip_safe=False -) + zip_safe=False, +) \ No newline at end of file