Skip to content

Commit a2eb672

Browse files
authoredAug 26, 2024··
Merge branch 'master' into FilFinder3D
2 parents d17287c + 34637b9 commit a2eb672

15 files changed

+340
-2122
lines changed
 

‎.github/workflows/main.yml

+19-19
Original file line numberDiff line numberDiff line change
@@ -21,49 +21,49 @@ jobs:
2121
toxenv: py39-test
2222

2323
- os: ubuntu-latest
24-
python-version: 3.8
25-
name: Python 3.8 with minimal dependencies
26-
toxenv: py38-test
24+
python-version: '3.10'
25+
name: Python 3.10 with minimal dependencies
26+
toxenv: py310-test
2727

2828
- os: ubuntu-latest
29-
python-version: 3.7
30-
name: Python 3.7 with minimal dependencies
31-
toxenv: py37-test
29+
python-version: 3.11
30+
name: Python 3.11 with minimal dependencies
31+
toxenv: py311-test
3232

3333
- os: ubuntu-latest
3434
python-version: 3.9
3535
name: Python 3.9 with minimal and dev dependencies
3636
toxenv: py39-test-dev
3737

3838
- os: ubuntu-latest
39-
python-version: 3.9
40-
name: Python 3.9, all dependencies, and dev versions of key dependencies
41-
toxenv: py39-test-dev
39+
python-version: 3.11
40+
name: Python 3.11, all dependencies, and dev versions of key dependencies
41+
toxenv: py311-test-dev
4242

4343
- os: ubuntu-latest
44-
python-version: 3.9
45-
name: Python 3.9, all dependencies, and coverage
46-
toxenv: py39-test-all-cov
44+
python-version: 3.11
45+
name: Python 3.11, all dependencies, and coverage
46+
toxenv: py311-test-all-cov
4747

4848
- os: macos-latest
49-
python-version: 3.9
50-
name: Python 3.9 with all dependencies on MacOS X
51-
toxenv: py39-test-all-dev
49+
python-version: 3.11
50+
name: Python 3.11 with all dependencies on MacOS X
51+
toxenv: py311-test-all-dev
5252

5353
# - os: windows-latest
5454
# python-version: 3.7
5555
# name: Python 3.7, with all dependencies on Windows
5656
# toxenv: py37-test-all-dev
5757

5858
- os: ubuntu-latest
59-
python-version: 3.9
59+
python-version: 3.11
6060
name: Documentation
6161
toxenv: build_docs
6262

6363
steps:
64-
- uses: actions/checkout@v2
64+
- uses: actions/checkout@v3
6565
- name: Set up Python ${{ matrix.python-version }}
66-
uses: actions/setup-python@v2
66+
uses: actions/setup-python@v4
6767
with:
6868
python-version: ${{ matrix.python-version }}
6969
- name: Install testing dependencies
@@ -72,6 +72,6 @@ jobs:
7272
run: tox -v -e ${{ matrix.toxenv }}
7373
- name: Upload coverage to codecov
7474
if: ${{ contains(matrix.toxenv,'-cov') }}
75-
uses: codecov/codecov-action@v1.0.13
75+
uses: codecov/codecov-action@v3
7676
with:
7777
file: ./coverage.xml

‎.github/workflows/publish.yml

+5-5
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@ jobs:
77
name: Build source distribution
88
runs-on: ubuntu-latest
99
steps:
10-
- uses: actions/checkout@v2
11-
- uses: actions/setup-python@v2
10+
- uses: actions/checkout@v3
11+
- uses: actions/setup-python@v4
1212
name: Install Python
1313
with:
14-
python-version: '3.9'
14+
python-version: '3.11'
1515
- name: Install build
1616
run: python -m pip install build
1717
- name: Build sdist
@@ -26,11 +26,11 @@ jobs:
2626
runs-on: ubuntu-latest
2727
if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/v')
2828
steps:
29-
- uses: actions/download-artifact@v2
29+
- uses: actions/download-artifact@v4
3030
with:
3131
name: artifact
3232
path: dist
33-
- uses: pypa/gh-action-pypi-publish@master
33+
- uses: pypa/gh-action-pypi-publish@release/v1
3434
with:
3535
user: __token__
3636
password: ${{ secrets.pypi_password }}

‎.readthedocs.yml

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
version: 2
22

33
build:
4-
image: latest
4+
os: "ubuntu-20.04"
5+
tools:
6+
python: "3.12"
57

68
# Install regular dependencies.
79
python:
8-
version: 3.7
910
install:
1011
- method: pip
1112
path: .
1213
extra_requirements:
1314
- docs
14-

