From c4c1b0274d045e16d2b742cd184b2c82e105632c Mon Sep 17 00:00:00 2001 From: rozyczko Date: Wed, 6 May 2026 14:03:13 +0200 Subject: [PATCH 01/15] initial implementation --- .../fitting/minimizers/minimizer_bumps.py | 84 +++++ src/easyscience/fitting/multi_fitter.py | 107 +++++++ .../integration/fitting/test_multi_fitter.py | 300 ++++++++++++++++++ 3 files changed, 491 insertions(+) diff --git a/src/easyscience/fitting/minimizers/minimizer_bumps.py b/src/easyscience/fitting/minimizers/minimizer_bumps.py index 13678854..0ead63b5 100644 --- a/src/easyscience/fitting/minimizers/minimizer_bumps.py +++ b/src/easyscience/fitting/minimizers/minimizer_bumps.py @@ -334,6 +334,90 @@ def _make_func(x, y, weights): return _outer(self) + def sample( + self, + x: np.ndarray, + y: np.ndarray, + weights: np.ndarray, + samples: int = 10000, + burn: int = 2000, + thin: int = 10, + chains: int | None = None, + population: int | None = None, + seed: int | None = None, + sampler_kwargs: dict | None = None, + ) -> dict: + """Run Bayesian MCMC sampling using the BUMPS DREAM sampler. + + Builds a BUMPS :class:`~bumps.names.FitProblem` from the current + model and runs the DREAM sampler. This is the public minimizer-level + entry point for Bayesian sampling; the higher-level + :meth:`easyscience.fitting.multi_fitter.MultiFitter.sample` delegates + to this method after flattening multi-dataset arrays. + + :param x: Flattened independent variable array. + :type x: np.ndarray + :param y: Flattened dependent variable array. + :type y: np.ndarray + :param weights: Flattened weight array. + :type weights: np.ndarray + :param samples: Number of retained DREAM samples requested from BUMPS. + :type samples: int + :param burn: Burn-in steps. + :type burn: int + :param thin: Thinning interval. + :type thin: int + :param chains: User-friendly alias for BUMPS DREAM population count. + :type chains: int | None + :param population: BUMPS DREAM population count (``pop``) for advanced users. + :type population: int | None + :param seed: Best-effort random seed passed to ``numpy.random.seed``. + BUMPS DREAM may use additional internal RNG state that is not + controlled by this seed, so exact reproducibility is not guaranteed. + :type seed: int | None + :param sampler_kwargs: Additional keyword arguments forwarded to + :func:`bumps.fitters.fit`. + :type sampler_kwargs: dict | None + :return: Dictionary with keys ``'draws'``, ``'param_names'``, ``'state'``, + and ``'logp'``. + :rtype: dict + """ + from bumps.fitters import fit as bumps_fit + from bumps.names import FitProblem + + # Build the BUMPS Curve model using the minimizer's existing machinery + model_func = self._make_model() + x_flat = np.linspace(0, y.size - 1, y.size) + curve = model_func(x_flat, y, weights) + problem = FitProblem(curve) + + # Best-effort seed: sets numpy's global RNG state just before DREAM starts. + # BUMPS DREAM may have its own internal RNG paths that are not fully + # controlled by this, so exact reproducibility is not guaranteed. + if seed is not None: + np.random.seed(seed) + + # Run DREAM sampler + dream_kwargs: dict = {'samples': samples, 'burn': burn, 'thin': thin} + if chains is not None or population is not None: + pop = chains if chains is not None else population + dream_kwargs['pop'] = pop + if sampler_kwargs: + dream_kwargs.update(sampler_kwargs) + result = bumps_fit(problem, method='dream', **dream_kwargs) + + # Extract posterior + draws = result.state.draw().points + param_names = [p.name[len(MINIMIZER_PARAMETER_PREFIX) :] for p in problem._parameters] + logp = getattr(result.state, 'logp', None) + + return { + 'draws': draws, + 'param_names': param_names, + 'state': result.state, + 'logp': logp, + } + def _set_parameter_fit_result( self, fit_result, diff --git a/src/easyscience/fitting/multi_fitter.py b/src/easyscience/fitting/multi_fitter.py index f66cd173..bc846e7a 100644 --- a/src/easyscience/fitting/multi_fitter.py +++ b/src/easyscience/fitting/multi_fitter.py @@ -2,6 +2,7 @@ # SPDX-License-Identifier: BSD-3-Clause from typing import Callable +from typing import Dict from typing import List from typing import Optional @@ -149,3 +150,109 @@ def _post_compute_reshaping( fit_results_list.append(current_results) sp = ep return fit_results_list + + def sample( + self, + x: List[np.ndarray], + y: List[np.ndarray], + weights: List[np.ndarray], + samples: int = 10000, + burn: int = 1000, + thin: int = 10, + chains: int | None = None, + population: int | None = None, + seed: int | None = None, + vectorized: bool = False, + sampler_kwargs: dict | None = None, + ) -> Dict: + """Run Bayesian MCMC sampling using the BUMPS DREAM sampler. + + Requires that the current minimizer is a BUMPS instance (i.e. the + minimizer was switched to ``AvailableMinimizers.Bumps`` or equivalent). + + :param x: List of independent variable arrays (one per dataset). + :type x: List[np.ndarray] + :param y: List of dependent variable arrays (one per dataset). + :type y: List[np.ndarray] + :param weights: List of weight arrays (one per dataset). + :type weights: List[np.ndarray] + :param samples: Number of retained DREAM samples requested from BUMPS. + :type samples: int + :param burn: Burn-in steps. + :type burn: int + :param thin: Thinning interval. + :type thin: int + :param chains: User-friendly alias for BUMPS DREAM population count. + :type chains: int | None + :param population: BUMPS DREAM population count (``pop``) for advanced users. + :type population: int | None + :param seed: Best-effort random seed. BUMPS DREAM may use additional + internal RNG state that is not controlled by this seed, so exact + reproducibility is not guaranteed. + :type seed: int | None + :param vectorized: Whether the fit function expects vectorized + (multidimensional) input. Defaults to ``False``. + :type vectorized: bool + :param sampler_kwargs: Additional keyword arguments forwarded to the + BUMPS DREAM sampler via :func:`bumps.fitters.fit`. + :type sampler_kwargs: dict | None + :return: Dictionary with keys ``'draws'``, ``'param_names'``, ``'state'``, + and ``'logp'``. + :rtype: dict + :raises RuntimeError: If the current minimizer is not a BUMPS instance. + """ + # --- Alias resolution --- + if chains is not None and population is not None: + if chains != population: + raise ValueError( + f'Conflicting population arguments: chains={chains}, population={population}. ' + 'Only provide one.' + ) + pop = chains + elif chains is not None: + pop = chains + elif population is not None: + pop = population + else: + pop = None + + # Flatten multi-dataset arrays + _, x_new, y_new, w_new, _dims = self._precompute_reshaping( + x, y, weights, vectorized=vectorized + ) + self._dependent_dims = _dims + + # Wrap fit functions for multi-dataset flattening, mirroring the + # ``Fitter.fit`` lifecycle: use the property setter so the minimizer + # is re-created with the wrapped fit function. + original_fit_func = self.fit_function + fit_fun_wrap = self._fit_function_wrapper(x_new, flatten=True) + self.fit_function = fit_fun_wrap + + try: + minimizer = self.minimizer + + # Verify it's a BUMPS minimizer (sampling only works with BUMPS/DREAM) + if not (hasattr(minimizer, 'package') and minimizer.package == 'bumps'): + raise RuntimeError( + 'Bayesian sampling requires a BUMPS minimizer. ' + 'Use ``fitter.switch_minimizer(AvailableMinimizers.Bumps)`` first.' + ) + + # Delegate to the BUMPS minimizer's public sample method + result = minimizer.sample( + x=x_new, + y=y_new, + weights=w_new, + samples=samples, + burn=burn, + thin=thin, + chains=None, # alias already resolved into `pop` + population=pop, + seed=seed, + sampler_kwargs=sampler_kwargs, + ) + finally: + self.fit_function = original_fit_func + + return result diff --git a/tests/integration/fitting/test_multi_fitter.py b/tests/integration/fitting/test_multi_fitter.py index 7e2d02d1..4ceb3739 100644 --- a/tests/integration/fitting/test_multi_fitter.py +++ b/tests/integration/fitting/test_multi_fitter.py @@ -286,3 +286,303 @@ def test_multi_fit_1D_2D(fit_engine): assert result.success assert np.all(result.x == X[idx]) assert np.all(result.y_obs == Y[idx]) + + +# --------------------------------------------------------------------------- +# Tests for MultiFitter.sample (Bayesian MCMC via BUMPS DREAM) +# --------------------------------------------------------------------------- + + +class TestSampleRequiresBumps: + def test_raises_runtime_error_when_not_bumps(self): + """sample() must raise RuntimeError if the minimizer is not a BUMPS instance.""" + sp = AbsSin(0.354, 3.05) + f = MultiFitter([sp], [sp]) + + x = np.linspace(0, 5, 50) + y = np.sin(x) + weights = np.ones_like(x) + + with pytest.raises(RuntimeError, match='Bayesian sampling requires a BUMPS minimizer'): + f.sample(x=[x], y=[y], weights=[weights], samples=10, burn=5, thin=1) + + +class TestSampleBasic: + @pytest.mark.filterwarnings('ignore::UserWarning') + def test_returns_expected_keys_and_shapes(self): + """sample() with BUMPS should return draws, param_names, state, logp.""" + ref_sin = AbsSin(0.2, np.pi) + sp = AbsSin(0.354, 3.05) + + x = np.linspace(0, 5, 50) + y = ref_sin(x) + weights = np.ones_like(x) + + sp.offset.fixed = False + sp.phase.fixed = False + + f = MultiFitter([sp], [sp]) + try: + f.switch_minimizer('Bumps') + except AttributeError: + pytest.skip('BUMPS is not installed') + + result = f.sample(x=[x], y=[y], weights=[weights], samples=100, burn=20, thin=2) + + assert isinstance(result, dict) + assert 'draws' in result + assert 'param_names' in result + assert 'state' in result + assert 'logp' in result + + # draws shape: (retained_samples, n_params) + assert result['draws'].ndim == 2 + assert result['draws'].shape[1] == len(result['param_names']) + + # param_names should match the model's fit parameters + expected_pars = {p.unique_name for p in sp.get_fit_parameters()} + assert set(result['param_names']) == expected_pars + + @pytest.mark.filterwarnings('ignore::UserWarning') + def test_multi_dataset_returns_consistent_param_names(self): + """sample() with multiple datasets should have correct param_names across all.""" + ref_sin_1 = AbsSin(0.2, np.pi) + sp_sin_1 = AbsSin(0.354, 3.05) + sp_line = Line(0.43, 6.1) + + # Link a parameter across models + sp_line.m.make_dependent_on( + dependency_expression='sp_sin1', dependency_map={'sp_sin1': sp_sin_1.offset} + ) + + x1 = np.linspace(0, 5, 50) + y1 = ref_sin_1(x1) + x2 = np.copy(x1) + y2 = Line(1, 4.6)(x2) + weights = np.ones_like(x1) + + sp_sin_1.offset.fixed = False + sp_sin_1.phase.fixed = False + sp_line.c.fixed = False + + f = MultiFitter([sp_sin_1, sp_line], [sp_sin_1, sp_line]) + try: + f.switch_minimizer('Bumps') + except AttributeError: + pytest.skip('BUMPS is not installed') + + result = f.sample( + x=[x1, x2], y=[y1, y2], weights=[weights, weights], samples=100, burn=20, thin=2 + ) + + # All parameters across both models should appear + all_params = {p.unique_name for p in sp_sin_1.get_fit_parameters()} + all_params |= {p.unique_name for p in sp_line.get_fit_parameters()} + assert set(result['param_names']) == all_params + + +class TestSampleAliasResolution: + def test_conflicting_chains_and_population_raises(self): + """Passing both chains and population with different values must raise.""" + sp = AbsSin(0.354, 3.05) + f = MultiFitter([sp], [sp]) + try: + f.switch_minimizer('Bumps') + except AttributeError: + pytest.skip('BUMPS is not installed') + + x = np.linspace(0, 5, 50) + y = np.sin(x) + weights = np.ones_like(x) + + with pytest.raises(ValueError, match='Conflicting population arguments'): + f.sample( + x=[x], y=[y], weights=[weights], samples=10, burn=5, thin=1, chains=3, population=5 + ) + + def test_chains_and_population_equal_is_ok(self): + """Passing chains == population should succeed (no conflict).""" + sp = AbsSin(0.354, 3.05) + sp.offset.fixed = False + sp.phase.fixed = False + f = MultiFitter([sp], [sp]) + try: + f.switch_minimizer('Bumps') + except AttributeError: + pytest.skip('BUMPS is not installed') + + x = np.linspace(0, 5, 50) + y = np.sin(x) + weights = np.ones_like(x) + + # Should not raise ValueError — chains and population are equal. + # DREAM needs a sufficient population; 5 is a safe minimum. + result = f.sample( + x=[x], y=[y], weights=[weights], samples=100, burn=20, thin=2, chains=5, population=5 + ) + assert 'draws' in result + + +class TestSampleSeedReproducibility: + @pytest.mark.filterwarnings('ignore::UserWarning') + def test_seed_produces_valid_draws(self): + """Running sample() with a seed must produce valid draws.""" + ref_sin = AbsSin(0.2, np.pi) + sp = AbsSin(0.354, 3.05) + + x = np.linspace(0, 5, 50) + y = ref_sin(x) + weights = np.ones_like(x) + + sp.offset.fixed = False + sp.phase.fixed = False + + f = MultiFitter([sp], [sp]) + try: + f.switch_minimizer('Bumps') + except AttributeError: + pytest.skip('BUMPS is not installed') + + result = f.sample(x=[x], y=[y], weights=[weights], samples=100, burn=20, thin=2, seed=42) + + assert result['draws'].ndim == 2 + assert result['draws'].shape[0] > 0 + assert result['draws'].shape[1] == len(result['param_names']) + # logp should be present (may be None if not computed) + assert 'logp' in result + + @pytest.mark.filterwarnings('ignore::UserWarning') + def test_different_seeds_both_produce_valid_draws(self): + """Running sample() with different seeds should each produce valid draws.""" + ref_sin = AbsSin(0.2, np.pi) + sp = AbsSin(0.354, 3.05) + + x = np.linspace(0, 5, 50) + y = ref_sin(x) + weights = np.ones_like(x) + + sp.offset.fixed = False + sp.phase.fixed = False + + f = MultiFitter([sp], [sp]) + try: + f.switch_minimizer('Bumps') + except AttributeError: + pytest.skip('BUMPS is not installed') + + result1 = f.sample(x=[x], y=[y], weights=[weights], samples=100, burn=20, thin=2, seed=42) + result2 = f.sample( + x=[x], y=[y], weights=[weights], samples=100, burn=20, thin=2, seed=12345 + ) + + # Both must produce valid draws + assert result1['draws'].shape[0] > 0 + assert result2['draws'].shape[0] > 0 + assert result1['draws'].ndim == 2 + assert result2['draws'].ndim == 2 + + +class TestSampleVectorized: + @pytest.mark.filterwarnings('ignore::UserWarning') + def test_vectorized_2d_input_produces_valid_draws(self): + """sample() with vectorized=True and 2D input should produce valid draws.""" + sp = AbsSin2D(0.1, 1.75) + + x = np.linspace(0, 5, 50) + X, Y = np.meshgrid(x, x) + x2D = np.stack((X, Y), axis=2) + y2D = np.abs(np.sin(X)) * np.abs(np.sin(Y)) + weights = np.ones_like(y2D) + + sp.offset.fixed = False + sp.phase.fixed = False + + f = MultiFitter([sp], [sp]) + try: + f.switch_minimizer('Bumps') + except AttributeError: + pytest.skip('BUMPS is not installed') + + result = f.sample( + x=[x2D], y=[y2D], weights=[weights], samples=100, burn=20, thin=2, vectorized=True + ) + + assert result['draws'].ndim == 2 + assert result['draws'].shape[0] > 0 + assert result['draws'].shape[1] == len(result['param_names']) + + +class TestSampleStateRestoration: + def test_fit_function_restored_after_runtime_error(self): + """fit_function must be restored to its original value even when sample() raises.""" + sp = AbsSin(0.354, 3.05) + f = MultiFitter([sp], [sp]) + + x = np.linspace(0, 5, 50) + y = np.sin(x) + weights = np.ones_like(x) + + original_func = f.fit_function + + with pytest.raises(RuntimeError): + f.sample(x=[x], y=[y], weights=[weights], samples=10, burn=5, thin=1) + + assert f.fit_function is original_func + + @pytest.mark.filterwarnings('ignore::UserWarning') + def test_fit_function_restored_after_successful_sample(self): + """fit_function must be restored to its original value after a successful sample().""" + ref_sin = AbsSin(0.2, np.pi) + sp = AbsSin(0.354, 3.05) + + x = np.linspace(0, 5, 50) + y = ref_sin(x) + weights = np.ones_like(x) + + sp.offset.fixed = False + sp.phase.fixed = False + + f = MultiFitter([sp], [sp]) + try: + f.switch_minimizer('Bumps') + except AttributeError: + pytest.skip('BUMPS is not installed') + + original_func = f.fit_function + f.sample(x=[x], y=[y], weights=[weights], samples=100, burn=20, thin=2) + assert f.fit_function is original_func + + +class TestSampleSamplerKwargs: + @pytest.mark.filterwarnings('ignore::UserWarning') + def test_sampler_kwargs_forwarded(self): + """sampler_kwargs dict is forwarded to the BUMPS DREAM sampler.""" + ref_sin = AbsSin(0.2, np.pi) + sp = AbsSin(0.354, 3.05) + + x = np.linspace(0, 5, 50) + y = ref_sin(x) + weights = np.ones_like(x) + + sp.offset.fixed = False + sp.phase.fixed = False + + f = MultiFitter([sp], [sp]) + try: + f.switch_minimizer('Bumps') + except AttributeError: + pytest.skip('BUMPS is not installed') + + # Pass extra kwargs — should not raise + result = f.sample( + x=[x], + y=[y], + weights=[weights], + samples=100, + burn=20, + thin=2, + sampler_kwargs={'init': 'random'}, + ) + + assert result['draws'].ndim == 2 + assert result['draws'].shape[0] > 0 From 96be6d0232740c8ce20fe7de4f9b739ef5d6751f Mon Sep 17 00:00:00 2001 From: Piotr Rozyczko Date: Sat, 9 May 2026 22:20:04 +0200 Subject: [PATCH 02/15] show max iteration, so users know how long to wait. Added cancellation of bayesian --- .../fitting/minimizers/minimizer_bumps.py | 122 ++++++++++++++---- src/easyscience/fitting/multi_fitter.py | 9 +- 2 files changed, 103 insertions(+), 28 deletions(-) diff --git a/src/easyscience/fitting/minimizers/minimizer_bumps.py b/src/easyscience/fitting/minimizers/minimizer_bumps.py index 0ead63b5..a2003d82 100644 --- a/src/easyscience/fitting/minimizers/minimizer_bumps.py +++ b/src/easyscience/fitting/minimizers/minimizer_bumps.py @@ -80,6 +80,7 @@ def fit( tolerance: float | None = None, max_evaluations: int | None = None, progress_callback: Callable[[dict], bool | None] | None = None, + abort_test: Callable[[], bool] | None = None, minimizer_kwargs: dict | None = None, engine_kwargs: dict | None = None, **kwargs, @@ -186,6 +187,7 @@ def fit( fitclass=fitclass, problem=problem, monitors=monitors, + abort_test=abort_test or (lambda: False), **minimizer_kwargs, **kwargs, ) @@ -346,6 +348,8 @@ def sample( population: int | None = None, seed: int | None = None, sampler_kwargs: dict | None = None, + progress_callback: Callable[[dict], bool | None] | None = None, + abort_test: Callable[[], bool] | None = None, ) -> dict: """Run Bayesian MCMC sampling using the BUMPS DREAM sampler. @@ -356,33 +360,23 @@ def sample( to this method after flattening multi-dataset arrays. :param x: Flattened independent variable array. - :type x: np.ndarray :param y: Flattened dependent variable array. - :type y: np.ndarray :param weights: Flattened weight array. - :type weights: np.ndarray :param samples: Number of retained DREAM samples requested from BUMPS. - :type samples: int :param burn: Burn-in steps. - :type burn: int :param thin: Thinning interval. - :type thin: int :param chains: User-friendly alias for BUMPS DREAM population count. - :type chains: int | None - :param population: BUMPS DREAM population count (``pop``) for advanced users. - :type population: int | None - :param seed: Best-effort random seed passed to ``numpy.random.seed``. - BUMPS DREAM may use additional internal RNG state that is not - controlled by this seed, so exact reproducibility is not guaranteed. - :type seed: int | None + :param population: BUMPS DREAM population count for advanced users. + :param seed: Best-effort random seed. :param sampler_kwargs: Additional keyword arguments forwarded to :func:`bumps.fitters.fit`. - :type sampler_kwargs: dict | None - :return: Dictionary with keys ``'draws'``, ``'param_names'``, ``'state'``, + :param progress_callback: Optional callback for progress updates during + sampling. The payload dict includes ``iteration`` (DREAM generation + number) and ``sampling: True``. + :return: Dictionary with keys ``'draws'``, ``'param_names'``, ``'state'`", and ``'logp'``. - :rtype: dict """ - from bumps.fitters import fit as bumps_fit + from bumps.fitters import DreamFit from bumps.names import FitProblem # Build the BUMPS Curve model using the minimizer's existing machinery @@ -392,32 +386,106 @@ def sample( problem = FitProblem(curve) # Best-effort seed: sets numpy's global RNG state just before DREAM starts. - # BUMPS DREAM may have its own internal RNG paths that are not fully - # controlled by this, so exact reproducibility is not guaranteed. if seed is not None: np.random.seed(seed) - # Run DREAM sampler + # Resolve population parameter + if chains is not None and population is not None: + if chains != population: + raise ValueError( + f'Conflicting population arguments: chains={chains}, population={population}. ' + 'Only provide one.' + ) + pop = chains + elif chains is not None: + pop = chains + elif population is not None: + pop = population + else: + pop = None + + # Build DREAM kwargs dream_kwargs: dict = {'samples': samples, 'burn': burn, 'thin': thin} - if chains is not None or population is not None: - pop = chains if chains is not None else population + if pop is not None: dream_kwargs['pop'] = pop if sampler_kwargs: dream_kwargs.update(sampler_kwargs) - result = bumps_fit(problem, method='dream', **dream_kwargs) - # Extract posterior - draws = result.state.draw().points + # Build monitors (same pattern as classical Bumps.fit()) + monitors = [] + if progress_callback is not None: + if not callable(progress_callback): + raise ValueError('progress_callback must be callable') + # Compute total DREAM steps for progress display (burn + sampling generations) + pop_val = pop if pop else 10 + _total_steps = burn + (samples + pop_val - 1) // pop_val + monitors.append( + BumpsProgressMonitor( + problem, + progress_callback, + lambda problem, iteration, point, nllf: { + **self._build_sample_progress_payload(problem, iteration, point, nllf), + 'total_steps': _total_steps, + }, + ) + ) + + driver = FitDriver( + fitclass=DreamFit, + problem=problem, + monitors=monitors, + abort_test=abort_test or (lambda: False), + **dream_kwargs, + ) + driver.clip() + + from easyscience import global_object + + stack_status = global_object.stack.enabled + global_object.stack.enabled = False + + try: + x_opt, fx = driver.fit() + result_state = getattr(driver.fitter, 'state', None) + if result_state is None: + raise FitError('Sampling aborted by user') + except Exception: + self._restore_parameter_values() + raise + finally: + global_object.stack.enabled = stack_status + + draws = result_state.draw().points param_names = [p.name[len(MINIMIZER_PARAMETER_PREFIX) :] for p in problem._parameters] - logp = getattr(result.state, 'logp', None) + logp = getattr(result_state, 'logp', None) return { 'draws': draws, 'param_names': param_names, - 'state': result.state, + 'state': result_state, 'logp': logp, } + def _build_sample_progress_payload( + self, problem, iteration: int, point: np.ndarray, nllf: float + ) -> dict: + """Build a progress payload for Bayesian DREAM sampling steps. + + Called by :class:`BumpsProgressMonitor` at each DREAM generation. + The payload includes ``sampling: True`` so downstream consumers can + distinguish sampling progress from classical fitting progress. + """ + parameter_values = self._current_parameter_snapshot(problem, point) + return { + 'iteration': iteration, + 'chi2': float(problem.chisq(nllf=nllf, norm=False)), + 'reduced_chi2': float(problem.chisq(nllf=nllf, norm=True)), + 'parameter_values': parameter_values, + 'refresh_plots': False, + 'finished': False, + 'sampling': True, + } + def _set_parameter_fit_result( self, fit_result, diff --git a/src/easyscience/fitting/multi_fitter.py b/src/easyscience/fitting/multi_fitter.py index bc846e7a..c8d7c871 100644 --- a/src/easyscience/fitting/multi_fitter.py +++ b/src/easyscience/fitting/multi_fitter.py @@ -164,6 +164,8 @@ def sample( seed: int | None = None, vectorized: bool = False, sampler_kwargs: dict | None = None, + progress_callback: Callable[[dict], bool | None] | None = None, + abort_test: Callable[[], bool] | None = None, ) -> Dict: """Run Bayesian MCMC sampling using the BUMPS DREAM sampler. @@ -196,7 +198,10 @@ def sample( :param sampler_kwargs: Additional keyword arguments forwarded to the BUMPS DREAM sampler via :func:`bumps.fitters.fit`. :type sampler_kwargs: dict | None - :return: Dictionary with keys ``'draws'``, ``'param_names'``, ``'state'``, + :param progress_callback: Optional callback for progress updates during + sampling. The payload dict includes ``iteration`` (DREAM generation + number) and ``sampling: True``. + :return: Dictionary with keys ``'draws'``, ``'param_names'``, ``'state'`", and ``'logp'``. :rtype: dict :raises RuntimeError: If the current minimizer is not a BUMPS instance. @@ -251,6 +256,8 @@ def sample( population=pop, seed=seed, sampler_kwargs=sampler_kwargs, + progress_callback=progress_callback, + abort_test=abort_test, ) finally: self.fit_function = original_fit_func From 181d43dff869462e2f421d17913233c5ba066187 Mon Sep 17 00:00:00 2001 From: Piotr Rozyczko Date: Sun, 10 May 2026 08:51:03 +0200 Subject: [PATCH 03/15] use absolute paths for wheel --- .github/workflows/test.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 05998190..2bd3fab7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -217,10 +217,10 @@ jobs: exit 1 fi - whl_url="file://$(python -c 'import os,sys; print(os.path.abspath(sys.argv[1]))' "${whl_path[0]}")" + whl_path=$(python -c 'import os,sys; print(os.path.relpath(os.path.abspath(sys.argv[1]), os.getcwd()))' "${whl_path[0]}") - echo "Adding easyscience from: $whl_url" - pixi add --pypi "easyscience[dev] @ ${whl_url}" + echo "Adding easyscience from local wheel: $whl_path" + pixi add --pypi "easyscience[dev] @ ${whl_path}" echo "Exiting pixi project directory" cd .. From 06cefc7fd1fdcfe06a29092736724210b4c0a677 Mon Sep 17 00:00:00 2001 From: Piotr Rozyczko Date: Sun, 10 May 2026 21:56:57 +0200 Subject: [PATCH 04/15] additional tests for bayesian --- .../minimizers/test_minimizer_bumps.py | 359 ++++++++++++++++++ tests/unit/fitting/test_multi_fitter.py | 255 +++++++++++++ 2 files changed, 614 insertions(+) diff --git a/tests/unit/fitting/minimizers/test_minimizer_bumps.py b/tests/unit/fitting/minimizers/test_minimizer_bumps.py index 6b587dd7..93ee68b6 100644 --- a/tests/unit/fitting/minimizers/test_minimizer_bumps.py +++ b/tests/unit/fitting/minimizers/test_minimizer_bumps.py @@ -672,3 +672,362 @@ def test_gen_fit_results_uses_nit_for_budget_check(self, minimizer: Bumps, monke domain_fit_results.message == 'Fit stopped: reached maximum optimizer steps (3); objective evaluated 2 times' ) + + +# =================================================================== +# Bumps.sample() — Bayesian DREAM sampling +# =================================================================== + + +class TestBumpsSample: + """Tests for the ``Bumps.sample()`` method and its helpers.""" + + # Sentinel value to signal "set fitter.state = None" in _setup_driver_mock + ABORT = object() + + @pytest.fixture + def minimizer(self) -> Bumps: + return Bumps( + obj='obj', + fit_function='fit_function', + minimizer_enum=MagicMock(package='bumps', method='amoeba'), + ) + + @pytest.fixture(autouse=True) + def _mock_bumps_internals(self, monkeypatch): + """Prevent sample() from constructing real BUMPS objects. + + ``sample()`` imports ``DreamFit`` and ``FitProblem`` from the real + ``bumps`` package internally, which would try to build real model + objects. We redirect those to mocks and also mock ``FitDriver`` + (which *is* a module-level import) so the whole flow stays under + test control. + + Also mock ``_make_model`` on the class so that the ``minimizer`` + fixture (which uses ``obj='obj'``) doesn't fail inside ``sample()``. + """ + import bumps.fitters + import bumps.names + + monkeypatch.setattr(bumps.fitters, 'DreamFit', MagicMock()) + monkeypatch.setattr(bumps.names, 'FitProblem', MagicMock(return_value=MagicMock())) + monkeypatch.setattr( + Bumps, '_make_model', MagicMock(return_value=MagicMock(return_value=MagicMock())) + ) + + def _setup_driver_mock( + self, monkeypatch, fitter_state_value=None, fit_result=None, fit_side_effect=None + ): + """Helper to create a mocked FitDriver with configurable behavior. + + :param fitter_state_value: If ``None``, ``driver.fitter.state`` will be + a regular MagicMock (non-None). Pass ``ABORT`` to set it to ``None`` + and simulate user abort. + """ + from easyscience import global_object + + global_object.stack.enabled = False + + mock_driver = MagicMock() + mock_driver.clip = MagicMock() + + if fit_side_effect is not None: + mock_driver.fit.side_effect = fit_side_effect + else: + mock_driver.fit.return_value = fit_result or (np.array([1.0]), 0.0) + + mock_driver.stderr = MagicMock(return_value=np.array([0.1])) + + if fitter_state_value is TestBumpsSample.ABORT: + mock_driver.fitter.state = None + else: + mock_state = MagicMock() + mock_state.draw.return_value.points = np.array([[1.0]]) + mock_state.logp = None + mock_driver.fitter.state = mock_state + + mock_FitDriver = MagicMock(return_value=mock_driver) + monkeypatch.setattr( + easyscience.fitting.minimizers.minimizer_bumps, 'FitDriver', mock_FitDriver + ) + return mock_FitDriver, mock_driver + + def test_sample_basic(self, minimizer: Bumps, monkeypatch) -> None: + """Verify that sample() returns a dict with expected keys.""" + mock_FitDriver, _ = self._setup_driver_mock(monkeypatch) + minimizer._make_model = MagicMock(return_value=MagicMock(return_value=MagicMock())) + + result = minimizer.sample( + x=np.array([1.0, 2.0]), + y=np.array([0.1, 0.2]), + weights=np.array([1.0, 1.0]), + samples=100, + burn=20, + thin=2, + population=5, + ) + + assert isinstance(result, dict) + assert 'draws' in result + assert 'param_names' in result + assert 'state' in result + assert 'logp' in result + mock_FitDriver.assert_called_once() + + def test_sample_with_progress_callback(self, minimizer: Bumps, monkeypatch) -> None: + """Verify progress callback is wired up as a monitor.""" + mock_FitDriver, _ = self._setup_driver_mock(monkeypatch) + minimizer._make_model = MagicMock(return_value=MagicMock(return_value=MagicMock())) + progress_callback = MagicMock() + + result = minimizer.sample( + x=np.array([1.0]), + y=np.array([0.1]), + weights=np.array([1.0]), + samples=10, + burn=5, + thin=1, + progress_callback=progress_callback, + ) + + assert result is not None + call_kwargs = mock_FitDriver.call_args.kwargs + assert 'monitors' in call_kwargs + assert len(call_kwargs['monitors']) == 1 + assert isinstance(call_kwargs['monitors'][0], BumpsProgressMonitor) + + def test_sample_aborted_by_user_raises_fit_error(self, minimizer: Bumps, monkeypatch) -> None: + """Verify that sampling abortion raises FitError.""" + self._setup_driver_mock(monkeypatch, fitter_state_value=TestBumpsSample.ABORT) + minimizer._make_model = MagicMock(return_value=MagicMock(return_value=MagicMock())) + + with pytest.raises(FitError, match='Sampling aborted by user'): + minimizer.sample(x=np.array([1.0]), y=np.array([0.1]), weights=np.array([1.0])) + + def test_sample_driver_exception_restores_parameters( + self, minimizer: Bumps, monkeypatch + ) -> None: + """Verify that a driver exception during sampling restores parameter values.""" + self._setup_driver_mock(monkeypatch, fit_side_effect=RuntimeError('driver failed')) + minimizer._make_model = MagicMock(return_value=MagicMock(return_value=MagicMock())) + minimizer._restore_parameter_values = MagicMock() + + with pytest.raises(RuntimeError, match='driver failed'): + minimizer.sample(x=np.array([1.0]), y=np.array([0.1]), weights=np.array([1.0])) + + minimizer._restore_parameter_values.assert_called_once() + + def test_sample_conflicting_population_raises(self, minimizer: Bumps) -> None: + with pytest.raises(ValueError, match='Conflicting population'): + minimizer.sample( + x=np.array([1.0]), + y=np.array([0.1]), + weights=np.array([1.0]), + chains=5, + population=10, + samples=10, + burn=0, + thin=1, + ) + + def test_sample_rejects_non_callable_callback(self, minimizer: Bumps, monkeypatch) -> None: + import bumps.names + + monkeypatch.setattr(bumps.names, 'FitProblem', MagicMock(return_value=MagicMock())) + minimizer._make_model = MagicMock(return_value=MagicMock(return_value=MagicMock())) + + with pytest.raises(ValueError, match='progress_callback must be callable'): + minimizer.sample( + x=np.array([1.0]), + y=np.array([0.1]), + weights=np.array([1.0]), + samples=10, + burn=5, + thin=1, + progress_callback='not-callable', + ) + + +# =================================================================== +# _build_sample_progress_payload +# =================================================================== + + +class TestBuildSampleProgressPayload: + @pytest.fixture + def minimizer(self) -> Bumps: + return Bumps( + obj='obj', + fit_function='fit_function', + minimizer_enum=MagicMock(package='bumps', method='amoeba'), + ) + + def test_payload_structure_and_sampling_flag(self, minimizer: Bumps) -> None: + b = minimizer + + mock_problem = MagicMock() + mock_problem.chisq.side_effect = [25.0, 12.5] + mock_problem.labels.return_value = ['palpha'] + mock_problem.getp.return_value = np.array([1.0]) + b._cached_pars = {'alpha': MagicMock(value=1.0)} + + payload = b._build_sample_progress_payload(mock_problem, 7, np.array([1.0]), 12.5) + + assert payload['iteration'] == 7 + assert payload['chi2'] == 25.0 + assert payload['reduced_chi2'] == 12.5 + assert payload['parameter_values'] == {'alpha': 1.0} + assert payload['sampling'] is True + assert payload['finished'] is False + assert payload['refresh_plots'] is False + + def test_payload_keys(self, minimizer: Bumps) -> None: + b = minimizer + mock_problem = MagicMock() + mock_problem.chisq.side_effect = [10.0, 5.0] + mock_problem.labels.return_value = ['pa'] + mock_problem.getp.return_value = np.array([5.0]) + b._cached_pars = {'a': MagicMock(value=5.0)} + + payload = b._build_sample_progress_payload(mock_problem, 1, np.array([5.0]), nllf=5.0) + + expected_keys = { + 'iteration', + 'chi2', + 'reduced_chi2', + 'parameter_values', + 'refresh_plots', + 'finished', + 'sampling', + } + assert set(payload.keys()) == expected_keys + + +# =================================================================== +# _set_parameter_fit_result with stack_status=True +# =================================================================== + + +class TestSetParameterFitResultWithStack: + @pytest.fixture + def minimizer(self) -> Bumps: + return Bumps( + obj='obj', + fit_function='fit_function', + minimizer_enum=MagicMock(package='bumps', method='amoeba'), + ) + + def test_stack_status_true_calls_begin_end_macro(self, minimizer: Bumps) -> None: + from easyscience import global_object + + global_object.stack.enabled = False + + minimizer._cached_pars = {'a': MagicMock(), 'b': MagicMock()} + minimizer._cached_pars['a'].value = 'old_a' + minimizer._cached_pars['b'].value = 'old_b' + minimizer._restore_parameter_values = MagicMock() + + mock_fit_result = MagicMock() + mock_fit_result.x = np.array([1.0, 2.0]) + mock_fit_result.dx = np.array([0.1, 0.2]) + + mock_par_a = MagicMock() + mock_par_a.name = 'pa' + mock_par_b = MagicMock() + mock_par_b.name = 'pb' + par_list = [mock_par_a, mock_par_b] + + minimizer._set_parameter_fit_result(mock_fit_result, True, par_list) + + assert minimizer._cached_pars['a'].value == 1.0 + assert minimizer._cached_pars['a'].error == 0.1 + assert minimizer._cached_pars['b'].value == 2.0 + assert minimizer._cached_pars['b'].error == 0.2 + minimizer._restore_parameter_values.assert_called_once() + + +# =================================================================== +# convert_to_par_object +# =================================================================== + + +class TestConvertToParObject: + def test_convert_parameter_object(self) -> None: + from easyscience.variable import Parameter + + param = Parameter('thickness', 42.0, min=0.0, max=100.0) + param.fixed = False + + result = Bumps.convert_to_par_object(param) + + # convert_to_par_object uses obj.unique_name which is auto-assigned + assert result.name.startswith('p') + assert result.value == 42.0 + assert result.bounds == (0.0, 100.0) + assert result.fixed is False + + def test_convert_fixed_parameter(self) -> None: + from easyscience.variable import Parameter + + param = Parameter('roughness', 5.0, min=0.0, max=20.0) + param.fixed = True + + result = Bumps.convert_to_par_object(param) + + assert result.name.startswith('p') + assert result.fixed is True + + +# =================================================================== +# fit() with abort_test +# =================================================================== + + +class TestFitWithAbortTest: + @pytest.fixture + def minimizer(self) -> Bumps: + return Bumps( + obj='obj', + fit_function='fit_function', + minimizer_enum=MagicMock(package='bumps', method='amoeba'), + ) + + def test_abort_test_passed_to_fit_driver(self, minimizer: Bumps, monkeypatch) -> None: + from easyscience import global_object + + global_object.stack.enabled = False + + mock_driver = MagicMock() + mock_driver.clip = MagicMock() + mock_driver.fit = MagicMock(return_value=(np.array([42.0]), 0.0)) + mock_driver.stderr = MagicMock(return_value=np.array([0.1])) + mock_driver.monitor_runner.history.step = [0] + mock_FitDriver = MagicMock(return_value=mock_driver) + monkeypatch.setattr( + easyscience.fitting.minimizers.minimizer_bumps, 'FitDriver', mock_FitDriver + ) + + mock_problem = MagicMock() + mock_problem._parameters = [] + monkeypatch.setattr( + easyscience.fitting.minimizers.minimizer_bumps, + 'FitProblem', + MagicMock(return_value=mock_problem), + ) + + minimizer._make_model = MagicMock(return_value=MagicMock(return_value=MagicMock())) + minimizer._gen_fit_results = MagicMock(return_value='result') + minimizer._resolve_fitclass = MagicMock(return_value=MagicMock(id='amoeba')) + minimizer._set_parameter_fit_result = MagicMock() + minimizer._cached_pars = {'a': MagicMock(value=1.0)} + minimizer._cached_pars_vals = {'a': (1.0, 0.0)} + + abort_test = MagicMock(return_value=False) + + minimizer.fit( + x=np.array([1.0]), y=np.array([2.0]), weights=np.array([1.0]), abort_test=abort_test + ) + + call_kwargs = mock_FitDriver.call_args.kwargs + assert callable(call_kwargs['abort_test']) + assert call_kwargs['abort_test'] is not (lambda: False) diff --git a/tests/unit/fitting/test_multi_fitter.py b/tests/unit/fitting/test_multi_fitter.py index a54a927b..f8d8495e 100644 --- a/tests/unit/fitting/test_multi_fitter.py +++ b/tests/unit/fitting/test_multi_fitter.py @@ -3,6 +3,7 @@ from unittest.mock import MagicMock +import numpy as np import pytest from easyscience import ObjBase @@ -61,3 +62,257 @@ def test_fit_progress_callback(self, multi_fitter: MultiFitter): max_evaluations=None, progress_callback=progress_callback, ) + + +# =================================================================== +# MultiFitter.sample() — Bayesian DREAM sampling +# =================================================================== + + +class TestMultiFitterSample: + @pytest.fixture + def multi_fitter(self, monkeypatch): + monkeypatch.setattr(Fitter, '_update_minimizer', MagicMock()) + fit_object_1 = Line(1.0, 0.5) + fit_object_2 = Line(2.0, 1.5) + return MultiFitter([fit_object_1, fit_object_2], [fit_object_1, fit_object_2]) + + def test_sample_basic(self, multi_fitter: MultiFitter): + """Verify sample() calls the minimizer's sample() and returns its result.""" + import numpy as np + + multi_fitter._precompute_reshaping = MagicMock( + return_value=('x_fit', 'x_new', 'y_new', 'weights', 'dims') + ) + multi_fitter._fit_function_wrapper = MagicMock(return_value='wrapped_fit_function') + multi_fitter._minimizer = MagicMock() + multi_fitter._minimizer.package = 'bumps' + expected_result = { + 'draws': np.array([[1.0]]), + 'param_names': ['a'], + 'state': 'stub', + 'logp': None, + } + multi_fitter._minimizer.sample = MagicMock(return_value=expected_result) + + result = multi_fitter.sample( + x=[np.array([1.0]), np.array([2.0])], + y=[np.array([0.1]), np.array([0.2])], + weights=[np.array([1.0]), np.array([1.0])], + samples=100, + burn=20, + thin=2, + population=5, + ) + + assert result == expected_result + multi_fitter._minimizer.sample.assert_called_once() + call_kwargs = multi_fitter._minimizer.sample.call_args.kwargs + assert call_kwargs['samples'] == 100 + assert call_kwargs['burn'] == 20 + assert call_kwargs['thin'] == 2 + assert call_kwargs['population'] == 5 + assert call_kwargs['progress_callback'] is None + + def test_sample_raises_if_not_bumps(self, multi_fitter: MultiFitter): + """sample() should raise RuntimeError if minimizer is not BUMPS.""" + multi_fitter._precompute_reshaping = MagicMock( + return_value=('x_fit', 'x_new', 'y_new', 'weights', 'dims') + ) + multi_fitter._fit_function_wrapper = MagicMock(return_value='wrapped_fit_function') + multi_fitter._minimizer = MagicMock() + multi_fitter._minimizer.package = 'lmfit' # Not bumps + + with pytest.raises(RuntimeError, match='Bayesian sampling requires a BUMPS minimizer'): + multi_fitter.sample( + x=[np.array([1.0])], + y=[np.array([0.1])], + weights=[np.array([1.0])], + ) + + def test_sample_with_progress_callback(self, multi_fitter: MultiFitter): + """Progress callback should be forwarded to minimizer.sample().""" + multi_fitter._precompute_reshaping = MagicMock( + return_value=('x_fit', 'x_new', 'y_new', 'weights', 'dims') + ) + multi_fitter._fit_function_wrapper = MagicMock(return_value='wrapped_fit_function') + multi_fitter._minimizer = MagicMock() + multi_fitter._minimizer.package = 'bumps' + multi_fitter._minimizer.sample = MagicMock( + return_value={'draws': [], 'param_names': [], 'state': None, 'logp': None} + ) + + progress_callback = MagicMock() + + multi_fitter.sample( + x=[np.array([1.0])], + y=[np.array([0.1])], + weights=[np.array([1.0])], + progress_callback=progress_callback, + ) + + kwargs = multi_fitter._minimizer.sample.call_args.kwargs + assert kwargs['progress_callback'] is progress_callback + + def test_sample_population_alias(self, multi_fitter: MultiFitter): + """chains parameter is aliased to population.""" + multi_fitter._precompute_reshaping = MagicMock( + return_value=('x_fit', 'x_new', 'y_new', 'weights', 'dims') + ) + multi_fitter._fit_function_wrapper = MagicMock(return_value='wrapped_fit_function') + multi_fitter._minimizer = MagicMock() + multi_fitter._minimizer.package = 'bumps' + multi_fitter._minimizer.sample = MagicMock( + return_value={'draws': [], 'param_names': [], 'state': None, 'logp': None} + ) + + multi_fitter.sample( + x=[np.array([1.0])], + y=[np.array([0.1])], + weights=[np.array([1.0])], + chains=7, # Should be forwarded as population=7 + ) + + kwargs = multi_fitter._minimizer.sample.call_args.kwargs + assert kwargs['population'] == 7 + assert kwargs['chains'] is None + + def test_sample_conflicting_population_raises(self, multi_fitter: MultiFitter): + multi_fitter._precompute_reshaping = MagicMock( + return_value=('x_fit', 'x_new', 'y_new', 'weights', 'dims') + ) + multi_fitter._fit_function_wrapper = MagicMock(return_value='wrapped_fit_function') + multi_fitter._minimizer = MagicMock() + multi_fitter._minimizer.package = 'bumps' + + with pytest.raises(ValueError, match='Conflicting population'): + multi_fitter.sample( + x=[np.array([1.0])], + y=[np.array([0.1])], + weights=[np.array([1.0])], + chains=5, + population=10, + ) + + def test_sample_restores_original_fit_function(self, multi_fitter: MultiFitter): + """After sample() completes (even on error) the original fit_function is restored.""" + original_ff = multi_fitter.fit_function + multi_fitter._precompute_reshaping = MagicMock( + return_value=('x_fit', 'x_new', 'y_new', 'weights', 'dims') + ) + multi_fitter._fit_function_wrapper = MagicMock(return_value='wrapped_fit_function') + multi_fitter._minimizer = MagicMock() + multi_fitter._minimizer.package = 'bumps' + multi_fitter._minimizer.sample = MagicMock(side_effect=RuntimeError('boom')) + + with pytest.raises(RuntimeError): + multi_fitter.sample( + x=[np.array([1.0])], + y=[np.array([0.1])], + weights=[np.array([1.0])], + ) + + assert multi_fitter.fit_function is original_ff + + +# =================================================================== +# MultiFitter._post_compute_reshaping +# =================================================================== + + +class TestPostComputeReshaping: + def test_splits_single_result_into_two(self): + """Verify _post_compute_reshaping splits combined result by dataset.""" + import numpy as np + + from easyscience.fitting.minimizers import FitResults + + fit_objects = [Line(1.0, 0.5), Line(2.0, 1.5)] + mf = MultiFitter(fit_objects, fit_objects) + + # Simulate a combined result + combined = FitResults() + combined.success = True + combined.x = np.array([0, 1, 2]) + combined.y_obs = np.array([0.5, 1.0, 1.5]) + combined.y_calc = np.array([0.51, 0.99, 1.51]) + combined.y_err = np.array([0.01, 0.02, 0.03]) + combined.p = {'pm': 1.0, 'pc': 0.5} + combined.p0 = {'pm': 0.0, 'pc': 0.0} + combined.n_evaluations = 100 + combined.iterations = 50 + combined.message = 'success' + combined.minimizer_engine = 'bumps' + combined.engine_result = 'engine_result' + + # Set dependent_dims as _precompute_reshaping would + mf._dependent_dims = [(2,), (1,)] + + x_input = [np.array([0.0, 1.0]), np.array([2.0])] + y_input = [np.array([0.5, 1.0]), np.array([1.5])] + + results = mf._post_compute_reshaping(combined, x_input, y_input) + + assert len(results) == 2 + assert results[0].success is True + assert results[1].success is True + assert len(results[0].y_obs) == 2 + assert len(results[1].y_obs) == 1 + assert results[0].total_results is combined + assert results[1].total_results is combined + assert np.allclose(results[0].y_calc, [0.51, 0.99]) + + def test_handles_single_dataset(self): + import numpy as np + + from easyscience.fitting.minimizers import FitResults + + fit_objects = [Line(1.0, 0.5)] + mf = MultiFitter(fit_objects, fit_objects) + + combined = FitResults() + combined.success = True + combined.y_obs = np.array([1.0, 2.0, 3.0]) + combined.y_calc = np.array([1.1, 2.1, 3.1]) + combined.y_err = np.array([0.1, 0.1, 0.1]) + combined.p = {'pm': 1.0} + combined.p0 = {'pm': 0.0} + combined.n_evaluations = 50 + combined.iterations = 25 + combined.message = 'ok' + combined.minimizer_engine = 'bumps' + combined.engine_result = 'er' + + mf._dependent_dims = [(3,)] + x_input = [np.array([0.0, 1.0, 2.0])] + y_input = [np.array([1.0, 2.0, 3.0])] + + results = mf._post_compute_reshaping(combined, x_input, y_input) + + assert len(results) == 1 + assert np.allclose(results[0].y_calc, [1.1, 2.1, 3.1]) + + +# =================================================================== +# MultiFitter._precompute_reshaping with weights=None +# =================================================================== + + +class TestPrecomputeReshaping: + def test_weights_all_none_returns_none(self): + """When all weights are None, _precompute_reshaping should return None for weights.""" + import numpy as np + + fit_objects = [Line(1.0, 0.5), Line(2.0, 1.5)] + mf = MultiFitter(fit_objects, fit_objects) + + x = [np.array([1.0, 2.0]), np.array([3.0])] + y = [np.array([1.5, 2.5]), np.array([4.5])] + weights = [None, None] + + x_fit, x_new, y_new, w_new, dims = MultiFitter._precompute_reshaping( + x, y, weights, vectorized=False + ) + + assert w_new is None + assert len(dims) == 2 From efd7ff176ab33f84d8a20843dfe18663560e39b8 Mon Sep 17 00:00:00 2001 From: rozyczko Date: Tue, 12 May 2026 13:51:12 +0200 Subject: [PATCH 05/15] proper docstrings --- .../fitting/minimizers/minimizer_bumps.py | 75 +++++++++++++----- src/easyscience/fitting/multi_fitter.py | 79 +++++++++++-------- 2 files changed, 102 insertions(+), 52 deletions(-) diff --git a/src/easyscience/fitting/minimizers/minimizer_bumps.py b/src/easyscience/fitting/minimizers/minimizer_bumps.py index 3479ff92..174b19f7 100644 --- a/src/easyscience/fitting/minimizers/minimizer_bumps.py +++ b/src/easyscience/fitting/minimizers/minimizer_bumps.py @@ -117,6 +117,9 @@ def fit( Optional callback for progress updates. The payload field ``iteration`` carries the BUMPS optimizer step index. By default, None. + abort_test : Callable[[], bool] | None, default=None + Optional callback that returns ``True`` to signal that sampling should be aborted. + Called periodically during the DREAM sampling loop. minimizer_kwargs : dict | None, default=None Additional keyword arguments passed to the BUMPS minimizer. By default, None. @@ -393,28 +396,58 @@ def sample( ) -> dict: """Run Bayesian MCMC sampling using the BUMPS DREAM sampler. - Builds a BUMPS :class:`~bumps.names.FitProblem` from the current - model and runs the DREAM sampler. This is the public minimizer-level - entry point for Bayesian sampling; the higher-level - :meth:`easyscience.fitting.multi_fitter.MultiFitter.sample` delegates - to this method after flattening multi-dataset arrays. - - :param x: Flattened independent variable array. - :param y: Flattened dependent variable array. - :param weights: Flattened weight array. - :param samples: Number of retained DREAM samples requested from BUMPS. - :param burn: Burn-in steps. - :param thin: Thinning interval. - :param chains: User-friendly alias for BUMPS DREAM population count. - :param population: BUMPS DREAM population count for advanced users. - :param seed: Best-effort random seed. - :param sampler_kwargs: Additional keyword arguments forwarded to - :func:`bumps.fitters.fit`. - :param progress_callback: Optional callback for progress updates during - sampling. The payload dict includes ``iteration`` (DREAM generation - number) and ``sampling: True``. - :return: Dictionary with keys ``'draws'``, ``'param_names'``, ``'state'`", + Builds a BUMPS `FitProblem` from the current model and runs the DREAM + sampler. This is the public minimizer-level entry point for Bayesian + sampling; the higher-level `MultiFitter.sample` delegates to this + method after flattening multi-dataset arrays. + + Parameters + ---------- + x : np.ndarray + Flattened independent variable array. + y : np.ndarray + Flattened dependent variable array. + weights : np.ndarray + Flattened weight array. + samples : int, default=10000 + Number of retained DREAM samples requested from BUMPS. + burn : int, default=2000 + Burn-in steps. + thin : int, default=10 + Thinning interval. + chains : int | None, default=None + User-friendly alias for BUMPS DREAM population count. + population : int | None, default=None + BUMPS DREAM population count for advanced users. + seed : int | None, default=None + Best-effort random seed. + sampler_kwargs : dict | None, default=None + Additional keyword arguments forwarded to `bumps.fitters.fit`. + progress_callback : Callable[[dict], bool | None] | None, default=None + Optional callback for progress updates during sampling. The + payload dict includes ``iteration`` (DREAM generation number) and + ``sampling: True``. + abort_test : Callable[[], bool] | None, default=None + Optional callback that returns ``True`` to signal that sampling + should be aborted. Called periodically during the DREAM sampling + loop. + + Returns + ------- + dict + Dictionary with keys ``'draws'``, ``'param_names'``, ``'state'``, and ``'logp'``. + + Raises + ------ + ValueError + If both ``chains`` and ``population`` are provided with different + values, or if ``progress_callback`` is not callable. + FitError + If DREAM sampling was aborted by the user (via ``abort_test``). + Exception + Re-raised from DREAM fitting if any unexpected error occurs + (parameter values are restored beforehand). """ from bumps.fitters import DreamFit from bumps.names import FitProblem diff --git a/src/easyscience/fitting/multi_fitter.py b/src/easyscience/fitting/multi_fitter.py index fd6655be..13a79350 100644 --- a/src/easyscience/fitting/multi_fitter.py +++ b/src/easyscience/fitting/multi_fitter.py @@ -211,39 +211,56 @@ def sample( Requires that the current minimizer is a BUMPS instance (i.e. the minimizer was switched to ``AvailableMinimizers.Bumps`` or equivalent). - :param x: List of independent variable arrays (one per dataset). - :type x: List[np.ndarray] - :param y: List of dependent variable arrays (one per dataset). - :type y: List[np.ndarray] - :param weights: List of weight arrays (one per dataset). - :type weights: List[np.ndarray] - :param samples: Number of retained DREAM samples requested from BUMPS. - :type samples: int - :param burn: Burn-in steps. - :type burn: int - :param thin: Thinning interval. - :type thin: int - :param chains: User-friendly alias for BUMPS DREAM population count. - :type chains: int | None - :param population: BUMPS DREAM population count (``pop``) for advanced users. - :type population: int | None - :param seed: Best-effort random seed. BUMPS DREAM may use additional - internal RNG state that is not controlled by this seed, so exact + Parameters + ---------- + x : List[np.ndarray] + List of independent variable arrays (one per dataset). + y : List[np.ndarray] + List of dependent variable arrays (one per dataset). + weights : List[np.ndarray] + List of weight arrays (one per dataset). + samples : int, default=10000 + Number of retained DREAM samples requested from BUMPS. + burn : int, default=1000 + Burn-in steps. + thin : int, default=10 + Thinning interval. + chains : int | None, default=None + User-friendly alias for BUMPS DREAM population count. + population : int | None, default=None + BUMPS DREAM population count (``pop``) for advanced users. + seed : int | None, default=None + Best-effort random seed. BUMPS DREAM may use additional internal + RNG state that is not controlled by this seed, so exact reproducibility is not guaranteed. - :type seed: int | None - :param vectorized: Whether the fit function expects vectorized - (multidimensional) input. Defaults to ``False``. - :type vectorized: bool - :param sampler_kwargs: Additional keyword arguments forwarded to the - BUMPS DREAM sampler via :func:`bumps.fitters.fit`. - :type sampler_kwargs: dict | None - :param progress_callback: Optional callback for progress updates during - sampling. The payload dict includes ``iteration`` (DREAM generation - number) and ``sampling: True``. - :return: Dictionary with keys ``'draws'``, ``'param_names'``, ``'state'`", + vectorized : bool, default=False + Whether the fit function expects vectorized (multidimensional) + input. + sampler_kwargs : dict | None, default=None + Additional keyword arguments forwarded to the BUMPS DREAM sampler + via `bumps.fitters.fit`. + progress_callback : Callable[[dict], bool | None] | None, default=None + Optional callback for progress updates during sampling. The + payload dict includes ``iteration`` (DREAM generation number) and + ``sampling: True``. + abort_test : Callable[[], bool] | None, default=None + Optional callback that returns ``True`` to signal that sampling + should be aborted. Called periodically during the DREAM sampling + loop. + + Returns + ------- + Dict + Dictionary with keys ``'draws'``, ``'param_names'``, ``'state'``, and ``'logp'``. - :rtype: dict - :raises RuntimeError: If the current minimizer is not a BUMPS instance. + + Raises + ------ + RuntimeError + If the current minimizer is not a BUMPS instance. + ValueError + If both ``chains`` and ``population`` are provided with different + values. """ # --- Alias resolution --- if chains is not None and population is not None: From 2913ca64c2194e22d25dd207932c993422f5b608 Mon Sep 17 00:00:00 2001 From: Piotr Rozyczko Date: Tue, 12 May 2026 17:11:08 +0200 Subject: [PATCH 06/15] we need `legacy` folder in the wheel --- pixi.lock | 13 +++---------- pyproject.toml | 5 ++--- 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/pixi.lock b/pixi.lock index 83a140f2..d22ec2c2 100644 --- a/pixi.lock +++ b/pixi.lock @@ -6,8 +6,6 @@ environments: - url: https://conda.anaconda.org/conda-forge/ indexes: - https://pypi.org/simple - options: - pypi-prerelease-mode: if-necessary-or-explicit packages: linux-64: - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda @@ -811,8 +809,6 @@ environments: - url: https://conda.anaconda.org/conda-forge/ indexes: - https://pypi.org/simple - options: - pypi-prerelease-mode: if-necessary-or-explicit packages: linux-64: - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda @@ -1606,8 +1602,6 @@ environments: - url: https://conda.anaconda.org/conda-forge/ indexes: - https://pypi.org/simple - options: - pypi-prerelease-mode: if-necessary-or-explicit packages: linux-64: - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda @@ -2411,8 +2405,6 @@ environments: - url: https://conda.anaconda.org/conda-forge/ indexes: - https://pypi.org/simple - options: - pypi-prerelease-mode: if-necessary-or-explicit packages: linux-64: - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda @@ -4174,8 +4166,8 @@ packages: requires_python: '>=3.11' - pypi: ./ name: easyscience - version: 2.3.1+devdirty16 - sha256: cfdcbbdca748c028ca05d142a93d60388cabb47e3afe6995d9bdb029a82ed28b + version: 2.3.1+devdirty11 + sha256: 9046eb543a77a4603204f7e67b55f0f0e82840101820ef1051cbfc23766ed4f0 requires_dist: - asteval - bumps @@ -4222,6 +4214,7 @@ packages: - validate-pyproject[all] ; extra == 'dev' - versioningit ; extra == 'dev' requires_python: '>=3.11' + editable: true - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda sha256: ee6cf346d017d954255bbcbdb424cddea4d14e4ed7e9813e429db1d795d01144 md5: 8e662bd460bda79b1ea39194e3c4c9ab diff --git a/pyproject.toml b/pyproject.toml index bdce82da..53acf732 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -96,7 +96,7 @@ requires = ['hatchling', 'versioningit'] [tool.hatch.build.targets.wheel] packages = ['src/easyscience'] -exclude = ['src/easyscience/legacy'] +# exclude = ['src/easyscience/legacy'] [tool.hatch.metadata] allow-direct-references = true @@ -211,8 +211,7 @@ select = [ # Ignore specific rules globally ignore = [ 'COM812', # https://docs.astral.sh/ruff/rules/missing-trailing-comma/ - # The following is replaced by 'D'/[tool.ruff.lint.pydocstyle] and [tool.pydoclint] - 'DOC', # https://docs.astral.sh/ruff/rules/#pydoclint-doc + # The following is replaced by 'D'/[tool.ruff.lint.pydocstyle] and [tool.pydoclint] 'DOC', # https://docs.astral.sh/ruff/rules/#pydoclint-doc # Disable, as [tool.format_docstring] split one-line docstrings into the canonical multi-line layout 'D200', # https://docs.astral.sh/ruff/rules/unnecessary-multiline-docstring/ ] From 8aa06c1645ff3d21346fe3387889879c100ea05d Mon Sep 17 00:00:00 2001 From: rozyczko Date: Wed, 13 May 2026 11:59:14 +0200 Subject: [PATCH 07/15] PR review issues addressed --- pixi.lock | 698 +++++++++++++----- .../fitting/minimizers/minimizer_bumps.py | 27 +- src/easyscience/fitting/multi_fitter.py | 4 +- 3 files changed, 542 insertions(+), 187 deletions(-) diff --git a/pixi.lock b/pixi.lock index d22ec2c2..f84d1522 100644 --- a/pixi.lock +++ b/pixi.lock @@ -6,6 +6,8 @@ environments: - url: https://conda.anaconda.org/conda-forge/ indexes: - https://pypi.org/simple + options: + pypi-prerelease-mode: if-necessary-or-explicit packages: linux-64: - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda @@ -162,7 +164,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4b/32/e0f13a1c5b0f8572d0ec6ae2f6c677b7991fafd95da523159c19eff0696a/contourpy-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/98/ba/fc20faf5b2bb04615fa906f8daecfe896e6f7a56b34debd93c4a4d63e6e5/copier-9.15.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ac/68/1666e3a4462f8202d836920114fa7a5ee9275d1fa45366d336c551a162dd/coverage-7.13.5-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/6f/5f/b5370068b2f57787454592ed7dcd1002f0f1703b7db1fa30f6a325a4ca6e/coverage-7.14.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c7/a0/5ff05d1919ca249508012cad89f08fdc6cfbdaa15b41651c5fe6dffaf1d3/dfo_ls-1.6.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl @@ -197,14 +199,14 @@ environments: - pypi: https://files.pythonhosted.org/packages/e0/07/a000fe835f76b7e1143242ab1122e6362ef1c03f23f83a045c38859c2ae0/jupyterlab_server-2.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ab/b5/36c712098e6191d1b4e349304ef73a8d06aed77e56ceaac8c0a306c7bda1/jupyterlab_widgets-3.0.16-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b3/52/bc858b1665d0dec3a2511f4e6f5c18ea85c0977563d624d597c95d6d0fd7/jupyterquiz-2.9.6.4-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/16/5a/736dd2f4535dbf3bf26523f9158c011389ef88dd06ec2eef67fd744f1c7b/jupytext-1.19.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4c/65/b4b86e5fa07543bfbbcdc6c9f7f9f561e66a5f3539992e3009973f2b1314/jupytext-1.19.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2b/0a/7b98e1e119878a27ba8618ca1e18b14f992ff1eda40f47bccccf4de44121/kiwisolver-1.5.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - pypi: https://files.pythonhosted.org/packages/38/7e/7b91c89a4cf0f543a83be978657afb20c86af6d725253e319589dcc4ce52/lmfit-1.3.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d2/f0/834e479e47e499b6478e807fb57b31cc2db696c4db30557bb6f5aea4a90b/mando-0.7.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a8/88/802c82060c54bc7dde21eb0033e337838b8181a1323254aa9ec41cbfc3d1/markdown_it_py-4.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8a/17/4402d0d14ccf1dfc70932600b68097fbbf9c898a4871d2cbbe79c7801a32/matplotlib-3.10.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/fb/86/dd6e5db36df29e76c7a7699123569a4a18c1623ce68d826ed96c62643cae/mdit_py_plugins-0.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/76/8e/56ccb09c7232a55403a7637caa21922f3b65901a37f5e8bdb405d0de0946/mike-2.2.0-py3-none-any.whl @@ -221,7 +223,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/5b/69/93b34728cc386efdde0c342f8c680b9187dea7beb7adaf6b58a0713be101/mpld3-0.5.12-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5d/ba/459f18c16f2b3fc1a1ca871f72f07d70c07bf768ad0a507a698b8052ac58/msgpack-1.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/b0/73/6e1b01cbeb458807aa0831742232dbdd1fa92bfa33f52a3f176b4ff3dc11/multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/d0/69/f24d3d1c38ad69e256138b4ec2452a8c7cf66be49dc214771ae99dd4f0a0/narwhals-2.20.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c7/e1/68c2256b69a314eba133673377ba9118c356f6342a0c02b61de449cf2bf2/narwhals-2.21.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/eb/be/b257e12f9710819fde40adc972578bee6b72c5992da1bc8369bef2597756/nbmake-1.5.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/28/88/4789719fbbe166d12d345b3ac66b96105f10001b16e00a9765ba29261a21/nbqa-1.9.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/20/16/e777eadfa0c0305878c36fae1d5e6db474fbb15dae202b9ec378809dfb4d/nbstripout-0.9.1-py3-none-any.whl @@ -229,7 +231,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/f9/33/bd5b9137445ea4b680023eb0469b2bb969d61303dedb2aac6560ff3d14a1/notebook_shim-0.2.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d1/73/a9d864e42a01896bb5974475438f16086be9ba1f0d19d0bb7a07427c4a8b/numpy-2.4.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/12/c5/cbb1ffefb20a93d3f0e1fdcda699fb84976210d411b008f97f48bf6ce27e/pandas-3.0.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/99/68/1237369725aa617bb358263d535803e3053fdbc593513ec5ed9c9896b5b6/pandas-3.0.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/67/ee/21d4e8536afd1a328f01b359b4d3997b291ffd35a237c877b331c1c3b71c/pillow-12.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/3a/eb/fea4d1d51c49832120f7f285d07306db3960f423a2612c6057caf3e8196f/pip-26.1.1-py3-none-any.whl @@ -238,7 +240,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/79/ad/45312df6b63ba64ea35b8d8f5f0c577aac16e6b416eafe8e1cb34e03f9a7/plumbum-1.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2a/2d/d4bf65e47cea8ff2c794a600c4fd1273a7902f268757c531e0ee9f18aa58/pooch-1.9.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f1/8b/544bc867e24e1bd48f3118cecd3b05c694e160a168478fa28770f22fd094/propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/8b/c6/979176efdaa3d239e36d503d5af63a0a773b36662ed8f52e5b6a6d9fd40e/propcache-0.5.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/f6/f0/10642828a8dfb741e5f3fbaac830550a518a775c7fff6f04a007259b0548/py-1.11.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl @@ -250,13 +252,13 @@ environments: - pypi: https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/30/d4/24d543ab8b8158b7f5a97113c831205f5c900c92c8762b1e7f44b7ea0405/python_discovery-1.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/6f/a05a317a66fee0aad270011461f1a63a453ed12471249f172f7d2e2bc7b4/python_discovery-1.3.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/aa/54/0cce26da03a981f949bb8449c9778537f75f5917c172e1d2992ff25cb57d/python_engineio-4.13.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/07/c7/deb8c5e604404dbf10a3808a858946ca3547692ff6316b698945bb72177e/python_socketio-5.16.1-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/3c/26/1062c7ec1b053db9e499b4d2d5bc231743201b74051c973dadeac80a8f43/questionary-2.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/93/f7/d00d9b4a0313a6be3a3e0818e6375e15da6d7076f4ae47d1324e7ca986a1/radon-6.0.1-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ef/e6/e300fce5fe83c30520607a015dabd985df3251e188d234bfe9492e17a389/requests-2.34.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e8/39/c61d193b8a1daaa8977f7dea9e8d8ba866e02ea7b65d32f6861693aa4c12/ruff-0.15.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/2e/75/5604f4d17ab607510d4702f156329194d8edfff7e29644ca9200b085e9a2/scipp-26.3.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/f5/5f/f17563f28ff03c7b6799c50d01d5d856a1d55f2676f537ca8d28c7f627cd/scipy-1.17.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl @@ -266,14 +268,14 @@ environments: - pypi: https://files.pythonhosted.org/packages/3e/17/1f31d8562e6f970d64911f1abc330d233bc0c0601411cf7e19c1292be6da/spdx_headers-1.5.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/33/f0/3fe8c6e69135a845f4106f2ff8b6805638d4e85c264e70114e8126689587/tokenize_rt-6.2.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d6/bb/fbdc4e57731efb86b4209ffdd2519520782bf27b3c961beac3e5c20d2b87/trove_classifiers-2026.4.28.13-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7b/e3/d81b065a2d866a33a541ac63a2a4cc5737e03ce2379ac3191c98bb8867e3/trove_classifiers-2026.5.7.17-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8f/5e/f1e1dd319e35e962a4e00b33150a8868b6329cc1d19fd533436ba5488f09/uncertainties-3.2.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b7/ee/e9c95cda829131f71a8dff5ce0406059fd16e591c074414e31ada19ba7c3/validate_pyproject-0.25-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1c/59/964ecb8008722d27d8a835baea81f56a91cea8e097b3be992bc6ccde6367/versioningit-3.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a4/ce/3b6fee91c85626eaf769d617f1be9d2e15c1cca027bbdeb2e0d751469355/verspec-0.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b1/4f/f71e641e504111a5a74e3a20bc52d01bd86788b22699dd3fee1c63253cf6/virtualenv-21.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/20/5b/885f479093f6627669d39b57bc3d4e674da532e1a4b247d473a61d8d2118/virtualenv-21.3.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/3f/0e/fa3b193432cfc60c93b42f3be03365f5f909d2b3ea410295cf36df739e31/widgetsnbextension-4.0.15-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-py3-none-any.whl @@ -431,7 +433,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/96/e4/7adcd9c8362745b2210728f209bfbcf7d91ba868a2c5f40d8b58f54c509b/contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/98/ba/fc20faf5b2bb04615fa906f8daecfe896e6f7a56b34debd93c4a4d63e6e5/copier-9.15.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/0c/c9/44fb661c55062f0818a6ffd2685c67aa30816200d5f2817543717d4b92eb/coverage-7.13.5-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/b3/af/e567cbad5ba69c013a50146dfa886dc7193361fda77521f51274ff620e1b/coverage-7.14.0-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c7/a0/5ff05d1919ca249508012cad89f08fdc6cfbdaa15b41651c5fe6dffaf1d3/dfo_ls-1.6.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl @@ -466,14 +468,14 @@ environments: - pypi: https://files.pythonhosted.org/packages/e0/07/a000fe835f76b7e1143242ab1122e6362ef1c03f23f83a045c38859c2ae0/jupyterlab_server-2.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ab/b5/36c712098e6191d1b4e349304ef73a8d06aed77e56ceaac8c0a306c7bda1/jupyterlab_widgets-3.0.16-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b3/52/bc858b1665d0dec3a2511f4e6f5c18ea85c0977563d624d597c95d6d0fd7/jupyterquiz-2.9.6.4-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/16/5a/736dd2f4535dbf3bf26523f9158c011389ef88dd06ec2eef67fd744f1c7b/jupytext-1.19.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4c/65/b4b86e5fa07543bfbbcdc6c9f7f9f561e66a5f3539992e3009973f2b1314/jupytext-1.19.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a8/3a/d0a972b34e1c63e2409413104216cd1caa02c5a37cb668d1687d466c1c45/kiwisolver-1.5.0-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/38/7e/7b91c89a4cf0f543a83be978657afb20c86af6d725253e319589dcc4ce52/lmfit-1.3.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d2/f0/834e479e47e499b6478e807fb57b31cc2db696c4db30557bb6f5aea4a90b/mando-0.7.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a8/88/802c82060c54bc7dde21eb0033e337838b8181a1323254aa9ec41cbfc3d1/markdown_it_py-4.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/63/d9/9e14bc7564bf92d5ffa801ae5fac819ce74b925dfb55e3ebde61a3bbad3e/matplotlib-3.10.9-cp313-cp313-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/fb/86/dd6e5db36df29e76c7a7699123569a4a18c1623ce68d826ed96c62643cae/mdit_py_plugins-0.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/76/8e/56ccb09c7232a55403a7637caa21922f3b65901a37f5e8bdb405d0de0946/mike-2.2.0-py3-none-any.whl @@ -490,7 +492,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/5b/69/93b34728cc386efdde0c342f8c680b9187dea7beb7adaf6b58a0713be101/mpld3-0.5.12-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/92/dc/c385f38f2c2433333345a82926c6bfa5ecfff3ef787201614317b58dd8be/msgpack-1.1.2-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/79/76/55cd7186f498ed080a18440c9013011eb548f77ae1b297206d030eb1180a/multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/d0/69/f24d3d1c38ad69e256138b4ec2452a8c7cf66be49dc214771ae99dd4f0a0/narwhals-2.20.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c7/e1/68c2256b69a314eba133673377ba9118c356f6342a0c02b61de449cf2bf2/narwhals-2.21.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/eb/be/b257e12f9710819fde40adc972578bee6b72c5992da1bc8369bef2597756/nbmake-1.5.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/28/88/4789719fbbe166d12d345b3ac66b96105f10001b16e00a9765ba29261a21/nbqa-1.9.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/20/16/e777eadfa0c0305878c36fae1d5e6db474fbb15dae202b9ec378809dfb4d/nbstripout-0.9.1-py3-none-any.whl @@ -498,7 +500,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/f9/33/bd5b9137445ea4b680023eb0469b2bb969d61303dedb2aac6560ff3d14a1/notebook_shim-0.2.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e5/87/499737bfba066b4a3bebff24a8f1c5b2dee410b209bc6668c9be692580f0/numpy-2.4.4-cp313-cp313-macosx_14_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/0b/77/dbc82ff2fb0e63c6564356682bf201edff0ba16c98630d21a1fb312a8182/pandas-3.0.2-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/7f/ae/6a6493c783a101f165e4356953ba3c74d6f77f0042fa7d753da9dfbb640c/pandas-3.0.3-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/71/43/905a14a8b17fdb1ccb58d282454490662d2cb89a6bfec26af6d3520da5ec/pillow-12.2.0-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/3a/eb/fea4d1d51c49832120f7f285d07306db3960f423a2612c6057caf3e8196f/pip-26.1.1-py3-none-any.whl @@ -507,7 +509,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/79/ad/45312df6b63ba64ea35b8d8f5f0c577aac16e6b416eafe8e1cb34e03f9a7/plumbum-1.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2a/2d/d4bf65e47cea8ff2c794a600c4fd1273a7902f268757c531e0ee9f18aa58/pooch-1.9.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/89/a4/92380f7ca60f99ebae761936bc48a72a639e8a47b29050615eef757cb2a7/propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/a9/12/a6ba6482bb5ea3260c000c9b20881c95fa11c6b30173715668259f844ed7/propcache-0.5.2-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/f6/f0/10642828a8dfb741e5f3fbaac830550a518a775c7fff6f04a007259b0548/py-1.11.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl @@ -519,13 +521,13 @@ environments: - pypi: https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/30/d4/24d543ab8b8158b7f5a97113c831205f5c900c92c8762b1e7f44b7ea0405/python_discovery-1.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/6f/a05a317a66fee0aad270011461f1a63a453ed12471249f172f7d2e2bc7b4/python_discovery-1.3.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/aa/54/0cce26da03a981f949bb8449c9778537f75f5917c172e1d2992ff25cb57d/python_engineio-4.13.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/07/c7/deb8c5e604404dbf10a3808a858946ca3547692ff6316b698945bb72177e/python_socketio-5.16.1-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/3c/26/1062c7ec1b053db9e499b4d2d5bc231743201b74051c973dadeac80a8f43/questionary-2.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/93/f7/d00d9b4a0313a6be3a3e0818e6375e15da6d7076f4ae47d1324e7ca986a1/radon-6.0.1-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ef/e6/e300fce5fe83c30520607a015dabd985df3251e188d234bfe9492e17a389/requests-2.34.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/aa/a4/f828e9718d3dce1f5f11c39c4f65afd32783c8b2aebb2e3d259e492c47bd/ruff-0.15.12-py3-none-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/79/fe/b14d806894cf05178f1e77d0d619f071db50cf698bc654c54f9241223bcf/scipp-26.3.1-cp313-cp313-macosx_14_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/5b/58/3ce96251560107b381cbd6e8413c483bbb1228a6b919fa8652b0d4090e7f/scipy-1.17.1-cp313-cp313-macosx_14_0_arm64.whl @@ -534,14 +536,14 @@ environments: - pypi: https://files.pythonhosted.org/packages/3e/17/1f31d8562e6f970d64911f1abc330d233bc0c0601411cf7e19c1292be6da/spdx_headers-1.5.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/33/f0/3fe8c6e69135a845f4106f2ff8b6805638d4e85c264e70114e8126689587/tokenize_rt-6.2.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d6/bb/fbdc4e57731efb86b4209ffdd2519520782bf27b3c961beac3e5c20d2b87/trove_classifiers-2026.4.28.13-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7b/e3/d81b065a2d866a33a541ac63a2a4cc5737e03ce2379ac3191c98bb8867e3/trove_classifiers-2026.5.7.17-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8f/5e/f1e1dd319e35e962a4e00b33150a8868b6329cc1d19fd533436ba5488f09/uncertainties-3.2.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b7/ee/e9c95cda829131f71a8dff5ce0406059fd16e591c074414e31ada19ba7c3/validate_pyproject-0.25-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1c/59/964ecb8008722d27d8a835baea81f56a91cea8e097b3be992bc6ccde6367/versioningit-3.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a4/ce/3b6fee91c85626eaf769d617f1be9d2e15c1cca027bbdeb2e0d751469355/verspec-0.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b1/4f/f71e641e504111a5a74e3a20bc52d01bd86788b22699dd3fee1c63253cf6/virtualenv-21.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/20/5b/885f479093f6627669d39b57bc3d4e674da532e1a4b247d473a61d8d2118/virtualenv-21.3.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/3f/0e/fa3b193432cfc60c93b42f3be03365f5f909d2b3ea410295cf36df739e31/widgetsnbextension-4.0.15-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-py3-none-any.whl @@ -686,7 +688,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/18/0b/0098c214843213759692cc638fce7de5c289200a830e5035d1791d7a2338/contourpy-1.3.3-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/98/ba/fc20faf5b2bb04615fa906f8daecfe896e6f7a56b34debd93c4a4d63e6e5/copier-9.15.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/66/40/7732d648ab9d069a46e686043241f01206348e2bbf128daea85be4d6414b/coverage-7.13.5-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/8f/b8/9228523e80321c2cb4880d1f589bc0171f2f71432c35118ad04dc01decce/coverage-7.14.0-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c7/a0/5ff05d1919ca249508012cad89f08fdc6cfbdaa15b41651c5fe6dffaf1d3/dfo_ls-1.6.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl @@ -721,14 +723,14 @@ environments: - pypi: https://files.pythonhosted.org/packages/e0/07/a000fe835f76b7e1143242ab1122e6362ef1c03f23f83a045c38859c2ae0/jupyterlab_server-2.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ab/b5/36c712098e6191d1b4e349304ef73a8d06aed77e56ceaac8c0a306c7bda1/jupyterlab_widgets-3.0.16-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b3/52/bc858b1665d0dec3a2511f4e6f5c18ea85c0977563d624d597c95d6d0fd7/jupyterquiz-2.9.6.4-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/16/5a/736dd2f4535dbf3bf26523f9158c011389ef88dd06ec2eef67fd744f1c7b/jupytext-1.19.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4c/65/b4b86e5fa07543bfbbcdc6c9f7f9f561e66a5f3539992e3009973f2b1314/jupytext-1.19.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/be/8a/be60e3bbcf513cc5a50f4a3e88e1dcecebb79c1ad607a7222877becaa101/kiwisolver-1.5.0-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/38/7e/7b91c89a4cf0f543a83be978657afb20c86af6d725253e319589dcc4ce52/lmfit-1.3.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d2/f0/834e479e47e499b6478e807fb57b31cc2db696c4db30557bb6f5aea4a90b/mando-0.7.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a8/88/802c82060c54bc7dde21eb0033e337838b8181a1323254aa9ec41cbfc3d1/markdown_it_py-4.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c5/e0/0840fd2f93da988ec660b8ad1984abe9f25d2aed22a5e394ff1c68c88307/matplotlib-3.10.9-cp313-cp313-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/fb/86/dd6e5db36df29e76c7a7699123569a4a18c1623ce68d826ed96c62643cae/mdit_py_plugins-0.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/76/8e/56ccb09c7232a55403a7637caa21922f3b65901a37f5e8bdb405d0de0946/mike-2.2.0-py3-none-any.whl @@ -745,7 +747,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/5b/69/93b34728cc386efdde0c342f8c680b9187dea7beb7adaf6b58a0713be101/mpld3-0.5.12-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/74/07/1ed8277f8653c40ebc65985180b007879f6a836c525b3885dcc6448ae6cb/msgpack-1.1.2-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/b2/35/e994121b0e90e46134673422dd564623f93304614f5d11886b1b3e06f503/multidict-6.7.1-cp313-cp313-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/d0/69/f24d3d1c38ad69e256138b4ec2452a8c7cf66be49dc214771ae99dd4f0a0/narwhals-2.20.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c7/e1/68c2256b69a314eba133673377ba9118c356f6342a0c02b61de449cf2bf2/narwhals-2.21.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/eb/be/b257e12f9710819fde40adc972578bee6b72c5992da1bc8369bef2597756/nbmake-1.5.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/28/88/4789719fbbe166d12d345b3ac66b96105f10001b16e00a9765ba29261a21/nbqa-1.9.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/20/16/e777eadfa0c0305878c36fae1d5e6db474fbb15dae202b9ec378809dfb4d/nbstripout-0.9.1-py3-none-any.whl @@ -753,7 +755,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/f9/33/bd5b9137445ea4b680023eb0469b2bb969d61303dedb2aac6560ff3d14a1/notebook_shim-0.2.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/57/fb/48649b4971cde70d817cf97a2a2fdc0b4d8308569f1dd2f2611959d2e0cf/numpy-2.4.4-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/5e/52/42855bf626868413f761addd574acc6195880ae247a5346477a4361c3acb/pandas-3.0.2-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/2c/20/559ace4200982c3887d0b86bfd0d856a2143ef8ddab63cc07934951a964c/pandas-3.0.3-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3f/82/8a3739a5e470b3c6cbb1d21d315800d8e16bff503d1f16b03a4ec3212786/pillow-12.2.0-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/3a/eb/fea4d1d51c49832120f7f285d07306db3960f423a2612c6057caf3e8196f/pip-26.1.1-py3-none-any.whl @@ -762,7 +764,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/79/ad/45312df6b63ba64ea35b8d8f5f0c577aac16e6b416eafe8e1cb34e03f9a7/plumbum-1.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2a/2d/d4bf65e47cea8ff2c794a600c4fd1273a7902f268757c531e0ee9f18aa58/pooch-1.9.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f5/ab/f76ec3c3627c883215b5c8080debb4394ef5a7a29be811f786415fc1e6fd/propcache-0.4.1-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/1c/fd/3f0f10dba4dabad3bf53102be007abf55481067952bde0fdddff439e7c61/propcache-0.5.2-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/f6/f0/10642828a8dfb741e5f3fbaac830550a518a775c7fff6f04a007259b0548/py-1.11.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl @@ -774,13 +776,13 @@ environments: - pypi: https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/30/d4/24d543ab8b8158b7f5a97113c831205f5c900c92c8762b1e7f44b7ea0405/python_discovery-1.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/6f/a05a317a66fee0aad270011461f1a63a453ed12471249f172f7d2e2bc7b4/python_discovery-1.3.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/aa/54/0cce26da03a981f949bb8449c9778537f75f5917c172e1d2992ff25cb57d/python_engineio-4.13.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/07/c7/deb8c5e604404dbf10a3808a858946ca3547692ff6316b698945bb72177e/python_socketio-5.16.1-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/3c/26/1062c7ec1b053db9e499b4d2d5bc231743201b74051c973dadeac80a8f43/questionary-2.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/93/f7/d00d9b4a0313a6be3a3e0818e6375e15da6d7076f4ae47d1324e7ca986a1/radon-6.0.1-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ef/e6/e300fce5fe83c30520607a015dabd985df3251e188d234bfe9492e17a389/requests-2.34.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/33/f1/9614e03e1cdcbf9437570b5400ced8a720b5db22b28d8e0f1bda429f660d/ruff-0.15.12-py3-none-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/37/fd/22621d3ee9e3ee87ef4c89b63bba55b265ab85039b3c1ba88ed2380a24c1/scipp-26.3.1-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/35/e5/d6d0e51fc888f692a35134336866341c08655d92614f492c6860dc45bb2c/scipy-1.17.1-cp313-cp313-win_amd64.whl @@ -790,14 +792,14 @@ environments: - pypi: https://files.pythonhosted.org/packages/3e/17/1f31d8562e6f970d64911f1abc330d233bc0c0601411cf7e19c1292be6da/spdx_headers-1.5.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/33/f0/3fe8c6e69135a845f4106f2ff8b6805638d4e85c264e70114e8126689587/tokenize_rt-6.2.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d6/bb/fbdc4e57731efb86b4209ffdd2519520782bf27b3c961beac3e5c20d2b87/trove_classifiers-2026.4.28.13-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7b/e3/d81b065a2d866a33a541ac63a2a4cc5737e03ce2379ac3191c98bb8867e3/trove_classifiers-2026.5.7.17-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8f/5e/f1e1dd319e35e962a4e00b33150a8868b6329cc1d19fd533436ba5488f09/uncertainties-3.2.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b7/ee/e9c95cda829131f71a8dff5ce0406059fd16e591c074414e31ada19ba7c3/validate_pyproject-0.25-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1c/59/964ecb8008722d27d8a835baea81f56a91cea8e097b3be992bc6ccde6367/versioningit-3.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a4/ce/3b6fee91c85626eaf769d617f1be9d2e15c1cca027bbdeb2e0d751469355/verspec-0.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b1/4f/f71e641e504111a5a74e3a20bc52d01bd86788b22699dd3fee1c63253cf6/virtualenv-21.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/20/5b/885f479093f6627669d39b57bc3d4e674da532e1a4b247d473a61d8d2118/virtualenv-21.3.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/3f/0e/fa3b193432cfc60c93b42f3be03365f5f909d2b3ea410295cf36df739e31/widgetsnbextension-4.0.15-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-py3-none-any.whl @@ -809,6 +811,8 @@ environments: - url: https://conda.anaconda.org/conda-forge/ indexes: - https://pypi.org/simple + options: + pypi-prerelease-mode: if-necessary-or-explicit packages: linux-64: - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda @@ -963,7 +967,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5f/4b/6157f24ca425b89fe2eb7e7be642375711ab671135be21e6faa100f7448c/contourpy-1.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/98/ba/fc20faf5b2bb04615fa906f8daecfe896e6f7a56b34debd93c4a4d63e6e5/copier-9.15.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/92/be/b1afb692be85b947f3401375851484496134c5554e67e822c35f28bf2fbc/coverage-7.13.5-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/fd/35/202235eb5c3c14c212462cd91d61b7386bf8fc44bc7a77f4742d2a69174b/coverage-7.14.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c7/a0/5ff05d1919ca249508012cad89f08fdc6cfbdaa15b41651c5fe6dffaf1d3/dfo_ls-1.6.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl @@ -998,14 +1002,14 @@ environments: - pypi: https://files.pythonhosted.org/packages/e0/07/a000fe835f76b7e1143242ab1122e6362ef1c03f23f83a045c38859c2ae0/jupyterlab_server-2.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ab/b5/36c712098e6191d1b4e349304ef73a8d06aed77e56ceaac8c0a306c7bda1/jupyterlab_widgets-3.0.16-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b3/52/bc858b1665d0dec3a2511f4e6f5c18ea85c0977563d624d597c95d6d0fd7/jupyterquiz-2.9.6.4-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/16/5a/736dd2f4535dbf3bf26523f9158c011389ef88dd06ec2eef67fd744f1c7b/jupytext-1.19.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4c/65/b4b86e5fa07543bfbbcdc6c9f7f9f561e66a5f3539992e3009973f2b1314/jupytext-1.19.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/80/46/bddc13df6c2a40741e0cc7865bb1c9ed4796b6760bd04ce5fae3928ef917/kiwisolver-1.5.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - pypi: https://files.pythonhosted.org/packages/38/7e/7b91c89a4cf0f543a83be978657afb20c86af6d725253e319589dcc4ce52/lmfit-1.3.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d2/f0/834e479e47e499b6478e807fb57b31cc2db696c4db30557bb6f5aea4a90b/mando-0.7.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a8/88/802c82060c54bc7dde21eb0033e337838b8181a1323254aa9ec41cbfc3d1/markdown_it_py-4.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/79/db/e28c1b83e3680740aa78925f5fb2ae4d16207207419ad75ea9fe604f8676/matplotlib-3.10.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/fb/86/dd6e5db36df29e76c7a7699123569a4a18c1623ce68d826ed96c62643cae/mdit_py_plugins-0.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/76/8e/56ccb09c7232a55403a7637caa21922f3b65901a37f5e8bdb405d0de0946/mike-2.2.0-py3-none-any.whl @@ -1022,7 +1026,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/5b/69/93b34728cc386efdde0c342f8c680b9187dea7beb7adaf6b58a0713be101/mpld3-0.5.12-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/da/e0/6cc2e852837cd6086fe7d8406af4294e66827a60a4cf60b86575a4a65ca8/msgpack-1.1.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/5a/56/21b27c560c13822ed93133f08aa6372c53a8e067f11fbed37b4adcdac922/multidict-6.7.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/d0/69/f24d3d1c38ad69e256138b4ec2452a8c7cf66be49dc214771ae99dd4f0a0/narwhals-2.20.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c7/e1/68c2256b69a314eba133673377ba9118c356f6342a0c02b61de449cf2bf2/narwhals-2.21.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/eb/be/b257e12f9710819fde40adc972578bee6b72c5992da1bc8369bef2597756/nbmake-1.5.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/28/88/4789719fbbe166d12d345b3ac66b96105f10001b16e00a9765ba29261a21/nbqa-1.9.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/20/16/e777eadfa0c0305878c36fae1d5e6db474fbb15dae202b9ec378809dfb4d/nbstripout-0.9.1-py3-none-any.whl @@ -1030,7 +1034,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/f9/33/bd5b9137445ea4b680023eb0469b2bb969d61303dedb2aac6560ff3d14a1/notebook_shim-0.2.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cf/c5/9fcb7e0e69cef59cf10c746b84f7d58b08bc66a6b7d459783c5a4f6101a6/numpy-2.4.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/20/17/ec40d981705654853726e7ac9aea9ddbb4a5d9cf54d8472222f4f3de06c2/pandas-3.0.2-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/32/f1/bbecd2f867b97abebe0f9b53d750f862251b40337e061b36676ded3d920f/pandas-3.0.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e9/bd/e51a61b1054f09437acfbc2ff9106c30d1eb76bc1453d428399946781253/pillow-12.2.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/3a/eb/fea4d1d51c49832120f7f285d07306db3960f423a2612c6057caf3e8196f/pip-26.1.1-py3-none-any.whl @@ -1039,7 +1043,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/79/ad/45312df6b63ba64ea35b8d8f5f0c577aac16e6b416eafe8e1cb34e03f9a7/plumbum-1.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2a/2d/d4bf65e47cea8ff2c794a600c4fd1273a7902f268757c531e0ee9f18aa58/pooch-1.9.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/52/6a/57f43e054fb3d3a56ac9fc532bc684fc6169a26c75c353e65425b3e56eef/propcache-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/27/1b/16ab7f2cf2041da2f60d156ba64c2484eadf9168075b4ff43c3ef60045af/propcache-0.5.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/f6/f0/10642828a8dfb741e5f3fbaac830550a518a775c7fff6f04a007259b0548/py-1.11.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl @@ -1051,13 +1055,13 @@ environments: - pypi: https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/30/d4/24d543ab8b8158b7f5a97113c831205f5c900c92c8762b1e7f44b7ea0405/python_discovery-1.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/6f/a05a317a66fee0aad270011461f1a63a453ed12471249f172f7d2e2bc7b4/python_discovery-1.3.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/aa/54/0cce26da03a981f949bb8449c9778537f75f5917c172e1d2992ff25cb57d/python_engineio-4.13.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/07/c7/deb8c5e604404dbf10a3808a858946ca3547692ff6316b698945bb72177e/python_socketio-5.16.1-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/3c/26/1062c7ec1b053db9e499b4d2d5bc231743201b74051c973dadeac80a8f43/questionary-2.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/93/f7/d00d9b4a0313a6be3a3e0818e6375e15da6d7076f4ae47d1324e7ca986a1/radon-6.0.1-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ef/e6/e300fce5fe83c30520607a015dabd985df3251e188d234bfe9492e17a389/requests-2.34.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e8/39/c61d193b8a1daaa8977f7dea9e8d8ba866e02ea7b65d32f6861693aa4c12/ruff-0.15.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/d4/06/19ff1efd58b85906149ce83dfddce23252cea5bec7e0fa5f834336cfe836/scipp-26.3.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/09/7d/af933f0f6e0767995b4e2d705a0665e454d1c19402aa7e895de3951ebb04/scipy-1.17.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl @@ -1067,14 +1071,14 @@ environments: - pypi: https://files.pythonhosted.org/packages/3e/17/1f31d8562e6f970d64911f1abc330d233bc0c0601411cf7e19c1292be6da/spdx_headers-1.5.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/33/f0/3fe8c6e69135a845f4106f2ff8b6805638d4e85c264e70114e8126689587/tokenize_rt-6.2.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d6/bb/fbdc4e57731efb86b4209ffdd2519520782bf27b3c961beac3e5c20d2b87/trove_classifiers-2026.4.28.13-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7b/e3/d81b065a2d866a33a541ac63a2a4cc5737e03ce2379ac3191c98bb8867e3/trove_classifiers-2026.5.7.17-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8f/5e/f1e1dd319e35e962a4e00b33150a8868b6329cc1d19fd533436ba5488f09/uncertainties-3.2.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b7/ee/e9c95cda829131f71a8dff5ce0406059fd16e591c074414e31ada19ba7c3/validate_pyproject-0.25-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1c/59/964ecb8008722d27d8a835baea81f56a91cea8e097b3be992bc6ccde6367/versioningit-3.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a4/ce/3b6fee91c85626eaf769d617f1be9d2e15c1cca027bbdeb2e0d751469355/verspec-0.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b1/4f/f71e641e504111a5a74e3a20bc52d01bd86788b22699dd3fee1c63253cf6/virtualenv-21.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/20/5b/885f479093f6627669d39b57bc3d4e674da532e1a4b247d473a61d8d2118/virtualenv-21.3.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/3f/0e/fa3b193432cfc60c93b42f3be03365f5f909d2b3ea410295cf36df739e31/widgetsnbextension-4.0.15-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-py3-none-any.whl @@ -1228,7 +1232,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/0d/44/c4b0b6095fef4dc9c420e041799591e3b63e9619e3044f7f4f6c21c0ab24/contourpy-1.3.3-cp311-cp311-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/98/ba/fc20faf5b2bb04615fa906f8daecfe896e6f7a56b34debd93c4a4d63e6e5/copier-9.15.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/35/8b/cd129b0ca4afe886a6ce9d183c44d8301acbd4ef248622e7c49a23145605/coverage-7.13.5-cp311-cp311-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/7f/8d/46692d24b3f395d4cbf17bfcc57136b4f2f9c0c0df864b0bddfc1d71a014/coverage-7.14.0-cp311-cp311-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c7/a0/5ff05d1919ca249508012cad89f08fdc6cfbdaa15b41651c5fe6dffaf1d3/dfo_ls-1.6.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl @@ -1263,14 +1267,14 @@ environments: - pypi: https://files.pythonhosted.org/packages/e0/07/a000fe835f76b7e1143242ab1122e6362ef1c03f23f83a045c38859c2ae0/jupyterlab_server-2.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ab/b5/36c712098e6191d1b4e349304ef73a8d06aed77e56ceaac8c0a306c7bda1/jupyterlab_widgets-3.0.16-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b3/52/bc858b1665d0dec3a2511f4e6f5c18ea85c0977563d624d597c95d6d0fd7/jupyterquiz-2.9.6.4-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/16/5a/736dd2f4535dbf3bf26523f9158c011389ef88dd06ec2eef67fd744f1c7b/jupytext-1.19.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4c/65/b4b86e5fa07543bfbbcdc6c9f7f9f561e66a5f3539992e3009973f2b1314/jupytext-1.19.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/0a/aa/510dc933d87767584abfe03efa445889996c70c2990f6f87c3ebaa0a18c5/kiwisolver-1.5.0-cp311-cp311-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/38/7e/7b91c89a4cf0f543a83be978657afb20c86af6d725253e319589dcc4ce52/lmfit-1.3.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d2/f0/834e479e47e499b6478e807fb57b31cc2db696c4db30557bb6f5aea4a90b/mando-0.7.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a8/88/802c82060c54bc7dde21eb0033e337838b8181a1323254aa9ec41cbfc3d1/markdown_it_py-4.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/51/18/325cd32ece1120d1da51cc4e4294c6580190699490183fc2fe8cb6d61ec5/matplotlib-3.10.9-cp311-cp311-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/fb/86/dd6e5db36df29e76c7a7699123569a4a18c1623ce68d826ed96c62643cae/mdit_py_plugins-0.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/76/8e/56ccb09c7232a55403a7637caa21922f3b65901a37f5e8bdb405d0de0946/mike-2.2.0-py3-none-any.whl @@ -1287,7 +1291,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/5b/69/93b34728cc386efdde0c342f8c680b9187dea7beb7adaf6b58a0713be101/mpld3-0.5.12-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/83/04/28a41024ccbd67467380b6fb440ae916c1e4f25e2cd4c63abe6835ac566e/msgpack-1.1.2-cp311-cp311-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/dd/a4/d45caf2b97b035c57267791ecfaafbd59c68212004b3842830954bb4b02e/multidict-6.7.1-cp311-cp311-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/d0/69/f24d3d1c38ad69e256138b4ec2452a8c7cf66be49dc214771ae99dd4f0a0/narwhals-2.20.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c7/e1/68c2256b69a314eba133673377ba9118c356f6342a0c02b61de449cf2bf2/narwhals-2.21.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/eb/be/b257e12f9710819fde40adc972578bee6b72c5992da1bc8369bef2597756/nbmake-1.5.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/28/88/4789719fbbe166d12d345b3ac66b96105f10001b16e00a9765ba29261a21/nbqa-1.9.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/20/16/e777eadfa0c0305878c36fae1d5e6db474fbb15dae202b9ec378809dfb4d/nbstripout-0.9.1-py3-none-any.whl @@ -1295,7 +1299,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/f9/33/bd5b9137445ea4b680023eb0469b2bb969d61303dedb2aac6560ff3d14a1/notebook_shim-0.2.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8a/dc/df98c095978fa6ee7b9a9387d1d58cbb3d232d0e69ad169a4ce784bde4fd/numpy-2.4.4-cp311-cp311-macosx_14_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/c4/d3/b7da1d5d7dbdc5ef52ed7debd2b484313b832982266905315dad5a0bf0b1/pandas-3.0.2-cp311-cp311-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/5a/b0/a4ffc4ae74d2d822200dcc46898987d8eb6032d1e2b219cae39da6f5cbcc/pandas-3.0.3-cp311-cp311-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/47/a1/d5ff69e747374c33a3b53b9f98cca7889fce1fd03d79cdc4e1bccc6c5a87/pillow-12.2.0-cp311-cp311-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/3a/eb/fea4d1d51c49832120f7f285d07306db3960f423a2612c6057caf3e8196f/pip-26.1.1-py3-none-any.whl @@ -1304,7 +1308,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/79/ad/45312df6b63ba64ea35b8d8f5f0c577aac16e6b416eafe8e1cb34e03f9a7/plumbum-1.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2a/2d/d4bf65e47cea8ff2c794a600c4fd1273a7902f268757c531e0ee9f18aa58/pooch-1.9.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d3/1d/11605e99ac8ea9435651ee71ab4cb4bf03f0949586246476a25aadfec54a/propcache-0.4.1-cp311-cp311-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/15/a8/8ede85d6aa1f79fc7dc2f8fd2c8d65920b8272c3892903c8a1affde48cfb/propcache-0.5.2-cp311-cp311-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/f6/f0/10642828a8dfb741e5f3fbaac830550a518a775c7fff6f04a007259b0548/py-1.11.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl @@ -1316,13 +1320,13 @@ environments: - pypi: https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/30/d4/24d543ab8b8158b7f5a97113c831205f5c900c92c8762b1e7f44b7ea0405/python_discovery-1.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/6f/a05a317a66fee0aad270011461f1a63a453ed12471249f172f7d2e2bc7b4/python_discovery-1.3.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/aa/54/0cce26da03a981f949bb8449c9778537f75f5917c172e1d2992ff25cb57d/python_engineio-4.13.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/07/c7/deb8c5e604404dbf10a3808a858946ca3547692ff6316b698945bb72177e/python_socketio-5.16.1-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/3c/26/1062c7ec1b053db9e499b4d2d5bc231743201b74051c973dadeac80a8f43/questionary-2.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/93/f7/d00d9b4a0313a6be3a3e0818e6375e15da6d7076f4ae47d1324e7ca986a1/radon-6.0.1-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ef/e6/e300fce5fe83c30520607a015dabd985df3251e188d234bfe9492e17a389/requests-2.34.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/aa/a4/f828e9718d3dce1f5f11c39c4f65afd32783c8b2aebb2e3d259e492c47bd/ruff-0.15.12-py3-none-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/60/54/5011adb56853caabfd90686c2e543d1e3c76a8ef2755809b7e12e3f3583b/scipp-26.3.1-cp311-cp311-macosx_14_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/6d/ee/18146b7757ed4976276b9c9819108adbc73c5aad636e5353e20746b73069/scipy-1.17.1-cp311-cp311-macosx_14_0_arm64.whl @@ -1331,14 +1335,14 @@ environments: - pypi: https://files.pythonhosted.org/packages/3e/17/1f31d8562e6f970d64911f1abc330d233bc0c0601411cf7e19c1292be6da/spdx_headers-1.5.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/33/f0/3fe8c6e69135a845f4106f2ff8b6805638d4e85c264e70114e8126689587/tokenize_rt-6.2.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d6/bb/fbdc4e57731efb86b4209ffdd2519520782bf27b3c961beac3e5c20d2b87/trove_classifiers-2026.4.28.13-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7b/e3/d81b065a2d866a33a541ac63a2a4cc5737e03ce2379ac3191c98bb8867e3/trove_classifiers-2026.5.7.17-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8f/5e/f1e1dd319e35e962a4e00b33150a8868b6329cc1d19fd533436ba5488f09/uncertainties-3.2.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b7/ee/e9c95cda829131f71a8dff5ce0406059fd16e591c074414e31ada19ba7c3/validate_pyproject-0.25-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1c/59/964ecb8008722d27d8a835baea81f56a91cea8e097b3be992bc6ccde6367/versioningit-3.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a4/ce/3b6fee91c85626eaf769d617f1be9d2e15c1cca027bbdeb2e0d751469355/verspec-0.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b1/4f/f71e641e504111a5a74e3a20bc52d01bd86788b22699dd3fee1c63253cf6/virtualenv-21.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/20/5b/885f479093f6627669d39b57bc3d4e674da532e1a4b247d473a61d8d2118/virtualenv-21.3.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/3f/0e/fa3b193432cfc60c93b42f3be03365f5f909d2b3ea410295cf36df739e31/widgetsnbextension-4.0.15-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-py3-none-any.whl @@ -1479,7 +1483,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/98/4b/9bd370b004b5c9d8045c6c33cf65bae018b27aca550a3f657cdc99acdbd8/contourpy-1.3.3-cp311-cp311-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/98/ba/fc20faf5b2bb04615fa906f8daecfe896e6f7a56b34debd93c4a4d63e6e5/copier-9.15.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/af/7f/4cd8a92531253f9d7c1bbecd9fa1b472907fb54446ca768c59b531248dc5/coverage-7.13.5-cp311-cp311-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/f0/f0/a71ddbd874431e7a7cd96071f0c331cfbbad07704833c765d24ffbab8a67/coverage-7.14.0-cp311-cp311-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c7/a0/5ff05d1919ca249508012cad89f08fdc6cfbdaa15b41651c5fe6dffaf1d3/dfo_ls-1.6.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl @@ -1514,14 +1518,14 @@ environments: - pypi: https://files.pythonhosted.org/packages/e0/07/a000fe835f76b7e1143242ab1122e6362ef1c03f23f83a045c38859c2ae0/jupyterlab_server-2.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ab/b5/36c712098e6191d1b4e349304ef73a8d06aed77e56ceaac8c0a306c7bda1/jupyterlab_widgets-3.0.16-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b3/52/bc858b1665d0dec3a2511f4e6f5c18ea85c0977563d624d597c95d6d0fd7/jupyterquiz-2.9.6.4-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/16/5a/736dd2f4535dbf3bf26523f9158c011389ef88dd06ec2eef67fd744f1c7b/jupytext-1.19.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4c/65/b4b86e5fa07543bfbbcdc6c9f7f9f561e66a5f3539992e3009973f2b1314/jupytext-1.19.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/be/6c/28f17390b62b8f2f520e2915095b3c94d88681ecf0041e75389d9667f202/kiwisolver-1.5.0-cp311-cp311-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/38/7e/7b91c89a4cf0f543a83be978657afb20c86af6d725253e319589dcc4ce52/lmfit-1.3.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d2/f0/834e479e47e499b6478e807fb57b31cc2db696c4db30557bb6f5aea4a90b/mando-0.7.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a8/88/802c82060c54bc7dde21eb0033e337838b8181a1323254aa9ec41cbfc3d1/markdown_it_py-4.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7e/0d/271aace3342157c64700c9ff4c59c7b392f3dbab393692e8db6fbe7ab96c/matplotlib-3.10.9-cp311-cp311-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/fb/86/dd6e5db36df29e76c7a7699123569a4a18c1623ce68d826ed96c62643cae/mdit_py_plugins-0.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/76/8e/56ccb09c7232a55403a7637caa21922f3b65901a37f5e8bdb405d0de0946/mike-2.2.0-py3-none-any.whl @@ -1538,7 +1542,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/5b/69/93b34728cc386efdde0c342f8c680b9187dea7beb7adaf6b58a0713be101/mpld3-0.5.12-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2a/79/309d0e637f6f37e83c711f547308b91af02b72d2326ddd860b966080ef29/msgpack-1.1.2-cp311-cp311-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/c9/68/f16a3a8ba6f7b6dc92a1f19669c0810bd2c43fc5a02da13b1cbf8e253845/multidict-6.7.1-cp311-cp311-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/d0/69/f24d3d1c38ad69e256138b4ec2452a8c7cf66be49dc214771ae99dd4f0a0/narwhals-2.20.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c7/e1/68c2256b69a314eba133673377ba9118c356f6342a0c02b61de449cf2bf2/narwhals-2.21.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/eb/be/b257e12f9710819fde40adc972578bee6b72c5992da1bc8369bef2597756/nbmake-1.5.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/28/88/4789719fbbe166d12d345b3ac66b96105f10001b16e00a9765ba29261a21/nbqa-1.9.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/20/16/e777eadfa0c0305878c36fae1d5e6db474fbb15dae202b9ec378809dfb4d/nbstripout-0.9.1-py3-none-any.whl @@ -1546,7 +1550,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/f9/33/bd5b9137445ea4b680023eb0469b2bb969d61303dedb2aac6560ff3d14a1/notebook_shim-0.2.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/bd/63/05d193dbb4b5eec1eca73822d80da98b511f8328ad4ae3ca4caf0f4db91d/numpy-2.4.4-cp311-cp311-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/44/a0/97a6339859d4acb2536efb24feb6708e82f7d33b2ed7e036f2983fcced82/pandas-3.0.2-cp311-cp311-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/eb/62/c321f13b5ba1819fc8dca456c7fce578da2dcfecff1abbf0eaddf8406c0f/pandas-3.0.3-cp311-cp311-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/69/42/836b6f3cd7f3e5fa10a1f1a5420447c17966044c8fbf589cc0452d5502db/pillow-12.2.0-cp311-cp311-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/3a/eb/fea4d1d51c49832120f7f285d07306db3960f423a2612c6057caf3e8196f/pip-26.1.1-py3-none-any.whl @@ -1555,7 +1559,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/79/ad/45312df6b63ba64ea35b8d8f5f0c577aac16e6b416eafe8e1cb34e03f9a7/plumbum-1.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2a/2d/d4bf65e47cea8ff2c794a600c4fd1273a7902f268757c531e0ee9f18aa58/pooch-1.9.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f4/78/6cce448e2098e9f3bfc91bb877f06aa24b6ccace872e39c53b2f707c4648/propcache-0.4.1-cp311-cp311-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/42/4e/f17363fb58c0afe05b067361cb6d86ed2d29de6506779a27547c4d183075/propcache-0.5.2-cp311-cp311-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/f6/f0/10642828a8dfb741e5f3fbaac830550a518a775c7fff6f04a007259b0548/py-1.11.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl @@ -1567,13 +1571,13 @@ environments: - pypi: https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/30/d4/24d543ab8b8158b7f5a97113c831205f5c900c92c8762b1e7f44b7ea0405/python_discovery-1.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/6f/a05a317a66fee0aad270011461f1a63a453ed12471249f172f7d2e2bc7b4/python_discovery-1.3.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/aa/54/0cce26da03a981f949bb8449c9778537f75f5917c172e1d2992ff25cb57d/python_engineio-4.13.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/07/c7/deb8c5e604404dbf10a3808a858946ca3547692ff6316b698945bb72177e/python_socketio-5.16.1-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/3c/26/1062c7ec1b053db9e499b4d2d5bc231743201b74051c973dadeac80a8f43/questionary-2.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/93/f7/d00d9b4a0313a6be3a3e0818e6375e15da6d7076f4ae47d1324e7ca986a1/radon-6.0.1-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ef/e6/e300fce5fe83c30520607a015dabd985df3251e188d234bfe9492e17a389/requests-2.34.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/33/f1/9614e03e1cdcbf9437570b5400ced8a720b5db22b28d8e0f1bda429f660d/ruff-0.15.12-py3-none-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/e6/0d/8882a4c7a5ebe59a46b709e82411d9c730d67250d41a2e11bc4bcd4d431d/scipp-26.3.1-cp311-cp311-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/95/da/0d1df507cf574b3f224ccc3d45244c9a1d732c81dcb26b1e8a766ae271a8/scipy-1.17.1-cp311-cp311-win_amd64.whl @@ -1583,14 +1587,14 @@ environments: - pypi: https://files.pythonhosted.org/packages/3e/17/1f31d8562e6f970d64911f1abc330d233bc0c0601411cf7e19c1292be6da/spdx_headers-1.5.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/33/f0/3fe8c6e69135a845f4106f2ff8b6805638d4e85c264e70114e8126689587/tokenize_rt-6.2.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d6/bb/fbdc4e57731efb86b4209ffdd2519520782bf27b3c961beac3e5c20d2b87/trove_classifiers-2026.4.28.13-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7b/e3/d81b065a2d866a33a541ac63a2a4cc5737e03ce2379ac3191c98bb8867e3/trove_classifiers-2026.5.7.17-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8f/5e/f1e1dd319e35e962a4e00b33150a8868b6329cc1d19fd533436ba5488f09/uncertainties-3.2.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b7/ee/e9c95cda829131f71a8dff5ce0406059fd16e591c074414e31ada19ba7c3/validate_pyproject-0.25-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1c/59/964ecb8008722d27d8a835baea81f56a91cea8e097b3be992bc6ccde6367/versioningit-3.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a4/ce/3b6fee91c85626eaf769d617f1be9d2e15c1cca027bbdeb2e0d751469355/verspec-0.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b1/4f/f71e641e504111a5a74e3a20bc52d01bd86788b22699dd3fee1c63253cf6/virtualenv-21.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/20/5b/885f479093f6627669d39b57bc3d4e674da532e1a4b247d473a61d8d2118/virtualenv-21.3.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/3f/0e/fa3b193432cfc60c93b42f3be03365f5f909d2b3ea410295cf36df739e31/widgetsnbextension-4.0.15-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-py3-none-any.whl @@ -1602,6 +1606,8 @@ environments: - url: https://conda.anaconda.org/conda-forge/ indexes: - https://pypi.org/simple + options: + pypi-prerelease-mode: if-necessary-or-explicit packages: linux-64: - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda @@ -1758,7 +1764,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4b/32/e0f13a1c5b0f8572d0ec6ae2f6c677b7991fafd95da523159c19eff0696a/contourpy-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/98/ba/fc20faf5b2bb04615fa906f8daecfe896e6f7a56b34debd93c4a4d63e6e5/copier-9.15.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ac/68/1666e3a4462f8202d836920114fa7a5ee9275d1fa45366d336c551a162dd/coverage-7.13.5-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/6f/5f/b5370068b2f57787454592ed7dcd1002f0f1703b7db1fa30f6a325a4ca6e/coverage-7.14.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c7/a0/5ff05d1919ca249508012cad89f08fdc6cfbdaa15b41651c5fe6dffaf1d3/dfo_ls-1.6.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl @@ -1793,14 +1799,14 @@ environments: - pypi: https://files.pythonhosted.org/packages/e0/07/a000fe835f76b7e1143242ab1122e6362ef1c03f23f83a045c38859c2ae0/jupyterlab_server-2.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ab/b5/36c712098e6191d1b4e349304ef73a8d06aed77e56ceaac8c0a306c7bda1/jupyterlab_widgets-3.0.16-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b3/52/bc858b1665d0dec3a2511f4e6f5c18ea85c0977563d624d597c95d6d0fd7/jupyterquiz-2.9.6.4-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/16/5a/736dd2f4535dbf3bf26523f9158c011389ef88dd06ec2eef67fd744f1c7b/jupytext-1.19.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4c/65/b4b86e5fa07543bfbbcdc6c9f7f9f561e66a5f3539992e3009973f2b1314/jupytext-1.19.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2b/0a/7b98e1e119878a27ba8618ca1e18b14f992ff1eda40f47bccccf4de44121/kiwisolver-1.5.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - pypi: https://files.pythonhosted.org/packages/38/7e/7b91c89a4cf0f543a83be978657afb20c86af6d725253e319589dcc4ce52/lmfit-1.3.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d2/f0/834e479e47e499b6478e807fb57b31cc2db696c4db30557bb6f5aea4a90b/mando-0.7.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a8/88/802c82060c54bc7dde21eb0033e337838b8181a1323254aa9ec41cbfc3d1/markdown_it_py-4.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8a/17/4402d0d14ccf1dfc70932600b68097fbbf9c898a4871d2cbbe79c7801a32/matplotlib-3.10.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/fb/86/dd6e5db36df29e76c7a7699123569a4a18c1623ce68d826ed96c62643cae/mdit_py_plugins-0.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/76/8e/56ccb09c7232a55403a7637caa21922f3b65901a37f5e8bdb405d0de0946/mike-2.2.0-py3-none-any.whl @@ -1817,7 +1823,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/5b/69/93b34728cc386efdde0c342f8c680b9187dea7beb7adaf6b58a0713be101/mpld3-0.5.12-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5d/ba/459f18c16f2b3fc1a1ca871f72f07d70c07bf768ad0a507a698b8052ac58/msgpack-1.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/b0/73/6e1b01cbeb458807aa0831742232dbdd1fa92bfa33f52a3f176b4ff3dc11/multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/d0/69/f24d3d1c38ad69e256138b4ec2452a8c7cf66be49dc214771ae99dd4f0a0/narwhals-2.20.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c7/e1/68c2256b69a314eba133673377ba9118c356f6342a0c02b61de449cf2bf2/narwhals-2.21.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/eb/be/b257e12f9710819fde40adc972578bee6b72c5992da1bc8369bef2597756/nbmake-1.5.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/28/88/4789719fbbe166d12d345b3ac66b96105f10001b16e00a9765ba29261a21/nbqa-1.9.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/20/16/e777eadfa0c0305878c36fae1d5e6db474fbb15dae202b9ec378809dfb4d/nbstripout-0.9.1-py3-none-any.whl @@ -1825,7 +1831,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/f9/33/bd5b9137445ea4b680023eb0469b2bb969d61303dedb2aac6560ff3d14a1/notebook_shim-0.2.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d1/73/a9d864e42a01896bb5974475438f16086be9ba1f0d19d0bb7a07427c4a8b/numpy-2.4.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/12/c5/cbb1ffefb20a93d3f0e1fdcda699fb84976210d411b008f97f48bf6ce27e/pandas-3.0.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/99/68/1237369725aa617bb358263d535803e3053fdbc593513ec5ed9c9896b5b6/pandas-3.0.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/67/ee/21d4e8536afd1a328f01b359b4d3997b291ffd35a237c877b331c1c3b71c/pillow-12.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/3a/eb/fea4d1d51c49832120f7f285d07306db3960f423a2612c6057caf3e8196f/pip-26.1.1-py3-none-any.whl @@ -1834,7 +1840,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/79/ad/45312df6b63ba64ea35b8d8f5f0c577aac16e6b416eafe8e1cb34e03f9a7/plumbum-1.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2a/2d/d4bf65e47cea8ff2c794a600c4fd1273a7902f268757c531e0ee9f18aa58/pooch-1.9.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f1/8b/544bc867e24e1bd48f3118cecd3b05c694e160a168478fa28770f22fd094/propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/8b/c6/979176efdaa3d239e36d503d5af63a0a773b36662ed8f52e5b6a6d9fd40e/propcache-0.5.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/f6/f0/10642828a8dfb741e5f3fbaac830550a518a775c7fff6f04a007259b0548/py-1.11.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl @@ -1846,13 +1852,13 @@ environments: - pypi: https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/30/d4/24d543ab8b8158b7f5a97113c831205f5c900c92c8762b1e7f44b7ea0405/python_discovery-1.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/6f/a05a317a66fee0aad270011461f1a63a453ed12471249f172f7d2e2bc7b4/python_discovery-1.3.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/aa/54/0cce26da03a981f949bb8449c9778537f75f5917c172e1d2992ff25cb57d/python_engineio-4.13.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/07/c7/deb8c5e604404dbf10a3808a858946ca3547692ff6316b698945bb72177e/python_socketio-5.16.1-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/3c/26/1062c7ec1b053db9e499b4d2d5bc231743201b74051c973dadeac80a8f43/questionary-2.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/93/f7/d00d9b4a0313a6be3a3e0818e6375e15da6d7076f4ae47d1324e7ca986a1/radon-6.0.1-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ef/e6/e300fce5fe83c30520607a015dabd985df3251e188d234bfe9492e17a389/requests-2.34.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e8/39/c61d193b8a1daaa8977f7dea9e8d8ba866e02ea7b65d32f6861693aa4c12/ruff-0.15.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/2e/75/5604f4d17ab607510d4702f156329194d8edfff7e29644ca9200b085e9a2/scipp-26.3.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/f5/5f/f17563f28ff03c7b6799c50d01d5d856a1d55f2676f537ca8d28c7f627cd/scipy-1.17.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl @@ -1862,14 +1868,14 @@ environments: - pypi: https://files.pythonhosted.org/packages/3e/17/1f31d8562e6f970d64911f1abc330d233bc0c0601411cf7e19c1292be6da/spdx_headers-1.5.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/33/f0/3fe8c6e69135a845f4106f2ff8b6805638d4e85c264e70114e8126689587/tokenize_rt-6.2.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d6/bb/fbdc4e57731efb86b4209ffdd2519520782bf27b3c961beac3e5c20d2b87/trove_classifiers-2026.4.28.13-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7b/e3/d81b065a2d866a33a541ac63a2a4cc5737e03ce2379ac3191c98bb8867e3/trove_classifiers-2026.5.7.17-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8f/5e/f1e1dd319e35e962a4e00b33150a8868b6329cc1d19fd533436ba5488f09/uncertainties-3.2.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b7/ee/e9c95cda829131f71a8dff5ce0406059fd16e591c074414e31ada19ba7c3/validate_pyproject-0.25-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1c/59/964ecb8008722d27d8a835baea81f56a91cea8e097b3be992bc6ccde6367/versioningit-3.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a4/ce/3b6fee91c85626eaf769d617f1be9d2e15c1cca027bbdeb2e0d751469355/verspec-0.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b1/4f/f71e641e504111a5a74e3a20bc52d01bd86788b22699dd3fee1c63253cf6/virtualenv-21.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/20/5b/885f479093f6627669d39b57bc3d4e674da532e1a4b247d473a61d8d2118/virtualenv-21.3.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/3f/0e/fa3b193432cfc60c93b42f3be03365f5f909d2b3ea410295cf36df739e31/widgetsnbextension-4.0.15-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-py3-none-any.whl @@ -2027,7 +2033,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/96/e4/7adcd9c8362745b2210728f209bfbcf7d91ba868a2c5f40d8b58f54c509b/contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/98/ba/fc20faf5b2bb04615fa906f8daecfe896e6f7a56b34debd93c4a4d63e6e5/copier-9.15.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/0c/c9/44fb661c55062f0818a6ffd2685c67aa30816200d5f2817543717d4b92eb/coverage-7.13.5-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/b3/af/e567cbad5ba69c013a50146dfa886dc7193361fda77521f51274ff620e1b/coverage-7.14.0-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c7/a0/5ff05d1919ca249508012cad89f08fdc6cfbdaa15b41651c5fe6dffaf1d3/dfo_ls-1.6.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl @@ -2062,14 +2068,14 @@ environments: - pypi: https://files.pythonhosted.org/packages/e0/07/a000fe835f76b7e1143242ab1122e6362ef1c03f23f83a045c38859c2ae0/jupyterlab_server-2.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ab/b5/36c712098e6191d1b4e349304ef73a8d06aed77e56ceaac8c0a306c7bda1/jupyterlab_widgets-3.0.16-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b3/52/bc858b1665d0dec3a2511f4e6f5c18ea85c0977563d624d597c95d6d0fd7/jupyterquiz-2.9.6.4-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/16/5a/736dd2f4535dbf3bf26523f9158c011389ef88dd06ec2eef67fd744f1c7b/jupytext-1.19.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4c/65/b4b86e5fa07543bfbbcdc6c9f7f9f561e66a5f3539992e3009973f2b1314/jupytext-1.19.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a8/3a/d0a972b34e1c63e2409413104216cd1caa02c5a37cb668d1687d466c1c45/kiwisolver-1.5.0-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/38/7e/7b91c89a4cf0f543a83be978657afb20c86af6d725253e319589dcc4ce52/lmfit-1.3.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d2/f0/834e479e47e499b6478e807fb57b31cc2db696c4db30557bb6f5aea4a90b/mando-0.7.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a8/88/802c82060c54bc7dde21eb0033e337838b8181a1323254aa9ec41cbfc3d1/markdown_it_py-4.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/63/d9/9e14bc7564bf92d5ffa801ae5fac819ce74b925dfb55e3ebde61a3bbad3e/matplotlib-3.10.9-cp313-cp313-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/fb/86/dd6e5db36df29e76c7a7699123569a4a18c1623ce68d826ed96c62643cae/mdit_py_plugins-0.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/76/8e/56ccb09c7232a55403a7637caa21922f3b65901a37f5e8bdb405d0de0946/mike-2.2.0-py3-none-any.whl @@ -2086,7 +2092,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/5b/69/93b34728cc386efdde0c342f8c680b9187dea7beb7adaf6b58a0713be101/mpld3-0.5.12-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/92/dc/c385f38f2c2433333345a82926c6bfa5ecfff3ef787201614317b58dd8be/msgpack-1.1.2-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/79/76/55cd7186f498ed080a18440c9013011eb548f77ae1b297206d030eb1180a/multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/d0/69/f24d3d1c38ad69e256138b4ec2452a8c7cf66be49dc214771ae99dd4f0a0/narwhals-2.20.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c7/e1/68c2256b69a314eba133673377ba9118c356f6342a0c02b61de449cf2bf2/narwhals-2.21.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/eb/be/b257e12f9710819fde40adc972578bee6b72c5992da1bc8369bef2597756/nbmake-1.5.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/28/88/4789719fbbe166d12d345b3ac66b96105f10001b16e00a9765ba29261a21/nbqa-1.9.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/20/16/e777eadfa0c0305878c36fae1d5e6db474fbb15dae202b9ec378809dfb4d/nbstripout-0.9.1-py3-none-any.whl @@ -2094,7 +2100,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/f9/33/bd5b9137445ea4b680023eb0469b2bb969d61303dedb2aac6560ff3d14a1/notebook_shim-0.2.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e5/87/499737bfba066b4a3bebff24a8f1c5b2dee410b209bc6668c9be692580f0/numpy-2.4.4-cp313-cp313-macosx_14_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/0b/77/dbc82ff2fb0e63c6564356682bf201edff0ba16c98630d21a1fb312a8182/pandas-3.0.2-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/7f/ae/6a6493c783a101f165e4356953ba3c74d6f77f0042fa7d753da9dfbb640c/pandas-3.0.3-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/71/43/905a14a8b17fdb1ccb58d282454490662d2cb89a6bfec26af6d3520da5ec/pillow-12.2.0-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/3a/eb/fea4d1d51c49832120f7f285d07306db3960f423a2612c6057caf3e8196f/pip-26.1.1-py3-none-any.whl @@ -2103,7 +2109,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/79/ad/45312df6b63ba64ea35b8d8f5f0c577aac16e6b416eafe8e1cb34e03f9a7/plumbum-1.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2a/2d/d4bf65e47cea8ff2c794a600c4fd1273a7902f268757c531e0ee9f18aa58/pooch-1.9.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/89/a4/92380f7ca60f99ebae761936bc48a72a639e8a47b29050615eef757cb2a7/propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/a9/12/a6ba6482bb5ea3260c000c9b20881c95fa11c6b30173715668259f844ed7/propcache-0.5.2-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/f6/f0/10642828a8dfb741e5f3fbaac830550a518a775c7fff6f04a007259b0548/py-1.11.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl @@ -2115,13 +2121,13 @@ environments: - pypi: https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/30/d4/24d543ab8b8158b7f5a97113c831205f5c900c92c8762b1e7f44b7ea0405/python_discovery-1.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/6f/a05a317a66fee0aad270011461f1a63a453ed12471249f172f7d2e2bc7b4/python_discovery-1.3.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/aa/54/0cce26da03a981f949bb8449c9778537f75f5917c172e1d2992ff25cb57d/python_engineio-4.13.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/07/c7/deb8c5e604404dbf10a3808a858946ca3547692ff6316b698945bb72177e/python_socketio-5.16.1-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/3c/26/1062c7ec1b053db9e499b4d2d5bc231743201b74051c973dadeac80a8f43/questionary-2.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/93/f7/d00d9b4a0313a6be3a3e0818e6375e15da6d7076f4ae47d1324e7ca986a1/radon-6.0.1-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ef/e6/e300fce5fe83c30520607a015dabd985df3251e188d234bfe9492e17a389/requests-2.34.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/aa/a4/f828e9718d3dce1f5f11c39c4f65afd32783c8b2aebb2e3d259e492c47bd/ruff-0.15.12-py3-none-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/79/fe/b14d806894cf05178f1e77d0d619f071db50cf698bc654c54f9241223bcf/scipp-26.3.1-cp313-cp313-macosx_14_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/5b/58/3ce96251560107b381cbd6e8413c483bbb1228a6b919fa8652b0d4090e7f/scipy-1.17.1-cp313-cp313-macosx_14_0_arm64.whl @@ -2130,14 +2136,14 @@ environments: - pypi: https://files.pythonhosted.org/packages/3e/17/1f31d8562e6f970d64911f1abc330d233bc0c0601411cf7e19c1292be6da/spdx_headers-1.5.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/33/f0/3fe8c6e69135a845f4106f2ff8b6805638d4e85c264e70114e8126689587/tokenize_rt-6.2.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d6/bb/fbdc4e57731efb86b4209ffdd2519520782bf27b3c961beac3e5c20d2b87/trove_classifiers-2026.4.28.13-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7b/e3/d81b065a2d866a33a541ac63a2a4cc5737e03ce2379ac3191c98bb8867e3/trove_classifiers-2026.5.7.17-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8f/5e/f1e1dd319e35e962a4e00b33150a8868b6329cc1d19fd533436ba5488f09/uncertainties-3.2.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b7/ee/e9c95cda829131f71a8dff5ce0406059fd16e591c074414e31ada19ba7c3/validate_pyproject-0.25-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1c/59/964ecb8008722d27d8a835baea81f56a91cea8e097b3be992bc6ccde6367/versioningit-3.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a4/ce/3b6fee91c85626eaf769d617f1be9d2e15c1cca027bbdeb2e0d751469355/verspec-0.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b1/4f/f71e641e504111a5a74e3a20bc52d01bd86788b22699dd3fee1c63253cf6/virtualenv-21.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/20/5b/885f479093f6627669d39b57bc3d4e674da532e1a4b247d473a61d8d2118/virtualenv-21.3.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/3f/0e/fa3b193432cfc60c93b42f3be03365f5f909d2b3ea410295cf36df739e31/widgetsnbextension-4.0.15-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-py3-none-any.whl @@ -2282,7 +2288,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/18/0b/0098c214843213759692cc638fce7de5c289200a830e5035d1791d7a2338/contourpy-1.3.3-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/98/ba/fc20faf5b2bb04615fa906f8daecfe896e6f7a56b34debd93c4a4d63e6e5/copier-9.15.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/66/40/7732d648ab9d069a46e686043241f01206348e2bbf128daea85be4d6414b/coverage-7.13.5-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/8f/b8/9228523e80321c2cb4880d1f589bc0171f2f71432c35118ad04dc01decce/coverage-7.14.0-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c7/a0/5ff05d1919ca249508012cad89f08fdc6cfbdaa15b41651c5fe6dffaf1d3/dfo_ls-1.6.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl @@ -2317,14 +2323,14 @@ environments: - pypi: https://files.pythonhosted.org/packages/e0/07/a000fe835f76b7e1143242ab1122e6362ef1c03f23f83a045c38859c2ae0/jupyterlab_server-2.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ab/b5/36c712098e6191d1b4e349304ef73a8d06aed77e56ceaac8c0a306c7bda1/jupyterlab_widgets-3.0.16-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b3/52/bc858b1665d0dec3a2511f4e6f5c18ea85c0977563d624d597c95d6d0fd7/jupyterquiz-2.9.6.4-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/16/5a/736dd2f4535dbf3bf26523f9158c011389ef88dd06ec2eef67fd744f1c7b/jupytext-1.19.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4c/65/b4b86e5fa07543bfbbcdc6c9f7f9f561e66a5f3539992e3009973f2b1314/jupytext-1.19.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/be/8a/be60e3bbcf513cc5a50f4a3e88e1dcecebb79c1ad607a7222877becaa101/kiwisolver-1.5.0-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/38/7e/7b91c89a4cf0f543a83be978657afb20c86af6d725253e319589dcc4ce52/lmfit-1.3.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d2/f0/834e479e47e499b6478e807fb57b31cc2db696c4db30557bb6f5aea4a90b/mando-0.7.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a8/88/802c82060c54bc7dde21eb0033e337838b8181a1323254aa9ec41cbfc3d1/markdown_it_py-4.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c5/e0/0840fd2f93da988ec660b8ad1984abe9f25d2aed22a5e394ff1c68c88307/matplotlib-3.10.9-cp313-cp313-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/fb/86/dd6e5db36df29e76c7a7699123569a4a18c1623ce68d826ed96c62643cae/mdit_py_plugins-0.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/76/8e/56ccb09c7232a55403a7637caa21922f3b65901a37f5e8bdb405d0de0946/mike-2.2.0-py3-none-any.whl @@ -2341,7 +2347,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/5b/69/93b34728cc386efdde0c342f8c680b9187dea7beb7adaf6b58a0713be101/mpld3-0.5.12-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/74/07/1ed8277f8653c40ebc65985180b007879f6a836c525b3885dcc6448ae6cb/msgpack-1.1.2-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/b2/35/e994121b0e90e46134673422dd564623f93304614f5d11886b1b3e06f503/multidict-6.7.1-cp313-cp313-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/d0/69/f24d3d1c38ad69e256138b4ec2452a8c7cf66be49dc214771ae99dd4f0a0/narwhals-2.20.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c7/e1/68c2256b69a314eba133673377ba9118c356f6342a0c02b61de449cf2bf2/narwhals-2.21.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/eb/be/b257e12f9710819fde40adc972578bee6b72c5992da1bc8369bef2597756/nbmake-1.5.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/28/88/4789719fbbe166d12d345b3ac66b96105f10001b16e00a9765ba29261a21/nbqa-1.9.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/20/16/e777eadfa0c0305878c36fae1d5e6db474fbb15dae202b9ec378809dfb4d/nbstripout-0.9.1-py3-none-any.whl @@ -2349,7 +2355,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/f9/33/bd5b9137445ea4b680023eb0469b2bb969d61303dedb2aac6560ff3d14a1/notebook_shim-0.2.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/57/fb/48649b4971cde70d817cf97a2a2fdc0b4d8308569f1dd2f2611959d2e0cf/numpy-2.4.4-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/5e/52/42855bf626868413f761addd574acc6195880ae247a5346477a4361c3acb/pandas-3.0.2-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/2c/20/559ace4200982c3887d0b86bfd0d856a2143ef8ddab63cc07934951a964c/pandas-3.0.3-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3f/82/8a3739a5e470b3c6cbb1d21d315800d8e16bff503d1f16b03a4ec3212786/pillow-12.2.0-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/3a/eb/fea4d1d51c49832120f7f285d07306db3960f423a2612c6057caf3e8196f/pip-26.1.1-py3-none-any.whl @@ -2358,7 +2364,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/79/ad/45312df6b63ba64ea35b8d8f5f0c577aac16e6b416eafe8e1cb34e03f9a7/plumbum-1.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2a/2d/d4bf65e47cea8ff2c794a600c4fd1273a7902f268757c531e0ee9f18aa58/pooch-1.9.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f5/ab/f76ec3c3627c883215b5c8080debb4394ef5a7a29be811f786415fc1e6fd/propcache-0.4.1-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/1c/fd/3f0f10dba4dabad3bf53102be007abf55481067952bde0fdddff439e7c61/propcache-0.5.2-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/f6/f0/10642828a8dfb741e5f3fbaac830550a518a775c7fff6f04a007259b0548/py-1.11.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl @@ -2370,13 +2376,13 @@ environments: - pypi: https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/30/d4/24d543ab8b8158b7f5a97113c831205f5c900c92c8762b1e7f44b7ea0405/python_discovery-1.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/6f/a05a317a66fee0aad270011461f1a63a453ed12471249f172f7d2e2bc7b4/python_discovery-1.3.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/aa/54/0cce26da03a981f949bb8449c9778537f75f5917c172e1d2992ff25cb57d/python_engineio-4.13.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/07/c7/deb8c5e604404dbf10a3808a858946ca3547692ff6316b698945bb72177e/python_socketio-5.16.1-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/3c/26/1062c7ec1b053db9e499b4d2d5bc231743201b74051c973dadeac80a8f43/questionary-2.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/93/f7/d00d9b4a0313a6be3a3e0818e6375e15da6d7076f4ae47d1324e7ca986a1/radon-6.0.1-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ef/e6/e300fce5fe83c30520607a015dabd985df3251e188d234bfe9492e17a389/requests-2.34.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/33/f1/9614e03e1cdcbf9437570b5400ced8a720b5db22b28d8e0f1bda429f660d/ruff-0.15.12-py3-none-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/37/fd/22621d3ee9e3ee87ef4c89b63bba55b265ab85039b3c1ba88ed2380a24c1/scipp-26.3.1-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/35/e5/d6d0e51fc888f692a35134336866341c08655d92614f492c6860dc45bb2c/scipy-1.17.1-cp313-cp313-win_amd64.whl @@ -2386,14 +2392,14 @@ environments: - pypi: https://files.pythonhosted.org/packages/3e/17/1f31d8562e6f970d64911f1abc330d233bc0c0601411cf7e19c1292be6da/spdx_headers-1.5.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/33/f0/3fe8c6e69135a845f4106f2ff8b6805638d4e85c264e70114e8126689587/tokenize_rt-6.2.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d6/bb/fbdc4e57731efb86b4209ffdd2519520782bf27b3c961beac3e5c20d2b87/trove_classifiers-2026.4.28.13-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7b/e3/d81b065a2d866a33a541ac63a2a4cc5737e03ce2379ac3191c98bb8867e3/trove_classifiers-2026.5.7.17-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8f/5e/f1e1dd319e35e962a4e00b33150a8868b6329cc1d19fd533436ba5488f09/uncertainties-3.2.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b7/ee/e9c95cda829131f71a8dff5ce0406059fd16e591c074414e31ada19ba7c3/validate_pyproject-0.25-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1c/59/964ecb8008722d27d8a835baea81f56a91cea8e097b3be992bc6ccde6367/versioningit-3.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a4/ce/3b6fee91c85626eaf769d617f1be9d2e15c1cca027bbdeb2e0d751469355/verspec-0.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b1/4f/f71e641e504111a5a74e3a20bc52d01bd86788b22699dd3fee1c63253cf6/virtualenv-21.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/20/5b/885f479093f6627669d39b57bc3d4e674da532e1a4b247d473a61d8d2118/virtualenv-21.3.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/3f/0e/fa3b193432cfc60c93b42f3be03365f5f909d2b3ea410295cf36df739e31/widgetsnbextension-4.0.15-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-py3-none-any.whl @@ -2405,6 +2411,8 @@ environments: - url: https://conda.anaconda.org/conda-forge/ indexes: - https://pypi.org/simple + options: + pypi-prerelease-mode: if-necessary-or-explicit packages: linux-64: - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda @@ -3884,45 +3892,45 @@ packages: - questionary>=1.8.1 - typing-extensions>=4.0.0,<5.0.0 ; python_full_version < '3.11' requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/0c/c9/44fb661c55062f0818a6ffd2685c67aa30816200d5f2817543717d4b92eb/coverage-7.13.5-cp313-cp313-macosx_11_0_arm64.whl +- pypi: https://files.pythonhosted.org/packages/6f/5f/b5370068b2f57787454592ed7dcd1002f0f1703b7db1fa30f6a325a4ca6e/coverage-7.14.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl name: coverage - version: 7.13.5 - sha256: 941617e518602e2d64942c88ec8499f7fbd49d3f6c4327d3a71d43a1973032f3 + version: 7.14.0 + sha256: 9d1aa57a1dc8e05bdc42e81c5d671d849577aeedf279f4c449d6d286f9ed88ca requires_dist: - tomli ; python_full_version <= '3.11' and extra == 'toml' requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/35/8b/cd129b0ca4afe886a6ce9d183c44d8301acbd4ef248622e7c49a23145605/coverage-7.13.5-cp311-cp311-macosx_11_0_arm64.whl +- pypi: https://files.pythonhosted.org/packages/7f/8d/46692d24b3f395d4cbf17bfcc57136b4f2f9c0c0df864b0bddfc1d71a014/coverage-7.14.0-cp311-cp311-macosx_11_0_arm64.whl name: coverage - version: 7.13.5 - sha256: 145ede53ccbafb297c1c9287f788d1bc3efd6c900da23bf6931b09eafc931587 + version: 7.14.0 + sha256: a1816c505187592dcd1c5a5f226601a549f70365fbd00930ac88b0c225b76bb4 requires_dist: - tomli ; python_full_version <= '3.11' and extra == 'toml' requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/66/40/7732d648ab9d069a46e686043241f01206348e2bbf128daea85be4d6414b/coverage-7.13.5-cp313-cp313-win_amd64.whl +- pypi: https://files.pythonhosted.org/packages/8f/b8/9228523e80321c2cb4880d1f589bc0171f2f71432c35118ad04dc01decce/coverage-7.14.0-cp313-cp313-win_amd64.whl name: coverage - version: 7.13.5 - sha256: 631efb83f01569670a5e866ceb80fe483e7c159fac6f167e6571522636104a0b + version: 7.14.0 + sha256: 0773d8329cf32b6fd222e4b52622c61fe8d503eb966cfc8d3c3c10c96266d50e requires_dist: - tomli ; python_full_version <= '3.11' and extra == 'toml' requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/92/be/b1afb692be85b947f3401375851484496134c5554e67e822c35f28bf2fbc/coverage-7.13.5-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl +- pypi: https://files.pythonhosted.org/packages/b3/af/e567cbad5ba69c013a50146dfa886dc7193361fda77521f51274ff620e1b/coverage-7.14.0-cp313-cp313-macosx_11_0_arm64.whl name: coverage - version: 7.13.5 - sha256: ec10e2a42b41c923c2209b846126c6582db5e43a33157e9870ba9fb70dc7854b + version: 7.14.0 + sha256: 23b81107f46d3f21d0cbce30664fcec0f5d9f585638a67081750f99738f6bf66 requires_dist: - tomli ; python_full_version <= '3.11' and extra == 'toml' requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/ac/68/1666e3a4462f8202d836920114fa7a5ee9275d1fa45366d336c551a162dd/coverage-7.13.5-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl +- pypi: https://files.pythonhosted.org/packages/f0/f0/a71ddbd874431e7a7cd96071f0c331cfbbad07704833c765d24ffbab8a67/coverage-7.14.0-cp311-cp311-win_amd64.whl name: coverage - version: 7.13.5 - sha256: 78e696e1cc714e57e8b25760b33a8b1026b7048d270140d25dafe1b0a1ee05a3 + version: 7.14.0 + sha256: bfb0ed8ec5d25e93face268115d7964db9df8b9aae8edcde9ec6b16c726a7cc1 requires_dist: - tomli ; python_full_version <= '3.11' and extra == 'toml' requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/af/7f/4cd8a92531253f9d7c1bbecd9fa1b472907fb54446ca768c59b531248dc5/coverage-7.13.5-cp311-cp311-win_amd64.whl +- pypi: https://files.pythonhosted.org/packages/fd/35/202235eb5c3c14c212462cd91d61b7386bf8fc44bc7a77f4742d2a69174b/coverage-7.14.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl name: coverage - version: 7.13.5 - sha256: 258354455f4e86e3e9d0d17571d522e13b4e1e19bf0f8596bcf9476d61e7d8a9 + version: 7.14.0 + sha256: 9336e23e8bb3a3925398261385e2a1533957d3e760e91070dcb0e98bfa514eed requires_dist: - tomli ; python_full_version <= '3.11' and extra == 'toml' requires_python: '>=3.10' @@ -4166,8 +4174,8 @@ packages: requires_python: '>=3.11' - pypi: ./ name: easyscience - version: 2.3.1+devdirty11 - sha256: 9046eb543a77a4603204f7e67b55f0f0e82840101820ef1051cbfc23766ed4f0 + version: 2.3.1+devdirty12 + sha256: ec0f736717da6cf2e6807feed4c5d7f304603284cb2d06fea08361420615c957 requires_dist: - asteval - bumps @@ -4214,7 +4222,6 @@ packages: - validate-pyproject[all] ; extra == 'dev' - versioningit ; extra == 'dev' requires_python: '>=3.11' - editable: true - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda sha256: ee6cf346d017d954255bbcbdb424cddea4d14e4ed7e9813e429db1d795d01144 md5: 8e662bd460bda79b1ea39194e3c4c9ab @@ -5262,10 +5269,10 @@ packages: name: jupyterquiz version: 2.9.6.4 sha256: f8c4418f6c827454523fc882a30d744b585cb58ac1ae277769c3059d04fc272b -- pypi: https://files.pythonhosted.org/packages/16/5a/736dd2f4535dbf3bf26523f9158c011389ef88dd06ec2eef67fd744f1c7b/jupytext-1.19.1-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/4c/65/b4b86e5fa07543bfbbcdc6c9f7f9f561e66a5f3539992e3009973f2b1314/jupytext-1.19.2-py3-none-any.whl name: jupytext - version: 1.19.1 - sha256: d8975035155d034bdfde5c0c37891425314b7ea8d3a6c4b5d18c294348714cd9 + version: 1.19.2 + sha256: 8a31e896c7e9215841783aade24336e945543057e1c2d7f00b22f9e870348688 requires_dist: - markdown-it-py>=1.0 - mdit-py-plugins @@ -6028,10 +6035,10 @@ packages: - mkdocs-section-index ; extra == 'docs' - mkdocs-literate-nav ; extra == 'docs' requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/a8/88/802c82060c54bc7dde21eb0033e337838b8181a1323254aa9ec41cbfc3d1/markdown_it_py-4.1.0-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl name: markdown-it-py - version: 4.1.0 - sha256: d4939a62a2dd0cd9cb80a191a711ba1d39bac8ed5ef9e9966895b0171c01c46d + version: 4.2.0 + sha256: 9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a requires_dist: - mdurl~=0.1 - psutil ; extra == 'benchmarking' @@ -6286,10 +6293,10 @@ packages: - pkg:pypi/matplotlib-inline?source=hash-mapping size: 15175 timestamp: 1761214578417 -- pypi: https://files.pythonhosted.org/packages/fb/86/dd6e5db36df29e76c7a7699123569a4a18c1623ce68d826ed96c62643cae/mdit_py_plugins-0.5.0-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl name: mdit-py-plugins - version: 0.5.0 - sha256: 07a08422fc1936a5d26d146759e9155ea466e842f5ab2f7d2266dd084c8dab1f + version: 0.6.1 + sha256: 214c82fb2ac524472ab6a5bcab1de80f73b50443e187f401bfd77efbc7c6481d requires_dist: - markdown-it-py>=2.0.0,<5.0.0 - pre-commit ; extra == 'code-style' @@ -6299,6 +6306,7 @@ packages: - pytest ; extra == 'testing' - pytest-cov ; extra == 'testing' - pytest-regressions ; extra == 'testing' + - pytest-timeout ; extra == 'testing' requires_python: '>=3.10' - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl name: mdurl @@ -6666,6 +6674,28 @@ packages: - sqlparse ; extra == 'sql' - sqlframe>=3.22.0,!=3.39.3 ; extra == 'sqlframe' requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/c7/e1/68c2256b69a314eba133673377ba9118c356f6342a0c02b61de449cf2bf2/narwhals-2.21.0-py3-none-any.whl + name: narwhals + version: 2.21.0 + sha256: 1e6617d0fca68ae1fda29e5397c4eaacd3ffc9fffe6bcd6ded0c690475e853be + requires_dist: + - cudf-cu12>=24.10.0 ; extra == 'cudf' + - dask[dataframe]>=2024.8 ; extra == 'dask' + - duckdb>=1.1 ; extra == 'duckdb' + - ibis-framework>=6.0.0 ; extra == 'ibis' + - packaging ; extra == 'ibis' + - pyarrow-hotfix ; extra == 'ibis' + - rich ; extra == 'ibis' + - modin ; extra == 'modin' + - pandas>=1.1.3 ; extra == 'pandas' + - polars>=0.20.4 ; extra == 'polars' + - pyarrow>=13.0.0 ; extra == 'pyarrow' + - pyspark>=3.5.0 ; extra == 'pyspark' + - pyspark[connect]>=3.5.0 ; extra == 'pyspark-connect' + - duckdb>=1.1 ; extra == 'sql' + - sqlparse ; extra == 'sql' + - sqlframe>=3.22.0,!=3.39.3 ; extra == 'sqlframe' + requires_python: '>=3.9' - conda: https://conda.anaconda.org/conda-forge/noarch/nbclient-0.10.4-pyhd8ed1ab_0.conda sha256: 1b66960ee06874ddceeebe375d5f17fb5f393d025a09e15b830ad0c4fffb585b md5: 00f5b8dafa842e0c27c1cd7296aa4875 @@ -7144,10 +7174,10 @@ packages: - xlsxwriter>=3.2.0 ; extra == 'all' - zstandard>=0.23.0 ; extra == 'all' requires_python: '>=3.11' -- pypi: https://files.pythonhosted.org/packages/20/17/ec40d981705654853726e7ac9aea9ddbb4a5d9cf54d8472222f4f3de06c2/pandas-3.0.2-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl +- pypi: https://files.pythonhosted.org/packages/5e/52/42855bf626868413f761addd574acc6195880ae247a5346477a4361c3acb/pandas-3.0.2-cp313-cp313-win_amd64.whl name: pandas version: 3.0.2 - sha256: 61c2fd96d72b983a9891b2598f286befd4ad262161a609c92dc1652544b46b76 + sha256: 07a10f5c36512eead51bc578eb3354ad17578b22c013d89a796ab5eee90cd991 requires_dist: - numpy>=1.26.0 ; python_full_version < '3.14' - numpy>=2.3.3 ; python_full_version >= '3.14' @@ -7234,10 +7264,10 @@ packages: - xlsxwriter>=3.2.0 ; extra == 'all' - zstandard>=0.23.0 ; extra == 'all' requires_python: '>=3.11' -- pypi: https://files.pythonhosted.org/packages/44/a0/97a6339859d4acb2536efb24feb6708e82f7d33b2ed7e036f2983fcced82/pandas-3.0.2-cp311-cp311-win_amd64.whl +- pypi: https://files.pythonhosted.org/packages/2c/20/559ace4200982c3887d0b86bfd0d856a2143ef8ddab63cc07934951a964c/pandas-3.0.3-cp313-cp313-win_amd64.whl name: pandas - version: 3.0.2 - sha256: ed72cb3f45190874eb579c64fa92d9df74e98fd63e2be7f62bce5ace0ade61df + version: 3.0.3 + sha256: a82d532a3351d435432cd913edbccaf8b8e01d4dd0e5ced5a8d2e8ecd94c7e44 requires_dist: - numpy>=1.26.0 ; python_full_version < '3.14' - numpy>=2.3.3 ; python_full_version >= '3.14' @@ -7285,7 +7315,7 @@ packages: - pyqt5>=5.15.9 ; extra == 'clipboard' - qtpy>=2.4.2 ; extra == 'clipboard' - zstandard>=0.23.0 ; extra == 'compression' - - pytz>=2024.2 ; extra == 'timezone' + - pytz>=2020.1 ; extra == 'timezone' - adbc-driver-postgresql>=1.2.0 ; extra == 'all' - adbc-driver-sqlite>=1.2.0 ; extra == 'all' - beautifulsoup4>=4.12.3 ; extra == 'all' @@ -7311,7 +7341,7 @@ packages: - pytest>=8.3.4 ; extra == 'all' - pytest-xdist>=3.6.1 ; extra == 'all' - python-calamine>=0.3.0 ; extra == 'all' - - pytz>=2024.2 ; extra == 'all' + - pytz>=2020.1 ; extra == 'all' - pyxlsb>=1.0.10 ; extra == 'all' - qtpy>=2.4.2 ; extra == 'all' - scipy>=1.14.1 ; extra == 'all' @@ -7324,10 +7354,10 @@ packages: - xlsxwriter>=3.2.0 ; extra == 'all' - zstandard>=0.23.0 ; extra == 'all' requires_python: '>=3.11' -- pypi: https://files.pythonhosted.org/packages/5e/52/42855bf626868413f761addd574acc6195880ae247a5346477a4361c3acb/pandas-3.0.2-cp313-cp313-win_amd64.whl +- pypi: https://files.pythonhosted.org/packages/32/f1/bbecd2f867b97abebe0f9b53d750f862251b40337e061b36676ded3d920f/pandas-3.0.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl name: pandas - version: 3.0.2 - sha256: 07a10f5c36512eead51bc578eb3354ad17578b22c013d89a796ab5eee90cd991 + version: 3.0.3 + sha256: 8a1e45c80cceb3b4a21bc5939d52e8cbd8d9b7305309219d59e9754d9ce09e27 requires_dist: - numpy>=1.26.0 ; python_full_version < '3.14' - numpy>=2.3.3 ; python_full_version >= '3.14' @@ -7375,7 +7405,7 @@ packages: - pyqt5>=5.15.9 ; extra == 'clipboard' - qtpy>=2.4.2 ; extra == 'clipboard' - zstandard>=0.23.0 ; extra == 'compression' - - pytz>=2024.2 ; extra == 'timezone' + - pytz>=2020.1 ; extra == 'timezone' - adbc-driver-postgresql>=1.2.0 ; extra == 'all' - adbc-driver-sqlite>=1.2.0 ; extra == 'all' - beautifulsoup4>=4.12.3 ; extra == 'all' @@ -7401,7 +7431,7 @@ packages: - pytest>=8.3.4 ; extra == 'all' - pytest-xdist>=3.6.1 ; extra == 'all' - python-calamine>=0.3.0 ; extra == 'all' - - pytz>=2024.2 ; extra == 'all' + - pytz>=2020.1 ; extra == 'all' - pyxlsb>=1.0.10 ; extra == 'all' - qtpy>=2.4.2 ; extra == 'all' - scipy>=1.14.1 ; extra == 'all' @@ -7414,10 +7444,10 @@ packages: - xlsxwriter>=3.2.0 ; extra == 'all' - zstandard>=0.23.0 ; extra == 'all' requires_python: '>=3.11' -- pypi: https://files.pythonhosted.org/packages/c4/d3/b7da1d5d7dbdc5ef52ed7debd2b484313b832982266905315dad5a0bf0b1/pandas-3.0.2-cp311-cp311-macosx_11_0_arm64.whl +- pypi: https://files.pythonhosted.org/packages/5a/b0/a4ffc4ae74d2d822200dcc46898987d8eb6032d1e2b219cae39da6f5cbcc/pandas-3.0.3-cp311-cp311-macosx_11_0_arm64.whl name: pandas - version: 3.0.2 - sha256: dbbd4aa20ca51e63b53bbde6a0fa4254b1aaabb74d2f542df7a7959feb1d760c + version: 3.0.3 + sha256: 4e15135e2ee5df1063313e2425ceef8ac0f4ae775893815b0923651b806a5639 requires_dist: - numpy>=1.26.0 ; python_full_version < '3.14' - numpy>=2.3.3 ; python_full_version >= '3.14' @@ -7465,7 +7495,7 @@ packages: - pyqt5>=5.15.9 ; extra == 'clipboard' - qtpy>=2.4.2 ; extra == 'clipboard' - zstandard>=0.23.0 ; extra == 'compression' - - pytz>=2024.2 ; extra == 'timezone' + - pytz>=2020.1 ; extra == 'timezone' - adbc-driver-postgresql>=1.2.0 ; extra == 'all' - adbc-driver-sqlite>=1.2.0 ; extra == 'all' - beautifulsoup4>=4.12.3 ; extra == 'all' @@ -7491,7 +7521,277 @@ packages: - pytest>=8.3.4 ; extra == 'all' - pytest-xdist>=3.6.1 ; extra == 'all' - python-calamine>=0.3.0 ; extra == 'all' - - pytz>=2024.2 ; extra == 'all' + - pytz>=2020.1 ; extra == 'all' + - pyxlsb>=1.0.10 ; extra == 'all' + - qtpy>=2.4.2 ; extra == 'all' + - scipy>=1.14.1 ; extra == 'all' + - s3fs>=2024.10.0 ; extra == 'all' + - sqlalchemy>=2.0.36 ; extra == 'all' + - tables>=3.10.1 ; extra == 'all' + - tabulate>=0.9.0 ; extra == 'all' + - xarray>=2024.10.0 ; extra == 'all' + - xlrd>=2.0.1 ; extra == 'all' + - xlsxwriter>=3.2.0 ; extra == 'all' + - zstandard>=0.23.0 ; extra == 'all' + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/7f/ae/6a6493c783a101f165e4356953ba3c74d6f77f0042fa7d753da9dfbb640c/pandas-3.0.3-cp313-cp313-macosx_11_0_arm64.whl + name: pandas + version: 3.0.3 + sha256: 39436b377d56d2a2e52d0395bdbee171f01068e99af5250509aceeb929f765c7 + requires_dist: + - numpy>=1.26.0 ; python_full_version < '3.14' + - numpy>=2.3.3 ; python_full_version >= '3.14' + - python-dateutil>=2.8.2 + - tzdata ; sys_platform == 'win32' + - tzdata ; sys_platform == 'emscripten' + - hypothesis>=6.116.0 ; extra == 'test' + - pytest>=8.3.4 ; extra == 'test' + - pytest-xdist>=3.6.1 ; extra == 'test' + - pyarrow>=13.0.0 ; extra == 'pyarrow' + - bottleneck>=1.4.2 ; extra == 'performance' + - numba>=0.60.0 ; extra == 'performance' + - numexpr>=2.10.2 ; extra == 'performance' + - scipy>=1.14.1 ; extra == 'computation' + - xarray>=2024.10.0 ; extra == 'computation' + - fsspec>=2024.10.0 ; extra == 'fss' + - s3fs>=2024.10.0 ; extra == 'aws' + - gcsfs>=2024.10.0 ; extra == 'gcp' + - odfpy>=1.4.1 ; extra == 'excel' + - openpyxl>=3.1.5 ; extra == 'excel' + - python-calamine>=0.3.0 ; extra == 'excel' + - pyxlsb>=1.0.10 ; extra == 'excel' + - xlrd>=2.0.1 ; extra == 'excel' + - xlsxwriter>=3.2.0 ; extra == 'excel' + - pyarrow>=13.0.0 ; extra == 'parquet' + - pyarrow>=13.0.0 ; extra == 'feather' + - pyiceberg>=0.8.1 ; extra == 'iceberg' + - tables>=3.10.1 ; extra == 'hdf5' + - pyreadstat>=1.2.8 ; extra == 'spss' + - sqlalchemy>=2.0.36 ; extra == 'postgresql' + - psycopg2>=2.9.10 ; extra == 'postgresql' + - adbc-driver-postgresql>=1.2.0 ; extra == 'postgresql' + - sqlalchemy>=2.0.36 ; extra == 'mysql' + - pymysql>=1.1.1 ; extra == 'mysql' + - sqlalchemy>=2.0.36 ; extra == 'sql-other' + - adbc-driver-postgresql>=1.2.0 ; extra == 'sql-other' + - adbc-driver-sqlite>=1.2.0 ; extra == 'sql-other' + - beautifulsoup4>=4.12.3 ; extra == 'html' + - html5lib>=1.1 ; extra == 'html' + - lxml>=5.3.0 ; extra == 'html' + - lxml>=5.3.0 ; extra == 'xml' + - matplotlib>=3.9.3 ; extra == 'plot' + - jinja2>=3.1.5 ; extra == 'output-formatting' + - tabulate>=0.9.0 ; extra == 'output-formatting' + - pyqt5>=5.15.9 ; extra == 'clipboard' + - qtpy>=2.4.2 ; extra == 'clipboard' + - zstandard>=0.23.0 ; extra == 'compression' + - pytz>=2020.1 ; extra == 'timezone' + - adbc-driver-postgresql>=1.2.0 ; extra == 'all' + - adbc-driver-sqlite>=1.2.0 ; extra == 'all' + - beautifulsoup4>=4.12.3 ; extra == 'all' + - bottleneck>=1.4.2 ; extra == 'all' + - fastparquet>=2024.11.0 ; extra == 'all' + - fsspec>=2024.10.0 ; extra == 'all' + - gcsfs>=2024.10.0 ; extra == 'all' + - html5lib>=1.1 ; extra == 'all' + - hypothesis>=6.116.0 ; extra == 'all' + - jinja2>=3.1.5 ; extra == 'all' + - lxml>=5.3.0 ; extra == 'all' + - matplotlib>=3.9.3 ; extra == 'all' + - numba>=0.60.0 ; extra == 'all' + - numexpr>=2.10.2 ; extra == 'all' + - odfpy>=1.4.1 ; extra == 'all' + - openpyxl>=3.1.5 ; extra == 'all' + - psycopg2>=2.9.10 ; extra == 'all' + - pyarrow>=13.0.0 ; extra == 'all' + - pyiceberg>=0.8.1 ; extra == 'all' + - pymysql>=1.1.1 ; extra == 'all' + - pyqt5>=5.15.9 ; extra == 'all' + - pyreadstat>=1.2.8 ; extra == 'all' + - pytest>=8.3.4 ; extra == 'all' + - pytest-xdist>=3.6.1 ; extra == 'all' + - python-calamine>=0.3.0 ; extra == 'all' + - pytz>=2020.1 ; extra == 'all' + - pyxlsb>=1.0.10 ; extra == 'all' + - qtpy>=2.4.2 ; extra == 'all' + - scipy>=1.14.1 ; extra == 'all' + - s3fs>=2024.10.0 ; extra == 'all' + - sqlalchemy>=2.0.36 ; extra == 'all' + - tables>=3.10.1 ; extra == 'all' + - tabulate>=0.9.0 ; extra == 'all' + - xarray>=2024.10.0 ; extra == 'all' + - xlrd>=2.0.1 ; extra == 'all' + - xlsxwriter>=3.2.0 ; extra == 'all' + - zstandard>=0.23.0 ; extra == 'all' + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/99/68/1237369725aa617bb358263d535803e3053fdbc593513ec5ed9c9896b5b6/pandas-3.0.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl + name: pandas + version: 3.0.3 + sha256: a4eeb6830daf35a71cc09649bd823e2b542dac246cdee9614c6e4bd65028cd6a + requires_dist: + - numpy>=1.26.0 ; python_full_version < '3.14' + - numpy>=2.3.3 ; python_full_version >= '3.14' + - python-dateutil>=2.8.2 + - tzdata ; sys_platform == 'win32' + - tzdata ; sys_platform == 'emscripten' + - hypothesis>=6.116.0 ; extra == 'test' + - pytest>=8.3.4 ; extra == 'test' + - pytest-xdist>=3.6.1 ; extra == 'test' + - pyarrow>=13.0.0 ; extra == 'pyarrow' + - bottleneck>=1.4.2 ; extra == 'performance' + - numba>=0.60.0 ; extra == 'performance' + - numexpr>=2.10.2 ; extra == 'performance' + - scipy>=1.14.1 ; extra == 'computation' + - xarray>=2024.10.0 ; extra == 'computation' + - fsspec>=2024.10.0 ; extra == 'fss' + - s3fs>=2024.10.0 ; extra == 'aws' + - gcsfs>=2024.10.0 ; extra == 'gcp' + - odfpy>=1.4.1 ; extra == 'excel' + - openpyxl>=3.1.5 ; extra == 'excel' + - python-calamine>=0.3.0 ; extra == 'excel' + - pyxlsb>=1.0.10 ; extra == 'excel' + - xlrd>=2.0.1 ; extra == 'excel' + - xlsxwriter>=3.2.0 ; extra == 'excel' + - pyarrow>=13.0.0 ; extra == 'parquet' + - pyarrow>=13.0.0 ; extra == 'feather' + - pyiceberg>=0.8.1 ; extra == 'iceberg' + - tables>=3.10.1 ; extra == 'hdf5' + - pyreadstat>=1.2.8 ; extra == 'spss' + - sqlalchemy>=2.0.36 ; extra == 'postgresql' + - psycopg2>=2.9.10 ; extra == 'postgresql' + - adbc-driver-postgresql>=1.2.0 ; extra == 'postgresql' + - sqlalchemy>=2.0.36 ; extra == 'mysql' + - pymysql>=1.1.1 ; extra == 'mysql' + - sqlalchemy>=2.0.36 ; extra == 'sql-other' + - adbc-driver-postgresql>=1.2.0 ; extra == 'sql-other' + - adbc-driver-sqlite>=1.2.0 ; extra == 'sql-other' + - beautifulsoup4>=4.12.3 ; extra == 'html' + - html5lib>=1.1 ; extra == 'html' + - lxml>=5.3.0 ; extra == 'html' + - lxml>=5.3.0 ; extra == 'xml' + - matplotlib>=3.9.3 ; extra == 'plot' + - jinja2>=3.1.5 ; extra == 'output-formatting' + - tabulate>=0.9.0 ; extra == 'output-formatting' + - pyqt5>=5.15.9 ; extra == 'clipboard' + - qtpy>=2.4.2 ; extra == 'clipboard' + - zstandard>=0.23.0 ; extra == 'compression' + - pytz>=2020.1 ; extra == 'timezone' + - adbc-driver-postgresql>=1.2.0 ; extra == 'all' + - adbc-driver-sqlite>=1.2.0 ; extra == 'all' + - beautifulsoup4>=4.12.3 ; extra == 'all' + - bottleneck>=1.4.2 ; extra == 'all' + - fastparquet>=2024.11.0 ; extra == 'all' + - fsspec>=2024.10.0 ; extra == 'all' + - gcsfs>=2024.10.0 ; extra == 'all' + - html5lib>=1.1 ; extra == 'all' + - hypothesis>=6.116.0 ; extra == 'all' + - jinja2>=3.1.5 ; extra == 'all' + - lxml>=5.3.0 ; extra == 'all' + - matplotlib>=3.9.3 ; extra == 'all' + - numba>=0.60.0 ; extra == 'all' + - numexpr>=2.10.2 ; extra == 'all' + - odfpy>=1.4.1 ; extra == 'all' + - openpyxl>=3.1.5 ; extra == 'all' + - psycopg2>=2.9.10 ; extra == 'all' + - pyarrow>=13.0.0 ; extra == 'all' + - pyiceberg>=0.8.1 ; extra == 'all' + - pymysql>=1.1.1 ; extra == 'all' + - pyqt5>=5.15.9 ; extra == 'all' + - pyreadstat>=1.2.8 ; extra == 'all' + - pytest>=8.3.4 ; extra == 'all' + - pytest-xdist>=3.6.1 ; extra == 'all' + - python-calamine>=0.3.0 ; extra == 'all' + - pytz>=2020.1 ; extra == 'all' + - pyxlsb>=1.0.10 ; extra == 'all' + - qtpy>=2.4.2 ; extra == 'all' + - scipy>=1.14.1 ; extra == 'all' + - s3fs>=2024.10.0 ; extra == 'all' + - sqlalchemy>=2.0.36 ; extra == 'all' + - tables>=3.10.1 ; extra == 'all' + - tabulate>=0.9.0 ; extra == 'all' + - xarray>=2024.10.0 ; extra == 'all' + - xlrd>=2.0.1 ; extra == 'all' + - xlsxwriter>=3.2.0 ; extra == 'all' + - zstandard>=0.23.0 ; extra == 'all' + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/eb/62/c321f13b5ba1819fc8dca456c7fce578da2dcfecff1abbf0eaddf8406c0f/pandas-3.0.3-cp311-cp311-win_amd64.whl + name: pandas + version: 3.0.3 + sha256: 6674ab18ad8c57802867264b00e15e7bb904700cdd9046e3b2fa1fce237439ea + requires_dist: + - numpy>=1.26.0 ; python_full_version < '3.14' + - numpy>=2.3.3 ; python_full_version >= '3.14' + - python-dateutil>=2.8.2 + - tzdata ; sys_platform == 'win32' + - tzdata ; sys_platform == 'emscripten' + - hypothesis>=6.116.0 ; extra == 'test' + - pytest>=8.3.4 ; extra == 'test' + - pytest-xdist>=3.6.1 ; extra == 'test' + - pyarrow>=13.0.0 ; extra == 'pyarrow' + - bottleneck>=1.4.2 ; extra == 'performance' + - numba>=0.60.0 ; extra == 'performance' + - numexpr>=2.10.2 ; extra == 'performance' + - scipy>=1.14.1 ; extra == 'computation' + - xarray>=2024.10.0 ; extra == 'computation' + - fsspec>=2024.10.0 ; extra == 'fss' + - s3fs>=2024.10.0 ; extra == 'aws' + - gcsfs>=2024.10.0 ; extra == 'gcp' + - odfpy>=1.4.1 ; extra == 'excel' + - openpyxl>=3.1.5 ; extra == 'excel' + - python-calamine>=0.3.0 ; extra == 'excel' + - pyxlsb>=1.0.10 ; extra == 'excel' + - xlrd>=2.0.1 ; extra == 'excel' + - xlsxwriter>=3.2.0 ; extra == 'excel' + - pyarrow>=13.0.0 ; extra == 'parquet' + - pyarrow>=13.0.0 ; extra == 'feather' + - pyiceberg>=0.8.1 ; extra == 'iceberg' + - tables>=3.10.1 ; extra == 'hdf5' + - pyreadstat>=1.2.8 ; extra == 'spss' + - sqlalchemy>=2.0.36 ; extra == 'postgresql' + - psycopg2>=2.9.10 ; extra == 'postgresql' + - adbc-driver-postgresql>=1.2.0 ; extra == 'postgresql' + - sqlalchemy>=2.0.36 ; extra == 'mysql' + - pymysql>=1.1.1 ; extra == 'mysql' + - sqlalchemy>=2.0.36 ; extra == 'sql-other' + - adbc-driver-postgresql>=1.2.0 ; extra == 'sql-other' + - adbc-driver-sqlite>=1.2.0 ; extra == 'sql-other' + - beautifulsoup4>=4.12.3 ; extra == 'html' + - html5lib>=1.1 ; extra == 'html' + - lxml>=5.3.0 ; extra == 'html' + - lxml>=5.3.0 ; extra == 'xml' + - matplotlib>=3.9.3 ; extra == 'plot' + - jinja2>=3.1.5 ; extra == 'output-formatting' + - tabulate>=0.9.0 ; extra == 'output-formatting' + - pyqt5>=5.15.9 ; extra == 'clipboard' + - qtpy>=2.4.2 ; extra == 'clipboard' + - zstandard>=0.23.0 ; extra == 'compression' + - pytz>=2020.1 ; extra == 'timezone' + - adbc-driver-postgresql>=1.2.0 ; extra == 'all' + - adbc-driver-sqlite>=1.2.0 ; extra == 'all' + - beautifulsoup4>=4.12.3 ; extra == 'all' + - bottleneck>=1.4.2 ; extra == 'all' + - fastparquet>=2024.11.0 ; extra == 'all' + - fsspec>=2024.10.0 ; extra == 'all' + - gcsfs>=2024.10.0 ; extra == 'all' + - html5lib>=1.1 ; extra == 'all' + - hypothesis>=6.116.0 ; extra == 'all' + - jinja2>=3.1.5 ; extra == 'all' + - lxml>=5.3.0 ; extra == 'all' + - matplotlib>=3.9.3 ; extra == 'all' + - numba>=0.60.0 ; extra == 'all' + - numexpr>=2.10.2 ; extra == 'all' + - odfpy>=1.4.1 ; extra == 'all' + - openpyxl>=3.1.5 ; extra == 'all' + - psycopg2>=2.9.10 ; extra == 'all' + - pyarrow>=13.0.0 ; extra == 'all' + - pyiceberg>=0.8.1 ; extra == 'all' + - pymysql>=1.1.1 ; extra == 'all' + - pyqt5>=5.15.9 ; extra == 'all' + - pyreadstat>=1.2.8 ; extra == 'all' + - pytest>=8.3.4 ; extra == 'all' + - pytest-xdist>=3.6.1 ; extra == 'all' + - python-calamine>=0.3.0 ; extra == 'all' + - pytz>=2020.1 ; extra == 'all' - pyxlsb>=1.0.10 ; extra == 'all' - qtpy>=2.4.2 ; extra == 'all' - scipy>=1.14.1 ; extra == 'all' @@ -7914,36 +8214,51 @@ packages: - pkg:pypi/prompt-toolkit?source=hash-mapping size: 273927 timestamp: 1756321848365 -- pypi: https://files.pythonhosted.org/packages/52/6a/57f43e054fb3d3a56ac9fc532bc684fc6169a26c75c353e65425b3e56eef/propcache-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - name: propcache - version: 0.4.1 - sha256: fd6f30fdcf9ae2a70abd34da54f18da086160e4d7d9251f81f3da0ff84fc5a48 - requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/89/a4/92380f7ca60f99ebae761936bc48a72a639e8a47b29050615eef757cb2a7/propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl name: propcache version: 0.4.1 sha256: cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74 requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/d3/1d/11605e99ac8ea9435651ee71ab4cb4bf03f0949586246476a25aadfec54a/propcache-0.4.1-cp311-cp311-macosx_11_0_arm64.whl - name: propcache - version: 0.4.1 - sha256: 6918ecbd897443087a3b7cd978d56546a812517dcaaca51b49526720571fa93e - requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/f1/8b/544bc867e24e1bd48f3118cecd3b05c694e160a168478fa28770f22fd094/propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl name: propcache version: 0.4.1 sha256: d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/f4/78/6cce448e2098e9f3bfc91bb877f06aa24b6ccace872e39c53b2f707c4648/propcache-0.4.1-cp311-cp311-win_amd64.whl - name: propcache - version: 0.4.1 - sha256: 364426a62660f3f699949ac8c621aad6977be7126c5807ce48c0aeb8e7333ea6 - requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/f5/ab/f76ec3c3627c883215b5c8080debb4394ef5a7a29be811f786415fc1e6fd/propcache-0.4.1-cp313-cp313-win_amd64.whl name: propcache version: 0.4.1 sha256: 381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/15/a8/8ede85d6aa1f79fc7dc2f8fd2c8d65920b8272c3892903c8a1affde48cfb/propcache-0.5.2-cp311-cp311-macosx_11_0_arm64.whl + name: propcache + version: 0.5.2 + sha256: c6844ba6364fb12f403928a82cfd295ab103a2b315c77c747b2dbe4a41894ea7 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/1c/fd/3f0f10dba4dabad3bf53102be007abf55481067952bde0fdddff439e7c61/propcache-0.5.2-cp313-cp313-win_amd64.whl + name: propcache + version: 0.5.2 + sha256: dfed59d0a5aeb01e242e66ff0300bc4a265a7c05f612d30016f0b60b1017d757 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/27/1b/16ab7f2cf2041da2f60d156ba64c2484eadf9168075b4ff43c3ef60045af/propcache-0.5.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + name: propcache + version: 0.5.2 + sha256: 5aaa2b923c1944ac8febd6609cb373540a5563e7cbcb0fd770f75dace2eb817b + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/42/4e/f17363fb58c0afe05b067361cb6d86ed2d29de6506779a27547c4d183075/propcache-0.5.2-cp311-cp311-win_amd64.whl + name: propcache + version: 0.5.2 + sha256: 44e488ef40dbb452700b2b1f8188934121f6648f52c295055662d2191959ff82 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/8b/c6/979176efdaa3d239e36d503d5af63a0a773b36662ed8f52e5b6a6d9fd40e/propcache-0.5.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + name: propcache + version: 0.5.2 + sha256: 4db0ba63d693afd40d249bd93f842b5f144f8fcbb83de05660373bcf30517b1d + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/a9/12/a6ba6482bb5ea3260c000c9b20881c95fa11c6b30173715668259f844ed7/propcache-0.5.2-cp313-cp313-macosx_11_0_arm64.whl + name: propcache + version: 0.5.2 + sha256: fc299c129490f55f254cd90be0deca4764e36e9a7c08b4aa588479a3bbed3098 + requires_python: '>=3.10' - conda: https://conda.anaconda.org/conda-forge/linux-64/psutil-7.2.2-py311haee01d2_0.conda sha256: 8d9325af538a8f56013e42bbb91a4dc6935aece34476e20bafacf6007b571e86 md5: 2ed8f6fe8b51d8e19f7621941f7bb95f @@ -8436,10 +8751,10 @@ packages: - pkg:pypi/python-dateutil?source=hash-mapping size: 233310 timestamp: 1751104122689 -- pypi: https://files.pythonhosted.org/packages/30/d4/24d543ab8b8158b7f5a97113c831205f5c900c92c8762b1e7f44b7ea0405/python_discovery-1.3.0-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/b7/6f/a05a317a66fee0aad270011461f1a63a453ed12471249f172f7d2e2bc7b4/python_discovery-1.3.1-py3-none-any.whl name: python-discovery - version: 1.3.0 - sha256: 441d9ced3dfce36e113beb35ca302c71c7ef06f3c0f9c227a0b9bb3bd49b9e9f + version: 1.3.1 + sha256: ed188687ebb3b82c01a17cd5ac62fc94d9f6487a7f1a0f9dfe89753fec91039c requires_dist: - filelock>=3.15.4 - platformdirs>=4.3.6,<5 @@ -8447,6 +8762,8 @@ packages: - sphinx-autodoc-typehints>=3.6.3 ; extra == 'docs' - sphinx>=9.1 ; extra == 'docs' - sphinxcontrib-mermaid>=2 ; extra == 'docs' + - sphinxcontrib-towncrier>=0.4 ; extra == 'docs' + - towncrier>=25.8 ; extra == 'docs' - covdefaults>=2.3 ; extra == 'testing' - coverage>=7.5.4 ; extra == 'testing' - pytest-mock>=3.14 ; extra == 'testing' @@ -8881,6 +9198,18 @@ packages: - pysocks>=1.5.6,!=1.5.7 ; extra == 'socks' - chardet>=3.0.2,<8 ; extra == 'use-chardet-on-py3' requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/ef/e6/e300fce5fe83c30520607a015dabd985df3251e188d234bfe9492e17a389/requests-2.34.0-py3-none-any.whl + name: requests + version: 2.34.0 + sha256: 917520a21b767485ce7c588f4ebb917c436b24a31231b44228715eaeb5a52c60 + requires_dist: + - charset-normalizer>=2,<4 + - idna>=2.5,<4 + - urllib3>=1.26,<3 + - certifi>=2023.5.7 + - pysocks>=1.5.6,!=1.5.7 ; extra == 'socks' + - chardet>=3.0.2,<8 ; extra == 'use-chardet-on-py3' + requires_python: '>=3.10' - conda: https://conda.anaconda.org/conda-forge/noarch/returns-0.27.0-pyhc364b38_0.conda sha256: 3b45efeae771f1a20307b36ecdb3a8911a89c05382836b50c62b0a99d8d3dfd8 md5: da94ff04d97ec5efc42cbe5da3c43a84 @@ -9843,10 +10172,10 @@ packages: - pkg:pypi/traitlets?source=compressed-mapping size: 115165 timestamp: 1778074251714 -- pypi: https://files.pythonhosted.org/packages/d6/bb/fbdc4e57731efb86b4209ffdd2519520782bf27b3c961beac3e5c20d2b87/trove_classifiers-2026.4.28.13-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/7b/e3/d81b065a2d866a33a541ac63a2a4cc5737e03ce2379ac3191c98bb8867e3/trove_classifiers-2026.5.7.17-py3-none-any.whl name: trove-classifiers - version: 2026.4.28.13 - sha256: 8f4b1eb4e16296b57d612965444f87a83861cc989a0451ac97fe4265ddef03b8 + version: 2026.5.7.17 + sha256: 5ec0800de5e2ddbd7c663cb4c0c15328f132dc168813897c18866c5c7b93db33 - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.15.0-h396c80c_0.conda sha256: 7c2df5721c742c2a47b2c8f960e718c930031663ac1174da67c1ed5999f7938c md5: edd329d7d3a4ab45dcf905899a7a6115 @@ -9941,6 +10270,17 @@ packages: - pysocks>=1.5.6,!=1.5.7,<2.0 ; extra == 'socks' - backports-zstd>=1.0.0 ; python_full_version < '3.14' and extra == 'zstd' requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl + name: urllib3 + version: 2.7.0 + sha256: 9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897 + requires_dist: + - brotli>=1.2.0 ; platform_python_implementation == 'CPython' and extra == 'brotli' + - brotlicffi>=1.2.0.0 ; platform_python_implementation != 'CPython' and extra == 'brotli' + - h2>=4,<5 ; extra == 'h2' + - pysocks>=1.5.6,!=1.5.7,<2.0 ; extra == 'socks' + - backports-zstd>=1.0.0 ; python_full_version < '3.14' and extra == 'zstd' + requires_python: '>=3.10' - pypi: https://files.pythonhosted.org/packages/b7/ee/e9c95cda829131f71a8dff5ce0406059fd16e591c074414e31ada19ba7c3/validate_pyproject-0.25-py3-none-any.whl name: validate-pyproject version: '0.25' @@ -10008,10 +10348,10 @@ packages: - mypy ; extra == 'test' - pretend ; extra == 'test' - pytest ; extra == 'test' -- pypi: https://files.pythonhosted.org/packages/b1/4f/f71e641e504111a5a74e3a20bc52d01bd86788b22699dd3fee1c63253cf6/virtualenv-21.3.1-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/20/5b/885f479093f6627669d39b57bc3d4e674da532e1a4b247d473a61d8d2118/virtualenv-21.3.2-py3-none-any.whl name: virtualenv - version: 21.3.1 - sha256: d1a71cf58f2f9228fff23a1f6ec15d39785c6b32e03658d104974247145edd35 + version: 21.3.2 + sha256: c58ea748fa50bb2a4367da5ba3d30b02458ed40b4ea888faad94021f3309f764 requires_dist: - distlib>=0.3.7,<1 - filelock>=3.24.2,<4 ; python_full_version >= '3.10' diff --git a/src/easyscience/fitting/minimizers/minimizer_bumps.py b/src/easyscience/fitting/minimizers/minimizer_bumps.py index 174b19f7..03c2fc9e 100644 --- a/src/easyscience/fitting/minimizers/minimizer_bumps.py +++ b/src/easyscience/fitting/minimizers/minimizer_bumps.py @@ -118,8 +118,9 @@ def fit( ``iteration`` carries the BUMPS optimizer step index. By default, None. abort_test : Callable[[], bool] | None, default=None - Optional callback that returns ``True`` to signal that sampling should be aborted. - Called periodically during the DREAM sampling loop. + Optional callback that returns ``True`` to signal that the fit + should be aborted. Called periodically during the + BUMPS optimizer iteration loop. minimizer_kwargs : dict | None, default=None Additional keyword arguments passed to the BUMPS minimizer. By default, None. @@ -441,8 +442,9 @@ def sample( Raises ------ ValueError - If both ``chains`` and ``population`` are provided with different - values, or if ``progress_callback`` is not callable. + If the input shapes or weights are invalid, if both ``chains`` + and ``population`` are provided with different values, or if + ``progress_callback`` is not callable. FitError If DREAM sampling was aborted by the user (via ``abort_test``). Exception @@ -452,10 +454,23 @@ def sample( from bumps.fitters import DreamFit from bumps.names import FitProblem + x, y, weights = np.asarray(x), np.asarray(y), np.asarray(weights) + + if y.shape != x.shape: + raise ValueError('x and y must have the same shape.') + + if weights.shape != x.shape: + raise ValueError('Weights must have the same shape as x and y.') + + if not np.isfinite(weights).all(): + raise ValueError('Weights cannot be NaN or infinite.') + + if (weights <= 0).any(): + raise ValueError('Weights must be strictly positive and non-zero.') + # Build the BUMPS Curve model using the minimizer's existing machinery model_func = self._make_model() - x_flat = np.linspace(0, y.size - 1, y.size) - curve = model_func(x_flat, y, weights) + curve = model_func(x, y, weights) problem = FitProblem(curve) # Best-effort seed: sets numpy's global RNG state just before DREAM starts. diff --git a/src/easyscience/fitting/multi_fitter.py b/src/easyscience/fitting/multi_fitter.py index 13a79350..f40411d7 100644 --- a/src/easyscience/fitting/multi_fitter.py +++ b/src/easyscience/fitting/multi_fitter.py @@ -278,7 +278,7 @@ def sample( pop = None # Flatten multi-dataset arrays - _, x_new, y_new, w_new, _dims = self._precompute_reshaping( + x_fit, x_new, y_new, w_new, _dims = self._precompute_reshaping( x, y, weights, vectorized=vectorized ) self._dependent_dims = _dims @@ -302,7 +302,7 @@ def sample( # Delegate to the BUMPS minimizer's public sample method result = minimizer.sample( - x=x_new, + x=x_fit, y=y_new, weights=w_new, samples=samples, From 131f7c6ea04b49f88517e196c378a30e2348b1e1 Mon Sep 17 00:00:00 2001 From: rozyczko Date: Mon, 18 May 2026 10:58:56 +0200 Subject: [PATCH 08/15] msg -> reason for exceptions --- tests/integration/fitting/test_fitter.py | 26 ++++++++++++------------ 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/tests/integration/fitting/test_fitter.py b/tests/integration/fitting/test_fitter.py index 9f97b54a..0fb9b327 100644 --- a/tests/integration/fitting/test_fitter.py +++ b/tests/integration/fitting/test_fitter.py @@ -125,7 +125,7 @@ def test_basic_fit(fit_engine: AvailableMinimizers): try: f.switch_minimizer(fit_engine) except AttributeError: - pytest.skip(msg=f'{fit_engine} is not installed') + pytest.skip(reason=f'{fit_engine} is not installed') result = f.fit(x=x, y=y, weights=weights) @@ -173,7 +173,7 @@ def test_fit_result(fit_engine): try: f.switch_minimizer(fit_engine) except AttributeError: - pytest.skip(msg=f'{fit_engine} is not installed') + pytest.skip(reason=f'{fit_engine} is not installed') result = f.fit(x, y, weights=weights) check_fit_results(result, sp_sin, ref_sin, x, sp_ref1=sp_ref1, sp_ref2=sp_ref2) @@ -205,7 +205,7 @@ def test_basic_max_evaluations(fit_engine): try: f.switch_minimizer(fit_engine) except AttributeError: - pytest.skip(msg=f'{fit_engine} is not installed') + pytest.skip(reason=f'{fit_engine} is not installed') f.max_evaluations = 3 result = f.fit(x=x, y=y, weights=weights) # Result should not be the same as the reference @@ -240,7 +240,7 @@ def test_max_evaluations_populates_fit_result_fields(fit_engine): try: f.switch_minimizer(fit_engine) except AttributeError: - pytest.skip(msg=f'{fit_engine} is not installed') + pytest.skip(reason=f'{fit_engine} is not installed') f.max_evaluations = 3 result = f.fit(x=x, y=y, weights=weights) @@ -268,7 +268,7 @@ def test_bumps_max_evaluations_counts_objective_calls() -> None: try: f.switch_minimizer(AvailableMinimizers.Bumps) except AttributeError: - pytest.skip(msg=f'{AvailableMinimizers.Bumps} is not installed') + pytest.skip(reason=f'{AvailableMinimizers.Bumps} is not installed') f.max_evaluations = 3 result = f.fit(x=x, y=y, weights=weights) @@ -306,7 +306,7 @@ def test_basic_tolerance(fit_engine, tolerance): try: f.switch_minimizer(fit_engine) except AttributeError: - pytest.skip(msg=f'{fit_engine} is not installed') + pytest.skip(reason=f'{fit_engine} is not installed') f.tolerance = tolerance result = f.fit(x=x, y=y, weights=weights) # Result should not be the same as the reference @@ -377,7 +377,7 @@ def test_dependent_parameter(fit_engine): try: f.switch_minimizer(fit_engine) except AttributeError: - pytest.skip(msg=f'{fit_engine} is not installed') + pytest.skip(reason=f'{fit_engine} is not installed') result = f.fit(x, y, weights=weights) check_fit_results(result, sp_sin, ref_sin, x) @@ -405,12 +405,12 @@ def test_2D_vectorized(fit_engine): try: ff.switch_minimizer(fit_engine) except AttributeError: - pytest.skip(msg=f'{fit_engine} is not installed') + pytest.skip(reason=f'{fit_engine} is not installed') try: result = ff.fit(x=XY, y=mm(XY), weights=weights, vectorized=True) except FitError as e: if 'Unable to allocate' in str(e): - pytest.skip(msg='MemoryError - Matrix too large') + pytest.skip(reason='MemoryError - Matrix too large') else: raise e assert result.n_pars == len(m2.get_fit_parameters()) @@ -444,12 +444,12 @@ def test_2D_non_vectorized(fit_engine): try: ff.switch_minimizer(fit_engine) except AttributeError: - pytest.skip(msg=f'{fit_engine} is not installed') + pytest.skip(reason=f'{fit_engine} is not installed') try: result = ff.fit(x=XY, y=mm(XY.reshape(-1, 2)), weights=weights, vectorized=False) except FitError as e: if 'Unable to allocate' in str(e): - pytest.skip(msg='MemoryError - Matrix too large') + pytest.skip(reason='MemoryError - Matrix too large') else: raise e assert result.n_pars == len(m2.get_fit_parameters()) @@ -492,7 +492,7 @@ def test_fixed_parameter_does_not_change(fit_engine): try: f.switch_minimizer(fit_engine) except AttributeError: - pytest.skip(msg=f'{fit_engine} is not installed') + pytest.skip(reason=f'{fit_engine} is not installed') result = f.fit(x=x, y=y, weights=weights) @@ -567,7 +567,7 @@ def run_fit(weights): try: f.switch_minimizer(fit_engine) except AttributeError: - pytest.skip(msg=f'{fit_engine} is not installed') + pytest.skip(reason=f'{fit_engine} is not installed') f.fit(x=x, y=y, weights=weights) return model.offset.value, model.phase.value From 916b1d3622ad06c0259ecfda2803bd17d8ad899c Mon Sep 17 00:00:00 2001 From: rozyczko Date: Mon, 18 May 2026 11:45:41 +0200 Subject: [PATCH 09/15] PR fixes --- .../fitting/minimizers/minimizer_bumps.py | 71 ++++++++++++------- src/easyscience/fitting/multi_fitter.py | 25 ++----- .../minimizers/test_minimizer_bumps.py | 55 ++++++++++++++ 3 files changed, 109 insertions(+), 42 deletions(-) diff --git a/src/easyscience/fitting/minimizers/minimizer_bumps.py b/src/easyscience/fitting/minimizers/minimizer_bumps.py index 03c2fc9e..bdc1e7cb 100644 --- a/src/easyscience/fitting/minimizers/minimizer_bumps.py +++ b/src/easyscience/fitting/minimizers/minimizer_bumps.py @@ -262,6 +262,43 @@ def _resolve_fitclass(method: str): return fitclass raise FitError(f'Unknown BUMPS fitting method: {method}') + @staticmethod + def _resolve_population_alias(chains: int | None, population: int | None) -> int | None: + """Resolve the DREAM population count from the ``chains`` alias. + + Both ``chains`` (user-friendly name) and ``population`` (BUMPS + native name) refer to the same DREAM ``pop`` parameter. This + helper enforces that at most one is provided and returns the + resolved value. + + Parameters + ---------- + chains : int | None + User-friendly alias for the DREAM population count. + population : int | None + BUMPS-native DREAM population count. + + Returns + ------- + int | None + The resolved population count, or ``None`` if neither was + provided. + + Raises + ------ + ValueError + If both ``chains`` and ``population`` are provided with + different values. + """ + if chains is not None and population is not None: + if chains != population: + raise ValueError( + f'Conflicting population arguments: chains={chains}, ' + f'population={population}. Only provide one.' + ) + return chains + return chains if chains is not None else population + def _build_progress_payload( self, problem, iteration: int, point: np.ndarray, nllf: float ) -> dict: @@ -421,7 +458,12 @@ def sample( population : int | None, default=None BUMPS DREAM population count for advanced users. seed : int | None, default=None - Best-effort random seed. + Best-effort random seed. Calls ``numpy.random.seed(seed)`` + before DREAM starts, which affects the *global* NumPy RNG + state and may interact with other code in the process. + BUMPS DREAM uses additional internal RNG state that is + **not** controlled by this seed, so exact reproducibility + across runs is **not** guaranteed. sampler_kwargs : dict | None, default=None Additional keyword arguments forwarded to `bumps.fitters.fit`. progress_callback : Callable[[dict], bool | None] | None, default=None @@ -478,19 +520,7 @@ def sample( np.random.seed(seed) # Resolve population parameter - if chains is not None and population is not None: - if chains != population: - raise ValueError( - f'Conflicting population arguments: chains={chains}, population={population}. ' - 'Only provide one.' - ) - pop = chains - elif chains is not None: - pop = chains - elif population is not None: - pop = population - else: - pop = None + pop = self._resolve_population_alias(chains, population) # Build DREAM kwargs dream_kwargs: dict = {'samples': samples, 'burn': burn, 'thin': thin} @@ -563,16 +593,9 @@ def _build_sample_progress_payload( The payload includes ``sampling: True`` so downstream consumers can distinguish sampling progress from classical fitting progress. """ - parameter_values = self._current_parameter_snapshot(problem, point) - return { - 'iteration': iteration, - 'chi2': float(problem.chisq(nllf=nllf, norm=False)), - 'reduced_chi2': float(problem.chisq(nllf=nllf, norm=True)), - 'parameter_values': parameter_values, - 'refresh_plots': False, - 'finished': False, - 'sampling': True, - } + payload = self._build_progress_payload(problem, iteration, point, nllf) + payload['sampling'] = True + return payload def _set_parameter_fit_result( self, diff --git a/src/easyscience/fitting/multi_fitter.py b/src/easyscience/fitting/multi_fitter.py index f40411d7..a7731972 100644 --- a/src/easyscience/fitting/multi_fitter.py +++ b/src/easyscience/fitting/multi_fitter.py @@ -196,7 +196,7 @@ def sample( y: List[np.ndarray], weights: List[np.ndarray], samples: int = 10000, - burn: int = 1000, + burn: int = 2000, thin: int = 10, chains: int | None = None, population: int | None = None, @@ -221,7 +221,7 @@ def sample( List of weight arrays (one per dataset). samples : int, default=10000 Number of retained DREAM samples requested from BUMPS. - burn : int, default=1000 + burn : int, default=2000 Burn-in steps. thin : int, default=10 Thinning interval. @@ -258,24 +258,13 @@ def sample( ------ RuntimeError If the current minimizer is not a BUMPS instance. - ValueError - If both ``chains`` and ``population`` are provided with different - values. """ # --- Alias resolution --- - if chains is not None and population is not None: - if chains != population: - raise ValueError( - f'Conflicting population arguments: chains={chains}, population={population}. ' - 'Only provide one.' - ) - pop = chains - elif chains is not None: - pop = chains - elif population is not None: - pop = population - else: - pop = None + # Delegate to the BUMPS minimizer's static helper so the logic + # stays in one place. + from easyscience.fitting.minimizers.minimizer_bumps import Bumps + + pop = Bumps._resolve_population_alias(chains, population) # Flatten multi-dataset arrays x_fit, x_new, y_new, w_new, _dims = self._precompute_reshaping( diff --git a/tests/unit/fitting/minimizers/test_minimizer_bumps.py b/tests/unit/fitting/minimizers/test_minimizer_bumps.py index 93ee68b6..28a55fe7 100644 --- a/tests/unit/fitting/minimizers/test_minimizer_bumps.py +++ b/tests/unit/fitting/minimizers/test_minimizer_bumps.py @@ -848,6 +848,38 @@ def test_sample_rejects_non_callable_callback(self, minimizer: Bumps, monkeypatc ) +# =================================================================== +# _resolve_population_alias (static helper) +# =================================================================== + + +class TestResolvePopulationAlias: + """Tests for ``Bumps._resolve_population_alias``.""" + + def test_both_none_returns_none(self) -> None: + assert Bumps._resolve_population_alias(None, None) is None + + def test_chains_only_returns_chains(self) -> None: + assert Bumps._resolve_population_alias(5, None) == 5 + + def test_population_only_returns_population(self) -> None: + assert Bumps._resolve_population_alias(None, 7) == 7 + + def test_both_equal_returns_value(self) -> None: + assert Bumps._resolve_population_alias(5, 5) == 5 + + def test_both_different_raises(self) -> None: + with pytest.raises(ValueError, match='Conflicting population'): + Bumps._resolve_population_alias(3, 10) + + def test_chains_zero_is_valid(self) -> None: + """Zero is a valid (though unusual) population value.""" + assert Bumps._resolve_population_alias(0, None) == 0 + + def test_population_zero_is_valid(self) -> None: + assert Bumps._resolve_population_alias(None, 0) == 0 + + # =================================================================== # _build_sample_progress_payload # =================================================================== @@ -902,6 +934,29 @@ def test_payload_keys(self, minimizer: Bumps) -> None: } assert set(payload.keys()) == expected_keys + def test_delegates_to_build_progress_payload(self, minimizer: Bumps) -> None: + """_build_sample_progress_payload calls _build_progress_payload and adds sampling.""" + mock_problem = MagicMock() + + # Patch _build_progress_payload to track calls + base_payload = { + 'iteration': 3, + 'chi2': 42.0, + 'reduced_chi2': 21.0, + 'parameter_values': {'x': 7.0}, + 'refresh_plots': False, + 'finished': False, + } + with patch.object( + minimizer, '_build_progress_payload', return_value=base_payload + ) as mock_bpp: + result = minimizer._build_sample_progress_payload( + mock_problem, 3, np.array([7.0]), 21.0 + ) + + mock_bpp.assert_called_once_with(mock_problem, 3, np.array([7.0]), 21.0) + assert result == {**base_payload, 'sampling': True} + # =================================================================== # _set_parameter_fit_result with stack_status=True From e7f345a9d0beadd6507074d9693715be687f41e8 Mon Sep 17 00:00:00 2001 From: rozyczko Date: Thu, 28 May 2026 08:27:51 +0200 Subject: [PATCH 10/15] added bayesian notebook --- docs/docs/tutorials/fitting-bayesian.ipynb | 430 +++++++++++++++++++++ docs/docs/tutorials/index.md | 3 + docs/mkdocs.yml | 1 + 3 files changed, 434 insertions(+) create mode 100644 docs/docs/tutorials/fitting-bayesian.ipynb diff --git a/docs/docs/tutorials/fitting-bayesian.ipynb b/docs/docs/tutorials/fitting-bayesian.ipynb new file mode 100644 index 00000000..380a7355 --- /dev/null +++ b/docs/docs/tutorials/fitting-bayesian.ipynb @@ -0,0 +1,430 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "0", + "metadata": {}, + "outputs": [], + "source": [ + "%matplotlib widget" + ] + }, + { + "cell_type": "markdown", + "id": "1", + "metadata": {}, + "source": [ + "# Bayesian analysis\n", + "\n", + "The previous tutorials demonstrate how to obtain *maximum-likelihood* estimates of the parameters of a model by minimising $\\chi^2$.\n", + "An alternative, and often more informative, view of the same problem is provided by a **Bayesian** analysis, in which the goal is to characterise the full *posterior distribution* over the parameters given the data.\n", + "\n", + "Recall Bayes' theorem,\n", + "\n", + "$$\n", + "p(\\theta \\mid d) \\propto p(d \\mid \\theta)\\, p(\\theta),\n", + "$$ (bayes)\n", + "\n", + "where $\\theta$ are the model parameters, $d$ is the observed data, $p(d \\mid \\theta)$ is the likelihood, and $p(\\theta)$ is the prior. In `easyscience`, the `min`/`max` bounds of a `Parameter` are interpreted as a **uniform prior**, and a Gaussian likelihood is constructed from the data and supplied weights.\n", + "\n", + "`easyscience` exposes a Bayesian Markov-chain Monte Carlo (MCMC) sampler through the `MultiFitter.sample` method. Under the hood this uses BUMPS' DREAM sampler, so the underlying minimizer must be switched to BUMPS.\n", + "\n", + "In this tutorial we re-use the QENS dataset and the Lorentzian-with-resolution model from the [Fitting QENS](fitting-qens.ipynb) tutorial, but instead of returning a single best-fit value with a symmetric error bar we will draw thousands of samples from the posterior." + ] + }, + { + "cell_type": "markdown", + "id": "2", + "metadata": {}, + "source": [ + "## Load the data\n", + "\n", + "We load the same simulated QENS dataset that was used in the previous tutorial." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3", + "metadata": {}, + "outputs": [], + "source": [ + "import matplotlib.pyplot as plt\n", + "import numpy as np" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4", + "metadata": {}, + "outputs": [], + "source": [ + "def fetch_data(name: str) -> str:\n", + " \"\"\"\n", + " Fetch pre-prepared data from a remote source and return the path to the file.\n", + " \"\"\"\n", + " import pooch\n", + "\n", + " return pooch.retrieve(\n", + " url=f'https://public.esss.dk/groups/scipp/dmsc-summer-school/2025/{name}',\n", + " known_hash=None,\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5", + "metadata": {}, + "outputs": [], + "source": [ + "filename = fetch_data('4-reduction/energy_transfer_QENS_unknown_quasi_elastic_many_neutrons.dat')\n", + "\n", + "\n", + "def load(filename: str):\n", + " \"\"\"Load data from file and filter NaN values.\"\"\"\n", + " x, y, e = np.loadtxt(filename, unpack=True)\n", + " sel = np.isfinite(y)\n", + " return x[sel], y[sel], e[sel]\n", + "\n", + "\n", + "omega, intensity_obs, di = load(filename)\n", + "\n", + "# Restrict to the region of interest, as in the QENS tutorial.\n", + "sel = (omega > -0.06) & (omega < 0.06)\n", + "omega, intensity_obs, di = omega[sel], intensity_obs[sel], di[sel]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6", + "metadata": {}, + "outputs": [], + "source": [ + "fig, ax = plt.subplots()\n", + "ax.errorbar(omega, intensity_obs, di, fmt='.')\n", + "ax.set(xlabel='$\\\\omega$/meV', ylabel='$I(\\\\omega)$')\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "7", + "metadata": {}, + "source": [ + "## The model\n", + "\n", + "We re-use the convolution of a Lorentzian with a Gaussian resolution function from the QENS tutorial:\n", + "\n", + "$$\n", + "I(\\omega) = \\frac{A\\gamma}{\\pi\\big[(\\omega - \\omega_0)^2 + \\gamma^2\\big]} \\;\\ast\\; \\mathcal{N}(0, \\sigma),\n", + "$$ (model)\n", + "\n", + "where $A$ is a scale factor, $\\gamma$ is the Lorentzian half-width at half-maximum, $\\omega_0$ is the centre offset and $\\sigma$ is the width of the Gaussian resolution kernel." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8", + "metadata": {}, + "outputs": [], + "source": [ + "from scipy.stats import norm\n", + "\n", + "\n", + "def lorentzian(x: np.ndarray) -> np.ndarray:\n", + " return A.value / np.pi * gamma.value / ((x - omega_0.value) ** 2 + gamma.value**2)\n", + "\n", + "\n", + "def intensity(x: np.ndarray) -> np.ndarray:\n", + " gauss = norm(0, sigma.value).pdf(x)\n", + " gauss /= gauss.sum()\n", + " return np.convolve(lorentzian(x), gauss, 'same')" + ] + }, + { + "cell_type": "markdown", + "id": "9", + "metadata": {}, + "source": [ + "## Exercise 1: parameters with priors\n", + "\n", + "Create four `Parameter` objects, for $A$, $\\gamma$, $\\omega_0$ and $\\sigma$. The `min` and `max` arguments define a **uniform prior** on each parameter — the sampler will only consider values inside this range and will treat every value inside the range as equally plausible *a priori*.\n", + "\n", + "| Parameter | Initial Value | Min | Max |\n", + "| --- | --- | --- | --- |\n", + "| $A$ | 10 | 1 | 100 |\n", + "| $\\gamma$ | 8.0 × 10-3 | 1.0 × 10-4 | 1.0 × 10-2 |\n", + "| $\\omega_0$ | 1.0 × 10-3 | 0 | 2.0 × 10-3 |\n", + "| $\\sigma$ | 1.0 × 10-3 | 1.0 × 10-5 | 1.0 × 10-1 |\n", + "\n", + "**Solution:**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "10", + "metadata": {}, + "outputs": [], + "source": [ + "from easyscience import Parameter\n", + "\n", + "A = Parameter(name='A', value=10, fixed=False, min=1, max=100)\n", + "gamma = Parameter(name='gamma', value=8e-3, fixed=False, min=1e-4, max=1e-2)\n", + "omega_0 = Parameter(name='omega_0', value=1e-3, fixed=False, min=0, max=2e-3)\n", + "sigma = Parameter(name='sigma', value=1e-3, fixed=False, min=1e-5, max=1e-1)" + ] + }, + { + "cell_type": "markdown", + "id": "11", + "metadata": {}, + "source": [ + "## Exercise 2: maximum-likelihood fit (for reference)\n", + "\n", + "Before running MCMC, perform a quick maximum-likelihood fit. The result will give us a good starting point and lets us compare the central tendency of the posterior with the classical best-fit values.\n", + "\n", + "**Solution:**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "12", + "metadata": {}, + "outputs": [], + "source": [ + "from easyscience import Fitter\n", + "from easyscience import ObjBase\n", + "\n", + "params = ObjBase(name='params', A=A, gamma=gamma, omega_0=omega_0, sigma=sigma)\n", + "\n", + "mle_fitter = Fitter(params, intensity)\n", + "mle_result = mle_fitter.fit(x=omega, y=intensity_obs, weights=1 / di)\n", + "\n", + "A, gamma, omega_0, sigma" + ] + }, + { + "cell_type": "markdown", + "id": "13", + "metadata": {}, + "source": [ + "## Exercise 3: switch to BUMPS and draw posterior samples\n", + "\n", + "`MultiFitter.sample` exposes the BUMPS DREAM sampler. It accepts a list of datasets (one entry per model) and returns a dictionary with the following keys:\n", + "\n", + "- `draws`: a `(n_samples, n_parameters)` array of posterior samples;\n", + "- `param_names`: the unique names of the parameters, in the same column order as `draws`;\n", + "- `state`: the underlying BUMPS `MCMCDraw` object, useful for advanced diagnostics;\n", + "- `logp`: the log-posterior of each retained sample.\n", + "\n", + "DREAM only works with the BUMPS minimizer, so the first step is to switch the minimizer over.\n", + "\n", + "**Solution:**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "14", + "metadata": {}, + "outputs": [], + "source": [ + "from easyscience import AvailableMinimizers\n", + "from easyscience.fitting.multi_fitter import MultiFitter\n", + "\n", + "sampler = MultiFitter([params], [intensity])\n", + "sampler.switch_minimizer(AvailableMinimizers.Bumps)\n", + "\n", + "result = sampler.sample(\n", + " x=[omega],\n", + " y=[intensity_obs],\n", + " weights=[1 / di],\n", + " samples=4000,\n", + " burn=500,\n", + " thin=2,\n", + " seed=42,\n", + ")\n", + "\n", + "print(f'Drew {result[\"draws\"].shape[0]} samples for {result[\"draws\"].shape[1]} parameters.')\n", + "print('parameters:', result['param_names'])" + ] + }, + { + "cell_type": "markdown", + "id": "15", + "metadata": {}, + "source": [ + "## Exercise 4: posterior summaries\n", + "\n", + "Summarise each marginal posterior by its **median** and the 16th/84th percentiles, which together give an asymmetric 68% credible interval. Note that the columns of `result['draws']` are ordered by `result['param_names']` (which use the parameters' `unique_name`), so it is worth building a small helper to look up a column by friendly name.\n", + "\n", + "**Solution:**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "16", + "metadata": {}, + "outputs": [], + "source": [ + "draws = result['draws']\n", + "name_to_col = {name: idx for idx, name in enumerate(result['param_names'])}\n", + "\n", + "\n", + "def column_for(parameter):\n", + " return draws[:, name_to_col[parameter.unique_name]]\n", + "\n", + "\n", + "summary_rows = []\n", + "for label, par in (('A', A), ('gamma', gamma), ('omega_0', omega_0), ('sigma', sigma)):\n", + " col = column_for(par)\n", + " lo, med, hi = np.percentile(col, [16, 50, 84])\n", + " summary_rows.append((label, med, med - lo, hi - med, par.value))\n", + "\n", + "print(f'{\"param\":<8s} {\"median\":>12s} {\"-1σ\":>12s} {\"+1σ\":>12s} {\"MLE\":>12s}')\n", + "for label, med, minus, plus, mle in summary_rows:\n", + " print(f'{label:<8s} {med:12.4g} {minus:12.4g} {plus:12.4g} {mle:12.4g}')" + ] + }, + { + "cell_type": "markdown", + "id": "17", + "metadata": {}, + "source": [ + "## Exercise 5: visualise the joint posterior\n", + "\n", + "Marginal summaries hide correlations between parameters. A *corner plot* (a triangular grid of pairwise scatter plots and 1-D histograms) is the standard way to display them. Below we build one with plain `matplotlib`; if you have the [`corner`](https://corner.readthedocs.io/) package installed it will produce a publication-quality version with a single call.\n", + "\n", + "**Solution:**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "18", + "metadata": {}, + "outputs": [], + "source": [ + "labels = ['A', 'gamma', 'omega_0', 'sigma']\n", + "cols = np.column_stack([column_for(p) for p in (A, gamma, omega_0, sigma)])\n", + "n = len(labels)\n", + "\n", + "fig, axes = plt.subplots(n, n, figsize=(8, 8))\n", + "for i in range(n):\n", + " for j in range(n):\n", + " ax = axes[i, j]\n", + " if j > i:\n", + " ax.set_visible(False)\n", + " continue\n", + " if i == j:\n", + " ax.hist(cols[:, i], bins=40, color='C0', histtype='stepfilled', alpha=0.7)\n", + " ax.set_yticks([])\n", + " else:\n", + " ax.hexbin(cols[:, j], cols[:, i], gridsize=30, cmap='Blues', mincnt=1)\n", + " if i == n - 1:\n", + " ax.set_xlabel(labels[j])\n", + " else:\n", + " ax.set_xticklabels([])\n", + " if j == 0 and i != 0:\n", + " ax.set_ylabel(labels[i])\n", + " else:\n", + " ax.set_yticklabels([])\n", + "fig.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "19", + "metadata": {}, + "source": [ + "## Exercise 6: posterior-predictive band\n", + "\n", + "One of the practical benefits of having the full posterior in hand is that it is trivial to propagate the uncertainty in the parameters through to predictions of the model. We pick a few hundred random draws, evaluate the model at each, and plot the resulting envelope alongside the data.\n", + "\n", + "**Solution:**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "20", + "metadata": {}, + "outputs": [], + "source": [ + "rng = np.random.default_rng(seed=0)\n", + "n_draws = 300\n", + "indices = rng.choice(draws.shape[0], size=n_draws, replace=False)\n", + "\n", + "predictions = np.empty((n_draws, omega.size))\n", + "saved = {p.unique_name: p.value for p in (A, gamma, omega_0, sigma)}\n", + "try:\n", + " for k, idx in enumerate(indices):\n", + " A.value = draws[idx, name_to_col[A.unique_name]]\n", + " gamma.value = draws[idx, name_to_col[gamma.unique_name]]\n", + " omega_0.value = draws[idx, name_to_col[omega_0.unique_name]]\n", + " sigma.value = draws[idx, name_to_col[sigma.unique_name]]\n", + " predictions[k] = intensity(omega)\n", + "finally:\n", + " for p in (A, gamma, omega_0, sigma):\n", + " p.value = saved[p.unique_name]\n", + "\n", + "lo = np.percentile(predictions, 2.5, axis=0)\n", + "hi = np.percentile(predictions, 97.5, axis=0)\n", + "mid = np.percentile(predictions, 50, axis=0)\n", + "\n", + "fig, ax = plt.subplots()\n", + "ax.errorbar(omega, intensity_obs, di, fmt='.', label='data')\n", + "ax.fill_between(omega, lo, hi, color='C1', alpha=0.3, label='95% posterior band')\n", + "ax.plot(omega, mid, '-', color='C1', label='posterior median')\n", + "ax.set(xlabel='$\\\\omega$/meV', ylabel='$I(\\\\omega)$')\n", + "ax.legend()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "21", + "metadata": {}, + "source": [ + "## What next?\n", + "\n", + "A few things to try as further exercises:\n", + "\n", + "- Tighten or widen the `min`/`max` bounds on one of the parameters and see how the posterior reacts. This is the simplest possible *prior-sensitivity* analysis.\n", + "- Use the `chains` argument to `sample` to increase the DREAM population, e.g. `chains=10`, and check that the marginal histograms are unchanged.\n", + "- Pass a `progress_callback` to `sample` to monitor the sampler in real time — the same callback protocol used by the classical fitters works during MCMC (with `payload['sampling']` set to `True`).\n", + "- Inspect `result['state']` directly; the BUMPS [`MCMCDraw`](https://bumps.readthedocs.io/en/latest/api/bumps.dream.html) object exposes additional diagnostics such as per-chain log-posteriors and autocorrelation estimates." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python (Pixi)", + "language": "python", + "name": "pixi-kernel-python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/docs/tutorials/index.md b/docs/docs/tutorials/index.md index 4a73fb8c..588cfe81 100644 --- a/docs/docs/tutorials/index.md +++ b/docs/docs/tutorials/index.md @@ -23,6 +23,9 @@ The tutorials are organized into the following categories: - [Fitting SANS](fitting-sans.ipynb) – A tutorial demonstrating how to fit a small-angle neutron scattering (SANS) data using EasyScience framework. +- [Bayesian analysis](fitting-bayesian.ipynb) – A tutorial showing how + to run Bayesian MCMC sampling on a model with EasyScience to obtain + full posterior distributions for the parameters. - [Progress Callback](progress-callback.ipynb) – A tutorial showing how to monitor fitting progress across minimizer backends and update a notebook UI while a fit is running. diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 15f15dea..bceed3b4 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -179,6 +179,7 @@ nav: - Workshops & Schools: - Fitting QENS: tutorials/fitting-qens.ipynb - Fitting SANS: tutorials/fitting-sans.ipynb + - Bayesian analysis: tutorials/fitting-bayesian.ipynb - Progress Callback: tutorials/progress-callback.ipynb - API Reference: - API Reference: api-reference/index.md From 1088a90e3d9edaa357ce632cb82ee49fe1c50937 Mon Sep 17 00:00:00 2001 From: Piotr Rozyczko Date: Fri, 29 May 2026 10:05:45 +0200 Subject: [PATCH 11/15] updated/enhanced bayesian notebook --- docs/docs/tutorials/fitting-bayesian.ipynb | 94 +++++++++++++++++++--- 1 file changed, 82 insertions(+), 12 deletions(-) diff --git a/docs/docs/tutorials/fitting-bayesian.ipynb b/docs/docs/tutorials/fitting-bayesian.ipynb index 380a7355..a36f1aa3 100644 --- a/docs/docs/tutorials/fitting-bayesian.ipynb +++ b/docs/docs/tutorials/fitting-bayesian.ipynb @@ -30,6 +30,17 @@ "\n", "`easyscience` exposes a Bayesian Markov-chain Monte Carlo (MCMC) sampler through the `MultiFitter.sample` method. Under the hood this uses BUMPS' DREAM sampler, so the underlying minimizer must be switched to BUMPS.\n", "\n", + "### When should you use a Bayesian analysis?\n", + "\n", + "A maximum-likelihood estimate (MLE) gives you a single best-fit value with a symmetric uncertainty, which is fast and often sufficient. A Bayesian analysis becomes valuable when:\n", + "\n", + "- **Your uncertainties are asymmetric** — MLE error bars assume the parameter distribution is Gaussian, which is not always true. The posterior samples capture skew naturally.\n", + "- **You have prior knowledge** — if you know from physics that a parameter *must* lie in a certain range, encoding that as a prior ($p(\\theta)$) is more principled than simply clamping bounds after the fit.\n", + "- **You care about parameter correlations** — the joint posterior (Exercise 6) reveals trade-offs between parameters that a single covariance matrix can miss.\n", + "- **You want to propagate uncertainty to predictions** — with the posterior in hand, you can compute credible bands on any function of the parameters (Exercise 7) without linearised error propagation.\n", + "\n", + "The trade-off is computational cost: MCMC requires thousands of model evaluations, whereas an MLE fit may converge in dozens. For the simple 4-parameter model used here the difference is negligible, but for expensive models it is worth starting with MLE and only switching to MCMC when you need the richer output.\n", + "\n", "In this tutorial we re-use the QENS dataset and the Lorentzian-with-resolution model from the [Fitting QENS](fitting-qens.ipynb) tutorial, but instead of returning a single best-fit value with a symmetric error bar we will draw thousands of samples from the posterior." ] }, @@ -123,7 +134,11 @@ "I(\\omega) = \\frac{A\\gamma}{\\pi\\big[(\\omega - \\omega_0)^2 + \\gamma^2\\big]} \\;\\ast\\; \\mathcal{N}(0, \\sigma),\n", "$$ (model)\n", "\n", - "where $A$ is a scale factor, $\\gamma$ is the Lorentzian half-width at half-maximum, $\\omega_0$ is the centre offset and $\\sigma$ is the width of the Gaussian resolution kernel." + "where $A$ is a scale factor, $\\gamma$ is the Lorentzian half-width at half-maximum, $\\omega_0$ is the centre offset and $\\sigma$ is the width of the Gaussian resolution kernel.\n", + "\n", + "```{note}\n", + "The convolution below uses `np.convolve(..., 'same')` for simplicity. In `'same'` mode, `numpy` pads the signal edges with zeros, which can introduce small artefacts at the boundaries. Because our Gaussian kernel ($\\sigma \\approx 10^{-3}$) is much narrower than the data range ($\\pm 0.06$ meV), these edge effects are negligible here. For production work with broader kernels, consider `scipy.signal.convolve` with an explicit boundary mode such as `'reflect'` or `'nearest'`.\n", + "```" ] }, { @@ -258,12 +273,17 @@ }, { "cell_type": "markdown", - "id": "15", + "id": "8766b170", "metadata": {}, "source": [ - "## Exercise 4: posterior summaries\n", + "## Exercise 4: convergence diagnostics\n", + "\n", + "Before we trust the posterior samples, we should check that the MCMC chains have **converged** — that is, the sampler has found the typical set of the posterior and is no longer drifting. Two simple visual checks are:\n", "\n", - "Summarise each marginal posterior by its **median** and the 16th/84th percentiles, which together give an asymmetric 68% credible interval. Note that the columns of `result['draws']` are ordered by `result['param_names']` (which use the parameters' `unique_name`), so it is worth building a small helper to look up a column by friendly name.\n", + "1. **Trace plot** — plot the sampled parameter values against the sample index. A well-converged chain looks like a \"hairy caterpillar\": it fluctuates around a stable mean with no long-term trends.\n", + "2. **Log-posterior plot** — the log-posterior $\\log p(\\theta \\mid d)$ should also stabilise after burn-in. If it is still climbing at the end of the run, the sampler has not yet converged.\n", + "\n", + "The `logp` array returned by `sample` contains the log-posterior for every *retained* sample (i.e. *after* burn-in and thinning). This means a flat `logp` trace is exactly what we want to see.\n", "\n", "**Solution:**" ] @@ -271,11 +291,15 @@ { "cell_type": "code", "execution_count": null, - "id": "16", + "id": "3c49ab6f", "metadata": {}, "outputs": [], "source": [ "draws = result['draws']\n", + "logp = result['logp']\n", + "if callable(logp):\n", + " _, logp = logp()\n", + " logp = logp.flatten()\n", "name_to_col = {name: idx for idx, name in enumerate(result['param_names'])}\n", "\n", "\n", @@ -283,6 +307,52 @@ " return draws[:, name_to_col[parameter.unique_name]]\n", "\n", "\n", + "fig, axes = plt.subplots(5, 1, figsize=(10, 12), sharex=True)\n", + "\n", + "# Trace plots for each parameter\n", + "for ax, (label, par) in zip(\n", + " axes[:4],\n", + " (('A', A), ('gamma', gamma), ('omega_0', omega_0), ('sigma', sigma)),\n", + "):\n", + " ax.plot(column_for(par), lw=0.5)\n", + " ax.set_ylabel(label)\n", + " ax.set_xlim(0, len(draws) - 1)\n", + "\n", + "# Log-posterior trace\n", + "axes[4].plot(logp, lw=0.5, color='C4')\n", + "axes[4].set_ylabel('log-posterior')\n", + "axes[4].set_xlabel('sample index')\n", + "\n", + "fig.suptitle('MCMC trace plots — check for \"hairy caterpillar\" behaviour')\n", + "fig.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "15", + "metadata": {}, + "source": [ + "## Exercise 5: posterior summaries\n", + "\n", + "Summarise each marginal posterior by its **median** and the 16th/84th percentiles, which together give an asymmetric 68% credible interval. Compare these with the MLE values you obtained in Exercise 2.\n", + "\n", + "```{note}\n", + "**Why might the Bayesian median differ from the MLE?** The MLE finds the single point that maximises the likelihood, while the Bayesian median is the central value of the *posterior* — which also accounts for the prior $p(\\theta)$. If a parameter's posterior is skewed (asymmetric), the median and the mode (which approximates the MLE) will not coincide. The table below shows both so you can spot any such differences.\n", + "```\n", + "\n", + "Note that the columns of `result['draws']` are ordered by `result['param_names']` (which use the parameters' `unique_name`), so it is worth building a small helper to look up a column by friendly name.\n", + "\n", + "**Solution:**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "16", + "metadata": {}, + "outputs": [], + "source": [ "summary_rows = []\n", "for label, par in (('A', A), ('gamma', gamma), ('omega_0', omega_0), ('sigma', sigma)):\n", " col = column_for(par)\n", @@ -299,7 +369,7 @@ "id": "17", "metadata": {}, "source": [ - "## Exercise 5: visualise the joint posterior\n", + "## Exercise 6: visualise the joint posterior\n", "\n", "Marginal summaries hide correlations between parameters. A *corner plot* (a triangular grid of pairwise scatter plots and 1-D histograms) is the standard way to display them. Below we build one with plain `matplotlib`; if you have the [`corner`](https://corner.readthedocs.io/) package installed it will produce a publication-quality version with a single call.\n", "\n", @@ -346,7 +416,7 @@ "id": "19", "metadata": {}, "source": [ - "## Exercise 6: posterior-predictive band\n", + "## Exercise 7: posterior-predictive band\n", "\n", "One of the practical benefits of having the full posterior in hand is that it is trivial to propagate the uncertainty in the parameters through to predictions of the model. We pick a few hundred random draws, evaluate the model at each, and plot the resulting envelope alongside the data.\n", "\n", @@ -400,17 +470,17 @@ "A few things to try as further exercises:\n", "\n", "- Tighten or widen the `min`/`max` bounds on one of the parameters and see how the posterior reacts. This is the simplest possible *prior-sensitivity* analysis.\n", - "- Use the `chains` argument to `sample` to increase the DREAM population, e.g. `chains=10`, and check that the marginal histograms are unchanged.\n", + "- Use the `chains` argument to `sample` to increase the DREAM population, e.g. `chains=10`, and check that the marginal histograms and trace plots are unchanged — a sign that the chains are sampling the same distribution.\n", "- Pass a `progress_callback` to `sample` to monitor the sampler in real time — the same callback protocol used by the classical fitters works during MCMC (with `payload['sampling']` set to `True`).\n", - "- Inspect `result['state']` directly; the BUMPS [`MCMCDraw`](https://bumps.readthedocs.io/en/latest/api/bumps.dream.html) object exposes additional diagnostics such as per-chain log-posteriors and autocorrelation estimates." + "- Inspect `result['state']` directly; the BUMPS [`MCMCDraw`](https://bumps.readthedocs.io/en/latest/api/bumps.dream.html) object exposes additional diagnostics such as per-chain log-posteriors, autocorrelation estimates, and the Gelman-Rubin $\\hat{R}$ statistic for formal convergence assessment." ] } ], "metadata": { "kernelspec": { - "display_name": "Python (Pixi)", + "display_name": "era", "language": "python", - "name": "pixi-kernel-python3" + "name": "python3" }, "language_info": { "codemirror_mode": { @@ -422,7 +492,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.13.12" + "version": "3.12.11" } }, "nbformat": 4, From db00f9ff4a7e67a7b79aa1a274026fda49eb157d Mon Sep 17 00:00:00 2001 From: Piotr Rozyczko Date: Fri, 29 May 2026 10:58:41 +0200 Subject: [PATCH 12/15] PR review of the notebook --- docs/docs/tutorials/fitting-bayesian.ipynb | 70 +++++++++++++++++++--- 1 file changed, 62 insertions(+), 8 deletions(-) diff --git a/docs/docs/tutorials/fitting-bayesian.ipynb b/docs/docs/tutorials/fitting-bayesian.ipynb index a36f1aa3..d896a4d2 100644 --- a/docs/docs/tutorials/fitting-bayesian.ipynb +++ b/docs/docs/tutorials/fitting-bayesian.ipynb @@ -72,7 +72,7 @@ "metadata": {}, "outputs": [], "source": [ - "def fetch_data(name: str) -> str:\n", + "def fetch_data(name: str, known_hash: str) -> str:\n", " \"\"\"\n", " Fetch pre-prepared data from a remote source and return the path to the file.\n", " \"\"\"\n", @@ -80,7 +80,7 @@ "\n", " return pooch.retrieve(\n", " url=f'https://public.esss.dk/groups/scipp/dmsc-summer-school/2025/{name}',\n", - " known_hash=None,\n", + " known_hash=known_hash,\n", " )" ] }, @@ -91,7 +91,10 @@ "metadata": {}, "outputs": [], "source": [ - "filename = fetch_data('4-reduction/energy_transfer_QENS_unknown_quasi_elastic_many_neutrons.dat')\n", + "filename = fetch_data(\n", + " '4-reduction/energy_transfer_QENS_unknown_quasi_elastic_many_neutrons.dat',\n", + " known_hash='sha256:e49fa9a1d2ef5eeb714524903e8ffa9de6616e5a299a799cbb0b2f3ee8fd459a',\n", + ")\n", "\n", "\n", "def load(filename: str):\n", @@ -215,9 +218,59 @@ "outputs": [], "source": [ "from easyscience import Fitter\n", - "from easyscience import ObjBase\n", + "from easyscience.base_classes import ModelBase\n", + "\n", + "\n", + "class QENSModel(ModelBase):\n", + " \"\"\"Expose the four fitting parameters to the (multi-)fitter.\n", + "\n", + " Subclassing ``ModelBase`` and declaring the parameters as\n", + " properties is the non-legacy replacement for the old ``ObjBase``\n", + " container: the fitter discovers the free parameters via\n", + " ``get_fit_parameters`` and updates them in place during the fit.\n", + " \"\"\"\n", + "\n", + " def __init__(self, A, gamma, omega_0, sigma):\n", + " super().__init__()\n", + " self._A = A\n", + " self._gamma = gamma\n", + " self._omega_0 = omega_0\n", + " self._sigma = sigma\n", + "\n", + " @property\n", + " def A(self):\n", + " return self._A\n", + "\n", + " @A.setter\n", + " def A(self, value):\n", + " self._A.value = value\n", + "\n", + " @property\n", + " def gamma(self):\n", + " return self._gamma\n", "\n", - "params = ObjBase(name='params', A=A, gamma=gamma, omega_0=omega_0, sigma=sigma)\n", + " @gamma.setter\n", + " def gamma(self, value):\n", + " self._gamma.value = value\n", + "\n", + " @property\n", + " def omega_0(self):\n", + " return self._omega_0\n", + "\n", + " @omega_0.setter\n", + " def omega_0(self, value):\n", + " self._omega_0.value = value\n", + "\n", + " @property\n", + " def sigma(self):\n", + " return self._sigma\n", + "\n", + " @sigma.setter\n", + " def sigma(self, value):\n", + " self._sigma.value = value\n", + "\n", + "\n", + "params = QENSModel(A=A, gamma=gamma, omega_0=omega_0, sigma=sigma)\n", "\n", "mle_fitter = Fitter(params, intensity)\n", "mle_result = mle_fitter.fit(x=omega, y=intensity_obs, weights=1 / di)\n", @@ -337,9 +390,10 @@ "\n", "Summarise each marginal posterior by its **median** and the 16th/84th percentiles, which together give an asymmetric 68% credible interval. Compare these with the MLE values you obtained in Exercise 2.\n", "\n", - "```{note}\n", - "**Why might the Bayesian median differ from the MLE?** The MLE finds the single point that maximises the likelihood, while the Bayesian median is the central value of the *posterior* — which also accounts for the prior $p(\\theta)$. If a parameter's posterior is skewed (asymmetric), the median and the mode (which approximates the MLE) will not coincide. The table below shows both so you can spot any such differences.\n", - "```\n", + "\n", + "**Why might the Bayesian median differ from the MLE?**\n", + "The MLE finds the single point that maximises the likelihood, while the Bayesian median is the central value of the *posterior* — which also accounts for the prior $p(\\theta)$. If a parameter's posterior is skewed (asymmetric), the median and the mode (which approximates the MLE) will not coincide. The table below shows both so you can spot any such differences.\n", + "\n", "\n", "Note that the columns of `result['draws']` are ordered by `result['param_names']` (which use the parameters' `unique_name`), so it is worth building a small helper to look up a column by friendly name.\n", "\n", From e912f5c05863b66d3645fdba8ddfb76d7b69cc72 Mon Sep 17 00:00:00 2001 From: rozyczko Date: Tue, 2 Jun 2026 11:26:58 +0200 Subject: [PATCH 13/15] PR review comments - part 1 --- docs/docs/tutorials/fitting-bayesian.ipynb | 302 ++++++++---------- docs/mkdocs.yml | 4 +- pyproject.toml | 3 +- .../fitting/minimizers/minimizer_bumps.py | 109 +++---- src/easyscience/fitting/multi_fitter.py | 91 +++--- .../integration/fitting/test_multi_fitter.py | 91 ++---- .../minimizers/test_minimizer_bumps.py | 79 ++--- tests/unit/fitting/test_multi_fitter.py | 80 ++--- 8 files changed, 303 insertions(+), 456 deletions(-) diff --git a/docs/docs/tutorials/fitting-bayesian.ipynb b/docs/docs/tutorials/fitting-bayesian.ipynb index d896a4d2..bddb86db 100644 --- a/docs/docs/tutorials/fitting-bayesian.ipynb +++ b/docs/docs/tutorials/fitting-bayesian.ipynb @@ -28,7 +28,11 @@ "\n", "where $\\theta$ are the model parameters, $d$ is the observed data, $p(d \\mid \\theta)$ is the likelihood, and $p(\\theta)$ is the prior. In `easyscience`, the `min`/`max` bounds of a `Parameter` are interpreted as a **uniform prior**, and a Gaussian likelihood is constructed from the data and supplied weights.\n", "\n", - "`easyscience` exposes a Bayesian Markov-chain Monte Carlo (MCMC) sampler through the `MultiFitter.sample` method. Under the hood this uses BUMPS' DREAM sampler, so the underlying minimizer must be switched to BUMPS.\n", + "`easyscience` exposes a Bayesian Markov-chain Monte Carlo (MCMC) sampler through the `MultiFitter.mcmc_sample` method. Under the hood this uses BUMPS' DREAM sampler, so the underlying minimizer must be switched to BUMPS.\n", + "\n", + "```{note}\n", + "This tutorial focuses on Bayesian analysis with a simple QENS model for illustration. For dedicated QENS fitting with more sophisticated models, consider using [`EasyDynamics`](https://github.com/easyscience/easydynamics).\n", + "```\n", "\n", "### When should you use a Bayesian analysis?\n", "\n", @@ -36,12 +40,12 @@ "\n", "- **Your uncertainties are asymmetric** — MLE error bars assume the parameter distribution is Gaussian, which is not always true. The posterior samples capture skew naturally.\n", "- **You have prior knowledge** — if you know from physics that a parameter *must* lie in a certain range, encoding that as a prior ($p(\\theta)$) is more principled than simply clamping bounds after the fit.\n", - "- **You care about parameter correlations** — the joint posterior (Exercise 6) reveals trade-offs between parameters that a single covariance matrix can miss.\n", - "- **You want to propagate uncertainty to predictions** — with the posterior in hand, you can compute credible bands on any function of the parameters (Exercise 7) without linearised error propagation.\n", + "- **You care about parameter correlations** — the joint posterior (shown in the corner plot below) reveals trade-offs between parameters that a single covariance matrix can miss.\n", + "- **You want to propagate uncertainty to predictions** — with the posterior in hand, you can compute credible bands on any function of the parameters (see the posterior-predictive band section) without linearised error propagation.\n", "\n", "The trade-off is computational cost: MCMC requires thousands of model evaluations, whereas an MLE fit may converge in dozens. For the simple 4-parameter model used here the difference is negligible, but for expensive models it is worth starting with MLE and only switching to MCMC when you need the richer output.\n", "\n", - "In this tutorial we re-use the QENS dataset and the Lorentzian-with-resolution model from the [Fitting QENS](fitting-qens.ipynb) tutorial, but instead of returning a single best-fit value with a symmetric error bar we will draw thousands of samples from the posterior." + "In this tutorial we re-use the QENS dataset and the Lorentzian-with-resolution model from the [Fitting QENS](fitting-qens.ipynb) tutorial, but instead of returning a single best-fit value with a symmetric error bar we will draw thousands of samples from the posterior.\n" ] }, { @@ -98,17 +102,25 @@ "\n", "\n", "def load(filename: str):\n", - " \"\"\"Load data from file and filter NaN values.\"\"\"\n", - " x, y, e = np.loadtxt(filename, unpack=True)\n", - " sel = np.isfinite(y)\n", - " return x[sel], y[sel], e[sel]\n", + " \"\"\"Load three-column text data (x, y, error) and filter NaN values.\n", + "\n", + " The file is expected to be a space-separated three-column text file\n", + " (commonly ``.dat`` or ``.txt``) with columns: x, y, error (1σ).\n", + " \"\"\"\n", + " x, y, error = np.loadtxt(filename, unpack=True)\n", + " selection = np.isfinite(y)\n", + " return x[selection], y[selection], error[selection]\n", "\n", "\n", - "omega, intensity_obs, di = load(filename)\n", + "omega, intensity_obs, intensity_error = load(filename)\n", "\n", "# Restrict to the region of interest, as in the QENS tutorial.\n", - "sel = (omega > -0.06) & (omega < 0.06)\n", - "omega, intensity_obs, di = omega[sel], intensity_obs[sel], di[sel]" + "selection = (omega > -0.06) & (omega < 0.06)\n", + "omega, intensity_obs, intensity_error = (\n", + " omega[selection],\n", + " intensity_obs[selection],\n", + " intensity_error[selection],\n", + ")" ] }, { @@ -119,83 +131,83 @@ "outputs": [], "source": [ "fig, ax = plt.subplots()\n", - "ax.errorbar(omega, intensity_obs, di, fmt='.')\n", + "ax.errorbar(omega, intensity_obs, intensity_error, fmt='.')\n", "ax.set(xlabel='$\\\\omega$/meV', ylabel='$I(\\\\omega)$')\n", "plt.show()" ] }, { "cell_type": "markdown", - "id": "7", + "id": "9", "metadata": {}, "source": [ - "## The model\n", + "## Defining parameters with priors\n", "\n", - "We re-use the convolution of a Lorentzian with a Gaussian resolution function from the QENS tutorial:\n", + "Create four `Parameter` objects, for the amplitude $A$, $\\gamma$, $\\omega_0$ and $\\sigma$. The `min` and `max` arguments define a **uniform prior** on each parameter — the sampler will only consider values inside this range and will treat every value inside the range as equally plausible *a priori*.\n", "\n", - "$$\n", - "I(\\omega) = \\frac{A\\gamma}{\\pi\\big[(\\omega - \\omega_0)^2 + \\gamma^2\\big]} \\;\\ast\\; \\mathcal{N}(0, \\sigma),\n", - "$$ (model)\n", - "\n", - "where $A$ is a scale factor, $\\gamma$ is the Lorentzian half-width at half-maximum, $\\omega_0$ is the centre offset and $\\sigma$ is the width of the Gaussian resolution kernel.\n", - "\n", - "```{note}\n", - "The convolution below uses `np.convolve(..., 'same')` for simplicity. In `'same'` mode, `numpy` pads the signal edges with zeros, which can introduce small artefacts at the boundaries. Because our Gaussian kernel ($\\sigma \\approx 10^{-3}$) is much narrower than the data range ($\\pm 0.06$ meV), these edge effects are negligible here. For production work with broader kernels, consider `scipy.signal.convolve` with an explicit boundary mode such as `'reflect'` or `'nearest'`.\n", - "```" + "| Parameter | Initial Value | Min | Max |\n", + "| --- | --- | --- | --- |\n", + "| $A$ (amplitude) | 10 | 1 | 100 |\n", + "| $\\gamma$ | 8.0 × 10-3 | 1.0 × 10-4 | 1.0 × 10-2 |\n", + "| $\\omega_0$ | 1.0 × 10-3 | 0 | 2.0 × 10-3 |\n", + "| $\\sigma$ | 1.0 × 10-3 | 1.0 × 10-5 | 1.0 × 10-1 |\n" ] }, { "cell_type": "code", "execution_count": null, - "id": "8", + "id": "10", "metadata": {}, "outputs": [], "source": [ - "from scipy.stats import norm\n", - "\n", - "\n", - "def lorentzian(x: np.ndarray) -> np.ndarray:\n", - " return A.value / np.pi * gamma.value / ((x - omega_0.value) ** 2 + gamma.value**2)\n", - "\n", + "from easyscience import Parameter\n", "\n", - "def intensity(x: np.ndarray) -> np.ndarray:\n", - " gauss = norm(0, sigma.value).pdf(x)\n", - " gauss /= gauss.sum()\n", - " return np.convolve(lorentzian(x), gauss, 'same')" + "amplitude = Parameter(name='amplitude', value=10, fixed=False, min=1, max=100)\n", + "gamma = Parameter(name='gamma', value=8e-3, fixed=False, min=1e-4, max=1e-2)\n", + "omega_0 = Parameter(name='omega_0', value=1e-3, fixed=False, min=0, max=2e-3)\n", + "sigma = Parameter(name='sigma', value=1e-3, fixed=False, min=1e-5, max=1e-1)" ] }, { "cell_type": "markdown", - "id": "9", + "id": "77ce3f63", "metadata": {}, "source": [ - "## Exercise 1: parameters with priors\n", + "## The model\n", "\n", - "Create four `Parameter` objects, for $A$, $\\gamma$, $\\omega_0$ and $\\sigma$. The `min` and `max` arguments define a **uniform prior** on each parameter — the sampler will only consider values inside this range and will treat every value inside the range as equally plausible *a priori*.\n", + "We re-use the convolution of a Lorentzian with a Gaussian resolution function from the QENS tutorial:\n", "\n", - "| Parameter | Initial Value | Min | Max |\n", - "| --- | --- | --- | --- |\n", - "| $A$ | 10 | 1 | 100 |\n", - "| $\\gamma$ | 8.0 × 10-3 | 1.0 × 10-4 | 1.0 × 10-2 |\n", - "| $\\omega_0$ | 1.0 × 10-3 | 0 | 2.0 × 10-3 |\n", - "| $\\sigma$ | 1.0 × 10-3 | 1.0 × 10-5 | 1.0 × 10-1 |\n", + "$$\n", + "I(\\omega) = \\frac{A\\gamma}{\\pi\\big[(\\omega - \\omega_0)^2 + \\gamma^2\\big]} \\;\\ast\\; \\mathcal{N}(0, \\sigma),\n", + "$$ (model)\n", + "\n", + "where $A$ is a scale factor (amplitude), $\\gamma$ is the Lorentzian half-width at half-maximum, $\\omega_0$ is the centre offset and $\\sigma$ is the width of the Gaussian resolution kernel.\n", + "\n", + "You might want to look at other QENS models, as implemented in the [EasyDynamics](https://github.com/easyscience/dynamics-lib) library.\n", "\n", - "**Solution:**" + "```{note}\n", + "The convolution below uses `np.convolve(..., 'same')` for simplicity. In `'same'` mode, `numpy` pads the signal edges with zeros, which can introduce small artefacts at the boundaries. Because our Gaussian kernel ($\\sigma \\approx 10^{-3}$) is much narrower than the data range ($\\pm 0.06$ meV), these edge effects are negligible here. For production work with broader kernels, consider `scipy.signal.convolve` with an explicit boundary mode such as `'reflect'` or `'nearest'`.\n", + "```\n" ] }, { "cell_type": "code", "execution_count": null, - "id": "10", + "id": "36a0c4f4", "metadata": {}, "outputs": [], "source": [ - "from easyscience import Parameter\n", + "from scipy.stats import norm\n", "\n", - "A = Parameter(name='A', value=10, fixed=False, min=1, max=100)\n", - "gamma = Parameter(name='gamma', value=8e-3, fixed=False, min=1e-4, max=1e-2)\n", - "omega_0 = Parameter(name='omega_0', value=1e-3, fixed=False, min=0, max=2e-3)\n", - "sigma = Parameter(name='sigma', value=1e-3, fixed=False, min=1e-5, max=1e-1)" + "\n", + "def lorentzian(x: np.ndarray) -> np.ndarray:\n", + " return amplitude.value / np.pi * gamma.value / ((x - omega_0.value) ** 2 + gamma.value**2)\n", + "\n", + "\n", + "def intensity_model(x: np.ndarray) -> np.ndarray:\n", + " gauss = norm(0, sigma.value).pdf(x)\n", + " gauss /= gauss.sum()\n", + " return np.convolve(lorentzian(x), gauss, 'same')" ] }, { @@ -203,11 +215,9 @@ "id": "11", "metadata": {}, "source": [ - "## Exercise 2: maximum-likelihood fit (for reference)\n", - "\n", - "Before running MCMC, perform a quick maximum-likelihood fit. The result will give us a good starting point and lets us compare the central tendency of the posterior with the classical best-fit values.\n", + "## Maximum-likelihood fit (for reference)\n", "\n", - "**Solution:**" + "Before running MCMC, perform a quick maximum-likelihood fit. The result will give us a good starting point and lets us compare the central tendency of the posterior with the classical best-fit values.\n" ] }, { @@ -218,64 +228,19 @@ "outputs": [], "source": [ "from easyscience import Fitter\n", - "from easyscience.base_classes import ModelBase\n", - "\n", - "\n", - "class QENSModel(ModelBase):\n", - " \"\"\"Expose the four fitting parameters to the (multi-)fitter.\n", - "\n", - " Subclassing ``ModelBase`` and declaring the parameters as\n", - " properties is the non-legacy replacement for the old ``ObjBase``\n", - " container: the fitter discovers the free parameters via\n", - " ``get_fit_parameters`` and updates them in place during the fit.\n", - " \"\"\"\n", - "\n", - " def __init__(self, A, gamma, omega_0, sigma):\n", - " super().__init__()\n", - " self._A = A\n", - " self._gamma = gamma\n", - " self._omega_0 = omega_0\n", - " self._sigma = sigma\n", - "\n", - " @property\n", - " def A(self):\n", - " return self._A\n", + "from easyscience import ObjBase\n", "\n", - " @A.setter\n", - " def A(self, value):\n", - " self._A.value = value\n", - "\n", - " @property\n", - " def gamma(self):\n", - " return self._gamma\n", - "\n", - " @gamma.setter\n", - " def gamma(self, value):\n", - " self._gamma.value = value\n", - "\n", - " @property\n", - " def omega_0(self):\n", - " return self._omega_0\n", - "\n", - " @omega_0.setter\n", - " def omega_0(self, value):\n", - " self._omega_0.value = value\n", - "\n", - " @property\n", - " def sigma(self):\n", - " return self._sigma\n", - "\n", - " @sigma.setter\n", - " def sigma(self, value):\n", - " self._sigma.value = value\n", - "\n", - "\n", - "params = QENSModel(A=A, gamma=gamma, omega_0=omega_0, sigma=sigma)\n", + "parameter_container = ObjBase(\n", + " name='params', A=amplitude, gamma=gamma, omega_0=omega_0, sigma=sigma\n", + ")\n", "\n", - "mle_fitter = Fitter(params, intensity)\n", - "mle_result = mle_fitter.fit(x=omega, y=intensity_obs, weights=1 / di)\n", + "mle_fitter = Fitter(parameter_container, intensity_model)\n", + "mle_result = mle_fitter.fit(x=omega, y=intensity_obs, weights=1 / intensity_error)\n", "\n", - "A, gamma, omega_0, sigma" + "print(f'A = {amplitude.value:.4g}')\n", + "print(f'gamma = {gamma.value:.4g}')\n", + "print(f'omega_0 = {omega_0.value:.4g}')\n", + "print(f'sigma = {sigma.value:.4g}')" ] }, { @@ -283,18 +248,25 @@ "id": "13", "metadata": {}, "source": [ - "## Exercise 3: switch to BUMPS and draw posterior samples\n", + "## Drawing posterior samples with DREAM\n", + "\n", + "We now draw samples from the posterior distribution $p(\\theta \\mid d)$ using the BUMPS DREAM (DiffeRential Evolution Adaptive Metropolis) algorithm. DREAM is an ensemble MCMC method that runs multiple chains in parallel and automatically tunes the proposal distribution.\n", "\n", - "`MultiFitter.sample` exposes the BUMPS DREAM sampler. It accepts a list of datasets (one entry per model) and returns a dictionary with the following keys:\n", + "DREAM only works with the BUMPS minimizer, so the first step is to switch to it. Then we call `MultiFitter.mcmc_sample`, which returns a dictionary with the following keys:\n", "\n", - "- `draws`: a `(n_samples, n_parameters)` array of posterior samples;\n", + "- `draws`: a `(n_samples, n_parameters)` array of posterior samples: each **row** is one complete draw from the joint posterior (one value for every parameter simultaneously), and each **column** holds all sampled values for a single parameter;\n", "- `param_names`: the unique names of the parameters, in the same column order as `draws`;\n", - "- `state`: the underlying BUMPS `MCMCDraw` object, useful for advanced diagnostics;\n", + "- `internal_bumps_object`: the underlying BUMPS `MCMCDraw` object, useful for advanced diagnostics;\n", "- `logp`: the log-posterior of each retained sample.\n", "\n", - "DREAM only works with the BUMPS minimizer, so the first step is to switch the minimizer over.\n", + "The key sampling parameters are:\n", "\n", - "**Solution:**" + "- `samples` (4000): the total number of posterior draws to generate across all chains;\n", + "- `burn` (500): the number of initial *burn-in* iterations to discard — the sampler needs time to find the typical set of the posterior, and early samples are not representative;\n", + "- `thin` (2): the *thinning* interval — only every second sample is kept, which reduces autocorrelation between consecutive draws;\n", + "- `seed` (42): a random seed for approximate reproducibility.\n", + "\n", + "First, we switch to the BUMPS minimizer:\n" ] }, { @@ -307,13 +279,21 @@ "from easyscience import AvailableMinimizers\n", "from easyscience.fitting.multi_fitter import MultiFitter\n", "\n", - "sampler = MultiFitter([params], [intensity])\n", - "sampler.switch_minimizer(AvailableMinimizers.Bumps)\n", - "\n", - "result = sampler.sample(\n", + "sampler = MultiFitter([parameter_container], [intensity_model])\n", + "sampler.switch_minimizer(AvailableMinimizers.Bumps)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5a219fcd", + "metadata": {}, + "outputs": [], + "source": [ + "result = sampler.mcmc_sample(\n", " x=[omega],\n", " y=[intensity_obs],\n", - " weights=[1 / di],\n", + " weights=[1 / intensity_error],\n", " samples=4000,\n", " burn=500,\n", " thin=2,\n", @@ -329,16 +309,14 @@ "id": "8766b170", "metadata": {}, "source": [ - "## Exercise 4: convergence diagnostics\n", + "## Convergence diagnostics\n", "\n", "Before we trust the posterior samples, we should check that the MCMC chains have **converged** — that is, the sampler has found the typical set of the posterior and is no longer drifting. Two simple visual checks are:\n", "\n", "1. **Trace plot** — plot the sampled parameter values against the sample index. A well-converged chain looks like a \"hairy caterpillar\": it fluctuates around a stable mean with no long-term trends.\n", "2. **Log-posterior plot** — the log-posterior $\\log p(\\theta \\mid d)$ should also stabilise after burn-in. If it is still climbing at the end of the run, the sampler has not yet converged.\n", "\n", - "The `logp` array returned by `sample` contains the log-posterior for every *retained* sample (i.e. *after* burn-in and thinning). This means a flat `logp` trace is exactly what we want to see.\n", - "\n", - "**Solution:**" + "The `logp` array returned by `sample` contains the log-posterior for every *retained* sample (i.e. *after* burn-in and thinning). This means a flat `logp` trace is exactly what we want to see.\n" ] }, { @@ -365,7 +343,7 @@ "# Trace plots for each parameter\n", "for ax, (label, par) in zip(\n", " axes[:4],\n", - " (('A', A), ('gamma', gamma), ('omega_0', omega_0), ('sigma', sigma)),\n", + " (('amplitude', amplitude), ('gamma', gamma), ('omega_0', omega_0), ('sigma', sigma)),\n", "):\n", " ax.plot(column_for(par), lw=0.5)\n", " ax.set_ylabel(label)\n", @@ -386,20 +364,26 @@ "id": "15", "metadata": {}, "source": [ - "## Exercise 5: posterior summaries\n", + "## Posterior summaries\n", "\n", - "Summarise each marginal posterior by its **median** and the 16th/84th percentiles, which together give an asymmetric 68% credible interval. Compare these with the MLE values you obtained in Exercise 2.\n", + "Summarise each marginal posterior by its **median** and the 16th/84th percentiles, which together give an asymmetric 68% credible interval. Compare these with the MLE values you obtained above.\n", "\n", "\n", "**Why might the Bayesian median differ from the MLE?**\n", "The MLE finds the single point that maximises the likelihood, while the Bayesian median is the central value of the *posterior* — which also accounts for the prior $p(\\theta)$. If a parameter's posterior is skewed (asymmetric), the median and the mode (which approximates the MLE) will not coincide. The table below shows both so you can spot any such differences.\n", "\n", "\n", - "Note that the columns of `result['draws']` are ordered by `result['param_names']` (which use the parameters' `unique_name`), so it is worth building a small helper to look up a column by friendly name.\n", - "\n", - "**Solution:**" + "Note that the columns of `result['draws']` are ordered by `result['param_names']` (which use the parameters' `unique_name`), so it is worth building a small helper to look up a column by friendly name.\n" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "ce3e38a8", + "metadata": {}, + "outputs": [], + "source": [] + }, { "cell_type": "code", "execution_count": null, @@ -408,14 +392,19 @@ "outputs": [], "source": [ "summary_rows = []\n", - "for label, par in (('A', A), ('gamma', gamma), ('omega_0', omega_0), ('sigma', sigma)):\n", + "for label, par in (\n", + " ('amplitude', amplitude),\n", + " ('gamma', gamma),\n", + " ('omega_0', omega_0),\n", + " ('sigma', sigma),\n", + "):\n", " col = column_for(par)\n", " lo, med, hi = np.percentile(col, [16, 50, 84])\n", " summary_rows.append((label, med, med - lo, hi - med, par.value))\n", "\n", - "print(f'{\"param\":<8s} {\"median\":>12s} {\"-1σ\":>12s} {\"+1σ\":>12s} {\"MLE\":>12s}')\n", - "for label, med, minus, plus, mle in summary_rows:\n", - " print(f'{label:<8s} {med:12.4g} {minus:12.4g} {plus:12.4g} {mle:12.4g}')" + "print(f'{\"param\":<8s} {\"median\":>12s} {\"16th\":>12s} {\"84th\":>12s} {\"MLE\":>12s}')\n", + "for label, med, low, high, mle in summary_rows:\n", + " print(f'{label:<8s} {med:12.4g} {low:12.4g} {high:12.4g} {mle:12.4g}')" ] }, { @@ -423,11 +412,9 @@ "id": "17", "metadata": {}, "source": [ - "## Exercise 6: visualise the joint posterior\n", - "\n", - "Marginal summaries hide correlations between parameters. A *corner plot* (a triangular grid of pairwise scatter plots and 1-D histograms) is the standard way to display them. Below we build one with plain `matplotlib`; if you have the [`corner`](https://corner.readthedocs.io/) package installed it will produce a publication-quality version with a single call.\n", + "## Visualise the joint posterior\n", "\n", - "**Solution:**" + "Marginal summaries hide correlations between parameters. A *corner plot* (a triangular grid of pairwise scatter plots and 1-D histograms) is the standard way to display them. Below we build one with plain `matplotlib`. To produce a publication-quality version, use plotting packages such as [`corner`](https://corner.readthedocs.io/) which can generate corner plots with a single function call.\n" ] }, { @@ -437,8 +424,8 @@ "metadata": {}, "outputs": [], "source": [ - "labels = ['A', 'gamma', 'omega_0', 'sigma']\n", - "cols = np.column_stack([column_for(p) for p in (A, gamma, omega_0, sigma)])\n", + "labels = ['amplitude', 'gamma', 'omega_0', 'sigma']\n", + "cols = np.column_stack([column_for(p) for p in (amplitude, gamma, omega_0, sigma)])\n", "n = len(labels)\n", "\n", "fig, axes = plt.subplots(n, n, figsize=(8, 8))\n", @@ -457,7 +444,7 @@ " ax.set_xlabel(labels[j])\n", " else:\n", " ax.set_xticklabels([])\n", - " if j == 0 and i != 0:\n", + " if j == 0:\n", " ax.set_ylabel(labels[i])\n", " else:\n", " ax.set_yticklabels([])\n", @@ -470,11 +457,9 @@ "id": "19", "metadata": {}, "source": [ - "## Exercise 7: posterior-predictive band\n", + "## Posterior-predictive band\n", "\n", - "One of the practical benefits of having the full posterior in hand is that it is trivial to propagate the uncertainty in the parameters through to predictions of the model. We pick a few hundred random draws, evaluate the model at each, and plot the resulting envelope alongside the data.\n", - "\n", - "**Solution:**" + "With the full posterior in hand we can propagate uncertainty through the model without assuming Gaussianity. Unlike error propagation from an MLE fit — which assumes parameters are normally distributed and independent — the posterior draws naturally capture any skew, heavy tails, and correlations between parameters. Below we pick a few hundred random draws, evaluate the model at each, and plot the resulting 95% credible band alongside the data.\n" ] }, { @@ -489,16 +474,16 @@ "indices = rng.choice(draws.shape[0], size=n_draws, replace=False)\n", "\n", "predictions = np.empty((n_draws, omega.size))\n", - "saved = {p.unique_name: p.value for p in (A, gamma, omega_0, sigma)}\n", + "saved = {p.unique_name: p.value for p in (amplitude, gamma, omega_0, sigma)}\n", "try:\n", " for k, idx in enumerate(indices):\n", - " A.value = draws[idx, name_to_col[A.unique_name]]\n", + " amplitude.value = draws[idx, name_to_col[amplitude.unique_name]]\n", " gamma.value = draws[idx, name_to_col[gamma.unique_name]]\n", " omega_0.value = draws[idx, name_to_col[omega_0.unique_name]]\n", " sigma.value = draws[idx, name_to_col[sigma.unique_name]]\n", - " predictions[k] = intensity(omega)\n", + " predictions[k] = intensity_model(omega)\n", "finally:\n", - " for p in (A, gamma, omega_0, sigma):\n", + " for p in (amplitude, gamma, omega_0, sigma):\n", " p.value = saved[p.unique_name]\n", "\n", "lo = np.percentile(predictions, 2.5, axis=0)\n", @@ -506,28 +491,13 @@ "mid = np.percentile(predictions, 50, axis=0)\n", "\n", "fig, ax = plt.subplots()\n", - "ax.errorbar(omega, intensity_obs, di, fmt='.', label='data')\n", + "ax.errorbar(omega, intensity_obs, intensity_error, fmt='.', label='data')\n", "ax.fill_between(omega, lo, hi, color='C1', alpha=0.3, label='95% posterior band')\n", "ax.plot(omega, mid, '-', color='C1', label='posterior median')\n", "ax.set(xlabel='$\\\\omega$/meV', ylabel='$I(\\\\omega)$')\n", "ax.legend()\n", "plt.show()" ] - }, - { - "cell_type": "markdown", - "id": "21", - "metadata": {}, - "source": [ - "## What next?\n", - "\n", - "A few things to try as further exercises:\n", - "\n", - "- Tighten or widen the `min`/`max` bounds on one of the parameters and see how the posterior reacts. This is the simplest possible *prior-sensitivity* analysis.\n", - "- Use the `chains` argument to `sample` to increase the DREAM population, e.g. `chains=10`, and check that the marginal histograms and trace plots are unchanged — a sign that the chains are sampling the same distribution.\n", - "- Pass a `progress_callback` to `sample` to monitor the sampler in real time — the same callback protocol used by the classical fitters works during MCMC (with `payload['sampling']` set to `True`).\n", - "- Inspect `result['state']` directly; the BUMPS [`MCMCDraw`](https://bumps.readthedocs.io/en/latest/api/bumps.dream.html) object exposes additional diagnostics such as per-chain log-posteriors, autocorrelation estimates, and the Gelman-Rubin $\\hat{R}$ statistic for formal convergence assessment." - ] } ], "metadata": { diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index bceed3b4..8620396a 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -176,11 +176,11 @@ nav: - User Guide: user-guide/index.md - Tutorials: - Tutorials: tutorials/index.md + - Bayesian analysis: tutorials/fitting-bayesian.ipynb + - Progress Callback: tutorials/progress-callback.ipynb - Workshops & Schools: - Fitting QENS: tutorials/fitting-qens.ipynb - Fitting SANS: tutorials/fitting-sans.ipynb - - Bayesian analysis: tutorials/fitting-bayesian.ipynb - - Progress Callback: tutorials/progress-callback.ipynb - API Reference: - API Reference: api-reference/index.md - base_classes: api-reference/base_classes.md diff --git a/pyproject.toml b/pyproject.toml index e2c8c24c..e555f419 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -211,7 +211,8 @@ select = [ # Ignore specific rules globally ignore = [ 'COM812', # https://docs.astral.sh/ruff/rules/missing-trailing-comma/ - # The following is replaced by 'D'/[tool.ruff.lint.pydocstyle] and [tool.pydoclint] 'DOC', # https://docs.astral.sh/ruff/rules/#pydoclint-doc + # The following is replaced by 'D'/[tool.ruff.lint.pydocstyle] and [tool.pydoclint] + 'DOC', # https://docs.astral.sh/ruff/rules/#pydoclint-doc # Disable, as [tool.format_docstring] split one-line docstrings into the canonical multi-line layout 'D200', # https://docs.astral.sh/ruff/rules/unnecessary-multiline-docstring/ ] diff --git a/src/easyscience/fitting/minimizers/minimizer_bumps.py b/src/easyscience/fitting/minimizers/minimizer_bumps.py index bdc1e7cb..5e068d0c 100644 --- a/src/easyscience/fitting/minimizers/minimizer_bumps.py +++ b/src/easyscience/fitting/minimizers/minimizer_bumps.py @@ -1,11 +1,12 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +from __future__ import annotations + import copy import warnings from typing import Any from typing import Callable -from typing import List import numpy as np from bumps.fitters import FIT_AVAILABLE_IDS @@ -64,11 +65,11 @@ def __init__( self._eval_counter: EvalCounter | None = None @staticmethod - def all_methods() -> List[str]: + def all_methods() -> list[str]: return FIT_AVAILABLE_IDS_FILTERED @staticmethod - def supported_methods() -> List[str]: + def supported_methods() -> list[str]: # only a small subset methods = ['amoeba', 'newton', 'lm'] return methods @@ -79,7 +80,7 @@ def fit( y: np.ndarray, weights: np.ndarray, model: Callable | None = None, - parameters: List[Parameter] | None = None, + parameters: list[Parameter] | None = None, method: str | None = None, tolerance: float | None = None, max_evaluations: int | None = None, @@ -102,7 +103,7 @@ def fit( Weights for supplied measured points. model : Callable | None, default=None Optional Model which is being fitted to. By default, None. - parameters : List[Parameter] | None, default=None + parameters : list[Parameter] | None, default=None Optional parameters for the fit. By default, None. method : str | None, default=None Method for minimization. By default, None. @@ -208,7 +209,7 @@ def fit( fitclass=fitclass, problem=problem, monitors=monitors, - abort_test=abort_test or (lambda: False), + abort_test=abort_test if abort_test is not None else (lambda: False), **minimizer_kwargs, **kwargs, ) @@ -262,43 +263,6 @@ def _resolve_fitclass(method: str): return fitclass raise FitError(f'Unknown BUMPS fitting method: {method}') - @staticmethod - def _resolve_population_alias(chains: int | None, population: int | None) -> int | None: - """Resolve the DREAM population count from the ``chains`` alias. - - Both ``chains`` (user-friendly name) and ``population`` (BUMPS - native name) refer to the same DREAM ``pop`` parameter. This - helper enforces that at most one is provided and returns the - resolved value. - - Parameters - ---------- - chains : int | None - User-friendly alias for the DREAM population count. - population : int | None - BUMPS-native DREAM population count. - - Returns - ------- - int | None - The resolved population count, or ``None`` if neither was - provided. - - Raises - ------ - ValueError - If both ``chains`` and ``population`` are provided with - different values. - """ - if chains is not None and population is not None: - if chains != population: - raise ValueError( - f'Conflicting population arguments: chains={chains}, ' - f'population={population}. Only provide one.' - ) - return chains - return chains if chains is not None else population - def _build_progress_payload( self, problem, iteration: int, point: np.ndarray, nllf: float ) -> dict: @@ -327,20 +291,20 @@ def _current_parameter_snapshot(self, problem, point: np.ndarray) -> dict: snapshot[dict_name] = float(value) return snapshot - def convert_to_pars_obj(self, par_list: List[Parameter] | None = None) -> List[BumpsParameter]: + def convert_to_pars_obj(self, par_list: list[Parameter] | None = None) -> list[BumpsParameter]: """ Create a container with the ``Parameters`` converted from the base object. Parameters ---------- - par_list : List[Parameter] | None, default=None + par_list : list[Parameter] | None, default=None If only a single/selection of parameter is required. Specify as a list. By default, None. Returns ------- - List[BumpsParameter] + list[BumpsParameter] Bumps Parameters list. """ if par_list is None: @@ -376,7 +340,7 @@ def convert_to_par_object(obj: Parameter) -> BumpsParameter: fixed=obj.fixed, ) - def _make_model(self, parameters: List[BumpsParameter] | None = None) -> Callable: + def _make_model(self, parameters: list[BumpsParameter] | None = None) -> Callable: """ Generate a bumps model from the supplied ``fit_function`` and parameters in the base object. Note that this makes a callable @@ -386,7 +350,7 @@ def _make_model(self, parameters: List[BumpsParameter] | None = None) -> Callabl Parameters ---------- - parameters : List[BumpsParameter] | None, default=None + parameters : list[BumpsParameter] | None, default=None Optional BUMPS parameters to bind into the model. Returns @@ -417,7 +381,7 @@ def _make_func(x, y, weights): return _outer(self) - def sample( + def mcmc_sample( self, x: np.ndarray, y: np.ndarray, @@ -425,7 +389,6 @@ def sample( samples: int = 10000, burn: int = 2000, thin: int = 10, - chains: int | None = None, population: int | None = None, seed: int | None = None, sampler_kwargs: dict | None = None, @@ -436,7 +399,7 @@ def sample( Builds a BUMPS `FitProblem` from the current model and runs the DREAM sampler. This is the public minimizer-level entry point for Bayesian - sampling; the higher-level `MultiFitter.sample` delegates to this + sampling; the higher-level `MultiFitter.mcmc_sample` delegates to this method after flattening multi-dataset arrays. Parameters @@ -453,10 +416,8 @@ def sample( Burn-in steps. thin : int, default=10 Thinning interval. - chains : int | None, default=None - User-friendly alias for BUMPS DREAM population count. population : int | None, default=None - BUMPS DREAM population count for advanced users. + BUMPS DREAM population count (number of parallel chains). seed : int | None, default=None Best-effort random seed. Calls ``numpy.random.seed(seed)`` before DREAM starts, which affects the *global* NumPy RNG @@ -478,14 +439,13 @@ def sample( Returns ------- dict - Dictionary with keys ``'draws'``, ``'param_names'``, ``'state'``, - and ``'logp'``. + Dictionary with keys ``'draws'``, ``'param_names'``, + ``'internal_bumps_object'``, and ``'logp'``. Raises ------ ValueError - If the input shapes or weights are invalid, if both ``chains`` - and ``population`` are provided with different values, or if + If the input shapes or weights are invalid, or if ``progress_callback`` is not callable. FitError If DREAM sampling was aborted by the user (via ``abort_test``). @@ -498,9 +458,21 @@ def sample( x, y, weights = np.asarray(x), np.asarray(y), np.asarray(weights) + if not isinstance(samples, int) or samples <= 0: + raise ValueError('samples must be a positive integer.') + if not isinstance(burn, int) or burn < 0: + raise ValueError('burn must be a non-negative integer.') + if not isinstance(thin, int) or thin < 1: + raise ValueError('thin must be a positive integer.') + if y.shape != x.shape: raise ValueError('x and y must have the same shape.') + if not np.isfinite(x).all(): + raise ValueError('x cannot contain NaN or infinite values.') + if not np.isfinite(y).all(): + raise ValueError('y cannot contain NaN or infinite values.') + if weights.shape != x.shape: raise ValueError('Weights must have the same shape as x and y.') @@ -519,13 +491,10 @@ def sample( if seed is not None: np.random.seed(seed) - # Resolve population parameter - pop = self._resolve_population_alias(chains, population) - # Build DREAM kwargs dream_kwargs: dict = {'samples': samples, 'burn': burn, 'thin': thin} - if pop is not None: - dream_kwargs['pop'] = pop + if population is not None: + dream_kwargs['pop'] = population if sampler_kwargs: dream_kwargs.update(sampler_kwargs) @@ -534,8 +503,10 @@ def sample( if progress_callback is not None: if not callable(progress_callback): raise ValueError('progress_callback must be callable') - # Compute total DREAM steps for progress display (burn + sampling generations) - pop_val = pop if pop else 10 + # Compute total DREAM steps for progress display (burn + sampling generations). + # BUMPS DREAM default population count is 10 when not specified by the user. + _dream_default_pop = 10 + pop_val = population if population is not None else _dream_default_pop _total_steps = burn + (samples + pop_val - 1) // pop_val monitors.append( BumpsProgressMonitor( @@ -552,7 +523,7 @@ def sample( fitclass=DreamFit, problem=problem, monitors=monitors, - abort_test=abort_test or (lambda: False), + abort_test=abort_test if abort_test is not None else (lambda: False), **dream_kwargs, ) driver.clip() @@ -580,7 +551,7 @@ def sample( return { 'draws': draws, 'param_names': param_names, - 'state': result_state, + 'internal_bumps_object': result_state, 'logp': logp, } @@ -601,7 +572,7 @@ def _set_parameter_fit_result( self, fit_result: Any, stack_status: bool, - par_list: List[BumpsParameter], + par_list: list[BumpsParameter], ) -> None: """ Update parameters to their final values and assign a std error @@ -613,7 +584,7 @@ def _set_parameter_fit_result( BUMPS OptimizeResult containing best-fit values and errors. stack_status : bool Whether the undo stack was enabled. - par_list : List[BumpsParameter] + par_list : list[BumpsParameter] List of BUMPS parameter objects. """ from easyscience import global_object diff --git a/src/easyscience/fitting/multi_fitter.py b/src/easyscience/fitting/multi_fitter.py index a7731972..fe52c639 100644 --- a/src/easyscience/fitting/multi_fitter.py +++ b/src/easyscience/fitting/multi_fitter.py @@ -1,10 +1,9 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +from __future__ import annotations + from typing import Callable -from typing import Dict -from typing import List -from typing import Optional import numpy as np @@ -27,8 +26,8 @@ class MultiFitter(Fitter): def __init__( self, - fit_objects: Optional[List] = None, - fit_functions: Optional[List[Callable]] = None, + fit_objects: list | None = None, + fit_functions: list[Callable] | None = None, ): # Create a dummy core object to hold all the fit objects. self._fit_objects = CollectionBase('multi', *fit_objects) @@ -38,7 +37,7 @@ def __init__( super().__init__(self._fit_objects, self._fit_functions[0]) def _fit_function_wrapper( - self, real_x: Optional[List[np.ndarray]] = None, flatten: bool = True + self, real_x: list[np.ndarray] | None = None, flatten: bool = True ) -> Callable: """ Simple fit function which injects the N real X (independent) @@ -48,7 +47,7 @@ def _fit_function_wrapper( Parameters ---------- - real_x : Optional[List[np.ndarray]], default=None + real_x : list[np.ndarray] | None, default=None List of independent x parameters to be injected. By default, None. flatten : bool, default=True @@ -81,31 +80,29 @@ def wrapped_fun(x, **kwargs): @staticmethod def _precompute_reshaping( - x: List[np.ndarray], - y: List[np.ndarray], - weights: Optional[List[np.ndarray]], + x: list[np.ndarray], + y: list[np.ndarray], + weights: list[np.ndarray] | None, vectorized: bool, - ) -> tuple[ - np.ndarray, List[np.ndarray], np.ndarray, Optional[np.ndarray], List[tuple[int, ...]] - ]: + ) -> tuple[np.ndarray, list[np.ndarray], np.ndarray, np.ndarray | None, list[tuple[int, ...]]]: """ Convert an array of X's and Y's to an acceptable shape for fitting. Parameters ---------- - x : List[np.ndarray] + x : list[np.ndarray] List of independent variables. - y : List[np.ndarray] + y : list[np.ndarray] List of dependent variables. - weights : Optional[List[np.ndarray]] + weights : list[np.ndarray] | None Optional weights for each dataset. vectorized : bool Is the fn input vectorized or point based? Returns ------- - tuple[np.ndarray, List[np.ndarray], np.ndarray, Optional[np.ndarray], List[tuple[int, ...]]] + tuple[np.ndarray, list[np.ndarray], np.ndarray, np.ndarray | None, list[tuple[int, ...]]] Reshaped x values, reshaped input data, flattened y values, flattened weights, and stored dependent dimensions. """ @@ -137,9 +134,9 @@ def _precompute_reshaping( def _post_compute_reshaping( self, fit_result_obj: FitResults, - x: List[np.ndarray], - y: List[np.ndarray], - ) -> List[FitResults]: + x: list[np.ndarray], + y: list[np.ndarray], + ) -> list[FitResults]: """ Split a multi-fit result object back into per-dataset results. @@ -147,14 +144,14 @@ def _post_compute_reshaping( ---------- fit_result_obj : FitResults Combined fit result returned by the minimizer. - x : List[np.ndarray] + x : list[np.ndarray] Original x coordinates for each dataset. - y : List[np.ndarray] + y : list[np.ndarray] Original y coordinates for each dataset. Returns ------- - List[FitResults] + list[FitResults] One fit result object per dataset. """ @@ -190,22 +187,21 @@ def _post_compute_reshaping( sp = ep return fit_results_list - def sample( + def mcmc_sample( self, - x: List[np.ndarray], - y: List[np.ndarray], - weights: List[np.ndarray], + x: list[np.ndarray], + y: list[np.ndarray], + weights: list[np.ndarray], samples: int = 10000, burn: int = 2000, thin: int = 10, - chains: int | None = None, population: int | None = None, seed: int | None = None, vectorized: bool = False, sampler_kwargs: dict | None = None, progress_callback: Callable[[dict], bool | None] | None = None, abort_test: Callable[[], bool] | None = None, - ) -> Dict: + ) -> dict: """Run Bayesian MCMC sampling using the BUMPS DREAM sampler. Requires that the current minimizer is a BUMPS instance (i.e. the @@ -213,11 +209,11 @@ def sample( Parameters ---------- - x : List[np.ndarray] + x : list[np.ndarray] List of independent variable arrays (one per dataset). - y : List[np.ndarray] + y : list[np.ndarray] List of dependent variable arrays (one per dataset). - weights : List[np.ndarray] + weights : list[np.ndarray] List of weight arrays (one per dataset). samples : int, default=10000 Number of retained DREAM samples requested from BUMPS. @@ -225,10 +221,8 @@ def sample( Burn-in steps. thin : int, default=10 Thinning interval. - chains : int | None, default=None - User-friendly alias for BUMPS DREAM population count. population : int | None, default=None - BUMPS DREAM population count (``pop``) for advanced users. + BUMPS DREAM population count (number of parallel chains). seed : int | None, default=None Best-effort random seed. BUMPS DREAM may use additional internal RNG state that is not controlled by this seed, so exact @@ -250,21 +244,23 @@ def sample( Returns ------- - Dict - Dictionary with keys ``'draws'``, ``'param_names'``, ``'state'``, - and ``'logp'``. + dict + Dictionary with keys ``'draws'``, ``'param_names'``, + ``'internal_bumps_object'``, and ``'logp'``. Raises ------ + ValueError + If ``samples``, ``burn``, or ``thin`` are invalid types or values. RuntimeError If the current minimizer is not a BUMPS instance. """ - # --- Alias resolution --- - # Delegate to the BUMPS minimizer's static helper so the logic - # stays in one place. - from easyscience.fitting.minimizers.minimizer_bumps import Bumps - - pop = Bumps._resolve_population_alias(chains, population) + if not isinstance(samples, int) or samples <= 0: + raise ValueError('samples must be a positive integer.') + if not isinstance(burn, int) or burn < 0: + raise ValueError('burn must be a non-negative integer.') + if not isinstance(thin, int) or thin < 1: + raise ValueError('thin must be a positive integer.') # Flatten multi-dataset arrays x_fit, x_new, y_new, w_new, _dims = self._precompute_reshaping( @@ -289,16 +285,15 @@ def sample( 'Use ``fitter.switch_minimizer(AvailableMinimizers.Bumps)`` first.' ) - # Delegate to the BUMPS minimizer's public sample method - result = minimizer.sample( + # Delegate to the BUMPS minimizer's public mcmc_sample method + result = minimizer.mcmc_sample( x=x_fit, y=y_new, weights=w_new, samples=samples, burn=burn, thin=thin, - chains=None, # alias already resolved into `pop` - population=pop, + population=population, seed=seed, sampler_kwargs=sampler_kwargs, progress_callback=progress_callback, diff --git a/tests/integration/fitting/test_multi_fitter.py b/tests/integration/fitting/test_multi_fitter.py index 4ceb3739..bfa3429f 100644 --- a/tests/integration/fitting/test_multi_fitter.py +++ b/tests/integration/fitting/test_multi_fitter.py @@ -289,13 +289,15 @@ def test_multi_fit_1D_2D(fit_engine): # --------------------------------------------------------------------------- -# Tests for MultiFitter.sample (Bayesian MCMC via BUMPS DREAM) +# Tests for MultiFitter.mcmc_sample (Bayesian MCMC via BUMPS DREAM) # --------------------------------------------------------------------------- -class TestSampleRequiresBumps: +class TestMultiFitterMcmcSample: + """Integration tests for ``MultiFitter.mcmc_sample``.""" + def test_raises_runtime_error_when_not_bumps(self): - """sample() must raise RuntimeError if the minimizer is not a BUMPS instance.""" + """mcmc_sample() must raise RuntimeError if the minimizer is not a BUMPS instance.""" sp = AbsSin(0.354, 3.05) f = MultiFitter([sp], [sp]) @@ -304,13 +306,11 @@ def test_raises_runtime_error_when_not_bumps(self): weights = np.ones_like(x) with pytest.raises(RuntimeError, match='Bayesian sampling requires a BUMPS minimizer'): - f.sample(x=[x], y=[y], weights=[weights], samples=10, burn=5, thin=1) - + f.mcmc_sample(x=[x], y=[y], weights=[weights], samples=10, burn=5, thin=1) -class TestSampleBasic: @pytest.mark.filterwarnings('ignore::UserWarning') def test_returns_expected_keys_and_shapes(self): - """sample() with BUMPS should return draws, param_names, state, logp.""" + """mcmc_sample() with BUMPS should return draws, param_names, state, logp.""" ref_sin = AbsSin(0.2, np.pi) sp = AbsSin(0.354, 3.05) @@ -327,12 +327,12 @@ def test_returns_expected_keys_and_shapes(self): except AttributeError: pytest.skip('BUMPS is not installed') - result = f.sample(x=[x], y=[y], weights=[weights], samples=100, burn=20, thin=2) + result = f.mcmc_sample(x=[x], y=[y], weights=[weights], samples=100, burn=20, thin=2) assert isinstance(result, dict) assert 'draws' in result assert 'param_names' in result - assert 'state' in result + assert 'internal_bumps_object' in result assert 'logp' in result # draws shape: (retained_samples, n_params) @@ -345,7 +345,7 @@ def test_returns_expected_keys_and_shapes(self): @pytest.mark.filterwarnings('ignore::UserWarning') def test_multi_dataset_returns_consistent_param_names(self): - """sample() with multiple datasets should have correct param_names across all.""" + """mcmc_sample() with multiple datasets should have correct param_names across all.""" ref_sin_1 = AbsSin(0.2, np.pi) sp_sin_1 = AbsSin(0.354, 3.05) sp_line = Line(0.43, 6.1) @@ -371,7 +371,7 @@ def test_multi_dataset_returns_consistent_param_names(self): except AttributeError: pytest.skip('BUMPS is not installed') - result = f.sample( + result = f.mcmc_sample( x=[x1, x2], y=[y1, y2], weights=[weights, weights], samples=100, burn=20, thin=2 ) @@ -380,28 +380,8 @@ def test_multi_dataset_returns_consistent_param_names(self): all_params |= {p.unique_name for p in sp_line.get_fit_parameters()} assert set(result['param_names']) == all_params - -class TestSampleAliasResolution: - def test_conflicting_chains_and_population_raises(self): - """Passing both chains and population with different values must raise.""" - sp = AbsSin(0.354, 3.05) - f = MultiFitter([sp], [sp]) - try: - f.switch_minimizer('Bumps') - except AttributeError: - pytest.skip('BUMPS is not installed') - - x = np.linspace(0, 5, 50) - y = np.sin(x) - weights = np.ones_like(x) - - with pytest.raises(ValueError, match='Conflicting population arguments'): - f.sample( - x=[x], y=[y], weights=[weights], samples=10, burn=5, thin=1, chains=3, population=5 - ) - - def test_chains_and_population_equal_is_ok(self): - """Passing chains == population should succeed (no conflict).""" + def test_population_param_controls_chain_count(self): + """Passing population should succeed and produce valid draws.""" sp = AbsSin(0.354, 3.05) sp.offset.fixed = False sp.phase.fixed = False @@ -415,18 +395,14 @@ def test_chains_and_population_equal_is_ok(self): y = np.sin(x) weights = np.ones_like(x) - # Should not raise ValueError — chains and population are equal. - # DREAM needs a sufficient population; 5 is a safe minimum. - result = f.sample( - x=[x], y=[y], weights=[weights], samples=100, burn=20, thin=2, chains=5, population=5 + result = f.mcmc_sample( + x=[x], y=[y], weights=[weights], samples=100, burn=20, thin=2, population=5 ) assert 'draws' in result - -class TestSampleSeedReproducibility: @pytest.mark.filterwarnings('ignore::UserWarning') def test_seed_produces_valid_draws(self): - """Running sample() with a seed must produce valid draws.""" + """Running mcmc_sample() with a seed must produce valid draws.""" ref_sin = AbsSin(0.2, np.pi) sp = AbsSin(0.354, 3.05) @@ -443,17 +419,18 @@ def test_seed_produces_valid_draws(self): except AttributeError: pytest.skip('BUMPS is not installed') - result = f.sample(x=[x], y=[y], weights=[weights], samples=100, burn=20, thin=2, seed=42) + result = f.mcmc_sample( + x=[x], y=[y], weights=[weights], samples=100, burn=20, thin=2, seed=42 + ) assert result['draws'].ndim == 2 assert result['draws'].shape[0] > 0 assert result['draws'].shape[1] == len(result['param_names']) - # logp should be present (may be None if not computed) assert 'logp' in result @pytest.mark.filterwarnings('ignore::UserWarning') def test_different_seeds_both_produce_valid_draws(self): - """Running sample() with different seeds should each produce valid draws.""" + """Running mcmc_sample() with different seeds should each produce valid draws.""" ref_sin = AbsSin(0.2, np.pi) sp = AbsSin(0.354, 3.05) @@ -470,22 +447,21 @@ def test_different_seeds_both_produce_valid_draws(self): except AttributeError: pytest.skip('BUMPS is not installed') - result1 = f.sample(x=[x], y=[y], weights=[weights], samples=100, burn=20, thin=2, seed=42) - result2 = f.sample( + result1 = f.mcmc_sample( + x=[x], y=[y], weights=[weights], samples=100, burn=20, thin=2, seed=42 + ) + result2 = f.mcmc_sample( x=[x], y=[y], weights=[weights], samples=100, burn=20, thin=2, seed=12345 ) - # Both must produce valid draws assert result1['draws'].shape[0] > 0 assert result2['draws'].shape[0] > 0 assert result1['draws'].ndim == 2 assert result2['draws'].ndim == 2 - -class TestSampleVectorized: @pytest.mark.filterwarnings('ignore::UserWarning') def test_vectorized_2d_input_produces_valid_draws(self): - """sample() with vectorized=True and 2D input should produce valid draws.""" + """mcmc_sample() with vectorized=True and 2D input should produce valid draws.""" sp = AbsSin2D(0.1, 1.75) x = np.linspace(0, 5, 50) @@ -503,7 +479,7 @@ def test_vectorized_2d_input_produces_valid_draws(self): except AttributeError: pytest.skip('BUMPS is not installed') - result = f.sample( + result = f.mcmc_sample( x=[x2D], y=[y2D], weights=[weights], samples=100, burn=20, thin=2, vectorized=True ) @@ -511,10 +487,8 @@ def test_vectorized_2d_input_produces_valid_draws(self): assert result['draws'].shape[0] > 0 assert result['draws'].shape[1] == len(result['param_names']) - -class TestSampleStateRestoration: def test_fit_function_restored_after_runtime_error(self): - """fit_function must be restored to its original value even when sample() raises.""" + """fit_function must be restored to its original value even when mcmc_sample() raises.""" sp = AbsSin(0.354, 3.05) f = MultiFitter([sp], [sp]) @@ -525,13 +499,13 @@ def test_fit_function_restored_after_runtime_error(self): original_func = f.fit_function with pytest.raises(RuntimeError): - f.sample(x=[x], y=[y], weights=[weights], samples=10, burn=5, thin=1) + f.mcmc_sample(x=[x], y=[y], weights=[weights], samples=10, burn=5, thin=1) assert f.fit_function is original_func @pytest.mark.filterwarnings('ignore::UserWarning') def test_fit_function_restored_after_successful_sample(self): - """fit_function must be restored to its original value after a successful sample().""" + """fit_function must be restored to its original value after a successful mcmc_sample().""" ref_sin = AbsSin(0.2, np.pi) sp = AbsSin(0.354, 3.05) @@ -549,11 +523,9 @@ def test_fit_function_restored_after_successful_sample(self): pytest.skip('BUMPS is not installed') original_func = f.fit_function - f.sample(x=[x], y=[y], weights=[weights], samples=100, burn=20, thin=2) + f.mcmc_sample(x=[x], y=[y], weights=[weights], samples=100, burn=20, thin=2) assert f.fit_function is original_func - -class TestSampleSamplerKwargs: @pytest.mark.filterwarnings('ignore::UserWarning') def test_sampler_kwargs_forwarded(self): """sampler_kwargs dict is forwarded to the BUMPS DREAM sampler.""" @@ -573,8 +545,7 @@ def test_sampler_kwargs_forwarded(self): except AttributeError: pytest.skip('BUMPS is not installed') - # Pass extra kwargs — should not raise - result = f.sample( + result = f.mcmc_sample( x=[x], y=[y], weights=[weights], diff --git a/tests/unit/fitting/minimizers/test_minimizer_bumps.py b/tests/unit/fitting/minimizers/test_minimizer_bumps.py index 28a55fe7..7a223f2c 100644 --- a/tests/unit/fitting/minimizers/test_minimizer_bumps.py +++ b/tests/unit/fitting/minimizers/test_minimizer_bumps.py @@ -675,12 +675,12 @@ def test_gen_fit_results_uses_nit_for_budget_check(self, minimizer: Bumps, monke # =================================================================== -# Bumps.sample() — Bayesian DREAM sampling +# Bumps.mcmc_sample() — Bayesian DREAM sampling # =================================================================== class TestBumpsSample: - """Tests for the ``Bumps.sample()`` method and its helpers.""" + """Tests for the ``Bumps.mcmc_sample()`` method and its helpers.""" # Sentinel value to signal "set fitter.state = None" in _setup_driver_mock ABORT = object() @@ -753,11 +753,11 @@ def _setup_driver_mock( return mock_FitDriver, mock_driver def test_sample_basic(self, minimizer: Bumps, monkeypatch) -> None: - """Verify that sample() returns a dict with expected keys.""" + """Verify that mcmc_sample() returns a dict with expected keys.""" mock_FitDriver, _ = self._setup_driver_mock(monkeypatch) minimizer._make_model = MagicMock(return_value=MagicMock(return_value=MagicMock())) - result = minimizer.sample( + result = minimizer.mcmc_sample( x=np.array([1.0, 2.0]), y=np.array([0.1, 0.2]), weights=np.array([1.0, 1.0]), @@ -770,7 +770,7 @@ def test_sample_basic(self, minimizer: Bumps, monkeypatch) -> None: assert isinstance(result, dict) assert 'draws' in result assert 'param_names' in result - assert 'state' in result + assert 'internal_bumps_object' in result assert 'logp' in result mock_FitDriver.assert_called_once() @@ -780,7 +780,7 @@ def test_sample_with_progress_callback(self, minimizer: Bumps, monkeypatch) -> N minimizer._make_model = MagicMock(return_value=MagicMock(return_value=MagicMock())) progress_callback = MagicMock() - result = minimizer.sample( + result = minimizer.mcmc_sample( x=np.array([1.0]), y=np.array([0.1]), weights=np.array([1.0]), @@ -802,7 +802,7 @@ def test_sample_aborted_by_user_raises_fit_error(self, minimizer: Bumps, monkeyp minimizer._make_model = MagicMock(return_value=MagicMock(return_value=MagicMock())) with pytest.raises(FitError, match='Sampling aborted by user'): - minimizer.sample(x=np.array([1.0]), y=np.array([0.1]), weights=np.array([1.0])) + minimizer.mcmc_sample(x=np.array([1.0]), y=np.array([0.1]), weights=np.array([1.0])) def test_sample_driver_exception_restores_parameters( self, minimizer: Bumps, monkeypatch @@ -813,22 +813,27 @@ def test_sample_driver_exception_restores_parameters( minimizer._restore_parameter_values = MagicMock() with pytest.raises(RuntimeError, match='driver failed'): - minimizer.sample(x=np.array([1.0]), y=np.array([0.1]), weights=np.array([1.0])) + minimizer.mcmc_sample(x=np.array([1.0]), y=np.array([0.1]), weights=np.array([1.0])) minimizer._restore_parameter_values.assert_called_once() - def test_sample_conflicting_population_raises(self, minimizer: Bumps) -> None: - with pytest.raises(ValueError, match='Conflicting population'): - minimizer.sample( - x=np.array([1.0]), - y=np.array([0.1]), - weights=np.array([1.0]), - chains=5, - population=10, - samples=10, - burn=0, - thin=1, - ) + def test_sample_population_param(self, minimizer: Bumps, monkeypatch) -> None: + """population kwarg is forwarded to DREAM as pop.""" + mock_FitDriver, _ = self._setup_driver_mock(monkeypatch) + minimizer._make_model = MagicMock(return_value=MagicMock(return_value=MagicMock())) + + minimizer.mcmc_sample( + x=np.array([1.0]), + y=np.array([0.1]), + weights=np.array([1.0]), + samples=10, + burn=0, + thin=1, + population=7, + ) + + call_kwargs = mock_FitDriver.call_args.kwargs + assert call_kwargs['pop'] == 7 def test_sample_rejects_non_callable_callback(self, minimizer: Bumps, monkeypatch) -> None: import bumps.names @@ -837,7 +842,7 @@ def test_sample_rejects_non_callable_callback(self, minimizer: Bumps, monkeypatc minimizer._make_model = MagicMock(return_value=MagicMock(return_value=MagicMock())) with pytest.raises(ValueError, match='progress_callback must be callable'): - minimizer.sample( + minimizer.mcmc_sample( x=np.array([1.0]), y=np.array([0.1]), weights=np.array([1.0]), @@ -848,38 +853,6 @@ def test_sample_rejects_non_callable_callback(self, minimizer: Bumps, monkeypatc ) -# =================================================================== -# _resolve_population_alias (static helper) -# =================================================================== - - -class TestResolvePopulationAlias: - """Tests for ``Bumps._resolve_population_alias``.""" - - def test_both_none_returns_none(self) -> None: - assert Bumps._resolve_population_alias(None, None) is None - - def test_chains_only_returns_chains(self) -> None: - assert Bumps._resolve_population_alias(5, None) == 5 - - def test_population_only_returns_population(self) -> None: - assert Bumps._resolve_population_alias(None, 7) == 7 - - def test_both_equal_returns_value(self) -> None: - assert Bumps._resolve_population_alias(5, 5) == 5 - - def test_both_different_raises(self) -> None: - with pytest.raises(ValueError, match='Conflicting population'): - Bumps._resolve_population_alias(3, 10) - - def test_chains_zero_is_valid(self) -> None: - """Zero is a valid (though unusual) population value.""" - assert Bumps._resolve_population_alias(0, None) == 0 - - def test_population_zero_is_valid(self) -> None: - assert Bumps._resolve_population_alias(None, 0) == 0 - - # =================================================================== # _build_sample_progress_payload # =================================================================== diff --git a/tests/unit/fitting/test_multi_fitter.py b/tests/unit/fitting/test_multi_fitter.py index f8d8495e..fe67d220 100644 --- a/tests/unit/fitting/test_multi_fitter.py +++ b/tests/unit/fitting/test_multi_fitter.py @@ -65,7 +65,7 @@ def test_fit_progress_callback(self, multi_fitter: MultiFitter): # =================================================================== -# MultiFitter.sample() — Bayesian DREAM sampling +# MultiFitter.mcmc_sample() — Bayesian DREAM sampling # =================================================================== @@ -78,7 +78,7 @@ def multi_fitter(self, monkeypatch): return MultiFitter([fit_object_1, fit_object_2], [fit_object_1, fit_object_2]) def test_sample_basic(self, multi_fitter: MultiFitter): - """Verify sample() calls the minimizer's sample() and returns its result.""" + """Verify mcmc_sample() calls the minimizer's mcmc_sample() and returns its result.""" import numpy as np multi_fitter._precompute_reshaping = MagicMock( @@ -90,12 +90,12 @@ def test_sample_basic(self, multi_fitter: MultiFitter): expected_result = { 'draws': np.array([[1.0]]), 'param_names': ['a'], - 'state': 'stub', + 'internal_bumps_object': 'stub', 'logp': None, } - multi_fitter._minimizer.sample = MagicMock(return_value=expected_result) + multi_fitter._minimizer.mcmc_sample = MagicMock(return_value=expected_result) - result = multi_fitter.sample( + result = multi_fitter.mcmc_sample( x=[np.array([1.0]), np.array([2.0])], y=[np.array([0.1]), np.array([0.2])], weights=[np.array([1.0]), np.array([1.0])], @@ -106,16 +106,17 @@ def test_sample_basic(self, multi_fitter: MultiFitter): ) assert result == expected_result - multi_fitter._minimizer.sample.assert_called_once() - call_kwargs = multi_fitter._minimizer.sample.call_args.kwargs + multi_fitter._minimizer.mcmc_sample.assert_called_once() + call_kwargs = multi_fitter._minimizer.mcmc_sample.call_args.kwargs assert call_kwargs['samples'] == 100 assert call_kwargs['burn'] == 20 assert call_kwargs['thin'] == 2 assert call_kwargs['population'] == 5 assert call_kwargs['progress_callback'] is None + assert 'chains' not in call_kwargs def test_sample_raises_if_not_bumps(self, multi_fitter: MultiFitter): - """sample() should raise RuntimeError if minimizer is not BUMPS.""" + """mcmc_sample() should raise RuntimeError if minimizer is not BUMPS.""" multi_fitter._precompute_reshaping = MagicMock( return_value=('x_fit', 'x_new', 'y_new', 'weights', 'dims') ) @@ -124,78 +125,43 @@ def test_sample_raises_if_not_bumps(self, multi_fitter: MultiFitter): multi_fitter._minimizer.package = 'lmfit' # Not bumps with pytest.raises(RuntimeError, match='Bayesian sampling requires a BUMPS minimizer'): - multi_fitter.sample( + multi_fitter.mcmc_sample( x=[np.array([1.0])], y=[np.array([0.1])], weights=[np.array([1.0])], ) def test_sample_with_progress_callback(self, multi_fitter: MultiFitter): - """Progress callback should be forwarded to minimizer.sample().""" + """Progress callback should be forwarded to minimizer.mcmc_sample().""" multi_fitter._precompute_reshaping = MagicMock( return_value=('x_fit', 'x_new', 'y_new', 'weights', 'dims') ) multi_fitter._fit_function_wrapper = MagicMock(return_value='wrapped_fit_function') multi_fitter._minimizer = MagicMock() multi_fitter._minimizer.package = 'bumps' - multi_fitter._minimizer.sample = MagicMock( - return_value={'draws': [], 'param_names': [], 'state': None, 'logp': None} + multi_fitter._minimizer.mcmc_sample = MagicMock( + return_value={ + 'draws': [], + 'param_names': [], + 'internal_bumps_object': None, + 'logp': None, + } ) progress_callback = MagicMock() - multi_fitter.sample( + multi_fitter.mcmc_sample( x=[np.array([1.0])], y=[np.array([0.1])], weights=[np.array([1.0])], progress_callback=progress_callback, ) - kwargs = multi_fitter._minimizer.sample.call_args.kwargs + kwargs = multi_fitter._minimizer.mcmc_sample.call_args.kwargs assert kwargs['progress_callback'] is progress_callback - def test_sample_population_alias(self, multi_fitter: MultiFitter): - """chains parameter is aliased to population.""" - multi_fitter._precompute_reshaping = MagicMock( - return_value=('x_fit', 'x_new', 'y_new', 'weights', 'dims') - ) - multi_fitter._fit_function_wrapper = MagicMock(return_value='wrapped_fit_function') - multi_fitter._minimizer = MagicMock() - multi_fitter._minimizer.package = 'bumps' - multi_fitter._minimizer.sample = MagicMock( - return_value={'draws': [], 'param_names': [], 'state': None, 'logp': None} - ) - - multi_fitter.sample( - x=[np.array([1.0])], - y=[np.array([0.1])], - weights=[np.array([1.0])], - chains=7, # Should be forwarded as population=7 - ) - - kwargs = multi_fitter._minimizer.sample.call_args.kwargs - assert kwargs['population'] == 7 - assert kwargs['chains'] is None - - def test_sample_conflicting_population_raises(self, multi_fitter: MultiFitter): - multi_fitter._precompute_reshaping = MagicMock( - return_value=('x_fit', 'x_new', 'y_new', 'weights', 'dims') - ) - multi_fitter._fit_function_wrapper = MagicMock(return_value='wrapped_fit_function') - multi_fitter._minimizer = MagicMock() - multi_fitter._minimizer.package = 'bumps' - - with pytest.raises(ValueError, match='Conflicting population'): - multi_fitter.sample( - x=[np.array([1.0])], - y=[np.array([0.1])], - weights=[np.array([1.0])], - chains=5, - population=10, - ) - def test_sample_restores_original_fit_function(self, multi_fitter: MultiFitter): - """After sample() completes (even on error) the original fit_function is restored.""" + """After mcmc_sample() completes (even on error) the original fit_function is restored.""" original_ff = multi_fitter.fit_function multi_fitter._precompute_reshaping = MagicMock( return_value=('x_fit', 'x_new', 'y_new', 'weights', 'dims') @@ -203,10 +169,10 @@ def test_sample_restores_original_fit_function(self, multi_fitter: MultiFitter): multi_fitter._fit_function_wrapper = MagicMock(return_value='wrapped_fit_function') multi_fitter._minimizer = MagicMock() multi_fitter._minimizer.package = 'bumps' - multi_fitter._minimizer.sample = MagicMock(side_effect=RuntimeError('boom')) + multi_fitter._minimizer.mcmc_sample = MagicMock(side_effect=RuntimeError('boom')) with pytest.raises(RuntimeError): - multi_fitter.sample( + multi_fitter.mcmc_sample( x=[np.array([1.0])], y=[np.array([0.1])], weights=[np.array([1.0])], From 9395b8fcdbf045cb8ea0775df37380334d21ce0b Mon Sep 17 00:00:00 2001 From: rozyczko Date: Tue, 2 Jun 2026 14:27:31 +0200 Subject: [PATCH 14/15] more PR review comments addressed --- docs/docs/tutorials/fitting-bayesian.ipynb | 20 +-- src/easyscience/fitting/fitter.py | 102 ++++++++++++ .../fitting/minimizers/minimizer_bumps.py | 12 -- src/easyscience/fitting/multi_fitter.py | 121 +------------- .../integration/fitting/test_multi_fitter.py | 59 ------- tests/unit/fitting/test_fitter.py | 153 ++++++++++++++++++ tests/unit/fitting/test_multi_fitter.py | 117 -------------- 7 files changed, 266 insertions(+), 318 deletions(-) diff --git a/docs/docs/tutorials/fitting-bayesian.ipynb b/docs/docs/tutorials/fitting-bayesian.ipynb index bddb86db..715f4301 100644 --- a/docs/docs/tutorials/fitting-bayesian.ipynb +++ b/docs/docs/tutorials/fitting-bayesian.ipynb @@ -28,7 +28,7 @@ "\n", "where $\\theta$ are the model parameters, $d$ is the observed data, $p(d \\mid \\theta)$ is the likelihood, and $p(\\theta)$ is the prior. In `easyscience`, the `min`/`max` bounds of a `Parameter` are interpreted as a **uniform prior**, and a Gaussian likelihood is constructed from the data and supplied weights.\n", "\n", - "`easyscience` exposes a Bayesian Markov-chain Monte Carlo (MCMC) sampler through the `MultiFitter.mcmc_sample` method. Under the hood this uses BUMPS' DREAM sampler, so the underlying minimizer must be switched to BUMPS.\n", + "`easyscience` exposes a Bayesian Markov-chain Monte Carlo (MCMC) sampler through the `Fitter.mcmc_sample` method. Under the hood this uses BUMPS' DREAM sampler, so the underlying minimizer must be switched to BUMPS.\n", "\n", "```{note}\n", "This tutorial focuses on Bayesian analysis with a simple QENS model for illustration. For dedicated QENS fitting with more sophisticated models, consider using [`EasyDynamics`](https://github.com/easyscience/easydynamics).\n", @@ -252,7 +252,7 @@ "\n", "We now draw samples from the posterior distribution $p(\\theta \\mid d)$ using the BUMPS DREAM (DiffeRential Evolution Adaptive Metropolis) algorithm. DREAM is an ensemble MCMC method that runs multiple chains in parallel and automatically tunes the proposal distribution.\n", "\n", - "DREAM only works with the BUMPS minimizer, so the first step is to switch to it. Then we call `MultiFitter.mcmc_sample`, which returns a dictionary with the following keys:\n", + "DREAM only works with the BUMPS minimizer. We reuse the ``mle_fitter`` created above, switch it to BUMPS, and call `Fitter.mcmc_sample`, which returns a dictionary with the following keys:\n", "\n", "- `draws`: a `(n_samples, n_parameters)` array of posterior samples: each **row** is one complete draw from the joint posterior (one value for every parameter simultaneously), and each **column** holds all sampled values for a single parameter;\n", "- `param_names`: the unique names of the parameters, in the same column order as `draws`;\n", @@ -264,7 +264,6 @@ "- `samples` (4000): the total number of posterior draws to generate across all chains;\n", "- `burn` (500): the number of initial *burn-in* iterations to discard — the sampler needs time to find the typical set of the posterior, and early samples are not representative;\n", "- `thin` (2): the *thinning* interval — only every second sample is kept, which reduces autocorrelation between consecutive draws;\n", - "- `seed` (42): a random seed for approximate reproducibility.\n", "\n", "First, we switch to the BUMPS minimizer:\n" ] @@ -277,10 +276,8 @@ "outputs": [], "source": [ "from easyscience import AvailableMinimizers\n", - "from easyscience.fitting.multi_fitter import MultiFitter\n", "\n", - "sampler = MultiFitter([parameter_container], [intensity_model])\n", - "sampler.switch_minimizer(AvailableMinimizers.Bumps)" + "mle_fitter.switch_minimizer(AvailableMinimizers.Bumps)" ] }, { @@ -290,14 +287,13 @@ "metadata": {}, "outputs": [], "source": [ - "result = sampler.mcmc_sample(\n", - " x=[omega],\n", - " y=[intensity_obs],\n", - " weights=[1 / intensity_error],\n", + "result = mle_fitter.mcmc_sample(\n", + " x=omega,\n", + " y=intensity_obs,\n", + " weights=1 / intensity_error,\n", " samples=4000,\n", " burn=500,\n", " thin=2,\n", - " seed=42,\n", ")\n", "\n", "print(f'Drew {result[\"draws\"].shape[0]} samples for {result[\"draws\"].shape[1]} parameters.')\n", @@ -502,7 +498,7 @@ ], "metadata": { "kernelspec": { - "display_name": "era", + "display_name": "p312", "language": "python", "name": "python3" }, diff --git a/src/easyscience/fitting/fitter.py b/src/easyscience/fitting/fitter.py index 38ef7351..861343ef 100644 --- a/src/easyscience/fitting/fitter.py +++ b/src/easyscience/fitting/fitter.py @@ -412,3 +412,105 @@ def _post_compute_reshaping( fit_result.y_calc = np.reshape(fit_result.y_calc, y.shape) fit_result.y_err = np.reshape(fit_result.y_err, y.shape) return fit_result + + def mcmc_sample( + self, + x: np.ndarray, + y: np.ndarray, + weights: np.ndarray, + samples: int = 10000, + burn: int = 2000, + thin: int = 10, + population: Optional[int] = None, + vectorized: bool = False, + sampler_kwargs: Optional[dict] = None, + progress_callback: Optional[Callable[[dict], Optional[bool]]] = None, + abort_test: Optional[Callable[[], bool]] = None, + ) -> dict: + """Run Bayesian MCMC sampling using the BUMPS DREAM sampler. + + Works with both a plain ``Fitter`` (single dataset) and a + ``MultiFitter`` (multiple datasets) via polymorphic dispatch: + ``_precompute_reshaping`` and ``_fit_function_wrapper`` are resolved + on the concrete subclass at call time, so multi-dataset flattening + is handled automatically when called on a ``MultiFitter`` instance. + + Parameters + ---------- + x : np.ndarray + Independent variable array (or list of arrays for ``MultiFitter``). + y : np.ndarray + Dependent variable array (or list of arrays for ``MultiFitter``). + weights : np.ndarray + Weight array (or list of arrays for ``MultiFitter``). + samples : int, default=10000 + Number of retained DREAM samples requested from BUMPS. + burn : int, default=2000 + Burn-in steps to discard before collecting samples. + thin : int, default=10 + Thinning interval — only every ``thin``-th sample is kept, + which reduces autocorrelation between consecutive draws. + population : Optional[int], default=None + BUMPS DREAM population count (number of parallel chains). + vectorized : bool, default=False + When ``True``, each x array may be multi-dimensional (e.g. an + ``(N, M, 2)`` grid for a 2D model) and is left as-is. When + ``False`` (default), each x array is expected to be 1-D. + sampler_kwargs : Optional[dict], default=None + Additional keyword arguments forwarded to the BUMPS DREAM sampler. + progress_callback : Optional[Callable[[dict], Optional[bool]]], default=None + Optional callback invoked at each DREAM generation. The payload + dict includes ``iteration`` and ``sampling: True``. + abort_test : Optional[Callable[[], bool]], default=None + Optional callable that returns ``True`` to abort sampling early. + + Returns + ------- + dict + Dictionary with keys ``'draws'``, ``'param_names'``, + ``'internal_bumps_object'``, and ``'logp'``. + + Raises + ------ + ValueError + If ``samples``, ``burn``, or ``thin`` are invalid. + RuntimeError + If the active minimizer is not a BUMPS instance. + """ + if not isinstance(samples, int) or samples <= 0: + raise ValueError('samples must be a positive integer.') + if not isinstance(burn, int) or burn < 0: + raise ValueError('burn must be a non-negative integer.') + if not isinstance(thin, int) or thin < 1: + raise ValueError('thin must be a positive integer.') + + minimizer = self.minimizer + if not (hasattr(minimizer, 'package') and minimizer.package == 'bumps'): + raise RuntimeError( + 'Bayesian sampling requires a BUMPS minimizer. ' + 'Use ``fitter.switch_minimizer(AvailableMinimizers.Bumps)`` first.' + ) + + x_fit, x_new, y_new, w_new, dims = self._precompute_reshaping(x, y, weights, vectorized) + self._dependent_dims = dims + + original_fit_func = self._fit_function + self.fit_function = self._fit_function_wrapper(x_new, flatten=True) + + try: + result = minimizer.mcmc_sample( + x=x_fit, + y=y_new, + weights=w_new, + samples=samples, + burn=burn, + thin=thin, + population=population, + sampler_kwargs=sampler_kwargs, + progress_callback=progress_callback, + abort_test=abort_test, + ) + finally: + self.fit_function = original_fit_func + + return result diff --git a/src/easyscience/fitting/minimizers/minimizer_bumps.py b/src/easyscience/fitting/minimizers/minimizer_bumps.py index 5e068d0c..bd0a9b9a 100644 --- a/src/easyscience/fitting/minimizers/minimizer_bumps.py +++ b/src/easyscience/fitting/minimizers/minimizer_bumps.py @@ -390,7 +390,6 @@ def mcmc_sample( burn: int = 2000, thin: int = 10, population: int | None = None, - seed: int | None = None, sampler_kwargs: dict | None = None, progress_callback: Callable[[dict], bool | None] | None = None, abort_test: Callable[[], bool] | None = None, @@ -418,13 +417,6 @@ def mcmc_sample( Thinning interval. population : int | None, default=None BUMPS DREAM population count (number of parallel chains). - seed : int | None, default=None - Best-effort random seed. Calls ``numpy.random.seed(seed)`` - before DREAM starts, which affects the *global* NumPy RNG - state and may interact with other code in the process. - BUMPS DREAM uses additional internal RNG state that is - **not** controlled by this seed, so exact reproducibility - across runs is **not** guaranteed. sampler_kwargs : dict | None, default=None Additional keyword arguments forwarded to `bumps.fitters.fit`. progress_callback : Callable[[dict], bool | None] | None, default=None @@ -487,10 +479,6 @@ def mcmc_sample( curve = model_func(x, y, weights) problem = FitProblem(curve) - # Best-effort seed: sets numpy's global RNG state just before DREAM starts. - if seed is not None: - np.random.seed(seed) - # Build DREAM kwargs dream_kwargs: dict = {'samples': samples, 'burn': burn, 'thin': thin} if population is not None: diff --git a/src/easyscience/fitting/multi_fitter.py b/src/easyscience/fitting/multi_fitter.py index fe52c639..92af9d9e 100644 --- a/src/easyscience/fitting/multi_fitter.py +++ b/src/easyscience/fitting/multi_fitter.py @@ -98,7 +98,9 @@ def _precompute_reshaping( weights : list[np.ndarray] | None Optional weights for each dataset. vectorized : bool - Is the fn input vectorized or point based? + When ``True``, each x array may be multi-dimensional (e.g. an + ``(N, M, 2)`` grid for a 2D model) and is left as-is. When + ``False`` (default), each x array is expected to be 1-D. Returns ------- @@ -186,120 +188,3 @@ def _post_compute_reshaping( fit_results_list.append(current_results) sp = ep return fit_results_list - - def mcmc_sample( - self, - x: list[np.ndarray], - y: list[np.ndarray], - weights: list[np.ndarray], - samples: int = 10000, - burn: int = 2000, - thin: int = 10, - population: int | None = None, - seed: int | None = None, - vectorized: bool = False, - sampler_kwargs: dict | None = None, - progress_callback: Callable[[dict], bool | None] | None = None, - abort_test: Callable[[], bool] | None = None, - ) -> dict: - """Run Bayesian MCMC sampling using the BUMPS DREAM sampler. - - Requires that the current minimizer is a BUMPS instance (i.e. the - minimizer was switched to ``AvailableMinimizers.Bumps`` or equivalent). - - Parameters - ---------- - x : list[np.ndarray] - List of independent variable arrays (one per dataset). - y : list[np.ndarray] - List of dependent variable arrays (one per dataset). - weights : list[np.ndarray] - List of weight arrays (one per dataset). - samples : int, default=10000 - Number of retained DREAM samples requested from BUMPS. - burn : int, default=2000 - Burn-in steps. - thin : int, default=10 - Thinning interval. - population : int | None, default=None - BUMPS DREAM population count (number of parallel chains). - seed : int | None, default=None - Best-effort random seed. BUMPS DREAM may use additional internal - RNG state that is not controlled by this seed, so exact - reproducibility is not guaranteed. - vectorized : bool, default=False - Whether the fit function expects vectorized (multidimensional) - input. - sampler_kwargs : dict | None, default=None - Additional keyword arguments forwarded to the BUMPS DREAM sampler - via `bumps.fitters.fit`. - progress_callback : Callable[[dict], bool | None] | None, default=None - Optional callback for progress updates during sampling. The - payload dict includes ``iteration`` (DREAM generation number) and - ``sampling: True``. - abort_test : Callable[[], bool] | None, default=None - Optional callback that returns ``True`` to signal that sampling - should be aborted. Called periodically during the DREAM sampling - loop. - - Returns - ------- - dict - Dictionary with keys ``'draws'``, ``'param_names'``, - ``'internal_bumps_object'``, and ``'logp'``. - - Raises - ------ - ValueError - If ``samples``, ``burn``, or ``thin`` are invalid types or values. - RuntimeError - If the current minimizer is not a BUMPS instance. - """ - if not isinstance(samples, int) or samples <= 0: - raise ValueError('samples must be a positive integer.') - if not isinstance(burn, int) or burn < 0: - raise ValueError('burn must be a non-negative integer.') - if not isinstance(thin, int) or thin < 1: - raise ValueError('thin must be a positive integer.') - - # Flatten multi-dataset arrays - x_fit, x_new, y_new, w_new, _dims = self._precompute_reshaping( - x, y, weights, vectorized=vectorized - ) - self._dependent_dims = _dims - - # Wrap fit functions for multi-dataset flattening, mirroring the - # ``Fitter.fit`` lifecycle: use the property setter so the minimizer - # is re-created with the wrapped fit function. - original_fit_func = self.fit_function - fit_fun_wrap = self._fit_function_wrapper(x_new, flatten=True) - self.fit_function = fit_fun_wrap - - try: - minimizer = self.minimizer - - # Verify it's a BUMPS minimizer (sampling only works with BUMPS/DREAM) - if not (hasattr(minimizer, 'package') and minimizer.package == 'bumps'): - raise RuntimeError( - 'Bayesian sampling requires a BUMPS minimizer. ' - 'Use ``fitter.switch_minimizer(AvailableMinimizers.Bumps)`` first.' - ) - - # Delegate to the BUMPS minimizer's public mcmc_sample method - result = minimizer.mcmc_sample( - x=x_fit, - y=y_new, - weights=w_new, - samples=samples, - burn=burn, - thin=thin, - population=population, - seed=seed, - sampler_kwargs=sampler_kwargs, - progress_callback=progress_callback, - abort_test=abort_test, - ) - finally: - self.fit_function = original_fit_func - - return result diff --git a/tests/integration/fitting/test_multi_fitter.py b/tests/integration/fitting/test_multi_fitter.py index bfa3429f..e4b87ea1 100644 --- a/tests/integration/fitting/test_multi_fitter.py +++ b/tests/integration/fitting/test_multi_fitter.py @@ -400,65 +400,6 @@ def test_population_param_controls_chain_count(self): ) assert 'draws' in result - @pytest.mark.filterwarnings('ignore::UserWarning') - def test_seed_produces_valid_draws(self): - """Running mcmc_sample() with a seed must produce valid draws.""" - ref_sin = AbsSin(0.2, np.pi) - sp = AbsSin(0.354, 3.05) - - x = np.linspace(0, 5, 50) - y = ref_sin(x) - weights = np.ones_like(x) - - sp.offset.fixed = False - sp.phase.fixed = False - - f = MultiFitter([sp], [sp]) - try: - f.switch_minimizer('Bumps') - except AttributeError: - pytest.skip('BUMPS is not installed') - - result = f.mcmc_sample( - x=[x], y=[y], weights=[weights], samples=100, burn=20, thin=2, seed=42 - ) - - assert result['draws'].ndim == 2 - assert result['draws'].shape[0] > 0 - assert result['draws'].shape[1] == len(result['param_names']) - assert 'logp' in result - - @pytest.mark.filterwarnings('ignore::UserWarning') - def test_different_seeds_both_produce_valid_draws(self): - """Running mcmc_sample() with different seeds should each produce valid draws.""" - ref_sin = AbsSin(0.2, np.pi) - sp = AbsSin(0.354, 3.05) - - x = np.linspace(0, 5, 50) - y = ref_sin(x) - weights = np.ones_like(x) - - sp.offset.fixed = False - sp.phase.fixed = False - - f = MultiFitter([sp], [sp]) - try: - f.switch_minimizer('Bumps') - except AttributeError: - pytest.skip('BUMPS is not installed') - - result1 = f.mcmc_sample( - x=[x], y=[y], weights=[weights], samples=100, burn=20, thin=2, seed=42 - ) - result2 = f.mcmc_sample( - x=[x], y=[y], weights=[weights], samples=100, burn=20, thin=2, seed=12345 - ) - - assert result1['draws'].shape[0] > 0 - assert result2['draws'].shape[0] > 0 - assert result1['draws'].ndim == 2 - assert result2['draws'].ndim == 2 - @pytest.mark.filterwarnings('ignore::UserWarning') def test_vectorized_2d_input_produces_valid_draws(self): """mcmc_sample() with vectorized=True and 2D input should produce valid draws.""" diff --git a/tests/unit/fitting/test_fitter.py b/tests/unit/fitting/test_fitter.py index 702f5e59..634492c5 100644 --- a/tests/unit/fitting/test_fitter.py +++ b/tests/unit/fitting/test_fitter.py @@ -258,3 +258,156 @@ def test_post_compute_reshaping(self, fitter: Fitter): # TODO # def test_fit_function_wrapper() # def test_precompute_reshaping() + + +# =================================================================== +# Fitter.mcmc_sample() — Bayesian DREAM sampling +# =================================================================== + + +class TestFitterMcmcSample: + @pytest.fixture + def fitter(self, monkeypatch): + monkeypatch.setattr(Fitter, '_update_minimizer', MagicMock()) + return Fitter(MagicMock(), MagicMock()) + + def test_basic(self, fitter: Fitter): + """mcmc_sample() calls minimizer.mcmc_sample() and returns its result.""" + fitter._precompute_reshaping = MagicMock( + return_value=('x_fit', 'x_new', 'y_new', 'w_new', 'dims') + ) + fitter._fit_function_wrapper = MagicMock(return_value='wrapped') + fitter._minimizer = MagicMock() + fitter._minimizer.package = 'bumps' + expected = { + 'draws': np.array([[1.0]]), + 'param_names': ['a'], + 'internal_bumps_object': 'stub', + 'logp': None, + } + fitter._minimizer.mcmc_sample = MagicMock(return_value=expected) + + result = fitter.mcmc_sample( + x=np.array([1.0]), + y=np.array([0.1]), + weights=np.array([1.0]), + samples=100, + burn=20, + thin=2, + population=5, + ) + + assert result == expected + fitter._precompute_reshaping.assert_called_once_with( + np.array([1.0]), np.array([0.1]), np.array([1.0]), False + ) + fitter._fit_function_wrapper.assert_called_once_with('x_new', flatten=True) + fitter._minimizer.mcmc_sample.assert_called_once() + kw = fitter._minimizer.mcmc_sample.call_args.kwargs + assert kw['x'] == 'x_fit' + assert kw['y'] == 'y_new' + assert kw['weights'] == 'w_new' + assert kw['samples'] == 100 + assert kw['burn'] == 20 + assert kw['thin'] == 2 + assert kw['population'] == 5 + assert kw['progress_callback'] is None + assert fitter._dependent_dims == 'dims' + + def test_raises_if_not_bumps(self, fitter: Fitter): + """RuntimeError raised when the active minimizer is not BUMPS.""" + fitter._precompute_reshaping = MagicMock( + return_value=('x_fit', 'x_new', 'y_new', 'w_new', 'dims') + ) + fitter._fit_function_wrapper = MagicMock(return_value='wrapped') + fitter._minimizer = MagicMock() + fitter._minimizer.package = 'lmfit' + + with pytest.raises(RuntimeError, match='Bayesian sampling requires a BUMPS minimizer'): + fitter.mcmc_sample(x=np.array([1.0]), y=np.array([0.1]), weights=np.array([1.0])) + + def test_progress_callback_forwarded(self, fitter: Fitter): + """progress_callback is forwarded to minimizer.mcmc_sample().""" + fitter._precompute_reshaping = MagicMock( + return_value=('x_fit', 'x_new', 'y_new', 'w_new', 'dims') + ) + fitter._fit_function_wrapper = MagicMock(return_value='wrapped') + fitter._minimizer = MagicMock() + fitter._minimizer.package = 'bumps' + fitter._minimizer.mcmc_sample = MagicMock( + return_value={ + 'draws': [], + 'param_names': [], + 'internal_bumps_object': None, + 'logp': None, + } + ) + cb = MagicMock() + + fitter.mcmc_sample( + x=np.array([1.0]), + y=np.array([0.1]), + weights=np.array([1.0]), + progress_callback=cb, + ) + + assert fitter._minimizer.mcmc_sample.call_args.kwargs['progress_callback'] is cb + + def test_fit_function_restored_on_success(self, fitter: Fitter): + """Original fit function is restored after a successful call.""" + fitter._precompute_reshaping = MagicMock( + return_value=('x_fit', 'x_new', 'y_new', 'w_new', 'dims') + ) + fitter._fit_function_wrapper = MagicMock(return_value='wrapped') + fitter._minimizer = MagicMock() + fitter._minimizer.package = 'bumps' + fitter._minimizer.mcmc_sample = MagicMock( + return_value={ + 'draws': [], + 'param_names': [], + 'internal_bumps_object': None, + 'logp': None, + } + ) + original = fitter._fit_function + + fitter.mcmc_sample(x=np.array([1.0]), y=np.array([0.1]), weights=np.array([1.0])) + + assert fitter._fit_function is original + + def test_fit_function_restored_on_error(self, fitter: Fitter): + """Original fit function is restored even when minimizer raises.""" + fitter._precompute_reshaping = MagicMock( + return_value=('x_fit', 'x_new', 'y_new', 'w_new', 'dims') + ) + fitter._fit_function_wrapper = MagicMock(return_value='wrapped') + fitter._minimizer = MagicMock() + fitter._minimizer.package = 'bumps' + fitter._minimizer.mcmc_sample = MagicMock(side_effect=RuntimeError('boom')) + original = fitter._fit_function + + with pytest.raises(RuntimeError): + fitter.mcmc_sample(x=np.array([1.0]), y=np.array([0.1]), weights=np.array([1.0])) + + assert fitter._fit_function is original + + @pytest.mark.parametrize( + 'kwargs, match', + [ + ({'samples': 0}, 'samples must be a positive integer'), + ({'samples': -1}, 'samples must be a positive integer'), + ({'burn': -1}, 'burn must be a non-negative integer'), + ({'thin': 0}, 'thin must be a positive integer'), + ], + ) + def test_invalid_args_raise(self, fitter: Fitter, kwargs, match): + """Invalid samples/burn/thin values raise ValueError before any I/O.""" + with pytest.raises(ValueError, match=match): + fitter.mcmc_sample( + x=np.array([1.0]), + y=np.array([0.1]), + weights=np.array([1.0]), + samples=kwargs.get('samples', 10), + burn=kwargs.get('burn', 0), + thin=kwargs.get('thin', 1), + ) diff --git a/tests/unit/fitting/test_multi_fitter.py b/tests/unit/fitting/test_multi_fitter.py index fe67d220..ac897653 100644 --- a/tests/unit/fitting/test_multi_fitter.py +++ b/tests/unit/fitting/test_multi_fitter.py @@ -64,123 +64,6 @@ def test_fit_progress_callback(self, multi_fitter: MultiFitter): ) -# =================================================================== -# MultiFitter.mcmc_sample() — Bayesian DREAM sampling -# =================================================================== - - -class TestMultiFitterSample: - @pytest.fixture - def multi_fitter(self, monkeypatch): - monkeypatch.setattr(Fitter, '_update_minimizer', MagicMock()) - fit_object_1 = Line(1.0, 0.5) - fit_object_2 = Line(2.0, 1.5) - return MultiFitter([fit_object_1, fit_object_2], [fit_object_1, fit_object_2]) - - def test_sample_basic(self, multi_fitter: MultiFitter): - """Verify mcmc_sample() calls the minimizer's mcmc_sample() and returns its result.""" - import numpy as np - - multi_fitter._precompute_reshaping = MagicMock( - return_value=('x_fit', 'x_new', 'y_new', 'weights', 'dims') - ) - multi_fitter._fit_function_wrapper = MagicMock(return_value='wrapped_fit_function') - multi_fitter._minimizer = MagicMock() - multi_fitter._minimizer.package = 'bumps' - expected_result = { - 'draws': np.array([[1.0]]), - 'param_names': ['a'], - 'internal_bumps_object': 'stub', - 'logp': None, - } - multi_fitter._minimizer.mcmc_sample = MagicMock(return_value=expected_result) - - result = multi_fitter.mcmc_sample( - x=[np.array([1.0]), np.array([2.0])], - y=[np.array([0.1]), np.array([0.2])], - weights=[np.array([1.0]), np.array([1.0])], - samples=100, - burn=20, - thin=2, - population=5, - ) - - assert result == expected_result - multi_fitter._minimizer.mcmc_sample.assert_called_once() - call_kwargs = multi_fitter._minimizer.mcmc_sample.call_args.kwargs - assert call_kwargs['samples'] == 100 - assert call_kwargs['burn'] == 20 - assert call_kwargs['thin'] == 2 - assert call_kwargs['population'] == 5 - assert call_kwargs['progress_callback'] is None - assert 'chains' not in call_kwargs - - def test_sample_raises_if_not_bumps(self, multi_fitter: MultiFitter): - """mcmc_sample() should raise RuntimeError if minimizer is not BUMPS.""" - multi_fitter._precompute_reshaping = MagicMock( - return_value=('x_fit', 'x_new', 'y_new', 'weights', 'dims') - ) - multi_fitter._fit_function_wrapper = MagicMock(return_value='wrapped_fit_function') - multi_fitter._minimizer = MagicMock() - multi_fitter._minimizer.package = 'lmfit' # Not bumps - - with pytest.raises(RuntimeError, match='Bayesian sampling requires a BUMPS minimizer'): - multi_fitter.mcmc_sample( - x=[np.array([1.0])], - y=[np.array([0.1])], - weights=[np.array([1.0])], - ) - - def test_sample_with_progress_callback(self, multi_fitter: MultiFitter): - """Progress callback should be forwarded to minimizer.mcmc_sample().""" - multi_fitter._precompute_reshaping = MagicMock( - return_value=('x_fit', 'x_new', 'y_new', 'weights', 'dims') - ) - multi_fitter._fit_function_wrapper = MagicMock(return_value='wrapped_fit_function') - multi_fitter._minimizer = MagicMock() - multi_fitter._minimizer.package = 'bumps' - multi_fitter._minimizer.mcmc_sample = MagicMock( - return_value={ - 'draws': [], - 'param_names': [], - 'internal_bumps_object': None, - 'logp': None, - } - ) - - progress_callback = MagicMock() - - multi_fitter.mcmc_sample( - x=[np.array([1.0])], - y=[np.array([0.1])], - weights=[np.array([1.0])], - progress_callback=progress_callback, - ) - - kwargs = multi_fitter._minimizer.mcmc_sample.call_args.kwargs - assert kwargs['progress_callback'] is progress_callback - - def test_sample_restores_original_fit_function(self, multi_fitter: MultiFitter): - """After mcmc_sample() completes (even on error) the original fit_function is restored.""" - original_ff = multi_fitter.fit_function - multi_fitter._precompute_reshaping = MagicMock( - return_value=('x_fit', 'x_new', 'y_new', 'weights', 'dims') - ) - multi_fitter._fit_function_wrapper = MagicMock(return_value='wrapped_fit_function') - multi_fitter._minimizer = MagicMock() - multi_fitter._minimizer.package = 'bumps' - multi_fitter._minimizer.mcmc_sample = MagicMock(side_effect=RuntimeError('boom')) - - with pytest.raises(RuntimeError): - multi_fitter.mcmc_sample( - x=[np.array([1.0])], - y=[np.array([0.1])], - weights=[np.array([1.0])], - ) - - assert multi_fitter.fit_function is original_ff - - # =================================================================== # MultiFitter._post_compute_reshaping # =================================================================== From e3979735d9d931196ebf14c3a03a64210f772ad7 Mon Sep 17 00:00:00 2001 From: rozyczko Date: Wed, 3 Jun 2026 14:20:43 +0200 Subject: [PATCH 15/15] fixed bayesian failing... --- docs/docs/tutorials/fitting-bayesian.ipynb | 32 ++++++++++------------ src/easyscience/fitting/fitter.py | 14 +++++----- 2 files changed, 22 insertions(+), 24 deletions(-) diff --git a/docs/docs/tutorials/fitting-bayesian.ipynb b/docs/docs/tutorials/fitting-bayesian.ipynb index 715f4301..77e8b884 100644 --- a/docs/docs/tutorials/fitting-bayesian.ipynb +++ b/docs/docs/tutorials/fitting-bayesian.ipynb @@ -143,11 +143,11 @@ "source": [ "## Defining parameters with priors\n", "\n", - "Create four `Parameter` objects, for the amplitude $A$, $\\gamma$, $\\omega_0$ and $\\sigma$. The `min` and `max` arguments define a **uniform prior** on each parameter — the sampler will only consider values inside this range and will treat every value inside the range as equally plausible *a priori*.\n", + "Create four `Parameter` objects, for the area $A$, $\\gamma$, $\\omega_0$ and $\\sigma$. The `min` and `max` arguments define a **uniform prior** on each parameter — the sampler will only consider values inside this range and will treat every value inside the range as equally plausible *a priori*.\n", "\n", "| Parameter | Initial Value | Min | Max |\n", "| --- | --- | --- | --- |\n", - "| $A$ (amplitude) | 10 | 1 | 100 |\n", + "| $A$ (area) | 10 | 1 | 100 |\n", "| $\\gamma$ | 8.0 × 10-3 | 1.0 × 10-4 | 1.0 × 10-2 |\n", "| $\\omega_0$ | 1.0 × 10-3 | 0 | 2.0 × 10-3 |\n", "| $\\sigma$ | 1.0 × 10-3 | 1.0 × 10-5 | 1.0 × 10-1 |\n" @@ -162,7 +162,7 @@ "source": [ "from easyscience import Parameter\n", "\n", - "amplitude = Parameter(name='amplitude', value=10, fixed=False, min=1, max=100)\n", + "area = Parameter(name='area', value=10, fixed=False, min=1, max=100)\n", "gamma = Parameter(name='gamma', value=8e-3, fixed=False, min=1e-4, max=1e-2)\n", "omega_0 = Parameter(name='omega_0', value=1e-3, fixed=False, min=0, max=2e-3)\n", "sigma = Parameter(name='sigma', value=1e-3, fixed=False, min=1e-5, max=1e-1)" @@ -181,7 +181,7 @@ "I(\\omega) = \\frac{A\\gamma}{\\pi\\big[(\\omega - \\omega_0)^2 + \\gamma^2\\big]} \\;\\ast\\; \\mathcal{N}(0, \\sigma),\n", "$$ (model)\n", "\n", - "where $A$ is a scale factor (amplitude), $\\gamma$ is the Lorentzian half-width at half-maximum, $\\omega_0$ is the centre offset and $\\sigma$ is the width of the Gaussian resolution kernel.\n", + "where $A$ is a scale factor (area), $\\gamma$ is the Lorentzian half-width at half-maximum, $\\omega_0$ is the centre offset and $\\sigma$ is the width of the Gaussian resolution kernel.\n", "\n", "You might want to look at other QENS models, as implemented in the [EasyDynamics](https://github.com/easyscience/dynamics-lib) library.\n", "\n", @@ -201,7 +201,7 @@ "\n", "\n", "def lorentzian(x: np.ndarray) -> np.ndarray:\n", - " return amplitude.value / np.pi * gamma.value / ((x - omega_0.value) ** 2 + gamma.value**2)\n", + " return area.value / np.pi * gamma.value / ((x - omega_0.value) ** 2 + gamma.value**2)\n", "\n", "\n", "def intensity_model(x: np.ndarray) -> np.ndarray:\n", @@ -230,14 +230,12 @@ "from easyscience import Fitter\n", "from easyscience import ObjBase\n", "\n", - "parameter_container = ObjBase(\n", - " name='params', A=amplitude, gamma=gamma, omega_0=omega_0, sigma=sigma\n", - ")\n", + "parameter_container = ObjBase(name='params', A=area, gamma=gamma, omega_0=omega_0, sigma=sigma)\n", "\n", "mle_fitter = Fitter(parameter_container, intensity_model)\n", "mle_result = mle_fitter.fit(x=omega, y=intensity_obs, weights=1 / intensity_error)\n", "\n", - "print(f'A = {amplitude.value:.4g}')\n", + "print(f'A = {area.value:.4g}')\n", "print(f'gamma = {gamma.value:.4g}')\n", "print(f'omega_0 = {omega_0.value:.4g}')\n", "print(f'sigma = {sigma.value:.4g}')" @@ -291,7 +289,7 @@ " x=omega,\n", " y=intensity_obs,\n", " weights=1 / intensity_error,\n", - " samples=4000,\n", + " samples=10000,\n", " burn=500,\n", " thin=2,\n", ")\n", @@ -339,7 +337,7 @@ "# Trace plots for each parameter\n", "for ax, (label, par) in zip(\n", " axes[:4],\n", - " (('amplitude', amplitude), ('gamma', gamma), ('omega_0', omega_0), ('sigma', sigma)),\n", + " (('area', area), ('gamma', gamma), ('omega_0', omega_0), ('sigma', sigma)),\n", "):\n", " ax.plot(column_for(par), lw=0.5)\n", " ax.set_ylabel(label)\n", @@ -389,7 +387,7 @@ "source": [ "summary_rows = []\n", "for label, par in (\n", - " ('amplitude', amplitude),\n", + " ('area', area),\n", " ('gamma', gamma),\n", " ('omega_0', omega_0),\n", " ('sigma', sigma),\n", @@ -420,8 +418,8 @@ "metadata": {}, "outputs": [], "source": [ - "labels = ['amplitude', 'gamma', 'omega_0', 'sigma']\n", - "cols = np.column_stack([column_for(p) for p in (amplitude, gamma, omega_0, sigma)])\n", + "labels = ['area', 'gamma', 'omega_0', 'sigma']\n", + "cols = np.column_stack([column_for(p) for p in (area, gamma, omega_0, sigma)])\n", "n = len(labels)\n", "\n", "fig, axes = plt.subplots(n, n, figsize=(8, 8))\n", @@ -470,16 +468,16 @@ "indices = rng.choice(draws.shape[0], size=n_draws, replace=False)\n", "\n", "predictions = np.empty((n_draws, omega.size))\n", - "saved = {p.unique_name: p.value for p in (amplitude, gamma, omega_0, sigma)}\n", + "saved = {p.unique_name: p.value for p in (area, gamma, omega_0, sigma)}\n", "try:\n", " for k, idx in enumerate(indices):\n", - " amplitude.value = draws[idx, name_to_col[amplitude.unique_name]]\n", + " area.value = draws[idx, name_to_col[area.unique_name]]\n", " gamma.value = draws[idx, name_to_col[gamma.unique_name]]\n", " omega_0.value = draws[idx, name_to_col[omega_0.unique_name]]\n", " sigma.value = draws[idx, name_to_col[sigma.unique_name]]\n", " predictions[k] = intensity_model(omega)\n", "finally:\n", - " for p in (amplitude, gamma, omega_0, sigma):\n", + " for p in (area, gamma, omega_0, sigma):\n", " p.value = saved[p.unique_name]\n", "\n", "lo = np.percentile(predictions, 2.5, axis=0)\n", diff --git a/src/easyscience/fitting/fitter.py b/src/easyscience/fitting/fitter.py index 861343ef..ef6db4ab 100644 --- a/src/easyscience/fitting/fitter.py +++ b/src/easyscience/fitting/fitter.py @@ -484,13 +484,6 @@ def mcmc_sample( if not isinstance(thin, int) or thin < 1: raise ValueError('thin must be a positive integer.') - minimizer = self.minimizer - if not (hasattr(minimizer, 'package') and minimizer.package == 'bumps'): - raise RuntimeError( - 'Bayesian sampling requires a BUMPS minimizer. ' - 'Use ``fitter.switch_minimizer(AvailableMinimizers.Bumps)`` first.' - ) - x_fit, x_new, y_new, w_new, dims = self._precompute_reshaping(x, y, weights, vectorized) self._dependent_dims = dims @@ -498,6 +491,13 @@ def mcmc_sample( self.fit_function = self._fit_function_wrapper(x_new, flatten=True) try: + minimizer = self.minimizer + if not (hasattr(minimizer, 'package') and minimizer.package == 'bumps'): + raise RuntimeError( + 'Bayesian sampling requires a BUMPS minimizer. ' + 'Use ``fitter.switch_minimizer(AvailableMinimizers.Bumps)`` first.' + ) + result = minimizer.mcmc_sample( x=x_fit, y=y_new,