stdpipe 0.4.1__tar.gz → 0.5.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 (83) hide show
  1. stdpipe-0.5.0/CHANGELOG.md +102 -0
  2. {stdpipe-0.4.1 → stdpipe-0.5.0}/MANIFEST.in +1 -0
  3. {stdpipe-0.4.1/stdpipe.egg-info → stdpipe-0.5.0}/PKG-INFO +3 -3
  4. {stdpipe-0.4.1 → stdpipe-0.5.0}/pyproject.toml +2 -2
  5. {stdpipe-0.4.1 → stdpipe-0.5.0}/stdpipe/__init__.py +1 -1
  6. {stdpipe-0.4.1 → stdpipe-0.5.0}/stdpipe/astrometry.py +104 -28
  7. {stdpipe-0.4.1 → stdpipe-0.5.0}/stdpipe/astrometry_quad.py +184 -68
  8. {stdpipe-0.4.1 → stdpipe-0.5.0}/stdpipe/catalogs.py +78 -21
  9. stdpipe-0.5.0/stdpipe/data/legacysurvey_bricks.fits.gz +0 -0
  10. {stdpipe-0.4.1 → stdpipe-0.5.0}/stdpipe/photometry.py +134 -35
  11. {stdpipe-0.4.1 → stdpipe-0.5.0}/stdpipe/photometry_iraf.py +15 -2
  12. {stdpipe-0.4.1 → stdpipe-0.5.0}/stdpipe/photometry_measure.py +221 -53
  13. {stdpipe-0.4.1 → stdpipe-0.5.0}/stdpipe/photometry_model.py +87 -3
  14. {stdpipe-0.4.1 → stdpipe-0.5.0}/stdpipe/photometry_psf.py +90 -47
  15. {stdpipe-0.4.1 → stdpipe-0.5.0}/stdpipe/pipeline.py +2 -2
  16. {stdpipe-0.4.1 → stdpipe-0.5.0}/stdpipe/plots.py +204 -57
  17. stdpipe-0.5.0/stdpipe/psf.py +2434 -0
  18. {stdpipe-0.4.1 → stdpipe-0.5.0}/stdpipe/resolve.py +34 -5
  19. {stdpipe-0.4.1 → stdpipe-0.5.0}/stdpipe/simulation.py +2 -0
  20. {stdpipe-0.4.1 → stdpipe-0.5.0}/stdpipe/templates.py +186 -80
  21. {stdpipe-0.4.1 → stdpipe-0.5.0}/stdpipe/utils.py +71 -0
  22. {stdpipe-0.4.1 → stdpipe-0.5.0/stdpipe.egg-info}/PKG-INFO +3 -3
  23. {stdpipe-0.4.1 → stdpipe-0.5.0}/stdpipe.egg-info/SOURCES.txt +1 -0
  24. {stdpipe-0.4.1 → stdpipe-0.5.0}/stdpipe.egg-info/requires.txt +1 -1
  25. {stdpipe-0.4.1 → stdpipe-0.5.0}/tests/test_astrometry.py +74 -0
  26. {stdpipe-0.4.1 → stdpipe-0.5.0}/tests/test_astrometry_quad.py +49 -2
  27. {stdpipe-0.4.1 → stdpipe-0.5.0}/tests/test_background_gradients.py +126 -8
  28. {stdpipe-0.4.1 → stdpipe-0.5.0}/tests/test_catalogs.py +141 -0
  29. {stdpipe-0.4.1 → stdpipe-0.5.0}/tests/test_photometry.py +158 -0
  30. {stdpipe-0.4.1 → stdpipe-0.5.0}/tests/test_photometry_biases.py +6 -2
  31. {stdpipe-0.4.1 → stdpipe-0.5.0}/tests/test_photometry_iraf.py +11 -10
  32. {stdpipe-0.4.1 → stdpipe-0.5.0}/tests/test_photometry_measure.py +109 -16
  33. stdpipe-0.5.0/tests/test_photometry_model.py +602 -0
  34. {stdpipe-0.4.1 → stdpipe-0.5.0}/tests/test_photometry_psf.py +348 -10
  35. {stdpipe-0.4.1 → stdpipe-0.5.0}/tests/test_plots.py +133 -0
  36. {stdpipe-0.4.1 → stdpipe-0.5.0}/tests/test_psf.py +64 -0
  37. {stdpipe-0.4.1 → stdpipe-0.5.0}/tests/test_resolve.py +76 -0
  38. {stdpipe-0.4.1 → stdpipe-0.5.0}/tests/test_sep_psf_bias.py +1 -1
  39. {stdpipe-0.4.1 → stdpipe-0.5.0}/tests/test_templates.py +221 -20
  40. {stdpipe-0.4.1 → stdpipe-0.5.0}/tests/test_utils.py +153 -0
  41. stdpipe-0.4.1/stdpipe/data/legacysurvey_bricks.fits.gz +0 -0
  42. stdpipe-0.4.1/stdpipe/psf.py +0 -1542
  43. stdpipe-0.4.1/tests/test_photometry_model.py +0 -291
  44. {stdpipe-0.4.1 → stdpipe-0.5.0}/LICENSE.md +0 -0
  45. {stdpipe-0.4.1 → stdpipe-0.5.0}/README.md +0 -0
  46. {stdpipe-0.4.1 → stdpipe-0.5.0}/setup.cfg +0 -0
  47. {stdpipe-0.4.1 → stdpipe-0.5.0}/setup.py +0 -0
  48. {stdpipe-0.4.1 → stdpipe-0.5.0}/stdpipe/artefacts.py +0 -0
  49. {stdpipe-0.4.1 → stdpipe-0.5.0}/stdpipe/astrometry_wcs.py +0 -0
  50. {stdpipe-0.4.1 → stdpipe-0.5.0}/stdpipe/cutouts.py +0 -0
  51. {stdpipe-0.4.1 → stdpipe-0.5.0}/stdpipe/data/ps1skycells.txt +0 -0
  52. {stdpipe-0.4.1 → stdpipe-0.5.0}/stdpipe/db.py +0 -0
  53. {stdpipe-0.4.1 → stdpipe-0.5.0}/stdpipe/fringe_removal.py +0 -0
  54. {stdpipe-0.4.1 → stdpipe-0.5.0}/stdpipe/lcs.py +0 -0
  55. {stdpipe-0.4.1 → stdpipe-0.5.0}/stdpipe/photometry_background.py +0 -0
  56. {stdpipe-0.4.1 → stdpipe-0.5.0}/stdpipe/photometry_quality.py +0 -0
  57. {stdpipe-0.4.1 → stdpipe-0.5.0}/stdpipe/realbogus.py +0 -0
  58. {stdpipe-0.4.1 → stdpipe-0.5.0}/stdpipe/realbogus_features.py +0 -0
  59. {stdpipe-0.4.1 → stdpipe-0.5.0}/stdpipe/reproject.py +0 -0
  60. {stdpipe-0.4.1 → stdpipe-0.5.0}/stdpipe/sfft.py +0 -0
  61. {stdpipe-0.4.1 → stdpipe-0.5.0}/stdpipe/smoothing.py +0 -0
  62. {stdpipe-0.4.1 → stdpipe-0.5.0}/stdpipe/subtraction.py +0 -0
  63. {stdpipe-0.4.1 → stdpipe-0.5.0}/stdpipe.egg-info/dependency_links.txt +0 -0
  64. {stdpipe-0.4.1 → stdpipe-0.5.0}/stdpipe.egg-info/not-zip-safe +0 -0
  65. {stdpipe-0.4.1 → stdpipe-0.5.0}/stdpipe.egg-info/top_level.txt +0 -0
  66. {stdpipe-0.4.1 → stdpipe-0.5.0}/tests/test_artefacts.py +0 -0
  67. {stdpipe-0.4.1 → stdpipe-0.5.0}/tests/test_astrometry_wcs.py +0 -0
  68. {stdpipe-0.4.1 → stdpipe-0.5.0}/tests/test_background_estimation.py +0 -0
  69. {stdpipe-0.4.1 → stdpipe-0.5.0}/tests/test_background_fringes.py +0 -0
  70. {stdpipe-0.4.1 → stdpipe-0.5.0}/tests/test_background_nongrid.py +0 -0
  71. {stdpipe-0.4.1 → stdpipe-0.5.0}/tests/test_background_polynomial.py +0 -0
  72. {stdpipe-0.4.1 → stdpipe-0.5.0}/tests/test_fringe_removal.py +0 -0
  73. {stdpipe-0.4.1 → stdpipe-0.5.0}/tests/test_fringe_removal_fourier.py +0 -0
  74. {stdpipe-0.4.1 → stdpipe-0.5.0}/tests/test_lcs.py +0 -0
  75. {stdpipe-0.4.1 → stdpipe-0.5.0}/tests/test_moffat_photometry_limits.py +0 -0
  76. {stdpipe-0.4.1 → stdpipe-0.5.0}/tests/test_photometry_quality.py +0 -0
  77. {stdpipe-0.4.1 → stdpipe-0.5.0}/tests/test_pipeline.py +0 -0
  78. {stdpipe-0.4.1 → stdpipe-0.5.0}/tests/test_realbogus.py +0 -0
  79. {stdpipe-0.4.1 → stdpipe-0.5.0}/tests/test_realbogus_features.py +0 -0
  80. {stdpipe-0.4.1 → stdpipe-0.5.0}/tests/test_reproject.py +0 -0
  81. {stdpipe-0.4.1 → stdpipe-0.5.0}/tests/test_sfft.py +0 -0
  82. {stdpipe-0.4.1 → stdpipe-0.5.0}/tests/test_simulation.py +0 -0
  83. {stdpipe-0.4.1 → stdpipe-0.5.0}/tests/test_smoothing.py +0 -0