‎fil_finder/__init__.py

-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
from ._astropy_init import __version__, test
44

5-
from .filfind_class import fil_finder_2D
65
from .filfinder2D import FilFinder2D
76
from .filfinderPPV import FilFinderPPV
87
from .filfinderPPP import FilFinderPPP

‎fil_finder/conftest.py

+15-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
# no matter how it is invoked within the source tree.
44
from __future__ import print_function, absolute_import, division
55

6-
import pytest
6+
import os
7+
from setuptools._distutils.version import LooseVersion
78

89
from astropy.version import version as astropy_version
910

@@ -13,8 +14,21 @@
1314
else:
1415
from pytest_astropy_header.display import PYTEST_HEADER_MODULES, TESTED_VERSIONS
1516

17+
import pytest
18+
from fil_finder.tests.testing_utils import generate_filament_model
19+
20+
1621
def pytest_configure(config):
1722

1823
config.option.astropy_header = True
1924

2025
PYTEST_HEADER_MODULES['Astropy'] = 'astropy'
26+
27+
28+
@pytest.fixture
29+
def simple_filament_model():
30+
31+
mod = generate_filament_model(return_hdu=True, pad_size=31, shape=150,
32+
width=10., background=0.1)[0]
33+
34+
yield mod

‎fil_finder/filament.py

+90-15
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@
1616
else:
1717
import cPickle as pickle
1818

19-
from .length import (init_lengths, main_length, make_final_skeletons,
20-
pre_graph, longest_path, prune_graph, all_shortest_paths)
21-
from .pixel_ident import pix_identify
19+
from .length import (init_lengths, main_length,
20+
pre_graph, longest_path, prune_graph)
21+
from .pixel_ident import pix_identify, make_final_skeletons
2222
from .utilities import pad_image, in_ipynb, red_chisq
2323
from .base_conversions import UnitConverter
2424
from .rollinghough import rht
@@ -200,7 +200,8 @@ def skeleton(self, pad_size=0, corner_pix=None, out_type='all'):
200200
def skeleton_analysis(self, image, verbose=False, save_png=False,
201201
save_name=None, prune_criteria='all',
202202
relintens_thresh=0.2, max_prune_iter=10,
203-
branch_thresh=0 * u.pix):
203+
branch_thresh=0 * u.pix,
204+
return_self=False):
204205
'''
205206
Run the skeleton analysis.
206207
@@ -230,6 +231,9 @@ def skeleton_analysis(self, image, verbose=False, save_png=False,
230231
Maximum number of pruning iterations to apply.
231232
branch_thresh : `~astropy.units.Quantity`, optional
232233
Minimum length for a branch to be eligible to be pruned.
234+
return_self : bool, optional
235+
Return the Filament2D object after skeleton analysis. This is needed
236+
for parallel processing in FilFinder2D.
233237
'''
234238

235239
# NOTE:
@@ -406,6 +410,9 @@ def skeleton_analysis(self, image, verbose=False, save_png=False,
406410
'number': branch_properties['number'][0],
407411
'pixels': branch_properties['pixels'][0]}
408412

413+
if return_self:
414+
return self
415+
409416
@property
410417
def branch_properties(self):
411418
'''
@@ -497,7 +504,8 @@ def plot_graph(self, save_name=None, layout_func=nx.spring_layout):
497504
# Add in the ipynb checker
498505

499506
def rht_analysis(self, radius=10 * u.pix, ntheta=180,
500-
background_percentile=25):
507+
background_percentile=25,
508+
return_self=False):
501509
'''
502510
Use the RHT to find the filament orientation and dispersion of the
503511
longest path.
@@ -512,6 +520,8 @@ def rht_analysis(self, radius=10 * u.pix, ntheta=180,
512520
background_percentile : float, optional
513521
Float between 0 and 100 that sets a background level for the RHT
514522
distribution before calculating orientation and curvature.
523+
return_self : bool, optional
524+
If True, return the `Filament2D` object. Defaults to False.
515525
'''
516526

517527
if not hasattr(radius, 'unit'):
@@ -539,6 +549,9 @@ def rht_analysis(self, radius=10 * u.pix, ntheta=180,
539549
self._orientation_hist = [theta, R]
540550
self._orientation_quantiles = [twofive, sevenfive]
541551

552+
if return_self:
553+
return self
554+
542555
@property
543556
def orientation_hist(self):
544557
'''
@@ -603,9 +616,10 @@ def plot_rht_distrib(self, save_name=None):
603616
else:
604617
plt.show()
605618

606-
def rht_branch_analysis(self, radius=10 * u.pix, ntheta=180,
619+
def rht_branch_analysis(self,radius=10 * u.pix, ntheta=180,
607620
background_percentile=25,
608-
min_branch_length=3 * u.pix):
621+
min_branch_length=3 * u.pix,
622+
return_self=False):
609623
'''
610624
Use the RHT to find the filament orientation and dispersion of each
611625
branch in the filament.
@@ -623,6 +637,8 @@ def rht_branch_analysis(self, radius=10 * u.pix, ntheta=180,
623637
min_branch_length : `~astropy.units.Quantity`, optional
624638
Minimum length of a branch to run the RHT on. Branches that are
625639
too short will cause spikes along the axis angles or 45 deg. off.
640+
return_self : bool, optional
641+
Return the `Filament2D` object with the results.
626642
'''
627643

628644
# Convert length cut to pixel units
@@ -680,6 +696,9 @@ def rht_branch_analysis(self, radius=10 * u.pix, ntheta=180,
680696
self._orientation_branches = np.array(means) * u.rad
681697
self._curvature_branches = np.array(iqrs) * u.rad
682698

699+
if return_self:
700+
return self
701+
683702
@property
684703
def orientation_branches(self):
685704
'''
@@ -706,6 +725,7 @@ def width_analysis(self, image, all_skeleton_array=None,
706725
beamwidth=None,
707726
fwhm_function=None,
708727
chisq_max=10.,
728+
return_self=False,
709729
**kwargs):
710730
'''
711731
@@ -756,6 +776,8 @@ def width_analysis(self, image, all_skeleton_array=None,
756776
chisq_max : float, optional
757777
Enable the fail flag if the reduced chi-squared value is above
758778
this limit.
779+
return_self : bool, optional
780+
Return the `Filament2D` object if True. Defaults to False.
759781
kwargs : Passed to `~fil_finder.width.radial_profile`.
760782
761783
'''
@@ -1018,6 +1040,9 @@ def width_analysis(self, image, all_skeleton_array=None,
10181040

10191041
self._radprof_failflag = fail_flag
10201042

1043+
if return_self:
1044+
return self
1045+
10211046
@property
10221047
def radprof_fit_fail_flag(self):
10231048
'''
@@ -1122,17 +1147,25 @@ def radprof_model(self):
11221147
'''
11231148
return self._radprof_model
11241149

1125-
def plot_radial_profile(self, save_name=None, xunit=u.pix,
1126-
ax=None):
1150+
def plot_radial_profile(self,
1151+
ax=None,
1152+
save_name=None,
1153+
show_plot=True,
1154+
xunit=u.pix
1155+
):
11271156
'''
11281157
Plot the radial profile of the filament and the fitted model.
11291158
11301159
Parameters
11311160
----------
1132-
xunit : `~astropy.units.Unit`, optional
1133-
Pixel, angular, or physical unit to convert to.
11341161
ax : `~matplotlib.axes`, optional
11351162
Use an existing set of axes to plot the profile.
1163+
save_name : str, optional
1164+
Name of saved plot. A plot is only saved if a name is given.
1165+
show_plot : bool, optional
1166+
Display open figure.
1167+
xunit : `~astropy.units.Unit`, optional
1168+
Pixel, angular, or physical unit to convert to.
11361169
'''
11371170

11381171
dist, radprof = self.radprofile
@@ -1164,8 +1197,12 @@ def plot_radial_profile(self, save_name=None, xunit=u.pix,
11641197

11651198
if save_name is not None:
11661199
plt.savefig(save_name)
1167-
1168-
plt.show()
1200+
if not show_plot:
1201+
plt.close()
1202+
1203+
if show_plot:
1204+
plt.show()
1205+
11691206
if in_ipynb():
11701207
plt.clf()
11711208

@@ -1459,7 +1496,10 @@ def branch_table(self, include_rht=False):
14591496
names=branch_data.keys())
14601497
return tab
14611498

1462-
def save_fits(self, savename, image, pad_size=20 * u.pix, header=None,
1499+
def save_fits(self, savename, image,
1500+
image_dict=None,
1501+
pad_size=20 * u.pix,
1502+
header=None,
14631503
model_kwargs={},
14641504
**kwargs):
14651505
'''
@@ -1468,6 +1508,8 @@ def save_fits(self, savename, image, pad_size=20 * u.pix, header=None,
14681508
14691509
Parameters
14701510
----------
1511+
savename : str
1512+
Filename to save to.
14711513
image : `~numpy.ndarray` or `~astropy.units.Quantity`
14721514
The image from which the filament was extracted.
14731515
pad_size : `~astropy.units.Quantity`, optional
@@ -1509,24 +1551,57 @@ def save_fits(self, savename, image, pad_size=20 * u.pix, header=None,
15091551
else:
15101552
header = fits.Header()
15111553

1554+
# Add the pixel extents into the header
1555+
from astropy.table import Table, Column
1556+
1557+
tab = Table()
1558+
tab.add_column(Column([self.pixel_extents[0][0],
1559+
self.pixel_extents[1][0]],
1560+
name='lower_coord'))
1561+
tab.add_column(Column([self.pixel_extents[0][1],
1562+
self.pixel_extents[1][1]],
1563+
name='upper_coord'))
1564+
1565+
15121566
# Strip off units if the image is a Quantity
15131567
if hasattr(input_image, 'unit'):
15141568
input_image = input_image.value.copy()
15151569

15161570
hdu = fits.PrimaryHDU(input_image, header)
1571+
hdu.name = 'IMAGE'
15171572

15181573
skel_hdr = header.copy()
15191574
skel_hdr['BUNIT'] = ("", "bool")
15201575
skel_hdr['COMMENT'] = "Skeleton created by fil_finder on " + \
15211576
time.strftime("%c")
15221577

15231578
skel_hdu = fits.ImageHDU(skels.astype(int), skel_hdr)
1579+
skel_hdu.name = 'SKELETON'
15241580

15251581
skel_lp_hdu = fits.ImageHDU(skels_lp.astype(int), skel_hdr)
1582+
skel_lp_hdu.name = 'SKELETON_LONGPATH'
15261583

15271584
model_hdu = fits.ImageHDU(model, header)
1585+
model_hdu.name = 'MODEL'
1586+
1587+
tab_hdu = fits.table_to_hdu(tab)
1588+
tab_hdu.name = 'PIXEXTENTS'
1589+
1590+
hdulist = fits.HDUList([hdu, skel_hdu, skel_lp_hdu, model_hdu, tab_hdu])
1591+
1592+
# If image_dict is provided, save cutouts from the image list
1593+
if image_dict is not None:
1594+
for key in image_dict:
1595+
img = image_dict[key]
1596+
img = pad_image(img, self.pixel_extents, pad_size)
1597+
if img.shape != skels.shape:
1598+
img = self.image_slicer(img, skels.shape,
1599+
pad_size=pad_size)
1600+
1601+
img_hdu = fits.ImageHDU(img, header)
1602+
img_hdu.name = key.upper()
15281603

1529-
hdulist = fits.HDUList([hdu, skel_hdu, skel_lp_hdu, model_hdu])
1604+
hdulist.append(img_hdu)
15301605

15311606
hdulist.writeto(savename, **kwargs)
15321607

‎fil_finder/filfind_class.py

-1,707
This file was deleted.

‎fil_finder/filfinder2D.py

+117-58
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import os
1515
import time
1616
import warnings
17+
import concurrent.futures
1718

1819
from .pixel_ident import recombine_skeletons, isolateregions
1920
from .utilities import eight_con, round_to_odd, threshold_local, in_ipynb
@@ -498,7 +499,7 @@ def create_mask(self, glob_thresh=None, adapt_thresh=None,
498499
if in_ipynb():
499500
p.clf()
500501

501-
def medskel(self, verbose=False, save_png=False):
502+
def medskel(self, verbose=False, save_png=False, rng=None):
502503
'''
503504
This function performs the medial axis transform (skeletonization)
504505
on the mask. This is essentially a wrapper function of
@@ -516,6 +517,9 @@ def medskel(self, verbose=False, save_png=False):
516517
Enables plotting.
517518
save_png : bool, optional
518519
Saves the plot made in verbose mode. Disabled by default.
520+
rng : numpy.random.RandomState or int, optional
521+
Random number generator for reproducibility. Used for tie breaks in
522+
the `medial_axis <https://scikit-image.org/docs/stable/api/skimage.morphology.html#skimage.morphology.medial_axis>`_ function.
519523
520524
Attributes
521525
----------
@@ -525,8 +529,17 @@ def medskel(self, verbose=False, save_png=False):
525529
The distance transform used to create the skeletons.
526530
'''
527531

528-
self.skeleton, self.medial_axis_distance = \
529-
medial_axis(self.mask, return_distance=True)
532+
if rng is None:
533+
rng = np.random.default_rng()
534+
535+
# The kwarg for rng has changed in different skimage versions.
536+
try:
537+
self.skeleton, self.medial_axis_distance = \
538+
medial_axis(self.mask, return_distance=True, rng=rng)
539+
except TypeError:
540+
self.skeleton, self.medial_axis_distance = \
541+
medial_axis(self.mask, return_distance=True, random_state=rng)
542+
530543
self.medial_axis_distance = \
531544
self.medial_axis_distance * self.skeleton * u.pix
532545
# Delete connection smaller than 2 pixels wide. Such a small
@@ -551,11 +564,18 @@ def medskel(self, verbose=False, save_png=False):
551564
if in_ipynb():
552565
p.clf()
553566

554-
def analyze_skeletons(self, prune_criteria='all', relintens_thresh=0.2,
555-
nbeam_lengths=5, branch_nbeam_lengths=3,
556-
skel_thresh=None, branch_thresh=None,
567+
def analyze_skeletons(self,
568+
nthreads=1,
569+
prune_criteria='all',
570+
relintens_thresh=0.2,
571+
nbeam_lengths=5,
572+
branch_nbeam_lengths=3,
573+
skel_thresh=None,
574+
branch_thresh=None,
557575
max_prune_iter=10,
558-
verbose=False, save_png=False, save_name=None):
576+
verbose=False,
577+
save_png=False,
578+
save_name=None):
559579
'''
560580
561581
Prune skeleton structure and calculate the branch and longest-path
@@ -564,6 +584,8 @@ def analyze_skeletons(self, prune_criteria='all', relintens_thresh=0.2,
564584
565585
Parameters
566586
----------
587+
nthreads : int, optional
588+
Number of threads to use to parallelize the skeleton analysis.
567589
prune_criteria : {'all', 'intensity', 'length'}, optional
568590
Choose the property to base pruning on. 'all' requires that the
569591
branch fails to satisfy the length and relative intensity checks.
@@ -638,25 +660,26 @@ def analyze_skeletons(self, prune_criteria='all', relintens_thresh=0.2,
638660
# Relabel after deleting short skeletons.
639661
labels, num = nd.label(self.skeleton, eight_con())
640662

663+
641664
self.filaments = [Filament2D(np.where(labels == lab),
642665
converter=self.converter) for lab in
643666
range(1, num + 1)]
644667

645-
self.number_of_filaments = num
646-
647668
# Now loop over the skeleton analysis for each filament object
648-
for n, fil in enumerate(self.filaments):
649-
savename = "{0}_{1}".format(save_name, n)
650-
if verbose:
651-
print("Filament: %s / %s" % (n + 1, self.number_of_filaments))
669+
with concurrent.futures.ProcessPoolExecutor(nthreads) as executor:
670+
futures = [executor.submit(fil.skeleton_analysis, self.image,
671+
verbose=verbose,
672+
save_png=save_png,
673+
save_name=save_name,
674+
prune_criteria=prune_criteria,
675+
relintens_thresh=relintens_thresh,
676+
branch_thresh=self.branch_thresh,
677+
max_prune_iter=max_prune_iter,
678+
return_self=True)
679+
for fil in self.filaments]
680+
self.filaments = [future.result() for future in futures]
652681

653-
fil.skeleton_analysis(self.image, verbose=verbose,
654-
save_png=save_png,
655-
save_name=savename,
656-
prune_criteria=prune_criteria,
657-
relintens_thresh=relintens_thresh,
658-
branch_thresh=self.branch_thresh,
659-
max_prune_iter=max_prune_iter)
682+
self.number_of_filaments = num
660683

661684
self.array_offsets = [fil.pixel_extents for fil in self.filaments]
662685

@@ -749,7 +772,9 @@ def end_pts(self):
749772
'''
750773
return [fil.end_pts for fil in self.filaments]
751774

752-
def exec_rht(self, radius=10 * u.pix,
775+
def exec_rht(self,
776+
nthreads=1,
777+
radius=10 * u.pix,
753778
ntheta=180, background_percentile=25,
754779
branches=False, min_branch_length=3 * u.pix,
755780
verbose=False, save_png=False, save_name=None):
@@ -774,6 +799,8 @@ def exec_rht(self, radius=10 * u.pix,
774799
775800
Parameters
776801
----------
802+
nthreads : int, optional
803+
The number of threads to use.
777804
radius : int
778805
Sets the patch size that the RHT uses.
779806
ntheta : int, optional
@@ -792,6 +819,7 @@ def exec_rht(self, radius=10 * u.pix,
792819
save_name : str, optional
793820
Prefix for the saved plots.
794821
822+
795823
Attributes
796824
----------
797825
rht_curvature : dict
@@ -813,23 +841,35 @@ def exec_rht(self, radius=10 * u.pix,
813841
if save_name is None:
814842
save_name = self.save_name
815843

816-
for n, fil in enumerate(self.filaments):
817-
if verbose:
818-
print("Filament: %s / %s" % (n + 1, self.number_of_filaments))
819844

820-
if branches:
821-
fil.rht_branch_analysis(radius=radius,
845+
if branches:
846+
with concurrent.futures.ProcessPoolExecutor(nthreads) as executor:
847+
futures = [executor.submit(fil.rht_branch_analysis,
848+
radius=radius,
822849
ntheta=ntheta,
823850
background_percentile=background_percentile,
824-
min_branch_length=min_branch_length)
851+
min_branch_length=min_branch_length,
852+
return_self=True)
853+
for fil in self.filaments]
854+
self.filaments = [future.result() for future in futures]
825855

826-
else:
827-
fil.rht_analysis(radius=radius, ntheta=ntheta,
828-
background_percentile=background_percentile)
829856

830-
if verbose:
857+
else:
858+
with concurrent.futures.ProcessPoolExecutor(nthreads) as executor:
859+
futures = [executor.submit(fil.rht_analysis,
860+
radius=radius,
861+
ntheta=ntheta,
862+
background_percentile=background_percentile,
863+
return_self=True)
864+
for fil in self.filaments]
865+
self.filaments = [future.result() for future in futures]
866+
867+
868+
if verbose:
869+
for n, fil in enumerate(self.filaments):
870+
831871
if save_png:
832-
savename = "{0}_{1}_rht.png".format(save_name, n)
872+
save_name = "{0}_{1}_rht.png".format(save_name, n)
833873
else:
834874
save_name = None
835875
fil.plot_rht_distrib(save_name=save_name)
@@ -886,7 +926,9 @@ def pre_recombine_mask_corners(self):
886926
'''
887927
return self._pre_recombine_mask_corners
888928

889-
def find_widths(self, max_dist=10 * u.pix,
929+
def find_widths(self,
930+
nthreads=1,
931+
max_dist=10 * u.pix,
890932
pad_to_distance=0 * u.pix,
891933
fit_model='gaussian_bkg',
892934
fitter=None,
@@ -915,12 +957,8 @@ def find_widths(self, max_dist=10 * u.pix,
915957
916958
Parameters
917959
----------
918-
image : `~astropy.unit.Quantity` or `~numpy.ndarray`
919-
The image from which the filament was extracted.
920-
all_skeleton_array : np.ndarray
921-
An array with the skeletons of other filaments. This is used to
922-
avoid double-counting pixels in the radial profiles in nearby
923-
filaments.
960+
nthreads : int, optional
961+
Number of threads to use.
924962
max_dist : `~astropy.units.Quantity`, optional
925963
Largest radius around the skeleton to create the profile from. This
926964
can be given in physical, angular, or physical units.
@@ -967,23 +1005,26 @@ def find_widths(self, max_dist=10 * u.pix,
9671005
if save_name is None:
9681006
save_name = self.save_name
9691007

970-
for n, fil in enumerate(self.filaments):
1008+
with concurrent.futures.ProcessPoolExecutor(nthreads) as executor:
1009+
futures = [executor.submit(fil.width_analysis, self.image,
1010+
all_skeleton_array=self.skeleton,
1011+
max_dist=max_dist,
1012+
pad_to_distance=pad_to_distance,
1013+
fit_model=fit_model,
1014+
fitter=fitter, try_nonparam=try_nonparam,
1015+
use_longest_path=use_longest_path,
1016+
add_width_to_length=add_width_to_length,
1017+
deconvolve_width=deconvolve_width,
1018+
beamwidth=self.beamwidth,
1019+
fwhm_function=fwhm_function,
1020+
chisq_max=chisq_max,
1021+
return_self=True,
1022+
**kwargs)
1023+
for fil in self.filaments]
1024+
self.filaments = [future.result() for future in futures]
9711025

972-
if verbose:
973-
print("Filament: %s / %s" % (n + 1, self.number_of_filaments))
974-
975-
fil.width_analysis(self.image, all_skeleton_array=self.skeleton,
976-
max_dist=max_dist,
977-
pad_to_distance=pad_to_distance,
978-
fit_model=fit_model,
979-
fitter=fitter, try_nonparam=try_nonparam,
980-
use_longest_path=use_longest_path,
981-
add_width_to_length=add_width_to_length,
982-
deconvolve_width=deconvolve_width,
983-
beamwidth=self.beamwidth,
984-
fwhm_function=fwhm_function,
985-
chisq_max=chisq_max,
986-
**kwargs)
1026+
1027+
for n, fil in enumerate(self.filaments):
9871028

9881029
if verbose:
9891030
if save_png:
@@ -1409,7 +1450,10 @@ def save_fits(self, save_name=None,
14091450
out_hdu.writeto("{0}_image_output.fits".format(save_name),
14101451
**kwargs)
14111452

1412-
def save_stamp_fits(self, save_name=None, pad_size=20 * u.pix,
1453+
def save_stamp_fits(self,
1454+
image_dict=None,
1455+
save_name=None,
1456+
pad_size=20 * u.pix,
14131457
model_kwargs={},
14141458
**kwargs):
14151459
'''
@@ -1421,6 +1465,10 @@ def save_stamp_fits(self, save_name=None, pad_size=20 * u.pix,
14211465
14221466
Parameters
14231467
----------
1468+
image_dict : dict, optional
1469+
Dictionary of arrays to save matching the pixel extents of each filament.
1470+
The shape of each array *must* be the same shape as the original image
1471+
given to `~FilFinder2D`.
14241472
save_name : str, optional
14251473
The prefix for the saved file. If None, the save name specified
14261474
when `~FilFinder2D` was first called.
@@ -1436,10 +1484,21 @@ def save_stamp_fits(self, save_name=None, pad_size=20 * u.pix,
14361484
else:
14371485
save_name = os.path.splitext(save_name)[0]
14381486

1487+
if image_dict is not None:
1488+
for ii, key in enumerate(image_dict):
1489+
this_image = image_dict[key]
1490+
if this_image.shape != self.image.shape:
1491+
raise ValueError("All images in image_dict must be same shape as fil.image. "
1492+
f"For index {ii}, found shape {this_image.shape} not {self.image.shape}")
1493+
1494+
14391495
for n, fil in enumerate(self.filaments):
14401496

1441-
savename = "{0}_stamp_{1}.fits".format(save_name, n)
1497+
savename = f"{save_name}_stamp_{n}.fits"
14421498

1443-
fil.save_fits(savename, self.image, pad_size=pad_size,
1499+
fil.save_fits(savename,
1500+
self.image,
1501+
image_dict=image_dict,
1502+
pad_size=pad_size,
14441503
model_kwargs=model_kwargs,
14451504
**kwargs)

‎fil_finder/length.py

+20-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
# Licensed under an MIT open source license - see LICENSE
22

33
from .utilities import *
4-
from .pixel_ident import *
5-
64

75
import numpy as np
86
import scipy.ndimage as nd
@@ -830,3 +828,23 @@ def get_weight(pat):
830828
long_path_length = max(all_weights)
831829

832830
return long_path, long_path_length
831+
832+
833+
def merge_nodes(node, G):
834+
'''
835+
Combine a node into its neighbors.
836+
'''
837+
838+
neigb = list(G[node])
839+
840+
if len(neigb) != 2:
841+
return G
842+
843+
new_weight = G[node][neigb[0]]['weight'] + \
844+
G[node][neigb[1]]['weight']
845+
846+
G.remove_node(node)
847+
G.add_edge(neigb[0], neigb[1], weight=new_weight)
848+
849+
return G
850+

‎fil_finder/pixel_ident.py

-19
Original file line numberDiff line numberDiff line change
@@ -862,22 +862,3 @@ def is_tpoint(vallist):
862862
return True
863863

864864
return False
865-
866-
867-
def merge_nodes(node, G):
868-
'''
869-
Combine a node into its neighbors.
870-
'''
871-
872-
neigb = list(G[node])
873-
874-
if len(neigb) != 2:
875-
return G
876-
877-
new_weight = G[node][neigb[0]]['weight'] + \
878-
G[node][neigb[1]]['weight']
879-
880-
G.remove_node(node)
881-
G.add_edge(neigb[0], neigb[1], weight=new_weight)
882-
883-
return G

‎fil_finder/tests/test_whole_simplefilament.py

+60-268
Large diffs are not rendered by default.

‎fil_finder/tests/test_widths.py

+6-6
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ def test_radial_profile_output(theta):
5656
model, skeleton = generate_filament_model(width=10.0,
5757
amplitude=1.0, background=0.0)
5858

59-
dist_transform = nd.distance_transform_edt((~skeleton).astype(np.int))
59+
dist_transform = nd.distance_transform_edt((~skeleton).astype(int))
6060

6161
dist, radprof, weights, unbin_dist, unbin_radprof = \
6262
radial_profile(model, dist_transform, dist_transform,
@@ -75,7 +75,7 @@ def test_radial_profile_cutoff(cutoff):
7575
model, skeleton = generate_filament_model(width=10.0,
7676
amplitude=1.0, background=0.0)
7777

78-
dist_transform = nd.distance_transform_edt((~skeleton).astype(np.int))
78+
dist_transform = nd.distance_transform_edt((~skeleton).astype(int))
7979

8080
dist, radprof, weights, unbin_dist, unbin_radprof = \
8181
radial_profile(model, dist_transform, dist_transform,
@@ -92,7 +92,7 @@ def test_radial_profile_padding(padding, max_distance=20.0):
9292
model, skeleton = generate_filament_model(width=10.0,
9393
amplitude=1.0, background=0.0)
9494

95-
dist_transform = nd.distance_transform_edt((~skeleton).astype(np.int))
95+
dist_transform = nd.distance_transform_edt((~skeleton).astype(int))
9696

9797
dist, radprof, weights, unbin_dist, unbin_radprof = \
9898
radial_profile(model, dist_transform, dist_transform,
@@ -116,7 +116,7 @@ def test_radial_profile_fail_pad(padding=30.0, max_distance=20.0):
116116
model, skeleton = generate_filament_model(width=10.0,
117117
amplitude=1.0, background=0.0)
118118

119-
dist_transform = nd.distance_transform_edt((~skeleton).astype(np.int))
119+
dist_transform = nd.distance_transform_edt((~skeleton).astype(int))
120120

121121
dist, radprof, weights, unbin_dist, unbin_radprof = \
122122
radial_profile(model, dist_transform, dist_transform,
@@ -138,7 +138,7 @@ def test_radial_profile_autocut():
138138

139139
# all_skeleton += np.roll(skeleton, -30, axis=0)
140140

141-
dist_transform = nd.distance_transform_edt((~skeleton).astype(np.int))
141+
dist_transform = nd.distance_transform_edt((~skeleton).astype(int))
142142

143143
dist, radprof, weights, unbin_dist, unbin_radprof = \
144144
radial_profile(model, dist_transform, dist_transform,
@@ -174,7 +174,7 @@ def test_radial_profile_autocut_plateau():
174174
amplitude=5.0,
175175
background=0.0)[::-1]
176176

177-
dist_transform = nd.distance_transform_edt((~skeleton).astype(np.int))
177+
dist_transform = nd.distance_transform_edt((~skeleton).astype(int))
178178

179179
dist, radprof, weights, unbin_dist, unbin_radprof = \
180180
radial_profile(model, dist_transform, dist_transform,

‎fil_finder/tests/testing_utils.py

-14
Original file line numberDiff line numberDiff line change
@@ -52,20 +52,6 @@ def generate_filament_model(shape=100, pad_size=0,
5252

5353
return fits.PrimaryHDU(filament, header), centers
5454

55-
56-
# def make_skeleton_mask(shape=(256, 256), nfils=1):
57-
# '''
58-
# Generate a skeleton mask defining positions of filaments.
59-
# '''
60-
61-
62-
# mask = np.zeros(shape, dtype=bool)
63-
64-
# for _ in range(nfils):
65-
66-
# # Sample random start and end points
67-
# start = (np.random.randint(shape[0]))
68-
6955
def create_image_header(pixel_scale, beamfwhm, imshape,
7056
restfreq, bunit):
7157
'''

‎setup.cfg

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ github_project = e-koch/FilFinder
1414
[options]
1515
zip_safe = False
1616
packages = find:
17-
python_requires = >=3.7
17+
python_requires = >=3.9
1818
setup_requires = setuptools_scm
1919
install_requires =
2020
astropy

‎tox.ini

+4-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tox]
22
envlist =
3-
py{36,37,38,39}-test{,-all,-dev,-cov}
3+
py{39,310,311}-test{,-all,-dev,-cov}
44
build_docs
55
codestyle
66
requires =
@@ -28,10 +28,11 @@ deps =
2828
extras =
2929
test
3030
all: all
31+
dev: dev
3132
commands =
3233
pip freeze
33-
!cov: pytest --open-files --pyargs fil_finder {toxinidir}/docs {posargs}
34-
cov: pytest --open-files --pyargs fil_finder {toxinidir}/docs --cov fil_finder --cov-config={toxinidir}/setup.cfg {posargs}
34+
!cov: pytest --pyargs fil_finder {toxinidir}/docs {posargs}
35+
cov: pytest --pyargs fil_finder {toxinidir}/docs --cov fil_finder --cov-config={toxinidir}/setup.cfg {posargs}
3536
cov: coverage xml -o {toxinidir}/coverage.xml
3637

3738
[testenv:build_docs]

0 commit comments

Comments
 (0)
Please sign in to comment.