Skip to content

refactor: samples adapter and Checkpointer/resume split (search-extensibility A3, stacked on A2) - #1680

Merged
Jammy2211 merged 9 commits into
mainfrom
feature/search-ext-a3-samples-checkpointer
Oct 8, 2026
Merged

Jammy2211 merged 9 commits into
mainfrom
feature/search-ext-a3-samples-checkpointer

Conversation

@Jammy2211

Copy link
Copy Markdown
Collaborator

Summary

Phase A3 of the search-extensibility epic (PyAutoFit#1677), stacked on #1679 (A2): this PR's base is feature/search-ext-a2-objective-bridge; merge #1679 first and GitHub retargets this one to main. Six A3 commits plus a merge of A2 and one reconciliation commit.

Phase A3 builds on A2's run(ctx) bridge. It replaces A2's provisional samples types with the single samples adapter (autofit/non_linear/samples/adapter.py: RawSamples, samples_from_raw, one log-prior helper, sentinel recognition without rewriting, ChainPosterior.thin shared by Emcee, Zeus and NUTS with the empty-chain fallback). Every search implements one raw_samples_from; samples_via_internal_from is written once on NonLinearSearch. Byte-identical golden samples.csv fixtures (generated on main, their own first commit) cover Emcee, Dynesty, Nautilus, NUTS and SMC. A search's archive (checkpointer: DillCheckpointer, PickleCheckpointer for NUTS/SMC, NativeFileCheckpointer for emcee HDF / dynesty savestate / nautilus hdf5 / NSS pkl; atomic writes, retain_after_completion) is separated from its resume_state, with corruption and retention precedence in docs/design/checkpointing.md (D5); a completed Dynesty folder keeps what it needs to reload. Constructing Emcee, BlackJAXNUTS or SMC no longer mutates conf.instance. Pre-A3 output folders load through format-aware legacy detection (the HDF probe moved, not replaced); loading is injected for directory, database (no-op), null, zipped and summary-only paths. FitContext.checkpointer and FitContext.resume are now wired from these strategies and docs/design/run_ctx.md Revision 1 says so. --auto run at effective level safe; tier judge → human /prm.

Strict xfails: 9 → 4. Deleted (now passing): the three CONFIG_MUTATION_XFAIL (Emcee, BlackJAXNUTS, SMC) and the two Dynesty DISK_INTERNAL_XFAIL. The four import-time AutoCorrelationsSettings xfails are A4's and stay strict. Golden identifiers unchanged.

API Changes

Added autofit.non_linear.samples.adapter (RawSamples with optional log_prior/samples_kwargs/label, samples_from_raw, log_prior_list_from, is_invalid/invalid_count, INVALID_SENTINELS, ChainPosterior), autofit.non_linear.checkpoint (DillCheckpointer(strip_attributes=), PickleCheckpointer, NativeFileCheckpointer(filename, loader) with save/load/exists/finalize/discard, retain_after_completion, path; load_legacy_search_internal, checkpointer_kind), NonLinearSearch.{checkpointer,resume_state,samples_cls,raw_samples_from,load_search_internal,retains_search_internal}, the checkpointer manifest capability (dill | pickle | native:<file>; schema-compatible addition; generated docs gain an "Archive" column), paths.stores_search_internal (False for NullPaths/DatabasePaths). Changed: concrete searches implement raw_samples_from instead of samples_via_internal_from (direct overrides still work); Emcee/NUTS/SMC keep their archive regardless of config; a completed Dynesty folder loads from dill, savestate only for an unfinished run; NUTS/SMC write no pickle on DatabasePaths; Emcee gains the empty-chain fallback and Zeus applies it in test mode; Zeus computes autocorrelations once per conversion; Nautilus's resume loader raises NotImplementedError (nautilus reopens its own file); base info_from returns {}. resumable values unchanged (Zeus stays False; resume not proven).
See full details below.

Test Plan

  • Full pytest test_autofit -x on the integrated head: 3531 passed, 1 skipped, 4 xfailed
  • Goldens byte-identical after every commit; test_adapter, test_checkpoint ×2, test_search_internal_loading, test_fit_context, plus A2's probe/timing/make_fitness/BFGS tests: 117 passed
  • nojax emulation test_autofit/non_linear: 1253 passed, 136 skipped, 2 xfailed; import autofit loads no optional backend
  • Fresh process: af.Emcee(), af.BlackJAXNUTS(), af.SMC() leave conf.instance["output"]["search_internal"] unchanged (re-run by the reviewer)
  • afT {Emcee,Zeus,DynestyStatic,Nautilus,Nautilus_jax,Drawer,SMC}.py under test mode exit 0 (BlackJAXNUTS and MultiStartResurrect fail their own asserts identically on main)
  • Nautilus kill-and-resume: SIGINT during exploration → rerun logs "Resuming Nautilus" and completes (SIGINT after the first full update is swallowed on main too)
  • A pre-A3 folder written by main (Emcee HDF, Dynesty dill, NUTS pickle, Drawer) lists in the aggregator and reloads
  • CI green on unittest 3.12 / 3.13 / nojax, docs, generated-search-docs
Full API Changes (for automation & release notes)

Removed

  • none (A2's provisional RawSamples/samples_from_raw in search/fit_context.py are re-exported from the adapter)

Added

  • see the API Changes section above (adapter, checkpoint module, NonLinearSearch hooks, checkpointer capability, paths.stores_search_internal)
  • docs/design/checkpointing.md

Migration

  • Before: a search overrides samples_via_internal_from(model, search_internal) → After: implement raw_samples_from(model, internal) -> RawSamples; the base converts once
  • Before: conf.instance["output"]["search_internal"] = True set by constructing Emcee/NUTS/SMC → After: never; those searches retain their archive via checkpointer.retain_after_completion
  • Before: Dynesty reloads from savestate.save → After: from the dill archive once complete; savestate only resumes an unfinished run

Validation checklist (--auto run — plan was not pre-approved)

Generated by the PyAutoLabs agent workflow.

🤖 Generated with Claude Code

Jammy2211 and others added 8 commits October 8, 2026 15:17
…and SMC

Stored backend internals from small real fits, with the samples.csv and
samples_info.json that samples_via_internal_from produces from them on main
(0dbf258). The A3 samples adapter must reproduce samples.csv byte for byte.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…once

Add autofit/non_linear/samples/adapter.py: RawSamples(parameters,
log_likelihood | log_posterior, weights, info) and samples_from_raw, the one
place for the log prior (log_prior_list_from), LL from a log posterior, the
#1628 length checks, recognition of the -1e99/-1e30/-inf sentinels and time /
class_path injection. Representations and weights are passed through as the
backend stores them; nothing is renormalised (D13).

NonLinearSearch.samples_via_internal_from is now implemented once on top of a
per-search raw_samples_from, with samples_cls declared on the family bases,
SMC and NSS. Every search (Emcee, Zeus, BlackJAXNUTS, SMC, Dynesty, Nautilus,
NSS, BFGS/LBFGS, Drawer, MultiStart) is ported; the golden samples.csv fixtures
stay byte-identical.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
One burn-in/thinning implementation for the chain searches: ChainPosterior
wraps emcee/zeus get_chain/get_log_prob (from_sampler) or stored chains
(from_arrays, emcee's slicing, draw- or chain-major), requests the log values
with the same discard/thin as the draws (#1628) and falls back to the whole
chain when burn-in removal leaves nothing. Emcee gains Zeus's empty-chain
fallback. Adds adapter and ChainPosterior unit tests; goldens unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
autofit/non_linear/checkpoint.py adds the three archive strategies chosen per
search by the checkpointer class attribute (the A1 capability slot):
DillCheckpointer (default; Nautilus strips its pools), PickleCheckpointer
(BlackJAXNUTS, SMC) and NativeFileCheckpointer(filename, loader) (emcee HDF,
dynesty savestate, nautilus hdf5, NSS pkl), each with save/load/exists/
finalize/discard and retain_after_completion. Writes are atomic; NullPaths and
DatabasePaths store nothing (paths.stores_search_internal).

resume_state is separate from the archive: Dynesty (savestate.save), Nautilus
(checkpoint.hdf5), NSS (nss_checkpoint.pkl), MultiStart and Emcee (their archive
file). NUTS and SMC have none and stay resumable=False.
NonLinearSearch.output_search_internal finalizes the archive and discards a
completed run's resume state; post_fit_output keeps search_internal/ when the
archive retains itself or output.search_internal is on.

Deletes the NUTS/SMC duplicate pickle code, the Emcee no-op and the Dynesty /
Nautilus output_search_internal overrides. A completed Dynesty folder now loads
its samples from the archive (search_internal.dill), falling back to the
savestate only for an unfinished run, so the two A0a(ii) Dynesty strict xfails
are deleted and pass. The checkpointer joins the capability manifest and the
generated search docs (Archive column).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…nstance

Delete the three conf.instance["output"]["search_internal"] = True
mutations (emcee/search.py, nuts/search.py, smc/search.py). They existed so a
completed fit kept the archive its samples are rebuilt from, and they leaked to
every later search in the process. The archives now declare
retain_after_completion instead (previous commit), so the three
CONFIG_MUTATION_XFAIL strict xfails are deleted and pass. Adds an end-to-end
check that, with output.search_internal off, a completed Emcee fit keeps its
HDF archive while a following Drawer fit drops its dill.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…n; D5 design note

DirectoryPaths.load_search_internal no longer knows any backend format: it
loads through the owning search's checkpointer (paths.search), so
Result.search_internal and samples_via_internal_from read emcee's HDF backend,
the NUTS/SMC pickle or the dill through one path, and the NUTS/SMC
load_search_internal overrides go. Format-aware legacy detection is kept, not
replaced by a dill-only fallback: the inline emcee probe (directory.py
238-246) moved to checkpoint.load_legacy_search_internal (hdf, then pickle,
then dill), used for a folder opened without its search and when a
checkpointer's own file is absent, so pre-A3 outputs still load.

Tests cover directory (injected and legacy), emcee HDF, zipped, database,
null and summary-only outputs. docs/design/checkpointing.md records the
archive/resume split, atomic writes, corruption handling and retention
precedence (D5).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…kpointer branch

Stacks phase A3 on phase A2 so the chain merges A2 -> A3 -> A3b.

Conflicts resolved:
- abstract_search.py: keep A2's start_points, _pools and bridge; keep A3's
  samples_via_internal_from (implemented once over the adapter),
  raw_samples_from and load_search_internal; one samples_cls and one base
  raw_samples_from (A2's duplicates dropped); A2's info_from hook kept.
- drawer/nautilus search.py: stay on run(ctx); one raw_samples_from each,
  A3's (adapter RawSamples with label), with info routed through A2's
  info_from hook.
- emcee/nautilus/smc search.py imports: A3's checkpoint imports plus A2's
  removal of the Fitness import and SMC's jax_objective import.
- fit_context.py: A2's provisional RawSamples/samples_from_raw deleted; the
  bridge re-exports A3's samples/adapter.py.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…tegies

ctx.checkpointer is the search's archive strategy (NonLinearSearch.checkpointer)
and ctx.resume its resume_state when the interrupted run's file exists, else
None; the Checkpointer handle rather than the loaded state, since a backend may
reopen its own native file (Nautilus). The base info_from returns {} (the
frozen contract's dict), which the adapter's RawSamples.info expects.
run_ctx.md Revision 1 now names the wired members and the adapter.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@Jammy2211 Jammy2211 added the pending-release PR queued for the next release build label Oct 8, 2026
The golden samples.csv comparison was byte for byte, but NumPy dispatches its
vectorised exp/log loops per CPU (AVX2 vs AVX-512), so the same NumPy on a
GitHub runner reproduced the dynesty and nautilus weights (and two nautilus
log priors) 1 ULP away from the AVX2-generated fixtures (max relative
difference 2.2e-16). Compare the header and row count exactly and every value
with rtol=1e-12, atol=0 instead: still far below any renormalisation,
reordering or dropped row (decision D13).

samples_info is compared exactly except for the ESS / bulk / tail ESS / R-hat
diagnostics that blackjax.diagnostics computes (blackjax 1.7 changed the
rank-normalised estimators); for those only the shape is pinned.

Add docs/design/checkpointing.md to the Searches toctree (the new
toc.not_included Sphinx warning that pushed the count to 31 > 30).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant