Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
a8946ce
Add Bayesian posterior sampling to Analysis1d
henrikjacobsenfys Aug 13, 2026
7db52f9
Add Bayesian posterior sampling to Analysis and ParameterAnalysis
henrikjacobsenfys Aug 13, 2026
7ad2900
Label the posterior plot axes with units and quantities
henrikjacobsenfys Aug 13, 2026
0c3c689
Qualify parameter labels by model name, and cover the remaining branches
henrikjacobsenfys Aug 13, 2026
46d745a
Warm the tutorial data cache before running notebooks in parallel
henrikjacobsenfys Aug 13, 2026
ff50a2c
Rebuild the fitter when a binding changes shape, and stabilise the in…
henrikjacobsenfys Aug 13, 2026
b28b9e4
Address the review findings on the sampling API
henrikjacobsenfys Aug 13, 2026
439135a
Cover the review fixes, and drop a redundant guard
henrikjacobsenfys Aug 13, 2026
e6bd67c
Gather the per-Q chains on Analysis after independent sampling
henrikjacobsenfys Aug 13, 2026
6170a43
Step through the per-Q corner plots with a slider
henrikjacobsenfys Aug 13, 2026
1292f44
Show the per-Q corner slider in the Bayesian tutorial
henrikjacobsenfys Aug 13, 2026
442a5bd
Put the corner slider under the figure
henrikjacobsenfys Aug 13, 2026
619c46a
Reach the whole library through one namespace
henrikjacobsenfys Aug 13, 2026
bb10623
Compose the posterior sampler instead of mixing it in
henrikjacobsenfys Aug 14, 2026
ca72513
Compose the sampler into Analysis and ParameterAnalysis
henrikjacobsenfys Aug 14, 2026
11a5e14
Merge branch 'bayesian-analysis' into import-style
henrikjacobsenfys Aug 14, 2026
8297346
Export the multi-Q sampler and drop the mixin's name
henrikjacobsenfys Aug 14, 2026
4441138
Merge branch 'bayesian-analysis' into import-style
henrikjacobsenfys Aug 14, 2026
29f27ff
Point the front door at the composed sampler
henrikjacobsenfys Aug 14, 2026
c8cf9e0
Unwrap the security-issue line again
henrikjacobsenfys Aug 14, 2026
85f8a71
Warm the tutorial data cache before running notebooks in parallel
henrikjacobsenfys Aug 13, 2026
7b11a44
Merge branch 'bayesian' into bayesian-analysis
henrikjacobsenfys Aug 14, 2026
f1218ec
Merge branch 'bayesian-analysis' into import-style
henrikjacobsenfys Aug 14, 2026
7e53eb7
Mark setup, action and expectation apart in the new tests
henrikjacobsenfys Aug 16, 2026
cecb994
Merge branch 'bayesian' into bayesian-analysis
henrikjacobsenfys Aug 16, 2026
2ecc29f
Mark setup, action and expectation apart in the multi-Q tests
henrikjacobsenfys Aug 16, 2026
d711bf2
Merge branch 'bayesian-analysis' into import-style
henrikjacobsenfys Aug 16, 2026
522a6e9
Mark setup, action and expectation apart in the namespace tests
henrikjacobsenfys Aug 16, 2026
fbfedd6
Give the sampler its own test file
henrikjacobsenfys Aug 16, 2026
1814139
Merge branch 'bayesian' into bayesian-analysis
henrikjacobsenfys Aug 16, 2026
c6d3e1b
Put each test in the file of the class it exercises
henrikjacobsenfys Aug 16, 2026
017e4c0
Merge branch 'bayesian-analysis' into import-style
henrikjacobsenfys Aug 16, 2026
f6756c7
Refuse silent chain corruption and harden the posterior sampler
henrikjacobsenfys Aug 17, 2026
46b45b4
Merge branch 'bayesian' into bayesian-analysis
henrikjacobsenfys Aug 17, 2026
d3d4679
Version collections so stale plans and caches rebuild
henrikjacobsenfys Aug 17, 2026
03c60d4
Repair detailed balance, diffusion collections and bound handling
henrikjacobsenfys Aug 17, 2026
fbc2078
Track model versions per consumer and wire missing invalidation
henrikjacobsenfys Aug 17, 2026
8ca876d
Declare runtime dependencies and align packaging, docs and tooling
henrikjacobsenfys Aug 17, 2026
c8be2ae
Evaluate expressions in a coherent scale and express them in y_unit
henrikjacobsenfys Aug 17, 2026
2b33a04
Cover counts-bearing units in the expression conversion tests
henrikjacobsenfys Aug 17, 2026
2839da4
Keep the multi-Q sampler pointed at the chain the user actually ran
henrikjacobsenfys Aug 17, 2026
edf2904
Merge branch 'bayesian-analysis' into import-style
henrikjacobsenfys Aug 17, 2026
9172d10
Merge branch 'import-style' into review-fixes
henrikjacobsenfys Aug 17, 2026
6ccc0ba
Add marginal posteriors, correlation heatmaps and sampling progress
henrikjacobsenfys Aug 17, 2026
26a0836
Merge branch 'bayesian' into bayesian-analysis
henrikjacobsenfys Aug 17, 2026
882ad87
Align test files with the layout conventions and hoist imports
henrikjacobsenfys Aug 17, 2026
8585771
Justify the exact no-op comparisons in the coefficient rescale
henrikjacobsenfys Aug 17, 2026
e9093a0
Move the remaining stray tests into their classes
henrikjacobsenfys Aug 17, 2026
74fa676
Give every posterior plot a Q slider over independent chains
henrikjacobsenfys Aug 17, 2026
866e1b2
Merge branch 'bayesian' into bayesian-analysis
henrikjacobsenfys Aug 17, 2026
26c168b
Write the progress line through sys.stdout
henrikjacobsenfys Aug 17, 2026
fa94bb2
Show the new posterior plots in the Bayesian tutorial
henrikjacobsenfys Aug 17, 2026
7a7dfe3
Apply the formatting fixes
henrikjacobsenfys Aug 17, 2026
132fc59
Satisfy the docstring and formatting checks
henrikjacobsenfys Aug 17, 2026
75a0e45
Merge branch 'bayesian' into bayesian-analysis
henrikjacobsenfys Aug 17, 2026
fbe8dd4
Document propagated exceptions the way the docstring linter expects
henrikjacobsenfys Aug 17, 2026
5074a8f
Merge branch 'bayesian-analysis' into import-style
henrikjacobsenfys Aug 17, 2026
b06d984
Merge branch 'import-style' into review-fixes
henrikjacobsenfys Aug 17, 2026
9cff699
Satisfy the linters across the merged fix branch
henrikjacobsenfys Aug 17, 2026
22fbde5
Keep prettier current and contained to the repository
henrikjacobsenfys Aug 17, 2026
2b218d5
Merge remote-tracking branch 'origin/develop' into bayesian-analysis
henrikjacobsenfys Aug 17, 2026
6310e72
Give the Bayesian tutorial the widget backend its sliders need
henrikjacobsenfys Aug 17, 2026
2de049f
Merge branch 'bayesian-analysis' into import-style
henrikjacobsenfys Aug 17, 2026
2f8c7fc
Merge branch 'import-style' into review-fixes
henrikjacobsenfys Aug 17, 2026
28f8ce8
Merge remote-tracking branch 'origin/develop' into review-fixes
henrikjacobsenfys Aug 17, 2026
1f43013
Drop the duplicate line and unused import the tie-merge reintroduced
henrikjacobsenfys Aug 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ node_modules
# Tox
.tox

# Git worktrees checked out inside the repository
.worktrees/

# Misc
.benchmarks
.cache
Expand Down
2 changes: 1 addition & 1 deletion docs/docs/tutorials/delta_lorentz.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@
"id": "0aee03b1",
"metadata": {},
"source": [
"Both `A_0` and `lorentzian_width` are here allowed to vary with Q. We here change a few of them just to show how this impacts the model. The `# noqa` comment is because we are accessing private members of the model (ones beginning with `_`), which is generally discouraged. Because of these changes, in the figure below, the delta function at Q=1.25 Å^-1 is much larger than the other ones, and the Lorentzian at Q=1.75 Å^-1 is much narrower and taller than the other ones."
"Both `A_0` and `lorentzian_width` are here allowed to vary with Q. We here change a few of them just to show how this impacts the model. The `# ruff: ignore[private-member-access]` comment is because we are accessing private members of the model (ones beginning with `_`), which is generally discouraged. Because of these changes, in the figure below, the delta function at Q=1.25 Å^-1 is much larger than the other ones, and the Lorentzian at Q=1.75 Å^-1 is much narrower and taller than the other ones."
]
},
{
Expand Down
2 changes: 1 addition & 1 deletion docs/mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ site_url: https://easyscience.github.io/dynamics-lib

# Repository
repo_url: https://github.com/easyscience/dynamics-lib
edit_uri: edit/develop/docs/
edit_uri: edit/develop/docs/docs/

# Copyright
copyright: © 2025-2026 EasyDynamics
Expand Down
4 changes: 4 additions & 0 deletions pixi.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 15 additions & 5 deletions pixi.toml
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,10 @@ docstring-lint-check = 'pydoclint --quiet src/'
notebook-lint-check = 'nbqa ruff docs/docs/tutorials/'
py-lint-check = 'ruff check src/ tests/ docs/docs/tutorials/'
py-format-check = 'ruff format --check src/ tests/ docs/docs/tutorials/'
nonpy-format-check = 'npx prettier --list-different --config=prettierrc.toml --ignore-unknown .'
# Refreshes prettier first, so the local version can never drift behind the one CI installs.
nonpy-format-check = { cmd = 'npx prettier --list-different --config=prettierrc.toml --ignore-unknown .', depends-on = [
'prettier-install',
] }
nonpy-format-check-modified = 'python tools/nonpy_prettier_modified.py'

check = 'pre-commit run --hook-stage manual --all-files'
Expand All @@ -134,7 +137,9 @@ notebook-lint-fix = 'nbqa ruff --fix docs/docs/tutorials/'
py-lint-fix = 'ruff check --fix src/ tests/ docs/docs/tutorials/'
py-lint-fix-unsafe = 'ruff check --fix --unsafe-fixes src/ tests/ docs/docs/tutorials/'
py-format-fix = 'ruff format src/ tests/ docs/docs/tutorials/'
nonpy-format-fix = 'npx prettier --write --list-different --config=prettierrc.toml --ignore-unknown .'
nonpy-format-fix = { cmd = 'npx prettier --write --list-different --config=prettierrc.toml --ignore-unknown .', depends-on = [
'prettier-install',
] }
nonpy-format-fix-modified = 'python tools/nonpy_prettier_modified.py --write'
success-message = 'echo "✅ All auto-formatting steps completed successfully!"'

Expand Down Expand Up @@ -178,9 +183,11 @@ cov = { depends-on = [
########################

notebook-convert = 'jupytext docs/docs/tutorials/*.py --from py:percent --to ipynb'
notebook-strip = 'nbstripout docs/docs/tutorials/*.ipynb'
notebook-strip = 'nbstripout docs/docs/tutorials/**/*.ipynb'
notebook-tweak = 'python tools/tweak_notebooks.py docs/docs/tutorials/'
notebook-exec = 'python -m pytest --nbmake docs/docs/tutorials/ --nbmake-timeout=1200 --overwrite --color=yes -n auto -v'
notebook-exec = { cmd = 'python -m pytest --nbmake docs/docs/tutorials/ --nbmake-timeout=1200 --overwrite --color=yes -n auto -v', depends-on = [
'prefetch-tutorial-data',
] }

notebook-prepare = { depends-on = [
#'notebook-convert',
Expand Down Expand Up @@ -270,7 +277,10 @@ default-build = 'python -m build'
dist-build = 'python -m build --wheel --outdir dist'

npm-config = 'npm config set registry https://registry.npmjs.org/'
prettier-install = 'npm install --no-save --no-audit --no-fund prettier prettier-plugin-toml'
# --prefix . keeps the install inside this repository: without it, npm walks up the directory
# tree and a stray package.json in a parent directory can silently pin an old prettier.
# @latest keeps local runs on the same version CI installs.
prettier-install = 'npm install --prefix . --no-save --no-audit --no-fund prettier@latest prettier-plugin-toml@latest'

clean-pycache = "find . -type d -name '__pycache__' -prune -exec rm -rf '{}' +"

Expand Down
9 changes: 8 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ classifiers = [
requires-python = '>=3.12'
dependencies = [
'easyscience>=2.5.1', # The base library of the EasyScience framework. 2.5.1 adds fitting.Sampler
'numpy', # Numerical arrays (used directly throughout the library)
'scipy', # Numerical routines (convolution, interpolation, special functions)
'scipp', # Labelled multi-dimensional arrays; backs Experiment data handling
'h5py', # HDF5 backend for scipp's HDF5 I/O (Experiment.load_hdf5)
'matplotlib', # Plotting (posterior trace, corner, and predictive plots)
'pooch', # Data downloader
'darkdetect', # Detecting dark mode (system-level)
Expand Down Expand Up @@ -167,7 +171,10 @@ fail_under = 0 # Minimum coverage percentage to pass

[tool.pytest.ini_options]
addopts = '--import-mode=importlib'
markers = ['fast: mark test as fast (should be run on every push)']
markers = [
'fast: mark test as fast (should be run on every push)',
'network: mark test as downloading data files (deselect with -m "not network" when offline)',
]
testpaths = ['tests']

########################
Expand Down
6 changes: 3 additions & 3 deletions src/easydynamics/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,16 @@
"""

from easydynamics.analysis import Analysis
from easydynamics.analysis import Analysis1d
from easydynamics.analysis import BoundsSuggestion
from easydynamics.analysis import BoundsSuggestions
from easydynamics.analysis import FitBinding
from easydynamics.analysis import MultiQPosteriorSampler
from easydynamics.analysis import ParameterAnalysis
from easydynamics.analysis import ParameterLabels
from easydynamics.analysis import ParameterPosterior
from easydynamics.analysis import PosteriorSampler
from easydynamics.analysis import PosteriorSummary
from easydynamics.analysis.analysis1d import Analysis1d
from easydynamics.analysis.fit_binding import FitBinding
from easydynamics.base_classes import EasyDynamicsBase
from easydynamics.base_classes import EasyDynamicsModelBase
from easydynamics.convolution import Convolution
Expand All @@ -42,11 +42,11 @@
from easydynamics.settings import ConvolutionSettings
from easydynamics.settings import DetailedBalanceSettings
from easydynamics.utils import detailed_balance_factor
from easydynamics.utils import hbar
from easydynamics.utils import plot_corner
from easydynamics.utils import plot_posterior_predictive
from easydynamics.utils import plot_trace
from easydynamics.utils import slicerplot_with_residuals
from easydynamics.utils.utils import hbar

__all__ = [
'Analysis',
Expand Down
4 changes: 4 additions & 0 deletions src/easydynamics/analysis/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
# SPDX-License-Identifier: BSD-3-Clause

from easydynamics.analysis.analysis import Analysis
from easydynamics.analysis.analysis1d import Analysis1d
from easydynamics.analysis.fit_binding import FitBinding
from easydynamics.analysis.parameter_analysis import ParameterAnalysis
from easydynamics.analysis.posterior import BoundsSuggestion
from easydynamics.analysis.posterior import BoundsSuggestions
Expand All @@ -13,8 +15,10 @@

__all__ = [
'Analysis',
'Analysis1d',
'BoundsSuggestion',
'BoundsSuggestions',
'FitBinding',
'MultiQPosteriorSampler',
'ParameterAnalysis',
'ParameterLabels',
Expand Down
74 changes: 59 additions & 15 deletions src/easydynamics/analysis/analysis.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
# SPDX-FileCopyrightText: 2026 EasyScience contributors <https://github.com/easyscience>
# SPDX-License-Identifier: BSD-3-Clause

import warnings
from copy import copy
from typing import Any

import numpy as np
import plopp as pp
import scipp as sc
from easyscience.fitting.minimizers.utils import FitResults
from easyscience.fitting.multi_fitter import MultiFitter
Expand Down Expand Up @@ -298,6 +300,10 @@ def rebin(
self.instrument_model.clear_Q(confirm=True)

self._analysis_list_is_dirty = True
self._owner_index = None
# The cached MultiFitter holds the old Analysis1d objects, and the Sampler binds its data
# at construction, so both are stale after a rebin.
self._invalidate_fitter()

def calculate(
self,
Expand Down Expand Up @@ -448,11 +454,6 @@ def plot_data_and_model(
self._verify_bool(add_background, 'add_background')
self._verify_bool(plot_residuals, 'plot_residuals')

if energy is None:
energy = self.energy

import plopp as pp

data_and_model = self.data_and_model_to_datagroup(
energy=energy,
add_background=add_background,
Expand All @@ -464,7 +465,8 @@ def plot_data_and_model(
plot_kwargs_defaults['keep'] = 'energy'
plot_kwargs_defaults.update(kwargs)

if plot_residuals:
# Residuals may have been omitted (with a warning) for a custom energy grid.
if plot_residuals and 'Residuals' in data_and_model:
fig = slicerplot_with_residuals(
data_and_model,
residuals_key='Residuals',
Expand Down Expand Up @@ -531,8 +533,20 @@ def data_and_model_to_datagroup(
self._verify_bool(include_components, 'include_components')
self._verify_bool(include_residuals, 'include_residuals')

custom_energy = energy is not None
energy = self._verify_energy(energy) if energy is not None else self.energy

if include_residuals and custom_energy:
# Residuals are data - model on the experiment grid; mixing them with a model on a
# custom grid would make the DataGroup internally inconsistent.
warnings.warn(
'Residuals are computed on the experiment energy grid and are omitted '
'when a custom energy grid is given.',
UserWarning,
stacklevel=2,
)
include_residuals = False

data_and_model = {
'Data': self.experiment.binned_data,
'Model': self._create_model_array(energy=energy),
Expand Down Expand Up @@ -683,8 +697,6 @@ def plot_parameters(

plot_kwargs_defaults.update(kwargs)

import plopp as pp

return pp.plot(
data_to_plot,
**plot_kwargs_defaults,
Expand Down Expand Up @@ -766,6 +778,18 @@ def _on_convolution_settings_changed(self) -> None:
self._owner_index = None
self._invalidate_fitter()

def _on_detailed_balance_settings_changed(self) -> None:
"""
Update the detailed balance settings when they change.

The per-Q analyses hold the settings object they were built with, so replacing it on this
Analysis requires rebuilding the list for the new object to reach every Q index.
"""
super()._on_detailed_balance_settings_changed()
self._analysis_list_is_dirty = True
self._owner_index = None
self._invalidate_fitter()

def _ensure_analysis_list_current(self) -> None:
"""Rebuild the analysis list if any dependency has changed since it was last built."""
if self._analysis_list_is_dirty and self.Q is not None:
Expand Down Expand Up @@ -953,17 +977,36 @@ def _fit_all_Q_simultaneously(self) -> FitResults:
energy=self.experiment.get_masked_energy(Q_index=analysis1d.Q_index, mask=mask_var)
)

mf = MultiFitter(
fit_objects=self.analysis_list,
fit_functions=self.get_fit_functions(),
)

return mf.fit(
# Use the configured fitter rather than a throwaway MultiFitter, so minimizer and
# tolerance settings applied through the ``fitter`` property take effect.
return self.fitter.fit(
x=xs,
y=ys,
weights=ws,
)

def get_all_variables(self) -> list[Parameter]:
"""
Get all variables used in the analysis, across every Q index.

Overrides the easyscience fallback, which scans every attribute of the object and would
therefore build the MultiFitter and the Sampler as side effects of merely listing variables
(and fail outright on an empty analysis).

Returns
-------
list[Parameter]
A list of all variables, including any extra parameters.
"""
variables = self.sample_model.get_all_variables()

variables.extend(self.instrument_model.get_all_variables())

if self._extra_parameters:
variables.extend(self._extra_parameters)

return variables

def get_fit_functions(self) -> list[callable]:
"""
Get fit functions for all Q indices, which can be used for simultaneous fitting.
Expand Down Expand Up @@ -1057,9 +1100,10 @@ def _create_components_dataset(
#############

def __repr__(self) -> str:
# The property ensures the list is current, so n_analyses is not reported stale.
return (
f'{self.__class__.__name__}('
f'display_name={self.display_name!r}, '
f'unique_name={self.unique_name!r}, '
f'n_analyses={len(self._analysis_list)})'
f'n_analyses={len(self.analysis_list)})'
)
Loading
Loading