Coordinates, beams, calibration, FITs files, and astropy

In [1]:
from IPython.display import Image
from IPython.terminal import embed
from IPython.core.display import display, HTML, Latex
from astropy.coordinates import EarthLocation, Distance, SkyCoord
from astropy import units as u
nch = EarthLocation(lat="37.8732", lon="-122.2573", height=123.1*u.m)
%matplotlib notebook
In [2]:
'''load laundry list of modules from <libradiomods.py>'''
import libradiomods
modules = libradiomods.get_modules()
for module in modules:
    try:
        exec(module)
    except ImportError:
        print("Error ", module)
        continue

'''load functions from <libradiofns.py>'''
from libradiofns import *
Modules have been exported.
Functions have been loaded.

First all the of raw data will be imported. This is often called a "measurement set". There is often weather information, seeing conditions, and space weather notes included. These will be appended in an ad-hoc way to reconstruct a more robust assessment of the observations.

In [3]:
crab_nebula = np.load('crab_6hrs_3-31-2020.npz')['arr_0']
orion_nebula = np.load('orion_3hrs_4-3-2020.npz')['arr_0']
virgo = np.load('virgoA_9.5hrs_4-2-2020.npz')['arr_0']
cassiopeia = np.load('casA_24hrs_4-5-2020.npz')['arr_0']
cygnus = np.load('cygA_17hrs_4-5-2020.npz')['arr_0']
targets = ['crab_nebula', 'orion_nebula', 'virgo', 'cassiopeia', 'cygnus']
observations = { targets[i]:globals()[targets[i]] for i in range(len(targets)) }

The FITs format is probably the most common and well supported ways to package observations. It allows for arbitrarily large images in both size (pixels) and in terms of per pixel encoding. Most computers are not able to show more than 24 bit colors with R, G, and B channels each having $2^8 = 256$ bits. Images in industry and the science can require "stretching" before being rendered. This is basically the same as using

$$ \log{P_\nu} $$

to see a power spectrum except with other options. Having an image calibrated in terms of location can also be important if it is going to be composed with an other observation. Plus if you want to show some scale or astronomical coordinates then it will be useful to have the image wrapped with the important parameters needed to transform from one coordinate system to another.

