How to use Mask objects#

In this tutorial, we go over the importance of masks and how they are made and used.

First import specpolFlow and any other packages.

Hide code cell content
## Importing Necessary Packages
import specpolFlow as pol

import pandas as pd
import numpy as np

What is a Mask?#

Analytically, a mask is a function with Dirac deltas at wavelengths corresponding to specific spectral lines. The amplitude of the Dirac delta function corresponds to the line depth. Numerically, a mask is an array of wavelengths with a depth at the center of each line. Thus, a mask tells us the location and depth of all lines in a spectrum but does not tell us about the shape of the lines or the spectrum as a whole.

Why do we care?#

The idea behind LSD is to model a spectrum as the convolution of a line mask and a line profile (the LSD profile). So given an LSD profile and a mask, we can convolve the LSD profile with the mask to get a model spectrum. Typically, though, we have an observed spectrum and a mask but want the LSD profile. This reverse process of going from a spectrum and a line mask to an LSD profile is called deconvolution. We need a mask to help us weigh each spectral line in the spectrum so that they can be combined into an LSD profile.

Mask creation#

We will use the make_mask function to create a mask. Usually you will only need the arguments lineListFile and outMaskName, as well as two optional arguments, depthCutoff and atomsOnly.

  • lineListFile is the name of the file containing the line list;

  • outMaskName is the name of the file to write the output mask to (default is None);

  • depthCutoff is a float that only include lines in the mask that are deeper than this value;

  • atomsOnly is a boolean that decides whether to include only atomic lines (no molecular lines and no H-lines).

Note

Hydrogen lines are automatically excluded when atomsOnly = True. This is done because the hydrogen lines, due to their broad wings, have a different shape than all the other lines in the spectrum.

The input line list is a VALD line list file obtained from the VALD website. It should be an “extract stellar” from VALD in their “long” format (to include Landé factors), and it should correspond to the \(T_\text{eff}\), \(\log g\), and chemical abundances of your star. More details about VALD are given in the tutorial From normalized spectrum to Bz measurement. In the example below, we start with a line list for a relatively hot star (LongList_T27000G35.dat). We use all atomic lines in the line list stronger than 0.02, except those without effective Landé factors and the H-lines.

LineList_file_name = '../GetStarted/OneObservationFlow_tutorialfiles/LongList_T27000G35.dat'
Mask_file_name = '../GetStarted/OneObservationFlow_tutorialfiles/test_output/T27000G35_depth0.02.mask'

mask_clean = pol.make_mask(LineList_file_name, outMaskName=Mask_file_name, 
                           depthCutoff = 0.02, atomsOnly = True)
Hide code cell output
missing Lande factors for 160 lines (skipped) from:
['He 2', 'O 2']
skipped all lines for species:
['H 1']

Warning

The make_mask function will automatically attempt to calculate the effective Landé factor for lines missing that value in the line list. It can usually make approximate estimates for lines in LS, JJ, and JK coupling schemes.

However, if a Landé factor is unable to be calculated the line will be excluded if includeNoLande = False (the default), or the Landé factor will equal the DefaultLande value if includeNoLande = True.

Mask Cleaning#

After obtaining our mask, the next step is to clean it. Mask cleaning involves removing lines that we do not want to use in the computation of LSD profiles. Typically, we exclude lines that fall within the Telluric regions and those within the H line wings. The lines within the Telluric regions are contaminated by lines from Earth’s atmosphere and are therefore unusable. Hydrogen lines can’t be modelled correctly in LSD because they have a different sizes and shapes from other lines. So lines in the H wings, blended with Hydrogen lines, also can’t be modelled correctly and are unusable. Lines blended with other big broad absorption features, such as the Ca H & K lines in cooler star, should also be excluded. Although these will vary with the spectra type of the star. When dealing with stars with emission, care should be taken to exclude emission lines as they have different shapes.

This tutorial will clean the mask using some already defined regions (see How to use the ExcludeMaskRegion objects for more details). For a more detailed by hand approach see How to clean masks with the interactive tool. First we get the pre-defined telluric regions with get_telluric_regions_default and pre-defined hydrogen Balmer line regions with get_Balmer_regions_default.

# inputs
velrange = 600.0 # width of region on either side of a Balmer line to exclude, as a velocity, in km/s
# get the two sets of excluded regions and combine them
excluded_regions = pol.get_Balmer_regions_default(velrange) + pol.get_telluric_regions_default()

# optionally, display the excluded regions using Pandas
pd.DataFrame(excluded_regions.to_dict())
start stop type
0 654.967529 657.594471 Halpha
1 485.167047 487.112953 Hbeta
2 433.181299 434.918701 Hgamma
3 409.349092 410.990908 Hdelta
4 396.215430 397.804570 Hepsilon
5 360.000000 392.000000 Hjump
6 587.500000 592.000000 telluric
7 627.500000 632.500000 telluric
8 684.000000 705.300000 telluric
9 717.000000 735.000000 telluric
10 757.000000 771.000000 telluric
11 790.000000 795.000000 telluric
12 809.000000 990.000000 telluric

Once we have our excluded regions, we can clean the mask using the mask.clean function. This function operates on an existing mask (it is part of the Mask class) and it takes the excluded regions. The output is a cleaned line mask, in which lines that fall within the excluded_regions have been removed. Finally we need to save the cleaned mask to a file using the mask’s save function.

