piratical 0.1.0__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
piratical/__init__.py ADDED
@@ -0,0 +1 @@
1
+ from .piratical import piratical_pipeline
piratical/piratical.py ADDED
@@ -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,6 @@
1
+ piratical/__init__.py,sha256=NMLBUxJJapBBaC1KhootZcj-2hB5LJoruf8-ttvVOmc,41
2
+ piratical/piratical.py,sha256=6Qqk_himX9ZeCYNlpRGBQMTi53uNobMmRuNIk2U1nWg,35198
3
+ piratical-0.1.0.dist-info/METADATA,sha256=dE3ManXaq9cZOV0xPTdVqlkJDaN6i6wQjtp4M_Tkyg8,689
4
+ piratical-0.1.0.dist-info/WHEEL,sha256=JNWh1Fm1UdwIQV075glCn4MVuCRs0sotJIq-J6rbxCU,109
5
+ piratical-0.1.0.dist-info/top_level.txt,sha256=Puy2NkUhL3a-I6Izlc1u4sRDxwofL2OcvhkkrnA8KPA,10
6
+ piratical-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,6 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py2-none-any
5
+ Tag: py3-none-any
6
+
@@ -0,0 +1 @@
1
+ piratical