Skip to content

Make Sampler completely Fitter-agnostic - #310

Merged
rozyczko merged 3 commits into
sampler-engine-structure-280from
280-more-refactoring
Sep 22, 2026
Merged

rozyczko merged 3 commits into
sampler-engine-structure-280from
280-more-refactoring

Conversation

@rozyczko

Copy link
Copy Markdown
Member

Addressing @damskii9992's issue in #287 (comment)

Made Sampler fully Fitter-independent.

This is a separate PR on top of #287 to allow for easier review.

@rozyczko rozyczko added [scope] maintenance Code/tooling cleanup, no feature or bugfix (major.minor.PATCH) [priority] high Should be prioritized soon [area] base classes Changes to or creation of new base classes labels Sep 17, 2026
@codecov

codecov Bot commented Sep 17, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.18182% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 83.82%. Comparing base (6a7fa25) to head (1214080).

Files with missing lines Patch % Lines
src/easyscience/fitting/reshaping.py 96.61% 2 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@                       Coverage Diff                        @@
##           sampler-engine-structure-280     #310      +/-   ##
================================================================
+ Coverage                         83.64%   83.82%   +0.17%     
================================================================
  Files                                68       69       +1     
  Lines                              5253     5272      +19     
================================================================
+ Hits                               4394     4419      +25     
+ Misses                              859      853       -6     
Flag Coverage Δ
unittests 83.82% <98.18%> (+0.17%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
src/easyscience/fitting/fitter.py 96.03% <100.00%> (+4.70%) ⬆️
src/easyscience/fitting/multi_fitter.py 100.00% <100.00%> (+1.26%) ⬆️
src/easyscience/fitting/sampler.py 100.00% <100.00%> (ø)
src/easyscience/fitting/reshaping.py 96.61% <96.61%> (ø)

@damskii9992 damskii9992 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One potentially important comment and some minor comments. Otherwise it looks good to me :)

"where $\\theta$ are the model parameters, $d$ is the observed data, $p(d \\mid \\theta)$ is the likelihood, and $p(\\theta)$ is the prior. In `easyscience`, the `min`/`max` bounds of a `Parameter` are interpreted as a **uniform prior**, and a Gaussian likelihood is constructed from the data and supplied weights.\n",
"\n",
"`easyscience` exposes a Bayesian Markov-chain Monte Carlo (MCMC) sampler through the `Sampler` class. Under the hood this uses BUMPS' DREAM sampler, so the underlying minimizer must be switched to BUMPS.\n",
"`easyscience` exposes a Bayesian Markov-chain Monte Carlo (MCMC) sampler through the `Sampler` class. It is a parallel entry point to `Fitter`: both take a model object and a model function, so you can sample without ever creating a `Fitter`. Under the hood `Sampler` uses BUMPS' DREAM sampler, so the `bumps` package must be installed.\n",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is a parallel entry point to Fitter: both take a model object and a model function, so you can sample without ever creating a Fitter.

Would remove this comment.

Comment on lines +224 to +225
"does not need a `Fitter` at all, and you can sample straight from the initial parameter\n",
"values.\n",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would also just remove this comment :)

"We now draw samples from the posterior distribution $p(\\theta \\mid d)$ using the BUMPS DREAM (DiffeRential Evolution Adaptive Metropolis) algorithm. DREAM is an ensemble MCMC method that runs multiple chains in parallel and automatically tunes the proposal distribution.\n",
"\n",
"DREAM only works with the BUMPS minimizer. We reuse the ``mle_fitter`` created above — any configured `Fitter` would do, and it does not have to have been fitted — switch it to BUMPS, and create a `Sampler` instance bound to the fitter and data. Calling `sampler.sample()` returns a `SamplingResults` object with the following attributes:\n",
"Create a `Sampler` from the same `parameter_container` and `intensity_model` we gave the `Fitter`, bound to the data. No `Fitter` is involved: the sampler only needs the model object, the model function, the data and the `bumps` package. Calling `sampler.sample()` returns a `SamplingResults` object with the following attributes:\n",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No Fitter is involved: the sampler only needs the model object, the model function, the data and the bumps package.

Would remove this line :)

Comment on lines +199 to +202
wrapped_fns = [
inject_x(this_fun, this_x, flatten=flatten)
for this_x, this_fun in zip(real_x, fit_functions)
]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, this is actually cleaner when factored out. No more setting the ._fit_function in the MultiFitter for each function :)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

explained below

self._fit_object = fit_object
self._update_minimizer(self._enum_current_minimizer)

def _fit_function_wrapper(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't we just forego having this method now? It is just an alias . . .

# Make a 'dummy' x array for the fit function
x_for_fit = np.array(range(y_new.size))
return x_for_fit, x_new, y_new, weights, x_shape
_precompute_reshaping = staticmethod(reshape_dataset)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And same with this one, can't we just get rid of it now? :)
And just call reshape_dataset where it is needed.

w_new = np.hstack(w_new)
x_fit = np.linspace(0, y_new.size - 1, y_new.size)
return x_fit, x_new, y_new, w_new, dims
_precompute_reshaping = staticmethod(reshape_datasets)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And the same here, can't we just use the functions directly? Is there any reasons to have the aliases?
Except for avoiding to change the few lines where they're used?

Comment thread src/easyscience/fitting/sampler.py Outdated
Comment thread src/easyscience/fitting/sampler.py
@rozyczko

Copy link
Copy Markdown
Member Author

One potentially important comment and some minor comments. Otherwise it looks good to me :)

_fit_function_wrapper methods are NOT plain aliases.
They are the template-method hooks that make the shared fit body work for both classes:

Fitter.fit calls self._precompute_reshaping and self._fit_function_wrapper.

MultiFitter overrides both. Multifitter binds reshape_datasets (plural) instead of reshape_dataset, and its wrapper calls inject_x_multi with self._fit_functions and self._dependent_dims. This isn't done in the Fitter.

_post_compute_reshaping does the same, btw.

Removing these means that either Fitter.fit gains an "am I multi?" branch, which sends subclass knowledge into the base class, or MultiFitter re-implements the whole PRE/FIT/POST body of fit.
I think that both are worse than the current one-liners.

No downstream package (reflectometry, dynamics) overrides them, so there is no external constraint either way. I would suggest keeping as-is and maybe add a comment on each saying it is the override point for MultiFitter?

@rozyczko
rozyczko merged commit 22513ad into sampler-engine-structure-280 Sep 22, 2026
28 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[area] base classes Changes to or creation of new base classes [priority] high Should be prioritized soon [scope] maintenance Code/tooling cleanup, no feature or bugfix (major.minor.PATCH)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants