Mm rework lingfeng into Dev - #26
Open
wei-lingfeng wants to merge 424 commits into
Open
Conversation
Pull Lingfeng's updates into main repo new branch
…eLab/flystar into mm_rework_lingfeng
…flystar into mm_rework_lingfeng
…ter ref table has been created in align; Added save path in plots; Moved all testing data files under flystar/test/test_data; Removed trailing spaces
…or; motion_model_used column does not need to be set separately outside of the motion model fit function
…otion_model_used column problem: Now it will only be determined by motion model fit, and the column will be removed if provided in the reference list
… and update_ref_table_aggregates; Fixed the data dimensions when N_times=1
wei-lingfeng
had a problem deploying
to
github-pages
August 25, 2026 06:46 — with
GitHub Actions
Failure
The README was the astropy template's stub -- a badge, a licence and the imposter-syndrome note -- with no statement of what FlyStar does and no link to either documentation build. It now opens with the one-sentence description, then names both: Read the Docs (per-branch, /en/mm_rework_lingfeng/, rebuilt on every push) and the ad-free GitHub Pages mirror (one version, whatever the branch last published), with the warning that both describe a branch whose motion-model framework is not on main, and the install lines including the optional extra. The front page ran the whole example as one block, so the reader could not see where their own work would start. It is now two: building the synthetic lists, which stands in for whatever produces the reader's star lists, and then the alignment, which is one object and one call. The check against the injected truth is called out as the part real data would not have. The sidebar gained a "Getting started" entry under FlyStar. sphinx_rtd_theme will not expand the master document's own sections -- `:maxdepth:` on the `self` toctree does nothing -- and a toctree cannot carry an anchor, so this is a second titled `self` entry. It lands at the top of the front page rather than on the section, which is the closest the theme allows without splitting the content back out into its own page.
wei-lingfeng
had a problem deploying
to
github-pages
August 26, 2026 01:16 — with
GitHub Actions
Failure
Two toctree entries pointed at the same document, so the theme marked both as
current and clicking FlyStar highlighted Getting started with it. The plain
`self` entry was redundant anyway: sphinx_rtd_theme already puts a home link
above the navigation, so the front page now has exactly one entry, and exactly
one thing highlights on any page.
Not a caption: in this theme a caption heads a group of pages -- Components,
Examples, API, Indices -- and this is one page, so a caption would be a heading
with nothing under it but the page it names.
The home link read `flystar`, taken from the distribution name in setup.cfg,
which has to stay lower case for pip. The documentation calls it FlyStar
everywhere else, so the displayed name is now set explicitly, which also fixes
the page titles ("FlyStar dev documentation").
wei-lingfeng
had a problem deploying
to
github-pages
August 26, 2026 01:27 — with
GitHub Actions
Failure
Aligning a mosaic peaked at ~9.5x the size of the reference table it was
building: 8 starlists of 10k stars produced a 122 MB table but a 264 MB
live working set and ~1.2 GB peak RSS. Most of that gap is allocation
churn -- the table is grown one starlist at a time and every step
reallocates every column -- and freed buffers are not returned to the OS,
so RSS tracks cumulative allocation rather than live memory.
The largest single object in the table was 'name_in_list', an
(N_stars x N_lists) array of U30: 120 bytes per entry, rebuilt in full
every time the table grew, and 44% of the whole table. The name of star i
in list j is already recoverable as star_lists[j]['name'][k], so store the
int32 k instead. That column drops from 54.05 MB to 1.80 MB at 8 lists.
It is renamed 'idx_in_list' so the column name still says what it holds,
and align.names_in_list() recovers the strings.
Also:
- Size motion_model_input/motion_model_used to the widest motion-model
name that exists rather than a flat U20, derived from
motion_model_map() so a new model widens the column automatically.
- Stop casting 'name' straight to U30, which silently truncated longer
identifiers (a 35-character Gaia-style name came back cut at 30). It
now widens past 30 instead, keeping the 30 characters of headroom that
analysis.py relies on when assigning label names in place.
- Replace set(range(N)) - set(idx_list) in add_rows_for_new_stars with a
boolean mask: same result, ~100x faster (208 ms -> 2.1 ms at 1e6 stars)
and without building a Python int per star.
Measured on a synthetic scan mosaic, comparing against the same run
before the change:
table live peak peak RSS
8 lists 122 -> 68 MB 264 -> 147 MB ~1192 -> ~600 MB
16 lists 443 -> 235 MB 920 -> 489 MB ~3480 -> ~1800 MB
Output is byte-identical at both scales, including a cross-check with
magnitude-dependent errors and trans_weights='both,var' active.
Note this changes the output format: tables written by earlier versions
carry a string 'name_in_list'. plots.py and analysis.py read either, but
external code that reads ref_table['name_in_list'] should move to
align.names_in_list().
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
align.py had 10 definitions with no docstring at all -- including both public classes -- and the ones that existed used six different spellings of the numpydoc section headers, so parts of the module did not render as API documentation. Every definition in the module now has a docstring, and all 51 parse cleanly under numpydoc.docscrape. Sections: - Inputs/Input -> Parameters, Output/Outputs -> Returns, Example -> Examples, "Parameters:" -> Parameters, and underlines resized to match their headers. - "Required Parameters"/"Optional Parameters" folded into one Parameters section, with optionality moved onto the type line where numpydoc expects it. - 37 entries written "name: type" corrected to "name : type", without which numpydoc does not recognise them as parameters at all. Content: - Docstrings added for MosaicSelfRef, MosaicToRef, fix_iterable_conditions, get_weights_for_lists, calc_mag_avg_all_stars, check_trans_input, update_old_and_new_names, get_weighting_scheme, logger and suppress_meta_warnings. - Parameters/Returns filled in where they were missing, most notably trans_initial_guess, which documented none of its 16 arguments. - Stale entries removed: motion_model_for_new_star (the code that would have used it is commented out), mosaic_object (it describes self), and find_transform's 'trans' (not one of its arguments). - find_transform's transModel default was documented as four_paramNW where the signature says PolyTransform. No code changed: stripping every docstring from this revision and from the previous one yields identical ASTs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Attributes section added to MosaicSelfRef listed star_lists, which
sphinx-autoapi already documents on its own: it is assigned in __init__,
and autoapi_python_class_content='both' merges the class and __init__
docs, so the attribute was described twice.
api/flystar/align/index.rst:910: WARNING: duplicate object
description of flystar.align.MosaicSelfRef.star_lists
Read the Docs sets fail_on_warning: false and so was unaffected, but
.github/workflows/docs.yml builds with -W --keep-going, where this was
enough to fail the GitHub Pages job.
ref_table, trans_list and trans_list_inverse are assigned in fit() rather
than __init__, so autoapi does not pick those up and they stay in the
Attributes section. star_lists moves into the prose above it, which keeps
the pointer to names_in_list that made it worth mentioning.
Verified by building the docs exactly as the workflow does
(sphinx -b html -W --keep-going): clean, where the same build on this
commit's parent failed on the warning above.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
wei-lingfeng
had a problem deploying
to
github-pages
August 26, 2026 06:10 — with
GitHub Actions
Failure
Four things stood between this and a working `pip install flystar`.
jplephem was undeclared. astropy reaches for it whenever a solar-system
ephemeris is evaluated, which the Parallax motion model does, so a clean
install raised ModuleNotFoundError there and failed 7 of the 71 tests.
It is a hard requirement, not an extra.
pyproject.toml's [project] silently shadowed setup.cfg. setuptools reads
[project] in preference to [metadata]/[options] and says so:
SetuptoolsWarning: `install_requires` overwritten in `pyproject.toml`
SetuptoolsWarning: `extras_require` overwritten in `pyproject.toml`
So setup.cfg's dependency list, its `optional` extra (shapely,
astroquery, plotly), its python_requires and its long-stale
`url = https://bitbucket.org/mwhosek/flystar` were all dead text. The
duplicated sections are removed and pyproject is now the single source;
setup.cfg keeps only [tool:pytest] and [coverage:*], which have no
[project] equivalent. The `optional` extra is carried over, so
`pip install flystar[optional]` works again -- those three are imported
lazily inside the functions that use them, so the package runs without
them but those features do not.
There were no classifiers at all, so the PyPI page would have carried no
license, Python versions or subject area, and the package would not have
appeared under any filtered search. `project.license` also moves from the
deprecated TOML table to an SPDX string, with license-files pointing at
the license this repo actually ships (licenses/LICENSE.rst, not the
LICENSE.rst that MANIFEST.in imagines). That form needs setuptools>=77,
now the build-system floor.
flystar/version.py was tracked in git. setuptools_scm rewrites it during
every build, which dirtied the tree and appended a ".d<date>" suffix to
the version -- so even a clean tagged build would not have produced a
plain release version. It is untracked and ignored; it is still generated
at build time, and nothing imports it except _astropy_init, which is
itself dead code.
Not addressed here: the wheel is still 96% tests/test_data (23.9 MB of a
9.5 MB wheel), and MANIFEST.in still references a CHANGES.rst and
LICENSE.rst that do not exist.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The wheel was 96% test data. flystar/tests/test_data is 24 MB, 22 MB of it
a single FITS catalogue (test_catalog.fits) that five tests in
test_startable.py read. Every user installing flystar was downloading and
storing it.
Tests now stay in the repository rather than in the distributions. Half
measures do not work here: the suite locates its fixtures through
flystar.__path__, so shipping the test modules without their data would
leave an installed suite that cannot run at all.
MANIFEST.in also referenced several things that were not there. It asked
for a CHANGES.rst and a top-level LICENSE.rst that do not exist (the
licence this repo ships is licenses/LICENSE.rst), and recursed into
cextern/ and scripts/, which do not exist either, and into *.pyx/*.c/*.pxd
for a package with no compiled extensions. It also pulled in all of
docs/examples, ~10 MB of .lis and .fits inputs for the Gaia notebook that
Read the Docs takes from the repository anyway.
CHANGES.rst is added rather than dropped from MANIFEST.in, since a release
wants release notes.
before after
wheel 9.09 MB 0.19 MB
sdist 16.66 MB 0.96 MB
Verified the sdist still builds a wheel once extracted outside git, and
that the wheel carries every module, the licence, and no tests.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Publishing with an API token means a long-lived secret that has to live in the repository settings and be rotated by hand. PyPI Trusted Publishing (OpenID Connect) avoids it: PyPI verifies the workflow's identity directly, so there is no secret to store at all. The upload runs when a GitHub Release is published, not when a tag is pushed. Creating a tag therefore stays reversible, and the upload is a deliberate second step -- worth having, because setuptools_scm takes the version from the tag and PyPI will never let that version number be uploaded again. A manual workflow_dispatch run publishes to TestPyPI instead, so the whole path can be rehearsed without spending a real version number. Requires a one-time setup on PyPI (Publishing -> Add a new pending publisher) naming this repository, publish.yml, and the pypi environment; the header comment records the exact values. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Removing the shadowed [metadata] section from setup.cfg broke the docs
build, which read the author and the GitHub project out of it:
configparser.NoSectionError: No section: 'metadata'
conf.py now parses pyproject.toml, which is the single source for project
metadata. github_project is derived from the repository URL there rather
than being a second copy of it, and the author is joined from the authors
table. tomllib is stdlib from 3.11; tomli is added to the docs
requirements for older interpreters.
Caught by CI rather than locally, because the docs build I had run was
from before the packaging change. Verified this time with the workflow's
own command, sphinx -b html -W --keep-going.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
wei-lingfeng
had a problem deploying
to
github-pages
August 26, 2026 21:43 — with
GitHub Actions
Failure
…dule The aligners took an iters argument that was never read: the iteration count has always come from len(dr_tol), so a call passing a disagreeing iters was already running the dr_tol schedule, silently. Two sources of truth for one number, with no check that they agreed. iters is gone. dr_tol becomes required, and moves ahead of the optional arguments. Matching is a radius search, so there is no meaningful default -- the old dr_tol=[1.0] silently applied a one-unit radius in whatever units the reference frame happened to use. Deriving the count from dr_tol alone was itself arbitrary, though, and it made the broadcast one-directional: dr_tol=[0.5, 0.3], dm_tol=1 worked while dr_tol=0.5, dm_tol=[1, 0.5] raised, even though the second is a perfectly sensible request for two passes at a constant radius. The count is now the length of the longest schedule -- dr_tol, dm_tol, outlier_tol or trans_args -- with single values broadcast up to it. Only single values broadcast, so two sequences of differing length remain an error rather than a guess, and nothing silently papers over a mismatch. mag_lim does not vote. Its [min, max] form is a pair, not a schedule, and a naive length vote would read mag_lim=[13, 21] as two iterations; it is still checked against the resulting count. dm_tol now defaults to None, no magnitude cut at all. The old default of [1.0] rejected pairs more than one magnitude apart, which is wrong across filters and arbitrary within one. fix_iterable_conditions needed no logic change -- its broadcast-and-assert loop was already right once self.iters was -- only sharper assertion messages, which used to name iters as though the caller could set it. Verified with the full suite (72 pass) and test_iters_from_longest_schedule, which covers each schedule setting the count in turn, the broadcast reaching every iteration rather than just the first, the conflict cases, and mag_lim abstaining. The docs build was not re-run: sphinx in this environment is missing the autoapi extension. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
One flush-left line in _normalize_schedule dropped the docstring's common indentation to zero, so every other line parsed as an indented block and the Raises heading landed inside it: "Unexpected section title". Sphinx runs with -W, so that lone docutils error failed the docs job. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both were added on this branch and run in MovingUniverseLab/flystar, where the Claude Code GitHub App is not installed and CLAUDE_CODE_OAUTH_TOKEN does not exist, so the app-token exchange 401s. 78 runs, 78 failures, none successful. Merging them into dev would plant two permanently-red workflows, and claude.yml fires on every issue and PR comment repo-wide. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
wei-lingfeng
had a problem deploying
to
github-pages
September 1, 2026 03:38 — with
GitHub Actions
Failure
…e.meta
suppress_meta_warnings() renamed over-long meta keys in place (e.g.
'list_times' -> 'HIERARCH list_times') to dodge a FITS-only warning, but
calc_bootstrap_errors() called it unconditionally before two plain
pickle.dump()s that never needed it, permanently breaking plain
meta['list_times'] lookups on the live ref_table regardless of output
format. It now returns a new dict instead of mutating the table, and
MosaicSelfRef/MosaicToRef gain a save_format parameter ('hdf5' default,
'fits', or 'pkl') so FITS's HIERARCH handling is applied only when
actually writing FITS, on a column-sharing copy of ref_table.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
wei-lingfeng
had a problem deploying
to
github-pages
September 1, 2026 06:18 — with
GitHub Actions
Failure
ref_mag_lim was applied identically on every iteration, with no way to tighten/loosen the reference magnitude cut pass-to-pass the way mag_lim, dr_tol, dm_tol and outlier_tol already can. Added MosaicToRef.fix_ref_mag_lim, called from __init__ alongside the base class's fix_iterable_conditions, to normalize None / a flat [min, max] / a per-iteration sequence into a length-iters list -- flat (N_iters, 2), not mag_lim's (N_iters, N_lists, 2), since there's exactly one reference list. match_and_transform's call in fit() now indexes self.ref_mag_lim[nn] instead of passing the same value every time, and the old ad hoc None->inf substitution (which only handled the single-pair case) is gone, folded into fix_ref_mag_lim. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Update docs/alignment.rst to match the previous commit: ref_mag_lim now takes a schedule shape ([min, max] or a sequence of iters entries), mirroring mag_lim minus the per-list axis (there's exactly one reference list). Added it to the per-iteration schedules table and the "which stars drive the fit" table, and a matching bullet alongside mag_lim in the narrative section. Verified with a full sphinx build (-W --keep-going): alignment.rst and the align.py API page build clean, no warnings from either. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
wei-lingfeng
had a problem deploying
to
github-pages
September 1, 2026 09:05 — with
GitHub Actions
Failure
infer_positions built each model's fit_params as
np.array([self[p][idx] for p in fit_param_names]).T, which collapses to
shape (0,) for Empty -- whose fit_param_names is [] -- no matter how many
stars that model covers. Empty.model then calls np.atleast_2d on it,
getting (1, 0), and reads N_stars = 1 from it, while times_grid[unique_index]
correctly carries one row per Empty star. broadcast_times compares the two
and raises:
ValueError: Empty.model: 2D time array must have one row per star --
got shape (10385, 1) for 1 star(s)
Any table with two or more stars whose parameters support no model at all
hit this, which is routine: a mosaic ref list carries a row per star that
was fit with too few valid epochs to place, and MosaicToRef propagates the
whole table to each list's epoch before matching. It surfaced as a crash
20 minutes into a MosaicToRef.fit(), naming a shape rather than the rows
responsible.
Build the no-fit-params case as np.empty((len(unique_index), 0)) instead,
so the star count survives and every model reaches model() with the same
(N_stars_this_model, N_params) convention. fit_param_errs gets the same
treatment, keeping the shapes equal for _check_param_dimensions' assert.
Verified against StarTables mixing Linear and Empty rows: a scalar epoch,
a shared multi-time grid, a per-star (N, 1) grid, an all-Empty table and a
single-Empty-star table all return nan positions / inf errors for the Empty
rows and leave the Linear rows unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Parallax.run_fit built its shared time axis with np.unique(t) over the
whole array and handed it to Time(..., format='decimalyear'). A star that
was not detected in an epoch carries nan there in t as well as in x/y --
StarTable._set_invalid_list_values fills every float column for an
undetected list, t included -- and Time rejects non-finite input outright:
ValueError: Input values did not match the format class decimalyear:
TypeError: Input values for decimalyear class must be finite doubles
So a single undetected epoch anywhere in the table killed the entire batch
fit. In a real mosaic that is not an edge case: with 27 lists and four
pointings per epoch, only 27 of 73695 stars in the ref table were detected
in every list, so effectively every row carried nan times.
The masking that already handles these epochs (`valid` zeroes them out of
every weighted sum, via weight_from_sigma and the np.where(valid, ..., 0)
block) all runs after the conversion. Every other operation in the routine
is arithmetic that propagates nan harmlessly until masked; Time is the one
step that validates its input, and it sits upstream of the mask.
Build the unique-time axis from the finite times alone, substituting a real
epoch from the batch where t is nan -- pvec for those entries is never read
-- and intersect them out of `valid` too, so a nan time arriving beside a
finite x/y cannot feed a nan dt into the sums.
Linear.run_fit never noticed because it does not convert times to MJD. The
test suite missed it on three counts: the only end-to-end MosaicToRef run
with motion_models=['Linear','Parallax'] sits under `if __name__ ==
'__main__'` and is never collected; test_startable.py never exercises
fit_motion_models with Parallax at all; and the nan-padding sweep covers
only Fixed/Linear/Acceleration, padding x/y while leaving t finite.
Verified on ragged StarTables: 5 epochs with stars missing one, missing
the first, and having only two, now fit (the two-epoch star correctly
demoting to Linear rather than fitting a 5-parameter model), while a table
with no nan times gives bit-identical results to before.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
fit() writes the ref table through _write_ref_table, which respects the save_format constructor option added in cf9c468 ('hdf5' by default). calc_bootstrap_errors instead pickled it unconditionally, so a run configured for hdf5 got PREFIX_ref_table.hdf5 from fit() and PREFIX_ref_table_bootstrap.pkl from the bootstrap -- the same table, in two formats, one of them not the one that was asked for. It also dumped the whole object unconditionally, ignoring save_object, which fit() does respect; for a mosaic that pickle runs to hundreds of MB. Route the bootstrap table through _write_ref_table as well, giving that helper a `suffix` argument so the existing PREFIX_ref_table_bootstrap name is preserved rather than becoming PREFIX_bootstrap_ref_table. A run with save_format='pkl' therefore produces exactly the same filename as before; only the default (hdf5) changes what it writes. Gate the object dump on save_object, whose default is True, so default behavior is unchanged there too. Both save_path docstrings now list the bootstrap outputs, which were previously undocumented. Verified for all three save_format values that fit()-style and bootstrap-style writes land side by side with the right extensions, and that the hdf5 bootstrap table round-trips with its meta intact. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
setup_ref_table_from_starlist builds the ref table with
StarTable(**col_arrays), from the input list's columns alone, so anything
the caller set in that list's meta is dropped. For MosaicToRef the input
is the user's ref_list, which is the natural place to attach the ra/dec a
Parallax fit needs -- and doing so failed with
KeyError: fit_motion_models: Missing required fixed parameter(s) for
the motion models used: 'ra', 'dec'! Please provide them in
fixed_params_dict, or as columns in the table, or as table metadata.
an error that names table metadata as a valid source while the metadata
in question had already been discarded. determine_motion_models and
infer_positions both consult meta the same way, so the gap was not
specific to fitting.
Copy across the meta keys that are motion model fixed parameters (ra,
dec, pa, obsLocation, t0), taken from the model classes rather than
hardcoded. Only those: the rest of a ref list's meta (n_lists, n_stars,
list_times, EPNAMES) describes the table it came from, not the one being
built, and a blanket copy would overwrite this table's own bookkeeping --
a ref list carried over from an earlier alignment reports that alignment's
list count where the new table starts with one. Keys already set by the
StarTable constructor are never overwritten, and an explicit
fixed_params_dict still takes precedence, since every lookup consults it
before the table.
Verified that ra/dec/pa on an input StarList reach the constructed ref
table while n_lists/n_stars/list_times planted alongside them do not.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
HDF5 and FITS store strings as fixed-length bytes, so a table written with a 'U' column reads back with an 'S' one: 'Linear' becomes b'Linear'. That compares unequal to every str literal, so code that works on a freshly fit table silently stops working once the table has been written and reloaded, without raising anything. A `motion_model_used == 'Parallax'` branch simply never fires, selecting the wrong degrees of freedom; `ref_table['name'] == 'star_012345'` and np.isin against a list of names match nothing at all. Override read() to call astropy's own convert_bytestring_to_unicode() on the result, so masked and multidimensional string columns are handled the way astropy handles them everywhere else, and a saved table behaves like the one that was written. Verified on a saved mosaic ref table: motion_model_used comes back <U12 with all 29130 rows matching 'Linear' (previously none did), name comes back <U30, no bytes columns remain, and the numeric columns are untouched. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
wei-lingfeng
had a problem deploying
to
github-pages
September 6, 2026 06:51 — with
GitHub Actions
Failure
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This merges
mm_rework_lingfengintodev. It supersedes #25 (which compared this branch againstmm_rework) — everything from that comparison is folded into the sections below, organized by what actually changed and why, alongside a substantial follow-on pass of correctness, performance, memory, and documentation work done since.API changes from
mm_reworkmm_reworkalready has amotion_model.pyand aFixed/Linear/Acceleration/Parallaxsystem of its own, so unlike the rest of this description, the API differences below are specifically against that branch's design (notdev, which predates motion models entirely and has neither side of any of this).default_motion_model(a single model name) →motion_models(a list):mm_rework'sMosaicSelfRef.__init__takesdefault_motion_model='Fixed', andfit_velocitiesseparately takes its owndefault_motion_model='Linear'— one model name, used as a single global fallback. Both are replaced by onemotion_modelsparameter (a list, e.g.motion_models=['Linear']): each star is now fit with the most complex model its own number of valid epochs actually supports, rather than every star sharing one default.motion_model_dict→fixed_params_dict, andt0stops being a special case:mm_rework'srun_fit(self, t, x, y, xe, ye, t0, ...)takest0as its own required positional argument everywhere, with a separate, loosely-typedmotion_model_dict={}alongside it for anything else. Both are unified into onefixed_params_dict(holdingt0forLinear/Acceleration;t0,ra,dec,pa,obsLocationforParallax), so every model's non-fit parameters flow through the same mechanism instead oft0being threaded separately from everything else.Emptymotion model:mm_reworkonly hasFixed/Linear/Acceleration/Parallax.Empty(a trivial placeholder for a star with no usable data — alwaysfill_value/inf) is new.StarTable.fit_velocities→StarTable.fit_motion_models, reflecting the move from "always fits a velocity" to "fits whichever model applies."fit_velocities_all_detected, a separate helper for the "every star has every epoch" case, has no equivalent — the general per-star model selection already covers it.fit_velocitiesparameters removed or replaced:mask_val→mask_value;fixed_t0=False(a special-cased boolean/array just fort0) is gone now thatt0is an ordinaryfixed_params_dictentry;show_progress/reassign_motion_modelare gone (reassignment now happens automatically every call from each star's current epoch count — seekeep_existing, new, to control whether stars not being refit keep their existing values instead of being overwritten withnan, described further down). New, with nomm_reworkequivalent at all:keep_existing,seed,method(scipy method name),fill_value,art_star, and the multiprocessing controlsprocesses/chunksize/mp_star_threshold.MotionModelsubclass internals renamed (relevant only if calling code subclasses or touchesMotionModeldirectly, rather than going throughStarTable/align):n_pts_req(hardcoded per class, e.g.Parallax.n_pts_req = 4) →n_params(derived asceil(n_fit_params / 2), givingParallax.n_params = 3— the same fix as then_pts_reqpoint already covered above);fitter_param_names→fit_param_names;optional_param_names(a bare list of names) →optional_fixed_params(a dict of name → actual default value);get_pos_at_time/get_batch_pos_at_time(separate per-star and per-batch methods) unified into onemodel()method that vectorizes automatically based on input shape;motion_model.get_weights→calc_sigma(see "Uncertainty model" above); newrun_fit_batchonEmpty/Fixed/Linear, with nomm_reworkequivalent (see "Vectorized, multiprocessing-aware motion-model fitting" below).mm_rework, covered in their own sections below:inherit_n_detect,match_workers,mp_star_threshold.Uncertainty model: error propagation, not weighted variance
The codebase's biggest conceptual change: uncertainty on an averaged/fit quantity is now computed by analytic error propagation (the classic "error on the mean,"
sigma = sqrt(1 / sum(1/sigma_i^2))) everywhere, instead of the sqrt of a weighted sample variance.scipy.curve_fit's covariance matrix already computes internally for models that use it — so this also makes every motion model's reported uncertainty consistent with every other one, instead ofFixedcomputing something structurally different fromLinear/Acceleration/Parallax.combine_listsswitched to error propagation (also removing its now-unnecessary >1-epoch requirement for weighted averaging), andFixed.run_fit's uncertainty was brought in line with the scipy-covariance convention every other model already used.motion_model.get_weights'sx_wt = 1/xe**2→sigma = 1/x_wt**0.5round-trip accumulated floating-point error across repeatedalign()iterations; replaced withcalc_sigma, which returnsxeorsqrt(xe)directly usable by scipy, computing an inverse-variance weight only where one's actually still needed.Parallax.run_fithad two real bugs from not using that same path:sigmawas never square-rooted before being passed tocurve_fit, andabsolute_sigmawas silently ignored. Both fixed by routing it throughcalc_sigma+absolute_sigmalike everything else. (Worked example provingcurve_fit(..., absolute_sigma=True)and analytic error propagation agree bit-for-bit is preserved from Update mm_rework to match mm_rework_lingfeng for comparison #25, at the bottom of this description.)MosaicSelfRef/MosaicToRef'sabsolute_sigmadefault changed fromFalsetoTrue— required for scipy's covariance matrix to actually mean "error propagation" rather than a reduced-chi2-rescaled quantity.Uncertainty columns:
inf, nevernan, for "no information"A second, related convention, enforced consistently for the first time: a star's uncertainty is
infwhen there's no real information to compute one from — nevernan.1/inf**2 == 0, so aninfuncertainty correctly contributes zero weight anywhere it's combined with other stars' measurements;naninstead poisons every downstream computation it touches (x + nan == nan) and vanishes silently from filters, sincenan == nanisFalseandnan < anythingisFalse. Several real bugs were all instances of this convention being violated in different places:xe/ye/medefaulted tonaninstead ofinffor invalid entries in the newly-vectorizedFixed/Emptybatch paths — corrected toinf.combine_lists' weighted "no usable uncertainty anywhere" fallback used to patch a fake weight into the raw error array, fit through that, and then had to remember to force the reported error back toinfafterward — fragile, and exactly the kind of place a fabricated finite value could silently leak through instead. Rewritten soinffalls out naturally: the weight sum is built only from real, known uncertainties, sostd = sqrt(1/wgt_sum)isinfvia ordinary1/0whenever none exist, with no override to remember.absolute_sigma=False's chi2 rescaling could turn a singular fit's correctinferror intonan—inf * sqrt(nan) == nan. Fixed by reapplying the singular/insufficient-data override unconditionally as the fit's final step, so nothing downstream can rescale it away.NaN-propagation bugs (not theinfconvention itself, but bugs the convention's violation exposed on real, imperfect data): a non-finitem0could never be excluded by a magnitude-range filter (NaNcomparisons are alwaysFalse), letting bad reference stars corrupt the magnitude-zeropoint transform; andfit_motion_models' defaultt0could silently come outNaNfor some multi-epoch stars from mixing a masked weights array with plain (not masked) averaging. Both fixed.Vectorized, multiprocessing-aware motion-model fitting
fit_motion_modelsnow fits a whole group of stars sharing a motion model in one vectorized pass instead of one star at a time, for every model that supports it:FixedandEmpty(trivial/closed-form) andLinear(closed-form 2×2 solve). On a real dataset,processes=3dropped from 7.07s to 2.68s (matching serial's 2.66s) once all three were batched — a multiprocessing pool no longer needs to spin up at all just to run an O(1) fill forEmptystars.numpy.maindexing bottleneck (tens of millions of slow per-elementnumpy.ma.core.__getitem__calls) madefit_motion_models~38% faster to fix; one multiprocessing pool is now reused across the whole call instead of respawned per motion-model group.match()gained aworkersparameter (default 1) threadingscipy.spatial.KDTree.query_ball_point;align.pyexposes it asmatch_workers(default 1 — production runs typically share a machine, so the speedup is opt-in). Verified this doesn't affect the delicatedm_min == dr_mintie-break, which depends on within-list neighbor order:workers=1vsworkers=-1give identical, order-preserved neighbor lists, checked against dense/duplicate-point edge cases and a full end-to-end run with all 37ref_tableoutput columns byte-identical.mp_star_threshold(default 100,000): a multiprocessing pool for motion-model fitting (Acceleration/Parallax, or any model withbootstrap > 0— the models with no vectorized path) is only spun up when the number of stars actually needing it meets this threshold, even ifprocesses > 1was requested. Measured break-even for that fixed pool-spawn/IPC overhead was between 20,000 and 100,000 stars on a 10-core machine.Memory footprint at mosaic scale
StarTableconstruction gained an opt-incopy=Falseand now builds all columns in one constructor call instead of manyadd_column()calls (~1.2s / +4.6GB → ~0.001s / ~0GB for ~29 columns at 1.4M rows).vstack-ed it onto the growingref_table, transiently holding old + new + concatenated data for every column at once — roughly doubling peak memory on every "add new stars" step. Now concatenates columns directly and drops old references immediately.fit_motion_modelsdid an O(N_stars) data-prep step regardless of how few stars actually needed refitting; now sliced down to just the selected rows first. A redundant double-copy incombine_lists/fit_motion_models's array prep (anarange-based fancy-index copy stacked on top of an already-copyingmasked_invalid/deepcopycall) was removed. A per-star fixed-params dict is now built lazily, only for stars whose motion model actually needs it — skipped for ~84% of stars (those already handled by the vectorized batch path, above) in one benchmark.Other correctness & behavior changes
fit_star_idxsno longer wipes unrelated stars: refitting only a subset of stars used to overwrite every other star's existing result withnan; now keeps existing values by default and only touches the requested subset.Parallax's minimum-epoch requirement wasn_pts_req=4; 3 (x, y) pairs are enough to solve its 5-parameter model, so lowered to 3 — a caller wanting to require 4+ can still do so via themotion_model_inputcolumn.inherit_n_detect: newMosaicSelfRef/MosaicToRefparameter (defaultTrue) so a star'sn_detectreflects the total number of raw detections it represents across nested alignment layers, not just 1 per input starlist.StarListwarning: every pickle round-trip (astropy passes columns positionally to__init__) incorrectly warned about missing required arguments.align()iterations) kept stale params (vx/vy) from its old model indefinitely; now reset tofill_value/infright after reclassification.parallax.parallax_in_direction'sTime(mjd + 2400000.5, format='jd', ...)simplified toTime(mjd, format='mjd', ...); a misleadingtqdmprogress bar over a handful-of-motion-model-types loop (never per-star) removed.Documentation
.readthedocs.yaml. Getting a working build also required fixing three real, pre-existing bugs unrelated to Read the Docs specifically — they'd have broken a freshpip install .for anyone on a current Python:pyproject.toml's[build-system]pinnedcython==0.29.14(leftover astropy-template boilerplate for a package with zero.pyx/C extensions), which imports the stdlibcgimodule removed in Python 3.13, breaking install outright; the declareddependencieswere missingscipy,matplotlib,tqdm,joblib, andpandas, all imported unconditionally at module level; andsetup.cfg'sgithub_projectwas still the template default (astropy/astropy), whichconf.pyuses unconditionally to build doc issue-links. Verified via two independent fresh-venv builds (editable and the exact non-editablepip install .[docs]Read the Docs runs) —sphinx-buildsucceeds, 92 warnings, all pre-existing docstring formatting nits unrelated to this change.wei-lingfeng/flystarrather than this repo directly: importingMovingUniverseLab/flystarinto Read the Docs needs repo-admin access (to add its webhook) not currently available on this account, and theflystarproject name is already taken there by an existing (currently stale) project for this same package — re-pointing that one at this branch, rather than creating a competing project, needs maintainer access to it instead. The fork tracks this branch 1:1 in the meantime, so content is identical either way.Validation
Each change was validated individually — synthetic fuzz tests against reference implementations (thousands to tens of thousands of cases per change), byte-for-bit output comparison on real multi-thousand and multi-million-star datasets, and dedicated new unit tests where behavior changed. Full test suite passes except two pre-existing, environment/data-dependent failures unrelated to any change here:
test_masked_cols(missing test fixture file) andtest_generic_match(test data contains no finite values). (Thetest_MosaicSelfRef_velfailure present at one point inmm_rework— see the magnitude-weighting fix above — is resolved on this branch.)Worked example:
scipy.curve_fitvs. analytic error propagation (referenced above)Output: