stdpipe 0.2.5__tar.gz → 0.4.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.
- {stdpipe-0.2.5/stdpipe.egg-info → stdpipe-0.4.0}/PKG-INFO +21 -8
- {stdpipe-0.2.5 → stdpipe-0.4.0}/README.md +11 -3
- {stdpipe-0.2.5 → stdpipe-0.4.0}/pyproject.toml +40 -7
- {stdpipe-0.2.5 → stdpipe-0.4.0}/stdpipe/__init__.py +1 -1
- stdpipe-0.4.0/stdpipe/artefacts.py +370 -0
- stdpipe-0.4.0/stdpipe/astrometry.py +2001 -0
- stdpipe-0.4.0/stdpipe/astrometry_quad.py +1643 -0
- stdpipe-0.4.0/stdpipe/astrometry_wcs.py +1141 -0
- {stdpipe-0.2.5 → stdpipe-0.4.0}/stdpipe/catalogs.py +442 -248
- {stdpipe-0.2.5 → stdpipe-0.4.0}/stdpipe/cutouts.py +176 -117
- {stdpipe-0.2.5 → stdpipe-0.4.0}/stdpipe/db.py +20 -9
- stdpipe-0.4.0/stdpipe/fringe_removal.py +1086 -0
- stdpipe-0.4.0/stdpipe/lcs.py +440 -0
- stdpipe-0.4.0/stdpipe/photometry.py +2340 -0
- stdpipe-0.4.0/stdpipe/photometry_background.py +998 -0
- stdpipe-0.4.0/stdpipe/photometry_iraf.py +913 -0
- stdpipe-0.4.0/stdpipe/photometry_measure.py +2268 -0
- stdpipe-0.4.0/stdpipe/photometry_model.py +931 -0
- stdpipe-0.4.0/stdpipe/photometry_psf.py +1124 -0
- stdpipe-0.4.0/stdpipe/photometry_quality.py +185 -0
- stdpipe-0.4.0/stdpipe/pipeline.py +1303 -0
- stdpipe-0.4.0/stdpipe/plots.py +1582 -0
- stdpipe-0.4.0/stdpipe/psf.py +1542 -0
- stdpipe-0.4.0/stdpipe/realbogus.py +971 -0
- stdpipe-0.4.0/stdpipe/realbogus_features.py +1657 -0
- stdpipe-0.4.0/stdpipe/reproject.py +943 -0
- stdpipe-0.4.0/stdpipe/resolve.py +267 -0
- stdpipe-0.4.0/stdpipe/sfft.py +1077 -0
- stdpipe-0.4.0/stdpipe/simulation.py +2131 -0
- stdpipe-0.4.0/stdpipe/smoothing.py +488 -0
- stdpipe-0.4.0/stdpipe/subtraction.py +1324 -0
- stdpipe-0.4.0/stdpipe/templates.py +929 -0
- {stdpipe-0.2.5 → stdpipe-0.4.0}/stdpipe/utils.py +33 -42
- {stdpipe-0.2.5 → stdpipe-0.4.0/stdpipe.egg-info}/PKG-INFO +21 -8
- stdpipe-0.4.0/stdpipe.egg-info/SOURCES.txt +77 -0
- {stdpipe-0.2.5 → stdpipe-0.4.0}/stdpipe.egg-info/requires.txt +11 -4
- stdpipe-0.4.0/tests/test_artefacts.py +160 -0
- stdpipe-0.4.0/tests/test_astrometry.py +371 -0
- stdpipe-0.4.0/tests/test_astrometry_quad.py +1881 -0
- stdpipe-0.4.0/tests/test_astrometry_wcs.py +453 -0
- stdpipe-0.4.0/tests/test_background_estimation.py +700 -0
- stdpipe-0.4.0/tests/test_background_fringes.py +547 -0
- stdpipe-0.4.0/tests/test_background_gradients.py +633 -0
- stdpipe-0.4.0/tests/test_background_nongrid.py +1184 -0
- stdpipe-0.4.0/tests/test_background_polynomial.py +216 -0
- stdpipe-0.4.0/tests/test_catalogs.py +373 -0
- stdpipe-0.4.0/tests/test_fringe_removal.py +334 -0
- stdpipe-0.4.0/tests/test_fringe_removal_fourier.py +595 -0
- stdpipe-0.4.0/tests/test_lcs.py +281 -0
- stdpipe-0.4.0/tests/test_moffat_photometry_limits.py +1617 -0
- stdpipe-0.4.0/tests/test_photometry.py +1627 -0
- stdpipe-0.4.0/tests/test_photometry_biases.py +729 -0
- stdpipe-0.4.0/tests/test_photometry_iraf.py +752 -0
- stdpipe-0.4.0/tests/test_photometry_measure.py +2851 -0
- stdpipe-0.4.0/tests/test_photometry_model.py +291 -0
- stdpipe-0.4.0/tests/test_photometry_psf.py +1497 -0
- stdpipe-0.4.0/tests/test_photometry_quality.py +244 -0
- stdpipe-0.4.0/tests/test_pipeline.py +1967 -0
- stdpipe-0.4.0/tests/test_plots.py +231 -0
- stdpipe-0.4.0/tests/test_psf.py +196 -0
- stdpipe-0.4.0/tests/test_realbogus.py +717 -0
- stdpipe-0.4.0/tests/test_realbogus_features.py +700 -0
- stdpipe-0.4.0/tests/test_reproject.py +703 -0
- stdpipe-0.4.0/tests/test_resolve.py +563 -0
- stdpipe-0.4.0/tests/test_sep_psf_bias.py +450 -0
- stdpipe-0.4.0/tests/test_sfft.py +1134 -0
- stdpipe-0.4.0/tests/test_simulation.py +1062 -0
- stdpipe-0.4.0/tests/test_smoothing.py +209 -0
- stdpipe-0.4.0/tests/test_templates.py +734 -0
- stdpipe-0.2.5/.gitignore +0 -85
- stdpipe-0.2.5/stdpipe/astrometry.py +0 -1084
- stdpipe-0.2.5/stdpipe/data/legacysurvey.py +0 -17
- stdpipe-0.2.5/stdpipe/data/ps1skycells.txt.orig +0 -199707
- stdpipe-0.2.5/stdpipe/lcs.py +0 -180
- stdpipe-0.2.5/stdpipe/photometry.py +0 -1363
- stdpipe-0.2.5/stdpipe/pipeline.py +0 -1090
- stdpipe-0.2.5/stdpipe/plots.py +0 -773
- stdpipe-0.2.5/stdpipe/psf.py +0 -459
- stdpipe-0.2.5/stdpipe/resolve.py +0 -142
- stdpipe-0.2.5/stdpipe/subtraction.py +0 -866
- stdpipe-0.2.5/stdpipe/templates.py +0 -972
- stdpipe-0.2.5/stdpipe.egg-info/SOURCES.txt +0 -35
- stdpipe-0.2.5/tests/test_astrometry.py +0 -184
- stdpipe-0.2.5/tests/test_catalogs.py +0 -144
- stdpipe-0.2.5/tests/test_photometry.py +0 -275
- stdpipe-0.2.5/tests/test_pipeline.py +0 -193
- {stdpipe-0.2.5 → stdpipe-0.4.0}/LICENSE.md +0 -0
- {stdpipe-0.2.5 → stdpipe-0.4.0}/MANIFEST.in +0 -0
- {stdpipe-0.2.5 → stdpipe-0.4.0}/setup.cfg +0 -0
- {stdpipe-0.2.5 → stdpipe-0.4.0}/setup.py +0 -0
- {stdpipe-0.2.5 → stdpipe-0.4.0}/stdpipe/data/legacysurvey_bricks.fits.gz +0 -0
- {stdpipe-0.2.5 → stdpipe-0.4.0}/stdpipe/data/ps1skycells.txt +0 -0
- {stdpipe-0.2.5 → stdpipe-0.4.0}/stdpipe.egg-info/dependency_links.txt +0 -0
- {stdpipe-0.2.5 → stdpipe-0.4.0}/stdpipe.egg-info/not-zip-safe +0 -0
- {stdpipe-0.2.5 → stdpipe-0.4.0}/stdpipe.egg-info/top_level.txt +0 -0
- {stdpipe-0.2.5 → stdpipe-0.4.0}/tests/test_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: stdpipe
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Simple Transient Detection Pipeline
|
|
5
5
|
Author-email: Sergey Karpov <karpov.sv@gmail.com>
|
|
6
6
|
Maintainer-email: Sergey Karpov <karpov.sv@gmail.com>
|
|
@@ -53,16 +53,17 @@ Requires-Dist: scipy
|
|
|
53
53
|
Requires-Dist: astropy>=4.1
|
|
54
54
|
Requires-Dist: matplotlib
|
|
55
55
|
Requires-Dist: astroquery>0.4.1
|
|
56
|
-
Requires-Dist: sep
|
|
56
|
+
Requires-Dist: sep-x
|
|
57
57
|
Requires-Dist: astroscrappy
|
|
58
|
-
Requires-Dist: photutils
|
|
58
|
+
Requires-Dist: photutils>=2.2
|
|
59
|
+
Requires-Dist: scikit-image>=0.22.0
|
|
59
60
|
Requires-Dist: statsmodels
|
|
60
61
|
Requires-Dist: tqdm
|
|
61
62
|
Requires-Dist: regions
|
|
62
63
|
Requires-Dist: python-dateutil
|
|
63
64
|
Requires-Dist: requests
|
|
64
|
-
Requires-Dist: sip_tpv
|
|
65
65
|
Requires-Dist: pyfftw
|
|
66
|
+
Requires-Dist: scikit-learn
|
|
66
67
|
Provides-Extra: dev
|
|
67
68
|
Requires-Dist: pytest>=6.0; extra == "dev"
|
|
68
69
|
Requires-Dist: pytest-cov>=2.10; extra == "dev"
|
|
@@ -79,8 +80,12 @@ Requires-Dist: numpydoc; extra == "docs"
|
|
|
79
80
|
Requires-Dist: myst-parser; extra == "docs"
|
|
80
81
|
Requires-Dist: sphinxcontrib-apidoc; extra == "docs"
|
|
81
82
|
Requires-Dist: sphinx-prompt; extra == "docs"
|
|
83
|
+
Provides-Extra: adaptive
|
|
84
|
+
Requires-Dist: powerbin; extra == "adaptive"
|
|
85
|
+
Provides-Extra: ml
|
|
86
|
+
Requires-Dist: tensorflow<2.20,>=2.10; extra == "ml"
|
|
82
87
|
Provides-Extra: all
|
|
83
|
-
Requires-Dist: stdpipe[dev,docs]; extra == "all"
|
|
88
|
+
Requires-Dist: stdpipe[adaptive,dev,docs,ml]; extra == "all"
|
|
84
89
|
Dynamic: license-file
|
|
85
90
|
|
|
86
91
|
# STDPipe - Simple Transient Detection Pipeline
|
|
@@ -100,20 +105,28 @@ Dynamic: license-file
|
|
|
100
105
|
- ~~pre-processing~~ - should be handled before in an instrument-specific way
|
|
101
106
|
- bias/dark subtraction, flatfielding, masking
|
|
102
107
|
- object detection and photometry
|
|
103
|
-
- SExtractor or
|
|
108
|
+
- SExtractor, SEP, or photutils (pure Python) for detection
|
|
109
|
+
- aperture, optimal extraction (Naylor 1998), and PSF fitting photometry
|
|
110
|
+
- grouped fitting for crowded fields
|
|
104
111
|
- astrometric calibration
|
|
105
112
|
- Astrometry.Net for blind WCS solving
|
|
106
|
-
-
|
|
113
|
+
- quad-hash pattern matching for refinement (pure Python, no external deps, default method)
|
|
114
|
+
- SCAMP or Astropy-based code for refinement (alternative)
|
|
107
115
|
- photometric calibration
|
|
108
116
|
- Vizier catalogues, passband conversion (PS1 to Johnson, Gaia to Johnson, ...)
|
|
109
117
|
- spatial polynomial + color term + intrinsic scatter
|
|
118
|
+
- image reprojection and stacking
|
|
119
|
+
- Lanczos interpolation with automatic oversampling and flux conservation (pure Python, default)
|
|
120
|
+
- SWarp wrapper (alternative)
|
|
110
121
|
- image subtraction
|
|
111
122
|
- HiPS templates
|
|
112
123
|
- PanSTARRS DR1 or Legacy Survey templates
|
|
113
124
|
- HOTPANTS + custom noise model
|
|
114
125
|
- ZOGY
|
|
115
|
-
- transient detection and
|
|
126
|
+
- transient detection and classification
|
|
116
127
|
- noise-weighted detection, cutout adjustment, ...
|
|
128
|
+
- feature-based real/bogus classification (no TensorFlow required)
|
|
129
|
+
- CNN-based real/bogus classification (optional, requires TensorFlow)
|
|
117
130
|
- auxiliary functions
|
|
118
131
|
- PSF estimation, simulated stars, FITS header utilities, plotting, ...
|
|
119
132
|
- light curve creation (soon)
|
|
@@ -15,20 +15,28 @@
|
|
|
15
15
|
- ~~pre-processing~~ - should be handled before in an instrument-specific way
|
|
16
16
|
- bias/dark subtraction, flatfielding, masking
|
|
17
17
|
- object detection and photometry
|
|
18
|
-
- SExtractor or
|
|
18
|
+
- SExtractor, SEP, or photutils (pure Python) for detection
|
|
19
|
+
- aperture, optimal extraction (Naylor 1998), and PSF fitting photometry
|
|
20
|
+
- grouped fitting for crowded fields
|
|
19
21
|
- astrometric calibration
|
|
20
22
|
- Astrometry.Net for blind WCS solving
|
|
21
|
-
-
|
|
23
|
+
- quad-hash pattern matching for refinement (pure Python, no external deps, default method)
|
|
24
|
+
- SCAMP or Astropy-based code for refinement (alternative)
|
|
22
25
|
- photometric calibration
|
|
23
26
|
- Vizier catalogues, passband conversion (PS1 to Johnson, Gaia to Johnson, ...)
|
|
24
27
|
- spatial polynomial + color term + intrinsic scatter
|
|
28
|
+
- image reprojection and stacking
|
|
29
|
+
- Lanczos interpolation with automatic oversampling and flux conservation (pure Python, default)
|
|
30
|
+
- SWarp wrapper (alternative)
|
|
25
31
|
- image subtraction
|
|
26
32
|
- HiPS templates
|
|
27
33
|
- PanSTARRS DR1 or Legacy Survey templates
|
|
28
34
|
- HOTPANTS + custom noise model
|
|
29
35
|
- ZOGY
|
|
30
|
-
- transient detection and
|
|
36
|
+
- transient detection and classification
|
|
31
37
|
- noise-weighted detection, cutout adjustment, ...
|
|
38
|
+
- feature-based real/bogus classification (no TensorFlow required)
|
|
39
|
+
- CNN-based real/bogus classification (optional, requires TensorFlow)
|
|
32
40
|
- auxiliary functions
|
|
33
41
|
- PSF estimation, simulated stars, FITS header utilities, plotting, ...
|
|
34
42
|
- light curve creation (soon)
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "stdpipe"
|
|
7
|
-
|
|
7
|
+
dynamic = ["version"]
|
|
8
8
|
description = "Simple Transient Detection Pipeline"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {file = "LICENSE.md"}
|
|
@@ -47,17 +47,17 @@ dependencies = [
|
|
|
47
47
|
"astropy>=4.1",
|
|
48
48
|
"matplotlib",
|
|
49
49
|
"astroquery>0.4.1",
|
|
50
|
-
"sep",
|
|
50
|
+
"sep-x",
|
|
51
51
|
"astroscrappy",
|
|
52
|
-
"photutils
|
|
52
|
+
"photutils>=2.2",
|
|
53
|
+
"scikit-image>=0.22.0", # Requires >=0.22.0 for NumPy 2.0 compatibility
|
|
53
54
|
"statsmodels",
|
|
54
55
|
"tqdm",
|
|
55
56
|
"regions",
|
|
56
57
|
"python-dateutil",
|
|
57
58
|
"requests",
|
|
58
|
-
# "sip_tpv @ git+https://github.com/pmvreeswijk/sip_tpv.git",
|
|
59
|
-
"sip_tpv",
|
|
60
59
|
"pyfftw",
|
|
60
|
+
"scikit-learn",
|
|
61
61
|
]
|
|
62
62
|
|
|
63
63
|
[project.optional-dependencies]
|
|
@@ -83,9 +83,19 @@ docs = [
|
|
|
83
83
|
"sphinx-prompt",
|
|
84
84
|
]
|
|
85
85
|
|
|
86
|
+
# Adaptive binning (Voronoi tessellation)
|
|
87
|
+
adaptive = [
|
|
88
|
+
"powerbin",
|
|
89
|
+
]
|
|
90
|
+
|
|
91
|
+
# Machine learning for real-bogus classifier
|
|
92
|
+
ml = [
|
|
93
|
+
"tensorflow>=2.10,<2.20",
|
|
94
|
+
]
|
|
95
|
+
|
|
86
96
|
# All optional dependencies
|
|
87
97
|
all = [
|
|
88
|
-
"stdpipe[dev,docs]",
|
|
98
|
+
"stdpipe[dev,docs,adaptive,ml]",
|
|
89
99
|
]
|
|
90
100
|
|
|
91
101
|
[project.urls]
|
|
@@ -103,13 +113,31 @@ Changelog = "https://github.com/karpov-sv/stdpipe/commits/master"
|
|
|
103
113
|
zip-safe = false
|
|
104
114
|
include-package-data = true
|
|
105
115
|
|
|
116
|
+
# Single source of truth for the version - parsed statically from stdpipe/__init__.py
|
|
117
|
+
[tool.setuptools.dynamic]
|
|
118
|
+
version = {attr = "stdpipe.__version__"}
|
|
119
|
+
|
|
106
120
|
[tool.setuptools.packages.find]
|
|
107
121
|
where = ["."]
|
|
108
122
|
include = ["stdpipe*"]
|
|
109
123
|
exclude = ["tests*", "docs*", "notebooks*", "build*", "dist*"]
|
|
124
|
+
# stdpipe/data/ has no __init__.py and is not meant to be importable - without
|
|
125
|
+
# this it would be picked up as a namespace package, shipping the scripts in it
|
|
126
|
+
namespaces = false
|
|
110
127
|
|
|
128
|
+
# Listed explicitly rather than globbed, so that stray files in the working
|
|
129
|
+
# directory (editor backups etc) can never end up in a release. Note that
|
|
130
|
+
# data/legacysurvey.py is a one-off regeneration script, not shipped on purpose.
|
|
111
131
|
[tool.setuptools.package-data]
|
|
112
|
-
stdpipe = [
|
|
132
|
+
stdpipe = [
|
|
133
|
+
"data/legacysurvey_bricks.fits.gz",
|
|
134
|
+
"data/ps1skycells.txt",
|
|
135
|
+
]
|
|
136
|
+
|
|
137
|
+
# data/legacysurvey.py is a one-off script for regenerating the bricks list, and
|
|
138
|
+
# is picked up by the generic "all .py files" sdist rule - keep it out of the wheel
|
|
139
|
+
[tool.setuptools.exclude-package-data]
|
|
140
|
+
stdpipe = ["data/legacysurvey.py"]
|
|
113
141
|
|
|
114
142
|
# ============================================================================
|
|
115
143
|
# pytest configuration
|
|
@@ -126,6 +154,7 @@ addopts = [
|
|
|
126
154
|
"--strict-markers",
|
|
127
155
|
"--showlocals",
|
|
128
156
|
"-ra",
|
|
157
|
+
"-m", "not slow",
|
|
129
158
|
]
|
|
130
159
|
markers = [
|
|
131
160
|
"unit: Unit tests that don't require external dependencies",
|
|
@@ -136,6 +165,7 @@ markers = [
|
|
|
136
165
|
"requires_psfex: Tests requiring PSFEx to be installed",
|
|
137
166
|
"requires_hotpants: Tests requiring HOTPANTS to be installed",
|
|
138
167
|
"requires_astrometry_net: Tests requiring Astrometry.Net to be installed",
|
|
168
|
+
"requires_pyraf: Tests requiring PyRAF/IRAF to be installed",
|
|
139
169
|
"requires_network: Tests that require network access",
|
|
140
170
|
]
|
|
141
171
|
filterwarnings = [
|
|
@@ -226,6 +256,9 @@ extend-exclude = [
|
|
|
226
256
|
"dist",
|
|
227
257
|
]
|
|
228
258
|
|
|
259
|
+
[tool.ruff.format]
|
|
260
|
+
quote-style = "preserve"
|
|
261
|
+
|
|
229
262
|
[tool.ruff.lint]
|
|
230
263
|
select = [
|
|
231
264
|
"E", # pycodestyle errors
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
# Various artefact filtering routines
|
|
2
|
+
#
|
|
3
|
+
# For more advanced real-bogus classification with cutout-based morphological
|
|
4
|
+
# features, see the realbogus_features module:
|
|
5
|
+
#
|
|
6
|
+
# from stdpipe import realbogus_features as rbf
|
|
7
|
+
# obj = rbf.classify(obj, image, method='hybrid', classifier='scoring')
|
|
8
|
+
#
|
|
9
|
+
# The realbogus_features module provides:
|
|
10
|
+
# - Catalog-only, cutout-only, or hybrid feature extraction
|
|
11
|
+
# - Multiple classifiers: scoring (no training), IsolationForest, RandomForest
|
|
12
|
+
# - Generalized trend removal for all features
|
|
13
|
+
# - Training utilities for custom classifiers
|
|
14
|
+
|
|
15
|
+
import numpy as np
|
|
16
|
+
from sklearn.ensemble import IsolationForest
|
|
17
|
+
|
|
18
|
+
from . import smoothing
|
|
19
|
+
from .realbogus_features import FLAG_BOGUS, FLAGS_UNRELIABLE_MASK
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _has_column(obj, name):
|
|
23
|
+
"""Check whether a table-like or dict-like catalog has a column."""
|
|
24
|
+
colnames = getattr(obj, 'colnames', None)
|
|
25
|
+
if colnames is not None:
|
|
26
|
+
return name in colnames
|
|
27
|
+
if hasattr(obj, 'keys'):
|
|
28
|
+
return name in obj.keys()
|
|
29
|
+
names = getattr(getattr(obj, 'dtype', None), 'names', None)
|
|
30
|
+
return names is not None and name in names
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _get_catalog_features(obj):
|
|
34
|
+
"""
|
|
35
|
+
Build the feature list from available catalog columns.
|
|
36
|
+
|
|
37
|
+
Prefers SExtractor-style columns (FLUX_RADIUS, FLUX_MAX/FLUX_AUTO) and
|
|
38
|
+
falls back to SEP-style ones (peak/flux) where possible, so the same
|
|
39
|
+
filter works on both `get_objects_sextractor()` and `get_objects_sep()`
|
|
40
|
+
catalogs.
|
|
41
|
+
"""
|
|
42
|
+
features = []
|
|
43
|
+
|
|
44
|
+
if _has_column(obj, 'FLUX_RADIUS'):
|
|
45
|
+
features.append([np.asarray(obj['FLUX_RADIUS'], dtype=float), 'FLUX_RADIUS'])
|
|
46
|
+
|
|
47
|
+
for name in ('fwhm', 'FWHM_IMAGE'):
|
|
48
|
+
if _has_column(obj, name):
|
|
49
|
+
features.append([np.asarray(obj[name], dtype=float), 'FWHM'])
|
|
50
|
+
break
|
|
51
|
+
|
|
52
|
+
for num, den in (('FLUX_MAX', 'FLUX_AUTO'), ('peak', 'flux')):
|
|
53
|
+
if _has_column(obj, num) and _has_column(obj, den):
|
|
54
|
+
with np.errstate(divide='ignore', invalid='ignore'):
|
|
55
|
+
ratio = np.asarray(obj[num], dtype=float) / np.asarray(obj[den], dtype=float)
|
|
56
|
+
features.append([ratio, f'{num} / {den}'])
|
|
57
|
+
break
|
|
58
|
+
|
|
59
|
+
return features
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def filter_sextractor_detections(
|
|
63
|
+
obj,
|
|
64
|
+
trend_cols=['x', 'y', 'MAG_AUTO'],
|
|
65
|
+
trend_scales=None,
|
|
66
|
+
contamination='auto',
|
|
67
|
+
return_features=False,
|
|
68
|
+
return_classifier=False,
|
|
69
|
+
random_state=0,
|
|
70
|
+
verbose=True,
|
|
71
|
+
**kwargs,
|
|
72
|
+
):
|
|
73
|
+
"""
|
|
74
|
+
Flag detections likely to be artefacts using IsolationForest.
|
|
75
|
+
|
|
76
|
+
Builds feature vectors from FLUX_RADIUS, FWHM, and peakiness
|
|
77
|
+
(FLUX_MAX/FLUX_AUTO, or peak/flux for SEP catalogs), using whichever
|
|
78
|
+
columns are present. Optionally removes smooth spatial trends (e.g.,
|
|
79
|
+
across x/y/MAG_AUTO) via an approximate LOESS regressor before fitting
|
|
80
|
+
the outlier model.
|
|
81
|
+
|
|
82
|
+
Expected columns in `obj`
|
|
83
|
+
-------------------------
|
|
84
|
+
Required:
|
|
85
|
+
- flags
|
|
86
|
+
- at least one of the feature columns: FLUX_RADIUS; fwhm or FWHM_IMAGE;
|
|
87
|
+
FLUX_MAX + FLUX_AUTO or peak + flux
|
|
88
|
+
Trend columns (when `trend_cols` is set):
|
|
89
|
+
- columns named in `trend_cols` (default: x, y, MAG_AUTO); missing ones
|
|
90
|
+
are dropped with a log message
|
|
91
|
+
|
|
92
|
+
Parameters
|
|
93
|
+
----------
|
|
94
|
+
obj : array-like / table
|
|
95
|
+
Detection catalog with required columns.
|
|
96
|
+
trend_cols : list[str] or None
|
|
97
|
+
Columns used to model smooth trends; set to None/[] to skip detrending.
|
|
98
|
+
trend_scales : list[float] or None
|
|
99
|
+
Per-dimension scaling for LOESS distances; must match trend_cols
|
|
100
|
+
length. If None (default), scales are auto-computed as ~3x the
|
|
101
|
+
standard deviation of each trend column.
|
|
102
|
+
contamination : float or 'auto'
|
|
103
|
+
Expected fraction of artefacts, passed to IsolationForest. With
|
|
104
|
+
'auto' (default) the threshold follows the original paper and a
|
|
105
|
+
data-dependent fraction is flagged even on clean catalogs; set an
|
|
106
|
+
explicit value (e.g. 0.02) to control the rejection rate.
|
|
107
|
+
return_features : bool
|
|
108
|
+
If True, return the feature list (arrays + labels) without fitting.
|
|
109
|
+
return_classifier : bool
|
|
110
|
+
If True, return a callable that classifies new catalogs.
|
|
111
|
+
random_state : int
|
|
112
|
+
Random seed for IsolationForest.
|
|
113
|
+
verbose : bool or callable
|
|
114
|
+
Logging control; can be a print-like function.
|
|
115
|
+
**kwargs :
|
|
116
|
+
Extra arguments passed to `ApproxLoessRegressor` (e.g., k, robust_iters).
|
|
117
|
+
|
|
118
|
+
Returns
|
|
119
|
+
-------
|
|
120
|
+
good : ndarray[bool] or callable
|
|
121
|
+
Boolean mask of “good” detections, or a classifier callable if
|
|
122
|
+
return_classifier is True.
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
# Simple wrapper around print for logging in verbose mode only
|
|
126
|
+
log = (verbose if callable(verbose) else print) if verbose else lambda *args, **kwargs: None
|
|
127
|
+
|
|
128
|
+
features = _get_catalog_features(obj)
|
|
129
|
+
|
|
130
|
+
if not features:
|
|
131
|
+
raise ValueError(
|
|
132
|
+
"No usable feature columns found in the catalog "
|
|
133
|
+
"(need FLUX_RADIUS, fwhm/FWHM_IMAGE, FLUX_MAX+FLUX_AUTO or peak+flux)"
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
if return_features:
|
|
137
|
+
return features
|
|
138
|
+
|
|
139
|
+
log(
|
|
140
|
+
"Using isolation forest outlier detection over columns ({})".format(
|
|
141
|
+
", ".join([_[1] for _ in features])
|
|
142
|
+
)
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
# LOESS neighbor count; popped here so it does not leak into other kwargs
|
|
146
|
+
k = kwargs.pop('k', 20)
|
|
147
|
+
|
|
148
|
+
# Exclude blends etc from the fit, as well as broken measurements;
|
|
149
|
+
# FLAG_BOGUS itself is ignored so re-running the filter is idempotent
|
|
150
|
+
idx = (obj['flags'] & FLAGS_UNRELIABLE_MASK) == 0
|
|
151
|
+
for f in features:
|
|
152
|
+
idx &= np.isfinite(f[0]) & (f[0] > 0)
|
|
153
|
+
|
|
154
|
+
pos = None
|
|
155
|
+
if trend_cols:
|
|
156
|
+
# Drop trend columns missing from the catalog (keeping user-provided
|
|
157
|
+
# scales aligned with the surviving columns)
|
|
158
|
+
available = [_ for _ in trend_cols if _has_column(obj, _)]
|
|
159
|
+
if trend_scales is not None and len(trend_scales) != len(trend_cols):
|
|
160
|
+
raise ValueError("trend_scales length is inconsistent with trend_cols length")
|
|
161
|
+
if len(available) < len(trend_cols):
|
|
162
|
+
missing = [_ for _ in trend_cols if _ not in available]
|
|
163
|
+
log("Trend columns not found in catalog: {}".format(", ".join(missing)))
|
|
164
|
+
if trend_scales is not None:
|
|
165
|
+
trend_scales = [s for c, s in zip(trend_cols, trend_scales) if c in available]
|
|
166
|
+
trend_cols = available
|
|
167
|
+
|
|
168
|
+
if trend_cols:
|
|
169
|
+
log("Removing smooth trends in {} using approximate LOESS".format(", ".join(trend_cols)))
|
|
170
|
+
|
|
171
|
+
if trend_scales is None:
|
|
172
|
+
# Auto-compute per-dimension scales as ~3 sigma of each column
|
|
173
|
+
trend_scales = []
|
|
174
|
+
for col in trend_cols:
|
|
175
|
+
scale = 3.0 * np.nanstd(np.asarray(obj[col], dtype=float))
|
|
176
|
+
if not np.isfinite(scale) or scale <= 0:
|
|
177
|
+
scale = 1.0
|
|
178
|
+
trend_scales.append(scale)
|
|
179
|
+
log(
|
|
180
|
+
"Auto trend scales: {}".format(
|
|
181
|
+
", ".join([f"{c}={s:.3g}" for c, s in zip(trend_cols, trend_scales)])
|
|
182
|
+
)
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
pos = np.column_stack([np.array(obj[_]) for _ in trend_cols])
|
|
186
|
+
# Rows with non-finite trend columns cannot be detrended and would
|
|
187
|
+
# get NaN features anyway; keep them out of the fits below
|
|
188
|
+
idx &= np.all(np.isfinite(pos), axis=1)
|
|
189
|
+
|
|
190
|
+
if np.sum(idx) < 3:
|
|
191
|
+
raise ValueError(
|
|
192
|
+
f"Too few clean detections ({np.sum(idx)}) to fit the outlier model"
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
if trend_cols:
|
|
196
|
+
trend_models = []
|
|
197
|
+
X = []
|
|
198
|
+
|
|
199
|
+
for f in features:
|
|
200
|
+
model = smoothing.ApproxLoessRegressor(k=k, scales=trend_scales, **kwargs)
|
|
201
|
+
model.fit(pos[idx], f[0][idx])
|
|
202
|
+
trend_models.append(model)
|
|
203
|
+
X.append(np.array(f[0]) - model.predict(pos))
|
|
204
|
+
|
|
205
|
+
else:
|
|
206
|
+
trend_models = None
|
|
207
|
+
X = [np.array(_[0]) for _ in features]
|
|
208
|
+
|
|
209
|
+
X = np.column_stack(X)
|
|
210
|
+
X[~np.isfinite(X)] = -100000 # Definitely outside of the good locus
|
|
211
|
+
|
|
212
|
+
clf = IsolationForest(contamination=contamination, random_state=random_state).fit(X[idx])
|
|
213
|
+
|
|
214
|
+
res = clf.predict(X)
|
|
215
|
+
|
|
216
|
+
log(f"{np.sum(res > 0)} good, {np.sum(res < 0)} outliers")
|
|
217
|
+
|
|
218
|
+
if return_classifier:
|
|
219
|
+
|
|
220
|
+
def classifier(obj):
|
|
221
|
+
features = _get_catalog_features(obj)
|
|
222
|
+
|
|
223
|
+
if trend_cols:
|
|
224
|
+
pos = np.column_stack([np.array(obj[_]) for _ in trend_cols])
|
|
225
|
+
X = []
|
|
226
|
+
|
|
227
|
+
for f, model in zip(features, trend_models):
|
|
228
|
+
X.append(np.array(f[0]) - model.predict(pos))
|
|
229
|
+
else:
|
|
230
|
+
X = [np.array(_[0]) for _ in features]
|
|
231
|
+
|
|
232
|
+
X = np.column_stack(X)
|
|
233
|
+
X[~np.isfinite(X)] = -100000 # Definitely outside of the good locus
|
|
234
|
+
|
|
235
|
+
return clf.predict(X) > 0
|
|
236
|
+
|
|
237
|
+
return classifier
|
|
238
|
+
|
|
239
|
+
return res > 0
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def filter_detections(
|
|
243
|
+
obj,
|
|
244
|
+
image=None,
|
|
245
|
+
bg=None,
|
|
246
|
+
err=None,
|
|
247
|
+
mask=None,
|
|
248
|
+
fwhm=None,
|
|
249
|
+
method='auto',
|
|
250
|
+
classifier='isolation',
|
|
251
|
+
threshold=0.5,
|
|
252
|
+
remove_trend=True,
|
|
253
|
+
trend_cols=None,
|
|
254
|
+
trend_scales=None,
|
|
255
|
+
add_score=False,
|
|
256
|
+
flag_bogus=False,
|
|
257
|
+
verbose=True,
|
|
258
|
+
**kwargs,
|
|
259
|
+
):
|
|
260
|
+
"""
|
|
261
|
+
Filter detections using feature-based real-bogus classification.
|
|
262
|
+
|
|
263
|
+
This is a convenience wrapper around realbogus_features.classify() that
|
|
264
|
+
provides a simpler interface similar to filter_sextractor_detections().
|
|
265
|
+
|
|
266
|
+
For full control over feature extraction and classification, use
|
|
267
|
+
realbogus_features.classify() directly.
|
|
268
|
+
|
|
269
|
+
Parameters
|
|
270
|
+
----------
|
|
271
|
+
obj : astropy.table.Table
|
|
272
|
+
Object catalog with 'x', 'y' columns. Modified in place when
|
|
273
|
+
`add_score` or `flag_bogus` is set.
|
|
274
|
+
image : ndarray, optional
|
|
275
|
+
Science image. If provided, cutout features will be extracted.
|
|
276
|
+
bg : ndarray or float, optional
|
|
277
|
+
Background map or scalar.
|
|
278
|
+
err : ndarray or float, optional
|
|
279
|
+
Error/noise map or scalar.
|
|
280
|
+
mask : ndarray, optional
|
|
281
|
+
Boolean mask (True = masked).
|
|
282
|
+
fwhm : float, optional
|
|
283
|
+
Image FWHM. If None, estimated from catalog.
|
|
284
|
+
method : str, optional
|
|
285
|
+
Feature extraction method:
|
|
286
|
+
- 'catalog': Catalog features only (no image needed)
|
|
287
|
+
- 'cutout': Cutout features only
|
|
288
|
+
- 'hybrid': Both catalog and cutout features
|
|
289
|
+
- 'auto': 'hybrid' if image provided, else 'catalog'
|
|
290
|
+
classifier : str, optional
|
|
291
|
+
Classifier to use:
|
|
292
|
+
- 'scoring': Rule-based scoring (no training needed)
|
|
293
|
+
- 'isolation': IsolationForest (unsupervised, default)
|
|
294
|
+
threshold : float, optional
|
|
295
|
+
Score threshold for classification. Default: 0.5.
|
|
296
|
+
remove_trend : bool, optional
|
|
297
|
+
Remove spatial trends from features. Default: True.
|
|
298
|
+
trend_cols : list of str, optional
|
|
299
|
+
Columns for trend removal. Default: ['x', 'y'].
|
|
300
|
+
trend_scales : list of float, optional
|
|
301
|
+
Scales for trend removal. Default: auto-computed.
|
|
302
|
+
add_score : bool, optional
|
|
303
|
+
Add 'rb_score' column to the input catalog. Default: False.
|
|
304
|
+
flag_bogus : bool, optional
|
|
305
|
+
Set FLAG_BOGUS (0x4000) on bogus objects in the input catalog.
|
|
306
|
+
Default: False.
|
|
307
|
+
verbose : bool, optional
|
|
308
|
+
Print progress.
|
|
309
|
+
**kwargs
|
|
310
|
+
Additional arguments passed to realbogus_features.classify().
|
|
311
|
+
|
|
312
|
+
Returns
|
|
313
|
+
-------
|
|
314
|
+
good : ndarray[bool]
|
|
315
|
+
Boolean mask of "good" (real) detections.
|
|
316
|
+
|
|
317
|
+
See Also
|
|
318
|
+
--------
|
|
319
|
+
realbogus_features.classify : Full-featured classification function.
|
|
320
|
+
filter_sextractor_detections : Original SExtractor-specific filter.
|
|
321
|
+
|
|
322
|
+
Examples
|
|
323
|
+
--------
|
|
324
|
+
>>> # Simple catalog-only filtering (like original function)
|
|
325
|
+
>>> good = filter_detections(obj, classifier='isolation')
|
|
326
|
+
|
|
327
|
+
>>> # Cutout-based filtering with scoring (no training)
|
|
328
|
+
>>> good = filter_detections(obj, image, classifier='scoring')
|
|
329
|
+
|
|
330
|
+
>>> # Hybrid with trend removal
|
|
331
|
+
>>> good = filter_detections(obj, image, method='hybrid',
|
|
332
|
+
... remove_trend=True, trend_cols=['x', 'y'])
|
|
333
|
+
"""
|
|
334
|
+
from . import realbogus_features as rbf
|
|
335
|
+
|
|
336
|
+
log = (verbose if callable(verbose) else print) if verbose else lambda *args, **kwargs: None
|
|
337
|
+
|
|
338
|
+
# Call the full classify function; the score is always needed to build
|
|
339
|
+
# the output mask
|
|
340
|
+
result = rbf.classify(
|
|
341
|
+
obj,
|
|
342
|
+
image=image,
|
|
343
|
+
bg=bg,
|
|
344
|
+
err=err,
|
|
345
|
+
mask=mask,
|
|
346
|
+
fwhm=fwhm,
|
|
347
|
+
method=method,
|
|
348
|
+
classifier=classifier,
|
|
349
|
+
threshold=threshold,
|
|
350
|
+
add_score=True,
|
|
351
|
+
flag_bogus=flag_bogus,
|
|
352
|
+
remove_trend=remove_trend,
|
|
353
|
+
trend_cols=trend_cols,
|
|
354
|
+
trend_scales=trend_scales,
|
|
355
|
+
verbose=verbose,
|
|
356
|
+
**kwargs,
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
good = np.asarray(result['rb_score'] >= threshold)
|
|
360
|
+
|
|
361
|
+
# classify() works on a copy; propagate the requested columns back to
|
|
362
|
+
# the caller's catalog so add_score/flag_bogus have a visible effect
|
|
363
|
+
if add_score:
|
|
364
|
+
obj['rb_score'] = result['rb_score']
|
|
365
|
+
if flag_bogus:
|
|
366
|
+
obj['flags'] = result['flags']
|
|
367
|
+
|
|
368
|
+
log(f"{np.sum(good)} good, {np.sum(~good)} outliers")
|
|
369
|
+
|
|
370
|
+
return good
|