@@ -0,0 +1,102 @@
1
+ # Changelog
2
+
3
+ All notable changes to STDPipe are documented in this file. Changes in
4
+ releases up to 0.4.0 are only available in the
5
+ [commit history](https://github.com/karpov-sv/stdpipe/commits/master).
6
+
7
+ ## 0.5.0 - 2026-10-31
8
+
9
+ Also published as 0.4.2, which was yanked.
10
+
11
+ ### Changed
12
+
13
+ - PSF fitters (`photometry_psf.measure_objects_psf()`,
14
+ `photometry_measure.measure_objects_sep()` and
15
+ `photometry_iraf.measure_objects_psf()`) now store fitted positions in
16
+ `x`/`y` and keep the input ones in `x_orig`/`y_orig`, as centroiding does.
17
+ The `x_psf`/`y_psf` columns are gone; `get_objects_sextractor()` still
18
+ reports SExtractor's own `x_psf`/`y_psf`.
19
+ - `psf.create_psf_model()` no longer uses photutils `EPSFBuilder`. The new
20
+ builder places star pixels at their true sub-pixel phases, iteratively
21
+ recentres and renormalizes the model, rejects outliers per pixel, subtracts
22
+ neighbours with the model itself, and sizes the stamp automatically from the
23
+ stacked stellar profile. This removes a ~0.8% PSF flux bias and 2-6% flux
24
+ losses for Moffat-like PSFs, and is several times faster. Resulting models
25
+ differ slightly from previous versions.
26
+ - `measure_objects_psf()` default `maxiters` raised from 3 to 100 (it is a
27
+ limit on function evaluations, and fits did not converge before), and
28
+ non-convergence is now detected using the correct photutils flag bit.
29
+ - `measure_objects_sep()` now uses a 5-sigma clipped mean for the local
30
+ background by default, for both aperture photometry (was unclipped median)
31
+ and optimal extraction (was 3-sigma clipped mean).
32
+ - Legacy Survey templates are taken from DR11 instead of DR10. Previously
33
+ cached DR10 bricks are reused as is, as their filenames do not encode the
34
+ data release.
35
+ - `resolve.simbadResolve()` queries Simbad only (instead of Simbad, NED and
36
+ VizieR), using the current Sesame URL.
37
+ - Requires `sep-x>=1.5.2`, which correctly resamples point-sampled
38
+ supersampled PSF models (PSFEx, `create_psf_model()`).
39
+
40
+ ### Added
41
+
42
+ - `create_psf_model(degree='auto')` selects the spatial polynomial degree of
43
+ the PSF model by spatial cross-validation, up to `max_degree`.
44
+ - `order='auto'` for SIP order selection in `refine_wcs_quadhash()` and
45
+ `refine_wcs_simple()`, as already supported by `refine_wcs_scamp()`.
46
+ - `bg_estimator` (`'mode'`, `'median'`, `'mean'`) with `clip_sigma`/`clip_iters`
47
+ for the local background in `measure_objects()` and `measure_objects_sep()`.
48
+ - `filter_type='matched'` in `get_objects_sextractor()`, so that the detection
49
+ threshold is in units of the filtered noise, as in `sep.extract()`.
50
+ - `get_objects_sep()` reports background flux in the aperture (`bg_flux`,
51
+ `bg_fluxerr`), and per-source `fwhm_model` and `aper` columns with a
52
+ position-dependent FWHM model. `measure_objects_sep()` also reports
53
+ `bg_fluxerr`.
54
+ - VizieR mirror fallback in `get_cat_vizier()` (`catalogs.vizier_mirrors`,
55
+ used on network errors only) and a `timeout` argument.
56
+ - Sesame mirror fallback in `resolve` (`resolve.sesame_mirrors`), and
57
+ `resolve()` falls through to the next method when Simbad or TNS lookups
58
+ fail.
59
+ - `cache` option in `templates.get_hips_image()`.
60
+ - `utils.file_lock()` and inter-process locking of skycell cache downloads in
61
+ `templates.get_skycells()`.
62
+ - `plots.render_lock` serializing Matplotlib rendering in `figure_saver`
63
+ across threads.
64
+ - Support for photutils 3.0.
65
+
66
+ ### Fixed
67
+
68
+ - Local background in `measure_objects()` was truncated to integers.
69
+ - Background aperture noise in `get_objects_sep()` counted the sky Poisson
70
+ noise twice.
71
+ - Masked object positions were measured at the values hidden under the mask.
72
+ - PSF fits of stars with masked pixels nearby were discarded as failed.
73
+ - PSF sub-pixel phase errors: centre of even-sized PSFEx models, conversion
74
+ of sub-pixel integrated models in the photutils backend, and resampling of
75
+ point-sampled models in the SEP backend.
76
+ - `get_psf_stamp()` lost >5% of the peak of even-sized point-sampled models.
77
+ - Robust `photometry_model.match()` with statsmodels 0.15, and huge cancelling
78
+ coefficients for rank-deficient designs; errors of terms not constrained by
79
+ the data are now NaN.
80
+ - Catalogue band augmentation: Gaia DR2 produced no Johnson-Cousins
81
+ magnitudes since astroquery 0.4.8, Gaia EDR3 was treated as DR2, and
82
+ `augment_bands=False` was ignored in `get_cat_vizier()`.
83
+ - Legacy Survey bricks present in both northern and southern lists were
84
+ always taken from the south, and bricks lacking the requested band produced
85
+ failed downloads.
86
+ - Remote FITS downloads no longer leave temporary files or fill the astropy
87
+ download cache.
88
+ - Three-color images in `plots.imshow()` are now properly normalized.
89
+ - `refine_wcs_simple(method='astrometrynet')` passed unmatched objects to
90
+ `fit-wcs`.
91
+
92
+ ## 0.4.1 - 2026-08-07
93
+
94
+ ### Fixed
95
+
96
+ - NumPy 2 incompatibilities in flag handling and set membership.
97
+ - A perfect photometric fit in `photometry_model.match()` is treated as
98
+ success rather than a degenerate model.
99
+
100
+ ## 0.4.0 - 2026-08-02
101
+
102
+ First release published through the automated PyPI release workflow.
@@ -1,3 +1,4 @@
1
1
  include README.md
2
+ include CHANGELOG.md
2
3
  recursive-include data *
3
4
  recursive-exclude notebooks *
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stdpipe
3
- Version: 0.4.1
3
+ Version: 0.5.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>
@@ -30,7 +30,7 @@ Project-URL: Homepage, https://github.com/karpov-sv/stdpipe
30
30
  Project-URL: Documentation, https://stdpipe.readthedocs.io/
31
31
  Project-URL: Repository, https://github.com/karpov-sv/stdpipe
32
32
  Project-URL: Bug Tracker, https://github.com/karpov-sv/stdpipe/issues
33
- Project-URL: Changelog, https://github.com/karpov-sv/stdpipe/commits/master
33
+ Project-URL: Changelog, https://github.com/karpov-sv/stdpipe/blob/master/CHANGELOG.md
34
34
  Keywords: astronomy,astrophysics,transients,photometry,astrometry,image-processing,pipeline
35
35
  Classifier: Development Status :: 4 - Beta
36
36
  Classifier: Intended Audience :: Science/Research
@@ -53,7 +53,7 @@ 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-x
56
+ Requires-Dist: sep-x>=1.5.2
57
57
  Requires-Dist: astroscrappy
58
58
  Requires-Dist: photutils>=2.2
59
59
  Requires-Dist: scikit-image>=0.22.0
@@ -47,7 +47,7 @@ dependencies = [
47
47
  "astropy>=4.1",
48
48
  "matplotlib",
49
49
  "astroquery>0.4.1",
50
- "sep-x",
50
+ "sep-x>=1.5.2",
51
51
  "astroscrappy",
52
52
  "photutils>=2.2",
53
53
  "scikit-image>=0.22.0", # Requires >=0.22.0 for NumPy 2.0 compatibility
@@ -103,7 +103,7 @@ Homepage = "https://github.com/karpov-sv/stdpipe"
103
103
  Documentation = "https://stdpipe.readthedocs.io/"
104
104
  Repository = "https://github.com/karpov-sv/stdpipe"
105
105
  "Bug Tracker" = "https://github.com/karpov-sv/stdpipe/issues"
106
- Changelog = "https://github.com/karpov-sv/stdpipe/commits/master"
106
+ Changelog = "https://github.com/karpov-sv/stdpipe/blob/master/CHANGELOG.md"
107
107
 
108
108
  # ============================================================================
109
109
  # Tool configurations
@@ -6,4 +6,4 @@ detection related tasks, intended for quick and easy implementation of custom
6
6
  pipelines, as well as for interactive data analysis.
7
7
  """
8
8
 
9
- __version__ = "0.4.1"
9
+ __version__ = "0.5.0"
@@ -594,6 +594,8 @@ def refine_wcs_simple(
594
594
  cat_col_ra='RAJ2000',
595
595
  cat_col_dec='DEJ2000',
596
596
  method='astropy',
597
+ auto_max_order=5,
598
+ auto_threshold=0.05,
597
599
  _tmpdir=None,
598
600
  verbose=False,
599
601
  ):
@@ -605,8 +607,12 @@ def refine_wcs_simple(
605
607
  List of detected objects with at least ``x``, ``y``, ``ra``, ``dec`` columns.
606
608
  cat : astropy.table.Table
607
609
  Reference astrometric catalogue.
608
- order : int, optional
609
- SIP polynomial order for distortion solution.
610
+ order : int or str, optional
611
+ SIP polynomial order for distortion solution, or ``'auto'`` to select it
612
+ based on hold-out residuals: the matched pairs are split in two halves,
613
+ every order is fitted on one and its residuals measured on the other,
614
+ and the lowest order that is within ``auto_threshold`` from the best one
615
+ is used.
610
616
  match : bool, optional
611
617
  If True, perform nearest-neighbor matching between objects and catalogue.
612
618
  If False, assume they are already matched line by line.
@@ -620,6 +626,11 @@ def refine_wcs_simple(
620
626
  Catalogue column name for Declination.
621
627
  method : str, optional
622
628
  Fitting method. Either ``'astropy'`` or ``'astrometrynet'``.
629
+ auto_max_order : int, optional
630
+ Largest SIP order to try when ``order='auto'``. Default is 5.
631
+ auto_threshold : float, optional
632
+ Minimal relative improvement of hold-out residuals for accepting a
633
+ higher SIP order when ``order='auto'``. Default is 0.05, i.e. 5 percent.
623
634
  _tmpdir : str, optional
624
635
  If specified, temporary files will be created inside this path.
625
636
  verbose : bool or callable, optional
@@ -634,6 +645,9 @@ def refine_wcs_simple(
634
645
  # Simple wrapper around print for logging in verbose mode only
635
646
  log = (verbose if callable(verbose) else print) if verbose else lambda *args, **kwargs: None
636
647
 
648
+ if isinstance(order, str) and order != 'auto':
649
+ raise ValueError("order must be an integer or 'auto', not '%s'" % order)
650
+
637
651
  if match:
638
652
  # Perform simple nearest-neighbor matching within given radius
639
653
  oidx, cidx, dist = spherical_match(
@@ -646,34 +660,39 @@ def refine_wcs_simple(
646
660
  _obj = obj
647
661
  _cat = cat
648
662
 
649
- wcs = None
650
-
651
- if method == 'astropy':
652
- wcs = fit_wcs_from_points(
653
- [_obj['x'], _obj['y']],
654
- SkyCoord(_cat[cat_col_ra], _cat[cat_col_dec], unit='deg'),
655
- sip_degree=order,
656
- )
657
- elif method == 'astrometrynet':
658
- binname = None
659
-
660
- # Rough estimate of frame dimensions
661
- width = np.max(obj['x'])
662
- height = np.max(obj['y'])
663
-
663
+ binname = None
664
+ if method == 'astrometrynet':
664
665
  for path in ['.', '/usr/local', '/opt/local']:
665
666
  if os.path.isfile(os.path.join(path, 'astrometry', 'bin', 'fit-wcs')):
666
667
  binname = os.path.join(path, 'astrometry', 'bin', 'fit-wcs')
667
668
  break
668
669
 
669
- if binname:
670
+ if not binname:
671
+ log("Astrometry.Net fit-wcs binary not found")
672
+
673
+ def fit(order, _obj, _cat, quiet=False):
674
+ """Fit WCS of a given SIP order to matched pairs of objects and catalogue stars"""
675
+ flog = (lambda *args, **kwargs: None) if quiet else log
676
+ wcs = None
677
+
678
+ if method == 'astropy':
679
+ wcs = fit_wcs_from_points(
680
+ [_obj['x'], _obj['y']],
681
+ SkyCoord(_cat[cat_col_ra], _cat[cat_col_dec], unit='deg'),
682
+ sip_degree=int(order),
683
+ )
684
+ elif method == 'astrometrynet' and binname:
685
+ # Rough estimate of frame dimensions
686
+ width = np.max(obj['x'])
687
+ height = np.max(obj['y'])
688
+
670
689
  dirname = tempfile.mkdtemp(prefix='astrometry', dir=_tmpdir)
671
690
 
672
691
  columns = [
673
- fits.Column(name='FIELD_X', format='1D', array=obj['x'] + 1),
674
- fits.Column(name='FIELD_Y', format='1D', array=obj['y'] + 1),
675
- fits.Column(name='INDEX_RA', format='1D', array=cat[cat_col_ra]),
676
- fits.Column(name='INDEX_DEC', format='1D', array=cat[cat_col_dec]),
692
+ fits.Column(name='FIELD_X', format='1D', array=_obj['x'] + 1),
693
+ fits.Column(name='FIELD_Y', format='1D', array=_obj['y'] + 1),
694
+ fits.Column(name='INDEX_RA', format='1D', array=_cat[cat_col_ra]),
695
+ fits.Column(name='INDEX_DEC', format='1D', array=_cat[cat_col_dec]),
677
696
  ]
678
697
  tbhdu = fits.BinTableHDU.from_columns(columns)
679
698
  filename = os.path.join(dirname, 'list.fits')
@@ -690,22 +709,79 @@ def refine_wcs_simple(
690
709
  order,
691
710
  )
692
711
 
693
- log('Running Astrometry.Net WCS fitter like that:')
694
- log(command)
712
+ flog('Running Astrometry.Net WCS fitter like that:')
713
+ flog(command)
695
714
 
696
715
  os.system(command)
697
716
 
698
717
  if os.path.isfile(wcsname):
699
718
  header = fits.getheader(wcsname)
700
719
  wcs = WCS(header)
701
- log('WCS fitter run succeeded')
720
+ flog('WCS fitter run succeeded')
702
721
  else:
703
- log('WCS fitter run failed')
722
+ flog('WCS fitter run failed')
704
723
 
705
724
  shutil.rmtree(dirname)
706
725
 
707
- else:
708
- log("Astrometry.Net fit-wcs binary not found")
726
+ return wcs
727
+
728
+ def select_order():
729
+ """Choose the SIP order by fitting one half of the matched pairs and
730
+ measuring the residuals on the other, unused one. Unlike the residuals
731
+ of the fit itself, these get worse as soon as it starts to overfit, so
732
+ we may just pick the lowest order that reaches the best of them. All
733
+ the orders are checked, as the improvement is not monotonic. Order 1
734
+ is skipped, as SIP only has terms of second order and above."""
735
+ log('Selecting the SIP order using half of the matches as a test set')
736
+
737
+ obj_fit, cat_fit = _obj[::2], _cat[::2]
738
+ obj_test, cat_test = _obj[1::2], _cat[1::2]
739
+
740
+ results = {}
741
+
742
+ for o in [0] + list(range(2, int(auto_max_order) + 1)):
743
+ # Linear part (CRPIX + CD) plus SIP terms with 2 <= i + j <= order,
744
+ # for both axes. Require at least 3 pairs per free parameter
745
+ n_sip = (o + 1) * (o + 2) // 2 - 3 if o >= 2 else 0
746
+ if len(obj_fit) < 3 * (6 + 2 * n_sip):
747
+ log(' order %d: %d matches, less than %d needed' % (o, len(obj_fit), 3 * (6 + 2 * n_sip)))
748
+ break
749
+
750
+ try:
751
+ w = fit(o, obj_fit, cat_fit, quiet=True)
752
+ except Exception:
753
+ w = None
754
+ if w is None:
755
+ break
756
+
757
+ ra, dec = w.all_pix2world(obj_test['x'], obj_test['y'], 0)
758
+ # Median is robust to the outliers among the test pairs
759
+ res = 3600 * np.nanmedian(
760
+ spherical_distance(ra, dec, cat_test[cat_col_ra], cat_test[cat_col_dec])
761
+ )
762
+ if not np.isfinite(res):
763
+ break
764
+
765
+ results[o] = res
766
+
767
+ log(' order %d: test residual %.3f arcsec' % (o, res))
768
+
769
+ if not results:
770
+ log('Order selection failed, using order 0')
771
+ return 0
772
+
773
+ # Lowest order that is within the threshold from the best residual
774
+ best_res = min(results.values())
775
+ best_order = min(o for o, res in results.items() if res <= best_res * (1 + auto_threshold))
776
+
777
+ log('Using SIP order %d' % best_order)
778
+
779
+ return best_order
780
+
781
+ if order == 'auto':
782
+ order = select_order()
783
+
784
+ wcs = fit(order, _obj, _cat)
709
785
 
710
786
  if wcs:
711
787
  if update: