Alignment¶
Alignment is the iterative loop that turns a pile of single-epoch star lists
into one cross-matched StarTable in a common
coordinate frame. Two driver classes in flystar.align implement it, and
they differ only in what the common frame is.
MosaicSelfRefThe frame is defined by the data itself. One list is nominated as the reference for the first pass (
ref_index, default 0); from the second iteration onward the reference becomes the sigma-clipped mean position of all lists. Use this when you have no external astrometric anchor and want a self-consistent relative frame.MosaicToRefThe frame is an external reference list you supply – Gaia, an HST catalog, a previous alignment. The reference is not redefined by the data.
Both are constructed with their parameters and then run with fit():
from flystar import align, transforms
msc = align.MosaicSelfRef(
list_of_starlists,
dr_tol=[1.0, 0.5, 0.3], # one entry per iteration
dm_tol=[2.0, 1.0, 1.0],
outlier_tol=[None, 3.0, 3.0],
trans_class=transforms.PolyTransform,
trans_args=[{'order': 1}, {'order': 2}, {'order': 2}],
motion_models=['Linear'],
matching='chi2',
)
msc.fit()
ref = msc.ref_table # the StarTable result
trans = msc.trans_list # the derived transform objects, one per list
Note the per-iteration lists. dr_tol, dm_tol, outlier_tol and
trans_args each take one entry per iteration, which is how you tighten
tolerances and raise the transformation order as the solution converges.
The parameters, group by group¶
Both aligners take the same arguments except where noted; there are a lot, so
they are grouped by what they control. Only list_of_starlists (and, for
MosaicToRef, ref_list) is required.
Per-iteration schedules¶
These are the ones that take one entry per iteration, and are how the solution converges: match loosely at first, then tighten as the transformation becomes better known.
Parameter |
Default |
What it does |
|---|---|---|
|
required |
Match radius per iteration, or one value for all of them. In reference-frame units, and must be generous enough on the first pass to cover the initial frame offsets. |
|
|
Match magnitude tolerance per iteration, or one value for all of them.
|
|
|
Sigma clipping on the transformation residuals, per iteration, or one
value for all of them. |
|
|
Arguments to |
|
|
Magnitude range for deriving the transformation. |
|
|
|
The number of match/transform/average passes is the length of the longest
schedule given: dr_tol, dm_tol, outlier_tol or trans_args.
Single values are broadcast to it, so dr_tol=0.5, dm_tol=[1.0, 0.5] is two
passes at a constant radius, and all single values is a single pass. Two
schedules of differing length are an error rather than a guess. mag_lim
and ref_mag_lim do not take part: a [min, max] pair is not itself a
schedule, so it is checked against the number of passes rather than setting
it.
Arguments |
Result |
|---|---|
|
One pass. |
|
Two passes, tightening the radius. |
|
Two passes at a constant radius, tightening the magnitude cut. |
|
Three passes, no clipping on the first. |
|
Two passes, raising the polynomial order once there are enough good matches to constrain it. |
|
One pass. |
|
|
|
|
trans_args has a per-starlist axis on top of its per-iteration one, like
mag_lim, and is normalized to (N_iters, N_lists). A nested list gives
individual starlists their own transformation arguments:
# Three lists, two iterations; the third list is fitted at a higher order.
trans_args=[[{'order': 1}, {'order': 1}, {'order': 2}],
[{'order': 2}, {'order': 2}, {'order': 3}]]
As with mag_lim, a flat list indexes iterations, never starlists, so
per-list arguments always use the nested form – see
Transformations for the full set of forms.
Matching¶
Parameter |
Default |
What it does |
|---|---|---|
|
|
How a star with several candidates inside the tolerances is resolved.
See the section above; use |
|
|
|
|
|
|
|
|
|
The initial guess¶
The first pass has no transformation yet, so one has to be found blind.
Parameter |
Default |
What it does |
|---|---|---|
|
|
|
|
|
Order of the transformation used for that first guess. |
|
|
|
|
|
|
|
|
A list of ready-made transform objects to start from, one per list. Skips the blind guess entirely. |
|
|
Polygon vertices per list, shape |
The transformation¶
Parameter |
Default |
What it does |
|---|---|---|
|
|
The transformation model. See Transformations. |
|
|
Which uncertainties weight the transformation fit:
|
|
|
Also derive the inverse transformations, into |
|
|
Fit a magnitude offset per list alongside the positional transformation. |
Which stars drive the fit¶
Parameter |
Default |
What it does |
|---|---|---|
|
|
Magnitude range used for deriving transformations, before |
|
|
Called with |
|
|
|
|
|
|
|
|
|
|
|
|
Per-star freeze control: update_ref_motion¶
While update_ref_orig applies a catalog-level update policy, the update_ref_motion column on ref_list (and ref_table) gives per-star control over whether a star’s motion model is refit each iteration.
Value |
Meaning |
|---|---|
True (default) |
Refit this star normally. The update_ref_orig setting still applies – original-catalog stars may or may not be updated depending on update_ref_orig. |
False |
Freeze this star unconditionally. Its position and all motion-model parameters are preserved on every iteration regardless of update_ref_orig. This is an override that takes precedence over everything else. |
Stars that do not have this column are treated as True. Stars added to the reference frame during alignment (via use_ref_new=True) always start with update_ref_motion=True so that they are immediately refittable.
To freeze a handful of high-quality anchor stars while letting the rest of the catalog evolve freely:
from astropy.table import Column
import numpy as np
# ref_list is your input StarTable / StarList
freeze_mask = np.zeros(len(ref_list), dtype=bool)
freeze_mask[anchor_indices] = True # indices of stars to freeze
ref_list['update_ref_motion'] = ~freeze_mask
align_obj = MosaicToRef(
ref_list=ref_list,
update_ref_orig=True, # all other stars update normally
...
)
Interaction between update_ref_orig and update_ref_motion
Think of update_ref_orig as a policy for the whole catalog and update_ref_motion as a per-star veto power:
update_ref_orig decides, for each iteration, which stars may be updated — it selects based on the ref_orig flag and the pass number.
A star with update_ref_motion=False is always frozen, no matter what update_ref_orig says. Internally, the frozen mask is OR-ed into the keep_orig array before any values are restored, so both controls act through the same code path.
Combining the two:
update_ref_orig=False, update_ref_motion=True # whole catalog frozen (Gaia mode)
update_ref_orig=True, update_ref_motion=True # whole catalog evolves
update_ref_orig=False, update_ref_motion=False # redundantly frozen; fine
update_ref_orig=True, update_ref_motion=False # THIS star frozen; rest evolve
Motion fitting¶
These are handed straight to
fit_motion_models(); see
Motion models.
Parameter |
Default |
What it does |
|---|---|---|
|
|
Candidate models. Each star gets the most complex one it has enough
epochs to support, unless a |
|
|
Fixed model parameters, e.g. |
|
|
Weighting scheme for the motion fit: |
|
|
|
Ways to name a motion model
motion_models accepts any of these, and mixes them freely:
motion_models='Linear' # a single name
motion_models=['Linear'] # a list of names
motion_models=['Linear', 'Acceleration'] # several
motion_models=[motion_model.Linear] # the classes themselves
motion_models=[motion_model.Linear, 'Acceleration'] # mixed
motion_models='linear' # case does not matter
The valid names are 'Empty', 'Fixed', 'Linear',
'Acceleration' and 'Parallax', matched case-insensitively, so
'linear' and 'Linear' are the same model. Only the canonical
spelling propagates – motion_model_used in the output reads 'Linear'
however you spelled the input – so there is still exactly one name to
compare against downstream. A genuine misspelling raises rather than being
silently ignored; the match is case-insensitive, not fuzzy.
Whatever you pass, 'Empty' and 'Fixed' are always added if absent,
and the list is sorted by increasing parameter count, since that ordering is
what “the most complex model this star can support” is resolved against. So
motion_models='Linear' becomes ['Empty', 'Fixed', 'Linear'], and
msc.motion_models reports the expanded list rather than what you passed.
trans_args broadcasts too, but over iterations and starlists rather
than over stars: a single dict is used everywhere, so
trans_args={'order': 1} and trans_args=[{'order': 1}] * iters are
equivalent. It also takes a nested (N_iters, N_lists) form, for when
one list needs a different transformation from the rest. See
Transformations.
Output and bookkeeping¶
Parameter |
Default |
What it does |
|---|---|---|
|
|
Directory for the results: |
|
|
Prefix for those file names. |
|
|
Also pickle the whole mosaic object. Heavier and tied to flystar’s class
definitions, but lets you reload and call methods like
|
|
|
Also save a transformation diagnostic plot per (list, iteration). Real cost on large lists – an unthinned scatter at dpi=300 each time. |
|
|
If an input list carries its own |
|
|
|
What fit() produces¶
fit()’s output is self.ref_table. It carries both the full 2D record of
every matched measurement – x, y, m and their uncertainties, one
column per input list – and the collapsed per-star quantities: x0, y0,
m0, their _err counterparts, n_detect, and the motion-model
parameters with motion_model_used recording which model each star got.
self.trans_list holds the derived Transform2D
objects, one per input list. If calc_trans_inverse=True was set,
self.trans_list_inverse holds their inverses.
Passing save_path writes the results to disk: PREFIX_input.txt (the fit
parameters), PREFIX_ref_table.hdf5, and PREFIX_trans_list.pkl (the
transform objects need pickling, as they are not plain data).
Matching¶
The matching keyword selects how a star with more than one candidate inside
the tolerances is resolved.
'legacy'(default)The historical rule: a multi-candidate star is matched only if its nearest candidate in position is also its nearest in magnitude. In a crowded field this discards good matches – a candidate 20x closer in position loses to one a few hundredths of a magnitude nearer – and each discarded star then becomes a duplicate reference row that makes the next star list ambiguous in turn, so one split seeds the next.
'chi2'Candidates are scored as
(dr/sigma_pos)**2 + (dm/sigma_mag)**2and only reciprocal best pairs winning bydchi2_tol(default 9.0, a 3-sigma margin) are kept; below that margin the star is treated as genuinely ambiguous and left unmatched. The scales are measured from the star lists’ own unambiguous pairs by default, so no error columns are required. Seematch_chi2().
If you are aligning a crowded field and seeing single stars split into several
rows, matching='chi2' is the fix.
The initial guess for the first transformation comes from
init_guess_mode='miracle' (miracle_match_briteN(),
a triangle match on the briteN brightest stars) at order init_order.
Where the lists only partially overlap, starlist_vertices restricts the
initial guess to stars inside the given polygons.
init_guess_mode='name' is the alternative, and is much more robust when
your star lists already carry consistent names. It has one sharp edge worth
knowing: ignore_contains (default 'star') excludes any name containing
that substring from the match. The default is deliberate – auto-detected
sources labelled star_1, star_2, … carry per-epoch detection indices
rather than stable identities, so matching on them would pair unrelated stars.
But it means a catalog whose names happen to contain 'star' gets filtered
away. Pass ignore_contains=None when your names are genuinely stable across
epochs. The filter warns when it excludes anything, and '' is rejected
rather than treated as “off”.
Controlling which stars drive the fit¶
Several mechanisms narrow the set of stars used to derive transformations, without dropping them from the output. In order of increasing specificity:
mag_limA magnitude range per list.
ref_mag_limMosaicToRefonly. The same idea, on the single reference list rather than per starlist.outlier_tolPer-iteration sigma clipping on the transformation residuals. Rejection triggers a re-derivation of the transformation, so the rejected stars do not continue to influence it.
- The
'w'column An optional per-star weight column on an input
StarList. The weight entering the transformation isstar_list['w'] * ref_list['w'] * weight_from_keyword. This is the most flexible knob, letting you decide as a function of both star and epoch what is good enough to constrain the transformation. Being usable (w > 0) is necessary but not sufficient – magnitude limits and outlier rejection still take precedence.iter_callbackA function called with
(ref_table, iteration)at the end of every iteration, and once more after the final re-matching pass with an index equal to the number of iterations – one past the last iteration – so that final call can be told apart from the end of the last iteration. The table handed in is the liveref_table, so settinguse_in_trans = Falseon a row excludes that star from subsequent transformations while keeping it in the output.
def reject_bright_saturated(table, i):
if i == 0:
table['use_in_trans'][table['m0'] < 10.0] = False
msc = align.MosaicSelfRef(lists, iter_callback=reject_bright_saturated,
dr_tol=[1., .5, .3], dm_tol=[2., 1., 1.])
msc.fit()
Parallelism¶
fit() takes processes for the per-star motion-model fitting (see
Motion models – it only helps when bootstrap > 0) and,
independently, match_workers for the KDTree neighbour search inside
match().
match_workers defaults to 1, deliberately: grabbing every core is the wrong
default on a shared machine. Set -1 to use all cores, which is measurably
faster on large star lists and does not change the matching result – the
per-query neighbour lists are identical, order included, regardless of thread
count.
Getting to an absolute frame¶
After a relative alignment, flystar.analysis provides the Gaia path:
query_gaia(),
prepare_gaia_for_flystar() and
project_gaia() to build the reference list, then
shift_reference_frame() to apply the
resulting velocity and parallax offsets to both the fitted motion parameters
and the time-series astrometry.
Note that query_gaia() contacts the live Gaia TAP
service and the astroquery version pinned here exposes no timeout, so a slow
day at ESA will block rather than fail. Cache the catalog once and read it from
disk in anything automated.