FilFinder Tutorial¶

This tutorial demonstrates the FilFinder algorithm on a simulated data set. The updated algorithm from FilFinder2D is used here, which is valid for versions >1.5. This tutorial was tested with python 3.6.

The example data is included in the github repository here.

In [1]:
%matplotlib inline
import matplotlib.pyplot as plt
import astropy.units as u

import numpy as np

# Optional settings for the plots. Comment out if needed.
import seaborn as sb
sb.set_context('poster')

import matplotlib as mpl
mpl.rcParams['figure.figsize'] = (12., 9.6)

Input Data¶

There are two caveats to the input data:

  1. All angular and physical conversions assume that pixels in the image can be treated as squares. FilFinder2D is not aware of any axis misalignments! If you're data does not have aligned celestial axes, we recommend reprojecting the data onto a square grid.

  2. The beam size is characterized by the major axis, assuming a 2D Gaussian beam shape. If the beam size of your data is highly elliptical, it is recommended to convolve the data to a circular beam.

FilFinder2D accepts several input types, including a FITS HDU and numpy arrays.

In [2]:
from fil_finder import FilFinder2D
from astropy.io import fits

hdu = fits.open("examples/filaments_updatedhdr.fits")[0]

fil = FilFinder2D(hdu)
In [3]:
# HDU data as an array
arr = hdu.data
hdr = hdu.header

fil = FilFinder2D(arr)
/Volumes/Cloud/Dropbox/code_development/FilFinder/fil_finder/filfinder2D.py:149: UserWarning: No beam width given. Using 0 pixels.
  warnings.warn("No beam width given. Using 0 pixels.")

In this case, no WCS information is given and all results will be returned in pixel units. Angular units can be returned when the header is specified:

In [4]:
fil = FilFinder2D(arr, header=hdr)

If spectral-cube is installed, the Projection or Slice classes can also be passed to FilFinder2D:

In [5]:
from spectral_cube import Projection

proj = Projection.from_hdu(hdu)

fil = FilFinder2D(proj)

Other Inputs to FilFinder2D:¶

Note that numerical inputs must be given as ~astropy.units.Quantity object with the appropriate unit.

Distance¶

To facilitate conversions to physical units, a distance can be given to FilFinder2D:

In [6]:
fil = FilFinder2D(hdu, distance=250 * u.pc)

Angular Scale¶

If no header information is given, the pixel-to-angular conversion can be given:

In [7]:
fil = FilFinder2D(arr, ang_scale=0.1 * u.deg)
/Volumes/Cloud/Dropbox/code_development/FilFinder/fil_finder/base_conversions.py:55: UserWarning: Cannot find 'BMAJ' in the header. Try installing the `radio_beam` package for loading header information.
  warn("Cannot find 'BMAJ' in the header. Try installing"
/Volumes/Cloud/Dropbox/code_development/FilFinder/fil_finder/base_conversions.py:63: UserWarning: Cannot find 'BMIN' in the header. Assuming circular beam.
  warn("Cannot find 'BMIN' in the header. Assuming circular beam.")
/Volumes/Cloud/Dropbox/code_development/FilFinder/fil_finder/base_conversions.py:69: UserWarning: Cannot find 'BPA' in the header. Assuming PA of 0.
  warn("Cannot find 'BPA' in the header. Assuming PA of 0.")
/Volumes/Cloud/Dropbox/code_development/FilFinder/fil_finder/filfinder2D.py:149: UserWarning: No beam width given. Using 0 pixels.
  warnings.warn("No beam width given. Using 0 pixels.")

Beam width¶

If the major axis of the beam is contained in the header, it will be automatically read in. If that information is not in the header, the beam size can be passed separately:

In [8]:
fil = FilFinder2D(hdu, beamwidth=10 * u.arcsec)

Custom Filament Masks¶

If you have a pre-computed filament mask, the mask array can be passed:

In [9]:
# Example custom mask
mask = hdu.data > 1.

fil = FilFinder2D(hdu, mask=mask)

The custom mask must have the same shape as the inputed image.

Save Name¶

A prefix for saved plots and table can be given:

In [10]:
fil = FilFinder2D(hdu, save_name="FilFinder_Output")

The beamwidth is $24''$ and is defined in the header.

Parallel Processing options¶

FilFinder2D now allows operations over invidividual filaments to be parallelized with the pool and nthreads kwargs.

The default is None which will trigger creating a oncurrent.futures.ProcessPoolExecutor with nthread processes (default is 1).

Alternatively, you can use mpi4py.futures.MPIPoolExecutor as the pool when working in an MPI environment. By default the max_workers will be set by the MPI4PY_FUTURES_MAX_WORKERS environment variable.

In [11]:
from concurrent.futures import ProcessPoolExecutor

mypool = ProcessPoolExecutor(max_workers=2)
fil = FilFinder2D(hdu, save_name="FilFinder_Output", pool=mypool)

# is equivalent to

fil = FilFinder2D(hdu, save_name="FilFinder_Output", pool=None, nthreads=2)

# Using mpi4py:
# from mpi4py.futures import MPIPoolExecutor

# mypool = MPIPoolExecutor(max_workers=None)
# fil = FilFinder2D(hdu, save_name="FilFinder_Output", pool=mypool)

For the purposes of this tutorial, we will assume that the data has WCS information and a well-defined distance:

In [12]:
fil = FilFinder2D(hdu, distance=260 * u.pc)

Image Preprocessing¶

Prior to creating the mask, it can be helpful to first flatten the image of bright compact sources. FilFinder2D uses an arctan transform, where the data are first normalized by some percentile value of the data:

In [13]:
fil.preprocess_image(flatten_percent=95)
In [14]:
plt.subplot(121)
plt.imshow(fil.image.value, origin='lower')
plt.title("Image")
plt.subplot(122)
plt.imshow(fil.flat_img.value, origin='lower')
plt.title("Flattened Image")
plt.tight_layout()

If a percentile is not given, FilFinder2D.preprocess_image will try to fit a log-normal distribution to the data and will set the threshold at $\mu + 2 \sigma$. There are no checks for the quality of the fit. Use only if you are confident that the brightness distribution is close to a log-normal.

If you wish to run the masking procedure without flattening the image, use the command:

In [15]:
fil.preprocess_image(skip_flatten=True)

The original image will be set as fil.flat_img and used in the masking step.

For this example, we will use the prior flattened image.

In [16]:
fil.preprocess_image(flatten_percent=95)

Masking¶

Creating the filament mask is a complex process performed by FilFinder2D.create_mask. There are several parameters that set the masking behaviour.

If a FITS header and distance were provided at the beginning, FilFinder2D will use default guesses based on typical filament sizes from Herschel studies of the Gould Belt clouds (e.g., Koch & Rosolowsky 2015). These choices will not be the optimal settings in general, and we recommend trying different different parameter setting before using the resulting mask for the analysis.

If a distance was not provided, these parameters must be set. FilFinder2D will raise errors until the required parameters are given.

This simulated data set is an example where the default FilFinder2D settings do not provide an ideal filament mask:

In [17]:
fil.create_mask(verbose=True)
No description has been provided for this image

Most of the filamentary structure has been ignored by the mask. There are several parameters that can be set to improve the mask:

  • glob_thresh -- Set a minimum intensity for a pixel to be included in the mask. This is useful for removing noisy regions in the data from the mask. Must have the same units as fil.image.
  • adapt_thresh -- The width of the element used for the adaptive thresholding mask. This is primarily the step that picks out the filamentary structure. The element size should be similar to the width of the expected filamentary structure. The default here, when distance is provided, is 0.1 pc.
  • smooth_size -- It is often helpful to smooth the data before calculating the mask. By smoothing in small scales, small noise variations are removed resulting in a simpler skeleton structure. The default is set to 0.05 pc.
  • size_thresh -- The minimum number of pixels a region of the mask must have to be considered real. The default is set by assuming a minimum filament size to be an ellipse with a 0.1 pc width and length of 0.5 pc. Most data sets will require this parameter to be manually set, as is used below.
  • regrid -- If the pixel width of adapt_thresh is less than 40 pixels, the resulting mask may be fragmented due to pixelization. To increase the size of adapt_thresh, regrid interpolates the data onto a larger grid, calculates the mask on the larger grid, then interpolates the mask at the original image size.
  • border_masking -- Observational maps may not fill the entire image, and the edges of the mapped regions tend to be noisier. border_masking finds regions of NaNs along the edge of the map and tries to remove noisy regions near the edges. Its behaviour can be controlled using border_kwargs, where the size of a NaN region (size), size of the filter used to define noisy edges (filt_width), and the number of times to apply that filter (eros_iter) can be controlled.
  • fill_hole_size -- If there are holes within a skeleton, fill_hole_size can be used to fill in holes smaller than the given size.
  • use_existing_mask -- If you gave a user-defined mask when calling FilFinder2D, enable this parameter to skip making a new mask.

Varying a few of these parameters will produce a much improved mask. First, since the data go right to the edges of the image, we can disable border_masking:

In [18]:
fil.create_mask(verbose=True, border_masking=False)
No description has been provided for this image

The mask now extends right to the edge of the data. However, only one structure was retained. This occurs because size_thresh is too large. We can manually set the value in pixel units. The size must have units of area:

In [19]:
fil.create_mask(verbose=True, border_masking=False, size_thresh=400 * u.pix**2)
No description has been provided for this image

Much better! Most of the filamentary structure is now being included in the mask.

This simulated image does not have noise added in, however, most data sets will. The cell below demonstrates how to set glob_thresh to avoid noisy regions:

In [20]:
# Define the noise value. As a demonstration, say values below the 20th percentile here are dominated by noise
noise_level = np.percentile(fil.image, 20)
noise_level

plt.imshow(fil.image > noise_level, origin='lower')
Out[20]:
<matplotlib.image.AxesImage at 0x30b6de180>

The dark regions will be excluded from the final mask. The filament mask with the threshold is then:

In [21]:
fil.create_mask(verbose=True, border_masking=False, size_thresh=400 * u.pix**2, glob_thresh=0.0267)
No description has been provided for this image

A few small region have been removed compared to the previous mask, but the structure is largely unchanged in this case.

This is a usable mask for the filament analysis. The effects of altering the other parameters are shown in Koch & Rosolowsky 2015.

Try varying each parameter to assess its affect on your data.

If you gave a user-defined mask at the beginning, run:

In [22]:
fil.create_mask(use_existing_mask=True)
/Volumes/Cloud/Dropbox/code_development/FilFinder/fil_finder/filfinder2D.py:313: UserWarning: Using inputted mask. Skipping creation of anew mask.
  warnings.warn("Using inputted mask. Skipping creation of a"

Skeletonization¶

The next step is to reduce the mask into single-pixel-width skeleton objects. These skeletons will define the location of a filament and its path.

In FilFinder2D, the medial axis is defined to be the skeleton:

In [23]:
fil.medskel(verbose=True)
No description has been provided for this image

Skeletons: Pruning & Length¶

We are now prepared to analyze the filaments. The first analysis step includes two parts: pruning the skeleton structures and finding the filament lengths. The first part removes small/unimportant spurs on the skeletons. To ensure important portions of the skeleton are retained, however, both parts are performed together.

Each skeleton is converted into a graph object using the networkx package. We use the graph to find the longest path through the skeleton, which is used to define the structure's length. All branches in the skeleton away from this longest path are eligible to be pruned off.

This process is handled by the FilFinder2D.analyze_skeletons function. When using verbose=True, a ton of plots will get returned. To save you some scrolling, the verbose mode is highlighted for just one filament below.

With just the default settings:

In [24]:
fil.analyze_skeletons()

plt.imshow(fil.skeleton, origin='lower')
/Volumes/Cloud/Dropbox/code_development/FilFinder/fil_finder/filament.py:361: UserWarning: Graph pruning reached max iterations.
  warnings.warn("Graph pruning reached max iterations.")
Out[24]:
<matplotlib.image.AxesImage at 0x30a34f4d0>

The skeletons are largely the same, with only short branches removed.

The default settings use minimum skeleton and branch lengths based off of the beam size. To be kept, a branch must be at least three times the length of the beam and a skeleton must have a length of 5 times the beam. Practically, this will only remove very small features.

These parameters, and ones related to the pruning, can be manually set:

  • prune_criteria -- The criteria for removing a branch can be altered. The default ('all') uses a mix of the average intensity along the branch and its length. The length alone can be used for pruning with prune_criteria='length'. All branches below this length will be removed. Finally, only the intensity can be used for pruning (prune_criteria='intensity'). A branch is kept in this case by comparing the average intensity along the branch to the average over the whole filament. The critical fraction that determines whether a branch is important is set by relintens_thresh.
  • relintens_thresh -- Set the critical intensity comparison for intensity-based pruning.
  • nbeam_lengths -- Number of beam widths a skeleton must have to be considered a valid structure. Default is 5.
  • branch_nbeam_lengths -- Number of beam widths a branch must have to avoid pruning. Default is 3.
  • skel_thresh -- Minimum length for a skeleton. Overrides nbeam_lengths. Must have a unit of length.
  • branch_thresh -- Minimum length for a branch. Overrides branch_nbeam_lengths. Must have a unit of length.
  • max_prune_iter -- Number of pruning iterations. The default is 10, which works well for multiple data sets used in the testing process. A warning is returned if the maximum is reached. New in FilFinder2D!

Here we will highlight the effect of pruning away moderately long branches. Note that re-running FilFinder2D.analyze_skeletons will start on the output from the previous call, not that original skeleton from FilFinder2D.medskel.

In [25]:
fil.analyze_skeletons(branch_thresh=40 * u.pix, prune_criteria='length')

plt.imshow(fil.skeleton, origin='lower')
plt.contour(fil.skeleton_longpath, colors='r')
Out[25]:
<matplotlib.contour.QuadContourSet at 0x3093deae0>

The structure have now been significantly pruned. The red contours highlight the longest paths through each skeleton.

If we continue to increase the branch threshold, the skeletons will converge to the longest path structures:

In [26]:
fil.analyze_skeletons(branch_thresh=400 * u.pix, prune_criteria='length')

plt.imshow(fil.skeleton, origin='lower')
plt.contour(fil.skeleton_longpath, colors='r')
Out[26]:
<matplotlib.contour.QuadContourSet at 0x30b727f80>

This is an extreme case of pruning and a significant amount of real structure was removed. We will return to a less pruned version to use for the rest of the tutorial:

In [27]:
fil.medskel(verbose=False)
fil.analyze_skeletons(branch_thresh=5 * u.pix, prune_criteria='length')

plt.imshow(fil.skeleton, origin='lower')
plt.contour(fil.skeleton_longpath, colors='r')
Out[27]:
<matplotlib.contour.QuadContourSet at 0x30d9296a0>

Another new feature of FilFinder2D is that each filament has its own analysis class defined in fil.filaments:

In [28]:
fil.filaments
Out[28]:
[<fil_finder.filament.Filament2D at 0x30b7429c0>,
 <fil_finder.filament.Filament2D at 0x30d94a990>,
 <fil_finder.filament.Filament2D at 0x30b6dec90>,
 <fil_finder.filament.Filament2D at 0x30a2ec8f0>,
 <fil_finder.filament.Filament2D at 0x30b7935c0>,
 <fil_finder.filament.Filament2D at 0x309422750>,
 <fil_finder.filament.Filament2D at 0x30a2ece00>,
 <fil_finder.filament.Filament2D at 0x30b7412e0>,
 <fil_finder.filament.Filament2D at 0x30d9cea50>,
 <fil_finder.filament.Filament2D at 0x3093dc1d0>,
 <fil_finder.filament.Filament2D at 0x30b6c3410>,
 <fil_finder.filament.Filament2D at 0x309417ec0>]

This allows for each skeleton to be analyzed independently, in case your analysis requires fine-tuning.

A separate tutorial on the Filament2D class is available from the docs page. We will highlight some of the features here to show the plotting outputs. Each Filament2D class does not contain that entire image, however, to avoid making multiple copies of the data.

The first filament is quite large with a lot of structure. We can plot the output from FilFinder2D.analyze_skeletons for just one filament with:

In [29]:
fil1 = fil.filaments[0]
fil1.skeleton_analysis(fil.image, verbose=True, branch_thresh=5 * u.pix, prune_criteria='length')
/Volumes/Cloud/Dropbox/code_development/FilFinder/fil_finder/filament.py:361: UserWarning: Graph pruning reached max iterations.
  warnings.warn("Graph pruning reached max iterations.")
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image

Three plots are returned:

  • The labeled branch array (left) with intersection points removed and the equivalent graph structure (right).
  • The longest path through the skeleton (left) and the same labeled branch array (right) as above.
  • The final, pruned skeleton structure.

Only one set of plots is shown after the iterative pruning has been finished.

The lengths of the filament's longest paths are now calculated:

In [30]:
fil.lengths()
Out[30]:
$[401.37468,~271.32085,~157.81118,~126.81118,~156.89444,~138.36753,~45.526912,~190.79394,~33.727922,~115.56854,~72.083261,~65.698485] \; \mathrm{pix}$

The default output is in pixel units, but if the angular and physical scales are defined, they can be converted into other units:

In [31]:
fil.lengths(u.deg)
Out[31]:
$[0.66895779,~0.45220142,~0.26301864,~0.21135197,~0.26149074,~0.23061255,~0.075878187,~0.3179899,~0.056213203,~0.19261424,~0.12013877,~0.10949747]\mathrm{{}^{\circ}}$
In [32]:
fil.lengths(u.pc)
Out[32]:
$[3.0356342,~2.0520249,~1.1935407,~0.95908483,~1.1866073,~1.0464866,~0.34432429,~1.4429924,~0.25508743,~0.87405568,~0.54517244,~0.49688378] \; \mathrm{pc}$

The properties of the branches are also saved in the FilFinder2D.branch_properties dictionary. This includes the length of each branch, the average intensity, the skeleton pixels of the branch, and the number of branches in each skeleton:

In [33]:
fil.branch_properties.keys()
Out[33]:
dict_keys(['length', 'intensity', 'pixels', 'number'])
In [34]:
fil.branch_properties['number']
Out[34]:
array([39, 15,  3,  7, 11, 12,  1,  8,  1,  8,  1,  3])
In [35]:
fil.branch_properties['length'][0]
Out[35]:
$[22.313708,~34.727922,~8.2426407,~21.899495,~5.4142136,~14.242641,~6,~18.656854,~9.6568542,~14.242641,~14.313708,~10.242641,~18.313708,~26.313708,~71.769553,~58.769553,~4.4142136,~14.656854,~16.071068,~11.242641,~24.727922,~16.727922,~10.071068,~5.6568542,~7.2426407,~18.071068,~18.899495,~6.6568542,~0.5,~30.970563,~55.284271,~16.313708,~22.142136,~8.2426407,~35.970563,~17.142136,~32.899495,~11.828427,~51.59798] \; \mathrm{pix}$

If only prominent branches are needed for your analysis, there is an option to reconstruct the skeletons using only branches above a min length, regardless of the overall filament connectivity.

For individual filaments, this mode is specified in Filament2D.skeleton using the kwarg options out_type='minbranchlength' and branch_thresh of some pixel length value.

In [36]:
fil1 = fil.filaments[1]

plt.imshow(fil1.skeleton(out_type='minbranchlength', branch_thresh=20.0 * u.pix))
plt.show()
branch_thresh 20.0 pix
No description has been provided for this image
In [37]:
fil.filaments[1].branch_properties['pixels'][0][:, 1]

# fil.filaments[1].pixel_extents
Out[37]:
array([51, 52, 53, 54, 55, 56, 57, 58])

The FilFinder2D.make_skeleton_min_branchlength function will return the all filaments with branches above a minimum length. This is a separate function with no state changes within FilFinder2D as the filament connectivity will be lost.

In [ ]:
skeleton_minbranch = fil.make_skeleton_minbranchlength(20.0 * u.pix)

plt.imshow(fil.skeleton)
plt.contour(skeleton_minbranch, levels=[0.5], colors='r')
plt.show()
branch_thresh 20.0 pix
branch_thresh 20.0 pix
branch_thresh 20.0 pix
branch_thresh 20.0 pix
branch_thresh 20.0 pix
branch_thresh 20.0 pix
branch_thresh 20.0 pix
branch_thresh 20.0 pix
branch_thresh 20.0 pix
branch_thresh 20.0 pix
branch_thresh 20.0 pix
branch_thresh 20.0 pix
False
No description has been provided for this image

Note that the pixels are defined with respect to the cut-out structures in Filament2D. These offsets are contained in FilFinder2D.filament_extents. See the FilFinder2D tutorial for more information.

The branch lengths can also be returned with:

In [40]:
fil.branch_lengths(u.pix)[0]
Out[40]:
$[22.313708,~34.727922,~8.2426407,~21.899495,~5.4142136,~14.242641,~6,~18.656854,~9.6568542,~14.242641,~14.313708,~10.242641,~18.313708,~26.313708,~71.769553,~58.769553,~4.4142136,~14.656854,~16.071068,~11.242641,~24.727922,~16.727922,~10.071068,~5.6568542,~7.2426407,~18.071068,~18.899495,~6.6568542,~0.5,~30.970563,~55.284271,~16.313708,~22.142136,~8.2426407,~35.970563,~17.142136,~32.899495,~11.828427,~51.59798] \; \mathrm{pix}$
In [41]:
fil.branch_lengths(u.pc)[0]
Out[41]:
$[0.16876066,~0.26265052,~0.062339862,~0.16562793,~0.040948203,~0.10771842,~0.045378561,~0.14110353,~0.073035691,~0.10771842,~0.10825591,~0.077466048,~0.13850829,~0.19901304,~0.54279983,~0.44447962,~0.03338511,~0.11085116,~0.12154699,~0.085029142,~0.18701958,~0.12651484,~0.076168427,~0.042783317,~0.054776768,~0.13667317,~0.14293865,~0.050346411,~0.0037815467,~0.23423326,~0.41812011,~0.1233821,~0.16746304,~0.062339862,~0.27204873,~0.12964757,~0.24882195,~0.089459499,~0.39024034] \; \mathrm{pc}$

Curvature and Orientation¶

A filament's curvature and orientation are calculated using a modified version of the Rolling Hough Transform (RHT). This can be run either on the longest path skeletons or on individual branches.

The default setting is to run on the longest path skeletons:

In [42]:
fil.exec_rht()
fil.filaments[0].plot_rht_distrib()
No description has been provided for this image

The RHT distribution is shown for the first skeleton, along with its longest path skeleton. The polar plot shows the distribution as a function of $2\theta$. Since there is no preferred direction, $0$ and $\pi$ are equivalent direction for a filament, and so the distribution is defined over $\theta \in [-\pi/2, \pi/2)$. Plotting the distribution as $2\theta$ makes it easier to visualize with discontinuities. The solid green line shows the mean orientation of the filament, and the curvature region is indicated by the angle between the dashed blue lines.

The RHT distribution is built by measuring the angular distribution in a circular region around each pixel in the skeleton, then accumulating the distribution over all pixels in the skeleton. There are three parameters that affect the distribution:

  • radius -- the radius of the circular region to use. The default is 10 pixels. The region must be large enough to avoid pixelization (causing spikes at 0, 45, and 90 deg) but small enough to only include the local filament direction.
  • ntheta -- The number of bins in $\theta$ to calculate the distribution at. Default is 180.
  • background_percentile -- The accumulation process used to create the distribution will create a constant background level over $\theta$. Peaks in the distribution are better characterized by removing this constant level. The default setting is to subtract the 25th percentile from the distribution.

The RHT returns the orientation and curvature of each filament. The orientation is defined as the circular mean and the curvature is the interquartile region about the mean. See the documentation for the definitions.

In [43]:
fil.orientation
Out[43]:
$[-0.2275954,~-0.47732119,~0.046629529,~0.47571282,~-0.5739298,~-0.95806639,~-0.57433324,~-1.2563861,~-0.054924874,~-0.41161293,~-0.74549871,~0.12195574] \; \mathrm{rad}$
In [44]:
fil.curvature
Out[44]:
$[0.60818639,~0.75708599,~0.50721127,~0.891042,~0.77017085,~0.69848674,~0.43408083,~1.0214703,~0.50431366,~0.74067012,~0.54552006,~0.55515504] \; \mathrm{rad}$

It can be more useful to run this analysis on individual branches to understand the distribution of orientation and curvature across the whole map. This can be performed by enabling branches=True:

In [45]:
fil.exec_rht(branches=True, min_branch_length=5 * u.pix)

There is no default plot setting in this case.

An additional parameter is enabled in this mode: min_branch_lengths. This avoids running the RHT on very short branches, where pixelization will lead to large spikes towards the axis directions.

The outputs are contained in FilFinder2D.orientation_branches and FilFinder2D.curvature_branches, which return a list of lists for each filament. These can be visualized as distributions:

In [47]:
_ = plt.hist(fil.orientation_branches[0].value, bins=10)
plt.xlabel("Orientation (rad)")

plt.show()
No description has been provided for this image
In [48]:
all_orient = np.array([orient.value for fil_orient in fil.orientation_branches for orient in fil_orient])
# Short, excluded branches have NaNs
all_orient = all_orient[np.isfinite(all_orient)]

_ = plt.hist(all_orient, bins=10)
plt.xlabel("Orientation (rad)")
plt.show()
No description has been provided for this image

No orientation is strongly preferred in the example data.

Radial Profiles and Widths¶

FilFinder2D finds filament widths by creating radial profiles centered on the skeleton. A simple model can then be fit to the radial profile to find the width.

There are several parameters used to control the creation of the radial profile:

  • max_dist -- The maximum radial distance to build the radial profile to. Must be given in units of length (pixel, degree, pc, etc...). The default is 10 pixels. In order to not bias the fit, the profile should extend far enough to adequately fit the background level.
  • pad_to_distance -- FilFinder only includes pixels in the radial profiles that are closest to the filament which avoids double-counting pixels. But if the filaments are closely packed together, this can severely limit the number of points used to make the profile. pad_to_distance forces all pixels within the given distance to be included in the profile. Must be given in length units and be less than max_dist.
  • use_longest_path -- Will use the longest path skeleton instead of the full skeleton. Default is False.
  • kwargs -- These are passed to the radial_profile function. Please see the documentation in the link for the different options. If an error about empty bins or an array with a shape of 0 is returned, try using auto_cut=False.

FilFinder supports 3 simple models for fitting the radial profiles: a Gaussian with a mean fixed to 0 and a constant background, the same Gaussian without a background, and a non-parametric method to estimate Gaussian widths. However, FilFinder uses the astropy.modeling package and will accept any 1D astropy model. For example, the radfil package has an astropy implementation of a Plummer model, which could be used here.

The parameters that control the fitting are:

  • fit_model -- The model to the profiles to. The defaults are gaussian_bkg, gaussian_nobkg, and nonparam. Otherwise, a 1D astropy model can be given, as discussed above.
  • fitter -- The fitter to use. See astropy.modeling.fitter. Defaults to a least-squares fitter.
  • try_nonparam -- If the fit to the model fails, the non-parametric method can be used instead. Default is True.
  • add_width_to_length -- The fitted FWHM can be added to the lengths (FilFinder2D.lengths), assuming that the skeleton's length was shortened by the width in the medial axis transform (FilFinder2D.medskel). The width will not be added if the fit was poor or highly unconstrained. Default is True.
  • deconvolve_width -- Subtract off the beam width when calculating the FWHM width. Default is True.
  • fwhm_function -- Pass a function that takes the fit_model and returns the FWHM and its uncertainty. If None is given, the FWHM is passed assuming a Gaussian profile.
  • chisq_max -- The critical reduced $\chi^2$ used to determine "bad" fits. The default is 10, and is entirely subjective. This seems to flag most bad fits, but the quality of the fits should always be visually checked.

With the default settings, a Gaussian with a constant background is fit to the profiles:

In [49]:
fil.find_widths(max_dist=0.2 * u.pc)
fil.filaments[0].plot_radial_profile(xunit=u.pc)
/Volumes/Cloud/Dropbox/code_development/FilFinder/fil_finder/filament.py:1052: UserWarning: Ignoring adding the width to the length because the fail flag was raised for the fit.
  warnings.warn("Ignoring adding the width to the length because"
No description has been provided for this image

The radial profile of the first filament is shown above. The binned radial profile is shown as black diamonds and the fit is shown with the red solid line.

The profile can be plotted with different xunits:

In [50]:
fil.filaments[0].plot_radial_profile(xunit=u.pix)
No description has been provided for this image

Based on the warning above, at least one of the filament profile fits failed. We can look at the list of widths. FilFinder2D.widths() returns the FWHMs and their uncertainties:

In [51]:
fil.widths()
Out[51]:
(<Quantity [ 6.88348177,  4.38611993,  7.51974158,  5.7582753 ,  6.88240202,
             6.95927064, 43.18439783,  5.60647754,  8.14114232,  4.55374535,
             2.41019355,  5.00890439] pix>,
 <Quantity [6.50310949e-04, 1.20577939e-03, 4.25187286e-03, 5.41517545e-03,
            6.72833715e-03, 2.28067623e-02, 5.81688527e+01, 1.74444672e-02,
            1.22131299e-02, 2.12077693e-03, 3.69637714e-01, 6.15489042e-03] pix>)

These widths can be returned in other units as well:

In [52]:
fil.widths(u.pc)
Out[52]:
(<Quantity [0.05206042, 0.03317263, 0.05687251, 0.04355037, 0.05205225,
            0.05263361, 0.32660764, 0.04240231, 0.06157222, 0.0344404 ,
            0.01822852, 0.03788281] pc>,
 <Quantity [4.91836246e-06, 9.11942219e-06, 3.21573117e-05, 4.09554779e-05,
            5.08870425e-05, 1.72489674e-04, 4.39936467e-01, 1.31934135e-04,
            9.23690421e-05, 1.60396341e-05, 2.79560457e-03, 4.65500113e-05] pc>)

The 6th filament has a much larger width, and its uncertainty is very large. We can look at this radial profile more closely:

In [53]:
fil.filaments[6].plot_radial_profile(xunit=u.pc)
No description has been provided for this image

This is a faint feature near another, and the simple modeling has failed here. This is a case where fine-tuning may lead to a better result for certain filaments. See the Filament2D tutorial.

The fit results can be returned as an astropy table:

In [54]:
fil.width_fits(xunit=u.pc)
Out[54]:
Table length=12
amplitude_0amplitude_0_errstddev_0stddev_0_erramplitude_1amplitude_1_errfwhmfwhm_errfail_flagmodel_type
KKpcpcKKpcpc
float64float64float64float64float64float64float64float64boolstr12
0.84314347335931996.902715087245868e-050.0255697105141025551.8058717468813343e-060.067369044199580012.125244001771127e-050.052060415686669314.9183624620610175e-06Falsegaussian_bkg
1.59932535833068860.000314803560200714760.0190654764461928062.8614369362857355e-060.099547011349023626.251658559619661e-050.033172634797823989.119422189715454e-06Falsegaussian_bkg
0.89779439697058980.00053695340196677240.0273558373990024541.2056372417097167e-050.157288595132686840.00012991708021139230.0568725080880664043.2157311700113566e-05Falsegaussian_bkg
0.6793651373578810.00051969205733540510.022518407336102231.4284030487954184e-050.116346698163181630.000145008369642205620.0435503740864312954.095547785629654e-05Falsegaussian_bkg
0.43629547939819980.00040962466672074630.025566712177311721.8683420242485376e-050.0252083773299924649.925064574204283e-050.0520522494372355555.088704245231567e-05Falsegaussian_bkg
0.135512085310480480.000350825993056557860.0257804614459675246.350678964552632e-050.047320744902081410.000151864757194172550.052633614044570440.00017248967431675031Falsegaussian_bkg
0.08127047866582870.017091369256377220.139291205102556210.18602750121461390.042368072271347050.0187787543982267380.326607635345818160.4399364671946228Truenonparam
0.207844825462311730.00048364678443912160.0221197473797892024.560905938587238e-050.057315144337862460.000119202320283691710.0424023134262641250.00013193413517172424Falsegaussian_bkg
1.5900986469637020.0028180097209623570.029132927459873693.520559401579874e-050.00228398050795464460.00048673936498659020.061572219923571979.236904214050718e-05Falsegaussian_bkg
1.57420972063408170.000472528951588146330.0194666491359482975.117478332277996e-060.21663677820673490.000156204500048593550.034440401486533661.6039634079137334e-05Falsegaussian_bkg
0.0387223932035026250.00244598852387116050.0149989185083394360.00061270543531068040.0289928547014768750.000288355184708890830.0182285190019105150.0027956045667443205Falsegaussian_bkg
2.58174544225477160.00225012806678348030.020587574235212481.5446901306754068e-050.21465751085410570.00084409089350962080.037882811873024244.6550011294653784e-05Falsegaussian_bkg

This provides the fit results, the parameter errors, whether or not the fit failed, and the type of model used. The table can then be saved.

Other Filament Properties¶

With the width models, we can define other filament properties, such as the total intensity within the FWHM of the filament:

In [55]:
fil.total_intensity()
Out[55]:
$[7298.5435,~3554.6616,~1184.3035,~849.76086,~822.86963,~354.75763,~98.836777,~354.68088,~385.24927,~2125.6602,~11.58816,~1203.6809] \; \mathrm{K}$

If a background was fit in the model, the background level can be subtracted off. The index of the background parameter needs to be given. For the ''gaussian_bkg'', this is bkg_mod_index=2 and set as the default:

In [56]:
fil.total_intensity(bkg_subtract=True)
Out[56]:
$[6915.6851,~3396.4814,~987.0636,~736.32281,~781.2002,~268.87048,~20.837156,~285.73077,~384.51611,~1887.793,~6.4274316,~1118.2472] \; \mathrm{K}$

The median brightness along the skeleton is calculated with:

In [57]:
fil.median_brightness()
Out[57]:
$[1.002587,~2.0058308,~1.0829422,~0.8928259,~0.50316799,~0.17868349,~0.057910323,~0.27634802,~0.92849165,~1.8334504,~0.062282987,~3.0347648] \; \mathrm{K}$

Based on the radial profile models, we can create an image based on the models:

In [58]:
fil_mod = fil.filament_model()
plt.imshow(fil_mod.value)
plt.colorbar()
plt.show()
No description has been provided for this image

By default, the background level is subtracted off, as was used for fil.total_intensity. The maximum radius around each skeleton to evaluate the model can also be given with max_radius. The default is 3 times the FWHM, which should account for most of the model flux.

This model can be used to estimate the fraction of the total flux contained in the filamentary structure:

In [59]:
fil.covering_fraction()
Out[59]:
0.6220013235017049

The same keywords given to FilFinder2D.filament_model can be passed here.

The values aligned along the longest path are returned with:

In [60]:
profs = fil.ridge_profiles()
plt.subplot(211)
plt.plot(profs[0])
plt.subplot(212)
plt.plot(profs[1])
plt.tight_layout()
plt.show()
No description has been provided for this image

This can be useful for examining the distribution of cores along filaments.

Output Table & Images¶

FilFinder2D returns result tables as astropy tables. FilFinder2D.width_fits is highlighted above.

The width results and additional properties are returned with:

In [61]:
fil.output_table(xunit=u.pc)
Out[61]:
Table length=12
lengthsbranchestotal_intensitymedian_brightnessX_posnY_posnamplitude_0amplitude_0_errstddev_0stddev_0_erramplitude_1amplitude_1_errfwhmfwhm_errfail_flagmodel_type
pcKKpixpixKKpcpcKKpcpc
float64int64float32float64float64float64float64float64float64float64float64float64float64float64boolstr12
3.0876945816511685397298.54351.0025869607925415106.095.00.84314347335931996.902715087245868e-050.0255697105141025551.8058717468813343e-060.067369044199580012.125244001771127e-050.052060415686669314.9183624620610175e-06Falsegaussian_bkg
2.0851975805015246153554.66162.005830764770508188.045.01.59932535833068860.000314803560200714760.0190654764461928062.8614369362857355e-060.099547011349023626.251658559619661e-050.033172634797823989.119422189715454e-06Falsegaussian_bkg
1.25041323007624431184.30351.082942247390747142.0127.50.89779439697058980.00053695340196677240.0273558373990024541.2056372417097167e-050.157288595132686840.00012991708021139230.0568725080880664043.2157311700113566e-05Falsegaussian_bkg
1.00263519988534827849.760860.892825901508331343.0123.00.6793651373578810.00051969205733540510.022518407336102231.4284030487954184e-050.116346698163181630.000145008369642205620.0435503740864312954.095547785629654e-05Falsegaussian_bkg
1.238659589634404111822.86960.503167986869812213.0120.00.43629547939819980.00040962466672074630.025566712177311721.8683420242485376e-050.0252083773299924649.925064574204283e-050.0520522494372355555.088704245231567e-05Falsegaussian_bkg
1.09912018839523112354.757630.17868348956108093218.0178.00.135512085310480480.000350825993056557860.0257804614459675246.350678964552632e-050.047320744902081410.000151864757194172550.052633614044570440.00017248967431675031Falsegaussian_bkg
0.3443242883339285198.836780.0579103231430053750.0177.00.08127047866582870.017091369256377220.139291205102556210.18602750121461390.042368072271347050.0187787543982267380.326607635345818160.4399364671946228Truenonparam
1.48539470090493558354.680880.276348024606704723.0216.00.207844825462311730.00048364678443912160.0221197473797892024.560905938587238e-050.057315144337862460.000119202320283691710.0424023134262641250.00013193413517172424Falsegaussian_bkg
0.31665964552025571385.249270.928491652011871383.0190.01.5900986469637020.0028180097209623570.029132927459873693.520559401579874e-050.00228398050795464460.00048673936498659020.061572219923571979.236904214050718e-05Falsegaussian_bkg
0.908496085419889282125.66021.833450436592102184.5229.01.57420972063408170.000472528951588146330.0194666491359482975.117478332277996e-060.21663677820673490.000156204500048593550.034440401486533661.6039634079137334e-05Falsegaussian_bkg
0.5634009572694831111.588160.0622829869389534234.0227.00.0387223932035026250.00244598852387116050.0149989185083394360.00061270543531068040.0289928547014768750.000288355184708890830.0182285190019105150.0027956045667443205Falsegaussian_bkg
0.534766590400972531203.68093.0347647666931152156.0238.02.58174544225477160.00225012806678348030.020587574235212481.5446901306754068e-050.21465751085410570.00084409089350962080.037882811873024244.6550011294653784e-05Falsegaussian_bkg

This will include units if attached to the image or radial profile models.

The median positions can also be returned in world coordinates if WCS information was given:

In [62]:
fil.output_table(xunit=u.pc, world_coord=True)
Out[62]:
Table length=12
lengthsbranchestotal_intensitymedian_brightnessRADecamplitude_0amplitude_0_errstddev_0stddev_0_erramplitude_1amplitude_1_errfwhmfwhm_errfail_flagmodel_type
pcKKdegdegKKpcpcKKpcpc
float64int64float32float64float64float64float64float64float64float64float64float64float64float64boolstr12
3.0876945816511685397298.54351.00258696079254150.0533333333344-0.03500000000070.84314347335931996.902715087245868e-050.0255697105141025551.8058717468813343e-060.067369044199580012.125244001771127e-050.052060415686669314.9183624620610175e-06Falsegaussian_bkg
2.0851975805015246153554.66162.0058307647705080.13666666666940.10166666666871.59932535833068860.000314803560200714760.0190654764461928062.8614369362857355e-060.099547011349023626.251658559619661e-050.033172634797823989.119422189715454e-06Falsegaussian_bkg
1.25041323007624431184.30351.082942247390747359.999166666666670.0250000000004999970.89779439697058980.00053695340196677240.0273558373990024541.2056372417097167e-050.157288595132686840.00012991708021139230.0568725080880664043.2157311700113566e-05Falsegaussian_bkg
1.00263519988534827849.760860.89282590150833130.0066666666668-0.14000000000280.6793651373578810.00051969205733540510.022518407336102231.4284030487954184e-050.116346698163181630.000145008369642205620.0435503740864312954.095547785629654e-05Falsegaussian_bkg
1.238659589634404111822.86960.5031679868698120.01166666666690.14333333333620.43629547939819980.00040962466672074630.025566712177311721.8683420242485376e-050.0252083773299924649.925064574204283e-050.0520522494372355555.088704245231567e-05Falsegaussian_bkg
1.09912018839523112354.757630.17868348956108093359.91499999999830.15166666666970.135512085310480480.000350825993056557860.0257804614459675246.350678964552632e-050.047320744902081410.000151864757194172550.052633614044570440.00017248967431675031Falsegaussian_bkg
0.3443242883339285198.836780.05791032314300537359.916666666665-0.12833333333590.08127047866582870.017091369256377220.139291205102556210.18602750121461390.042368072271347050.0187787543982267380.326607635345818160.4399364671946228Truenonparam
1.48539470090493558354.680880.2763480246067047359.8516666666637-0.173333333336799980.207844825462311730.00048364678443912160.0221197473797892024.560905938587238e-050.057315144337862460.000119202320283691710.0424023134262641250.00013193413517172424Falsegaussian_bkg
0.31665964552025571385.249270.9284916520118713359.8949999999979-0.07333333333481.5900986469637020.0028180097209623570.029132927459873693.520559401579874e-050.00228398050795464460.00048673936498659020.061572219923571979.236904214050718e-05Falsegaussian_bkg
0.908496085419889282125.66021.833450436592102359.82999999999660.095833333335251.57420972063408170.000472528951588146330.0194666491359482975.117478332277996e-060.21663677820673490.000156204500048593550.034440401486533661.6039634079137334e-05Falsegaussian_bkg
0.5634009572694831111.588160.0622829869389534359.833333333330.17833333333690.0387223932035026250.00244598852387116050.0149989185083394360.00061270543531068040.0289928547014768750.000288355184708890830.0182285190019105150.0027956045667443205Falsegaussian_bkg
0.534766590400972531203.68093.0347647666931152359.81499999999630.04833333333432.58174544225477160.00225012806678348030.020587574235212481.5446901306754068e-050.21465751085410570.00084409089350962080.037882811873024244.6550011294653784e-05Falsegaussian_bkg

A table for each of the branch properties of the filaments is returned with:

In [63]:
branch_tables = fil.branch_tables()
branch_tables[0]
Out[63]:
Table length=39
lengthintensity
pix
float64float32
22.313708498984764.368228
34.727922061357863.3066635
8.2426406871192866.6625805
21.8994949366116671.1975303
5.4142135623730950.6047616
14.2426406871192865.276611
6.00.95888233
18.656854249492381.3061414
9.656854249492381.0430864
......
55.28427124746190.7516269
16.313708498984761.2288334
22.142135623730950.95138097
8.2426406871192860.5127995
35.970562748477140.7805252
17.142135623730950.4626319
32.899494936611670.3444026
11.828427124746190.3573772
51.597979746446670.30130994

If the RHT was run on branches, these data can also be added to the branch tables:

In [64]:
branch_tables = fil.branch_tables(include_rht=True)
branch_tables[0]
Out[64]:
Table length=39
lengthintensityorientationcurvature
pixradrad
float64float32float64float64
22.313708498984764.3682280.347129120412485170.5356649790323493
34.727922061357863.30666351.02631158433790850.9897537529847784
8.2426406871192866.6625805-0.081941502551671060.6102609075455621
21.8994949366116671.19753030.84547370482445590.9958654899145456
5.4142135623730950.6047616-1.38424592671597120.5519922958530006
14.2426406871192865.2766111.33348505935001270.502395390650006
6.00.95888233-1.2026740743085595e-160.33111411368698485
18.656854249492381.3061414-0.239190165178854330.46392880110826706
9.656854249492381.04308641.57079632679489660.4102044376306768
............
55.28427124746190.7516269-0.076093816057320820.715684541265381
16.313708498984761.2288334-0.64016015061287020.49344844321513387
22.142135623730950.95138097-0.50147969276849150.4894984783239946
8.2426406871192860.51279950.213846657975803160.4521066330961103
35.970562748477140.78052520.19391342996803730.7501634765150539
17.142135623730950.4626319-0.81451878103254280.45800046066457
32.899494936611670.3444026-0.12497970013682681.4978359568548987
11.828427124746190.35737720.25364362496446790.4531023113101953
51.597979746446670.30130994-0.59527898387810650.5380559718540621

These tables can be saved to a format supported by astropy tables.

Finally, the mask, skeletons, longest path skeletons, and the filament model can be saved as a FITS file:

In [65]:
fil.save_fits()

This will save the file with prefix given at the beginning. This can be changed by specifying save_name here. The keywords for FilFinder2D.filament_model can also be specified here.

The regions and stamps around each filament can also be saved with:

In [66]:
fil.save_stamp_fits()

The same arguments for FilFinder2D.save_fits can be given, along with pad_size which sets how large of an area around each skeleton is included in the stamp. This will create a FITS file for each filament.