The specifications of FITs files are available here: {https://fits.gsfc.nasa.gov/fits_standard.html}

In [4]:
# use astropy to get object coordinates
nch = EarthLocation(lat="37.8732", lon="-122.2573", height=123.1*u.m)
crab_nebula_wcs = SkyCoord.from_name("crab nebula")
orion_nebula_wcs = SkyCoord.from_name("orion nebula")
virgo_wcs = SkyCoord.from_name("virgo A")
cassiopeia_wcs = SkyCoord.from_name("cassiopeia A")
cygnus_wcs = SkyCoord.from_name("cygnus A")
wcsnames = [ "".join([targets[i], "_wcs"]) for i in range(len(targets)) ]
skycoordinates = { targets[i]:globals()[wcsnames[i]] for i in range(len(targets)) }

The methods above will get coordinates on the celestial sphere but will not provide any information about distance. These objects have similar radio fluxes, with varying spectral energy distributions, however two of them are extragalactic. The best way to calibrate the telescope is with a point source because it effectively normalizes the range of beams and sensitivities. If there is an established measurement of the absolute flux from a source then that value can be used as a fixed variable in solving for the gain of a telescope. The more often this is done the more checks you will have to adjust the pipeline to get accurate measures of error, accuracy, and precision.*

*A good reference for this is J. Baars, “History of Flux-Density Calibration in Radio Astronomy,” The Radio Science Bulletin, vol. March, no. 348, pp. 47-66, (2014). These sources can vary in time so just as the coordinates of the telescope are updated so should the calibration factors.

In [5]:
# import standard catalog distances manually
crab_nebula_z = (6500*u.lyr).to(u.pc); crab_nebula_z = Distance(crab_nebula_z)
orion_nebula_z = (1344*u.lyr).to(u.pc); orion_nebula_z = Distance(orion_nebula_z) 
virgo_z = (5.35e7*u.lyr).to(u.pc); virgo_z = Distance(virgo_z) 
cassiopeia_z = (11090*u.lyr).to(u.pc); cassiopeia_z = Distance(cassiopeia_z) 
cygnus_z = (6e8*u.lyr).to(u.pc); cygnus_z = Distance(cygnus_z) 
targets_z = [ "".join([targets[i], "_z"]) for i in range(len(targets))]
z_lookup = { targets[i]:globals()[f'{targets_z[i]}'] for i in range(len(targets)) }

Interf beam

One of the important measures is the beam size of the telescope. For single dish radio telescopes the "full width half maximum" is typically used as a proxy for this. The Rayleigh criterion is used

$$ \theta = \frac{\lambda}{2R} $$

to approximate this, with $\lambda$ being the wavelength of the band of interest and $R$ being the radius of the antenna/aperture. For two element interferometers $2R \rightarrow B$ and is the typically much larger length of the baseline. In this special case the result is more like the spacing of a fringe pattern than a beam. The resolution is increased, however the sensitivity is not. For larger arrays it is the same although the result is a synthesized beam usually shown as an elliptical stereographic projection. There is an astropy-related package for radio beams which can be used.

In [6]:
# constants (priors)
beam_fwhm = 3*u.degree # the larger beam of the antenae sets the field of view (FOV)
baseline = (20)*u.m # estimate the baseline
xband = 10.5e9*u.hertz # set the observation frequency/wavelength 
xband_wavelength = (xband).to(u.cm, equivalencies=u.spectral()) # get the right units, see astropy units
print(f'The X-band at {xband:1.2e} is equivalently {xband_wavelength:1.3f}.')
The X-band at 1.05e+10 Hz is equivalently 2.855 cm.
In [7]:
# get field of view
fwhm_to_sigma = 1. / (8 * np.log(2))**0.5 # this is a Gaussian-like factor to shape the beam
beam_sigma = beam_fwhm * fwhm_to_sigma # make the shaped beam
omega_B = 2 * np.pi * beam_sigma**2 # get the beam solid angle

# get resolution
interfres = (xband_wavelength/baseline).to(u.dimensionless_unscaled) # calculate interf resolution (radians)
interfres_degrees = (interfres*u.radian).to(u.degree) # convert to coordinate-like units
interfres_arcmin = (interfres*u.radian).to(u.arcmin) # convert to coordinate-like units
interfres_arcsec = (interfres*u.radian).to(u.arcsec) # convert to coordinate-like units

'''instantiate a Beam obejct'''
interfbeam = Beam(interfres_degrees)
In [8]:
print(f'The interf dishes have a beam size approximated by a FWHM of {beam_fwhm} and solid angle {omega_B:2.1f} [sr]')
print(f'The interf baseline corresponds to a synthetic beam size of {interfres_degrees:3.2e}')
print(f'The interf baseline corresponds to a synthetic beam size of {interfres_arcmin:3.2e}')
print(f'The interf baseline corresponds to a synthetic beam size of {interfres_arcsec:3.2e}')
The interf dishes have a beam size approximated by a FWHM of 3.0 deg and solid angle 10.2 deg2 [sr]
The interf baseline corresponds to a synthetic beam size of 8.18e-02 deg
The interf baseline corresponds to a synthetic beam size of 4.91e+00 arcmin
The interf baseline corresponds to a synthetic beam size of 2.94e+02 arcsec

The beam object now has functionality. It can be projected to sources, observations can be simulated, and archival files can be re-calibrated. The FITs format is good for packaging, however there are better data structures when using python like pandas dataframes. This provides a more flexible interface with other modules geared towards quantitative needs.

In [9]:
# print out the beam pattern area; what is the best way to size up extended radio sources?
# if the beam area is less than the object area how many beams fill the area?
for target in targets:
    print(f'For {target} @ {z_lookup[target]:1.2e} the interf beam covers {interfbeam.beam_projected_area(distance=z_lookup[target]):1.3e}')
For crab_nebula @ 1.99e+03 pc the interf beam covers 9.172e+00 pc2
For orion_nebula @ 4.12e+02 pc the interf beam covers 3.921e-01 pc2
For virgo @ 1.64e+07 pc the interf beam covers 6.213e+08 pc2
For cassiopeia @ 3.40e+03 pc the interf beam covers 2.670e+01 pc2
For cygnus @ 1.84e+08 pc the interf beam covers 7.815e+10 pc2

This step should include checking for other potential sources around the field of view of the beam. The reason is any additional flux entering into the main beam or the side lobes could reduce the fidelity of the observation so filtering or at least accounting for this is a good idea.

Construct FITs file headers for the observations

One of the most crucial parts of the FITs file is the header. This is were all the metadata is stored. The coordinates of the source, the name of the source, the telescope and so on. There is a standardized format for this detailed at the link given above and in the included PDF. A blank template for radio observations is also included and will be filled in below. astropy treats the header like a python dictionary. The FITs keywords are the keys and their values are the values in the dictionary. The header allows for interchange between formats hence "Flexible Image Transport System".

In [10]:
# import template file and get the header
template = fits.open('template.fits')
template_header = template[0].header # pick the first HDU in the HDU list
template_data = template[0].data
print(repr(template_header)) # repr() grabs the __repr__ of an object, the print representation
SIMPLE  =                    T /Standard FITS                                   
BITPIX  =                  -32 /Floating point (32 bit)                         
NAXIS   =                    3 / The number of axes in the data                 
NAXIS1  =                 1024 / Image size in pixels for x-axis                
NAXIS2  =                 1024 / Image size in pixels for y-axis                
NAXIS3  =                    1 / This is a frequency/velocity axis              
EXTEND  =                    T                                                  
BSCALE  =   1.000000000000E+00 /PHYSICAL = PIXEL*BSCALE + BZERO                 
BZERO   =   0.000000000000E+00                                                  
BTYPE   = 'Intensity'                                                           
OBJECT  = 'model   '           / This is a computer generated image             
BUNIT   = '"Data Value"'       /Brightness (pixel) unit                         
EQUINOX =   2.000000000000E+03                                                  
RADESYS = 'FK5     '                                                            
LONPOLE =   1.800000000000E+02                                                  
LATPOLE =   0.000000000000E+00                                                  
CTYPE1  = 'RA---SIN'                                                            
CRVAL1  =   0.000000000000E+00                                                  
CDELT1  =  -8.333333333333E-06                                                  
CRPIX1  =   5.125000000000E+02                                                  
CUNIT1  = 'deg     '                                                            
CTYPE2  = 'DEC--SIN'                                                            
CRVAL2  =   0.000000000000E+00                                                  
CDELT2  =   8.333333333333E-06                                                  
CRPIX2  =   5.125000000000E+02                                                  
CUNIT2  = 'deg     '                                                            
CTYPE3  = 'VOPT    '                                                            
CRVAL3  =  -1.421000000000E+04                                                  
CDELT3  =   1.999999999982E+03                                                  
CRPIX3  =   1.000000000000E+00                                                  
CUNIT3  = 'm/s     '                                                            
RESTFRQ =   1.000000000000E+00 / Rest Frequency (Hz)                            
SPECSYS = 'LSRK    '           /Spectral reference frame                        
VELREF  =                    1 /1 LSR, 2 HEL, 3 OBS, +256 Radio                 
DATE    = '2021-03-15T05:31:07.442000' /Date FITS file was written              
TIMESYS = 'UTC     '           /Time system for HDU                             
ORIGIN  = 'Miriad  '                                                            
HISTORY                                                                         
HISTORY                                                                         

To get the observations organized correctly, the data is first loaded into the primary header/data unit (HDU). For these observations the FITs files will be simply the primary HDU. There are other more complicated types of FITs files including "cubes" with a third axis. This is common with spectroscopic observations over range of frequencies.

In [11]:
# all the observation data is used to generate HDU objects
hdus = dict()
for target in targets:
    hdus[target] = fits.PrimaryHDU(observations[target])
    hdus[target].header = template_header
In [12]:
# the primary HDU is supposed to be in an HDU list so the last step is repeated
hdu_lists = dict()
for target in targets:
    hdu_lists[target] = fits.HDUList(hdus[target])

With the data ready all that is needed is to modify the headers. Generating headers is possible with astropy and makes sense to do as a script-generated object while observing. However, for building it up by hand like this it will be a little easier to use a template and change the values to match the observations. Editing FITs headers can be tedious and something like fv FITs viewer is probably the way to go.*

*https://heasarc.gsfc.nasa.gov/docs/software/ftools/fv/

This template example shows three axes: NAXIS1 is a pixel image x-axis, NAXIS2 is a pixel image y-axis, and NAXIS3 is a pixel image z-axis. The full file (this is actually an integrated version) is like a movie with the frames being the elements of the z-axis. The data from the interf observations should be similarly summarized. The units of the x-axis will be time series measurements, the y-axis a voltage reading from the measurement, and the z-axis can be removed to reduce clutter. The value of CDELT1 and CDELT2 should also be changed to reflect the increments along each axis. For the x-axis it could be the time between measurements and the y-axis would be the range divided by the bit depth of the sampler.

Before making the headers it would be good to grab all the weather information that is missing. There are tutorials on how to get this type of information with an API. There's an example implemented in libradiofns but you will have to get your own API Key if you want to use it.

*https://www.visualcrossing.com/resources/documentation/weather-api/weather-api-documentation/

After sending in a query the database will return a good amount of information. Astropy wants the pressure, temperature, and relative_humidity to make corrections. Noting cloud cover, wind speed, and precipitation might be good too so they can be included as notes.

In [13]:
# start by updating all the SkyCoord objects to get derivied values
# at the same time make a dictionary for the weather information
weather_info = dict()
for target in targets:
    observation_time = Time(hdus[target].data[1][0], format='unix')
    skycoordinates[target].obstime = observation_time
    skycoordinates[target].location = nch
    weather_info[target] = get_weather(observation_time.value, observation_time.value)
    pressure = float(weather_info[target]['Sea Level Pressure'][0])
    skycoordinates[target].pressure = (pressure*u.mbar).to(u.hPa)
    temperature = float(weather_info[target]['Temperature'])
    skycoordinates[target].temperature = f2C(temperature)*u.deg_C
    skycoordinates[target].relative_humidity = float(weather_info[target]['Relative Humidity'])
    skycoordinates[target].obswl = xband_wavelength.to(u.micron)
In [14]:
# the SkyCoord objects are all set and now provide alt/az info with atmospheric corrections
# these could be compared with the uncorrected values to get a more robust error
for target in targets:
    print(f'{target} is located in the constellation {skycoordinates[target].get_constellation()}')
    alt = skycoordinates[target].altaz.alt; az = skycoordinates[target].altaz.az;
    print(f'{target} had the following corrected alt/az coordinates {alt:3.1f} {az:3.1f}\n')
crab_nebula is located in the constellation Taurus
crab_nebula had the following corrected alt/az coordinates 69.5 deg 223.8 deg

orion_nebula is located in the constellation Orion
orion_nebula had the following corrected alt/az coordinates 28.4 deg 236.3 deg

virgo is located in the constellation Virgo
virgo had the following corrected alt/az coordinates 13.3 deg 84.6 deg

cassiopeia is located in the constellation Cassiopeia
cassiopeia had the following corrected alt/az coordinates 37.4 deg 320.5 deg

cygnus is located in the constellation Cygnus
cygnus had the following corrected alt/az coordinates 6.0 deg 41.2 deg

In [15]:
# the weather data now stored as pandas data frames which can be displayed
for target in targets:
    display(weather_info[target])
Address Date time Minimum Temperature Maximum Temperature Temperature Dew Point Relative Humidity Heat Index Wind Speed Wind Gust ... Visibility Cloud Cover Sea Level Pressure Weather Type Latitude Longitude Resolved Address Name Info Conditions
0 37.8732,-122.25730000000001 04/01/2020 48.7 63.3 55.7 40.4 57.87 13.6 21.9 ... 9.9 15.0 1015.8 37.8732 -122.25730000000001 37.8732,-122.25730000000001 37.8732,-122.25730000000001 Clear

1 rows × 25 columns

Address Date time Minimum Temperature Maximum Temperature Temperature Dew Point Relative Humidity Heat Index Wind Speed Wind Gust ... Visibility Cloud Cover Sea Level Pressure Weather Type Latitude Longitude Resolved Address Name Info Conditions
0 37.8732,-122.25730000000001 04/04/2020 50.4 59.4 54.1 46.5 75.85 17.7 23.7 ... 8.7 69.8 1011.5 Mist, Rain, Heavy Rain, Light Rain 37.8732 -122.25730000000001 37.8732,-122.25730000000001 37.8732,-122.25730000000001 Rain, Partially cloudy

1 rows × 25 columns

Address Date time Minimum Temperature Maximum Temperature Temperature Dew Point Relative Humidity Heat Index Wind Speed Wind Gust ... Visibility Cloud Cover Sea Level Pressure Weather Type Latitude Longitude Resolved Address Name Info Conditions
0 37.8732,-122.25730000000001 04/03/2020 44.9 63.5 53.9 39.2 59.49 14.3 21.9 ... 9.9 16.7 1017.3 37.8732 -122.25730000000001 37.8732,-122.25730000000001 37.8732,-122.25730000000001 Clear

1 rows × 25 columns

Address Date time Minimum Temperature Maximum Temperature Temperature Dew Point Relative Humidity Heat Index Wind Speed Wind Gust ... Visibility Cloud Cover Sea Level Pressure Weather Type Latitude Longitude Resolved Address Name Info Conditions
0 37.8732,-122.25730000000001 04/05/2020 51.0 55.1 53.8 46.1 75.55 16.8 25.3 ... 8.6 68.6 1004.9 Mist, Light Drizzle, Rain, Heavy Rain, Light R... 37.8732 -122.25730000000001 37.8732,-122.25730000000001 37.8732,-122.25730000000001 Rain, Partially cloudy

1 rows × 25 columns

Address Date time Minimum Temperature Maximum Temperature Temperature Dew Point Relative Humidity Heat Index Wind Speed Wind Gust ... Visibility Cloud Cover Sea Level Pressure Weather Type Latitude Longitude Resolved Address Name Info Conditions
0 37.8732,-122.25730000000001 04/05/2020 51.0 55.1 53.8 46.1 75.55 16.8 25.3 ... 8.6 68.6 1004.9 Mist, Light Drizzle, Rain, Heavy Rain, Light R... 37.8732 -122.25730000000001 37.8732,-122.25730000000001 37.8732,-122.25730000000001 Rain, Partially cloudy

1 rows × 25 columns

The weather information can be written into a second HDU (with its own header) of the FITs files and the rest can go into the more-or-less typical locations. The Beam object from above will be the same for all of them although the projection would vary the geometry. If there is a way to convert the data into an image then new FITs files can be made with WCS axes to allow accurate positions when plotting.*

*https://docs.astropy.org/en/stable/wcs/index.html

The HDU lists need to be updated and the weather information can be appended before writing each HDU list to separate FITs files. Thankfully there are a few built-in methods that make going from pandas to FITs not too tortuous.

In [18]:
# the number of time series measurements is the length of the second array
# in the .data extension of each HDU the voltage readings are uncalibrated
# so the value can be set to the number of mesurements but the increment
# should be set to uncalibrated; comments are changed by passing tuples
for target in targets:
    header = hdu_lists[target][0].header
    header['OBJECT'] = (target, 'Point source calibrator for interf')
    header['OBSERVER'] = 'spacewaves'
    header['TELESCOP'] = 'interf'
    header['INTRUME'] = 'HP Multimeter'
    header['BTYPE'] = ('Uncalibrated Intensity', 'These should be set once calibrated')
    header['BSCALE'] = ('Uncalibrated')
    header['BUNIT'] = ('Uncalibated', 'Needs to be calibrated into Jy/beam')
    header['BMAJ'] = (interfbeam.major.value, 'Major axis of beam ellipse [degrees]')
    header['BMIN'] = (interfbeam.minor.value, 'Minor axis of beam ellipse [degrees]')
    header['BPA'] = (interfbeam.beam_projected_area(z_lookup[target]).value, 'Beam projected area [pc^2]')
    header['RESTFRQ'] = (xband.value/1e9, 'GHz +- 20 MHz')
    header['DATE-OBS'] = (ugradio.timing.utc(hdus[target].data[1][0], '%Y-%m-%d'), 'UTC')
    header['NAXIS'] = (3, 'Number of axes in the data')
    header['NAXIS1'] = (hdus[target].data[1].shape[0], 'Unix time of time series measurements')
    header['NAXIS2'] = (hdus[target].data[0].shape[0], 'Voltage readings (time series)')
    header['CTYPE1'] = ('s', 'Seconds with unix epoch offset')
    header['CTYPE2'] = ('voltage', 'Uncalibrated')
    header['CUNIT1'] = ('s', 'Seconds with unix epoch offset')
    header['CUNIT2'] = ('voltage', 'Needs to be calibrated into Jy/beam')
    header['CDELT1'] = (.2, 'HP mm @ dt = 0.1 -> sampling slower/varied')
    header['CDELT2'] = (1, 'Needs to be calibrated into Jy/beam')
    header['CRVAL1'] = (hdus[target].data[1][0], 'Unix time at start')
    header['CRVAL2'] = (0, 'voltage should be zero without source')
    header['CRPIX1'] = (0, 'Reference pixel coordinates')
    header['CRPIX2'] = (0, 'Reference pixel coordinates')
    header['OBSRA'] = (skycoordinates[target].ra.value, 'Right ascension [degrees]') 
    header['OBSDEC'] = (skycoordinates[target].dec.value, 'Declination [degrees]')
    header['OBSALT'] = (skycoordinates[target].altaz.alt.value, 'Altitude [degrees]')
    header['OBSAZ'] = (skycoordinates[target].altaz.az.value, 'Azimuth [degrees]')
    v_radial_correction = skycoordinates[targets[0]].radial_velocity_correction().to(u.km/u.s).value
    header['VRAD'] = (v_radial_correction, 'km/s add this value to observered v_radial') 
    header['OBSLAT'] = nch.lat.value
    header['OBSLON'] = nch.lon.value
    header['NAXIS3'] = (1, 'Could add proper motion of object')
    header['CTYPE3'] = ('parsecs', f'Distance to {target} in pc')
    header['CRVAL3'] = (z_lookup[target].value, 'Distance to {target} in pc')
    header['CRPIX3'] = (0, 'Reference pixel coordinates')
    header['CONSTEL'] = (skycoordinates[target].get_constellation(), 'Constellation')
    header['CDELT3'] = (1, 'Could add proper motion of object')
    header['CUNIT3'] = ('parsecs', f'Distance to {target} in pc')
    header['DATE'] = (ugradio.timing.local_time(), 'UTC')
    header['ORIGIN'] = ('RADIOLAB', 'generated with astropy')
    start = hdus[target].data[1][0]; stop = hdus[target].data[1][-1]
    header['ELAPTIME'] = f'total observation time was {(stop-start)/3600} hours'
    header['HISTORY'] = 'weather information added with table in second HDU'
    table = astropy.table.Table.from_pandas(weather_info[target])
    hdu_lists[target].append(fits.table_to_hdu(table))
    hdu_lists[target].writeto(f'{target}.fits')
In [19]:
crab_nebula_fits = fits.open('crab_nebula.fits')
print(repr(crab_nebula_fits[0].header))
SIMPLE  =                    T /Standard FITS                                   
BITPIX  =                  -64 / Floating point (32 bit)                        
NAXIS   =                    2 / Number of axes in the data                     
NAXIS1  =               134260 / Unix time of time series measurements          
NAXIS2  =                    2 / Voltage readings (time series)                 
EXTEND  =                    T                                                  
BSCALE  = 'Uncalibrated'       / PHYSICAL = PIXEL*BSCALE + BZERO                
BZERO   =   0.000000000000E+00                                                  
BTYPE   = 'Uncalibrated Intensity' / These should be set once calibrated        
OBJECT  = 'crab_nebula'        / Point source calibrator for interf             
BUNIT   = 'Uncalibated'        / Needs to be calibrated into Jy/beam            
EQUINOX =   2.000000000000E+03                                                  
RADESYS = 'FK5     '                                                            
LONPOLE =   1.800000000000E+02                                                  
LATPOLE =   0.000000000000E+00                                                  
CTYPE1  = 's       '           / Seconds with unix epoch offset                 
CRVAL1  =     1585706753.12939 / Unix time at start                             
CDELT1  =                  0.2 / HP mm @ dt = 0.1 -> sampling slower/varied     
CRPIX1  =                    0 / Reference pixel coordinates                    
CUNIT1  = 's       '           / Seconds with unix epoch offset                 
CTYPE2  = 'voltage '           / Uncalibrated                                   
CRVAL2  =                    0 / voltage should be zero without source          
CDELT2  =                    1 / Needs to be calibrated into Jy/beam            
CRPIX2  =                    0 / Reference pixel coordinates                    
CUNIT2  = 'voltage '           / Needs to be calibrated into Jy/beam            
CTYPE3  = 'parsecs '           / Distance to crab_nebula in pc                  
CRVAL3  =    1992.909059606079 / Distance to {target} in pc                     
CDELT3  =                    1 / Could add proper motion of object              
CRPIX3  =                    0 / Reference pixel coordinates                    
CUNIT3  = 'parsecs '           / Distance to crab_nebula in pc                  
RESTFRQ =                 10.5 / GHz +- 20 MHz                                  
SPECSYS = 'LSRK    '           /Spectral reference frame                        
VELREF  =                    1 /1 LSR, 2 HEL, 3 OBS, +256 Radio                 
DATE    = 'Sat Mar 27 21:30:25 2021' / UTC                                      
TIMESYS = 'UTC     '           /Time system for HDU                             
ORIGIN  = 'RADIOLAB'           / generated with astropy                         
OBSERVER= 'spacewaves'                                                          
TELESCOP= 'interf  '                                                            
INTRUME = 'HP Multimeter'                                                       
BMAJ    =  0.08179448844406187 / Major axis of beam ellipse [degrees]           
BMIN    =  0.08179448844406187 / Minor axis of beam ellipse [degrees]           
BPA     =    9.171538628987124 / Beam projected area [pc^2]                     
DATE-OBS= '2020-04-01'         / UTC                                            
OBSRA   =    83.63308333000001 / Right ascension [degrees]                      
OBSDEC  =              22.0145 / Declination [degrees]                          
OBSALT  =    69.47323544025194 / Altitude [degrees]                             
OBSAZ   =    223.7895994105853 / Azimuth [degrees]                              
VRAD    =   -28.66057305515247 / km/s add this value to observered v_radial     
OBSLAT  =              37.8732                                                  
OBSLON  =            -122.2573                                                  
CONSTEL = 'Taurus  '           / Constellation                                  
ELAPTIME= 'total observation time was 11.670799326631759 hours'                 
HISTORY                                                                         
HISTORY                                                                         
HISTORY weather information added with table in second HDU                      
HISTORY weather information added with table in second HDU                      
HISTORY weather information added with table in second HDU                      
HISTORY weather information added with table in second HDU                      

Calibrating these observations is the next step before imaging/plotting