How analyze individual spectral lines#

Many of the tools in SpecpolFlow can be applied to a single line in an observed spectrum. In this example we analyze a few emission lines in the spectrum of a T Tauri star.

First, import SpecpolFlow and some packages that will help with data visualization.

import specpolFlow as pol

import pandas as pd
import matplotlib.pyplot as plt

1. Create individual line file#

In this tutorial, we use the UPENA normalized spectrum (IndividualLine_tutorialfiles/1423137pn.s) of the active cool star BP Tau.

We first load in the spectrum.

spec = pol.read_spectrum("IndividualLine_tutorialfiles/1423137pn.s")

To create extract an individual line from the spectrum, we have to define the transition wavelength lambda0 and a range with respect to the line center. In the example below, we select the HeI line at 667.815 nm in a 0.6 nm window.

# Create individual line profile 
lambda0 = 667.815 # He i 667.815 nm
geff = 1.0 # He i 667.815 nm, used later for calculating Bz
lwidth = 0.3 

prof = spec.individual_line(lambda0 = lambda0, lwidth = lwidth)

The individual_line function returns prof, which is really an LSD profile object, and inherits all the attributes from the LSD class. Therefore, you can use all the functionality discussed in the LSD Class Tutorial (i.e., one can normalize, slice, or shift the line profile).

Below, we visualize the individual line using the plot class function.

fig, axes = prof.plot()
axes[-1].axhline(1, color ='k', ls = '--')
<matplotlib.lines.Line2D at 0x7f040b391a50>
../_images/e84639e63fe5f49e7ed2f0071212608451d30583a85d2366fe73a9a33e1a1f68.png

2. Calculate Bz#

To calculate the longitudinal field (Bz) of the individual line, we can use the calc_bz function of the LSD class.

# Define the limits for the continuum normalization
cmin = -50.0; cmax = 80.0

# Compute Bz in a 45 km/s window
Bz, fig = prof.calc_bz(norm = 'auto', cog = 'I', 
                   velrange = [cmin, cmax], bzwidth = 45.0, 
                   geff = geff, lambda0 = lambda0,
                   plot = True)
using AUTO method for the normalization
  using the median of the continuum outside of the line
../_images/39b8b7b8e46a2949144e83636e9cac0e360d106f83a01a1b41ccec6a2bd0e06c.png

We can then display the results using the pandas dataframe. For this particular line, we find Bz \(= 1.2 \pm 0.1\) kG.

Bz_df = pd.DataFrame(data = [Bz])

# Simple display of the pandas dataframe
Bz_df.style
  V bz (G) V bz sig (G) V FAP N1 bz (G) N1 bz sig (G) N1 FAP N2 bz (G) N2 bz sig (G) N2 FAP norm_method norm cog_method cog int. range start int. range end
0 1191.652271 130.184898 0.000000 -34.686996 130.041356 0.564259 0.000000 0.000000 0.000000 auto 0.931285 I 21.173209 -23.826791 66.173209

3. Normalize the individual line#

In the figure above, we see that calc_bz improved the continuum normalization, taking the median of the continuum outside of the line. Therefore, we propagate this normalization in the prof object before saving the individual line profile into a file.

# Renormalize prof using the continuum value obtained by calc_bz
prof_norm = prof.norm(Bz['norm'])

# Display the profile
fig, axes = prof_norm.plot()
axes[-1].axhline(1, color = 'k', ls = '--')
<matplotlib.lines.Line2D at 0x7f0401fed2a0>
../_images/5e9a25e14bd76cd2db59d4a9f472e9d1aaa5b337e711756c84395f9bcd416a95.png

4. Saving individual lines#

Finally, you can save the individual line into a file using the save function, shown below. This file will have a text format like an LSD profile.

prof_norm.save("Output/1423137.Helium")

5. Dealing with order overlaps#

Now, let’s do the same for the Halpha line. However, there is a complication, the Halpha line is split across two orders in this echelle spectrum.

# reading the spectrum
spec = pol.read_spectrum("IndividualLine_tutorialfiles/1423137pn.s")
# defining the line parameters
lambda0 = 656.281 ;  geff = 1.0 # Halpha
lwidth = 700. * lambda0 / 2.99e5 #convert a 700 km/s width into a width in nm
# getting the profile for that line
prof = spec.individual_line(lambda0 = lambda0, lwidth = lwidth)

# Define the limits for the continuum normalization
# Compute Bz in a 200 km/s window around the cog
cogRange = [-400., 400.]
intRange = 200.
Bz, fig = prof.calc_bz(norm = 'auto', cog = 'I', 
                       velrange = cogRange, bzwidth = intRange, 
                       geff = geff, lambda0 = lambda0,
                       plot = True)   

