Plate Solver

Finds the celestial coordinates of every pixel in an image — given a photo of the sky, it works out where the telescope was pointing, at what rotation and scale, and writes a full WCS (World Coordinate System) solution into the image's FITS header. Three solver backends run as a fallback chain: SASpro's In-House Gaia DR3 solver first, then ASTAP (seeded, then blind), and finally Astrometry.net (local solve-field or web API). The Gaia solver also solves blind — no coordinates, no field of view, nothing but the star pattern — using a quad-hash index built locally from the Gaia data you already downloaded (§15). Three operating modes: Active View, single File, and Batch directory processing.

SASpro-exclusive solver orchestration and in-house Gaia DR3 engine. The four-pass fallback chain (Gaia DR3 → ASTAP seeded → ASTAP blind → Astrometry.net), the entire In-House Gaia DR3 plate-solving engine (triangle-invariant matcher with vertex voting, Hough-space translation accumulator with 3×3 neighbourhood merging, RANSAC similarity and full-affine consensus, spiral seed search over scale and parity variants, pixel-relative RMS quality gate, iterative WCS refinement by reprojection and nearest-neighbour re-match, bidirectional SIP degree search on parameter-penalised residuals, focal-length recomputation from the solved plate scale, failed-match star exclusion for retry rounds), the native blind solver (AB-frame quad-hash index built locally from the installed Gaia tiers, scale-banded layer selection, per-quad adaptive code tolerance, Bayesian log-odds verification, and the seed-grade fallback), the acquisition-header-preserving WCS merge policy, the catalog object lookup with M/NGC/IC/Sh2/PK prefix normalisation, the ASTAP seed heuristics (radius/FOV mode controls), the preset system with draggable canvas shortcuts, and the View Bundle application are all original work by Franklin Marek for SetiAstro Suite Pro. External solvers (ASTAP by Han Kleijn and Astrometry.net by Lang et al.) are credited separately in their sections and in the final Credits box.
Workflow
Open an image — pick Active View (solve current window), File (load, solve, save), or Batch (whole folder) — set the Seed mode (Auto reads your header, Manual lets you type RA/Dec/scale or use Lookup… to search 100,000+ catalogued objects, None goes blind) — set the Solver preference (default: run all three in the fallback chain) — Start.

With no coordinates at all, or with Force blind solve ticked, the Gaia solver finds the field from the star pattern alone. Build the index once from the Open Gaia Database… button (§15).

The in-house Gaia DR3 solver's core idea: find rotation-, scale-, and parity-invariant triangle shapes shared between detected image stars and Gaia catalog stars projected through a seed WCS. Each triangle match is encoded as an invariant pair of side ratios:

\mathbf{I} = \left(\frac{s_c}{s_a},\;\frac{s_b}{s_a}\right),\quad s_a \leq s_b \leq s_c

These two numbers survive any similarity transform including reflections, so a wrong CROTA2 sign, a 90° rotator offset, or a mirrored optical train doesn't break them — vertex votes aggregate across matched triangles and RANSAC extracts the consensus transform. §14.7 derives the full procedure.

Contents
1. Dialog layout
2. Operating modes
3. Seed mode
4. Manual coordinates & Lookup
5. Search radius & FOV
6. Solver preference
7. The four-pass fallback chain
8. Active View mode
9. File mode
10. Batch mode
11. Apply to View Bundle
12. Presets & drag shortcuts
13. Output & the merged header
14. Technical details
14.1 Pixel scale estimation
14.2 Header merging policy
14.3 Seed WCS construction
14.4 Gaia library (astrometric vs spectral)
14.5 Star detection (SEP)
14.6 Catalog gathering
14.7 Triangle invariant matching
14.8 Hough translation vote
14.9 RANSAC consensus
14.10 TAN WCS fit
14.11 Iterative refinement
14.12 SIP distortion fit
14.13 Quality gate
14.14 Spiral seed search
14.15 Focal length recomputation
14.16 ASTAP integration
14.17 Astrometry.net integration
15. Blind solving (quad index)
15.1 Why quads
15.2 The AB-frame code
15.3 Index layers
15.4 Matching and verification
15.5 Blind as a seed source
15.6 Blind retry after a failed seed
15.7 Seed-grade fallback

1. Dialog layout

The dialog has four blocks top-to-bottom: the Mode selector (Active View / File / Batch), a shared Seeding & Constraints group that applies to every mode, the mode-specific controls page (file picker, batch I/O, etc.), and a status strip plus the Apply to View Bundle… / Start / Close buttons. A draggable preset handle in the bottom-left turns your current settings into a reusable shortcut (§12).

The Seeding & Constraints group also carries the two blind-solve checkboxes (§3) and a quad index status line reporting how many index layers are built, how many quads they hold, the range of field widths they cover, and how many Gaia catalogue bands are installed — next to an Open Gaia Database… button that opens the Gaia DR3 Library where both are managed. The status line reports coverage rather than just a count, because each layer answers a band of field widths roughly 4× wide: three layers built tells you nothing useful on its own, but “solves fields 20′ to 34° wide” does. The button is disabled while a solve is running, since rebuilding the index closes the memory maps the running solve is reading from.

2. Operating modes

Active View

Solves the image in the currently-focused SASpro document window in-place. The WCS solution is merged into the document's existing header (acquisition metadata preserved; see §13 for the merge policy), and the header dock refreshes to show the new coordinates. This is the usual mode when you've just loaded a sub, want to solve it, and keep working with it.

File

Loads a single image file from disk (FITS, XISF, TIFF, PNG, JPEG), solves it, and prompts for a save-as path. Output is always a 32-bit float FITS with the merged header. Useful for solving images that aren't currently open in SASpro.

Batch

Loops over every image in an input folder, solves each, and saves <name>_plate_solved.fits into an output folder. Accepted extensions: .fits, .fit, .xisf, .tif, .tiff, .png, .jpg, .jpeg. Progress is logged in a scrolling text area; each file shows a ▶ when starting, a ✔ with the saved path on success, or a ❌ with the error on failure.

3. Seed mode

Every solver works better when it starts with a rough idea of where the telescope was pointed. The Seed mode dropdown picks where that initial guess comes from:

Blind-solve checkboxes

Two independent controls sit below the seed mode:

Which one do I want? Leave both at their defaults. Missing coordinates are handled automatically by the first box. Reach for Force blind solve only when a solve is failing and you suspect the header pointing — and note that since SASpro now retries blind automatically after a failed seeded solve (§15.6), you rarely need to tick it by hand.

4. Manual coordinates & Lookup

In Manual seed mode three fields are enabled:

Lookup…

Clicking Lookup… opens a dialog where you type an object name. The lookup searches two sources:

Name normalisation handles common inconsistencies — both M42 and M 42 work, both Sh2-155 and Sh2 155 work, both PK 164+31.1 and PN-G 164.8+31.1 work (PK designations auto-translate to PN-G form before searching). If multiple matches turn up, a picker lets you choose. The selected object's RA, Dec and alt-name (or type/magnitude if available) populate the RA/Dec fields and the seed mode auto-switches to Manual.

5. Search radius & FOV

These two options only affect ASTAP; the in-house Gaia DR3 solver uses its own spiral search (§14.14) and Astrometry.net has its own interface. If you aren't running ASTAP in your fallback chain, you can leave both on Auto.

Search radius

FOV

6. Solver preference

Which solvers run and in what order:

PreferenceWhat runs
Gaia DR3 → ASTAP → Astrometry.net (all)The default four-pass fallback chain (see §7). Fastest overall for well-seeded data; most robust for difficult fields.
In-House Gaia DR3 onlyTry the Gaia solver and stop. Requires a seed. Pick this when ASTAP catalogs aren't installed or you want the fastest in-house solve on good seeds.
ASTAP onlyTry ASTAP seeded, then ASTAP blind. Classic workflow for people with ASTAP catalogs installed and no Gaia library.
Astrometry.net onlyTry local solve-field if configured, else the web API. Slowest option; most useful when nothing else is available or for cross-checking a questionable solution.

7. The four-pass fallback chain

With Gaia DR3 → ASTAP → Astrometry.net (all) selected, SASpro tries up to four passes in sequence and stops at the first that succeeds:

  1. In-house Gaia DR3 — triangle/Hough + RANSAC against the local Gaia catalog. Fast when a seed is available, no external processes, handles rotation/scale/parity automatically. With no RA/Dec in the header it recovers one blind from the quad index first (§15.5), so a missing pointing no longer forces the chain down to ASTAP.
  2. In-house Gaia DR3, blind retry — if the seeded attempt above failed, the seed itself is the prime suspect, so SASpro discards both the pointing and the scale and asks the quad index outright (§15.6). Runs only when the first pass actually used a seed; costs about a second on a path that has already failed.
  3. ASTAP seeded — runs ASTAP with -ra, -spd, -scale, -r and computed -fov arguments. Very fast when seed is accurate.
  4. ASTAP blind — runs ASTAP with -r 179 -fov 0 -z 0, no coordinates. Slower but can solve from nothing.
  5. Astrometry.net — local solve-field if the path is configured, otherwise uploads to nova.astrometry.net via API. The slowest option; a lifeboat for fields the other solvers can't crack.

Each pass's status is reported live in the dialog's status strip and (for headless scripted runs) in a floating status popup. The log records which pass actually succeeded.

8. Active View mode

When you click Start with Mode = Active View, SASpro grabs the current document's image array and its metadata, derives a seed header, runs the fallback chain, and merges the resulting WCS into the document's header. The merge preserves everything in your acquisition header (OBJECT, EXPTIME, GAIN, FILTER, DATE-OBS, OBSERVER, telescope details, etc.) and only overwrites the WCS/SIP cards — so you don't lose metadata by solving.

The document emits its changed signal and the header dock refreshes automatically to show the newly-written CTYPE1/CTYPE2, CRPIX1/CRPIX2, CRVAL1/CRVAL2, CD1_1…CD2_2, and (if a Gaia DR3 solve succeeded) SOLVRMS / SOLVP95 / SOLVNSTR cards reporting fit quality.

On success, a non-modal Plate Solve Successful popup shows:

9. File mode

Click Browse… to pick a file, then Start. SASpro loads the image via its shared image-I/O layer (legacy.load_image), extracts whatever header exists as the seed, runs the fallback chain, merges the WCS, and asks where to save a plate-solved FITS copy. The original file is never modified. On successful save, the solve-summary popup appears as in Active View mode.

10. Batch mode

Set Input directory and Output directory (both must exist) and click Start. SASpro iterates alphabetically through every image file in the input folder, applies the full fallback chain to each, and saves <basename>_plate_solved.fits into the output folder. The log pane shows progress per file:

The Qt event loop is pumped between files so the dialog stays responsive and you can watch progress live. Batch doesn't currently support mid-run cancel — close the dialog to stop between files.

11. Apply to View Bundle

Click Apply to View Bundle… to solve every target in a saved SASpro View Bundle at once — both open sub-windows and on-disk files referenced by the bundle. A picker shows the bundles you've saved; selecting one and clicking OK runs the fallback chain on each target in turn, showing per-target status in the status strip and summarising any errors at the end. Open views get the WCS merged in-memory; file references get resaved to disk through save_image with their original format and bit depth preserved. Useful for solving a whole night's calibrated subs in one click.

12. Presets & drag shortcuts

Every control in the dialog (seed mode, manual RA/Dec/scale, search radius, FOV, solver preference) can be captured as a preset and dropped on the SASpro canvas as a reusable shortcut.

Grab the drag handle in the bottom-left of the dialog and drag it to the canvas — a shortcut icon appears, pre-loaded with your current settings. Dropping an image on the shortcut (or dropping the shortcut on a View Bundle chip) runs apply_plate_solve_to_doc() headlessly: no dialog, no progress bar, just the fallback chain running on the dropped target with the preset's settings.

Presets store nine fields: seed mode, manual RA, manual Dec, manual scale, search-radius mode, radius value, FOV mode, FOV value, and solver preference. The normalize_plate_solve_preset() helper validates and defaults each field so stale or partial preset dicts never crash the dialog.

13. Output & the merged header

Every success path produces one artefact: a FITS header containing both the original acquisition metadata and the solved WCS. The merge policy, implemented in _merge_wcs_into_base_header():

The merged header is also parsed into an astropy.wcs.WCS object stored at meta['wcs'], and a WCS-only copy (just the celestial cards, no acquisition metadata) is stored at meta['wcs_header']. Downstream SASpro tools (SFCC, SSSC, catalogue overlays, mosaic alignment) read from these canonical fields.

14. Technical details

The seeding, matching and WCS-fitting pipeline, in order.

14.1 Pixel scale estimation

Both ASTAP's -scale argument and the in-house Gaia solver's seed WCS need an initial pixel scale. SASpro tries five sources in priority order:

  1. Full WCS — parse the header as an astropy.wcs.WCS and take proj_plane_pixel_scales, which handles CD, PC+CDELT, SIP all together. This is the preferred path.
  2. CD matrix — from CD1_1 and CD2_1 (degrees/pixel):
    s_{\mathrm{px}} = 3600 \cdot \sqrt{\mathrm{CD1\_1}^2 + \mathrm{CD2\_1}^2}
    gives arcseconds per pixel. For a non-skewed rotation, this is equivalent to sqrt(|det(CD)|) × 3600.
  3. PC × CDELT — the older FITS WCS form: multiply the rotation matrix by the CDELT scale vector and apply the same magnitude formula.
  4. Explicit pixel-scale keywords — PIXSCALE, SECPIX, SECPIX1, SECPIX2, SCALE, PLATESCL, PLTSCALE (any of these, if present, is already in arcsec/px).
  5. Pixel size + focal length — the last-resort derivation, using XPIXSZ/YPIXSZ (pixel size in µm), FOCALLEN (mm), and XBINNING/YBINNING:
    s_{\mathrm{px}} = \frac{206.265 \cdot p_{\mu m} \cdot b}{f_{\mathrm{mm}}}
    where b is the mean binning factor. If both XPIXSZ and YPIXSZ are present, their mean is used.

Every candidate is sanity-checked: values below 0.01 that look like they're in degrees get a ×3600 rescue; values above 1000"/px are rejected as implausible. Values between 0.01 and 1000 are accepted.

14.2 Header merging policy

Covered in §13 above. The implementation lives in _merge_wcs_into_base_header(). Key invariants:

14.3 Seed WCS construction

Given RA, Dec and pixel scale, the Gaia solver builds a TAN projection seed WCS centred at the given coordinates with CRPIX at image centre. Rotation is pulled from the first non-null of ANGLE, POSANGLE, CROTA2, CROTA1, OBJCTROT, ROTANG, ROT_ANGL, CAMROTAN. Parity (determinant sign) is tried both ways in the spiral search (§14.14).

The CD matrix for a rotated TAN projection with scale s and rotation θ:

CD1_1 = -s cos(θ) CD1_2 = +s sin(θ) CD2_1 = -s sin(θ) CD2_2 = -s cos(θ)

for the det>0 branch, and the mirrored equivalent for det<0. §14.14 explains why both are tried.

Parity belongs to the optics, not the roll angle. Every CD matrix SASpro builds — seed WCS, blind seed, and the header synthesised from an Astrometry.net calibration dict (§14.17) — now comes from one shared constructor, so det(CD) is exactly +s² or −s² at every rotation. An earlier form multiplied CD1_1 and CD2_1 by the parity sign while leaving CD1_2 and CD2_2 alone. That is not a mirror — a mirror negates a whole axis, so it must take a full row or a full column, never half of each. What it produced was a shear: det = −p·s²·cos(2θ), zero at a 45° roll (singular matrix), sign-flipped past it, and with the two column vectors no longer perpendicular at any roll that is not a multiple of 90°. It hid for a long time because pixel scale is read back from column norms, which stay exactly s under the bug — the scale looked right while the WCS underneath was skewed.

14.4 Gaia library (astrometric vs spectral)

SASpro ships with two Gaia DR3 library formats, pulled from different ESA data products:

Install either (or both) via Settings → Gaia DR3 Library. If neither is installed, the Gaia solver refuses with a clear error message pointing to the installer.

14.5 Star detection (SEP)

The in-house Gaia solver detects stars in the input image via SEP (Barbary 2016), the Python port of Source Extractor. SEP is called through SASpro's shared _detect_stars_uniform helper, which does grid-balanced extraction so no cell of the image dominates the star list:

14.6 Catalog gathering

For each seed candidate (centre, scale, parity — see §14.14), SASpro projects the seed WCS to find the four image corners' sky coordinates, pads the resulting RA/Dec bounding box by 0.1°, and queries the Gaia library for all catalog stars in that box.

Candidates are then de-duplicated by Gaia source_id, sorted by G-band magnitude (brightest first), projected to pixel coordinates through the seed WCS, filtered to a 50-pixel margin inside the image bounds, and grid-balanced the same way as the image stars (3×3 grid, up to 1000/9 per cell). Match-time runs with a dense bright-first catalog subset rather than the raw box query.

14.7 Triangle invariant matching

The core idea of the in-house solver, based on Groth (1986)'s classical star-pattern triangle method and refined following the astroalign (Beroiz, Cabral & Sanchez 2020) implementation style.

Build triangle invariants

For every triple of stars (a, b, c), measure the three side lengths s_a, s_b, s_c (where side i is opposite vertex i), sort them:

s_a \leq s_b \leq s_c

and encode the triangle's shape as the two side ratios:

\mathbf{I} = \left(\frac{s_c}{s_a},\;\frac{s_b}{s_a}\right)

Why these two numbers: the triangle's absolute size depends on scale, its orientation depends on rotation, its handedness depends on parity — but the ratios of its sides depend on none of those. Any similarity transform (rotation + uniform scale + reflection) maps a triangle to a congruent triangle with the same two ratios. So if the same three stars appear in both the image and the catalog projection, their invariant pair is identical regardless of the seed WCS being rotated or mirrored or wrongly scaled.

Prune degenerate triangles

Two filters: reject triangles with shortest side under 8 px (centroid noise dominates), and reject triangles whose longest/shortest ratio exceeds 8 (needle-thin triangles have poor invariant stability).

k-NN match in invariant space

Both image-star and catalog-star triangle invariants are loaded into a cKDTree and image invariants are queried against the catalog tree with distance upper bound 0.05 (adimensional, since the invariants are ratios). Each matched image-triangle↔catalog-triangle pair implies three vertex correspondences.

Vertex voting

For every matched triangle pair (T_s, T_r), cast a vote on each of the three vertex correspondences (V_s[a], V_r[a]) for a = 0, 1, 2. True correspondences accumulate votes across many triangles; coincidental matches don't. Correspondences with at least 2 votes are kept, in descending vote order, with duplicate assignments blocked (each image vertex and each catalog vertex can match only once).

RANSAC full affine

From the voted correspondences, fit a 6-parameter affine transform via RANSAC:

x' = m_{11}x + m_{12}y + t_x,\quad y' = m_{21}x + m_{22}y + t_y

300 iterations, 3-point minimal samples, 10-pixel inlier tolerance. The best inlier set must contain at least max(6, 15% of candidates) or the solve attempt is abandoned. The full-affine form (as opposed to similarity) is deliberate: it handles reflection (det(M) < 0) correctly, which matters because the Gaia solver tries both parities and lets the matcher decide which is right.

Re-match the full star set at the recovered transform

With the final (M_x, M_y) fit from the inlier consensus, transform every image star and query the full catalog KDTree at distance 0. An iterative tolerance tighten runs 3 cycles: refit affine on current inliers, re-query, shrink the tolerance toward 4× the observed median residual (bounded below by 2 px). This stops dense-field false nearest-neighbours from inflating the final RMS past the quality gate.

14.8 Hough translation vote

If triangle matching returns too few pairs or RANSAC fails, SASpro falls back to the Hough translation vote — a Generalized Hough Transform (Hough 1962, Ballard 1981) restricted to pure 2D translation. This is the method used by Tabur (2007) in his fast catalog-matching routine for CCD astrometry.

Vote on (Δx, Δy)

For every image star s_i and every catalog star c_j, compute the implied translation (c_j - s_i) and vote in a 2D accumulator binned at 2× the match tolerance. The peak bin reveals the dominant translation offset:

(\Delta x,\Delta y) = \arg\max_{(b_x,b_y)}\;\#\{(i,j) : (\mathbf{c}_j - \mathbf{s}_i) \in \mathrm{bin}(b_x,b_y)\}

Translations outside half the image size are rejected (no sense voting for a shift that would place every star off-screen).

3×3 neighbourhood merge

The vote is deliberately imperfect: when the seed WCS has residual rotation or scale error on top of the dominant translation, correspondences near the rotation centre vote in the peak bin but correspondences far from the centre drift into neighbouring bins. SASpro merges the 3×3 neighbourhood around the peak into one candidate pool, which gives the next step (similarity RANSAC) a noisier but broader set to work with.

RANSAC similarity transform

Fit a 4-parameter similarity transform (rotation, uniform scale, translation) over the pool:

x' = ax - by + t_x,\quad y' = bx + ay + t_y

with 200 minimal 2-point samples and a tolerance of 2× the match tolerance. The recovered scale is sqrt(a² + b²) and rotation is atan2(b, a), which are logged for diagnostic purposes.

Final affine fit

With the similarity-corrected positions, nearest-neighbour match against the catalog KDTree at 1.5 × match_tol_px, then do one final 6-parameter affine fit for inlier rejection. Returns the matched pair list the same way triangle matching does.

14.9 RANSAC consensus

Both matchers use the classical RANSAC algorithm (Fischler & Bolles 1981):

\mathbf{M}^{*} = \arg\max_{\mathbf{M}}\;\#\{i : \|\mathbf{p}'_i - \mathbf{M}(\mathbf{p}_i)\| < \tau\}

Over N trials, sample the minimum number of correspondences needed to fit the model (3 for affine, 2 for similarity), fit, count inliers within tolerance τ, keep the fit with the most inliers. Refit on the best inlier set for the final transform. RANSAC avoids being dragged off by false correspondences the way a plain least-squares fit over the whole noisy pool would be.

14.10 TAN WCS fit

Once matched pairs are in hand, SASpro uses astropy.wcs.utils.fit_wcs_from_points to fit a TAN (gnomonic) projection WCS through them. The projection point is the unit-vector mean of the matched sky coordinates; the CD matrix emerges from the least-squares fit.

\hat{\mathbf{n}} = \frac{\sum_i \mathbf{u}_i}{\left\|\sum_i \mathbf{u}_i\right\|},\qquad \mathbf{u}_i = (\cos\delta_i\cos\alpha_i,\;\cos\delta_i\sin\alpha_i,\;\sin\delta_i)
Why not just average RA and Dec. Because averaging an angle is wrong wherever the set wraps, and both failure cases are ordinary astrophotography. A field straddling RA 0h averages to 12h. A field containing a celestial pole holds stars at every RA at once, so its mean RA is meaningless — on a real 1920×1080 frame centred 0.7° from the north celestial pole, the matched stars span RA 0.1° to 360.0° and average to 137.7° against a true centre of 37.0°. Either way the tangent point lands degrees away from the field, and a TAN fit anchored off the field cannot represent the true WCS: gnomonic projections about different points differ by a projective transform, which no CD matrix and no SIP polynomial can absorb.

Measured on that frame, same 489 pairs, the only difference being the projection point:
Projection pointOffset from field centreTAN RMSSIP-3 RMSFitted scale
mean of RA, mean of Dec1.29°551.45"186.63"4.5916"/px
unit-vector mean0.07°0.844"0.009"4.0253"/px
True plate scale 4.0257"/px. The SIP-3 figure recovers the published Astrometry.net solution for the same frame to 0.009".

On a field straddling RA 0h the arithmetic mean is worse still: the tangent point lands 139.9° away and fit_wcs_from_points raises Residuals are not finite in the initial point rather than merely fitting badly. Away from the wrap and the poles the two means agree to four decimal places, so nothing changes for ordinary fields.

The gnomonic / TAN projection is the FITS WCS standard for most astronomical imaging and follows Greisen & Calabretta (2002). The forward (pixel → sky) transform in the simplest case:

(ξ, η) = CD · (x - CRPIX1, y - CRPIX2) sky coordinates derived from (ξ, η) via standard gnomonic equations

centred on the reference point (CRVAL1, CRVAL2).

14.11 Iterative refinement

The initial TAN fit uses only the matched set from the triangle/Hough matcher. SASpro then iteratively refines it by re-matching against the catalog through the fitted WCS:

  1. Project every detected image star through the current fitted WCS, giving each a sky position.
  2. Re-query the Gaia catalog for all stars within the current field, build a fresh KDTree in pixel space.
  3. Nearest-neighbour match image stars against the catalog with an 8-pixel tolerance.
  4. Refit the TAN WCS on the new (larger, cleaner) correspondence set.
  5. Compute the new RMS. If it improved by more than roughly 0.02 pixels equivalent arcseconds, iterate again; otherwise stop. Capped at 5 iterations.

An iteration is adopted if it gained pairs, or if it improved the RMS without losing more than 20% of them. That retention rule is right when the fit is already good — do not shed a fifth of the pairs for a marginal gain — and backwards when the fit is broken, so it carries one exception:

adopt also if RMS_new < 0.5 × RMS_old and pairs ≥ 20

A fit whose RMS halves was being dragged by pairs that do not belong in it, and protecting the pair count throws the solve away. Measured on a near-pole frame: the refit cut RMS from 519" to 24", a 21× improvement, and was refused for dropping 975 pairs to 507. The clause cannot misfire on a healthy fit, because once the RMS is near optimal a halving is not available.

Each iteration either tightens the fit by picking up more real correspondences from the catalog's dense inner region, or exposes the current fit as being as good as it's going to get. The “0.02 px” convergence threshold is pixel-relative so it works equally on 0.3"/px long-focal-length rigs and 7"/px wide-field rigs.

14.12 SIP distortion fit

Linear TAN cannot absorb the geometric distortion of wide-field lenses or fast astrographs. SASpro fits a SIP distortion polynomial on top of the TAN fit, following Shupe et al. (2005). SIP adds correction polynomials to the pixel-space side of the transform:

u' = u + \sum_{p+q \leq n} A_{p,q}\,u^p v^q
v' = v + \sum_{p+q \leq n} B_{p,q}\,u^p v^q

with (u, v) the pixel offset from CRPIX and A_{p,q}, B_{p,q} fitted coefficients up to order n. CTYPE1/CTYPE2 become RA---TAN-SIP/DEC--TAN-SIP when SIP is present.

Choosing the degree

The degree is searched, not assumed. A higher order is not merely redundant when it fails — it is actively worse, because the extra freedom lets the polynomial diverge at the frame edges where stars are sparsest and nothing holds it down. SASpro therefore starts at the degree the matched-pair count comfortably supports (≥ 100 pairs → 4, ≥ 50 → 3, else 2) and walks outward in both directions, each walk anchored at that base degree:

Degrees are compared on a parameter-penalised residual, never on raw RMS. In-sample RMS almost always falls as coefficients are added, so comparing on it alone biases the choice toward the highest degree:

\mathrm{RMS}_{\mathrm{adj}} = \mathrm{RMS}\sqrt{\frac{2N}{2N-k}},\qquad k = 2\left[\tfrac{(d+1)(d+2)}{2}-3\right]+6

where k counts both axes' distortion coefficients plus the six linear WCS parameters. The correction is negligible when pairs vastly outnumber coefficients and decisive when they do not.

Why both directions matter — measured on synthetic 6170×4013 frames with known radial distortion:

CasepairsSIP-2SIP-3SIP-4SIP-5TANChosen
heavy radial distortion1200281.6"4.5"283.0"243.1"283.9"SIP-3
mild distortion120016.3"1.57"1.56"14.6"16.7"SIP-4
no distortion12001.40"1.40"1.39"2.59"2.59"SIP-4
heavy distortion250274.6"4.54"4.50"238.6"280.8"SIP-4

The first row is the case that makes the point: degree 3 is excellent, 4 and 5 both diverge, and the quality is not monotonic in the degree. Anything that climbed in one direction only would have sat on SIP-5 at 243" while SIP-3 at 4.5" went unfitted. Each fit costs roughly 0.1–0.4 s at 1200 pairs, so the worst case adds under a second.

Adopting SIP at all

SIP is accepted over plain TAN only if it clears the same penalised comparison and the same 3% margin, judged against TAN's own six parameters. Otherwise an undistorted field adopts a distortion polynomial for a fraction of a percent of RMS, which is just fitting the centroid noise — measured at 0.6% on a distortion-free frame, for six extra parameters.

SASpro fits SIP before the quality gate (§14.13), so a distorted wide-field frame that would fail the gate on linear TAN can still pass once SIP absorbs the distortion. The RMS used by the gate is the SIP RMS when SIP is adopted, else TAN.

14.13 Quality gate

Before accepting a solve, SASpro checks fit quality. The RMS of final residuals (pair-wise angular separations):

\mathrm{RMS} = \sqrt{\frac{1}{N}\sum_{i=1}^{N} \Delta\theta_i^2}

and the 95th-percentile residual:

P_{95} = \mathrm{percentile}_{95}(\Delta\theta_i)

are computed where Δθ_i is the great-circle angular separation between the ith matched star's catalog position and the position the fitted WCS puts it at.

The gate is pixel-relative, not raw arcseconds, because a flat arcsecond threshold is wrong on both ends of the pixel-scale spectrum:

\mathrm{RMS}_{\max} = \max\!\left(\mathrm{floor}_{\mathrm{arcsec}},\;k_{\mathrm{px}} \cdot s_{\mathrm{px}}\right)

with k_px = 2.0 (two pixels equivalent) and floor_arcsec = 3.0". A fine-scale 0.5"/px rig gets a 3.0" limit (strict as it should be); a coarse 7"/px rig gets a 14" limit (sensible given the sampling). A flat 3.0" limit would wrongly fail a sub-pixel solve on the coarse rig.

Minimum pair count: 20. If the fit RMS exceeds RMS_max, or fewer than 20 pairs matched, the gate rejects and SASpro either retries the field with a different matcher (triangle ↔ Hough), excludes the previously-matched catalog stars and tries again, retries blind (§15.6), or falls back to the next solver in the chain. When every one of those has failed but the quad index did identify the field, SASpro returns that placement as a seed-grade solution rather than nothing at all (§15.7).

14.14 Spiral seed search

Even when the acquisition header has RA/Dec, those values can be off by many arcminutes due to mount pointing error. SASpro's Gaia solver covers this by searching a spiral grid of seed centres around the nominal RA/Dec:

Expressed in sky coordinates with ring radius r and azimuth φ:

\Delta\mathrm{RA} = \frac{r\cos\phi}{\cos(\mathrm{Dec}_0)},\quad \Delta\mathrm{Dec} = r\sin\phi

For each centre, SASpro also tries five scale variants (×1.00, ×0.95, ×1.05, ×0.90, ×1.10) and two parity variants (det>0 and det<0 CD matrices). The parity sweep is critical because camera flip is one of the most common reasons for a solve to silently fail — and nothing in the acquisition header normally tells SASpro whether the frame is flipped.

The search stops at the first (centre, scale, parity) combination that produces a matched pair set passing the quality gate. In practice the nominal centre + nominal scale + det<0 is right the vast majority of the time, but the spiral cover rescues the awkward cases without the user having to intervene.

14.15 Focal length recomputation

Once a solve succeeds and the plate scale is known accurately, SASpro back-computes the true effective focal length from the physical pixel size and binning:

f_{\mathrm{mm}} = \frac{206.265 \cdot p_{\mu m} \cdot b}{s_{\mathrm{px}}}

and overwrites the FOCALLEN card. This matters because the user-entered FOCALLEN often disagrees with reality (incorrect for the specific flattener/reducer in use, wrong for the actual spacing, or simply never updated after a scope swap). A HISTORY card logs the before/after values when they differ by at least 0.5 mm, so the original is still auditable. Sanity bounds: the recomputed value must fall in [1, 50000] mm to be accepted.

14.16 ASTAP integration

Passes 2 and 3 of the fallback chain run ASTAP, Han Kleijn's standalone plate-solving program. SASpro drives it as an external process via QProcess, writing a temporary single-channel 32-bit float FITS of the current image (passed through SASpro's star-visibility stretch so dim stars show) and reading back the sidecar .wcs file ASTAP writes next to the input.

Seeded invocation arguments (Pass 2):

astap.exe -f <tmp.fit> -ra <hours> -spd <south-pole-distance-deg> -scale <arcsec-per-px> -r <radius-deg> -fov <fov-deg> -wcs -sip

where SPD = Dec + 90°, the radius comes from the Search Radius control (§5), and the FOV comes from the FOV control. Blind invocation (Pass 3) drops the seed arguments and uses -r 179 -fov 0 -z 0.

ASTAP's documented exit codes are decoded and reported in the log:

CodeMeaning
0Solution found.
1No solution found within the search area/scale.
2Not enough stars detected in the image.
16Error reading the image file.
17Error reading the star reference database (catalogs missing/misconfigured).
32Not enough memory.
33Error writing output files (temp directory permissions).

Even on exit code 0, SASpro verifies the .wcs sidecar was actually written and contains CRVAL1/CTYPE1 — ASTAP occasionally exits normally without producing a solution, and treating that as success would propagate a broken WCS to downstream tools.

14.17 Astrometry.net integration

Pass 4 of the fallback chain runs Astrometry.net (Lang et al. 2010), either locally via solve-field or over the web API at nova.astrometry.net.

Local solve-field

If the Astrometry.net path is set in Preferences (paths/solve_field or astrometry/solvefield_path), SASpro runs solve-field as an external process with --overwrite --no-remove-lines --cpulimit 300 --downsample 2 --write-wcs wcs. On success, SASpro reads the .wcs sidecar or the .new FITS and extracts the WCS header for merging.

Web API

If local solve-field isn't configured, SASpro falls back to the web API — the API key is read from Preferences (api/astrometry_key, with legacy keys auto-migrated). The sequence:

  1. Login — POST to api/login with the API key, receive a session token.
  2. Upload — convert the image to 16-bit mono FITS, POST to api/upload, receive a submission ID.
  3. Poll for job assignment — GET api/submissions/<subid> every second until the job ID appears (max 15 minutes).
  4. Poll for calibration — GET api/jobs/<jobid>/calibration/ every 1.5 seconds until the solution dict appears with ra, dec, pixscale, orientation, parity (max 15 minutes).
  5. Download the WCS FITS — GET wcs_file/<jobid> (includes SIP terms when the solve produced them). Parse into a header.

Every request has retry-with-exponential-backoff on 429, 500, 502, 503, 504 and connection/timeout errors (up to 5 retries, 0.5·2^(n-1) + jitter seconds between). File handles are rewound before each retry since requests consumes them on each POST attempt.

If the WCS FITS download fails, SASpro builds a plain TAN header from the calibration dict instead, through the same shared CD constructor the seed WCS uses (§14.3). With s the pixel scale in degrees and θ the orientation (positive CCW from north):

unmirrored CD1_1 = -s cosθ CD1_2 = +s sinθ det = +s² CD2_1 = -s sinθ CD2_2 = -s cosθ mirrored CD1_1 = -s cosθ CD1_2 = -s sinθ det = -s² CD2_1 = -s sinθ CD2_2 = +s cosθ

Astrometry.net reports parity > 0 when the solution required an E/W flip, which selects the mirrored form. The determinant is ±s² at every orientation and the two columns stay perpendicular — see the warning in §14.3 for what the earlier half-row/half-column form did instead. Downside of this path regardless: no SIP, so the fit is linear only.

15. Blind solving (quad index)

Finding the field from the star pattern alone — no coordinates, no field of view, no hints.

15.1 Why quads

Triangle matching (§14.7) needs a seed: it compares the image against a projection of the catalogue, and to project the catalogue you must already know roughly where you are. Blind solving inverts the problem — identify the field with no prior at all — and that needs a geometric signature you can look up in a table rather than compare pairwise.

A triangle gives two invariant numbers (§14.7). Two is not enough to be distinctive across the whole sky: with a few hundred million catalogue triangles, a 2-dimensional code collides constantly. Four stars give a 4-dimensional code, and the false-match rate falls as the fourth power of the tolerance. That is the whole reason the index stores quads rather than triangles.

SASpro's implementation follows the scheme Lang, Hogg, Mierle, Blanton & Roweis set out for Astrometry.net (2010, AJ 139:1782). The index itself is built locally from the Gaia tiers you already downloaded — nothing extra comes off the wire.

15.2 The AB-frame code

Take four stars. Call the widest-separated pair A and B. Define a local coordinate frame in which A sits at (0, 0) and B at (1, 1), and express the two remaining stars C and D in that frame:

\mathbf{c} = (x_C,\,y_C,\,x_D,\,y_D)

Because the frame is defined by the stars themselves, the code is unchanged by translation, rotation and uniform scale — exactly the degrees of freedom an unknown pointing, rotator angle and plate scale represent. Only quads whose C and D fall inside the circle with AB as diameter are kept, which bounds the code to a known box and makes the hash dense.

Two symmetries remain and are removed by canonicalisation: A↔B and C↔D swaps are collapsed by requiring x_C ≤ x_D and then x_C + x_D ≤ 1.

Parity is not symmetrised away — deliberately. A reflection maps (x, y) → (y, x) in the AB frame, so a mirrored image produces a different code rather than the same one. SASpro queries both the code and its mirror, and which one hits tells you the parity of the frame outright. Folding parity into the code would have thrown that information away and left the solver to rediscover it.

15.3 Index layers

A quad only helps if it is roughly the size of your field: a 4′ quad is invisible in a 20° frame, and a 4° quad does not fit inside a 20′ one. The index is therefore built as layers, each tied to one angular scale, each solving fields from about 2.5× to 10× its quad size. Layer spacing is a factor of 2, so adjacent layers overlap and a field near a boundary is covered twice.

Built from the Bright (G < 17) astrometric tier, the figures below are measured from real builds, not estimates:

LayerQuad sizeNeeds stars toSolves fieldsQuadsOn diskBuild time
Layer 74.3°G ≤ 8.010.7° – 34.1°18,308447 KBseconds
Layer 62.1°G ≤ 8.05.3° – 17.1°42,0571 MBseconds
Layer 51.1°G ≤ 9.12.7° – 8.5°208,8415 MBseconds
Layer 432′G ≤ 10.51.3° – 4.3°754,15318 MBunder a minute
Layer 316′G ≤ 12.040′ – 2.1°2,738,16365 MBabout a minute
Layer 28′G ≤ 13.620′ – 1.1°9,653,192230 MBa few minutes
Layer 14′G ≤ 15.410′ – 32′34,900,003832 MBabout 6 minutes
Layer 02′G ≤ 17.45′ – 16′127,719,1632.97 GBroughly 30–45 min

Layers 7–2 are marked Recommended and together cover roughly 20′ to 34° — everything most imaging rigs produce — for under 320 MB and a few minutes of building. Layers 1 and 0 are for long-focal-length narrow fields and are the only expensive ones. Build them from the Gaia DR3 Library's Blind Solve Index tab, reachable from the Open Gaia Database… button in this dialog.

Each layer is a single .saq file: a 128-byte header followed by four contiguous memory-mapped arrays (index stars, sorted hash keys, quantised codes, quad references). Nothing is loaded into RAM up front; lookups page in from the map.

15.4 Matching and verification

Detected stars are ranked by brightness and formed into quads, each quad's code is computed and hashed, and the matching layers are queried. Code bins were sized once at BIN_TOL = 0.010, but the tolerance actually applied per quad is adaptive:

\tau \approx 3\sigma_{\mathrm{px}} / |AB|_{\mathrm{px}},\qquad \tau \in [0.0025,\;0.010]

A given centroid error matters far more to a 30-pixel quad than to a 120-pixel one, and since false hits scale as τ⁴, giving large quads the tight tolerance they have earned removes most of the spurious traffic before verification.

A code hit is only a hypothesis. Each one implies a similarity transform from the four correspondences; SASpro applies it to the whole field and asks how much better the data is explained by that pointing than by stars landing where they did at random. The test is the Bayesian log-odds of Lang et al. §3, and a candidate is accepted at:

\ln(\text{odds}) > \ln(10^9) \approx 20.7

That threshold is what makes an all-sky search safe: it is strict enough that millions of wrong hypotheses can be tested without one slipping through. In practice the separation is not marginal — correct candidates score in the hundreds of nats while the best wrong candidate sits well below zero, so the decision is rarely close. The accepted log-odds is written to the header as BLNDODDS.

15.5 Blind as a seed source

Blind solving does not replace the Gaia solver — it seeds it. A quad match returns centre, scale, roll and parity, which is enough to collapse the spiral search (§14.14) from 19 centres × 5 scales × 2 parities down to a single attempt that is already correct. The Gaia solver then refines it to a full SIP solution exactly as it would from a header seed.

This is why blind solving is fast here rather than a last resort.

The quad index is not consulted when the header already has a usable RA/Dec. The checkbox means what it says. An earlier build did run it on every solve, as an accelerator — one lookup collapses the whole grid — but that made a frame with perfectly good pointing go through the index anyway, and it is no longer needed: a seed that turns out to be wrong is caught by the blind retry (§15.6) once the seeded pass has failed. The cost of that choice is a second or so on a badly-seeded frame, which now fails a spiral search before retrying blind rather than being placed instantly.

If no pixel scale is available either, every built layer is tried coarsest-first, which costs more but needs nothing from the header at all.

15.6 Blind retry after a failed seed

Blind solving fires on its own only when the header has no RA/Dec — and SASpro accepts eight different keys as pointing (CRVAL1, RA, OBJCTRA, TELRA, MNTRA, CENTERRA, RA_OBJ, ALPHA_J2000). Capture software writes the mount's RA/DEC into every light, and a stacked master inherits them, so a frame almost always looks seeded even when those numbers are stale or wrong.

So when a seeded attempt fails, SASpro retries it blind before descending to ASTAP. The seed is the prime suspect by definition — it is the one thing that just demonstrably did not work.

The retry discards the scale as well as the pointing. That is more than the Force blind solve checkbox does, and deliberately so. The checkbox keeps the header scale because focal length × pixel size stays true even when the mount lied. On a retry, though, we only know that something in the seed was wrong — and if it was the scale, keeping it poisons the retry twice over: it picks the wrong index layers and it searches the wrong scale band. With no hint at all the blind solver tries every built layer coarsest-first and works it out.

Measured on master headers with deliberately broken seeds:

Master headerBeforeAfter
right RA/Dec, wrong scalefail → ASTAPsolved, 1.34 s
wrong RA/Dec (40° off), right scalesolved, 0.51 ssolved, 0.51 s (quad seed, no retry needed)
both wrongfail → ASTAPsolved, 1.06 s
good seedsolved, 0.47 ssolved, 0.47 s, no retry
no pointing at allsolved, 0.51 ssolved, 0.51 s, no retry

The retry is skipped whenever it would merely repeat the first pass: force-blind already ticked, seed mode None, blind disabled, no RA/Dec in the header, or no index layers installed. So it costs nothing on the paths that already work. Acquisition keys (OBJECT, EXPTIME, …) survive the retry; only WCS and scale cards are dropped from the header handed to it.

Because this happens inside the solver, tools that call it — the Stacking Suite's master plate-solve among them — get the behaviour without any change of their own.

15.7 Seed-grade fallback

Sometimes the quad index identifies the field correctly and the refinement still cannot reach the quality gate. Returning nothing in that situation throws away a real identification, so SASpro instead returns the quad placement itself, clearly labelled:

The WCS is built from the quad match's own centre, scale, roll and parity — not the header's rotation, since a wrong CROTA2 is one of the reasons refinement fails in the first place. Expect accuracy of a few pixels: enough to know what you photographed and to overlay a catalogue, not enough to measure with.

Credits. The solver orchestration (four-pass fallback chain with cross-solver state preservation and settings restoration), the In-House Gaia DR3 engine (triangle-invariant matcher with vertex-vote-and-RANSAC consensus, Hough translation-vote fallback with 3×3 neighbourhood merging and similarity-RANSAC refinement, spiral seed search over scale and parity variants, iterative WCS refinement with pixel-relative convergence, pixel-relative RMS quality gate, failed-match catalog-star exclusion for retry rounds, SIP-order-by-match-count fitting, focal-length recomputation from solved plate scale, bidirectional SIP degree search on parameter-penalised residuals, unit-vector projection-point centroid for pole- and wrap-safe TAN fitting), the native blind solver and its locally-built quad index (layer design and scale banding, .saq memory-mapped index format, multi-process local builder, per-quad adaptive code tolerance, quad-seeded grid collapse, automatic blind retry after a failed seeded solve, and the labelled seed-grade fallback), the acquisition-header-preserving WCS merge policy, the catalog object lookup with M/NGC/IC/UGC/Sh2/LBN/LDN/Abell/PK/PN-G/Arp prefix normalisation and both bundled bright-star and 100 000+-object catalog search, the ASTAP seed heuristics (radius and FOV mode controls, scale-aware FOV computation, exit-code decoding and sidecar validation), the preset system with draggable canvas shortcuts, and the View Bundle application are all original work by Franklin Marek for SetiAstro Suite Pro.

Standard algorithms credited to community: triangle star-pattern matching follows Groth (1986, AJ 91:1244), with the invariant-pair + vertex-vote formulation and RANSAC consensus structure following Beroiz, Cabral & Sanchez (2020, A&C 32:100384) (astroalign); Hough-transform catalog matching follows Hough (1962, US Patent 3,069,654) and Ballard (1981, Pattern Recognition 13(2):111), with the voting-on-translation-offsets application following Tabur (2007, PASA 24:189); FOCAS voting-based catalog matching is Valdes et al. (1995, PASP 107:1119); RANSAC is Fischler & Bolles (1981, Comm. ACM 24(6):381). FITS WCS standards: TAN gnomonic projection follows Greisen & Calabretta (2002, A&A 395:1061); SIP distortion polynomial follows Shupe et al. (2005, ADASS XIV); the four-star AB-frame geometric hash and the Bayesian log-odds acceptance test used by the blind solver follow Lang, Hogg, Mierle, Blanton & Roweis (2010, AJ 139:1782), implemented natively against SASpro's own locally-built Gaia quad index.

External tools and libraries: ASTAP is Han Kleijn's standalone plate solver (used as an external process; SASpro drives it with seed arguments and parses its .wcs sidecar); Astrometry.net is Lang et al. (2010, AJ 139:1782) with the web API at nova.astrometry.net and local solve-field both supported. Gaia DR3 catalog data is Gaia Collaboration et al. (2023, A&A 674:A1), delivered through SASpro's local astrometric-tier library (preferred) or the XP-spectral library (fallback, shared with SSSC). SEP (Barbary 2016, JOSS 1:58) provides star detection, deriving from Source Extractor (Bertin & Arnouts 1996, A&AS 117:393). Astropy provides FITS I/O, the WCS class, fit_wcs_from_points, proj_plane_pixel_scales, and SkyCoord. SciPy provides cKDTree / KDTree for nearest-neighbour matching and linalg.lstsq for affine fits. NumPy provides the array engine, PyQt6 provides the UI framework (including QProcess for driving external solvers), and requests provides HTTP transport for the Astrometry.net API.