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.
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:
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.
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.
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.
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.
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.
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:
Two independent controls sit below the seed mode:
In Manual seed mode three fields are enabled:
HH:MM:SS[.s], decimal hours (0–24), or decimal degrees (0–360).±DD:MM:SS[.s] or decimal degrees (−90 to +90).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.
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.
Which solvers run and in what order:
| Preference | What 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 only | Try 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 only | Try ASTAP seeded, then ASTAP blind. Classic workflow for people with ASTAP catalogs installed and no Gaia library. |
| Astrometry.net only | Try 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. |
With Gaia DR3 → ASTAP → Astrometry.net (all) selected, SASpro tries up to four passes in sequence and stops at the first that succeeds:
-ra, -spd, -scale, -r and computed -fov arguments. Very fast when seed is accurate.-r 179 -fov 0 -z 0, no coordinates. Slower but can solve from nothing.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.
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:
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.
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:
▶ /path/to/image.fits — starting this file. ✔ saved: /output/image_plate_solved.fits — success. ❌ <error> — failure (continues with the next 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.
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.
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.
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():
RA---TAN/DEC--TAN to RA---TAN-SIP/DEC--TAN-SIP.BLNDODDS (log-odds of the quad match against the background hypothesis), BLNDLAYR (which index layer matched) and BLNDPAR (match parity). A seed-grade fallback additionally sets SOLVSRC = 'quad-seed' and writes HISTORY cards saying so; it deliberately carries no SOLVRMS, because there is no fit to quote a residual from and a fabricated number would let approximate astrometry pass for refined.meta['pre_solve_header'] on the first solve of a document, so re-solving never loses the original acquisition cards even after repeated solves.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.
The seeding, matching and WCS-fitting pipeline, in order.
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:
astropy.wcs.WCS and take proj_plane_pixel_scales, which handles CD, PC+CDELT, SIP all together. This is the preferred path.CD1_1 and CD2_1 (degrees/pixel):
sqrt(|det(CD)|) × 3600.PIXSCALE, SECPIX, SECPIX1, SECPIX2, SCALE, PLATESCL, PLTSCALE (any of these, if present, is already in arcsec/px).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.
Covered in §13 above. The implementation lives in _merge_wcs_into_base_header(). Key invariants:
FILE_PATH, FITS_HEADER, BIT_DEPTH, WCS_HEADER, __HEADER_SNAPSHOT__, ORIGINAL_HEADER, PRE_SOLVE_HEADER) are stripped from any header before it's handed to FITS/WCS code — those names can hold Python objects or very long strings that trigger HIERARCH warnings or WCS failures.A_ORDER='2', CRPIX1='1024.0') can't trip astropy's string-vs-int comparisons.A_ORDER/B_ORDER, AP_ORDER/BP_ORDER) is auto-filled if only one of a pair is present.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 θ:
for the det>0 branch, and the mirrored equivalent for det<0. §14.14 explains why both are tried.
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.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.
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:
σ = 50, 25, 15, 10, 5, 3 in sequence. The first sigma that returns at least 1000 stars wins; if 3σ still gives fewer, whatever it found is used.2000/9 ≈ 222 brightest-first stars are kept per cell, preventing the brightest region of the image from monopolising the match._normalize_for_astap (which applies the shared mono/colour stretch for faint-star visibility), then collapse to float32 greyscale.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.
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.
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:
and encode the triangle's shape as the two side ratios:
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.
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).
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.
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).
From the voted correspondences, fit a 6-parameter affine transform via RANSAC:
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.
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.
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.
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:
Translations outside half the image size are rejected (no sense voting for a shift that would place every star off-screen).
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.
Fit a 4-parameter similarity transform (rotation, uniform scale, translation) over the pool:
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.
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.
Both matchers use the classical RANSAC algorithm (Fischler & Bolles 1981):
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.
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.
| Projection point | Offset from field centre | TAN RMS | SIP-3 RMS | Fitted scale |
|---|---|---|---|---|
| mean of RA, mean of Dec | 1.29° | 551.45" | 186.63" | 4.5916"/px |
| unit-vector mean | 0.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:
centred on the reference point (CRVAL1, CRVAL2).
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:
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:
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.
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:
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.
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:
k parameters — or it is not attempted.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:
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:
| Case | pairs | SIP-2 | SIP-3 | SIP-4 | SIP-5 | TAN | Chosen |
|---|---|---|---|---|---|---|---|
| heavy radial distortion | 1200 | 281.6" | 4.5" | 283.0" | 243.1" | 283.9" | SIP-3 |
| mild distortion | 1200 | 16.3" | 1.57" | 1.56" | 14.6" | 16.7" | SIP-4 |
| no distortion | 1200 | 1.40" | 1.40" | 1.39" | 2.59" | 2.59" | SIP-4 |
| heavy distortion | 250 | 274.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.
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.
Before accepting a solve, SASpro checks fit quality. The RMS of final residuals (pair-wise angular separations):
and the 95th-percentile residual:
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:
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).
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:
cos(Dec) correction on the RA offset.Expressed in sky coordinates with ring radius r and azimuth φ:
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.
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:
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.
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):
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:
| Code | Meaning |
|---|---|
| 0 | Solution found. |
| 1 | No solution found within the search area/scale. |
| 2 | Not enough stars detected in the image. |
| 16 | Error reading the image file. |
| 17 | Error reading the star reference database (catalogs missing/misconfigured). |
| 32 | Not enough memory. |
| 33 | Error 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.
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.
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.
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:
api/login with the API key, receive a session token.api/upload, receive a submission ID.api/submissions/<subid> every second until the job ID appears (max 15 minutes).api/jobs/<jobid>/calibration/ every 1.5 seconds until the solution dict appears with ra, dec, pixscale, orientation, parity (max 15 minutes).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):
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.
Finding the field from the star pattern alone — no coordinates, no field of view, no hints.
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.
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:
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.
(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.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:
| Layer | Quad size | Needs stars to | Solves fields | Quads | On disk | Build time |
|---|---|---|---|---|---|---|
| Layer 7 | 4.3° | G ≤ 8.0 | 10.7° – 34.1° | 18,308 | 447 KB | seconds |
| Layer 6 | 2.1° | G ≤ 8.0 | 5.3° – 17.1° | 42,057 | 1 MB | seconds |
| Layer 5 | 1.1° | G ≤ 9.1 | 2.7° – 8.5° | 208,841 | 5 MB | seconds |
| Layer 4 | 32′ | G ≤ 10.5 | 1.3° – 4.3° | 754,153 | 18 MB | under a minute |
| Layer 3 | 16′ | G ≤ 12.0 | 40′ – 2.1° | 2,738,163 | 65 MB | about a minute |
| Layer 2 | 8′ | G ≤ 13.6 | 20′ – 1.1° | 9,653,192 | 230 MB | a few minutes |
| Layer 1 | 4′ | G ≤ 15.4 | 10′ – 32′ | 34,900,003 | 832 MB | about 6 minutes |
| Layer 0 | 2′ | G ≤ 17.4 | 5′ – 16′ | 127,719,163 | 2.97 GB | roughly 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.
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:
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:
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.
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.
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.
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.
Measured on master headers with deliberately broken seeds:
| Master header | Before | After |
|---|---|---|
| right RA/Dec, wrong scale | fail → ASTAP | solved, 1.34 s |
| wrong RA/Dec (40° off), right scale | solved, 0.51 s | solved, 0.51 s (quad seed, no retry needed) |
| both wrong | fail → ASTAP | solved, 1.06 s |
| good seed | solved, 0.47 s | solved, 0.47 s, no retry |
| no pointing at all | solved, 0.51 s | solved, 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.
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:
SOLVSRC = 'quad-seed', plus BLNDODDS, BLNDLAYR and HISTORY cards recording that refinement failed its gate.SOLVRMS, deliberately. There is no fit to quote a residual from, and inventing one would let seed-grade astrometry pass for refined in every downstream tool.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.
.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.
.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.