# reading in the mask that we created earlier
mask = pol.read_mask('../GetStarted/OneObservationFlow_tutorialfiles/test_output/T27000G35_depth0.02.mask')

# applying the ExcludeMaskRegions that we created
mask_clean = mask.clean(excluded_regions)

# saving the new mask to a file
mask_clean.save('../GetStarted/OneObservationFlow_tutorialfiles/test_output/hd46328_test_depth0.02_clean.mask')

Other useful tools#

  1. Interactive Line Cleaning

    SpecpolFlow also includes an interactive tool to visually inspect a mask, select/deselect lines, and compare an observation with the LSD model spectrum calculated on the fly. This can be useful for fine tuning a mask. See How to clean masks with the interactive tool.

  2. Prune

    Additionally, the Mask class has a function to prune the mask object, removing all lines from the list that have iuse = 0. The clean function works by setting the flag iuse = 0 for lines, making them not used in a LSD calculation, but not deleting them from the line list completely. Calling prune after calling clean can be used to remove the lines completely.

# using the mask that we created earlier, and re-running the clean function
mask_clean = mask.clean(excluded_regions)
print('Number of lines in the clean mask with iuse = 0: {}, from a total of: {}'.format(
    len(mask_clean[mask_clean.iuse == 0]), len(mask_clean)))

mask_clean_prune=mask_clean.prune()
print('Number of lines in the pruned mask with iuse = 0: {}, from a total of: {}'.format(
    len(mask_clean_prune[mask_clean_prune.iuse == 0]), len(mask_clean_prune)))
Number of lines in the clean mask with iuse = 0: 533, from a total of: 1601
Number of lines in the pruned mask with iuse = 0: 0, from a total of: 1068
  1. Get Line Weights

    We can calculate the LSD weight of all lines in the mask using the get_weights function. This function requires the following inputs:

    • normDepth: the normalizing line depth, as used for LSD;

    • normWave: the normalizing wavelength in nm;

    • normLande: the normalizing effective Landé factor.

    The function then outputs two arrays, the weights of the Stokes I lines, and the weights of the Stokes V lines. Stokes I weights are generally the line depth divided by normDepth. Stokes V weights are (line depth * wavelength * Lande factor)/(normDepth * normWave * normLande).

weightI, weightV = mask_clean_prune.get_weights(normDepth=0.2, normWave=500.0, normLande=1.2)

print(weightI)
print(weightV)
[1.735 1.99  0.14  ... 0.25  0.285 0.335]
[1.51113292 1.52142026 0.08239234 ... 0.39070416 0.4466502  0.52675226]

Advanced mask filtering#

The Mask class supports slicing and advanced slicing like numpy. A Mask object is essentially a container for a set of numpy arrays. This means you can get a line, or range of lines, from a mask using standard syntax like mask[index_start:index_end]. This is most useful if you want to filter an existing Mask object to get only some types of lines in the mask.

# Get only lines deeper than some value
mask_deep = mask_clean_prune[mask_clean_prune.depth > 0.2]
print('total lines:', len(mask_clean_prune))
print('deep lines:', len(mask_deep))

# Get only lines in some wavelength range
mask_wl_range = mask_clean_prune[(mask_clean_prune.wl > 450.) & (mask_clean_prune.wl < 600.)]
print('mid wavelength lines:', len(mask_wl_range))

# Get only lines with larger effective Lande factors
mask_highLande = mask_clean_prune[mask_clean_prune.lande > 1.2]
print('high Lande lines:', len(mask_highLande))

# Line lists can be sliced based on element type.
# The elements codes use the format atomic number + ionization*0.01
# so they need to be rounded off before comparing numerically.
# For a line list with only iron:
mask_Fe = mask_clean_prune[np.round(mask_clean_prune.element).astype(int) == 26]
print('Fe lines:', len(mask_Fe))
# Or for a line list with no He:
mask_noHe = mask_clean_prune[np.round(mask_clean_prune.element).astype(int) != 2]
print('non-He lines:', len(mask_noHe))

# These can be combined, with numpy's logic functions.
# The parentheses are important for evaluating expressions th the right order.
# e.g. to get only strong iron lines in some wavelength range:
mask_short = mask_clean_prune[(mask_clean_prune.depth > 0.2) & 
                              (mask_clean_prune.wl > 450.) & (mask_clean_prune.wl < 600.) &
                              (np.round(mask_clean_prune.element).astype(int) == 26)]
print('strong Fe lines in wavelength range:', len(mask_short))
print('wavelengths', mask_short.wl)
print('elements', mask_short.element)
print('depths', mask_short.depth)

#If you want to save the filtered mask for later use
mask_short.save('../GetStarted/OneObservationFlow_tutorialfiles/test_output/T27000G35_Fe_depth0.2.mask')
total lines: 1068
deep lines: 160
mid wavelength lines: 444
high Lande lines: 422
Fe lines: 270
non-He lines: 1024
strong Fe lines in wavelength range: 5
wavelengths [512.7371 515.6111 524.3306 583.3938 592.9685]
elements [26.02 26.02 26.02 26.02 26.02]
depths [0.21  0.263 0.25  0.262 0.213]