eye-patch 1.0.0a0__tar.gz → 2.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. {eye_patch-1.0.0a0 → eye_patch-2.0.0}/.github/workflows/cd.yml +2 -2
  2. {eye_patch-1.0.0a0 → eye_patch-2.0.0}/.github/workflows/ci.yml +9 -7
  3. {eye_patch-1.0.0a0 → eye_patch-2.0.0}/.pre-commit-config.yaml +5 -4
  4. {eye_patch-1.0.0a0 → eye_patch-2.0.0}/PKG-INFO +9 -9
  5. {eye_patch-1.0.0a0 → eye_patch-2.0.0}/README.md +7 -6
  6. eye_patch-2.0.0/docs/_static/adaptive_mac_dark.png +0 -0
  7. eye_patch-2.0.0/docs/_static/adaptive_mac_light.png +0 -0
  8. eye_patch-2.0.0/docs/_static/open_filter.mp4 +0 -0
  9. eye_patch-2.0.0/docs/masking.md +387 -0
  10. eye_patch-2.0.0/docs/scripts/adaptive_mac_figure.py +304 -0
  11. eye_patch-2.0.0/eye_patch/__main__.py +8 -0
  12. {eye_patch-1.0.0a0 → eye_patch-2.0.0}/eye_patch/_version.py +2 -2
  13. eye_patch-2.0.0/eye_patch/cli.py +49 -0
  14. eye_patch-2.0.0/eye_patch/convolve.py +219 -0
  15. eye_patch-2.0.0/eye_patch/erosion.py +325 -0
  16. eye_patch-2.0.0/eye_patch/extract.py +185 -0
  17. eye_patch-2.0.0/eye_patch/flood_fill.py +198 -0
  18. eye_patch-2.0.0/eye_patch/logging.py +48 -0
  19. eye_patch-2.0.0/eye_patch/mac.py +213 -0
  20. eye_patch-2.0.0/eye_patch/masking.py +313 -0
  21. {eye_patch-1.0.0a0 → eye_patch-2.0.0}/eye_patch/naming.py +7 -5
  22. eye_patch-2.0.0/eye_patch/options.py +49 -0
  23. {eye_patch-1.0.0a0 → eye_patch-2.0.0}/eye_patch/pixel_utils.py +15 -5
  24. eye_patch-2.0.0/eye_patch/snr.py +116 -0
  25. {eye_patch-1.0.0a0 → eye_patch-2.0.0}/pyproject.toml +4 -4
  26. eye_patch-2.0.0/tests/conftest.py +42 -0
  27. eye_patch-2.0.0/tests/test_cli.py +122 -0
  28. eye_patch-2.0.0/tests/test_convolve.py +118 -0
  29. eye_patch-2.0.0/tests/test_erosion.py +337 -0
  30. eye_patch-2.0.0/tests/test_extract.py +180 -0
  31. eye_patch-2.0.0/tests/test_flood_fill.py +68 -0
  32. eye_patch-2.0.0/tests/test_logging.py +45 -0
  33. eye_patch-2.0.0/tests/test_mac.py +97 -0
  34. eye_patch-2.0.0/tests/test_masking.py +410 -0
  35. eye_patch-2.0.0/tests/test_snr.py +41 -0
  36. {eye_patch-1.0.0a0 → eye_patch-2.0.0}/uv.lock +135 -0
  37. eye_patch-1.0.0a0/docs/_static/open_filter.mp4 +0 -0
  38. eye_patch-1.0.0a0/docs/masking.md +0 -214
  39. eye_patch-1.0.0a0/eye_patch/masking.py +0 -1174
  40. eye_patch-1.0.0a0/tests/test_masking.py +0 -514
  41. {eye_patch-1.0.0a0 → eye_patch-2.0.0}/.copier-answers.yml +0 -0
  42. {eye_patch-1.0.0a0 → eye_patch-2.0.0}/.git_archival.txt +0 -0
  43. {eye_patch-1.0.0a0 → eye_patch-2.0.0}/.gitattributes +0 -0
  44. {eye_patch-1.0.0a0 → eye_patch-2.0.0}/.github/CONTRIBUTING.md +0 -0
  45. {eye_patch-1.0.0a0 → eye_patch-2.0.0}/.github/dependabot.yml +0 -0
  46. {eye_patch-1.0.0a0 → eye_patch-2.0.0}/.github/release.yml +0 -0
  47. {eye_patch-1.0.0a0 → eye_patch-2.0.0}/.gitignore +0 -0
  48. {eye_patch-1.0.0a0 → eye_patch-2.0.0}/.readthedocs.yaml +0 -0
  49. {eye_patch-1.0.0a0 → eye_patch-2.0.0}/LICENSE +0 -0
  50. {eye_patch-1.0.0a0 → eye_patch-2.0.0}/docs/Makefile +0 -0
  51. {eye_patch-1.0.0a0 → eye_patch-2.0.0}/docs/_static/multi_scale_erode.jpg +0 -0
  52. {eye_patch-1.0.0a0 → eye_patch-2.0.0}/docs/conf.py +0 -0
  53. {eye_patch-1.0.0a0 → eye_patch-2.0.0}/docs/index.md +0 -0
  54. {eye_patch-1.0.0a0 → eye_patch-2.0.0}/eye_patch/__init__.py +0 -0
  55. {eye_patch-1.0.0a0 → eye_patch-2.0.0}/eye_patch/_version.pyi +0 -0
  56. {eye_patch-1.0.0a0 → eye_patch-2.0.0}/eye_patch/py.typed +0 -0
  57. {eye_patch-1.0.0a0 → eye_patch-2.0.0}/tests/test_package.py +0 -0
@@ -25,11 +25,11 @@ jobs:
25
25
  runs-on: ubuntu-latest
26
26
 
27
27
  steps:
28
- - uses: actions/checkout@v6
28
+ - uses: actions/checkout@v7
29
29
  with:
30
30
  fetch-depth: 0
31
31
 
32
- - uses: hynek/build-and-inspect-python-package@v2
32
+ - uses: hynek/build-and-inspect-python-package@v3.0.1
33
33
 
34
34
  publish:
35
35
  needs: [dist]
@@ -22,12 +22,14 @@ jobs:
22
22
  name: Format
23
23
  runs-on: ubuntu-latest
24
24
  steps:
25
- - uses: actions/checkout@v6
25
+ - uses: actions/checkout@v7
26
26
  with:
27
27
  fetch-depth: 0
28
- - uses: actions/setup-python@v6
28
+ - uses: actions/setup-python@v7
29
29
  with:
30
- python-version: "3.x"
30
+ python-version: |
31
+ 3.11
32
+ 3.x
31
33
 
32
34
  - uses: astral-sh/setup-uv@v7
33
35
 
@@ -42,15 +44,15 @@ jobs:
42
44
  strategy:
43
45
  fail-fast: false
44
46
  matrix:
45
- python-version: ["3.10", "3.12", "3.13", "3.14"]
47
+ python-version: ["3.11", "3.12", "3.13", "3.14"]
46
48
  runs-on: [ubuntu-latest]
47
49
 
48
50
  steps:
49
- - uses: actions/checkout@v6
51
+ - uses: actions/checkout@v7
50
52
  with:
51
53
  fetch-depth: 0
52
54
 
53
- - uses: actions/setup-python@v6
55
+ - uses: actions/setup-python@v7
54
56
  with:
55
57
  python-version: ${{ matrix.python-version }}
56
58
  allow-prereleases: true
@@ -66,6 +68,6 @@ jobs:
66
68
  --durations=20
67
69
 
68
70
  - name: Upload coverage report
69
- uses: codecov/codecov-action@v6
71
+ uses: codecov/codecov-action@v7
70
72
  with:
71
73
  token: ${{ secrets.CODECOV_TOKEN }}
@@ -36,25 +36,26 @@ repos:
36
36
  - id: rst-inline-touching-normal
37
37
 
38
38
  - repo: https://github.com/rbubley/mirrors-prettier
39
- rev: "v3.8.3"
39
+ rev: "v3.9.4"
40
40
  hooks:
41
41
  - id: prettier
42
42
  types_or: [yaml, markdown, html, css, scss, javascript, json]
43
43
  args: [--prose-wrap=always]
44
44
 
45
45
  - repo: https://github.com/astral-sh/ruff-pre-commit
46
- rev: "v0.15.12"
46
+ rev: "v0.15.20"
47
47
  hooks:
48
48
  - id: ruff-check
49
49
  args: ["--fix", "--show-fixes"]
50
50
  - id: ruff-format
51
51
 
52
52
  - repo: https://github.com/pre-commit/mirrors-mypy
53
- rev: "v2.0.0"
53
+ rev: "v2.1.0"
54
54
  hooks:
55
55
  - id: mypy
56
56
  files: eye_patch|tests
57
57
  args: ["--ignore-missing-imports"]
58
+ language_version: python3.11
58
59
  additional_dependencies:
59
60
  - numpy
60
61
  - capn-crunch
@@ -87,7 +88,7 @@ repos:
87
88
  additional_dependencies: ["validate-pyproject-schema-store[all]"]
88
89
 
89
90
  - repo: https://github.com/python-jsonschema/check-jsonschema
90
- rev: "0.37.2"
91
+ rev: "0.37.4"
91
92
  hooks:
92
93
  - id: check-dependabot
