diff --git a/pixi.lock b/pixi.lock index eb033db0..0c633805 100644 --- a/pixi.lock +++ b/pixi.lock @@ -1,13 +1,13 @@ version: 7 platforms: -- name: p1 +- name: linux-64-glibc-2-35 subdir: linux-64 virtual-packages: - __glibc=2.35 - __unix=0=0 - __linux=4.18 - __archspec=0=x86_64 -- name: p2 +- name: osx-arm64-macos-14-0 subdir: osx-arm64 virtual-packages: - __osx=14.0 @@ -25,7 +25,7 @@ environments: indexes: - https://pypi.org/simple packages: - p1: + linux-64-glibc-2-35: - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/argon2-cffi-bindings-25.1.0-py314h5bd0f2a_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_9.conda @@ -298,7 +298,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/fe/3b/8ec5074bcfc450fe84273713b4b0a0dd47c0249358f5d82eb8104ffe2520/multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/fe/d7/a71b12b625c44cfcb95d41397752edccc0a731903081609cffd6720f671d/asteval-1.0.9-py3-none-any.whl - p2: + osx-arm64-macos-14-0: - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_3.conda - conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.14.2-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/appnope-0.1.4-pyhd8ed1ab_1.conda @@ -832,7 +832,7 @@ environments: indexes: - https://pypi.org/simple packages: - p1: + linux-64-glibc-2-35: - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/argon2-cffi-bindings-25.1.0-py311h49ec1c0_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_9.conda @@ -1103,7 +1103,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/fe/d7/a71b12b625c44cfcb95d41397752edccc0a731903081609cffd6720f671d/asteval-1.0.9-py3-none-any.whl - p2: + osx-arm64-macos-14-0: - conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.14.2-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/appnope-0.1.4-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/argon2-cffi-25.1.0-pyhd8ed1ab_0.conda @@ -1628,7 +1628,7 @@ environments: indexes: - https://pypi.org/simple packages: - p1: + linux-64-glibc-2-35: - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/argon2-cffi-bindings-25.1.0-py314h5bd0f2a_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_9.conda @@ -1901,7 +1901,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/fe/3b/8ec5074bcfc450fe84273713b4b0a0dd47c0249358f5d82eb8104ffe2520/multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/fe/d7/a71b12b625c44cfcb95d41397752edccc0a731903081609cffd6720f671d/asteval-1.0.9-py3-none-any.whl - p2: + osx-arm64-macos-14-0: - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_3.conda - conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.14.2-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/appnope-0.1.4-pyhd8ed1ab_1.conda @@ -2435,7 +2435,7 @@ environments: indexes: - https://pypi.org/simple packages: - p1: + linux-64-glibc-2-35: - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/argon2-cffi-bindings-25.1.0-py314h5bd0f2a_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_9.conda @@ -2598,7 +2598,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/fe/3b/8ec5074bcfc450fe84273713b4b0a0dd47c0249358f5d82eb8104ffe2520/multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/fe/d7/a71b12b625c44cfcb95d41397752edccc0a731903081609cffd6720f671d/asteval-1.0.9-py3-none-any.whl - p2: + osx-arm64-macos-14-0: - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_3.conda - conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.14.2-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/appnope-0.1.4-pyhd8ed1ab_1.conda diff --git a/pixi.toml b/pixi.toml index a3237349..4873d04f 100644 --- a/pixi.toml +++ b/pixi.toml @@ -3,14 +3,13 @@ ########### [workspace] - # Supported platforms for the lock file (pixi.lock) platforms = [ 'win-64', # Set minimum supported version for glibc to be 2.35 to ensure packages # like `crysfml` that only have wheels for glibc 2.35+ # (manylinux_2_35_x86_64) are used. - #libc = { family = 'glibc', version = '2.35' } + # libc = { family = 'glibc', version = '2.35' } { platform = 'linux-64', glibc = '2.35' }, # Set minimum supported version for macOS to be 14.0 to ensure packages # like `scipp` that only have wheels for macOS 14.0+ (macosx_14_0_arm64) @@ -44,8 +43,8 @@ python = '3.14.*' # editable installations. [feature.dev.dependencies] -nodejs = '*' # Required for Prettier (non-Python formatting) -pixi-kernel = '*' # Pixi Jupyter kernel +nodejs = '*' # Required for Prettier (non-Python formatting) +pixi-kernel = '*' # Pixi Jupyter kernel [feature.dev.pypi-dependencies] pip = '*' @@ -55,7 +54,7 @@ easyscience = { path = '.', editable = true, extras = ['dev'] } # of editable installations. [feature.user.dependencies] -pixi-kernel = '*' # Pixi Jupyter kernel +pixi-kernel = '*' # Pixi Jupyter kernel [feature.user.pypi-dependencies] pip = '*' @@ -66,7 +65,6 @@ easyscience = '*' ############## [environments] - # The `default` feature is always included in all environments. # Additional features can be specified per environment. @@ -87,7 +85,6 @@ user = { features = ['py-max', 'user'] } ####### [tasks] - ################## # ๐Ÿงช Testing Tasks ################## @@ -172,9 +169,9 @@ notebook-tweak = 'python tools/tweak_notebooks.py docs/docs/tutorials/' notebook-exec = 'python -m pytest --nbmake docs/docs/tutorials/ --nbmake-timeout=1200 --overwrite --color=yes -n auto -v' notebook-prepare = { depends-on = [ - #'notebook-convert', + # 'notebook-convert', 'notebook-strip', - #'notebook-tweak', + # 'notebook-tweak', ] } ######################## @@ -266,7 +263,7 @@ clean-pycache = "find . -type d -name '__pycache__' -prune -exec rm -rf '{}' +" post-install = { depends-on = [ 'npm-config', 'prettier-install', - #'pre-commit-setup', + # 'pre-commit-setup', ] } ########################## diff --git a/prettierrc.toml b/prettierrc.toml index b98c86eb..d8a956f5 100644 --- a/prettierrc.toml +++ b/prettierrc.toml @@ -1,22 +1,24 @@ plugins = [ - "prettier-plugin-toml", # use the TOML plugin + 'prettier-plugin-toml', # use the TOML plugin ] -endOfLine = 'lf' # change line endings to LF -proseWrap = 'always' # change wrapping in Markdown files -semi = false # remove semicolons -singleQuote = true # use single quotes instead of double quotes -tabWidth = 2 # change tab width to 2 spaces -useTabs = false # use spaces instead of tabs +endOfLine = 'lf' # change line endings to LF +proseWrap = 'always' # change wrapping in Markdown files +semi = false # remove semicolons +singleQuote = true # use single quotes instead of double quotes +tabWidth = 2 # change tab width to 2 spaces +useTabs = false # use spaces instead of tabs -printWidth = 79 # wrap lines at 79 characters +printWidth = 79 # wrap lines at 79 characters [[overrides]] -files = ["*.md"] +files = ['*.md'] + [overrides.options] -printWidth = 72 # wrap Markdown files at 72 characters +printWidth = 72 # wrap Markdown files at 72 characters [[overrides]] -files = ["*.yml", "*.yaml"] +files = ['*.yml', '*.yaml'] + [overrides.options] -printWidth = 88 # wrap YAML files at 88 characters +printWidth = 88 # wrap YAML files at 88 characters diff --git a/pyproject.toml b/pyproject.toml index 89d28050..aa57296e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ [project] name = 'easyscience' -dynamic = ['version'] # Use versioningit to manage the version +dynamic = ['version'] # Use versioningit to manage the version description = 'Core building blocks for EasyScience' authors = [{ name = 'EasyScience contributors' }] readme = 'README.md' @@ -24,54 +24,54 @@ classifiers = [ ] requires-python = '>=3.11' dependencies = [ - 'asteval', # Safely evaluate Python expressions from strings - 'lmfit', # Non-linear least squares fitting - 'bumps>=1.0.4', # Bayesian uncertainty estimation - 'dfo-ls', # Derivative-free optimization - 'numpy', # Numerical computing - 'scipp', # Handling and analysis of scientific data + 'asteval', # Safely evaluate Python expressions from strings + 'lmfit', # Non-linear least squares fitting + 'bumps>=1.0.4', # Bayesian uncertainty estimation + 'dfo-ls', # Derivative-free optimization + 'numpy', # Numerical computing + 'scipp', # Handling and analysis of scientific data ] [project.optional-dependencies] dev = [ - 'pooch', # Data downloader - 'matplotlib', # Plotting library - 'jupyterlab', # Jupyter notebooks - 'ipykernel', # Jupyter kernel (required for running notebooks) - 'ipywidgets', # Widgets (needed for interactive matplotlib backends) - 'ipympl', # Matplotlib Jupyter widget backend (%matplotlib widget) - 'IPython', # Interactive Python shell - 'GitPython', # Interact with Git repositories - 'build', # Building the package - 'pre-commit', # Pre-commit hooks - 'jinja2', # Templating - 'nbmake', # Building notebooks - 'nbstripout', # Strip output from notebooks - 'nbqa', # Linting and formatting notebooks - 'pytest', # Testing - 'pytest-cov', # Test coverage - 'pytest-xdist', # Enable parallel testing - 'ruff', # Linting and formatting code - 'radon', # Code complexity and maintainability - 'validate-pyproject[all]', # Validate pyproject.toml - 'versioningit', # Automatic versioning from git tags - 'jupytext', # Jupyter notebook text format support - 'jupyterquiz', # Quizzes in Jupyter notebooks - 'pydoclint', # Docstring linter - 'format-docstring', # Docstring formatter - 'docstripy', # Convert docstrings to other formats - 'interrogate', # Docstring coverage checker - 'copier', # Template management - 'mike', # MkDocs: Versioned documentation support - 'mkdocs', # Static site generator - 'mkdocs-material', # Documentation framework on top of MkDocs - 'mkdocs-autorefs', # MkDocs: Auto-references support - 'mkdocs-jupyter', # MkDocs: Jupyter notebook support - 'mkdocs-plugin-inline-svg', # MkDocs: Inline SVG support - 'mkdocs-markdownextradata-plugin', # MkDocs: Markdown extra data support, such as global variables - 'mkdocstrings-python', # MkDocs: Python docstring support - 'pyyaml', # YAML parser - 'spdx-headers', # SPDX license header validation + 'pooch', # Data downloader + 'matplotlib', # Plotting library + 'jupyterlab', # Jupyter notebooks + 'ipykernel', # Jupyter kernel (required for running notebooks) + 'ipywidgets', # Widgets (needed for interactive matplotlib backends) + 'ipympl', # Matplotlib Jupyter widget backend (%matplotlib widget) + 'IPython', # Interactive Python shell + 'GitPython', # Interact with Git repositories + 'build', # Building the package + 'pre-commit', # Pre-commit hooks + 'jinja2', # Templating + 'nbmake', # Building notebooks + 'nbstripout', # Strip output from notebooks + 'nbqa', # Linting and formatting notebooks + 'pytest', # Testing + 'pytest-cov', # Test coverage + 'pytest-xdist', # Enable parallel testing + 'ruff', # Linting and formatting code + 'radon', # Code complexity and maintainability + 'validate-pyproject[all]', # Validate pyproject.toml + 'versioningit', # Automatic versioning from git tags + 'jupytext', # Jupyter notebook text format support + 'jupyterquiz', # Quizzes in Jupyter notebooks + 'pydoclint', # Docstring linter + 'format-docstring', # Docstring formatter + 'docstripy', # Convert docstrings to other formats + 'interrogate', # Docstring coverage checker + 'copier', # Template management + 'mike', # MkDocs: Versioned documentation support + 'mkdocs', # Static site generator + 'mkdocs-material', # Documentation framework on top of MkDocs + 'mkdocs-autorefs', # MkDocs: Auto-references support + 'mkdocs-jupyter', # MkDocs: Jupyter notebook support + 'mkdocs-plugin-inline-svg', # MkDocs: Inline SVG support + 'mkdocs-markdownextradata-plugin', # MkDocs: Markdown extra data support, such as global variables + 'mkdocstrings-python', # MkDocs: Python docstring support + 'pyyaml', # YAML parser + 'spdx-headers', # SPDX license header validation ] [project.urls] @@ -102,7 +102,7 @@ packages = ['src/easyscience'] allow-direct-references = true [tool.hatch.version] -source = 'versioningit' # Use versioningit to manage the version +source = 'versioningit' # Use versioningit to manage the version ################################ # Configuration for versioningit @@ -118,9 +118,9 @@ source = 'versioningit' # Use versioningit to manage the version # pixi.lock update without any changes to the source code. [tool.versioningit.format] -distance = '{base_version}+dev{distance}' # example: 1.2.3.post4+dev3 -dirty = '{base_version}+dirty{distance}' # example: 0.5.8+dirty3 -distance-dirty = '{base_version}+devdirty{distance}' # example: 0.5.8+devdirty3 +distance = '{base_version}+dev{distance}' # example: 1.2.3.post4+dev3 +dirty = '{base_version}+dirty{distance}' # example: 0.5.8+dirty3 +distance-dirty = '{base_version}+devdirty{distance}' # example: 0.5.8+devdirty3 # Configure how versioningit detects versions from Git # - 'match' ensures it only considers tags starting with 'v' @@ -138,7 +138,7 @@ default-tag = 'v999.0.0' # https://interrogate.readthedocs.io/en/latest/ [tool.interrogate] -fail-under = 45 # Minimum docstring coverage percentage to pass +fail-under = 45 # Minimum docstring coverage percentage to pass verbose = 1 exclude = ['src/easyscience/legacy'] @@ -150,13 +150,13 @@ exclude = ['src/easyscience/legacy'] # https://coverage.readthedocs.io/en/latest/ [tool.coverage.run] -branch = true # Measure branch coverage as well -source = ['src'] # Limit coverage to the source code directory +branch = true # Measure branch coverage as well +source = ['src'] # Limit coverage to the source code directory [tool.coverage.report] show_missing = true # Show missing lines -skip_covered = false # Skip files with 100% coverage in the report -fail_under = 80 # Minimum coverage percentage to pass +skip_covered = false # Skip files with 100% coverage in the report +fail_under = 80 # Minimum coverage percentage to pass ########################## # Configuration for pytest @@ -180,31 +180,31 @@ testpaths = ['tests'] [tool.ruff] exclude = ['tests', 'legacy', 'src/easyscience/legacy'] indent-width = 4 -line-length = 99 # See also `max-line-length` in [tool.ruff.lint.pycodestyle] -preview = true # Enable new rules that are not yet stable, like DOC +line-length = 99 # See also `max-line-length` in [tool.ruff.lint.pycodestyle] +preview = true # Enable new rules that are not yet stable, like DOC builtins = [ 'display', -] # Clutch-fix/patch to https://github.com/nbQA-dev/nbQA/issues/882 +] # Clutch-fix/patch to https://github.com/nbQA-dev/nbQA/issues/882 # Formatting options for Ruff [tool.ruff.format] -docstring-code-format = true # Whether to format code snippets in docstrings -docstring-code-line-length = 72 # Line length for code snippets in docstrings -indent-style = 'space' # PEP 8 recommends using spaces over tabs -quote-style = 'single' # But double quotes in docstrings (PEP 8, PEP 257) +docstring-code-format = true # Whether to format code snippets in docstrings +docstring-code-line-length = 72 # Line length for code snippets in docstrings +indent-style = 'space' # PEP 8 recommends using spaces over tabs +quote-style = 'single' # But double quotes in docstrings (PEP 8, PEP 257) # Linting rules to use with Ruff [tool.ruff.lint] select = [ - # 'E', # General PEP 8 style errors - 'E9', # Runtime errors (e.g., syntax errors, undefined names) - # 'F', # Pyflakes-specific checks (e.g., unused variables, imports) - 'F63', # Issues related to invalid escape sequences in strings + # 'E', # General PEP 8 style errors + 'E9', # Runtime errors (e.g., syntax errors, undefined names) + # 'F', # Pyflakes-specific checks (e.g., unused variables, imports) + 'F63', # Issues related to invalid escape sequences in strings 'F7', # Type-related errors (e.g., mismatched argument types, unsupported operations) - 'F82', # Import-related errors (e.g., unresolved imports, reimported modules) - 'I', # Import sorting issues (e.g., unsorted imports) - # 'S', # Security-related issues (e.g., use of insecure functions or libraries) + 'F82', # Import-related errors (e.g., unresolved imports, reimported modules) + 'I', # Import sorting issues (e.g., unsorted imports) + # 'S', # Security-related issues (e.g., use of insecure functions or libraries) # 'W', # General PEP 8 warnings (e.g., lines too long, trailing whitespace) ] @@ -212,31 +212,31 @@ select = [ # Ignore specific rules globally ignore = [ - 'COM812', # https://docs.astral.sh/ruff/rules/missing-trailing-comma/ + 'COM812', # https://docs.astral.sh/ruff/rules/missing-trailing-comma/ # Replaced by 'D' plus pydocstyle and pydoclint - 'DOC', # https://docs.astral.sh/ruff/rules/#pydoclint-doc + '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/ + 'D200', # https://docs.astral.sh/ruff/rules/unnecessary-multiline-docstring/ ] # Ignore specific rules in certain files or directories [tool.ruff.lint.per-file-ignores] '*/__init__.py' = [ - 'F401', # re-exports are intentional in __init__.py + 'F401', # re-exports are intentional in __init__.py ] 'tests/**' = [ - 'ANN', # https://docs.astral.sh/ruff/rules/#flake8-annotations-ann - 'D', # https://docs.astral.sh/ruff/rules/#pydocstyle-d - 'DOC', # https://docs.astral.sh/ruff/rules/#pydoclint-doc - 'INP001', # https://docs.astral.sh/ruff/rules/implicit-namespace-package/ - 'S101', # https://docs.astral.sh/ruff/rules/assert/ + 'ANN', # https://docs.astral.sh/ruff/rules/#flake8-annotations-ann + 'D', # https://docs.astral.sh/ruff/rules/#pydocstyle-d + 'DOC', # https://docs.astral.sh/ruff/rules/#pydoclint-doc + 'INP001', # https://docs.astral.sh/ruff/rules/implicit-namespace-package/ + 'S101', # https://docs.astral.sh/ruff/rules/assert/ ] 'docs/**' = [ - 'INP001', # https://docs.astral.sh/ruff/rules/implicit-namespace-package/ - 'T201', # https://docs.astral.sh/ruff/rules/print/ + 'INP001', # https://docs.astral.sh/ruff/rules/implicit-namespace-package/ + 'T201', # https://docs.astral.sh/ruff/rules/print/ ] 'docs/docs/tutorials/**' = [ - 'E402', # https://docs.astral.sh/ruff/rules/module-import-not-at-top-of-file/ + 'E402', # https://docs.astral.sh/ruff/rules/module-import-not-at-top-of-file/ ] # Specific options for certain rules @@ -258,7 +258,7 @@ max-complexity = 10 # https://peps.python.org/pep-0008/#maximum-line-length # Use 99 characters as the project-wide maximum for regular code lines. # Use 72 characters for docstrings. -max-line-length = 99 # See also `line-length` in [tool.ruff] +max-line-length = 99 # See also `line-length` in [tool.ruff] max-doc-length = 72 [tool.ruff.lint.pydocstyle] diff --git a/src/easyscience/fitting/minimizers/minimizer_bumps.py b/src/easyscience/fitting/minimizers/minimizer_bumps.py index 315d856c..500edce6 100644 --- a/src/easyscience/fitting/minimizers/minimizer_bumps.py +++ b/src/easyscience/fitting/minimizers/minimizer_bumps.py @@ -417,42 +417,45 @@ def mcmc_sample( weights : np.ndarray Flattened weight array. samples : int, default=10000 - Number of raw samples to draw across all chains, before thinning. - A guaranteed minimum, not an exact count: DREAM advances in - blocks of 10 generations (one generation = one draw per chain) - and stops at the first block boundary at or past ``samples``. + Number of raw samples to draw across all chains, before + thinning. A guaranteed minimum, not an exact count: DREAM + advances in blocks of 10 generations (one generation = one + draw per chain) and stops at the first block boundary at or + past ``samples``. burn : int, default=2000 Burn-in generations to discard. BUMPS counts ``burn`` in - generations while ``samples`` counts raw draws, so ``burn=500`` - discards ``500 * n_chains`` raw samples. + generations while ``samples`` counts raw draws, so + ``burn=500`` discards ``500 * n_chains`` raw samples. thin : int, default=10 - Thinning interval โ€” only every ``thin``-th generation is stored. + Thinning interval โ€” only every ``thin``-th generation is + stored. population : int | None, default=None - BUMPS DREAM population count per parameter (number of parallel - chains): BUMPS creates ``ceil(population * n_parameters)`` chains. + BUMPS DREAM population count per parameter (number of + parallel chains): BUMPS creates ``ceil(population * + n_parameters)`` chains. resume_state : MCMCDraw | None, default=None A BUMPS ``MCMCDraw`` state object from a previous - ``mcmc_sample()`` call (e.g. ``PosteriorResults.sampler_state``). - When provided, DREAM **continues** the saved chain instead of - starting cold. The population, parameter count, and parameter - names must match the current model โ€” a ``ValueError`` is raised - otherwise. - - ``samples`` must be the **total** number of raw samples, not an - increment: to extend an existing chain of ``N`` raw samples by - ``M``, pass ``samples=N + M`` (DREAM keeps only the last - ``samples`` draws in its buffer). The `Sampler.extend` helper - computes this for you. - - ``burn`` is forced to 0 on resume: a previously-converged chain is - never re-burned. - - The ``population`` and ``initializer`` parameters - have **no effect** when ``resume_state`` is provided โ€” they - are determined by the saved state. - - Resuming against *different* data is undefined behaviour (the - chain's likelihood changes underneath it). + ``mcmc_sample()`` call (e.g. + ``PosteriorResults.sampler_state``). When provided, DREAM + **continues** the saved chain instead of starting cold. The + population, parameter count, and parameter names must match + the current model โ€” a ``ValueError`` is raised otherwise. + + ``samples`` must be the **total** number of raw samples, not + an increment: to extend an existing chain of ``N`` raw + samples by ``M``, pass ``samples=N + M`` (DREAM keeps only + the last ``samples`` draws in its buffer). The + ``Sampler.extend`` helper computes this for you. + + ``burn`` is forced to 0 on resume: a previously-converged + chain is never re-burned. + + The ``population`` and ``initializer`` parameters have **no + effect** when ``resume_state`` is provided โ€” they are + determined by the saved state. + + Resuming against *different* data is undefined behaviour + (the chain's likelihood changes underneath it). sampler_kwargs : dict | None, default=None Additional keyword arguments forwarded to ``bumps.fitters.fit``. @@ -475,9 +478,9 @@ def mcmc_sample( ------ ValueError If the input shapes or weights are invalid, if - ``progress_callback`` is not callable, or if ``resume_state`` - is incompatible with the current model (parameter count, - names/order, or population mismatch). + ``progress_callback`` is not callable, or if + ``resume_state`` is incompatible with the current model + (parameter count, names/order, or population mismatch). FitError If DREAM sampling was aborted by the user (via ``abort_test``). @@ -608,28 +611,32 @@ def _validate_resume_state( population: int | None, burn: int, ) -> tuple[int, int]: - """Check that ``resume_state`` is compatible with ``problem`` and + """ + Check that ``resume_state`` is compatible with ``problem`` and resolve the population and burn values to use when resuming. Parameters ---------- problem : FitProblem - The freshly built BUMPS ``FitProblem`` for the current model. + The freshly built BUMPS ``FitProblem`` for the current + model. resume_state : MCMCDraw The saved chain state to resume from. population : int | None The caller-supplied population scale factor, or ``None``. burn : int - The caller-supplied burn-in, ignored (with a warning) on resume. + The caller-supplied burn-in, ignored (with a warning) on + resume. Returns ------- tuple[int, int] ``(population, burn)`` to pass to DREAM. The population is returned as a **negative** number, which BUMPS' - ``initpop.generate`` reads as an absolute chain count, exactly - reproducing the saved state's population. ``burn`` is always 0: - a previously converged chain is never re-burned. + ``initpop.generate`` reads as an absolute chain count, + exactly reproducing the saved state's population. ``burn`` + is always 0: a previously converged chain is never + re-burned. Raises ------ diff --git a/src/easyscience/fitting/sampler.py b/src/easyscience/fitting/sampler.py index d3915115..3917a5bc 100644 --- a/src/easyscience/fitting/sampler.py +++ b/src/easyscience/fitting/sampler.py @@ -1,6 +1,8 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -"""Bayesian MCMC sampling โ€” the ``Sampler`` class and persistence helpers.""" +""" +Bayesian MCMC sampling โ€” the ``Sampler`` class and persistence helpers. +""" from __future__ import annotations @@ -38,7 +40,9 @@ def _data_fingerprint( y_list: list, w_list: list, ) -> str | None: - """Return a SHA-256 hex digest of concatenated (x|y|weights), or None.""" + """ + Return a SHA-256 hex digest of concatenated (x|y|weights), or None. + """ try: h = hashlib.sha256() for arr in list(x_list) + list(y_list) + list(w_list): @@ -53,7 +57,8 @@ def _validate_dataset_arrays( data: np.ndarray | list | tuple, allow_none_entries: bool = False, ) -> None: - """Check that ``data`` (an array or list of arrays) holds numeric, + """ + Check that ``data`` (an array or list of arrays) holds numeric, at-least-1-D, non-empty arrays. Structural checks (array vs list, matching dataset counts) are in @@ -101,12 +106,13 @@ def _validate_dataset_arrays( def _copy_data(data): - """Copy an array (or list of arrays) and mark the copies read-only. + """ + Copy an array (or list of arrays) and mark the copies read-only. - The ``Sampler`` binds its data at construction; copying decouples the - bound data from the caller's arrays, and the read-only flag stops - in-place mutation of the copies, so the chain and the ``save()`` - fingerprint always describe the data actually sampled. + The ``Sampler`` binds its data at construction; copying decouples + the bound data from the caller's arrays, and the read-only flag + stops in-place mutation of the copies, so the chain and the + ``save()`` fingerprint always describe the data actually sampled. """ if data is None: return None @@ -118,10 +124,13 @@ def _copy_data(data): def _validate_chain_path(path: str | os.PathLike, skip: int = 0) -> str: - """Validate the persistence arguments shared by the save/load functions. + """ + Validate the persistence arguments shared by the save/load + functions. - Returns ``path`` coerced to ``str``. Raises ``TypeError`` if ``path`` is - not path-like and ``ValueError`` if ``skip`` is not a non-negative int. + Returns ``path`` coerced to ``str``. Raises ``TypeError`` if + ``path`` is not path-like and ``ValueError`` if ``skip`` is not a + non-negative int. """ if not isinstance(path, (str, os.PathLike)): raise TypeError(f'path must be a str or os.PathLike, got {type(path).__name__}.') @@ -131,14 +140,15 @@ def _validate_chain_path(path: str | os.PathLike, skip: int = 0) -> str: def _load_bumps_state(path: str, skip: int = 0) -> MCMCDraw: - """Read a BUMPS chain from disk, working around a BUMPS bug. + """ + Read a BUMPS chain from disk, working around a BUMPS bug. ``bumps.dream.state.load_state`` reads the saved buffers with - ``numpy.loadtxt``, which collapses a single-row file to a 1-D array. A - short chain stores a single CR-weight update row, so ``load_state``'s - subsequent ``stats[:, 0]`` indexing raises ``IndexError: too many indices - for array``. We coerce each buffer read back to 2-D before ``load_state`` - consumes it. + ``numpy.loadtxt``, which collapses a single-row file to a 1-D array. + A short chain stores a single CR-weight update row, so + ``load_state``'s subsequent ``stats[:, 0]`` indexing raises + ``IndexError: too many indices for array``. We coerce each buffer + read back to 2-D before ``load_state`` consumes it. Parameters ---------- @@ -170,28 +180,30 @@ def _loadtxt_2d(file, report: int = 0) -> np.ndarray: def load_chain(path: str | os.PathLike, skip: int = 0) -> tuple[MCMCDraw, list[str] | None, dict]: - """Reload a DREAM chain state saved by ``Sampler.save``. + """ + Reload a DREAM chain state saved by ``Sampler.save``. - This is the standalone reader: unlike ``Sampler.load_state`` it needs no - fitter, model or data, so a saved chain can be inspected or post-processed - on a machine that does not have the model. Parameter names are restored - from the sidecar when available (schema versions 1 and 2), falling back to - the state's labels with the minimizer prefix stripped. + This is the standalone reader: unlike ``Sampler.load_state`` it + needs no fitter, model or data, so a saved chain can be inspected or + post-processed on a machine that does not have the model. Parameter + names are restored from the sidecar when available (schema versions + 1 and 2), falling back to the state's labels with the minimizer + prefix stripped. Parameters ---------- path : str | os.PathLike File path prefix used when saving. skip : int, default=0 - Discard the first ``skip`` saved generations on load, forwarded to - ``bumps.dream.state.load_state``. + Discard the first ``skip`` saved generations on load, forwarded + to ``bumps.dream.state.load_state``. Returns ------- tuple[MCMCDraw, list[str] | None, dict] - The reloaded BUMPS chain state, the parameter names (or ``None`` if - neither sidecar nor labels yielded them), and the raw sidecar dict - (empty if absent/unreadable). + The reloaded BUMPS chain state, the parameter names (or ``None`` + if neither sidecar nor labels yielded them), and the raw sidecar + dict (empty if absent/unreadable). Raises ------ @@ -229,15 +241,18 @@ def load_chain(path: str | os.PathLike, skip: int = 0) -> tuple[MCMCDraw, list[s @dataclass class SamplingResults: - """Structured result of an MCMC sampling run (analogous to ``FitResults``). + """ + Structured result of an MCMC sampling run (analogous to + ``FitResults``). Attributes ---------- draws : np.ndarray - Posterior samples, shape ``(n_samples, n_params)``. For results from - ``Sampler.sample()``/``Sampler.extend()`` this is a *trimmed, - outlier-filtered* view of the chain, not the raw buffer, so it holds - fewer rows than ``samples / thin`` โ€” see the notes on ``Sampler``. + Posterior samples, shape ``(n_samples, n_params)``. For results + from ``Sampler.sample()``/``Sampler.extend()`` this is a + *trimmed, outlier-filtered* view of the chain, not the raw + buffer, so it holds fewer rows than ``samples / thin`` โ€” see the + notes on ``Sampler``. param_names : list[str] Parameter names (one per column of ``draws``). logp : np.ndarray @@ -252,8 +267,10 @@ class SamplingResults: state: MCMCDraw def to_legacy_dict(self) -> dict: - """Return the legacy dict shape produced by the deprecated - ``mcmc_sample()`` APIs.""" + """ + Return the legacy dict shape produced by the deprecated + ``mcmc_sample()`` APIs. + """ return { 'draws': self.draws, 'param_names': self.param_names, @@ -263,58 +280,64 @@ def to_legacy_dict(self) -> dict: class Sampler: - """Bayesian MCMC sampler for one dataset, backed by a Fitter's BUMPS minimizer. - - One ``Sampler`` instance represents one chain over one ``(x, y, weights)`` - dataset. The data is bound at construction; ``sample()`` and ``extend()`` - take no data arguments, so a chain can never be extended against different - data (undefined behaviour in BUMPS). The bound data is a defensive, - read-only copy of the caller's arrays, exposed via the ``x``, ``y`` and - ``weights`` properties โ€” mutating the originals after construction has no - effect on the sampler, and there are deliberately no setters: to sample - different data, create a new ``Sampler``. - - Construct directly with a configured ``Fitter`` (or ``MultiFitter``) whose - minimizer has been switched to ``AvailableMinimizers.Bumps``. **Running a - fit first is not required** โ€” the ``Fitter`` supplies the model and the - minimizer, not a fit result, and sampling from the initial parameter values - works fine. - - It is often worth fitting first anyway. DREAM seeds its whole starting - population inside a tiny ball around the parameters' *current* values - (BUMPS' default ``init='eps'``), so sampling from fitted values starts the - chain in the right region and shortens the burn-in needed to reach the - typical set. From a poor initial guess, expect to burn for longer. - - The sampler is BUMPS/DREAM-specific for now: the BUMPS check in ``_run()`` - is the seam where another backend would plug in. + """ + Bayesian MCMC sampler for one dataset, backed by a Fitter's BUMPS + minimizer. + + One ``Sampler`` instance represents one chain over one ``(x, y, + weights)`` dataset. The data is bound at construction; ``sample()`` + and ``extend()`` take no data arguments, so a chain can never be + extended against different data (undefined behaviour in BUMPS). The + bound data is a defensive, read-only copy of the caller's arrays, + exposed via the ``x``, ``y`` and ``weights`` properties โ€” mutating + the originals after construction has no effect on the sampler, and + there are deliberately no setters: to sample different data, create + a new ``Sampler``. + + Construct directly with a configured ``Fitter`` (or ``MultiFitter``) + whose minimizer has been switched to ``AvailableMinimizers.Bumps``. + **Running a fit first is not required** โ€” the ``Fitter`` supplies + the model and the minimizer, not a fit result, and sampling from the + initial parameter values works fine. + + It is often worth fitting first anyway. DREAM seeds its whole + starting population inside a tiny ball around the parameters' + *current* values (BUMPS' default ``init='eps'``), so sampling from + fitted values starts the chain in the right region and shortens the + burn-in needed to reach the typical set. From a poor initial guess, + expect to burn for longer. + + The sampler is BUMPS/DREAM-specific for now: the BUMPS check in + ``_run()`` is the seam where another backend would plug in. Parameters ---------- - fitter : Fitter - A configured ``Fitter`` (or ``MultiFitter``) whose minimizer has been - switched to ``AvailableMinimizers.Bumps``. + fitter : 'Fitter' + A configured ``Fitter`` (or ``MultiFitter``) whose minimizer has + been switched to ``AvailableMinimizers.Bumps``. x : np.ndarray | list[np.ndarray] - Independent variable array (or list of arrays for ``MultiFitter``). + Independent variable array (or list of arrays for + ``MultiFitter``). y : np.ndarray | list[np.ndarray] - Dependent variable array (or list of arrays for ``MultiFitter``). + Dependent variable array (or list of arrays for + ``MultiFitter``). weights : np.ndarray | list[np.ndarray | None] | None, default=None Weight array (or list of arrays for ``MultiFitter``). 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. sampler_kwargs : dict | None, default=None - Per-instance default keyword arguments forwarded to the BUMPS DREAM - sampler on every run, merged with (and overridden by) per-call - ``sampler_kwargs``. + Per-instance default keyword arguments forwarded to the BUMPS + DREAM sampler on every run, merged with (and overridden by) + per-call ``sampler_kwargs``. Raises ------ TypeError - If ``fitter`` is not Fitter-shaped (no ``minimizer``/``fit_function``), - if any dataset in ``x``/``y``/``weights`` is not a numeric array - (e.g. a string), or ``vectorized``/``sampler_kwargs`` have the wrong - type. + If ``fitter`` is not Fitter-shaped (no + ``minimizer``/``fit_function``), if any dataset in + ``x``/``y``/``weights`` is not a numeric array (e.g. a string), + or ``vectorized``/``sampler_kwargs`` have the wrong type. ValueError If ``x``, ``y`` and ``weights`` do not hold matching structures (all arrays, or lists of the same length), or any dataset is a @@ -322,42 +345,44 @@ class Sampler: Notes ----- - **The retained draws are a trimmed view, not the whole chain.** BUMPS' - DREAM sampler defaults to ``trim=True``: once sampling finishes it runs a - convergence-based burn-point detector over the chain and returns only the - portion after that point. ``state.draw()`` additionally drops chains - flagged as outliers. So ``results.draws`` is usually *smaller* than the - chain, and its length is not deterministic โ€” the detector re-runs from - scratch on every ``sample()`` and ``extend()`` call and may place the burn - point differently each time. Read the count off the array; do not predict - it. - - Nor is the chain itself exactly ``samples / thin`` rows: ``samples`` is a - guaranteed minimum, not an exact count. DREAM advances in blocks of 10 - generations (one generation = one draw per chain) and only checks its - stopping condition between blocks, so the raw chain length is ``samples`` - rounded up to a multiple of ``10 * n_chains``. - - The trimming is only a *view* โ€” nothing is dropped from the buffer. To - read the full untrimmed chain:: + **The retained draws are a trimmed view, not the whole chain.** + BUMPS' DREAM sampler defaults to ``trim=True``: once sampling + finishes it runs a convergence-based burn-point detector over the + chain and returns only the portion after that point. + ``state.draw()`` additionally drops chains flagged as outliers. So + ``results.draws`` is usually *smaller* than the chain, and its + length is not deterministic โ€” the detector re-runs from scratch on + every ``sample()`` and ``extend()`` call and may place the burn + point differently each time. Read the count off the array; do not + predict it. + + Nor is the chain itself exactly ``samples / thin`` rows: ``samples`` + is a guaranteed minimum, not an exact count. DREAM advances in + blocks of 10 generations (one generation = one draw per chain) and + only checks its stopping condition between blocks, so the raw chain + length is ``samples`` rounded up to a multiple of ``10 * n_chains``. + + The trimming is only a *view* โ€” nothing is dropped from the buffer. + To read the full untrimmed chain:: results = sampler.sample(samples=10000, burn=500, thin=2) full = results.state.draw(portion=1.0, outliers=True) full.points # (n_chain_rows, n_params) full.logp - To switch the automatic trimming off entirely, pass BUMPS' own ``trim`` - option straight through ``sampler_kwargs``; ``results.draws`` then holds - the whole chain:: + To switch the automatic trimming off entirely, pass BUMPS' own + ``trim`` option straight through ``sampler_kwargs``; + ``results.draws`` then holds the whole chain:: sampler = Sampler( fitter, x, y, weights=w, sampler_kwargs={'trim': False} ) - Note also that trimming does not survive a ``save()``/``load_state()`` - round-trip: BUMPS does not persist the trim point, so a reloaded chain - comes back untrimmed and ``load_state()`` reports *more* draws than the - ``sample()`` call that created it. The chain itself is identical. + Note also that trimming does not survive a + ``save()``/``load_state()`` round-trip: BUMPS does not persist the + trim point, so a reloaded chain comes back untrimmed and + ``load_state()`` reports *more* draws than the ``sample()`` call + that created it. The chain itself is identical. """ def __init__( @@ -435,7 +460,9 @@ def weights(self) -> np.ndarray | list[np.ndarray | None] | None: @property def state(self) -> MCMCDraw | None: - """Raw BUMPS MCMCDraw state (None before first sample/load_state).""" + """ + Raw BUMPS MCMCDraw state (None before first sample/load_state). + """ return self._state @property @@ -459,7 +486,9 @@ def logp(self) -> np.ndarray | None: return self._results.logp if self._results is not None else None def _fingerprint(self) -> str | None: - """SHA-256 fingerprint of the bound (x, y, weights) data, or None.""" + """ + SHA-256 fingerprint of the bound (x, y, weights) data, or None. + """ x_list = list(self._x) if isinstance(self._x, (list, tuple)) else [self._x] y_list = list(self._y) if isinstance(self._y, (list, tuple)) else [self._y] if self._weights is None: @@ -481,7 +510,8 @@ def _run( progress_callback: Callable[[dict], bool | None] | None, abort_test: Callable[[], bool] | None, ) -> SamplingResults: - """Shared sampling engine for ``sample()`` and ``extend()``. + """ + Shared sampling engine for ``sample()`` and ``extend()``. Argument validation for ``samples``/``burn``/``thin`` lives in ``Bumps.mcmc_sample`` (single source of truth). @@ -545,37 +575,43 @@ def sample( progress_callback: Callable[[dict], bool | None] | None = None, abort_test: Callable[[], bool] | None = None, ) -> SamplingResults: - """Run fresh Bayesian MCMC sampling on the bound data. + """ + Run fresh Bayesian MCMC sampling on the bound data. - Calling ``sample()`` on a sampler that already holds a chain starts a - **fresh** chain โ€” the previous state and results are replaced. Use - ``extend()`` to continue an existing chain. + Calling ``sample()`` on a sampler that already holds a chain + starts a **fresh** chain โ€” the previous state and results are + replaced. Use ``extend()`` to continue an existing chain. Parameters ---------- samples : int, default=10000 - Number of raw samples to draw across all chains, before thinning. - This is a guaranteed minimum, not an exact count: DREAM advances - in blocks of 10 generations (one generation = one draw per chain) - and stops at the first block boundary at or past ``samples``. + Number of raw samples to draw across all chains, before + thinning. This is a guaranteed minimum, not an exact count: + DREAM advances in blocks of 10 generations (one generation = + one draw per chain) and stops at the first block boundary at + or past ``samples``. burn : int, default=2000 - Burn-in generations to discard before collecting samples. Note - BUMPS counts ``burn`` in generations while ``samples`` counts raw - draws, so ``burn=500`` discards ``500 * n_chains`` raw samples. + Burn-in generations to discard before collecting samples. + Note BUMPS counts ``burn`` in generations while ``samples`` + counts raw draws, so ``burn=500`` discards ``500 * + n_chains`` raw samples. thin : int, default=10 - Thinning interval โ€” only every ``thin``-th generation is kept, - which reduces autocorrelation between consecutive draws. + Thinning interval โ€” only every ``thin``-th generation is + kept, which reduces autocorrelation between consecutive + draws. population : int | None, default=None - DREAM population **scale factor** (not an absolute chain count): - BUMPS creates ``ceil(population * n_parameters)`` parallel chains. + DREAM population **scale factor** (not an absolute chain + count): BUMPS creates ``ceil(population * n_parameters)`` + parallel chains. sampler_kwargs : dict | None, default=None Additional keyword arguments forwarded to the BUMPS DREAM sampler (merged over the instance defaults). progress_callback : Callable[[dict], bool | None] | None, default=None - Optional callback invoked at each DREAM generation. The payload - dict includes ``iteration`` and ``sampling: True``. + Optional callback invoked at each DREAM generation. The + payload dict includes ``iteration`` and ``sampling: True``. abort_test : Callable[[], bool] | None, default=None - Optional callable that returns ``True`` to abort sampling early. + Optional callable that returns ``True`` to abort sampling + early. Returns ------- @@ -584,14 +620,16 @@ def sample( Notes ----- - ``results.draws`` is a trimmed, outlier-filtered view of the chain, - so it is usually smaller than the chain and its length is not - predictable from ``samples``/``thin``. See the ``Sampler`` class notes - for how to read the full chain or switch trimming off. + ``results.draws`` is a trimmed, outlier-filtered view of the + chain, so it is usually smaller than the chain and its length is + not predictable from ``samples``/``thin``. See the ``Sampler`` + class notes for how to read the full chain or switch trimming + off. Exceptions propagate from the sampling engine: ``ValueError`` if - ``samples``, ``burn``, or ``thin`` are invalid, and ``RuntimeError`` - if the active minimizer is not a BUMPS instance. + ``samples``, ``burn``, or ``thin`` are invalid, and + ``RuntimeError`` if the active minimizer is not a BUMPS + instance. """ if self._state is not None: global_object.log.getLogger('fitting').warning( @@ -618,38 +656,43 @@ def extend( progress_callback: Callable[[dict], bool | None] | None = None, abort_test: Callable[[], bool] | None = None, ) -> SamplingResults: - """Continue the existing chain with additional samples. + """ + Continue the existing chain with additional samples. DREAM stores draws in a fixed-size ring buffer sized to its - ``samples`` parameter; this method does the ring-buffer arithmetic - for you (``samples = stored_generations * population + - additional_samples``) so no existing draws are dropped from the - buffer, regardless of the thinning interval. Runs with ``burn=0`` โ€” - re-burning a converged chain is usually a mistake, and BUMPS forces - it to 0 on resume in any case. The DREAM population is recovered from - the saved state and cannot be changed on extend. + ``samples`` parameter; this method does the ring-buffer + arithmetic for you (``samples = stored_generations * population + + additional_samples``) so no existing draws are dropped from + the buffer, regardless of the thinning interval. Runs with + ``burn=0`` โ€” re-burning a converged chain is usually a mistake, + and BUMPS forces it to 0 on resume in any case. The DREAM + population is recovered from the saved state and cannot be + changed on extend. Parameters ---------- additional_samples : int, default=5000 - Number of additional DREAM samples to draw, in the same units - as ``samples`` in ``sample()``. This grows the ring buffer by - exactly ``additional_samples``; how many of the new draws become - *visible* in ``results.draws`` depends on the thinning interval - and on BUMPS' automatic trimming (see Notes). + Number of additional DREAM samples to draw, in the same + units as ``samples`` in ``sample()``. This grows the ring + buffer by exactly ``additional_samples``; how many of the + new draws become *visible* in ``results.draws`` depends on + the thinning interval and on BUMPS' automatic trimming (see + Notes). thin : int, default=10 Thinning interval for the retained draws. total_samples : int | None, default=None - Advanced: total retained samples requested from the ring buffer, - **overriding** the ``additional_samples`` arithmetic. With - ``total_samples=N``, only the last N draws are retained. + Advanced: total retained samples requested from the ring + buffer, **overriding** the ``additional_samples`` + arithmetic. With ``total_samples=N``, only the last N draws + are retained. sampler_kwargs : dict | None, default=None Additional keyword arguments forwarded to the BUMPS DREAM sampler (merged over the instance defaults). progress_callback : Callable[[dict], bool | None] | None, default=None Optional callback invoked at each DREAM generation. abort_test : Callable[[], bool] | None, default=None - Optional callable that returns ``True`` to abort sampling early. + Optional callable that returns ``True`` to abort sampling + early. Returns ------- @@ -665,16 +708,17 @@ def extend( Notes ----- ``results.draws`` will **not** grow by exactly - ``additional_samples / thin``. Two things get in the way, neither of - them a re-applied burn-in: BUMPS re-runs its burn-point detector over - the whole extended chain and re-trims the returned view (so the - visible count can even shrink), and DREAM advances in blocks of 10 - generations, so the raw growth is ``additional_samples`` rounded up - to a multiple of ``10 * n_chains`` (i.e. at least - ``additional_samples / thin`` retained rows). To see the chain - itself, compare ``results.state.draw(portion=1.0, outliers=True)`` - before and after, or run with ``sampler_kwargs={'trim': False}``. See - the ``Sampler`` class notes. + ``additional_samples / thin``. Two things get in the way, + neither of them a re-applied burn-in: BUMPS re-runs its + burn-point detector over the whole extended chain and re-trims + the returned view (so the visible count can even shrink), and + DREAM advances in blocks of 10 generations, so the raw growth is + ``additional_samples`` rounded up to a multiple of ``10 * + n_chains`` (i.e. at least ``additional_samples / thin`` retained + rows). To see the chain itself, compare + ``results.state.draw(portion=1.0, outliers=True)`` before and + after, or run with ``sampler_kwargs={'trim': False}``. See the + ``Sampler`` class notes. """ if self._state is None: raise RuntimeError('No chain to extend. Call sample() or load_state() first.') @@ -698,14 +742,15 @@ def extend( ) def save(self, path: str | os.PathLike) -> None: - """Persist the chain state and metadata to disk. + """ + Persist the chain state and metadata to disk. Writes the BUMPS native files (``-chain.mc.gz``, ``-point.mc.gz`` and ``-stats.mc.gz``) plus a ``.params.json`` sidecar with the parameter names, the - easyscience version, and a fingerprint of the bound data (verified - with a warning on ``load_state()``). Use ``load_chain`` to read the - files back without a fitter. + easyscience version, and a fingerprint of the bound data + (verified with a warning on ``load_state()``). Use + ``load_chain`` to read the files back without a fitter. Parameters ---------- @@ -746,13 +791,14 @@ def save(self, path: str | os.PathLike) -> None: json.dump(sidecar, f, indent=2) def load_state(self, path: str | os.PathLike, skip: int = 0) -> SamplingResults: - """Load a previously saved chain into this sampler. + """ + Load a previously saved chain into this sampler. - The sampler must be constructed with the same fitter and data used to - create the chain โ€” ``extend()`` then continues the saved chain. If the - sidecar carries a data fingerprint and it does not match this - sampler's bound data, a warning is logged (extending a chain against - different data is undefined behaviour). + The sampler must be constructed with the same fitter and data + used to create the chain โ€” ``extend()`` then continues the saved + chain. If the sidecar carries a data fingerprint and it does not + match this sampler's bound data, a warning is logged (extending + a chain against different data is undefined behaviour). Populates ``state`` and ``results`` (draws, log-posterior and parameter names) from the saved chain, so summaries and @@ -763,8 +809,8 @@ def load_state(self, path: str | os.PathLike, skip: int = 0) -> SamplingResults: path : str | os.PathLike File path prefix used in ``save()``. skip : int, default=0 - Discard the first ``skip`` saved generations on load. Useful for - trimming additional burn-in without re-sampling. + Discard the first ``skip`` saved generations on load. Useful + for trimming additional burn-in without re-sampling. Returns ------- @@ -780,11 +826,11 @@ def load_state(self, path: str | os.PathLike, skip: int = 0) -> SamplingResults: Notes ----- - BUMPS does not persist its automatic trim point, so a reloaded chain - comes back **untrimmed**: this method reports more draws than the - ``sample()`` call that wrote the file, even though the chain is - identical. Use ``skip`` to discard leading generations explicitly. - See the ``Sampler`` class notes. + BUMPS does not persist its automatic trim point, so a reloaded + chain comes back **untrimmed**: this method reports more draws + than the ``sample()`` call that wrote the file, even though the + chain is identical. Use ``skip`` to discard leading generations + explicitly. See the ``Sampler`` class notes. """ # noqa: DOC502 -- raised in _validate_chain_path via load_chain state, param_names, sidecar = load_chain(path, skip=skip) diff --git a/tests/unit/base_classes/test_easy_list.py b/tests/unit/base_classes/test_easy_list.py index 978d5571..7978292e 100644 --- a/tests/unit/base_classes/test_easy_list.py +++ b/tests/unit/base_classes/test_easy_list.py @@ -32,7 +32,9 @@ def __init__(self, unique_name=None, display_name=None): class MockModel(ModelBase): """A ModelBase subclass with a Parameter and a DescriptorNumber for testing get_all_variables.""" - def __init__(self, unique_name=None, display_name=None, temperature=25, volume=1.0, items=None): + def __init__( + self, unique_name=None, display_name=None, temperature=25, volume=1.0, items=None + ): super().__init__(unique_name=unique_name, display_name=display_name) self._temperature = Parameter(name='temperature', value=temperature) self._volume = DescriptorNumber(name='volume', value=volume) @@ -643,9 +645,12 @@ def test_from_dict_missing_data_key(self): el_dict = { '@module': 'easyscience', '@class': 'EasyList', - 'protected_types': [{'@module': 'easyscience.base_classes', '@class': 'ModelBase'}] + 'protected_types': [{'@module': 'easyscience.base_classes', '@class': 'ModelBase'}], } - with pytest.raises(ValueError, match='The provided dictionary does not represent an EasyList. Missing the "data" key.'): + with pytest.raises( + ValueError, + match='The provided dictionary does not represent an EasyList. Missing the "data" key.', + ): EasyList.from_dict(el_dict) # --- get_all_variables --- diff --git a/tests/unit/base_classes/test_new_base.py b/tests/unit/base_classes/test_new_base.py index 7d74cac2..f3b7cbe6 100644 --- a/tests/unit/base_classes/test_new_base.py +++ b/tests/unit/base_classes/test_new_base.py @@ -168,7 +168,6 @@ def test_to_dict_nested_new_base_reverse(self): assert 'unique_name' in obj_dict['nested_new_base'] assert 'display_name' in obj_dict['nested_new_base'] - def test_from_dict(self): # When obj_dict = {