Skip to content

feat: add n_effective to DynestyDynamic; fix truncated runs under a finite iterations_per_full_update - #1665

Open
samlange04 wants to merge 4 commits into
PyAutoLabs:mainfrom
samlange04:feature/dynesty-dynamic-n-effective
Open

samlange04 wants to merge 4 commits into
PyAutoLabs:mainfrom
samlange04:feature/dynesty-dynamic-n-effective

Conversation

@samlange04

Copy link
Copy Markdown
Contributor

Summary

Discussion: https://github.com/orgs/PyAutoLabs/discussions/33

Two changes to DynestyDynamic, both motivated by making a dynamic run cheaper while still writing output on the fly.

  1. n_effective argument. dynesty's dynamic sampler keeps adding batches until the estimated effective sample size reaches n_effective (dynesty 2.x default max(10000, ndim ** 2)). PyAutoFit had no way to set it. DynestyDynamic.__init__ now takes n_effective: Optional[int] = None, forwarded to run_nested only when not None, so default behaviour is unchanged. Not an identifier field. No yaml default, following refactor: replace search YAML config with explicit Python defaults #1202. DynestyStatic is left alone because dynesty deprecates n_effective on the static run_nested.

  2. Fix: a finite iterations_per_full_update truncated a dynamic run. run_search_internal assumed dynesty's per-run call counter resets on every run_nested call. That is true for the static sampler but the dynamic sampler compares maxcall with its cumulative self.ncall, so the second chunk got a budget it had already spent, returned without sampling, and the "no new calls" criterion returned a cut-short baseline (ESS about 1, no batches). A baseline run cut by maxcall also cannot be continued (the next call reset()s it; resume=True refuses after RUN_DONE), and results.ncall undercounts the sampler's own counter (it omits batch live-point initialisation).

    The fix adds per-sampler hooks on AbstractDynesty (total_calls_from, maxcall_from, chunk_kwargs, chunk_is_finished) whose defaults are the existing static behaviour. DynestyDynamic overrides them: the first chunk of a chunked run completes the whole baseline (maxbatch=0, no budget), the batch phase is then chunked by cumulative budget, and a batch chunk is finished only if it stopped strictly inside it. Intermediate output for dynamic runs starts after the baseline. The run_search_internal docstring records the two samplers' counting conventions.

API Changes

  • DynestyDynamic(n_effective=None): new optional keyword, default None (dynesty's own default applies).
  • New overridable methods on AbstractDynesty: total_calls_from, maxcall_from, chunk_kwargs, chunk_is_finished. Defaults reproduce the previous static behaviour. DynestyDynamic.chunked property.
  • Behavioural: DynestyDynamic with a finite iterations_per_full_update now returns a complete run; its first on-the-fly update happens after the baseline rather than mid-baseline.

Test Plan

  • New tests in test_dynesty.py: n_effective is passed to run_nested only when set; the dynamic hooks give the baseline chunk an unbounded budget and maxbatch=0, cumulative budgets for batch chunks, and the strict-inside-budget finished criterion; the static defaults are unchanged.
  • End to end, 3-parameter Gaussian, iterations_per_full_update=1500: before, ESS about 1 and no batches; after, baseline + 3 batch chunks + 1 no-op, ESS 3587 against 3944 for the same run in a single chunk, same logZ and parameters. n_effective=2000 stops batching earlier with the ESS floored at what the baseline gives. DynestyStatic unaffected (4 chunks of about 1500 calls, same result).
  • test_autofit/non_linear/search/nest/: 44 passed, 13 skipped on macOS / Python 3.13 / dynesty 2.1.5, on the branch rebased onto main at 710f4b3.

🤖 Generated with Claude Code

samlange04 and others added 4 commits October 8, 2026 21:10
…le size

Expose dynesty's `n_effective` argument on `DynestyDynamic`. It is the
minimum effective posterior sample size the dynamic sampler keeps adding
batches until it reaches. When left as `None` (the default) the kwarg is
not passed, so dynesty's own default (`max(10000, ndim**2)` in 2.x)
applies and existing behaviour is unchanged.

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

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

`run_search_internal` handed every `run_nested` chunk `maxcall=iterations_per_full_update`.
That is right for the static sampler, whose call counter resets per call, but dynesty's
dynamic sampler compares `maxcall` with its cumulative `self.ncall`. The second chunk's
budget was therefore already spent, dynesty returned without sampling, the no-new-calls
criterion fired and a baseline run cut short mid-way was returned as the converged
result (ESS of 1 in the reproducer), with `n_effective` never reached. Hidden by the
packaged default of 1e99, which gives a single chunk.

A cumulative budget alone is not enough: dynesty cannot continue a baseline run that
`maxcall` cut short (the next call `reset()`s it, and `resume=True` refuses after
`RUN_DONE`). It can, however, add batches across `run_nested` calls. A chunked
`DynestyDynamic` run now:

- runs the whole baseline in the first chunk (`maxbatch=0`, `maxcall` only if set);
- chunks the batch phase by cumulative budget `total_calls + iterations_per_full_update`;
- counts calls with the sampler's own `ncall` (what dynesty compares against; `results.ncall`
  omits batch live-point initialisation and undercounts);
- is finished only when a batch chunk stopped strictly inside its budget, or added no calls.

These are three per-sampler hooks on `AbstractDynesty` (`maxcall_from`, `chunk_kwargs`,
`chunk_is_finished`, plus `total_calls_from`) with the static behaviour as the default, so
`DynestyStatic` is unchanged. The `run_search_internal` docstring, which stated the per-call
reset for both samplers, is corrected.

Verified with a 3-parameter Gaussian fit, `n_effective=3000`: 1e99 gives one chunk, ESS 3944;
1500 now gives baseline + 3 batch chunks + a no-op confirming chunk, ESS 3587, same logZ and
parameters, where before it returned 232 samples with ESS 1.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The constructor-argument golden table (PyAutoLabs#1666) was frozen before this branch
added n_effective to DynestyDynamic; register it so
test_constructor_argument_set passes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@samlange04
samlange04 force-pushed the feature/dynesty-dynamic-n-effective branch from d6612c8 to e6e7e5e Compare October 8, 2026 13:13
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