93
94
  - id: check-github-workflows
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: eye-patch
3
- Version: 1.0.0a0
3
+ Version: 2.0.0
4
4
  Summary: The pirate mask.
5
5
  Project-URL: Homepage, https://github.com/flint-crew/eye-patch
6
6
  Project-URL: Bug Tracker, https://github.com/flint-crew/eye-patch/issues
@@ -16,7 +16,6 @@ Classifier: Operating System :: OS Independent
16
16
  Classifier: Programming Language :: Python
17
17
  Classifier: Programming Language :: Python :: 3
18
18
  Classifier: Programming Language :: Python :: 3 :: Only
19
- Classifier: Programming Language :: Python :: 3.10
20
19
  Classifier: Programming Language :: Python :: 3.11
21
20
  Classifier: Programming Language :: Python :: 3.12
22
21
  Classifier: Programming Language :: Python :: 3.13
@@ -57,7 +56,7 @@ Description-Content-Type: text/markdown
57
56
 
58
57
  The pirate mask!
59
58
 
60
- A python library and CLI tools for creating masks to assist deconvolution of
59
+ A Python library and CLI tools for creating masks to assist deconvolution of
61
60
  interferometric data.
62
61
 
63
62
  This library was originally the masking module of the
@@ -69,7 +68,7 @@ separated out for flexible use.
69
68
  ## Documentation
70
69
 
71
70
  Full documentation is provided on
72
- [ReadtheDocs](https://eye-patch.readthedocs.io/).
71
+ [Read the Docs](https://eye-patch.readthedocs.io/).
73
72
 
74
73
  ## Installation
75
74
 
@@ -91,7 +90,7 @@ Or, from a local clone:
91
90
 
92
91
  ```bash
93
92
  git clone https://github.com/flint-crew/eye-patch.git
94
- cd flint
93
+ cd eye-patch
95
94
  pip install -e .
96
95
  ```
97
96
 
@@ -100,13 +99,14 @@ installations. Reproducible builds can be created using `uv sync`:
100
99
 
101
100
  ```bash
102
101
  git clone https://github.com/flint-crew/eye-patch.git
102
+ cd eye-patch
103
103
  uv venv
104
104
  uv sync
105
105
  ```
106
106
 
107
107
  ## Contributions
108
108
 
109
- Contributions are welcome! Please do submit a pull-request or issue if you spot
109
+ Contributions are welcome! Please do submit a pull request or issue if you spot
110
110
  something you would like to address.
111
111
 
112
112
  The full set of dev tooling can be installed via:
@@ -114,13 +114,13 @@ The full set of dev tooling can be installed via:
114
114
  ```bash
115
115
  git clone https://github.com/flint-crew/eye-patch.git
116
116
  cd eye-patch
117
- pip install '.[dev]'
117
+ pip install -e . --group dev # --group needs pip 25.1 or newer
118
118
  ```
119
119
 
120
120
  Or, using `uv`:
121
121
 
122
122
  ```bash
123
123
  git clone https://github.com/flint-crew/eye-patch.git
124
- cd flint
124
+ cd eye-patch
125
125
  uv sync --group dev
126
126
  ```
@@ -23,7 +23,7 @@
23
23
 
24
24
  The pirate mask!
25
25
 
26
- A python library and CLI tools for creating masks to assist deconvolution of
26
+ A Python library and CLI tools for creating masks to assist deconvolution of
27
27
  interferometric data.
28
28
 
29
29
  This library was originally the masking module of the
@@ -35,7 +35,7 @@ separated out for flexible use.
35
35
  ## Documentation
36
36
 
37
37
  Full documentation is provided on
38
- [ReadtheDocs](https://eye-patch.readthedocs.io/).
38
+ [Read the Docs](https://eye-patch.readthedocs.io/).
39
39
 
40
40
  ## Installation
41
41
 
@@ -57,7 +57,7 @@ Or, from a local clone:
57
57
 
58
58
  ```bash
59
59
  git clone https://github.com/flint-crew/eye-patch.git
60
- cd flint
60
+ cd eye-patch
61
61
  pip install -e .
62
62
  ```
63
63
 
@@ -66,13 +66,14 @@ installations. Reproducible builds can be created using `uv sync`:
66
66
 
67
67
  ```bash
68
68
  git clone https://github.com/flint-crew/eye-patch.git
69
+ cd eye-patch
69
70
  uv venv
70
71
  uv sync
71
72
  ```
72
73
 
73
74
  ## Contributions
74
75
 
75
- Contributions are welcome! Please do submit a pull-request or issue if you spot
76
+ Contributions are welcome! Please do submit a pull request or issue if you spot
76
77
  something you would like to address.
77
78
 
78
79
  The full set of dev tooling can be installed via:
@@ -80,13 +81,13 @@ The full set of dev tooling can be installed via:
80
81
  ```bash
81
82
  git clone https://github.com/flint-crew/eye-patch.git
82
83
  cd eye-patch
83
- pip install '.[dev]'
84
+ pip install -e . --group dev # --group needs pip 25.1 or newer
84
85
  ```
85
86
 
86
87
  Or, using `uv`:
87
88
 
88
89
  ```bash
89
90
  git clone https://github.com/flint-crew/eye-patch.git
90
- cd flint
91
+ cd eye-patch
91
92
  uv sync --group dev
92
93
  ```
@@ -0,0 +1,387 @@
1
+ # Creating Clean Masks
2
+
3
+ The imaging performance of CLEAN and its derivatives can be improved through the
4
+ construction of reliable clean mask regions. These regions restrict the set of
5
+ pixels that are involved during the peak finding stage of CLEAN. Here the aim of
6
+ the game is to optimally select pixels that contain genuine emission. Issues
7
+ such as clean bias, sub-optimal self-calibration and clean divergence can be
8
+ minimised or outright eliminated provided a clean mask has been reliably
9
+ constructed.
10
+
11
+ `eye-patch` provides functionality to construct such clean masks, where the
12
+ output is a pixel-wise mask of arbitrary shapes that either allow or deny
13
+ cleaning to occur at certain pixel locations. These are intended to be evaluated
14
+ against a restored FITS image. Pixels in the output clean mask are either `0`
15
+ (cleaning is not allowed here) or `1` (cleaning is allowed here), apart from the
16
+ per-scale masks described below.
17
+
18
+ ## Available statistics
19
+
20
+ `eye-patch` currently supports two statistics to identify pixels of significance
21
+ that should be cleaned.
22
+
23
+ ### Signal-to-noise (SNR)
24
+
25
+ The obvious method is based on signal-to-noise (SNR). After constructing
26
+ background ({math}`\mu`) and noise ({math}`\sigma`) measures across an image, a
27
+ direction-dependent SNR ({math}`\mathrm{SNR}`) across the image
28
+ ({math}`\mathrm{img}`) can be expressed as:
29
+
30
+ {math}`\mathrm{SNR}(x,y) = \frac{\left(\mathrm{img}-\mu\right)}{\sigma}.`
31
+
32
+ In the simplest case, constant values may be set across the extent of the image,
33
+ with {math}`\mu=0` and {math}`\sigma=\mathrm{SD}(\mathrm{img})`. These can be
34
+ replaced with more sophisticated schemes that compute position-dependent metrics
35
+ that also incorporate iterative outlier clipping (e.g.
36
+ [Background and Noise Estimator](https://github.com/PaulHancock/Aegean)) or
37
+ robust statistics (e.g.
38
+ [Selavy](https://www.atnf.csiro.au/computing/software/askapsoft/sdp/docs/current/analysis/selavy.html)).
39
+
40
+ At their heart, these SNR-based processes assume Gaussian-distributed, zero-mean
41
+ noise, where pixel intensities that are unlikely to occur by chance are assumed
42
+ to be genuine emission. That is to say, pixels that are {math}`\gg5\sigma` are
43
+ likely to be real and should be included in a mask for cleaning. Without the
44
+ flood fill (see below), the pixels with an SNR above `MaskingOptions.base_clip`
45
+ form the mask.
46
+
47
+ ### Minimum absolute clip
48
+
49
+ SNR-based measures have a clear statistical foundation when identifying bright
50
+ pixels. However, there are situations when such a metric could be perturbed or
51
+ otherwise corrupted, including:
52
+
53
+ - Calibration errors that produce imaging artefacts,
54
+ - Deconvolution errors that accumulate over minor/major iterations,
55
+ - Mishandling of the w-term so that the 2D Fourier transform approximation
56
+ breaks down,
57
+ - Extended diffuse structures that represent a large fraction of the region used
58
+ to calculate the local {math}`\mu` or {math}`\sigma` quantities,
59
+ - Combinations of the above, or
60
+ - Gremlins lurking around in the data.
61
+
62
+ Ultimately, if the regions being considered in the derivation of the noise do
63
+ not appear Gaussian-like, the robustness of methods to calculate the local noise
64
+ level is suspect. Additionally, it is unclear whether an accurate noise
65
+ estimation is even an appropriate basis for such regions. For instance,
66
+ artefacts around a bright source produced by phase error could be
67
+ {math}`\gg\sigma` if the genuine source is sufficiently bright.
68
+
69
+ We introduce the Minimum Absolute Clip ({math}`\mathrm{MAC}`) as an alternative
70
+ metric. It assumes:
71
+
72
+ 1. approximately zero-mean Gaussian noise,
73
+ 2. the sky brightness is positive definite, and
74
+ 3. should there be a significantly bright negative component, there will be a
75
+ positive one of comparable brightness nearby.
76
+
77
+ These assumptions are fair for Stokes-I images. A masking function can therefore
78
+ be constructed, where:
79
+
80
+ {math}`\mathrm{MAC}(x, y) = \mathrm{img} > P \times |\mathrm{RollingMinimum}\left(\mathrm{img}, \mathrm{BoxSize}(\mathrm{img}, x, y)\right)|`
81
+
82
+ where {math}`\mathrm{RollingMinimum}` is a minimum boxcar filter whose size, in
83
+ pixels, is returned by {math}`\mathrm{BoxSize}`, which could be tuned in a
84
+ position-dependent way. The absolute value of the
85
+ {math}`\mathrm{RollingMinimum}` function is multiplied by an increase factor
86
+ {math}`P` to raise the minimum positive threshold. The {math}`\mathrm{MAC}` is a
87
+ simple and efficient statistic to compute.
88
+
89
+ The choice of an appropriate boxcar size is an important consideration. A larger
90
+ size enforces more conservative behaviour from the {math}`\mathrm{MAC}` process.
91
+ Should too small a region be supplied, then large multi-scale features are
92
+ typically less reliably assessed. Extensions can be made to detect when the
93
+ rolling box filter size is too small (e.g. by an imbalance of positive to
94
+ negative pixels). A basic adaptive boxcar size is implemented via the
95
+ `MaskingOptions.mac_adaptive_max_depth`,
96
+ `MaskingOptions.mac_adaptive_step_factor` and
97
+ `MaskingOptions.mac_adaptive_skew_delta` parameters.
98
+
99
+ <!-- The figures are made with docs/scripts/adaptive_mac_figure.py -->
100
+
101
+ ```{figure} _static/adaptive_mac_light.png
102
+ :alt: A cut through an image, where a fixed box gives a clip level that rises far above a wide, bright source and leaves its middle unmasked, while the adaptive box keeps the clip level at the noise and masks the whole source.
103
+ :figclass: only-light
104
+ :width: 100%
105
+
106
+ The top panel is a cut through an image of unit noise (grey), with a compact
107
+ source and a bright source that is wider than the default 75-pixel box. The blue
108
+ and orange lines are the clip levels of the {math}`\mathrm{MAC}`, with an
109
+ increase factor of 4, from a fixed box and from the adaptive box
110
+ (`mac_adaptive_max_depth=3`). A pixel is included in the clean mask wherever the
111
+ image is above the clip level, and the bottom panel shows the resulting masks along the cut, where
112
+ the bars are True. With a fixed box, the rolling minimum in the middle of the
113
+ wide source only sees the source, so the clip level rises above it and the
114
+ middle of the source is missed, leaving a hole. The adaptive box doubles in size
115
+ wherever positive pixels dominate it, so its clip level stays at the noise and
116
+ the whole source is masked. Away from the wide source the two clip levels are
117
+ the same, and their lines overlap.
118
+ ```
119
+
120
+ ```{figure} _static/adaptive_mac_dark.png
121
+ :alt: A cut through an image, where a fixed box gives a clip level that rises far above a wide, bright source and leaves its middle unmasked, while the adaptive box keeps the clip level at the noise and masks the whole source.
122
+ :figclass: only-dark
123
+ :width: 100%
124
+
125
+ The top panel is a cut through an image of unit noise (grey), with a compact
126
+ source and a bright source that is wider than the default 75-pixel box. The blue
127
+ and orange lines are the clip levels of the {math}`\mathrm{MAC}`, with an
128
+ increase factor of 4, from a fixed box and from the adaptive box
129
+ (`mac_adaptive_max_depth=3`). A pixel is included in the clean mask wherever the
130
+ image is above the clip level, and the bottom panel shows the resulting masks along the cut, where
131
+ the bars are True. With a fixed box, the rolling minimum in the middle of the
132
+ wide source only sees the source, so the clip level rises above it and the
133
+ middle of the source is missed, leaving a hole. The adaptive box doubles in size
134
+ wherever positive pixels dominate it, so its clip level stays at the noise and
135
+ the whole source is masked. Away from the wide source the two clip levels are
136
+ the same, and their lines overlap.
137
+ ```
138
+
139
+ The {math}`\mathrm{MAC}` is selected with `MaskingOptions.use_mac`, and may be
140
+ used with or without the flood fill (see below). Without the flood fill, the
141
+ increase factor {math}`P` is `MaskingOptions.base_clip`, and with it, the seed
142
+ and flood clips take its place. As the {math}`\mathrm{MAC}` operates on the
143
+ image itself, no background or noise images are needed.
144
+
145
+ ## Reverse flood filling
146
+
147
+ Cleaning sources with features that are both faint and diffuse is difficult. On
148
+ a per-pixel basis it is often a reality that a minimum signal-to-noise threshold
149
+ is not met. Should the cleaning thresholds (set in either absolute
150
+ {math}`\mathrm{Jy}/\mathrm{beam}` units or in terms of {math}`\sigma` units) be
151
+ too high, this diffuse emission is never cleaned. In `eye-patch` we include a
152
+ reverse flood fill procedure that grows islands of pixels above an initial
153
+ criterion out to adjacent pixels that meet a secondary, lower level. This
154
+ process is the initial step of most source finders.
155
+
156
+ There are two steps to this process:
157
+
158
+ 1. _Initial significant island detection_: The statistic of choice (see above)
159
+ is computed, and all pixels above the threshold are marked as pixels to
160
+ clean. This threshold is set via `MaskingOptions.seed_clip`.
161
+ 2. _Growing the initial set of islands_: Islands that are identified as
162
+ significant go through a dilation process, which is restricted to the pixels
163
+ above a lower threshold. This lower clip is set via
164
+ `MaskingOptions.flood_clip`.
165
+
166
+ The flood fill procedure is 'opt in', and is activated via
167
+ `MaskingOptions.flood_fill`. Either statistic may be used to seed and grow
168
+ islands. With the {math}`\mathrm{MAC}`, the seed and flood clips are increase
169
+ factors {math}`P` of the rolling minimum, rather than SNR levels.
170
+
171
+ ## Islands of faint and diffuse emission
172
+
173
+ Islands of faint and diffuse emission that would otherwise be missed may also be
174
+ added via `MaskingOptions.grow_low_snr_island`, with or without the flood fill.
175
+ Pixels above `MaskingOptions.grow_low_snr_island_clip` are joined together, and
176
+ islands made up of at least `MaskingOptions.grow_low_snr_island_min_pixels`
177
+ pixels are added to the mask. When the {math}`\mathrm{MAC}` statistic is used,
178
+ the clip is an increase factor of the same clipping level as the other clips.
179
+ For a 75-pixel box over noise, this clipping level is around {math}`3.7\sigma`,
180
+ so a factor near `0.5` is comparable to a {math}`1.75\sigma` clip of a signal
181
+ map.
182
+
183
+ This option is 'opt in', and complements rather than replaces the flood fill or
184
+ `MaskingOptions.base_clip`. Its islands are formed separately from the rest of
185
+ the mask, using only `MaskingOptions.grow_low_snr_island_clip` and
186
+ `MaskingOptions.grow_low_snr_island_min_pixels` rather than the seed and flood
187
+ clips, and are then added to the mask. As it only ever adds pixels, it can go
188
+ awry. Anything it picks up in error, such as a large region of noise, artefacts
189
+ or poorly subtracted background above its clip, stays in the mask, as nothing
190
+ later removes such an island. The beam erosion (see below), should it be used,
191
+ only trims the edges of islands. Care should be taken with a low clip or a small
192
+ minimum island size, as both make such errors more likely.
193
+
194
+ <!-- TODO: Need to include some other image here -->
195
+
196
+ ## Eroding output islands
197
+
198
+ Output binary clean masks are naturally at the resolution of the input image (at
199
+ least in `eye-patch`). All pixels that reside in an island are candidate pixels
200
+ where cleaning is allowed to occur. Consider an unresolved source that is
201
+ perfectly aligned to the discrete pixel grid. In principle, its clean component
202
+ should be confined to a single pixel. However, if the source is sufficiently
203
+ bright, the resulting mask is made up of many pixels (i.e. the shape of the
204
+ restoring beam). Should deep cleaning be employed, it is possible that small
205
+ perturbations from the noise underlying a source would also be cleaned.
206
+
207
+ A binary erosion process with the erosion structure set to the shape of the
208
+ restoring beam at a particular power level can be used to 'contract' all pixel
209
+ masks. The output should better reflect where clean components ought to be
210
+ placed. See the `MaskingOptions.beam_shape_erode` and
211
+ `MaskingOptions.beam_shape_erode_minimum_response` options to activate and
212
+ control this process. The minimum response should be set between 0 and 1
213
+ (exclusive), where numbers closer to 0 represent a larger binary erosion
214
+ structure shape. That is to say, islands need to be _larger_ for any pixels to
215
+ remain as `MaskingOptions.beam_shape_erode_minimum_response` approaches 0.
216
+
217
+ ### Multi-scale beam erosion
218
+
219
+ Basic routines to enable per-scale beam shape erosion are provided via
220
+ `MaskingOptions.multiscale_scales`, together with
221
+ `MaskingOptions.beam_shape_erode`. For each specified scale the restoring beam
222
+ (the base shape in this erosion) is convolved with the circular Gaussian that
223
+ wsclean uses for that scale. The resulting beam is then evaluated in the basic
224
+ process outlined above, eroding the clean islands at the native resolution to
225
+ the clean islands at the specified scale.
226
+
227
+ The scales are wsclean multi-scale scales in pixels, and are not the FWHM of the
228
+ Gaussian. wsclean (via radler) forms the Gaussian of a scale with
229
+ {math}`\sigma = 3/16` of the scale, which is a FWHM of about `0.44` of the
230
+ scale. A scale of 16 pixels is therefore a Gaussian with a FWHM of about 7
231
+ pixels. These masks are intended for wsclean's Gaussian scales
232
+ (`-multiscale-shape gaussian`).
233
+
234
+ ```{note}
235
+ Versions of `eye-patch` up to v1.0.1 used each scale as the FWHM of the Gaussian.
236
+ This made the erosion structure of a scale about 2.3 times wider than the
237
+ wsclean Gaussian of the same scale, so larger scales were eroded more than
238
+ intended. Masks at larger scales are now larger than before, and settings tuned
239
+ against the earlier behaviour may need to be revisited. The example image below
240
+ was made with the earlier behaviour.
241
+
242
+ To compare against the earlier behaviour, set
243
+ `MaskingOptions.beam_shape_erode_scales_as_fwhm` (or
244
+ `--beam-shape-erode-scales-as-fwhm` on the CLI). Only the width of each scale is
245
+ restored. Other fixes, such as centring the erosion structure on a pixel, still
246
+ apply.
247
+ ```
248
+
249
+ The clean mask of each scale is stored as a 'bitmapped' value. As the masks are
250
+ floats, a pixel within the mask of the `n`'th scale has `2**n` added to it.
251
+ Scales are first sorted into ascending order, as wsclean reads the `n`'th bit as
252
+ its `n`'th smallest scale.
253
+
254
+ When more than one scale is specified, this bitmapped mask is written to
255
+ `<image>.scalemask.fits`. With a single scale, or without erosion, the mask is
256
+ written to `<image>.mask.fits`. Should the image header be missing the beam
257
+ information, no erosion is performed, and the mask is also written to
258
+ `<image>.mask.fits`.
259
+
260
+ This is intended to provide a mechanism to ensure cleaning at larger scales
261
+ occurs in well-defined regions. An example of the erosion process for
262
+ `0 1 2 4 8 16 32 64 128 256 512 1024 2056` is shown below. Point source islands
263
+ are still in the mask. As they are only present at the first scale, they are
264
+ stored as `2**0 = 1`.
265
+
266
+ ```{image} _static/multi_scale_erode.jpg
267
+ :alt: Example of multi-scale beam erode
268
+ :width: 100%
269
+ ```
270
+
271
+ ## `--convolve-first` option
272
+
273
+ Historically, for extragalactic fields the reverse flood fill process described
274
+ above, using a minimum absolute clip as the threshold statistic, produced
275
+ reliable clean masks. A subsequent binary erosion process to attempt to
276
+ characterise per-scale clean masks produced meaningful results. For regions like
277
+ the Galactic Plane, however, the reverse flood fill process would often miss
278
+ diffuse emission that was both extended and below the flood fill clip level.
279
+ Reducing this lower level clip would not consistently capture such faint
280
+ extended structures, and pixels boosted by noise would often leak into the clean
281
+ mask from legitimate islands (flying spaghetti monsters).
282
+
283
+ The `--convolve-first` option activates a routine where the base image is first
284
+ put through an `open` filter before being convolved to some desired scale, using
285
+ the same Gaussian that wsclean uses for that scale. Significance thresholding
286
+ operations are then applied to this filtered image. Importantly, the `open`
287
+ filter attempts to isolate small structures from larger ones. This is
288
+ implemented through the combined usage of a minimum filter followed by a maximum
289
+ filter (could be considered a high-pass filter). The per-scale clean masks are
290
+ always written to `<image>.scalemask.fits`, using the same bitmapped
291
+ representation as the multi-scale beam erosion.
292
+
293
+ ```{raw} html
294
+ <video width="100%" controls>
295
+ <source src="_static/open_filter.mp4" type="video/mp4">
296
+ Your browser does not support the video tag.
297
+ </video>
298
+ ```
299
+
300
+ Should this option be activated, be aware that the seed and flood clipping
301
+ thresholds change, and it should be possible to use lower limits. For example,
302
+ `seed_clip=1.02` and `flood_clip=0.15` may be used, as these are increase
303
+ factors of the minimum absolute clip, which this option always uses. At the
304
+ native scale (a scale of `0`), the flood clip is raised to at least `0.8`.
305
+
306
+ ## Accessing via the CLI
307
+
308
+ The masking utility may be accessed via the `eye-patch` CLI entry point:
309
+
310
+ ```{argparse}
311
+ :ref: eye_patch.cli.get_parser
312
+ :prog: eye_patch_masking
313
+ ```
314
+
315
+ ## The `MaskingOptions` class
316
+
317
+ Embedded below is the `MaskingOptions` class used to construct clean masks.
318
+ Input values are validated by `pydantic` to ensure they are appropriately typed.
319
+
320
+ ```{literalinclude} ../eye_patch/options.py
321
+ :pyobject: MaskingOptions
322
+ ```
323
+
324
+ ## Changes from v1.0.1 and earlier versions
325
+
326
+ ### Renamed options
327
+
328
+ Several `MaskingOptions` attributes were renamed so that each name describes
329
+ what the option does, rather than where it was first used. The table below maps
330
+ each earlier name to its new name.
331
+
332
+ | Earlier name | New name |
333
+ | ----------------------------------------- | -------------------------------- |
334
+ | `base_snr_clip` | `base_clip` |
335
+ | `flood_fill_positive_seed_clip` | `seed_clip` |
336
+ | `flood_fill_positive_flood_clip` | `flood_clip` |
337
+ | `flood_fill_use_mac` | `use_mac` |
338
+ | `flood_fill_use_mac_box_size` | `mac_box_size` |
339
+ | `flood_fill_use_mac_adaptive_step_factor` | `mac_adaptive_step_factor` |
340
+ | `flood_fill_use_mac_adaptive_skew_delta` | `mac_adaptive_skew_delta` |
341
+ | `flood_fill_use_mac_adaptive_max_depth` | `mac_adaptive_max_depth` |
342
+ | `beam_shape_erode_scales` | `multiscale_scales` |
343
+ | `grow_low_snr_island_size` | `grow_low_snr_island_min_pixels` |
344
+
345
+ The CLI flags follow the same mapping, with dashes in place of underscores. For
346
+ example, `--flood-fill-positive-seed-clip` is now `--seed-clip`.
347
+
348
+ Earlier names are no longer accepted. An earlier name, whether in a strategy
349
+ file or passed to `MaskingOptions`, raises a validation error rather than being
350
+ silently ignored, so a stale setting is caught when it is loaded. The masking
351
+ options of [Flint](https://github.com/flint-crew/flint) use the earlier names,
352
+ so this table also maps a Flint strategy file onto `eye-patch`.
353
+
354
+ ### Changes in behaviour
355
+
356
+ - **The {math}`\mathrm{MAC}` no longer needs the flood fill.** `use_mac`
357
+ (formerly `flood_fill_use_mac`) applies with or without `flood_fill`. Without
358
+ the flood fill, the mask is the pixels above `base_clip` times the
359
+ {math}`\mathrm{MAC}` clipping level. Previously `flood_fill_use_mac` was
360
+ ignored unless `flood_fill` was set, and `base_snr_clip` was applied to the
361
+ signal image instead. As the {math}`\mathrm{MAC}` operates on the image
362
+ itself, no signal image is formed, so the RMS and background images are not
363
+ needed.
364
+ - **Islands of faint and diffuse emission no longer need the flood fill.**
365
+ `grow_low_snr_island` adds its islands to the mask whether or not `flood_fill`
366
+ is set. Previously it was silently ignored without the flood fill. It remains
367
+ opt in, and complements rather than replaces the flood fill or `base_clip`.
368
+ Its islands are formed separately, from `grow_low_snr_island_clip` and
369
+ `grow_low_snr_island_min_pixels` rather than the seed and flood clips, and are
370
+ then added to the mask. As it only ever adds pixels, it can go awry: anything
371
+ it picks up in error, such as a large region of noise, artefacts or poorly
372
+ subtracted background above its clip, stays in the mask. Nothing later removes
373
+ such an island, as the beam erosion, should it be used, only trims the edges
374
+ of islands.
375
+ - **`base_clip` follows the statistic.** With `use_mac`, it is an increase
376
+ factor of the {math}`\mathrm{MAC}` clipping level, as `seed_clip` and
377
+ `flood_clip` already were.
378
+ - **Masks made without the flood fill are written as 32-bit floats**, rather
379
+ than 64-bit floats. Their values are unchanged.
380
+ - **Masks extracted from a mosaic take the shape of their template image.**
381
+ Should the template carry degenerate frequency and Stokes axes, as images
382
+ written by wsclean do, the extracted masks now carry them too, rather than
383
+ always being two-dimensional. Their values are unchanged.
384
+
385
+ Settings that set `flood_fill_use_mac` or `grow_low_snr_island` without
386
+ `flood_fill` now produce a different mask, and should be revisited. The
387
+ `convolve_first` option is unaffected.