From d61648b135f7ac96ce4afde9948a6edc4f1cd9f5 Mon Sep 17 00:00:00 2001 From: rozyczko Date: Thu, 18 Jun 2026 15:30:33 +0200 Subject: [PATCH 1/5] moved to Sampler class from Core --- pixi.lock | 23 +-- src/easyreflectometry/analysis/bayesian.py | 5 +- src/easyreflectometry/fitting.py | 19 +- tests/test_fitting.py | 193 +++++++++++---------- 4 files changed, 136 insertions(+), 104 deletions(-) diff --git a/pixi.lock b/pixi.lock index 6b3a38bc..167028d6 100644 --- a/pixi.lock +++ b/pixi.lock @@ -190,7 +190,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/websocket-client-1.9.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.1-pyhcf101f3_0.conda - pypi: . - - pypi: git+https://github.com/easyscience/corelib?rev=develop#aadbd4891b94f6aa18187d48be8c2ab6f81113b0 + - pypi: git+https://github.com/easyscience/corelib.git?rev=develop#aadbd4891b94f6aa18187d48be8c2ab6f81113b0 - pypi: https://files.pythonhosted.org/packages/00/bb/90ba423612b6aa0adccc6b1874bcd4a9b44b660c0c16f346611e00f64ac3/backrefs-7.0-py313-none-any.whl - pypi: https://files.pythonhosted.org/packages/01/7c/fa07d3da2b6253eb8474be16eab2eadf670460e364ccc895ca7ff388ee30/oscrypto-1.3.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl @@ -501,7 +501,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zeromq-4.3.5-h4818236_10.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hbf9d68e_6.conda - pypi: . - - pypi: git+https://github.com/easyscience/corelib?rev=develop#aadbd4891b94f6aa18187d48be8c2ab6f81113b0 + - pypi: git+https://github.com/easyscience/corelib.git?rev=develop#aadbd4891b94f6aa18187d48be8c2ab6f81113b0 - pypi: https://files.pythonhosted.org/packages/00/bb/90ba423612b6aa0adccc6b1874bcd4a9b44b660c0c16f346611e00f64ac3/backrefs-7.0-py313-none-any.whl - pypi: https://files.pythonhosted.org/packages/01/7c/fa07d3da2b6253eb8474be16eab2eadf670460e364ccc895ca7ff388ee30/oscrypto-1.3.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl @@ -802,7 +802,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/zeromq-4.3.5-h507cc87_10.conda - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_6.conda - pypi: . - - pypi: git+https://github.com/easyscience/corelib?rev=develop#aadbd4891b94f6aa18187d48be8c2ab6f81113b0 + - pypi: git+https://github.com/easyscience/corelib.git?rev=develop#aadbd4891b94f6aa18187d48be8c2ab6f81113b0 - pypi: https://files.pythonhosted.org/packages/00/bb/90ba423612b6aa0adccc6b1874bcd4a9b44b660c0c16f346611e00f64ac3/backrefs-7.0-py313-none-any.whl - pypi: https://files.pythonhosted.org/packages/01/7c/fa07d3da2b6253eb8474be16eab2eadf670460e364ccc895ca7ff388ee30/oscrypto-1.3.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl @@ -1125,7 +1125,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/websocket-client-1.9.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.1-pyhcf101f3_0.conda - pypi: . - - pypi: git+https://github.com/easyscience/corelib?rev=develop#aadbd4891b94f6aa18187d48be8c2ab6f81113b0 + - pypi: git+https://github.com/easyscience/corelib.git?rev=develop#aadbd4891b94f6aa18187d48be8c2ab6f81113b0 - pypi: https://files.pythonhosted.org/packages/01/7c/fa07d3da2b6253eb8474be16eab2eadf670460e364ccc895ca7ff388ee30/oscrypto-1.3.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/06/8a/5e156e31ba656ce93c1cc895dd8f051ec351cb382940dca655aaec475005/python_bidi-0.6.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl @@ -1430,7 +1430,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zeromq-4.3.5-h4818236_10.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hbf9d68e_6.conda - pypi: . - - pypi: git+https://github.com/easyscience/corelib?rev=develop#aadbd4891b94f6aa18187d48be8c2ab6f81113b0 + - pypi: git+https://github.com/easyscience/corelib.git?rev=develop#aadbd4891b94f6aa18187d48be8c2ab6f81113b0 - pypi: https://files.pythonhosted.org/packages/01/7c/fa07d3da2b6253eb8474be16eab2eadf670460e364ccc895ca7ff388ee30/oscrypto-1.3.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/07/c7/deb8c5e604404dbf10a3808a858946ca3547692ff6316b698945bb72177e/python_socketio-5.16.1-py3-none-any.whl @@ -1725,7 +1725,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/zeromq-4.3.5-h507cc87_10.conda - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_6.conda - pypi: . - - pypi: git+https://github.com/easyscience/corelib?rev=develop#aadbd4891b94f6aa18187d48be8c2ab6f81113b0 + - pypi: git+https://github.com/easyscience/corelib.git?rev=develop#aadbd4891b94f6aa18187d48be8c2ab6f81113b0 - pypi: https://files.pythonhosted.org/packages/01/7c/fa07d3da2b6253eb8474be16eab2eadf670460e364ccc895ca7ff388ee30/oscrypto-1.3.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/02/05/d60c732b56da5085175c07c74b2df4e6d181b0c9a61e1691474f06ef4b39/lxml-6.1.0-cp311-cp311-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl @@ -2048,7 +2048,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/websocket-client-1.9.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.1-pyhcf101f3_0.conda - pypi: . - - pypi: git+https://github.com/easyscience/corelib?rev=develop#aadbd4891b94f6aa18187d48be8c2ab6f81113b0 + - pypi: git+https://github.com/easyscience/corelib.git?rev=develop#aadbd4891b94f6aa18187d48be8c2ab6f81113b0 - pypi: https://files.pythonhosted.org/packages/00/bb/90ba423612b6aa0adccc6b1874bcd4a9b44b660c0c16f346611e00f64ac3/backrefs-7.0-py313-none-any.whl - pypi: https://files.pythonhosted.org/packages/01/7c/fa07d3da2b6253eb8474be16eab2eadf670460e364ccc895ca7ff388ee30/oscrypto-1.3.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl @@ -2359,7 +2359,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zeromq-4.3.5-h4818236_10.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hbf9d68e_6.conda - pypi: . - - pypi: git+https://github.com/easyscience/corelib?rev=develop#aadbd4891b94f6aa18187d48be8c2ab6f81113b0 + - pypi: git+https://github.com/easyscience/corelib.git?rev=develop#aadbd4891b94f6aa18187d48be8c2ab6f81113b0 - pypi: https://files.pythonhosted.org/packages/00/bb/90ba423612b6aa0adccc6b1874bcd4a9b44b660c0c16f346611e00f64ac3/backrefs-7.0-py313-none-any.whl - pypi: https://files.pythonhosted.org/packages/01/7c/fa07d3da2b6253eb8474be16eab2eadf670460e364ccc895ca7ff388ee30/oscrypto-1.3.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl @@ -2660,7 +2660,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/zeromq-4.3.5-h507cc87_10.conda - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_6.conda - pypi: . - - pypi: git+https://github.com/easyscience/corelib?rev=develop#aadbd4891b94f6aa18187d48be8c2ab6f81113b0 + - pypi: git+https://github.com/easyscience/corelib.git?rev=develop#aadbd4891b94f6aa18187d48be8c2ab6f81113b0 - pypi: https://files.pythonhosted.org/packages/00/bb/90ba423612b6aa0adccc6b1874bcd4a9b44b660c0c16f346611e00f64ac3/backrefs-7.0-py313-none-any.whl - pypi: https://files.pythonhosted.org/packages/01/7c/fa07d3da2b6253eb8474be16eab2eadf670460e364ccc895ca7ff388ee30/oscrypto-1.3.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl @@ -8301,8 +8301,9 @@ packages: name: easyreflectometry requires_dist: - bumps - - easyscience @ git+https://github.com/easyscience/corelib@develop + - easyscience @ git+https://github.com/easyscience/corelib.git@develop - orsopy + - plotly - pooch - refl1d>=1.0.0 - refnx @@ -8344,7 +8345,7 @@ packages: - validate-pyproject[all] ; extra == 'dev' - versioningit ; extra == 'dev' requires_python: '>=3.11' -- pypi: git+https://github.com/easyscience/corelib?rev=develop#aadbd4891b94f6aa18187d48be8c2ab6f81113b0 +- pypi: git+https://github.com/easyscience/corelib.git?rev=develop#aadbd4891b94f6aa18187d48be8c2ab6f81113b0 name: easyscience version: 2.3.1+dev8 requires_dist: diff --git a/src/easyreflectometry/analysis/bayesian.py b/src/easyreflectometry/analysis/bayesian.py index 39389a84..17ab252c 100644 --- a/src/easyreflectometry/analysis/bayesian.py +++ b/src/easyreflectometry/analysis/bayesian.py @@ -1332,8 +1332,9 @@ def load_posterior(path: str, skip: int = 0) -> 'PosteriorResults': """Reload a trace saved by :func:`save_posterior` into a :class:`PosteriorResults`. - The returned object's ``sampler_state`` can be fed back into - ``MultiFitter.mcmc_sample(..., resume_state=...)`` to extend the chain. + The returned object's ``sampler_state`` can be fed back into the core + ``Sampler`` (via ``Sampler.load_state(...)`` / ``Sampler.extend(...)``) + to extend the chain. :param path: File path prefix used in :func:`save_posterior`. :type path: str diff --git a/src/easyreflectometry/fitting.py b/src/easyreflectometry/fitting.py index 83a3f6eb..f90211ff 100644 --- a/src/easyreflectometry/fitting.py +++ b/src/easyreflectometry/fitting.py @@ -10,6 +10,7 @@ import scipp as sc from easyscience.fitting import AvailableMinimizers from easyscience.fitting import FitResults +from easyscience.fitting import Sampler from easyscience.fitting.multi_fitter import MultiFitter as EasyScienceMultiFitter from easyreflectometry.data import DataSet1D @@ -419,14 +420,22 @@ def mcmc_sample( y.append(y_eff) dy.append(weights) - # Delegate the actual BUMPS/DREAM sampling to the core MultiFitter + # Delegate the actual BUMPS/DREAM sampling to the core ``Sampler``. + # The core API moved from ``MultiFitter.mcmc_sample()`` to a dedicated + # ``Sampler`` class: construct it with the configured fitter and the + # bound data, then call ``sample()``. ``Sampler`` handles the + # multi-dataset reshaping internally. sampler_kwargs = {} if initializer is not None: sampler_kwargs['init'] = initializer - return self.easy_science_multi_fitter.mcmc_sample( + + sampler = Sampler( + self.easy_science_multi_fitter, x=x, y=y, weights=dy, + ) + results = sampler.sample( samples=samples, burn=burn, thin=thin, @@ -435,6 +444,12 @@ def mcmc_sample( progress_callback=progress_callback, abort_test=abort_test, ) + return { + 'draws': results.draws, + 'param_names': results.param_names, + 'state': results.state, + 'logp': results.logp, + } @property def chi2(self) -> float | None: diff --git a/tests/test_fitting.py b/tests/test_fitting.py index e4f938c8..2c519867 100644 --- a/tests/test_fitting.py +++ b/tests/test_fitting.py @@ -4,6 +4,7 @@ import os from unittest.mock import MagicMock +from unittest.mock import patch import numpy as np import pytest @@ -808,6 +809,44 @@ def _fake_fit(*, x, y, weights): # --------------------------------------------------------------------------- +def _fake_sampling_results(draws=None, param_names=None, state=None, logp=None): + """Build a stand-in for the core ``SamplingResults`` returned by ``Sampler.sample``.""" + res = MagicMock() + res.draws = np.ones((10, 2)) if draws is None else draws + res.param_names = ['a', 'b'] if param_names is None else param_names + res.state = state + res.logp = logp + return res + + +def _patch_sampler(capture, results=None): + """Patch ``easyreflectometry.fitting.Sampler`` and capture its call args. + + Records the constructor's ``(x, y, weights)`` and the ``sample()`` + hyperparameters into the ``capture`` dict, and returns ``results`` (a + fake ``SamplingResults``) from ``sample()``. + """ + results = results if results is not None else _fake_sampling_results() + + def _ctor(fitter, *, x, y, weights, **kwargs): + capture['fitter'] = fitter + capture['x'] = x + capture['y'] = y + capture['weights'] = weights + capture.update(kwargs) # e.g. sampler_kwargs if passed to the ctor + instance = MagicMock() + + def _sample(**sample_kwargs): + capture.update(sample_kwargs) + return results + + instance.sample = MagicMock(side_effect=_sample) + capture['instance'] = instance + return instance + + return patch('easyreflectometry.fitting.Sampler', side_effect=_ctor) + + class TestMCMCSampleRequiresBumpsEngine: """mcmc_sample() must raise when the core engine is not a BUMPS instance.""" @@ -824,138 +863,129 @@ def test_raises_runtime_error_when_not_bumps(self): with pytest.raises(RuntimeError, match='Bayesian sampling requires a BUMPS minimizer'): fitter.mcmc_sample(data) - def test_wrapper_check_runs_before_core_mcmc_sample(self): - """The wrapper-level guard must fire before delegating to the core sampler. + def test_wrapper_check_runs_before_sampler(self): + """The wrapper-level guard must fire before constructing the core ``Sampler``. - Replace the core ``mcmc_sample`` with a sentinel that would record any call; - the guard should raise without invoking it. + Patch ``Sampler`` with a sentinel that would record any instantiation; + the guard should raise without ever building it. """ model = Model() model.interface = CalculatorFactory() fitter = MultiFitter(model) # default minimizer is LMFit, not BUMPS - core_called = {'count': 0} - - def _should_not_be_called(**_kwargs): - core_called['count'] += 1 - return {'draws': np.empty((0, 0)), 'param_names': [], 'state': None, 'logp': None} - - fitter.easy_science_multi_fitter.mcmc_sample = _should_not_be_called + capture = {} data = sc.DataGroup({ 'coords': {'Qz_0': sc.array(dims=['Qz_0'], values=np.linspace(0.01, 0.3, 10))}, 'data': {'R_0': sc.array(dims=['Qz_0'], values=np.ones(10), variances=np.ones(10) * 0.01)}, }) - with pytest.raises(RuntimeError, match='Bayesian sampling requires a BUMPS minimizer'): - fitter.mcmc_sample(data) - assert core_called['count'] == 0 + with _patch_sampler(capture) as sampler_cls: + with pytest.raises(RuntimeError, match='Bayesian sampling requires a BUMPS minimizer'): + fitter.mcmc_sample(data) + sampler_cls.assert_not_called() class TestMCMCSampleBasic: """Basic mcmc_sample() dispatch and return-value forwarding.""" - def test_returns_core_result_dict(self): - """mcmc_sample() returns whatever the core MultiFitter.mcmc_sample() returns.""" + def test_returns_result_dict_from_sampler(self): + """mcmc_sample() returns a dict built from the core Sampler's SamplingResults.""" model = Model() model.interface = CalculatorFactory() fitter = MultiFitter(model) - # Mock the core MultiFitter.mcmc_sample to return a known dict - fake_result = {'draws': np.ones((10, 2)), 'param_names': ['a', 'b'], 'state': None, 'logp': None} fitter.easy_science_multi_fitter = MagicMock() fitter.easy_science_multi_fitter.minimizer.package = 'bumps' - fitter.easy_science_multi_fitter.mcmc_sample = MagicMock(return_value=fake_result) + + draws = np.ones((10, 2)) + sentinel_state = object() + logp = np.zeros(10) + results = _fake_sampling_results(draws=draws, param_names=['a', 'b'], state=sentinel_state, logp=logp) + + capture = {} data = sc.DataGroup({ 'coords': {'Qz_0': sc.array(dims=['Qz_0'], values=np.linspace(0.01, 0.3, 10))}, 'data': {'R_0': sc.array(dims=['Qz_0'], values=np.ones(10), variances=np.ones(10) * 0.01)}, }) - result = fitter.mcmc_sample(data, samples=100, burn=20, thin=2, population=5) - assert result is fake_result + with _patch_sampler(capture, results=results): + result = fitter.mcmc_sample(data, samples=100, burn=20, thin=2, population=5) + + # The fitter passed to Sampler is the core MultiFitter + assert capture['fitter'] is fitter.easy_science_multi_fitter + assert result['draws'] is draws + assert result['param_names'] == ['a', 'b'] + assert result['state'] is sentinel_state + assert result['logp'] is logp - def test_forwards_hyperparams_to_core(self): - """Samples, burn, thin, population, chains are forwarded to core.""" + def test_forwards_hyperparams_to_sampler(self): + """Samples, burn, thin, population are forwarded to Sampler.sample().""" model = Model() model.interface = CalculatorFactory() fitter = MultiFitter(model) - captured = {} - - def _fake_mcmc_sample(*, x, y, weights, samples, burn, thin, population, **kwargs): - captured['samples'] = samples - captured['burn'] = burn - captured['thin'] = thin - captured['population'] = population - return {'draws': np.ones((10, 2)), 'param_names': ['a', 'b'], 'state': None, 'logp': None} - fitter.easy_science_multi_fitter = MagicMock() fitter.easy_science_multi_fitter.minimizer.package = 'bumps' - fitter.easy_science_multi_fitter.mcmc_sample = MagicMock(side_effect=_fake_mcmc_sample) + + capture = {} data = sc.DataGroup({ 'coords': {'Qz_0': sc.array(dims=['Qz_0'], values=np.linspace(0.01, 0.3, 10))}, 'data': {'R_0': sc.array(dims=['Qz_0'], values=np.ones(10), variances=np.ones(10) * 0.01)}, }) - fitter.mcmc_sample(data, samples=500, burn=100, thin=5, population=8) - assert captured['samples'] == 500 - assert captured['burn'] == 100 - assert captured['thin'] == 5 - assert captured['population'] == 8 + with _patch_sampler(capture): + fitter.mcmc_sample(data, samples=500, burn=100, thin=5, population=8) + assert capture['samples'] == 500 + assert capture['burn'] == 100 + assert capture['thin'] == 5 + assert capture['population'] == 8 - def test_forwards_population_to_core(self): - """'population' argument is forwarded to core.""" + def test_forwards_population_to_sampler(self): + """'population' argument is forwarded to Sampler.sample().""" model = Model() model.interface = CalculatorFactory() fitter = MultiFitter(model) - captured = {} - - def _fake_mcmc_sample(*, x, y, weights, population, **kwargs): - captured['population'] = population - return {'draws': np.ones((10, 2)), 'param_names': ['a', 'b'], 'state': None, 'logp': None} - fitter.easy_science_multi_fitter = MagicMock() fitter.easy_science_multi_fitter.minimizer.package = 'bumps' - fitter.easy_science_multi_fitter.mcmc_sample = MagicMock(side_effect=_fake_mcmc_sample) + + capture = {} data = sc.DataGroup({ 'coords': {'Qz_0': sc.array(dims=['Qz_0'], values=np.linspace(0.01, 0.3, 10))}, 'data': {'R_0': sc.array(dims=['Qz_0'], values=np.ones(10), variances=np.ones(10) * 0.01)}, }) - fitter.mcmc_sample(data, samples=100, burn=20, thin=2, population=6) - assert captured['population'] == 6 + with _patch_sampler(capture): + fitter.mcmc_sample(data, samples=100, burn=20, thin=2, population=6) + assert capture['population'] == 6 class TestMCMCSampleInitializer: """initializer parameter is forwarded via sampler_kwargs.""" def test_initializer_passed_as_sampler_kwargs_init(self): - """initializer='lhs' should be passed as sampler_kwargs={'init': 'lhs'} to core.""" + """initializer='lhs' should be passed as sampler_kwargs={'init': 'lhs'} to Sampler.sample().""" model = Model() model.interface = CalculatorFactory() fitter = MultiFitter(model) - captured = {} - - def _fake_mcmc_sample(*, sampler_kwargs, **kwargs): - captured['sampler_kwargs'] = sampler_kwargs - return {'draws': np.ones((10, 2)), 'param_names': ['a', 'b'], 'state': None, 'logp': None} - fitter.easy_science_multi_fitter = MagicMock() fitter.easy_science_multi_fitter.minimizer.package = 'bumps' - fitter.easy_science_multi_fitter.mcmc_sample = MagicMock(side_effect=_fake_mcmc_sample) + + capture = {} data = sc.DataGroup({ 'coords': {'Qz_0': sc.array(dims=['Qz_0'], values=np.linspace(0.01, 0.3, 10))}, 'data': {'R_0': sc.array(dims=['Qz_0'], values=np.ones(10), variances=np.ones(10) * 0.01)}, }) - fitter.mcmc_sample(data, samples=100, burn=20, thin=2, initializer='lhs') - assert captured['sampler_kwargs'] == {'init': 'lhs'} + with _patch_sampler(capture): + fitter.mcmc_sample(data, samples=100, burn=20, thin=2, initializer='lhs') + assert capture['sampler_kwargs'] == {'init': 'lhs'} def test_initializer_none_omits_sampler_kwargs(self): """When initializer is None, sampler_kwargs should be None, not an empty dict.""" @@ -963,23 +993,19 @@ def test_initializer_none_omits_sampler_kwargs(self): model.interface = CalculatorFactory() fitter = MultiFitter(model) - captured = {} - - def _fake_mcmc_sample(*, sampler_kwargs, **kwargs): - captured['sampler_kwargs'] = sampler_kwargs - return {'draws': np.ones((10, 2)), 'param_names': ['a', 'b'], 'state': None, 'logp': None} - fitter.easy_science_multi_fitter = MagicMock() fitter.easy_science_multi_fitter.minimizer.package = 'bumps' - fitter.easy_science_multi_fitter.mcmc_sample = MagicMock(side_effect=_fake_mcmc_sample) + + capture = {} data = sc.DataGroup({ 'coords': {'Qz_0': sc.array(dims=['Qz_0'], values=np.linspace(0.01, 0.3, 10))}, 'data': {'R_0': sc.array(dims=['Qz_0'], values=np.ones(10), variances=np.ones(10) * 0.01)}, }) - fitter.mcmc_sample(data, samples=100, burn=20, thin=2) - assert captured['sampler_kwargs'] is None + with _patch_sampler(capture): + fitter.mcmc_sample(data, samples=100, burn=20, thin=2) + assert capture['sampler_kwargs'] is None class TestMCMCSampleZeroVariance: @@ -994,17 +1020,10 @@ def test_hybrid_transforms_zero_variance_points(self): # Use legacy_mask so zero-variance points are dropped fitter = MultiFitter(model, objective='legacy_mask') - captured = {} - - def _fake_mcmc_sample(*, x, y, weights, **kwargs): - captured['x'] = x - captured['y'] = y - captured['weights'] = weights - return {'draws': np.ones((10, 2)), 'param_names': ['a', 'b'], 'state': None, 'logp': None} + capture = {} fitter.easy_science_multi_fitter = MagicMock() fitter.easy_science_multi_fitter.minimizer.package = 'bumps' - fitter.easy_science_multi_fitter.mcmc_sample = MagicMock(side_effect=_fake_mcmc_sample) qz = np.linspace(0.01, 0.3, 10) r = np.exp(-qz * 50) @@ -1018,12 +1037,13 @@ def _fake_mcmc_sample(*, x, y, weights, **kwargs): with warnings.catch_warnings(record=True) as w: warnings.simplefilter('always') - fitter.mcmc_sample(data, samples=100, burn=20, thin=2) + with _patch_sampler(capture): + fitter.mcmc_sample(data, samples=100, burn=20, thin=2) # legacy_mask should drop the 2 zero-variance points - assert len(captured['x'][0]) == 8 - assert len(captured['y'][0]) == 8 - assert len(captured['weights'][0]) == 8 + assert len(capture['x'][0]) == 8 + assert len(capture['y'][0]) == 8 + assert len(capture['weights'][0]) == 8 mask_warnings = [str(ww.message) for ww in w if 'Masked' in str(ww.message)] assert len(mask_warnings) == 1 @@ -1037,16 +1057,10 @@ def test_per_call_objective_override(self): model.interface = CalculatorFactory() fitter = MultiFitter(model, objective='legacy_mask') # default - captured = {} - - def _fake_mcmc_sample(*, x, y, weights, **kwargs): - captured['x'] = x - captured['y'] = y - return {'draws': np.ones((10, 2)), 'param_names': ['a', 'b'], 'state': None, 'logp': None} + capture = {} fitter.easy_science_multi_fitter = MagicMock() fitter.easy_science_multi_fitter.minimizer.package = 'bumps' - fitter.easy_science_multi_fitter.mcmc_sample = MagicMock(side_effect=_fake_mcmc_sample) qz = np.linspace(0.01, 0.3, 10) r = np.exp(-qz * 50) @@ -1061,6 +1075,7 @@ def _fake_mcmc_sample(*, x, y, weights, **kwargs): # Override to hybrid β€” should keep all 10 points with warnings.catch_warnings(record=True): warnings.simplefilter('always') - fitter.mcmc_sample(data, samples=100, burn=20, thin=2, objective='hybrid') + with _patch_sampler(capture): + fitter.mcmc_sample(data, samples=100, burn=20, thin=2, objective='hybrid') - assert len(captured['x'][0]) == 10 # all points kept (Mighell-substituted) + assert len(capture['x'][0]) == 10 # all points kept (Mighell-substituted) From 0f9e6e5d7b63737b4b5d5447d248c5c5508154e4 Mon Sep 17 00:00:00 2001 From: rozyczko Date: Thu, 18 Jun 2026 22:02:26 +0200 Subject: [PATCH 2/5] reparent branch --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index ac183b21..596b7f27 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,8 @@ classifiers = [ ] requires-python = '>=3.11' dependencies = [ - 'easyscience @ git+https://github.com/easyscience/corelib.git@develop', + # 'easyscience @ git+https://github.com/easyscience/corelib.git@develop', + 'easyscience @ git+https://github.com/easyscience/corelib.git@bayesian_extend', # 'easyscience', 'scipp', 'refnx', From 25808440fc26ea0dcd206892094e6f341ba9f226 Mon Sep 17 00:00:00 2001 From: Piotr Rozyczko Date: Thu, 16 Jul 2026 12:42:57 +0200 Subject: [PATCH 3/5] fixed issue #367 (#382) * fixed issue #367 * disable automatic parallelization for windows * Changes after code review * added test * arviz fix * try to fix prettier issues --- .github/workflows/pypi-test.yml | 4 +- .github/workflows/test.yml | 4 +- CHANGELOG.md | 31 +++ CONTRIBUTING.md | 3 +- docs/mkdocs.yml | 3 +- pixi.toml | 10 +- .../calculators/refl1d/wrapper.py | 7 +- .../calculators/refnx/wrapper.py | 10 +- .../model/resolution_functions.py | 42 +++- tests/calculators/refnx/test_refnx_wrapper.py | 2 +- .../test_resolution_conventions.py | 207 ++++++++++++++++++ .../test_cross_engine_resolution.py | 187 ++++++++++++++++ tests/model/test_model.py | 13 +- tests/model/test_resolution_functions.py | 34 +-- 14 files changed, 514 insertions(+), 43 deletions(-) create mode 100644 tests/calculators/test_resolution_conventions.py create mode 100644 tests/integration/test_cross_engine_resolution.py diff --git a/.github/workflows/pypi-test.yml b/.github/workflows/pypi-test.yml index 2f4f58ac..8bcb4b5e 100644 --- a/.github/workflows/pypi-test.yml +++ b/.github/workflows/pypi-test.yml @@ -71,7 +71,9 @@ jobs: - name: Run integration tests to verify the installation working-directory: easyreflectometry - run: pixi run python -m pytest ../tests/integration/ --color=yes -n auto + # No -n auto: concurrent xdist workers race on arviz's daily-warning + # stamp file when they import easyreflectometry. See pixi.toml. + run: pixi run python -m pytest ../tests/integration/ --color=yes # Job 2: Build and publish dashboard (reusable workflow) run-reusable-workflows: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6bf99c45..530c154f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -293,7 +293,9 @@ jobs: cd easyreflectometry_py$py_ver echo "Running tests" - pixi run python -m pytest ../tests/integration/ --color=yes -n auto -v ${{ needs.env-prepare.outputs.pytest-marks }} + # No -n auto: concurrent xdist workers race on arviz's daily-warning + # stamp file when they import easyreflectometry. See pixi.toml. + pixi run python -m pytest ../tests/integration/ --color=yes -v ${{ needs.env-prepare.outputs.pytest-marks }} echo "Exiting pixi project directory" cd .. diff --git a/CHANGELOG.md b/CHANGELOG.md index 2537591f..30ca4e2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,36 @@ # Unreleased +Fixed inconsistent interpretation of vector resolution functions between +the refnx and refl1d engines (issue #367). + +- **Reflectivity results change for two engine / resolution + combinations.** `LinearSpline` on refl1d previously **over-smeared by + a factor of 2.355** (its FWHM widths were passed to refl1d's + `probe.dQ`, which expects sigma). `Pointwise` on refnx previously + **under-smeared by the same factor** (its sigma widths were passed to + refnx's `x_err`, which expects FWHM). Both are now correct. Fits and + simulations that used either combination will produce different β€” + previously wrong β€” results and should be re-run. `PercentageFwhm` on + either engine, `LinearSpline` on refnx, and `Pointwise` on refl1d are + numerically unchanged. +- `ResolutionFunction.smearing()` now returns **sigma** (the Gaussian + standard deviation) for every subclass; each engine wrapper converts + to its backend's convention. This is a behavioural change to a public + method. Most visibly, `PercentageFwhm.smearing(q)` used to return the + _percentage_ itself (e.g. `5.0`) and now returns an absolute sigma + (e.g. `0.00212` at `q=0.1`); `LinearSpline.smearing(q)` returns its + `fwhm_values` divided by `2*sqrt(2*ln2)`. Callers relying on the old + values need to convert. The new `SIGMA_TO_FWHM` constant is exported + from `easyreflectometry.model.resolution_functions`. +- Constructors are **unchanged**: `PercentageFwhm(5)` still means 5% + FWHM and `LinearSpline(q, fwhm_values)` still takes FWHM. Only the + `smearing()` output convention moved, so existing model-building code + needs no edits. +- `PercentageFwhm.smearing(q)` given a scalar `q` now returns a 0-d + numpy scalar rather than a shape-`(1,)` array, matching + `LinearSpline`. `smearing(0.1)[0]` therefore raises `IndexError` where + it previously returned a value. + Migrated sample / model classes off the deprecated `easyscience.ObjBase` and `easyscience.CollectionBase` pipeline. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a4ac1fbc..a6ceeebd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -42,8 +42,7 @@ Please make sure you follow the EasyScience organization-wide If you are not planning to contribute code, you may want to: - 🐞 Report a bug β€” see [Reporting Issues](#11-reporting-issues) -- πŸ›‘ Report a security issue β€” see - [Security Issues](#12-security-issues) +- πŸ›‘ Report a security issue β€” see [Security Issues](#12-security-issues) - πŸ’¬ Ask a question or start a discussion at [Project Discussions](https://github.com/easyscience/reflectometry-lib/discussions) diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index f75e4657..1cd19cde 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -208,7 +208,8 @@ nav: - Elements: - Layers: - Layer: api-reference/elements/layer.md - - Layer Area Per Molecule: api-reference/elements/layer_area_per_molecule.md + - Layer Area Per Molecule: + api-reference/elements/layer_area_per_molecule.md - Materials: - Material: api-reference/elements/material.md - Material Density: api-reference/elements/material_density.md diff --git a/pixi.toml b/pixi.toml index b0fb2b8d..ab6c977e 100644 --- a/pixi.toml +++ b/pixi.toml @@ -94,7 +94,15 @@ user = { features = ['py-max', 'user'] } unit-tests = 'python -m pytest tests/unit/ --color=yes -v' functional-tests = 'python -m pytest tests/functional/ --color=yes -v' -integration-tests = 'python -m pytest tests/integration/ --color=yes -n auto -v' +# No -n auto: importing easyreflectometry pulls in arviz, and arviz 0.23.4 +# (py-311-env) writes a "warn once per day" stamp file on import via a +# _atomic_write_text() that is not atomic -- every process writes the same +# fixed `daily_warning.tmp` and renames it onto `daily_warning`. Concurrent +# xdist workers therefore race on that rename: FileNotFoundError on Linux, +# PermissionError (WinError 32) on Windows. The real fix is +# to stop importing arviz in easyreflectometry/__init__.py, after which +# xdist can come back. +integration-tests = 'python -m pytest tests/integration/ --color=yes -v' notebook-tests = 'python -m pytest --nbmake docs/docs/tutorials/**/ --nbmake-timeout=1200 --color=yes -n auto -v' test = { depends-on = ['unit-tests'] } diff --git a/src/easyreflectometry/calculators/refl1d/wrapper.py b/src/easyreflectometry/calculators/refl1d/wrapper.py index 7a07c917..6985d47c 100644 --- a/src/easyreflectometry/calculators/refl1d/wrapper.py +++ b/src/easyreflectometry/calculators/refl1d/wrapper.py @@ -8,8 +8,6 @@ from refl1d import names from refl1d.sample.layers import Repeat -from easyreflectometry.model import PercentageFwhm - from ..wrapper_base import WrapperBase RESOLUTION_PADDING = 3.5 @@ -205,12 +203,9 @@ def calculate(self, q_array: np.ndarray, model_name: str) -> np.ndarray: Reflectivity calculated at q. """ sample = _build_sample(self.storage, model_name) + # smearing() returns sigma, which is exactly what refl1d's probe.dQ expects. dq_array = self._resolution_function.smearing(q_array) - if isinstance(self._resolution_function, PercentageFwhm): - # Get percentage of Q and change from sigma to FWHM - dq_array = dq_array * q_array / 100 / (2 * np.sqrt(2 * np.log(2))) - if not self._magnetism: probe = _get_probe( q_array=q_array, diff --git a/src/easyreflectometry/calculators/refnx/wrapper.py b/src/easyreflectometry/calculators/refnx/wrapper.py index 3742d727..65dc8662 100644 --- a/src/easyreflectometry/calculators/refnx/wrapper.py +++ b/src/easyreflectometry/calculators/refnx/wrapper.py @@ -8,6 +8,7 @@ from refnx import reflect from easyreflectometry.model import PercentageFwhm +from easyreflectometry.model.resolution_functions import SIGMA_TO_FWHM from ..wrapper_base import WrapperBase @@ -191,9 +192,12 @@ def calculate(self, q_array: np.ndarray, model_name: str) -> np.ndarray: dq_vector = self._resolution_function.smearing(q_array) if isinstance(self._resolution_function, PercentageFwhm): - # FWHM Percentage resolution is constant given as - # For a constant resolution percentage refnx supports to pass a scalar value rather than a vector - dq_vector = dq_vector[0] + # refnx interprets a scalar x_err as a constant dq/q (FWHM percentage), + # so pass the percentage directly rather than a per-point vector. + dq_vector = self._resolution_function.constant + else: + # smearing() returns sigma; refnx expects the FWHM at each point. + dq_vector = dq_vector * SIGMA_TO_FWHM return model(x=q_array, x_err=dq_vector) diff --git a/src/easyreflectometry/model/resolution_functions.py b/src/easyreflectometry/model/resolution_functions.py index fe5d2254..9579ad66 100644 --- a/src/easyreflectometry/model/resolution_functions.py +++ b/src/easyreflectometry/model/resolution_functions.py @@ -6,6 +6,14 @@ Gaussian distribution with a FWHM of the percentage of the q value. To convert from a sigma value to a FWHM value we use the formula FWHM = 2.35 * sigma [2 * np.sqrt(2 * np.log(2)) * sigma]. + +The :meth:`ResolutionFunction.smearing` contract returns **sigma** +(the standard deviation of the Gaussian resolution) for every resolution +type. This matches the ``sQz`` convention used by data reduction and the +natural output of :class:`Pointwise`. Each calculation engine wrapper is +responsible for converting sigma to the width convention of its backend +(FWHM for refnx, sigma for refl1d), so that vector resolutions are +interpreted consistently across engines (see GitHub issue #367). """ from __future__ import annotations @@ -19,10 +27,15 @@ DEFAULT_RESOLUTION_FWHM_PERCENTAGE = 5.0 +# Conversion factor between sigma and FWHM for a Gaussian: FWHM = SIGMA_TO_FWHM * sigma. +SIGMA_TO_FWHM = 2 * np.sqrt(2 * np.log(2)) + class ResolutionFunction: @abstractmethod - def smearing(self, q: Union[np.array, float]) -> np.array: ... + def smearing(self, q: Union[np.array, float]) -> np.array: + """Return the resolution as sigma (standard deviation) at each ``q``.""" + ... @abstractmethod def as_dict(self, skip: Optional[List[str]] = None) -> dict: ... @@ -51,8 +64,14 @@ def __init__(self, constant: Union[None, float] = None): self.constant = constant def smearing(self, q: Union[np.array, float]) -> np.array: - """Smearing function.""" - return np.ones(np.array(q).size) * self.constant + """Return per-point sigma values from the constant FWHM percentage. + + ``constant`` is a FWHM percentage of ``q``; it is converted to an + absolute sigma so the smearing() contract is sigma for all types. + """ + q_array = np.asarray(q, dtype=float) + fwhm = (self.constant / 100.0) * q_array + return fwhm / SIGMA_TO_FWHM def as_dict( self, skip: Optional[List[str]] = None @@ -68,8 +87,13 @@ def __init__(self, q_data_points: np.array, fwhm_values: np.array): self.fwhm_values = fwhm_values def smearing(self, q: Union[np.array, float]) -> np.array: - """Smearing function.""" - return np.interp(q, self.q_data_points, self.fwhm_values) + """Return per-point sigma values from the FWHM knots. + + The stored ``fwhm_values`` are FWHM widths; they are interpolated + onto ``q`` and converted to sigma to satisfy the smearing() contract. + """ + fwhm = np.interp(np.asarray(q, dtype=float), self.q_data_points, self.fwhm_values) + return fwhm / SIGMA_TO_FWHM def as_dict( self, skip: Optional[List[str]] = None @@ -110,10 +134,12 @@ def __init__(self, q_data_points: List[np.ndarray]): self.q_data_points = q_data_points def smearing(self, q: Optional[Union[np.ndarray, float]] = None) -> np.ndarray: - """Return the resolution width interpolated onto ``q``. + """Return the resolution sigma interpolated onto ``q``. - The width at each data point is ``sqrt(sQz)``; values are linearly - interpolated onto the requested ``q``. When ``q`` is ``None`` the widths + ``sQz`` is the variance of ``Qz``, so the sigma at each data point is + ``sqrt(sQz)``; values are linearly interpolated onto the requested + ``q``. This already satisfies the sigma smearing() contract, so no + FWHM conversion is applied. When ``q`` is ``None`` the sigma values are returned at the stored data points. """ Qz = np.asarray(self.q_data_points[0], dtype=float) diff --git a/tests/calculators/refnx/test_refnx_wrapper.py b/tests/calculators/refnx/test_refnx_wrapper.py index f9d2a4cc..bb99d633 100644 --- a/tests/calculators/refnx/test_refnx_wrapper.py +++ b/tests/calculators/refnx/test_refnx_wrapper.py @@ -451,7 +451,7 @@ def test_calculate_github_test4_spline_resolution(self): p.add_item('Item3', 'MyModel') p.add_item('Item4', 'MyModel') p.update_model('MyModel', bkg=0) - sigma_to_fwhm = 2.355 + sigma_to_fwhm = 2.0 * np.sqrt(2.0 * np.log(2.0)) p.set_resolution_function(LinearSpline(test4_dat[:, 0], sigma_to_fwhm * test4_dat[:, 3])) assert_allclose(p.calculate(test4_dat[:, 0], 'MyModel'), test4_dat[:, 1], rtol=0.03) diff --git a/tests/calculators/test_resolution_conventions.py b/tests/calculators/test_resolution_conventions.py new file mode 100644 index 00000000..5b7c7fb7 --- /dev/null +++ b/tests/calculators/test_resolution_conventions.py @@ -0,0 +1,207 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + +"""Absolute checks on the width convention each wrapper hands to its backend. + +``ResolutionFunction.smearing()`` returns sigma for every resolution type; each +wrapper is then responsible for converting to what its backend expects: + +* refnx -- ``x_err`` is the **FWHM** at each q (a scalar ``x_err`` is instead a + constant dQ/Q FWHM *percentage*). +* refl1d -- ``QProbe.dQ`` is **sigma**. + +The cross-engine tests in ``tests/integration/test_cross_engine_resolution.py`` +only pin the engines against each other, so they cannot catch an error applied +consistently to both. These tests intercept the value at each engine boundary +and assert the exact numbers, which pins the convention absolutely. In +particular this is the only absolute check on the refl1d resolution path. + +See GitHub issue #367 for background. +""" + +import numpy as np +import pytest +from numpy.testing import assert_allclose +from refl1d import names +from refnx import reflect + +from easyreflectometry.calculators.refl1d.wrapper import Refl1dWrapper +from easyreflectometry.calculators.refnx.wrapper import RefnxWrapper +from easyreflectometry.model.resolution_functions import SIGMA_TO_FWHM +from easyreflectometry.model.resolution_functions import LinearSpline +from easyreflectometry.model.resolution_functions import PercentageFwhm +from easyreflectometry.model.resolution_functions import Pointwise + +Q = np.linspace(0.01, 0.3, 20) + +Q_KNOTS = np.linspace(0.001, 0.5, 10) +FWHM_KNOTS = 0.02 * Q_KNOTS + 0.001 + +QZ = np.linspace(0.001, 0.5, 50) +SIGMA_POINTS = 0.01 * QZ + 0.0005 +SQZ = SIGMA_POINTS**2 + + +def _build_refnx(): + wrapper = RefnxWrapper() + wrapper.reset_storage() + wrapper.create_material('Substrate') + wrapper.update_material('Substrate', real=2.07, imag=0.0) + wrapper.create_material('Film') + wrapper.update_material('Film', real=3.45, imag=0.0) + wrapper.create_layer('SubstrateLayer') + wrapper.assign_material_to_layer('Substrate', 'SubstrateLayer') + wrapper.create_layer('FilmLayer') + wrapper.assign_material_to_layer('Film', 'FilmLayer') + wrapper.update_layer('FilmLayer', thick=100.0, rough=3.0) + wrapper.create_item('Item') + wrapper.add_layer_to_item('FilmLayer', 'Item') + wrapper.add_layer_to_item('SubstrateLayer', 'Item') + wrapper.create_model('MyModel') + wrapper.add_item('Item', 'MyModel') + wrapper.update_model('MyModel', bkg=0.0) + return wrapper + + +def _build_refl1d(): + wrapper = Refl1dWrapper() + wrapper.reset_storage() + wrapper.create_material('Substrate') + wrapper.update_material('Substrate', rho=2.07, irho=0.0) + wrapper.create_material('Film') + wrapper.update_material('Film', rho=3.45, irho=0.0) + wrapper.create_layer('SubstrateLayer') + wrapper.assign_material_to_layer('Substrate', 'SubstrateLayer') + wrapper.create_layer('FilmLayer') + wrapper.assign_material_to_layer('Film', 'FilmLayer') + wrapper.update_layer('FilmLayer', thickness=100.0, interface=3.0) + wrapper.create_item('Item') + wrapper.add_layer_to_item('FilmLayer', 'Item') + wrapper.add_layer_to_item('SubstrateLayer', 'Item') + wrapper.create_model('MyModel') + wrapper.add_item('Item', 'MyModel') + wrapper.update_model('MyModel', bkg=0.0) + return wrapper + + +def _capture_refnx_x_err(monkeypatch, resolution_function): + """Run RefnxWrapper.calculate and return the x_err handed to refnx.""" + captured = {} + real_call = reflect.ReflectModel.__call__ + + def spy(self, x, p=None, x_err=None): + captured['x_err'] = x_err + return real_call(self, x, p=p, x_err=x_err) + + monkeypatch.setattr(reflect.ReflectModel, '__call__', spy) + + wrapper = _build_refnx() + wrapper.set_resolution_function(resolution_function) + wrapper.calculate(Q, 'MyModel') + return captured['x_err'] + + +def _capture_refl1d_dq(monkeypatch, resolution_function): + """Run Refl1dWrapper.calculate and return the dQ handed to refl1d's QProbe.""" + captured = {} + real_qprobe = names.QProbe + + def spy(**kwargs): + captured['dQ'] = np.asarray(kwargs['dQ'], dtype=float) + return real_qprobe(**kwargs) + + monkeypatch.setattr(names, 'QProbe', spy) + + wrapper = _build_refl1d() + wrapper.set_resolution_function(resolution_function) + wrapper.calculate(Q, 'MyModel') + return captured['dQ'] + + +# ----- the constant itself ----- + + +@pytest.mark.fast +def test_sigma_to_fwhm_is_the_gaussian_ratio(): + """Pin SIGMA_TO_FWHM against a literal. + + Every other test in this module imports SIGMA_TO_FWHM -- the same constant + the production code uses -- so a wrong value would cancel out on both sides + of the assertion and stay invisible. This is the one place the constant is + checked against an external fact: the FWHM/sigma ratio of a Gaussian, + 2*sqrt(2*ln2). + """ + assert SIGMA_TO_FWHM == pytest.approx(2.3548200450309493) + + +# ----- refnx expects FWHM ----- + + +@pytest.mark.fast +def test_refnx_receives_fwhm_for_linear_spline(monkeypatch): + x_err = _capture_refnx_x_err(monkeypatch, LinearSpline(Q_KNOTS, FWHM_KNOTS)) + + expected_fwhm = np.interp(Q, Q_KNOTS, FWHM_KNOTS) + assert_allclose(x_err, expected_fwhm) + + +@pytest.mark.fast +def test_refnx_receives_fwhm_for_pointwise(monkeypatch): + x_err = _capture_refnx_x_err(monkeypatch, Pointwise([QZ, np.ones_like(QZ), SQZ])) + + expected_sigma = np.interp(Q, QZ, np.sqrt(SQZ)) + assert_allclose(x_err, expected_sigma * SIGMA_TO_FWHM) + + +@pytest.mark.fast +def test_refnx_receives_scalar_percentage_for_percentage_fwhm(monkeypatch): + x_err = _capture_refnx_x_err(monkeypatch, PercentageFwhm(5.0)) + + # refnx reads a scalar x_err as a constant dQ/Q FWHM percentage, so the + # percentage is passed through verbatim -- not converted to a width. + assert np.isscalar(x_err) or np.ndim(x_err) == 0 + assert_allclose(x_err, 5.0) + + +# ----- refl1d expects sigma ----- + + +@pytest.mark.fast +def test_refl1d_receives_sigma_for_linear_spline(monkeypatch): + dq = _capture_refl1d_dq(monkeypatch, LinearSpline(Q_KNOTS, FWHM_KNOTS)) + + expected_fwhm = np.interp(Q, Q_KNOTS, FWHM_KNOTS) + assert_allclose(dq, expected_fwhm / SIGMA_TO_FWHM) + + +@pytest.mark.fast +def test_refl1d_receives_sigma_for_pointwise(monkeypatch): + dq = _capture_refl1d_dq(monkeypatch, Pointwise([QZ, np.ones_like(QZ), SQZ])) + + expected_sigma = np.interp(Q, QZ, np.sqrt(SQZ)) + assert_allclose(dq, expected_sigma) + + +@pytest.mark.fast +def test_refl1d_receives_sigma_for_percentage_fwhm(monkeypatch): + dq = _capture_refl1d_dq(monkeypatch, PercentageFwhm(5.0)) + + expected_sigma = (5.0 / 100.0) * Q / SIGMA_TO_FWHM + assert_allclose(dq, expected_sigma) + + +# ----- the two backends must receive widths that differ by exactly SIGMA_TO_FWHM ----- + + +@pytest.mark.fast +def test_engines_receive_widths_differing_by_sigma_to_fwhm(monkeypatch): + """The whole point of issue #367, stated directly. + + Catches a common-mode error that the cross-engine reflectivity comparison + cannot see: whatever the widths are, refnx's must be exactly SIGMA_TO_FWHM + times refl1d's. + """ + x_err = _capture_refnx_x_err(monkeypatch, LinearSpline(Q_KNOTS, FWHM_KNOTS)) + dq = _capture_refl1d_dq(monkeypatch, LinearSpline(Q_KNOTS, FWHM_KNOTS)) + + assert_allclose(x_err, dq * SIGMA_TO_FWHM) diff --git a/tests/integration/test_cross_engine_resolution.py b/tests/integration/test_cross_engine_resolution.py new file mode 100644 index 00000000..6da7be16 --- /dev/null +++ b/tests/integration/test_cross_engine_resolution.py @@ -0,0 +1,187 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + +"""Cross-engine consistency checks for resolution function width conventions. + +The same model + resolution function should produce broadly the same +reflectivity on refnx and refl1d. A width-convention error at one engine +boundary shows up as a systematic disagreement between them. + +.. warning:: + + These are **smoke tests, not the regression tests for issue #367.** They + compare the engines against each other, so they are blind to any error + applied consistently to both, and -- measured, not assumed -- they are only + sensitive enough to catch *one* of the two bugs #367 fixed: + + * ``LinearSpline``: the pre-fix refl1d code **over**-smeared by 2.355x, + which moves the curve enough to be caught here (measured separation ~2x). + * ``Pointwise``: the pre-fix refnx code **under**-smeared by 2.355x from an + already-small width. That barely moves the curve -- measured separation + ~1.1x, against a baseline engine disagreement of the same size -- so **no + tolerance can catch it here.** The Pointwise test below is a consistency + check only. + + The actual, exact regression tests for both conventions live in + ``tests/calculators/test_resolution_conventions.py``, which intercepts the + widths handed to each backend and asserts them to floating-point precision. + Fix that file first if these ever conflict. + +.. note:: + + Reflectivity spans several decades and the engines' different resolution + algorithms (refnx: pointwise convolution; refl1d: oversampling) disagree + most at fringe minima, where R is tiny and *relative* differences explode. + The comparison is therefore made on ``log10(R)``, and the tolerances are + measured values with roughly 1.5x headroom rather than round numbers. + +See GitHub issue #367 for background. +""" + +import numpy as np +import pytest + +from easyreflectometry.calculators.refl1d.wrapper import Refl1dWrapper +from easyreflectometry.calculators.refnx.wrapper import RefnxWrapper +from easyreflectometry.model.resolution_functions import SIGMA_TO_FWHM +from easyreflectometry.model.resolution_functions import LinearSpline +from easyreflectometry.model.resolution_functions import PercentageFwhm +from easyreflectometry.model.resolution_functions import Pointwise + +Q = np.geomspace(0.005, 0.3, 100) + + +def _build_simple_model_refnx(wrapper): + """Build an ambient | 100 A film | substrate model on a refnx wrapper. + + The ambient layer is not optional decoration: both engines treat the first + layer as the semi-infinite superphase and ignore its thickness. Without it + the "film" becomes the ambient, leaving a bare interface with no Kiessig + fringes -- and resolution smearing acts almost entirely on fringes, so the + model would be insensitive to the very thing under test. + """ + wrapper.reset_storage() + wrapper.create_material('Ambient') + wrapper.update_material('Ambient', real=0.0, imag=0.0) + wrapper.create_material('Film') + wrapper.update_material('Film', real=3.45, imag=0.0) + wrapper.create_material('Substrate') + wrapper.update_material('Substrate', real=2.07, imag=0.0) + wrapper.create_layer('AmbientLayer') + wrapper.assign_material_to_layer('Ambient', 'AmbientLayer') + wrapper.create_layer('FilmLayer') + wrapper.assign_material_to_layer('Film', 'FilmLayer') + wrapper.update_layer('FilmLayer', thick=100.0, rough=3.0) + wrapper.create_layer('SubstrateLayer') + wrapper.assign_material_to_layer('Substrate', 'SubstrateLayer') + wrapper.update_layer('SubstrateLayer', rough=3.0) + wrapper.create_item('Item') + wrapper.add_layer_to_item('AmbientLayer', 'Item') + wrapper.add_layer_to_item('FilmLayer', 'Item') + wrapper.add_layer_to_item('SubstrateLayer', 'Item') + wrapper.create_model('MyModel') + wrapper.add_item('Item', 'MyModel') + wrapper.update_model('MyModel', bkg=0.0) + + +def _build_simple_model_refl1d(wrapper): + """Build the same ambient | 100 A film | substrate model on refl1d.""" + wrapper.reset_storage() + wrapper.create_material('Ambient') + wrapper.update_material('Ambient', rho=0.0, irho=0.0) + wrapper.create_material('Film') + wrapper.update_material('Film', rho=3.45, irho=0.0) + wrapper.create_material('Substrate') + wrapper.update_material('Substrate', rho=2.07, irho=0.0) + wrapper.create_layer('AmbientLayer') + wrapper.assign_material_to_layer('Ambient', 'AmbientLayer') + wrapper.create_layer('FilmLayer') + wrapper.assign_material_to_layer('Film', 'FilmLayer') + wrapper.update_layer('FilmLayer', thickness=100.0, interface=3.0) + wrapper.create_layer('SubstrateLayer') + wrapper.assign_material_to_layer('Substrate', 'SubstrateLayer') + wrapper.update_layer('SubstrateLayer', interface=3.0) + wrapper.create_item('Item') + wrapper.add_layer_to_item('AmbientLayer', 'Item') + wrapper.add_layer_to_item('FilmLayer', 'Item') + wrapper.add_layer_to_item('SubstrateLayer', 'Item') + wrapper.create_model('MyModel') + wrapper.add_item('Item', 'MyModel') + wrapper.update_model('MyModel', bkg=0.0) + + +def _both_engines(resolution_function): + """Return (refnx_reflectivity, refl1d_reflectivity) for one resolution.""" + refnx_w = RefnxWrapper() + _build_simple_model_refnx(refnx_w) + refnx_w.set_resolution_function(resolution_function) + refnx_r = refnx_w.calculate(Q, 'MyModel') + + refl1d_w = Refl1dWrapper() + _build_simple_model_refl1d(refl1d_w) + refl1d_w.set_resolution_function(resolution_function) + refl1d_r = refl1d_w.calculate(Q, 'MyModel') + + return refnx_r, refl1d_r + + +def _assert_log_close(refnx_r, refl1d_r, atol): + """Assert the engines agree to `atol` decades of R at every q.""" + deviation = np.abs(np.log10(refnx_r) - np.log10(refl1d_r)) + assert deviation.max() <= atol, ( + f'engines disagree by {deviation.max():.4f} decades ' + f'(factor {10 ** deviation.max():.2f}) at q={Q[np.argmax(deviation)]:.4f}, tolerance {atol}' + ) + + +@pytest.mark.fast +@pytest.mark.parametrize(('resolution_pct', 'atol'), [(1.0, 0.07), (5.0, 0.23), (10.0, 0.29)]) +def test_percentage_fwhm_consistent_across_engines(resolution_pct, atol): + """PercentageFwhm gives consistent results across engines. + + Measured disagreement grows with the width (0.041 / 0.149 / 0.191 decades + at 1% / 5% / 10% dQ/Q), so the tolerance is parametrized with it rather + than set to one blanket value. This combination was correct both before + and after issue #367; the test guards against regression. + """ + refnx_r, refl1d_r = _both_engines(PercentageFwhm(resolution_pct)) + _assert_log_close(refnx_r, refl1d_r, atol=atol) + + +@pytest.mark.fast +def test_linear_spline_consistent_across_engines(): + """LinearSpline gives consistent results across engines. + + This one does earn its keep: pre-fix, refl1d read the FWHM knots as sigma + and over-smeared by 2.355x. Measured max |dlog10(R)|: 0.085 with the fix, + 0.182 without it, so atol=0.13 separates them with ~1.5x headroom either + way. + """ + q_knots = np.linspace(0.001, 0.5, 10) + fwhm_knots = 0.02 * q_knots + 0.001 + + refnx_r, refl1d_r = _both_engines(LinearSpline(q_knots, fwhm_knots)) + _assert_log_close(refnx_r, refl1d_r, atol=0.13) + + +@pytest.mark.fast +def test_pointwise_consistent_across_engines(): + """Pointwise (sigma from sQz) is consistent across engines. + + Consistency check only. Pre-fix, refnx under-smeared these widths by + 2.355x, but measured max |dlog10(R)| is 0.092 pre-fix versus 0.085 with + the fix -- indistinguishable, because under-smearing an already-small + width barely moves the curve. Do not add a tolerance here expecting it to + catch that bug; ``tests/calculators/test_resolution_conventions.py`` is + what actually pins it. + + The sQz values mirror the LinearSpline knots, so the applied smearing -- + and hence the measured agreement -- matches that test. + """ + qz = np.linspace(0.001, 0.5, 50) + r = np.ones_like(qz) # only kept for serialization round-trips + sigma = (0.02 * qz + 0.001) / SIGMA_TO_FWHM + sqz = sigma**2 + + refnx_r, refl1d_r = _both_engines(Pointwise([qz, r, sqz])) + _assert_log_close(refnx_r, refl1d_r, atol=0.13) diff --git a/tests/model/test_model.py b/tests/model/test_model.py index b11149c1..a2dd46ea 100644 --- a/tests/model/test_model.py +++ b/tests/model/test_model.py @@ -45,8 +45,9 @@ def test_default(self): assert_equal(p.background.min, 0.0) assert_equal(p.background.max, np.inf) assert_equal(p.background.fixed, True) - assert p._resolution_function.smearing([1]) == 5.0 - assert p._resolution_function.smearing([100]) == 5.0 + sigma_to_fwhm = 2.0 * np.sqrt(2.0 * np.log(2.0)) + assert np.allclose(p._resolution_function.smearing([1]), 5.0 / 100.0 * 1.0 / sigma_to_fwhm) + assert np.allclose(p._resolution_function.smearing([100]), 5.0 / 100.0 * 100.0 / sigma_to_fwhm) def test_from_pars(self): m1 = Material(6.908, -0.278, 'Boron') @@ -81,8 +82,9 @@ def test_from_pars(self): assert_equal(mod.background.min, 0.0) assert_equal(mod.background.max, np.inf) assert_equal(mod.background.fixed, True) - assert mod._resolution_function.smearing([1]) == 2.0 - assert mod._resolution_function.smearing([100]) == 2.0 + sigma_to_fwhm = 2.0 * np.sqrt(2.0 * np.log(2.0)) + assert np.allclose(mod._resolution_function.smearing([1]), 2.0 / 100.0 * 1.0 / sigma_to_fwhm) + assert np.allclose(mod._resolution_function.smearing([100]), 2.0 / 100.0 * 100.0 / sigma_to_fwhm) def test_add_assemblies(self): m1 = Material(6.908, -0.278, 'Boron') @@ -525,7 +527,8 @@ def test_round_trip_preserves_resolution_function(self): d = model.as_dict() global_object.map._clear() restored = Model.from_dict(d) - assert restored._resolution_function.smearing(100) == 3.0 + sigma_to_fwhm = 2.0 * np.sqrt(2.0 * np.log(2.0)) + assert np.allclose(restored._resolution_function.smearing(100), 3.0 / 100.0 * 100.0 / sigma_to_fwhm) def test_round_trip_preserves_interface(self): global_object.map._clear() diff --git a/tests/model/test_resolution_functions.py b/tests/model/test_resolution_functions.py index b8a4ea18..480ca2c6 100644 --- a/tests/model/test_resolution_functions.py +++ b/tests/model/test_resolution_functions.py @@ -6,6 +6,7 @@ import numpy as np from easyreflectometry.model.resolution_functions import DEFAULT_RESOLUTION_FWHM_PERCENTAGE +from easyreflectometry.model.resolution_functions import SIGMA_TO_FWHM from easyreflectometry.model.resolution_functions import LinearSpline from easyreflectometry.model.resolution_functions import PercentageFwhm from easyreflectometry.model.resolution_functions import Pointwise @@ -17,21 +18,24 @@ def test_constructor(self): # When resolution_function = PercentageFwhm(1.0) - # Then Expect - assert np.all(resolution_function.smearing([0, 2.5]) == np.array([1.0, 1.0])) - assert resolution_function.smearing([-100]) == np.array([1.0]) - assert resolution_function.smearing([100]) == np.array([1.0]) + # Then Expect: smearing() returns sigma = (constant / 100) * q / SIGMA_TO_FWHM + # Negative q is not asserted: sigma scales with q here, so q < 0 yields a + # negative width, which is meaningless. Leaving it unpinned keeps the door + # open to guarding with abs(q) without failing this test. + expected = (1.0 / 100.0) * np.array([0.0, 2.5]) / SIGMA_TO_FWHM + assert np.allclose(resolution_function.smearing([0, 2.5]), expected) + assert np.allclose(resolution_function.smearing([100]), (1.0 / 100.0) * 100.0 / SIGMA_TO_FWHM) def test_constructor_none(self): # When resolution_function = PercentageFwhm() - # Then Expect - assert np.all( - resolution_function.smearing([0, 2.5]) == [DEFAULT_RESOLUTION_FWHM_PERCENTAGE, DEFAULT_RESOLUTION_FWHM_PERCENTAGE] - ) - assert resolution_function.smearing([-100]) == DEFAULT_RESOLUTION_FWHM_PERCENTAGE - assert resolution_function.smearing([100]) == DEFAULT_RESOLUTION_FWHM_PERCENTAGE + # Then Expect: defaults to DEFAULT_RESOLUTION_FWHM_PERCENTAGE, returned as sigma + # Negative q is not asserted -- see test_constructor. + c = DEFAULT_RESOLUTION_FWHM_PERCENTAGE + expected = (c / 100.0) * np.array([0.0, 2.5]) / SIGMA_TO_FWHM + assert np.allclose(resolution_function.smearing([0, 2.5]), expected) + assert np.allclose(resolution_function.smearing([100]), (c / 100.0) * 100.0 / SIGMA_TO_FWHM) def test_as_dict(self): # When @@ -57,10 +61,12 @@ def test_constructor(self): # When resolution_function = LinearSpline(q_data_points=[0, 10], fwhm_values=[5, 10]) - # Then Expect - assert np.all(resolution_function.smearing([0, 2.5]) == np.array([5, 6.25])) - assert resolution_function.smearing([-100]) == np.array([5.0]) - assert resolution_function.smearing([100]) == np.array([10.0]) + # Then Expect: smearing() returns sigma (FWHM knots converted to sigma) + # Unlike PercentageFwhm, q outside the knot range is meaningful here: + # np.interp clamps to the end knots, so the width stays positive. + assert np.allclose(resolution_function.smearing([0, 2.5]), np.array([5, 6.25]) / SIGMA_TO_FWHM) + assert np.allclose(resolution_function.smearing([-100]), np.array([5.0]) / SIGMA_TO_FWHM) + assert np.allclose(resolution_function.smearing([100]), np.array([10.0]) / SIGMA_TO_FWHM) def test_as_dict(self): # When From 3480597849d0f3d8f6c19cd86920927c6831389f Mon Sep 17 00:00:00 2001 From: rozyczko Date: Thu, 16 Jul 2026 15:24:55 +0200 Subject: [PATCH 4/5] updated notebook --- CONTRIBUTING.md | 3 +- .../advancedfitting/bayesian_bumps.ipynb | 557 ++++++++++-------- docs/mkdocs.yml | 3 +- pixi.lock | 24 +- src/easyreflectometry/fitting.py | 21 +- 5 files changed, 335 insertions(+), 273 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a6ceeebd..a4ac1fbc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -42,7 +42,8 @@ Please make sure you follow the EasyScience organization-wide If you are not planning to contribute code, you may want to: - 🐞 Report a bug β€” see [Reporting Issues](#11-reporting-issues) -- πŸ›‘ Report a security issue β€” see [Security Issues](#12-security-issues) +- πŸ›‘ Report a security issue β€” see + [Security Issues](#12-security-issues) - πŸ’¬ Ask a question or start a discussion at [Project Discussions](https://github.com/easyscience/reflectometry-lib/discussions) diff --git a/docs/docs/tutorials/advancedfitting/bayesian_bumps.ipynb b/docs/docs/tutorials/advancedfitting/bayesian_bumps.ipynb index 1c7e1bf6..dcaaa025 100644 --- a/docs/docs/tutorials/advancedfitting/bayesian_bumps.ipynb +++ b/docs/docs/tutorials/advancedfitting/bayesian_bumps.ipynb @@ -16,11 +16,18 @@ "- Classical optimisation first (good starting point for Bayesian sampling).\n", "- High-level DREAM MCMC sampling via\n", " ``MultiFitter.mcmc_sample()`` and ``PosteriorResults``.\n", + "- Checking convergence with the Gelman-Rubin R-hat diagnostic, then\n", + " **extending an under-converged chain** with ``fitter.sampler.extend()`` and\n", + " comparing the posterior before and after.\n", "- Posterior inspection: summary table, marginal distributions, corner plot,\n", - " trace plot, credible intervals, Gelman-Rubin R-hat.\n", + " trace plot, credible intervals.\n", "- Posterior-predictive checks: reflectivity and SLD profile with 95 %\n", " credible bands.\n", "\n", + "The sampling run below is deliberately started short so that the diagnostic\n", + "flags it as not converged β€” this gives us something real to fix when we extend\n", + "the chain, rather than a cosmetic demonstration.\n", + "\n", "All posterior plots are rendered as **interactive Plotly figures**, the\n", "same ones shown by the EasyReflectometryApp Bayesian Posterior tab.\n", "\n", @@ -30,25 +37,10 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "id": "61aa83ac", - "metadata": { - "execution": { - "iopub.execute_input": "2026-05-29T06:29:06.282846Z", - "iopub.status.busy": "2026-05-29T06:29:06.282846Z", - "iopub.status.idle": "2026-05-29T06:29:09.516170Z", - "shell.execute_reply": "2026-05-29T06:29:09.516170Z" - } - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "All libraries imported successfully.\n" - ] - } - ], + "metadata": {}, + "outputs": [], "source": [ "import warnings\n", "\n", @@ -79,35 +71,10 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "id": "14986b98", - "metadata": { - "execution": { - "iopub.execute_input": "2026-05-29T06:29:09.517778Z", - "iopub.status.busy": "2026-05-29T06:29:09.517778Z", - "iopub.status.idle": "2026-05-29T06:29:09.958508Z", - "shell.execute_reply": "2026-05-29T06:29:09.958508Z" - } - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Data loaded with keys: ['data', 'coords', 'attrs']\n" - ] - }, - { - "data": { - "image/png": "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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "metadata": {}, + "outputs": [], "source": [ "# ---- Load experimental data -------------------------------------------------\n", "# Fetch the .ort test data from the easyscience/reflectometry data repository.\n", @@ -134,29 +101,10 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": null, "id": "cd56a6eb", - "metadata": { - "execution": { - "iopub.execute_input": "2026-05-29T06:29:09.960548Z", - "iopub.status.busy": "2026-05-29T06:29:09.960548Z", - "iopub.status.idle": "2026-05-29T06:29:09.973595Z", - "shell.execute_reply": "2026-05-29T06:29:09.973595Z" - } - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Model created with the following free parameters:\n", - " background: value=1e-06, bounds=(1e-07, 1e-05)\n", - " sld: value=2.0, bounds=(0.5, 4.0)\n", - " thickness: value=250.0, bounds=(100, 400)\n", - " scale: value=1.0, bounds=(0.8, 1.2)\n" - ] - } - ], + "metadata": {}, + "outputs": [], "source": [ "# ---- Create a monolayer model (Si / Film / Dβ‚‚O) ----------------------------\n", "si = Material(sld=2.07, isld=0.0, name='Si')\n", @@ -204,25 +152,10 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": null, "id": "991e1169", - "metadata": { - "execution": { - "iopub.execute_input": "2026-05-29T06:29:09.974614Z", - "iopub.status.busy": "2026-05-29T06:29:09.974614Z", - "iopub.status.idle": "2026-05-29T06:29:09.982333Z", - "shell.execute_reply": "2026-05-29T06:29:09.982333Z" - } - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Fitter ready with minimizer: Bumps\n" - ] - } - ], + "metadata": {}, + "outputs": [], "source": [ "# ---- Set up the calculator and fitter ---------------------------------------\n", "interface = CalculatorFactory()\n", @@ -238,36 +171,10 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": null, "id": "eb0f989e", - "metadata": { - "execution": { - "iopub.execute_input": "2026-05-29T06:29:09.983788Z", - "iopub.status.busy": "2026-05-29T06:29:09.983788Z", - "iopub.status.idle": "2026-05-29T06:29:11.760084Z", - "shell.execute_reply": "2026-05-29T06:29:11.760084Z" - } - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Classical fit successful: True\n", - "Reduced χ² β‰ˆ 73.7860402885366\n" - ] - }, - { - "data": { - "image/png": "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b5OaY0/QrZ636WfIc0cYBr5EREQ6EoyaY+/niDMZO962jcLLaATiEt0b3IuSBIVkxWWTA+oiHJcaIiIioi7zlD+MyE9VSxzLam9yKpneSF/mlvSHGV8iIiKKirZtRO1h4EtERERR0baNqD0sdSAiIiIiXWDgS0RERES6wFIHIiIi0i2XS2tVm0zRK+oD3/3792POnDk+l1955RXMnj07rOMiIiKi8Np6rKJ5MQ67w73YhiztPPeyi1CQEO7RUShEfeA7dOhQ7NixQ52vqalB//79MWPGjHAPi4iIiMIc9Er/4UpbI7It0n84XvUf3nO6Ck+s24d5k3IwPTs73MOkINNVje9bb72FadOmISkpKdxDISIiojCWN0imV4Le/n0SkRRvQozRoE4LMxJRXdeIV/9Zpu5H0SXsge8nn3yCG264Afn5+TAYDHjjjTda3eeZZ55RmVqz2YyJEydi8+bNXfpZa9as8Sl7ICIiIv2QQHZfaTVe23YSu09XIcsSr2IPb3I50xKHoxX1OFBWE7axUpSWOtTW1mLMmDG49957cfPNN7e6ffXq1ViwYAGef/55FfQuW7YMM2fOVLW62U1fQYwdOxYOh6PVYz/44AMVUIvq6mps3LgRq1at6oG9IiIiokit562y2XG2xo6a+kYUZCQhLSHW576y7LLdoanML0WXsAe+s2bNUlsgTz/9NO6//37cc8896rIEwGvXrsWf//xnPPLII+o6Tw1vW958801cc801KmvcloaGBrV5SMAsXC6X2rpKHqtpWreeg3oHzrV+cK71hfPde209dl7V7VbVNSLLEoeEODPO2+yornPg4BkrhmQnq44OHnV2J2JjDLDEx3C+e4mOzlPYA9+22O12bN26FT/72c+arzMajZg+fTo2bdrU6TKHBx54oN37LV68GI8++mir68+ePYv6+np0Z0KqqqrUH03ZB4penGv94FzrC+e7d3JpGl74+AgqaurQNzUeBrgQY9CQGBeD2gan6uZw7FwthsZIGweDmt8zVQ0YkGZCqsGGsrILyTCKXFartfcHvuXl5XA6ncjJyfG5Xi7v27evw88jf6ikLvi1115r974SZEtphXfGt6CgAFlZWUhJSenWH0ypG5Ln4R/M6Ma51g/Otb5wvnunfaVWnLI6kJuWCHP8hbCnsI8RB6WNmdMFm92JBqcRMTEGlNfYkZFs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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "metadata": {}, + "outputs": [], "source": [ "# ---- Classical fit first ----------------------------------------------------\n", "# A classical optimisation gives a good starting point and a sanity check\n", @@ -296,61 +203,44 @@ "cell_type": "code", "execution_count": null, "id": "20c9b0e0", - "metadata": { - "execution": { - "iopub.execute_input": "2026-05-29T06:29:11.762093Z", - "iopub.status.busy": "2026-05-29T06:29:11.762093Z", - "iopub.status.idle": "2026-05-29T06:30:41.903328Z", - "shell.execute_reply": "2026-05-29T06:30:41.903328Z" - } - }, - "outputs": [ - { - "ename": "TypeError", - "evalue": "Fitter.mcmc_sample() got an unexpected keyword argument 'chains'", - "output_type": "error", - "traceback": [ - "\u001b[31m---------------------------------------------------------------------------\u001b[39m", - "\u001b[31mTypeError\u001b[39m Traceback (most recent call last)", - "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[11]\u001b[39m\u001b[32m, line 10\u001b[39m\n\u001b[32m 1\u001b[39m \u001b[38;5;66;03m# ---- Bayesian MCMC sampling -------------------------------------------------\u001b[39;00m\n\u001b[32m 2\u001b[39m \u001b[38;5;66;03m# ``MultiFitter.mcmc_sample()`` delegates to the BUMPS DREAM sampler.\u001b[39;00m\n\u001b[32m 3\u001b[39m \u001b[38;5;66;03m# All keyword arguments are forwarded with user-friendly names:\u001b[39;00m\n\u001b[32m (...)\u001b[39m\u001b[32m 7\u001b[39m \u001b[38;5;66;03m# ``chains`` ← DREAM population count (alias for ``pop``)\u001b[39;00m\n\u001b[32m 8\u001b[39m \u001b[38;5;66;03m# ``population``← BUMPS‑native ``pop`` for advanced users\u001b[39;00m\n\u001b[32m---> \u001b[39m\u001b[32m10\u001b[39m posterior_dict = \u001b[43mfitter\u001b[49m\u001b[43m.\u001b[49m\u001b[43mmcmc_sample\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 11\u001b[39m \u001b[43m \u001b[49m\u001b[43mdata\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 12\u001b[39m \u001b[43m \u001b[49m\u001b[43msamples\u001b[49m\u001b[43m=\u001b[49m\u001b[32;43m2000\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;66;43;03m# Short for demo; use 20 k+ in production\u001b[39;49;00m\n\u001b[32m 13\u001b[39m \u001b[43m \u001b[49m\u001b[43mburn\u001b[49m\u001b[43m=\u001b[49m\u001b[32;43m500\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[32m 14\u001b[39m \u001b[43m \u001b[49m\u001b[43mthin\u001b[49m\u001b[43m=\u001b[49m\u001b[32;43m10\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[32m 15\u001b[39m \u001b[43m)\u001b[49m\n\u001b[32m 17\u001b[39m \u001b[38;5;28mprint\u001b[39m(\u001b[33m'\u001b[39m\u001b[33mDREAM sampling complete.\u001b[39m\u001b[33m'\u001b[39m)\n\u001b[32m 18\u001b[39m \u001b[38;5;28mprint\u001b[39m(\u001b[33mf\u001b[39m\u001b[33m'\u001b[39m\u001b[33m Posterior shape : \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mposterior_dict[\u001b[33m\"\u001b[39m\u001b[33mdraws\u001b[39m\u001b[33m\"\u001b[39m].shape\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m'\u001b[39m)\n", - "\u001b[36mFile \u001b[39m\u001b[32m~\\projects\\easy\\ERA\\reflectometry-lib\\src\\easyreflectometry\\fitting.py:451\u001b[39m, in \u001b[36mMultiFitter.mcmc_sample\u001b[39m\u001b[34m(self, data, samples, burn, thin, chains, population, objective, initializer, resume_state, progress_callback, abort_test)\u001b[39m\n\u001b[32m 449\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m initializer \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[32m 450\u001b[39m sampler_kwargs[\u001b[33m'\u001b[39m\u001b[33minit\u001b[39m\u001b[33m'\u001b[39m] = initializer\n\u001b[32m--> \u001b[39m\u001b[32m451\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43measy_science_multi_fitter\u001b[49m\u001b[43m.\u001b[49m\u001b[43mmcmc_sample\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 452\u001b[39m \u001b[43m \u001b[49m\u001b[43mx\u001b[49m\u001b[43m=\u001b[49m\u001b[43mx\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 453\u001b[39m \u001b[43m \u001b[49m\u001b[43my\u001b[49m\u001b[43m=\u001b[49m\u001b[43my\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 454\u001b[39m \u001b[43m \u001b[49m\u001b[43mweights\u001b[49m\u001b[43m=\u001b[49m\u001b[43mdy\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 455\u001b[39m \u001b[43m \u001b[49m\u001b[43msamples\u001b[49m\u001b[43m=\u001b[49m\u001b[43msamples\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 456\u001b[39m \u001b[43m \u001b[49m\u001b[43mburn\u001b[49m\u001b[43m=\u001b[49m\u001b[43mburn\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 457\u001b[39m \u001b[43m \u001b[49m\u001b[43mthin\u001b[49m\u001b[43m=\u001b[49m\u001b[43mthin\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 458\u001b[39m \u001b[43m \u001b[49m\u001b[43mchains\u001b[49m\u001b[43m=\u001b[49m\u001b[43mchains\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 459\u001b[39m \u001b[43m \u001b[49m\u001b[43mpopulation\u001b[49m\u001b[43m=\u001b[49m\u001b[43mpopulation\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 460\u001b[39m \u001b[43m \u001b[49m\u001b[43mresume_state\u001b[49m\u001b[43m=\u001b[49m\u001b[43mresume_state\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 461\u001b[39m \u001b[43m \u001b[49m\u001b[43msampler_kwargs\u001b[49m\u001b[43m=\u001b[49m\u001b[43msampler_kwargs\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;129;43;01mor\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[38;5;28;43;01mNone\u001b[39;49;00m\u001b[43m,\u001b[49m\n\u001b[32m 462\u001b[39m \u001b[43m \u001b[49m\u001b[43mprogress_callback\u001b[49m\u001b[43m=\u001b[49m\u001b[43mprogress_callback\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 463\u001b[39m \u001b[43m \u001b[49m\u001b[43mabort_test\u001b[49m\u001b[43m=\u001b[49m\u001b[43mabort_test\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 464\u001b[39m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n", - "\u001b[31mTypeError\u001b[39m: Fitter.mcmc_sample() got an unexpected keyword argument 'chains'" - ] - } - ], + "metadata": {}, + "outputs": [], "source": [ "# ---- Bayesian MCMC sampling -------------------------------------------------\n", "# ``MultiFitter.mcmc_sample()`` delegates to the BUMPS DREAM sampler.\n", "# All keyword arguments are forwarded with user-friendly names:\n", "# ``samples`` ← total retained samples\n", - "# ``burn`` ← burn‑in steps\n", + "# ``burn`` ← burn-in steps\n", "# ``thin`` ← thinning interval\n", - "# ``population``← BUMPS‑native ``pop`` for advanced users\n", + "# ``population``← BUMPS-native ``pop`` for advanced users\n", + "#\n", + "# We deliberately start with a *very* short run. It finishes in seconds but is\n", + "# far too short to trust β€” which is exactly the situation the \"extend the chain\"\n", + "# section below exists to fix. In production you would ask for 20 k+ samples.\n", "\n", "posterior_dict = fitter.mcmc_sample(\n", " data,\n", - " samples=2000, # Short for demo; use 20 k+ in production\n", - " burn=500,\n", + " samples=500, # Deliberately too short β€” extended later in this notebook\n", + " burn=100,\n", " thin=10,\n", ")\n", "\n", "print('DREAM sampling complete.')\n", "print(f' Posterior shape : {posterior_dict[\"draws\"].shape}')\n", - "print(f' Parameters : {posterior_dict[\"param_names\"]}')" + "print(f' Parameters : {posterior_dict[\"param_names\"]}')\n", + "print()\n", + "print(\n", + " 'Note: the retained draws are fewer than samples/thin. BUMPS trims the\\n'\n", + " 'chain at a detected burn point and drops outlier chains, so the number of\\n'\n", + " 'rows is not predictable from the arguments β€” read it off the array.'\n", + ")" ] }, { "cell_type": "code", "execution_count": null, "id": "f23934a0", - "metadata": { - "execution": { - "iopub.execute_input": "2026-05-29T06:30:41.905335Z", - "iopub.status.busy": "2026-05-29T06:30:41.905335Z", - "iopub.status.idle": "2026-05-29T06:30:41.908528Z", - "shell.execute_reply": "2026-05-29T06:30:41.908528Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "# ---- Wrap in PosteriorResults -----------------------------------------------\n", @@ -372,14 +262,7 @@ "cell_type": "code", "execution_count": null, "id": "ef300cb1", - "metadata": { - "execution": { - "iopub.execute_input": "2026-05-29T06:30:41.909855Z", - "iopub.status.busy": "2026-05-29T06:30:41.909855Z", - "iopub.status.idle": "2026-05-29T06:30:41.913882Z", - "shell.execute_reply": "2026-05-29T06:30:41.913882Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "# ---- Posterior summary table ------------------------------------------------\n", @@ -388,6 +271,248 @@ "print(posterior.summary())" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "34e3cc3e", + "metadata": {}, + "outputs": [], + "source": [ + "# ---- Gelman-Rubin R-hat convergence diagnostic ------------------------------\n", + "# R-hat compares the variance *between* DREAM's parallel chains with the\n", + "# variance *within* each one. Values close to 1.0 mean the chains have mixed\n", + "# and agree on the same posterior; the usual rule of thumb is RΜ‚ < 1.1.\n", + "#\n", + "# ``posterior.gelman_rubin()`` needs draws pre-split into chains, shape\n", + "# ``(n_chains, n_draws, n_params)``. ``mcmc_sample()`` returns the draws\n", + "# already pooled into a flat ``(n_samples, n_params)`` array, so we ask the\n", + "# BUMPS state itself β€” it still knows the individual chains.\n", + "\n", + "\n", + "def rhat_table(state, param_names):\n", + " \"\"\"R-hat per parameter, read from the chain-aware BUMPS state.\"\"\"\n", + " return dict(zip(param_names, state.gelman()))\n", + "\n", + "\n", + "rhat_short = rhat_table(posterior.sampler_state, posterior.param_names)\n", + "\n", + "print('Gelman-Rubin R-hat after the short run:')\n", + "for name, r in rhat_short.items():\n", + " flag = ' βœ“' if r < 1.1 else ' ⚠ not converged'\n", + " print(f' {name:<30s} RΜ‚ = {r:.3f}{flag}')\n", + "\n", + "# DREAM is stochastic and unseeded, so the exact values differ run to run.\n", + "n_bad = sum(r >= 1.1 for r in rhat_short.values())\n", + "if n_bad:\n", + " print(f'\\n{n_bad} parameter(s) above 1.1 β€” this chain is not converged and needs more samples.')\n", + "else:\n", + " print(\n", + " f'\\nRΜ‚ passes, but this chain holds only {posterior.draws.shape[0]} draws β€” '\n", + " 'far too few to rely on,\\nso every posterior estimate from it is coarse. '\n", + " 'Either way, the answer is more samples.'\n", + " )" + ] + }, + { + "cell_type": "markdown", + "id": "223f79f1", + "metadata": {}, + "source": [ + "## Extending the chain\n", + "\n", + "The short run above is not something to draw conclusions from β€” it usually\n", + "fails the RΜ‚ < 1.1 check outright, and even when it scrapes past it, it holds\n", + "only a few dozen draws. The fix is more samples, but restarting from scratch\n", + "would throw away the work already done and pay the burn-in cost a second time.\n", + "\n", + "Instead, **continue the existing chain**. `MultiFitter.mcmc_sample()` keeps the\n", + "underlying `Sampler` on `fitter.sampler`, and `Sampler.extend()` picks the chain\n", + "up exactly where DREAM left off:\n", + "\n", + "```python\n", + "extended = fitter.sampler.extend(additional_samples=8000, thin=10)\n", + "```\n", + "\n", + "`extend()` runs with `burn=0` β€” re-burning an already-converged chain would be a\n", + "mistake, and BUMPS forces it to 0 on resume anyway. It also grows DREAM's\n", + "fixed-size ring buffer by exactly `additional_samples`, so none of the existing\n", + "draws are evicted.\n", + "\n", + "### What \"improvement\" does and does not mean here\n", + "\n", + "Two things get better, and one thing deliberately does not:\n", + "\n", + "* **RΜ‚ drops** β€” the chains mix and settle onto the same posterior.\n", + "* **The Monte-Carlo error shrinks** β€” with more draws the posterior *estimate*\n", + " (mean, credible interval, histogram shape) stops moving from run to run.\n", + "* **The posterior width does _not_ shrink.** The spread of a parameter is set by\n", + " the data and the model, not by how long you sample. If the credible interval\n", + " collapsed as we added samples, that would be a bug, not a win. What improves is\n", + " how precisely we know that interval.\n", + "\n", + "DREAM is stochastic and this notebook does not seed it, so the exact numbers\n", + "below change on every run. The direction of travel does not." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a5d26166", + "metadata": {}, + "outputs": [], + "source": [ + "# ---- Continue the existing chain --------------------------------------------\n", + "# ``extend()`` mutates the sampler's BUMPS state in place, so snapshot anything\n", + "# we want to compare against first. ``rhat_short`` (plain floats) was already\n", + "# captured above; the draws need an explicit copy.\n", + "\n", + "draws_short = np.array(posterior.draws, copy=True)\n", + "n_short = draws_short.shape[0]\n", + "\n", + "extended_results = fitter.sampler.extend(\n", + " additional_samples=8000, # Added on top of the original 2000\n", + " thin=10,\n", + ")\n", + "\n", + "posterior_extended = PosteriorResults(\n", + " draws=extended_results.draws,\n", + " param_names=extended_results.param_names,\n", + " logp=extended_results.logp,\n", + " sampler_state=extended_results.state,\n", + ")\n", + "\n", + "print('Chain extended.')\n", + "print(f' Retained draws before : {n_short}')\n", + "print(f' Retained draws after : {posterior_extended.draws.shape[0]}')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f6b17af8", + "metadata": {}, + "outputs": [], + "source": [ + "# ---- Did convergence actually improve? --------------------------------------\n", + "# The headline check: R-hat before vs after.\n", + "\n", + "rhat_extended = rhat_table(posterior_extended.sampler_state, posterior_extended.param_names)\n", + "\n", + "print(f'{\"parameter\":<32s} {\"short\":>8s} {\"extended\":>10s}')\n", + "print('-' * 54)\n", + "for name in posterior_extended.param_names:\n", + " before, after = rhat_short[name], rhat_extended[name]\n", + " flag = ' βœ“' if after < 1.1 else ' ⚠'\n", + " print(f'{name:<32s} {before:8.3f} {after:10.3f}{flag}')\n", + "\n", + "n_bad_before = sum(r >= 1.1 for r in rhat_short.values())\n", + "n_bad_after = sum(r >= 1.1 for r in rhat_extended.values())\n", + "print(f'\\nParameters failing RΜ‚ < 1.1: {n_bad_before} β†’ {n_bad_after}')\n", + "print(f'Worst RΜ‚ across parameters: {max(rhat_short.values()):.3f} β†’ {max(rhat_extended.values()):.3f}')\n", + "\n", + "if n_bad_after == 0 and n_bad_before > 0:\n", + " print('\\nThe extended chain has converged; the short one had not.')\n", + "elif n_bad_after == 0:\n", + " print('\\nBoth chains pass RΜ‚, but the extended one does so with far more draws behind it.')\n", + "else:\n", + " print('\\nStill above threshold β€” extend again, or revisit the model and its bounds.')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f620d42d", + "metadata": {}, + "outputs": [], + "source": [ + "# ---- Side-by-side posterior statistics --------------------------------------\n", + "# ``extend()`` returns the parameters in the same order as the original run, so\n", + "# the columns line up β€” but map by name rather than trusting the index.\n", + "\n", + "col_short = {name: i for i, name in enumerate(posterior.param_names)}\n", + "col_ext = {name: i for i, name in enumerate(posterior_extended.param_names)}\n", + "\n", + "\n", + "def ci_width(column, alpha=0.95):\n", + " lo, hi = np.percentile(column, [100 * (1 - alpha) / 2, 100 * (1 + alpha) / 2])\n", + " return hi - lo\n", + "\n", + "\n", + "header = (\n", + " f'{\"parameter\":<32s} {\"mean (short)\":>13s} {\"mean (ext)\":>13s} {\"shift\":>8s} {\"95% w (short)\":>14s} {\"95% w (ext)\":>13s}'\n", + ")\n", + "print(header)\n", + "print('-' * len(header))\n", + "\n", + "for name in posterior_extended.param_names:\n", + " a = draws_short[:, col_short[name]]\n", + " b = posterior_extended.draws[:, col_ext[name]]\n", + " # Mean shift expressed in extended-posterior standard deviations: how far the\n", + " # short run's answer sits from the better-resolved one, in units that matter.\n", + " shift = abs(np.mean(a) - np.mean(b)) / np.std(b)\n", + " print(f'{name:<32s} {np.mean(a):13.5g} {np.mean(b):13.5g} {shift:7.2f}Οƒ {ci_width(a):14.4g} {ci_width(b):13.4g}')\n", + "\n", + "print(\n", + " '\\nRead this alongside the RΜ‚ table, not on its own. The credible intervals do\\n'\n", + " 'not systematically shrink β€” they should not, since posterior width is set by\\n'\n", + " 'the data, not by sampling effort. What extending buys is trust: the means\\n'\n", + " 'stop drifting and the marginals below are resolved by many more draws.'\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "67c37350", + "metadata": {}, + "outputs": [], + "source": [ + "# ---- Visual overlay: marginals, short vs extended ----------------------------\n", + "# Same posterior, better resolved. The short run's histogram is ragged because\n", + "# it is built from few draws; the extended one traces a smooth marginal over the\n", + "# same range. Densities are normalised so the two are comparable despite the\n", + "# very different sample counts.\n", + "\n", + "n_params = len(posterior_extended.param_names)\n", + "fig, axes = plt.subplots(1, n_params, figsize=(4 * n_params, 3.2))\n", + "axes = np.atleast_1d(axes)\n", + "\n", + "for ax, name in zip(axes, posterior_extended.param_names):\n", + " a = draws_short[:, col_short[name]]\n", + " b = posterior_extended.draws[:, col_ext[name]]\n", + " bins = np.histogram_bin_edges(np.concatenate([a, b]), bins=30)\n", + " ax.hist(a, bins=bins, density=True, alpha=0.55, label=f'short (n={a.shape[0]})')\n", + " ax.hist(b, bins=bins, density=True, alpha=0.55, label=f'extended (n={b.shape[0]})')\n", + " ax.set_title(name, fontsize=9)\n", + " ax.set_ylabel('density')\n", + " ax.legend(fontsize=8)\n", + " ax.grid(True, alpha=0.3)\n", + "\n", + "fig.suptitle('Marginal posterior: original chain vs extended chain')\n", + "fig.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e46c3dd4", + "metadata": {}, + "outputs": [], + "source": [ + "# ---- Adopt the extended chain for the rest of the notebook -------------------\n", + "# Everything below (corner plots, credible intervals, posterior-predictive\n", + "# checks) now runs on the converged chain. ``posterior_short`` is kept around\n", + "# in case you want to re-run the comparisons above.\n", + "\n", + "posterior_short = posterior\n", + "posterior = posterior_extended\n", + "\n", + "print(posterior)\n", + "print()\n", + "print(posterior.summary())" + ] + }, { "cell_type": "markdown", "id": "d1f0a300", @@ -413,14 +538,7 @@ "cell_type": "code", "execution_count": null, "id": "5b9d3c2a", - "metadata": { - "execution": { - "iopub.execute_input": "2026-05-29T06:30:41.914887Z", - "iopub.status.busy": "2026-05-29T06:30:41.914887Z", - "iopub.status.idle": "2026-05-29T06:30:43.463799Z", - "shell.execute_reply": "2026-05-29T06:30:43.463252Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "# ---- Marginal posterior distributions ---------------------------------------\n", @@ -448,14 +566,7 @@ "cell_type": "code", "execution_count": null, "id": "e759a2a0", - "metadata": { - "execution": { - "iopub.execute_input": "2026-05-29T06:30:43.464801Z", - "iopub.status.busy": "2026-05-29T06:30:43.464801Z", - "iopub.status.idle": "2026-05-29T06:30:43.847435Z", - "shell.execute_reply": "2026-05-29T06:30:43.847435Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "# ---- Corner plot ------------------------------------------------------------\n", @@ -470,14 +581,7 @@ "cell_type": "code", "execution_count": null, "id": "593f5ed3", - "metadata": { - "execution": { - "iopub.execute_input": "2026-05-29T06:30:43.852778Z", - "iopub.status.busy": "2026-05-29T06:30:43.852778Z", - "iopub.status.idle": "2026-05-29T06:30:43.905089Z", - "shell.execute_reply": "2026-05-29T06:30:43.905089Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "# ---- Trace plot -------------------------------------------------------------\n", @@ -492,14 +596,7 @@ "cell_type": "code", "execution_count": null, "id": "3049e68a", - "metadata": { - "execution": { - "iopub.execute_input": "2026-05-29T06:30:43.906601Z", - "iopub.status.busy": "2026-05-29T06:30:43.906601Z", - "iopub.status.idle": "2026-05-29T06:30:43.912231Z", - "shell.execute_reply": "2026-05-29T06:30:43.911707Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "# ---- Credible intervals -----------------------------------------------------\n", @@ -524,14 +621,7 @@ "cell_type": "code", "execution_count": null, "id": "806bf47a", - "metadata": { - "execution": { - "iopub.execute_input": "2026-05-29T06:30:43.913735Z", - "iopub.status.busy": "2026-05-29T06:30:43.913735Z", - "iopub.status.idle": "2026-05-29T06:30:44.004052Z", - "shell.execute_reply": "2026-05-29T06:30:44.004052Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "# ---- Pairwise focus: thickness vs SLD ---------------------------------------\n", @@ -548,49 +638,11 @@ "fig.show()" ] }, - { - "cell_type": "code", - "execution_count": null, - "id": "34e3cc3e", - "metadata": { - "execution": { - "iopub.execute_input": "2026-05-29T06:30:44.006603Z", - "iopub.status.busy": "2026-05-29T06:30:44.006603Z", - "iopub.status.idle": "2026-05-29T06:30:44.010758Z", - "shell.execute_reply": "2026-05-29T06:30:44.010226Z" - } - }, - "outputs": [], - "source": [ - "# ---- Gelman-Rubin R‑hat convergence diagnostic ------------------------------\n", - "# Requires ``arviz`` and at least 2 chains. Values close to 1.0 indicate\n", - "# good convergence.\n", - "\n", - "try:\n", - " rhat = posterior.gelman_rubin()\n", - " print('Gelman-Rubin R‑hat:')\n", - " for name, r in rhat.items():\n", - " flag = ' βœ“' if r < 1.1 else ' ⚠'\n", - " print(f' {name:<30s} RΜ‚ = {r:.3f}{flag}')\n", - "except ValueError:\n", - " print(\n", - " 'Skipped: Gelman-Rubin R‑hat requires at least 2 chains. '\n", - " 'Run with multiple DREAM populations to obtain multi-chain draws.'\n", - " )" - ] - }, { "cell_type": "code", "execution_count": null, "id": "c7e5816e", - "metadata": { - "execution": { - "iopub.execute_input": "2026-05-29T06:30:44.012760Z", - "iopub.status.busy": "2026-05-29T06:30:44.012760Z", - "iopub.status.idle": "2026-05-29T06:30:45.165079Z", - "shell.execute_reply": "2026-05-29T06:30:45.165079Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "# ---- Posterior-predictive reflectivity --------------------------------------\n", @@ -630,14 +682,7 @@ "cell_type": "code", "execution_count": null, "id": "ef27b8ec", - "metadata": { - "execution": { - "iopub.execute_input": "2026-05-29T06:30:45.166642Z", - "iopub.status.busy": "2026-05-29T06:30:45.166642Z", - "iopub.status.idle": "2026-05-29T06:30:45.413146Z", - "shell.execute_reply": "2026-05-29T06:30:45.413146Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "# ---- Posterior-predictive SLD profile ---------------------------------------\n", @@ -671,14 +716,7 @@ "cell_type": "code", "execution_count": null, "id": "37e78b4e", - "metadata": { - "execution": { - "iopub.execute_input": "2026-05-29T06:30:45.415149Z", - "iopub.status.busy": "2026-05-29T06:30:45.415149Z", - "iopub.status.idle": "2026-05-29T06:30:45.419700Z", - "shell.execute_reply": "2026-05-29T06:30:45.419700Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "# ---- Summary ----------------------------------------------------------------\n", @@ -688,13 +726,17 @@ "print()\n", "print('API surface demonstrated:')\n", "print(' MultiFitter.mcmc_sample(data, samples=, burn=, thin=)')\n", + "print(' MultiFitter.sampler β€” the Sampler behind the last run')\n", + "print(' .extend(additional_samples=, thin=)')\n", + "print(' β€” continue the chain, no re-burn')\n", + "print(' .state.gelman() β€” chain-aware RΜ‚ (works on pooled draws)')\n", "print(' PosteriorResults(draws, param_names, logp=, sampler_state=)')\n", "print(' .summary() β€” formatted parameter table')\n", "print(' .distribution() β€” per-parameter marginal Plotly figure')\n", "print(' .corner() β€” pairwise correlation Plotly figure')\n", "print(' .trace() β€” MCMC chain trace plot')\n", "print(' .credible_interval(alpha) β€” equal-tailed credible intervals')\n", - "print(' .gelman_rubin() β€” RΜ‚ convergence diagnostic')\n", + "print(' .gelman_rubin() β€” RΜ‚ for draws pre-split into chains')\n", "print()\n", "print(' Standalone functions (work on raw dict; the plot helpers return')\n", "print(' the same interactive Plotly figures as the App):')\n", @@ -706,7 +748,8 @@ "print(' posterior_predictive_reflectivity(draws, names, model, q, n)')\n", "print(' posterior_predictive_sld_profile(draws, names, model, n)')\n", "print()\n", - "print('The high-level API provides clean, safe access to BUMPS DREAM sampling.')" + "print('Workflow: fit β†’ sample short β†’ check RΜ‚ β†’ extend until converged β†’ analyse.')\n", + "print('Extending continues the existing chain, so the burn-in is paid only once.')" ] } ], @@ -726,7 +769,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.11" + "version": "3.12.12" } }, "nbformat": 4, diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 1cd19cde..f75e4657 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -208,8 +208,7 @@ nav: - Elements: - Layers: - Layer: api-reference/elements/layer.md - - Layer Area Per Molecule: - api-reference/elements/layer_area_per_molecule.md + - Layer Area Per Molecule: api-reference/elements/layer_area_per_molecule.md - Materials: - Material: api-reference/elements/material.md - Material Density: api-reference/elements/material_density.md diff --git a/pixi.lock b/pixi.lock index 167028d6..36a67a04 100644 --- a/pixi.lock +++ b/pixi.lock @@ -190,7 +190,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/websocket-client-1.9.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.1-pyhcf101f3_0.conda - pypi: . - - pypi: git+https://github.com/easyscience/corelib.git?rev=develop#aadbd4891b94f6aa18187d48be8c2ab6f81113b0 + - pypi: git+https://github.com/easyscience/corelib.git?rev=bayesian_extend#8c83ab3cb6f7e598f16f31c70cc3935f326bf2ca - pypi: https://files.pythonhosted.org/packages/00/bb/90ba423612b6aa0adccc6b1874bcd4a9b44b660c0c16f346611e00f64ac3/backrefs-7.0-py313-none-any.whl - pypi: https://files.pythonhosted.org/packages/01/7c/fa07d3da2b6253eb8474be16eab2eadf670460e364ccc895ca7ff388ee30/oscrypto-1.3.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl @@ -501,7 +501,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zeromq-4.3.5-h4818236_10.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hbf9d68e_6.conda - pypi: . - - pypi: git+https://github.com/easyscience/corelib.git?rev=develop#aadbd4891b94f6aa18187d48be8c2ab6f81113b0 + - pypi: git+https://github.com/easyscience/corelib.git?rev=bayesian_extend#8c83ab3cb6f7e598f16f31c70cc3935f326bf2ca - pypi: https://files.pythonhosted.org/packages/00/bb/90ba423612b6aa0adccc6b1874bcd4a9b44b660c0c16f346611e00f64ac3/backrefs-7.0-py313-none-any.whl - pypi: https://files.pythonhosted.org/packages/01/7c/fa07d3da2b6253eb8474be16eab2eadf670460e364ccc895ca7ff388ee30/oscrypto-1.3.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl @@ -802,7 +802,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/zeromq-4.3.5-h507cc87_10.conda - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_6.conda - pypi: . - - pypi: git+https://github.com/easyscience/corelib.git?rev=develop#aadbd4891b94f6aa18187d48be8c2ab6f81113b0 + - pypi: git+https://github.com/easyscience/corelib.git?rev=bayesian_extend#8c83ab3cb6f7e598f16f31c70cc3935f326bf2ca - pypi: https://files.pythonhosted.org/packages/00/bb/90ba423612b6aa0adccc6b1874bcd4a9b44b660c0c16f346611e00f64ac3/backrefs-7.0-py313-none-any.whl - pypi: https://files.pythonhosted.org/packages/01/7c/fa07d3da2b6253eb8474be16eab2eadf670460e364ccc895ca7ff388ee30/oscrypto-1.3.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl @@ -1125,7 +1125,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/websocket-client-1.9.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.1-pyhcf101f3_0.conda - pypi: . - - pypi: git+https://github.com/easyscience/corelib.git?rev=develop#aadbd4891b94f6aa18187d48be8c2ab6f81113b0 + - pypi: git+https://github.com/easyscience/corelib.git?rev=bayesian_extend#8c83ab3cb6f7e598f16f31c70cc3935f326bf2ca - pypi: https://files.pythonhosted.org/packages/01/7c/fa07d3da2b6253eb8474be16eab2eadf670460e364ccc895ca7ff388ee30/oscrypto-1.3.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/06/8a/5e156e31ba656ce93c1cc895dd8f051ec351cb382940dca655aaec475005/python_bidi-0.6.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl @@ -1430,7 +1430,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zeromq-4.3.5-h4818236_10.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hbf9d68e_6.conda - pypi: . - - pypi: git+https://github.com/easyscience/corelib.git?rev=develop#aadbd4891b94f6aa18187d48be8c2ab6f81113b0 + - pypi: git+https://github.com/easyscience/corelib.git?rev=bayesian_extend#8c83ab3cb6f7e598f16f31c70cc3935f326bf2ca - pypi: https://files.pythonhosted.org/packages/01/7c/fa07d3da2b6253eb8474be16eab2eadf670460e364ccc895ca7ff388ee30/oscrypto-1.3.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/07/c7/deb8c5e604404dbf10a3808a858946ca3547692ff6316b698945bb72177e/python_socketio-5.16.1-py3-none-any.whl @@ -1725,7 +1725,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/zeromq-4.3.5-h507cc87_10.conda - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_6.conda - pypi: . - - pypi: git+https://github.com/easyscience/corelib.git?rev=develop#aadbd4891b94f6aa18187d48be8c2ab6f81113b0 + - pypi: git+https://github.com/easyscience/corelib.git?rev=bayesian_extend#8c83ab3cb6f7e598f16f31c70cc3935f326bf2ca - pypi: https://files.pythonhosted.org/packages/01/7c/fa07d3da2b6253eb8474be16eab2eadf670460e364ccc895ca7ff388ee30/oscrypto-1.3.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/02/05/d60c732b56da5085175c07c74b2df4e6d181b0c9a61e1691474f06ef4b39/lxml-6.1.0-cp311-cp311-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl @@ -2048,7 +2048,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/websocket-client-1.9.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.1-pyhcf101f3_0.conda - pypi: . - - pypi: git+https://github.com/easyscience/corelib.git?rev=develop#aadbd4891b94f6aa18187d48be8c2ab6f81113b0 + - pypi: git+https://github.com/easyscience/corelib.git?rev=bayesian_extend#8c83ab3cb6f7e598f16f31c70cc3935f326bf2ca - pypi: https://files.pythonhosted.org/packages/00/bb/90ba423612b6aa0adccc6b1874bcd4a9b44b660c0c16f346611e00f64ac3/backrefs-7.0-py313-none-any.whl - pypi: https://files.pythonhosted.org/packages/01/7c/fa07d3da2b6253eb8474be16eab2eadf670460e364ccc895ca7ff388ee30/oscrypto-1.3.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl @@ -2359,7 +2359,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zeromq-4.3.5-h4818236_10.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hbf9d68e_6.conda - pypi: . - - pypi: git+https://github.com/easyscience/corelib.git?rev=develop#aadbd4891b94f6aa18187d48be8c2ab6f81113b0 + - pypi: git+https://github.com/easyscience/corelib.git?rev=bayesian_extend#8c83ab3cb6f7e598f16f31c70cc3935f326bf2ca - pypi: https://files.pythonhosted.org/packages/00/bb/90ba423612b6aa0adccc6b1874bcd4a9b44b660c0c16f346611e00f64ac3/backrefs-7.0-py313-none-any.whl - pypi: https://files.pythonhosted.org/packages/01/7c/fa07d3da2b6253eb8474be16eab2eadf670460e364ccc895ca7ff388ee30/oscrypto-1.3.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl @@ -2660,7 +2660,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/zeromq-4.3.5-h507cc87_10.conda - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_6.conda - pypi: . - - pypi: git+https://github.com/easyscience/corelib.git?rev=develop#aadbd4891b94f6aa18187d48be8c2ab6f81113b0 + - pypi: git+https://github.com/easyscience/corelib.git?rev=bayesian_extend#8c83ab3cb6f7e598f16f31c70cc3935f326bf2ca - pypi: https://files.pythonhosted.org/packages/00/bb/90ba423612b6aa0adccc6b1874bcd4a9b44b660c0c16f346611e00f64ac3/backrefs-7.0-py313-none-any.whl - pypi: https://files.pythonhosted.org/packages/01/7c/fa07d3da2b6253eb8474be16eab2eadf670460e364ccc895ca7ff388ee30/oscrypto-1.3.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl @@ -8301,7 +8301,7 @@ packages: name: easyreflectometry requires_dist: - bumps - - easyscience @ git+https://github.com/easyscience/corelib.git@develop + - easyscience @ git+https://github.com/easyscience/corelib.git@bayesian_extend - orsopy - plotly - pooch @@ -8345,9 +8345,9 @@ packages: - validate-pyproject[all] ; extra == 'dev' - versioningit ; extra == 'dev' requires_python: '>=3.11' -- pypi: git+https://github.com/easyscience/corelib.git?rev=develop#aadbd4891b94f6aa18187d48be8c2ab6f81113b0 +- pypi: git+https://github.com/easyscience/corelib.git?rev=bayesian_extend#8c83ab3cb6f7e598f16f31c70cc3935f326bf2ca name: easyscience - version: 2.3.1+dev8 + version: 2.4.0+dev17 requires_dist: - asteval - bumps diff --git a/src/easyreflectometry/fitting.py b/src/easyreflectometry/fitting.py index f90211ff..944d8e23 100644 --- a/src/easyreflectometry/fitting.py +++ b/src/easyreflectometry/fitting.py @@ -195,6 +195,7 @@ def wrapped(*args, **kwargs): self._fit_results: list[FitResults] | None = None self._classical_fit_metrics: list[dict] | None = None self._objective = _validate_objective(objective) + self._sampler: Sampler | None = None def fit(self, data: sc.DataGroup, id: int = 0, objective: str | None = None) -> sc.DataGroup: """Perform the fitting and populate the DataGroups with the result. @@ -382,12 +383,18 @@ def mcmc_sample( :param initializer: DREAM population initializer. One of ``'eps'``, ``'cov'``, ``'lhs'``, or ``'random'``. By default, None (BUMPS uses ``'eps'``). - β€” the population already exists in the saved state. :param progress_callback: Optional callback for progress updates during sampling. Forwarded to the core MultiFitter. :return: Dictionary with keys ``'draws'``, ``'param_names'``, ``'state'``, and ``'logp'``. :raises RuntimeError: If the current minimizer is not a BUMPS instance. + + The underlying :class:`~easyscience.fitting.Sampler` is retained on + :attr:`sampler`, so the chain can be continued without re-running the + burn-in:: + + fitter.mcmc_sample(data, samples=2000, burn=500, thin=10) + extended = fitter.sampler.extend(additional_samples=8000, thin=10) """ minimizer = self.easy_science_multi_fitter.minimizer if not (hasattr(minimizer, 'package') and minimizer.package == 'bumps'): @@ -435,6 +442,8 @@ def mcmc_sample( y=y, weights=dy, ) + # Retained so the chain can be continued afterwards via ``self.sampler.extend()``. + self._sampler = sampler results = sampler.sample( samples=samples, burn=burn, @@ -451,6 +460,16 @@ def mcmc_sample( 'logp': results.logp, } + @property + def sampler(self) -> Sampler | None: + """The ``Sampler`` behind the most recent :meth:`mcmc_sample` call, or None. + + Holds the live BUMPS chain state, so the sampling run can be continued + with ``fitter.sampler.extend(additional_samples=...)`` instead of + starting a fresh chain. + """ + return self._sampler + @property def chi2(self) -> float | None: """Total chi-squared across all fitted datasets, or None if no fit has been performed.""" From 5cb7b9d9bda4aea4bfa5490d9f2efebd261bde8e Mon Sep 17 00:00:00 2001 From: rozyczko Date: Fri, 17 Jul 2026 09:58:59 +0200 Subject: [PATCH 5/5] linting --- CONTRIBUTING.md | 3 +-- docs/mkdocs.yml | 3 ++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a4ac1fbc..a6ceeebd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -42,8 +42,7 @@ Please make sure you follow the EasyScience organization-wide If you are not planning to contribute code, you may want to: - 🐞 Report a bug β€” see [Reporting Issues](#11-reporting-issues) -- πŸ›‘ Report a security issue β€” see - [Security Issues](#12-security-issues) +- πŸ›‘ Report a security issue β€” see [Security Issues](#12-security-issues) - πŸ’¬ Ask a question or start a discussion at [Project Discussions](https://github.com/easyscience/reflectometry-lib/discussions) diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index f75e4657..1cd19cde 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -208,7 +208,8 @@ nav: - Elements: - Layers: - Layer: api-reference/elements/layer.md - - Layer Area Per Molecule: api-reference/elements/layer_area_per_molecule.md + - Layer Area Per Molecule: + api-reference/elements/layer_area_per_molecule.md - Materials: - Material: api-reference/elements/material.md - Material Density: api-reference/elements/material_density.md