piratical 0.1.0__tar.gz
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.
- piratical-0.1.0/PKG-INFO +27 -0
- piratical-0.1.0/README.rst +4 -0
- piratical-0.1.0/piratical/__init__.py +1 -0
- piratical-0.1.0/piratical/piratical.py +853 -0
- piratical-0.1.0/piratical.egg-info/PKG-INFO +27 -0
- piratical-0.1.0/piratical.egg-info/SOURCES.txt +10 -0
- piratical-0.1.0/piratical.egg-info/dependency_links.txt +1 -0
- piratical-0.1.0/piratical.egg-info/requires.txt +7 -0
- piratical-0.1.0/piratical.egg-info/top_level.txt +1 -0
- piratical-0.1.0/setup.cfg +7 -0
- piratical-0.1.0/setup.py +35 -0
piratical-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: piratical
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Plunder spectra from the JWST NIRSpec archive
|
|
5
|
+
Home-page: https://bagpipes.readthedocs.io
|
|
6
|
+
Author: Adam Carnall, Ho-Hin Leung
|
|
7
|
+
Author-email: adamc@roe.ac.uk
|
|
8
|
+
Project-URL: GitHub, https://github.com/ACCarnall/piratical_pipeline
|
|
9
|
+
Requires-Dist: numpy<=2.2
|
|
10
|
+
Requires-Dist: pandas
|
|
11
|
+
Requires-Dist: astropy
|
|
12
|
+
Requires-Dist: matplotlib>=2.2.2
|
|
13
|
+
Requires-Dist: scipy
|
|
14
|
+
Requires-Dist: mastquery
|
|
15
|
+
Requires-Dist: jwst
|
|
16
|
+
Dynamic: author
|
|
17
|
+
Dynamic: author-email
|
|
18
|
+
Dynamic: description
|
|
19
|
+
Dynamic: home-page
|
|
20
|
+
Dynamic: project-url
|
|
21
|
+
Dynamic: requires-dist
|
|
22
|
+
Dynamic: summary
|
|
23
|
+
|
|
24
|
+
NIRSpec Piratical Pipeline
|
|
25
|
+
--------------------------
|
|
26
|
+
|
|
27
|
+
Plunder spectra from the JWST archive.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .piratical import piratical_pipeline
|
|
@@ -0,0 +1,853 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import glob
|
|
3
|
+
import os
|
|
4
|
+
import logging as log
|
|
5
|
+
import pandas as pd
|
|
6
|
+
import jwst
|
|
7
|
+
import datetime
|
|
8
|
+
import warnings
|
|
9
|
+
import matplotlib.pyplot as plt
|
|
10
|
+
import matplotlib as mpl
|
|
11
|
+
|
|
12
|
+
from scipy.stats import median_abs_deviation
|
|
13
|
+
from scipy.optimize import minimize
|
|
14
|
+
|
|
15
|
+
from astropy.io import fits
|
|
16
|
+
from astropy.table import Table
|
|
17
|
+
from mastquery import jwst as jwst_mq
|
|
18
|
+
from astropy.time import Time
|
|
19
|
+
|
|
20
|
+
from jwst import datamodels
|
|
21
|
+
from jwst.pipeline import Detector1Pipeline
|
|
22
|
+
from jwst.pipeline import Spec2Pipeline
|
|
23
|
+
from jwst.pipeline import Spec3Pipeline
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class piratical_pipeline:
|
|
27
|
+
"""
|
|
28
|
+
Code to return reduced version of all NIRSpec MSA data for an input
|
|
29
|
+
source catalogue.
|
|
30
|
+
|
|
31
|
+
Parameters
|
|
32
|
+
----------
|
|
33
|
+
catalogue : str or astropy Table or pandas DataFrame
|
|
34
|
+
Catalogue of sources to search for NIRSpec MSA data on
|
|
35
|
+
|
|
36
|
+
force_mast_update : bool, optional
|
|
37
|
+
Forces the code to re-query MAST for exposures and re-download
|
|
38
|
+
MSA files rather than using previously saved versions.
|
|
39
|
+
|
|
40
|
+
skip_l1_pipeline : bool, optional
|
|
41
|
+
Whether to skip the Level 1 pipeline processing and just download
|
|
42
|
+
the rate files from MAST. Much faster and uses less disk space
|
|
43
|
+
but the MAST rate files don't have the flicker noise correction
|
|
44
|
+
applied. I'd recommend skipping L1 for a first look at the data,
|
|
45
|
+
then re-running with the L1 pipeline to get final science spectra
|
|
46
|
+
|
|
47
|
+
gratings : list or "all", optional
|
|
48
|
+
Can be used to restrict which optical elements to pull data for.
|
|
49
|
+
Defaults to all, but can be set to a list of gratings, e.g.,
|
|
50
|
+
["PRISM", "G140M"] - note the required use of capital letters.
|
|
51
|
+
|
|
52
|
+
match_radius : float, optional
|
|
53
|
+
Radius within which to match input catalogue sources to NIRSpec
|
|
54
|
+
sources in arcseconds. Default value is 0.2 arcsec.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
def __init__(self, catalogue_name, force_mast_update=False,
|
|
58
|
+
skip_l1_pipeline=False, gratings="all", match_radius=0.2):
|
|
59
|
+
|
|
60
|
+
self.skip_l1_pipeline = skip_l1_pipeline
|
|
61
|
+
self.force_mast_update = force_mast_update
|
|
62
|
+
self.catalogue_name = catalogue_name
|
|
63
|
+
self.gratings = gratings
|
|
64
|
+
self.match_radius = match_radius
|
|
65
|
+
|
|
66
|
+
self.catalogue = Table.read(self.catalogue_name)
|
|
67
|
+
|
|
68
|
+
# Change id column to ID if needed for later matching
|
|
69
|
+
if ("id" in self.catalogue.columns
|
|
70
|
+
and not "ID" in self.catalogue.columns):
|
|
71
|
+
self.catalogue.rename_column("id", "ID")
|
|
72
|
+
|
|
73
|
+
if force_mast_update:
|
|
74
|
+
os.system("rm all_exposures.fits nirspec_msa_all_objects.fits")
|
|
75
|
+
|
|
76
|
+
folders = ["msa_files", "uncal_files", "rate_files", "cal_files",
|
|
77
|
+
"s2d_files", "x1d_files", "plots"]
|
|
78
|
+
|
|
79
|
+
for folder in folders:
|
|
80
|
+
if not os.path.exists(folder):
|
|
81
|
+
os.mkdir(folder)
|
|
82
|
+
|
|
83
|
+
self.get_all_exposures()
|
|
84
|
+
self.pull_msa_files()
|
|
85
|
+
|
|
86
|
+
out_cat_name = (self.catalogue_name.replace(".fits", "")
|
|
87
|
+
+ "_nirspec_observations.fits")
|
|
88
|
+
|
|
89
|
+
# Match with exposure table to find exposures object is in
|
|
90
|
+
os.system("stilts tmatch2 in1=" + self.catalogue_name
|
|
91
|
+
+ " in2=NIRSpec_all_observations_all_objects.fits out="
|
|
92
|
+
+ out_cat_name + " matcher=sky values1='ra dec'"
|
|
93
|
+
+ " values2='ra dec' params=" + str(self.match_radius)
|
|
94
|
+
+ " join=1and2 find=all")
|
|
95
|
+
|
|
96
|
+
matched = Table.read(out_cat_name).to_pandas()
|
|
97
|
+
matched["msa_file_met_id"] = matched["msa_file_met_id"].str.decode("utf-8")
|
|
98
|
+
matched["grating"] = matched["grating"].str.decode("utf-8")
|
|
99
|
+
|
|
100
|
+
if self.gratings != "all":
|
|
101
|
+
grating_mask = np.isin(matched["grating"], self.gratings)
|
|
102
|
+
matched = matched.groupby(grating_mask).get_group(True)
|
|
103
|
+
|
|
104
|
+
self.matched = matched
|
|
105
|
+
|
|
106
|
+
self.all_exp["msa_file_met_id"] = self.all_exp["msa_file_met_id"].str.decode("utf-8")
|
|
107
|
+
self.all_exp["fileSetName"] = self.all_exp["fileSetName"].str.decode("utf-8")
|
|
108
|
+
|
|
109
|
+
print("\nTable of NIRSpec observations for input catalogue has been"
|
|
110
|
+
+ " saved to", out_cat_name, "To download and reduce the"
|
|
111
|
+
+ " corresponding data,execute the run_pipeline() method.\n")
|
|
112
|
+
|
|
113
|
+
def get_all_exposures(self):
|
|
114
|
+
|
|
115
|
+
# Don't remake the nirspec exposures catalogue if it exists
|
|
116
|
+
if os.path.exists("all_exposures.fits"):
|
|
117
|
+
print("\n")
|
|
118
|
+
print("all_exposures.fits exists, skipping MAST query.")
|
|
119
|
+
print("To pull new MAST obs delete this file and run again.\n")
|
|
120
|
+
|
|
121
|
+
self.all_exp = Table.read("all_exposures.fits").to_pandas()
|
|
122
|
+
return
|
|
123
|
+
|
|
124
|
+
print("\n")
|
|
125
|
+
print("Querying MAST for all NIRSpec exposures...\n")
|
|
126
|
+
|
|
127
|
+
# Controls what's printed to terminal whilst the query is running
|
|
128
|
+
fmt_str = "%(module)s.%(funcName)s : %(levelname)s : %(message)s"
|
|
129
|
+
log.basicConfig(level=log.INFO,
|
|
130
|
+
handlers=[log.StreamHandler(),
|
|
131
|
+
log.FileHandler('/tmp/mastquery.log')],
|
|
132
|
+
format=fmt_str)
|
|
133
|
+
|
|
134
|
+
# Set up query params
|
|
135
|
+
filters = []
|
|
136
|
+
filters += jwst_mq.make_query_filter('productLevel', values=['2b'])
|
|
137
|
+
filters += jwst_mq.make_query_filter('exp_type', text='NRS_MSASPEC')
|
|
138
|
+
|
|
139
|
+
# Extra example filters
|
|
140
|
+
"""
|
|
141
|
+
# Just guided exposures
|
|
142
|
+
filters += jwst_mq.make_query_filter('pcs_mode', values=['FINEGUIDE'])
|
|
143
|
+
|
|
144
|
+
filters += jwst_mq.make_program_filter([3543])
|
|
145
|
+
filters += jwst_mq.make_query_filter('filename',
|
|
146
|
+
text='%_[cr]a%[le].fits')
|
|
147
|
+
filters += jwst_mq.make_query_filter('expstart',
|
|
148
|
+
range=[59744.5, 59764.5])
|
|
149
|
+
"""
|
|
150
|
+
|
|
151
|
+
# Run query with mastquery
|
|
152
|
+
res = jwst_mq.query_all_jwst(filters=filters, columns='*').to_pandas()
|
|
153
|
+
|
|
154
|
+
Table.from_pandas(res).write("all_uncal_files.fits", overwrite=True)
|
|
155
|
+
|
|
156
|
+
# Drop files that have not yet been publicly released
|
|
157
|
+
now = datetime.datetime.now(datetime.timezone.utc)
|
|
158
|
+
mjd = Time(now, scale='utc').mjd
|
|
159
|
+
res = res.groupby(res["t_obs_release"] < mjd).get_group(True)
|
|
160
|
+
|
|
161
|
+
# Cut to a sensible number of columns, the whole table is huge
|
|
162
|
+
res = res[["fileSetName", "msametfl", "msametid", "grating", "title",
|
|
163
|
+
"proposal_pi", "filter-pupil", "asntable"]]
|
|
164
|
+
|
|
165
|
+
# Swap out any msametfl not ending with 01_msa.fits (identical)
|
|
166
|
+
res["orig_msametfl"] = res["msametfl"]
|
|
167
|
+
|
|
168
|
+
for i in range(len(res)):
|
|
169
|
+
if not res["msametfl"].iloc[i].endswith("01_msa.fits"):
|
|
170
|
+
res["msametfl"].iloc[i] = (res["msametfl"].iloc[i][:-11]
|
|
171
|
+
+ "01_msa.fits")
|
|
172
|
+
|
|
173
|
+
# Create joined column of msametfl + msa meta id for later merge
|
|
174
|
+
res["msa_file_met_id"] = (res["msametfl"] + "_"
|
|
175
|
+
+ res["msametid"].astype(str))
|
|
176
|
+
|
|
177
|
+
# Save to file and load again to make formatting identical
|
|
178
|
+
Table.from_pandas(res).write("all_exposures.fits",
|
|
179
|
+
overwrite=True)
|
|
180
|
+
|
|
181
|
+
res = Table.read("all_exposures.fits").to_pandas()
|
|
182
|
+
|
|
183
|
+
self.all_exp = res
|
|
184
|
+
|
|
185
|
+
def pull_msa_files(self):
|
|
186
|
+
|
|
187
|
+
if os.path.exists("nirspec_msa_all_objects.fits"):
|
|
188
|
+
print("\n")
|
|
189
|
+
print("nirspec_msa_all_objects.fits exists, skipping MSA download.")
|
|
190
|
+
print("To re-process MSA files delete this file and run again.\n")
|
|
191
|
+
return
|
|
192
|
+
|
|
193
|
+
# Get list of unique msa metafiles
|
|
194
|
+
msa_files = self.all_exp["msametfl"]
|
|
195
|
+
msa_files = msa_files.drop_duplicates().str.decode('utf-8').values
|
|
196
|
+
|
|
197
|
+
print("\n")
|
|
198
|
+
print("Downloading MSA files from MAST...\n")
|
|
199
|
+
|
|
200
|
+
# Pull all msa metafiles from MAST
|
|
201
|
+
for i in range(len(msa_files)):
|
|
202
|
+
|
|
203
|
+
if os.path.exists("msa_files/" + msa_files[i]):
|
|
204
|
+
continue
|
|
205
|
+
|
|
206
|
+
print("Downloading metafile", i + 1, "of", len(msa_files))
|
|
207
|
+
|
|
208
|
+
os.system("curl -H --globoff --location-trusted -f --progress-bar "
|
|
209
|
+
+ "--output ./msa_files/" + msa_files[i]
|
|
210
|
+
+ " https://mast.stsci.edu/api/v0.1/Download/file"
|
|
211
|
+
+ "?uri=mast:JWST/product/" + msa_files[i])
|
|
212
|
+
|
|
213
|
+
msa_files = glob.glob("msa_files/jw*")
|
|
214
|
+
|
|
215
|
+
print("\n")
|
|
216
|
+
print("Processing MSA files...\n")
|
|
217
|
+
|
|
218
|
+
# Merge all msa files to one catalogue
|
|
219
|
+
for i in range(len(msa_files)):
|
|
220
|
+
print("Merging msa file", str(i + 1), "of", str(len(msa_files)))
|
|
221
|
+
if not os.path.exists(msa_files[i].split("/")[0]
|
|
222
|
+
+ "/objects_with_meta_ids_"
|
|
223
|
+
+ msa_files[i].split("/")[-1]):
|
|
224
|
+
|
|
225
|
+
# This next bit is maybe unnecessarily complicated?
|
|
226
|
+
# could maybe just pull the info from the shutter table
|
|
227
|
+
# ignoring the object table? Object coords are only in
|
|
228
|
+
# Object table though? Shutter coords in shutter table?
|
|
229
|
+
|
|
230
|
+
# Read in the objects table
|
|
231
|
+
file = Table.read(msa_files[i], hdu=3)
|
|
232
|
+
file = file.to_pandas()
|
|
233
|
+
|
|
234
|
+
# Add column containing msa file name object came from
|
|
235
|
+
file["msa_file"] = msa_files[i].split("/")[-1]
|
|
236
|
+
Table.from_pandas(file).write(msa_files[i].split("/")[0]
|
|
237
|
+
+ "/objects_"
|
|
238
|
+
+ msa_files[i].split("/")[-1],
|
|
239
|
+
overwrite=True)
|
|
240
|
+
|
|
241
|
+
# Read in the shutter table
|
|
242
|
+
shut_file = Table.read(msa_files[i], hdu=2).to_pandas()
|
|
243
|
+
|
|
244
|
+
# Create unique id for each source in each msa config
|
|
245
|
+
source_id_meta_id = (shut_file["source_id"].astype(str) + "_"
|
|
246
|
+
+ shut_file["msa_metadata_id"].astype(str))
|
|
247
|
+
|
|
248
|
+
shut_file["source_id_meta_id"] = source_id_meta_id
|
|
249
|
+
|
|
250
|
+
# Create table containing each object in each msa config
|
|
251
|
+
col_list = ["source_id_meta_id"]
|
|
252
|
+
unique_shut_file = shut_file.drop_duplicates(subset=col_list)
|
|
253
|
+
|
|
254
|
+
unique_shut_file = unique_shut_file[["source_id",
|
|
255
|
+
"msa_metadata_id"]]
|
|
256
|
+
|
|
257
|
+
# Save to file
|
|
258
|
+
tosave = Table.from_pandas(unique_shut_file)
|
|
259
|
+
tosave.write(msa_files[i].split("/")[0] + "/shutters_"
|
|
260
|
+
+ msa_files[i].split("/")[-1], overwrite=True)
|
|
261
|
+
|
|
262
|
+
# Match object and shutter tables to find which objects
|
|
263
|
+
# are observed in which msa config
|
|
264
|
+
os.system("stilts tmatch2 in1=" + msa_files[i].split("/")[0]
|
|
265
|
+
+ "/objects_" + msa_files[i].split("/")[-1]
|
|
266
|
+
+ " in2=" + msa_files[i].split("/")[0] + "/shutters_"
|
|
267
|
+
+ msa_files[i].split("/")[-1] + " out="
|
|
268
|
+
+ msa_files[i].split("/")[0]
|
|
269
|
+
+ "/objects_with_meta_ids_"
|
|
270
|
+
+ msa_files[i].split("/")[-1]
|
|
271
|
+
+ " matcher=exact values1=source_id"
|
|
272
|
+
+ " values2=source_id join=1and2 find=all")
|
|
273
|
+
|
|
274
|
+
if i == 0:
|
|
275
|
+
msametfl_table = Table.read(msa_files[i].split("/")[0]
|
|
276
|
+
+ "/objects_with_meta_ids_"
|
|
277
|
+
+ msa_files[i].split("/")[-1])
|
|
278
|
+
|
|
279
|
+
msametfl_table = msametfl_table.to_pandas()
|
|
280
|
+
|
|
281
|
+
else:
|
|
282
|
+
single_table = Table.read(msa_files[i].split("/")[0]
|
|
283
|
+
+ "/objects_with_meta_ids_"
|
|
284
|
+
+ msa_files[i].split("/")[-1])
|
|
285
|
+
|
|
286
|
+
single_table = single_table.to_pandas()
|
|
287
|
+
msametfl_table = pd.concat([msametfl_table, single_table])
|
|
288
|
+
|
|
289
|
+
# rename and cut down columns
|
|
290
|
+
msametfl_table["source_id"] = msametfl_table["source_id_1"]
|
|
291
|
+
msametfl_table = msametfl_table[["source_id", "program", "ra", "dec",
|
|
292
|
+
"msa_metadata_id", "msa_file"]]
|
|
293
|
+
|
|
294
|
+
# Unique ID for each source + msa config to merge with exp table
|
|
295
|
+
msa_file_met_id = (msametfl_table["msa_file"].str.decode('utf-8') + "_"
|
|
296
|
+
+ msametfl_table["msa_metadata_id"].astype(str))
|
|
297
|
+
|
|
298
|
+
msametfl_table["msa_file_met_id"] = msa_file_met_id
|
|
299
|
+
|
|
300
|
+
Table.from_pandas(msametfl_table).write("nirspec_msa_all_objects.fits",
|
|
301
|
+
overwrite=True)
|
|
302
|
+
|
|
303
|
+
# Merge table of objects+meta ids with table of exposures
|
|
304
|
+
os.system("stilts tmatch2 in1=nirspec_msa_all_objects.fits"
|
|
305
|
+
+ " in2=all_exposures.fits"
|
|
306
|
+
+ " out=all_objects_all_exposures.fits matcher=exact"
|
|
307
|
+
+ " values1=msa_file_met_id values2=msa_file_met_id"
|
|
308
|
+
+ " join=1and2 find=all")
|
|
309
|
+
|
|
310
|
+
# Cut merged table to unique obs+grating combos for each object
|
|
311
|
+
final = Table.read("all_objects_all_exposures.fits").to_pandas()
|
|
312
|
+
final["unique"] = (final["source_id"].astype(str) + "_"
|
|
313
|
+
+ final["msametfl"].str.decode('utf-8') + "_"
|
|
314
|
+
+ final["grating"].str.decode('utf-8'))
|
|
315
|
+
|
|
316
|
+
final.drop_duplicates(subset="unique", inplace=True)
|
|
317
|
+
|
|
318
|
+
# Cut back to key columns and save to file
|
|
319
|
+
final["msa_file_met_id"] = final["msa_file_met_id_1"]
|
|
320
|
+
final = final[["source_id", "program", "ra", "dec", "msa_metadata_id",
|
|
321
|
+
"msa_file", "fileSetName", "grating", "title",
|
|
322
|
+
"proposal_pi", "filter-pupil", "msa_file_met_id",
|
|
323
|
+
"orig_msametfl", "asntable"]]
|
|
324
|
+
|
|
325
|
+
tosave = Table.from_pandas(final)
|
|
326
|
+
tosave.write("NIRSpec_all_observations_all_objects.fits",
|
|
327
|
+
overwrite=True)
|
|
328
|
+
|
|
329
|
+
self.all_obs = final
|
|
330
|
+
|
|
331
|
+
def run_pipeline(self):
|
|
332
|
+
|
|
333
|
+
self.pull_uncal_files()
|
|
334
|
+
self.run_l1_pipeline()
|
|
335
|
+
self.run_l2_pipeline()
|
|
336
|
+
self.run_l3_pipeline()
|
|
337
|
+
self.post_pipeline_analysis()
|
|
338
|
+
|
|
339
|
+
def pull_uncal_files(self):
|
|
340
|
+
|
|
341
|
+
mask = np.isin(self.all_exp["msa_file_met_id"].str.strip().values,
|
|
342
|
+
self.matched["msa_file_met_id"].str.strip().values)
|
|
343
|
+
|
|
344
|
+
fileset = self.all_exp[mask]["fileSetName"]
|
|
345
|
+
|
|
346
|
+
self.uncal_files = []
|
|
347
|
+
self.req_msa_files = []
|
|
348
|
+
self.req_l2_asn_files = []
|
|
349
|
+
for i in range(np.sum(mask)):
|
|
350
|
+
self.uncal_files.append(fileset.values[i].strip()
|
|
351
|
+
+ "_nrs1_uncal.fits")
|
|
352
|
+
self.uncal_files.append(fileset.values[i].strip()
|
|
353
|
+
+ "_nrs2_uncal.fits")
|
|
354
|
+
|
|
355
|
+
self.req_msa_files = self.all_exp[mask]["orig_msametfl"]
|
|
356
|
+
self.req_msa_files = self.req_msa_files.str.decode("utf-8").values
|
|
357
|
+
|
|
358
|
+
self.req_l2_asn_files = self.all_exp[mask]["asntable"]
|
|
359
|
+
self.req_l2_asn_files = self.req_l2_asn_files.str.decode("utf-8").values
|
|
360
|
+
|
|
361
|
+
# Download uncal files for each exposure
|
|
362
|
+
for i in range(len(self.uncal_files)):
|
|
363
|
+
|
|
364
|
+
if not self.skip_l1_pipeline:
|
|
365
|
+
if os.path.exists("uncal_files/" + self.uncal_files[i]):
|
|
366
|
+
continue
|
|
367
|
+
|
|
368
|
+
print("Downloading uncal file", i + 1, "of",
|
|
369
|
+
len(self.uncal_files))
|
|
370
|
+
|
|
371
|
+
os.system("curl -H --globoff --location-trusted -f "
|
|
372
|
+
+ "--progress-bar --output ./uncal_files/"
|
|
373
|
+
+ self.uncal_files[i]
|
|
374
|
+
+ " https://mast.stsci.edu/api/v0.1/Download/file"
|
|
375
|
+
+ "?uri=mast:JWST/product/" + self.uncal_files[i])
|
|
376
|
+
|
|
377
|
+
else:
|
|
378
|
+
if os.path.exists("rate_files/" + self.uncal_files[i][:-10]
|
|
379
|
+
+ "rate.fits"):
|
|
380
|
+
continue
|
|
381
|
+
|
|
382
|
+
print("Downloading rate file", i + 1,"of",
|
|
383
|
+
len(self.uncal_files))
|
|
384
|
+
|
|
385
|
+
print(self.uncal_files[i][:-10] + "rate.fits")
|
|
386
|
+
os.system("curl -H --globoff --location-trusted -f "
|
|
387
|
+
+ "--progress-bar --output ./rate_files/"
|
|
388
|
+
+ self.uncal_files[i][:-10] + "rate.fits"
|
|
389
|
+
+ " https://mast.stsci.edu/api/v0.1/Download/file"
|
|
390
|
+
+ "?uri=mast:JWST/product/"
|
|
391
|
+
+ self.uncal_files[i][:-10] + "rate.fits")
|
|
392
|
+
|
|
393
|
+
def run_l1_pipeline(self):
|
|
394
|
+
|
|
395
|
+
msa_files = self.req_msa_files
|
|
396
|
+
|
|
397
|
+
# msa files not ending with 01_msa.fits are identical to those that do
|
|
398
|
+
# so just copy the 01_msa.fits files and rename them for the pipeline
|
|
399
|
+
for i in range(len(msa_files)):
|
|
400
|
+
os.system("cp msa_files/" + msa_files[i][:-11] + "01_msa.fits"
|
|
401
|
+
+ " uncal_files/" + msa_files[i])
|
|
402
|
+
|
|
403
|
+
if self.skip_l1_pipeline:
|
|
404
|
+
return
|
|
405
|
+
|
|
406
|
+
print("\n")
|
|
407
|
+
print("Running level 1 pipeline...\n")
|
|
408
|
+
|
|
409
|
+
for file in self.uncal_files:
|
|
410
|
+
|
|
411
|
+
if os.path.exists("rate_files/" + file[:-10] + "rate.fits"):
|
|
412
|
+
continue
|
|
413
|
+
|
|
414
|
+
print(file)
|
|
415
|
+
|
|
416
|
+
pipe = Detector1Pipeline()
|
|
417
|
+
|
|
418
|
+
pipe.save_results = True
|
|
419
|
+
pipe.output_dir = "./rate_files"
|
|
420
|
+
pipe.output_file = file[:-11]
|
|
421
|
+
|
|
422
|
+
pipe.jump.expand_large_events = True
|
|
423
|
+
pipe.jump.max_cores = 16
|
|
424
|
+
|
|
425
|
+
pipe.clean_flicker_noise.skip = False
|
|
426
|
+
pipe.clean_flicker_noise.fit_method = 'median'
|
|
427
|
+
pipe.clean_flicker_noise.mask_science_regions = True
|
|
428
|
+
pipe.clean_flicker_noise.background_method = None
|
|
429
|
+
pipe.clean_flicker_noise.n_sigma = 2
|
|
430
|
+
|
|
431
|
+
pipe.ramp_fit.maximum_cores = "all"
|
|
432
|
+
|
|
433
|
+
pipe.run("uncal_files/" + file)
|
|
434
|
+
|
|
435
|
+
os.system("rm uncal_files/*rateints.fits")
|
|
436
|
+
|
|
437
|
+
def run_l2_pipeline(self):
|
|
438
|
+
|
|
439
|
+
# Download level 2 association files
|
|
440
|
+
l2_asn_files = self.req_l2_asn_files
|
|
441
|
+
|
|
442
|
+
for l2_asn_file in l2_asn_files:
|
|
443
|
+
|
|
444
|
+
if os.path.exists("rate_files/" + l2_asn_file):
|
|
445
|
+
continue
|
|
446
|
+
|
|
447
|
+
os.system("curl -H --globoff --location-trusted -f --progress-bar "
|
|
448
|
+
+ "--output ./rate_files/" + l2_asn_file
|
|
449
|
+
+ " https://mast.stsci.edu/api/v0.1/Download/file"
|
|
450
|
+
+ "?uri=mast:JWST/product/" + l2_asn_file)
|
|
451
|
+
|
|
452
|
+
msa_files = self.req_msa_files
|
|
453
|
+
|
|
454
|
+
for i in range(len(msa_files)):
|
|
455
|
+
# Chop the msa files down to just the slitlets we want
|
|
456
|
+
msam = fits.open("uncal_files/" + msa_files[i])
|
|
457
|
+
shut = Table.read("uncal_files/" + msa_files[i], hdu=2)
|
|
458
|
+
shut_pd = shut.to_pandas()
|
|
459
|
+
|
|
460
|
+
unique = (shut_pd["source_id"].astype(str).values
|
|
461
|
+
+ "_" + shut_pd["msa_metadata_id"].astype(str).values
|
|
462
|
+
+ "_" + msa_files[i])
|
|
463
|
+
newcol = fits.Column(name="id_msam_msafile", format="50A",
|
|
464
|
+
array=(unique))
|
|
465
|
+
|
|
466
|
+
msam[2] = fits.BinTableHDU.from_columns(newcol + msam[2].columns,
|
|
467
|
+
name="SHUTTER_INFO")
|
|
468
|
+
|
|
469
|
+
unique = (self.matched["source_id"].astype(str)+ "_"
|
|
470
|
+
+ self.matched["msa_metadata_id"].astype(str) + "_"
|
|
471
|
+
+ self.matched["orig_msametfl"].str.decode("utf-8"))
|
|
472
|
+
self.matched["id_msam_msafile"] = unique
|
|
473
|
+
|
|
474
|
+
mask = np.isin(msam[2].data["id_msam_msafile"],
|
|
475
|
+
self.matched["id_msam_msafile"].values)
|
|
476
|
+
|
|
477
|
+
slit_ids = np.unique(msam[2].data["slitlet_id"][mask])
|
|
478
|
+
mask2 = np.isin(msam[2].data["slitlet_id"], slit_ids)
|
|
479
|
+
msam[2].data = msam[2].data[mask2]
|
|
480
|
+
|
|
481
|
+
msam.writeto("rate_files/" + msa_files[i], overwrite=True)
|
|
482
|
+
|
|
483
|
+
print("\n")
|
|
484
|
+
print("Running level 2 pipeline...\n")
|
|
485
|
+
|
|
486
|
+
# Load up association files
|
|
487
|
+
asc2_list = glob.glob("rate_files/*_spec2_*_asn.json")
|
|
488
|
+
|
|
489
|
+
# Run level 2 pipeline
|
|
490
|
+
for asc in asc2_list:
|
|
491
|
+
print(asc)
|
|
492
|
+
spec2 = Spec2Pipeline()
|
|
493
|
+
spec2.save_results = True
|
|
494
|
+
spec2.output_dir = "cal_files"
|
|
495
|
+
|
|
496
|
+
try:
|
|
497
|
+
result = spec2.run(asc)
|
|
498
|
+
except jwst.assign_wcs.util.NoDataOnDetectorError:
|
|
499
|
+
pass
|
|
500
|
+
|
|
501
|
+
def run_l3_pipeline(self):
|
|
502
|
+
|
|
503
|
+
# Load up list of level 2b products
|
|
504
|
+
cal_files = glob.glob("cal_files/*_cal.fits")
|
|
505
|
+
|
|
506
|
+
# Find unique datasets to run level 3 pipeline on
|
|
507
|
+
sep = pd.Series(cal_files).str.split("_")
|
|
508
|
+
|
|
509
|
+
cal_files_base = sep.str[0] + "_" + sep.str[1] + "_" + sep.str[2]
|
|
510
|
+
unique = cal_files_base.drop_duplicates().values
|
|
511
|
+
|
|
512
|
+
# L2 pipeline refuses to reduce if no slits open on detector
|
|
513
|
+
# L3 pipeline refuses to run unless nrs1 and nrs2 files exist
|
|
514
|
+
# So make dummy files, add to asn file, delete after L3 pipe
|
|
515
|
+
cal_files_no_dummy = np.copy(np.array(cal_files))
|
|
516
|
+
|
|
517
|
+
for i in range(len(unique)):
|
|
518
|
+
mask = np.isin(cal_files_base, unique[i])
|
|
519
|
+
|
|
520
|
+
for file in cal_files_no_dummy[mask]:
|
|
521
|
+
if file.endswith("nrs1_cal.fits"):
|
|
522
|
+
mirror_file = file.replace("nrs1_cal.fits", "nrs2_cal.fits")
|
|
523
|
+
|
|
524
|
+
elif file.endswith("nrs2_cal.fits"):
|
|
525
|
+
mirror_file = file.replace("nrs2_cal.fits", "nrs1_cal.fits")
|
|
526
|
+
|
|
527
|
+
if not np.max(np.isin(cal_files_no_dummy[mask], mirror_file)):
|
|
528
|
+
cal_files.append(mirror_file[:-5] + "_dummy.fits")
|
|
529
|
+
os.system("cp " + file + " " + mirror_file[:-5]
|
|
530
|
+
+ "_dummy.fits")
|
|
531
|
+
|
|
532
|
+
# Find unique datasets to run level 3 pipeline on (re-do with dummies)
|
|
533
|
+
sep = pd.Series(cal_files).str.split("_")
|
|
534
|
+
|
|
535
|
+
cal_files_base = sep.str[0] + "_" + sep.str[1] + "_" + sep.str[2]
|
|
536
|
+
unique = cal_files_base.drop_duplicates().values
|
|
537
|
+
|
|
538
|
+
# Make level 3 association files
|
|
539
|
+
for i in range(len(unique)):
|
|
540
|
+
mask = np.isin(cal_files_base, unique[i])
|
|
541
|
+
|
|
542
|
+
if os.path.exists(unique[i] + "_spec3_asn.json"):
|
|
543
|
+
os.system("rm " + unique[i] + "_spec3_asn.json")
|
|
544
|
+
|
|
545
|
+
f = open(unique[i] + "_spec3_asn.json", "w")
|
|
546
|
+
f.write('{"asn_type": "spec3",\n')
|
|
547
|
+
f.write('"asn_pool": "flubflubflub",\n')
|
|
548
|
+
f.write('"products": [\n')
|
|
549
|
+
f.write('{"name": "' + unique[i] + '_{source_id}",\n')
|
|
550
|
+
f.write('"members": [\n')
|
|
551
|
+
for j in range(np.sum(mask)-1):
|
|
552
|
+
f.write('{"expname": "'
|
|
553
|
+
+ np.array(cal_files)[mask][j].split("/")[1]
|
|
554
|
+
+ '", "exptype": "science"},\n')
|
|
555
|
+
f.write('{"expname": "'
|
|
556
|
+
+ np.array(cal_files)[mask][-1].split("/")[1]
|
|
557
|
+
+ '", "exptype": "science"}]}]}\n')
|
|
558
|
+
f.close()
|
|
559
|
+
|
|
560
|
+
print("\n")
|
|
561
|
+
print("Running level 3 pipeline...\n")
|
|
562
|
+
|
|
563
|
+
# Load up L3 association files
|
|
564
|
+
asc3_list = glob.glob("cal_files/*_spec3_asn.json")
|
|
565
|
+
|
|
566
|
+
# Run level 3 pipeline
|
|
567
|
+
for asc in asc3_list:
|
|
568
|
+
print(asc)
|
|
569
|
+
spec3 = Spec3Pipeline()
|
|
570
|
+
spec3.save_results = True
|
|
571
|
+
spec3.output_dir = "s2d_files"
|
|
572
|
+
result = spec3.run(asc)
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
for i in range(len(self.matched)):
|
|
576
|
+
|
|
577
|
+
split = self.matched["fileSetName"].str.decode("utf-8").str.split("_")
|
|
578
|
+
filebase = (split.str[0] + "_" + split.str[1]).values
|
|
579
|
+
|
|
580
|
+
prod = glob.glob("s2d_files/" + filebase[i] + "*"
|
|
581
|
+
+ self.matched["source_id"].iloc[i].astype(str)
|
|
582
|
+
+ "_s2d.fits")
|
|
583
|
+
|
|
584
|
+
print(prod[0],
|
|
585
|
+
self.matched["ID"].iloc[i].astype(str),
|
|
586
|
+
filebase[i],
|
|
587
|
+
self.matched["grating"].iloc[i])
|
|
588
|
+
|
|
589
|
+
os.system("mv " + prod[0] + " s2d_files/"
|
|
590
|
+
+ self.matched["ID"].iloc[i].astype(str) + "_"
|
|
591
|
+
+ filebase[i] + "_"
|
|
592
|
+
+ self.matched["grating"].iloc[i]
|
|
593
|
+
+ "_s2d.fits")
|
|
594
|
+
|
|
595
|
+
os.system("rm s2d_files/*crf.fits s2d_files/*x1d.fits"
|
|
596
|
+
+ " s2d_files/*cal.fits")
|
|
597
|
+
|
|
598
|
+
os.system("rm cal_files/*_dummy.fits")
|
|
599
|
+
|
|
600
|
+
def _get_wavs(self, reduced):
|
|
601
|
+
reducedsci = reduced.data
|
|
602
|
+
wcsobj = reduced.meta.wcs
|
|
603
|
+
y, x = np.mgrid[:reducedsci.shape[0], : reducedsci.shape[1]]
|
|
604
|
+
det2sky = wcsobj.get_transform('detector', 'world')
|
|
605
|
+
reducedra, reduceddec, reducedwave = det2sky(x, y)
|
|
606
|
+
return reducedwave[0, :]
|
|
607
|
+
|
|
608
|
+
def _model(self, param, x_vals):
|
|
609
|
+
return param[0]*np.exp(-0.5*(x_vals-param[1])**2/param[2]**2)
|
|
610
|
+
|
|
611
|
+
def _chisq(self, x, args):
|
|
612
|
+
x_vals = args[0]
|
|
613
|
+
y_vals = args[1]
|
|
614
|
+
y_start = args[2]
|
|
615
|
+
|
|
616
|
+
mod = self._model(x, x_vals)
|
|
617
|
+
|
|
618
|
+
# Controls how far the centroid can stray from input position
|
|
619
|
+
if np.abs(y_start - x[1]) > self.y_tolerance:
|
|
620
|
+
return 9.9*10**99
|
|
621
|
+
|
|
622
|
+
return np.nansum((mod - y_vals)**2)
|
|
623
|
+
|
|
624
|
+
def rolling_extraction(self, wavs, spec2d, spec2d_err, full_result,
|
|
625
|
+
half_width_pix, weights_collapsed):
|
|
626
|
+
|
|
627
|
+
# the range in spec2d we want to keep, all other weights set to 0
|
|
628
|
+
clip_range_pix = [int(np.round(full_result['x'][1]
|
|
629
|
+
- 3*full_result['x'][2])),
|
|
630
|
+
int(np.round(full_result['x'][1]
|
|
631
|
+
+ 3*full_result['x'][2]))]
|
|
632
|
+
|
|
633
|
+
weights = np.zeros(spec2d.shape)
|
|
634
|
+
with warnings.catch_warnings():
|
|
635
|
+
warnings.simplefilter("ignore", category=RuntimeWarning)
|
|
636
|
+
for i in range(spec2d.shape[1]):
|
|
637
|
+
left = np.max([i-half_width_pix, 0])
|
|
638
|
+
right = np.min([spec2d.shape[1], i+half_width_pix+1])
|
|
639
|
+
if np.isnan(spec2d[:,left:right]).all():
|
|
640
|
+
weights[:,i] = 0
|
|
641
|
+
else:
|
|
642
|
+
# check if the SNR is too low for this slice
|
|
643
|
+
SNR = (np.nansum(spec2d[clip_range_pix[0]:clip_range_pix[1]+1,i])/
|
|
644
|
+
np.sqrt(np.nansum(spec2d_err[clip_range_pix[0]:clip_range_pix[1]+1,i]**2)))
|
|
645
|
+
|
|
646
|
+
if SNR < 5 or np.isnan(SNR):
|
|
647
|
+
# set weights with the collapsed weights
|
|
648
|
+
weights[:,i] = weights_collapsed
|
|
649
|
+
else:
|
|
650
|
+
profile = 10**20*np.nanmedian(spec2d[:,left:right],
|
|
651
|
+
axis=1)
|
|
652
|
+
profile[profile < 0] = 0.
|
|
653
|
+
profile[np.isnan(list(profile))] = 0.
|
|
654
|
+
# remove outside source range
|
|
655
|
+
profile[:clip_range_pix[0]] = 0.
|
|
656
|
+
profile[clip_range_pix[1]+1:] = 0.
|
|
657
|
+
if np.sum(profile) == 0:
|
|
658
|
+
weights[:,i] = weights_collapsed
|
|
659
|
+
else:
|
|
660
|
+
profile /= np.sum(profile)
|
|
661
|
+
weights[:,i] = profile
|
|
662
|
+
|
|
663
|
+
# Do optimal extraction
|
|
664
|
+
extr = np.nansum(weights*spec2d/spec2d_err**2, axis=0)
|
|
665
|
+
extr /= np.nansum(weights**2/spec2d_err**2, axis=0)
|
|
666
|
+
extr_err = np.sqrt(1./np.nansum(weights**2/spec2d_err**2, axis=0))
|
|
667
|
+
|
|
668
|
+
spec1d = np.c_[wavs, extr, extr_err]
|
|
669
|
+
|
|
670
|
+
return spec1d
|
|
671
|
+
|
|
672
|
+
def extract_1d(self, s2d_file, y_centroid=None, y_tolerance=2,
|
|
673
|
+
y_max_width=2, width_pix=51):
|
|
674
|
+
print(s2d_file)
|
|
675
|
+
reduced = datamodels.open(s2d_file)
|
|
676
|
+
|
|
677
|
+
self.y_tolerance = y_tolerance
|
|
678
|
+
|
|
679
|
+
# Units are inconsistent between output files, fix to cgs
|
|
680
|
+
photmjsr = reduced.meta.photometry.conversion_megajanskys
|
|
681
|
+
|
|
682
|
+
wavs = self._get_wavs(reduced)*10000
|
|
683
|
+
spec2d = reduced.data*10**-17*2.9979*10**18/wavs**2/photmjsr
|
|
684
|
+
spec2d_err = reduced.err*10**-17*2.9979*10**18/wavs**2/photmjsr
|
|
685
|
+
|
|
686
|
+
# Weirdly in some cases all the errors are zero, not sure why
|
|
687
|
+
if np.nansum(spec2d_err) == 0:
|
|
688
|
+
spec2d_err += 1.
|
|
689
|
+
else:
|
|
690
|
+
spec2d_err[spec2d_err == 0] = np.nan
|
|
691
|
+
|
|
692
|
+
x = np.arange(spec2d.shape[0])
|
|
693
|
+
|
|
694
|
+
# Fit the full profile to get centroids
|
|
695
|
+
|
|
696
|
+
profile = 10**20*np.nanmedian(spec2d, axis=1)
|
|
697
|
+
profile[profile < 0] = 0.
|
|
698
|
+
|
|
699
|
+
# This is where you specify the y centroid for the 1D extraction
|
|
700
|
+
#y_start = spec2d.shape[0]/2
|
|
701
|
+
if y_centroid is None:
|
|
702
|
+
y_start = np.nanargmax(profile[3:-3]) + 3
|
|
703
|
+
else:
|
|
704
|
+
y_start = y_centroid
|
|
705
|
+
"""
|
|
706
|
+
# Attempt to figure out y centroids from msa metafile
|
|
707
|
+
msametfl = reduced.meta.instrument.msa_metadata_file
|
|
708
|
+
msametid = reduced.meta.instrument.msa_metadata_id
|
|
709
|
+
shut = Table.read("rate_files/" + msametfl, hdu=2).to_pandas()
|
|
710
|
+
shut = shut.groupby(shut["dither_point_index"] == 1).get_group(True)
|
|
711
|
+
shut = shut.groupby(shut["msa_metadata_id"] == msametid).get_group(True)
|
|
712
|
+
print(shut["primary_source"].values)
|
|
713
|
+
filebase = s2d_file.split("_")[2] + "_" + s2d_file.split("_")[3]
|
|
714
|
+
mask = self.matched["fileSetName"].str.decode("utf-8").str.contains(filebase)
|
|
715
|
+
mask2 = shut["source_id"] == self.matched["source_id"][mask].values[0]
|
|
716
|
+
source_shut = shut.groupby(mask2).get_group(True)
|
|
717
|
+
if len(source_shut) > 1:
|
|
718
|
+
primary_mask = (source_shut["primary_source"].str.decode("utf-8") == "Y")
|
|
719
|
+
source_shut = source_shut.groupby(primary_mask).get_group(True)
|
|
720
|
+
|
|
721
|
+
obj_shut_col = source_shut["shutter_column"].values[0]
|
|
722
|
+
shut_col_min = np.min(shut["shutter_column"].values)
|
|
723
|
+
|
|
724
|
+
shutter_height_pix = (0.46+0.07)/0.1
|
|
725
|
+
|
|
726
|
+
n_shut = (obj_shut_col - shut_col_min)
|
|
727
|
+
|
|
728
|
+
if len(shut) < 3:
|
|
729
|
+
n_shut += 3 - len(shut)
|
|
730
|
+
|
|
731
|
+
# + source_shut["estimated_source_in_shutter_y"].values[0]
|
|
732
|
+
print(n_shut, source_shut["estimated_source_in_shutter_y"].values[0])
|
|
733
|
+
y_start = spec2d.shape[0] - 2 - (n_shut*shutter_height_pix + 0.7 + source_shut["estimated_source_in_shutter_y"].values[0]*4.6)
|
|
734
|
+
print("Initial guess for y centroid:", y_start)
|
|
735
|
+
input()
|
|
736
|
+
"""
|
|
737
|
+
|
|
738
|
+
# Fit the 1D extraction profile
|
|
739
|
+
result = minimize(self._chisq, [1., y_start, 1.],
|
|
740
|
+
args=[x, profile, y_start],
|
|
741
|
+
bounds=[(None, None), (y_start-5, y_start+5),
|
|
742
|
+
(0, y_max_width)])
|
|
743
|
+
|
|
744
|
+
y_mod = self._model(result["x"], x)
|
|
745
|
+
weights_collapsed = y_mod/np.sum(y_mod)
|
|
746
|
+
spec1d = self.rolling_extraction(wavs, spec2d, spec2d_err, result,
|
|
747
|
+
int((width_pix-1)/2),
|
|
748
|
+
weights_collapsed)
|
|
749
|
+
spec1d_mask = np.invert(np.isnan(spec1d[:, 1]))
|
|
750
|
+
spec1d = spec1d[spec1d_mask, :]
|
|
751
|
+
|
|
752
|
+
# Simplest 1D extraction from Adam's old code
|
|
753
|
+
#weights = np.expand_dims(y_mod/np.sum(y_mod), axis=1)
|
|
754
|
+
#extr = np.nansum(weights*spec2d/spec2d_err**2, axis=0)
|
|
755
|
+
#extr /= np.nansum(weights**2/spec2d_err**2, axis=0)
|
|
756
|
+
#extr_err = np.sqrt(1./np.nansum(weights**2/spec2d_err**2, axis=0))
|
|
757
|
+
#mask = (extr != 0.)
|
|
758
|
+
#spec1d = np.c_[wavs, extr, extr_err]
|
|
759
|
+
#spec1d = spec1d[mask, :]
|
|
760
|
+
|
|
761
|
+
return spec1d, result, profile
|
|
762
|
+
|
|
763
|
+
def post_pipeline_analysis(self):
|
|
764
|
+
s2d_file_list = glob.glob("s2d_files/*_s2d.fits")
|
|
765
|
+
|
|
766
|
+
for s2d_file in s2d_file_list:
|
|
767
|
+
|
|
768
|
+
# check if already done
|
|
769
|
+
if os.path.isfile("x1d_files/" + s2d_file.split('/')[-1][:-8]
|
|
770
|
+
+ "x1d.txt"):
|
|
771
|
+
continue
|
|
772
|
+
|
|
773
|
+
# Do 1D extraction
|
|
774
|
+
spec1d, result, profile = self.extract_1d(s2d_file)
|
|
775
|
+
|
|
776
|
+
# Make plot
|
|
777
|
+
reduced = datamodels.open(s2d_file)
|
|
778
|
+
# Units are inconsistent between output files, fix to cgs
|
|
779
|
+
photmjsr = reduced.meta.photometry.conversion_megajanskys
|
|
780
|
+
|
|
781
|
+
wavs = self._get_wavs(reduced)*10000
|
|
782
|
+
spec2d = reduced.data*10**-17*2.9979*10**18/wavs**2/photmjsr
|
|
783
|
+
spec2d_err = reduced.err*10**-17*2.9979*10**18/wavs**2/photmjsr
|
|
784
|
+
spec2d_err[spec2d_err == 0] = np.nan
|
|
785
|
+
|
|
786
|
+
x = np.arange(spec2d.shape[0])
|
|
787
|
+
|
|
788
|
+
plt.figure(figsize=(12, 5))
|
|
789
|
+
gs = mpl.gridspec.GridSpec(3, 7, hspace=0.1, wspace=0.1)
|
|
790
|
+
|
|
791
|
+
# profile plot in bottom right
|
|
792
|
+
profile = profile.astype('float64')
|
|
793
|
+
profile[np.isnan(profile)] = 0
|
|
794
|
+
ax_profile = plt.subplot(gs[-1,6])
|
|
795
|
+
ax_profile.stairs(profile, edges=[0.5]+list(x+0.5), color='k',
|
|
796
|
+
orientation='horizontal')
|
|
797
|
+
ax_profile.axhline(result["x"][1], color="blue", lw=0.5, ls="--",
|
|
798
|
+
label="actual extraction centroid")
|
|
799
|
+
ax_profile.set_yticklabels([])
|
|
800
|
+
|
|
801
|
+
# Save off 1D spectrum file
|
|
802
|
+
spec1d = spec1d[np.invert(np.isnan(spec1d[:, 1])), :]
|
|
803
|
+
np.savetxt("x1d_files/" + s2d_file.split('/')[1][:-8]+ "x1d.txt",
|
|
804
|
+
spec1d)
|
|
805
|
+
|
|
806
|
+
# Plot 1D spectrum
|
|
807
|
+
ax = plt.subplot(gs[:-1,:6])
|
|
808
|
+
ax.plot(spec1d[:, 0], spec1d[:, 1]*10**19, color="dodgerblue")
|
|
809
|
+
ax.axhline(0, color='gray', ls='--', zorder=-1)
|
|
810
|
+
ax.fill_between(spec1d[:, 0], 0, spec1d[:, 2]*10**19,
|
|
811
|
+
color='lightgray', zorder=-2)
|
|
812
|
+
|
|
813
|
+
ax.set_ylabel("$f_\lambda\ /\ \mathrm{10^{-19}"
|
|
814
|
+
+ "\ erg\ s^{-1}\ cm^{-2}\ \AA^{-1}}$")
|
|
815
|
+
|
|
816
|
+
ax.set_xlim(spec1d[0,0]-(spec1d[-1,0]-spec1d[0,0])*0.02,
|
|
817
|
+
spec1d[-1,0]+(spec1d[-1,0]-spec1d[0,0])*0.02)
|
|
818
|
+
|
|
819
|
+
mask = ((spec1d[:, 1] < np.nanmedian(spec1d[:, 1])
|
|
820
|
+
+ 1.426*5*median_abs_deviation(spec1d[:, 1],
|
|
821
|
+
nan_policy="omit"))
|
|
822
|
+
& (spec1d[:, 1] > np.nanmedian(spec1d[:, 1])
|
|
823
|
+
- 1.426*5*median_abs_deviation(spec1d[:, 1],
|
|
824
|
+
nan_policy="omit")))
|
|
825
|
+
|
|
826
|
+
ymax = 1.2*10**19*np.nanmax(spec1d[mask, 1])
|
|
827
|
+
|
|
828
|
+
ax.set_ylim(-0.1*ymax, ymax)
|
|
829
|
+
ax.set_title(s2d_file.split('/')[-1])
|
|
830
|
+
ax.set_xticklabels([])
|
|
831
|
+
|
|
832
|
+
# 2D plot panel
|
|
833
|
+
ax2 = plt.subplot(gs[-1,:6])
|
|
834
|
+
vmin = -1.426*median_abs_deviation(spec2d.flatten(),
|
|
835
|
+
nan_policy="omit")
|
|
836
|
+
vmax = 1.426*3*median_abs_deviation(spec2d.flatten(),
|
|
837
|
+
nan_policy="omit")
|
|
838
|
+
ax2.pcolor(np.tile(wavs, (spec2d.shape[0],1)),
|
|
839
|
+
np.tile(np.arange(spec2d.shape[0]),
|
|
840
|
+
(spec2d.shape[1],1)).T, spec2d,
|
|
841
|
+
vmin=vmin, vmax=vmax, cmap='hot')
|
|
842
|
+
|
|
843
|
+
ax2.set_xlabel("Wavelength / \AA")
|
|
844
|
+
ax2.set_xlim(ax.get_xlim())
|
|
845
|
+
|
|
846
|
+
ax2.axhline(result["x"][1], color="blue", lw=0.5, ls="--",
|
|
847
|
+
label="actual extraction centroid")
|
|
848
|
+
|
|
849
|
+
ax2.set_facecolor('lightgray')
|
|
850
|
+
ax_profile.set_ylim(ax2.get_ylim())
|
|
851
|
+
|
|
852
|
+
plt.savefig("plots/" + s2d_file.split('/')[1][:-9] + ".pdf",
|
|
853
|
+
bbox_inches="tight")
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: piratical
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Plunder spectra from the JWST NIRSpec archive
|
|
5
|
+
Home-page: https://bagpipes.readthedocs.io
|
|
6
|
+
Author: Adam Carnall, Ho-Hin Leung
|
|
7
|
+
Author-email: adamc@roe.ac.uk
|
|
8
|
+
Project-URL: GitHub, https://github.com/ACCarnall/piratical_pipeline
|
|
9
|
+
Requires-Dist: numpy<=2.2
|
|
10
|
+
Requires-Dist: pandas
|
|
11
|
+
Requires-Dist: astropy
|
|
12
|
+
Requires-Dist: matplotlib>=2.2.2
|
|
13
|
+
Requires-Dist: scipy
|
|
14
|
+
Requires-Dist: mastquery
|
|
15
|
+
Requires-Dist: jwst
|
|
16
|
+
Dynamic: author
|
|
17
|
+
Dynamic: author-email
|
|
18
|
+
Dynamic: description
|
|
19
|
+
Dynamic: home-page
|
|
20
|
+
Dynamic: project-url
|
|
21
|
+
Dynamic: requires-dist
|
|
22
|
+
Dynamic: summary
|
|
23
|
+
|
|
24
|
+
NIRSpec Piratical Pipeline
|
|
25
|
+
--------------------------
|
|
26
|
+
|
|
27
|
+
Plunder spectra from the JWST archive.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
piratical
|
piratical-0.1.0/setup.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from setuptools import setup
|
|
2
|
+
from os import path
|
|
3
|
+
|
|
4
|
+
here = path.abspath(path.dirname(__file__))
|
|
5
|
+
|
|
6
|
+
# Get the long description from the README file
|
|
7
|
+
with open(path.join(here, 'README.rst'), encoding='utf-8') as f:
|
|
8
|
+
long_description = f.read()
|
|
9
|
+
|
|
10
|
+
setup(
|
|
11
|
+
name='piratical',
|
|
12
|
+
|
|
13
|
+
version='0.1.0',
|
|
14
|
+
|
|
15
|
+
description='Plunder spectra from the JWST NIRSpec archive',
|
|
16
|
+
|
|
17
|
+
long_description=long_description,
|
|
18
|
+
|
|
19
|
+
url='https://bagpipes.readthedocs.io',
|
|
20
|
+
|
|
21
|
+
author='Adam Carnall, Ho-Hin Leung',
|
|
22
|
+
|
|
23
|
+
author_email='adamc@roe.ac.uk',
|
|
24
|
+
|
|
25
|
+
packages=["piratical"],
|
|
26
|
+
|
|
27
|
+
include_package_data=True,
|
|
28
|
+
|
|
29
|
+
install_requires=["numpy<=2.2", "pandas", "astropy", "matplotlib>=2.2.2",
|
|
30
|
+
"scipy", "mastquery", "jwst"],
|
|
31
|
+
|
|
32
|
+
project_urls={
|
|
33
|
+
"GitHub": "https://github.com/ACCarnall/piratical_pipeline"
|
|
34
|
+
}
|
|
35
|
+
)
|