On-The-Fly (OTF) Data Reduction#
This notebook shows how to use dysh to calibrate and grid an On-The-Fly (OTF) observation. See Mangum et al. (2007) for the background on this method.
OTF observations can be calibrated in dysh, however, generating FITS cubes requires the use of additional applications.
The workflow to go from raw data to a FITS cube would then require the following steps:
Calibrate the data using dysh. This can include baseline subtraction, altough in some cases baseline removal is more effective in a FITS cube.
Write the calibrated spectra to a format compatible with the gridding tool being used. For GBT observations the recommended format is SDFITS and the tool to grid the data is the gbtgridder.
Baseline subtraction in the FITS cube. This is optional, depending on the data quality, and dysh does not provide convenience functions for this.
Dysh commands#
The following dysh commands are introduced (leaving out all the function arguments):
filename = dysh_data()
sdf = GBTFITSLoad()
sb = sdf.getsigref()
ta = sb.timeaverage()
ta.baseline()
ta.plot()
Loading Modules#
We start by loading the modules we will use for the data reduction.
# We use dysh_data to retrieve the example data set.
from dysh.util.files import dysh_data
from pathlib import Path
# This is required to load and calibrate the example data set.
from dysh.fits.gbtfitsload import GBTFITSLoad
from dysh.log import init_logging
Setup#
We start the dysh logging, so we get more information about what is happening.
This is only needed if working on a notebook.
If using the CLI through the dysh command, then logging is setup for you.
init_logging(2)
# also create a local "output" directory where temporary notebook files can be stored.
output_dir = Path.cwd() / "output"
output_dir.mkdir(exist_ok=True)
Data Retrieval#
Download the example SDFITS data, if necessary.
filename = dysh_data(example='otf1')
23:07:22.743 I Resolving example=otf1 -> mapping-L/data/TGBT17A_506_11.raw.vegas/TGBT17A_506_11.raw.vegas.A.fits
23:07:22.744 I url: http://www.gb.nrao.edu/dysh//example_data/mapping-L/data/TGBT17A_506_11.raw.vegas/TGBT17A_506_11.raw.vegas.A.fits
Downloading TGBT17A_506_11.raw.vegas.A.fits from http://www.gb.nrao.edu/dysh//example_data/mapping-L/data/TGBT17A_506_11.raw.vegas/TGBT17A_506_11.raw.vegas.A.fits
23:07:23.029 I Starting download...
23:07:39.999 I Saved TGBT17A_506_11.raw.vegas.A.fits to TGBT17A_506_11.raw.vegas.A.fits
Retrieved TGBT17A_506_11.raw.vegas.A.fits
sdfits = GBTFITSLoad(filename)
sdfits.summary()
| SCAN | OBJECT | VELOCITY | PROC | PROCSEQN | RESTFREQ | # IF | # POL | # INT | # FEED | AZIMUTH | ELEVATION |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 6 | 3C286 | 0.0 | OnOff | 1 | 1.6168 | 5 | 2 | 13 | 1 | 248.3657 | 72.5531 |
| 7 | 3C286 | 0.0 | OnOff | 2 | 1.6168 | 5 | 2 | 13 | 1 | 250.0385 | 73.6777 |
| 8 | SgrB2M | 57.0 | Track | 1 | 1.6168 | 5 | 2 | 61 | 1 | 142.3845 | 12.2545 |
| 9 | W33B | 64.0 | Track | 1 | 1.6168 | 5 | 2 | 61 | 1 | 132.2057 | 17.9786 |
| 10 | G30.589-0.044 | 38.0 | Track | 1 | 1.6168 | 5 | 2 | 61 | 1 | 115.4811 | 25.5523 |
| 11 | G31.412+0.307 | 97.0 | Track | 1 | 1.6168 | 5 | 2 | 61 | 1 | 115.8034 | 27.0986 |
| 12 | G35.577-0.029 | 49.0 | Track | 1 | 1.6168 | 5 | 2 | 61 | 1 | 112.3139 | 29.0337 |
| 13 | G40.622-0.137 | 32.0 | Track | 1 | 1.6168 | 5 | 2 | 61 | 1 | 107.7736 | 31.3159 |
| 14 | NGC6946 | 45.0 | DecLatMap | 1 | 1.6168 | 5 | 2 | 61 | 1 | 38.9626 | 41.3644 |
| 15 | NGC6946 | 45.0 | DecLatMap | 2 | 1.6168 | 5 | 2 | 61 | 1 | 38.9910 | 41.4488 |
| 16 | NGC6946 | 45.0 | DecLatMap | 3 | 1.6168 | 5 | 2 | 61 | 1 | 38.9918 | 41.5360 |
| 17 | NGC6946 | 45.0 | DecLatMap | 4 | 1.6168 | 5 | 2 | 61 | 1 | 39.0193 | 41.6203 |
| 18 | NGC6946 | 45.0 | DecLatMap | 5 | 1.6168 | 5 | 2 | 61 | 1 | 39.0198 | 41.7075 |
| 19 | NGC6946 | 45.0 | DecLatMap | 6 | 1.6168 | 5 | 2 | 61 | 1 | 39.0468 | 41.7919 |
| 20 | NGC6946 | 45.0 | DecLatMap | 7 | 1.6168 | 5 | 2 | 61 | 1 | 39.0465 | 41.8791 |
| 21 | NGC6946 | 45.0 | DecLatMap | 8 | 1.6168 | 5 | 2 | 61 | 1 | 39.0730 | 41.9637 |
| 22 | NGC6946 | 45.0 | DecLatMap | 9 | 1.6168 | 5 | 2 | 61 | 1 | 39.0722 | 42.0508 |
| 23 | NGC6946 | 45.0 | DecLatMap | 10 | 1.6168 | 5 | 2 | 61 | 1 | 39.0983 | 42.1355 |
| 24 | NGC6946 | 45.0 | DecLatMap | 11 | 1.6168 | 5 | 2 | 61 | 1 | 39.0968 | 42.2227 |
| 25 | NGC6946 | 45.0 | DecLatMap | 12 | 1.6168 | 5 | 2 | 61 | 1 | 39.1224 | 42.3073 |
| 26 | NGC6946 | 45.0 | DecLatMap | 13 | 1.6168 | 5 | 2 | 61 | 1 | 39.1202 | 42.3945 |
| 27 | NGC6946 | 45.0 | Track | 1 | 1.6168 | 5 | 2 | 61 | 1 | 39.0881 | 42.1493 |
In this particular observation the OTF slew over the galaxy NGC6946 in scans 14-26, followed by an Off position scan 27. Each SCAN, in this case, corresponds to a row, with 61 integrations as the telescope slews slowly over the sky.
Data Reduction#
We use getsigref to calibrate the OTF scans using scan 27 as the reference position.
getsigref takes as input a list of scan numbers, or a single one, and a number, or Spectrum, for the reference position.
It calibrates all the input scans using
We start by defining the variables we will use to calibrate the 21 cm-HI line observations present in this data set.
ifnum = 0 # The 21 cm line is in the spectral window labeled 0.
fdnum = 0 # Only one feed in this data set
ref = 27 # The reference ("OFF") scan
scans = list(range(14,27)) # The signal ("ON") scans 14..26
Now, we calibrate a single polarization. The processing for the second polarization should be almost identical.
sb0 = sdfits.getsigref(scan=scans, ref=ref, fdnum=fdnum, ifnum=ifnum, plnum=0)
23:07:43.339 I Ignoring 1 blanked integration(s).
23:07:44.349 I Ignoring 1 blanked integration(s).
23:07:44.598 I Ignoring 1 blanked integration(s).
23:07:44.826 I Ignoring 1 blanked integration(s).
23:07:45.054 I Ignoring 1 blanked integration(s).
23:07:45.284 I Ignoring 1 blanked integration(s).
23:07:45.513 I Ignoring 1 blanked integration(s).
23:07:45.742 I Ignoring 1 blanked integration(s).
23:07:45.971 I Ignoring 1 blanked integration(s).
23:07:46.200 I Ignoring 1 blanked integration(s).
23:07:46.429 I Ignoring 1 blanked integration(s).
23:07:46.660 I Ignoring 1 blanked integration(s).
23:07:46.889 I Ignoring 1 blanked integration(s).
23:07:47.120 I Ignoring 1 blanked integration(s).
The return from getsigref is a ScanBlock with 13 PSScans in it.
sb0
ScanBlock([<dysh.spectra.scan.PSScan at 0x7f364122f3d0>,
<dysh.spectra.scan.PSScan at 0x7f364122d5d0>,
<dysh.spectra.scan.PSScan at 0x7f3641253160>,
<dysh.spectra.scan.PSScan at 0x7f3641252e90>,
<dysh.spectra.scan.PSScan at 0x7f3641252d40>,
<dysh.spectra.scan.PSScan at 0x7f3641253550>,
<dysh.spectra.scan.PSScan at 0x7f36d2e37940>,
<dysh.spectra.scan.PSScan at 0x7f3641253ca0>,
<dysh.spectra.scan.PSScan at 0x7f3641251fc0>,
<dysh.spectra.scan.PSScan at 0x7f3642e2ae30>,
<dysh.spectra.scan.PSScan at 0x7f36412c2050>,
<dysh.spectra.scan.PSScan at 0x7f364122cf10>,
<dysh.spectra.scan.PSScan at 0x7f3642e2ba30>])
Data Inspection#
After calibrating the data, we can inspect the calibrated data using the plotting methods available in dysh.
First, we will extract a single spectrum from the middle of the observations and plot it.
To get the spectrum from the middle of the OTF map, we use len(sb0)//2 to specify the middle scan and sb0[len(sb0)//2].nint//2 to specify the middle integration (in this case there are 61 integrations).
We store the number of integrations in the nint variable to reuse it later.
nint = sb0[len(sb0)//2].nint
print(nint)
spec = sb0[len(sb0)//2].getspec(nint//2)
60
Although summary told us there were 61 integrations, only 60 were recorded by getsigref. Apparently one integration was blanked.
sp = spec.plot(xaxis_unit="km/s")
A clear and strong signal is present, as well as a ~2 K continuum. There is also an obvious negative signal, most likely local HI since the Off position will never be line free near vlsr ~ 0
Now we will plot the time average for the middle scan, where the noise should be able \(\sqrt{60} ~ \approx 7\) times smaller.
spec_avg = sb0[len(sb0)//2].timeaverage()
Again, a clear signal, but the line brightness and continuum are lower due to the dilution from the time averaging. This is because the source is smaller than the area mapped, so the signal gets averaged with empty sky.
Baseline Subtraction#
If we are only interested in the spectral line, then we use baseline subtraction to remove the continuum. We will use an order 1 polynomial to remove the continuum, and make sure to exclude the edge channels as well as the channels with 21 cm signal during the baseline fit.
We explore two approaches to continuum subtraction (baseline removal), using a model derived from a time average and deriving a baseline for each integration.
Using a Time Average#
We start by using the time average of the spectra in one scan to derive a baseline model, and then we will use this baseline model to subtract the continuum from all of the integrations in the scan. This approach has the benefit of being faster than deriving a baseline from each integration, and the spectrum used to derive the baseline model has a higher signal-to-noise. However, this approach requires that the baseline be stable in time/sky position. If that is not the case, then there will be left over baseline/continuum in the baseline subtracted data.
We start by plotting the time average as a function of channel number to determine where the 21 cm signal is.
sp_avg = spec_avg.plot(xaxis_unit="channel")
From the plot we see that channels ~1600 to 2250 have 21 cm signal, and the edges are between 0 and 250, and 4095-250 and 4095. We define an exclude region with these.
exclude = [(0,250),
(1600,2250),
(4095-250,4095)]
Now do the baseline fitting, using an order 1 polynomial and removing the best fit model from the data.
spec_avg.baseline(1, model="poly", exclude=exclude, remove=True)
23:07:48.042 I EXCLUDING [Spectral Region, 1 sub-regions:
(1408148687.1396484 Hz, 1409579198.6142578 Hz)
, Spectral Region, 1 sub-regions:
(1418705861.8222656 Hz, 1422425191.65625 Hz)
, Spectral Region, 1 sub-regions:
(1430149953.6191406 Hz, 1431580465.09375 Hz)
]
Plot the baseline subtracted data.
(Alternatively, one could use spec_avg.plot().)
sp_avg.plot(xaxis_unit = "km/s")
The continuum has been removed (the spectrum is centered around 0 K). We can look at the statistics to verify this. We will be using the left wing where the baseline fits was used.
spec_avg[250:1600].stats()
23:07:48.440 I Note: found 1 NaN (masked) values
{'mean': <Quantity -0.00040599 K>,
'median': <Quantity -0.00123781 K>,
'rms': <Quantity 0.04531066 K>,
'min': <Quantity -0.14616574 K>,
'max': <Quantity 0.17465374 K>,
'npt': 1350,
'nan': np.int64(1)}
The mean is around -0.4 mK, the median -1.4 mK and the rms 46 mK.
We can subtract this baseline model from all of the integrations using the subtract_baseline method of a ScanBlock or Scan.
The input to subtract_baseline should be a baseline model, which can be accessed through the baseline_model attribute of a Spectrum object.
We could also subtract it from all scans, which we will do below in order to write a whole grid of baseline subtracted spectra.
sb0[len(sb0)//2].subtract_baseline(spec_avg.baseline_model)
Generate a time average again to see how the data changed after the baseline subtraction.
spec_avg_bsub = sb0[len(sb0)//2].timeaverage()
sp_avg_bsub = spec_avg_bsub.plot()
The time average for the middle scan of the OTF map is now centered around zero as expected. However, since we used the diluted time average for the middle scan as our baseline model, the spectra that cover the source still have continuum left.
spec_bsub = sb0[len(sb0)//2].getspec(nint//2)
sp_spec_bsub = spec_bsub.plot()
Using the Integrations#
Now we will repeat the calibration, but deriving a baseline model from each integration. This is slower, so we only do this for the middle scan of the OTF map. This approach has the benefit of being more flexible than the previous one, however the signal-to-noise is worse.
To access the data for the integrations we will use the calibrated method and the _calibrated property of a Scan.
calibrated returns the data for a specific integration (starting at 0) as a Spectrum object, while _calibrated is the array that contains the calibrated data for the Scan.
We will use the Spectrum to derive the baseline, and then update the data by updating the _calibrated property of the Scan.
We put the middle scan of the OTF map in a new variable scan, then loop over its integrations fitting a baseline model and subtracting it from the calibrated data.
We ignore integrations that were blanked (all values would be NaN).
We use the math library to check for NaNs.
import math # Load the `math` library. Use this instead of `numpy` to save memory.
scan = sb0[len(sb0)//2]
for i,_c in enumerate(scan.calibrated):
if math.isnan(_c.data.sum()):
# If the sum is NaN, then skip (continue) this item.
# This is not a great solution, as even a single NaN value
# in the spectrum would cause the sum to be NaN,
# but there should be no NaN values for single channels
# in this data set.
continue
s_i = scan.getspec(i) # Fetch the `Spectrum` for integration `i`.
s_i.baseline(1, model="poly", exclude=exclude, remove=True) # Fit a baseline model.
scan.calibrated[i] -= s_i.baseline_model(s_i.spectral_axis).value # Subtract the baseline model from the data.
23:07:49.009 I EXCLUDING [Spectral Region, 1 sub-regions:
(1408148687.1396484 Hz, 1409579198.6142578 Hz)
, Spectral Region, 1 sub-regions:
(1418705861.8222656 Hz, 1422425191.65625 Hz)
, Spectral Region, 1 sub-regions:
(1430149953.6191406 Hz, 1431580465.09375 Hz)
]
23:07:49.118 I EXCLUDING [Spectral Region, 1 sub-regions:
(1408148674.1396484 Hz, 1409579185.6142578 Hz)
, Spectral Region, 1 sub-regions:
(1418705848.8222656 Hz, 1422425178.65625 Hz)
, Spectral Region, 1 sub-regions:
(1430149940.6191406 Hz, 1431580452.09375 Hz)
]
23:07:49.228 I EXCLUDING [Spectral Region, 1 sub-regions:
(1408148648.1396484 Hz, 1409579159.6142578 Hz)
, Spectral Region, 1 sub-regions:
(1418705822.8222656 Hz, 1422425152.65625 Hz)
, Spectral Region, 1 sub-regions:
(1430149914.6191406 Hz, 1431580426.09375 Hz)
]
23:07:49.338 I EXCLUDING [Spectral Region, 1 sub-regions:
(1408148622.1396484 Hz, 1409579133.6142578 Hz)
, Spectral Region, 1 sub-regions:
(1418705796.8222656 Hz, 1422425126.65625 Hz)
, Spectral Region, 1 sub-regions:
(1430149888.6191406 Hz, 1431580400.09375 Hz)
]
23:07:49.448 I EXCLUDING [Spectral Region, 1 sub-regions:
(1408148596.1396484 Hz, 1409579107.6142578 Hz)
, Spectral Region, 1 sub-regions:
(1418705770.8222656 Hz, 1422425100.65625 Hz)
, Spectral Region, 1 sub-regions:
(1430149862.6191406 Hz, 1431580374.09375 Hz)
]
23:07:49.557 I EXCLUDING [Spectral Region, 1 sub-regions:
(1408148570.1396484 Hz, 1409579081.6142578 Hz)
, Spectral Region, 1 sub-regions:
(1418705744.8222656 Hz, 1422425074.65625 Hz)
, Spectral Region, 1 sub-regions:
(1430149836.6191406 Hz, 1431580348.09375 Hz)
]
23:07:49.667 I EXCLUDING [Spectral Region, 1 sub-regions:
(1408148543.1396484 Hz, 1409579054.6142578 Hz)
, Spectral Region, 1 sub-regions:
(1418705717.8222656 Hz, 1422425047.65625 Hz)
, Spectral Region, 1 sub-regions:
(1430149809.6191406 Hz, 1431580321.09375 Hz)
]
23:07:49.777 I EXCLUDING [Spectral Region, 1 sub-regions:
(1408148517.1396484 Hz, 1409579028.6142578 Hz)
, Spectral Region, 1 sub-regions:
(1418705691.8222656 Hz, 1422425021.65625 Hz)
, Spectral Region, 1 sub-regions:
(1430149783.6191406 Hz, 1431580295.09375 Hz)
]
23:07:49.887 I EXCLUDING [Spectral Region, 1 sub-regions:
(1408148491.1396484 Hz, 1409579002.6142578 Hz)
, Spectral Region, 1 sub-regions:
(1418705665.8222656 Hz, 1422424995.65625 Hz)
, Spectral Region, 1 sub-regions:
(1430149757.6191406 Hz, 1431580269.09375 Hz)
]
23:07:49.997 I EXCLUDING [Spectral Region, 1 sub-regions:
(1408148465.1396484 Hz, 1409578976.6142578 Hz)
, Spectral Region, 1 sub-regions:
(1418705639.8222656 Hz, 1422424969.65625 Hz)
, Spectral Region, 1 sub-regions:
(1430149731.6191406 Hz, 1431580243.09375 Hz)
]
23:07:50.107 I EXCLUDING [Spectral Region, 1 sub-regions:
(1408148439.1396484 Hz, 1409578950.6142578 Hz)
, Spectral Region, 1 sub-regions:
(1418705613.8222656 Hz, 1422424943.65625 Hz)
, Spectral Region, 1 sub-regions:
(1430149705.6191406 Hz, 1431580217.09375 Hz)
]
23:07:50.217 I EXCLUDING [Spectral Region, 1 sub-regions:
(1408148413.1396484 Hz, 1409578924.6142578 Hz)
, Spectral Region, 1 sub-regions:
(1418705587.8222656 Hz, 1422424917.65625 Hz)
, Spectral Region, 1 sub-regions:
(1430149679.6191406 Hz, 1431580191.09375 Hz)
]
23:07:50.326 I EXCLUDING [Spectral Region, 1 sub-regions:
(1408148387.1396484 Hz, 1409578898.6142578 Hz)
, Spectral Region, 1 sub-regions:
(1418705561.8222656 Hz, 1422424891.65625 Hz)
, Spectral Region, 1 sub-regions:
(1430149653.6191406 Hz, 1431580165.09375 Hz)
]
23:07:50.435 I EXCLUDING [Spectral Region, 1 sub-regions:
(1408148361.1396484 Hz, 1409578872.6142578 Hz)
, Spectral Region, 1 sub-regions:
(1418705535.8222656 Hz, 1422424865.65625 Hz)
, Spectral Region, 1 sub-regions:
(1430149627.6191406 Hz, 1431580139.09375 Hz)
]
23:07:50.545 I EXCLUDING [Spectral Region, 1 sub-regions:
(1408148334.1396484 Hz, 1409578845.6142578 Hz)
, Spectral Region, 1 sub-regions:
(1418705508.8222656 Hz, 1422424838.65625 Hz)
, Spectral Region, 1 sub-regions:
(1430149600.6191406 Hz, 1431580112.09375 Hz)
]
23:07:50.654 I EXCLUDING [Spectral Region, 1 sub-regions:
(1408148308.1396484 Hz, 1409578819.6142578 Hz)
, Spectral Region, 1 sub-regions:
(1418705482.8222656 Hz, 1422424812.65625 Hz)
, Spectral Region, 1 sub-regions:
(1430149574.6191406 Hz, 1431580086.09375 Hz)
]
23:07:50.763 I EXCLUDING [Spectral Region, 1 sub-regions:
(1408148282.1396484 Hz, 1409578793.6142578 Hz)
, Spectral Region, 1 sub-regions:
(1418705456.8222656 Hz, 1422424786.65625 Hz)
, Spectral Region, 1 sub-regions:
(1430149548.6191406 Hz, 1431580060.09375 Hz)
]
23:07:50.873 I EXCLUDING [Spectral Region, 1 sub-regions:
(1408148256.1396484 Hz, 1409578767.6142578 Hz)
, Spectral Region, 1 sub-regions:
(1418705430.8222656 Hz, 1422424760.65625 Hz)
, Spectral Region, 1 sub-regions:
(1430149522.6191406 Hz, 1431580034.09375 Hz)
]
23:07:50.982 I EXCLUDING [Spectral Region, 1 sub-regions:
(1408148230.1396484 Hz, 1409578741.6142578 Hz)
, Spectral Region, 1 sub-regions:
(1418705404.8222656 Hz, 1422424734.65625 Hz)
, Spectral Region, 1 sub-regions:
(1430149496.6191406 Hz, 1431580008.09375 Hz)
]
23:07:51.091 I EXCLUDING [Spectral Region, 1 sub-regions:
(1408148204.1396484 Hz, 1409578715.6142578 Hz)
, Spectral Region, 1 sub-regions:
(1418705378.8222656 Hz, 1422424708.65625 Hz)
, Spectral Region, 1 sub-regions:
(1430149470.6191406 Hz, 1431579982.09375 Hz)
]
23:07:51.201 I EXCLUDING [Spectral Region, 1 sub-regions:
(1408148178.1396484 Hz, 1409578689.6142578 Hz)
, Spectral Region, 1 sub-regions:
(1418705352.8222656 Hz, 1422424682.65625 Hz)
, Spectral Region, 1 sub-regions:
(1430149444.6191406 Hz, 1431579956.09375 Hz)
]
23:07:51.310 I EXCLUDING [Spectral Region, 1 sub-regions:
(1408148151.1396484 Hz, 1409578662.6142578 Hz)
, Spectral Region, 1 sub-regions:
(1418705325.8222656 Hz, 1422424655.65625 Hz)
, Spectral Region, 1 sub-regions:
(1430149417.6191406 Hz, 1431579929.09375 Hz)
]
23:07:51.419 I EXCLUDING [Spectral Region, 1 sub-regions:
(1408148125.1396484 Hz, 1409578636.6142578 Hz)
, Spectral Region, 1 sub-regions:
(1418705299.8222656 Hz, 1422424629.65625 Hz)
, Spectral Region, 1 sub-regions:
(1430149391.6191406 Hz, 1431579903.09375 Hz)
]
23:07:51.528 I EXCLUDING [Spectral Region, 1 sub-regions:
(1408148099.1396484 Hz, 1409578610.6142578 Hz)
, Spectral Region, 1 sub-regions:
(1418705273.8222656 Hz, 1422424603.65625 Hz)
, Spectral Region, 1 sub-regions:
(1430149365.6191406 Hz, 1431579877.09375 Hz)
]
23:07:51.636 I EXCLUDING [Spectral Region, 1 sub-regions:
(1408148073.1396484 Hz, 1409578584.6142578 Hz)
, Spectral Region, 1 sub-regions:
(1418705247.8222656 Hz, 1422424577.65625 Hz)
, Spectral Region, 1 sub-regions:
(1430149339.6191406 Hz, 1431579851.09375 Hz)
]
23:07:51.745 I EXCLUDING [Spectral Region, 1 sub-regions:
(1408148047.1396484 Hz, 1409578558.6142578 Hz)
, Spectral Region, 1 sub-regions:
(1418705221.8222656 Hz, 1422424551.65625 Hz)
, Spectral Region, 1 sub-regions:
(1430149313.6191406 Hz, 1431579825.09375 Hz)
]
23:07:51.855 I EXCLUDING [Spectral Region, 1 sub-regions:
(1408148021.1396484 Hz, 1409578532.6142578 Hz)
, Spectral Region, 1 sub-regions:
(1418705195.8222656 Hz, 1422424525.65625 Hz)
, Spectral Region, 1 sub-regions:
(1430149287.6191406 Hz, 1431579799.09375 Hz)
]
23:07:51.964 I EXCLUDING [Spectral Region, 1 sub-regions:
(1408147995.1396484 Hz, 1409578506.6142578 Hz)
, Spectral Region, 1 sub-regions:
(1418705169.8222656 Hz, 1422424499.65625 Hz)
, Spectral Region, 1 sub-regions:
(1430149261.6191406 Hz, 1431579773.09375 Hz)
]
23:07:52.073 I EXCLUDING [Spectral Region, 1 sub-regions:
(1408147968.1396484 Hz, 1409578479.6142578 Hz)
, Spectral Region, 1 sub-regions:
(1418705142.8222656 Hz, 1422424472.65625 Hz)
, Spectral Region, 1 sub-regions:
(1430149234.6191406 Hz, 1431579746.09375 Hz)
]
23:07:52.182 I EXCLUDING [Spectral Region, 1 sub-regions:
(1408147942.1396484 Hz, 1409578453.6142578 Hz)
, Spectral Region, 1 sub-regions:
(1418705116.8222656 Hz, 1422424446.65625 Hz)
, Spectral Region, 1 sub-regions:
(1430149208.6191406 Hz, 1431579720.09375 Hz)
]
23:07:52.290 I EXCLUDING [Spectral Region, 1 sub-regions:
(1408147916.1396484 Hz, 1409578427.6142578 Hz)
, Spectral Region, 1 sub-regions:
(1418705090.8222656 Hz, 1422424420.65625 Hz)
, Spectral Region, 1 sub-regions:
(1430149182.6191406 Hz, 1431579694.09375 Hz)
]
23:07:52.399 I EXCLUDING [Spectral Region, 1 sub-regions:
(1408147890.1396484 Hz, 1409578401.6142578 Hz)
, Spectral Region, 1 sub-regions:
(1418705064.8222656 Hz, 1422424394.65625 Hz)
, Spectral Region, 1 sub-regions:
(1430149156.6191406 Hz, 1431579668.09375 Hz)
]
23:07:52.509 I EXCLUDING [Spectral Region, 1 sub-regions:
(1408147864.1396484 Hz, 1409578375.6142578 Hz)
, Spectral Region, 1 sub-regions:
(1418705038.8222656 Hz, 1422424368.65625 Hz)
, Spectral Region, 1 sub-regions:
(1430149130.6191406 Hz, 1431579642.09375 Hz)
]
23:07:52.618 I EXCLUDING [Spectral Region, 1 sub-regions:
(1408147838.1396484 Hz, 1409578349.6142578 Hz)
, Spectral Region, 1 sub-regions:
(1418705012.8222656 Hz, 1422424342.65625 Hz)
, Spectral Region, 1 sub-regions:
(1430149104.6191406 Hz, 1431579616.09375 Hz)
]
23:07:52.726 I EXCLUDING [Spectral Region, 1 sub-regions:
(1408147811.1396484 Hz, 1409578322.6142578 Hz)
, Spectral Region, 1 sub-regions:
(1418704985.8222656 Hz, 1422424315.65625 Hz)
, Spectral Region, 1 sub-regions:
(1430149077.6191406 Hz, 1431579589.09375 Hz)
]
23:07:52.836 I EXCLUDING [Spectral Region, 1 sub-regions:
(1408147785.1396484 Hz, 1409578296.6142578 Hz)
, Spectral Region, 1 sub-regions:
(1418704959.8222656 Hz, 1422424289.65625 Hz)
, Spectral Region, 1 sub-regions:
(1430149051.6191406 Hz, 1431579563.09375 Hz)
]
23:07:52.944 I EXCLUDING [Spectral Region, 1 sub-regions:
(1408147759.1396484 Hz, 1409578270.6142578 Hz)
, Spectral Region, 1 sub-regions:
(1418704933.8222656 Hz, 1422424263.65625 Hz)
, Spectral Region, 1 sub-regions:
(1430149025.6191406 Hz, 1431579537.09375 Hz)
]
23:07:53.054 I EXCLUDING [Spectral Region, 1 sub-regions:
(1408147733.1396484 Hz, 1409578244.6142578 Hz)
, Spectral Region, 1 sub-regions:
(1418704907.8222656 Hz, 1422424237.65625 Hz)
, Spectral Region, 1 sub-regions:
(1430148999.6191406 Hz, 1431579511.09375 Hz)
]
23:07:53.163 I EXCLUDING [Spectral Region, 1 sub-regions:
(1408147707.1396484 Hz, 1409578218.6142578 Hz)
, Spectral Region, 1 sub-regions:
(1418704881.8222656 Hz, 1422424211.65625 Hz)
, Spectral Region, 1 sub-regions:
(1430148973.6191406 Hz, 1431579485.09375 Hz)
]
23:07:53.271 I EXCLUDING [Spectral Region, 1 sub-regions:
(1408147681.1396484 Hz, 1409578192.6142578 Hz)
, Spectral Region, 1 sub-regions:
(1418704855.8222656 Hz, 1422424185.65625 Hz)
, Spectral Region, 1 sub-regions:
(1430148947.6191406 Hz, 1431579459.09375 Hz)
]
23:07:53.380 I EXCLUDING [Spectral Region, 1 sub-regions:
(1408147654.1396484 Hz, 1409578165.6142578 Hz)
, Spectral Region, 1 sub-regions:
(1418704828.8222656 Hz, 1422424158.65625 Hz)
, Spectral Region, 1 sub-regions:
(1430148920.6191406 Hz, 1431579432.09375 Hz)
]
23:07:53.489 I EXCLUDING [Spectral Region, 1 sub-regions:
(1408147628.1396484 Hz, 1409578139.6142578 Hz)
, Spectral Region, 1 sub-regions:
(1418704802.8222656 Hz, 1422424132.65625 Hz)
, Spectral Region, 1 sub-regions:
(1430148894.6191406 Hz, 1431579406.09375 Hz)
]
23:07:53.598 I EXCLUDING [Spectral Region, 1 sub-regions:
(1408147602.1396484 Hz, 1409578113.6142578 Hz)
, Spectral Region, 1 sub-regions:
(1418704776.8222656 Hz, 1422424106.65625 Hz)
, Spectral Region, 1 sub-regions:
(1430148868.6191406 Hz, 1431579380.09375 Hz)
]
23:07:53.706 I EXCLUDING [Spectral Region, 1 sub-regions:
(1408147576.1396484 Hz, 1409578087.6142578 Hz)
, Spectral Region, 1 sub-regions:
(1418704750.8222656 Hz, 1422424080.65625 Hz)
, Spectral Region, 1 sub-regions:
(1430148842.6191406 Hz, 1431579354.09375 Hz)
]
23:07:53.816 I EXCLUDING [Spectral Region, 1 sub-regions:
(1408147550.1396484 Hz, 1409578061.6142578 Hz)
, Spectral Region, 1 sub-regions:
(1418704724.8222656 Hz, 1422424054.65625 Hz)
, Spectral Region, 1 sub-regions:
(1430148816.6191406 Hz, 1431579328.09375 Hz)
]
23:07:53.924 I EXCLUDING [Spectral Region, 1 sub-regions:
(1408147524.1396484 Hz, 1409578035.6142578 Hz)
, Spectral Region, 1 sub-regions:
(1418704698.8222656 Hz, 1422424028.65625 Hz)
, Spectral Region, 1 sub-regions:
(1430148790.6191406 Hz, 1431579302.09375 Hz)
]
23:07:54.033 I EXCLUDING [Spectral Region, 1 sub-regions:
(1408147497.1396484 Hz, 1409578008.6142578 Hz)
, Spectral Region, 1 sub-regions:
(1418704671.8222656 Hz, 1422424001.65625 Hz)
, Spectral Region, 1 sub-regions:
(1430148763.6191406 Hz, 1431579275.09375 Hz)
]
23:07:54.142 I EXCLUDING [Spectral Region, 1 sub-regions:
(1408147471.1396484 Hz, 1409577982.6142578 Hz)
, Spectral Region, 1 sub-regions:
(1418704645.8222656 Hz, 1422423975.65625 Hz)
, Spectral Region, 1 sub-regions:
(1430148737.6191406 Hz, 1431579249.09375 Hz)
]
23:07:54.251 I EXCLUDING [Spectral Region, 1 sub-regions:
(1408147445.1396484 Hz, 1409577956.6142578 Hz)
, Spectral Region, 1 sub-regions:
(1418704619.8222656 Hz, 1422423949.65625 Hz)
, Spectral Region, 1 sub-regions:
(1430148711.6191406 Hz, 1431579223.09375 Hz)
]
23:07:54.360 I EXCLUDING [Spectral Region, 1 sub-regions:
(1408147419.1396484 Hz, 1409577930.6142578 Hz)
, Spectral Region, 1 sub-regions:
(1418704593.8222656 Hz, 1422423923.65625 Hz)
, Spectral Region, 1 sub-regions:
(1430148685.6191406 Hz, 1431579197.09375 Hz)
]
23:07:54.469 I EXCLUDING [Spectral Region, 1 sub-regions:
(1408147393.1396484 Hz, 1409577904.6142578 Hz)
, Spectral Region, 1 sub-regions:
(1418704567.8222656 Hz, 1422423897.65625 Hz)
, Spectral Region, 1 sub-regions:
(1430148659.6191406 Hz, 1431579171.09375 Hz)
]
23:07:54.578 I EXCLUDING [Spectral Region, 1 sub-regions:
(1408147366.1396484 Hz, 1409577877.6142578 Hz)
, Spectral Region, 1 sub-regions:
(1418704540.8222656 Hz, 1422423870.65625 Hz)
, Spectral Region, 1 sub-regions:
(1430148632.6191406 Hz, 1431579144.09375 Hz)
]
23:07:54.687 I EXCLUDING [Spectral Region, 1 sub-regions:
(1408147340.1396484 Hz, 1409577851.6142578 Hz)
, Spectral Region, 1 sub-regions:
(1418704514.8222656 Hz, 1422423844.65625 Hz)
, Spectral Region, 1 sub-regions:
(1430148606.6191406 Hz, 1431579118.09375 Hz)
]
23:07:54.797 I EXCLUDING [Spectral Region, 1 sub-regions:
(1408147314.1396484 Hz, 1409577825.6142578 Hz)
, Spectral Region, 1 sub-regions:
(1418704488.8222656 Hz, 1422423818.65625 Hz)
, Spectral Region, 1 sub-regions:
(1430148580.6191406 Hz, 1431579092.09375 Hz)
]
23:07:54.907 I EXCLUDING [Spectral Region, 1 sub-regions:
(1408147288.1396484 Hz, 1409577799.6142578 Hz)
, Spectral Region, 1 sub-regions:
(1418704462.8222656 Hz, 1422423792.65625 Hz)
, Spectral Region, 1 sub-regions:
(1430148554.6191406 Hz, 1431579066.09375 Hz)
]
23:07:55.016 I EXCLUDING [Spectral Region, 1 sub-regions:
(1408147262.1396484 Hz, 1409577773.6142578 Hz)
, Spectral Region, 1 sub-regions:
(1418704436.8222656 Hz, 1422423766.65625 Hz)
, Spectral Region, 1 sub-regions:
(1430148528.6191406 Hz, 1431579040.09375 Hz)
]
23:07:55.126 I EXCLUDING [Spectral Region, 1 sub-regions:
(1408147235.1396484 Hz, 1409577746.6142578 Hz)
, Spectral Region, 1 sub-regions:
(1418704409.8222656 Hz, 1422423739.65625 Hz)
, Spectral Region, 1 sub-regions:
(1430148501.6191406 Hz, 1431579013.09375 Hz)
]
23:07:55.235 I EXCLUDING [Spectral Region, 1 sub-regions:
(1408147209.1396484 Hz, 1409577720.6142578 Hz)
, Spectral Region, 1 sub-regions:
(1418704383.8222656 Hz, 1422423713.65625 Hz)
, Spectral Region, 1 sub-regions:
(1430148475.6191406 Hz, 1431578987.09375 Hz)
]
23:07:55.345 I EXCLUDING [Spectral Region, 1 sub-regions:
(1408147183.1396484 Hz, 1409577694.6142578 Hz)
, Spectral Region, 1 sub-regions:
(1418704357.8222656 Hz, 1422423687.65625 Hz)
, Spectral Region, 1 sub-regions:
(1430148449.6191406 Hz, 1431578961.09375 Hz)
]
23:07:55.454 I EXCLUDING [Spectral Region, 1 sub-regions:
(1408147157.1396484 Hz, 1409577668.6142578 Hz)
, Spectral Region, 1 sub-regions:
(1418704331.8222656 Hz, 1422423661.65625 Hz)
, Spectral Region, 1 sub-regions:
(1430148423.6191406 Hz, 1431578935.09375 Hz)
]
Plot the middle integration of the middle scan.
spec_bsub_int = sb0[len(sb0)//2].getspec(nint//2)
sp_spec_bsub_int = spec_bsub_int.plot()
Now the continuum has been removed.
We leave it as an exercise to repeat the above for every scan in the OTF map. The answer is hidden below.
Answer
for i,_s in enumerate(sb0):
for j,_c in enumerate(_s._calibrated):
if math.isnan(_c.data.sum()):
# If the sum is NaN, then skip (continue) this item.
continue
s_i = getspec(j)
s_i.baseline(1, model="poly", exclude=exclude, remove=True)
_s._calibrated[j] -= s_i.baseline_model(s_i.spectral_axis).value
This can be speed up if we only create a Spectrum once, and then update its data attribute with the data for each integration before computing the baseline. That would be:
sp0 = sb0.timeaverage()
for i,_s in enumerate(sb0):
for j,_c in enumerate(_s._calibrated):
if math.isnan(_c.data.sum()):
# If the sum is NaN, then skip (continue) this item.
continue
sp0._data = _s._calibrated[j] # Update the data of the `Spectrum`.
sp0.baseline(1, model="poly", exclude=exclude, remove=True)
_s._calibrated[j] -= sp0.baseline_model(sp0.spectral_axis).value
Writing the Data#
After calibration, we write the data to disk in SDFITS format so it can be gridded by the gbtgridder (GBO’s supported data gridding tool).
sb0.write(output_dir / "otf_calibrated.fits", overwrite=True)
Gridding#
To grid GBT data GBO supports the use of the gbtgridder.
This is not included as part of dysh.
If you don’t have it installed, here’s a super short blurb how to get it, with the note that it is important to get the correct release branch at the time of this writing.
git clone -b release_3.0 https://github.com/GreenBankObservatory/gbtgridder
cd gbtgridder
pip install .
This was the situation in the summer of 2025, and it may change, be sure to be in touch with the gbtgridder developers.
After installation, either from the shell, or from the notebook, one can grid as follows:
gbtgridder --size 32 32 --channels 500:3500 -o otf --auto otf_calibrated.fits
This is telling the gridder to produce an output cube with 32 by 32 pixels using only channels between 500 and 3500, and to use “otf” as the name of the output files. The --auto part is to skip a confirmation prompt. The last argument, otf_calibrated.fits, is the input SDFITS (which was created in the previous cell).
This call to the gbtgridder will create two files: otf_cube.fits and otf_weight.fits.
The first file contains the gridded data, and the second the weights used during the gridding process.
Working with Cubes#
dysh is not meant to work with image cubes, there are plenty of great tools for this.
Here we show how to use astropy to load the FITS cube and visualize its contents.
We use astropy.io.fits to load the data, astropy.wcs.WCS to generate a World Coordinate System (WCS) representation out of the FITS header, this is handy for plotting.
And, matplotlib.pyplot to plot.
from astropy.io import fits
from astropy.wcs import WCS
import matplotlib.pyplot as plt
Before proceeding we download the cube, in case you did not make your own.
Note that this cube was made using data which had the baseline subtracted using a per integration model. If you generate your own cubes, there might be differences with respect to the cube used here.
cube_filename = dysh_data(example='mapping-L/outputs/otf_cube.fits')
23:07:56.173 I url: http://www.gb.nrao.edu/dysh//example_data/mapping-L/outputs/otf_cube.fits
Downloading otf_cube.fits from http://www.gb.nrao.edu/dysh//example_data/mapping-L/outputs/otf_cube.fits
23:07:56.420 I Starting download...
23:07:57.243 I Saved otf_cube.fits to otf_cube.fits
Retrieved otf_cube.fits
Open the FITS cube, extract the data and header and then create a WCS object.
with fits.open(cube_filename) as hdu:
data = hdu[0].data
head = hdu[0].header
wcs = WCS(head)
WARNING: FITSFixedWarning: 'datfix' made the change 'Set MJD-OBS to 57890.224583 from DATE-OBS'. [astropy.wcs.wcs]
The data object is an array with 4 dimensions, the first being the Stokes axis, the second the spectral axis, the third the latitude (e.g., Dec), and the fourth the longitude (e.g., RA). Altough the FITS cube has a Stokes axis, the gbtgridder does not handle polarizations properly, so if you have multiple polarizations in the input SDFITS file(s), the gbtgridder will average them.
To plot the spectrum from the center pixel we use.
plt.figure()
plt.plot(data[0,:,16,16])
plt.show()
/home/docs/checkouts/readthedocs.org/user_builds/dysh/envs/release-1.0.0/lib/python3.10/site-packages/traitlets/traitlets.py:1385: DeprecationWarning: Passing unrecognized arguments to super(Toolbar).__init__().
NavigationToolbar2WebAgg.__init__() missing 1 required positional argument: 'canvas'
This is deprecated in traitlets 4.2.This error will be raised in a future release of traitlets.
warn(
Now plot an image with the mean of the spectral axis.
We use the projection argument of fig.add_subplot to tell matplotlib to use the projection defined by the WCS object.
This takes care of using sky coordinates in the figure.
While plotting, imshow, we tell matplotlib to put the origin of the data, pixel (0,0), in the lower left corner (origin="lower"), and to automatically adjust the aspect ratio (aspect="auto") for the axes (not really needed since the data is already a square).
fig = plt.figure(dpi=150)
ax = fig.add_subplot(111, projection=wcs.celestial)
ax.imshow(data[0].mean(axis=0), origin="lower", aspect="auto")
plt.show()
Plot the data for channel 1500.
We use the celestial representation of the WCS object (wcs.celestial) to ignore the non celestial axes (e.g., Stokes and spectral).
fig = plt.figure(dpi=150)
ax = fig.add_subplot(111, projection=wcs.celestial)
ax.imshow(data[0,1500], origin="lower", aspect="auto")
plt.show()
/home/docs/checkouts/readthedocs.org/user_builds/dysh/envs/release-1.0.0/lib/python3.10/site-packages/traitlets/traitlets.py:1385: DeprecationWarning: Passing unrecognized arguments to super(Toolbar).__init__().
NavigationToolbar2WebAgg.__init__() missing 1 required positional argument: 'canvas'
This is deprecated in traitlets 4.2.This error will be raised in a future release of traitlets.
warn(
Plot a PV diagram.
Since we have multiple WCS axes (4), we use the slices argument, which tells the WCS object which axes to use for which axis. In this case the first WCS axis (longitude) goes in the y-axis of the figure, the latitude is fixed to its value at pixel 16, the third dimension (spectral axis) is shown in the x-axis, and the last dimensions (Stokes) is fixed to its value at pixel 0 (the only possibility in this case).
See these links for more details on how to use astropy for plotting cubes with multiple WCS axes: link1, and link2.
fig = plt.figure(dpi=150)
ax = fig.add_subplot(111, projection=wcs, slices=('y', 16, 'x', 0))
ax.imshow(data[0,:,:,16].T, origin="lower", aspect="auto")
plt.show()
/home/docs/checkouts/readthedocs.org/user_builds/dysh/envs/release-1.0.0/lib/python3.10/site-packages/traitlets/traitlets.py:1385: DeprecationWarning: Passing unrecognized arguments to super(Toolbar).__init__().
NavigationToolbar2WebAgg.__init__() missing 1 required positional argument: 'canvas'
This is deprecated in traitlets 4.2.This error will be raised in a future release of traitlets.
warn(
Final Stats#
Finally, at the end we compute some statistics over a spectrum, merely as a checksum if the notebook is reproducible.
spec_bsub_int.check_stats(0.596340)
23:07:58.132 W Found rms=0.5962464383824206, but expected 0.59634.
spec_bsub_int[250:1750].radiometer() # 1.133795557361736
23:07:58.193 I Note: found 1 NaN (masked) values
np.float64(1.1339743703709932)