# Create a dataframe to record the result of the different methods in this section
d = {'Merge type':'None'}
d.update(Bz)
df_results = pd.DataFrame(data = [d])
df_results.style
using AUTO method for the normalization
  using the median of the continuum outside of the line
/tmp/ipykernel_980312/871814974.py:13: UserWarning: 
 The velocity array is not monotonically increasing. 
 There might be an order overlap in the region of the observation used. 
 calc_bz will sort the LSD profile in velocity order. 
 Make sure this is what you want -- merge orders before running calc_bz()!
  Bz, fig = prof.calc_bz(norm = 'auto', cog = 'I',
  Merge type V bz (G) V bz sig (G) V FAP N1 bz (G) N1 bz sig (G) N1 FAP N2 bz (G) N2 bz sig (G) N2 FAP norm_method norm cog_method cog int. range start int. range end
0 None 72.964936 17.843167 0.000000 7.405907 17.843162 0.435065 0.000000 0.000000 0.000000 auto 1.101100 I 9.725353 -190.274647 209.725353
../_images/901b692eee281a9375045084a07ccd60b06222bdb5a56ddc0b9ddf537abf210b.png

The calc_bz function gave us a warning about there probably being different spectral orders here. And the plot of results is relatively messy.

5.1 Option 1: Selecting only one order#

The simplest solution to this problem is to only use one spectral order for the Bz calculation. In the case of this Halpha line, both orders include the center of the line that we are interested in. So we can calculate Bz from the two orders separately.

To get the spectral orders that include a wavelength, or range of wavelengths, we can use the get_orders_in_range function of the Spectrum class. This returns a list containing Spectrum objects for each order.

## Selecting only one order
orders = spec.get_orders_in_range(lambda0)
print('There are {} orders with Halpha in them'.format(len(orders)))

# Illustration of the order overlap
fig, ax = plt.subplots(1,1)
ax.plot(orders[0].wl, orders[0].specI, label = 'Lower order')
ax.plot(orders[1].wl, orders[1].specI, label = 'Higher order')
ax.set_xlabel('Wavelength (nm)')
ax.set_ylabel('Stokes I / Ic')
ax.legend(loc = 0)

# loop over the orders
type = ['lower only', 'higher only']
i = 0
for ord in orders:

    # get the profile for the line in this order
    prof = ord.individual_line(lambda0 = lambda0, lwidth = lwidth)

    # compute Bz, in the defined velocity window around the cog
    Bz, fig = prof.calc_bz(norm = 'auto', cog = 'I', 
                           velrange = cogRange, bzwidth = intRange, 
                           geff = geff, lambda0 = lambda0,
                           plot = True)
    d = {'Merge type':type[i]}
    d.update(Bz)
    df = pd.DataFrame(data=[d])
    df_results = pd.concat([df_results,df], ignore_index=True)
    i += 1

# display the Bz result as a dataframe
df_results.style
There are 2 orders with Halpha in them
using AUTO method for the normalization
  using the median of the continuum outside of the line
using AUTO method for the normalization
  using the median of the continuum outside of the line
  Merge type V bz (G) V bz sig (G) V FAP N1 bz (G) N1 bz sig (G) N1 FAP N2 bz (G) N2 bz sig (G) N2 FAP norm_method norm cog_method cog int. range start int. range end
0 None 72.964936 17.843167 0.000000 7.405907 17.843162 0.435065 0.000000 0.000000 0.000000 auto 1.101100 I 9.725353 -190.274647 209.725353
1 lower only 94.012588 25.428151 0.000003 5.209152 25.428140 0.221315 0.000000 0.000000 0.000000 auto 1.104800 I 14.197130 -185.802870 214.197130
2 higher only 54.213318 21.662775 0.000007 3.435552 21.662771 0.803308 0.000000 0.000000 0.000000 auto 1.096300 I 13.943177 -186.056823 213.943177
../_images/e9da5b37b109527ac245eb2331fdabc3d7f1723d878bf7e6a27412929af1ff21.png ../_images/1d5e8d76e99e3b61f3e15c8861984a8a3b11113ffbdf4908df0d1027690e8b51.png ../_images/aca4e57b31acea3a0b9f544dc099339dc7fa0e890ef3fa72cd659e2485f0259f.png

5.2 Option 2: merging the orders#

An alternative solution is to merge the spectral orders before calculating Bz. Combining orders is more complex, but it can optimize S/N, and it can be useful when the wavelength range of interest extends beyond the edge of an order.

We can combine spectral orders with the merge_orders function of the Spectrum class. This function provides two methods of combining orders 'trim' and 'coadd'. This function only really works properly if the continuum levels are consistent between orders.

In merge_orders with mode = 'trim', the function just uses the first order up to the midpoint of the region where the orders overlap, then it uses the second order beyond that midpoint. This approach is relatively simple and essentially throws away a bit of signal. But it is more robust against errors in continuum normalization (or spectrum extraction) at the edges of spectral orders, and these errors are relatively common.

In merge_orders with mode = 'coadd', the function essentially averages the orders, weighted by the uncertainties on individual pixels (specifically by \(1/\sigma^2\)). To do this it needs to interpolate in wavelength, and it uses the wavelengths from the first order, interpolating the second order onto that grid. This mode requires reliable uncertainties for the Stokes I spectrum. This approach has the advantage that it optimizes the S/N in the merged spectrum. But it is more vulnerable to continuum normalization errors at the edges of orders, since an error in either order will cause problems for the merged result.

Here we try out both methods to compare the results. In this case the normalization is pretty good in the region we care about, so the 'coadd' mode gives a more precise result.

## Merging using the trim method
merge = spec.merge_orders(mode = 'trim', midpoint = 0.5)

prof = merge.individual_line(lambda0 = lambda0, lwidth = lwidth)
Bz, fig = prof.calc_bz(norm = 'auto', cog = 'I', 
                       velrange = cogRange, bzwidth = intRange, 
                       geff = geff, lambda0 = lambda0,
                       plot = True)
d = {'Merge type':'trim at mid-point'}
d.update(Bz)
df = pd.DataFrame(data = [d])
df_results = pd.concat([df_results,df], ignore_index=True)


## Merging using the coadd method
merge = spec.merge_orders(mode='coadd')

prof = merge.individual_line(lambda0 = lambda0, lwidth = lwidth)
Bz, fig = prof.calc_bz(norm = 'auto', cog = 'I', 
                       velrange = cogRange, bzwidth = intRange, 
                       geff = geff, lambda0 = lambda0,
                       plot = True)
d = {'Merge type':'coadd'}
d.update(Bz)
df = pd.DataFrame(data = [d])
df_results = pd.concat([df_results,df], ignore_index = True)

# Display the Bz result as a dataframe
df_results.style
using AUTO method for the normalization
  using the median of the continuum outside of the line
using AUTO method for the normalization
  using the median of the continuum outside of the line
  Merge type V bz (G) V bz sig (G) V FAP N1 bz (G) N1 bz sig (G) N1 FAP N2 bz (G) N2 bz sig (G) N2 FAP norm_method norm cog_method cog int. range start int. range end
0 None 72.964936 17.843167 0.000000 7.405907 17.843162 0.435065 0.000000 0.000000 0.000000 auto 1.101100 I 9.725353 -190.274647 209.725353
1 lower only 94.012588 25.428151 0.000003 5.209152 25.428140 0.221315 0.000000 0.000000 0.000000 auto 1.104800 I 14.197130 -185.802870 214.197130
2 higher only 54.213318 21.662775 0.000007 3.435552 21.662771 0.803308 0.000000 0.000000 0.000000 auto 1.096300 I 13.943177 -186.056823 213.943177
3 trim at mid-point 54.213318 21.662775 0.000007 3.435552 21.662771 0.803308 0.000000 0.000000 0.000000 auto 1.096300 I 13.943177 -186.056823 213.943177
4 coadd 69.328145 15.158243 0.000000 5.479275 15.158239 0.999346 0.000000 0.000000 0.000000 auto 1.098296 I 14.040448 -185.959552 214.040448
../_images/bcb97e262172b466b0cf238dd20a34e44c54b0cf16a2c67bca22df9861c4b599.png ../_images/a44608a645e1bcd48333684dd01d349bb66acee43861149b5f62c7dc6b7f018e.png

The Halpha line is somewhat complicated to analyze, in that it has a very broad emission component from regions not associated with the magnetic field. This makes the correct velocity range to use for integration in the Bz calculation ambiguous.

For a somewhat cleaner example that is still in a region where two spectral orders overlap, we can look at the He I 587.56 nm line.

# reading the spectrum
spec = pol.read_spectrum("IndividualLine_tutorialfiles/1423137pn.s")
# defining the line parameters
lambda0 = 587.56 ;  geff = 1.0 # He I 587, really a multiplet (so geff is rather approximate)
lwidth = 500. * lambda0 / 2.99e5 #width in nm from width in km/s
# getting the profile for that line
prof = spec.individual_line(lambda0 = lambda0, lwidth = lwidth)

# define the limits for the continuum normalization
# compute Bz in a 50 km/s window around the cog
cogRange = [-100., 150.]
intRange = 50.
Bz = prof.calc_bz(norm = 'auto', cog = 'I', 
                  velrange = cogRange, bzwidth = intRange, 
                  geff = geff, lambda0 = lambda0,
                  plot = False)   

# creating a dataframe to record the result of the different methods in this section
d = {'Merge type':'None'}
d.update(Bz)
df_results = pd.DataFrame(data = [d])

orders = spec.get_orders_in_range(lambda0)
# loop over the individual orders
for ord, type in zip(orders,['lower only', 'higher only']):
    # get the profile for the line in this order
    prof = ord.individual_line(lambda0 = lambda0, lwidth = lwidth)
    Bz = prof.calc_bz(norm = 'auto', cog = 'I', 
                           velrange = cogRange, bzwidth = intRange, 
                           geff = geff, lambda0 = lambda0,
                           plot = False)
    d = {'Merge type':type}
    d.update(Bz)
    df = pd.DataFrame(data = [d])
    df_results = pd.concat([df_results,df], ignore_index=True)

# merging using the trim method
merge = spec.merge_orders(mode = 'trim', midpoint = 0.5)
prof = merge.individual_line(lambda0 = lambda0, lwidth = lwidth)
Bz = prof.calc_bz(norm = 'auto', cog = 'I', 
                  velrange = cogRange, bzwidth = intRange, 
                  geff = geff, lambda0 = lambda0,
                  plot = False)
d = {'Merge type':'trim at mid-point'}
d.update(Bz)
df = pd.DataFrame(data = [d])
df_results = pd.concat([df_results,df], ignore_index=True)

# merging using the coadd method
merge = spec.merge_orders(mode = 'coadd')
prof = merge.individual_line(lambda0 = lambda0, lwidth = lwidth)
Bz, fig = prof.calc_bz(norm = 'auto', cog = 'I', 
                       velrange = cogRange, bzwidth = intRange, 
                       geff = geff, lambda0 = lambda0,
                       plot = True)
d = {'Merge type':'coadd'}
d.update(Bz)
df = pd.DataFrame(data = [d])
df_results = pd.concat([df_results,df], ignore_index=True)

# display the Bz result as a dataframe
df_results.style
using AUTO method for the normalization
  using the median of the continuum outside of the line
using AUTO method for the normalization
  using the median of the continuum outside of the line
using AUTO method for the normalization
  using the median of the continuum outside of the line
using AUTO method for the normalization
  using the median of the continuum outside of the line
using AUTO method for the normalization
  using the median of the continuum outside of the line
/tmp/ipykernel_980312/2408171684.py:13: UserWarning: 
 The velocity array is not monotonically increasing. 
 There might be an order overlap in the region of the observation used. 
 calc_bz will sort the LSD profile in velocity order. 
 Make sure this is what you want -- merge orders before running calc_bz()!
  Bz = prof.calc_bz(norm = 'auto', cog = 'I',
  Merge type V bz (G) V bz sig (G) V FAP N1 bz (G) N1 bz sig (G) N1 FAP N2 bz (G) N2 bz sig (G) N2 FAP norm_method norm cog_method cog int. range start int. range end
0 None 699.503139 66.521645 0.000000 49.584163 66.504701 0.000385 0.000000 0.000000 0.000000 auto 0.991090 I 18.941158 -31.058842 68.941158
1 lower only 822.700767 91.691815 0.000000 -17.652850 91.658895 0.031436 0.000000 0.000000 0.000000 auto 0.992395 I 25.706350 -24.293650 75.706350
2 higher only 793.994679 90.964654 0.000000 220.357069 90.937349 0.002410 0.000000 0.000000 0.000000 auto 0.990170 I 25.578820 -24.421180 75.578820
3 trim at mid-point 797.884180 91.419382 0.000000 221.513065 91.391673 0.002410 0.000000 0.000000 0.000000 auto 0.993520 I 25.587490 -24.412510 75.587490
4 coadd 805.924582 60.001692 0.000000 116.774813 59.981432 0.097482 0.000000 0.000000 0.000000 auto 0.990281 I 25.660364 -24.339636 75.660364
../_images/96c634c868118bde43d21f081804d9d62a9ccb6a8a4d25c2952741b72b5114a9.png