From 347a3c8a1d738c25ae5b11b3b84a67098bada669 Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Fri, 13 Mar 2026 12:31:35 +0100 Subject: [PATCH 01/31] Switch to pydoclint --- .copier-answers.yml | 2 +- .github/workflows/lint-format.yml | 90 ++++----- .pre-commit-config.yaml | 31 +-- CONTRIBUTING.md | 179 +++++++++++------- pixi.lock | 50 ++++- pixi.toml | 17 +- pyproject.toml | 143 +++++++++++--- tools/{add_spdx.py => add_license_headers.py} | 0 ...check_spdx.py => check_license_headers.py} | 0 ...move_spdx.py => remove_license_headers.py} | 0 10 files changed, 352 insertions(+), 160 deletions(-) rename tools/{add_spdx.py => add_license_headers.py} (100%) rename tools/{check_spdx.py => check_license_headers.py} (100%) rename tools/{remove_spdx.py => remove_license_headers.py} (100%) diff --git a/.copier-answers.yml b/.copier-answers.yml index 4003a370e..b8de5ab8f 100644 --- a/.copier-answers.yml +++ b/.copier-answers.yml @@ -1,6 +1,6 @@ # WARNING: Do not edit this file manually. # Any changes will be overwritten by Copier. -_commit: v0.10.1-2-gd05af4f +_commit: v0.10.1-23-g55e8e0e _src_path: gh:easyscience/templates app_docs_url: https://easyscience.github.io/dynamics-app app_doi: 10.5281/zenodo.18877180 diff --git a/.github/workflows/lint-format.yml b/.github/workflows/lint-format.yml index fc451d14f..457d67f4b 100644 --- a/.github/workflows/lint-format.yml +++ b/.github/workflows/lint-format.yml @@ -1,9 +1,12 @@ -# The workflow is divided into several steps: -# - Check the validity of pyproject.toml -# - Check code linting -# - Check code formatting -# - Check formatting of docstrings in the code -# - Check formatting of Markdown, YAML, TOML, etc. files +# The workflow checks +# - the validity of pyproject.toml, +# - the presence and correctness of SPDX license headers, +# - linting and formatting of Python code, +# - linting and formatting of docstrings in Python code, +# - formatting of non-Python files (like markdown and toml). +# - linting of Python code in Jupyter notebooks (for library template). +# +# A summary of the checks is added to the GitHub Actions summary. name: Lint and format checks @@ -46,56 +49,53 @@ jobs: - name: Run post-install developer steps run: pixi run post-install - # Check the validity of pyproject.toml - name: Check validity of pyproject.toml - id: check_pyproject + id: pyproject continue-on-error: true shell: bash run: pixi run pyproject-check - # Check the presence and correctness of SPDX license headers in - # the code files - name: Check SPDX license headers - id: check_spdx_headers + id: license_headers continue-on-error: true shell: bash - run: pixi run spdx-check + run: pixi run license-check - # Check code linting with Ruff in the project root - - name: Check code linting - id: check_code_linting + - name: Check linting of Python code + id: py_lint continue-on-error: true shell: bash run: pixi run py-lint-check - # Check code formatting with Ruff in the project root - - name: Check code formatting - id: check_code_formatting + - name: Check formatting of Python code + id: py_format continue-on-error: true shell: bash run: pixi run py-format-check - # Check formatting of docstrings in the code with docformatter - - name: Check formatting of docstrings in the code - id: check_docs_formatting + - name: Check linting of docstrings in Python code + id: docstring_lint continue-on-error: true shell: bash - run: pixi run docs-format-check + run: pixi run docstring-lint-check - # Check formatting of MD, YAML, TOML, etc. files with Prettier in - # the project root - - name: Check formatting of MD, YAML, TOML, etc. files - id: check_others_formatting + - name: Check formatting of docstrings in Python code + id: docstring_format + continue-on-error: true + shell: bash + run: pixi run docstring-format-check + + - name: Check formatting of non-Python files (md, toml, etc.) + id: nonpy_format continue-on-error: true shell: bash run: pixi run nonpy-format-check - # Check formatting of Jupyter Notebooks in the tutorials folder - - name: Prepare notebooks and check formatting - id: check_notebooks_formatting + - name: Check linting of Python code in Jupyter notebooks (ipynb) + id: notebook_lint continue-on-error: true shell: bash - run: pixi run notebook-format-check + run: pixi run notebook-lint-check # Add summary - name: Add quality checks summary @@ -107,24 +107,26 @@ jobs: echo "" echo "| Check | Status |" echo "|-------|--------|" - echo "| pyproject.toml | ${{ steps.check_pyproject.outcome == 'success' && '✅' || '❌' }} |" - echo "| SPDX headers | ${{ steps.check_spdx_headers.outcome == 'success' && '✅' || '❌' }} |" - echo "| py lint | ${{ steps.check_code_linting.outcome == 'success' && '✅' || '❌' }} |" - echo "| py format | ${{ steps.check_code_formatting.outcome == 'success' && '✅' || '❌' }} |" - echo "| docstring format | ${{ steps.check_docs_formatting.outcome == 'success' && '✅' || '❌' }} |" - echo "| nonpy format | ${{ steps.check_others_formatting.outcome == 'success' && '✅' || '❌' }} |" - echo "| notebooks format | ${{ steps.check_notebooks_formatting.outcome == 'success' && '✅' || '❌' }} |" + echo "| pyproject.toml | ${{ steps.pyproject.outcome == 'success' && '✅' || '❌' }} |" + echo "| license headers | ${{ steps.license_headers.outcome == 'success' && '✅' || '❌' }} |" + echo "| py lint | ${{ steps.py_lint.outcome == 'success' && '✅' || '❌' }} |" + echo "| py format | ${{ steps.py_format.outcome == 'success' && '✅' || '❌' }} |" + echo "| docstring lint | ${{ steps.docstring_lint.outcome == 'success' && '✅' || '❌' }} |" + echo "| docstring format | ${{ steps.docstring_format.outcome == 'success' && '✅' || '❌' }} |" + echo "| nonpy format | ${{ steps.nonpy_format.outcome == 'success' && '✅' || '❌' }} |" + echo "| notebooks lint | ${{ steps.notebook_lint.outcome == 'success' && '✅' || '❌' }} |" } >> "$GITHUB_STEP_SUMMARY" # Fail job if any check failed - name: Fail job if any check failed if: | - steps.check_pyproject.outcome == 'failure' - || steps.check_spdx_headers.outcome == 'failure' - || steps.check_code_linting.outcome == 'failure' - || steps.check_code_formatting.outcome == 'failure' - || steps.check_docs_formatting.outcome == 'failure' - || steps.check_others_formatting.outcome == 'failure' - || steps.check_notebooks_formatting.outcome == 'failure' + steps.pyproject.outcome == 'failure' + || steps.license_headers.outcome == 'failure' + || steps.py_lint.outcome == 'failure' + || steps.py_format.outcome == 'failure' + || steps.docstring_lint.outcome == 'failure' + || steps.docstring_format.outcome == 'failure' + || steps.nonpy_format.outcome == 'failure' + || steps.notebook_lint.outcome == 'failure' shell: bash run: exit 1 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index feb102db2..5fa0c2cbb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,9 +11,9 @@ repos: pass_filenames: false stages: [manual] - - id: spdx-headers-check - name: pixi run spdx-check - entry: pixi run spdx-check + - id: license-headers-check + name: pixi run license-check + entry: pixi run license-check language: system pass_filenames: false stages: [manual] @@ -32,23 +32,30 @@ repos: pass_filenames: false stages: [manual] - - id: pixi-nonpy-format-check - name: pixi run nonpy-format-check - entry: pixi run nonpy-format-check + - id: pixi-docstring-lint-check + name: pixi run docstring-lint-check + entry: pixi run docstring-lint-check + language: system + pass_filenames: false + stages: [manual] + + - id: pixi-docstring-format-check + name: pixi run docstring-format-check + entry: pixi run docstring-format-check language: system pass_filenames: false stages: [manual] - - id: pixi-docs-format-check - name: pixi run docs-format-check - entry: pixi run docs-format-check + - id: pixi-nonpy-format-check + name: pixi run nonpy-format-check + entry: pixi run nonpy-format-check language: system pass_filenames: false stages: [manual] - - id: pixi-notebook-format-check - name: pixi run notebook-format-check - entry: pixi run notebook-format-check + - id: pixi-notebook-lint-check + name: pixi run notebook-lint-check + entry: pixi run notebook-lint-check language: system pass_filenames: false stages: [manual] diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d3dd2479e..85365779f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,7 +2,7 @@ Thank you for your interest in contributing to **EasyDynamics**! -This guide explains how to: +This guide explains how you can: - Report issues - Contribute code @@ -39,7 +39,7 @@ Please make sure you follow the EasyScience organization-wide ## How to Interact With This Project -If you are not planning to modify the code, you may want to: +If you are not planning to contribute code, you may want to: - 🐞 Report a bug — see [Reporting Issues](#11-reporting-issues) - 🛡 Report a security issue — see @@ -62,17 +62,18 @@ We use the following branches: - `master` — stable releases only - `develop` — active development branch -- Short-lived branches — one branch per contribution - -All normal contributions must target the `develop` branch. - -This means: - -- Do **not** open Pull Requests against `master` -- Always create your branch from `develop` -- Always target `develop` when opening a Pull Request - -See ADR easyscience/.github#12 for full details on the branching +- Short-lived branches — feature or fix branches created for a single + contribution and deleted after merge + +> [!IMPORTANT] +> +> All normal contributions must target the `develop` branch. +> +> - Do **not** open Pull Requests against `master` +> - Always create your branch from `develop` +> - Always target `develop` when opening a Pull Request + +See ADR easyscience/.github#12 for more details on the branching strategy. --- @@ -122,8 +123,8 @@ This ensures you are working on the latest version of the project. ### 2.2. If You Are a Core Team Member -Core team members do not need to fork the repository. You can create a -new branch directly from `develop`, but the rest of the workflow remains +Core team members can create branches directly in this repository and +therefore do not need to fork it, but the rest of the workflow remains the same. --- @@ -150,12 +151,13 @@ Set up the environment: ```bash pixi install -pixi run post-install +pixi run post-install # Install additional tooling ``` After this step, your development environment is ready. -See ADR easyscience/.github#63 for more details about this decision. +See ADR easyscience/.github#63 for more details about using Pixi for +development. --- @@ -166,14 +168,16 @@ Never work directly on `develop`. Create a new branch: ```bash -git checkout -b my-change +git checkout -b my-change develop ``` -Use a clear and descriptive name, for example: - -- `improve-solver-speed` -- `fix-boundary-condition` -- `add-tutorial-example` +> [!IMPORTANT] +> +> Use a clear and descriptive name, for example: +> +> - `improve-solver-speed` +> - `fix-boundary-condition` +> - `add-tutorial-example` Clear branch names make reviews and history easier to understand. @@ -181,12 +185,7 @@ Clear branch names make reviews and history easier to understand. ## 5. Implementing Your Changes -While developing: - -- Make small, logical commits -- Write clear and descriptive commit messages -- Follow the Google docstring convention -- Add or update unit tests if behavior changes +While developing, make small, logical commits with clear messages. Example: @@ -195,48 +194,57 @@ git add . git commit -m "Improve performance of time integrator for large systems" ``` -Run tests locally: - -```bash -pixi run unit-tests -``` - -Running tests frequently is strongly recommended. - --- ## 6. Code Quality Checks +> [!IMPORTANT] +> +> When adding new functionality or making changes, make sure to add or +> update the following as needed: +> +> - 📘 docstrings +> - 🧪 unit tests + Before opening a Pull Request, always run: ```bash pixi run check ``` -This command runs: +This command: -- Formatting checks -- Linting -- Docstring validation -- Notebook checks -- Unit tests -- Other project validations +- Validates the pyproject.toml file +- Checks for licence headers in code files +- Identifies linting and formatting issues in Python code +- Checks docstring linting and formatting issues in Python code +- Detects formatting issues in non-Python files (MD, YAML, TOML etc.) +- Checks linting issues in Jupyter notebooks (if applicable) +- Runs unit tests A successful run should look like this: ```bash -pixi run pyproject-check...................................Passed -pixi run py-lint-check.....................................Passed -pixi run py-format-check...................................Passed -pixi run nonpy-format-check................................Passed -pixi run docs-format-check.................................Passed -pixi run notebook-format-check.............................Passed -pixi run unit-tests........................................Passed +pixi run pyproject-check.......................Passed +pixi run license-check.........................Passed +pixi run py-lint-check.........................Passed +pixi run py-format-check.......................Passed +pixi run docstring-lint-check..................Passed +pixi run docstring-format-check................Passed +pixi run nonpy-format-check....................Passed +pixi run notebook-lint-check...................Passed +pixi run unit-tests............................Passed ``` If something fails, read the error message carefully and fix the issue. -You can run individual checks, for example: +You can run individual checks, for example, to run only unit tests: + +```bash +pixi run unit-tests +``` + +or to run only Python linting checks: ```bash pixi run py-lint-check @@ -251,7 +259,7 @@ pixi run fix If everything is correctly formatted, you will see: ```text -✅ All code auto-formatting steps have been applied. +✅ All auto-formatting steps completed successfully! ``` This indicates that the auto-formatting pipeline completed successfully. @@ -264,7 +272,9 @@ If errors are reported, resolve them and re-run: pixi run check ``` -All checks must pass before your Pull Request can be merged. +> [!IMPORTANT] +> +> All checks must pass before your Pull Request can be merged. If you are unsure how to fix an issue, ask for help in your Pull Request discussion. @@ -289,8 +299,10 @@ On GitHub: ### Pull Request Title -The PR title appears in release notes and changelogs. It should be -concise and informative. +> [!IMPORTANT] +> +> The PR title appears in release notes and changelogs. It should be +> concise and informative. Good examples: @@ -302,7 +314,12 @@ Good examples: ### Required `[scope]` Label -Each Pull Request must include one `[scope]` label: +> [!IMPORTANT] +> +> Each Pull Request must include a `[scope]` label, which is used for +> automatically suggesting version bumps when preparing a new release. + +The available scopes are: | Label | Description | | ----------------------- | ----------------------------------------------------------------------- | @@ -312,7 +329,8 @@ Each Pull Request must include one `[scope]` label: | `[scope] maintenance` | Code/tooling cleanup without feature or bug fix (major.minor.**PATCH**) | | `[scope] significant` | Breaking or major changes (**MAJOR**.minor.patch) | -See ADR easyscience/.github#33 for full versioning rules. +See ADR easyscience/.github#33 for more details on the standardized +labeling scheme. --- @@ -355,7 +373,19 @@ git push ## 10. Documentation Contributions -If your change affects users, update the documentation. +> [!IMPORTANT] +> +> If your change affects user-facing functionality, update the project +> documentation accordingly — specifically the `nav:` (navigation) +> structure in `mkdocs.yml` and the relevant documentation Markdown +> files in `docs/docs/`. +> +> ```text +> 📁 docs +> ├── 📁 docs - Markdown files for documentation +> │ └── ... +> └── 📄 mkdocs.yml - Configuration file (navigation, theme, etc.) +> ``` This may include: @@ -376,31 +406,40 @@ Open the URL shown in the terminal to review your changes. ## 11. Reporting Issues -If you find a bug but do not want to fix it: +If you find a bug but cannot work on a fix, please consider opening an +issue. + +When reporting an issue, it helps to: -- Search existing issues first -- Provide clear reproduction steps -- Include logs and environment details +- Search existing issues first. +- Provide clear reproduction steps. +- Include logs, screenshots, and environment details. -Clear issue reports help maintainers significantly. +Clear and detailed reports help maintainers investigate and resolve +issues more effectively. --- ## 12. Security Issues -Do **not** report security vulnerabilities publicly. +> [!IMPORTANT] +> +> Please do **not** report security vulnerabilities publicly. -If you discover a potential vulnerability, contact the maintainers -privately. +If you discover a potential vulnerability, please contact the +maintainers privately so the issue can be investigated and addressed +responsibly. --- ## 13. Releases -Releases are created by merging `develop` into `master`. +Once your contribution is merged into `develop`, it will eventually be +included in the next stable release. -Once your contribution is merged into `develop`, it will be included in -the next stable release. +When enough changes have accumulated in `develop`, core team members +merge `develop` into `master` to prepare a new release. The release is +then tagged and published on GitHub and PyPI. --- diff --git a/pixi.lock b/pixi.lock index 79b0bdd02..44a745743 100644 --- a/pixi.lock +++ b/pixi.lock @@ -84,6 +84,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/dc/b4/a7ec1eaee86761a9dbfd339732b4706db3c6b65e970c12f0f56cfcce3dcf/docformatter-1.7.7-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bf/50/98b146aea0f1cd7531d25f12bea69fa9ce8d1662124f93fb30dc4511b65e/docstring_parser_fork-0.0.14-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/87/10/2c7edbf230e5c507d38367af498fa94258ed97205d9b4b6f63a921fe9c49/dunamai-1.26.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/64/f2/9d779717fd4ff4136d009a8023704f7eb37f2231fbfbe49eb9b430315bcc/easyscience-2.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl @@ -200,6 +201,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/87/6f/cc2b231dc78d8c3aaa674a676db190b8f8071c50134af8f8cf39b9b8e8df/pydoclint-0.8.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/6f/2c/5b079febdc65e1c3fb2729bf958d18b45be7113828528e8a0b5850dd819a/pymdown_extensions-10.21-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl @@ -334,6 +336,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/dc/b4/a7ec1eaee86761a9dbfd339732b4706db3c6b65e970c12f0f56cfcce3dcf/docformatter-1.7.7-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bf/50/98b146aea0f1cd7531d25f12bea69fa9ce8d1662124f93fb30dc4511b65e/docstring_parser_fork-0.0.14-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/87/10/2c7edbf230e5c507d38367af498fa94258ed97205d9b4b6f63a921fe9c49/dunamai-1.26.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/64/f2/9d779717fd4ff4136d009a8023704f7eb37f2231fbfbe49eb9b430315bcc/easyscience-2.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl @@ -450,6 +453,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/87/6f/cc2b231dc78d8c3aaa674a676db190b8f8071c50134af8f8cf39b9b8e8df/pydoclint-0.8.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/6f/2c/5b079febdc65e1c3fb2729bf958d18b45be7113828528e8a0b5850dd819a/pymdown_extensions-10.21-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl @@ -584,6 +588,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/dc/b4/a7ec1eaee86761a9dbfd339732b4706db3c6b65e970c12f0f56cfcce3dcf/docformatter-1.7.7-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bf/50/98b146aea0f1cd7531d25f12bea69fa9ce8d1662124f93fb30dc4511b65e/docstring_parser_fork-0.0.14-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/87/10/2c7edbf230e5c507d38367af498fa94258ed97205d9b4b6f63a921fe9c49/dunamai-1.26.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/64/f2/9d779717fd4ff4136d009a8023704f7eb37f2231fbfbe49eb9b430315bcc/easyscience-2.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl @@ -700,6 +705,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/87/6f/cc2b231dc78d8c3aaa674a676db190b8f8071c50134af8f8cf39b9b8e8df/pydoclint-0.8.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/6f/2c/5b079febdc65e1c3fb2729bf958d18b45be7113828528e8a0b5850dd819a/pymdown_extensions-10.21-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl @@ -824,6 +830,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/dc/b4/a7ec1eaee86761a9dbfd339732b4706db3c6b65e970c12f0f56cfcce3dcf/docformatter-1.7.7-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bf/50/98b146aea0f1cd7531d25f12bea69fa9ce8d1662124f93fb30dc4511b65e/docstring_parser_fork-0.0.14-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/87/10/2c7edbf230e5c507d38367af498fa94258ed97205d9b4b6f63a921fe9c49/dunamai-1.26.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/64/f2/9d779717fd4ff4136d009a8023704f7eb37f2231fbfbe49eb9b430315bcc/easyscience-2.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl @@ -938,6 +945,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/87/6f/cc2b231dc78d8c3aaa674a676db190b8f8071c50134af8f8cf39b9b8e8df/pydoclint-0.8.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/6f/2c/5b079febdc65e1c3fb2729bf958d18b45be7113828528e8a0b5850dd819a/pymdown_extensions-10.21-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl @@ -1089,6 +1097,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/dc/b4/a7ec1eaee86761a9dbfd339732b4706db3c6b65e970c12f0f56cfcce3dcf/docformatter-1.7.7-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bf/50/98b146aea0f1cd7531d25f12bea69fa9ce8d1662124f93fb30dc4511b65e/docstring_parser_fork-0.0.14-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/87/10/2c7edbf230e5c507d38367af498fa94258ed97205d9b4b6f63a921fe9c49/dunamai-1.26.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/64/f2/9d779717fd4ff4136d009a8023704f7eb37f2231fbfbe49eb9b430315bcc/easyscience-2.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl @@ -1206,6 +1215,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/87/6f/cc2b231dc78d8c3aaa674a676db190b8f8071c50134af8f8cf39b9b8e8df/pydoclint-0.8.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/6f/2c/5b079febdc65e1c3fb2729bf958d18b45be7113828528e8a0b5850dd819a/pymdown_extensions-10.21-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl @@ -1340,6 +1350,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/dc/b4/a7ec1eaee86761a9dbfd339732b4706db3c6b65e970c12f0f56cfcce3dcf/docformatter-1.7.7-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bf/50/98b146aea0f1cd7531d25f12bea69fa9ce8d1662124f93fb30dc4511b65e/docstring_parser_fork-0.0.14-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/87/10/2c7edbf230e5c507d38367af498fa94258ed97205d9b4b6f63a921fe9c49/dunamai-1.26.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/64/f2/9d779717fd4ff4136d009a8023704f7eb37f2231fbfbe49eb9b430315bcc/easyscience-2.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl @@ -1457,6 +1468,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/87/6f/cc2b231dc78d8c3aaa674a676db190b8f8071c50134af8f8cf39b9b8e8df/pydoclint-0.8.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/6f/2c/5b079febdc65e1c3fb2729bf958d18b45be7113828528e8a0b5850dd819a/pymdown_extensions-10.21-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl @@ -1591,6 +1603,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/dc/b4/a7ec1eaee86761a9dbfd339732b4706db3c6b65e970c12f0f56cfcce3dcf/docformatter-1.7.7-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bf/50/98b146aea0f1cd7531d25f12bea69fa9ce8d1662124f93fb30dc4511b65e/docstring_parser_fork-0.0.14-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/87/10/2c7edbf230e5c507d38367af498fa94258ed97205d9b4b6f63a921fe9c49/dunamai-1.26.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/64/f2/9d779717fd4ff4136d009a8023704f7eb37f2231fbfbe49eb9b430315bcc/easyscience-2.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl @@ -1708,6 +1721,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/87/6f/cc2b231dc78d8c3aaa674a676db190b8f8071c50134af8f8cf39b9b8e8df/pydoclint-0.8.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/6f/2c/5b079febdc65e1c3fb2729bf958d18b45be7113828528e8a0b5850dd819a/pymdown_extensions-10.21-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl @@ -1832,6 +1846,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/dc/b4/a7ec1eaee86761a9dbfd339732b4706db3c6b65e970c12f0f56cfcce3dcf/docformatter-1.7.7-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bf/50/98b146aea0f1cd7531d25f12bea69fa9ce8d1662124f93fb30dc4511b65e/docstring_parser_fork-0.0.14-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/87/10/2c7edbf230e5c507d38367af498fa94258ed97205d9b4b6f63a921fe9c49/dunamai-1.26.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/64/f2/9d779717fd4ff4136d009a8023704f7eb37f2231fbfbe49eb9b430315bcc/easyscience-2.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl @@ -1947,6 +1962,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/87/6f/cc2b231dc78d8c3aaa674a676db190b8f8071c50134af8f8cf39b9b8e8df/pydoclint-0.8.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/6f/2c/5b079febdc65e1c3fb2729bf958d18b45be7113828528e8a0b5850dd819a/pymdown_extensions-10.21-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl @@ -2098,6 +2114,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/dc/b4/a7ec1eaee86761a9dbfd339732b4706db3c6b65e970c12f0f56cfcce3dcf/docformatter-1.7.7-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bf/50/98b146aea0f1cd7531d25f12bea69fa9ce8d1662124f93fb30dc4511b65e/docstring_parser_fork-0.0.14-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/87/10/2c7edbf230e5c507d38367af498fa94258ed97205d9b4b6f63a921fe9c49/dunamai-1.26.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/64/f2/9d779717fd4ff4136d009a8023704f7eb37f2231fbfbe49eb9b430315bcc/easyscience-2.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl @@ -2214,6 +2231,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/87/6f/cc2b231dc78d8c3aaa674a676db190b8f8071c50134af8f8cf39b9b8e8df/pydoclint-0.8.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/6f/2c/5b079febdc65e1c3fb2729bf958d18b45be7113828528e8a0b5850dd819a/pymdown_extensions-10.21-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl @@ -2348,6 +2366,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/dc/b4/a7ec1eaee86761a9dbfd339732b4706db3c6b65e970c12f0f56cfcce3dcf/docformatter-1.7.7-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bf/50/98b146aea0f1cd7531d25f12bea69fa9ce8d1662124f93fb30dc4511b65e/docstring_parser_fork-0.0.14-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/87/10/2c7edbf230e5c507d38367af498fa94258ed97205d9b4b6f63a921fe9c49/dunamai-1.26.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/64/f2/9d779717fd4ff4136d009a8023704f7eb37f2231fbfbe49eb9b430315bcc/easyscience-2.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl @@ -2464,6 +2483,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/87/6f/cc2b231dc78d8c3aaa674a676db190b8f8071c50134af8f8cf39b9b8e8df/pydoclint-0.8.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/6f/2c/5b079febdc65e1c3fb2729bf958d18b45be7113828528e8a0b5850dd819a/pymdown_extensions-10.21-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl @@ -2598,6 +2618,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/dc/b4/a7ec1eaee86761a9dbfd339732b4706db3c6b65e970c12f0f56cfcce3dcf/docformatter-1.7.7-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bf/50/98b146aea0f1cd7531d25f12bea69fa9ce8d1662124f93fb30dc4511b65e/docstring_parser_fork-0.0.14-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/87/10/2c7edbf230e5c507d38367af498fa94258ed97205d9b4b6f63a921fe9c49/dunamai-1.26.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/64/f2/9d779717fd4ff4136d009a8023704f7eb37f2231fbfbe49eb9b430315bcc/easyscience-2.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl @@ -2714,6 +2735,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/87/6f/cc2b231dc78d8c3aaa674a676db190b8f8071c50134af8f8cf39b9b8e8df/pydoclint-0.8.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/6f/2c/5b079febdc65e1c3fb2729bf958d18b45be7113828528e8a0b5850dd819a/pymdown_extensions-10.21-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl @@ -2838,6 +2860,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/dc/b4/a7ec1eaee86761a9dbfd339732b4706db3c6b65e970c12f0f56cfcce3dcf/docformatter-1.7.7-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bf/50/98b146aea0f1cd7531d25f12bea69fa9ce8d1662124f93fb30dc4511b65e/docstring_parser_fork-0.0.14-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/87/10/2c7edbf230e5c507d38367af498fa94258ed97205d9b4b6f63a921fe9c49/dunamai-1.26.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/64/f2/9d779717fd4ff4136d009a8023704f7eb37f2231fbfbe49eb9b430315bcc/easyscience-2.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl @@ -2952,6 +2975,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/87/6f/cc2b231dc78d8c3aaa674a676db190b8f8071c50134af8f8cf39b9b8e8df/pydoclint-0.8.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/6f/2c/5b079febdc65e1c3fb2729bf958d18b45be7113828528e8a0b5850dd819a/pymdown_extensions-10.21-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl @@ -4029,6 +4053,17 @@ packages: - tomli>=2.0.0,<3.0.0 ; python_full_version < '3.11' and extra == 'tomli' - untokenize>=0.1.1,<0.2.0 requires_python: '>=3.9,<4.0' +- pypi: https://files.pythonhosted.org/packages/bf/50/98b146aea0f1cd7531d25f12bea69fa9ce8d1662124f93fb30dc4511b65e/docstring_parser_fork-0.0.14-py3-none-any.whl + name: docstring-parser-fork + version: 0.0.14 + sha256: 4c544f234ef2cc2749a3df32b70c437d77888b1099143a1ad5454452c574b9af + requires_dist: + - docstring-parser[docs] ; extra == 'dev' + - docstring-parser[test] ; extra == 'dev' + - pre-commit>=2.16.0 ; python_full_version >= '3.9' and extra == 'dev' + - pydoctor>=25.4.0 ; extra == 'docs' + - pytest ; extra == 'test' + requires_python: '>=3.8' - pypi: https://files.pythonhosted.org/packages/87/10/2c7edbf230e5c507d38367af498fa94258ed97205d9b4b6f63a921fe9c49/dunamai-1.26.0-py3-none-any.whl name: dunamai version: 1.26.0 @@ -4039,8 +4074,8 @@ packages: requires_python: '>=3.5' - pypi: ./ name: easydynamics - version: 0.2.0+dev17 - sha256: a7275b719cb0a568d02f7ae8fd44370175019772c2629eac30bbec5e0adbfdd3 + version: 0.2.1+dev5 + sha256: 84cc733227e0d2e7a4af52c5ffeab65da604d6253e6d7584f192bf59685802e8 requires_dist: - darkdetect - easyscience @@ -4073,6 +4108,7 @@ packages: - nbqa ; extra == 'dev' - nbstripout ; extra == 'dev' - pre-commit ; extra == 'dev' + - pydoclint ; extra == 'dev' - pytest ; extra == 'dev' - pytest-cov ; extra == 'dev' - pytest-forked ; extra == 'dev' @@ -8564,6 +8600,16 @@ packages: requires_dist: - typing-extensions>=4.14.1 requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/87/6f/cc2b231dc78d8c3aaa674a676db190b8f8071c50134af8f8cf39b9b8e8df/pydoclint-0.8.3-py3-none-any.whl + name: pydoclint + version: 0.8.3 + sha256: 5fc9b82d0d515afce0908cb70e8ff695a68b19042785c248c4f227ad66b4a164 + requires_dist: + - click>=8.1.0 + - docstring-parser-fork>=0.0.12 + - tomli>=2.0.1 ; python_full_version < '3.11' + - flake8>=4 ; extra == 'flake8' + requires_python: '>=3.10' - pypi: https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl name: pygments version: 2.19.2 diff --git a/pixi.toml b/pixi.toml index 7593fd30f..9b6cd33df 100644 --- a/pixi.toml +++ b/pixi.toml @@ -90,8 +90,9 @@ test = { depends-on = ['unit-tests'] } ########### pyproject-check = 'python -m validate_pyproject pyproject.toml' -docs-format-check = 'docformatter --check src/ docs/docs/tutorials/' -notebook-format-check = 'nbqa ruff docs/docs/tutorials/' +docstring-lint-check = 'pydoclint --quiet src/' +docstring-format-check = 'docformatter --check src/' +notebook-lint-check = 'nbqa ruff docs/docs/tutorials/' py-lint-check = 'ruff check src/ tests/ docs/docs/tutorials/' py-format-check = "ruff format --check src/ tests/ docs/docs/tutorials/" nonpy-format-check = "npx prettier --list-different --config=prettierrc.toml --ignore-unknown ." @@ -104,19 +105,19 @@ check = 'pre-commit run --hook-stage manual --all-files' ########## docs-format-fix = 'docformatter --in-place src/ docs/docs/tutorials/' -notebook-format-fix = 'nbqa ruff --fix docs/docs/tutorials/' +notebook-lint-fix = 'nbqa ruff --fix docs/docs/tutorials/' py-lint-fix = 'ruff check --fix src/ tests/ docs/docs/tutorials/' py-format-fix = "ruff format src/ tests/ docs/docs/tutorials/" nonpy-format-fix = 'npx prettier --write --list-different --config=prettierrc.toml --ignore-unknown .' nonpy-format-fix-modified = "python tools/nonpy_prettier_modified.py --write" -success-message-fix = 'echo "✅ All code auto-formatting steps have been applied."' +success-message-fix = 'echo "✅ All auto-formatting steps completed successfully!"' fix = { depends-on = [ 'py-format-fix', 'docs-format-fix', 'py-lint-fix', 'nonpy-format-fix', - 'notebook-format-fix', + 'notebook-lint-fix', 'success-message-fix', ] } @@ -229,9 +230,9 @@ github-labels = 'python tools/update_github_labels.py' # ⚖️ SPDX License Headers ######################### -spdx-remove = 'python tools/remove_spdx.py src/ tests/' -spdx-add = 'python tools/add_spdx.py src/ tests/' -spdx-check = 'python tools/check_spdx.py src/ tests/' +license-remove = 'python tools/remove_license_headers.py src/ tests/' +license-add = 'python tools/add_license_headers.py src/ tests/' +license-check = 'python tools/check_license_headers.py src/ tests/' #################################### # 🚀 Other Development & Build Tasks diff --git a/pyproject.toml b/pyproject.toml index 6f7d9d21b..dba43620a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,8 +54,9 @@ dev = [ 'versioningit', # Automatic versioning from git tags 'jupytext', # Jupyter notebook text format support 'jupyterquiz', # Quizzes in Jupyter notebooks - 'docformatter', # Code formatter for docstrings - 'interrogate', # Check for missing docstrings + 'pydoclint', # Docstring linter + 'docformatter', # Docstring formatter + 'interrogate', # Docstring coverage checker 'copier', # Template management 'mike', # MkDocs: Versioned documentation support 'mkdocs', # Static site generator @@ -80,12 +81,6 @@ Documentation = 'https://easyscience.github.io/dynamics-lib' # Build system configuration ############################ -# Build system 'hatch' -- Python project manager -# https://hatch.pypa.io/ - -# Versioning system 'versioningit' -- Versioning from git tags -# https://versioningit.readthedocs.io/ - [build-system] build-backend = 'hatchling.build' requires = ['hatchling', 'versioningit'] @@ -94,6 +89,9 @@ requires = ['hatchling', 'versioningit'] # Configuration for hatchling ############################# +# 'hatch' -- Build system for Python +# https://hatch.pypa.io/ + [tool.hatch.build.targets.wheel] packages = ['src/easydynamics'] @@ -107,6 +105,9 @@ source = 'versioningit' # Use versioningit to manage the version # Configuration for versioningit ################################ +# 'versioningit' -- Versioning from git tags +# https://versioningit.readthedocs.io/ + # Versioningit generates versions from git tags, so we don't need to # either specify them statically in pyproject.toml or save them in the # source code. Do not use {distance} in the version format, as it @@ -130,7 +131,7 @@ default-tag = 'v999.0.0' # Configuration for interrogate ################################ -# 'interrogate' -- Check for missing docstrings +# 'interrogate' -- Docstring coverage checker # https://interrogate.readthedocs.io/en/latest/ [tool.interrogate] @@ -142,6 +143,9 @@ verbose = 1 # Configuration for coverage/pytest-cov ####################################### +# 'coverage' -- Code coverage measurement tool +# 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 @@ -155,6 +159,9 @@ fail_under = 0 # Minimum coverage percentage to pass # Configuration for pytest ########################## +# 'pytest' -- Testing framework +# https://docs.pytest.org/en/stable/ + [tool.pytest.ini_options] addopts = '--import-mode=importlib' markers = ['fast: mark test as fast (should be run on every push)'] @@ -173,6 +180,8 @@ indent-width = 4 line-length = 99 preview = true # Enable new rules that are not yet stable, like DOC +# 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 @@ -180,31 +189,102 @@ indent-style = 'space' # PEP 8 recommends using spaces over tabs line-ending = 'lf' # Line endings will be converted to \n quote-style = 'single' # But double quotes in docstrings (PEP 8, PEP 257) -# https://docs.astral.sh/ruff/rules/ +# 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 - '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) - 'W', # General PEP 8 warnings (e.g., lines too long, trailing whitespace) + # Various rules + #'C90', # https://docs.astral.sh/ruff/rules/#mccabe-c90 + 'F', # https://docs.astral.sh/ruff/rules/#pyflakes-f + #'FLY', # https://docs.astral.sh/ruff/rules/#flynt-fly + #'FURB', # https://docs.astral.sh/ruff/rules/#refurb-furb + 'I', # https://docs.astral.sh/ruff/rules/#isort-i + #'N', # https://docs.astral.sh/ruff/rules/#pep8-naming-n + #'NPY', # https://docs.astral.sh/ruff/rules/#numpy-specific-rules-npy + #'PGH', # https://docs.astral.sh/ruff/rules/#pygrep-hooks-pgh + #'PERF', # https://docs.astral.sh/ruff/rules/#perflint-perf + #'RUF', # https://docs.astral.sh/ruff/rules/#ruff-specific-rules-ruf + #'TRY', # https://docs.astral.sh/ruff/rules/#tryceratops-try + #'UP', # https://docs.astral.sh/ruff/rules/#pyupgrade-up + # pycodestyle (E, W) rules + 'E', # https://docs.astral.sh/ruff/rules/#error-e + 'W', # https://docs.astral.sh/ruff/rules/#warning-w + # Pylint (PL) rules + #'PLC', # https://docs.astral.sh/ruff/rules/#convention-plc + #'PLE', # https://docs.astral.sh/ruff/rules/#error-ple + #'PLR', # https://docs.astral.sh/ruff/rules/#refactor-plr + #'PLW', # https://docs.astral.sh/ruff/rules/#warning-plw + # flake8 rules + #'A', # https://docs.astral.sh/ruff/rules/#flake8-builtins-a + 'ANN', # https://docs.astral.sh/ruff/rules/#flake8-annotations-ann + #'ARG', # https://docs.astral.sh/ruff/rules/#flake8-unused-arguments-arg + #'ASYNC', # https://docs.astral.sh/ruff/rules/#flake8-async-async + #'B', # https://docs.astral.sh/ruff/rules/#flake8-bugbear-b + #'BLE', # https://docs.astral.sh/ruff/rules/#flake8-blind-except-ble + #'C4', # https://docs.astral.sh/ruff/rules/#flake8-comprehensions-c4 + #'COM', # https://docs.astral.sh/ruff/rules/#flake8-commas-com + #'DTZ', # https://docs.astral.sh/ruff/rules/#flake8-datetimez-dtz + #'EM', # https://docs.astral.sh/ruff/rules/#flake8-errmsg-em + #'FA', # https://docs.astral.sh/ruff/rules/#flake8-future-annotations-fa + #'FBT', # https://docs.astral.sh/ruff/rules/#flake8-boolean-trap-fbt + #'FIX', # https://docs.astral.sh/ruff/rules/#flake8-fixme-fix + #'G', # https://docs.astral.sh/ruff/rules/#flake8-logging-format-g + #'ICN', # https://docs.astral.sh/ruff/rules/#flake8-import-conventions-icn + #'INP', # https://docs.astral.sh/ruff/rules/#flake8-no-pep420-inp + #'ISC', # https://docs.astral.sh/ruff/rules/#flake8-implicit-str-concat-isc + #'LOG', # https://docs.astral.sh/ruff/rules/#flake8-logging-log + #'PIE', # https://docs.astral.sh/ruff/rules/#flake8-pie-pie + #'PT', # https://docs.astral.sh/ruff/rules/#flake8-pytest-style-pt + #'PTH', # https://docs.astral.sh/ruff/rules/#flake8-use-pathlib-pth + #'PYI', # https://docs.astral.sh/ruff/rules/#flake8-pyi-pyi + #'RET', # https://docs.astral.sh/ruff/rules/#flake8-return-ret + #'RSE', # https://docs.astral.sh/ruff/rules/#flake8-raise-rse + 'S', # https://docs.astral.sh/ruff/rules/#flake8-bandit-s + #'SIM', # https://docs.astral.sh/ruff/rules/#flake8-simplify-sim + #'SLF', # https://docs.astral.sh/ruff/rules/#flake8-self-slf + #'SLOT', # https://docs.astral.sh/ruff/rules/#flake8-slots-slot + #'T20', # https://docs.astral.sh/ruff/rules/#flake8-print-t20 + #'TC', # https://docs.astral.sh/ruff/rules/#flake8-type-checking-tc + #'TD', # https://docs.astral.sh/ruff/rules/#flake8-todos-td + #'TID', # https://docs.astral.sh/ruff/rules/#flake8-tidy-imports-tid +] + +# Exceptions to the linting rules + +# Ignore specific rules globally +ignore = [ + 'COM812', # https://docs.astral.sh/ruff/rules/missing-trailing-comma/ + # The following covered by [tool.pydoclint] section below + 'D', # https://docs.astral.sh/ruff/rules/#pydocstyle-d + 'DOC', # https://docs.astral.sh/ruff/rules/#pydoclint-doc ] +# Ignore specific rules in certain files or directories [tool.ruff.lint.per-file-ignores] -'tests/**/*.py' = ['S101'] # Allow assert statements in tests +'tests/**' = [ + 'ANN', # https://docs.astral.sh/ruff/rules/#flake8-annotations-ann + '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/ +] + +# Specific options for certain rules [tool.ruff.lint.flake8-tidy-imports] +# Disallow all relative imports ban-relative-imports = 'all' [tool.ruff.lint.isort] +# Forces all from imports to appear on their own line force-single-line = true [tool.ruff.lint.mccabe] -max-complexity = 10 # default is 10 +# Cyclomatic complexity threshold (default is 10) +max-complexity = 10 [tool.ruff.lint.pycodestyle] # PEP 8 line length guidance: @@ -222,8 +302,25 @@ max-line-length = 99 # as defined in the [tool.ruff.format] section above. max-doc-length = 99 -[tool.ruff.lint.pydocstyle] -convention = 'google' +############################# +# Configuration for pydoclint +############################# + +# 'pydoclint' -- Docstring linter, a faster alternative to +# 'darglint' or 'darglint2'. +# https://pypi.org/project/pydoclint/ + +# This is a more advanced docstring linter compared to Ruff's built-in +# docstring check rules D or DOC. For example, among many other things, +# it can check that arguments in the docstring, which are used by MkDocs +# and IDEs to render parameter documentation, remain synchronized with +# the parameter declarations in the code (in function's signature). + +[tool.pydoclint] +style = "google" +check-style-mismatch = true +check-arg-defaults = true +allow-init-docstring = true ################################ # Configuration for docformatter diff --git a/tools/add_spdx.py b/tools/add_license_headers.py similarity index 100% rename from tools/add_spdx.py rename to tools/add_license_headers.py diff --git a/tools/check_spdx.py b/tools/check_license_headers.py similarity index 100% rename from tools/check_spdx.py rename to tools/check_license_headers.py diff --git a/tools/remove_spdx.py b/tools/remove_license_headers.py similarity index 100% rename from tools/remove_spdx.py rename to tools/remove_license_headers.py From 469cbb6e99443d86480215dc0c0f151c9c713e8f Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Fri, 13 Mar 2026 14:25:34 +0100 Subject: [PATCH 02/31] Fix utils --- src/easydynamics/utils/detailed_balance.py | 86 ++++++++++++---------- src/easydynamics/utils/utils.py | 20 +++-- 2 files changed, 61 insertions(+), 45 deletions(-) diff --git a/src/easydynamics/utils/detailed_balance.py b/src/easydynamics/utils/detailed_balance.py index ab323530d..28802068a 100644 --- a/src/easydynamics/utils/detailed_balance.py +++ b/src/easydynamics/utils/detailed_balance.py @@ -25,8 +25,8 @@ def _detailed_balance_factor( energy: int | float | list | np.ndarray | sc.Variable, temperature: int | float | sc.Variable | Parameter, - energy_unit: str | sc.Unit = 'meV', - temperature_unit: str | sc.Unit = 'K', + energy_unit: str | sc.Unit = "meV", + temperature_unit: str | sc.Unit = "K", divide_by_temperature: bool = True, ) -> np.ndarray: r""" @@ -41,26 +41,32 @@ def _detailed_balance_factor( $k_B*T$ to have value 1 at $E=0$. Args: - energy (number | list | np.ndarray | scipp.Variable): The energy + energy (int | float | list | np.ndarray | sc.Variable): The energy transfer. If number, assumed to be in meV unless energy_unit is set. - temperature (number | scipp.Variable | Parameter): The + temperature (int | float | sc.Variable | Parameter): The temperature. If number, assumed to be in K unless temperature_unit is set. - energy_unit (str | sc.Unit |None): Unit for energy if energy is + energy_unit (str | sc.Unit, default='meV'): Unit for energy if energy is given as a number or list. Default is 'meV' - temperature_unit (str | sc.Unit |None): Unit for temperature if + temperature_unit (str | sc.Unit, default='K'): Unit for temperature if temperature is given as a number. Default is 'K' - divide_by_temperature (bool | None): If True, divide the result + divide_by_temperature (bool, default=True): If True, divide the result by $k_B*T$ to make it dimensionless and have value 1 at E=0. Default is True. Returns: - DBF (np.ndarray): Detailed balance factor evaluated at the + np.ndarray: Detailed balance factor evaluated at the given energy and temperature. - Examples - -------- + Raises: + TypeError: If energy or temperature is not a number, list, numpy array, or scipp Variable, or if energy_unit or temperature_unit is not a string or scipp Unit, or if divide_by_temperature is not a boolean. + ValueError: If temperature is negative, or if energy is a numpy array with more than 1 dimension, or if temperature is a scipp Variable that does not have a single dimension named 'temperature', or if energy is a scipp Variable that does not have a single dimension named 'energy'. + UnitError: If the provided energy_unit or temperature_unit is invalid, or if the units of energy or temperature cannot be converted to the expected + units. + ZeroDivisionError: If divide_by_temperature is True and temperature is zero. + + Examples: >>> detailed_balance_factor(1.0, 300) # 1 meV at 300 K >>> detailed_balance_factor( ... energy=[1.0, 2.0], @@ -73,60 +79,60 @@ def _detailed_balance_factor( # Input validation if not isinstance(divide_by_temperature, bool): - raise TypeError('divide_by_temperature must be True or False.') + raise TypeError("divide_by_temperature must be True or False.") if not isinstance(energy_unit, (str, sc.Unit)): - raise TypeError('energy_unit must be a string or scipp.Unit.') + raise TypeError("energy_unit must be a string or scipp.Unit.") if not isinstance(temperature_unit, (str, sc.Unit)): - raise TypeError('temperature_unit must be a string or scipp.Unit.') + raise TypeError("temperature_unit must be a string or scipp.Unit.") # Convert temperature and energy to sc variables # to make units easy to handle temperature = _convert_to_scipp_variable( - value=temperature, unit=temperature_unit, name='temperature' + value=temperature, unit=temperature_unit, name="temperature" ) if temperature.value < 0: - raise ValueError('Temperature must be non-negative.') + raise ValueError("Temperature must be non-negative.") - energy = _convert_to_scipp_variable(value=energy, unit=energy_unit, name='energy') + energy = _convert_to_scipp_variable(value=energy, unit=energy_unit, name="energy") # What if people give units that don't make sense? try: - sc.to_unit(energy, unit='meV') + sc.to_unit(energy, unit="meV") except Exception as e: raise UnitError( - f'The unit of energy is wrong: {energy.unit}: {e} Check that energy has a valid unit.' + f"The unit of energy is wrong: {energy.unit}: {e} Check that energy has a valid unit." ) # We give users the option to specify the unit of the energy, # but if the input has a unit, they might clash if energy.unit != energy_unit: warnings.warn( - f'Input energy has unit {energy.unit}, but energy_unit was set to {energy_unit}. ' - f'Using {energy.unit}.' + f"Input energy has unit {energy.unit}, but energy_unit was set to {energy_unit}. " + f"Using {energy.unit}." ) # Same for temperature try: - sc.to_unit(temperature, unit='K') + sc.to_unit(temperature, unit="K") except Exception as e: raise UnitError( - f'The unit of temperature is wrong: {temperature.unit}: {e} ' - f'Check that temperature has a valid unit.' + f"The unit of temperature is wrong: {temperature.unit}: {e} " + f"Check that temperature has a valid unit." ) if temperature.unit != temperature_unit: warnings.warn( - f'Input temperature has unit {temperature.unit}, ' - f'but temperature_unit was set to {temperature_unit}. Using {temperature.unit}.' + f"Input temperature has unit {temperature.unit}, " + f"but temperature_unit was set to {temperature_unit}. Using {temperature.unit}." ) # Zero temperature deserves special treatment. # Here, DBF is 0 for energy<0 and energy for energy>0 if temperature.value == 0: if divide_by_temperature: - raise ZeroDivisionError('Cannot divide by T when T = 0.') + raise ZeroDivisionError("Cannot divide by T when T = 0.") DBF = sc.where(energy < 0.0 * energy.unit, 0.0 * energy.unit, energy) if DBF.sizes == {}: @@ -142,7 +148,7 @@ def _detailed_balance_factor( x = energy / (kB * temperature) - x = sc.to_unit(x, unit='1') # Make sure the unit is 1 and not e.g. 1e3 + x = sc.to_unit(x, unit="1") # Make sure the unit is 1 and not e.g. 1e3 # Now compute DBF. First handle small and large x, then general. @@ -159,7 +165,9 @@ def _detailed_balance_factor( # General case: exact formula mid = sc.logical_not(small) & sc.logical_not(large) - DBF = sc.where(mid, x / (1 - sc.exp(-x)), DBF) # zeros in x are handled by SMALL_THRESHOLD + DBF = sc.where( + mid, x / (1 - sc.exp(-x)), DBF + ) # zeros in x are handled by SMALL_THRESHOLD # if not divide_by_temperature: @@ -182,12 +190,12 @@ def _convert_to_scipp_variable( units. Args: - value (int | float | list | np.ndarray | Parameter | - sc.Variable): The value to convert. Can be a number, list, - numpy array, Parameter, or scipp Variable. If a number or - list, the unit must be specified in the unit argument. + value (int | float | list | np.ndarray | Parameter | sc.Variable): + The value to convert. Can be a number, list, numpy array, + Parameter, or scipp Variable. If a number or list, the unit + must be specified in the unit argument. name (str): The name of the variable, used for error messages. - unit (str | None): The unit to use if value is a number or list. + unit (str | None, default=None): The unit to use if value is a number or list. Must be specified if value is a number or list. Ignored if value is a Parameter or sc.Variable, which have their own units. @@ -195,8 +203,6 @@ def _convert_to_scipp_variable( Raises: TypeError: If value is not one of the accepted types, or if unit is not a string when needed. - ValueError: If value is a number or list and unit is not - provided. UnitError: If the provided unit is invalid. Returns: @@ -217,11 +223,13 @@ def _convert_to_scipp_variable( array_value = np.array(value.value) unit = value.unit else: - if name == 'energy': - raise TypeError(f'{name} must be a number, list, numpy array or scipp Variable') + if name == "energy": + raise TypeError( + f"{name} must be a number, list, numpy array or scipp Variable" + ) else: raise TypeError( - f'{name} must be a number, list, numpy array, Parameter or scipp Variable' + f"{name} must be a number, list, numpy array, Parameter or scipp Variable" ) # Create appropriate scipp variable based on shape @@ -234,6 +242,6 @@ def _convert_to_scipp_variable( else: # Multi-element array try: - return sc.array(dims=['x'], values=array_value, unit=unit) + return sc.array(dims=["x"], values=array_value, unit=unit) except UnitError as e: raise UnitError(f"Invalid unit string '{unit}' for {name}: {e}") diff --git a/src/easydynamics/utils/utils.py b/src/easydynamics/utils/utils.py index 03965c6e9..0ef80f1fd 100644 --- a/src/easydynamics/utils/utils.py +++ b/src/easydynamics/utils/utils.py @@ -11,20 +11,28 @@ energy_type = np.ndarray | Numeric | list | ArrayLike | sc.Variable -def _validate_and_convert_Q(Q: Q_type | None) -> np.ndarray | None: +def _validate_and_convert_Q( + Q: np.ndarray | Numeric | list | ArrayLike | sc.Variable | None, +) -> np.ndarray | None: """Validate and convert Q to a numpy array. Args: - Q (Number, list, np.ndarray or sc.Variable): Scattering vector - values in 1/angstrom. + Q (np.ndarray | Numeric | list | ArrayLike | sc.Variable | None): + Scattering vector values in 1/angstrom. Returns: - np.ndarray: Q as a np.ndarray. + np.ndarray | None: Q as a np.ndarray or None if Q is None. + + Raises: + TypeError: If Q is not a number, list, numpy array, or scipp + Variable. + ValueError: If Q is a numpy array with more than 1 dimension, or + if Q is a scipp Variable that does not have a single dimension named 'Q'. """ if Q is None: return None - if not isinstance(Q, (Numeric, list, np.ndarray, sc.Variable)): - raise TypeError('Q must be a number, list, numpy array, or scipp array.') + if not isinstance(Q, (np.ndarray | Numeric | list | ArrayLike | sc.Variable)): + raise TypeError('Q must be a number, list, numpy array, or scipp Variable.') if isinstance(Q, Numeric): Q = np.array([Q]) From 496cd583b27a0adc4b096650d10a6cebcb961fe7 Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Fri, 13 Mar 2026 14:42:45 +0100 Subject: [PATCH 03/31] Fix sample_model --- src/easydynamics/sample_model/sample_model.py | 149 ++++++++---------- 1 file changed, 66 insertions(+), 83 deletions(-) diff --git a/src/easydynamics/sample_model/sample_model.py b/src/easydynamics/sample_model/sample_model.py index a624ac586..1e3acc5e7 100644 --- a/src/easydynamics/sample_model/sample_model.py +++ b/src/easydynamics/sample_model/sample_model.py @@ -5,7 +5,9 @@ import scipp as sc from easyscience.variable import Parameter -from easydynamics.sample_model.diffusion_model.diffusion_model_base import DiffusionModelBase +from easydynamics.sample_model.diffusion_model.diffusion_model_base import ( + DiffusionModelBase, +) from easydynamics.sample_model.model_base import ModelBase from easydynamics.utils import _detailed_balance_factor from easydynamics.utils.utils import Numeric @@ -22,85 +24,48 @@ class SampleModel(ModelBase): components from the base model and diffusion models. Applies detailed balancing based on temperature if provided. - - Args: - display_name (str): Display name of the model. - unique_name (str | None): Unique name of the model. If None, a - unique name will be generated. - unit (str | sc.Unit | None): Unit of the model. If None, - defaults to "meV". - components (ModelComponent | ComponentCollection | None): - Template components of the model. If None, no components are - added. These components are copied into ComponentCollections - for each Q value. - Q (Number, list, np.ndarray, sc.array | None): - Q values for the model. If None, Q is not set. - diffusion_models (DiffusionModelBase | list[DiffusionModelBase] | None): - Diffusion models to include in the SampleModel. - If None, no diffusion models are added. - temperature (float | None): Temperature for detailed balancing. - If None, no detailed balancing is applied. - temperature_unit (str | sc.Unit): Unit of the temperature. - Defaults to "K". - divide_by_temperature (bool): Whether to divide the detailed - balance factor by temperature. Defaults to True. - - Attributes: - unit (str | sc.Unit): Unit of the model. - components (list[ModelComponent]): List of ModelComponents in - the model. - Q (np.ndarray | Numeric | list | ArrayLike | sc.Variable | None): - Q values of the model. - diffusion_models (list[DiffusionModelBase]): List of diffusion - models in the SampleModel. - temperature (Parameter | None): Temperature Parameter for - detailed balancing, or None if not set. - divide_by_temperature (bool): Whether to divide the detailed - balance factor by temperature. """ def __init__( self, - display_name: str = 'MySampleModel', + display_name: str = "MySampleModel", unique_name: str | None = None, - unit: str | sc.Unit = 'meV', - components: ComponentCollection | ModelComponent | None = None, + unit: str | sc.Unit = "meV", + components: ModelComponent | ComponentCollection | None = None, Q: Q_type | None = None, diffusion_models: DiffusionModelBase | list[DiffusionModelBase] | None = None, temperature: float | None = None, - temperature_unit: str | sc.Unit = 'K', + temperature_unit: str | sc.Unit = "K", divide_by_temperature: bool = True, ): """Initialize the SampleModel. Args: - display_name (str): Display name of the model. - unique_name (str | None): Unique name of the model. If None, + display_name (str, default="MySampleModel"): Display name of the model. + unique_name (str | None, default=None): Unique name of the model. If None, a unique name will be generated. - unit (str | sc.Unit | None): Unit of the model. If None, + unit (str | sc.Unit, default="meV"): Unit of the model. If None, defaults to "meV". - components (ModelComponent | ComponentCollection | None): + components (ModelComponent | ComponentCollection | None, default=None): Template components of the model. If None, no components are added. These components are copied into ComponentCollections for each Q value. - Q (Number, list, np.ndarray, sc.array | None): + Q (Q_type | None, default=None): Q values for the model. If None, Q is not set. - diffusion_models (DiffusionModelBase | list[DiffusionModelBase] | None): + diffusion_models (DiffusionModelBase | list[DiffusionModelBase] | None, default=None): Diffusion models to include in the SampleModel. If None, no diffusion models are added. - temperature (float | None): Temperature for detailed + temperature (float | None, default=None): Temperature for detailed balancing. If None, no detailed balancing is applied. - temperature_unit (str | sc.Unit): Unit of the temperature. + temperature_unit (str | sc.Unit, default="K"): Unit of the temperature. Defaults to "K". - divide_by_temperature (bool): Whether to divide the detailed + divide_by_temperature (bool, default=True): Whether to divide the detailed balance factor by temperature. Defaults to True. Raises: TypeError: If diffusion_models is not a DiffusionModelBase, - a list of DiffusionModelBase, or None. - TypeError: If temperature is not a number or None. + a list of DiffusionModelBase, or None, or if temperature is not a number or None, or if divide_by_temperature is not a bool. ValueError: If temperature is negative. - TypeError: If divide_by_temperature is not a bool. """ if diffusion_models is None: self._diffusion_models = [] @@ -111,8 +76,8 @@ def __init__( isinstance(dm, DiffusionModelBase) for dm in diffusion_models ): raise TypeError( - 'diffusion_models must be a DiffusionModelBase, ' - 'a list of DiffusionModelBase or None' + "diffusion_models must be a DiffusionModelBase, " + "a list of DiffusionModelBase or None" ) self._diffusion_models = diffusion_models @@ -128,21 +93,21 @@ def __init__( self._temperature = None else: if not isinstance(temperature, Numeric): - raise TypeError('temperature must be a number or None') + raise TypeError("temperature must be a number or None") if temperature < 0: - raise ValueError('temperature must be non-negative') + raise ValueError("temperature must be non-negative") self._temperature = Parameter( - name='Temperature', + name="Temperature", value=temperature, unit=temperature_unit, - display_name='Temperature', + display_name="Temperature", fixed=True, ) self._temperature_unit = temperature_unit if not isinstance(divide_by_temperature, bool): - raise TypeError('divide_by_temperature must be True or False') + raise TypeError("divide_by_temperature must be True or False") self._divide_by_temperature = divide_by_temperature # ------------------------------------------------------------------ @@ -163,13 +128,13 @@ def append_diffusion_model(self, diffusion_model: DiffusionModelBase) -> None: if not isinstance(diffusion_model, DiffusionModelBase): raise TypeError( - f'diffusion_model must be a DiffusionModelBase, got {type(diffusion_model).__name__}' # noqa: E501 + f"diffusion_model must be a DiffusionModelBase, got {type(diffusion_model).__name__}" # noqa: E501 ) self._diffusion_models.append(diffusion_model) self._generate_component_collections() - def remove_diffusion_model(self, name: 'str') -> None: + def remove_diffusion_model(self, name: "str") -> None: """Remove a DiffusionModel from the SampleModel by unique name. Args: @@ -185,8 +150,8 @@ def remove_diffusion_model(self, name: 'str') -> None: self._generate_component_collections() return raise ValueError( - f'No DiffusionModel with unique name {name} found. \n' - f'The available unique names are: {[dm.unique_name for dm in self._diffusion_models]}' + f"No DiffusionModel with unique name {name} found. \n" + f"The available unique names are: {[dm.unique_name for dm in self._diffusion_models]}" ) def clear_diffusion_models(self) -> None: @@ -215,11 +180,14 @@ def diffusion_models( """Set the diffusion models of the SampleModel. Args: - value (DiffusionModelBase | list[DiffusionModelBase] | - None): + value (DiffusionModelBase | list[DiffusionModelBase] | None): The diffusion model(s) to set. Can be a single DiffusionModelBase, a list of DiffusionModelBase, or None to clear all diffusion models. + + Raises: + TypeError: If value is not a DiffusionModelBase, a list of + DiffusionModelBase, or None. """ if value is None: @@ -232,8 +200,8 @@ def diffusion_models( isinstance(dm, DiffusionModelBase) for dm in value ): raise TypeError( - 'diffusion_models must be a DiffusionModelBase, a list of DiffusionModelBase, ' - 'or None' + "diffusion_models must be a DiffusionModelBase, a list of DiffusionModelBase, " + "or None" ) self._diffusion_models = value self._on_diffusion_models_change() @@ -255,23 +223,27 @@ def temperature(self, value: Numeric | None) -> None: Args: value (Numeric | None): The temperature value to set. Can be a number or None to unset the temperature. + + Raises: + TypeError: If value is not a number or None. + ValueError: If value is negative. """ if value is None: self._temperature = None return if not isinstance(value, Numeric): - raise TypeError('temperature must be a number or None') + raise TypeError("temperature must be a number or None") if value < 0: - raise ValueError('temperature must be non-negative') + raise ValueError("temperature must be non-negative") if self._temperature is None: self._temperature = Parameter( - name='Temperature', + name="Temperature", value=value, unit=self._temperature_unit, - display_name='Temperature', + display_name="Temperature", fixed=True, ) else: @@ -299,8 +271,8 @@ def temperature_unit(self, value: str | sc.Unit) -> None: """ raise AttributeError( - f'Temperature_unit is read-only. Use convert_temperature_unit to change the unit between allowed types ' # noqa: E501 - f'or create a new {self.__class__.__name__} with the desired unit.' + f"Temperature_unit is read-only. Use convert_temperature_unit to change the unit between allowed types " # noqa: E501 + f"or create a new {self.__class__.__name__} with the desired unit." ) # noqa: E501 def convert_temperature_unit(self, unit: str | sc.Unit) -> None: @@ -312,10 +284,11 @@ def convert_temperature_unit(self, unit: str | sc.Unit) -> None: Raises: ValueError: If temperature is not set or conversion fails. + Exception: If the provided unit is invalid or cannot be converted. """ if self._temperature is None: - raise ValueError('Temperature is not set, cannot convert unit.') + raise ValueError("Temperature is not set, cannot convert unit.") old_unit = self._temperature.unit @@ -349,9 +322,12 @@ def divide_by_temperature(self, value: bool) -> None: Args: value (bool): True to divide the detailed balance factor by temperature, False otherwise. + + Raises: + TypeError: If value is not a bool. """ if not isinstance(value, bool): - raise TypeError('divide_by_temperature must be True or False') + raise TypeError("divide_by_temperature must be True or False") self._divide_by_temperature = value # ------------------------------------------------------------------ @@ -394,9 +370,12 @@ def get_all_variables(self, Q_index: int | None = None) -> list[Parameter]: self._components as these are just templates. Args: - Q_index (int | None): If specified, only get variables from + Q_index (int | None, default=None): If specified, only get variables from the ComponentCollection at the given Q index. If None, get variables from all ComponentCollections. + + Returns: + list[Parameter]: List of all Parameters and Descriptors, including temperature if set and all variables from diffusion models. """ all_vars = super().get_all_variables(Q_index=Q_index) @@ -425,8 +404,12 @@ def _generate_component_collections(self) -> None: # Generate components from diffusion models # and add to component collections for diffusion_model in self._diffusion_models: - diffusion_collections = diffusion_model.create_component_collections(Q=self._Q) - for target, source in zip(self._component_collections, diffusion_collections): + diffusion_collections = diffusion_model.create_component_collections( + Q=self._Q + ) + for target, source in zip( + self._component_collections, diffusion_collections + ): for component in source.components: target.append_component(component) @@ -446,9 +429,9 @@ def __repr__(self) -> str: """ return ( - f'{self.__class__.__name__}(unique_name={self.unique_name}, unit={self._unit}), ' - f'Q = {self._Q}, ' - f'components = {self._components}, diffusion_models = {self._diffusion_models}, ' - f'temperature = {self._temperature}, ' - f'divide_by_temperature = {self._divide_by_temperature}' + f"{self.__class__.__name__}(unique_name={self.unique_name}, unit={self._unit}), " + f"Q = {self._Q}, " + f"components = {self._components}, diffusion_models = {self._diffusion_models}, " + f"temperature = {self._temperature}, " + f"divide_by_temperature = {self._divide_by_temperature}" ) From bf77cd48dc460fa9f615718b83e283cc8e16c3af Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Fri, 13 Mar 2026 14:44:36 +0100 Subject: [PATCH 04/31] fix resolution_model --- .../sample_model/resolution_model.py | 44 ++++--------------- 1 file changed, 9 insertions(+), 35 deletions(-) diff --git a/src/easydynamics/sample_model/resolution_model.py b/src/easydynamics/sample_model/resolution_model.py index c1913a3b3..3d2f828b7 100644 --- a/src/easydynamics/sample_model/resolution_model.py +++ b/src/easydynamics/sample_model/resolution_model.py @@ -15,55 +15,29 @@ class ResolutionModel(ModelBase): """ResolutionModel represents a model of the instrment resolution in an experiment at various Q. - - Args: - display_name (str): Display name of the model. - unique_name (str | None): Unique name of the model. If None, a - unique name will be generated. - unit (str | sc.Unit | None): Unit of the model. Defaults to - "meV". - components (ModelComponent | ComponentCollection | None): - Template components of the model. If None, no components - are added. These components are copied into - ComponentCollections for each Q value. - Q (Q_type | None): Q values for the model. If None, Q is not - set. - - Attributes: - unit (str | sc.Unit): Unit of the model. - components (list[ModelComponent]): List of ModelComponents in - the model. - Q (np.ndarray | Numeric | list | ArrayLike | sc.Variable | None): - Q values of the model. """ def __init__( self, - display_name: str = 'MyResolutionModel', + display_name: str = "MyResolutionModel", unique_name: str | None = None, - unit: str | sc.Unit = 'meV', - components: ComponentCollection | ModelComponent | None = None, + unit: str | sc.Unit = "meV", + components: ModelComponent | ComponentCollection | None = None, Q: Q_type | None = None, ): """Initialize a ResolutionModel. Args: - display_name (str): Display name of the model. - unique_name (str | None): Unique name of the model. If None, + display_name (str, default="MyResolutionModel"): Display name of the model. + unique_name (str | None, default=None): Unique name of the model. If None, a unique name will be generated. - unit (str | sc.Unit | None): Unit of the model. Defaults to - "meV". - components (ModelComponent | ComponentCollection | None): + unit (str | sc.Unit, default="meV"): Unit of the model. + components (ModelComponent | ComponentCollection | None, default=None): Template components of the model. If None, no components are added. These components are copied into ComponentCollections for each Q value. - Q (Q_type | None): Q values for the model. If None, Q is not + Q (Q_type | None, default=None): Q values for the model. If None, Q is not set. - - Raises: - TypeError: If components is not a ModelComponent or - ComponentCollection. - ValueError: If Q is not a valid Q_type. """ super().__init__( @@ -95,7 +69,7 @@ def append_component(self, component: ModelComponent | ComponentCollection) -> N for comp in components: if isinstance(comp, (DeltaFunction, Polynomial)): raise TypeError( - f'Component in ResolutionModel cannot be a {comp.__class__.__name__}' + f"Component in ResolutionModel cannot be a {comp.__class__.__name__}" ) super().append_component(component) From b8917c78d098155c2500e3a2c56abbe247a8985e Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Fri, 13 Mar 2026 14:51:50 +0100 Subject: [PATCH 05/31] fix model_base --- src/easydynamics/sample_model/model_base.py | 101 ++++++++++---------- 1 file changed, 49 insertions(+), 52 deletions(-) diff --git a/src/easydynamics/sample_model/model_base.py b/src/easydynamics/sample_model/model_base.py index c78cef357..dfa52cbfc 100644 --- a/src/easydynamics/sample_model/model_base.py +++ b/src/easydynamics/sample_model/model_base.py @@ -22,48 +22,32 @@ class ModelBase(EasyScienceModelBase): Contains common functionality for models with components and Q dependence. - Args: - display_name (str): Display name of the model. - unique_name (str | None): Unique name of the model. If None, a - unique name will be generated. - unit (str | sc.Unit | None): Unit of the model. Defaults to - "meV". - components (ModelComponent | ComponentCollection | None): - Template components of the model. If None, no components - are added. These components are copied into - ComponentCollections for each Q value. - Q (ArrayLike | sc.Variable | None): Q values for the model. If - None, Q is not set. - - Attributes: - unit (str | sc.Unit): Unit of the model. - components (list[ModelComponent]): List of ModelComponents in - the model. - Q (ArrayLike | sc.Variable | None): Q values of the model. """ def __init__( self, - display_name: str = 'MyModelBase', + display_name: str = "MyModelBase", unique_name: str | None = None, - unit: str | sc.Unit | None = 'meV', + unit: str | sc.Unit | None = "meV", components: ModelComponent | ComponentCollection | None = None, Q: Q_type | None = None, ): """Initialize the ModelBase. Args: - display_name (str): Display name of the model. - unique_name (str | None): Unique name of the model. If None, + display_name (str, default="MyModelBase"): Display name of the model. + unique_name (str | None, default=None): Unique name of the model. If None, a unique name will be generated. - unit (str | sc.Unit | None): Unit of the model. Defaults to - "meV". - components (ModelComponent | ComponentCollection | None): + unit (str | sc.Unit | None, default="meV"): Unit of the model. + components (ModelComponent | ComponentCollection | None, default=None): Template components of the model. If None, no components are added. These components are copied into ComponentCollections for each Q value. - Q (ArrayLike | sc.Variable | None): Q values for the model. + Q (Q_type | None, default=None): Q values for the model. If None, Q is not set. + + Raises: + TypeError: If components is not a ModelComponent or ComponentCollection. """ super().__init__( display_name=display_name, @@ -76,8 +60,8 @@ def __init__( components, (ModelComponent, ComponentCollection) ): raise TypeError( - f'Components must be a ModelComponent, a ComponentCollection or None, ' - f'got {type(components).__name__}' + f"Components must be a ModelComponent, a ComponentCollection or None, " + f"got {type(components).__name__}" ) self._components = ComponentCollection() @@ -92,11 +76,11 @@ def evaluate( """Evaluate the sample model at all Q for the given x values. Args: - x (Numeric | list | np.ndarray | sc.Variable | sc.DataArray): - Energy axis values to evaluate the model at. If a scipp - Variable or DataArray is provided, the unit of the model - will be converted to match the unit of x for evaluation, and - the result will be returned in the same unit as x. + x (Numeric | list | np.ndarray | sc.Variable | sc.DataArray): + Energy axis values to evaluate the model at. If a scipp + Variable or DataArray is provided, the unit of the model + will be converted to match the unit of x for evaluation, and + the result will be returned in the same unit as x. Returns: list[np.ndarray]: A list of numpy arrays containing the @@ -110,8 +94,8 @@ def evaluate( if not self._component_collections: raise ValueError( - 'No components in the model to evaluate. ' - 'Run generate_component_collections() first' + "No components in the model to evaluate. " + "Run generate_component_collections() first" ) y = [collection.evaluate(x) for collection in self._component_collections] @@ -152,11 +136,11 @@ def clear_components(self) -> None: # ------------------------------------------------------------------ @property - def unit(self) -> str | sc.Unit: + def unit(self) -> str | sc.Unit | None: """Get the unit of the ComponentCollection. Returns: - str | sc.Unit |None: The unit of the ComponentCollection. + str | sc.Unit | None: The unit of the ComponentCollection. """ return self._unit @@ -174,8 +158,8 @@ def unit(self, unit_str: str) -> None: """ raise AttributeError( ( - f'Unit is read-only. Use convert_unit to change the unit between allowed types ' - f'or create a new {self.__class__.__name__} with the desired unit.' + f"Unit is read-only. Use convert_unit to change the unit between allowed types " + f"or create a new {self.__class__.__name__} with the desired unit." ) ) # noqa: E501 @@ -188,12 +172,16 @@ def convert_unit(self, unit: str | sc.Unit) -> None: Raises: TypeError: If the provided unit is not a string or sc.Unit. - UnitError: If the provided unit is not compatible with the + Exception: If the provided unit is not compatible with the current unit. """ old_unit = self._unit + if not isinstance(unit, (str, sc.Unit)): + raise TypeError( + f"Unit must be a string or sc.Unit, got {type(unit).__name__}" + ) try: for component in self.components: component.convert_unit(unit) @@ -223,14 +211,16 @@ def components(self, value: ModelComponent | ComponentCollection | None) -> None Args: value (ModelComponent | ComponentCollection | None): The new - components to set. If None, all components will be cleared. + components to set. If None, all components will be cleared. Raises: TypeError: If value is not a ModelComponent, ComponentCollection, or None. """ if not isinstance(value, (ModelComponent, ComponentCollection, type(None))): - raise TypeError('Components must be a ModelComponent or a ComponentCollection') + raise TypeError( + "Components must be a ModelComponent or a ComponentCollection" + ) self.clear_components() if value is not None: @@ -252,7 +242,7 @@ def Q(self, value: Q_type | None) -> None: Args: value (Q_type | None): The new Q values to set. If None, Q - will be unset. + will be unset. """ old_Q = self._Q new_Q = _validate_and_convert_Q(value) @@ -287,13 +277,18 @@ def get_all_variables(self, Q_index: int | None = None) -> list[Parameter]: templates. Args: - Q_index (int | None): If None, get variables for all + Q_index (int | None, default=None): If None, get variables for all ComponentCollections. If int, get variables for the ComponentCollection at this index. Defaults to None. Returns: list[Parameter]: A list of all Parameters and Descriptors from the ComponentCollections in the ModelBase. + + Raises: + TypeError: If Q_index is not an int or None. + IndexError: If Q_index is out of bounds for the number of + ComponentCollections. """ if Q_index is None: @@ -304,11 +299,13 @@ def get_all_variables(self, Q_index: int | None = None) -> list[Parameter]: ] else: if not isinstance(Q_index, int): - raise TypeError(f'Q_index must be an int or None, got {type(Q_index).__name__}') + raise TypeError( + f"Q_index must be an int or None, got {type(Q_index).__name__}" + ) if Q_index < 0 or Q_index >= len(self._component_collections): raise IndexError( - f'Q_index {Q_index} is out of bounds for component collections ' - f'of length {len(self._component_collections)}' + f"Q_index {Q_index} is out of bounds for component collections " + f"of length {len(self._component_collections)}" ) all_vars = self._component_collections[Q_index].get_all_variables() return all_vars @@ -329,11 +326,11 @@ def get_component_collection(self, Q_index: int) -> ComponentCollection: ComponentCollections. """ if not isinstance(Q_index, int): - raise TypeError(f'Q_index must be an int, got {type(Q_index).__name__}') + raise TypeError(f"Q_index must be an int, got {type(Q_index).__name__}") if Q_index < 0 or Q_index >= len(self._component_collections): raise IndexError( - f'Q_index {Q_index} is out of bounds for component collections ' - f'of length {len(self._component_collections)}' + f"Q_index {Q_index} is out of bounds for component collections " + f"of length {len(self._component_collections)}" ) return self._component_collections[Q_index] @@ -371,6 +368,6 @@ def __repr__(self) -> str: str: A string representation of the ModelBase. """ return ( - f'{self.__class__.__name__}(unique_name={self.unique_name}, ' - f'unit={self.unit}), Q = {self.Q}, components = {self.components}' + f"{self.__class__.__name__}(unique_name={self.unique_name}, " + f"unit={self.unit}), Q = {self.Q}, components = {self.components}" ) From 2d59670207769ffd58b254c2ea249553c0b3c715 Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Fri, 13 Mar 2026 14:57:46 +0100 Subject: [PATCH 06/31] Fix instrument_model --- .../sample_model/instrument_model.py | 139 +++++++----------- 1 file changed, 52 insertions(+), 87 deletions(-) diff --git a/src/easydynamics/sample_model/instrument_model.py b/src/easydynamics/sample_model/instrument_model.py index 89ae38884..f8cdf812e 100644 --- a/src/easydynamics/sample_model/instrument_model.py +++ b/src/easydynamics/sample_model/instrument_model.py @@ -21,79 +21,42 @@ class InstrumentModel(NewBase): experiment at various Q. It can contain a model of the resolution function for convolutions, of the background and an offset in the energy axis. - - Args: - display_name (str | None): The display name of the - InstrumentModel. Default is "MyInstrumentModel". - unique_name (str | None): The unique name of the - InstrumentModel. Default is None. - Q (np.ndarray | list | sc.Variable | None): The Q values where - the instrument is modelled. - resolution_model (ResolutionModel | None): The resolution model - of the instrument. If None, an empty resolution model is - created and no resolution convolution is carried out. - Default is None. - background_model (BackgroundModel | None): The background model - of the instrument. If None, an empty background model is - created, and the background evaluates to 0. Default is None. - energy_offset (float | int | None): Template energy offset of - the instrument. Will be copied to each Q value. If None, the - energy offset will be 0. Default is None. - unit (str | sc.Unit): The unit of the energy axis. Default is - 'meV'. - - Attributes: - resolution_model (ResolutionModel): The resolution model of the - instrument. - background_model (BackgroundModel): The background model of the - instrument. - Q (np.ndarray | None): The Q values where the instrument is - modelled. - energy_offset (Parameter): The template energy offset Parameter - of the instrument. Will be copied to each Q value. - unit (str | sc.Unit): The unit of the energy axis. """ def __init__( self, - display_name: str = 'MyInstrumentModel', + display_name: str = "MyInstrumentModel", unique_name: str | None = None, Q: Q_type | None = None, resolution_model: ResolutionModel | None = None, background_model: BackgroundModel | None = None, energy_offset: Numeric | None = None, - unit: str | sc.Unit = 'meV', + unit: str | sc.Unit = "meV", ): """Initialize an InstrumentModel. Args: - display_name (str | None): The display name of the + display_name (str, default="MyInstrumentModel"): The display name of the InstrumentModel. Default is "MyInstrumentModel". - unique_name (str | None): The unique name of the - InstrumentModel. Default is None. - Q (np.ndarray | list | sc.Variable | None): The Q values - where the instrument is modelled. - resolution_model (ResolutionModel | None): The resolution + unique_name (str | None, default=None): The unique name of the + InstrumentModel. + Q (Q_type | None, default=None): The Q values where the instrument is modelled. + resolution_model (ResolutionModel | None, default=None): The resolution model of the instrument. If None, an empty resolution model is created and no resolution convolution is - carried out. Default is None. - background_model (BackgroundModel | None): The background + carried out. + background_model (BackgroundModel | None, default=None): The background model of the instrument. If None, an empty background model is created, and the background evaluates to 0. - Default is None. - energy_offset (float | int | None): Template energy offset + energy_offset (Numeric | None, default=None): Template energy offset of the instrument. Will be copied to each Q value. If - None, the energy offset will be 0. Default is None. - unit (str | sc.Unit): The unit of the energy axis. Default - is 'meV'. + None, the energy offset will be 0. + unit (str | sc.Unit, default="meV"): The unit of the energy axis. Raises: TypeError: If resolution_model is not a ResolutionModel or - None - TypeError: If background_model is not a BackgroundModel or - None - TypeError: If energy_offset is not a number or None - UnitError: If unit is not a valid unit string or scipp Unit. + None, or if background_model is not a BackgroundModel or None, or + if energy_offset is not a number or None. """ super().__init__( display_name=display_name, @@ -107,8 +70,8 @@ def __init__( else: if not isinstance(resolution_model, ResolutionModel): raise TypeError( - f'resolution_model must be a ResolutionModel or None, ' - f'got {type(resolution_model).__name__}' + f"resolution_model must be a ResolutionModel or None, " + f"got {type(resolution_model).__name__}" ) self._resolution_model = resolution_model @@ -117,8 +80,8 @@ def __init__( else: if not isinstance(background_model, BackgroundModel): raise TypeError( - f'background_model must be a BackgroundModel or None, ' - f'got {type(background_model).__name__}' + f"background_model must be a BackgroundModel or None, " + f"got {type(background_model).__name__}" ) self._background_model = background_model @@ -126,10 +89,10 @@ def __init__( energy_offset = 0.0 if not isinstance(energy_offset, Numeric): - raise TypeError('energy_offset must be a number or None') + raise TypeError("energy_offset must be a number or None") self._energy_offset = Parameter( - name='energy_offset', + name="energy_offset", value=float(energy_offset), unit=self.unit, fixed=False, @@ -151,7 +114,7 @@ def resolution_model(self) -> ResolutionModel: return self._resolution_model @resolution_model.setter - def resolution_model(self, value: ResolutionModel): + def resolution_model(self, value: ResolutionModel) -> None: """Set the resolution model of the instrument. Args: @@ -163,7 +126,7 @@ def resolution_model(self, value: ResolutionModel): """ if not isinstance(value, ResolutionModel): raise TypeError( - f'resolution_model must be a ResolutionModel, got {type(value).__name__}' + f"resolution_model must be a ResolutionModel, got {type(value).__name__}" ) self._resolution_model = value self._on_resolution_model_change() @@ -179,7 +142,7 @@ def background_model(self) -> BackgroundModel: return self._background_model @background_model.setter - def background_model(self, value: BackgroundModel): + def background_model(self, value: BackgroundModel) -> None: """Set the background model of the instrument. Args: @@ -192,7 +155,7 @@ def background_model(self, value: BackgroundModel): if not isinstance(value, BackgroundModel): raise TypeError( - f'background_model must be a BackgroundModel, got {type(value).__name__}' + f"background_model must be a BackgroundModel, got {type(value).__name__}" ) self._background_model = value self._on_background_model_change() @@ -202,7 +165,7 @@ def Q(self) -> np.ndarray | None: """Get the Q values of the InstrumentModel. Returns: - np.ndarray or None: The Q values of the InstrumentModel, or + np.ndarray | None: The Q values of the InstrumentModel, or None if not set """ return self._Q @@ -213,20 +176,17 @@ def Q(self, value: Q_type | None) -> None: Args: value (Q_type | None): The new Q values for the - InstrumentModel. - - Raises: - TypeError: If value is not a valid Q_type or None. + InstrumentModel. """ self._Q = _validate_and_convert_Q(value) self._on_Q_change() @property - def unit(self) -> sc.Unit: + def unit(self) -> str | sc.Unit: """Get the unit of the InstrumentModel. Returns: - (str | sc.Unit): The unit of the InstrumentModel. + str | sc.Unit: The unit of the InstrumentModel. """ return self._unit @@ -246,8 +206,8 @@ def unit(self, unit_str: str) -> None: """ raise AttributeError( ( - f'Unit is read-only. Use convert_unit to change the unit between allowed types ' - f'or create a new {self.__class__.__name__} with the desired unit.' + f"Unit is read-only. Use convert_unit to change the unit between allowed types " + f"or create a new {self.__class__.__name__} with the desired unit." ) ) # noqa: E501 @@ -263,18 +223,20 @@ def energy_offset(self) -> Parameter: return self._energy_offset @energy_offset.setter - def energy_offset(self, value: Numeric): + def energy_offset(self, value: Numeric) -> None: """Set the offset parameter of the instrument model. Args: - value (float | int): The new value for the energy offset + value (Numeric): The new value for the energy offset parameter. Will be copied to all Q values. Raises: TypeError: If value is not a number. """ if not isinstance(value, Numeric): - raise TypeError(f'energy_offset must be a number, got {type(value).__name__}') + raise TypeError( + f"energy_offset must be a number, got {type(value).__name__}" + ) self._energy_offset.value = value self._on_energy_offset_change() @@ -290,13 +252,12 @@ def convert_unit(self, unit_str: str | sc.Unit) -> None: unit_str (str | sc.Unit): The unit to convert to. Raises: - TypeError: If unit_str is not a string or scipp Unit. ValueError: If unit_str is not a valid unit string or scipp Unit. """ unit = _validate_unit(unit_str) if unit is None: - raise ValueError('unit_str must be a valid unit string or scipp Unit') + raise ValueError("unit_str must be a valid unit string or scipp Unit") self._background_model.convert_unit(unit) self._resolution_model.convert_unit(unit) @@ -310,7 +271,7 @@ def get_all_variables(self, Q_index: int | None = None) -> list[Parameter]: """Get all variables in the InstrumentModel. Args: - Q_index (int | None): The index of the Q value to get + Q_index (int | None, default=None): The index of the Q value to get variables for. If None, get variables for all Q values. Returns: @@ -332,10 +293,12 @@ def get_all_variables(self, Q_index: int | None = None) -> list[Parameter]: variables = [self._energy_offsets[i] for i in range(len(self._Q))] else: if not isinstance(Q_index, int): - raise TypeError(f'Q_index must be an int or None, got {type(Q_index).__name__}') + raise TypeError( + f"Q_index must be an int or None, got {type(Q_index).__name__}" + ) if Q_index < 0 or Q_index >= len(self._Q): raise IndexError( - f'Q_index {Q_index} is out of bounds for Q of length {len(self._Q)}' + f"Q_index {Q_index} is out of bounds for Q of length {len(self._Q)}" ) variables = [self._energy_offsets[Q_index]] @@ -368,10 +331,12 @@ def get_energy_offset_at_Q(self, Q_index: int) -> Parameter: IndexError: If Q_index is out of bounds. """ if self._Q is None: - raise ValueError('No Q values are set in the InstrumentModel.') + raise ValueError("No Q values are set in the InstrumentModel.") if Q_index < 0 or Q_index >= len(self._Q): - raise IndexError(f'Q_index {Q_index} is out of bounds for Q of length {len(self._Q)}') + raise IndexError( + f"Q_index {Q_index} is out of bounds for Q of length {len(self._Q)}" + ) return self._energy_offsets[Q_index] @@ -418,11 +383,11 @@ def __repr__(self) -> str: """ return ( - f'{self.__class__.__name__}(' - f'unique_name={self.unique_name!r}, ' - f'unit={self.unit}, ' - f'Q_len={None if self._Q is None else len(self._Q)}, ' - f'resolution_model={self._resolution_model!r}, ' - f'background_model={self._background_model!r}' - f')' + f"{self.__class__.__name__}(" + f"unique_name={self.unique_name!r}, " + f"unit={self.unit}, " + f"Q_len={None if self._Q is None else len(self._Q)}, " + f"resolution_model={self._resolution_model!r}, " + f"background_model={self._background_model!r}" + f")" ) From 64236c79979dfdc6d129559458477c5d5f55360b Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Fri, 13 Mar 2026 15:00:34 +0100 Subject: [PATCH 07/31] fix jump_diffusion --- .../jump_translational_diffusion.py | 115 ++++++++---------- 1 file changed, 50 insertions(+), 65 deletions(-) diff --git a/src/easydynamics/sample_model/diffusion_model/jump_translational_diffusion.py b/src/easydynamics/sample_model/diffusion_model/jump_translational_diffusion.py index 3ee24a998..ecdb2b9cd 100644 --- a/src/easydynamics/sample_model/diffusion_model/jump_translational_diffusion.py +++ b/src/easydynamics/sample_model/diffusion_model/jump_translational_diffusion.py @@ -12,7 +12,9 @@ from easydynamics.sample_model.component_collection import ComponentCollection from easydynamics.sample_model.components import Lorentzian -from easydynamics.sample_model.diffusion_model.diffusion_model_base import DiffusionModelBase +from easydynamics.sample_model.diffusion_model.diffusion_model_base import ( + DiffusionModelBase, +) from easydynamics.utils.utils import Numeric from easydynamics.utils.utils import Q_type from easydynamics.utils.utils import _validate_and_convert_Q @@ -30,27 +32,7 @@ class JumpTranslationalDiffusion(DiffusionModelBase): time. Q is assumed to have units of 1/angstrom. Creates ComponentCollections with Lorentzian components for given Q-values. - Args: - display_name (str): Display name of the diffusion model. - unique_name (str | None): Unique name of the diffusion model. If - None, a unique name will be generated. - unit (str | sc.Unit): Unit of the diffusion model. Must be - convertible to meV. Defaults to "meV". - scale (Numeric): Scale factor for the diffusion model. Must be - a non-negative number. Defaults to 1.0. - diffusion_coefficient (Numeric): Diffusion coefficient D in - m^2/s. Defaults to 1.0. - relaxation_time (Numeric): Relaxation time t in ps. Defaults to - 1.0. - - Attributes: - unit (str | sc.Unit): Unit of the diffusion model. - scale (Parameter): Scale parameter of the diffusion model. - diffusion_coefficient (Parameter): Diffusion coefficient D in - m^2/s. - relaxation_time (Parameter): Relaxation time t in ps. - - Example usage: + Example: >>> Q = np.linspace(0.5, 2, 7) >>> energy = np.linspace(-2, 2, 501) >>> scale = 1.0 @@ -61,14 +43,14 @@ class JumpTranslationalDiffusion(DiffusionModelBase): >>> relaxation_time=relaxation_time) >>> component_collections= >>> diffusion_model.create_component_collections(Q) - See also the examples. + See also the tutorials.. """ def __init__( self, - display_name: str | None = 'JumpTranslationalDiffusion', + display_name: str | None = "JumpTranslationalDiffusion", unique_name: str | None = None, - unit: str | sc.Unit = 'meV', + unit: str | sc.Unit = "meV", scale: Numeric = 1.0, diffusion_coefficient: Numeric = 1.0, relaxation_time: Numeric = 1.0, @@ -76,23 +58,20 @@ def __init__( """Initialize a new JumpTranslationalDiffusion model. Args: - display_name (str): Display name of the diffusion model. - unique_name (str | None): Unique name of the diffusion + display_name (str | None, default="JumpTranslationalDiffusion"): Display name of the diffusion model. + unique_name (str | None, default=None): Unique name of the diffusion model. If None, a unique name will be generated. - unit (str | sc.Unit): Unit of the diffusion model. Must be - convertible to meV. Defaults to "meV". - scale (Numeric): Scale factor for the diffusion model. Must - be a non-negative number. Defaults to 1.0. - diffusion_coefficient (Numeric): Diffusion coefficient D in - m^2/s. Defaults to 1.0. - relaxation_time (Numeric): Relaxation time t in ps. Defaults - to 1.0. + unit (str | sc.Unit, default="meV"): Unit of the diffusion model. Must be + convertible to meV. + scale (Numeric, default=1.0): Scale factor for the diffusion model. Must + be a non-negative number. + diffusion_coefficient (Numeric, default=1.0): Diffusion coefficient D in + m^2/s. + relaxation_time (Numeric, default=1.0): Relaxation time t in ps. Raises: TypeError: If scale, diffusion_coefficient, or relaxation_time are not numbers. - ValueError: If scale is negative. - UnitError: If unit is not a valid unit string or scipp Unit. """ super().__init__( display_name=display_name, @@ -102,27 +81,27 @@ def __init__( ) if not isinstance(diffusion_coefficient, Numeric): - raise TypeError('diffusion_coefficient must be a number.') + raise TypeError("diffusion_coefficient must be a number.") if not isinstance(relaxation_time, Numeric): - raise TypeError('relaxation_time must be a number.') + raise TypeError("relaxation_time must be a number.") diffusion_coefficient = Parameter( - name='diffusion_coefficient', + name="diffusion_coefficient", value=float(diffusion_coefficient), fixed=False, - unit='m**2/s', + unit="m**2/s", ) relaxation_time = Parameter( - name='relaxation_time', + name="relaxation_time", value=float(relaxation_time), fixed=False, - unit='ps', + unit="ps", ) - self._hbar = DescriptorNumber.from_scipp('hbar', scipp_hbar) - self._angstrom = DescriptorNumber('angstrom', 1e-10, unit='m') + self._hbar = DescriptorNumber.from_scipp("hbar", scipp_hbar) + self._angstrom = DescriptorNumber("angstrom", 1e-10, unit="m") self._diffusion_coefficient = diffusion_coefficient self._relaxation_time = relaxation_time @@ -152,9 +131,9 @@ def diffusion_coefficient(self, diffusion_coefficient: Numeric) -> None: ValueError: If diffusion_coefficient is negative. """ if not isinstance(diffusion_coefficient, Numeric): - raise TypeError('diffusion_coefficient must be a number.') + raise TypeError("diffusion_coefficient must be a number.") if float(diffusion_coefficient) < 0: - raise ValueError('diffusion_coefficient must be non-negative.') + raise ValueError("diffusion_coefficient must be non-negative.") self._diffusion_coefficient.value = float(diffusion_coefficient) @property @@ -178,10 +157,10 @@ def relaxation_time(self, relaxation_time: Numeric) -> None: ValueError: If relaxation_time is negative. """ if not isinstance(relaxation_time, Numeric): - raise TypeError('relaxation_time must be a number.') + raise TypeError("relaxation_time must be a number.") if float(relaxation_time) < 0: - raise ValueError('relaxation_time must be non-negative.') + raise ValueError("relaxation_time must be non-negative.") self._relaxation_time.value = float(relaxation_time) ################################ @@ -214,7 +193,7 @@ def calculate_width(self, Q: Q_type) -> np.ndarray: unit_conversion_factor_denominator = ( self.diffusion_coefficient / self._angstrom**2 * self.relaxation_time ) - unit_conversion_factor_denominator.convert_unit('dimensionless') + unit_conversion_factor_denominator.convert_unit("dimensionless") denominator = 1 + unit_conversion_factor_denominator.value * Q**2 @@ -252,7 +231,7 @@ def calculate_QISF(self, Q: Q_type) -> np.ndarray: def create_component_collections( self, Q: Q_type, - component_display_name: str = 'Jump translational diffusion', + component_display_name: str = "Jump translational diffusion", ) -> List[ComponentCollection]: """Create ComponentCollection components for the diffusion model at given Q values. @@ -260,7 +239,7 @@ def create_component_collections( Args: Q (Q_type): Scattering vector in 1/angstrom. Can be a single value or an array of values. - component_display_name (str): Name of the Jump Diffusion + component_display_name (str, default="Jump translational diffusion"): Name of the Jump Diffusion Lorentzian component. Returns: @@ -273,7 +252,7 @@ def create_component_collections( Q = _validate_and_convert_Q(Q) if not isinstance(component_display_name, str): - raise TypeError('component_name must be a string.') + raise TypeError("component_name must be a string.") component_collection_list = [None] * len(Q) # In more complex models, this is used to scale the area of the @@ -285,7 +264,7 @@ def create_component_collections( # is 0. for i, Q_value in enumerate(Q): component_collection_list[i] = ComponentCollection( - display_name=f'{self.display_name}_Q{Q_value:.2f}', unit=self.unit + display_name=f"{self.display_name}_Q{Q_value:.2f}", unit=self.unit ) lorentzian_component = Lorentzian( @@ -327,12 +306,15 @@ def _write_width_dependency_expression(self, Q: float) -> str: Returns: str: Dependency expression for the width. + + Raises: + TypeError: If Q is not a float. """ if not isinstance(Q, (float)): - raise TypeError('Q must be a float.') + raise TypeError("Q must be a float.") # Q is given as a float, so we need to add the units - return f'hbar * D* {Q} **2/(angstrom**2)/(1 + (D * t* {Q} **2/(angstrom**2)))' + return f"hbar * D* {Q} **2/(angstrom**2)/(1 + (D * t* {Q} **2/(angstrom**2)))" def _write_width_dependency_map_expression(self) -> Dict[str, DescriptorNumber]: """Write the dependency map expression to make dependent @@ -342,10 +324,10 @@ def _write_width_dependency_map_expression(self) -> Dict[str, DescriptorNumber]: Dict[str, DescriptorNumber]: Dependency map for the width. """ return { - 'D': self._diffusion_coefficient, - 't': self._relaxation_time, - 'hbar': self._hbar, - 'angstrom': self._angstrom, + "D": self._diffusion_coefficient, + "t": self._relaxation_time, + "hbar": self._hbar, + "angstrom": self._angstrom, } def _write_area_dependency_expression(self, QISF: float) -> str: @@ -357,12 +339,15 @@ def _write_area_dependency_expression(self, QISF: float) -> str: Returns: str: Dependency expression for the area. + + Raises: + TypeError: If QISF is not a float. """ if not isinstance(QISF, (float)): - raise TypeError('QISF must be a float.') + raise TypeError("QISF must be a float.") - return f'{QISF} * scale' + return f"{QISF} * scale" def _write_area_dependency_map_expression(self) -> Dict[str, DescriptorNumber]: """Write the dependency map expression to make dependent @@ -372,7 +357,7 @@ def _write_area_dependency_map_expression(self) -> Dict[str, DescriptorNumber]: Dict[str, DescriptorNumber]: Dependency map for the area. """ return { - 'scale': self._scale, + "scale": self._scale, } ################################ @@ -388,6 +373,6 @@ def __repr__(self) -> str: model. """ return ( - f'JumpTranslationalDiffusion(display_name={self.display_name}, ' - f'diffusion_coefficient={self._diffusion_coefficient}, scale={self._scale})' + f"JumpTranslationalDiffusion(display_name={self.display_name}, " + f"diffusion_coefficient={self._diffusion_coefficient}, scale={self._scale})" ) From d7caad17fea8f6b96a53b61580dc317d8168a73f Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Fri, 13 Mar 2026 15:02:04 +0100 Subject: [PATCH 08/31] diffusion model base --- .../diffusion_model/diffusion_model_base.py | 56 +++++++------------ 1 file changed, 21 insertions(+), 35 deletions(-) diff --git a/src/easydynamics/sample_model/diffusion_model/diffusion_model_base.py b/src/easydynamics/sample_model/diffusion_model/diffusion_model_base.py index 837f5271e..fb4a5705d 100644 --- a/src/easydynamics/sample_model/diffusion_model/diffusion_model_base.py +++ b/src/easydynamics/sample_model/diffusion_model/diffusion_model_base.py @@ -11,39 +11,25 @@ class DiffusionModelBase(ModelBase): - """Base class for constructing diffusion models. - - Args: - display_name (str): Display name of the diffusion model. - unique_name (str | None): Unique name of the diffusion model. - If None, a unique name will be generated. - scale (Numeric): Scale factor for the diffusion model. Must be a - non-negative number. Defaults to 1.0. - unit (str | sc.Unit): Unit of the diffusion model. Must be - convertible to meV. Defaults to "meV". - - Attributes: - unit (str | sc.Unit): Unit of the diffusion model. - scale (Parameter): Scale parameter of the diffusion model. - """ + """Base class for constructing diffusion models.""" def __init__( self, - display_name='MyDiffusionModel', + display_name: str = "MyDiffusionModel", unique_name: str | None = None, scale: Numeric = 1.0, - unit: str | sc.Unit = 'meV', + unit: str | sc.Unit = "meV", ): """Initialize a new DiffusionModel. Args: - display_name (str): Display name of the diffusion model. - unique_name (str | None): Unique name of the diffusion + display_name (str, default='MyDiffusionModel'): Display name of the diffusion model. + unique_name (str | None, default=None): Unique name of the diffusion model. If None, a unique name will be generated. - scale (Numeric): Scale factor for the diffusion model. Must - be a non-negative number. Defaults to 1.0. - unit (str | sc.Unit): Unit of the diffusion model. Must be - convertible to meV. Defaults to "meV". + scale (Numeric, default=1.0): Scale factor for the diffusion model. Must + be a non-negative number. + unit (str | sc.Unit, default='meV'): Unit of the diffusion model. Must be + convertible to meV. Raises: TypeError: If scale is not a number. @@ -51,16 +37,16 @@ def __init__( cannot be converted to meV. """ if not isinstance(scale, Numeric): - raise TypeError('scale must be a number.') + raise TypeError("scale must be a number.") - scale = Parameter(name='scale', value=float(scale), fixed=False, min=0.0) + scale = Parameter(name="scale", value=float(scale), fixed=False, min=0.0) try: - test = DescriptorNumber(name='test', value=1, unit=unit) - test.convert_unit('meV') + test = DescriptorNumber(name="test", value=1, unit=unit) + test.convert_unit("meV") except Exception as e: raise UnitError( - f'Invalid unit: {unit}. Unit must be a string or scipp Unit and convertible to meV.' # noqa: E501 + f"Invalid unit: {unit}. Unit must be a string or scipp Unit and convertible to meV." # noqa: E501 ) from e super().__init__(display_name=display_name, unique_name=unique_name) @@ -72,11 +58,11 @@ def __init__( # ------------------------------------------------------------------ @property - def unit(self) -> str: + def unit(self) -> str | sc.Unit | None: """Get the unit of the energy axis of the DiffusionModel. Returns: - (str | sc.Unit | None): Unit of the DiffusionModel. + str | sc.Unit | None: Unit of the DiffusionModel. """ return str(self._unit) @@ -94,8 +80,8 @@ def unit(self, unit_str: str) -> None: """ raise AttributeError( ( - f'Unit is read-only. Use convert_unit to change the unit between allowed types ' - f'or create a new {self.__class__.__name__} with the desired unit.' + f"Unit is read-only. Use convert_unit to change the unit between allowed types " + f"or create a new {self.__class__.__name__} with the desired unit." ) ) # noqa: E501 @@ -121,10 +107,10 @@ def scale(self, scale: Numeric) -> None: ValueError: If scale is negative. """ if not isinstance(scale, Numeric): - raise TypeError('scale must be a number.') + raise TypeError("scale must be a number.") if float(scale) < 0: - raise ValueError('scale must be non-negative.') + raise ValueError("scale must be non-negative.") self._scale.value = scale # ------------------------------------------------------------------ @@ -137,4 +123,4 @@ def __repr__(self) -> str: Returns: str: String representation of the DiffusionModel. """ - return f'{self.__class__.__name__}(display_name={self.display_name}, unit={self.unit})' + return f"{self.__class__.__name__}(display_name={self.display_name}, unit={self.unit})" From 05348112b01592e04a0dbb68b92c0bfec1fe3c74 Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Fri, 13 Mar 2026 18:29:45 +0100 Subject: [PATCH 09/31] brownian --- .../brownian_translational_diffusion.py | 95 ++++++++----------- .../diffusion_model/diffusion_model_base.py | 4 +- 2 files changed, 42 insertions(+), 57 deletions(-) diff --git a/src/easydynamics/sample_model/diffusion_model/brownian_translational_diffusion.py b/src/easydynamics/sample_model/diffusion_model/brownian_translational_diffusion.py index 87f37f598..0a26bc0d8 100644 --- a/src/easydynamics/sample_model/diffusion_model/brownian_translational_diffusion.py +++ b/src/easydynamics/sample_model/diffusion_model/brownian_translational_diffusion.py @@ -12,7 +12,9 @@ from easydynamics.sample_model.component_collection import ComponentCollection from easydynamics.sample_model.components import Lorentzian -from easydynamics.sample_model.diffusion_model.diffusion_model_base import DiffusionModelBase +from easydynamics.sample_model.diffusion_model.diffusion_model_base import ( + DiffusionModelBase, +) from easydynamics.utils.utils import Numeric from easydynamics.utils.utils import Q_type from easydynamics.utils.utils import _validate_and_convert_Q @@ -28,24 +30,7 @@ class BrownianTranslationalDiffusion(DiffusionModelBase): 1/angstrom. Creates ComponentCollections with Lorentzian components for given Q-values. - Args: - display_name (str): Display name of the diffusion model. - unique_name (str | None): Unique name of the diffusion model. If - None, a unique name will be generated. - unit (str | sc.Unit): Unit of the diffusion model. Must be - convertible to meV. Defaults to "meV". - scale (Numeric): Scale factor for the diffusion model. Must be - a non-negative number. Defaults to 1.0. - diffusion_coefficient (Numeric): Diffusion coefficient D in - m^2/s. Defaults to 1.0. - - Attributes: - unit (str | sc.Unit): Unit of the diffusion model. - scale (Parameter): Scale parameter of the diffusion model. - diffusion_coefficient (Parameter): Diffusion coefficient D in - m^2/s. - - Example usage: + Example: >>>Q=np.linspace(0.5,2,7) >>>energy=np.linspace(-2, 2, 501) >>>scale=1.0 @@ -53,47 +38,45 @@ class BrownianTranslationalDiffusion(DiffusionModelBase): >>>diffusion_model=BrownianTranslationalDiffusion(display_name="DiffusionModel", >>>scale=scale, diffusion_coefficient= diffusion_coefficient) >>>component_collections=diffusion_model.create_component_collections(Q) - See also the examples. + See also the tutorials. """ def __init__( self, - display_name: str | None = 'BrownianTranslationalDiffusion', + display_name: str | None = "BrownianTranslationalDiffusion", unique_name: str | None = None, - unit: str | sc.Unit = 'meV', + unit: str | sc.Unit = "meV", scale: Numeric = 1.0, diffusion_coefficient: Numeric = 1.0, ): """Initialize a new BrownianTranslationalDiffusion model. Args: - display_name (str): Display name of the diffusion model. - unique_name (str | None): Unique name of the diffusion + display_name (str | None, default='BrownianTranslationalDiffusion'): Display name of the diffusion model. + unique_name (str | None, default=None): Unique name of the diffusion model. If None, a unique name will be generated. - unit (str | sc.Unit): Unit of the diffusion model. Must be - convertible to meV. Defaults to "meV". - scale (Numeric): Scale factor for the diffusion model. Must - be a non-negative number. Defaults to 1.0. - diffusion_coefficient (Numeric): Diffusion coefficient D in - m^2/s. Defaults to 1.0. + unit (str | sc.Unit, default='meV'): Unit of the diffusion model. Must be + convertible to meV. + scale (Numeric, default=1.0): Scale factor for the diffusion model. Must + be a non-negative number. + diffusion_coefficient (Numeric, default=1.0): Diffusion coefficient D in + m^2/s. Raises: TypeError: If scale or diffusion_coefficient is not a number. - ValueError: If scale is negative. - UnitError: If unit is not a string or scipp Unit. """ if not isinstance(scale, Numeric): - raise TypeError('scale must be a number.') + raise TypeError("scale must be a number.") if not isinstance(diffusion_coefficient, Numeric): - raise TypeError('diffusion_coefficient must be a number.') + raise TypeError("diffusion_coefficient must be a number.") diffusion_coefficient = Parameter( - name='diffusion_coefficient', + name="diffusion_coefficient", value=float(diffusion_coefficient), fixed=False, - unit='m**2/s', + unit="m**2/s", min=0.0, ) super().__init__( @@ -102,8 +85,8 @@ def __init__( unit=unit, scale=scale, ) - self._hbar = DescriptorNumber.from_scipp('hbar', scipp_hbar) - self._angstrom = DescriptorNumber('angstrom', 1e-10, unit='m') + self._hbar = DescriptorNumber.from_scipp("hbar", scipp_hbar) + self._angstrom = DescriptorNumber("angstrom", 1e-10, unit="m") self._diffusion_coefficient = diffusion_coefficient # ------------------------------------------------------------------ @@ -132,10 +115,10 @@ def diffusion_coefficient(self, diffusion_coefficient: Numeric) -> None: ValueError: If diffusion_coefficient is negative. """ if not isinstance(diffusion_coefficient, Numeric): - raise TypeError('diffusion_coefficient must be a number.') + raise TypeError("diffusion_coefficient must be a number.") if float(diffusion_coefficient) < 0: - raise ValueError('diffusion_coefficient must be non-negative.') + raise ValueError("diffusion_coefficient must be non-negative.") self._diffusion_coefficient.value = float(diffusion_coefficient) # ------------------------------------------------------------------ @@ -157,7 +140,9 @@ def calculate_width(self, Q: Q_type) -> np.ndarray: Q = _validate_and_convert_Q(Q) - unit_conversion_factor = self._hbar * self.diffusion_coefficient / (self._angstrom**2) + unit_conversion_factor = ( + self._hbar * self.diffusion_coefficient / (self._angstrom**2) + ) unit_conversion_factor.convert_unit(self.unit) width = Q**2 * unit_conversion_factor.value @@ -197,7 +182,7 @@ def calculate_QISF(self, Q: Q_type) -> np.ndarray: def create_component_collections( self, Q: Q_type, - component_display_name: str = 'Brownian translational diffusion', + component_display_name: str = "Brownian translational diffusion", ) -> List[ComponentCollection]: r"""Create ComponentCollection components for the Brownian translational diffusion model at given Q values. @@ -220,7 +205,7 @@ def create_component_collections( Q = _validate_and_convert_Q(Q) if not isinstance(component_display_name, str): - raise TypeError('component_name must be a string.') + raise TypeError("component_name must be a string.") component_collection_list = [None] * len(Q) # In more complex models, this is used to scale the area of the @@ -232,7 +217,7 @@ def create_component_collections( # No delta function, as the EISF is 0. for i, Q_value in enumerate(Q): component_collection_list[i] = ComponentCollection( - display_name=f'{self.display_name}_Q{Q_value:.2f}', unit=self.unit + display_name=f"{self.display_name}_Q{Q_value:.2f}", unit=self.unit ) lorentzian_component = Lorentzian( @@ -270,7 +255,7 @@ def _write_width_dependency_expression(self, Q: float) -> str: of Q to make dependent Parameters. Args: - Q (float): Scattering vector in 1/angstrom + Q (float): Scattering vector in 1/angstrom Returns: str: Dependency expression for the width. @@ -279,10 +264,10 @@ def _write_width_dependency_expression(self, Q: float) -> str: TypeError: If Q is not a float. """ if not isinstance(Q, (float)): - raise TypeError('Q must be a float.') + raise TypeError("Q must be a float.") # Q is given as a float, so we need to add the units - return f'hbar * D* {Q} **2*1/(angstrom**2)' + return f"hbar * D* {Q} **2*1/(angstrom**2)" def _write_width_dependency_map_expression(self) -> Dict[str, DescriptorNumber]: """Write the dependency map expression to make dependent @@ -292,9 +277,9 @@ def _write_width_dependency_map_expression(self) -> Dict[str, DescriptorNumber]: Dict[str, DescriptorNumber]: Dependency map for the width. """ return { - 'D': self.diffusion_coefficient, - 'hbar': self._hbar, - 'angstrom': self._angstrom, + "D": self.diffusion_coefficient, + "hbar": self._hbar, + "angstrom": self._angstrom, } def _write_area_dependency_expression(self, QISF: float) -> str: @@ -311,9 +296,9 @@ def _write_area_dependency_expression(self, QISF: float) -> str: TypeError: If QISF is not a float. """ if not isinstance(QISF, (float)): - raise TypeError('QISF must be a float.') + raise TypeError("QISF must be a float.") - return f'{QISF} * scale' + return f"{QISF} * scale" def _write_area_dependency_map_expression(self) -> Dict[str, DescriptorNumber]: """Write the dependency map expression to make dependent @@ -323,7 +308,7 @@ def _write_area_dependency_map_expression(self) -> Dict[str, DescriptorNumber]: Dict[str, DescriptorNumber]: Dependency map for the area. """ return { - 'scale': self.scale, + "scale": self.scale, } # ------------------------------------------------------------------ @@ -339,6 +324,6 @@ def __repr__(self) -> str: BrownianTranslationalDiffusion model. """ return ( - f'BrownianTranslationalDiffusion(display_name={self.display_name},' - f'diffusion_coefficient={self.diffusion_coefficient}, scale={self.scale})' + f"BrownianTranslationalDiffusion(display_name={self.display_name}," + f"diffusion_coefficient={self.diffusion_coefficient}, scale={self.scale})" ) diff --git a/src/easydynamics/sample_model/diffusion_model/diffusion_model_base.py b/src/easydynamics/sample_model/diffusion_model/diffusion_model_base.py index fb4a5705d..760d591bb 100644 --- a/src/easydynamics/sample_model/diffusion_model/diffusion_model_base.py +++ b/src/easydynamics/sample_model/diffusion_model/diffusion_model_base.py @@ -15,7 +15,7 @@ class DiffusionModelBase(ModelBase): def __init__( self, - display_name: str = "MyDiffusionModel", + display_name: str | None = "MyDiffusionModel", unique_name: str | None = None, scale: Numeric = 1.0, unit: str | sc.Unit = "meV", @@ -23,7 +23,7 @@ def __init__( """Initialize a new DiffusionModel. Args: - display_name (str, default='MyDiffusionModel'): Display name of the diffusion model. + display_name (str | None, default='MyDiffusionModel'): Display name of the diffusion model. unique_name (str | None, default=None): Unique name of the diffusion model. If None, a unique name will be generated. scale (Numeric, default=1.0): Scale factor for the diffusion model. Must From baa745020aed6c5529f2d3f9793c600dc6912836 Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Fri, 13 Mar 2026 18:51:49 +0100 Subject: [PATCH 10/31] Fix pixi-scipp related issue --- pixi.lock | 73 +++++++++++++++++++++++++------------------------- pixi.toml | 12 +++++++++ pyproject.toml | 1 - 3 files changed, 48 insertions(+), 38 deletions(-) diff --git a/pixi.lock b/pixi.lock index 44a745743..f977d25a5 100644 --- a/pixi.lock +++ b/pixi.lock @@ -425,7 +425,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f9/33/bd5b9137445ea4b680023eb0469b2bb969d61303dedb2aac6560ff3d14a1/notebook_shim-0.2.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/51/6e/6f394c9c77668153e14d4da83bcc247beb5952f6ead7699a1a2992613bea/numpy-2.4.2-cp312-cp312-macosx_10_13_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/7d/33/2eb97c8a77daaba34eaa3fa7241a14ac5f51c46a6bd5911361b644c4a1e2/numpy-2.4.2-cp312-cp312-macosx_14_0_x86_64.whl - pypi: https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.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/37/51/b467209c08dae2c624873d7491ea47d2b47336e5403309d433ea79c38571/pandas-3.0.1-cp312-cp312-macosx_10_13_x86_64.whl @@ -481,7 +481,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl - pypi: https://files.pythonhosted.org/packages/44/ed/e81dd668547da281e5dce710cf0bc60193f8d3d43833e8241d006720e42b/ruff-0.15.5-py3-none-macosx_10_12_x86_64.whl - pypi: https://files.pythonhosted.org/packages/3d/1e/f1e27cc6cfa9d5f941d9a0fabd86c3ddd515e8513ada591d1cfac143cec8/scipp-25.12.0-cp312-cp312-macosx_11_0_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/35/48/b992b488d6f299dbe3f11a20b24d3dda3d46f1a635ede1c46b5b17a7b163/scipy-1.17.1-cp312-cp312-macosx_10_14_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/35/f5/906eda513271c8deb5af284e5ef0206d17a96239af79f9fa0aebfe0e36b4/scipy-1.17.1-cp312-cp312-macosx_14_0_x86_64.whl - pypi: https://files.pythonhosted.org/packages/1c/78/504fdd027da3b84ff1aecd9f6957e65f35134534ccc6da8628eb71e76d3f/send2trash-2.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e1/c6/76dc613121b793286a3f91621d7b75a2b493e0390ddca50f11993eadf192/setuptools-82.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/52/59/0782e51887ac6b07ffd1570e0364cf901ebc36345fea669969d2084baebb/simple_websocket-1.1.0-py3-none-any.whl @@ -677,7 +677,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f9/33/bd5b9137445ea4b680023eb0469b2bb969d61303dedb2aac6560ff3d14a1/notebook_shim-0.2.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/1f/f8/55483431f2b2fd015ae6ed4fe62288823ce908437ed49db5a03d15151678/numpy-2.4.2-cp312-cp312-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/2f/20/18026832b1845cdc82248208dd929ca14c9d8f2bac391f67440707fff27c/numpy-2.4.2-cp312-cp312-macosx_14_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.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/7c/f1/e2567ffc8951ab371db2e40b2fe068e36b81d8cf3260f06ae508700e5504/pandas-3.0.1-cp312-cp312-macosx_11_0_arm64.whl @@ -733,7 +733,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/c4/8f/533075f00aaf19b07c5cd6aa6e5d89424b06b3b3f4583bfa9c640a079059/ruff-0.15.5-py3-none-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/ca/26/c38eeff61db9477fcc1af14f3dc0b1761a2e5567fe3d87b463f9beae2907/scipp-25.12.0-cp312-cp312-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/b2/02/cf107b01494c19dc100f1d0b7ac3cc08666e96ba2d64db7626066cee895e/scipy-1.17.1-cp312-cp312-macosx_12_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/cf/a9/599c28631bad314d219cf9ffd40e985b24d603fc8a2f4ccc5ae8419a535b/scipy-1.17.1-cp312-cp312-macosx_14_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/1c/78/504fdd027da3b84ff1aecd9f6957e65f35134534ccc6da8628eb71e76d3f/send2trash-2.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e1/c6/76dc613121b793286a3f91621d7b75a2b493e0390ddca50f11993eadf192/setuptools-82.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/52/59/0782e51887ac6b07ffd1570e0364cf901ebc36345fea669969d2084baebb/simple_websocket-1.1.0-py3-none-any.whl @@ -1439,7 +1439,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f9/33/bd5b9137445ea4b680023eb0469b2bb969d61303dedb2aac6560ff3d14a1/notebook_shim-0.2.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d3/44/71852273146957899753e69986246d6a176061ea183407e95418c2aa4d9a/numpy-2.4.2-cp311-cp311-macosx_10_9_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/40/a9/3feb49f17bbd1300dd2570432961f5c8a4ffeff1db6f02c7273bd020a4c9/numpy-2.4.2-cp311-cp311-macosx_14_0_x86_64.whl - pypi: https://files.pythonhosted.org/packages/2c/ab/fc8290c6a4c722e5514d80f62b2dc4c4df1a68a41d1364e625c35990fcf3/overrides-7.7.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl @@ -1496,7 +1496,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/4d/6e/f964e88b3d2abee2a82c1ac8366da848fce1c6d834dc2132c3fda3970290/rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl - pypi: https://files.pythonhosted.org/packages/44/ed/e81dd668547da281e5dce710cf0bc60193f8d3d43833e8241d006720e42b/ruff-0.15.5-py3-none-macosx_10_12_x86_64.whl - pypi: https://files.pythonhosted.org/packages/12/0d/3f98a936a30bff4a460b51b9f85c4d994f94249930b2d8bedeb8111a359e/scipp-25.12.0-cp311-cp311-macosx_11_0_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/df/75/b4ce781849931fef6fd529afa6b63711d5a733065722d0c3e2724af9e40a/scipy-1.17.1-cp311-cp311-macosx_10_14_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/ec/e6/cef1cf3557f0c54954198554a10016b6a03b2ec9e22a4e1df734936bd99c/scipy-1.17.1-cp311-cp311-macosx_14_0_x86_64.whl - pypi: https://files.pythonhosted.org/packages/1c/78/504fdd027da3b84ff1aecd9f6957e65f35134534ccc6da8628eb71e76d3f/send2trash-2.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e1/c6/76dc613121b793286a3f91621d7b75a2b493e0390ddca50f11993eadf192/setuptools-82.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/52/59/0782e51887ac6b07ffd1570e0364cf901ebc36345fea669969d2084baebb/simple_websocket-1.1.0-py3-none-any.whl @@ -1692,7 +1692,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f9/33/bd5b9137445ea4b680023eb0469b2bb969d61303dedb2aac6560ff3d14a1/notebook_shim-0.2.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/74/41/5d17d4058bd0cd96bcbd4d9ff0fb2e21f52702aab9a72e4a594efa18692f/numpy-2.4.2-cp311-cp311-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/49/48/fb1ce8136c19452ed15f033f8aee91d5defe515094e330ce368a0647846f/numpy-2.4.2-cp311-cp311-macosx_14_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/2c/ab/fc8290c6a4c722e5514d80f62b2dc4c4df1a68a41d1364e625c35990fcf3/overrides-7.7.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl @@ -1749,7 +1749,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/94/ba/24e5ebb7c1c82e74c4e4f33b2112a5573ddc703915b13a073737b59b86e0/rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/c4/8f/533075f00aaf19b07c5cd6aa6e5d89424b06b3b3f4583bfa9c640a079059/ruff-0.15.5-py3-none-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/6a/3a/ab0eb61593569d5a0d080002e4b8c0998cb1116d8710781b7225c304b880/scipp-25.12.0-cp311-cp311-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/f7/58/bccc2861b305abdd1b8663d6130c0b3d7cc22e8d86663edbc8401bfd40d4/scipy-1.17.1-cp311-cp311-macosx_12_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/6d/ee/18146b7757ed4976276b9c9819108adbc73c5aad636e5353e20746b73069/scipy-1.17.1-cp311-cp311-macosx_14_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/1c/78/504fdd027da3b84ff1aecd9f6957e65f35134534ccc6da8628eb71e76d3f/send2trash-2.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e1/c6/76dc613121b793286a3f91621d7b75a2b493e0390ddca50f11993eadf192/setuptools-82.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/52/59/0782e51887ac6b07ffd1570e0364cf901ebc36345fea669969d2084baebb/simple_websocket-1.1.0-py3-none-any.whl @@ -2455,7 +2455,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f9/33/bd5b9137445ea4b680023eb0469b2bb969d61303dedb2aac6560ff3d14a1/notebook_shim-0.2.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/51/6e/6f394c9c77668153e14d4da83bcc247beb5952f6ead7699a1a2992613bea/numpy-2.4.2-cp312-cp312-macosx_10_13_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/7d/33/2eb97c8a77daaba34eaa3fa7241a14ac5f51c46a6bd5911361b644c4a1e2/numpy-2.4.2-cp312-cp312-macosx_14_0_x86_64.whl - pypi: https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.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/37/51/b467209c08dae2c624873d7491ea47d2b47336e5403309d433ea79c38571/pandas-3.0.1-cp312-cp312-macosx_10_13_x86_64.whl @@ -2511,7 +2511,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl - pypi: https://files.pythonhosted.org/packages/44/ed/e81dd668547da281e5dce710cf0bc60193f8d3d43833e8241d006720e42b/ruff-0.15.5-py3-none-macosx_10_12_x86_64.whl - pypi: https://files.pythonhosted.org/packages/3d/1e/f1e27cc6cfa9d5f941d9a0fabd86c3ddd515e8513ada591d1cfac143cec8/scipp-25.12.0-cp312-cp312-macosx_11_0_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/35/48/b992b488d6f299dbe3f11a20b24d3dda3d46f1a635ede1c46b5b17a7b163/scipy-1.17.1-cp312-cp312-macosx_10_14_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/35/f5/906eda513271c8deb5af284e5ef0206d17a96239af79f9fa0aebfe0e36b4/scipy-1.17.1-cp312-cp312-macosx_14_0_x86_64.whl - pypi: https://files.pythonhosted.org/packages/1c/78/504fdd027da3b84ff1aecd9f6957e65f35134534ccc6da8628eb71e76d3f/send2trash-2.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e1/c6/76dc613121b793286a3f91621d7b75a2b493e0390ddca50f11993eadf192/setuptools-82.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/52/59/0782e51887ac6b07ffd1570e0364cf901ebc36345fea669969d2084baebb/simple_websocket-1.1.0-py3-none-any.whl @@ -2707,7 +2707,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f9/33/bd5b9137445ea4b680023eb0469b2bb969d61303dedb2aac6560ff3d14a1/notebook_shim-0.2.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/1f/f8/55483431f2b2fd015ae6ed4fe62288823ce908437ed49db5a03d15151678/numpy-2.4.2-cp312-cp312-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/2f/20/18026832b1845cdc82248208dd929ca14c9d8f2bac391f67440707fff27c/numpy-2.4.2-cp312-cp312-macosx_14_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.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/7c/f1/e2567ffc8951ab371db2e40b2fe068e36b81d8cf3260f06ae508700e5504/pandas-3.0.1-cp312-cp312-macosx_11_0_arm64.whl @@ -2763,7 +2763,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/c4/8f/533075f00aaf19b07c5cd6aa6e5d89424b06b3b3f4583bfa9c640a079059/ruff-0.15.5-py3-none-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/ca/26/c38eeff61db9477fcc1af14f3dc0b1761a2e5567fe3d87b463f9beae2907/scipp-25.12.0-cp312-cp312-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/b2/02/cf107b01494c19dc100f1d0b7ac3cc08666e96ba2d64db7626066cee895e/scipy-1.17.1-cp312-cp312-macosx_12_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/cf/a9/599c28631bad314d219cf9ffd40e985b24d603fc8a2f4ccc5ae8419a535b/scipy-1.17.1-cp312-cp312-macosx_14_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/1c/78/504fdd027da3b84ff1aecd9f6957e65f35134534ccc6da8628eb71e76d3f/send2trash-2.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e1/c6/76dc613121b793286a3f91621d7b75a2b493e0390ddca50f11993eadf192/setuptools-82.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/52/59/0782e51887ac6b07ffd1570e0364cf901ebc36345fea669969d2084baebb/simple_websocket-1.1.0-py3-none-any.whl @@ -4074,8 +4074,8 @@ packages: requires_python: '>=3.5' - pypi: ./ name: easydynamics - version: 0.2.1+dev5 - sha256: 84cc733227e0d2e7a4af52c5ffeab65da604d6253e6d7584f192bf59685802e8 + version: 0.2.1+devdirty13 + sha256: e9e7b1f27a2af5d6267f3dd75e7e46785211b1046865a7a70a86d278b0a30dd0 requires_dist: - darkdetect - easyscience @@ -4087,7 +4087,6 @@ packages: - pixi-kernel - plopp - pooch - - scipp<26.1.0 - build ; extra == 'dev' - copier ; extra == 'dev' - docformatter ; extra == 'dev' @@ -7032,35 +7031,35 @@ packages: version: 2.4.2 sha256: c02ef4401a506fb60b411467ad501e1429a3487abca4664871d9ae0b46c8ba32 requires_python: '>=3.11' -- pypi: https://files.pythonhosted.org/packages/1f/f8/55483431f2b2fd015ae6ed4fe62288823ce908437ed49db5a03d15151678/numpy-2.4.2-cp312-cp312-macosx_11_0_arm64.whl +- pypi: https://files.pythonhosted.org/packages/2f/20/18026832b1845cdc82248208dd929ca14c9d8f2bac391f67440707fff27c/numpy-2.4.2-cp312-cp312-macosx_14_0_arm64.whl name: numpy version: 2.4.2 - sha256: 40397bda92382fcec844066efb11f13e1c9a3e2a8e8f318fb72ed8b6db9f60f1 + sha256: b3a24467af63c67829bfaa61eecf18d5432d4f11992688537be59ecd6ad32f5e requires_python: '>=3.11' -- pypi: https://files.pythonhosted.org/packages/51/6e/6f394c9c77668153e14d4da83bcc247beb5952f6ead7699a1a2992613bea/numpy-2.4.2-cp312-cp312-macosx_10_13_x86_64.whl +- pypi: https://files.pythonhosted.org/packages/40/a9/3feb49f17bbd1300dd2570432961f5c8a4ffeff1db6f02c7273bd020a4c9/numpy-2.4.2-cp311-cp311-macosx_14_0_x86_64.whl name: numpy version: 2.4.2 - sha256: 21982668592194c609de53ba4933a7471880ccbaadcc52352694a59ecc860b3a + sha256: cb7bbb88aa74908950d979eeaa24dbdf1a865e3c7e45ff0121d8f70387b55f73 requires_python: '>=3.11' -- pypi: https://files.pythonhosted.org/packages/74/41/5d17d4058bd0cd96bcbd4d9ff0fb2e21f52702aab9a72e4a594efa18692f/numpy-2.4.2-cp311-cp311-macosx_11_0_arm64.whl +- pypi: https://files.pythonhosted.org/packages/49/48/fb1ce8136c19452ed15f033f8aee91d5defe515094e330ce368a0647846f/numpy-2.4.2-cp311-cp311-macosx_14_0_arm64.whl name: numpy version: 2.4.2 - sha256: 7edc794af8b36ca37ef5fcb5e0d128c7e0595c7b96a2318d1badb6fcd8ee86b1 + sha256: 6e9f61981ace1360e42737e2bae58b27bf28a1b27e781721047d84bd754d32e7 requires_python: '>=3.11' - pypi: https://files.pythonhosted.org/packages/76/ae/e0265e0163cf127c24c3969d29f1c4c64551a1e375d95a13d32eab25d364/numpy-2.4.2-cp311-cp311-win_amd64.whl name: numpy version: 2.4.2 sha256: b9c618d56a29c9cb1c4da979e9899be7578d2e0b3c24d52079c166324c9e8695 requires_python: '>=3.11' -- pypi: https://files.pythonhosted.org/packages/92/0f/7ceaaeaacb40567071e94dbf2c9480c0ae453d5bb4f52bea3892c39dc83c/numpy-2.4.2-cp312-cp312-win_amd64.whl +- pypi: https://files.pythonhosted.org/packages/7d/33/2eb97c8a77daaba34eaa3fa7241a14ac5f51c46a6bd5911361b644c4a1e2/numpy-2.4.2-cp312-cp312-macosx_14_0_x86_64.whl name: numpy version: 2.4.2 - sha256: 209fae046e62d0ce6435fcfe3b1a10537e858249b3d9b05829e2a05218296a85 + sha256: 805cc8de9fd6e7a22da5aed858e0ab16be5a4db6c873dde1d7451c541553aa27 requires_python: '>=3.11' -- pypi: https://files.pythonhosted.org/packages/d3/44/71852273146957899753e69986246d6a176061ea183407e95418c2aa4d9a/numpy-2.4.2-cp311-cp311-macosx_10_9_x86_64.whl +- pypi: https://files.pythonhosted.org/packages/92/0f/7ceaaeaacb40567071e94dbf2c9480c0ae453d5bb4f52bea3892c39dc83c/numpy-2.4.2-cp312-cp312-win_amd64.whl name: numpy version: 2.4.2 - sha256: e7e88598032542bd49af7c4747541422884219056c268823ef6e5e89851c8825 + sha256: 209fae046e62d0ce6435fcfe3b1a10537e858249b3d9b05829e2a05218296a85 requires_python: '>=3.11' - pypi: https://files.pythonhosted.org/packages/f5/c6/a18e59f3f0b8071cc85cbc8d80cd02d68aa9710170b2553a117203d46936/numpy-2.4.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl name: numpy @@ -9517,10 +9516,10 @@ packages: - ruff>=0.12.0 ; extra == 'dev' - cython-lint>=0.12.2 ; extra == 'dev' requires_python: '>=3.11' -- pypi: https://files.pythonhosted.org/packages/35/48/b992b488d6f299dbe3f11a20b24d3dda3d46f1a635ede1c46b5b17a7b163/scipy-1.17.1-cp312-cp312-macosx_10_14_x86_64.whl +- pypi: https://files.pythonhosted.org/packages/35/f5/906eda513271c8deb5af284e5ef0206d17a96239af79f9fa0aebfe0e36b4/scipy-1.17.1-cp312-cp312-macosx_14_0_x86_64.whl name: scipy version: 1.17.1 - sha256: 35c3a56d2ef83efc372eaec584314bd0ef2e2f0d2adb21c55e6ad5b344c0dcb8 + sha256: c80be5ede8f3f8eded4eff73cc99a25c388ce98e555b17d31da05287015ffa5b requires_dist: - numpy>=1.26.4,<2.7 - pytest>=8.0.0 ; extra == 'test' @@ -9561,10 +9560,10 @@ packages: - ruff>=0.12.0 ; extra == 'dev' - cython-lint>=0.12.2 ; extra == 'dev' requires_python: '>=3.11' -- pypi: https://files.pythonhosted.org/packages/95/da/0d1df507cf574b3f224ccc3d45244c9a1d732c81dcb26b1e8a766ae271a8/scipy-1.17.1-cp311-cp311-win_amd64.whl +- pypi: https://files.pythonhosted.org/packages/6d/ee/18146b7757ed4976276b9c9819108adbc73c5aad636e5353e20746b73069/scipy-1.17.1-cp311-cp311-macosx_14_0_arm64.whl name: scipy version: 1.17.1 - sha256: d30e57c72013c2a4fe441c2fcb8e77b14e152ad48b5464858e07e2ad9fbfceff + sha256: a3472cfbca0a54177d0faa68f697d8ba4c80bbdc19908c3465556d9f7efce9ee requires_dist: - numpy>=1.26.4,<2.7 - pytest>=8.0.0 ; extra == 'test' @@ -9605,10 +9604,10 @@ packages: - ruff>=0.12.0 ; extra == 'dev' - cython-lint>=0.12.2 ; extra == 'dev' requires_python: '>=3.11' -- pypi: https://files.pythonhosted.org/packages/a2/84/dc08d77fbf3d87d3ee27f6a0c6dcce1de5829a64f2eae85a0ecc1f0daa73/scipy-1.17.1-cp312-cp312-win_amd64.whl +- pypi: https://files.pythonhosted.org/packages/95/da/0d1df507cf574b3f224ccc3d45244c9a1d732c81dcb26b1e8a766ae271a8/scipy-1.17.1-cp311-cp311-win_amd64.whl name: scipy version: 1.17.1 - sha256: 41b71f4a3a4cab9d366cd9065b288efc4d4f3c0b37a91a8e0947fb5bd7f31d87 + sha256: d30e57c72013c2a4fe441c2fcb8e77b14e152ad48b5464858e07e2ad9fbfceff requires_dist: - numpy>=1.26.4,<2.7 - pytest>=8.0.0 ; extra == 'test' @@ -9649,10 +9648,10 @@ packages: - ruff>=0.12.0 ; extra == 'dev' - cython-lint>=0.12.2 ; extra == 'dev' requires_python: '>=3.11' -- pypi: https://files.pythonhosted.org/packages/b2/02/cf107b01494c19dc100f1d0b7ac3cc08666e96ba2d64db7626066cee895e/scipy-1.17.1-cp312-cp312-macosx_12_0_arm64.whl +- pypi: https://files.pythonhosted.org/packages/a2/84/dc08d77fbf3d87d3ee27f6a0c6dcce1de5829a64f2eae85a0ecc1f0daa73/scipy-1.17.1-cp312-cp312-win_amd64.whl name: scipy version: 1.17.1 - sha256: fcb310ddb270a06114bb64bbe53c94926b943f5b7f0842194d585c65eb4edd76 + sha256: 41b71f4a3a4cab9d366cd9065b288efc4d4f3c0b37a91a8e0947fb5bd7f31d87 requires_dist: - numpy>=1.26.4,<2.7 - pytest>=8.0.0 ; extra == 'test' @@ -9693,10 +9692,10 @@ packages: - ruff>=0.12.0 ; extra == 'dev' - cython-lint>=0.12.2 ; extra == 'dev' requires_python: '>=3.11' -- pypi: https://files.pythonhosted.org/packages/df/75/b4ce781849931fef6fd529afa6b63711d5a733065722d0c3e2724af9e40a/scipy-1.17.1-cp311-cp311-macosx_10_14_x86_64.whl +- pypi: https://files.pythonhosted.org/packages/cf/a9/599c28631bad314d219cf9ffd40e985b24d603fc8a2f4ccc5ae8419a535b/scipy-1.17.1-cp312-cp312-macosx_14_0_arm64.whl name: scipy version: 1.17.1 - sha256: 1f95b894f13729334fb990162e911c9e5dc1ab390c58aa6cbecb389c5b5e28ec + sha256: cc90d2e9c7e5c7f1a482c9875007c095c3194b1cfedca3c2f3291cdc2bc7c086 requires_dist: - numpy>=1.26.4,<2.7 - pytest>=8.0.0 ; extra == 'test' @@ -9737,10 +9736,10 @@ packages: - ruff>=0.12.0 ; extra == 'dev' - cython-lint>=0.12.2 ; extra == 'dev' requires_python: '>=3.11' -- pypi: https://files.pythonhosted.org/packages/f7/58/bccc2861b305abdd1b8663d6130c0b3d7cc22e8d86663edbc8401bfd40d4/scipy-1.17.1-cp311-cp311-macosx_12_0_arm64.whl +- pypi: https://files.pythonhosted.org/packages/ec/e6/cef1cf3557f0c54954198554a10016b6a03b2ec9e22a4e1df734936bd99c/scipy-1.17.1-cp311-cp311-macosx_14_0_x86_64.whl name: scipy version: 1.17.1 - sha256: e18f12c6b0bc5a592ed23d3f7b891f68fd7f8241d69b7883769eb5d5dfb52696 + sha256: 766e0dc5a616d026a3a1cffa379af959671729083882f50307e18175797b3dfd requires_dist: - numpy>=1.26.4,<2.7 - pytest>=8.0.0 ; extra == 'test' diff --git a/pixi.toml b/pixi.toml index 9b6cd33df..e35cbaf16 100644 --- a/pixi.toml +++ b/pixi.toml @@ -34,6 +34,18 @@ platforms = ['win-64', 'linux-64', 'osx-64', 'osx-arm64'] # Channels for fetching packages channels = ['conda-forge'] +##################### +# System requirements +##################### + +[system-requirements] + +# Set minimum supported version for macOS to be 14.0 to ensure packages +# like `skipp` that only have wheels for macOS 14.0+ (macosx_14_0_arm64) +# are used instead of building from source. This is a workaround for +# Pixi, see https://github.com/prefix-dev/pixi/issues/5667 +macos = '14.0' + ########## # FEATURES ########## diff --git a/pyproject.toml b/pyproject.toml index dba43620a..24fd334de 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,7 +32,6 @@ dependencies = [ 'ipywidgets', # Widgets (needed for interactive matplotlib backends) 'ipympl', # Matplotlib Jupyter widget backend (%matplotlib widget) 'IPython', # Interactive Python shell - 'scipp<26.1.0', # TEMPORARY: ARM macOS fails with higher versions ] [project.optional-dependencies] From e0318c5ea1a3421fc7ca0664d08b0737791e98a2 Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Fri, 13 Mar 2026 18:53:57 +0100 Subject: [PATCH 11/31] Apply auto formatting --- .../brownian_translational_diffusion.py | 54 +++++++------- .../diffusion_model/diffusion_model_base.py | 24 +++---- .../jump_translational_diffusion.py | 62 ++++++++-------- .../sample_model/instrument_model.py | 63 ++++++++--------- src/easydynamics/sample_model/model_base.py | 47 ++++++------- .../sample_model/resolution_model.py | 6 +- src/easydynamics/sample_model/sample_model.py | 70 +++++++++---------- src/easydynamics/utils/detailed_balance.py | 52 +++++++------- 8 files changed, 175 insertions(+), 203 deletions(-) diff --git a/src/easydynamics/sample_model/diffusion_model/brownian_translational_diffusion.py b/src/easydynamics/sample_model/diffusion_model/brownian_translational_diffusion.py index 0a26bc0d8..25dcb1102 100644 --- a/src/easydynamics/sample_model/diffusion_model/brownian_translational_diffusion.py +++ b/src/easydynamics/sample_model/diffusion_model/brownian_translational_diffusion.py @@ -12,9 +12,7 @@ from easydynamics.sample_model.component_collection import ComponentCollection from easydynamics.sample_model.components import Lorentzian -from easydynamics.sample_model.diffusion_model.diffusion_model_base import ( - DiffusionModelBase, -) +from easydynamics.sample_model.diffusion_model.diffusion_model_base import DiffusionModelBase from easydynamics.utils.utils import Numeric from easydynamics.utils.utils import Q_type from easydynamics.utils.utils import _validate_and_convert_Q @@ -43,9 +41,9 @@ class BrownianTranslationalDiffusion(DiffusionModelBase): def __init__( self, - display_name: str | None = "BrownianTranslationalDiffusion", + display_name: str | None = 'BrownianTranslationalDiffusion', unique_name: str | None = None, - unit: str | sc.Unit = "meV", + unit: str | sc.Unit = 'meV', scale: Numeric = 1.0, diffusion_coefficient: Numeric = 1.0, ): @@ -67,16 +65,16 @@ def __init__( number. """ if not isinstance(scale, Numeric): - raise TypeError("scale must be a number.") + raise TypeError('scale must be a number.') if not isinstance(diffusion_coefficient, Numeric): - raise TypeError("diffusion_coefficient must be a number.") + raise TypeError('diffusion_coefficient must be a number.') diffusion_coefficient = Parameter( - name="diffusion_coefficient", + name='diffusion_coefficient', value=float(diffusion_coefficient), fixed=False, - unit="m**2/s", + unit='m**2/s', min=0.0, ) super().__init__( @@ -85,8 +83,8 @@ def __init__( unit=unit, scale=scale, ) - self._hbar = DescriptorNumber.from_scipp("hbar", scipp_hbar) - self._angstrom = DescriptorNumber("angstrom", 1e-10, unit="m") + self._hbar = DescriptorNumber.from_scipp('hbar', scipp_hbar) + self._angstrom = DescriptorNumber('angstrom', 1e-10, unit='m') self._diffusion_coefficient = diffusion_coefficient # ------------------------------------------------------------------ @@ -115,10 +113,10 @@ def diffusion_coefficient(self, diffusion_coefficient: Numeric) -> None: ValueError: If diffusion_coefficient is negative. """ if not isinstance(diffusion_coefficient, Numeric): - raise TypeError("diffusion_coefficient must be a number.") + raise TypeError('diffusion_coefficient must be a number.') if float(diffusion_coefficient) < 0: - raise ValueError("diffusion_coefficient must be non-negative.") + raise ValueError('diffusion_coefficient must be non-negative.') self._diffusion_coefficient.value = float(diffusion_coefficient) # ------------------------------------------------------------------ @@ -140,9 +138,7 @@ def calculate_width(self, Q: Q_type) -> np.ndarray: Q = _validate_and_convert_Q(Q) - unit_conversion_factor = ( - self._hbar * self.diffusion_coefficient / (self._angstrom**2) - ) + unit_conversion_factor = self._hbar * self.diffusion_coefficient / (self._angstrom**2) unit_conversion_factor.convert_unit(self.unit) width = Q**2 * unit_conversion_factor.value @@ -182,7 +178,7 @@ def calculate_QISF(self, Q: Q_type) -> np.ndarray: def create_component_collections( self, Q: Q_type, - component_display_name: str = "Brownian translational diffusion", + component_display_name: str = 'Brownian translational diffusion', ) -> List[ComponentCollection]: r"""Create ComponentCollection components for the Brownian translational diffusion model at given Q values. @@ -205,7 +201,7 @@ def create_component_collections( Q = _validate_and_convert_Q(Q) if not isinstance(component_display_name, str): - raise TypeError("component_name must be a string.") + raise TypeError('component_name must be a string.') component_collection_list = [None] * len(Q) # In more complex models, this is used to scale the area of the @@ -217,7 +213,7 @@ def create_component_collections( # No delta function, as the EISF is 0. for i, Q_value in enumerate(Q): component_collection_list[i] = ComponentCollection( - display_name=f"{self.display_name}_Q{Q_value:.2f}", unit=self.unit + display_name=f'{self.display_name}_Q{Q_value:.2f}', unit=self.unit ) lorentzian_component = Lorentzian( @@ -264,10 +260,10 @@ def _write_width_dependency_expression(self, Q: float) -> str: TypeError: If Q is not a float. """ if not isinstance(Q, (float)): - raise TypeError("Q must be a float.") + raise TypeError('Q must be a float.') # Q is given as a float, so we need to add the units - return f"hbar * D* {Q} **2*1/(angstrom**2)" + return f'hbar * D* {Q} **2*1/(angstrom**2)' def _write_width_dependency_map_expression(self) -> Dict[str, DescriptorNumber]: """Write the dependency map expression to make dependent @@ -277,9 +273,9 @@ def _write_width_dependency_map_expression(self) -> Dict[str, DescriptorNumber]: Dict[str, DescriptorNumber]: Dependency map for the width. """ return { - "D": self.diffusion_coefficient, - "hbar": self._hbar, - "angstrom": self._angstrom, + 'D': self.diffusion_coefficient, + 'hbar': self._hbar, + 'angstrom': self._angstrom, } def _write_area_dependency_expression(self, QISF: float) -> str: @@ -296,9 +292,9 @@ def _write_area_dependency_expression(self, QISF: float) -> str: TypeError: If QISF is not a float. """ if not isinstance(QISF, (float)): - raise TypeError("QISF must be a float.") + raise TypeError('QISF must be a float.') - return f"{QISF} * scale" + return f'{QISF} * scale' def _write_area_dependency_map_expression(self) -> Dict[str, DescriptorNumber]: """Write the dependency map expression to make dependent @@ -308,7 +304,7 @@ def _write_area_dependency_map_expression(self) -> Dict[str, DescriptorNumber]: Dict[str, DescriptorNumber]: Dependency map for the area. """ return { - "scale": self.scale, + 'scale': self.scale, } # ------------------------------------------------------------------ @@ -324,6 +320,6 @@ def __repr__(self) -> str: BrownianTranslationalDiffusion model. """ return ( - f"BrownianTranslationalDiffusion(display_name={self.display_name}," - f"diffusion_coefficient={self.diffusion_coefficient}, scale={self.scale})" + f'BrownianTranslationalDiffusion(display_name={self.display_name},' + f'diffusion_coefficient={self.diffusion_coefficient}, scale={self.scale})' ) diff --git a/src/easydynamics/sample_model/diffusion_model/diffusion_model_base.py b/src/easydynamics/sample_model/diffusion_model/diffusion_model_base.py index 760d591bb..d8ba1a3a9 100644 --- a/src/easydynamics/sample_model/diffusion_model/diffusion_model_base.py +++ b/src/easydynamics/sample_model/diffusion_model/diffusion_model_base.py @@ -15,10 +15,10 @@ class DiffusionModelBase(ModelBase): def __init__( self, - display_name: str | None = "MyDiffusionModel", + display_name: str | None = 'MyDiffusionModel', unique_name: str | None = None, scale: Numeric = 1.0, - unit: str | sc.Unit = "meV", + unit: str | sc.Unit = 'meV', ): """Initialize a new DiffusionModel. @@ -37,16 +37,16 @@ def __init__( cannot be converted to meV. """ if not isinstance(scale, Numeric): - raise TypeError("scale must be a number.") + raise TypeError('scale must be a number.') - scale = Parameter(name="scale", value=float(scale), fixed=False, min=0.0) + scale = Parameter(name='scale', value=float(scale), fixed=False, min=0.0) try: - test = DescriptorNumber(name="test", value=1, unit=unit) - test.convert_unit("meV") + test = DescriptorNumber(name='test', value=1, unit=unit) + test.convert_unit('meV') except Exception as e: raise UnitError( - f"Invalid unit: {unit}. Unit must be a string or scipp Unit and convertible to meV." # noqa: E501 + f'Invalid unit: {unit}. Unit must be a string or scipp Unit and convertible to meV.' # noqa: E501 ) from e super().__init__(display_name=display_name, unique_name=unique_name) @@ -80,8 +80,8 @@ def unit(self, unit_str: str) -> None: """ raise AttributeError( ( - f"Unit is read-only. Use convert_unit to change the unit between allowed types " - f"or create a new {self.__class__.__name__} with the desired unit." + f'Unit is read-only. Use convert_unit to change the unit between allowed types ' + f'or create a new {self.__class__.__name__} with the desired unit.' ) ) # noqa: E501 @@ -107,10 +107,10 @@ def scale(self, scale: Numeric) -> None: ValueError: If scale is negative. """ if not isinstance(scale, Numeric): - raise TypeError("scale must be a number.") + raise TypeError('scale must be a number.') if float(scale) < 0: - raise ValueError("scale must be non-negative.") + raise ValueError('scale must be non-negative.') self._scale.value = scale # ------------------------------------------------------------------ @@ -123,4 +123,4 @@ def __repr__(self) -> str: Returns: str: String representation of the DiffusionModel. """ - return f"{self.__class__.__name__}(display_name={self.display_name}, unit={self.unit})" + return f'{self.__class__.__name__}(display_name={self.display_name}, unit={self.unit})' diff --git a/src/easydynamics/sample_model/diffusion_model/jump_translational_diffusion.py b/src/easydynamics/sample_model/diffusion_model/jump_translational_diffusion.py index ecdb2b9cd..46d3572c4 100644 --- a/src/easydynamics/sample_model/diffusion_model/jump_translational_diffusion.py +++ b/src/easydynamics/sample_model/diffusion_model/jump_translational_diffusion.py @@ -12,9 +12,7 @@ from easydynamics.sample_model.component_collection import ComponentCollection from easydynamics.sample_model.components import Lorentzian -from easydynamics.sample_model.diffusion_model.diffusion_model_base import ( - DiffusionModelBase, -) +from easydynamics.sample_model.diffusion_model.diffusion_model_base import DiffusionModelBase from easydynamics.utils.utils import Numeric from easydynamics.utils.utils import Q_type from easydynamics.utils.utils import _validate_and_convert_Q @@ -48,9 +46,9 @@ class JumpTranslationalDiffusion(DiffusionModelBase): def __init__( self, - display_name: str | None = "JumpTranslationalDiffusion", + display_name: str | None = 'JumpTranslationalDiffusion', unique_name: str | None = None, - unit: str | sc.Unit = "meV", + unit: str | sc.Unit = 'meV', scale: Numeric = 1.0, diffusion_coefficient: Numeric = 1.0, relaxation_time: Numeric = 1.0, @@ -81,27 +79,27 @@ def __init__( ) if not isinstance(diffusion_coefficient, Numeric): - raise TypeError("diffusion_coefficient must be a number.") + raise TypeError('diffusion_coefficient must be a number.') if not isinstance(relaxation_time, Numeric): - raise TypeError("relaxation_time must be a number.") + raise TypeError('relaxation_time must be a number.') diffusion_coefficient = Parameter( - name="diffusion_coefficient", + name='diffusion_coefficient', value=float(diffusion_coefficient), fixed=False, - unit="m**2/s", + unit='m**2/s', ) relaxation_time = Parameter( - name="relaxation_time", + name='relaxation_time', value=float(relaxation_time), fixed=False, - unit="ps", + unit='ps', ) - self._hbar = DescriptorNumber.from_scipp("hbar", scipp_hbar) - self._angstrom = DescriptorNumber("angstrom", 1e-10, unit="m") + self._hbar = DescriptorNumber.from_scipp('hbar', scipp_hbar) + self._angstrom = DescriptorNumber('angstrom', 1e-10, unit='m') self._diffusion_coefficient = diffusion_coefficient self._relaxation_time = relaxation_time @@ -131,9 +129,9 @@ def diffusion_coefficient(self, diffusion_coefficient: Numeric) -> None: ValueError: If diffusion_coefficient is negative. """ if not isinstance(diffusion_coefficient, Numeric): - raise TypeError("diffusion_coefficient must be a number.") + raise TypeError('diffusion_coefficient must be a number.') if float(diffusion_coefficient) < 0: - raise ValueError("diffusion_coefficient must be non-negative.") + raise ValueError('diffusion_coefficient must be non-negative.') self._diffusion_coefficient.value = float(diffusion_coefficient) @property @@ -157,10 +155,10 @@ def relaxation_time(self, relaxation_time: Numeric) -> None: ValueError: If relaxation_time is negative. """ if not isinstance(relaxation_time, Numeric): - raise TypeError("relaxation_time must be a number.") + raise TypeError('relaxation_time must be a number.') if float(relaxation_time) < 0: - raise ValueError("relaxation_time must be non-negative.") + raise ValueError('relaxation_time must be non-negative.') self._relaxation_time.value = float(relaxation_time) ################################ @@ -193,7 +191,7 @@ def calculate_width(self, Q: Q_type) -> np.ndarray: unit_conversion_factor_denominator = ( self.diffusion_coefficient / self._angstrom**2 * self.relaxation_time ) - unit_conversion_factor_denominator.convert_unit("dimensionless") + unit_conversion_factor_denominator.convert_unit('dimensionless') denominator = 1 + unit_conversion_factor_denominator.value * Q**2 @@ -231,7 +229,7 @@ def calculate_QISF(self, Q: Q_type) -> np.ndarray: def create_component_collections( self, Q: Q_type, - component_display_name: str = "Jump translational diffusion", + component_display_name: str = 'Jump translational diffusion', ) -> List[ComponentCollection]: """Create ComponentCollection components for the diffusion model at given Q values. @@ -252,7 +250,7 @@ def create_component_collections( Q = _validate_and_convert_Q(Q) if not isinstance(component_display_name, str): - raise TypeError("component_name must be a string.") + raise TypeError('component_name must be a string.') component_collection_list = [None] * len(Q) # In more complex models, this is used to scale the area of the @@ -264,7 +262,7 @@ def create_component_collections( # is 0. for i, Q_value in enumerate(Q): component_collection_list[i] = ComponentCollection( - display_name=f"{self.display_name}_Q{Q_value:.2f}", unit=self.unit + display_name=f'{self.display_name}_Q{Q_value:.2f}', unit=self.unit ) lorentzian_component = Lorentzian( @@ -311,10 +309,10 @@ def _write_width_dependency_expression(self, Q: float) -> str: TypeError: If Q is not a float. """ if not isinstance(Q, (float)): - raise TypeError("Q must be a float.") + raise TypeError('Q must be a float.') # Q is given as a float, so we need to add the units - return f"hbar * D* {Q} **2/(angstrom**2)/(1 + (D * t* {Q} **2/(angstrom**2)))" + return f'hbar * D* {Q} **2/(angstrom**2)/(1 + (D * t* {Q} **2/(angstrom**2)))' def _write_width_dependency_map_expression(self) -> Dict[str, DescriptorNumber]: """Write the dependency map expression to make dependent @@ -324,10 +322,10 @@ def _write_width_dependency_map_expression(self) -> Dict[str, DescriptorNumber]: Dict[str, DescriptorNumber]: Dependency map for the width. """ return { - "D": self._diffusion_coefficient, - "t": self._relaxation_time, - "hbar": self._hbar, - "angstrom": self._angstrom, + 'D': self._diffusion_coefficient, + 't': self._relaxation_time, + 'hbar': self._hbar, + 'angstrom': self._angstrom, } def _write_area_dependency_expression(self, QISF: float) -> str: @@ -345,9 +343,9 @@ def _write_area_dependency_expression(self, QISF: float) -> str: """ if not isinstance(QISF, (float)): - raise TypeError("QISF must be a float.") + raise TypeError('QISF must be a float.') - return f"{QISF} * scale" + return f'{QISF} * scale' def _write_area_dependency_map_expression(self) -> Dict[str, DescriptorNumber]: """Write the dependency map expression to make dependent @@ -357,7 +355,7 @@ def _write_area_dependency_map_expression(self) -> Dict[str, DescriptorNumber]: Dict[str, DescriptorNumber]: Dependency map for the area. """ return { - "scale": self._scale, + 'scale': self._scale, } ################################ @@ -373,6 +371,6 @@ def __repr__(self) -> str: model. """ return ( - f"JumpTranslationalDiffusion(display_name={self.display_name}, " - f"diffusion_coefficient={self._diffusion_coefficient}, scale={self._scale})" + f'JumpTranslationalDiffusion(display_name={self.display_name}, ' + f'diffusion_coefficient={self._diffusion_coefficient}, scale={self._scale})' ) diff --git a/src/easydynamics/sample_model/instrument_model.py b/src/easydynamics/sample_model/instrument_model.py index f8cdf812e..1b0b54ab9 100644 --- a/src/easydynamics/sample_model/instrument_model.py +++ b/src/easydynamics/sample_model/instrument_model.py @@ -18,20 +18,21 @@ class InstrumentModel(NewBase): """InstrumentModel represents a model of the instrument in an - experiment at various Q. It can contain a model of the resolution - function for convolutions, of the background and an offset in the - energy axis. + experiment at various Q. + + It can contain a model of the resolution function for convolutions, + of the background and an offset in the energy axis. """ def __init__( self, - display_name: str = "MyInstrumentModel", + display_name: str = 'MyInstrumentModel', unique_name: str | None = None, Q: Q_type | None = None, resolution_model: ResolutionModel | None = None, background_model: BackgroundModel | None = None, energy_offset: Numeric | None = None, - unit: str | sc.Unit = "meV", + unit: str | sc.Unit = 'meV', ): """Initialize an InstrumentModel. @@ -70,8 +71,8 @@ def __init__( else: if not isinstance(resolution_model, ResolutionModel): raise TypeError( - f"resolution_model must be a ResolutionModel or None, " - f"got {type(resolution_model).__name__}" + f'resolution_model must be a ResolutionModel or None, ' + f'got {type(resolution_model).__name__}' ) self._resolution_model = resolution_model @@ -80,8 +81,8 @@ def __init__( else: if not isinstance(background_model, BackgroundModel): raise TypeError( - f"background_model must be a BackgroundModel or None, " - f"got {type(background_model).__name__}" + f'background_model must be a BackgroundModel or None, ' + f'got {type(background_model).__name__}' ) self._background_model = background_model @@ -89,10 +90,10 @@ def __init__( energy_offset = 0.0 if not isinstance(energy_offset, Numeric): - raise TypeError("energy_offset must be a number or None") + raise TypeError('energy_offset must be a number or None') self._energy_offset = Parameter( - name="energy_offset", + name='energy_offset', value=float(energy_offset), unit=self.unit, fixed=False, @@ -126,7 +127,7 @@ def resolution_model(self, value: ResolutionModel) -> None: """ if not isinstance(value, ResolutionModel): raise TypeError( - f"resolution_model must be a ResolutionModel, got {type(value).__name__}" + f'resolution_model must be a ResolutionModel, got {type(value).__name__}' ) self._resolution_model = value self._on_resolution_model_change() @@ -155,7 +156,7 @@ def background_model(self, value: BackgroundModel) -> None: if not isinstance(value, BackgroundModel): raise TypeError( - f"background_model must be a BackgroundModel, got {type(value).__name__}" + f'background_model must be a BackgroundModel, got {type(value).__name__}' ) self._background_model = value self._on_background_model_change() @@ -206,8 +207,8 @@ def unit(self, unit_str: str) -> None: """ raise AttributeError( ( - f"Unit is read-only. Use convert_unit to change the unit between allowed types " - f"or create a new {self.__class__.__name__} with the desired unit." + f'Unit is read-only. Use convert_unit to change the unit between allowed types ' + f'or create a new {self.__class__.__name__} with the desired unit.' ) ) # noqa: E501 @@ -234,9 +235,7 @@ def energy_offset(self, value: Numeric) -> None: TypeError: If value is not a number. """ if not isinstance(value, Numeric): - raise TypeError( - f"energy_offset must be a number, got {type(value).__name__}" - ) + raise TypeError(f'energy_offset must be a number, got {type(value).__name__}') self._energy_offset.value = value self._on_energy_offset_change() @@ -257,7 +256,7 @@ def convert_unit(self, unit_str: str | sc.Unit) -> None: """ unit = _validate_unit(unit_str) if unit is None: - raise ValueError("unit_str must be a valid unit string or scipp Unit") + raise ValueError('unit_str must be a valid unit string or scipp Unit') self._background_model.convert_unit(unit) self._resolution_model.convert_unit(unit) @@ -293,12 +292,10 @@ def get_all_variables(self, Q_index: int | None = None) -> list[Parameter]: variables = [self._energy_offsets[i] for i in range(len(self._Q))] else: if not isinstance(Q_index, int): - raise TypeError( - f"Q_index must be an int or None, got {type(Q_index).__name__}" - ) + raise TypeError(f'Q_index must be an int or None, got {type(Q_index).__name__}') if Q_index < 0 or Q_index >= len(self._Q): raise IndexError( - f"Q_index {Q_index} is out of bounds for Q of length {len(self._Q)}" + f'Q_index {Q_index} is out of bounds for Q of length {len(self._Q)}' ) variables = [self._energy_offsets[Q_index]] @@ -331,12 +328,10 @@ def get_energy_offset_at_Q(self, Q_index: int) -> Parameter: IndexError: If Q_index is out of bounds. """ if self._Q is None: - raise ValueError("No Q values are set in the InstrumentModel.") + raise ValueError('No Q values are set in the InstrumentModel.') if Q_index < 0 or Q_index >= len(self._Q): - raise IndexError( - f"Q_index {Q_index} is out of bounds for Q of length {len(self._Q)}" - ) + raise IndexError(f'Q_index {Q_index} is out of bounds for Q of length {len(self._Q)}') return self._energy_offsets[Q_index] @@ -383,11 +378,11 @@ def __repr__(self) -> str: """ return ( - f"{self.__class__.__name__}(" - f"unique_name={self.unique_name!r}, " - f"unit={self.unit}, " - f"Q_len={None if self._Q is None else len(self._Q)}, " - f"resolution_model={self._resolution_model!r}, " - f"background_model={self._background_model!r}" - f")" + f'{self.__class__.__name__}(' + f'unique_name={self.unique_name!r}, ' + f'unit={self.unit}, ' + f'Q_len={None if self._Q is None else len(self._Q)}, ' + f'resolution_model={self._resolution_model!r}, ' + f'background_model={self._background_model!r}' + f')' ) diff --git a/src/easydynamics/sample_model/model_base.py b/src/easydynamics/sample_model/model_base.py index dfa52cbfc..b0bdfc445 100644 --- a/src/easydynamics/sample_model/model_base.py +++ b/src/easydynamics/sample_model/model_base.py @@ -19,16 +19,15 @@ class ModelBase(EasyScienceModelBase): """Base class for Sample Models. - Contains common functionality for models with components and - Q dependence. - + Contains common functionality for models with components and Q + dependence. """ def __init__( self, - display_name: str = "MyModelBase", + display_name: str = 'MyModelBase', unique_name: str | None = None, - unit: str | sc.Unit | None = "meV", + unit: str | sc.Unit | None = 'meV', components: ModelComponent | ComponentCollection | None = None, Q: Q_type | None = None, ): @@ -60,8 +59,8 @@ def __init__( components, (ModelComponent, ComponentCollection) ): raise TypeError( - f"Components must be a ModelComponent, a ComponentCollection or None, " - f"got {type(components).__name__}" + f'Components must be a ModelComponent, a ComponentCollection or None, ' + f'got {type(components).__name__}' ) self._components = ComponentCollection() @@ -94,8 +93,8 @@ def evaluate( if not self._component_collections: raise ValueError( - "No components in the model to evaluate. " - "Run generate_component_collections() first" + 'No components in the model to evaluate. ' + 'Run generate_component_collections() first' ) y = [collection.evaluate(x) for collection in self._component_collections] @@ -158,8 +157,8 @@ def unit(self, unit_str: str) -> None: """ raise AttributeError( ( - f"Unit is read-only. Use convert_unit to change the unit between allowed types " - f"or create a new {self.__class__.__name__} with the desired unit." + f'Unit is read-only. Use convert_unit to change the unit between allowed types ' + f'or create a new {self.__class__.__name__} with the desired unit.' ) ) # noqa: E501 @@ -179,9 +178,7 @@ def convert_unit(self, unit: str | sc.Unit) -> None: old_unit = self._unit if not isinstance(unit, (str, sc.Unit)): - raise TypeError( - f"Unit must be a string or sc.Unit, got {type(unit).__name__}" - ) + raise TypeError(f'Unit must be a string or sc.Unit, got {type(unit).__name__}') try: for component in self.components: component.convert_unit(unit) @@ -218,9 +215,7 @@ def components(self, value: ModelComponent | ComponentCollection | None) -> None ComponentCollection, or None. """ if not isinstance(value, (ModelComponent, ComponentCollection, type(None))): - raise TypeError( - "Components must be a ModelComponent or a ComponentCollection" - ) + raise TypeError('Components must be a ModelComponent or a ComponentCollection') self.clear_components() if value is not None: @@ -299,13 +294,11 @@ def get_all_variables(self, Q_index: int | None = None) -> list[Parameter]: ] else: if not isinstance(Q_index, int): - raise TypeError( - f"Q_index must be an int or None, got {type(Q_index).__name__}" - ) + raise TypeError(f'Q_index must be an int or None, got {type(Q_index).__name__}') if Q_index < 0 or Q_index >= len(self._component_collections): raise IndexError( - f"Q_index {Q_index} is out of bounds for component collections " - f"of length {len(self._component_collections)}" + f'Q_index {Q_index} is out of bounds for component collections ' + f'of length {len(self._component_collections)}' ) all_vars = self._component_collections[Q_index].get_all_variables() return all_vars @@ -326,11 +319,11 @@ def get_component_collection(self, Q_index: int) -> ComponentCollection: ComponentCollections. """ if not isinstance(Q_index, int): - raise TypeError(f"Q_index must be an int, got {type(Q_index).__name__}") + raise TypeError(f'Q_index must be an int, got {type(Q_index).__name__}') if Q_index < 0 or Q_index >= len(self._component_collections): raise IndexError( - f"Q_index {Q_index} is out of bounds for component collections " - f"of length {len(self._component_collections)}" + f'Q_index {Q_index} is out of bounds for component collections ' + f'of length {len(self._component_collections)}' ) return self._component_collections[Q_index] @@ -368,6 +361,6 @@ def __repr__(self) -> str: str: A string representation of the ModelBase. """ return ( - f"{self.__class__.__name__}(unique_name={self.unique_name}, " - f"unit={self.unit}), Q = {self.Q}, components = {self.components}" + f'{self.__class__.__name__}(unique_name={self.unique_name}, ' + f'unit={self.unit}), Q = {self.Q}, components = {self.components}' ) diff --git a/src/easydynamics/sample_model/resolution_model.py b/src/easydynamics/sample_model/resolution_model.py index 3d2f828b7..998d862f6 100644 --- a/src/easydynamics/sample_model/resolution_model.py +++ b/src/easydynamics/sample_model/resolution_model.py @@ -19,9 +19,9 @@ class ResolutionModel(ModelBase): def __init__( self, - display_name: str = "MyResolutionModel", + display_name: str = 'MyResolutionModel', unique_name: str | None = None, - unit: str | sc.Unit = "meV", + unit: str | sc.Unit = 'meV', components: ModelComponent | ComponentCollection | None = None, Q: Q_type | None = None, ): @@ -69,7 +69,7 @@ def append_component(self, component: ModelComponent | ComponentCollection) -> N for comp in components: if isinstance(comp, (DeltaFunction, Polynomial)): raise TypeError( - f"Component in ResolutionModel cannot be a {comp.__class__.__name__}" + f'Component in ResolutionModel cannot be a {comp.__class__.__name__}' ) super().append_component(component) diff --git a/src/easydynamics/sample_model/sample_model.py b/src/easydynamics/sample_model/sample_model.py index 1e3acc5e7..f92f3880e 100644 --- a/src/easydynamics/sample_model/sample_model.py +++ b/src/easydynamics/sample_model/sample_model.py @@ -5,9 +5,7 @@ import scipp as sc from easyscience.variable import Parameter -from easydynamics.sample_model.diffusion_model.diffusion_model_base import ( - DiffusionModelBase, -) +from easydynamics.sample_model.diffusion_model.diffusion_model_base import DiffusionModelBase from easydynamics.sample_model.model_base import ModelBase from easydynamics.utils import _detailed_balance_factor from easydynamics.utils.utils import Numeric @@ -28,14 +26,14 @@ class SampleModel(ModelBase): def __init__( self, - display_name: str = "MySampleModel", + display_name: str = 'MySampleModel', unique_name: str | None = None, - unit: str | sc.Unit = "meV", + unit: str | sc.Unit = 'meV', components: ModelComponent | ComponentCollection | None = None, Q: Q_type | None = None, diffusion_models: DiffusionModelBase | list[DiffusionModelBase] | None = None, temperature: float | None = None, - temperature_unit: str | sc.Unit = "K", + temperature_unit: str | sc.Unit = 'K', divide_by_temperature: bool = True, ): """Initialize the SampleModel. @@ -76,8 +74,8 @@ def __init__( isinstance(dm, DiffusionModelBase) for dm in diffusion_models ): raise TypeError( - "diffusion_models must be a DiffusionModelBase, " - "a list of DiffusionModelBase or None" + 'diffusion_models must be a DiffusionModelBase, ' + 'a list of DiffusionModelBase or None' ) self._diffusion_models = diffusion_models @@ -93,21 +91,21 @@ def __init__( self._temperature = None else: if not isinstance(temperature, Numeric): - raise TypeError("temperature must be a number or None") + raise TypeError('temperature must be a number or None') if temperature < 0: - raise ValueError("temperature must be non-negative") + raise ValueError('temperature must be non-negative') self._temperature = Parameter( - name="Temperature", + name='Temperature', value=temperature, unit=temperature_unit, - display_name="Temperature", + display_name='Temperature', fixed=True, ) self._temperature_unit = temperature_unit if not isinstance(divide_by_temperature, bool): - raise TypeError("divide_by_temperature must be True or False") + raise TypeError('divide_by_temperature must be True or False') self._divide_by_temperature = divide_by_temperature # ------------------------------------------------------------------ @@ -128,13 +126,13 @@ def append_diffusion_model(self, diffusion_model: DiffusionModelBase) -> None: if not isinstance(diffusion_model, DiffusionModelBase): raise TypeError( - f"diffusion_model must be a DiffusionModelBase, got {type(diffusion_model).__name__}" # noqa: E501 + f'diffusion_model must be a DiffusionModelBase, got {type(diffusion_model).__name__}' # noqa: E501 ) self._diffusion_models.append(diffusion_model) self._generate_component_collections() - def remove_diffusion_model(self, name: "str") -> None: + def remove_diffusion_model(self, name: 'str') -> None: """Remove a DiffusionModel from the SampleModel by unique name. Args: @@ -150,8 +148,8 @@ def remove_diffusion_model(self, name: "str") -> None: self._generate_component_collections() return raise ValueError( - f"No DiffusionModel with unique name {name} found. \n" - f"The available unique names are: {[dm.unique_name for dm in self._diffusion_models]}" + f'No DiffusionModel with unique name {name} found. \n' + f'The available unique names are: {[dm.unique_name for dm in self._diffusion_models]}' ) def clear_diffusion_models(self) -> None: @@ -200,8 +198,8 @@ def diffusion_models( isinstance(dm, DiffusionModelBase) for dm in value ): raise TypeError( - "diffusion_models must be a DiffusionModelBase, a list of DiffusionModelBase, " - "or None" + 'diffusion_models must be a DiffusionModelBase, a list of DiffusionModelBase, ' + 'or None' ) self._diffusion_models = value self._on_diffusion_models_change() @@ -233,17 +231,17 @@ def temperature(self, value: Numeric | None) -> None: return if not isinstance(value, Numeric): - raise TypeError("temperature must be a number or None") + raise TypeError('temperature must be a number or None') if value < 0: - raise ValueError("temperature must be non-negative") + raise ValueError('temperature must be non-negative') if self._temperature is None: self._temperature = Parameter( - name="Temperature", + name='Temperature', value=value, unit=self._temperature_unit, - display_name="Temperature", + display_name='Temperature', fixed=True, ) else: @@ -271,8 +269,8 @@ def temperature_unit(self, value: str | sc.Unit) -> None: """ raise AttributeError( - f"Temperature_unit is read-only. Use convert_temperature_unit to change the unit between allowed types " # noqa: E501 - f"or create a new {self.__class__.__name__} with the desired unit." + f'Temperature_unit is read-only. Use convert_temperature_unit to change the unit between allowed types ' # noqa: E501 + f'or create a new {self.__class__.__name__} with the desired unit.' ) # noqa: E501 def convert_temperature_unit(self, unit: str | sc.Unit) -> None: @@ -288,7 +286,7 @@ def convert_temperature_unit(self, unit: str | sc.Unit) -> None: """ if self._temperature is None: - raise ValueError("Temperature is not set, cannot convert unit.") + raise ValueError('Temperature is not set, cannot convert unit.') old_unit = self._temperature.unit @@ -327,7 +325,7 @@ def divide_by_temperature(self, value: bool) -> None: TypeError: If value is not a bool. """ if not isinstance(value, bool): - raise TypeError("divide_by_temperature must be True or False") + raise TypeError('divide_by_temperature must be True or False') self._divide_by_temperature = value # ------------------------------------------------------------------ @@ -404,12 +402,8 @@ def _generate_component_collections(self) -> None: # Generate components from diffusion models # and add to component collections for diffusion_model in self._diffusion_models: - diffusion_collections = diffusion_model.create_component_collections( - Q=self._Q - ) - for target, source in zip( - self._component_collections, diffusion_collections - ): + diffusion_collections = diffusion_model.create_component_collections(Q=self._Q) + for target, source in zip(self._component_collections, diffusion_collections): for component in source.components: target.append_component(component) @@ -429,9 +423,9 @@ def __repr__(self) -> str: """ return ( - f"{self.__class__.__name__}(unique_name={self.unique_name}, unit={self._unit}), " - f"Q = {self._Q}, " - f"components = {self._components}, diffusion_models = {self._diffusion_models}, " - f"temperature = {self._temperature}, " - f"divide_by_temperature = {self._divide_by_temperature}" + f'{self.__class__.__name__}(unique_name={self.unique_name}, unit={self._unit}), ' + f'Q = {self._Q}, ' + f'components = {self._components}, diffusion_models = {self._diffusion_models}, ' + f'temperature = {self._temperature}, ' + f'divide_by_temperature = {self._divide_by_temperature}' ) diff --git a/src/easydynamics/utils/detailed_balance.py b/src/easydynamics/utils/detailed_balance.py index 28802068a..8b47b22da 100644 --- a/src/easydynamics/utils/detailed_balance.py +++ b/src/easydynamics/utils/detailed_balance.py @@ -25,8 +25,8 @@ def _detailed_balance_factor( energy: int | float | list | np.ndarray | sc.Variable, temperature: int | float | sc.Variable | Parameter, - energy_unit: str | sc.Unit = "meV", - temperature_unit: str | sc.Unit = "K", + energy_unit: str | sc.Unit = 'meV', + temperature_unit: str | sc.Unit = 'K', divide_by_temperature: bool = True, ) -> np.ndarray: r""" @@ -79,60 +79,60 @@ def _detailed_balance_factor( # Input validation if not isinstance(divide_by_temperature, bool): - raise TypeError("divide_by_temperature must be True or False.") + raise TypeError('divide_by_temperature must be True or False.') if not isinstance(energy_unit, (str, sc.Unit)): - raise TypeError("energy_unit must be a string or scipp.Unit.") + raise TypeError('energy_unit must be a string or scipp.Unit.') if not isinstance(temperature_unit, (str, sc.Unit)): - raise TypeError("temperature_unit must be a string or scipp.Unit.") + raise TypeError('temperature_unit must be a string or scipp.Unit.') # Convert temperature and energy to sc variables # to make units easy to handle temperature = _convert_to_scipp_variable( - value=temperature, unit=temperature_unit, name="temperature" + value=temperature, unit=temperature_unit, name='temperature' ) if temperature.value < 0: - raise ValueError("Temperature must be non-negative.") + raise ValueError('Temperature must be non-negative.') - energy = _convert_to_scipp_variable(value=energy, unit=energy_unit, name="energy") + energy = _convert_to_scipp_variable(value=energy, unit=energy_unit, name='energy') # What if people give units that don't make sense? try: - sc.to_unit(energy, unit="meV") + sc.to_unit(energy, unit='meV') except Exception as e: raise UnitError( - f"The unit of energy is wrong: {energy.unit}: {e} Check that energy has a valid unit." + f'The unit of energy is wrong: {energy.unit}: {e} Check that energy has a valid unit.' ) # We give users the option to specify the unit of the energy, # but if the input has a unit, they might clash if energy.unit != energy_unit: warnings.warn( - f"Input energy has unit {energy.unit}, but energy_unit was set to {energy_unit}. " - f"Using {energy.unit}." + f'Input energy has unit {energy.unit}, but energy_unit was set to {energy_unit}. ' + f'Using {energy.unit}.' ) # Same for temperature try: - sc.to_unit(temperature, unit="K") + sc.to_unit(temperature, unit='K') except Exception as e: raise UnitError( - f"The unit of temperature is wrong: {temperature.unit}: {e} " - f"Check that temperature has a valid unit." + f'The unit of temperature is wrong: {temperature.unit}: {e} ' + f'Check that temperature has a valid unit.' ) if temperature.unit != temperature_unit: warnings.warn( - f"Input temperature has unit {temperature.unit}, " - f"but temperature_unit was set to {temperature_unit}. Using {temperature.unit}." + f'Input temperature has unit {temperature.unit}, ' + f'but temperature_unit was set to {temperature_unit}. Using {temperature.unit}.' ) # Zero temperature deserves special treatment. # Here, DBF is 0 for energy<0 and energy for energy>0 if temperature.value == 0: if divide_by_temperature: - raise ZeroDivisionError("Cannot divide by T when T = 0.") + raise ZeroDivisionError('Cannot divide by T when T = 0.') DBF = sc.where(energy < 0.0 * energy.unit, 0.0 * energy.unit, energy) if DBF.sizes == {}: @@ -148,7 +148,7 @@ def _detailed_balance_factor( x = energy / (kB * temperature) - x = sc.to_unit(x, unit="1") # Make sure the unit is 1 and not e.g. 1e3 + x = sc.to_unit(x, unit='1') # Make sure the unit is 1 and not e.g. 1e3 # Now compute DBF. First handle small and large x, then general. @@ -165,9 +165,7 @@ def _detailed_balance_factor( # General case: exact formula mid = sc.logical_not(small) & sc.logical_not(large) - DBF = sc.where( - mid, x / (1 - sc.exp(-x)), DBF - ) # zeros in x are handled by SMALL_THRESHOLD + DBF = sc.where(mid, x / (1 - sc.exp(-x)), DBF) # zeros in x are handled by SMALL_THRESHOLD # if not divide_by_temperature: @@ -223,13 +221,11 @@ def _convert_to_scipp_variable( array_value = np.array(value.value) unit = value.unit else: - if name == "energy": - raise TypeError( - f"{name} must be a number, list, numpy array or scipp Variable" - ) + if name == 'energy': + raise TypeError(f'{name} must be a number, list, numpy array or scipp Variable') else: raise TypeError( - f"{name} must be a number, list, numpy array, Parameter or scipp Variable" + f'{name} must be a number, list, numpy array, Parameter or scipp Variable' ) # Create appropriate scipp variable based on shape @@ -242,6 +238,6 @@ def _convert_to_scipp_variable( else: # Multi-element array try: - return sc.array(dims=["x"], values=array_value, unit=unit) + return sc.array(dims=['x'], values=array_value, unit=unit) except UnitError as e: raise UnitError(f"Invalid unit string '{unit}' for {name}: {e}") From 886a53ce98e2fe808f5b06eb484a1c3b67b82555 Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Fri, 13 Mar 2026 18:55:25 +0100 Subject: [PATCH 12/31] Aply unsafe auto formatting --- src/easydynamics/analysis/analysis.py | 2 +- src/easydynamics/analysis/analysis1d.py | 2 +- src/easydynamics/analysis/analysis_base.py | 2 +- src/easydynamics/convolution/analytical_convolution.py | 2 +- src/easydynamics/convolution/convolution.py | 2 +- src/easydynamics/convolution/convolution_base.py | 2 +- src/easydynamics/convolution/numerical_convolution.py | 2 +- .../convolution/numerical_convolution_base.py | 2 +- src/easydynamics/experiment/experiment.py | 8 ++++---- src/easydynamics/sample_model/background_model.py | 2 +- src/easydynamics/sample_model/component_collection.py | 2 +- .../sample_model/components/damped_harmonic_oscillator.py | 2 +- .../sample_model/components/delta_function.py | 2 +- src/easydynamics/sample_model/components/exponential.py | 4 ++-- src/easydynamics/sample_model/components/gaussian.py | 2 +- src/easydynamics/sample_model/components/lorentzian.py | 2 +- .../sample_model/components/model_component.py | 8 ++++---- src/easydynamics/sample_model/components/polynomial.py | 4 ++-- src/easydynamics/sample_model/components/voigt.py | 2 +- .../diffusion_model/brownian_translational_diffusion.py | 2 +- .../sample_model/diffusion_model/diffusion_model_base.py | 2 +- .../diffusion_model/jump_translational_diffusion.py | 2 +- src/easydynamics/sample_model/instrument_model.py | 2 +- src/easydynamics/sample_model/model_base.py | 2 +- src/easydynamics/sample_model/resolution_model.py | 2 +- src/easydynamics/sample_model/sample_model.py | 2 +- 26 files changed, 34 insertions(+), 34 deletions(-) diff --git a/src/easydynamics/analysis/analysis.py b/src/easydynamics/analysis/analysis.py index acd122c41..4bad97757 100644 --- a/src/easydynamics/analysis/analysis.py +++ b/src/easydynamics/analysis/analysis.py @@ -63,7 +63,7 @@ def __init__( sample_model: SampleModel | None = None, instrument_model: InstrumentModel | None = None, extra_parameters: Parameter | list[Parameter] | None = None, - ): + ) -> None: """Initialize an Analysis object. Args: diff --git a/src/easydynamics/analysis/analysis1d.py b/src/easydynamics/analysis/analysis1d.py index 0fdaa9cec..809b5e343 100644 --- a/src/easydynamics/analysis/analysis1d.py +++ b/src/easydynamics/analysis/analysis1d.py @@ -69,7 +69,7 @@ def __init__( instrument_model: InstrumentModel | None = None, Q_index: int | None = None, extra_parameters: Parameter | list[Parameter] | None = None, - ): + ) -> None: """Initialize a Analysis1d. Args: diff --git a/src/easydynamics/analysis/analysis_base.py b/src/easydynamics/analysis/analysis_base.py index 0c1ab1714..365b54fb2 100644 --- a/src/easydynamics/analysis/analysis_base.py +++ b/src/easydynamics/analysis/analysis_base.py @@ -61,7 +61,7 @@ def __init__( sample_model: SampleModel | None = None, instrument_model: InstrumentModel | None = None, extra_parameters: Parameter | list[Parameter] | None = None, - ): + ) -> None: """Initialize the AnalysisBase. Args: diff --git a/src/easydynamics/convolution/analytical_convolution.py b/src/easydynamics/convolution/analytical_convolution.py index 2026c461e..534b4d7a6 100644 --- a/src/easydynamics/convolution/analytical_convolution.py +++ b/src/easydynamics/convolution/analytical_convolution.py @@ -54,7 +54,7 @@ def __init__( sample_components: ComponentCollection | ModelComponent | None = None, resolution_components: ComponentCollection | ModelComponent | None = None, energy_offset: Numeric | Parameter = 0.0, - ): + ) -> None: """Initialize an AnalyticalConvolution. Args: diff --git a/src/easydynamics/convolution/convolution.py b/src/easydynamics/convolution/convolution.py index 14df08f49..65bbf9ec8 100644 --- a/src/easydynamics/convolution/convolution.py +++ b/src/easydynamics/convolution/convolution.py @@ -104,7 +104,7 @@ def __init__( temperature_unit: str | sc.Unit = 'K', energy_unit: str | sc.Unit = 'meV', normalize_detailed_balance: bool = True, - ): + ) -> None: """Initialize the Convolution class. Args: diff --git a/src/easydynamics/convolution/convolution_base.py b/src/easydynamics/convolution/convolution_base.py index bd7638994..7972681e5 100644 --- a/src/easydynamics/convolution/convolution_base.py +++ b/src/easydynamics/convolution/convolution_base.py @@ -45,7 +45,7 @@ def __init__( resolution_components: ComponentCollection | ModelComponent = None, energy_unit: str | sc.Unit = 'meV', energy_offset: Numeric | Parameter = 0.0, - ): + ) -> None: """Initialize the ConvolutionBase. Args: diff --git a/src/easydynamics/convolution/numerical_convolution.py b/src/easydynamics/convolution/numerical_convolution.py index 79cc033d0..27c10b2b3 100644 --- a/src/easydynamics/convolution/numerical_convolution.py +++ b/src/easydynamics/convolution/numerical_convolution.py @@ -76,7 +76,7 @@ def __init__( temperature_unit: str | sc.Unit = 'K', energy_unit: str | sc.Unit = 'meV', normalize_detailed_balance: bool = True, - ): + ) -> None: """Initialize the NumericalConvolution object. Args: diff --git a/src/easydynamics/convolution/numerical_convolution_base.py b/src/easydynamics/convolution/numerical_convolution_base.py index e5e37a54a..ad4dffa77 100644 --- a/src/easydynamics/convolution/numerical_convolution_base.py +++ b/src/easydynamics/convolution/numerical_convolution_base.py @@ -86,7 +86,7 @@ def __init__( temperature_unit: str | sc.Unit = 'K', energy_unit: str | sc.Unit = 'meV', normalize_detailed_balance: bool = True, - ): + ) -> None: """Initialize the NumericalConvolutionBase. Args: diff --git a/src/easydynamics/experiment/experiment.py b/src/easydynamics/experiment/experiment.py index 06ecd2663..b7d3845bc 100644 --- a/src/easydynamics/experiment/experiment.py +++ b/src/easydynamics/experiment/experiment.py @@ -41,7 +41,7 @@ def __init__( display_name: str = 'MyExperiment', unique_name: str | None = None, data: sc.DataArray | str | None = None, - ): + ) -> None: """Initialize the Experiment object. Args: @@ -219,7 +219,7 @@ def get_masked_energy(self, Q_index: int) -> sc.Variable | None: # Handle data ########### - def load_hdf5(self, filename: str, display_name: str | None = None): + def load_hdf5(self, filename: str, display_name: str | None = None) -> None: """Load data from an HDF5 file. Args: @@ -251,7 +251,7 @@ def load_hdf5(self, filename: str, display_name: str | None = None): self._validate_coordinates(loaded_data) self.data = loaded_data - def save_hdf5(self, filename: str | None = None): + def save_hdf5(self, filename: str | None = None) -> None: """Save the dataset to HDF5. Args: @@ -279,7 +279,7 @@ def save_hdf5(self, filename: str | None = None): sc_save_hdf5(self._data, filename) - def remove_data(self): + def remove_data(self) -> None: """Remove the dataset from the experiment.""" self._data = None self._binned_data = None diff --git a/src/easydynamics/sample_model/background_model.py b/src/easydynamics/sample_model/background_model.py index 420fce403..91ced6db1 100644 --- a/src/easydynamics/sample_model/background_model.py +++ b/src/easydynamics/sample_model/background_model.py @@ -42,7 +42,7 @@ def __init__( unit: str | sc.Unit = 'meV', components: ComponentCollection | ModelComponent | None = None, Q: Q_type | None = None, - ): + ) -> None: """Initialize the BackgroundModel. Args: diff --git a/src/easydynamics/sample_model/component_collection.py b/src/easydynamics/sample_model/component_collection.py index 90ae96194..4b90f85cf 100644 --- a/src/easydynamics/sample_model/component_collection.py +++ b/src/easydynamics/sample_model/component_collection.py @@ -44,7 +44,7 @@ def __init__( display_name: str = 'MyComponentCollection', unique_name: str | None = None, components: List[ModelComponent] | None = None, - ): + ) -> None: """Initialize a new ComponentCollection. Args: diff --git a/src/easydynamics/sample_model/components/damped_harmonic_oscillator.py b/src/easydynamics/sample_model/components/damped_harmonic_oscillator.py index bf1007e6c..ce4a08fb1 100644 --- a/src/easydynamics/sample_model/components/damped_harmonic_oscillator.py +++ b/src/easydynamics/sample_model/components/damped_harmonic_oscillator.py @@ -56,7 +56,7 @@ def __init__( unit: str | sc.Unit = 'meV', display_name: str | None = 'DampedHarmonicOscillator', unique_name: str | None = None, - ): + ) -> None: """Initialize the Damped Harmonic Oscillator. Args: diff --git a/src/easydynamics/sample_model/components/delta_function.py b/src/easydynamics/sample_model/components/delta_function.py index 5a06a26f9..11e444f34 100644 --- a/src/easydynamics/sample_model/components/delta_function.py +++ b/src/easydynamics/sample_model/components/delta_function.py @@ -48,7 +48,7 @@ def __init__( unit: str | sc.Unit = 'meV', display_name: str | None = 'DeltaFunction', unique_name: str | None = None, - ): + ) -> None: """Initialize the Delta function. Args: diff --git a/src/easydynamics/sample_model/components/exponential.py b/src/easydynamics/sample_model/components/exponential.py index 8ad9fe09f..2f9e41ad8 100644 --- a/src/easydynamics/sample_model/components/exponential.py +++ b/src/easydynamics/sample_model/components/exponential.py @@ -55,7 +55,7 @@ def __init__( unit: str | sc.Unit = 'meV', display_name: str | None = 'Exponential', unique_name: str | None = None, - ): + ) -> None: """Initialize the Exponential component. Args: @@ -216,7 +216,7 @@ def evaluate( return self.amplitude.value * np.exp(exponent) - def convert_unit(self, unit: str | sc.Unit): + def convert_unit(self, unit: str | sc.Unit) -> None: """Convert the unit of the Parameters in the component. Args: diff --git a/src/easydynamics/sample_model/components/gaussian.py b/src/easydynamics/sample_model/components/gaussian.py index 7e89cf18a..cfc83c16a 100644 --- a/src/easydynamics/sample_model/components/gaussian.py +++ b/src/easydynamics/sample_model/components/gaussian.py @@ -60,7 +60,7 @@ def __init__( unit: str | sc.Unit = 'meV', display_name: str | None = 'Gaussian', unique_name: str | None = None, - ): + ) -> None: """Initialize the Gaussian component. Args: diff --git a/src/easydynamics/sample_model/components/lorentzian.py b/src/easydynamics/sample_model/components/lorentzian.py index b61b7896b..64ebe5948 100644 --- a/src/easydynamics/sample_model/components/lorentzian.py +++ b/src/easydynamics/sample_model/components/lorentzian.py @@ -55,7 +55,7 @@ def __init__( unit: str | sc.Unit = 'meV', display_name: str | None = 'Lorentzian', unique_name: str | None = None, - ): + ) -> None: """Initialize the Lorentzian component. Args: diff --git a/src/easydynamics/sample_model/components/model_component.py b/src/easydynamics/sample_model/components/model_component.py index d4d5bc494..e1ecad34d 100644 --- a/src/easydynamics/sample_model/components/model_component.py +++ b/src/easydynamics/sample_model/components/model_component.py @@ -39,7 +39,7 @@ def __init__( unit: str | sc.Unit = 'meV', display_name: str | None = None, unique_name: str | None = None, - ): + ) -> None: """Initialize the ModelComponent. Args: @@ -85,14 +85,14 @@ def unit(self, unit_str: str) -> None: ) ) # noqa: E501 - def fix_all_parameters(self): + def fix_all_parameters(self) -> None: """Fix all parameters in the model component.""" pars = self.get_fittable_parameters() for p in pars: p.fixed = True - def free_all_parameters(self): + def free_all_parameters(self) -> None: """Free all parameters in the model component.""" for p in self.get_fittable_parameters(): p.fixed = False @@ -182,7 +182,7 @@ def validate_unit(unit) -> None: f'unit must be None, a string, or a scipp Unit, got {type(unit).__name__}' ) - def convert_unit(self, unit: str | sc.Unit): + def convert_unit(self, unit: str | sc.Unit) -> None: """Convert the unit of the Parameters in the component. Args: diff --git a/src/easydynamics/sample_model/components/polynomial.py b/src/easydynamics/sample_model/components/polynomial.py index 4cd60902c..d3a8477d6 100644 --- a/src/easydynamics/sample_model/components/polynomial.py +++ b/src/easydynamics/sample_model/components/polynomial.py @@ -48,7 +48,7 @@ def __init__( unit: str | sc.Unit = 'meV', display_name: str | None = 'Polynomial', unique_name: str | None = None, - ): + ) -> None: """Initialize the Polynomial component. Args: @@ -216,7 +216,7 @@ def get_all_variables(self) -> list[DescriptorBase]: """ return list(self._coefficients) - def convert_unit(self, unit: str | sc.Unit): + def convert_unit(self, unit: str | sc.Unit) -> None: """Convert the unit of the polynomial. Args: diff --git a/src/easydynamics/sample_model/components/voigt.py b/src/easydynamics/sample_model/components/voigt.py index 16d5d1e08..d9ee918b7 100644 --- a/src/easydynamics/sample_model/components/voigt.py +++ b/src/easydynamics/sample_model/components/voigt.py @@ -54,7 +54,7 @@ def __init__( unit: str | sc.Unit = 'meV', display_name: str | None = 'Voigt', unique_name: str | None = None, - ): + ) -> None: """Initialize a Voigt component. Args: diff --git a/src/easydynamics/sample_model/diffusion_model/brownian_translational_diffusion.py b/src/easydynamics/sample_model/diffusion_model/brownian_translational_diffusion.py index 25dcb1102..77285141e 100644 --- a/src/easydynamics/sample_model/diffusion_model/brownian_translational_diffusion.py +++ b/src/easydynamics/sample_model/diffusion_model/brownian_translational_diffusion.py @@ -46,7 +46,7 @@ def __init__( unit: str | sc.Unit = 'meV', scale: Numeric = 1.0, diffusion_coefficient: Numeric = 1.0, - ): + ) -> None: """Initialize a new BrownianTranslationalDiffusion model. Args: diff --git a/src/easydynamics/sample_model/diffusion_model/diffusion_model_base.py b/src/easydynamics/sample_model/diffusion_model/diffusion_model_base.py index d8ba1a3a9..7a0d3e08f 100644 --- a/src/easydynamics/sample_model/diffusion_model/diffusion_model_base.py +++ b/src/easydynamics/sample_model/diffusion_model/diffusion_model_base.py @@ -19,7 +19,7 @@ def __init__( unique_name: str | None = None, scale: Numeric = 1.0, unit: str | sc.Unit = 'meV', - ): + ) -> None: """Initialize a new DiffusionModel. Args: diff --git a/src/easydynamics/sample_model/diffusion_model/jump_translational_diffusion.py b/src/easydynamics/sample_model/diffusion_model/jump_translational_diffusion.py index 46d3572c4..629251723 100644 --- a/src/easydynamics/sample_model/diffusion_model/jump_translational_diffusion.py +++ b/src/easydynamics/sample_model/diffusion_model/jump_translational_diffusion.py @@ -52,7 +52,7 @@ def __init__( scale: Numeric = 1.0, diffusion_coefficient: Numeric = 1.0, relaxation_time: Numeric = 1.0, - ): + ) -> None: """Initialize a new JumpTranslationalDiffusion model. Args: diff --git a/src/easydynamics/sample_model/instrument_model.py b/src/easydynamics/sample_model/instrument_model.py index 1b0b54ab9..2768cb621 100644 --- a/src/easydynamics/sample_model/instrument_model.py +++ b/src/easydynamics/sample_model/instrument_model.py @@ -33,7 +33,7 @@ def __init__( background_model: BackgroundModel | None = None, energy_offset: Numeric | None = None, unit: str | sc.Unit = 'meV', - ): + ) -> None: """Initialize an InstrumentModel. Args: diff --git a/src/easydynamics/sample_model/model_base.py b/src/easydynamics/sample_model/model_base.py index b0bdfc445..88f95c766 100644 --- a/src/easydynamics/sample_model/model_base.py +++ b/src/easydynamics/sample_model/model_base.py @@ -30,7 +30,7 @@ def __init__( unit: str | sc.Unit | None = 'meV', components: ModelComponent | ComponentCollection | None = None, Q: Q_type | None = None, - ): + ) -> None: """Initialize the ModelBase. Args: diff --git a/src/easydynamics/sample_model/resolution_model.py b/src/easydynamics/sample_model/resolution_model.py index 998d862f6..639bf8f81 100644 --- a/src/easydynamics/sample_model/resolution_model.py +++ b/src/easydynamics/sample_model/resolution_model.py @@ -24,7 +24,7 @@ def __init__( unit: str | sc.Unit = 'meV', components: ModelComponent | ComponentCollection | None = None, Q: Q_type | None = None, - ): + ) -> None: """Initialize a ResolutionModel. Args: diff --git a/src/easydynamics/sample_model/sample_model.py b/src/easydynamics/sample_model/sample_model.py index f92f3880e..ea5623afa 100644 --- a/src/easydynamics/sample_model/sample_model.py +++ b/src/easydynamics/sample_model/sample_model.py @@ -35,7 +35,7 @@ def __init__( temperature: float | None = None, temperature_unit: str | sc.Unit = 'K', divide_by_temperature: bool = True, - ): + ) -> None: """Initialize the SampleModel. Args: From dec92c5e746cece247b0a2ba92728ef887a6bf05 Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Sat, 14 Mar 2026 20:54:57 +0100 Subject: [PATCH 13/31] More linting --- .../sample_model/components/polynomial.py | 88 ++++++++----------- .../sample_model/components/voigt.py | 86 +++++++----------- .../brownian_translational_diffusion.py | 78 ++++++++-------- 3 files changed, 109 insertions(+), 143 deletions(-) diff --git a/src/easydynamics/sample_model/components/polynomial.py b/src/easydynamics/sample_model/components/polynomial.py index d3a8477d6..bf26ef710 100644 --- a/src/easydynamics/sample_model/components/polynomial.py +++ b/src/easydynamics/sample_model/components/polynomial.py @@ -25,59 +25,41 @@ class Polynomial(ModelComponent): I(x) = c_0 + c_1 x + c_2 x^2 + ... + c_N x^N, $$ where $C_i$ are the coefficients. - - Args: - coefficients (list or tuple): Coefficients c0, c1, ..., cN - unit (str or sc.Unit): Unit of the Polynomial component. - display_name (str): Display name of the Polynomial component. - unique_name (str or None): Unique name of the component. - If None, a unique_name is automatically generated. - - Attributes: - coefficients (list of Parameter): Coefficients of the polynomial - as Parameters. - unit (str): Unit of the Polynomial component. - display_name (str): Display name of the Polynomial component. - unique_name (str or None): Unique name of the component. - If None, a unique_name is automatically generated. """ def __init__( self, coefficients: Sequence[Numeric | Parameter] = (0.0,), - unit: str | sc.Unit = 'meV', - display_name: str | None = 'Polynomial', + unit: str | sc.Unit = "meV", + display_name: str | None = "Polynomial", unique_name: str | None = None, ) -> None: """Initialize the Polynomial component. Args: - coefficients (list or tuple): Coefficients c0, c1, ..., cN - unit (str or sc.Unit): Unit of the Polynomial component. - display_name (str): Display name of the Polynomial + coefficients (Sequence[Numeric | Parameter], default=(0.0,)): Coefficients c0, c1, ..., cN + unit (str | sc.Unit, default='meV'): Unit of the Polynomial component. + display_name (str | None, default='Polynomial'): Display name of the Polynomial component. - unique_name (str or None): Unique name of the component. + unique_name (str | None, default=None): Unique name of the component. If None, a unique_name is automatically generated. Raises: TypeError: If coefficients is not a sequence of numbers or - Parameters. + Parameters or if any item in coefficients is not a number or Parameter. ValueError: If coefficients is an empty sequence. - TypeError: If any item in coefficients is not a number or - Parameter. - UnitError: If unit is not a string or sc.Unit. """ super().__init__(display_name=display_name, unit=unit, unique_name=unique_name) if not isinstance(coefficients, (list, tuple, np.ndarray)): raise TypeError( - 'coefficients must be a sequence (list/tuple/ndarray) \ - of numbers or Parameter objects.' + "coefficients must be a sequence (list/tuple/ndarray) \ + of numbers or Parameter objects." ) if len(coefficients) == 0: - raise ValueError('At least one coefficient must be provided.') + raise ValueError("At least one coefficient must be provided.") # Internal storage of Parameter objects self._coefficients: list[Parameter] = [] @@ -87,9 +69,11 @@ def __init__( if isinstance(coef, Parameter): param = coef elif isinstance(coef, Numeric): - param = Parameter(name=f'{display_name}_c{i}', value=float(coef)) + param = Parameter(name=f"{display_name}_c{i}", value=float(coef)) else: - raise TypeError('Each coefficient must be either a numeric value or a Parameter.') + raise TypeError( + "Each coefficient must be either a numeric value or a Parameter." + ) self._coefficients.append(param) # Helper scipp scalar to track unit conversions @@ -118,19 +102,17 @@ def coefficients(self, coeffs: Sequence[Numeric | Parameter]) -> None: Raises: TypeError: If coeffs is not a sequence of numbers or - Parameters. + Parameters or if any item in coeffs is not a number or Parameter. ValueError: If the length of coeffs does not match the existing number of coefficients. - TypeError: If any item in coeffs is not a number or - Parameter. """ if not isinstance(coeffs, (list, tuple, np.ndarray)): raise TypeError( - 'coefficients must be a sequence (list/tuple/ndarray) of numbers or Parameter .' + "coefficients must be a sequence (list/tuple/ndarray) of numbers or Parameter ." ) if len(coeffs) != len(self._coefficients): raise ValueError( - 'Number of coefficients must match the existing number of coefficients.' + "Number of coefficients must match the existing number of coefficients." ) for i, coef in enumerate(coeffs): if isinstance(coef, Parameter): @@ -139,7 +121,9 @@ def coefficients(self, coeffs: Sequence[Numeric | Parameter]) -> None: elif isinstance(coef, Numeric): self._coefficients[i].value = float(coef) else: - raise TypeError('Each coefficient must be either a numeric value or a Parameter.') + raise TypeError( + "Each coefficient must be either a numeric value or a Parameter." + ) def coefficient_values(self) -> list[float]: """Get the coefficients of the polynomial as a list. @@ -150,7 +134,9 @@ def coefficient_values(self) -> list[float]: coefficient_list = [param.value for param in self._coefficients] return coefficient_list - def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray: + def evaluate( + self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray + ) -> np.ndarray: r"""Evaluate the Polynomial at the given x values. The intensity is given by @@ -176,8 +162,8 @@ def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) if any(result < 0): warnings.warn( - f'The Polynomial with unique_name {self.unique_name} has negative values, ' - 'which may not be physically meaningful.', + f"The Polynomial with unique_name {self.unique_name} has negative values, " + "which may not be physically meaningful.", UserWarning, ) return result @@ -204,15 +190,15 @@ def degree(self, value: int) -> None: directly. """ raise AttributeError( - 'The degree of the polynomial is determined by the number of coefficients \ - and cannot be set directly.' + "The degree of the polynomial is determined by the number of coefficients \ + and cannot be set directly." ) def get_all_variables(self) -> list[DescriptorBase]: - """Get all parameters from the model component. + """Get all variables from the model component. Returns: - List[Parameter]: List of parameters in the component. + list[DescriptorBase]: List of variables in the component. """ return list(self._coefficients) @@ -220,19 +206,21 @@ def convert_unit(self, unit: str | sc.Unit) -> None: """Convert the unit of the polynomial. Args: - unit (str or sc.Unit): The target unit to convert to. + unit (str | sc.Unit): The target unit to convert to. Raises: UnitError: If the provided unit is not a string or sc.Unit. """ if not isinstance(unit, (str, sc.Unit)): - raise UnitError('unit must be a string or a scipp unit.') + raise UnitError("unit must be a string or a scipp unit.") # Find out how much the unit changes # by converting a helper variable conversion_value_before = self._unit_conversion_helper.value - self._unit_conversion_helper = sc.to_unit(self._unit_conversion_helper, unit=unit) + self._unit_conversion_helper = sc.to_unit( + self._unit_conversion_helper, unit=unit + ) conversion_value_after = self._unit_conversion_helper.value for i, param in enumerate(self._coefficients): param.value *= ( @@ -248,9 +236,11 @@ def __repr__(self) -> str: str: A string representation of the Polynomial. """ - coeffs_str = ', '.join(f'{param.name}={param.value}' for param in self._coefficients) - return f'Polynomial(unique_name = {self.unique_name}, \ - unit = {self._unit},\n coefficients = [{coeffs_str}])' + coeffs_str = ", ".join( + f"{param.name}={param.value}" for param in self._coefficients + ) + return f"Polynomial(unique_name = {self.unique_name}, \ + unit = {self._unit},\n coefficients = [{coeffs_str}])" # from typing import Callable, Dict diff --git a/src/easydynamics/sample_model/components/voigt.py b/src/easydynamics/sample_model/components/voigt.py index d9ee918b7..088f3f6f2 100644 --- a/src/easydynamics/sample_model/components/voigt.py +++ b/src/easydynamics/sample_model/components/voigt.py @@ -20,29 +20,6 @@ class Voigt(CreateParametersMixin, ModelComponent): typically what you want in QENS. Use scipy.special.voigt_profile to evaluate the Voigt profile. - - Args: - area (Int | float): Total area under the curve. - center (Int | float | None): Center of the Voigt profile. - gaussian_width (Int | float): Standard deviation of the - Gaussian part. - lorentzian_width (Int | float): Half width at half max (HWHM) - of the Lorentzian part. - unit (str | sc.Unit): Unit of the parameters. Defaults to "meV" - display_name (str | None): Display name of the component. - unique_name (str | None): Unique name of the component. - If None, a unique_name is automatically generated. - - Attributes: - area (Parameter): Total area under the curve. - center (Parameter): Center of the Voigt profile. - gaussian_width (Parameter): Standard deviation of the Gaussian - part. - lorentzian_width (Parameter): Half width at half max (HWHM) of - the Lorentzian part. - unit (str | sc.Unit): Unit of the parameters. - display_name (str | None): Display name of the component. - unique_name (str | None): Unique name of the component. """ def __init__( @@ -51,30 +28,23 @@ def __init__( center: Numeric | Parameter | None = None, gaussian_width: Numeric | Parameter = 1.0, lorentzian_width: Numeric | Parameter = 1.0, - unit: str | sc.Unit = 'meV', - display_name: str | None = 'Voigt', + unit: str | sc.Unit = "meV", + display_name: str | None = "Voigt", unique_name: str | None = None, ) -> None: """Initialize a Voigt component. Args: - area (Int | float): Total area under the curve. - center (Int | float | None): Center of the Voigt profile. - gaussian_width (Int | float): Standard deviation of the + area (Numeric | Parameter, default=1.0): Total area under the curve. + center (Numeric | Parameter | None, default=None): Center of the Voigt profile. + gaussian_width (Numeric | Parameter, default=1.0): Standard deviation of the Gaussian part. - lorentzian_width (Int | float): Half width at half max + lorentzian_width (Numeric | Parameter, default=1.0): Half width at half max (HWHM) of the Lorentzian part. - unit (str | sc.Unit): Unit of the parameters. Defaults to - "meV" - display_name (str | None): Display name of the component. - unique_name (str | None): Unique name of the component. + unit (str | sc.Unit, default='meV'): Unit of the parameters. + display_name (str | None, default='Voigt'): Display name of the component. + unique_name (str | None, default=None): Unique name of the component. If None, a unique_name is automatically generated. - - Raises: - TypeError: If any of the parameters are not of the correct - type. - ValueError: If any of the parameters are not valid (e.g. - negative widths). """ super().__init__( @@ -84,20 +54,22 @@ def __init__( ) # These methods live in ValidationMixin - area = self._create_area_parameter(area=area, name=display_name, unit=self._unit) + area = self._create_area_parameter( + area=area, name=display_name, unit=self._unit + ) center = self._create_center_parameter( center=center, name=display_name, fix_if_none=True, unit=self._unit ) gaussian_width = self._create_width_parameter( width=gaussian_width, name=display_name, - param_name='gaussian_width', + param_name="gaussian_width", unit=self._unit, ) lorentzian_width = self._create_width_parameter( width=lorentzian_width, name=display_name, - param_name='lorentzian_width', + param_name="lorentzian_width", unit=self._unit, ) @@ -126,7 +98,7 @@ def area(self, value: Numeric) -> None: TypeError: If the value is not a number. """ if not isinstance(value, Numeric): - raise TypeError('area must be a number') + raise TypeError("area must be a number") self._area.value = value @property @@ -144,7 +116,7 @@ def center(self, value: Numeric | None) -> None: Args: value (Numeric | None): The new value for the center - parameter. If None, defaults to 0 and is fixed. + parameter. If None, defaults to 0 and is fixed. Raises: TypeError: If the value is not a number. @@ -153,7 +125,7 @@ def center(self, value: Numeric | None) -> None: value = 0.0 self._center.fixed = True if not isinstance(value, Numeric): - raise TypeError('center must be a number') + raise TypeError("center must be a number") self._center.value = value @property @@ -170,17 +142,17 @@ def gaussian_width(self, value: Numeric) -> None: """Set the width parameter value. Args: - value (Numeric | None): The new value for the width - parameter. + value (Numeric): The new value for the width + parameter. Raises: - TypeError: If the value is not a number or None. + TypeError: If the value is not a number. ValueError: If the value is not positive. """ if not isinstance(value, Numeric): - raise TypeError('gaussian_width must be a number') + raise TypeError("gaussian_width must be a number") if float(value) <= 0: - raise ValueError('gaussian_width must be positive') + raise ValueError("gaussian_width must be positive") self._gaussian_width.value = value @property @@ -205,12 +177,14 @@ def lorentzian_width(self, value: Numeric) -> None: ValueError: If the value is not positive. """ if not isinstance(value, Numeric): - raise TypeError('lorentzian_width must be a number') + raise TypeError("lorentzian_width must be a number") if float(value) <= 0: - raise ValueError('lorentzian_width must be positive') + raise ValueError("lorentzian_width must be positive") self._lorentzian_width.value = value - def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray: + def evaluate( + self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray + ) -> np.ndarray: r"""Evaluate the Voigt at the given x values. If x is a scipp Variable, the unit of the Voigt will be @@ -221,7 +195,7 @@ def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) Args: - x (Numeric | list[Numeric] | np.ndarray | sc.Variable | sc.DataArray): + x (Numeric | list | np.ndarray | sc.Variable | sc.DataArray): The x values at which to evaluate the Voigt. Returns: @@ -244,8 +218,8 @@ def __repr__(self) -> str: str: A string representation of the Voigt. """ - return f'Voigt(unique_name = {self.unique_name}, unit = {self._unit},\n \ + return f"Voigt(unique_name = {self.unique_name}, unit = {self._unit},\n \ area = {self.area},\n \ center = {self.center},\n \ gaussian_width = {self.gaussian_width},\n \ - lorentzian_width = {self.lorentzian_width})' + lorentzian_width = {self.lorentzian_width})" diff --git a/src/easydynamics/sample_model/diffusion_model/brownian_translational_diffusion.py b/src/easydynamics/sample_model/diffusion_model/brownian_translational_diffusion.py index 77285141e..65d20de29 100644 --- a/src/easydynamics/sample_model/diffusion_model/brownian_translational_diffusion.py +++ b/src/easydynamics/sample_model/diffusion_model/brownian_translational_diffusion.py @@ -12,7 +12,9 @@ from easydynamics.sample_model.component_collection import ComponentCollection from easydynamics.sample_model.components import Lorentzian -from easydynamics.sample_model.diffusion_model.diffusion_model_base import DiffusionModelBase +from easydynamics.sample_model.diffusion_model.diffusion_model_base import ( + DiffusionModelBase, +) from easydynamics.utils.utils import Numeric from easydynamics.utils.utils import Q_type from easydynamics.utils.utils import _validate_and_convert_Q @@ -41,9 +43,9 @@ class BrownianTranslationalDiffusion(DiffusionModelBase): def __init__( self, - display_name: str | None = 'BrownianTranslationalDiffusion', + display_name: str | None = "BrownianTranslationalDiffusion", unique_name: str | None = None, - unit: str | sc.Unit = 'meV', + unit: str | sc.Unit = "meV", scale: Numeric = 1.0, diffusion_coefficient: Numeric = 1.0, ) -> None: @@ -65,16 +67,16 @@ def __init__( number. """ if not isinstance(scale, Numeric): - raise TypeError('scale must be a number.') + raise TypeError("scale must be a number.") if not isinstance(diffusion_coefficient, Numeric): - raise TypeError('diffusion_coefficient must be a number.') + raise TypeError("diffusion_coefficient must be a number.") diffusion_coefficient = Parameter( - name='diffusion_coefficient', + name="diffusion_coefficient", value=float(diffusion_coefficient), fixed=False, - unit='m**2/s', + unit="m**2/s", min=0.0, ) super().__init__( @@ -83,8 +85,8 @@ def __init__( unit=unit, scale=scale, ) - self._hbar = DescriptorNumber.from_scipp('hbar', scipp_hbar) - self._angstrom = DescriptorNumber('angstrom', 1e-10, unit='m') + self._hbar = DescriptorNumber.from_scipp("hbar", scipp_hbar) + self._angstrom = DescriptorNumber("angstrom", 1e-10, unit="m") self._diffusion_coefficient = diffusion_coefficient # ------------------------------------------------------------------ @@ -113,10 +115,10 @@ def diffusion_coefficient(self, diffusion_coefficient: Numeric) -> None: ValueError: If diffusion_coefficient is negative. """ if not isinstance(diffusion_coefficient, Numeric): - raise TypeError('diffusion_coefficient must be a number.') + raise TypeError("diffusion_coefficient must be a number.") if float(diffusion_coefficient) < 0: - raise ValueError('diffusion_coefficient must be non-negative.') + raise ValueError("diffusion_coefficient must be non-negative.") self._diffusion_coefficient.value = float(diffusion_coefficient) # ------------------------------------------------------------------ @@ -128,8 +130,7 @@ def calculate_width(self, Q: Q_type) -> np.ndarray: diffusion model. Args: - Q (np.ndarray | Numeric | list | ArrayLike): Scattering - vector in 1/angstrom + Q (Q_type): Scattering vector in 1/angstrom Returns: np.ndarray: HWHM values in the unit of the model @@ -138,7 +139,9 @@ def calculate_width(self, Q: Q_type) -> np.ndarray: Q = _validate_and_convert_Q(Q) - unit_conversion_factor = self._hbar * self.diffusion_coefficient / (self._angstrom**2) + unit_conversion_factor = ( + self._hbar * self.diffusion_coefficient / (self._angstrom**2) + ) unit_conversion_factor.convert_unit(self.unit) width = Q**2 * unit_conversion_factor.value @@ -149,7 +152,7 @@ def calculate_EISF(self, Q: Q_type) -> np.ndarray: the Brownian translational diffusion model. Args: - Q (np.ndarray | Numeric | list | ArrayLike): Scattering + Q (Q_type): Scattering vector in 1/angstrom Returns: @@ -164,8 +167,7 @@ def calculate_QISF(self, Q: Q_type) -> np.ndarray: (QISF). Args: - Q (np.ndarray | Numeric | list | ArrayLike): Scattering - vector in 1/angstrom + Q (Q_type): Scattering vector in 1/angstrom Returns: np.ndarray: QISF values (dimensionless). @@ -178,15 +180,15 @@ def calculate_QISF(self, Q: Q_type) -> np.ndarray: def create_component_collections( self, Q: Q_type, - component_display_name: str = 'Brownian translational diffusion', + component_display_name: str = "Brownian diffusion", ) -> List[ComponentCollection]: r"""Create ComponentCollection components for the Brownian translational diffusion model at given Q values. Args: - Q (Number, list, or np.ndarray): Scattering vector values. - component_display_name (str): Name of the Lorentzian - component. + Q (Q_type): Scattering vector values. + component_display_name (str, default="Brownian diffusion"): + Name of the Lorentzian component. Returns: List[ComponentCollection]: List of ComponentCollections with @@ -201,7 +203,7 @@ def create_component_collections( Q = _validate_and_convert_Q(Q) if not isinstance(component_display_name, str): - raise TypeError('component_name must be a string.') + raise TypeError("component_name must be a string.") component_collection_list = [None] * len(Q) # In more complex models, this is used to scale the area of the @@ -213,7 +215,7 @@ def create_component_collections( # No delta function, as the EISF is 0. for i, Q_value in enumerate(Q): component_collection_list[i] = ComponentCollection( - display_name=f'{self.display_name}_Q{Q_value:.2f}', unit=self.unit + display_name=f"{self.display_name}_Q{Q_value:.2f}", unit=self.unit ) lorentzian_component = Lorentzian( @@ -251,19 +253,19 @@ def _write_width_dependency_expression(self, Q: float) -> str: of Q to make dependent Parameters. Args: - Q (float): Scattering vector in 1/angstrom + Q (float): Scattering vector in 1/angstrom. - Returns: - str: Dependency expression for the width. + Returns: + str: Dependency expression for the width. - Raises: - TypeError: If Q is not a float. + Raises: + TypeError: If Q is not a float. """ if not isinstance(Q, (float)): - raise TypeError('Q must be a float.') + raise TypeError("Q must be a float.") # Q is given as a float, so we need to add the units - return f'hbar * D* {Q} **2*1/(angstrom**2)' + return f"hbar * D* {Q} **2*1/(angstrom**2)" def _write_width_dependency_map_expression(self) -> Dict[str, DescriptorNumber]: """Write the dependency map expression to make dependent @@ -273,9 +275,9 @@ def _write_width_dependency_map_expression(self) -> Dict[str, DescriptorNumber]: Dict[str, DescriptorNumber]: Dependency map for the width. """ return { - 'D': self.diffusion_coefficient, - 'hbar': self._hbar, - 'angstrom': self._angstrom, + "D": self.diffusion_coefficient, + "hbar": self._hbar, + "angstrom": self._angstrom, } def _write_area_dependency_expression(self, QISF: float) -> str: @@ -292,9 +294,9 @@ def _write_area_dependency_expression(self, QISF: float) -> str: TypeError: If QISF is not a float. """ if not isinstance(QISF, (float)): - raise TypeError('QISF must be a float.') + raise TypeError("QISF must be a float.") - return f'{QISF} * scale' + return f"{QISF} * scale" def _write_area_dependency_map_expression(self) -> Dict[str, DescriptorNumber]: """Write the dependency map expression to make dependent @@ -304,7 +306,7 @@ def _write_area_dependency_map_expression(self) -> Dict[str, DescriptorNumber]: Dict[str, DescriptorNumber]: Dependency map for the area. """ return { - 'scale': self.scale, + "scale": self.scale, } # ------------------------------------------------------------------ @@ -320,6 +322,6 @@ def __repr__(self) -> str: BrownianTranslationalDiffusion model. """ return ( - f'BrownianTranslationalDiffusion(display_name={self.display_name},' - f'diffusion_coefficient={self.diffusion_coefficient}, scale={self.scale})' + f"BrownianTranslationalDiffusion(display_name={self.display_name}," + f"diffusion_coefficient={self.diffusion_coefficient}, scale={self.scale})" ) From 1170f8fa05a945fc96278d445aa6b1540c27c6b9 Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Sat, 14 Mar 2026 20:55:34 +0100 Subject: [PATCH 14/31] pixi run fix --- .../sample_model/components/polynomial.py | 50 +++++++---------- .../sample_model/components/voigt.py | 32 +++++------ .../brownian_translational_diffusion.py | 54 +++++++++---------- 3 files changed, 59 insertions(+), 77 deletions(-) diff --git a/src/easydynamics/sample_model/components/polynomial.py b/src/easydynamics/sample_model/components/polynomial.py index bf26ef710..a79bd2372 100644 --- a/src/easydynamics/sample_model/components/polynomial.py +++ b/src/easydynamics/sample_model/components/polynomial.py @@ -30,8 +30,8 @@ class Polynomial(ModelComponent): def __init__( self, coefficients: Sequence[Numeric | Parameter] = (0.0,), - unit: str | sc.Unit = "meV", - display_name: str | None = "Polynomial", + unit: str | sc.Unit = 'meV', + display_name: str | None = 'Polynomial', unique_name: str | None = None, ) -> None: """Initialize the Polynomial component. @@ -54,12 +54,12 @@ def __init__( if not isinstance(coefficients, (list, tuple, np.ndarray)): raise TypeError( - "coefficients must be a sequence (list/tuple/ndarray) \ - of numbers or Parameter objects." + 'coefficients must be a sequence (list/tuple/ndarray) \ + of numbers or Parameter objects.' ) if len(coefficients) == 0: - raise ValueError("At least one coefficient must be provided.") + raise ValueError('At least one coefficient must be provided.') # Internal storage of Parameter objects self._coefficients: list[Parameter] = [] @@ -69,11 +69,9 @@ def __init__( if isinstance(coef, Parameter): param = coef elif isinstance(coef, Numeric): - param = Parameter(name=f"{display_name}_c{i}", value=float(coef)) + param = Parameter(name=f'{display_name}_c{i}', value=float(coef)) else: - raise TypeError( - "Each coefficient must be either a numeric value or a Parameter." - ) + raise TypeError('Each coefficient must be either a numeric value or a Parameter.') self._coefficients.append(param) # Helper scipp scalar to track unit conversions @@ -108,11 +106,11 @@ def coefficients(self, coeffs: Sequence[Numeric | Parameter]) -> None: """ if not isinstance(coeffs, (list, tuple, np.ndarray)): raise TypeError( - "coefficients must be a sequence (list/tuple/ndarray) of numbers or Parameter ." + 'coefficients must be a sequence (list/tuple/ndarray) of numbers or Parameter .' ) if len(coeffs) != len(self._coefficients): raise ValueError( - "Number of coefficients must match the existing number of coefficients." + 'Number of coefficients must match the existing number of coefficients.' ) for i, coef in enumerate(coeffs): if isinstance(coef, Parameter): @@ -121,9 +119,7 @@ def coefficients(self, coeffs: Sequence[Numeric | Parameter]) -> None: elif isinstance(coef, Numeric): self._coefficients[i].value = float(coef) else: - raise TypeError( - "Each coefficient must be either a numeric value or a Parameter." - ) + raise TypeError('Each coefficient must be either a numeric value or a Parameter.') def coefficient_values(self) -> list[float]: """Get the coefficients of the polynomial as a list. @@ -134,9 +130,7 @@ def coefficient_values(self) -> list[float]: coefficient_list = [param.value for param in self._coefficients] return coefficient_list - def evaluate( - self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray - ) -> np.ndarray: + def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray: r"""Evaluate the Polynomial at the given x values. The intensity is given by @@ -162,8 +156,8 @@ def evaluate( if any(result < 0): warnings.warn( - f"The Polynomial with unique_name {self.unique_name} has negative values, " - "which may not be physically meaningful.", + f'The Polynomial with unique_name {self.unique_name} has negative values, ' + 'which may not be physically meaningful.', UserWarning, ) return result @@ -190,8 +184,8 @@ def degree(self, value: int) -> None: directly. """ raise AttributeError( - "The degree of the polynomial is determined by the number of coefficients \ - and cannot be set directly." + 'The degree of the polynomial is determined by the number of coefficients \ + and cannot be set directly.' ) def get_all_variables(self) -> list[DescriptorBase]: @@ -213,14 +207,12 @@ def convert_unit(self, unit: str | sc.Unit) -> None: """ if not isinstance(unit, (str, sc.Unit)): - raise UnitError("unit must be a string or a scipp unit.") + raise UnitError('unit must be a string or a scipp unit.') # Find out how much the unit changes # by converting a helper variable conversion_value_before = self._unit_conversion_helper.value - self._unit_conversion_helper = sc.to_unit( - self._unit_conversion_helper, unit=unit - ) + self._unit_conversion_helper = sc.to_unit(self._unit_conversion_helper, unit=unit) conversion_value_after = self._unit_conversion_helper.value for i, param in enumerate(self._coefficients): param.value *= ( @@ -236,11 +228,9 @@ def __repr__(self) -> str: str: A string representation of the Polynomial. """ - coeffs_str = ", ".join( - f"{param.name}={param.value}" for param in self._coefficients - ) - return f"Polynomial(unique_name = {self.unique_name}, \ - unit = {self._unit},\n coefficients = [{coeffs_str}])" + coeffs_str = ', '.join(f'{param.name}={param.value}' for param in self._coefficients) + return f'Polynomial(unique_name = {self.unique_name}, \ + unit = {self._unit},\n coefficients = [{coeffs_str}])' # from typing import Callable, Dict diff --git a/src/easydynamics/sample_model/components/voigt.py b/src/easydynamics/sample_model/components/voigt.py index 088f3f6f2..d57c91bcc 100644 --- a/src/easydynamics/sample_model/components/voigt.py +++ b/src/easydynamics/sample_model/components/voigt.py @@ -28,8 +28,8 @@ def __init__( center: Numeric | Parameter | None = None, gaussian_width: Numeric | Parameter = 1.0, lorentzian_width: Numeric | Parameter = 1.0, - unit: str | sc.Unit = "meV", - display_name: str | None = "Voigt", + unit: str | sc.Unit = 'meV', + display_name: str | None = 'Voigt', unique_name: str | None = None, ) -> None: """Initialize a Voigt component. @@ -54,22 +54,20 @@ def __init__( ) # These methods live in ValidationMixin - area = self._create_area_parameter( - area=area, name=display_name, unit=self._unit - ) + area = self._create_area_parameter(area=area, name=display_name, unit=self._unit) center = self._create_center_parameter( center=center, name=display_name, fix_if_none=True, unit=self._unit ) gaussian_width = self._create_width_parameter( width=gaussian_width, name=display_name, - param_name="gaussian_width", + param_name='gaussian_width', unit=self._unit, ) lorentzian_width = self._create_width_parameter( width=lorentzian_width, name=display_name, - param_name="lorentzian_width", + param_name='lorentzian_width', unit=self._unit, ) @@ -98,7 +96,7 @@ def area(self, value: Numeric) -> None: TypeError: If the value is not a number. """ if not isinstance(value, Numeric): - raise TypeError("area must be a number") + raise TypeError('area must be a number') self._area.value = value @property @@ -125,7 +123,7 @@ def center(self, value: Numeric | None) -> None: value = 0.0 self._center.fixed = True if not isinstance(value, Numeric): - raise TypeError("center must be a number") + raise TypeError('center must be a number') self._center.value = value @property @@ -150,9 +148,9 @@ def gaussian_width(self, value: Numeric) -> None: ValueError: If the value is not positive. """ if not isinstance(value, Numeric): - raise TypeError("gaussian_width must be a number") + raise TypeError('gaussian_width must be a number') if float(value) <= 0: - raise ValueError("gaussian_width must be positive") + raise ValueError('gaussian_width must be positive') self._gaussian_width.value = value @property @@ -177,14 +175,12 @@ def lorentzian_width(self, value: Numeric) -> None: ValueError: If the value is not positive. """ if not isinstance(value, Numeric): - raise TypeError("lorentzian_width must be a number") + raise TypeError('lorentzian_width must be a number') if float(value) <= 0: - raise ValueError("lorentzian_width must be positive") + raise ValueError('lorentzian_width must be positive') self._lorentzian_width.value = value - def evaluate( - self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray - ) -> np.ndarray: + def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray: r"""Evaluate the Voigt at the given x values. If x is a scipp Variable, the unit of the Voigt will be @@ -218,8 +214,8 @@ def __repr__(self) -> str: str: A string representation of the Voigt. """ - return f"Voigt(unique_name = {self.unique_name}, unit = {self._unit},\n \ + return f'Voigt(unique_name = {self.unique_name}, unit = {self._unit},\n \ area = {self.area},\n \ center = {self.center},\n \ gaussian_width = {self.gaussian_width},\n \ - lorentzian_width = {self.lorentzian_width})" + lorentzian_width = {self.lorentzian_width})' diff --git a/src/easydynamics/sample_model/diffusion_model/brownian_translational_diffusion.py b/src/easydynamics/sample_model/diffusion_model/brownian_translational_diffusion.py index 65d20de29..8234e812d 100644 --- a/src/easydynamics/sample_model/diffusion_model/brownian_translational_diffusion.py +++ b/src/easydynamics/sample_model/diffusion_model/brownian_translational_diffusion.py @@ -12,9 +12,7 @@ from easydynamics.sample_model.component_collection import ComponentCollection from easydynamics.sample_model.components import Lorentzian -from easydynamics.sample_model.diffusion_model.diffusion_model_base import ( - DiffusionModelBase, -) +from easydynamics.sample_model.diffusion_model.diffusion_model_base import DiffusionModelBase from easydynamics.utils.utils import Numeric from easydynamics.utils.utils import Q_type from easydynamics.utils.utils import _validate_and_convert_Q @@ -43,9 +41,9 @@ class BrownianTranslationalDiffusion(DiffusionModelBase): def __init__( self, - display_name: str | None = "BrownianTranslationalDiffusion", + display_name: str | None = 'BrownianTranslationalDiffusion', unique_name: str | None = None, - unit: str | sc.Unit = "meV", + unit: str | sc.Unit = 'meV', scale: Numeric = 1.0, diffusion_coefficient: Numeric = 1.0, ) -> None: @@ -67,16 +65,16 @@ def __init__( number. """ if not isinstance(scale, Numeric): - raise TypeError("scale must be a number.") + raise TypeError('scale must be a number.') if not isinstance(diffusion_coefficient, Numeric): - raise TypeError("diffusion_coefficient must be a number.") + raise TypeError('diffusion_coefficient must be a number.') diffusion_coefficient = Parameter( - name="diffusion_coefficient", + name='diffusion_coefficient', value=float(diffusion_coefficient), fixed=False, - unit="m**2/s", + unit='m**2/s', min=0.0, ) super().__init__( @@ -85,8 +83,8 @@ def __init__( unit=unit, scale=scale, ) - self._hbar = DescriptorNumber.from_scipp("hbar", scipp_hbar) - self._angstrom = DescriptorNumber("angstrom", 1e-10, unit="m") + self._hbar = DescriptorNumber.from_scipp('hbar', scipp_hbar) + self._angstrom = DescriptorNumber('angstrom', 1e-10, unit='m') self._diffusion_coefficient = diffusion_coefficient # ------------------------------------------------------------------ @@ -115,10 +113,10 @@ def diffusion_coefficient(self, diffusion_coefficient: Numeric) -> None: ValueError: If diffusion_coefficient is negative. """ if not isinstance(diffusion_coefficient, Numeric): - raise TypeError("diffusion_coefficient must be a number.") + raise TypeError('diffusion_coefficient must be a number.') if float(diffusion_coefficient) < 0: - raise ValueError("diffusion_coefficient must be non-negative.") + raise ValueError('diffusion_coefficient must be non-negative.') self._diffusion_coefficient.value = float(diffusion_coefficient) # ------------------------------------------------------------------ @@ -139,9 +137,7 @@ def calculate_width(self, Q: Q_type) -> np.ndarray: Q = _validate_and_convert_Q(Q) - unit_conversion_factor = ( - self._hbar * self.diffusion_coefficient / (self._angstrom**2) - ) + unit_conversion_factor = self._hbar * self.diffusion_coefficient / (self._angstrom**2) unit_conversion_factor.convert_unit(self.unit) width = Q**2 * unit_conversion_factor.value @@ -180,7 +176,7 @@ def calculate_QISF(self, Q: Q_type) -> np.ndarray: def create_component_collections( self, Q: Q_type, - component_display_name: str = "Brownian diffusion", + component_display_name: str = 'Brownian diffusion', ) -> List[ComponentCollection]: r"""Create ComponentCollection components for the Brownian translational diffusion model at given Q values. @@ -203,7 +199,7 @@ def create_component_collections( Q = _validate_and_convert_Q(Q) if not isinstance(component_display_name, str): - raise TypeError("component_name must be a string.") + raise TypeError('component_name must be a string.') component_collection_list = [None] * len(Q) # In more complex models, this is used to scale the area of the @@ -215,7 +211,7 @@ def create_component_collections( # No delta function, as the EISF is 0. for i, Q_value in enumerate(Q): component_collection_list[i] = ComponentCollection( - display_name=f"{self.display_name}_Q{Q_value:.2f}", unit=self.unit + display_name=f'{self.display_name}_Q{Q_value:.2f}', unit=self.unit ) lorentzian_component = Lorentzian( @@ -262,10 +258,10 @@ def _write_width_dependency_expression(self, Q: float) -> str: TypeError: If Q is not a float. """ if not isinstance(Q, (float)): - raise TypeError("Q must be a float.") + raise TypeError('Q must be a float.') # Q is given as a float, so we need to add the units - return f"hbar * D* {Q} **2*1/(angstrom**2)" + return f'hbar * D* {Q} **2*1/(angstrom**2)' def _write_width_dependency_map_expression(self) -> Dict[str, DescriptorNumber]: """Write the dependency map expression to make dependent @@ -275,9 +271,9 @@ def _write_width_dependency_map_expression(self) -> Dict[str, DescriptorNumber]: Dict[str, DescriptorNumber]: Dependency map for the width. """ return { - "D": self.diffusion_coefficient, - "hbar": self._hbar, - "angstrom": self._angstrom, + 'D': self.diffusion_coefficient, + 'hbar': self._hbar, + 'angstrom': self._angstrom, } def _write_area_dependency_expression(self, QISF: float) -> str: @@ -294,9 +290,9 @@ def _write_area_dependency_expression(self, QISF: float) -> str: TypeError: If QISF is not a float. """ if not isinstance(QISF, (float)): - raise TypeError("QISF must be a float.") + raise TypeError('QISF must be a float.') - return f"{QISF} * scale" + return f'{QISF} * scale' def _write_area_dependency_map_expression(self) -> Dict[str, DescriptorNumber]: """Write the dependency map expression to make dependent @@ -306,7 +302,7 @@ def _write_area_dependency_map_expression(self) -> Dict[str, DescriptorNumber]: Dict[str, DescriptorNumber]: Dependency map for the area. """ return { - "scale": self.scale, + 'scale': self.scale, } # ------------------------------------------------------------------ @@ -322,6 +318,6 @@ def __repr__(self) -> str: BrownianTranslationalDiffusion model. """ return ( - f"BrownianTranslationalDiffusion(display_name={self.display_name}," - f"diffusion_coefficient={self.diffusion_coefficient}, scale={self.scale})" + f'BrownianTranslationalDiffusion(display_name={self.display_name},' + f'diffusion_coefficient={self.diffusion_coefficient}, scale={self.scale})' ) From 8356c54209e5e69441bd427660cafe2a841bbd0d Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Sun, 15 Mar 2026 12:58:19 +0100 Subject: [PATCH 15/31] model_component --- .../components/model_component.py | 86 ++++++++----------- .../sample_model/components/polynomial.py | 66 ++++++++------ 2 files changed, 76 insertions(+), 76 deletions(-) diff --git a/src/easydynamics/sample_model/components/model_component.py b/src/easydynamics/sample_model/components/model_component.py index e1ecad34d..1289773bc 100644 --- a/src/easydynamics/sample_model/components/model_component.py +++ b/src/easydynamics/sample_model/components/model_component.py @@ -5,7 +5,6 @@ import warnings from abc import abstractmethod -from typing import List import numpy as np import scipp as sc @@ -16,42 +15,22 @@ class ModelComponent(ModelBase): - """Abstract base class for all model components. - - Args: - unit (str | sc.Unit): The unit of the model component. - Default is 'meV'. - display_name (str | None): A human-readable name for the - component. Default is None. - unique_name (str | None): A unique identifier for the - component. Default is None. - - Attributes: - unit (str): The unit of the model component. - display_name (str | None): A human-readable name for the - component. - unique_name (str | None): A unique identifier for the - component. - """ + """Abstract base class for all model components.""" def __init__( self, - unit: str | sc.Unit = 'meV', + unit: str | sc.Unit = "meV", display_name: str | None = None, unique_name: str | None = None, ) -> None: """Initialize the ModelComponent. Args: - unit (str | sc.Unit): The unit of the model component. - Default is 'meV'. - display_name (str | None): A human-readable name for the - component. Default is None. - unique_name (str | None): A unique identifier for the - component. Default is None. - - Raises: - TypeError: If unit is not a string or scipp Unit. + unit (str | sc.Unit, default="meV"): The unit of the model component. + display_name (str | None, default=None): A human-readable name for the + component. + unique_name (str | None, default=None): A unique identifier for the + component. """ self.validate_unit(unit) super().__init__(display_name=display_name, unique_name=unique_name) @@ -80,8 +59,8 @@ def unit(self, unit_str: str) -> None: """ raise AttributeError( ( - f'Unit is read-only. Use convert_unit to change the unit between allowed types ' - f'or create a new {self.__class__.__name__} with the desired unit.' + f"Unit is read-only. Use convert_unit to change the unit between allowed types " + f"or create a new {self.__class__.__name__} with the desired unit." ) ) # noqa: E501 @@ -98,14 +77,14 @@ def free_all_parameters(self) -> None: p.fixed = False def _prepare_x_for_evaluate( - self, x: Numeric | List[Numeric] | np.ndarray | sc.Variable | sc.DataArray + self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray ) -> np.ndarray: """Prepare the input x for evaluation by handling units and converting to a numpy array. Args: - x (Numeric | List[Numeric] | np.ndarray | sc.Variable | - sc.DataArray): The input data to prepare. + x (Numeric | list | np.ndarray | sc.Variable | sc.DataArray): + The input data to prepare. Returns: np.ndarray: The prepared input data as a numpy array. @@ -123,10 +102,10 @@ def _prepare_x_for_evaluate( coords = dict(x.coords) ncoords = len(coords) if ncoords != 1: - coord_names = ', '.join(coords.keys()) + coord_names = ", ".join(coords.keys()) raise ValueError( - f'scipp.DataArray must have exactly one coordinate to be used as input `x`. ' - f'Found {ncoords} coordinates: {coord_names}.' + f"scipp.DataArray must have exactly one coordinate to be used as input `x`. " + f"Found {ncoords} coordinates: {coord_names}." ) # get the coordinate, it's a sc.Variable coord_name, coord_obj = next(iter(coords.items())) @@ -144,15 +123,15 @@ def _prepare_x_for_evaluate( self.convert_unit(x.unit.name) except Exception as e: raise UnitError( - f'Input x has unit {x.unit}, but {self.__class__.__name__} component \ + f"Input x has unit {x.unit}, but {self.__class__.__name__} component \ has unit {self._unit}. \ - Failed to convert {self.__class__.__name__} to {x.unit}.' + Failed to convert {self.__class__.__name__} to {x.unit}." ) from e warnings.warn( - f'Input x has unit {x.unit}, but {self.__class__.__name__} component \ + f"Input x has unit {x.unit}, but {self.__class__.__name__} component \ has unit {self_unit_for_warning}. \ - Converting {self.__class__.__name__} to {x.unit}.' + Converting {self.__class__.__name__} to {x.unit}." ) else: x_in = x @@ -163,30 +142,37 @@ def _prepare_x_for_evaluate( x_in = np.array(x_in) if any(np.isnan(x_in)): - raise ValueError('Input x contains NaN values.') + raise ValueError("Input x contains NaN values.") if any(np.isinf(x_in)): - raise ValueError('Input x contains infinite values.') + raise ValueError("Input x contains infinite values.") return np.sort(x_in) @staticmethod - def validate_unit(unit) -> None: + def validate_unit(unit: str | sc.Unit | None) -> None: """Validate that the unit is either a string or a scipp Unit. + Args: + unit (str | sc.Unit | None): The unit to validate. + Raises: TypeError: If unit is not a string or scipp Unit. """ if unit is not None and not isinstance(unit, (str, sc.Unit)): raise TypeError( - f'unit must be None, a string, or a scipp Unit, got {type(unit).__name__}' + f"unit must be None, a string, or a scipp Unit, got {type(unit).__name__}" ) def convert_unit(self, unit: str | sc.Unit) -> None: """Convert the unit of the Parameters in the component. Args: - unit (str or sc.Unit): The new unit to convert to. + unit (str | sc.Unit): The new unit to convert to. + + Raises: + Exception: If the provided unit is invalid or incompatible + with the component's parameters. """ old_unit = self._unit @@ -199,7 +185,7 @@ def convert_unit(self, unit: str | sc.Unit) -> None: # Attempt to rollback on failure try: for p in pars: - if hasattr(p, 'convert_unit'): + if hasattr(p, "convert_unit"): p.convert_unit(old_unit) except Exception: # noqa: S110 pass # Best effort rollback @@ -207,14 +193,14 @@ def convert_unit(self, unit: str | sc.Unit) -> None: @abstractmethod def evaluate( - self, x: Numeric | List[Numeric] | np.ndarray | sc.Variable | sc.DataArray + self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray ) -> np.ndarray: """Abstract method to evaluate the model component at input x. Must be implemented by subclasses. Args: - x (Numeric | list[Numeric] | np.ndarray | sc.Variable | sc.DataArray): - Input values. + x (Numeric | list | np.ndarray | sc.Variable | sc.DataArray): + The x values at which to evaluate the component. Returns: np.ndarray: Evaluated function values. @@ -228,4 +214,4 @@ def __repr__(self) -> str: str: A string representation of the ModelComponent. """ - return f'{self.__class__.__name__}(unique_name={self.unique_name}, unit={self._unit})' + return f"{self.__class__.__name__}(unique_name={self.unique_name}, unit={self._unit})" diff --git a/src/easydynamics/sample_model/components/polynomial.py b/src/easydynamics/sample_model/components/polynomial.py index a79bd2372..e5ecaa117 100644 --- a/src/easydynamics/sample_model/components/polynomial.py +++ b/src/easydynamics/sample_model/components/polynomial.py @@ -30,23 +30,27 @@ class Polynomial(ModelComponent): def __init__( self, coefficients: Sequence[Numeric | Parameter] = (0.0,), - unit: str | sc.Unit = 'meV', - display_name: str | None = 'Polynomial', + unit: str | sc.Unit = "meV", + display_name: str | None = "Polynomial", unique_name: str | None = None, ) -> None: """Initialize the Polynomial component. Args: - coefficients (Sequence[Numeric | Parameter], default=(0.0,)): Coefficients c0, c1, ..., cN - unit (str | sc.Unit, default='meV'): Unit of the Polynomial component. - display_name (str | None, default='Polynomial'): Display name of the Polynomial + coefficients (Sequence[Numeric | Parameter], default=(0.0,)): + Coefficients c0, c1, ..., cN + unit (str | sc.Unit, default='meV'): Unit of the Polynomial component. - unique_name (str | None, default=None): Unique name of the component. - If None, a unique_name is automatically generated. + display_name (str | None, default='Polynomial'): Display + name of the Polynomial component. + unique_name (str | None, default=None): Unique name of the + component. If None, a unique_name is automatically + generated. Raises: TypeError: If coefficients is not a sequence of numbers or - Parameters or if any item in coefficients is not a number or Parameter. + Parameters or if any item in coefficients is not a + number or Parameter. ValueError: If coefficients is an empty sequence. """ @@ -54,12 +58,12 @@ def __init__( if not isinstance(coefficients, (list, tuple, np.ndarray)): raise TypeError( - 'coefficients must be a sequence (list/tuple/ndarray) \ - of numbers or Parameter objects.' + "coefficients must be a sequence (list/tuple/ndarray) \ + of numbers or Parameter objects." ) if len(coefficients) == 0: - raise ValueError('At least one coefficient must be provided.') + raise ValueError("At least one coefficient must be provided.") # Internal storage of Parameter objects self._coefficients: list[Parameter] = [] @@ -69,9 +73,11 @@ def __init__( if isinstance(coef, Parameter): param = coef elif isinstance(coef, Numeric): - param = Parameter(name=f'{display_name}_c{i}', value=float(coef)) + param = Parameter(name=f"{display_name}_c{i}", value=float(coef)) else: - raise TypeError('Each coefficient must be either a numeric value or a Parameter.') + raise TypeError( + "Each coefficient must be either a numeric value or a Parameter." + ) self._coefficients.append(param) # Helper scipp scalar to track unit conversions @@ -106,11 +112,11 @@ def coefficients(self, coeffs: Sequence[Numeric | Parameter]) -> None: """ if not isinstance(coeffs, (list, tuple, np.ndarray)): raise TypeError( - 'coefficients must be a sequence (list/tuple/ndarray) of numbers or Parameter .' + "coefficients must be a sequence (list/tuple/ndarray) of numbers or Parameter ." ) if len(coeffs) != len(self._coefficients): raise ValueError( - 'Number of coefficients must match the existing number of coefficients.' + "Number of coefficients must match the existing number of coefficients." ) for i, coef in enumerate(coeffs): if isinstance(coef, Parameter): @@ -119,7 +125,9 @@ def coefficients(self, coeffs: Sequence[Numeric | Parameter]) -> None: elif isinstance(coef, Numeric): self._coefficients[i].value = float(coef) else: - raise TypeError('Each coefficient must be either a numeric value or a Parameter.') + raise TypeError( + "Each coefficient must be either a numeric value or a Parameter." + ) def coefficient_values(self) -> list[float]: """Get the coefficients of the polynomial as a list. @@ -130,7 +138,9 @@ def coefficient_values(self) -> list[float]: coefficient_list = [param.value for param in self._coefficients] return coefficient_list - def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray: + def evaluate( + self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray + ) -> np.ndarray: r"""Evaluate the Polynomial at the given x values. The intensity is given by @@ -156,8 +166,8 @@ def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) if any(result < 0): warnings.warn( - f'The Polynomial with unique_name {self.unique_name} has negative values, ' - 'which may not be physically meaningful.', + f"The Polynomial with unique_name {self.unique_name} has negative values, " + "which may not be physically meaningful.", UserWarning, ) return result @@ -184,8 +194,8 @@ def degree(self, value: int) -> None: directly. """ raise AttributeError( - 'The degree of the polynomial is determined by the number of coefficients \ - and cannot be set directly.' + "The degree of the polynomial is determined by the number of coefficients \ + and cannot be set directly." ) def get_all_variables(self) -> list[DescriptorBase]: @@ -207,12 +217,14 @@ def convert_unit(self, unit: str | sc.Unit) -> None: """ if not isinstance(unit, (str, sc.Unit)): - raise UnitError('unit must be a string or a scipp unit.') + raise UnitError("unit must be a string or a scipp unit.") # Find out how much the unit changes # by converting a helper variable conversion_value_before = self._unit_conversion_helper.value - self._unit_conversion_helper = sc.to_unit(self._unit_conversion_helper, unit=unit) + self._unit_conversion_helper = sc.to_unit( + self._unit_conversion_helper, unit=unit + ) conversion_value_after = self._unit_conversion_helper.value for i, param in enumerate(self._coefficients): param.value *= ( @@ -228,9 +240,11 @@ def __repr__(self) -> str: str: A string representation of the Polynomial. """ - coeffs_str = ', '.join(f'{param.name}={param.value}' for param in self._coefficients) - return f'Polynomial(unique_name = {self.unique_name}, \ - unit = {self._unit},\n coefficients = [{coeffs_str}])' + coeffs_str = ", ".join( + f"{param.name}={param.value}" for param in self._coefficients + ) + return f"Polynomial(unique_name = {self.unique_name}, \ + unit = {self._unit},\n coefficients = [{coeffs_str}])" # from typing import Callable, Dict From 9dbc9e90c2894a0aab36644da640fe4f5ec06d7f Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Sun, 15 Mar 2026 19:51:31 +0100 Subject: [PATCH 16/31] mixins --- .../sample_model/components/mixins.py | 58 +++++++++++-------- 1 file changed, 33 insertions(+), 25 deletions(-) diff --git a/src/easydynamics/sample_model/components/mixins.py b/src/easydynamics/sample_model/components/mixins.py index c0b14d585..22f9cc221 100644 --- a/src/easydynamics/sample_model/components/mixins.py +++ b/src/easydynamics/sample_model/components/mixins.py @@ -27,7 +27,7 @@ def _create_area_parameter( self, area: Numeric | Parameter, name: str, - unit: str | sc.Unit = 'meV', + unit: str | sc.Unit = "meV", minimum_area: float = MINIMUM_AREA, ) -> Parameter: """Validate and convert a number to a Parameter describing the @@ -40,28 +40,29 @@ def _create_area_parameter( Args: area (Numeric | Parameter): The area value or Parameter. name (str): The name of the model component. - unit (str | sc.Unit): The unit of the area Parameter. - minimum_area (float): The minimum allowed area. + unit (str | sc.Unit, default='meV'): The unit of the area Parameter. + minimum_area (float, default=MINIMUM_AREA): The minimum allowed area. Returns: Parameter: The validated area Parameter. Raises: TypeError: If area is not a number or a Parameter. - Warning: If area is negative. + ValueError: If area is not a finite number or if the area + Parameter has a non-finite value. """ if not isinstance(area, (Parameter, Numeric)): - raise TypeError('area must be a number or a Parameter.') + raise TypeError("area must be a number or a Parameter.") if isinstance(area, Numeric): if not np.isfinite(area): - raise ValueError('area must be a finite number or a Parameter') + raise ValueError("area must be a finite number or a Parameter") - area = Parameter(name=name + ' area', value=float(area), unit=unit) + area = Parameter(name=name + " area", value=float(area), unit=unit) if area.value < 0: warnings.warn( - f'The area of {name} is negative, which may not be physically meaningful.' + f"The area of {name} is negative, which may not be physically meaningful." ) else: area.min = minimum_area @@ -73,7 +74,7 @@ def _create_center_parameter( center: Numeric | Parameter | None, name: str, fix_if_none: bool, - unit: str | sc.Unit = 'meV', + unit: str | sc.Unit = "meV", enforce_minimum_center: bool = False, ) -> Parameter: """Validate and convert a number to a Parameter describing the @@ -85,29 +86,35 @@ def _create_center_parameter( name (str): The name of the model component. fix_if_none (bool): Whether to fix the center Parameter if center is None. - unit (str | sc.Unit): The unit of the center Parameter. + unit (str | sc.Unit, default='meV'): The unit of the center + Parameter. + enforce_minimum_center (bool, default=False): Whether to + enforce a minimum center value to avoid zero center in + DHO. Returns: Parameter: The validated center Parameter. Raises: TypeError: If center is not None, a number, or a Parameter. + ValueError: If center is a number but not finite, or if + center is a Parameter but has a non-finite value. """ if center is not None and not isinstance(center, (Numeric, Parameter)): - raise TypeError('center must be None, a number, or a Parameter.') + raise TypeError("center must be None, a number, or a Parameter.") if center is None: center = Parameter( - name=name + ' center', + name=name + " center", value=0.0, unit=unit, fixed=fix_if_none, ) elif isinstance(center, Numeric): if not np.isfinite(center): - raise ValueError('center must be None, a finite number or a Parameter') + raise ValueError("center must be None, a finite number or a Parameter") - center = Parameter(name=name + ' center', value=float(center), unit=unit) + center = Parameter(name=name + " center", value=float(center), unit=unit) if enforce_minimum_center: center.min = DHO_MINIMUM_CENTER return center @@ -116,19 +123,20 @@ def _create_width_parameter( self, width: Numeric | Parameter, name: str, - param_name: str = 'width', - unit: str | sc.Unit = 'meV', + param_name: str = "width", + unit: str | sc.Unit = "meV", minimum_width: float = MINIMUM_WIDTH, ) -> Parameter: """Validate and convert a number to a Parameter describing the width of a function. Args: - width (Numeric or Parameter): The width value or Parameter. + width (Numeric | Parameter): The width value or Parameter. name (str): The name of the model component. - param_name (str): The name of the width parameter. - unit (str or sc.Unit): The unit of the width Parameter. - minimum_width (float): The minimum allowed width. + param_name (str, default='width'): The name of the width parameter. + unit (str | sc.Unit, default='meV'): The unit of the width Parameter. + minimum_width (float, default=MINIMUM_WIDTH): The minimum + allowed width. Returns: Parameter: The validated width Parameter. @@ -138,18 +146,18 @@ def _create_width_parameter( ValueError: If width is non-positive. """ if not isinstance(width, (Numeric, Parameter)): - raise TypeError(f'{param_name} must be a number or a Parameter.') + raise TypeError(f"{param_name} must be a number or a Parameter.") if isinstance(width, Numeric): if not np.isfinite(width): - raise ValueError(f'{param_name} must be a finite number or a Parameter') + raise ValueError(f"{param_name} must be a finite number or a Parameter") if float(width) < minimum_width: raise ValueError( - f'The {param_name} of a {self.__class__.__name__} must be greater than zero.' + f"The {param_name} of a {self.__class__.__name__} must be greater than zero." ) width = Parameter( - name=name + ' ' + param_name, + name=name + " " + param_name, value=float(width), unit=unit, min=minimum_width, @@ -157,7 +165,7 @@ def _create_width_parameter( else: if width.value <= 0: raise ValueError( - f'The {param_name} of a {self.__class__.__name__} must be greater than zero.' + f"The {param_name} of a {self.__class__.__name__} must be greater than zero." ) width.min = minimum_width From 109c9681158544c02716412fa41c125a769a61a3 Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Sun, 15 Mar 2026 19:54:49 +0100 Subject: [PATCH 17/31] lorentzian --- .../sample_model/components/lorentzian.py | 75 +++++++------------ 1 file changed, 28 insertions(+), 47 deletions(-) diff --git a/src/easydynamics/sample_model/components/lorentzian.py b/src/easydynamics/sample_model/components/lorentzian.py index 64ebe5948..6a0375db6 100644 --- a/src/easydynamics/sample_model/components/lorentzian.py +++ b/src/easydynamics/sample_model/components/lorentzian.py @@ -25,26 +25,6 @@ class Lorentzian(CreateParametersMixin, ModelComponent): If the center is not provided, it will be centered at 0 and fixed, which is typically what you want in QENS. - - Args: - area (Int | float | Parameter): Area of the Lorentzian. - center (Int | float | None | Parameter): Center of the - Lorentzian. If None, defaults to 0 and is fixed - width (Int | float | Parameter): Half width at half maximum - (HWHM). - unit (str | sc.Unit): Unit of the parameters. Defaults to "meV". - display_name (str | None): Name of the component. - unique_name (str | None): Unique name of the component. If None, - a unique_name is automatically generated. - - Attributes: - area (Parameter): Area of the Lorentzian. - center (Parameter): Center of the Lorentzian. - width (Parameter): Half width at half maximum (HWHM) of the - Lorentzian. - unit (str | sc.Unit): Unit of the parameters. - display_name (str | None): Name of the component. - unique_name (str | None): Unique name of the component. """ def __init__( @@ -52,27 +32,22 @@ def __init__( area: Numeric | Parameter = 1.0, center: Numeric | Parameter | None = None, width: Numeric | Parameter = 1.0, - unit: str | sc.Unit = 'meV', - display_name: str | None = 'Lorentzian', + unit: str | sc.Unit = "meV", + display_name: str | None = "Lorentzian", unique_name: str | None = None, ) -> None: """Initialize the Lorentzian component. Args: - area (Int | float | Parameter): Area of the Lorentzian. - center (Int | float | None | Parameter): Center of the - Lorentzian. If None, defaults to 0 and is fixed - width (Int | float | Parameter): Half width at half maximum + area (Numeric | Parameter, default=1.0): Area of the Lorentzian. + center (Numeric | Parameter | None, default=None): Center of the + Lorentzian. If None, defaults to 0 and is fixed. + width (Numeric | Parameter, default=1.0): Half width at half maximum (HWHM). - unit (str | sc.Unit): Unit of the parameters. Defaults to - "meV". - display_name (str | None): Name of the component. - unique_name (str | None): Unique name of the component. If + unit (str | sc.Unit, default='meV'): Unit of the parameters. + display_name (str | None, default='Lorentzian'): Name of the component. + unique_name (str | None, default=None): Unique name of the component. If None, a unique_name is automatically generated. - - Raises: - TypeError: If any of the parameters are of the wrong type. - ValueError: If width is not positive. """ super().__init__( @@ -82,11 +57,15 @@ def __init__( ) # These methods live in ValidationMixin - area = self._create_area_parameter(area=area, name=display_name, unit=self._unit) + area = self._create_area_parameter( + area=area, name=display_name, unit=self._unit + ) center = self._create_center_parameter( center=center, name=display_name, fix_if_none=True, unit=self._unit ) - width = self._create_width_parameter(width=width, name=display_name, unit=self._unit) + width = self._create_width_parameter( + width=width, name=display_name, unit=self._unit + ) self._area = area self._center = center @@ -112,7 +91,7 @@ def area(self, value: Numeric) -> None: TypeError: If the value is not a number. """ if not isinstance(value, Numeric): - raise TypeError('area must be a number') + raise TypeError("area must be a number") self._area.value = value @property @@ -140,7 +119,7 @@ def center(self, value: Numeric | None) -> None: value = 0.0 self._center.fixed = True if not isinstance(value, Numeric): - raise TypeError('center must be a number') + raise TypeError("center must be a number") self._center.value = value @property @@ -157,21 +136,23 @@ def width(self, value: Numeric) -> None: """Set the width parameter value (HWHM). Args: - value (Numeric | None): The new value for the width - parameter. + value (Numeric): The new value for the width + parameter. Raises: - TypeError: If the value is not a number or None. + TypeError: If the value is not a number. ValueError: If the value is not positive. """ if not isinstance(value, Numeric): - raise TypeError('width must be a number') + raise TypeError("width must be a number") if float(value) <= 0: - raise ValueError('width must be positive') + raise ValueError("width must be positive") self._width.value = value - def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray: + def evaluate( + self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray + ) -> np.ndarray: r"""Evaluate the Lorentzian at the given x values. If x is a scipp Variable, the unit of the Lorentzian will be @@ -186,7 +167,7 @@ def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) the half width at half maximum (HWHM). Args: - x (Numeric or list or np.ndarray or sc.Variable or sc.DataArray): + x (Numeric | list | np.ndarray | sc.Variable | sc.DataArray): The x values at which to evaluate the Lorentzian. Returns: @@ -207,5 +188,5 @@ def __repr__(self) -> str: Returns: str: A string representation of the Lorentzian. """ - return f'Lorentzian(unique_name = {self.unique_name}, unit = {self._unit},\n \ - area = {self.area},\n center = {self.center},\n width = {self.width})' + return f"Lorentzian(unique_name = {self.unique_name}, unit = {self._unit},\n \ + area = {self.area},\n center = {self.center},\n width = {self.width})" From 6b32460de608e4debd869ac85619ab57f57419e1 Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Sun, 15 Mar 2026 19:59:45 +0100 Subject: [PATCH 18/31] gaussian --- .../sample_model/components/gaussian.py | 74 +++++++------------ 1 file changed, 26 insertions(+), 48 deletions(-) diff --git a/src/easydynamics/sample_model/components/gaussian.py b/src/easydynamics/sample_model/components/gaussian.py index cfc83c16a..8216586f0 100644 --- a/src/easydynamics/sample_model/components/gaussian.py +++ b/src/easydynamics/sample_model/components/gaussian.py @@ -31,25 +31,6 @@ class Gaussian(CreateParametersMixin, ModelComponent): If the center is not provided, it will be centered at 0 and fixed, which is typically what you want in QENS. - - Args: - area (Int | float | Parameter | None): Area of the Gaussian. - center (Int | float | Parameter | None): Center of the - Gaussian. If None, defaults to 0 and is fixed. - width (Int | float | Parameter | None): Standard deviation. - unit (str | sc.Unit): Unit of the parameters. Defaults to - "meV". - display_name (str | None): Name of the component. - unique_name (str | None): Unique name of the component. if - None, a unique_name is automatically generated. - - Attributes: - area (Parameter): Area of the Gaussian. - center (Parameter): Center of the Gaussian. - width (Parameter): Standard deviation of the Gaussian. - unit (str | sc.Unit): Unit of the parameters. - display_name (str | None): Name of the component. - unique_name (str | None): Unique name of the component. """ def __init__( @@ -57,28 +38,21 @@ def __init__( area: Numeric | Parameter = 1.0, center: Numeric | Parameter | None = None, width: Numeric | Parameter = 1.0, - unit: str | sc.Unit = 'meV', - display_name: str | None = 'Gaussian', + unit: str | sc.Unit = "meV", + display_name: str | None = "Gaussian", unique_name: str | None = None, ) -> None: """Initialize the Gaussian component. Args: - area (Int | float | Parameter | None): Area of the Gaussian. - center (Int | float | Parameter | None): Center of the + area (Numeric | Parameter, default=1.0): Area of the Gaussian. + center (Numeric | Parameter | None, default=None): Center of the Gaussian. If None, defaults to 0 and is fixed. - width (Int | float | Parameter | None): Standard deviation. - unit (str | sc.Unit): Unit of the parameters. Defaults to - "meV". - display_name (str | None): Name of the component. - unique_name (str | None): Unique name of the component. if - None, a unique_name is automatically generated. - - Raises: - TypeError: If area, center, or width are not numbers or - Parameters. - ValueError: If width is not positive. - TypeError: If unit is not a string or sc.Unit. + width (Numeric | Parameter, default=1.0): Standard deviation. + unit (str | sc.Unit, default='meV'): Unit of the parameters. + display_name (str | None, default='Gaussian'): Name of the component. + unique_name (str | None, default=None): Unique name of the component. + if None, a unique_name is automatically generated. """ # Validate inputs and create Parameters if not given super().__init__( @@ -88,11 +62,15 @@ def __init__( ) # These methods live in ValidationMixin - area = self._create_area_parameter(area=area, name=display_name, unit=self._unit) + area = self._create_area_parameter( + area=area, name=display_name, unit=self._unit + ) center = self._create_center_parameter( center=center, name=display_name, fix_if_none=True, unit=self._unit ) - width = self._create_width_parameter(width=width, name=display_name, unit=self._unit) + width = self._create_width_parameter( + width=width, name=display_name, unit=self._unit + ) self._area = area self._center = center @@ -120,7 +98,7 @@ def area(self, value: Numeric) -> None: """ if not isinstance(value, Numeric): - raise TypeError('area must be a number') + raise TypeError("area must be a number") self._area.value = value @property @@ -134,12 +112,12 @@ def center(self) -> Parameter: return self._center @center.setter - def center(self, value: Numeric) -> None: + def center(self, value: Numeric | None) -> None: """Set the center parameter value. Args: value (Numeric | None): The new value for the center - parameter. If None, defaults to 0 and is fixed. + parameter. If None, defaults to 0 and is fixed. Raises: TypeError: If the value is not a number or None. @@ -149,7 +127,7 @@ def center(self, value: Numeric) -> None: value = 0.0 self._center.fixed = True if not isinstance(value, Numeric): - raise TypeError('center must be a number') + raise TypeError("center must be a number") self._center.value = value @property @@ -166,18 +144,18 @@ def width(self, value: Numeric) -> None: """Set the width parameter value. Args: - value (Numeric | None): The new value for the width - parameter. + value (Numeric): The new value for the width + parameter. Raises: TypeError: If the value is not a number or None. ValueError: If the value is not positive. """ if not isinstance(value, Numeric): - raise TypeError('width must be a number') + raise TypeError("width must be a number") if float(value) <= 0: - raise ValueError('width must be positive') + raise ValueError("width must be positive") self._width.value = value @@ -203,7 +181,7 @@ def evaluate( Args: - x (Numeric or list or np.ndarray or sc.Variable or sc.DataArray): + x (Numeric | list | np.ndarray | sc.Variable | sc.DataArray): The x values at which to evaluate the Gaussian. Returns: @@ -225,5 +203,5 @@ def __repr__(self) -> str: str: A string representation of the Gaussian. """ - return f'Gaussian(unique_name = {self.unique_name}, unit = {self._unit},\n \ - area = {self.area},\n center = {self.center},\n width = {self.width})' + return f"Gaussian(unique_name = {self.unique_name}, unit = {self._unit},\n \ + area = {self.area},\n center = {self.center},\n width = {self.width})" From 41d84090a1f1ca56ef53502b56ccd371240acbe8 Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Sun, 15 Mar 2026 20:08:26 +0100 Subject: [PATCH 19/31] exponential --- .../sample_model/components/exponential.py | 95 +++++++++---------- 1 file changed, 43 insertions(+), 52 deletions(-) diff --git a/src/easydynamics/sample_model/components/exponential.py b/src/easydynamics/sample_model/components/exponential.py index 2f9e41ad8..f7c30b91e 100644 --- a/src/easydynamics/sample_model/components/exponential.py +++ b/src/easydynamics/sample_model/components/exponential.py @@ -24,27 +24,6 @@ class Exponential(CreateParametersMixin, ModelComponent): where $A$ is the amplitude, $x_0$ is the center, and $B$ describes the rate of decay or growth. - - - Args: - amplitude (Int | float | Parameter | None): Amplitude of the - exponential. - center (Int | float | Parameter | None): Center of the - exponential. - rate (Int | float | Parameter | None): Decay or growth constant - unit (str | sc.Unit): Unit of the parameters. Defaults to - "meV". - display_name (str | None): Name of the component. - unique_name (str | None): Unique name of the component. if - None, a unique_name is automatically generated. - - Attributes: - amplitude (Parameter): Amplitude of the exponential. - center (Parameter): Center of the exponential. - rate (Parameter): Decay or growth constant of the exponential. - unit (str | sc.Unit): Unit of the parameters. - display_name (str | None): Name of the component. - unique_name (str | None): Unique name of the component. """ def __init__( @@ -52,28 +31,27 @@ def __init__( amplitude: Numeric | Parameter = 1.0, center: Numeric | Parameter | None = None, rate: Numeric | Parameter = 1.0, - unit: str | sc.Unit = 'meV', - display_name: str | None = 'Exponential', + unit: str | sc.Unit = "meV", + display_name: str | None = "Exponential", unique_name: str | None = None, ) -> None: """Initialize the Exponential component. Args: - amplitude (Int | float | Parameter | None): Amplitude of the + amplitude (Numeric | Parameter, default=1.0): Amplitude of the Exponential. - center (Int | float | Parameter): Center of the Exponential. - rate (Int | float | Parameter | None): Decay or growth + center (Numeric | Parameter | None, default=None): Center of the Exponential. + rate (Numeric | Parameter, default=1.0): Decay or growth constant of the Exponential. - unit (str | sc.Unit): Unit of the parameters. Defaults to - "meV". - display_name (str | None): Name of the component. - unique_name (str | None): Unique name of the component. if + unit (str | sc.Unit, default='meV'): Unit of the parameters. + display_name (str | None, default='Exponential'): Name of the component. + unique_name (str | None, default=None): Unique name of the component. if None, a unique_name is automatically generated. Raises: TypeError: If amplitude, center, or rate are not numbers or Parameters. - TypeError: If unit is not a string or sc.Unit. + ValueError: If amplitude, center or rate are not finite numbers """ # Validate inputs and create Parameters if not given super().__init__( @@ -83,14 +61,14 @@ def __init__( ) if not isinstance(amplitude, (Parameter, Numeric)): - raise TypeError('amplitude must be a number or a Parameter.') + raise TypeError("amplitude must be a number or a Parameter.") if isinstance(amplitude, Numeric): if not np.isfinite(amplitude): - raise ValueError('amplitude must be a finite number or a Parameter') + raise ValueError("amplitude must be a finite number or a Parameter") amplitude = Parameter( - name=display_name + ' amplitude', value=float(amplitude), unit=unit + name=display_name + " amplitude", value=float(amplitude), unit=unit ) center = self._create_center_parameter( @@ -98,13 +76,15 @@ def __init__( ) if not isinstance(rate, (Parameter, Numeric)): - raise TypeError('rate must be a number or a Parameter.') + raise TypeError("rate must be a number or a Parameter.") if isinstance(rate, Numeric): if not np.isfinite(rate): - raise ValueError('rate must be a finite number or a Parameter') + raise ValueError("rate must be a finite number or a Parameter") - rate = Parameter(name=display_name + ' rate', value=float(rate), unit='1/' + str(unit)) + rate = Parameter( + name=display_name + " rate", value=float(rate), unit="1/" + str(unit) + ) self._amplitude = amplitude self._center = center @@ -132,7 +112,7 @@ def amplitude(self, value: Numeric) -> None: """ if not isinstance(value, Numeric): - raise TypeError('amplitude must be a number') + raise TypeError("amplitude must be a number") self._amplitude.value = value @property @@ -146,19 +126,23 @@ def center(self) -> Parameter: return self._center @center.setter - def center(self, value: Numeric) -> None: + def center(self, value: Numeric | None) -> None: """Set the center parameter value. Args: - value (Numeric): The new value for the center - parameter. + value (Numeric | None): The new value for the center + parameter. Raises: TypeError: If the value is not a number. """ + if value is None: + value = 0.0 + self._center.fixed = True + if not isinstance(value, Numeric): - raise TypeError('center must be a number') + raise TypeError("center must be a number") self._center.value = value @property @@ -175,14 +159,14 @@ def rate(self, value: Numeric) -> None: """Set the rate parameter value. Args: - value (Numeric | None): The new value for the rate - parameter. + value (Numeric): The new value for the rate + parameter. Raises: - TypeError: If the value is not a number or None. + TypeError: If the value is not a number. """ if not isinstance(value, Numeric): - raise TypeError('rate must be a number') + raise TypeError("rate must be a number") self._rate.value = value @@ -203,7 +187,7 @@ def evaluate( rate. Args: - x (Numeric or list or np.ndarray or sc.Variable or sc.DataArray): + x (Numeric | list | np.ndarray | sc.Variable | sc.DataArray): The x values at which to evaluate the Exponential. Returns: @@ -220,22 +204,29 @@ def convert_unit(self, unit: str | sc.Unit) -> None: """Convert the unit of the Parameters in the component. Args: - unit (str or sc.Unit): The new unit to convert to. + unit (str | sc.Unit): The new unit to convert to. + + Raises: + TypeError: If unit is not a string or sc.Unit. + Exception: If conversion fails for any parameter. """ + if not isinstance(unit, (str, sc.Unit)): + raise TypeError("unit must be a string or sc.Unit") + old_unit = self._unit pars = [self.amplitude, self.center] try: for p in pars: p.convert_unit(unit) - self.rate.convert_unit('1/' + str(unit)) + self.rate.convert_unit("1/" + str(unit)) self._unit = unit except Exception as e: # Attempt to rollback on failure try: for p in pars: p.convert_unit(old_unit) - self.rate.convert_unit('1/' + str(old_unit)) + self.rate.convert_unit("1/" + str(old_unit)) except Exception: # noqa: S110 pass # Best effort rollback raise e @@ -247,5 +238,5 @@ def __repr__(self) -> str: str: A string representation of the Exponential. """ - return f'Exponential(unique_name = {self.unique_name}, unit = {self._unit},\n \ - amplitude = {self.amplitude},\n center = {self.center},\n rate = {self.rate})' + return f"Exponential(unique_name = {self.unique_name}, unit = {self._unit},\n \ + amplitude = {self.amplitude},\n center = {self.center},\n rate = {self.rate})" From 2523d96130f4ede5db8083e20f0861999e9ac21f Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Sun, 15 Mar 2026 20:10:52 +0100 Subject: [PATCH 20/31] delta function --- .../sample_model/components/delta_function.py | 57 +++++++------------ 1 file changed, 19 insertions(+), 38 deletions(-) diff --git a/src/easydynamics/sample_model/components/delta_function.py b/src/easydynamics/sample_model/components/delta_function.py index 11e444f34..08b81c4e7 100644 --- a/src/easydynamics/sample_model/components/delta_function.py +++ b/src/easydynamics/sample_model/components/delta_function.py @@ -22,49 +22,26 @@ class DeltaFunction(CreateParametersMixin, ModelComponent): as an identity. This is handled by the Convolution method. If the center is not provided, it will be centered at 0 and fixed, which is typically what you want in QENS. - - Args: - center (Int | float | None): Center of the delta function. If - None, defaults to 0 and is fixed. - area (Int | float): Total area under the curve. - unit (str | sc.Unit): Unit of the parameters. - Defaults to "meV". - display_name (str | None): Name of the component. - unique_name (str | None): Unique name of the component. - If None, a unique_name is automatically generated. - - Attributes: - center (Parameter): Center of the delta function. - area (Parameter): Total area under the curve. - unit (str | sc.Unit): Unit of the parameters. - display_name (str | None): Name of the component. - unique_name (str | None): Unique name of the component. """ def __init__( self, - center: None | Numeric | Parameter = None, + center: Numeric | Parameter | None = None, area: Numeric | Parameter = 1.0, - unit: str | sc.Unit = 'meV', - display_name: str | None = 'DeltaFunction', + unit: str | sc.Unit = "meV", + display_name: str | None = "DeltaFunction", unique_name: str | None = None, ) -> None: """Initialize the Delta function. Args: - center (Int | float | None): Center of the delta function. + center (Numeric | Parameter | None, default=None): Center of the delta function. If None, defaults to 0 and is fixed. - area (Int | float): Total area under the curve. - unit (str | sc.Unit): Unit of the parameters. - Defaults to "meV". - display_name (str | None): Name of the component. - unique_name (str | None): Unique name of the component. + area (Numeric | Parameter, default=1.0): Total area under the curve. + unit (str | sc.Unit, default='meV'): Unit of the parameters. + display_name (str | None, default='DeltaFunction'): Name of the component. + unique_name (str | None, default=None): Unique name of the component. If None, a unique_name is automatically generated. - - Raises: - TypeError: If center is not a number or None. - TypeError: If area is not a number. - TypeError: If unit is not a string or sc.Unit. """ # Validate inputs and create Parameters if not given super().__init__( @@ -74,7 +51,9 @@ def __init__( ) # These methods live in ValidationMixin - area = self._create_area_parameter(area=area, name=display_name, unit=self._unit) + area = self._create_area_parameter( + area=area, name=display_name, unit=self._unit + ) center = self._create_center_parameter( center=center, name=display_name, fix_if_none=True, unit=self._unit ) @@ -104,7 +83,7 @@ def area(self, value: Numeric) -> None: """ if not isinstance(value, Numeric): - raise TypeError('area must be a number') + raise TypeError("area must be a number") self._area.value = value @property @@ -123,7 +102,7 @@ def center(self, value: Numeric | None) -> None: Args: value (Numeric | None): The new value for the center - parameter. If None, defaults to 0 and is fixed. + parameter. If None, defaults to 0 and is fixed. Raises: TypeError: If the value is not a number or None. @@ -133,10 +112,12 @@ def center(self, value: Numeric | None) -> None: value = 0.0 self._center.fixed = True if not isinstance(value, Numeric): - raise TypeError('center must be a number') + raise TypeError("center must be a number") self._center.value = value - def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray: + def evaluate( + self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray + ) -> np.ndarray: """Evaluate the Delta function at the given x values. The Delta function evaluates to zero everywhere, except at the @@ -188,5 +169,5 @@ def __repr__(self) -> str: str: A string representation of the Delta function. """ - return f'DeltaFunction(unique_name = {self.unique_name}, unit = {self._unit},\n \ - area = {self.area},\n center = {self.center}' + return f"DeltaFunction(unique_name = {self.unique_name}, unit = {self._unit},\n \ + area = {self.area},\n center = {self.center}" From e52139b30a37274322afb58158eac1a9574ec177 Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Sun, 15 Mar 2026 20:13:48 +0100 Subject: [PATCH 21/31] dho --- .../components/damped_harmonic_oscillator.py | 78 +++++++------------ 1 file changed, 28 insertions(+), 50 deletions(-) diff --git a/src/easydynamics/sample_model/components/damped_harmonic_oscillator.py b/src/easydynamics/sample_model/components/damped_harmonic_oscillator.py index ce4a08fb1..65679b534 100644 --- a/src/easydynamics/sample_model/components/damped_harmonic_oscillator.py +++ b/src/easydynamics/sample_model/components/damped_harmonic_oscillator.py @@ -23,29 +23,6 @@ class DampedHarmonicOscillator(CreateParametersMixin, ModelComponent): $$ where $A$ is the area, $x_0$ is the center, and $\gamma$ is the width. - - - Args: - area (Int | float): Area under the curve. - center (Int | float): Resonance frequency, approximately the - peak position. - width (Int | float): Damping constant, approximately the - half width at half max (HWHM) of the peaks. - unit (str | sc.Unit): Unit of the parameters. - Defaults to "meV". - display_name (str | None): Display name of the component. - unique_name (str | None): Unique name of the component. - If None, a unique_name is automatically generated. - - Attributes: - area (Parameter): Area under the curve. - center (Parameter): Resonance frequency, approximately the - peak position. - width (Parameter): Damping constant, approximately the - half width at half max (HWHM) of the peaks. - unit (str | sc.Unit): Unit of the parameters. - display_name (str | None): Display name of the component. - unique_name (str | None): Unique name of the component. """ def __init__( @@ -53,28 +30,23 @@ def __init__( area: Numeric | Parameter = 1.0, center: Numeric | Parameter = 1.0, width: Numeric | Parameter = 1.0, - unit: str | sc.Unit = 'meV', - display_name: str | None = 'DampedHarmonicOscillator', + unit: str | sc.Unit = "meV", + display_name: str | None = "DampedHarmonicOscillator", unique_name: str | None = None, ) -> None: """Initialize the Damped Harmonic Oscillator. Args: - area (Int | float): Area under the curve. - center (Int | float): Resonance frequency, approximately the + area (Numeric | Parameter, default=1.0): Area under the curve. + center (Numeric | Parameter, default=1.0): Resonance frequency, approximately the peak position. - width (Int | float): Damping constant, approximately the + width (Numeric | Parameter, default=1.0): Damping constant, approximately the half width at half max (HWHM) of the peaks. - unit (str | sc.Unit): Unit of the parameters. - Defaults to "meV". - display_name (str | None): Display name of the component. - unique_name (str | None): Unique name of the component. + unit (str | sc.Unit, default='meV'): Unit of the parameters. + display_name (str | None, default='DampedHarmonicOscillator'): + Display name of the component. + unique_name (str | None, default=None): Unique name of the component. If None, a unique_name is automatically generated. - - Raises: - TypeError: If any of the parameters are not numbers or - Parameters. - ValueError: If center or width are not positive. """ super().__init__( @@ -84,7 +56,9 @@ def __init__( ) # These methods live in ValidationMixin - area = self._create_area_parameter(area=area, name=display_name, unit=self._unit) + area = self._create_area_parameter( + area=area, name=display_name, unit=self._unit + ) center = self._create_center_parameter( center=center, name=display_name, @@ -93,7 +67,9 @@ def __init__( enforce_minimum_center=True, ) - width = self._create_width_parameter(width=width, name=display_name, unit=self._unit) + width = self._create_width_parameter( + width=width, name=display_name, unit=self._unit + ) self._area = area self._center = center @@ -119,7 +95,7 @@ def area(self, value: Numeric) -> None: TypeError: If the value is not a number. """ if not isinstance(value, Numeric): - raise TypeError('area must be a number') + raise TypeError("area must be a number") self._area.value = value @property @@ -143,10 +119,10 @@ def center(self, value: Numeric) -> None: ValueError: If the value is not positive. """ if not isinstance(value, Numeric): - raise TypeError('center must be a number') + raise TypeError("center must be a number") if float(value) <= 0: - raise ValueError('center must be positive') + raise ValueError("center must be positive") self._center.value = value @property @@ -170,14 +146,16 @@ def width(self, value: Numeric) -> None: ValueError: If the value is not positive. """ if not isinstance(value, Numeric): - raise TypeError('width must be a number') + raise TypeError("width must be a number") if float(value) <= 0: - raise ValueError('width must be positive') + raise ValueError("width must be positive") self._width.value = value - def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray: + def evaluate( + self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray + ) -> np.ndarray: r"""Evaluate the Damped Harmonic Oscillator at the given x values. @@ -202,7 +180,9 @@ def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) normalization = 2 * self.center.value**2 * self.width.value / np.pi # No division by zero here, width>0 enforced in setter - denominator = (x**2 - self.center.value**2) ** 2 + (2 * self.width.value * x) ** 2 + denominator = (x**2 - self.center.value**2) ** 2 + ( + 2 * self.width.value * x + ) ** 2 return self.area.value * normalization / (denominator) @@ -214,7 +194,5 @@ def __repr__(self) -> str: str: A string representation of the Damped Harmonic Oscillator. """ - return ( - f'DampedHarmonicOscillator(display_name = {self.display_name}, unit = {self._unit},\n \ - area = {self.area},\n center = {self.center},\n width = {self.width})' - ) + return f"DampedHarmonicOscillator(display_name = {self.display_name}, unit = {self._unit},\n \ + area = {self.area},\n center = {self.center},\n width = {self.width})" From 81f67a1277799a0f947d93206d410b09dfa5afa2 Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Sun, 15 Mar 2026 20:29:12 +0100 Subject: [PATCH 22/31] component collection --- .../sample_model/component_collection.py | 184 ++++++++++-------- .../components/model_component.py | 5 + 2 files changed, 113 insertions(+), 76 deletions(-) diff --git a/src/easydynamics/sample_model/component_collection.py b/src/easydynamics/sample_model/component_collection.py index 4b90f85cf..42a8ec660 100644 --- a/src/easydynamics/sample_model/component_collection.py +++ b/src/easydynamics/sample_model/component_collection.py @@ -20,53 +20,36 @@ class ComponentCollection(ModelBase): """Collection of model components representing a sample, background or resolution model. - - Args: - unit (str | sc.Unit): Unit of the sample model. Defaults to - "meV". - display_name (str): Display name of the sample model. - unique_name (str | None): Unique name of the sample model. - If None, a unique_name is automatically generated. - components (List[ModelComponent] | None): Initial model - components to add to the ComponentCollection. - - Attributes: - components (List[ModelComponent]): List of model components in - the collection. - unit (str | sc.Unit): Unit of the sample model. - display_name (str): Display name of the sample model. - unique_name (str): Unique name of the sample model. """ def __init__( self, - unit: str | sc.Unit = 'meV', - display_name: str = 'MyComponentCollection', + unit: str | sc.Unit = "meV", + display_name: str | None = "MyComponentCollection", unique_name: str | None = None, - components: List[ModelComponent] | None = None, + components: list[ModelComponent] | None = None, ) -> None: """Initialize a new ComponentCollection. Args: - unit (str | sc.Unit | None): Unit of the sample model. - Defaults to "meV". - display_name (str | None): Display name of the sample model. - unique_name (str | None): Unique name of the sample model. - Defaults to None. - components (List[ModelComponent] | None): Initial model - components to add to the ComponentCollection. + unit (str | sc.Unit, default='meV'): Unit of the collection. + display_name (str | None, default="MyComponentCollection"): Display + name of the collection + unique_name (str | None, default=None): Unique name of the collection + Defaults to None. + components (list[ModelComponent] | None, default=None): Initial model + components to add to the ComponentCollection. Raises: TypeError: If unit is not a string or sc.Unit, or if components is not a list of ModelComponent. - ValueError: If components contains duplicate unique names. """ super().__init__(display_name=display_name, unique_name=unique_name) if unit is not None and not isinstance(unit, (str, sc.Unit)): raise TypeError( - f'unit must be None, a string, or a scipp Unit, got {type(unit).__name__}' + f"unit must be None, a string, or a scipp Unit, got {type(unit).__name__}" ) self._unit = unit self._components = [] @@ -74,7 +57,9 @@ def __init__( # Add initial components if provided. Used for serialization. if components is not None: if not isinstance(components, list): - raise TypeError('components must be a list of ModelComponent instances.') + raise TypeError( + "components must be a list of ModelComponent instances." + ) for comp in components: self.append_component(comp) @@ -87,17 +72,17 @@ def components(self) -> list[ModelComponent]: """Get the list of components in the collection. Returns: - List[ModelComponent]: The components in the collection. + list[ModelComponent]: The components in the collection. """ return list(self._components) @components.setter - def components(self, components: List[ModelComponent]) -> None: + def components(self, components: list[ModelComponent]) -> None: """Set the list of components in the collection. Args: - components (List[ModelComponent]): The new list of + components (list[ModelComponent]): The new list of components. Raises: @@ -105,12 +90,12 @@ def components(self, components: List[ModelComponent]) -> None: """ if not isinstance(components, list): - raise TypeError('components must be a list of ModelComponent instances.') + raise TypeError("components must be a list of ModelComponent instances.") for comp in components: if not isinstance(comp, ModelComponent): raise TypeError( - 'All items in components must be instances of ModelComponent. ' - f'Got {type(comp).__name__} instead.' + "All items in components must be instances of ModelComponent. " + f"Got {type(comp).__name__} instead." ) self._components = components @@ -137,8 +122,8 @@ def is_empty(self, value: bool) -> None: AttributeError: Always raised since is_empty is read-only. """ raise AttributeError( - 'is_empty is a read-only property that indicates ' - 'whether the collection has components.' + "is_empty is a read-only property that indicates " + "whether the collection has components." ) @property @@ -147,7 +132,7 @@ def unit(self) -> str | sc.Unit | None: Returns: str | sc.Unit | None: The unit of the ComponentCollection, - which is the same as the unit of its components. + which is the same as the unit of its components. """ return self._unit @@ -164,8 +149,8 @@ def unit(self, unit_str: str) -> None: raise AttributeError( ( - f'Unit is read-only. Use convert_unit to change the unit between allowed types ' - f'or create a new {self.__class__.__name__} with the desired unit.' + f"Unit is read-only. Use convert_unit to change the unit between allowed types " + f"or create a new {self.__class__.__name__} with the desired unit." ) ) # noqa: E501 @@ -178,10 +163,15 @@ def convert_unit(self, unit: str | sc.Unit) -> None: Raises: TypeError: If unit is not a string or sc.Unit. - UnitError: If any component cannot be converted to the + Exception: If any component cannot be converted to the specified unit. """ + if not isinstance(unit, (str, sc.Unit)): + raise TypeError( + f"Unit must be a string or sc.Unit, got {type(unit).__name__}" + ) + old_unit = self._unit try: @@ -201,14 +191,16 @@ def convert_unit(self, unit: str | sc.Unit) -> None: # Component management # ------------------------------------------------------------------ - def append_component(self, component: ModelComponent | 'ComponentCollection') -> None: + def append_component( + self, component: ModelComponent | "ComponentCollection" + ) -> None: """Append a model component or the components from another ComponentCollection to this ComponentCollection. Args: - component (ModelComponent | ComponentCollection): The component - to append. If a ComponentCollection is provided, all of its - components will be appended. + component (ModelComponent | "ComponentCollection"): The component + to append. If a ComponentCollection is provided, all of its + components will be appended. Raises: TypeError: If component is not a ModelComponent or @@ -218,8 +210,8 @@ def append_component(self, component: ModelComponent | 'ComponentCollection') -> """ if not isinstance(component, (ModelComponent, ComponentCollection)): raise TypeError( - 'Component must be an instance of ModelComponent or ComponentCollection. ' - f'Got {type(component).__name__} instead.' + "Component must be an instance of ModelComponent or ComponentCollection. " + f"Got {type(component).__name__} instead." ) if isinstance(component, ModelComponent): components = (component,) @@ -230,7 +222,7 @@ def append_component(self, component: ModelComponent | 'ComponentCollection') -> if comp in self._components: raise ValueError( f"Component '{comp.unique_name}' is already in the collection. " - f'Existing components: {self.list_component_names()}' + f"Existing components: {self.list_component_names()}" ) self._components.append(comp) @@ -240,6 +232,7 @@ def remove_component(self, unique_name: str) -> None: Args: unique_name (str): Unique name of the component to remove. + Raises: TypeError: If unique_name is not a string. KeyError: If no component with the given unique name exists @@ -247,7 +240,7 @@ def remove_component(self, unique_name: str) -> None: """ if not isinstance(unique_name, str): - raise TypeError('Component name must be a string.') + raise TypeError("Component name must be a string.") for comp in self._components: if comp.unique_name == unique_name: @@ -256,36 +249,65 @@ def remove_component(self, unique_name: str) -> None: raise KeyError( f"No component named '{unique_name}' exists. " - f'Did you accidentally use the display_name? ' - f'Here is a list of the components in the collection: {self.list_component_names()}' + f"Did you accidentally use the display_name? " + f"Here is a list of the components in the collection: {self.list_component_names()}" ) @property def components(self) -> list[ModelComponent]: + """Get the list of components in the collection. + + Returns: + list[ModelComponent]: The components in the collection. + """ return list(self._components) @components.setter - def components(self, components: List[ModelComponent]) -> None: + def components(self, components: list[ModelComponent]) -> None: + """ + Set the components in the collection. + + Args: + components (list[ModelComponent]): The new components in the collection + + Raises: + TypeError: If components is not a list of ModelComponent + """ if not isinstance(components, list): - raise TypeError('components must be a list of ModelComponent instances.') + raise TypeError("components must be a list of ModelComponent instances.") for comp in components: if not isinstance(comp, ModelComponent): raise TypeError( - 'All items in components must be instances of ModelComponent. ' - f'Got {type(comp).__name__} instead.' + "All items in components must be instances of ModelComponent. " + f"Got {type(comp).__name__} instead." ) self._components = components @property def is_empty(self) -> bool: + """ + Returns True if the collection has no components, otherwise False + + Returns: + bool: True if the collection has no components, otherwise False + """ return not self._components @is_empty.setter def is_empty(self, value: bool) -> None: + """ + is_empty is read-only. + + Args: + value (bool): ignored. + + Raises: + AttributeError: Always raised since is_empty is read-only + """ raise AttributeError( - 'is_empty is a read-only property that indicates ' - 'whether the collection has components.' + "is_empty is a read-only property that indicates " + "whether the collection has components." ) def list_component_names(self) -> List[str]: @@ -293,7 +315,7 @@ def list_component_names(self) -> List[str]: Returns: List[str]: List of unique names of the components in the - collection. + collection. """ return [component.unique_name for component in self._components] @@ -307,32 +329,32 @@ def normalize_area(self) -> None: is useful for convolutions. Raises: - ValueError: If there are no components in the model. - ValueError: If the total area is zero or not finite, which + ValueError: If there are no components in the model or + if the total area is zero or not finite, which would prevent normalization. """ if not self.components: - raise ValueError('No components in the model to normalize.') + raise ValueError("No components in the model to normalize.") area_params = [] - total_area = Parameter(name='total_area', value=0.0, unit=self._unit) + total_area = Parameter(name="total_area", value=0.0, unit=self._unit) for component in self.components: - if hasattr(component, 'area'): + if hasattr(component, "area"): area_params.append(component.area) total_area += component.area else: warnings.warn( f"Component '{component.unique_name}' does not have an 'area' attribute " - f'and will be skipped in normalization.', + f"and will be skipped in normalization.", UserWarning, ) if total_area.value == 0: - raise ValueError('Total area is zero; cannot normalize.') + raise ValueError("Total area is zero; cannot normalize.") if not np.isfinite(total_area.value): - raise ValueError('Total area is not finite; cannot normalize.') + raise ValueError("Total area is not finite; cannot normalize.") for param in area_params: param.value /= total_area.value @@ -345,19 +367,25 @@ def get_all_variables(self) -> list[DescriptorBase]: """Get all parameters from the model component. Returns: - List[Parameter]: List of parameters in the component. + list[DescriptorBase]: List of parameters in the component. """ - return [var for component in self.components for var in component.get_all_variables()] + return [ + var + for component in self.components + for var in component.get_all_variables() + ] - def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray: + def evaluate( + self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray + ) -> np.ndarray: """Evaluate the sum of all components. Args: - x (Number, list, np.ndarray, sc.Variable, or sc.DataArray): + x (Numeric | list | np.ndarray | sc.Variable | sc.DataArray): Energy axis. - Returns + Returns: np.ndarray: Evaluated model values. """ @@ -373,7 +401,7 @@ def evaluate_component( """Evaluate a single component by name. Args: - x (Number, list, np.ndarray, sc.Variable, or sc.DataArray): + x (Numeric | list | np.ndarray | sc.Variable | sc.DataArray): Energy axis. unique_name (str): Component unique name. @@ -386,11 +414,13 @@ def evaluate_component( in the collection. """ if not self.components: - raise ValueError('No components in the model to evaluate.') + raise ValueError("No components in the model to evaluate.") if not isinstance(unique_name, str): raise TypeError( - (f'Component unique name must be a string, got {type(unique_name)} instead.') + ( + f"Component unique name must be a string, got {type(unique_name)} instead." + ) ) matches = [comp for comp in self.components if comp.unique_name == unique_name] @@ -422,7 +452,7 @@ def __contains__(self, item: str | ModelComponent) -> bool: in the ComponentCollection. Args: - item (str or ModelComponent): The component name or instance + item (str | ModelComponent): The component name or instance to check for. Returns: @@ -444,6 +474,8 @@ def __repr__(self) -> str: Returns: str: String representation of the ComponentCollection. """ - comp_names = ', '.join(c.unique_name for c in self.components) or 'No components' + comp_names = ( + ", ".join(c.unique_name for c in self.components) or "No components" + ) return f"" diff --git a/src/easydynamics/sample_model/components/model_component.py b/src/easydynamics/sample_model/components/model_component.py index 1289773bc..03567f52f 100644 --- a/src/easydynamics/sample_model/components/model_component.py +++ b/src/easydynamics/sample_model/components/model_component.py @@ -171,9 +171,14 @@ def convert_unit(self, unit: str | sc.Unit) -> None: unit (str | sc.Unit): The new unit to convert to. Raises: + TypeError: If the provided unit is not a str or sc.Unit Exception: If the provided unit is invalid or incompatible with the component's parameters. """ + if not isinstance(unit, (str, sc.Unit)): + raise TypeError( + f"Unit must be a string or sc.Unit, got {type(unit).__name__}" + ) old_unit = self._unit pars = self.get_all_parameters() From 2ba2420a48b268f9a4c6e160e8e7224ed8b58fe5 Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Sun, 15 Mar 2026 20:30:44 +0100 Subject: [PATCH 23/31] background model --- .../sample_model/background_model.py | 37 ++++--------------- 1 file changed, 8 insertions(+), 29 deletions(-) diff --git a/src/easydynamics/sample_model/background_model.py b/src/easydynamics/sample_model/background_model.py index 91ced6db1..e2a1adde6 100644 --- a/src/easydynamics/sample_model/background_model.py +++ b/src/easydynamics/sample_model/background_model.py @@ -13,49 +13,28 @@ class BackgroundModel(ModelBase): """BackgroundModel represents a model of the background in an experiment at various Q. - - Args: - display_name (str): Display name of the model. - unique_name (str | None): Unique name of the model. If None, a - unique name will be generated. - unit (str | sc.Unit | None): Unit of the model. Defaults to - "meV". - components (ModelComponent | ComponentCollection | None): - Template components of the model. If None, no components - are added. These components are copied into - ComponentCollections for each Q value. - Q (Q_type | None): Q values for the model. If None, Q is not - set. - - Attributes: - unit (str | sc.Unit): Unit of the model. - components (list[ModelComponent]): List of ModelComponents in - the model. - Q (np.ndarray | Numeric | list | ArrayLike | sc.Variable | None): - Q values of the model. """ def __init__( self, - display_name: str = 'MyBackgroundModel', + display_name: str | None = "MyBackgroundModel", unique_name: str | None = None, - unit: str | sc.Unit = 'meV', - components: ComponentCollection | ModelComponent | None = None, + unit: str | sc.Unit = "meV", + components: ModelComponent | ComponentCollection | None = None, Q: Q_type | None = None, ) -> None: """Initialize the BackgroundModel. Args: - display_name (str): Display name of the model. - unique_name (str | None): Unique name of the model. If None, + display_name (str | None, default='MyBackgroundModel'): Display name of the model. + unique_name (str | None, default=None): Unique name of the model. If None, a unique name will be generated. - unit (str | sc.Unit | None): Unit of the model. Defaults to - "meV". - components (ModelComponent | ComponentCollection | None): + unit (str | sc.Unit, default='meV'): Unit of the model. + components (ModelComponent | ComponentCollection | None, default=None): Template components of the model. If None, no components are added. These components are copied into ComponentCollections for each Q value. - Q (Q_type | None): Q values for the model. If None, Q is not + Q (Q_type | None, default=None): Q values for the model. If None, Q is not set. """ super().__init__( From 105824597f484d90766af2fa901cf4297e71ddcd Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Sun, 15 Mar 2026 20:35:04 +0100 Subject: [PATCH 24/31] experiment --- src/easydynamics/experiment/experiment.py | 127 ++++++++++------------ 1 file changed, 60 insertions(+), 67 deletions(-) diff --git a/src/easydynamics/experiment/experiment.py b/src/easydynamics/experiment/experiment.py index b7d3845bc..2bb351104 100644 --- a/src/easydynamics/experiment/experiment.py +++ b/src/easydynamics/experiment/experiment.py @@ -19,43 +19,26 @@ class Experiment(NewBase): This is a minimal implementation that will be extended in the future. - - Args: - display_name (str): Display name of the experiment. - unique_name (str | None): Unique name of the experiment. If - None, a unique name will be generated. - data (sc.DataArray | str | None): Dataset associated with the - experiment. Can be a sc.DataArray or a filename string to - load from. If None, no data is loaded. - - Attributes: - data (sc.DataArray | None): Dataset associated with the - experiment. - binned_data (sc.DataArray | None): Binned dataset associated - with the experiment. This is derived from `data` and is - updated whenever `data` is set. """ def __init__( self, - display_name: str = 'MyExperiment', + display_name: str | None = "MyExperiment", unique_name: str | None = None, data: sc.DataArray | str | None = None, ) -> None: """Initialize the Experiment object. Args: - display_name (str): Display name of the experiment. - unique_name (str | None): Unique name of the experiment. If + display_name (str | None, default="MyExperiment"): Display name of the experiment. + unique_name (str | None, default=None): Unique name of the experiment. If None, a unique name will be generated. - data (sc.DataArray | str | None): Dataset associated with + data (sc.DataArray | str | None, default=None): Dataset associated with the experiment. Can be a sc.DataArray or a filename string to load from. If None, no data is loaded. Raises: TypeError: If data is not a sc.DataArray, a string, or None. - ValueError: If the loaded data is missing required - coordinates. """ super().__init__( display_name=display_name, @@ -71,7 +54,7 @@ def __init__( self._data = data else: raise TypeError( - f'Data must be a sc.DataArray or a filename string, not {type(data).__name__}' + f"Data must be a sc.DataArray or a filename string, not {type(data).__name__}" ) self._binned_data = ( @@ -102,10 +85,9 @@ def data(self, value: sc.DataArray) -> None: Raises: TypeError: If the value is not a sc.DataArray. - ValueError: If the dataset is missing required coordinates. """ if not isinstance(value, sc.DataArray): - raise TypeError(f'Data must be a sc.DataArray, not {type(value).__name__}') + raise TypeError(f"Data must be a sc.DataArray, not {type(value).__name__}") self._validate_coordinates(value) self._data = value self._binned_data = ( @@ -129,12 +111,14 @@ def binned_data(self, value: sc.DataArray) -> None: Args: value (sc.DataArray): The new binned dataset to associate - with this experiment (ignored) + with this experiment (ignored) Raises: AttributeError: Always, since binned_data is read-only. """ - raise AttributeError('binned_data is a read-only property. Use rebin() to rebin the data') + raise AttributeError( + "binned_data is a read-only property. Use rebin() to rebin the data" + ) @property def Q(self) -> sc.Variable | None: @@ -146,7 +130,7 @@ def Q(self) -> sc.Variable | None: """ if self._binned_data is None: return None - return self._binned_data.coords['Q'] + return self._binned_data.coords["Q"] @Q.setter def Q(self, value: sc.Variable) -> None: @@ -159,7 +143,7 @@ def Q(self, value: sc.Variable) -> None: Raises: AttributeError: Always, since Q is read-only. """ - raise AttributeError('Q is a read-only property derived from the data.') + raise AttributeError("Q is a read-only property derived from the data.") @property def energy(self) -> sc.Variable | None: @@ -171,7 +155,7 @@ def energy(self) -> sc.Variable | None: """ if self._binned_data is None: return None - return self._binned_data.coords['energy'] + return self._binned_data.coords["energy"] @energy.setter def energy(self, value: sc.Variable) -> None: @@ -184,7 +168,7 @@ def energy(self, value: sc.Variable) -> None: Raises: AttributeError: Always, since energy is read-only. """ - raise AttributeError('energy is a read-only property derived from the data.') + raise AttributeError("energy is a read-only property derived from the data.") def get_masked_energy(self, Q_index: int) -> sc.Variable | None: """Get the energy values from the dataset, removing points where @@ -197,6 +181,9 @@ def get_masked_energy(self, Q_index: int) -> sc.Variable | None: Returns: sc.Variable | None: The masked energy values from the dataset, or None if no data is loaded. + + Raises: + IndexError: If Q_index is not a valid index for the Q values. """ if self._binned_data is None: return None @@ -206,12 +193,12 @@ def get_masked_energy(self, Q_index: int) -> sc.Variable | None: or Q_index < 0 or (self.Q is not None and Q_index >= len(self.Q)) ): - raise IndexError('Q_index must be a valid index for the Q values.') + raise IndexError("Q_index must be a valid index for the Q values.") - energy = self._binned_data.coords['energy'] + energy = self._binned_data.coords["energy"] _, _, _, mask = self._extract_x_y_weights_only_finite(Q_index=Q_index) - mask_var = sc.array(dims=['energy'], values=mask) + mask_var = sc.array(dims=["energy"], values=mask) masked_energy = energy[mask_var] return masked_energy @@ -224,29 +211,27 @@ def load_hdf5(self, filename: str, display_name: str | None = None) -> None: Args: filename (str ): Path to the HDF5 file. - display_name (str | None): Optional display name for the + display_name (str | None, default=None): Optional display name for the experiment. Raises: TypeError: If filename is not a string or if display_name is - not a string or None. - ValueError: If the loaded data is missing required - coordinates. + not a string or None or if the loaded data is not a sc.DataArray. """ if not isinstance(filename, str): - raise TypeError(f'Filename must be a string, not {type(filename).__name__}') + raise TypeError(f"Filename must be a string, not {type(filename).__name__}") if display_name is not None: if not isinstance(display_name, str): raise TypeError( - f'Display name must be a string, not {type(display_name).__name__}' + f"Display name must be a string, not {type(display_name).__name__}" ) self.display_name = display_name loaded_data = sc_load_hdf5(filename) if not isinstance(loaded_data, sc.DataArray): raise TypeError( - f'Loaded data must be a sc.DataArray, not {type(loaded_data).__name__}' + f"Loaded data must be a sc.DataArray, not {type(loaded_data).__name__}" ) self._validate_coordinates(loaded_data) self.data = loaded_data @@ -255,7 +240,7 @@ def save_hdf5(self, filename: str | None = None) -> None: """Save the dataset to HDF5. Args: - filename (str | None): Path to the output HDF5 file. + filename (str | None, default=None): Path to the output HDF5 file. If None, the file will be named after the unique_name of the experiment with a .h5 extension. @@ -265,13 +250,13 @@ def save_hdf5(self, filename: str | None = None) -> None: """ if filename is None: - filename = f'{self.unique_name}.h5' + filename = f"{self.unique_name}.h5" if not isinstance(filename, str): - raise TypeError(f'Filename must be a string, not {type(filename).__name__}') + raise TypeError(f"Filename must be a string, not {type(filename).__name__}") if self._data is None: - raise ValueError('No data to save.') + raise ValueError("No data to save.") dir_name = os.path.dirname(filename) if dir_name: @@ -295,36 +280,39 @@ def rebin(self, dimensions: dict[str, int | sc.Variable]) -> None: Raises: TypeError: If dimensions is not a dictionary or if keys/values are of incorrect types. + ValueError: If there is no data to rebin. KeyError: If a specified dimension is not in the dataset. """ if not isinstance(dimensions, dict): raise TypeError( - 'dimensions must be a dictionary mapping dimension names ' - 'to number of bins or bin values as sc.Variable.' + "dimensions must be a dictionary mapping dimension names " + "to number of bins or bin values as sc.Variable." ) if self._data is None: - raise ValueError('No data to rebin. Please load data first.') + raise ValueError("No data to rebin. Please load data first.") binned_data = self._data.copy() dim_copy = dimensions.copy() for dim, value in dim_copy.items(): if not isinstance(dim, str): raise TypeError( - f'Dimension keys must be strings. Got {type(dim)} for {dim} instead.' + f"Dimension keys must be strings. Got {type(dim)} for {dim} instead." ) if dim not in self._data.dims: raise KeyError( f"Dimension '{dim}' not a valid dimension for rebinning. " - f'Should be one of {self._data.dims}.' + f"Should be one of {self._data.dims}." ) - if isinstance(value, float) and value.is_integer(): # I allow eg. 2.0 as well as 2 + if ( + isinstance(value, float) and value.is_integer() + ): # I allow eg. 2.0 as well as 2 value = int(value) # This line can be removed when scipp resize support # resizing with coordinates dimensions[dim] = value if not (isinstance(value, int) or isinstance(value, sc.Variable)): raise TypeError( - f'Dimension values must be integers or sc.Variable. ' + f"Dimension values must be integers or sc.Variable. " f"Got {type(value)} for dimension '{dim}' instead." ) binned_data = binned_data.bin({dim: value}) @@ -337,12 +325,12 @@ def rebin(self, dimensions: dict[str, int | sc.Variable]) -> None: # other methods ########### - def plot_data(self, slicer=False, **kwargs) -> None: + def plot_data(self, slicer: bool = False, **kwargs: dict) -> None: """Plot the dataset using plopp: https://scipp.github.io/plopp/ Args: - slicer (bool): If True, use plopp's slicer instead of plot. - **kwargs (Any): Additional keyword arguments to pass to plopp. + slicer (bool, default=False): If True, use plopp's slicer instead of plot. + **kwargs (dict): Additional keyword arguments to pass to plopp. Raises: ValueError: If there is no data to plot. @@ -350,13 +338,15 @@ def plot_data(self, slicer=False, **kwargs) -> None: """ if self._binned_data is None: - raise ValueError('No data to plot. Please load data first.') + raise ValueError("No data to plot. Please load data first.") if not _in_notebook(): - raise RuntimeError('plot_data() can only be used in a Jupyter notebook environment.') + raise RuntimeError( + "plot_data() can only be used in a Jupyter notebook environment." + ) plot_kwargs_defaults = { - 'title': self.display_name, + "title": self.display_name, } # Overwrite defaults with any user-provided kwargs plot_kwargs_defaults.update(kwargs) @@ -367,7 +357,7 @@ def plot_data(self, slicer=False, **kwargs) -> None: ) else: fig = pp.plot( - self._binned_data.transpose(dims=['energy', 'Q']), + self._binned_data.transpose(dims=["energy", "Q"]), **plot_kwargs_defaults, ) return fig @@ -384,12 +374,13 @@ def _validate_coordinates(data: sc.DataArray) -> None: data (sc.DataArray): The data to validate. Raises: + TypeError: If data is not a sc.DataArray. ValueError: If required coordinates are missing. """ if not isinstance(data, sc.DataArray): - raise TypeError('Data must be a sc.DataArray.') + raise TypeError("Data must be a sc.DataArray.") - required_coords = ['Q', 'energy'] + required_coords = ["Q", "energy"] for coord in required_coords: if coord not in data.coords: raise ValueError(f"Data is missing required coordinate: '{coord}'") @@ -410,7 +401,9 @@ def _convert_to_bin_centers(self, data: sc.DataArray) -> sc.DataArray: data = data.assign_coords({dim: sc.midpoints(coord)}) return data - def _extract_x_y_var(self, Q_index: int) -> tuple[np.ndarray, np.ndarray, np.ndarray]: + def _extract_x_y_var( + self, Q_index: int + ) -> tuple[np.ndarray, np.ndarray, np.ndarray]: """Extract the x, y, and weights arrays from the experiment for the given Q index. @@ -422,8 +415,8 @@ def _extract_x_y_var(self, Q_index: int) -> tuple[np.ndarray, np.ndarray, np.nda variances arrays extracted from the experiment for the given Q index. """ - data = self.binned_data['Q', Q_index] - x = data.coords['energy'].values + data = self.binned_data["Q", Q_index] + x = data.coords["energy"].values y = data.values var = data.variances return x, y, var @@ -462,7 +455,7 @@ def _extract_x_y_weights_only_finite( var = var[mask] if np.any(var == 0): - raise ValueError('Cannot compute weights: some variances are zero.') + raise ValueError("Cannot compute weights: some variances are zero.") weights = 1.0 / var**0.5 @@ -479,15 +472,15 @@ def __repr__(self) -> str: str: A string representation of the Experiment object. """ - return f'Experiment `{self.unique_name}` with data: {self._data}' + return f"Experiment `{self.unique_name}` with data: {self._data}" - def __copy__(self) -> 'Experiment': + def __copy__(self) -> "Experiment": """Return a copy of the object. Returns: Experiment: A copy of the Experiment object. """ - temp = self.to_dict(skip=['unique_name']) + temp = self.to_dict(skip=["unique_name"]) new_obj = self.__class__.from_dict(temp) new_obj.data = self.data.copy() if self.data is not None else None return new_obj From 85699507a599bbf94ed55bb0c0996dfd997340fd Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Sun, 15 Mar 2026 20:42:51 +0100 Subject: [PATCH 25/31] numerical convolution base --- .../convolution/numerical_convolution_base.py | 158 +++++++----------- 1 file changed, 63 insertions(+), 95 deletions(-) diff --git a/src/easydynamics/convolution/numerical_convolution_base.py b/src/easydynamics/convolution/numerical_convolution_base.py index ad4dffa77..46abef465 100644 --- a/src/easydynamics/convolution/numerical_convolution_base.py +++ b/src/easydynamics/convolution/numerical_convolution_base.py @@ -32,46 +32,6 @@ class NumericalConvolutionBase(ConvolutionBase): models. Provides methods to handle upsampling, extension, and detailed balance correction. This base class has no convolution functionality. - - Args: - energy (np.ndarray | sc.Variable): 1D array of energy values - where the convolution is evaluated. - sample_components (ComponentCollection | ModelComponent): The - components to be convolved. - resolution_components (ComponentCollection | ModelComponent): - The resolution components to convolve with. - upsample_factor (int | None): The factor by which to upsample - the input data before convolution. Default is 5. - extension_factor (float | None): The factor by which to extend - the input data range before convolution. Default is 0.2. - temperature (Parameter | float | None): The temperature to use - for detailed balance correction. Default is None. - temperature_unit (str | sc.Unit): The unit of the temperature - parameter. Default is 'K'. - energy_unit (str | sc.Unit): The unit of the energy. Default is - 'meV'. - normalize_detailed_balance (bool): Whether to normalize the - detailed balance correction. Default is True. - - Attributes: - energy (np.ndarray | sc.Variable): 1D array of energy values - where the convolution is evaluated. - sample_components (ComponentCollection | ModelComponent): The - components to be convolved. - resolution_components (ComponentCollection | ModelComponent): - The resolution components to convolve with. - upsample_factor (int | None): The factor by which to upsample - the input data before convolution. - extension_factor (float | None): The factor by which to extend - the input data range before convolution. - temperature (Parameter | None): The temperature parameter for - detailed balance correction, or None if detailed balance is - disabled. - temperature_unit (str | sc.Unit): The unit of the temperature - parameter. - energy_unit (str | sc.Unit): The unit of the energy. - normalize_detailed_balance (bool): Whether to normalize the - detailed balance correction. """ def __init__( @@ -80,11 +40,11 @@ def __init__( sample_components: ComponentCollection | ModelComponent, resolution_components: ComponentCollection | ModelComponent, energy_offset: Numerical | Parameter = 0.0, - upsample_factor: Numerical = 5, - extension_factor: float = 0.2, + upsample_factor: Numerical | None = 5, + extension_factor: float | None = 0.2, temperature: Parameter | float | None = None, - temperature_unit: str | sc.Unit = 'K', - energy_unit: str | sc.Unit = 'meV', + temperature_unit: str | sc.Unit = "K", + energy_unit: str | sc.Unit = "meV", normalize_detailed_balance: bool = True, ) -> None: """Initialize the NumericalConvolutionBase. @@ -96,30 +56,25 @@ def __init__( The components to be convolved. resolution_components (ComponentCollection | ModelComponent): The resolution components to convolve with. - upsample_factor (int | None): The factor by which to - upsample the input data before convolution. Default is - 5. - extension_factor (float | None): The factor by which to - extend the input data range before convolution. Default - is 0.2. - temperature (Parameter | float | None): The temperature to - use for detailed balance correction. Default is None. - temperature_unit (str | sc.Unit): The unit of the - temperature parameter. Default is 'K'. - energy_unit (str | sc.Unit): The unit of the energy. Default - is 'meV'. - normalize_detailed_balance (bool): Whether to normalize the - detailed balance correction. Default is True. + energy_offset (Numerical | Parameter, default=0.0): An energy + offset to apply to the energy values before convolution. + upsample_factor (Numerical | None, default=5): The factor by which to + upsample the input data before convolution. + extension_factor (float | None, default=0.2): The factor by which to + extend the input data range before convolution. + temperature (Parameter | float | None, default=None): The temperature to + use for detailed balance correction. + temperature_unit (str | sc.Unit, default='K'): The unit of the + temperature parameter. + energy_unit (str | sc.Unit, default='meV'): The unit of the energy. + normalize_detailed_balance (bool, default=True): Whether to normalize the + detailed balance correction. Raises: TypeError: If temperature is not None, a number, or a - Parameter. - TypeError: If temperature_unit is not a string or sc.Unit. - TypeError: If upsample_factor is not a number or None. - ValueError: If upsample_factor is not greater than 1. - TypeError: If extension_factor is not a number. - ValueError: If extension_factor is negative. - TypeError: If normalize_detailed_balance is not a bool. + Parameter, or if temperature_unit is not a string or sc.Unit, or if + upsample_factor is not a number or None, or if extension_factor + is not a number, or if normalize_detailed_balance is not a bool. """ super().__init__( energy=energy, @@ -129,11 +84,13 @@ def __init__( energy_offset=energy_offset, ) - if temperature is not None and not isinstance(temperature, (Numerical, Parameter)): - raise TypeError('Temperature must be None, a number or a Parameter.') + if temperature is not None and not isinstance( + temperature, (Numerical, Parameter) + ): + raise TypeError("Temperature must be None, a number or a Parameter.") if not isinstance(temperature_unit, (str, sc.Unit)): - raise TypeError('Temperature_unit must be a string or sc.Unit.') + raise TypeError("Temperature_unit must be a string or sc.Unit.") self._temperature_unit = temperature_unit self._temperature = None self.temperature = temperature @@ -186,10 +143,10 @@ def upsample_factor(self, factor: Numerical) -> None: return if not isinstance(factor, Numerical): - raise TypeError('Upsample factor must be a numerical value or None.') + raise TypeError("Upsample factor must be a numerical value or None.") factor = float(factor) if factor <= 1.0: - raise ValueError('Upsample factor must be greater than 1.') + raise ValueError("Upsample factor must be greater than 1.") self._upsample_factor = factor @@ -225,14 +182,15 @@ def extension_factor(self, factor: Numerical) -> None: Raises: TypeError: If factor is not a number. + ValueError: If factor is negative. """ if not isinstance(factor, Numerical): - raise TypeError('Extension factor must be a number.') + raise TypeError("Extension factor must be a number.") if factor < 0.0: - raise ValueError('Extension factor must be non-negative.') + raise ValueError("Extension factor must be non-negative.") - self._extension_factor = factor + self._extension_factor = float(factor) # Recreate dense grid when extension factor is updated self._energy_grid = self._create_energy_grid() @@ -270,7 +228,7 @@ def temperature(self, temp: Parameter | float | None) -> None: self._temperature.value = float(temp) else: self._temperature = Parameter( - name='temperature', + name="temperature", value=float(temp), unit=self._temperature_unit, fixed=True, @@ -278,7 +236,7 @@ def temperature(self, temp: Parameter | float | None) -> None: elif isinstance(temp, Parameter): self._temperature = temp else: - raise TypeError('Temperature must be None, a float or a Parameter.') + raise TypeError("Temperature must be None, a float or a Parameter.") @property def normalize_detailed_balance(self) -> bool: @@ -300,14 +258,14 @@ def normalize_detailed_balance(self, normalize: bool) -> None: Args: normalize (bool): Whether to normalize the detailed balance - factor. + factor. Raises: TypeError: If normalize is not a bool. """ if not isinstance(normalize, bool): - raise TypeError('normalize_detailed_balance must be True or False.') + raise TypeError("normalize_detailed_balance must be True or False.") self._normalize_detailed_balance = normalize @@ -324,6 +282,10 @@ def _create_energy_grid( Returns: EnergyGrid: The dense grid created by upsampling and extending energy. + + Raises: + ValueError: If energy array is not uniformly spaced when + upsample_factor is None, or if energy array has less than 2 points. """ if self.upsample_factor is None: # Check if the array is uniformly spaced. @@ -331,7 +293,7 @@ def _create_energy_grid( is_uniform = np.allclose(energy_diff, energy_diff[0]) if not is_uniform: raise ValueError( - 'Input array `energy` must be uniformly spaced if upsample_factor is not given.' # noqa: E501 + "Input array `energy` must be uniformly spaced if upsample_factor is not given." # noqa: E501 ) energy_dense = self.energy.values @@ -348,7 +310,7 @@ def _create_energy_grid( energy_span_dense = extended_max - extended_min if len(energy_dense) < 2: - raise ValueError('Energy array must have at least two points.') + raise ValueError("Energy array must have at least two points.") energy_dense_step = energy_dense[1] - energy_dense[0] # Handle offset for even length of energy_dense in convolution. @@ -412,23 +374,29 @@ def _check_width_thresholds( components = [model] # Treat single ModelComponent as a list for comp in components: - if hasattr(comp, 'width'): - if comp.width.value > LARGE_WIDTH_THRESHOLD * self._energy_grid.energy_span_dense: + if hasattr(comp, "width"): + if ( + comp.width.value + > LARGE_WIDTH_THRESHOLD * self._energy_grid.energy_span_dense + ): warnings.warn( f"The width of the {model_name} component '{comp.unique_name}' \ ({comp.width.value}) is large compared to the span of the input " - f'array ({self._energy_grid.energy_span_dense}). \ + f"array ({self._energy_grid.energy_span_dense}). \ This may lead to inaccuracies in the convolution. \ - Increase extension_factor to improve accuracy.', + Increase extension_factor to improve accuracy.", UserWarning, ) - if comp.width.value < SMALL_WIDTH_THRESHOLD * self._energy_grid.energy_dense_step: + if ( + comp.width.value + < SMALL_WIDTH_THRESHOLD * self._energy_grid.energy_dense_step + ): warnings.warn( f"The width of the {model_name} component '{comp.unique_name}' \ ({comp.width.value}) is small compared to the spacing of the input " - f'array ({self._energy_grid.energy_dense_step}). \ + f"array ({self._energy_grid.energy_dense_step}). \ This may lead to inaccuracies in the convolution. \ - Increase upsample_factor to improve accuracy.', + Increase upsample_factor to improve accuracy.", UserWarning, ) @@ -441,13 +409,13 @@ def __repr__(self) -> str: NumericalConvolutionBase. """ return ( - f'{self.__class__.__name__}(' - f'energy=array of shape {self.energy.values.shape},\n ' - f'sample_components={repr(self.sample_components)}, \n' - f'resolution_components={repr(self.resolution_components)},\n ' - f'energy_unit={self._energy_unit}, ' - f'upsample_factor={self.upsample_factor}, ' - f'extension_factor={self.extension_factor}, ' - f'temperature={self.temperature}, ' - f'normalize_detailed_balance={self.normalize_detailed_balance})' + f"{self.__class__.__name__}(" + f"energy=array of shape {self.energy.values.shape},\n " + f"sample_components={repr(self.sample_components)}, \n" + f"resolution_components={repr(self.resolution_components)},\n " + f"energy_unit={self._energy_unit}, " + f"upsample_factor={self.upsample_factor}, " + f"extension_factor={self.extension_factor}, " + f"temperature={self.temperature}, " + f"normalize_detailed_balance={self.normalize_detailed_balance})" ) From b3fb3ad8e9b2f4d0ef0e5baa31ec4214b205b25e Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Sun, 15 Mar 2026 20:52:25 +0100 Subject: [PATCH 26/31] numerical convolution --- .../convolution/numerical_convolution.py | 96 +++++-------------- .../convolution/numerical_convolution_base.py | 45 +++++---- 2 files changed, 44 insertions(+), 97 deletions(-) diff --git a/src/easydynamics/convolution/numerical_convolution.py b/src/easydynamics/convolution/numerical_convolution.py index 27c10b2b3..bad409221 100644 --- a/src/easydynamics/convolution/numerical_convolution.py +++ b/src/easydynamics/convolution/numerical_convolution.py @@ -9,7 +9,9 @@ from easydynamics.convolution.numerical_convolution_base import NumericalConvolutionBase from easydynamics.sample_model.component_collection import ComponentCollection from easydynamics.sample_model.components.model_component import ModelComponent -from easydynamics.utils.detailed_balance import _detailed_balance_factor as detailed_balance_factor +from easydynamics.utils.detailed_balance import ( + _detailed_balance_factor as detailed_balance_factor, +) from easydynamics.utils.utils import Numeric @@ -19,49 +21,6 @@ class NumericalConvolution(NumericalConvolutionBase): extended range to improve accuracy. Warns about very wide or very narrow peaks in the models. If temperature is provided, detailed balance correction is applied to the sample model. - - Args: - energy (np.ndarray | sc.Variable): 1D array of energy values - where the convolution is evaluated. - sample_components (ComponentCollection | ModelComponent): The - sample model to be convolved. - resolution_components (ComponentCollection | ModelComponent): - The resolution model to convolve with. - upsample_factor (int, optional): The factor by which to upsample - the input data before convolution. Default is 5. - extension_factor (float, optional): The factor by which to - extend the input data range before convolution. Default is - 0.2. - temperature (Parameter | float | None, optional): The - temperature to use for detailed balance correction. Default - is None. - temperature_unit (str | sc.Unit, optional): The unit of the - temperature parameter. Default is 'K'. - energy_unit (str | sc.Unit, optional): The unit of the energy. - Default is 'meV'. - normalize_detailed_balance (bool, optional): Whether to - normalize the detailed balance correction. Default is True. - - Attributes: - energy (np.ndarray | sc.Variable): The energy values where the - convolution is evaluated. - sample_components (ComponentCollection | ModelComponent): The - sample model to be convolved. - resolution_components (ComponentCollection | ModelComponent): - The resolution model to convolve with. - energy_offset (Parameter): The energy offset to apply to the - sample model before convolution. - upsample_factor (int): The factor by which to upsample the input - data before convolution. - extension_factor (float): The factor by which to extend the - input data range before convolution. - temperature (Parameter | float | None): The temperature to use - for detailed balance correction. - temperature_unit (str | sc.Unit): The unit of the temperature - parameter. - energy_unit (str | sc.Unit): The unit of the energy. - normalize_detailed_balance (bool): Whether to normalize the - detailed balance correction. """ def __init__( @@ -70,11 +29,11 @@ def __init__( sample_components: ComponentCollection | ModelComponent, resolution_components: ComponentCollection | ModelComponent, energy_offset: Numeric | Parameter = 0.0, - upsample_factor: Numeric = 5, - extension_factor: Numeric = 0.2, + upsample_factor: Numeric | None = 5, + extension_factor: Numeric | None = 0.2, temperature: Parameter | Numeric | None = None, - temperature_unit: str | sc.Unit = 'K', - energy_unit: str | sc.Unit = 'meV', + temperature_unit: str | sc.Unit = "K", + energy_unit: str | sc.Unit = "meV", normalize_detailed_balance: bool = True, ) -> None: """Initialize the NumericalConvolution object. @@ -86,32 +45,22 @@ def __init__( The sample model to be convolved. resolution_components (ComponentCollection | ModelComponent): The resolution model to convolve with. - upsample_factor (int, optional): The factor by which to - upsample the input data before convolution. Default is - 5. - extension_factor (float, optional): The factor by which to - extend the input data range before convolution. Default - is 0.2. - temperature (Parameter | float | None, optional): The + energy_offset (Numeric | Parameter, default=0.0): An energy + offset to apply to the energy values before convolution. + upsample_factor (Numeric | None, default=5): The factor by which to + upsample the input data before convolution. + extension_factor (Numeric | None, default=0.2): The factor by which to + extend the input data range before convolution. + temperature (Parameter | Numeric | None, default=None): The temperature to use for detailed balance correction. Default is None. - temperature_unit (str | sc.Unit, optional): The unit of the - temperature parameter. Default is 'K'. - energy_unit (str | sc.Unit, optional): The unit of the + temperature_unit (str | sc.Unit, default='K'): The unit of the + temperature parameter. + energy_unit (str | sc.Unit, default='meV'): The unit of the energy. Default is 'meV'. - normalize_detailed_balance (bool, optional): Whether to + normalize_detailed_balance (bool, default=True): Whether to normalize the detailed balance correction. Default is True. - - Raises: - TypeError: If temperature is not None, a number, or a - Parameter. - TypeError: If temperature_unit is not a string or sc.Unit. - TypeError: If upsample_factor is not a number or None. - ValueError: If upsample_factor is not greater than 1. - TypeError: If extension_factor is not a number. - ValueError: If extension_factor is negative. - TypeError: If normalize_detailed_balance is not a bool. """ super().__init__( energy=energy, @@ -134,18 +83,17 @@ def convolution( correction if temperature is provided. Returns: - np.ndarray - The convolved values evaluated at energy. + np.ndarray: The convolved values evaluated at energy. """ # Give warnings if peaks are very wide or very narrow self._check_width_thresholds( model=self.sample_components, - model_name='sample model', + model_name="sample model", ) self._check_width_thresholds( model=self.resolution_components, - model_name='resolution model', + model_name="resolution model", ) # Evaluate sample model. If called via the Convolution class, @@ -172,7 +120,7 @@ def convolution( ) # Convolution - convolved = fftconvolve(sample_vals, resolution_vals, mode='same') + convolved = fftconvolve(sample_vals, resolution_vals, mode="same") convolved *= self._energy_grid.energy_dense_step # normalize if self.upsample_factor is not None: diff --git a/src/easydynamics/convolution/numerical_convolution_base.py b/src/easydynamics/convolution/numerical_convolution_base.py index 46abef465..9c5ffda24 100644 --- a/src/easydynamics/convolution/numerical_convolution_base.py +++ b/src/easydynamics/convolution/numerical_convolution_base.py @@ -14,8 +14,7 @@ from easydynamics.convolution.energy_grid import EnergyGrid from easydynamics.sample_model.component_collection import ComponentCollection from easydynamics.sample_model.components.model_component import ModelComponent - -Numerical = float | int +from easydynamics.utils.utils import Numeric # The thresholds are illustrated in # performance_tests/convolution/convolution_width_thresholds.ipynb @@ -39,10 +38,10 @@ def __init__( energy: np.ndarray | sc.Variable, sample_components: ComponentCollection | ModelComponent, resolution_components: ComponentCollection | ModelComponent, - energy_offset: Numerical | Parameter = 0.0, - upsample_factor: Numerical | None = 5, - extension_factor: float | None = 0.2, - temperature: Parameter | float | None = None, + energy_offset: Numeric | Parameter = 0.0, + upsample_factor: Numeric | None = 5, + extension_factor: Numeric | None = 0.2, + temperature: Parameter | Numeric | None = None, temperature_unit: str | sc.Unit = "K", energy_unit: str | sc.Unit = "meV", normalize_detailed_balance: bool = True, @@ -56,13 +55,13 @@ def __init__( The components to be convolved. resolution_components (ComponentCollection | ModelComponent): The resolution components to convolve with. - energy_offset (Numerical | Parameter, default=0.0): An energy + energy_offset (Numeric | Parameter, default=0.0): An energy offset to apply to the energy values before convolution. - upsample_factor (Numerical | None, default=5): The factor by which to + upsample_factor (Numeric | None, default=5): The factor by which to upsample the input data before convolution. - extension_factor (float | None, default=0.2): The factor by which to + extension_factor (Numeric | None, default=0.2): The factor by which to extend the input data range before convolution. - temperature (Parameter | float | None, default=None): The temperature to + temperature (Parameter | Numeric | None, default=None): The temperature to use for detailed balance correction. temperature_unit (str | sc.Unit, default='K'): The unit of the temperature parameter. @@ -85,7 +84,7 @@ def __init__( ) if temperature is not None and not isinstance( - temperature, (Numerical, Parameter) + temperature, (Numeric, Parameter) ): raise TypeError("Temperature must be None, a number or a Parameter.") @@ -117,21 +116,21 @@ def energy(self, energy: np.ndarray) -> None: self._energy_grid = self._create_energy_grid() @property - def upsample_factor(self) -> Numerical: + def upsample_factor(self) -> Numeric | None: """Get the upsample factor. Returns: - Numerical: The upsample factor. + Numeric | None: The upsample factor. """ return self._upsample_factor @upsample_factor.setter - def upsample_factor(self, factor: Numerical) -> None: + def upsample_factor(self, factor: Numeric | None) -> None: """Set the upsample factor and recreate the dense grid. Args: - factor (Numerical): The new upsample factor. + factor (Numeric | None): The new upsample factor. Raises: TypeError: If factor is not a number or None. @@ -142,7 +141,7 @@ def upsample_factor(self, factor: Numerical) -> None: self._energy_grid = self._create_energy_grid() return - if not isinstance(factor, Numerical): + if not isinstance(factor, Numeric): raise TypeError("Upsample factor must be a numerical value or None.") factor = float(factor) if factor <= 1.0: @@ -169,7 +168,7 @@ def extension_factor(self) -> float: return self._extension_factor @extension_factor.setter - def extension_factor(self, factor: Numerical) -> None: + def extension_factor(self, factor: Numeric) -> None: """ Set the extension factor and recreate the dense grid. The extension factor determines how much the energy range is @@ -178,14 +177,14 @@ def extension_factor(self, factor: Numerical) -> None: on each side. Args: - factor (Numerical): The new extension factor. + factor (Numeric): The new extension factor. Raises: TypeError: If factor is not a number. ValueError: If factor is negative. """ - if not isinstance(factor, Numerical): + if not isinstance(factor, Numeric): raise TypeError("Extension factor must be a number.") if factor < 0.0: raise ValueError("Extension factor must be non-negative.") @@ -206,24 +205,24 @@ def temperature(self) -> Optional[Parameter]: return self._temperature @temperature.setter - def temperature(self, temp: Parameter | float | None) -> None: + def temperature(self, temp: Parameter | Numeric | None) -> None: """Set the temperature. If None, disables detailed balance correction and removes the temperature parameter. Args: - temp (Parameter | float | None): The temperature to set. + temp (Parameter | Numeric | None): The temperature to set. The unit will be the same as the existing temperature parameter if it exists, otherwise 'K'. Raises: - TypeError: If temp is not a float, Parameter, or None. + TypeError: If temp is not a Numeric, Parameter, or None. """ if temp is None: self._temperature = None - elif isinstance(temp, Numerical): + elif isinstance(temp, Numeric): if self._temperature is not None: self._temperature.value = float(temp) else: From 7287387d235eaa466c1133a233e2cc7d28aa59c7 Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Mon, 16 Mar 2026 11:29:08 +0100 Subject: [PATCH 27/31] docstring lint done --- src/easydynamics/analysis/analysis.py | 197 ++++++++------- src/easydynamics/analysis/analysis1d.py | 228 +++++++++--------- src/easydynamics/analysis/analysis_base.py | 141 ++++++----- .../convolution/analytical_convolution.py | 67 +++-- src/easydynamics/convolution/convolution.py | 116 ++++----- .../convolution/convolution_base.py | 129 +++++----- 6 files changed, 407 insertions(+), 471 deletions(-) diff --git a/src/easydynamics/analysis/analysis.py b/src/easydynamics/analysis/analysis.py index 4bad97757..d0f80be82 100644 --- a/src/easydynamics/analysis/analysis.py +++ b/src/easydynamics/analysis/analysis.py @@ -1,6 +1,8 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +from typing import Any + import numpy as np import scipp as sc from easyscience.fitting.minimizers.utils import FitResults @@ -21,43 +23,11 @@ class Analysis(AnalysisBase): """For analysing two-dimensional data, i.e. intensity as function of energy and Q. Supports independent fits of each Q value and simultaneous fits of all Q. - - Args: - display_name (str): Display name of the analysis. - unique_name (str or None): Unique name of the analysis. If None, - a unique name is automatically generated. - experiment (Experiment | None): The Experiment associated with - this Analysis. If None, a default Experiment is created. - sample_model (SampleModel | None): The SampleModel associated - with this Analysis. If None, a default SampleModel is - created. - instrument_model (InstrumentModel | None): The InstrumentModel - associated with this Analysis. If None, a default - InstrumentModel is created. - extra_parameters (Parameter | list[Parameter] | None): Extra - parameters to be included in the analysis for advanced - users. If None, no extra parameters are added. - - Attributes: - experiment (Experiment): The Experiment associated with this - Analysis. - sample_model (SampleModel): The SampleModel associated with this - Analysis. - instrument_model (InstrumentModel): The InstrumentModel - associated with this Analysis. - Q (sc.Variable | None): The Q values from the associated - Experiment, if available. - energy (sc.Variable | None): The energy values from the - associated Experiment, if available. - temperature (Parameter | None): The temperature from the - associated SampleModel, if available. - extra_parameters (list[Parameter]): The extra parameters - included in this Analysis. """ def __init__( self, - display_name: str = 'MyAnalysis', + display_name: str | None = "MyAnalysis", unique_name: str | None = None, experiment: Experiment | None = None, sample_model: SampleModel | None = None, @@ -67,19 +37,19 @@ def __init__( """Initialize an Analysis object. Args: - display_name (str): Display name of the analysis. - unique_name (str or None): Unique name of the analysis. If + display_name (str | None, default='MyAnalysis'): Display name of the analysis. + unique_name (str | None, default=None): Unique name of the analysis. If None, a unique name is automatically generated. - experiment (Experiment | None): The Experiment associated + experiment (Experiment | None, default=None): The Experiment associated with this Analysis. If None, a default Experiment is created. - sample_model (SampleModel | None): The SampleModel + sample_model (SampleModel | None, default=None): The SampleModel associated with this Analysis. If None, a default SampleModel is created. - instrument_model (InstrumentModel | None): The + instrument_model (InstrumentModel | None, default=None): The InstrumentModel associated with this Analysis. If None, a default InstrumentModel is created. - extra_parameters (Parameter | list[Parameter] | None): Extra + extra_parameters (Parameter | list[Parameter] | None, default=None): Extra parameters to be included in the analysis for advanced users. If None, no extra parameters are added. """ @@ -100,8 +70,8 @@ def __init__( if self.Q is not None: for Q_index in range(len(self.Q)): analysis = Analysis1d( - display_name=f'{self.display_name}_Q{Q_index}', - unique_name=(f'{self.unique_name}_Q{Q_index}'), + display_name=f"{self.display_name}_Q{Q_index}", + unique_name=(f"{self.unique_name}_Q{Q_index}"), experiment=self.experiment, sample_model=self.sample_model, instrument_model=self.instrument_model, @@ -133,15 +103,19 @@ def analysis_list(self, value: list[Analysis1d]) -> None: To change the analysis list, modify the experiment, sample model, or instrument model. + Args: + value (list[Analysis1d]): The new list of Analysis1d objects. This + argument is ignored, as analysis_list is read-only. + Raises: AttributeError: Always raised, since analysis_list is read-only. """ raise AttributeError( - 'analysis_list is read-only. ' - 'To change the analysis list, modify the experiment, sample model, ' - 'or instrument model.' + "analysis_list is read-only. " + "To change the analysis list, modify the experiment, sample model, " + "or instrument model." ) ############# @@ -156,71 +130,73 @@ def calculate( None, calculate for all Q indices and return a list of arrays. Args: - Q_index (int or None): Index of the Q value to calculate + Q_index (int | None, default=None): Index of the Q value to calculate for. If None, calculate for all Q values. + energy (sc.Variable | None, default=None): The energy values to use for + calculating the model. If None, uses the energy from the + experiment. Returns: list[np.ndarray] | np.ndarray: If Q_index is None, returns a list of numpy arrays, one for each Q index. If Q_index is an integer, returns a single numpy array for that Q index. - - Raises: - IndexError: If Q_index is not None and is out of bounds. """ if energy is None: energy = self.energy if Q_index is None: - return [analysis.calculate(energy=energy) for analysis in self.analysis_list] + return [ + analysis.calculate(energy=energy) for analysis in self.analysis_list + ] Q_index = self._verify_Q_index(Q_index) return self.analysis_list[Q_index].calculate(energy=energy) def fit( self, - fit_method: str = 'independent', + fit_method: str = "independent", Q_index: int | None = None, ) -> FitResults | list[FitResults]: """Fit the model to the experimental data. Args: - fit_method (str): Method to use for fitting. Options are + fit_method (str, default="independent"): Method to use for fitting. Options are "independent" (fit each Q index independently, one after the other) or "simultaneous" (fit all Q indices simultaneously). Default is "independent". - Q_index (int or None): If fit_method is "independent", + Q_index (int | None, default=None): If fit_method is "independent", specify which Q index to fit. If None, fit all Q indices independently. Ignored if fit_method is "simultaneous". Default is None. Returns: - FitResults: a list of FitResults if fitting independently, + FitResults | list[FitResults]: a list of FitResults if fitting independently, or a single FitResults object if fitting simultaneously. Raises: ValueError: If fit_method is not "independent" or - "simultaneous" - IndexError: If fit_method is "independent" and Q_index is - out of bounds. + "simultaneous" or if there are no Q values available for fitting. """ if self.Q is None: raise ValueError( - 'No Q values available for fitting. Please check the experiment data.' + "No Q values available for fitting. Please check the experiment data." ) Q_index = self._verify_Q_index(Q_index) - if fit_method == 'independent': + if fit_method == "independent": if Q_index is not None: return self._fit_single_Q(Q_index) else: return self._fit_all_Q_independently() - elif fit_method == 'simultaneous': + elif fit_method == "simultaneous": return self._fit_all_Q_simultaneously() else: - raise ValueError("Invalid fit method. Choose 'independent' or 'simultaneous'.") + raise ValueError( + "Invalid fit method. Choose 'independent' or 'simultaneous'." + ) def plot_data_and_model( self, @@ -228,7 +204,7 @@ def plot_data_and_model( plot_components: bool = True, add_background: bool = True, energy: sc.Variable | None = None, - **kwargs, + **kwargs: dict[str, Any], ) -> InteractiveFigure: """Plot the experimental data and the model prediction. Optionally also plot the individual components of the model. @@ -236,14 +212,17 @@ def plot_data_and_model( Uses Plopp for plotting: https://scipp.github.io/plopp/ Args: - Q_index (int or None): Index of the Q value to plot. If + Q_index (int | None, default=None): Index of the Q value to plot. If None, plot all Q values. Default is None. - plot_components (bool): Whether to plot the individual + plot_components (bool, default=True): Whether to plot the individual components. Default is True. - add_background (bool): Whether to add background components + add_background (bool, default=True): Whether to add background components to the sample model components when plotting. Default is True. - **kwargs (Any): Additional keyword arguments passed to plopp + energy (sc.Variable | None, default=None): The energy values to use for + calculating the model. If None, uses the energy from the + experiment. + **kwargs (dict[str, Any]): Additional keyword arguments passed to plopp for customizing the plot. Raises: @@ -269,21 +248,23 @@ def plot_data_and_model( ) if self.experiment.binned_data is None: - raise ValueError('No data to plot. Please load data first.') + raise ValueError("No data to plot. Please load data first.") if not _in_notebook(): - raise RuntimeError('plot_data() can only be used in a Jupyter notebook environment.') + raise RuntimeError( + "plot_data() can only be used in a Jupyter notebook environment." + ) if self.Q is None: raise ValueError( - 'No Q values available for plotting. Please check the experiment data.' + "No Q values available for plotting. Please check the experiment data." ) if not isinstance(plot_components, bool): - raise TypeError('plot_components must be True or False.') + raise TypeError("plot_components must be True or False.") if not isinstance(add_background, bool): - raise TypeError('add_background must be True or False.') + raise TypeError("add_background must be True or False.") if energy is None: energy = self.energy @@ -291,16 +272,16 @@ def plot_data_and_model( import plopp as pp plot_kwargs_defaults = { - 'title': self.display_name, - 'linestyle': {'Data': 'none', 'Model': '-'}, - 'marker': {'Data': 'o', 'Model': None}, - 'color': {'Data': 'black', 'Model': 'red'}, - 'markerfacecolor': {'Data': 'none', 'Model': 'none'}, - 'keep': 'energy', + "title": self.display_name, + "linestyle": {"Data": "none", "Model": "-"}, + "marker": {"Data": "o", "Model": None}, + "color": {"Data": "black", "Model": "red"}, + "markerfacecolor": {"Data": "none", "Model": "none"}, + "keep": "energy", } data_and_model = { - 'Data': self.experiment.binned_data, - 'Model': self._create_model_array(energy=energy), + "Data": self.experiment.binned_data, + "Model": self._create_model_array(energy=energy), } if plot_components: @@ -309,8 +290,8 @@ def plot_data_and_model( ) for key in components.keys(): data_and_model[key] = components[key] - plot_kwargs_defaults['linestyle'][key] = '--' - plot_kwargs_defaults['marker'][key] = None + plot_kwargs_defaults["linestyle"][key] = "--" + plot_kwargs_defaults["marker"][key] = None # Overwrite defaults with any user-provided kwargs plot_kwargs_defaults.update(kwargs) @@ -329,15 +310,15 @@ def parameters_to_dataset(self) -> sc.Dataset: Returns: sc.Dataset: A dataset where each entry is a parameter, with - dimensions "Q" and values corresponding to the parameter - values. + dimensions "Q" and values corresponding to the parameter + values. Raises: UnitError: If there are inconsistent units for the same parameter across different Q values. """ - ds = sc.Dataset(coords={'Q': self.Q}) + ds = sc.Dataset(coords={"Q": self.Q}) # Collect all parameter names all_names = { @@ -367,7 +348,7 @@ def parameters_to_dataset(self) -> sc.Dataset: except Exception as e: raise UnitError( f"Inconsistent units for parameter '{name}': " - f'{units[name]} vs {p.unit}' + f"{units[name]} vs {p.unit}" ) from e values[name].append(p.value) @@ -379,7 +360,7 @@ def parameters_to_dataset(self) -> sc.Dataset: # Build dataset variables for name in all_names: ds[name] = sc.Variable( - dims=['Q'], + dims=["Q"], values=np.asarray(values[name], dtype=float), variances=np.asarray(variances[name], dtype=float), unit=units.get(name, None), @@ -390,20 +371,24 @@ def parameters_to_dataset(self) -> sc.Dataset: def plot_parameters( self, names: str | list[str] | None = None, - **kwargs, + **kwargs: dict[str, Any], ) -> InteractiveFigure: """Plot fitted parameters as a function of Q. Args: - names (str | list[str] | None): Name(s) of the parameter(s) + names (str | list[str] | None, default=None): Name(s) of the parameter(s) to plot. If None, plots all parameters. - kwargs (Any): Additional keyword arguments passed to + **kwargs (dict[str, Any]): Additional keyword arguments passed to plopp.slicer for customizing the plot (e.g., title, linestyle, marker, color). Returns: InteractiveFigure: A Plopp InteractiveFigure containing the plot of the parameters. + + Raises: + TypeError: If names is not a string, list of strings, or None. + ValueError: If any of the specified parameter names are not found in the dataset. """ ds = self.parameters_to_dataset() @@ -414,8 +399,10 @@ def plot_parameters( if isinstance(names, str): names = [names] - if not isinstance(names, list) or not all(isinstance(name, str) for name in names): - raise TypeError('names must be a string or a list of strings.') + if not isinstance(names, list) or not all( + isinstance(name, str) for name in names + ): + raise TypeError("names must be a string or a list of strings.") for name in names: if name not in ds: @@ -423,9 +410,9 @@ def plot_parameters( data_to_plot = {name: ds[name] for name in names} plot_kwargs_defaults = { - 'linestyle': {name: 'none' for name in names}, - 'marker': {name: 'o' for name in names}, - 'markerfacecolor': {name: 'none' for name in names}, + "linestyle": {name: "none" for name in names}, + "marker": {name: "o" for name in names}, + "markerfacecolor": {name: "none" for name in names}, } plot_kwargs_defaults.update(kwargs) @@ -517,7 +504,9 @@ def _fit_all_Q_simultaneously(self) -> FitResults: ws = [] for analysis in self.analysis_list: - x, y, weight, _ = self.experiment._extract_x_y_weights_only_finite(analysis.Q_index) + x, y, weight, _ = self.experiment._extract_x_y_weights_only_finite( + analysis.Q_index + ) xs.append(x) ys.append(y) ws.append(weight) @@ -550,16 +539,21 @@ def get_fit_functions(self) -> list[callable]: def _create_model_array(self, energy: sc.Variable | None = None) -> sc.DataArray: """Create a scipp array for the model. + Args: + energy (sc.Variable | None, default=None): The energy values to use for + calculating the model. If None, uses the energy from the + experiment. + Returns: sc.DataArray: A DataArray containing the model values, with dimensions "Q" and "energy". """ if energy is None: energy = self.energy - model = sc.array(dims=['Q', 'energy'], values=self.calculate(energy=energy)) + model = sc.array(dims=["Q", "energy"], values=self.calculate(energy=energy)) model_data_array = sc.DataArray( data=model, - coords={'Q': self.Q, 'energy': energy}, + coords={"Q": self.Q, "energy": energy}, ) return model_data_array @@ -572,9 +566,12 @@ def _create_components_dataset( of the model for plotting. Args: - add_background (bool): Whether to add background components + add_background (bool, default=True): Whether to add background components to the sample model components when creating the - dataset. Default is True. + dataset. + energy (sc.Variable | None, default=None): The energy values to use for + calculating the components. If None, uses the energy from + the experiment. Raises: TypeError: If add_background is not True or False. @@ -584,7 +581,7 @@ def _create_components_dataset( of the model, with dimensions "Q". """ if not isinstance(add_background, bool): - raise TypeError('add_background must be True or False.') + raise TypeError("add_background must be True or False.") if energy is None: energy = self.energy @@ -596,7 +593,7 @@ def _create_components_dataset( for analysis in self.analysis_list ] - return sc.concat(datasets, dim='Q') + return sc.concat(datasets, dim="Q") ############# # Dunder methods diff --git a/src/easydynamics/analysis/analysis1d.py b/src/easydynamics/analysis/analysis1d.py index 809b5e343..41bffaee7 100644 --- a/src/easydynamics/analysis/analysis1d.py +++ b/src/easydynamics/analysis/analysis1d.py @@ -1,6 +1,8 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +from typing import Any + import numpy as np import scipp as sc from easyscience.fitting.fitter import Fitter as EasyScienceFitter @@ -22,47 +24,11 @@ class Analysis1d(AnalysisBase): """For analysing one-dimensional data, i.e. intensity as function of energy for a single Q index. Is used primarily in the Analysis class, but can also be used on its own for simpler analyses. - - Args: - display_name (str): Display name of the analysis. - unique_name (str or None): Unique name of the analysis. If None, - a unique name is automatically generated. - experiment (Experiment | None): The Experiment associated with - this Analysis. If None, a default Experiment is created. - sample_model (SampleModel | None): The SampleModel associated - with this Analysis. If None, a default SampleModel is - created. - instrument_model (InstrumentModel | None): The InstrumentModel - associated with this Analysis. If None, a default - InstrumentModel is created. - Q_index (int | None): The Q index to analyze. If None, the - analysis will not be able to calculate or fit until a - Q index is set. - extra_parameters (Parameter | list[Parameter] | None): Extra - parameters to be included in the analysis for advanced - users. If None, no extra parameters are added. - - Attributes: - experiment (Experiment): The Experiment associated with this - Analysis. - sample_model (SampleModel): The SampleModel associated with this - Analysis. - instrument_model (InstrumentModel): The InstrumentModel - associated with this Analysis. - Q (sc.Variable | None): The Q values from the associated - Experiment, if available. - energy (sc.Variable | None): The energy values from the - associated Experiment, if available. - temperature (Parameter | None): The temperature from the - associated SampleModel, if available. - Q_index (int | None): The Q index being analyzed. - extra_parameters (list[Parameter]): The extra parameters - included in this Analysis. """ def __init__( self, - display_name: str = 'MyAnalysis', + display_name: str | None = "MyAnalysis", unique_name: str | None = None, experiment: Experiment | None = None, sample_model: SampleModel | None = None, @@ -73,22 +39,22 @@ def __init__( """Initialize a Analysis1d. Args: - display_name (str): Display name of the analysis. - unique_name (str or None): Unique name of the analysis. If + display_name (str | None, default='MyAnalysis'): Display name of the analysis. + unique_name (str | None, default=None): Unique name of the analysis. If None, a unique name is automatically generated. - experiment (Experiment | None): The Experiment associated + experiment (Experiment | None, default=None): The Experiment associated with this Analysis. If None, a default Experiment is created. - sample_model (SampleModel | None): The SampleModel + sample_model (SampleModel | None, default=None): The SampleModel associated with this Analysis. If None, a default SampleModel is created. - instrument_model (InstrumentModel | None): The + instrument_model (InstrumentModel | None, default=None): The InstrumentModel associated with this Analysis. If None, a default InstrumentModel is created. - Q_index (int | None): The Q index to analyze. If None, the + Q_index (int | None, default=None): The Q index to analyze. If None, the analysis will not be able to calculate or fit until a Q index is set. - extra_parameters (Parameter | list[Parameter] | None): Extra + extra_parameters (Parameter | list[Parameter] | None, default=None): Extra parameters to be included in the analysis for advanced users. If None, no extra parameters are added. """ @@ -134,7 +100,7 @@ def Q_index(self, value: int | None) -> None: """Set the Q index for single Q analysis. Args: - index (int | None): The Q index. + value (int | None): The Q index. """ self._Q_index = self._verify_Q_index(value) @@ -149,7 +115,7 @@ def calculate(self, energy: sc.Variable | None = None) -> np.ndarray: sure the convolver is up to date before calculating. Args: - energy (sc.Variable | None): Optional energy grid to use for + energy (sc.Variable | None, default=None): Optional energy grid to use for calculation. If None, the energy grid from the experiment is used. @@ -166,7 +132,7 @@ def _calculate(self, energy: sc.Variable | None = None) -> np.ndarray: not check if the convolver is up to date. Args: - energy (sc.Variable | None): Optional energy grid to use for + energy (sc.Variable | None, default=None): Optional energy grid to use for calculation. If None, the energy grid from the experiment is used. @@ -191,17 +157,14 @@ def fit(self) -> FitResults: parameter optimization for performance reasons. Returns: - FitResult: The result of the fit. + FitResults: The result of the fit. Raises: ValueError: If no experiment is associated with this Analysis. - - Returns: - FitResults: The result of the fit. """ if self._experiment is None: - raise ValueError('No experiment is associated with this Analysis.') + raise ValueError("No experiment is associated with this Analysis.") # Create convolver once to reuse during fitting self._convolver = self._create_convolver() @@ -220,7 +183,9 @@ def fit(self) -> FitResults: return fit_result - def as_fit_function(self, x=None, **kwargs) -> callable: + def as_fit_function( + self, x: np.ndarray | sc.Variable | None = None, **kwargs: dict[str, Any] + ) -> callable: """Return self._calculate as a fit function. The EasyScience fitter requires x as input, but @@ -229,13 +194,20 @@ def as_fit_function(self, x=None, **kwargs) -> callable: calculated model. Args: - x (Any): Ignored. The energy grid is taken from the - experiment. - kwargs (dict): Ignored. Included for compatibility with the + x (np.ndarray | sc.Variable | None, default=None): Ignored. + The energy grid is taken from the experiment. + **kwargs (dict[str, Any]): Ignored. Included for compatibility with the EasyScience fitter. + + Returns: + callable: A function that can be used as a fit function in the + EasyScience fitter, which returns the calculated model. """ - def fit_function(x, **kwargs): + def fit_function( + x: np.ndarray | sc.Variable | None = None, + **kwargs: dict[str, Any], + ) -> np.ndarray: return self._calculate() return fit_function @@ -260,7 +232,7 @@ def plot_data_and_model( plot_components: bool = True, add_background: bool = True, energy: sc.Variable | None = None, - **kwargs, + **kwargs: dict[str, Any], ) -> InteractiveFigure: """Plot the experimental data and the model prediction for the chosen Q index. Optionally also plot the individual components @@ -269,29 +241,32 @@ def plot_data_and_model( Uses Plopp for plotting: https://scipp.github.io/plopp/ Args: - plot_components (bool): Whether to plot the individual - components of the model. Default is True. - add_background (bool): Whether to add the background to the + plot_components (bool, default=True): Whether to plot the individual + components of the model. + add_background (bool, default=True): Whether to add the background to the model prediction when plotting individual components. - energy (sc.Variable | None): Optional energy grid to use for + energy (sc.Variable | None, default=None): Optional energy grid to use for plotting. If None, the energy grid from the experiment is used. - kwargs (dict): Keyword arguments to pass to the plotting + **kwargs (dict[str, Any]): Keyword arguments to pass to the plotting function. Returns: InteractiveFigure: A plot of the data and model. + + Raises: + ValueError: If no data is available to plot. """ import plopp as pp if self.experiment.data is None: - raise ValueError('No data to plot. Please load data first.') + raise ValueError("No data to plot. Please load data first.") energy = self._verify_energy(energy) if energy is None: energy = self._masked_energy - data = self.experiment.data['Q', self.Q_index] + data = self.experiment.data["Q", self.Q_index] model_array = self._create_sample_scipp_array(energy=energy) component_dataset = self._create_components_dataset_single_Q( @@ -300,24 +275,26 @@ def plot_data_and_model( # Create a dataset containing the data, model, and individual # components for plotting. - data_and_model = sc.Dataset({ - 'Data': data, - 'Model': model_array, - }) + data_and_model = sc.Dataset( + { + "Data": data, + "Model": model_array, + } + ) data_and_model = sc.merge(data_and_model, component_dataset) plot_kwargs_defaults = { - 'title': self.display_name, - 'linestyle': {'Data': 'none', 'Model': '-'}, - 'marker': {'Data': 'o', 'Model': 'none'}, - 'color': {'Data': 'black', 'Model': 'red'}, - 'markerfacecolor': {'Data': 'none', 'Model': 'none'}, + "title": self.display_name, + "linestyle": {"Data": "none", "Model": "-"}, + "marker": {"Data": "o", "Model": "none"}, + "color": {"Data": "black", "Model": "red"}, + "markerfacecolor": {"Data": "none", "Model": "none"}, } if plot_components: for comp_name in component_dataset.keys(): - plot_kwargs_defaults['linestyle'][comp_name] = '--' - plot_kwargs_defaults['marker'][comp_name] = None + plot_kwargs_defaults["linestyle"][comp_name] = "--" + plot_kwargs_defaults["marker"][comp_name] = None # Overwrite defaults with any user-provided kwargs plot_kwargs_defaults.update(kwargs) @@ -343,7 +320,7 @@ def _require_Q_index(self) -> int: ValueError: If the Q index is not set. """ if self._Q_index is None: - raise ValueError('Q_index must be set.') + raise ValueError("Q_index must be set.") return self._Q_index def _on_Q_index_changed(self) -> None: @@ -366,16 +343,21 @@ def _verify_energy(self, energy: sc.Variable | None) -> sc.Variable | None: Returns: sc.Variable | None: The verified energy, or None if no energy is provided. + + Raises: + TypeError: If energy is not a sc.Variable or None. """ if energy is not None and not isinstance(energy, sc.Variable): - raise TypeError(f'Energy must be a sc.Variable or None. Got {type(energy)}.') + raise TypeError( + f"Energy must be a sc.Variable or None. Got {type(energy)}." + ) return energy def _calculate_energy_with_offset( self, energy: sc.Variable, - energy_offset: sc.Variable, + energy_offset: Parameter, ) -> sc.Variable: """Calculate the energy grid with the energy offset applied. @@ -396,8 +378,8 @@ def _calculate_energy_with_offset( energy_offset.convert_unit(str(energy.unit)) except Exception as e: raise sc.UnitError( - f'Energy and energy offset must have compatible units. ' - f'Got {energy.unit} and {energy_offset.unit}.' + f"Energy and energy offset must have compatible units. " + f"Got {energy.unit} and {energy_offset.unit}." ) from e energy_with_offset = energy.copy(deep=True) @@ -431,21 +413,17 @@ def _evaluate_components( Args: components (ComponentCollection | ModelComponent): The components to evaluate. - convolver (Convolution | None): An optional Convolution + convolver (Convolution | None, default=None): An optional Convolution object to use for convolution. If None, a new Convolution object will be created if convolve is True. - convolve (bool): Whether to perform convolution with the + convolve (bool, default=True): Whether to perform convolution with the resolution. Default is True. - energy (sc.Variable | None): Optional energy grid to use for + energy (sc.Variable | None, default=None): Optional energy grid to use for evaluation. If None, the energy grid from the experiment is used. Returns: np.ndarray: The evaluated contribution of the components. - - Raises: - UnitError: If the energy and energy offset have incompatible - units. """ Q_index = self._require_Q_index() @@ -477,7 +455,9 @@ def _evaluate_components( # performance is not important. # We don't create a convolver if the resolution is empty. - resolution = self.instrument_model.resolution_model.get_component_collection(Q_index) + resolution = self.instrument_model.resolution_model.get_component_collection( + Q_index + ) if resolution.is_empty: return components.evaluate(energy_with_offset) @@ -490,13 +470,16 @@ def _evaluate_components( ) return conv.convolution() - def _evaluate_sample(self, energy: sc.Variable | None = None) -> np.ndarray: + def _evaluate_sample( + self, + energy: sc.Variable | None = None, + ) -> np.ndarray: """Evaluate the sample contribution for a given Q index. Assumes that self._convolver is up to date. Args: - energy (sc.Variable | None): Optional energy grid to use for + energy (sc.Variable | None, default=None): Optional energy grid to use for evaluation. If None, the energy grid from the experiment is used. @@ -522,7 +505,7 @@ def _evaluate_sample_component( Args: component (ModelComponent): The sample component to evaluate. - energy (sc.Variable | None): Optional energy grid to use for + energy (sc.Variable | None, default=None): Optional energy grid to use for evaluation. If None, the energy grid from the experiment is used. @@ -540,7 +523,7 @@ def _evaluate_background(self, energy: sc.Variable | None = None) -> np.ndarray: """Evaluate the background contribution for the chosen Q index. Args: - energy (sc.Variable | None): Optional energy grid to use for + energy (sc.Variable | None, default=None): Optional energy grid to use for evaluation. If None, the energy grid from the experiment is used. @@ -548,8 +531,10 @@ def _evaluate_background(self, energy: sc.Variable | None = None) -> np.ndarray: np.ndarray: The evaluated background contribution. """ Q_index = self._require_Q_index() - background_components = self.instrument_model.background_model.get_component_collection( - Q_index=Q_index + background_components = ( + self.instrument_model.background_model.get_component_collection( + Q_index=Q_index + ) ) return self._evaluate_components( components=background_components, @@ -569,7 +554,7 @@ def _evaluate_background_component( Args: component (ModelComponent): The background component to evaluate. - energy (sc.Variable | None): Optional energy grid to use for + energy (sc.Variable | None, default=None): Optional energy grid to use for evaluation. If None, the energy grid from the experiment is used. @@ -593,7 +578,7 @@ def _create_convolver( available, return None. Args: - energy (sc.Variable | None): Optional energy grid to use for + energy (sc.Variable | None, default=None): Optional energy grid to use for convolution. If None, the energy grid from the experiment is used. @@ -610,8 +595,8 @@ def _create_convolver( if sample_components.is_empty: return None - resolution_components = self.instrument_model.resolution_model.get_component_collection( - Q_index + resolution_components = ( + self.instrument_model.resolution_model.get_component_collection(Q_index) ) if resolution_components.is_empty: return None @@ -640,10 +625,10 @@ def _create_component_scipp_array( background if it is not None. Args: - component (ModelComponent): The component to evaluate - background (np.ndarray | None): Optional background to add + component (ModelComponent): The component to evaluate. + background (np.ndarray | None, default=None): Optional background to add to the component. - energy (sc.Variable | None): Optional energy grid to use for + energy (sc.Variable | None, default=None): Optional energy grid to use for evaluation. If None, the energy grid from the experiment is used. @@ -665,7 +650,7 @@ def _create_background_component_scipp_array( Args: component (ModelComponent): The component to evaluate. - energy (sc.Variable | None): Optional energy grid to use for + energy (sc.Variable | None, default=None): Optional energy grid to use for evaluation. If None, the energy grid from the experiment is used. @@ -679,12 +664,14 @@ def _create_background_component_scipp_array( ) return self._to_scipp_array(values=values, energy=energy) - def _create_sample_scipp_array(self, energy: sc.Variable | None = None) -> sc.DataArray: + def _create_sample_scipp_array( + self, energy: sc.Variable | None = None + ) -> sc.DataArray: """Create a scipp DataArray for the full sample model including background. Args: - energy (sc.Variable | None): Optional energy grid to use for + energy (sc.Variable | None, default=None): Optional energy grid to use for evaluation. If None, the energy grid from the experiment is used. @@ -704,8 +691,8 @@ def _create_components_dataset_single_Q( components. Args: - add_background (bool): Whether to add background components. - energy (sc.Variable | None): Optional energy grid to use for + add_background (bool, default=True): Whether to add background components. + energy (sc.Variable | None, default=None): Optional energy grid to use for evaluation. If None, the energy grid from the experiment is used. @@ -718,22 +705,28 @@ def _create_components_dataset_single_Q( Q_index=self.Q_index ).components - background_components = self.instrument_model.background_model.get_component_collection( - Q_index=self.Q_index - ).components + background_components = ( + self.instrument_model.background_model.get_component_collection( + Q_index=self.Q_index + ).components + ) if energy is None: energy = self._masked_energy - background = self._evaluate_background(energy=energy) if add_background else None + background = ( + self._evaluate_background(energy=energy) if add_background else None + ) for component in sample_components: scipp_arrays[component.display_name] = self._create_component_scipp_array( component=component, background=background, energy=energy ) for component in background_components: - scipp_arrays[component.display_name] = self._create_background_component_scipp_array( - component=component, energy=energy + scipp_arrays[component.display_name] = ( + self._create_background_component_scipp_array( + component=component, energy=energy + ) ) return sc.Dataset(scipp_arrays) @@ -747,6 +740,9 @@ def _to_scipp_array( Args: values (np.ndarray): The values to convert. + energy (sc.Variable | None, default=None): Optional energy grid to use for the + energy coordinate. If None, the energy grid from the + experiment is used. Returns: sc.DataArray: The converted sc.DataArray. @@ -755,9 +751,9 @@ def _to_scipp_array( if energy is None: energy = self._masked_energy return sc.DataArray( - data=sc.array(dims=['energy'], values=values), + data=sc.array(dims=["energy"], values=values), coords={ - 'energy': energy, - 'Q': self.Q[self.Q_index], + "energy": energy, + "Q": self.Q[self.Q_index], }, ) diff --git a/src/easydynamics/analysis/analysis_base.py b/src/easydynamics/analysis/analysis_base.py index 365b54fb2..9ab308f6d 100644 --- a/src/easydynamics/analysis/analysis_base.py +++ b/src/easydynamics/analysis/analysis_base.py @@ -1,6 +1,7 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +import numpy as np import scipp as sc from easyscience.base_classes.model_base import ModelBase as EasyScienceModelBase from easyscience.variable import Parameter @@ -19,43 +20,11 @@ class AnalysisBase(EasyScienceModelBase): SampleModel contains the model for the sample, and the InstrumentModel contains the model for the instrument, including background and resolution - - Args: - display_name (str): Display name of the analysis. - unique_name (str or None): Unique name of the analysis. If None, - a unique name is automatically generated. - experiment (Experiment | None): The Experiment associated with - this Analysis. If None, a default Experiment is created. - sample_model (SampleModel | None): The SampleModel associated - with this Analysis. If None, a default SampleModel is - created. - instrument_model (InstrumentModel | None): The InstrumentModel - associated with this Analysis. If None, a default - InstrumentModel is created. - extra_parameters (Parameter | list[Parameter] | None): Extra - parameters to be included in the analysis for advanced - users. If None, no extra parameters are added. - - Attributes: - experiment (Experiment): The Experiment associated with this - Analysis. - sample_model (SampleModel): The SampleModel associated with this - Analysis. - instrument_model (InstrumentModel): The InstrumentModel - associated with this Analysis. - Q (sc.Variable | None): The Q values from the associated - Experiment, if available. - energy (sc.Variable | None): The energy values from the - associated Experiment, if available. - temperature (Parameter | None): The temperature from the - associated SampleModel, if available. - extra_parameters (list[Parameter]): The extra parameters - included in this Analysis. """ def __init__( self, - display_name: str = 'MyAnalysis', + display_name: str | None = "MyAnalysis", unique_name: str | None = None, experiment: Experiment | None = None, sample_model: SampleModel | None = None, @@ -65,29 +34,28 @@ def __init__( """Initialize the AnalysisBase. Args: - display_name (str): Display name of the analysis. - unique_name (str or None): Unique name of the analysis. If + display_name (str | None, default='MyAnalysis'): Display name of the analysis. + unique_name (str | None, default=None): Unique name of the analysis. If None, a unique name is automatically generated. - experiment (Experiment | None): The Experiment associated + experiment (Experiment | None, default=None): The Experiment associated with this Analysis. If None, a default Experiment is created. - sample_model (SampleModel | None): The SampleModel + sample_model (SampleModel | None, default=None): The SampleModel associated with this Analysis. If None, a default SampleModel is created. - instrument_model (InstrumentModel | None): The + instrument_model (InstrumentModel | None, default=None): The InstrumentModel associated with this Analysis. If None, a default InstrumentModel is created. - extra_parameters (Parameter | list[Parameter] | None): Extra + extra_parameters (Parameter | list[Parameter] | None, default=None): Extra parameters to be included in the analysis for advanced users. If None, no extra parameters are added. Raises: - TypeError: If experiment is not an Experiment or None. - TypeError: If sample_model is not a SampleModel or None. - TypeError: If instrument_model is not an InstrumentModel or - None. - TypeError: If extra_parameters is not a Parameter, a list of - Parameters, or None. + TypeError: If experiment is not an Experiment or None or + if sample_model is not a SampleModel or None or if + instrument_model is not an InstrumentModel or None or if + extra_parameters is not a Parameter, a list of Parameters, + or None. """ super().__init__(display_name=display_name, unique_name=unique_name) @@ -97,21 +65,23 @@ def __init__( elif isinstance(experiment, Experiment): self._experiment = experiment else: - raise TypeError('experiment must be an instance of Experiment or None.') + raise TypeError("experiment must be an instance of Experiment or None.") if sample_model is None: self._sample_model = SampleModel() elif isinstance(sample_model, SampleModel): self._sample_model = sample_model else: - raise TypeError('sample_model must be an instance of SampleModel or None.') + raise TypeError("sample_model must be an instance of SampleModel or None.") if instrument_model is None: self._instrument_model = InstrumentModel() elif isinstance(instrument_model, InstrumentModel): self._instrument_model = instrument_model else: - raise TypeError('instrument_model must be an instance of InstrumentModel or None.') + raise TypeError( + "instrument_model must be an instance of InstrumentModel or None." + ) if extra_parameters is not None: if isinstance(extra_parameters, Parameter): @@ -121,7 +91,9 @@ def __init__( ): self._extra_parameters = extra_parameters else: - raise TypeError('extra_parameters must be a Parameter or a list of Parameters.') + raise TypeError( + "extra_parameters must be a Parameter or a list of Parameters." + ) else: self._extra_parameters = [] @@ -145,12 +117,15 @@ def experiment(self) -> Experiment: def experiment(self, value: Experiment) -> None: """Set the Experiment for this Analysis. + Args: + value (Experiment): The Experiment to set for this Analysis. + Raises: TypeError: if value is not an Experiment. """ if not isinstance(value, Experiment): - raise TypeError('experiment must be an instance of Experiment') + raise TypeError("experiment must be an instance of Experiment") self._experiment = value self._on_experiment_changed() @@ -168,11 +143,14 @@ def sample_model(self) -> SampleModel: def sample_model(self, value: SampleModel) -> None: """Set the SampleModel for this Analysis. + Args: + value (SampleModel): The SampleModel to set for this Analysis. + Raises: TypeError: if value is not a SampleModel. """ if not isinstance(value, SampleModel): - raise TypeError('sample_model must be an instance of SampleModel') + raise TypeError("sample_model must be an instance of SampleModel") self._sample_model = value self._on_sample_model_changed() @@ -190,11 +168,15 @@ def instrument_model(self) -> InstrumentModel: def instrument_model(self, value: InstrumentModel) -> None: """Set the InstrumentModel for this Analysis. + Args: + value (InstrumentModel): The InstrumentModel to set for this + Analysis. + Raises: TypeError: if value is not an InstrumentModel. """ if not isinstance(value, InstrumentModel): - raise TypeError('instrument_model must be an instance of InstrumentModel') + raise TypeError("instrument_model must be an instance of InstrumentModel") self._instrument_model = value self._on_instrument_model_changed() @@ -204,21 +186,24 @@ def Q(self) -> sc.Variable | None: available. Returns: - sc.Variable: The Q values from the associated Experiment, - if available. - None: If the Experiment does not have any data. + sc.Variable | None: The Q values from the associated Experiment, + if available, and None if not. """ return self.experiment.Q @Q.setter - def Q(self, value) -> None: + def Q(self, value: sc.Variable) -> None: """Q cannot be set, as it is a read-only property derived from the Experiment. + Args: + value (sc.Variable): The Q values to set. This argument is + ignored, as Q is a read-only property. + Raises: AttributeError: If trying to set Q. """ - raise AttributeError('Q is a read-only property derived from the Experiment.') + raise AttributeError("Q is a read-only property derived from the Experiment.") @property def energy(self) -> sc.Variable | None: @@ -226,23 +211,28 @@ def energy(self) -> sc.Variable | None: available. Returns: - sc.Variable: The energy values from the associated - Experiment, if available. - None: If the Experiment does not have any data. + sc.Variable | None: The energy values from the associated + Experiment, if available, and None if not. """ return self.experiment.energy @energy.setter - def energy(self, value) -> None: + def energy(self, value: sc.Variable) -> None: """Energy cannot be set, as it is a read-only property derived from the Experiment. + Args: + value (sc.Variable): The energy values to set. This argument is + ignored, as energy is a read-only property. + Raises: AttributeError: If trying to set energy. """ - raise AttributeError('energy is a read-only property derived from the Experiment.') + raise AttributeError( + "energy is a read-only property derived from the Experiment." + ) @property def temperature(self) -> Parameter | None: @@ -250,23 +240,28 @@ def temperature(self) -> Parameter | None: available. Returns: - Parameter: The temperature from the associated SampleModel, - if available. - None: If the SampleModel does not have a temperature. + Parameter | None: The temperature from the associated SampleModel, + if available, and None if not. """ return self.sample_model.temperature @temperature.setter - def temperature(self, value) -> None: + def temperature(self, value: np.ndarray | Parameter) -> None: """Temperature cannot be set, as it is a read-only property derived from the SampleModel. + Args: + value (np.ndarray | Parameter): The temperature to set. This argument is + ignored, as temperature is a read-only property. + Raises: AttributeError: If trying to set temperature. """ - raise AttributeError('temperature is a read-only property derived from the SampleModel.') + raise AttributeError( + "temperature is a read-only property derived from the SampleModel." + ) @property def extra_parameters(self) -> list[Parameter]: @@ -288,7 +283,7 @@ def extra_parameters(self, value: Parameter | list[Parameter]) -> None: Raises: TypeError: If value is not a Parameter, a list of - Parameters, or None. + Parameters, or None. """ if isinstance(value, Parameter): self._extra_parameters = [value] @@ -297,7 +292,9 @@ def extra_parameters(self, value: Parameter | list[Parameter]) -> None: elif value is None: self._extra_parameters = [] else: - raise TypeError('extra_parameters must be a Parameter, a list of Parameters, or None.') + raise TypeError( + "extra_parameters must be a Parameter, a list of Parameters, or None." + ) ############# # Other methods @@ -344,7 +341,7 @@ def _verify_Q_index(self, Q_index: int | None) -> int | None: or Q_index < 0 or (self.Q is not None and Q_index >= len(self.Q)) ): - raise IndexError('Q_index must be a valid index for the Q values.') + raise IndexError("Q_index must be a valid index for the Q values.") return Q_index ############# @@ -357,5 +354,5 @@ def __repr__(self) -> str: Returns: str: A string representation of the Analysis. """ - return f' {self.__class__.__name__} (display_name={self.display_name}, \ - unique_name={self.unique_name})' + return f" {self.__class__.__name__} (display_name={self.display_name}, \ + unique_name={self.unique_name})" diff --git a/src/easydynamics/convolution/analytical_convolution.py b/src/easydynamics/convolution/analytical_convolution.py index 534b4d7a6..5c4b79183 100644 --- a/src/easydynamics/convolution/analytical_convolution.py +++ b/src/easydynamics/convolution/analytical_convolution.py @@ -22,35 +22,23 @@ class AnalyticalConvolution(ConvolutionBase): Possible analytical convolutions are any combination of delta functions, Gaussians, Lorentzians and Voigt profiles. - - Args: - energy (np.ndarray | sc.Variable): 1D array of energy values - where the convolution is evaluated. - sample_components (ComponentCollection | ModelComponent): The - sample model to be convolved. - resolution_components (ComponentCollection | ModelComponent): - The resolution model to convolve with. - energy_offset (Numeric | Parameter, optional): An offset to - shift the energy values by. Default is 0.0. - energy_unit (str | sc.Unit, optional): The unit of the energy. - Default is 'meV'. """ # Mapping of supported component type pairs to convolution methods. # Delta functions are handled separately. _CONVOLUTIONS = { - ('Gaussian', 'Gaussian'): '_convolute_gaussian_gaussian', - ('Gaussian', 'Lorentzian'): '_convolute_gaussian_lorentzian', - ('Gaussian', 'Voigt'): '_convolute_gaussian_voigt', - ('Lorentzian', 'Lorentzian'): '_convolute_lorentzian_lorentzian', - ('Lorentzian', 'Voigt'): '_convolute_lorentzian_voigt', - ('Voigt', 'Voigt'): '_convolute_voigt_voigt', + ("Gaussian", "Gaussian"): "_convolute_gaussian_gaussian", + ("Gaussian", "Lorentzian"): "_convolute_gaussian_lorentzian", + ("Gaussian", "Voigt"): "_convolute_gaussian_voigt", + ("Lorentzian", "Lorentzian"): "_convolute_lorentzian_lorentzian", + ("Lorentzian", "Voigt"): "_convolute_lorentzian_voigt", + ("Voigt", "Voigt"): "_convolute_voigt_voigt", } def __init__( self, energy: np.ndarray | sc.Variable, - energy_unit: str | sc.Unit = 'meV', + energy_unit: str | sc.Unit = "meV", sample_components: ComponentCollection | ModelComponent | None = None, resolution_components: ComponentCollection | ModelComponent | None = None, energy_offset: Numeric | Parameter = 0.0, @@ -60,14 +48,14 @@ def __init__( Args: energy (np.ndarray | sc.Variable): 1D array of energy values where the convolution is evaluated. - sample_components (ComponentCollection | ModelComponent): + energy_unit (str | sc.Unit, default='meV'): The unit of the + energy. + sample_components (ComponentCollection | ModelComponent | None, default=None): The sample model to be convolved. - resolution_components (ComponentCollection | ModelComponent): + resolution_components (ComponentCollection | ModelComponent | None, default=None): The resolution model to convolve with. - energy_offset (Numeric | Parameter, optional): An offset to - shift the energy values by. Default is 0.0. - energy_unit (str | sc.Unit, optional): The unit of the - energy. Default is 'meV'. + energy_offset (Numeric | Parameter, default=0.0): An offset to + shift the energy values by. """ super().__init__( energy=energy, @@ -88,12 +76,6 @@ def convolution( Returns: np.ndarray: The convolution of the sample_components and resolution_components values evaluated at self.energy. - - Raises: - ValueError: If resolution_components contains delta - functions. - ValueError: If component pair cannot be handled - analytically. """ sample_components = self.sample_components.components @@ -164,8 +146,8 @@ def _convolute_analytic_pair( if isinstance(resolution_component, DeltaFunction): raise ValueError( - 'Analytical convolution with a delta function \ - in the resolution model is not supported.' + "Analytical convolution with a delta function \ + in the resolution model is not supported." ) # Delta function + anything --> @@ -191,8 +173,8 @@ def _convolute_analytic_pair( if func_name is None: raise ValueError( - f'Analytical convolution not supported for component pair: ' - f'{type(sample_component).__name__}, {type(resolution_component).__name__}' + f"Analytical convolution not supported for component pair: " + f"{type(sample_component).__name__}, {type(resolution_component).__name__}" ) # Call the corresponding method @@ -205,7 +187,7 @@ def _convolute_delta_any( self, sample_component: DeltaFunction, resolution_components: ComponentCollection | ModelComponent, - ): + ) -> np.ndarray: """Convolution of delta function with any ModelComponent or ComponentCollection results in the same component or ComponentCollection shifted by the delta center. The areas are @@ -243,7 +225,9 @@ def _convolute_gaussian_gaussian( np.ndarray: The evaluated convolution values at self.energy. """ - width = np.sqrt(sample_component.width.value**2 + resolution_component.width.value**2) + width = np.sqrt( + sample_component.width.value**2 + resolution_component.width.value**2 + ) area = sample_component.area.value * resolution_component.area.value @@ -302,7 +286,8 @@ def _convolute_gaussian_voigt( center = sample_component.center.value + resolution_component.center.value gaussian_width = np.sqrt( - sample_component.width.value**2 + resolution_component.gaussian_width.value**2 + sample_component.width.value**2 + + resolution_component.gaussian_width.value**2 ) lorentzian_width = resolution_component.lorentzian_width.value @@ -404,11 +389,13 @@ def _convolute_voigt_voigt( center = sample_component.center.value + resolution_component.center.value gaussian_width = np.sqrt( - sample_component.gaussian_width.value**2 + resolution_component.gaussian_width.value**2 + sample_component.gaussian_width.value**2 + + resolution_component.gaussian_width.value**2 ) lorentzian_width = ( - sample_component.lorentzian_width.value + resolution_component.lorentzian_width.value + sample_component.lorentzian_width.value + + resolution_component.lorentzian_width.value ) return self._voigt_eval( area=area, diff --git a/src/easydynamics/convolution/convolution.py b/src/easydynamics/convolution/convolution.py index 65bbf9ec8..c8461d74f 100644 --- a/src/easydynamics/convolution/convolution.py +++ b/src/easydynamics/convolution/convolution.py @@ -32,64 +32,21 @@ class Convolution(NumericalConvolutionBase): Includes optional upsampling and extended range to improve accuracy of the numerical convolutions. Also warns about numerical instabilities if peaks are very wide or very narrow. - - Args: - energy (np.ndarray | scipp.Variable): 1D array of energy values - where the convolution is evaluated. - sample_components (ComponentCollection | ModelComponent): The - sample components to be convolved. - resolution_components (ComponentCollection | ModelComponent): - The resolution components to convolve with. - upsample_factor (int | None): The factor by which to upsample - the input data before convolution. Default is 5. - extension_factor (float | None): The factor by which to - extend the input data range before convolution. Default is - 0.2. - temperature (Parameter | float | None): The - temperature to use for detailed balance correction. Default - is None. - temperature_unit (str | sc.Unit | None): The unit of the - temperature parameter. Default is 'K'. - energy_unit (str | sc.Unit | None): The unit of the energy. - Default is 'meV'. - normalize_detailed_balance (bool | None): Whether to - normalize the detailed balance correction. Default is True. - - Attributes: - energy (scipp.Variable): 1D array of energy values where the - convolution is evaluated. - sample_components (ComponentCollection): The sample components - to be convolved. - resolution_components (ComponentCollection): The resolution - components to convolve with. - energy_offset (Parameter): Energy offset to apply to the energy - values before convolution. - upsample_factor (int): The factor by which to upsample the input - data before convolution. - extension_factor (float): The factor by which to extend the - input data range before convolution. - temperature (Parameter | None): The temperature to use for - detailed balance correction. - temperature_unit (str | sc.Unit): The unit of the temperature - parameter. - energy_unit (str | sc.Unit): The unit of the energy. - normalize_detailed_balance (bool): Whether to normalize the - detailed balance correction. """ # When these attributes are changed, the convolution plan # needs to be rebuilt _invalidate_plan_on_change = { - 'energy', - '_energy', - '_energy_grid', - '_sample_components', - '_resolution_components', - '_temperature', - '_upsample_factor', - '_extension_factor', - '_energy_unit', - '_normalize_detailed_balance', + "energy", + "_energy", + "_energy_grid", + "_sample_components", + "_resolution_components", + "_temperature", + "_upsample_factor", + "_extension_factor", + "_energy_unit", + "_normalize_detailed_balance", } def __init__( @@ -98,36 +55,38 @@ def __init__( sample_components: ComponentCollection | ModelComponent, resolution_components: ComponentCollection | ModelComponent, energy_offset: Numeric | Parameter = 0.0, - upsample_factor: Numeric = 5, - extension_factor: Numeric = 0.2, + upsample_factor: Numeric | None = 5, + extension_factor: Numeric | None = 0.2, temperature: Parameter | Numeric | None = None, - temperature_unit: str | sc.Unit = 'K', - energy_unit: str | sc.Unit = 'meV', + temperature_unit: str | sc.Unit = "K", + energy_unit: str | sc.Unit = "meV", normalize_detailed_balance: bool = True, ) -> None: """Initialize the Convolution class. Args: - energy (np.ndarray | scipp.Variable): 1D array of energy + energy (np.ndarray | sc.Variable): 1D array of energy values where the convolution is evaluated. sample_components (ComponentCollection | ModelComponent): The sample components to be convolved. resolution_components (ComponentCollection | ModelComponent): The resolution components to convolve with. - upsample_factor (int | None): The factor by which to + energy_offset (Numeric | Parameter, default=0.0): An energy + offset to apply to the energy values before convolution. + upsample_factor (Numeric | None, default=5): The factor by which to upsample the input data before convolution. Default is 5. - extension_factor (float | None): The factor by which to + extension_factor (Numeric | None, default=0.2): The factor by which to extend the input data range before convolution. Default is 0.2. - temperature (Parameter | float | None): The + temperature (Parameter | Numeric | None, default=None): The temperature to use for detailed balance correction. Default is None. - temperature_unit (str | sc.Unit | None): The unit of the + temperature_unit (str | sc.Unit, default='K'): The unit of the temperature parameter. Default is 'K'. - energy_unit (str | sc.Unit | None): The unit of the energy. + energy_unit (str | sc.Unit, default='meV'): The unit of the energy. Default is 'meV'. - normalize_detailed_balance (bool | None): Whether to + normalize_detailed_balance (bool, default=True): Whether to normalize the detailed balance correction. Default is True. """ @@ -216,23 +175,27 @@ def _check_if_pair_is_analytic( Returns: bool: True if the component pair can be handled analytically, False otherwise. + + Raises: + TypeError: If either component is not a ModelComponent, or if + the resolution component is a DeltaFunction. """ if not isinstance(sample_component, ModelComponent): raise TypeError( - f'`sample_component` is an instance of {type(sample_component).__name__}, \ - but must be a ModelComponent.' + f"`sample_component` is an instance of {type(sample_component).__name__}, \ + but must be a ModelComponent." ) if not isinstance(resolution_component, ModelComponent): raise TypeError( - f'`resolution_component` is an instance of {type(resolution_component).__name__}, \ - but must be a ModelComponent.' + f"`resolution_component` is an instance of {type(resolution_component).__name__}, \ + but must be a ModelComponent." ) if isinstance(resolution_component, DeltaFunction): raise TypeError( - 'resolution components contains delta functions. This is not supported.' + "resolution components contains delta functions. This is not supported." ) analytical_types = (Gaussian, Lorentzian, Voigt) @@ -271,7 +234,9 @@ def _build_convolution_plan(self) -> None: pair_is_analytic = [] for resolution_component in self._resolution_components.components: pair_is_analytic.append( - self._check_if_pair_is_analytic(sample_component, resolution_component) + self._check_if_pair_is_analytic( + sample_component, resolution_component + ) ) # If all resolution components can be convolved analytically # with this sample component, add it to analytical @@ -323,17 +288,24 @@ def _set_convolvers(self) -> None: self._numerical_convolver = None # Update some setters so the internal sample models are updated - def __setattr__(self, name, value) -> None: + def __setattr__(self, name: str, value: any) -> None: """Custom setattr to invalidate convolution plan on relevant attribute changes, and build a new plan. The new plan is only built after initialization (when _reactions_enabled is True) to avoid issues during __init__. + + Args: + name (str): The name of the attribute to set. + value (any): The value to set the attribute to. """ super().__setattr__(name, value) if name in self._invalidate_plan_on_change: self._convolution_plan_is_valid = False - if getattr(self, '_reactions_enabled', False) and name in self._invalidate_plan_on_change: + if ( + getattr(self, "_reactions_enabled", False) + and name in self._invalidate_plan_on_change + ): self._build_convolution_plan() diff --git a/src/easydynamics/convolution/convolution_base.py b/src/easydynamics/convolution/convolution_base.py index 7972681e5..c5af8fe97 100644 --- a/src/easydynamics/convolution/convolution_base.py +++ b/src/easydynamics/convolution/convolution_base.py @@ -13,82 +13,59 @@ class ConvolutionBase: """Base class for convolutions of sample and resolution models. This base class has no convolution functionality. - - Args: - energy (np.ndarray | scipp.Variable): 1D array of energy values - where the convolution is evaluated. - sample_components (ComponentCollection | ModelComponent): The - sample model to be convolved. - resolution_components (ComponentCollection | ModelComponent): - The resolution model to convolve with. - energy_unit (str | sc.Unit, optional): The unit of the energy. - Default is 'meV'. - energy_offset (Numeric | Parameter, optional): The energy offset - applied to the convolution. Default is 0.0. - - Attributes: - energy (scipp.Variable): 1D array of energy values where the - convolution is evaluated - sample_components (ComponentCollection | ModelComponent): The - sample model to be convolved. - resolution_components (ComponentCollection | ModelComponent): - The resolution model to convolve with. - energy_unit (str | sc.Unit): The unit of the energy. - energy_offset (Parameter): The energy offset applied to the - convolution. """ def __init__( self, energy: np.ndarray | sc.Variable, - sample_components: ComponentCollection | ModelComponent = None, - resolution_components: ComponentCollection | ModelComponent = None, - energy_unit: str | sc.Unit = 'meV', + sample_components: ComponentCollection | ModelComponent | None = None, + resolution_components: ComponentCollection | ModelComponent | None = None, + energy_unit: str | sc.Unit = "meV", energy_offset: Numeric | Parameter = 0.0, ) -> None: """Initialize the ConvolutionBase. Args: - energy (np.ndarray | scipp.Variable): 1D array of energy + energy (np.ndarray | sc.Variable): 1D array of energy values where the convolution is evaluated. - sample_components (ComponentCollection | ModelComponent): + sample_components (ComponentCollection | ModelComponent | None, default=None): The sample model to be convolved. - resolution_components (ComponentCollection | ModelComponent): + resolution_components (ComponentCollection | ModelComponent | None, default=None): The resolution model to convolve with. - energy_unit (str | sc.Unit, optional): The unit of the - energy. Default is 'meV'. - energy_offset (Numeric | Parameter, optional): The energy + energy_unit (str | sc.Unit, default='meV'): The unit of the + energy. + energy_offset (Numeric | Parameter, default=0.0): The energy offset applied to the convolution. Default is 0.0. Raises: TypeError: If energy is not a numpy ndarray or a scipp - Variable. - TypeError: If energy_unit is not a string or scipp unit. - TypeError: If energy_offset is not a number or a Parameter. - TypeError: If sample_components is not a ComponentCollection - or ModelComponent. - TypeError: If resolution_components is not a - ComponentCollection or ModelComponent. + Variable or if energy_unit is not a string or scipp unit, or if + energy_offset is not a number or a Parameter, or if + sample_components is not a ComponentCollection or ModelComponent, or if + resolution_components is not a ComponentCollection or ModelComponent. + """ if isinstance(energy, Numeric): energy = np.array([float(energy)]) if not isinstance(energy, (np.ndarray, sc.Variable)): - raise TypeError(f'Energy must be a numpy ndarray or a scipp Variable. Got {energy}') + raise TypeError( + f"Energy must be a numpy ndarray or a scipp Variable. Got {energy}" + ) if not isinstance(energy_unit, (str, sc.Unit)): - raise TypeError('Energy_unit must be a string or sc.Unit.') + raise TypeError("Energy_unit must be a string or sc.Unit.") if isinstance(energy, np.ndarray): - energy = sc.array(dims=['energy'], values=energy, unit=energy_unit) + energy = sc.array(dims=["energy"], values=energy, unit=energy_unit) if isinstance(energy_offset, Numeric): energy_offset = Parameter( - name='energy_offset', value=float(energy_offset), unit=energy_unit + name="energy_offset", value=float(energy_offset), unit=energy_unit ) if not isinstance(energy_offset, Parameter): - raise TypeError('Energy_offset must be a number or a Parameter.') + raise TypeError("Energy_offset must be a number or a Parameter.") self._energy = energy self._energy_unit = energy_unit @@ -99,7 +76,7 @@ def __init__( or isinstance(sample_components, ModelComponent) ): raise TypeError( - f'`sample_components` is an instance of {type(sample_components).__name__}, but must be a ComponentCollection or ModelComponent.' # noqa: E501 + f"`sample_components` is an instance of {type(sample_components).__name__}, but must be a ComponentCollection or ModelComponent." # noqa: E501 ) if isinstance(sample_components, ModelComponent): sample_components = ComponentCollection(components=[sample_components]) @@ -110,10 +87,12 @@ def __init__( or isinstance(resolution_components, ModelComponent) ): raise TypeError( - f'`resolution_components` is an instance of {type(resolution_components).__name__}, but must be a ComponentCollection or ModelComponent.' # noqa: E501 + f"`resolution_components` is an instance of {type(resolution_components).__name__}, but must be a ComponentCollection or ModelComponent." # noqa: E501 ) if isinstance(resolution_components, ModelComponent): - resolution_components = ComponentCollection(components=[resolution_components]) + resolution_components = ComponentCollection( + components=[resolution_components] + ) self._resolution_components = resolution_components @property @@ -130,14 +109,14 @@ def energy_offset(self, energy_offset: Numeric | Parameter) -> None: """Set the energy offset. Args: - energy_offset : Number or Parameter - The energy offset to apply to the convolution. + energy_offset (Numeric | Parameter): The energy offset to + apply to the convolution. Raises: TypeError: If energy_offset is not a number or a Parameter. """ if not isinstance(energy_offset, Parameter | Numeric): - raise TypeError('Energy_offset must be a number or a Parameter.') + raise TypeError("Energy_offset must be a number or a Parameter.") if isinstance(energy_offset, Numeric): self._energy_offset.value = float(energy_offset) @@ -157,19 +136,19 @@ def energy_with_offset(self) -> sc.Variable: return energy_with_offset @energy_with_offset.setter - def energy_with_offset(self, value) -> None: + def energy_with_offset(self, value: sc.Variable) -> None: """Energy with offset is a read-only property derived from energy and energy_offset. Args: - value: The value to set (ignored). + value (sc.Variable): The value to set (ignored). Raises: AttributeError: Always raised since energy_with_offset is read-only. """ raise AttributeError( - 'Energy with offset is a read-only property derived from energy and energy_offset.' + "Energy with offset is a read-only property derived from energy and energy_offset." ) @property @@ -187,8 +166,8 @@ def energy(self) -> sc.Variable: def energy(self, energy: np.ndarray | sc.Variable) -> None: """Set the energy. - Args: - energy (np.ndarray | scipp.Variable): 1D array of energy + Args: + energy (np.ndarray | sc.Variable): 1D array of energy values where the convolution is evaluated. Raises: @@ -200,10 +179,14 @@ def energy(self, energy: np.ndarray | sc.Variable) -> None: energy = np.array([float(energy)]) if not isinstance(energy, (np.ndarray, sc.Variable)): - raise TypeError('Energy must be a Number, a numpy ndarray or a scipp Variable.') + raise TypeError( + "Energy must be a Number, a numpy ndarray or a scipp Variable." + ) if isinstance(energy, np.ndarray): - self._energy = sc.array(dims=['energy'], values=energy, unit=self._energy.unit) + self._energy = sc.array( + dims=["energy"], values=energy, unit=self._energy.unit + ) if isinstance(energy, sc.Variable): self._energy = energy @@ -222,8 +205,8 @@ def energy_unit(self) -> str: def energy_unit(self, unit_str: str) -> None: raise AttributeError( ( - f'Unit is read-only. Use convert_unit to change the unit between allowed types ' - f'or create a new {self.__class__.__name__} with the desired unit.' + f"Unit is read-only. Use convert_unit to change the unit between allowed types " + f"or create a new {self.__class__.__name__} with the desired unit." ) ) # noqa: E501 @@ -235,11 +218,11 @@ def convert_energy_unit(self, energy_unit: str | sc.Unit) -> None: Raises: TypeError: If energy_unit is not a string or scipp unit. - UnitError: If energy cannot be converted to the specified + Exception: If energy cannot be converted to the specified unit. """ if not isinstance(energy_unit, (str, sc.Unit)): - raise TypeError('Energy unit must be a string or scipp unit.') + raise TypeError("Energy unit must be a string or scipp unit.") old_energy = self.energy.copy() try: @@ -262,17 +245,19 @@ def sample_components(self) -> ComponentCollection | ModelComponent: """Get the sample model. Returns: - ComponentCollection or ModelComponent: The sample model to + ComponentCollection | ModelComponent: The sample model to be convolved. """ return self._sample_components @sample_components.setter - def sample_components(self, sample_components: ComponentCollection | ModelComponent) -> None: + def sample_components( + self, sample_components: ComponentCollection | ModelComponent + ) -> None: """Set the sample model. Args: - sample_components : ComponentCollection or ModelComponent + sample_components (ComponentCollection | ModelComponent): The sample model to be convolved. Raises: @@ -281,7 +266,7 @@ def sample_components(self, sample_components: ComponentCollection | ModelCompon """ if not isinstance(sample_components, (ComponentCollection, ModelComponent)): raise TypeError( - f'`sample_components` is an instance of {type(sample_components).__name__}, but must be a ComponentCollection or ModelComponent.' # noqa: E501 + f"`sample_components` is an instance of {type(sample_components).__name__}, but must be a ComponentCollection or ModelComponent." # noqa: E501 ) if isinstance(sample_components, ModelComponent): @@ -293,7 +278,7 @@ def resolution_components(self) -> ComponentCollection | ModelComponent: """Get the resolution model. Returns: - ComponentCollection or ModelComponent: The resolution model + ComponentCollection | ModelComponent: The resolution model to be convolved. """ return self._resolution_components @@ -305,9 +290,9 @@ def resolution_components( """Set the resolution model. Args: - resolution_components (ComponentCollection | ModelComponent) - : The resolution model to be convolved. Can be a - ComponentCollection or a single ModelComponent + resolution_components (ComponentCollection | ModelComponent): + The resolution model to be convolved. Can be a + ComponentCollection or a single ModelComponent Raises: TypeError: If resolution_components is not a @@ -315,9 +300,11 @@ def resolution_components( """ if not isinstance(resolution_components, (ComponentCollection, ModelComponent)): raise TypeError( - f'`resolution_components` is an instance of {type(resolution_components).__name__}, but must be a ComponentCollection or ModelComponent.' # noqa: E501 + f"`resolution_components` is an instance of {type(resolution_components).__name__}, but must be a ComponentCollection or ModelComponent." # noqa: E501 ) if isinstance(resolution_components, ModelComponent): - resolution_components = ComponentCollection(components=[resolution_components]) + resolution_components = ComponentCollection( + components=[resolution_components] + ) self._resolution_components = resolution_components From b79f3b09014bd719592e500fb2cf54b74411fe7e Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Mon, 16 Mar 2026 11:32:48 +0100 Subject: [PATCH 28/31] pixi run fix --- pixi.lock | 4 +- pyproject.toml | 24 ++--- src/easydynamics/analysis/analysis.py | 96 +++++++++---------- src/easydynamics/analysis/analysis1d.py | 90 ++++++++--------- src/easydynamics/analysis/analysis_base.py | 40 +++----- .../convolution/analytical_convolution.py | 39 ++++---- src/easydynamics/convolution/convolution.py | 52 +++++----- .../convolution/convolution_base.py | 58 +++++------ .../convolution/numerical_convolution.py | 24 ++--- .../convolution/numerical_convolution_base.py | 74 +++++++------- src/easydynamics/experiment/experiment.py | 82 +++++++--------- .../sample_model/background_model.py | 4 +- .../sample_model/component_collection.py | 96 ++++++++----------- .../components/damped_harmonic_oscillator.py | 36 +++---- .../sample_model/components/delta_function.py | 20 ++-- .../sample_model/components/exponential.py | 38 ++++---- .../sample_model/components/gaussian.py | 24 ++--- .../sample_model/components/lorentzian.py | 28 +++--- .../sample_model/components/mixins.py | 34 +++---- .../components/model_component.py | 38 ++++---- .../sample_model/components/polynomial.py | 50 ++++------ .../brownian_translational_diffusion.py | 3 +- .../diffusion_model/diffusion_model_base.py | 3 +- .../jump_translational_diffusion.py | 7 +- src/easydynamics/sample_model/sample_model.py | 8 +- src/easydynamics/utils/detailed_balance.py | 16 +++- 26 files changed, 439 insertions(+), 549 deletions(-) diff --git a/pixi.lock b/pixi.lock index f977d25a5..f827c396e 100644 --- a/pixi.lock +++ b/pixi.lock @@ -4074,8 +4074,8 @@ packages: requires_python: '>=3.5' - pypi: ./ name: easydynamics - version: 0.2.1+devdirty13 - sha256: e9e7b1f27a2af5d6267f3dd75e7e46785211b1046865a7a70a86d278b0a30dd0 + version: 0.2.1+devdirty31 + sha256: 173a472b278902a0a8f520f54a773fa824d5d033443bd820536c6d6c60fe492f requires_dist: - darkdetect - easyscience diff --git a/pyproject.toml b/pyproject.toml index 24fd334de..18d906078 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,16 +22,16 @@ classifiers = [ ] requires-python = '>=3.11' dependencies = [ - 'easyscience', # The base library of the EasyScience framework - 'pooch', # Data downloader - 'darkdetect', # Detecting dark mode (system-level) - 'plopp', # Plotting library - 'jupyterlab', # Jupyter notebooks - 'pixi-kernel', # Pixi Jupyter kernel - '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 + 'easyscience', # The base library of the EasyScience framework + 'pooch', # Data downloader + 'darkdetect', # Detecting dark mode (system-level) + 'plopp', # Plotting library + 'jupyterlab', # Jupyter notebooks + 'pixi-kernel', # Pixi Jupyter kernel + '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 ] [project.optional-dependencies] @@ -194,7 +194,7 @@ quote-style = 'single' # But double quotes in docstrings (PEP 8, PEP 25 select = [ # Various rules #'C90', # https://docs.astral.sh/ruff/rules/#mccabe-c90 - 'F', # https://docs.astral.sh/ruff/rules/#pyflakes-f + 'F', # https://docs.astral.sh/ruff/rules/#pyflakes-f #'FLY', # https://docs.astral.sh/ruff/rules/#flynt-fly #'FURB', # https://docs.astral.sh/ruff/rules/#refurb-furb 'I', # https://docs.astral.sh/ruff/rules/#isort-i @@ -215,7 +215,7 @@ select = [ #'PLW', # https://docs.astral.sh/ruff/rules/#warning-plw # flake8 rules #'A', # https://docs.astral.sh/ruff/rules/#flake8-builtins-a - 'ANN', # https://docs.astral.sh/ruff/rules/#flake8-annotations-ann + 'ANN', # https://docs.astral.sh/ruff/rules/#flake8-annotations-ann #'ARG', # https://docs.astral.sh/ruff/rules/#flake8-unused-arguments-arg #'ASYNC', # https://docs.astral.sh/ruff/rules/#flake8-async-async #'B', # https://docs.astral.sh/ruff/rules/#flake8-bugbear-b diff --git a/src/easydynamics/analysis/analysis.py b/src/easydynamics/analysis/analysis.py index d0f80be82..483e214fb 100644 --- a/src/easydynamics/analysis/analysis.py +++ b/src/easydynamics/analysis/analysis.py @@ -21,13 +21,15 @@ class Analysis(AnalysisBase): """For analysing two-dimensional data, i.e. intensity as function of - energy and Q. Supports independent fits of each Q value and - simultaneous fits of all Q. + energy and Q. + + Supports independent fits of each Q value and simultaneous fits of + all Q. """ def __init__( self, - display_name: str | None = "MyAnalysis", + display_name: str | None = 'MyAnalysis', unique_name: str | None = None, experiment: Experiment | None = None, sample_model: SampleModel | None = None, @@ -70,8 +72,8 @@ def __init__( if self.Q is not None: for Q_index in range(len(self.Q)): analysis = Analysis1d( - display_name=f"{self.display_name}_Q{Q_index}", - unique_name=(f"{self.unique_name}_Q{Q_index}"), + display_name=f'{self.display_name}_Q{Q_index}', + unique_name=(f'{self.unique_name}_Q{Q_index}'), experiment=self.experiment, sample_model=self.sample_model, instrument_model=self.instrument_model, @@ -113,9 +115,9 @@ def analysis_list(self, value: list[Analysis1d]) -> None: """ raise AttributeError( - "analysis_list is read-only. " - "To change the analysis list, modify the experiment, sample model, " - "or instrument model." + 'analysis_list is read-only. ' + 'To change the analysis list, modify the experiment, sample model, ' + 'or instrument model.' ) ############# @@ -146,16 +148,14 @@ def calculate( energy = self.energy if Q_index is None: - return [ - analysis.calculate(energy=energy) for analysis in self.analysis_list - ] + return [analysis.calculate(energy=energy) for analysis in self.analysis_list] Q_index = self._verify_Q_index(Q_index) return self.analysis_list[Q_index].calculate(energy=energy) def fit( self, - fit_method: str = "independent", + fit_method: str = 'independent', Q_index: int | None = None, ) -> FitResults | list[FitResults]: """Fit the model to the experimental data. @@ -181,22 +181,20 @@ def fit( if self.Q is None: raise ValueError( - "No Q values available for fitting. Please check the experiment data." + 'No Q values available for fitting. Please check the experiment data.' ) Q_index = self._verify_Q_index(Q_index) - if fit_method == "independent": + if fit_method == 'independent': if Q_index is not None: return self._fit_single_Q(Q_index) else: return self._fit_all_Q_independently() - elif fit_method == "simultaneous": + elif fit_method == 'simultaneous': return self._fit_all_Q_simultaneously() else: - raise ValueError( - "Invalid fit method. Choose 'independent' or 'simultaneous'." - ) + raise ValueError("Invalid fit method. Choose 'independent' or 'simultaneous'.") def plot_data_and_model( self, @@ -248,23 +246,21 @@ def plot_data_and_model( ) if self.experiment.binned_data is None: - raise ValueError("No data to plot. Please load data first.") + raise ValueError('No data to plot. Please load data first.') if not _in_notebook(): - raise RuntimeError( - "plot_data() can only be used in a Jupyter notebook environment." - ) + raise RuntimeError('plot_data() can only be used in a Jupyter notebook environment.') if self.Q is None: raise ValueError( - "No Q values available for plotting. Please check the experiment data." + 'No Q values available for plotting. Please check the experiment data.' ) if not isinstance(plot_components, bool): - raise TypeError("plot_components must be True or False.") + raise TypeError('plot_components must be True or False.') if not isinstance(add_background, bool): - raise TypeError("add_background must be True or False.") + raise TypeError('add_background must be True or False.') if energy is None: energy = self.energy @@ -272,16 +268,16 @@ def plot_data_and_model( import plopp as pp plot_kwargs_defaults = { - "title": self.display_name, - "linestyle": {"Data": "none", "Model": "-"}, - "marker": {"Data": "o", "Model": None}, - "color": {"Data": "black", "Model": "red"}, - "markerfacecolor": {"Data": "none", "Model": "none"}, - "keep": "energy", + 'title': self.display_name, + 'linestyle': {'Data': 'none', 'Model': '-'}, + 'marker': {'Data': 'o', 'Model': None}, + 'color': {'Data': 'black', 'Model': 'red'}, + 'markerfacecolor': {'Data': 'none', 'Model': 'none'}, + 'keep': 'energy', } data_and_model = { - "Data": self.experiment.binned_data, - "Model": self._create_model_array(energy=energy), + 'Data': self.experiment.binned_data, + 'Model': self._create_model_array(energy=energy), } if plot_components: @@ -290,8 +286,8 @@ def plot_data_and_model( ) for key in components.keys(): data_and_model[key] = components[key] - plot_kwargs_defaults["linestyle"][key] = "--" - plot_kwargs_defaults["marker"][key] = None + plot_kwargs_defaults['linestyle'][key] = '--' + plot_kwargs_defaults['marker'][key] = None # Overwrite defaults with any user-provided kwargs plot_kwargs_defaults.update(kwargs) @@ -318,7 +314,7 @@ def parameters_to_dataset(self) -> sc.Dataset: parameter across different Q values. """ - ds = sc.Dataset(coords={"Q": self.Q}) + ds = sc.Dataset(coords={'Q': self.Q}) # Collect all parameter names all_names = { @@ -348,7 +344,7 @@ def parameters_to_dataset(self) -> sc.Dataset: except Exception as e: raise UnitError( f"Inconsistent units for parameter '{name}': " - f"{units[name]} vs {p.unit}" + f'{units[name]} vs {p.unit}' ) from e values[name].append(p.value) @@ -360,7 +356,7 @@ def parameters_to_dataset(self) -> sc.Dataset: # Build dataset variables for name in all_names: ds[name] = sc.Variable( - dims=["Q"], + dims=['Q'], values=np.asarray(values[name], dtype=float), variances=np.asarray(variances[name], dtype=float), unit=units.get(name, None), @@ -399,10 +395,8 @@ def plot_parameters( if isinstance(names, str): names = [names] - if not isinstance(names, list) or not all( - isinstance(name, str) for name in names - ): - raise TypeError("names must be a string or a list of strings.") + if not isinstance(names, list) or not all(isinstance(name, str) for name in names): + raise TypeError('names must be a string or a list of strings.') for name in names: if name not in ds: @@ -410,9 +404,9 @@ def plot_parameters( data_to_plot = {name: ds[name] for name in names} plot_kwargs_defaults = { - "linestyle": {name: "none" for name in names}, - "marker": {name: "o" for name in names}, - "markerfacecolor": {name: "none" for name in names}, + 'linestyle': {name: 'none' for name in names}, + 'marker': {name: 'o' for name in names}, + 'markerfacecolor': {name: 'none' for name in names}, } plot_kwargs_defaults.update(kwargs) @@ -504,9 +498,7 @@ def _fit_all_Q_simultaneously(self) -> FitResults: ws = [] for analysis in self.analysis_list: - x, y, weight, _ = self.experiment._extract_x_y_weights_only_finite( - analysis.Q_index - ) + x, y, weight, _ = self.experiment._extract_x_y_weights_only_finite(analysis.Q_index) xs.append(x) ys.append(y) ws.append(weight) @@ -550,10 +542,10 @@ def _create_model_array(self, energy: sc.Variable | None = None) -> sc.DataArray """ if energy is None: energy = self.energy - model = sc.array(dims=["Q", "energy"], values=self.calculate(energy=energy)) + model = sc.array(dims=['Q', 'energy'], values=self.calculate(energy=energy)) model_data_array = sc.DataArray( data=model, - coords={"Q": self.Q, "energy": energy}, + coords={'Q': self.Q, 'energy': energy}, ) return model_data_array @@ -581,7 +573,7 @@ def _create_components_dataset( of the model, with dimensions "Q". """ if not isinstance(add_background, bool): - raise TypeError("add_background must be True or False.") + raise TypeError('add_background must be True or False.') if energy is None: energy = self.energy @@ -593,7 +585,7 @@ def _create_components_dataset( for analysis in self.analysis_list ] - return sc.concat(datasets, dim="Q") + return sc.concat(datasets, dim='Q') ############# # Dunder methods diff --git a/src/easydynamics/analysis/analysis1d.py b/src/easydynamics/analysis/analysis1d.py index 41bffaee7..d2802a4a3 100644 --- a/src/easydynamics/analysis/analysis1d.py +++ b/src/easydynamics/analysis/analysis1d.py @@ -22,13 +22,15 @@ class Analysis1d(AnalysisBase): """For analysing one-dimensional data, i.e. intensity as function of - energy for a single Q index. Is used primarily in the Analysis - class, but can also be used on its own for simpler analyses. + energy for a single Q index. + + Is used primarily in the Analysis class, but can also be used on its + own for simpler analyses. """ def __init__( self, - display_name: str | None = "MyAnalysis", + display_name: str | None = 'MyAnalysis', unique_name: str | None = None, experiment: Experiment | None = None, sample_model: SampleModel | None = None, @@ -164,7 +166,7 @@ def fit(self) -> FitResults: Analysis. """ if self._experiment is None: - raise ValueError("No experiment is associated with this Analysis.") + raise ValueError('No experiment is associated with this Analysis.') # Create convolver once to reuse during fitting self._convolver = self._create_convolver() @@ -260,13 +262,13 @@ def plot_data_and_model( import plopp as pp if self.experiment.data is None: - raise ValueError("No data to plot. Please load data first.") + raise ValueError('No data to plot. Please load data first.') energy = self._verify_energy(energy) if energy is None: energy = self._masked_energy - data = self.experiment.data["Q", self.Q_index] + data = self.experiment.data['Q', self.Q_index] model_array = self._create_sample_scipp_array(energy=energy) component_dataset = self._create_components_dataset_single_Q( @@ -275,26 +277,24 @@ def plot_data_and_model( # Create a dataset containing the data, model, and individual # components for plotting. - data_and_model = sc.Dataset( - { - "Data": data, - "Model": model_array, - } - ) + data_and_model = sc.Dataset({ + 'Data': data, + 'Model': model_array, + }) data_and_model = sc.merge(data_and_model, component_dataset) plot_kwargs_defaults = { - "title": self.display_name, - "linestyle": {"Data": "none", "Model": "-"}, - "marker": {"Data": "o", "Model": "none"}, - "color": {"Data": "black", "Model": "red"}, - "markerfacecolor": {"Data": "none", "Model": "none"}, + 'title': self.display_name, + 'linestyle': {'Data': 'none', 'Model': '-'}, + 'marker': {'Data': 'o', 'Model': 'none'}, + 'color': {'Data': 'black', 'Model': 'red'}, + 'markerfacecolor': {'Data': 'none', 'Model': 'none'}, } if plot_components: for comp_name in component_dataset.keys(): - plot_kwargs_defaults["linestyle"][comp_name] = "--" - plot_kwargs_defaults["marker"][comp_name] = None + plot_kwargs_defaults['linestyle'][comp_name] = '--' + plot_kwargs_defaults['marker'][comp_name] = None # Overwrite defaults with any user-provided kwargs plot_kwargs_defaults.update(kwargs) @@ -320,7 +320,7 @@ def _require_Q_index(self) -> int: ValueError: If the Q index is not set. """ if self._Q_index is None: - raise ValueError("Q_index must be set.") + raise ValueError('Q_index must be set.') return self._Q_index def _on_Q_index_changed(self) -> None: @@ -349,9 +349,7 @@ def _verify_energy(self, energy: sc.Variable | None) -> sc.Variable | None: """ if energy is not None and not isinstance(energy, sc.Variable): - raise TypeError( - f"Energy must be a sc.Variable or None. Got {type(energy)}." - ) + raise TypeError(f'Energy must be a sc.Variable or None. Got {type(energy)}.') return energy def _calculate_energy_with_offset( @@ -378,8 +376,8 @@ def _calculate_energy_with_offset( energy_offset.convert_unit(str(energy.unit)) except Exception as e: raise sc.UnitError( - f"Energy and energy offset must have compatible units. " - f"Got {energy.unit} and {energy_offset.unit}." + f'Energy and energy offset must have compatible units. ' + f'Got {energy.unit} and {energy_offset.unit}.' ) from e energy_with_offset = energy.copy(deep=True) @@ -455,9 +453,7 @@ def _evaluate_components( # performance is not important. # We don't create a convolver if the resolution is empty. - resolution = self.instrument_model.resolution_model.get_component_collection( - Q_index - ) + resolution = self.instrument_model.resolution_model.get_component_collection(Q_index) if resolution.is_empty: return components.evaluate(energy_with_offset) @@ -531,10 +527,8 @@ def _evaluate_background(self, energy: sc.Variable | None = None) -> np.ndarray: np.ndarray: The evaluated background contribution. """ Q_index = self._require_Q_index() - background_components = ( - self.instrument_model.background_model.get_component_collection( - Q_index=Q_index - ) + background_components = self.instrument_model.background_model.get_component_collection( + Q_index=Q_index ) return self._evaluate_components( components=background_components, @@ -595,8 +589,8 @@ def _create_convolver( if sample_components.is_empty: return None - resolution_components = ( - self.instrument_model.resolution_model.get_component_collection(Q_index) + resolution_components = self.instrument_model.resolution_model.get_component_collection( + Q_index ) if resolution_components.is_empty: return None @@ -664,9 +658,7 @@ def _create_background_component_scipp_array( ) return self._to_scipp_array(values=values, energy=energy) - def _create_sample_scipp_array( - self, energy: sc.Variable | None = None - ) -> sc.DataArray: + def _create_sample_scipp_array(self, energy: sc.Variable | None = None) -> sc.DataArray: """Create a scipp DataArray for the full sample model including background. @@ -705,28 +697,22 @@ def _create_components_dataset_single_Q( Q_index=self.Q_index ).components - background_components = ( - self.instrument_model.background_model.get_component_collection( - Q_index=self.Q_index - ).components - ) + background_components = self.instrument_model.background_model.get_component_collection( + Q_index=self.Q_index + ).components if energy is None: energy = self._masked_energy - background = ( - self._evaluate_background(energy=energy) if add_background else None - ) + background = self._evaluate_background(energy=energy) if add_background else None for component in sample_components: scipp_arrays[component.display_name] = self._create_component_scipp_array( component=component, background=background, energy=energy ) for component in background_components: - scipp_arrays[component.display_name] = ( - self._create_background_component_scipp_array( - component=component, energy=energy - ) + scipp_arrays[component.display_name] = self._create_background_component_scipp_array( + component=component, energy=energy ) return sc.Dataset(scipp_arrays) @@ -751,9 +737,9 @@ def _to_scipp_array( if energy is None: energy = self._masked_energy return sc.DataArray( - data=sc.array(dims=["energy"], values=values), + data=sc.array(dims=['energy'], values=values), coords={ - "energy": energy, - "Q": self.Q[self.Q_index], + 'energy': energy, + 'Q': self.Q[self.Q_index], }, ) diff --git a/src/easydynamics/analysis/analysis_base.py b/src/easydynamics/analysis/analysis_base.py index 9ab308f6d..b5daa54db 100644 --- a/src/easydynamics/analysis/analysis_base.py +++ b/src/easydynamics/analysis/analysis_base.py @@ -24,7 +24,7 @@ class AnalysisBase(EasyScienceModelBase): def __init__( self, - display_name: str | None = "MyAnalysis", + display_name: str | None = 'MyAnalysis', unique_name: str | None = None, experiment: Experiment | None = None, sample_model: SampleModel | None = None, @@ -65,23 +65,21 @@ def __init__( elif isinstance(experiment, Experiment): self._experiment = experiment else: - raise TypeError("experiment must be an instance of Experiment or None.") + raise TypeError('experiment must be an instance of Experiment or None.') if sample_model is None: self._sample_model = SampleModel() elif isinstance(sample_model, SampleModel): self._sample_model = sample_model else: - raise TypeError("sample_model must be an instance of SampleModel or None.") + raise TypeError('sample_model must be an instance of SampleModel or None.') if instrument_model is None: self._instrument_model = InstrumentModel() elif isinstance(instrument_model, InstrumentModel): self._instrument_model = instrument_model else: - raise TypeError( - "instrument_model must be an instance of InstrumentModel or None." - ) + raise TypeError('instrument_model must be an instance of InstrumentModel or None.') if extra_parameters is not None: if isinstance(extra_parameters, Parameter): @@ -91,9 +89,7 @@ def __init__( ): self._extra_parameters = extra_parameters else: - raise TypeError( - "extra_parameters must be a Parameter or a list of Parameters." - ) + raise TypeError('extra_parameters must be a Parameter or a list of Parameters.') else: self._extra_parameters = [] @@ -125,7 +121,7 @@ def experiment(self, value: Experiment) -> None: """ if not isinstance(value, Experiment): - raise TypeError("experiment must be an instance of Experiment") + raise TypeError('experiment must be an instance of Experiment') self._experiment = value self._on_experiment_changed() @@ -150,7 +146,7 @@ def sample_model(self, value: SampleModel) -> None: TypeError: if value is not a SampleModel. """ if not isinstance(value, SampleModel): - raise TypeError("sample_model must be an instance of SampleModel") + raise TypeError('sample_model must be an instance of SampleModel') self._sample_model = value self._on_sample_model_changed() @@ -176,7 +172,7 @@ def instrument_model(self, value: InstrumentModel) -> None: TypeError: if value is not an InstrumentModel. """ if not isinstance(value, InstrumentModel): - raise TypeError("instrument_model must be an instance of InstrumentModel") + raise TypeError('instrument_model must be an instance of InstrumentModel') self._instrument_model = value self._on_instrument_model_changed() @@ -203,7 +199,7 @@ def Q(self, value: sc.Variable) -> None: Raises: AttributeError: If trying to set Q. """ - raise AttributeError("Q is a read-only property derived from the Experiment.") + raise AttributeError('Q is a read-only property derived from the Experiment.') @property def energy(self) -> sc.Variable | None: @@ -230,9 +226,7 @@ def energy(self, value: sc.Variable) -> None: AttributeError: If trying to set energy. """ - raise AttributeError( - "energy is a read-only property derived from the Experiment." - ) + raise AttributeError('energy is a read-only property derived from the Experiment.') @property def temperature(self) -> Parameter | None: @@ -259,9 +253,7 @@ def temperature(self, value: np.ndarray | Parameter) -> None: AttributeError: If trying to set temperature. """ - raise AttributeError( - "temperature is a read-only property derived from the SampleModel." - ) + raise AttributeError('temperature is a read-only property derived from the SampleModel.') @property def extra_parameters(self) -> list[Parameter]: @@ -292,9 +284,7 @@ def extra_parameters(self, value: Parameter | list[Parameter]) -> None: elif value is None: self._extra_parameters = [] else: - raise TypeError( - "extra_parameters must be a Parameter, a list of Parameters, or None." - ) + raise TypeError('extra_parameters must be a Parameter, a list of Parameters, or None.') ############# # Other methods @@ -341,7 +331,7 @@ def _verify_Q_index(self, Q_index: int | None) -> int | None: or Q_index < 0 or (self.Q is not None and Q_index >= len(self.Q)) ): - raise IndexError("Q_index must be a valid index for the Q values.") + raise IndexError('Q_index must be a valid index for the Q values.') return Q_index ############# @@ -354,5 +344,5 @@ def __repr__(self) -> str: Returns: str: A string representation of the Analysis. """ - return f" {self.__class__.__name__} (display_name={self.display_name}, \ - unique_name={self.unique_name})" + return f' {self.__class__.__name__} (display_name={self.display_name}, \ + unique_name={self.unique_name})' diff --git a/src/easydynamics/convolution/analytical_convolution.py b/src/easydynamics/convolution/analytical_convolution.py index 5c4b79183..761f1cec4 100644 --- a/src/easydynamics/convolution/analytical_convolution.py +++ b/src/easydynamics/convolution/analytical_convolution.py @@ -20,25 +20,25 @@ class AnalyticalConvolution(ConvolutionBase): """Analytical convolution of a ModelComponent or ComponentCollection with a ResolutionModel. - Possible analytical convolutions are any combination of - delta functions, Gaussians, Lorentzians and Voigt profiles. + Possible analytical convolutions are any combination of delta + functions, Gaussians, Lorentzians and Voigt profiles. """ # Mapping of supported component type pairs to convolution methods. # Delta functions are handled separately. _CONVOLUTIONS = { - ("Gaussian", "Gaussian"): "_convolute_gaussian_gaussian", - ("Gaussian", "Lorentzian"): "_convolute_gaussian_lorentzian", - ("Gaussian", "Voigt"): "_convolute_gaussian_voigt", - ("Lorentzian", "Lorentzian"): "_convolute_lorentzian_lorentzian", - ("Lorentzian", "Voigt"): "_convolute_lorentzian_voigt", - ("Voigt", "Voigt"): "_convolute_voigt_voigt", + ('Gaussian', 'Gaussian'): '_convolute_gaussian_gaussian', + ('Gaussian', 'Lorentzian'): '_convolute_gaussian_lorentzian', + ('Gaussian', 'Voigt'): '_convolute_gaussian_voigt', + ('Lorentzian', 'Lorentzian'): '_convolute_lorentzian_lorentzian', + ('Lorentzian', 'Voigt'): '_convolute_lorentzian_voigt', + ('Voigt', 'Voigt'): '_convolute_voigt_voigt', } def __init__( self, energy: np.ndarray | sc.Variable, - energy_unit: str | sc.Unit = "meV", + energy_unit: str | sc.Unit = 'meV', sample_components: ComponentCollection | ModelComponent | None = None, resolution_components: ComponentCollection | ModelComponent | None = None, energy_offset: Numeric | Parameter = 0.0, @@ -146,8 +146,8 @@ def _convolute_analytic_pair( if isinstance(resolution_component, DeltaFunction): raise ValueError( - "Analytical convolution with a delta function \ - in the resolution model is not supported." + 'Analytical convolution with a delta function \ + in the resolution model is not supported.' ) # Delta function + anything --> @@ -173,8 +173,8 @@ def _convolute_analytic_pair( if func_name is None: raise ValueError( - f"Analytical convolution not supported for component pair: " - f"{type(sample_component).__name__}, {type(resolution_component).__name__}" + f'Analytical convolution not supported for component pair: ' + f'{type(sample_component).__name__}, {type(resolution_component).__name__}' ) # Call the corresponding method @@ -225,9 +225,7 @@ def _convolute_gaussian_gaussian( np.ndarray: The evaluated convolution values at self.energy. """ - width = np.sqrt( - sample_component.width.value**2 + resolution_component.width.value**2 - ) + width = np.sqrt(sample_component.width.value**2 + resolution_component.width.value**2) area = sample_component.area.value * resolution_component.area.value @@ -286,8 +284,7 @@ def _convolute_gaussian_voigt( center = sample_component.center.value + resolution_component.center.value gaussian_width = np.sqrt( - sample_component.width.value**2 - + resolution_component.gaussian_width.value**2 + sample_component.width.value**2 + resolution_component.gaussian_width.value**2 ) lorentzian_width = resolution_component.lorentzian_width.value @@ -389,13 +386,11 @@ def _convolute_voigt_voigt( center = sample_component.center.value + resolution_component.center.value gaussian_width = np.sqrt( - sample_component.gaussian_width.value**2 - + resolution_component.gaussian_width.value**2 + sample_component.gaussian_width.value**2 + resolution_component.gaussian_width.value**2 ) lorentzian_width = ( - sample_component.lorentzian_width.value - + resolution_component.lorentzian_width.value + sample_component.lorentzian_width.value + resolution_component.lorentzian_width.value ) return self._voigt_eval( area=area, diff --git a/src/easydynamics/convolution/convolution.py b/src/easydynamics/convolution/convolution.py index c8461d74f..7cd742f5e 100644 --- a/src/easydynamics/convolution/convolution.py +++ b/src/easydynamics/convolution/convolution.py @@ -23,11 +23,10 @@ class Convolution(NumericalConvolutionBase): ComponentCollections with ResolutionComponents. Supports analytical convolution for pairs of analytical model - components (DeltaFunction, Gaussian, Lorentzian, Voigt), while - using numerical convolution for other components. - If temperature is provided, detailed balance correction is applied - to the sample model. In this case, all convolutions are handled - numerically. + components (DeltaFunction, Gaussian, Lorentzian, Voigt), while using + numerical convolution for other components. If temperature is + provided, detailed balance correction is applied to the sample + model. In this case, all convolutions are handled numerically. Includes a setting to normalize the detailed balance correction. Includes optional upsampling and extended range to improve accuracy of the numerical convolutions. Also warns about numerical @@ -37,16 +36,16 @@ class Convolution(NumericalConvolutionBase): # When these attributes are changed, the convolution plan # needs to be rebuilt _invalidate_plan_on_change = { - "energy", - "_energy", - "_energy_grid", - "_sample_components", - "_resolution_components", - "_temperature", - "_upsample_factor", - "_extension_factor", - "_energy_unit", - "_normalize_detailed_balance", + 'energy', + '_energy', + '_energy_grid', + '_sample_components', + '_resolution_components', + '_temperature', + '_upsample_factor', + '_extension_factor', + '_energy_unit', + '_normalize_detailed_balance', } def __init__( @@ -58,8 +57,8 @@ def __init__( upsample_factor: Numeric | None = 5, extension_factor: Numeric | None = 0.2, temperature: Parameter | Numeric | None = None, - temperature_unit: str | sc.Unit = "K", - energy_unit: str | sc.Unit = "meV", + temperature_unit: str | sc.Unit = 'K', + energy_unit: str | sc.Unit = 'meV', normalize_detailed_balance: bool = True, ) -> None: """Initialize the Convolution class. @@ -183,19 +182,19 @@ def _check_if_pair_is_analytic( if not isinstance(sample_component, ModelComponent): raise TypeError( - f"`sample_component` is an instance of {type(sample_component).__name__}, \ - but must be a ModelComponent." + f'`sample_component` is an instance of {type(sample_component).__name__}, \ + but must be a ModelComponent.' ) if not isinstance(resolution_component, ModelComponent): raise TypeError( - f"`resolution_component` is an instance of {type(resolution_component).__name__}, \ - but must be a ModelComponent." + f'`resolution_component` is an instance of {type(resolution_component).__name__}, \ + but must be a ModelComponent.' ) if isinstance(resolution_component, DeltaFunction): raise TypeError( - "resolution components contains delta functions. This is not supported." + 'resolution components contains delta functions. This is not supported.' ) analytical_types = (Gaussian, Lorentzian, Voigt) @@ -234,9 +233,7 @@ def _build_convolution_plan(self) -> None: pair_is_analytic = [] for resolution_component in self._resolution_components.components: pair_is_analytic.append( - self._check_if_pair_is_analytic( - sample_component, resolution_component - ) + self._check_if_pair_is_analytic(sample_component, resolution_component) ) # If all resolution components can be convolved analytically # with this sample component, add it to analytical @@ -304,8 +301,5 @@ def __setattr__(self, name: str, value: any) -> None: if name in self._invalidate_plan_on_change: self._convolution_plan_is_valid = False - if ( - getattr(self, "_reactions_enabled", False) - and name in self._invalidate_plan_on_change - ): + if getattr(self, '_reactions_enabled', False) and name in self._invalidate_plan_on_change: self._build_convolution_plan() diff --git a/src/easydynamics/convolution/convolution_base.py b/src/easydynamics/convolution/convolution_base.py index c5af8fe97..1ace459c0 100644 --- a/src/easydynamics/convolution/convolution_base.py +++ b/src/easydynamics/convolution/convolution_base.py @@ -11,8 +11,9 @@ class ConvolutionBase: - """Base class for convolutions of sample and resolution models. This - base class has no convolution functionality. + """Base class for convolutions of sample and resolution models. + + This base class has no convolution functionality. """ def __init__( @@ -20,7 +21,7 @@ def __init__( energy: np.ndarray | sc.Variable, sample_components: ComponentCollection | ModelComponent | None = None, resolution_components: ComponentCollection | ModelComponent | None = None, - energy_unit: str | sc.Unit = "meV", + energy_unit: str | sc.Unit = 'meV', energy_offset: Numeric | Parameter = 0.0, ) -> None: """Initialize the ConvolutionBase. @@ -43,29 +44,26 @@ def __init__( energy_offset is not a number or a Parameter, or if sample_components is not a ComponentCollection or ModelComponent, or if resolution_components is not a ComponentCollection or ModelComponent. - """ if isinstance(energy, Numeric): energy = np.array([float(energy)]) if not isinstance(energy, (np.ndarray, sc.Variable)): - raise TypeError( - f"Energy must be a numpy ndarray or a scipp Variable. Got {energy}" - ) + raise TypeError(f'Energy must be a numpy ndarray or a scipp Variable. Got {energy}') if not isinstance(energy_unit, (str, sc.Unit)): - raise TypeError("Energy_unit must be a string or sc.Unit.") + raise TypeError('Energy_unit must be a string or sc.Unit.') if isinstance(energy, np.ndarray): - energy = sc.array(dims=["energy"], values=energy, unit=energy_unit) + energy = sc.array(dims=['energy'], values=energy, unit=energy_unit) if isinstance(energy_offset, Numeric): energy_offset = Parameter( - name="energy_offset", value=float(energy_offset), unit=energy_unit + name='energy_offset', value=float(energy_offset), unit=energy_unit ) if not isinstance(energy_offset, Parameter): - raise TypeError("Energy_offset must be a number or a Parameter.") + raise TypeError('Energy_offset must be a number or a Parameter.') self._energy = energy self._energy_unit = energy_unit @@ -76,7 +74,7 @@ def __init__( or isinstance(sample_components, ModelComponent) ): raise TypeError( - f"`sample_components` is an instance of {type(sample_components).__name__}, but must be a ComponentCollection or ModelComponent." # noqa: E501 + f'`sample_components` is an instance of {type(sample_components).__name__}, but must be a ComponentCollection or ModelComponent.' # noqa: E501 ) if isinstance(sample_components, ModelComponent): sample_components = ComponentCollection(components=[sample_components]) @@ -87,12 +85,10 @@ def __init__( or isinstance(resolution_components, ModelComponent) ): raise TypeError( - f"`resolution_components` is an instance of {type(resolution_components).__name__}, but must be a ComponentCollection or ModelComponent." # noqa: E501 + f'`resolution_components` is an instance of {type(resolution_components).__name__}, but must be a ComponentCollection or ModelComponent.' # noqa: E501 ) if isinstance(resolution_components, ModelComponent): - resolution_components = ComponentCollection( - components=[resolution_components] - ) + resolution_components = ComponentCollection(components=[resolution_components]) self._resolution_components = resolution_components @property @@ -116,7 +112,7 @@ def energy_offset(self, energy_offset: Numeric | Parameter) -> None: TypeError: If energy_offset is not a number or a Parameter. """ if not isinstance(energy_offset, Parameter | Numeric): - raise TypeError("Energy_offset must be a number or a Parameter.") + raise TypeError('Energy_offset must be a number or a Parameter.') if isinstance(energy_offset, Numeric): self._energy_offset.value = float(energy_offset) @@ -148,7 +144,7 @@ def energy_with_offset(self, value: sc.Variable) -> None: read-only. """ raise AttributeError( - "Energy with offset is a read-only property derived from energy and energy_offset." + 'Energy with offset is a read-only property derived from energy and energy_offset.' ) @property @@ -179,14 +175,10 @@ def energy(self, energy: np.ndarray | sc.Variable) -> None: energy = np.array([float(energy)]) if not isinstance(energy, (np.ndarray, sc.Variable)): - raise TypeError( - "Energy must be a Number, a numpy ndarray or a scipp Variable." - ) + raise TypeError('Energy must be a Number, a numpy ndarray or a scipp Variable.') if isinstance(energy, np.ndarray): - self._energy = sc.array( - dims=["energy"], values=energy, unit=self._energy.unit - ) + self._energy = sc.array(dims=['energy'], values=energy, unit=self._energy.unit) if isinstance(energy, sc.Variable): self._energy = energy @@ -205,8 +197,8 @@ def energy_unit(self) -> str: def energy_unit(self, unit_str: str) -> None: raise AttributeError( ( - f"Unit is read-only. Use convert_unit to change the unit between allowed types " - f"or create a new {self.__class__.__name__} with the desired unit." + f'Unit is read-only. Use convert_unit to change the unit between allowed types ' + f'or create a new {self.__class__.__name__} with the desired unit.' ) ) # noqa: E501 @@ -222,7 +214,7 @@ def convert_energy_unit(self, energy_unit: str | sc.Unit) -> None: unit. """ if not isinstance(energy_unit, (str, sc.Unit)): - raise TypeError("Energy unit must be a string or scipp unit.") + raise TypeError('Energy unit must be a string or scipp unit.') old_energy = self.energy.copy() try: @@ -251,9 +243,7 @@ def sample_components(self) -> ComponentCollection | ModelComponent: return self._sample_components @sample_components.setter - def sample_components( - self, sample_components: ComponentCollection | ModelComponent - ) -> None: + def sample_components(self, sample_components: ComponentCollection | ModelComponent) -> None: """Set the sample model. Args: @@ -266,7 +256,7 @@ def sample_components( """ if not isinstance(sample_components, (ComponentCollection, ModelComponent)): raise TypeError( - f"`sample_components` is an instance of {type(sample_components).__name__}, but must be a ComponentCollection or ModelComponent." # noqa: E501 + f'`sample_components` is an instance of {type(sample_components).__name__}, but must be a ComponentCollection or ModelComponent.' # noqa: E501 ) if isinstance(sample_components, ModelComponent): @@ -300,11 +290,9 @@ def resolution_components( """ if not isinstance(resolution_components, (ComponentCollection, ModelComponent)): raise TypeError( - f"`resolution_components` is an instance of {type(resolution_components).__name__}, but must be a ComponentCollection or ModelComponent." # noqa: E501 + f'`resolution_components` is an instance of {type(resolution_components).__name__}, but must be a ComponentCollection or ModelComponent.' # noqa: E501 ) if isinstance(resolution_components, ModelComponent): - resolution_components = ComponentCollection( - components=[resolution_components] - ) + resolution_components = ComponentCollection(components=[resolution_components]) self._resolution_components = resolution_components diff --git a/src/easydynamics/convolution/numerical_convolution.py b/src/easydynamics/convolution/numerical_convolution.py index bad409221..b2f45b450 100644 --- a/src/easydynamics/convolution/numerical_convolution.py +++ b/src/easydynamics/convolution/numerical_convolution.py @@ -9,18 +9,18 @@ from easydynamics.convolution.numerical_convolution_base import NumericalConvolutionBase from easydynamics.sample_model.component_collection import ComponentCollection from easydynamics.sample_model.components.model_component import ModelComponent -from easydynamics.utils.detailed_balance import ( - _detailed_balance_factor as detailed_balance_factor, -) +from easydynamics.utils.detailed_balance import _detailed_balance_factor as detailed_balance_factor from easydynamics.utils.utils import Numeric class NumericalConvolution(NumericalConvolutionBase): """Numerical convolution of a ComponentCollection with a - ComponentCollection using FFT. Includes optional upsampling and - extended range to improve accuracy. Warns about very wide or very - narrow peaks in the models. If temperature is provided, detailed - balance correction is applied to the sample model. + ComponentCollection using FFT. + + Includes optional upsampling and extended range to improve accuracy. + Warns about very wide or very narrow peaks in the models. If + temperature is provided, detailed balance correction is applied to + the sample model. """ def __init__( @@ -32,8 +32,8 @@ def __init__( upsample_factor: Numeric | None = 5, extension_factor: Numeric | None = 0.2, temperature: Parameter | Numeric | None = None, - temperature_unit: str | sc.Unit = "K", - energy_unit: str | sc.Unit = "meV", + temperature_unit: str | sc.Unit = 'K', + energy_unit: str | sc.Unit = 'meV', normalize_detailed_balance: bool = True, ) -> None: """Initialize the NumericalConvolution object. @@ -89,11 +89,11 @@ def convolution( # Give warnings if peaks are very wide or very narrow self._check_width_thresholds( model=self.sample_components, - model_name="sample model", + model_name='sample model', ) self._check_width_thresholds( model=self.resolution_components, - model_name="resolution model", + model_name='resolution model', ) # Evaluate sample model. If called via the Convolution class, @@ -120,7 +120,7 @@ def convolution( ) # Convolution - convolved = fftconvolve(sample_vals, resolution_vals, mode="same") + convolved = fftconvolve(sample_vals, resolution_vals, mode='same') convolved *= self._energy_grid.energy_dense_step # normalize if self.upsample_factor is not None: diff --git a/src/easydynamics/convolution/numerical_convolution_base.py b/src/easydynamics/convolution/numerical_convolution_base.py index 9c5ffda24..54ccac6dc 100644 --- a/src/easydynamics/convolution/numerical_convolution_base.py +++ b/src/easydynamics/convolution/numerical_convolution_base.py @@ -28,8 +28,10 @@ class NumericalConvolutionBase(ConvolutionBase): """Base class for numerical convolutions of sample and resolution - models. Provides methods to handle upsampling, extension, and - detailed balance correction. This base class has no convolution + models. + + Provides methods to handle upsampling, extension, and detailed + balance correction. This base class has no convolution functionality. """ @@ -42,8 +44,8 @@ def __init__( upsample_factor: Numeric | None = 5, extension_factor: Numeric | None = 0.2, temperature: Parameter | Numeric | None = None, - temperature_unit: str | sc.Unit = "K", - energy_unit: str | sc.Unit = "meV", + temperature_unit: str | sc.Unit = 'K', + energy_unit: str | sc.Unit = 'meV', normalize_detailed_balance: bool = True, ) -> None: """Initialize the NumericalConvolutionBase. @@ -83,13 +85,11 @@ def __init__( energy_offset=energy_offset, ) - if temperature is not None and not isinstance( - temperature, (Numeric, Parameter) - ): - raise TypeError("Temperature must be None, a number or a Parameter.") + if temperature is not None and not isinstance(temperature, (Numeric, Parameter)): + raise TypeError('Temperature must be None, a number or a Parameter.') if not isinstance(temperature_unit, (str, sc.Unit)): - raise TypeError("Temperature_unit must be a string or sc.Unit.") + raise TypeError('Temperature_unit must be a string or sc.Unit.') self._temperature_unit = temperature_unit self._temperature = None self.temperature = temperature @@ -142,10 +142,10 @@ def upsample_factor(self, factor: Numeric | None) -> None: return if not isinstance(factor, Numeric): - raise TypeError("Upsample factor must be a numerical value or None.") + raise TypeError('Upsample factor must be a numerical value or None.') factor = float(factor) if factor <= 1.0: - raise ValueError("Upsample factor must be greater than 1.") + raise ValueError('Upsample factor must be greater than 1.') self._upsample_factor = factor @@ -185,9 +185,9 @@ def extension_factor(self, factor: Numeric) -> None: """ if not isinstance(factor, Numeric): - raise TypeError("Extension factor must be a number.") + raise TypeError('Extension factor must be a number.') if factor < 0.0: - raise ValueError("Extension factor must be non-negative.") + raise ValueError('Extension factor must be non-negative.') self._extension_factor = float(factor) # Recreate dense grid when extension factor is updated @@ -227,7 +227,7 @@ def temperature(self, temp: Parameter | Numeric | None) -> None: self._temperature.value = float(temp) else: self._temperature = Parameter( - name="temperature", + name='temperature', value=float(temp), unit=self._temperature_unit, fixed=True, @@ -235,7 +235,7 @@ def temperature(self, temp: Parameter | Numeric | None) -> None: elif isinstance(temp, Parameter): self._temperature = temp else: - raise TypeError("Temperature must be None, a float or a Parameter.") + raise TypeError('Temperature must be None, a float or a Parameter.') @property def normalize_detailed_balance(self) -> bool: @@ -264,7 +264,7 @@ def normalize_detailed_balance(self, normalize: bool) -> None: """ if not isinstance(normalize, bool): - raise TypeError("normalize_detailed_balance must be True or False.") + raise TypeError('normalize_detailed_balance must be True or False.') self._normalize_detailed_balance = normalize @@ -292,7 +292,7 @@ def _create_energy_grid( is_uniform = np.allclose(energy_diff, energy_diff[0]) if not is_uniform: raise ValueError( - "Input array `energy` must be uniformly spaced if upsample_factor is not given." # noqa: E501 + 'Input array `energy` must be uniformly spaced if upsample_factor is not given.' # noqa: E501 ) energy_dense = self.energy.values @@ -309,7 +309,7 @@ def _create_energy_grid( energy_span_dense = extended_max - extended_min if len(energy_dense) < 2: - raise ValueError("Energy array must have at least two points.") + raise ValueError('Energy array must have at least two points.') energy_dense_step = energy_dense[1] - energy_dense[0] # Handle offset for even length of energy_dense in convolution. @@ -373,29 +373,23 @@ def _check_width_thresholds( components = [model] # Treat single ModelComponent as a list for comp in components: - if hasattr(comp, "width"): - if ( - comp.width.value - > LARGE_WIDTH_THRESHOLD * self._energy_grid.energy_span_dense - ): + if hasattr(comp, 'width'): + if comp.width.value > LARGE_WIDTH_THRESHOLD * self._energy_grid.energy_span_dense: warnings.warn( f"The width of the {model_name} component '{comp.unique_name}' \ ({comp.width.value}) is large compared to the span of the input " - f"array ({self._energy_grid.energy_span_dense}). \ + f'array ({self._energy_grid.energy_span_dense}). \ This may lead to inaccuracies in the convolution. \ - Increase extension_factor to improve accuracy.", + Increase extension_factor to improve accuracy.', UserWarning, ) - if ( - comp.width.value - < SMALL_WIDTH_THRESHOLD * self._energy_grid.energy_dense_step - ): + if comp.width.value < SMALL_WIDTH_THRESHOLD * self._energy_grid.energy_dense_step: warnings.warn( f"The width of the {model_name} component '{comp.unique_name}' \ ({comp.width.value}) is small compared to the spacing of the input " - f"array ({self._energy_grid.energy_dense_step}). \ + f'array ({self._energy_grid.energy_dense_step}). \ This may lead to inaccuracies in the convolution. \ - Increase upsample_factor to improve accuracy.", + Increase upsample_factor to improve accuracy.', UserWarning, ) @@ -408,13 +402,13 @@ def __repr__(self) -> str: NumericalConvolutionBase. """ return ( - f"{self.__class__.__name__}(" - f"energy=array of shape {self.energy.values.shape},\n " - f"sample_components={repr(self.sample_components)}, \n" - f"resolution_components={repr(self.resolution_components)},\n " - f"energy_unit={self._energy_unit}, " - f"upsample_factor={self.upsample_factor}, " - f"extension_factor={self.extension_factor}, " - f"temperature={self.temperature}, " - f"normalize_detailed_balance={self.normalize_detailed_balance})" + f'{self.__class__.__name__}(' + f'energy=array of shape {self.energy.values.shape},\n ' + f'sample_components={repr(self.sample_components)}, \n' + f'resolution_components={repr(self.resolution_components)},\n ' + f'energy_unit={self._energy_unit}, ' + f'upsample_factor={self.upsample_factor}, ' + f'extension_factor={self.extension_factor}, ' + f'temperature={self.temperature}, ' + f'normalize_detailed_balance={self.normalize_detailed_balance})' ) diff --git a/src/easydynamics/experiment/experiment.py b/src/easydynamics/experiment/experiment.py index 2bb351104..9e087a6d6 100644 --- a/src/easydynamics/experiment/experiment.py +++ b/src/easydynamics/experiment/experiment.py @@ -23,7 +23,7 @@ class Experiment(NewBase): def __init__( self, - display_name: str | None = "MyExperiment", + display_name: str | None = 'MyExperiment', unique_name: str | None = None, data: sc.DataArray | str | None = None, ) -> None: @@ -54,7 +54,7 @@ def __init__( self._data = data else: raise TypeError( - f"Data must be a sc.DataArray or a filename string, not {type(data).__name__}" + f'Data must be a sc.DataArray or a filename string, not {type(data).__name__}' ) self._binned_data = ( @@ -87,7 +87,7 @@ def data(self, value: sc.DataArray) -> None: TypeError: If the value is not a sc.DataArray. """ if not isinstance(value, sc.DataArray): - raise TypeError(f"Data must be a sc.DataArray, not {type(value).__name__}") + raise TypeError(f'Data must be a sc.DataArray, not {type(value).__name__}') self._validate_coordinates(value) self._data = value self._binned_data = ( @@ -116,9 +116,7 @@ def binned_data(self, value: sc.DataArray) -> None: Raises: AttributeError: Always, since binned_data is read-only. """ - raise AttributeError( - "binned_data is a read-only property. Use rebin() to rebin the data" - ) + raise AttributeError('binned_data is a read-only property. Use rebin() to rebin the data') @property def Q(self) -> sc.Variable | None: @@ -130,7 +128,7 @@ def Q(self) -> sc.Variable | None: """ if self._binned_data is None: return None - return self._binned_data.coords["Q"] + return self._binned_data.coords['Q'] @Q.setter def Q(self, value: sc.Variable) -> None: @@ -143,7 +141,7 @@ def Q(self, value: sc.Variable) -> None: Raises: AttributeError: Always, since Q is read-only. """ - raise AttributeError("Q is a read-only property derived from the data.") + raise AttributeError('Q is a read-only property derived from the data.') @property def energy(self) -> sc.Variable | None: @@ -155,7 +153,7 @@ def energy(self) -> sc.Variable | None: """ if self._binned_data is None: return None - return self._binned_data.coords["energy"] + return self._binned_data.coords['energy'] @energy.setter def energy(self, value: sc.Variable) -> None: @@ -168,7 +166,7 @@ def energy(self, value: sc.Variable) -> None: Raises: AttributeError: Always, since energy is read-only. """ - raise AttributeError("energy is a read-only property derived from the data.") + raise AttributeError('energy is a read-only property derived from the data.') def get_masked_energy(self, Q_index: int) -> sc.Variable | None: """Get the energy values from the dataset, removing points where @@ -193,12 +191,12 @@ def get_masked_energy(self, Q_index: int) -> sc.Variable | None: or Q_index < 0 or (self.Q is not None and Q_index >= len(self.Q)) ): - raise IndexError("Q_index must be a valid index for the Q values.") + raise IndexError('Q_index must be a valid index for the Q values.') - energy = self._binned_data.coords["energy"] + energy = self._binned_data.coords['energy'] _, _, _, mask = self._extract_x_y_weights_only_finite(Q_index=Q_index) - mask_var = sc.array(dims=["energy"], values=mask) + mask_var = sc.array(dims=['energy'], values=mask) masked_energy = energy[mask_var] return masked_energy @@ -219,19 +217,19 @@ def load_hdf5(self, filename: str, display_name: str | None = None) -> None: not a string or None or if the loaded data is not a sc.DataArray. """ if not isinstance(filename, str): - raise TypeError(f"Filename must be a string, not {type(filename).__name__}") + raise TypeError(f'Filename must be a string, not {type(filename).__name__}') if display_name is not None: if not isinstance(display_name, str): raise TypeError( - f"Display name must be a string, not {type(display_name).__name__}" + f'Display name must be a string, not {type(display_name).__name__}' ) self.display_name = display_name loaded_data = sc_load_hdf5(filename) if not isinstance(loaded_data, sc.DataArray): raise TypeError( - f"Loaded data must be a sc.DataArray, not {type(loaded_data).__name__}" + f'Loaded data must be a sc.DataArray, not {type(loaded_data).__name__}' ) self._validate_coordinates(loaded_data) self.data = loaded_data @@ -250,13 +248,13 @@ def save_hdf5(self, filename: str | None = None) -> None: """ if filename is None: - filename = f"{self.unique_name}.h5" + filename = f'{self.unique_name}.h5' if not isinstance(filename, str): - raise TypeError(f"Filename must be a string, not {type(filename).__name__}") + raise TypeError(f'Filename must be a string, not {type(filename).__name__}') if self._data is None: - raise ValueError("No data to save.") + raise ValueError('No data to save.') dir_name = os.path.dirname(filename) if dir_name: @@ -286,33 +284,31 @@ def rebin(self, dimensions: dict[str, int | sc.Variable]) -> None: if not isinstance(dimensions, dict): raise TypeError( - "dimensions must be a dictionary mapping dimension names " - "to number of bins or bin values as sc.Variable." + 'dimensions must be a dictionary mapping dimension names ' + 'to number of bins or bin values as sc.Variable.' ) if self._data is None: - raise ValueError("No data to rebin. Please load data first.") + raise ValueError('No data to rebin. Please load data first.') binned_data = self._data.copy() dim_copy = dimensions.copy() for dim, value in dim_copy.items(): if not isinstance(dim, str): raise TypeError( - f"Dimension keys must be strings. Got {type(dim)} for {dim} instead." + f'Dimension keys must be strings. Got {type(dim)} for {dim} instead.' ) if dim not in self._data.dims: raise KeyError( f"Dimension '{dim}' not a valid dimension for rebinning. " - f"Should be one of {self._data.dims}." + f'Should be one of {self._data.dims}.' ) - if ( - isinstance(value, float) and value.is_integer() - ): # I allow eg. 2.0 as well as 2 + if isinstance(value, float) and value.is_integer(): # I allow eg. 2.0 as well as 2 value = int(value) # This line can be removed when scipp resize support # resizing with coordinates dimensions[dim] = value if not (isinstance(value, int) or isinstance(value, sc.Variable)): raise TypeError( - f"Dimension values must be integers or sc.Variable. " + f'Dimension values must be integers or sc.Variable. ' f"Got {type(value)} for dimension '{dim}' instead." ) binned_data = binned_data.bin({dim: value}) @@ -338,15 +334,13 @@ def plot_data(self, slicer: bool = False, **kwargs: dict) -> None: """ if self._binned_data is None: - raise ValueError("No data to plot. Please load data first.") + raise ValueError('No data to plot. Please load data first.') if not _in_notebook(): - raise RuntimeError( - "plot_data() can only be used in a Jupyter notebook environment." - ) + raise RuntimeError('plot_data() can only be used in a Jupyter notebook environment.') plot_kwargs_defaults = { - "title": self.display_name, + 'title': self.display_name, } # Overwrite defaults with any user-provided kwargs plot_kwargs_defaults.update(kwargs) @@ -357,7 +351,7 @@ def plot_data(self, slicer: bool = False, **kwargs: dict) -> None: ) else: fig = pp.plot( - self._binned_data.transpose(dims=["energy", "Q"]), + self._binned_data.transpose(dims=['energy', 'Q']), **plot_kwargs_defaults, ) return fig @@ -378,9 +372,9 @@ def _validate_coordinates(data: sc.DataArray) -> None: ValueError: If required coordinates are missing. """ if not isinstance(data, sc.DataArray): - raise TypeError("Data must be a sc.DataArray.") + raise TypeError('Data must be a sc.DataArray.') - required_coords = ["Q", "energy"] + required_coords = ['Q', 'energy'] for coord in required_coords: if coord not in data.coords: raise ValueError(f"Data is missing required coordinate: '{coord}'") @@ -401,9 +395,7 @@ def _convert_to_bin_centers(self, data: sc.DataArray) -> sc.DataArray: data = data.assign_coords({dim: sc.midpoints(coord)}) return data - def _extract_x_y_var( - self, Q_index: int - ) -> tuple[np.ndarray, np.ndarray, np.ndarray]: + def _extract_x_y_var(self, Q_index: int) -> tuple[np.ndarray, np.ndarray, np.ndarray]: """Extract the x, y, and weights arrays from the experiment for the given Q index. @@ -415,8 +407,8 @@ def _extract_x_y_var( variances arrays extracted from the experiment for the given Q index. """ - data = self.binned_data["Q", Q_index] - x = data.coords["energy"].values + data = self.binned_data['Q', Q_index] + x = data.coords['energy'].values y = data.values var = data.variances return x, y, var @@ -455,7 +447,7 @@ def _extract_x_y_weights_only_finite( var = var[mask] if np.any(var == 0): - raise ValueError("Cannot compute weights: some variances are zero.") + raise ValueError('Cannot compute weights: some variances are zero.') weights = 1.0 / var**0.5 @@ -472,15 +464,15 @@ def __repr__(self) -> str: str: A string representation of the Experiment object. """ - return f"Experiment `{self.unique_name}` with data: {self._data}" + return f'Experiment `{self.unique_name}` with data: {self._data}' - def __copy__(self) -> "Experiment": + def __copy__(self) -> 'Experiment': """Return a copy of the object. Returns: Experiment: A copy of the Experiment object. """ - temp = self.to_dict(skip=["unique_name"]) + temp = self.to_dict(skip=['unique_name']) new_obj = self.__class__.from_dict(temp) new_obj.data = self.data.copy() if self.data is not None else None return new_obj diff --git a/src/easydynamics/sample_model/background_model.py b/src/easydynamics/sample_model/background_model.py index e2a1adde6..2f4645f5a 100644 --- a/src/easydynamics/sample_model/background_model.py +++ b/src/easydynamics/sample_model/background_model.py @@ -17,9 +17,9 @@ class BackgroundModel(ModelBase): def __init__( self, - display_name: str | None = "MyBackgroundModel", + display_name: str | None = 'MyBackgroundModel', unique_name: str | None = None, - unit: str | sc.Unit = "meV", + unit: str | sc.Unit = 'meV', components: ModelComponent | ComponentCollection | None = None, Q: Q_type | None = None, ) -> None: diff --git a/src/easydynamics/sample_model/component_collection.py b/src/easydynamics/sample_model/component_collection.py index 42a8ec660..24548bf50 100644 --- a/src/easydynamics/sample_model/component_collection.py +++ b/src/easydynamics/sample_model/component_collection.py @@ -24,8 +24,8 @@ class ComponentCollection(ModelBase): def __init__( self, - unit: str | sc.Unit = "meV", - display_name: str | None = "MyComponentCollection", + unit: str | sc.Unit = 'meV', + display_name: str | None = 'MyComponentCollection', unique_name: str | None = None, components: list[ModelComponent] | None = None, ) -> None: @@ -49,7 +49,7 @@ def __init__( if unit is not None and not isinstance(unit, (str, sc.Unit)): raise TypeError( - f"unit must be None, a string, or a scipp Unit, got {type(unit).__name__}" + f'unit must be None, a string, or a scipp Unit, got {type(unit).__name__}' ) self._unit = unit self._components = [] @@ -57,9 +57,7 @@ def __init__( # Add initial components if provided. Used for serialization. if components is not None: if not isinstance(components, list): - raise TypeError( - "components must be a list of ModelComponent instances." - ) + raise TypeError('components must be a list of ModelComponent instances.') for comp in components: self.append_component(comp) @@ -90,12 +88,12 @@ def components(self, components: list[ModelComponent]) -> None: """ if not isinstance(components, list): - raise TypeError("components must be a list of ModelComponent instances.") + raise TypeError('components must be a list of ModelComponent instances.') for comp in components: if not isinstance(comp, ModelComponent): raise TypeError( - "All items in components must be instances of ModelComponent. " - f"Got {type(comp).__name__} instead." + 'All items in components must be instances of ModelComponent. ' + f'Got {type(comp).__name__} instead.' ) self._components = components @@ -122,8 +120,8 @@ def is_empty(self, value: bool) -> None: AttributeError: Always raised since is_empty is read-only. """ raise AttributeError( - "is_empty is a read-only property that indicates " - "whether the collection has components." + 'is_empty is a read-only property that indicates ' + 'whether the collection has components.' ) @property @@ -149,8 +147,8 @@ def unit(self, unit_str: str) -> None: raise AttributeError( ( - f"Unit is read-only. Use convert_unit to change the unit between allowed types " - f"or create a new {self.__class__.__name__} with the desired unit." + f'Unit is read-only. Use convert_unit to change the unit between allowed types ' + f'or create a new {self.__class__.__name__} with the desired unit.' ) ) # noqa: E501 @@ -168,9 +166,7 @@ def convert_unit(self, unit: str | sc.Unit) -> None: """ if not isinstance(unit, (str, sc.Unit)): - raise TypeError( - f"Unit must be a string or sc.Unit, got {type(unit).__name__}" - ) + raise TypeError(f'Unit must be a string or sc.Unit, got {type(unit).__name__}') old_unit = self._unit @@ -191,9 +187,7 @@ def convert_unit(self, unit: str | sc.Unit) -> None: # Component management # ------------------------------------------------------------------ - def append_component( - self, component: ModelComponent | "ComponentCollection" - ) -> None: + def append_component(self, component: ModelComponent | 'ComponentCollection') -> None: """Append a model component or the components from another ComponentCollection to this ComponentCollection. @@ -210,8 +204,8 @@ def append_component( """ if not isinstance(component, (ModelComponent, ComponentCollection)): raise TypeError( - "Component must be an instance of ModelComponent or ComponentCollection. " - f"Got {type(component).__name__} instead." + 'Component must be an instance of ModelComponent or ComponentCollection. ' + f'Got {type(component).__name__} instead.' ) if isinstance(component, ModelComponent): components = (component,) @@ -222,7 +216,7 @@ def append_component( if comp in self._components: raise ValueError( f"Component '{comp.unique_name}' is already in the collection. " - f"Existing components: {self.list_component_names()}" + f'Existing components: {self.list_component_names()}' ) self._components.append(comp) @@ -240,7 +234,7 @@ def remove_component(self, unique_name: str) -> None: """ if not isinstance(unique_name, str): - raise TypeError("Component name must be a string.") + raise TypeError('Component name must be a string.') for comp in self._components: if comp.unique_name == unique_name: @@ -249,8 +243,8 @@ def remove_component(self, unique_name: str) -> None: raise KeyError( f"No component named '{unique_name}' exists. " - f"Did you accidentally use the display_name? " - f"Here is a list of the components in the collection: {self.list_component_names()}" + f'Did you accidentally use the display_name? ' + f'Here is a list of the components in the collection: {self.list_component_names()}' ) @property @@ -264,8 +258,7 @@ def components(self) -> list[ModelComponent]: @components.setter def components(self, components: list[ModelComponent]) -> None: - """ - Set the components in the collection. + """Set the components in the collection. Args: components (list[ModelComponent]): The new components in the collection @@ -274,20 +267,20 @@ def components(self, components: list[ModelComponent]) -> None: TypeError: If components is not a list of ModelComponent """ if not isinstance(components, list): - raise TypeError("components must be a list of ModelComponent instances.") + raise TypeError('components must be a list of ModelComponent instances.') for comp in components: if not isinstance(comp, ModelComponent): raise TypeError( - "All items in components must be instances of ModelComponent. " - f"Got {type(comp).__name__} instead." + 'All items in components must be instances of ModelComponent. ' + f'Got {type(comp).__name__} instead.' ) self._components = components @property def is_empty(self) -> bool: - """ - Returns True if the collection has no components, otherwise False + """Returns True if the collection has no components, otherwise + False. Returns: bool: True if the collection has no components, otherwise False @@ -296,8 +289,7 @@ def is_empty(self) -> bool: @is_empty.setter def is_empty(self, value: bool) -> None: - """ - is_empty is read-only. + """is_empty is read-only. Args: value (bool): ignored. @@ -306,8 +298,8 @@ def is_empty(self, value: bool) -> None: AttributeError: Always raised since is_empty is read-only """ raise AttributeError( - "is_empty is a read-only property that indicates " - "whether the collection has components." + 'is_empty is a read-only property that indicates ' + 'whether the collection has components.' ) def list_component_names(self) -> List[str]: @@ -334,27 +326,27 @@ def normalize_area(self) -> None: would prevent normalization. """ if not self.components: - raise ValueError("No components in the model to normalize.") + raise ValueError('No components in the model to normalize.') area_params = [] - total_area = Parameter(name="total_area", value=0.0, unit=self._unit) + total_area = Parameter(name='total_area', value=0.0, unit=self._unit) for component in self.components: - if hasattr(component, "area"): + if hasattr(component, 'area'): area_params.append(component.area) total_area += component.area else: warnings.warn( f"Component '{component.unique_name}' does not have an 'area' attribute " - f"and will be skipped in normalization.", + f'and will be skipped in normalization.', UserWarning, ) if total_area.value == 0: - raise ValueError("Total area is zero; cannot normalize.") + raise ValueError('Total area is zero; cannot normalize.') if not np.isfinite(total_area.value): - raise ValueError("Total area is not finite; cannot normalize.") + raise ValueError('Total area is not finite; cannot normalize.') for param in area_params: param.value /= total_area.value @@ -370,15 +362,9 @@ def get_all_variables(self) -> list[DescriptorBase]: list[DescriptorBase]: List of parameters in the component. """ - return [ - var - for component in self.components - for var in component.get_all_variables() - ] + return [var for component in self.components for var in component.get_all_variables()] - def evaluate( - self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray - ) -> np.ndarray: + def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray: """Evaluate the sum of all components. Args: @@ -414,13 +400,11 @@ def evaluate_component( in the collection. """ if not self.components: - raise ValueError("No components in the model to evaluate.") + raise ValueError('No components in the model to evaluate.') if not isinstance(unique_name, str): raise TypeError( - ( - f"Component unique name must be a string, got {type(unique_name)} instead." - ) + (f'Component unique name must be a string, got {type(unique_name)} instead.') ) matches = [comp for comp in self.components if comp.unique_name == unique_name] @@ -474,8 +458,6 @@ def __repr__(self) -> str: Returns: str: String representation of the ComponentCollection. """ - comp_names = ( - ", ".join(c.unique_name for c in self.components) or "No components" - ) + comp_names = ', '.join(c.unique_name for c in self.components) or 'No components' return f"" diff --git a/src/easydynamics/sample_model/components/damped_harmonic_oscillator.py b/src/easydynamics/sample_model/components/damped_harmonic_oscillator.py index 65679b534..81a766f74 100644 --- a/src/easydynamics/sample_model/components/damped_harmonic_oscillator.py +++ b/src/easydynamics/sample_model/components/damped_harmonic_oscillator.py @@ -30,8 +30,8 @@ def __init__( area: Numeric | Parameter = 1.0, center: Numeric | Parameter = 1.0, width: Numeric | Parameter = 1.0, - unit: str | sc.Unit = "meV", - display_name: str | None = "DampedHarmonicOscillator", + unit: str | sc.Unit = 'meV', + display_name: str | None = 'DampedHarmonicOscillator', unique_name: str | None = None, ) -> None: """Initialize the Damped Harmonic Oscillator. @@ -56,9 +56,7 @@ def __init__( ) # These methods live in ValidationMixin - area = self._create_area_parameter( - area=area, name=display_name, unit=self._unit - ) + area = self._create_area_parameter(area=area, name=display_name, unit=self._unit) center = self._create_center_parameter( center=center, name=display_name, @@ -67,9 +65,7 @@ def __init__( enforce_minimum_center=True, ) - width = self._create_width_parameter( - width=width, name=display_name, unit=self._unit - ) + width = self._create_width_parameter(width=width, name=display_name, unit=self._unit) self._area = area self._center = center @@ -95,7 +91,7 @@ def area(self, value: Numeric) -> None: TypeError: If the value is not a number. """ if not isinstance(value, Numeric): - raise TypeError("area must be a number") + raise TypeError('area must be a number') self._area.value = value @property @@ -119,10 +115,10 @@ def center(self, value: Numeric) -> None: ValueError: If the value is not positive. """ if not isinstance(value, Numeric): - raise TypeError("center must be a number") + raise TypeError('center must be a number') if float(value) <= 0: - raise ValueError("center must be positive") + raise ValueError('center must be positive') self._center.value = value @property @@ -146,16 +142,14 @@ def width(self, value: Numeric) -> None: ValueError: If the value is not positive. """ if not isinstance(value, Numeric): - raise TypeError("width must be a number") + raise TypeError('width must be a number') if float(value) <= 0: - raise ValueError("width must be positive") + raise ValueError('width must be positive') self._width.value = value - def evaluate( - self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray - ) -> np.ndarray: + def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray: r"""Evaluate the Damped Harmonic Oscillator at the given x values. @@ -180,9 +174,7 @@ def evaluate( normalization = 2 * self.center.value**2 * self.width.value / np.pi # No division by zero here, width>0 enforced in setter - denominator = (x**2 - self.center.value**2) ** 2 + ( - 2 * self.width.value * x - ) ** 2 + denominator = (x**2 - self.center.value**2) ** 2 + (2 * self.width.value * x) ** 2 return self.area.value * normalization / (denominator) @@ -194,5 +186,7 @@ def __repr__(self) -> str: str: A string representation of the Damped Harmonic Oscillator. """ - return f"DampedHarmonicOscillator(display_name = {self.display_name}, unit = {self._unit},\n \ - area = {self.area},\n center = {self.center},\n width = {self.width})" + return ( + f'DampedHarmonicOscillator(display_name = {self.display_name}, unit = {self._unit},\n \ + area = {self.area},\n center = {self.center},\n width = {self.width})' + ) diff --git a/src/easydynamics/sample_model/components/delta_function.py b/src/easydynamics/sample_model/components/delta_function.py index 08b81c4e7..4dbf80444 100644 --- a/src/easydynamics/sample_model/components/delta_function.py +++ b/src/easydynamics/sample_model/components/delta_function.py @@ -28,8 +28,8 @@ def __init__( self, center: Numeric | Parameter | None = None, area: Numeric | Parameter = 1.0, - unit: str | sc.Unit = "meV", - display_name: str | None = "DeltaFunction", + unit: str | sc.Unit = 'meV', + display_name: str | None = 'DeltaFunction', unique_name: str | None = None, ) -> None: """Initialize the Delta function. @@ -51,9 +51,7 @@ def __init__( ) # These methods live in ValidationMixin - area = self._create_area_parameter( - area=area, name=display_name, unit=self._unit - ) + area = self._create_area_parameter(area=area, name=display_name, unit=self._unit) center = self._create_center_parameter( center=center, name=display_name, fix_if_none=True, unit=self._unit ) @@ -83,7 +81,7 @@ def area(self, value: Numeric) -> None: """ if not isinstance(value, Numeric): - raise TypeError("area must be a number") + raise TypeError('area must be a number') self._area.value = value @property @@ -112,12 +110,10 @@ def center(self, value: Numeric | None) -> None: value = 0.0 self._center.fixed = True if not isinstance(value, Numeric): - raise TypeError("center must be a number") + raise TypeError('center must be a number') self._center.value = value - def evaluate( - self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray - ) -> np.ndarray: + def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray: """Evaluate the Delta function at the given x values. The Delta function evaluates to zero everywhere, except at the @@ -169,5 +165,5 @@ def __repr__(self) -> str: str: A string representation of the Delta function. """ - return f"DeltaFunction(unique_name = {self.unique_name}, unit = {self._unit},\n \ - area = {self.area},\n center = {self.center}" + return f'DeltaFunction(unique_name = {self.unique_name}, unit = {self._unit},\n \ + area = {self.area},\n center = {self.center}' diff --git a/src/easydynamics/sample_model/components/exponential.py b/src/easydynamics/sample_model/components/exponential.py index f7c30b91e..14e82c229 100644 --- a/src/easydynamics/sample_model/components/exponential.py +++ b/src/easydynamics/sample_model/components/exponential.py @@ -18,9 +18,7 @@ class Exponential(CreateParametersMixin, ModelComponent): The intensity is given by - $$ - I(x) = A e^{B (x-x_0)}, - $$ + $$ I(x) = A e^{B (x-x_0)}, $$ where $A$ is the amplitude, $x_0$ is the center, and $B$ describes the rate of decay or growth. @@ -31,8 +29,8 @@ def __init__( amplitude: Numeric | Parameter = 1.0, center: Numeric | Parameter | None = None, rate: Numeric | Parameter = 1.0, - unit: str | sc.Unit = "meV", - display_name: str | None = "Exponential", + unit: str | sc.Unit = 'meV', + display_name: str | None = 'Exponential', unique_name: str | None = None, ) -> None: """Initialize the Exponential component. @@ -61,14 +59,14 @@ def __init__( ) if not isinstance(amplitude, (Parameter, Numeric)): - raise TypeError("amplitude must be a number or a Parameter.") + raise TypeError('amplitude must be a number or a Parameter.') if isinstance(amplitude, Numeric): if not np.isfinite(amplitude): - raise ValueError("amplitude must be a finite number or a Parameter") + raise ValueError('amplitude must be a finite number or a Parameter') amplitude = Parameter( - name=display_name + " amplitude", value=float(amplitude), unit=unit + name=display_name + ' amplitude', value=float(amplitude), unit=unit ) center = self._create_center_parameter( @@ -76,15 +74,13 @@ def __init__( ) if not isinstance(rate, (Parameter, Numeric)): - raise TypeError("rate must be a number or a Parameter.") + raise TypeError('rate must be a number or a Parameter.') if isinstance(rate, Numeric): if not np.isfinite(rate): - raise ValueError("rate must be a finite number or a Parameter") + raise ValueError('rate must be a finite number or a Parameter') - rate = Parameter( - name=display_name + " rate", value=float(rate), unit="1/" + str(unit) - ) + rate = Parameter(name=display_name + ' rate', value=float(rate), unit='1/' + str(unit)) self._amplitude = amplitude self._center = center @@ -112,7 +108,7 @@ def amplitude(self, value: Numeric) -> None: """ if not isinstance(value, Numeric): - raise TypeError("amplitude must be a number") + raise TypeError('amplitude must be a number') self._amplitude.value = value @property @@ -142,7 +138,7 @@ def center(self, value: Numeric | None) -> None: self._center.fixed = True if not isinstance(value, Numeric): - raise TypeError("center must be a number") + raise TypeError('center must be a number') self._center.value = value @property @@ -166,7 +162,7 @@ def rate(self, value: Numeric) -> None: TypeError: If the value is not a number. """ if not isinstance(value, Numeric): - raise TypeError("rate must be a number") + raise TypeError('rate must be a number') self._rate.value = value @@ -212,21 +208,21 @@ def convert_unit(self, unit: str | sc.Unit) -> None: """ if not isinstance(unit, (str, sc.Unit)): - raise TypeError("unit must be a string or sc.Unit") + raise TypeError('unit must be a string or sc.Unit') old_unit = self._unit pars = [self.amplitude, self.center] try: for p in pars: p.convert_unit(unit) - self.rate.convert_unit("1/" + str(unit)) + self.rate.convert_unit('1/' + str(unit)) self._unit = unit except Exception as e: # Attempt to rollback on failure try: for p in pars: p.convert_unit(old_unit) - self.rate.convert_unit("1/" + str(old_unit)) + self.rate.convert_unit('1/' + str(old_unit)) except Exception: # noqa: S110 pass # Best effort rollback raise e @@ -238,5 +234,5 @@ def __repr__(self) -> str: str: A string representation of the Exponential. """ - return f"Exponential(unique_name = {self.unique_name}, unit = {self._unit},\n \ - amplitude = {self.amplitude},\n center = {self.center},\n rate = {self.rate})" + return f'Exponential(unique_name = {self.unique_name}, unit = {self._unit},\n \ + amplitude = {self.amplitude},\n center = {self.center},\n rate = {self.rate})' diff --git a/src/easydynamics/sample_model/components/gaussian.py b/src/easydynamics/sample_model/components/gaussian.py index 8216586f0..892577e17 100644 --- a/src/easydynamics/sample_model/components/gaussian.py +++ b/src/easydynamics/sample_model/components/gaussian.py @@ -38,8 +38,8 @@ def __init__( area: Numeric | Parameter = 1.0, center: Numeric | Parameter | None = None, width: Numeric | Parameter = 1.0, - unit: str | sc.Unit = "meV", - display_name: str | None = "Gaussian", + unit: str | sc.Unit = 'meV', + display_name: str | None = 'Gaussian', unique_name: str | None = None, ) -> None: """Initialize the Gaussian component. @@ -62,15 +62,11 @@ def __init__( ) # These methods live in ValidationMixin - area = self._create_area_parameter( - area=area, name=display_name, unit=self._unit - ) + area = self._create_area_parameter(area=area, name=display_name, unit=self._unit) center = self._create_center_parameter( center=center, name=display_name, fix_if_none=True, unit=self._unit ) - width = self._create_width_parameter( - width=width, name=display_name, unit=self._unit - ) + width = self._create_width_parameter(width=width, name=display_name, unit=self._unit) self._area = area self._center = center @@ -98,7 +94,7 @@ def area(self, value: Numeric) -> None: """ if not isinstance(value, Numeric): - raise TypeError("area must be a number") + raise TypeError('area must be a number') self._area.value = value @property @@ -127,7 +123,7 @@ def center(self, value: Numeric | None) -> None: value = 0.0 self._center.fixed = True if not isinstance(value, Numeric): - raise TypeError("center must be a number") + raise TypeError('center must be a number') self._center.value = value @property @@ -152,10 +148,10 @@ def width(self, value: Numeric) -> None: ValueError: If the value is not positive. """ if not isinstance(value, Numeric): - raise TypeError("width must be a number") + raise TypeError('width must be a number') if float(value) <= 0: - raise ValueError("width must be positive") + raise ValueError('width must be positive') self._width.value = value @@ -203,5 +199,5 @@ def __repr__(self) -> str: str: A string representation of the Gaussian. """ - return f"Gaussian(unique_name = {self.unique_name}, unit = {self._unit},\n \ - area = {self.area},\n center = {self.center},\n width = {self.width})" + return f'Gaussian(unique_name = {self.unique_name}, unit = {self._unit},\n \ + area = {self.area},\n center = {self.center},\n width = {self.width})' diff --git a/src/easydynamics/sample_model/components/lorentzian.py b/src/easydynamics/sample_model/components/lorentzian.py index 6a0375db6..8f5697bd5 100644 --- a/src/easydynamics/sample_model/components/lorentzian.py +++ b/src/easydynamics/sample_model/components/lorentzian.py @@ -32,8 +32,8 @@ def __init__( area: Numeric | Parameter = 1.0, center: Numeric | Parameter | None = None, width: Numeric | Parameter = 1.0, - unit: str | sc.Unit = "meV", - display_name: str | None = "Lorentzian", + unit: str | sc.Unit = 'meV', + display_name: str | None = 'Lorentzian', unique_name: str | None = None, ) -> None: """Initialize the Lorentzian component. @@ -57,15 +57,11 @@ def __init__( ) # These methods live in ValidationMixin - area = self._create_area_parameter( - area=area, name=display_name, unit=self._unit - ) + area = self._create_area_parameter(area=area, name=display_name, unit=self._unit) center = self._create_center_parameter( center=center, name=display_name, fix_if_none=True, unit=self._unit ) - width = self._create_width_parameter( - width=width, name=display_name, unit=self._unit - ) + width = self._create_width_parameter(width=width, name=display_name, unit=self._unit) self._area = area self._center = center @@ -91,7 +87,7 @@ def area(self, value: Numeric) -> None: TypeError: If the value is not a number. """ if not isinstance(value, Numeric): - raise TypeError("area must be a number") + raise TypeError('area must be a number') self._area.value = value @property @@ -119,7 +115,7 @@ def center(self, value: Numeric | None) -> None: value = 0.0 self._center.fixed = True if not isinstance(value, Numeric): - raise TypeError("center must be a number") + raise TypeError('center must be a number') self._center.value = value @property @@ -144,15 +140,13 @@ def width(self, value: Numeric) -> None: ValueError: If the value is not positive. """ if not isinstance(value, Numeric): - raise TypeError("width must be a number") + raise TypeError('width must be a number') if float(value) <= 0: - raise ValueError("width must be positive") + raise ValueError('width must be positive') self._width.value = value - def evaluate( - self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray - ) -> np.ndarray: + def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray: r"""Evaluate the Lorentzian at the given x values. If x is a scipp Variable, the unit of the Lorentzian will be @@ -188,5 +182,5 @@ def __repr__(self) -> str: Returns: str: A string representation of the Lorentzian. """ - return f"Lorentzian(unique_name = {self.unique_name}, unit = {self._unit},\n \ - area = {self.area},\n center = {self.center},\n width = {self.width})" + return f'Lorentzian(unique_name = {self.unique_name}, unit = {self._unit},\n \ + area = {self.area},\n center = {self.center},\n width = {self.width})' diff --git a/src/easydynamics/sample_model/components/mixins.py b/src/easydynamics/sample_model/components/mixins.py index 22f9cc221..97c324224 100644 --- a/src/easydynamics/sample_model/components/mixins.py +++ b/src/easydynamics/sample_model/components/mixins.py @@ -27,7 +27,7 @@ def _create_area_parameter( self, area: Numeric | Parameter, name: str, - unit: str | sc.Unit = "meV", + unit: str | sc.Unit = 'meV', minimum_area: float = MINIMUM_AREA, ) -> Parameter: """Validate and convert a number to a Parameter describing the @@ -52,17 +52,17 @@ def _create_area_parameter( Parameter has a non-finite value. """ if not isinstance(area, (Parameter, Numeric)): - raise TypeError("area must be a number or a Parameter.") + raise TypeError('area must be a number or a Parameter.') if isinstance(area, Numeric): if not np.isfinite(area): - raise ValueError("area must be a finite number or a Parameter") + raise ValueError('area must be a finite number or a Parameter') - area = Parameter(name=name + " area", value=float(area), unit=unit) + area = Parameter(name=name + ' area', value=float(area), unit=unit) if area.value < 0: warnings.warn( - f"The area of {name} is negative, which may not be physically meaningful." + f'The area of {name} is negative, which may not be physically meaningful.' ) else: area.min = minimum_area @@ -74,7 +74,7 @@ def _create_center_parameter( center: Numeric | Parameter | None, name: str, fix_if_none: bool, - unit: str | sc.Unit = "meV", + unit: str | sc.Unit = 'meV', enforce_minimum_center: bool = False, ) -> Parameter: """Validate and convert a number to a Parameter describing the @@ -101,20 +101,20 @@ def _create_center_parameter( center is a Parameter but has a non-finite value. """ if center is not None and not isinstance(center, (Numeric, Parameter)): - raise TypeError("center must be None, a number, or a Parameter.") + raise TypeError('center must be None, a number, or a Parameter.') if center is None: center = Parameter( - name=name + " center", + name=name + ' center', value=0.0, unit=unit, fixed=fix_if_none, ) elif isinstance(center, Numeric): if not np.isfinite(center): - raise ValueError("center must be None, a finite number or a Parameter") + raise ValueError('center must be None, a finite number or a Parameter') - center = Parameter(name=name + " center", value=float(center), unit=unit) + center = Parameter(name=name + ' center', value=float(center), unit=unit) if enforce_minimum_center: center.min = DHO_MINIMUM_CENTER return center @@ -123,8 +123,8 @@ def _create_width_parameter( self, width: Numeric | Parameter, name: str, - param_name: str = "width", - unit: str | sc.Unit = "meV", + param_name: str = 'width', + unit: str | sc.Unit = 'meV', minimum_width: float = MINIMUM_WIDTH, ) -> Parameter: """Validate and convert a number to a Parameter describing the @@ -146,18 +146,18 @@ def _create_width_parameter( ValueError: If width is non-positive. """ if not isinstance(width, (Numeric, Parameter)): - raise TypeError(f"{param_name} must be a number or a Parameter.") + raise TypeError(f'{param_name} must be a number or a Parameter.') if isinstance(width, Numeric): if not np.isfinite(width): - raise ValueError(f"{param_name} must be a finite number or a Parameter") + raise ValueError(f'{param_name} must be a finite number or a Parameter') if float(width) < minimum_width: raise ValueError( - f"The {param_name} of a {self.__class__.__name__} must be greater than zero." + f'The {param_name} of a {self.__class__.__name__} must be greater than zero.' ) width = Parameter( - name=name + " " + param_name, + name=name + ' ' + param_name, value=float(width), unit=unit, min=minimum_width, @@ -165,7 +165,7 @@ def _create_width_parameter( else: if width.value <= 0: raise ValueError( - f"The {param_name} of a {self.__class__.__name__} must be greater than zero." + f'The {param_name} of a {self.__class__.__name__} must be greater than zero.' ) width.min = minimum_width diff --git a/src/easydynamics/sample_model/components/model_component.py b/src/easydynamics/sample_model/components/model_component.py index 03567f52f..176d32e48 100644 --- a/src/easydynamics/sample_model/components/model_component.py +++ b/src/easydynamics/sample_model/components/model_component.py @@ -19,7 +19,7 @@ class ModelComponent(ModelBase): def __init__( self, - unit: str | sc.Unit = "meV", + unit: str | sc.Unit = 'meV', display_name: str | None = None, unique_name: str | None = None, ) -> None: @@ -59,8 +59,8 @@ def unit(self, unit_str: str) -> None: """ raise AttributeError( ( - f"Unit is read-only. Use convert_unit to change the unit between allowed types " - f"or create a new {self.__class__.__name__} with the desired unit." + f'Unit is read-only. Use convert_unit to change the unit between allowed types ' + f'or create a new {self.__class__.__name__} with the desired unit.' ) ) # noqa: E501 @@ -102,10 +102,10 @@ def _prepare_x_for_evaluate( coords = dict(x.coords) ncoords = len(coords) if ncoords != 1: - coord_names = ", ".join(coords.keys()) + coord_names = ', '.join(coords.keys()) raise ValueError( - f"scipp.DataArray must have exactly one coordinate to be used as input `x`. " - f"Found {ncoords} coordinates: {coord_names}." + f'scipp.DataArray must have exactly one coordinate to be used as input `x`. ' + f'Found {ncoords} coordinates: {coord_names}.' ) # get the coordinate, it's a sc.Variable coord_name, coord_obj = next(iter(coords.items())) @@ -123,15 +123,15 @@ def _prepare_x_for_evaluate( self.convert_unit(x.unit.name) except Exception as e: raise UnitError( - f"Input x has unit {x.unit}, but {self.__class__.__name__} component \ + f'Input x has unit {x.unit}, but {self.__class__.__name__} component \ has unit {self._unit}. \ - Failed to convert {self.__class__.__name__} to {x.unit}." + Failed to convert {self.__class__.__name__} to {x.unit}.' ) from e warnings.warn( - f"Input x has unit {x.unit}, but {self.__class__.__name__} component \ + f'Input x has unit {x.unit}, but {self.__class__.__name__} component \ has unit {self_unit_for_warning}. \ - Converting {self.__class__.__name__} to {x.unit}." + Converting {self.__class__.__name__} to {x.unit}.' ) else: x_in = x @@ -142,10 +142,10 @@ def _prepare_x_for_evaluate( x_in = np.array(x_in) if any(np.isnan(x_in)): - raise ValueError("Input x contains NaN values.") + raise ValueError('Input x contains NaN values.') if any(np.isinf(x_in)): - raise ValueError("Input x contains infinite values.") + raise ValueError('Input x contains infinite values.') return np.sort(x_in) @@ -161,7 +161,7 @@ def validate_unit(unit: str | sc.Unit | None) -> None: """ if unit is not None and not isinstance(unit, (str, sc.Unit)): raise TypeError( - f"unit must be None, a string, or a scipp Unit, got {type(unit).__name__}" + f'unit must be None, a string, or a scipp Unit, got {type(unit).__name__}' ) def convert_unit(self, unit: str | sc.Unit) -> None: @@ -176,9 +176,7 @@ def convert_unit(self, unit: str | sc.Unit) -> None: with the component's parameters. """ if not isinstance(unit, (str, sc.Unit)): - raise TypeError( - f"Unit must be a string or sc.Unit, got {type(unit).__name__}" - ) + raise TypeError(f'Unit must be a string or sc.Unit, got {type(unit).__name__}') old_unit = self._unit pars = self.get_all_parameters() @@ -190,16 +188,14 @@ def convert_unit(self, unit: str | sc.Unit) -> None: # Attempt to rollback on failure try: for p in pars: - if hasattr(p, "convert_unit"): + if hasattr(p, 'convert_unit'): p.convert_unit(old_unit) except Exception: # noqa: S110 pass # Best effort rollback raise e @abstractmethod - def evaluate( - self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray - ) -> np.ndarray: + def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray: """Abstract method to evaluate the model component at input x. Must be implemented by subclasses. @@ -219,4 +215,4 @@ def __repr__(self) -> str: str: A string representation of the ModelComponent. """ - return f"{self.__class__.__name__}(unique_name={self.unique_name}, unit={self._unit})" + return f'{self.__class__.__name__}(unique_name={self.unique_name}, unit={self._unit})' diff --git a/src/easydynamics/sample_model/components/polynomial.py b/src/easydynamics/sample_model/components/polynomial.py index e5ecaa117..554fb2545 100644 --- a/src/easydynamics/sample_model/components/polynomial.py +++ b/src/easydynamics/sample_model/components/polynomial.py @@ -30,8 +30,8 @@ class Polynomial(ModelComponent): def __init__( self, coefficients: Sequence[Numeric | Parameter] = (0.0,), - unit: str | sc.Unit = "meV", - display_name: str | None = "Polynomial", + unit: str | sc.Unit = 'meV', + display_name: str | None = 'Polynomial', unique_name: str | None = None, ) -> None: """Initialize the Polynomial component. @@ -58,12 +58,12 @@ def __init__( if not isinstance(coefficients, (list, tuple, np.ndarray)): raise TypeError( - "coefficients must be a sequence (list/tuple/ndarray) \ - of numbers or Parameter objects." + 'coefficients must be a sequence (list/tuple/ndarray) \ + of numbers or Parameter objects.' ) if len(coefficients) == 0: - raise ValueError("At least one coefficient must be provided.") + raise ValueError('At least one coefficient must be provided.') # Internal storage of Parameter objects self._coefficients: list[Parameter] = [] @@ -73,11 +73,9 @@ def __init__( if isinstance(coef, Parameter): param = coef elif isinstance(coef, Numeric): - param = Parameter(name=f"{display_name}_c{i}", value=float(coef)) + param = Parameter(name=f'{display_name}_c{i}', value=float(coef)) else: - raise TypeError( - "Each coefficient must be either a numeric value or a Parameter." - ) + raise TypeError('Each coefficient must be either a numeric value or a Parameter.') self._coefficients.append(param) # Helper scipp scalar to track unit conversions @@ -112,11 +110,11 @@ def coefficients(self, coeffs: Sequence[Numeric | Parameter]) -> None: """ if not isinstance(coeffs, (list, tuple, np.ndarray)): raise TypeError( - "coefficients must be a sequence (list/tuple/ndarray) of numbers or Parameter ." + 'coefficients must be a sequence (list/tuple/ndarray) of numbers or Parameter .' ) if len(coeffs) != len(self._coefficients): raise ValueError( - "Number of coefficients must match the existing number of coefficients." + 'Number of coefficients must match the existing number of coefficients.' ) for i, coef in enumerate(coeffs): if isinstance(coef, Parameter): @@ -125,9 +123,7 @@ def coefficients(self, coeffs: Sequence[Numeric | Parameter]) -> None: elif isinstance(coef, Numeric): self._coefficients[i].value = float(coef) else: - raise TypeError( - "Each coefficient must be either a numeric value or a Parameter." - ) + raise TypeError('Each coefficient must be either a numeric value or a Parameter.') def coefficient_values(self) -> list[float]: """Get the coefficients of the polynomial as a list. @@ -138,9 +134,7 @@ def coefficient_values(self) -> list[float]: coefficient_list = [param.value for param in self._coefficients] return coefficient_list - def evaluate( - self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray - ) -> np.ndarray: + def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray: r"""Evaluate the Polynomial at the given x values. The intensity is given by @@ -166,8 +160,8 @@ def evaluate( if any(result < 0): warnings.warn( - f"The Polynomial with unique_name {self.unique_name} has negative values, " - "which may not be physically meaningful.", + f'The Polynomial with unique_name {self.unique_name} has negative values, ' + 'which may not be physically meaningful.', UserWarning, ) return result @@ -194,8 +188,8 @@ def degree(self, value: int) -> None: directly. """ raise AttributeError( - "The degree of the polynomial is determined by the number of coefficients \ - and cannot be set directly." + 'The degree of the polynomial is determined by the number of coefficients \ + and cannot be set directly.' ) def get_all_variables(self) -> list[DescriptorBase]: @@ -217,14 +211,12 @@ def convert_unit(self, unit: str | sc.Unit) -> None: """ if not isinstance(unit, (str, sc.Unit)): - raise UnitError("unit must be a string or a scipp unit.") + raise UnitError('unit must be a string or a scipp unit.') # Find out how much the unit changes # by converting a helper variable conversion_value_before = self._unit_conversion_helper.value - self._unit_conversion_helper = sc.to_unit( - self._unit_conversion_helper, unit=unit - ) + self._unit_conversion_helper = sc.to_unit(self._unit_conversion_helper, unit=unit) conversion_value_after = self._unit_conversion_helper.value for i, param in enumerate(self._coefficients): param.value *= ( @@ -240,11 +232,9 @@ def __repr__(self) -> str: str: A string representation of the Polynomial. """ - coeffs_str = ", ".join( - f"{param.name}={param.value}" for param in self._coefficients - ) - return f"Polynomial(unique_name = {self.unique_name}, \ - unit = {self._unit},\n coefficients = [{coeffs_str}])" + coeffs_str = ', '.join(f'{param.name}={param.value}' for param in self._coefficients) + return f'Polynomial(unique_name = {self.unique_name}, \ + unit = {self._unit},\n coefficients = [{coeffs_str}])' # from typing import Callable, Dict diff --git a/src/easydynamics/sample_model/diffusion_model/brownian_translational_diffusion.py b/src/easydynamics/sample_model/diffusion_model/brownian_translational_diffusion.py index 8234e812d..881a158a1 100644 --- a/src/easydynamics/sample_model/diffusion_model/brownian_translational_diffusion.py +++ b/src/easydynamics/sample_model/diffusion_model/brownian_translational_diffusion.py @@ -50,7 +50,8 @@ def __init__( """Initialize a new BrownianTranslationalDiffusion model. Args: - display_name (str | None, default='BrownianTranslationalDiffusion'): Display name of the diffusion model. + display_name (str | None, default='BrownianTranslationalDiffusion'): + Display name of the diffusion model. unique_name (str | None, default=None): Unique name of the diffusion model. If None, a unique name will be generated. unit (str | sc.Unit, default='meV'): Unit of the diffusion model. Must be diff --git a/src/easydynamics/sample_model/diffusion_model/diffusion_model_base.py b/src/easydynamics/sample_model/diffusion_model/diffusion_model_base.py index 7a0d3e08f..2f23779bf 100644 --- a/src/easydynamics/sample_model/diffusion_model/diffusion_model_base.py +++ b/src/easydynamics/sample_model/diffusion_model/diffusion_model_base.py @@ -23,7 +23,8 @@ def __init__( """Initialize a new DiffusionModel. Args: - display_name (str | None, default='MyDiffusionModel'): Display name of the diffusion model. + display_name (str | None, default='MyDiffusionModel'): + Display name of the diffusion model. unique_name (str | None, default=None): Unique name of the diffusion model. If None, a unique name will be generated. scale (Numeric, default=1.0): Scale factor for the diffusion model. Must diff --git a/src/easydynamics/sample_model/diffusion_model/jump_translational_diffusion.py b/src/easydynamics/sample_model/diffusion_model/jump_translational_diffusion.py index 629251723..bc27f3d86 100644 --- a/src/easydynamics/sample_model/diffusion_model/jump_translational_diffusion.py +++ b/src/easydynamics/sample_model/diffusion_model/jump_translational_diffusion.py @@ -56,7 +56,8 @@ def __init__( """Initialize a new JumpTranslationalDiffusion model. Args: - display_name (str | None, default="JumpTranslationalDiffusion"): Display name of the diffusion model. + display_name (str | None, default="JumpTranslationalDiffusion"): + Display name of the diffusion model. unique_name (str | None, default=None): Unique name of the diffusion model. If None, a unique name will be generated. unit (str | sc.Unit, default="meV"): Unit of the diffusion model. Must be @@ -237,8 +238,8 @@ def create_component_collections( Args: Q (Q_type): Scattering vector in 1/angstrom. Can be a single value or an array of values. - component_display_name (str, default="Jump translational diffusion"): Name of the Jump Diffusion - Lorentzian component. + component_display_name (str, default="Jump translational diffusion"): + Name of the Jump Diffusion Lorentzian component. Returns: List[ComponentCollection]: List of ComponentCollections with diff --git a/src/easydynamics/sample_model/sample_model.py b/src/easydynamics/sample_model/sample_model.py index ea5623afa..5cf9bf77c 100644 --- a/src/easydynamics/sample_model/sample_model.py +++ b/src/easydynamics/sample_model/sample_model.py @@ -62,7 +62,9 @@ def __init__( Raises: TypeError: If diffusion_models is not a DiffusionModelBase, - a list of DiffusionModelBase, or None, or if temperature is not a number or None, or if divide_by_temperature is not a bool. + a list of DiffusionModelBase, or None, or if temperature + is not a number or None, or if divide_by_temperature is + not a bool. ValueError: If temperature is negative. """ if diffusion_models is None: @@ -373,7 +375,9 @@ def get_all_variables(self, Q_index: int | None = None) -> list[Parameter]: get variables from all ComponentCollections. Returns: - list[Parameter]: List of all Parameters and Descriptors, including temperature if set and all variables from diffusion models. + list[Parameter]: List of all Parameters and Descriptors, + including temperature if set and all variables from + diffusion models. """ all_vars = super().get_all_variables(Q_index=Q_index) diff --git a/src/easydynamics/utils/detailed_balance.py b/src/easydynamics/utils/detailed_balance.py index 8b47b22da..086917fa3 100644 --- a/src/easydynamics/utils/detailed_balance.py +++ b/src/easydynamics/utils/detailed_balance.py @@ -60,10 +60,18 @@ def _detailed_balance_factor( given energy and temperature. Raises: - TypeError: If energy or temperature is not a number, list, numpy array, or scipp Variable, or if energy_unit or temperature_unit is not a string or scipp Unit, or if divide_by_temperature is not a boolean. - ValueError: If temperature is negative, or if energy is a numpy array with more than 1 dimension, or if temperature is a scipp Variable that does not have a single dimension named 'temperature', or if energy is a scipp Variable that does not have a single dimension named 'energy'. - UnitError: If the provided energy_unit or temperature_unit is invalid, or if the units of energy or temperature cannot be converted to the expected - units. + TypeError: If energy or temperature is not a number, list, + numpy array, or scipp Variable, or if energy_unit or + temperature_unit is not a string or scipp Unit, + or if divide_by_temperature is not a boolean. + ValueError: If temperature is negative, or if energy is a numpy + array with more than 1 dimension, or if temperature is a + scipp Variable that does not have a single dimension named + 'temperature', or if energy is a scipp Variable that does + not have a single dimension named 'energy'. + UnitError: If the provided energy_unit or temperature_unit is + invalid, or if the units of energy or temperature cannot be + converted to the expected units. ZeroDivisionError: If divide_by_temperature is True and temperature is zero. Examples: From e68c9e3cf90d251d362adcd0247c60ac76c50b3d Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Mon, 16 Mar 2026 11:36:21 +0100 Subject: [PATCH 29/31] fix tests --- src/easydynamics/utils/utils.py | 2 +- tests/unit/easydynamics/sample_model/test_sample_model.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/easydynamics/utils/utils.py b/src/easydynamics/utils/utils.py index 0ef80f1fd..ec5cb0563 100644 --- a/src/easydynamics/utils/utils.py +++ b/src/easydynamics/utils/utils.py @@ -31,7 +31,7 @@ def _validate_and_convert_Q( """ if Q is None: return None - if not isinstance(Q, (np.ndarray | Numeric | list | ArrayLike | sc.Variable)): + if not isinstance(Q, (np.ndarray, Numeric, list, sc.Variable)): raise TypeError('Q must be a number, list, numpy array, or scipp Variable.') if isinstance(Q, Numeric): diff --git a/tests/unit/easydynamics/sample_model/test_sample_model.py b/tests/unit/easydynamics/sample_model/test_sample_model.py index 22a5f2fb5..e1f7223e9 100644 --- a/tests/unit/easydynamics/sample_model/test_sample_model.py +++ b/tests/unit/easydynamics/sample_model/test_sample_model.py @@ -352,7 +352,7 @@ def test_generate_component_collections(self, sample_model): assert collection.components[0].area.value == 1.0 assert collection.components[1].display_name == 'TestLorentzian1' assert collection.components[1].area.value == 2.0 - assert collection.components[2].display_name == 'Brownian translational diffusion' + assert collection.components[2].display_name == 'Brownian diffusion' assert isinstance(collection.components[2], Lorentzian) def test_get_all_variables(self, sample_model): From 16197df7c96bb805174422b82238fe9e8e369416 Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Mon, 16 Mar 2026 12:08:02 +0100 Subject: [PATCH 30/31] add a few tests --- .../analysis1dnc5wfazv_nbqa_ipynb.py | 54 +++++ .../tutorials/analysis5dsibvoj_nbqa_ipynb.py | 208 ++++++++++++++++++ ...component_collectionnkus_vfh_nbqa_ipynb.py | 39 ++++ .../componentsxo0xyr8r_nbqa_ipynb.py | 76 +++++++ .../convolution98p78z7o_nbqa_ipynb.py | 167 ++++++++++++++ .../detailed_balancetqju34kv_nbqa_ipynb.py | 46 ++++ .../diffusion_model58y3qb6__nbqa_ipynb.py | 53 +++++ .../experimentbgw31e06_nbqa_ipynb.py | 29 +++ .../tutorials/indexqpz_wmw5_nbqa_ipynb.py | 0 .../instrument_modelw4cdd861_nbqa_ipynb.py | 35 +++ .../sample_modelzmrku316_nbqa_ipynb.py | 89 ++++++++ .../tutorial1_browniant5se346o_nbqa_ipynb.py | 193 ++++++++++++++++ .../sample_model/components/exponential.py | 3 +- .../components/test_exponential.py | 16 ++ .../components/test_model_component.py | 5 + .../sample_model/test_component_collection.py | 9 + .../sample_model/test_model_base.py | 5 + 17 files changed, 1026 insertions(+), 1 deletion(-) create mode 100644 docs/docs/tutorials/analysis1dnc5wfazv_nbqa_ipynb.py create mode 100644 docs/docs/tutorials/analysis5dsibvoj_nbqa_ipynb.py create mode 100644 docs/docs/tutorials/component_collectionnkus_vfh_nbqa_ipynb.py create mode 100644 docs/docs/tutorials/componentsxo0xyr8r_nbqa_ipynb.py create mode 100644 docs/docs/tutorials/convolution98p78z7o_nbqa_ipynb.py create mode 100644 docs/docs/tutorials/detailed_balancetqju34kv_nbqa_ipynb.py create mode 100644 docs/docs/tutorials/diffusion_model58y3qb6__nbqa_ipynb.py create mode 100644 docs/docs/tutorials/experimentbgw31e06_nbqa_ipynb.py create mode 100644 docs/docs/tutorials/indexqpz_wmw5_nbqa_ipynb.py create mode 100644 docs/docs/tutorials/instrument_modelw4cdd861_nbqa_ipynb.py create mode 100644 docs/docs/tutorials/sample_modelzmrku316_nbqa_ipynb.py create mode 100644 docs/docs/tutorials/tutorial1_browniant5se346o_nbqa_ipynb.py diff --git a/docs/docs/tutorials/analysis1dnc5wfazv_nbqa_ipynb.py b/docs/docs/tutorials/analysis1dnc5wfazv_nbqa_ipynb.py new file mode 100644 index 000000000..93a5d380b --- /dev/null +++ b/docs/docs/tutorials/analysis1dnc5wfazv_nbqa_ipynb.py @@ -0,0 +1,54 @@ +# %%NBQA-CELL-SEP5932b7 +import pooch + +from easydynamics.analysis.analysis1d import Analysis1d +from easydynamics.experiment import Experiment +from easydynamics.sample_model import DeltaFunction +from easydynamics.sample_model import Gaussian +from easydynamics.sample_model import Polynomial +from easydynamics.sample_model.background_model import BackgroundModel +from easydynamics.sample_model.instrument_model import InstrumentModel +from easydynamics.sample_model.resolution_model import ResolutionModel +from easydynamics.sample_model.sample_model import SampleModel + +hash(0xF69AF408) + +# %%NBQA-CELL-SEP5932b7 +vanadium_experiment = Experiment('Vanadium') + +file_path = pooch.retrieve( + url='https://github.com/easyscience/dynamics-lib/raw/refs/heads/master/docs/docs/tutorials/data/vanadium_data_example.h5', + known_hash='16cc1b327c303feeb88fb9dda5390dc4880b62396b1793f98c6fef0b27c7b873', +) + +vanadium_experiment.load_hdf5(filename=file_path) + +# %%NBQA-CELL-SEP5932b7 +# Example of Analysis1d with a simple sample model and instrument model +delta_function = DeltaFunction(display_name='DeltaFunction', area=1) +sample_model = SampleModel( + components=delta_function, +) + +res_gauss = Gaussian(width=0.1) +resolution_model = ResolutionModel(components=res_gauss) + + +background_model = BackgroundModel(components=Polynomial(coefficients=[0.001])) + +instrument_model = InstrumentModel( + resolution_model=resolution_model, + background_model=background_model, +) + +my_analysis = Analysis1d( + display_name='Vanadium Analysis', + experiment=vanadium_experiment, + sample_model=sample_model, + instrument_model=instrument_model, + Q_index=5, +) + +fit_result = my_analysis.fit() +fig = my_analysis.plot_data_and_model() +fig diff --git a/docs/docs/tutorials/analysis5dsibvoj_nbqa_ipynb.py b/docs/docs/tutorials/analysis5dsibvoj_nbqa_ipynb.py new file mode 100644 index 000000000..5a84a3762 --- /dev/null +++ b/docs/docs/tutorials/analysis5dsibvoj_nbqa_ipynb.py @@ -0,0 +1,208 @@ +# %%NBQA-CELL-SEP5932b7 +# Imports +import pooch + +from easydynamics.analysis.analysis import Analysis +from easydynamics.experiment import Experiment +from easydynamics.sample_model import BrownianTranslationalDiffusion +from easydynamics.sample_model import ComponentCollection +from easydynamics.sample_model import DeltaFunction +from easydynamics.sample_model import Gaussian +from easydynamics.sample_model import Lorentzian +from easydynamics.sample_model import Polynomial +from easydynamics.sample_model.background_model import BackgroundModel +from easydynamics.sample_model.instrument_model import InstrumentModel +from easydynamics.sample_model.resolution_model import ResolutionModel +from easydynamics.sample_model.sample_model import SampleModel + +hash(0x5D2FE732) + +# %%NBQA-CELL-SEP5932b7 +# Load the vanadium data +vanadium_experiment = Experiment('Vanadium') +file_path = pooch.retrieve( + url='https://github.com/easyscience/dynamics-lib/raw/refs/heads/master/docs/docs/tutorials/data/vanadium_data_example.h5', + known_hash='16cc1b327c303feeb88fb9dda5390dc4880b62396b1793f98c6fef0b27c7b873', +) + +vanadium_experiment.load_hdf5(filename=file_path) + +# %%NBQA-CELL-SEP5932b7 +# Example of Analysis with a simple sample model and instrument model +# The scattering from vanadium is purely elastic, so we model it with a +# delta function +delta_function = DeltaFunction(display_name='DeltaFunction', area=1) +sample_model = SampleModel( + components=delta_function, +) + +# The resolution is in this case modeled as a Gaussian. However, we can +# add as many components as we like to the resolution model +res_gauss = Gaussian(width=0.1) +res_gauss.area.fixed = True +resolution_components = ComponentCollection() +resolution_components.append_component(res_gauss) +resolution_model = ResolutionModel(components=resolution_components) + +# The background model is created in the same way. In this case, we use +# a flat background +background_model = BackgroundModel(components=Polynomial(coefficients=[0.001])) + +# We combine the resolution abd background model into an instrument +# model. This model also contains a small energy offset to account for +# instrument misalignment. + +instrument_model = InstrumentModel( + resolution_model=resolution_model, + background_model=background_model, +) + +# Collect everything into an analysis object. +vanadium_analysis = Analysis( + display_name='Vanadium Full Analysis', + experiment=vanadium_experiment, + sample_model=sample_model, + instrument_model=instrument_model, +) + +# Let us first fit a single Q index and plot the data and model to see +# how it looks +fit_result_independent_single_Q = vanadium_analysis.fit(fit_method='independent', Q_index=5) +vanadium_analysis.plot_data_and_model(Q_index=5) + +# %%NBQA-CELL-SEP5932b7 +# It looks good, so let us fit all Q indices independently and plot the +# results +fit_result_independent_all_Q = vanadium_analysis.fit(fit_method='independent') +vanadium_analysis.plot_data_and_model() + +# %%NBQA-CELL-SEP5932b7 +# Inspect the Parameters as a scipp Dataset +vanadium_analysis.parameters_to_dataset() + +# %%NBQA-CELL-SEP5932b7 +# Plot some of fitted parameters as a function of Q +vanadium_analysis.plot_parameters(names=['DeltaFunction area']) + +# %%NBQA-CELL-SEP5932b7 +vanadium_analysis.plot_parameters(names=['Gaussian width']) + +# %%NBQA-CELL-SEP5932b7 +vanadium_analysis.plot_parameters(names=['energy_offset']) + +# %%NBQA-CELL-SEP5932b7 +# Now it's time to look at the data we want to fit. We first load the +# data +diffusion_experiment = Experiment('Diffusion') + +file_path = pooch.retrieve( + url='https://github.com/easyscience/dynamics-lib/raw/refs/heads/master/docs/docs/tutorials/data/diffusion_data_example.h5', + known_hash='5fe846b19aacbda8b8b936eb2e5310d025dc56c25b0b353521e7d6b921f229ab', +) + +diffusion_experiment.load_hdf5(filename=file_path) + +# %%NBQA-CELL-SEP5932b7 +# Now we set up the model, similarly to how we set up the model for the +# vanadium data. + +delta_function = DeltaFunction(display_name='DeltaFunction', area=0.2) +lorentzian = Lorentzian(display_name='Lorentzian', area=0.5, width=0.3) +component_collection = ComponentCollection( + components=[delta_function, lorentzian], +) + +sample_model = SampleModel( + components=component_collection, +) + +background_model = BackgroundModel(components=Polynomial(coefficients=[0.001])) + +instrument_model = InstrumentModel( + background_model=background_model, +) + +diffusion_analysis = Analysis( + display_name='Diffusion Full Analysis', + experiment=diffusion_experiment, + sample_model=sample_model, + instrument_model=instrument_model, +) + +# We need to hack in the resolution model from the vanadium analysis, +# since the setters and getters overwrite the model. This will be fixed +# asap. +diffusion_analysis.instrument_model._resolution_model = ( + vanadium_analysis.instrument_model.resolution_model +) + +# We fix all parameters of the resolution model. +diffusion_analysis.instrument_model.resolution_model.fix_all_parameters() + +# %%NBQA-CELL-SEP5932b7 +# Let us see how good the starting parameters are +diffusion_analysis.plot_data_and_model() + +# %%NBQA-CELL-SEP5932b7 +# Now we fit the data and plot the result. Looks good! +diffusion_analysis.fit(fit_method='independent') +diffusion_analysis.plot_data_and_model() + +# %%NBQA-CELL-SEP5932b7 +# Let us look at the most interesting fit parameters +diffusion_analysis.plot_parameters(names=['Lorentzian width', 'Lorentzian area']) + +# %%NBQA-CELL-SEP5932b7 +# It will be possible to fit this to a DiffusionModel, but that will +# come later. + +# %%NBQA-CELL-SEP5932b7 +# Let us now fit directly to a diffusion model. We replace the +# Lorentzian with a Brownian translational diffusion model and keep the +# other parameters the same. +delta_function = DeltaFunction(display_name='DeltaFunction', area=0.2) +component_collection = ComponentCollection( + components=[delta_function], +) +diffusion_model = BrownianTranslationalDiffusion( + display_name='Brownian Translational Diffusion', diffusion_coefficient=2.4e-9, scale=0.5 +) + +sample_model = SampleModel( + components=component_collection, + diffusion_models=diffusion_model, +) + +background_model = BackgroundModel(components=Polynomial(coefficients=[0.001])) + +instrument_model = InstrumentModel( + background_model=background_model, +) + +diffusion_model_analysis = Analysis( + display_name='Diffusion Full Analysis', + experiment=diffusion_experiment, + sample_model=sample_model, + instrument_model=instrument_model, +) + +# We again need to hack in the resolution model from the vanadium +# analysis, since the setters and getters overwrite the model. This will +# be fixed asap. +diffusion_model_analysis.instrument_model._resolution_model = ( + vanadium_analysis.instrument_model.resolution_model +) +diffusion_model_analysis.instrument_model.resolution_model.fix_all_parameters() + +# Let us see how good the starting parameters are +diffusion_model_analysis.plot_data_and_model() + +# %%NBQA-CELL-SEP5932b7 +# We now fit all the data simultaneously to the diffusion model, then +# plot the result. Looks good. +diffusion_model_analysis.fit(fit_method='simultaneous') +diffusion_model_analysis.plot_data_and_model() + +# %%NBQA-CELL-SEP5932b7 +# Let us look at the fitted diffusion coefficient +diffusion_model.get_all_parameters() diff --git a/docs/docs/tutorials/component_collectionnkus_vfh_nbqa_ipynb.py b/docs/docs/tutorials/component_collectionnkus_vfh_nbqa_ipynb.py new file mode 100644 index 000000000..c84b8f440 --- /dev/null +++ b/docs/docs/tutorials/component_collectionnkus_vfh_nbqa_ipynb.py @@ -0,0 +1,39 @@ +# %%NBQA-CELL-SEP5932b7 +import matplotlib.pyplot as plt +import numpy as np + +from easydynamics.sample_model import ComponentCollection +from easydynamics.sample_model import DampedHarmonicOscillator +from easydynamics.sample_model import Gaussian +from easydynamics.sample_model import Lorentzian +from easydynamics.sample_model import Polynomial + +hash(0x9AFE4A6B) + +# %%NBQA-CELL-SEP5932b7 +component_collection = ComponentCollection() + +# Creating components +gaussian = Gaussian(display_name='Gaussian', width=0.5, area=1) +dho = DampedHarmonicOscillator(display_name='DHO', center=1.0, width=0.3, area=2.0) +lorentzian = Lorentzian(display_name='Lorentzian', center=-1.0, width=0.2, area=1.0) +polynomial = Polynomial(display_name='Polynomial', coefficients=[0.1, 0, 0.5]) # y=0.1+0.5*x^2 + +# Adding components to the component collection +component_collection.append_component(gaussian) +component_collection.append_component(dho) +component_collection.append_component(lorentzian) +component_collection.append_component(polynomial) + +x = np.linspace(-2, 2, 100) + +plt.figure() +y = component_collection.evaluate(x) +plt.plot(x, y, label='Component collection') + +for component in component_collection.components: + y = component.evaluate(x) + plt.plot(x, y, label=component.display_name) + +plt.legend() +plt.show() diff --git a/docs/docs/tutorials/componentsxo0xyr8r_nbqa_ipynb.py b/docs/docs/tutorials/componentsxo0xyr8r_nbqa_ipynb.py new file mode 100644 index 000000000..1b66e3f4a --- /dev/null +++ b/docs/docs/tutorials/componentsxo0xyr8r_nbqa_ipynb.py @@ -0,0 +1,76 @@ +# %%NBQA-CELL-SEP5932b7 +import matplotlib.pyplot as plt +import numpy as np +import scipp as sc + +from easydynamics.sample_model import DampedHarmonicOscillator +from easydynamics.sample_model import DeltaFunction +from easydynamics.sample_model import Exponential +from easydynamics.sample_model import Gaussian +from easydynamics.sample_model import Lorentzian +from easydynamics.sample_model import Polynomial + +hash(0xE0A2D949) + +# %%NBQA-CELL-SEP5932b7 +# Creating a component +gaussian = Gaussian(display_name='Gaussian', width=0.5, area=1) +dho = DampedHarmonicOscillator(display_name='DHO', center=1.0, width=0.3, area=2.0) +lorentzian = Lorentzian(display_name='Lorentzian', center=-1.0, width=0.2, area=1.0) +polynomial = Polynomial(display_name='Polynomial', coefficients=[0.1, 0, 0.5]) # y=0.1+0.5*x^2 +exponential = Exponential(display_name='Exponential', amplitude=1.0, rate=-0.5) + +x = np.linspace(-2, 2, 100) + +plt.figure() +y = gaussian.evaluate(x) +plt.plot(x, y, label='Gaussian') +y = dho.evaluate(x) +plt.plot(x, y, label='DHO') +y = lorentzian.evaluate(x) +plt.plot(x, y, label='Lorentzian') +y = polynomial.evaluate(x) +plt.plot(x, y, label='Polynomial') +y = exponential.evaluate(x) +plt.plot(x, y, label='Exponential') +plt.legend() +plt.show() + +# %%NBQA-CELL-SEP5932b7 +# The area under the DHO curve is indeed equal to the area parameter. +xx = np.linspace(-15, 15, 10000) +yy = dho.evaluate(xx) +area = np.trapezoid(yy, xx) +print(f'Area under DHO curve: {area:.4f}') + +# %%NBQA-CELL-SEP5932b7 +delta = DeltaFunction(display_name='Delta', center=0.0, area=1.0) +x1 = np.linspace(-2, 2, 100) +y = delta.evaluate(x1) +x2 = np.linspace(-2, 2, 51) +y2 = delta.evaluate(x2) +plt.figure() +plt.plot(x1, y, label='Delta Function') +plt.plot(x2, y2, label='Delta Function (coarser)') +plt.legend() +plt.show() +# The area under the Delta function is indeed equal to +# the area parameter. +xx = np.linspace(-2, 2, 10000) +yy = delta.evaluate(xx) +area = np.trapezoid(y, x1) +print(area) + +# %%NBQA-CELL-SEP5932b7 +x1 = sc.linspace(dim='x', start=-2.0, stop=2.0, num=100, unit='meV') +x2 = sc.linspace(dim='x', start=-2.0 * 1e3, stop=2.0 * 1e3, num=101, unit='microeV') + +polynomial = Polynomial(display_name='Polynomial', coefficients=[0.1, 0, 0.5]) # y=0.1+0.5*x^2 +y1 = polynomial.evaluate(x1) +y2 = polynomial.evaluate(x2) + +plt.figure() +plt.plot(x1.values, y1, label='Polynomial meV', color='blue') +plt.plot(x2.values / 1000, y2, label='Polynomial microeV', linestyle='dashed', color='orange') +plt.legend() +plt.show() diff --git a/docs/docs/tutorials/convolution98p78z7o_nbqa_ipynb.py b/docs/docs/tutorials/convolution98p78z7o_nbqa_ipynb.py new file mode 100644 index 000000000..3a6950fdd --- /dev/null +++ b/docs/docs/tutorials/convolution98p78z7o_nbqa_ipynb.py @@ -0,0 +1,167 @@ +# %%NBQA-CELL-SEP5932b7 +import matplotlib.pyplot as plt +import numpy as np + +from easydynamics.convolution import Convolution +from easydynamics.sample_model import DampedHarmonicOscillator +from easydynamics.sample_model import DeltaFunction +from easydynamics.sample_model import Gaussian +from easydynamics.sample_model import Lorentzian +from easydynamics.sample_model.component_collection import ComponentCollection +from easydynamics.utils import _detailed_balance_factor as detailed_balance_factor + +hash(0xCC7EF502) + +# %%NBQA-CELL-SEP5932b7 +# Standard example of convolution of a sample model with a +# resolution model +sample_components = ComponentCollection() +gaussian = Gaussian(display_name='Gaussian', width=0.5, area=1) +dho = DampedHarmonicOscillator(display_name='DHO', center=1.0, width=0.3, area=2.0) +lorentzian = Lorentzian(display_name='Lorentzian', center=-1.0, width=0.2, area=1.0) +delta = DeltaFunction(display_name='Delta', center=0.4, area=0.5) +sample_components.append_component(gaussian) +# sample_components.append_component(dho) +sample_components.append_component(lorentzian) +sample_components.append_component(delta) + +resolution_components = ComponentCollection() +resolution_gaussian = Gaussian(display_name='Resolution Gaussian', width=0.015, area=0.8) +resolution_lorentzian = Lorentzian(display_name='Resolution Lorentzian', width=0.025, area=0.2) +resolution_components.append_component(resolution_gaussian) +resolution_components.append_component(resolution_lorentzian) + +energy = np.linspace(-2, 2, 100) + +convolver = Convolution( + sample_components=sample_components, resolution_components=resolution_components, energy=energy +) +y = convolver.convolution() +plt.figure() +plt.plot(energy, y, label='Convoluted Model') +plt.xlabel('Energy (meV)') +plt.ylabel('Intensity (arb. units)') +plt.title('Convolution of Sample Model with Resolution Model') + +plt.plot(energy, sample_components.evaluate(energy), label='Sample Model', linestyle='--') +plt.plot(energy, resolution_components.evaluate(energy), label='Resolution Model', linestyle=':') + +plt.legend() +# set the limit on the y axis +plt.ylim(0, 6) +plt.show() + +# %%NBQA-CELL-SEP5932b7 +# Use some of the extra settings for the numerical convolution +sample_components = ComponentCollection() +gaussian = Gaussian(display_name='Gaussian', width=0.3, area=1) +dho = DampedHarmonicOscillator(display_name='DHO', center=1.0, width=0.3, area=2.0) +lorentzian = Lorentzian(display_name='Lorentzian', center=-1.0, width=0.2, area=1.0) +delta = DeltaFunction(display_name='Delta', center=0.4, area=0.5) +sample_components.append_component(gaussian) +sample_components.append_component(dho) +sample_components.append_component(lorentzian) +sample_components.append_component(delta) + +resolution_components = ComponentCollection() +resolution_gaussian = Gaussian(display_name='Resolution Gaussian', width=0.15, area=0.8) +resolution_lorentzian = Lorentzian(display_name='Resolution Lorentzian', width=0.25, area=0.2) +resolution_components.append_component(resolution_gaussian) +resolution_components.append_component(resolution_lorentzian) + +energy = np.linspace(-2, 2, 100) + + +temperature = 10.0 # Temperature in Kelvin +energy_offset = 0.5 +upsample_factor = 5 +extension_factor = 0.5 +plt.figure() +plt.xlabel('Energy (meV)') +plt.ylabel('Intensity (arb. units)') + +convolver = Convolution( + sample_components=sample_components, + resolution_components=resolution_components, + energy=energy - energy_offset, + upsample_factor=upsample_factor, + extension_factor=extension_factor, + temperature=temperature, + normalize_detailed_balance=True, +) +y = convolver.convolution() + + +plt.plot(energy, y, label='Convoluted Model') + +plt.plot( + energy, + sample_components.evaluate(energy - energy_offset) + * detailed_balance_factor(energy - energy_offset, temperature), + label='Sample Model with DB', + linestyle='--', +) + +plt.plot(energy, resolution_components.evaluate(energy), label='Resolution Model', linestyle=':') +plt.title('Convolution of Sample Model with Resolution Model with detailed balancing') + +plt.legend() +plt.ylim(0, 2.5) +plt.show() + +# %%NBQA-CELL-SEP5932b7 +# Use some of the extra settings for the numerical convolution +sample_components = ComponentCollection() +gaussian = Gaussian(display_name='Gaussian', width=0.3, area=1) +dho = DampedHarmonicOscillator(display_name='DHO', center=1.0, width=0.3, area=2.0) +lorentzian = Lorentzian(display_name='Lorentzian', center=-1.0, width=0.2, area=1.0) +delta = DeltaFunction(display_name='Delta', center=0.4, area=0.5) +sample_components.append_component(gaussian) +# sample_components.append_component(dho) +sample_components.append_component(lorentzian) +# sample_components.append_component(delta) + +resolution_components = ComponentCollection() +resolution_gaussian = Gaussian(display_name='Resolution Gaussian', width=0.15, area=0.8) +resolution_lorentzian = Lorentzian(display_name='Resolution Lorentzian', width=0.25, area=0.2) +resolution_components.append_component(resolution_gaussian) +# resolution_components.append_component(resolution_lorentzian) + +energy = np.linspace(-2, 2, 100) + + +temperature = 10.0 # Temperature in Kelvin +energy_offset = 0.2 +upsample_factor = 5 +extension_factor = 0.5 +plt.figure() +plt.xlabel('Energy (meV)') +plt.ylabel('Intensity (arb. units)') + +convolver = Convolution( + sample_components=sample_components, + resolution_components=resolution_components, + energy=energy, + upsample_factor=upsample_factor, + extension_factor=extension_factor, + energy_offset=energy_offset, + temperature=temperature, +) +y = convolver.convolution() + + +plt.plot(energy, y, label='Convoluted Model') + +plt.plot( + energy, + sample_components.evaluate(energy - energy_offset), + label='Sample Model', + linestyle='--', +) + +plt.plot(energy, resolution_components.evaluate(energy), label='Resolution Model', linestyle=':') +plt.title('Convolution of Sample Model with Resolution Model') + +plt.legend() +plt.ylim(0, 2.5) +plt.show() diff --git a/docs/docs/tutorials/detailed_balancetqju34kv_nbqa_ipynb.py b/docs/docs/tutorials/detailed_balancetqju34kv_nbqa_ipynb.py new file mode 100644 index 000000000..58cbc2fc9 --- /dev/null +++ b/docs/docs/tutorials/detailed_balancetqju34kv_nbqa_ipynb.py @@ -0,0 +1,46 @@ +# %%NBQA-CELL-SEP5932b7 +import matplotlib.pyplot as plt +import numpy as np + +from easydynamics.utils import _detailed_balance_factor as detailed_balance_factor + +hash(0x36C9BD93) + +# %%NBQA-CELL-SEP5932b7 +temperatures = [1, 10, 100] +temperature_unit = 'K' +energy = np.linspace(-1, 1, 100) +# energy=1.0 +energy_unit = 'meV' + +plt.figure() +for temperature in temperatures: + DBF = detailed_balance_factor(energy, temperature, energy_unit, temperature_unit) + plt.plot(energy, DBF, label=f'T={temperature} K') +plt.legend() +plt.xlabel('Energy transfer (meV)') +plt.ylabel('Detailed balance factor') +plt.title( + 'Detailed balance factor for different temperatures, \n ' + 'normalized to 1 at zero energy transfer' +) +plt.show() + +# %%NBQA-CELL-SEP5932b7 +temperatures = [1, 10, 100] +temperature_unit = 'K' +energy = np.linspace(-1, 1, 100) +# energy=1.0 +energy_unit = 'meV' + +plt.figure() +for temperature in temperatures: + DBF = detailed_balance_factor( + energy, temperature, energy_unit, temperature_unit, divide_by_temperature=False + ) + plt.plot(energy, DBF, label=f'T={temperature} K') +plt.legend() +plt.xlabel('Energy transfer (meV)') +plt.ylabel('Detailed balance factor') +plt.title('Detailed balance factor for different temperatures, not normalized') +plt.show() diff --git a/docs/docs/tutorials/diffusion_model58y3qb6__nbqa_ipynb.py b/docs/docs/tutorials/diffusion_model58y3qb6__nbqa_ipynb.py new file mode 100644 index 000000000..3236521c6 --- /dev/null +++ b/docs/docs/tutorials/diffusion_model58y3qb6__nbqa_ipynb.py @@ -0,0 +1,53 @@ +# %%NBQA-CELL-SEP5932b7 +import matplotlib.pyplot as plt +import numpy as np + +from easydynamics.sample_model import BrownianTranslationalDiffusion + +hash(0x4D38875) + +# %%NBQA-CELL-SEP5932b7 +# Create Brownian Translational Diffusion model +# and plot the model for different Q values. +# Q is in Angstrom^-1 and energy in meV. + +Q = np.linspace(0.5, 2, 7) +energy = np.linspace(-2, 2, 501) +scale = 1.0 +diffusion_coefficient = 2.4e-9 # m^2/s + +diffusion_model = BrownianTranslationalDiffusion( + display_name='DiffusionModel', + scale=scale, + diffusion_coefficient=diffusion_coefficient, +) + +component_collections = diffusion_model.create_component_collections(Q) + + +cmap = plt.cm.jet +nQ = len(component_collections) +plt.figure() +for Q_index in range(len(component_collections)): + color = cmap(Q_index / (nQ - 1)) + y = component_collections[Q_index].evaluate(energy) + plt.plot(energy, y, label=f'Q={Q[Q_index]} Å^-1', color=color) + +plt.legend() +plt.show() +plt.xlabel('Energy (meV)') +plt.ylabel('Intensity (arb. units)') +plt.title('Brownian Translational Diffusion Model') + +# %%NBQA-CELL-SEP5932b7 +# Calculate and plot the half width at half maximum (HWHM) as function +# of Q +Q = np.linspace(0.1, 2, 101) +HWHM = diffusion_model.calculate_width(Q) +plt.figure() +plt.plot(Q, HWHM) +plt.xlabel('Q (Å$^{-1}$)') +plt.ylabel('HWHM (meV)') +plt.xlim(0, 2.5) +plt.ylim(0, max(HWHM) * 1.1) +plt.title('HWHM vs Q for Brownian Translational Diffusion') diff --git a/docs/docs/tutorials/experimentbgw31e06_nbqa_ipynb.py b/docs/docs/tutorials/experimentbgw31e06_nbqa_ipynb.py new file mode 100644 index 000000000..d4d7f3416 --- /dev/null +++ b/docs/docs/tutorials/experimentbgw31e06_nbqa_ipynb.py @@ -0,0 +1,29 @@ +# %%NBQA-CELL-SEP5932b7 +import pooch + +from easydynamics.experiment import Experiment + +hash(0x3AE3B342) + +# %%NBQA-CELL-SEP5932b7 +# Load and plot example vanadium data +# Load the vanadium data +vanadium_experiment = Experiment('Vanadium') + +file_path = pooch.retrieve( + url='https://github.com/easyscience/dynamics-lib/raw/refs/heads/master/docs/docs/tutorials/data/vanadium_data_example.h5', + known_hash='16cc1b327c303feeb88fb9dda5390dc4880b62396b1793f98c6fef0b27c7b873', +) + +vanadium_experiment.load_hdf5(filename=file_path) + +vanadium_experiment.plot_data() + +# %%NBQA-CELL-SEP5932b7 +# Rebin the data and plot again +vanadium_experiment.rebin({'Q': 5, 'energy': 50}) +vanadium_experiment.plot_data() + +# %%NBQA-CELL-SEP5932b7 +# Plot using the plopp slicer with extra arguments +vanadium_experiment.plot_data(slicer=True, keep='energy', vmin=0, vmax=2.0) diff --git a/docs/docs/tutorials/indexqpz_wmw5_nbqa_ipynb.py b/docs/docs/tutorials/indexqpz_wmw5_nbqa_ipynb.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs/docs/tutorials/instrument_modelw4cdd861_nbqa_ipynb.py b/docs/docs/tutorials/instrument_modelw4cdd861_nbqa_ipynb.py new file mode 100644 index 000000000..9b614b9e0 --- /dev/null +++ b/docs/docs/tutorials/instrument_modelw4cdd861_nbqa_ipynb.py @@ -0,0 +1,35 @@ +# %%NBQA-CELL-SEP5932b7 +import numpy as np + +from easydynamics.sample_model import Gaussian +from easydynamics.sample_model import Polynomial +from easydynamics.sample_model.background_model import BackgroundModel +from easydynamics.sample_model.instrument_model import InstrumentModel +from easydynamics.sample_model.resolution_model import ResolutionModel + +hash(0x51DBF3) + +# %%NBQA-CELL-SEP5932b7 +# Create a BackgroundModel and a ResolutionModel and add them to an +# InstrumentModel + +Q = np.linspace(0.1, 2.0, 5) + +background_model = BackgroundModel() +background_model.components = Polynomial(coefficients=[1, 0.1, 0.01]) + +resolution_model = ResolutionModel() +resolution_model.append_component(Gaussian(width=0.05)) + +instrument_model = InstrumentModel( + Q=Q, + resolution_model=resolution_model, + background_model=background_model, +) + +# %%NBQA-CELL-SEP5932b7 +instrument_model.get_all_variables(Q_index=1) + +# %%NBQA-CELL-SEP5932b7 +instrument_model.fix_resolution_parameters() +instrument_model.get_all_variables(Q_index=1) diff --git a/docs/docs/tutorials/sample_modelzmrku316_nbqa_ipynb.py b/docs/docs/tutorials/sample_modelzmrku316_nbqa_ipynb.py new file mode 100644 index 000000000..ea977d759 --- /dev/null +++ b/docs/docs/tutorials/sample_modelzmrku316_nbqa_ipynb.py @@ -0,0 +1,89 @@ +# %%NBQA-CELL-SEP5932b7 +import matplotlib.pyplot as plt +import numpy as np + +from easydynamics.sample_model import BrownianTranslationalDiffusion +from easydynamics.sample_model import ComponentCollection +from easydynamics.sample_model import DampedHarmonicOscillator +from easydynamics.sample_model import Gaussian +from easydynamics.sample_model import Polynomial +from easydynamics.sample_model.background_model import BackgroundModel +from easydynamics.sample_model.resolution_model import ResolutionModel +from easydynamics.sample_model.sample_model import SampleModel + +hash(0xFF4B0398) + +# %%NBQA-CELL-SEP5932b7 +# Create a diffusion_model and components for the SampleModel +Q = np.linspace(0.5, 2, 7) +energy = np.linspace(-2, 2, 501) + +scale = 1.0 +diffusion_coefficient = 2.4e-9 # m^2/s +diffusion_model = BrownianTranslationalDiffusion( + display_name='DiffusionModel', + scale=scale, + diffusion_coefficient=diffusion_coefficient, +) + + +# Creating components +component_collection = ComponentCollection() +gaussian = Gaussian(display_name='Gaussian', width=0.2, area=1, center=1.5) +dho = DampedHarmonicOscillator(display_name='DHO', center=1.0, width=0.3, area=2.0) + +# Adding components to the component collection +component_collection.append_component(gaussian) +component_collection.append_component(dho) + +sample_model = SampleModel( + diffusion_models=diffusion_model, + components=component_collection, + Q=Q, + unit='meV', + display_name='MySampleModel', + temperature=10, +) + +# Calculate the model intensity at the specified Q and energy values +# y_list will be a list of intensity arrays, one for each Q value +sample_intensity = sample_model.evaluate(energy) + +plt.figure() +for i, Q_value in enumerate(Q): + plt.plot(energy, sample_intensity[i], label=f'Q={Q_value:.2f} Å⁻¹') +plt.xlabel('Energy (meV)') +plt.ylabel('Intensity (a.u.)') +plt.title('Sample Model Evaluation at 10 K') +plt.legend() +plt.show() + +# %%NBQA-CELL-SEP5932b7 +# Create a BackgroundModel and show other ways to set Q and components + +background_model = BackgroundModel() +background_model.Q = Q + +background_model.components = Polynomial(coefficients=[1, 0.1, 0.01]) +background = background_model.evaluate(energy) + +# Also create a ResolutionModel. +# It doesn't do anything here, but shows how to set it up. +resolution_model = ResolutionModel() +resolution_model.Q = Q +resolution_model.append_component(Gaussian(width=0.05)) +resolution = resolution_model.evaluate(energy) + +# %%NBQA-CELL-SEP5932b7 +# Plot the three models together +for i, Q_value in enumerate(Q): + plt.figure() + plt.plot(energy, sample_intensity[i], label='Sample model') + plt.plot(energy, background[i], label='Background model') + plt.plot(energy, resolution[i], label='Resolution model') + plt.legend() + plt.xlabel('Energy (meV)') + plt.ylabel('Intensity (a.u.)') + plt.title(f'Sample Model Evaluation at 10 K and Q ={Q_value:.2f} Å⁻¹') + plt.legend() + plt.show() diff --git a/docs/docs/tutorials/tutorial1_browniant5se346o_nbqa_ipynb.py b/docs/docs/tutorials/tutorial1_browniant5se346o_nbqa_ipynb.py new file mode 100644 index 000000000..c9ff23f92 --- /dev/null +++ b/docs/docs/tutorials/tutorial1_browniant5se346o_nbqa_ipynb.py @@ -0,0 +1,193 @@ +# %%NBQA-CELL-SEP5932b7 +# Imports +import pooch + +from easydynamics.analysis.analysis import Analysis +from easydynamics.experiment import Experiment +from easydynamics.sample_model import BrownianTranslationalDiffusion +from easydynamics.sample_model import ComponentCollection +from easydynamics.sample_model import DeltaFunction +from easydynamics.sample_model import Gaussian +from easydynamics.sample_model import Lorentzian +from easydynamics.sample_model import Polynomial +from easydynamics.sample_model.background_model import BackgroundModel +from easydynamics.sample_model.instrument_model import InstrumentModel +from easydynamics.sample_model.resolution_model import ResolutionModel +from easydynamics.sample_model.sample_model import SampleModel + +# Make the plots interactive +hash(0x69D94788) + +# %%NBQA-CELL-SEP5932b7 +# Load the vanadium data +vanadium_experiment = Experiment('Vanadium') + +file_path = pooch.retrieve( + url='https://github.com/easyscience/dynamics-lib/raw/refs/heads/master/docs/docs/tutorials/data/vanadium_data_example.h5', + known_hash='16cc1b327c303feeb88fb9dda5390dc4880b62396b1793f98c6fef0b27c7b873', +) + + +vanadium_experiment.load_hdf5(filename=file_path) + +# %%NBQA-CELL-SEP5932b7 +vanadium_experiment.plot_data(slicer=False) + +# %%NBQA-CELL-SEP5932b7 +vanadium_experiment.plot_data(slicer=True) + +# %%NBQA-CELL-SEP5932b7 +delta_function = DeltaFunction(display_name='DeltaFunction', area=1) +sample_model = SampleModel(components=delta_function) + +# %%NBQA-CELL-SEP5932b7 +resolution_components = ComponentCollection() +res_gauss = Gaussian(width=0.1, area=1, display_name='Res. Gauss') +res_gauss.area.fixed = True +resolution_components.append_component(res_gauss) +resolution_model = ResolutionModel(components=resolution_components) + +# %%NBQA-CELL-SEP5932b7 +background_model = BackgroundModel(components=Polynomial(coefficients=[0.001])) + +# %%NBQA-CELL-SEP5932b7 +instrument_model = InstrumentModel( + resolution_model=resolution_model, + background_model=background_model, +) + +# %%NBQA-CELL-SEP5932b7 +vanadium_analysis = Analysis( + display_name='Vanadium Full Analysis', + experiment=vanadium_experiment, + sample_model=sample_model, + instrument_model=instrument_model, +) + +# %%NBQA-CELL-SEP5932b7 +fit_result_independent_single_Q = vanadium_analysis.fit(fit_method='independent', Q_index=5) +vanadium_analysis.plot_data_and_model(Q_index=5) + +# %%NBQA-CELL-SEP5932b7 +fit_result_independent_all_Q = vanadium_analysis.fit(fit_method='independent') +vanadium_analysis.plot_data_and_model() + +# %%NBQA-CELL-SEP5932b7 +# Inspect the Parameters as a scipp Dataset +vanadium_analysis.parameters_to_dataset() + +# %%NBQA-CELL-SEP5932b7 +# Plot some of fitted parameters as a function of Q +vanadium_analysis.plot_parameters(names=['DeltaFunction area']) + +# %%NBQA-CELL-SEP5932b7 +vanadium_analysis.plot_parameters(names=['Res. Gauss width']) + +# %%NBQA-CELL-SEP5932b7 +vanadium_analysis.plot_parameters(names=['energy_offset']) + +# %%NBQA-CELL-SEP5932b7 +diffusion_experiment = Experiment('Diffusion') + +file_path = pooch.retrieve( + url='https://github.com/easyscience/dynamics-lib/raw/refs/heads/master/docs/docs/tutorials/data/diffusion_data_example.h5', + known_hash='5fe846b19aacbda8b8b936eb2e5310d025dc56c25b0b353521e7d6b921f229ab', +) + +diffusion_experiment.load_hdf5(filename=file_path) + +# %%NBQA-CELL-SEP5932b7 +diffusion_experiment.plot_data(slicer=True) + +# %%NBQA-CELL-SEP5932b7 +delta_function = DeltaFunction(display_name='DeltaFunction', area=0.2) +lorentzian = Lorentzian(display_name='Lorentzian', area=0.5, width=0.3) +component_collection = ComponentCollection( + components=[delta_function, lorentzian], +) + +sample_model = SampleModel( + components=component_collection, +) + +background_model = BackgroundModel(components=Polynomial(coefficients=[0.001])) + +# %%NBQA-CELL-SEP5932b7 +instrument_model = InstrumentModel( + background_model=background_model, +) + +diffusion_analysis = Analysis( + display_name='Diffusion Analysis', + experiment=diffusion_experiment, + sample_model=sample_model, + instrument_model=instrument_model, +) + +# We need to hack in the resolution model from the vanadium analysis, +# since the setters and getters overwrite the model. This will be fixed +# asap. +diffusion_analysis.instrument_model._resolution_model = ( + vanadium_analysis.instrument_model.resolution_model +) + +# %%NBQA-CELL-SEP5932b7 +# We fix all parameters of the resolution model. +diffusion_analysis.instrument_model.resolution_model.fix_all_parameters() + +# %%NBQA-CELL-SEP5932b7 +diffusion_analysis.plot_data_and_model() + +# %%NBQA-CELL-SEP5932b7 +diffusion_analysis.fit(fit_method='independent') +diffusion_analysis.plot_data_and_model() + +# %%NBQA-CELL-SEP5932b7 +# Let us look at the most interesting fit parameters +diffusion_analysis.plot_parameters(names=['Lorentzian width', 'Lorentzian area']) + +# %%NBQA-CELL-SEP5932b7 +delta_function = DeltaFunction(display_name='DeltaFunction', area=0.2) +component_collection = ComponentCollection( + components=[delta_function], +) +diffusion_model = BrownianTranslationalDiffusion( + display_name='Brownian Translational Diffusion', diffusion_coefficient=2.4e-9, scale=0.5 +) + +sample_model = SampleModel( + components=component_collection, + diffusion_models=diffusion_model, +) + +background_model = BackgroundModel(components=Polynomial(coefficients=[0.001])) + +instrument_model = InstrumentModel( + background_model=background_model, +) + +# %%NBQA-CELL-SEP5932b7 +diffusion_model_analysis = Analysis( + display_name='Diffusion Full Analysis', + experiment=diffusion_experiment, + sample_model=sample_model, + instrument_model=instrument_model, +) + +# We again need to hack in the resolution model from the vanadium +# analysis, since the setters and getters overwrite the model. This will +# be fixed asap. +diffusion_model_analysis.instrument_model._resolution_model = ( + vanadium_analysis.instrument_model.resolution_model +) +diffusion_model_analysis.instrument_model.resolution_model.fix_all_parameters() + +# %%NBQA-CELL-SEP5932b7 +diffusion_model_analysis.plot_data_and_model() + +# %%NBQA-CELL-SEP5932b7 +diffusion_model_analysis.fit(fit_method='simultaneous') +diffusion_model_analysis.plot_data_and_model() + +# %%NBQA-CELL-SEP5932b7 +diffusion_model.get_all_parameters() diff --git a/src/easydynamics/sample_model/components/exponential.py b/src/easydynamics/sample_model/components/exponential.py index 14e82c229..56a72912c 100644 --- a/src/easydynamics/sample_model/components/exponential.py +++ b/src/easydynamics/sample_model/components/exponential.py @@ -39,6 +39,7 @@ def __init__( amplitude (Numeric | Parameter, default=1.0): Amplitude of the Exponential. center (Numeric | Parameter | None, default=None): Center of the Exponential. + If None, the center is fixed at 0. rate (Numeric | Parameter, default=1.0): Decay or growth constant of the Exponential. unit (str | sc.Unit, default='meV'): Unit of the parameters. @@ -70,7 +71,7 @@ def __init__( ) center = self._create_center_parameter( - center=center, name=display_name, fix_if_none=False, unit=self._unit + center=center, name=display_name, fix_if_none=True, unit=self._unit ) if not isinstance(rate, (Parameter, Numeric)): diff --git a/tests/unit/easydynamics/sample_model/components/test_exponential.py b/tests/unit/easydynamics/sample_model/components/test_exponential.py index 7e9499c36..d61be76e5 100644 --- a/tests/unit/easydynamics/sample_model/components/test_exponential.py +++ b/tests/unit/easydynamics/sample_model/components/test_exponential.py @@ -125,6 +125,17 @@ def test_property_setters( with pytest.raises(TypeError, match=invalid_message): setattr(exponential, prop, invalid_value) + def test_center_is_fixed_if_set_to_None(self, exponential: Exponential): + # WHEN + assert exponential.center.fixed is False + + # THEN + exponential.center = None + + # EXPECT + assert exponential.center.value == 0.0 + assert exponential.center.fixed is True + def test_evaluate(self, exponential: Exponential): # WHEN x = np.array([0.0, 0.5, 1.0]) @@ -168,6 +179,11 @@ def test_convert_unit(self, exponential: Exponential): assert exponential.rate.value == 1.2 / 1e3 assert str(exponential.rate.unit) == '1/ueV' + def test_convert_unit_incorrect_unit_raises(self, exponential: Exponential): + # WHEN THEN EXPECT + with pytest.raises(TypeError, match='unit must be a string or sc.Unit'): + exponential.convert_unit(123) + def test_convert_unit_rollback(self, exponential: Exponential): # WHEN with pytest.raises( diff --git a/tests/unit/easydynamics/sample_model/components/test_model_component.py b/tests/unit/easydynamics/sample_model/components/test_model_component.py index 3bd917422..93e42bc09 100644 --- a/tests/unit/easydynamics/sample_model/components/test_model_component.py +++ b/tests/unit/easydynamics/sample_model/components/test_model_component.py @@ -46,6 +46,11 @@ def test_convert_unit(self, dummy: DummyComponent): assert dummy.center.value == 2 * 1e3 assert dummy.width.value == 3 * 1e3 + def test_convert_unit_incorrect_unit_raises(self, dummy: DummyComponent): + # WHEN THEN EXPECT + with pytest.raises(TypeError, match='Unit must be a string or sc.Unit'): + dummy.convert_unit(123) + def test_free_and_fix_all_parameters(self, dummy): # WHEN THEN EXPECT dummy.free_all_parameters() diff --git a/tests/unit/easydynamics/sample_model/test_component_collection.py b/tests/unit/easydynamics/sample_model/test_component_collection.py index 595cdb8ed..fcc65b6f1 100644 --- a/tests/unit/easydynamics/sample_model/test_component_collection.py +++ b/tests/unit/easydynamics/sample_model/test_component_collection.py @@ -177,6 +177,10 @@ def test_is_empty_setter(self, component_collection): with pytest.raises(AttributeError, match='is_empty is a read-only property.'): component_collection.is_empty = True + def test_component_setter_empty_list(self, component_collection): + component_collection.components = [] + assert component_collection.components == [] + def test_list_component_names(self, component_collection): # WHEN THEN components = component_collection.list_component_names() @@ -198,6 +202,11 @@ def test_convert_unit(self, component_collection): for component in component_collection.components: assert component.unit == 'eV' + def test_convert_unit_incorrect_unit_raises(self, component_collection): + # WHEN THEN EXPECT + with pytest.raises(TypeError, match='Unit must be a string or sc.Unit'): + component_collection.convert_unit(123) + def test_convert_unit_failure_rolls_back(self, component_collection): # WHEN THEN # Introduce a faulty component that will fail conversion diff --git a/tests/unit/easydynamics/sample_model/test_model_base.py b/tests/unit/easydynamics/sample_model/test_model_base.py index 8b39c6f38..4368823aa 100644 --- a/tests/unit/easydynamics/sample_model/test_model_base.py +++ b/tests/unit/easydynamics/sample_model/test_model_base.py @@ -266,6 +266,11 @@ def test_convert_unit_invalid_raises(self, model_base): with pytest.raises(Exception): model_base.convert_unit('invalid_unit') + def test_convert_unit_incorrect_unit_raises(self, model_base): + # WHEN THEN EXPECT + with pytest.raises(TypeError, match='Unit must be a string or sc.Unit'): + model_base.convert_unit(123) + def test_components_setter(self, model_base): # WHEN new_component = Lorentzian(unique_name='NewLorentzian') From 6450ead852b93ed79ceb2f90cfa9f8c6acf55e87 Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Mon, 16 Mar 2026 12:08:29 +0100 Subject: [PATCH 31/31] delete temp files --- .../analysis1dnc5wfazv_nbqa_ipynb.py | 54 ----- .../tutorials/analysis5dsibvoj_nbqa_ipynb.py | 208 ------------------ ...component_collectionnkus_vfh_nbqa_ipynb.py | 39 ---- .../componentsxo0xyr8r_nbqa_ipynb.py | 76 ------- .../convolution98p78z7o_nbqa_ipynb.py | 167 -------------- .../detailed_balancetqju34kv_nbqa_ipynb.py | 46 ---- .../diffusion_model58y3qb6__nbqa_ipynb.py | 53 ----- .../experimentbgw31e06_nbqa_ipynb.py | 29 --- .../tutorials/indexqpz_wmw5_nbqa_ipynb.py | 0 .../instrument_modelw4cdd861_nbqa_ipynb.py | 35 --- .../sample_modelzmrku316_nbqa_ipynb.py | 89 -------- .../tutorial1_browniant5se346o_nbqa_ipynb.py | 193 ---------------- 12 files changed, 989 deletions(-) delete mode 100644 docs/docs/tutorials/analysis1dnc5wfazv_nbqa_ipynb.py delete mode 100644 docs/docs/tutorials/analysis5dsibvoj_nbqa_ipynb.py delete mode 100644 docs/docs/tutorials/component_collectionnkus_vfh_nbqa_ipynb.py delete mode 100644 docs/docs/tutorials/componentsxo0xyr8r_nbqa_ipynb.py delete mode 100644 docs/docs/tutorials/convolution98p78z7o_nbqa_ipynb.py delete mode 100644 docs/docs/tutorials/detailed_balancetqju34kv_nbqa_ipynb.py delete mode 100644 docs/docs/tutorials/diffusion_model58y3qb6__nbqa_ipynb.py delete mode 100644 docs/docs/tutorials/experimentbgw31e06_nbqa_ipynb.py delete mode 100644 docs/docs/tutorials/indexqpz_wmw5_nbqa_ipynb.py delete mode 100644 docs/docs/tutorials/instrument_modelw4cdd861_nbqa_ipynb.py delete mode 100644 docs/docs/tutorials/sample_modelzmrku316_nbqa_ipynb.py delete mode 100644 docs/docs/tutorials/tutorial1_browniant5se346o_nbqa_ipynb.py diff --git a/docs/docs/tutorials/analysis1dnc5wfazv_nbqa_ipynb.py b/docs/docs/tutorials/analysis1dnc5wfazv_nbqa_ipynb.py deleted file mode 100644 index 93a5d380b..000000000 --- a/docs/docs/tutorials/analysis1dnc5wfazv_nbqa_ipynb.py +++ /dev/null @@ -1,54 +0,0 @@ -# %%NBQA-CELL-SEP5932b7 -import pooch - -from easydynamics.analysis.analysis1d import Analysis1d -from easydynamics.experiment import Experiment -from easydynamics.sample_model import DeltaFunction -from easydynamics.sample_model import Gaussian -from easydynamics.sample_model import Polynomial -from easydynamics.sample_model.background_model import BackgroundModel -from easydynamics.sample_model.instrument_model import InstrumentModel -from easydynamics.sample_model.resolution_model import ResolutionModel -from easydynamics.sample_model.sample_model import SampleModel - -hash(0xF69AF408) - -# %%NBQA-CELL-SEP5932b7 -vanadium_experiment = Experiment('Vanadium') - -file_path = pooch.retrieve( - url='https://github.com/easyscience/dynamics-lib/raw/refs/heads/master/docs/docs/tutorials/data/vanadium_data_example.h5', - known_hash='16cc1b327c303feeb88fb9dda5390dc4880b62396b1793f98c6fef0b27c7b873', -) - -vanadium_experiment.load_hdf5(filename=file_path) - -# %%NBQA-CELL-SEP5932b7 -# Example of Analysis1d with a simple sample model and instrument model -delta_function = DeltaFunction(display_name='DeltaFunction', area=1) -sample_model = SampleModel( - components=delta_function, -) - -res_gauss = Gaussian(width=0.1) -resolution_model = ResolutionModel(components=res_gauss) - - -background_model = BackgroundModel(components=Polynomial(coefficients=[0.001])) - -instrument_model = InstrumentModel( - resolution_model=resolution_model, - background_model=background_model, -) - -my_analysis = Analysis1d( - display_name='Vanadium Analysis', - experiment=vanadium_experiment, - sample_model=sample_model, - instrument_model=instrument_model, - Q_index=5, -) - -fit_result = my_analysis.fit() -fig = my_analysis.plot_data_and_model() -fig diff --git a/docs/docs/tutorials/analysis5dsibvoj_nbqa_ipynb.py b/docs/docs/tutorials/analysis5dsibvoj_nbqa_ipynb.py deleted file mode 100644 index 5a84a3762..000000000 --- a/docs/docs/tutorials/analysis5dsibvoj_nbqa_ipynb.py +++ /dev/null @@ -1,208 +0,0 @@ -# %%NBQA-CELL-SEP5932b7 -# Imports -import pooch - -from easydynamics.analysis.analysis import Analysis -from easydynamics.experiment import Experiment -from easydynamics.sample_model import BrownianTranslationalDiffusion -from easydynamics.sample_model import ComponentCollection -from easydynamics.sample_model import DeltaFunction -from easydynamics.sample_model import Gaussian -from easydynamics.sample_model import Lorentzian -from easydynamics.sample_model import Polynomial -from easydynamics.sample_model.background_model import BackgroundModel -from easydynamics.sample_model.instrument_model import InstrumentModel -from easydynamics.sample_model.resolution_model import ResolutionModel -from easydynamics.sample_model.sample_model import SampleModel - -hash(0x5D2FE732) - -# %%NBQA-CELL-SEP5932b7 -# Load the vanadium data -vanadium_experiment = Experiment('Vanadium') -file_path = pooch.retrieve( - url='https://github.com/easyscience/dynamics-lib/raw/refs/heads/master/docs/docs/tutorials/data/vanadium_data_example.h5', - known_hash='16cc1b327c303feeb88fb9dda5390dc4880b62396b1793f98c6fef0b27c7b873', -) - -vanadium_experiment.load_hdf5(filename=file_path) - -# %%NBQA-CELL-SEP5932b7 -# Example of Analysis with a simple sample model and instrument model -# The scattering from vanadium is purely elastic, so we model it with a -# delta function -delta_function = DeltaFunction(display_name='DeltaFunction', area=1) -sample_model = SampleModel( - components=delta_function, -) - -# The resolution is in this case modeled as a Gaussian. However, we can -# add as many components as we like to the resolution model -res_gauss = Gaussian(width=0.1) -res_gauss.area.fixed = True -resolution_components = ComponentCollection() -resolution_components.append_component(res_gauss) -resolution_model = ResolutionModel(components=resolution_components) - -# The background model is created in the same way. In this case, we use -# a flat background -background_model = BackgroundModel(components=Polynomial(coefficients=[0.001])) - -# We combine the resolution abd background model into an instrument -# model. This model also contains a small energy offset to account for -# instrument misalignment. - -instrument_model = InstrumentModel( - resolution_model=resolution_model, - background_model=background_model, -) - -# Collect everything into an analysis object. -vanadium_analysis = Analysis( - display_name='Vanadium Full Analysis', - experiment=vanadium_experiment, - sample_model=sample_model, - instrument_model=instrument_model, -) - -# Let us first fit a single Q index and plot the data and model to see -# how it looks -fit_result_independent_single_Q = vanadium_analysis.fit(fit_method='independent', Q_index=5) -vanadium_analysis.plot_data_and_model(Q_index=5) - -# %%NBQA-CELL-SEP5932b7 -# It looks good, so let us fit all Q indices independently and plot the -# results -fit_result_independent_all_Q = vanadium_analysis.fit(fit_method='independent') -vanadium_analysis.plot_data_and_model() - -# %%NBQA-CELL-SEP5932b7 -# Inspect the Parameters as a scipp Dataset -vanadium_analysis.parameters_to_dataset() - -# %%NBQA-CELL-SEP5932b7 -# Plot some of fitted parameters as a function of Q -vanadium_analysis.plot_parameters(names=['DeltaFunction area']) - -# %%NBQA-CELL-SEP5932b7 -vanadium_analysis.plot_parameters(names=['Gaussian width']) - -# %%NBQA-CELL-SEP5932b7 -vanadium_analysis.plot_parameters(names=['energy_offset']) - -# %%NBQA-CELL-SEP5932b7 -# Now it's time to look at the data we want to fit. We first load the -# data -diffusion_experiment = Experiment('Diffusion') - -file_path = pooch.retrieve( - url='https://github.com/easyscience/dynamics-lib/raw/refs/heads/master/docs/docs/tutorials/data/diffusion_data_example.h5', - known_hash='5fe846b19aacbda8b8b936eb2e5310d025dc56c25b0b353521e7d6b921f229ab', -) - -diffusion_experiment.load_hdf5(filename=file_path) - -# %%NBQA-CELL-SEP5932b7 -# Now we set up the model, similarly to how we set up the model for the -# vanadium data. - -delta_function = DeltaFunction(display_name='DeltaFunction', area=0.2) -lorentzian = Lorentzian(display_name='Lorentzian', area=0.5, width=0.3) -component_collection = ComponentCollection( - components=[delta_function, lorentzian], -) - -sample_model = SampleModel( - components=component_collection, -) - -background_model = BackgroundModel(components=Polynomial(coefficients=[0.001])) - -instrument_model = InstrumentModel( - background_model=background_model, -) - -diffusion_analysis = Analysis( - display_name='Diffusion Full Analysis', - experiment=diffusion_experiment, - sample_model=sample_model, - instrument_model=instrument_model, -) - -# We need to hack in the resolution model from the vanadium analysis, -# since the setters and getters overwrite the model. This will be fixed -# asap. -diffusion_analysis.instrument_model._resolution_model = ( - vanadium_analysis.instrument_model.resolution_model -) - -# We fix all parameters of the resolution model. -diffusion_analysis.instrument_model.resolution_model.fix_all_parameters() - -# %%NBQA-CELL-SEP5932b7 -# Let us see how good the starting parameters are -diffusion_analysis.plot_data_and_model() - -# %%NBQA-CELL-SEP5932b7 -# Now we fit the data and plot the result. Looks good! -diffusion_analysis.fit(fit_method='independent') -diffusion_analysis.plot_data_and_model() - -# %%NBQA-CELL-SEP5932b7 -# Let us look at the most interesting fit parameters -diffusion_analysis.plot_parameters(names=['Lorentzian width', 'Lorentzian area']) - -# %%NBQA-CELL-SEP5932b7 -# It will be possible to fit this to a DiffusionModel, but that will -# come later. - -# %%NBQA-CELL-SEP5932b7 -# Let us now fit directly to a diffusion model. We replace the -# Lorentzian with a Brownian translational diffusion model and keep the -# other parameters the same. -delta_function = DeltaFunction(display_name='DeltaFunction', area=0.2) -component_collection = ComponentCollection( - components=[delta_function], -) -diffusion_model = BrownianTranslationalDiffusion( - display_name='Brownian Translational Diffusion', diffusion_coefficient=2.4e-9, scale=0.5 -) - -sample_model = SampleModel( - components=component_collection, - diffusion_models=diffusion_model, -) - -background_model = BackgroundModel(components=Polynomial(coefficients=[0.001])) - -instrument_model = InstrumentModel( - background_model=background_model, -) - -diffusion_model_analysis = Analysis( - display_name='Diffusion Full Analysis', - experiment=diffusion_experiment, - sample_model=sample_model, - instrument_model=instrument_model, -) - -# We again need to hack in the resolution model from the vanadium -# analysis, since the setters and getters overwrite the model. This will -# be fixed asap. -diffusion_model_analysis.instrument_model._resolution_model = ( - vanadium_analysis.instrument_model.resolution_model -) -diffusion_model_analysis.instrument_model.resolution_model.fix_all_parameters() - -# Let us see how good the starting parameters are -diffusion_model_analysis.plot_data_and_model() - -# %%NBQA-CELL-SEP5932b7 -# We now fit all the data simultaneously to the diffusion model, then -# plot the result. Looks good. -diffusion_model_analysis.fit(fit_method='simultaneous') -diffusion_model_analysis.plot_data_and_model() - -# %%NBQA-CELL-SEP5932b7 -# Let us look at the fitted diffusion coefficient -diffusion_model.get_all_parameters() diff --git a/docs/docs/tutorials/component_collectionnkus_vfh_nbqa_ipynb.py b/docs/docs/tutorials/component_collectionnkus_vfh_nbqa_ipynb.py deleted file mode 100644 index c84b8f440..000000000 --- a/docs/docs/tutorials/component_collectionnkus_vfh_nbqa_ipynb.py +++ /dev/null @@ -1,39 +0,0 @@ -# %%NBQA-CELL-SEP5932b7 -import matplotlib.pyplot as plt -import numpy as np - -from easydynamics.sample_model import ComponentCollection -from easydynamics.sample_model import DampedHarmonicOscillator -from easydynamics.sample_model import Gaussian -from easydynamics.sample_model import Lorentzian -from easydynamics.sample_model import Polynomial - -hash(0x9AFE4A6B) - -# %%NBQA-CELL-SEP5932b7 -component_collection = ComponentCollection() - -# Creating components -gaussian = Gaussian(display_name='Gaussian', width=0.5, area=1) -dho = DampedHarmonicOscillator(display_name='DHO', center=1.0, width=0.3, area=2.0) -lorentzian = Lorentzian(display_name='Lorentzian', center=-1.0, width=0.2, area=1.0) -polynomial = Polynomial(display_name='Polynomial', coefficients=[0.1, 0, 0.5]) # y=0.1+0.5*x^2 - -# Adding components to the component collection -component_collection.append_component(gaussian) -component_collection.append_component(dho) -component_collection.append_component(lorentzian) -component_collection.append_component(polynomial) - -x = np.linspace(-2, 2, 100) - -plt.figure() -y = component_collection.evaluate(x) -plt.plot(x, y, label='Component collection') - -for component in component_collection.components: - y = component.evaluate(x) - plt.plot(x, y, label=component.display_name) - -plt.legend() -plt.show() diff --git a/docs/docs/tutorials/componentsxo0xyr8r_nbqa_ipynb.py b/docs/docs/tutorials/componentsxo0xyr8r_nbqa_ipynb.py deleted file mode 100644 index 1b66e3f4a..000000000 --- a/docs/docs/tutorials/componentsxo0xyr8r_nbqa_ipynb.py +++ /dev/null @@ -1,76 +0,0 @@ -# %%NBQA-CELL-SEP5932b7 -import matplotlib.pyplot as plt -import numpy as np -import scipp as sc - -from easydynamics.sample_model import DampedHarmonicOscillator -from easydynamics.sample_model import DeltaFunction -from easydynamics.sample_model import Exponential -from easydynamics.sample_model import Gaussian -from easydynamics.sample_model import Lorentzian -from easydynamics.sample_model import Polynomial - -hash(0xE0A2D949) - -# %%NBQA-CELL-SEP5932b7 -# Creating a component -gaussian = Gaussian(display_name='Gaussian', width=0.5, area=1) -dho = DampedHarmonicOscillator(display_name='DHO', center=1.0, width=0.3, area=2.0) -lorentzian = Lorentzian(display_name='Lorentzian', center=-1.0, width=0.2, area=1.0) -polynomial = Polynomial(display_name='Polynomial', coefficients=[0.1, 0, 0.5]) # y=0.1+0.5*x^2 -exponential = Exponential(display_name='Exponential', amplitude=1.0, rate=-0.5) - -x = np.linspace(-2, 2, 100) - -plt.figure() -y = gaussian.evaluate(x) -plt.plot(x, y, label='Gaussian') -y = dho.evaluate(x) -plt.plot(x, y, label='DHO') -y = lorentzian.evaluate(x) -plt.plot(x, y, label='Lorentzian') -y = polynomial.evaluate(x) -plt.plot(x, y, label='Polynomial') -y = exponential.evaluate(x) -plt.plot(x, y, label='Exponential') -plt.legend() -plt.show() - -# %%NBQA-CELL-SEP5932b7 -# The area under the DHO curve is indeed equal to the area parameter. -xx = np.linspace(-15, 15, 10000) -yy = dho.evaluate(xx) -area = np.trapezoid(yy, xx) -print(f'Area under DHO curve: {area:.4f}') - -# %%NBQA-CELL-SEP5932b7 -delta = DeltaFunction(display_name='Delta', center=0.0, area=1.0) -x1 = np.linspace(-2, 2, 100) -y = delta.evaluate(x1) -x2 = np.linspace(-2, 2, 51) -y2 = delta.evaluate(x2) -plt.figure() -plt.plot(x1, y, label='Delta Function') -plt.plot(x2, y2, label='Delta Function (coarser)') -plt.legend() -plt.show() -# The area under the Delta function is indeed equal to -# the area parameter. -xx = np.linspace(-2, 2, 10000) -yy = delta.evaluate(xx) -area = np.trapezoid(y, x1) -print(area) - -# %%NBQA-CELL-SEP5932b7 -x1 = sc.linspace(dim='x', start=-2.0, stop=2.0, num=100, unit='meV') -x2 = sc.linspace(dim='x', start=-2.0 * 1e3, stop=2.0 * 1e3, num=101, unit='microeV') - -polynomial = Polynomial(display_name='Polynomial', coefficients=[0.1, 0, 0.5]) # y=0.1+0.5*x^2 -y1 = polynomial.evaluate(x1) -y2 = polynomial.evaluate(x2) - -plt.figure() -plt.plot(x1.values, y1, label='Polynomial meV', color='blue') -plt.plot(x2.values / 1000, y2, label='Polynomial microeV', linestyle='dashed', color='orange') -plt.legend() -plt.show() diff --git a/docs/docs/tutorials/convolution98p78z7o_nbqa_ipynb.py b/docs/docs/tutorials/convolution98p78z7o_nbqa_ipynb.py deleted file mode 100644 index 3a6950fdd..000000000 --- a/docs/docs/tutorials/convolution98p78z7o_nbqa_ipynb.py +++ /dev/null @@ -1,167 +0,0 @@ -# %%NBQA-CELL-SEP5932b7 -import matplotlib.pyplot as plt -import numpy as np - -from easydynamics.convolution import Convolution -from easydynamics.sample_model import DampedHarmonicOscillator -from easydynamics.sample_model import DeltaFunction -from easydynamics.sample_model import Gaussian -from easydynamics.sample_model import Lorentzian -from easydynamics.sample_model.component_collection import ComponentCollection -from easydynamics.utils import _detailed_balance_factor as detailed_balance_factor - -hash(0xCC7EF502) - -# %%NBQA-CELL-SEP5932b7 -# Standard example of convolution of a sample model with a -# resolution model -sample_components = ComponentCollection() -gaussian = Gaussian(display_name='Gaussian', width=0.5, area=1) -dho = DampedHarmonicOscillator(display_name='DHO', center=1.0, width=0.3, area=2.0) -lorentzian = Lorentzian(display_name='Lorentzian', center=-1.0, width=0.2, area=1.0) -delta = DeltaFunction(display_name='Delta', center=0.4, area=0.5) -sample_components.append_component(gaussian) -# sample_components.append_component(dho) -sample_components.append_component(lorentzian) -sample_components.append_component(delta) - -resolution_components = ComponentCollection() -resolution_gaussian = Gaussian(display_name='Resolution Gaussian', width=0.015, area=0.8) -resolution_lorentzian = Lorentzian(display_name='Resolution Lorentzian', width=0.025, area=0.2) -resolution_components.append_component(resolution_gaussian) -resolution_components.append_component(resolution_lorentzian) - -energy = np.linspace(-2, 2, 100) - -convolver = Convolution( - sample_components=sample_components, resolution_components=resolution_components, energy=energy -) -y = convolver.convolution() -plt.figure() -plt.plot(energy, y, label='Convoluted Model') -plt.xlabel('Energy (meV)') -plt.ylabel('Intensity (arb. units)') -plt.title('Convolution of Sample Model with Resolution Model') - -plt.plot(energy, sample_components.evaluate(energy), label='Sample Model', linestyle='--') -plt.plot(energy, resolution_components.evaluate(energy), label='Resolution Model', linestyle=':') - -plt.legend() -# set the limit on the y axis -plt.ylim(0, 6) -plt.show() - -# %%NBQA-CELL-SEP5932b7 -# Use some of the extra settings for the numerical convolution -sample_components = ComponentCollection() -gaussian = Gaussian(display_name='Gaussian', width=0.3, area=1) -dho = DampedHarmonicOscillator(display_name='DHO', center=1.0, width=0.3, area=2.0) -lorentzian = Lorentzian(display_name='Lorentzian', center=-1.0, width=0.2, area=1.0) -delta = DeltaFunction(display_name='Delta', center=0.4, area=0.5) -sample_components.append_component(gaussian) -sample_components.append_component(dho) -sample_components.append_component(lorentzian) -sample_components.append_component(delta) - -resolution_components = ComponentCollection() -resolution_gaussian = Gaussian(display_name='Resolution Gaussian', width=0.15, area=0.8) -resolution_lorentzian = Lorentzian(display_name='Resolution Lorentzian', width=0.25, area=0.2) -resolution_components.append_component(resolution_gaussian) -resolution_components.append_component(resolution_lorentzian) - -energy = np.linspace(-2, 2, 100) - - -temperature = 10.0 # Temperature in Kelvin -energy_offset = 0.5 -upsample_factor = 5 -extension_factor = 0.5 -plt.figure() -plt.xlabel('Energy (meV)') -plt.ylabel('Intensity (arb. units)') - -convolver = Convolution( - sample_components=sample_components, - resolution_components=resolution_components, - energy=energy - energy_offset, - upsample_factor=upsample_factor, - extension_factor=extension_factor, - temperature=temperature, - normalize_detailed_balance=True, -) -y = convolver.convolution() - - -plt.plot(energy, y, label='Convoluted Model') - -plt.plot( - energy, - sample_components.evaluate(energy - energy_offset) - * detailed_balance_factor(energy - energy_offset, temperature), - label='Sample Model with DB', - linestyle='--', -) - -plt.plot(energy, resolution_components.evaluate(energy), label='Resolution Model', linestyle=':') -plt.title('Convolution of Sample Model with Resolution Model with detailed balancing') - -plt.legend() -plt.ylim(0, 2.5) -plt.show() - -# %%NBQA-CELL-SEP5932b7 -# Use some of the extra settings for the numerical convolution -sample_components = ComponentCollection() -gaussian = Gaussian(display_name='Gaussian', width=0.3, area=1) -dho = DampedHarmonicOscillator(display_name='DHO', center=1.0, width=0.3, area=2.0) -lorentzian = Lorentzian(display_name='Lorentzian', center=-1.0, width=0.2, area=1.0) -delta = DeltaFunction(display_name='Delta', center=0.4, area=0.5) -sample_components.append_component(gaussian) -# sample_components.append_component(dho) -sample_components.append_component(lorentzian) -# sample_components.append_component(delta) - -resolution_components = ComponentCollection() -resolution_gaussian = Gaussian(display_name='Resolution Gaussian', width=0.15, area=0.8) -resolution_lorentzian = Lorentzian(display_name='Resolution Lorentzian', width=0.25, area=0.2) -resolution_components.append_component(resolution_gaussian) -# resolution_components.append_component(resolution_lorentzian) - -energy = np.linspace(-2, 2, 100) - - -temperature = 10.0 # Temperature in Kelvin -energy_offset = 0.2 -upsample_factor = 5 -extension_factor = 0.5 -plt.figure() -plt.xlabel('Energy (meV)') -plt.ylabel('Intensity (arb. units)') - -convolver = Convolution( - sample_components=sample_components, - resolution_components=resolution_components, - energy=energy, - upsample_factor=upsample_factor, - extension_factor=extension_factor, - energy_offset=energy_offset, - temperature=temperature, -) -y = convolver.convolution() - - -plt.plot(energy, y, label='Convoluted Model') - -plt.plot( - energy, - sample_components.evaluate(energy - energy_offset), - label='Sample Model', - linestyle='--', -) - -plt.plot(energy, resolution_components.evaluate(energy), label='Resolution Model', linestyle=':') -plt.title('Convolution of Sample Model with Resolution Model') - -plt.legend() -plt.ylim(0, 2.5) -plt.show() diff --git a/docs/docs/tutorials/detailed_balancetqju34kv_nbqa_ipynb.py b/docs/docs/tutorials/detailed_balancetqju34kv_nbqa_ipynb.py deleted file mode 100644 index 58cbc2fc9..000000000 --- a/docs/docs/tutorials/detailed_balancetqju34kv_nbqa_ipynb.py +++ /dev/null @@ -1,46 +0,0 @@ -# %%NBQA-CELL-SEP5932b7 -import matplotlib.pyplot as plt -import numpy as np - -from easydynamics.utils import _detailed_balance_factor as detailed_balance_factor - -hash(0x36C9BD93) - -# %%NBQA-CELL-SEP5932b7 -temperatures = [1, 10, 100] -temperature_unit = 'K' -energy = np.linspace(-1, 1, 100) -# energy=1.0 -energy_unit = 'meV' - -plt.figure() -for temperature in temperatures: - DBF = detailed_balance_factor(energy, temperature, energy_unit, temperature_unit) - plt.plot(energy, DBF, label=f'T={temperature} K') -plt.legend() -plt.xlabel('Energy transfer (meV)') -plt.ylabel('Detailed balance factor') -plt.title( - 'Detailed balance factor for different temperatures, \n ' - 'normalized to 1 at zero energy transfer' -) -plt.show() - -# %%NBQA-CELL-SEP5932b7 -temperatures = [1, 10, 100] -temperature_unit = 'K' -energy = np.linspace(-1, 1, 100) -# energy=1.0 -energy_unit = 'meV' - -plt.figure() -for temperature in temperatures: - DBF = detailed_balance_factor( - energy, temperature, energy_unit, temperature_unit, divide_by_temperature=False - ) - plt.plot(energy, DBF, label=f'T={temperature} K') -plt.legend() -plt.xlabel('Energy transfer (meV)') -plt.ylabel('Detailed balance factor') -plt.title('Detailed balance factor for different temperatures, not normalized') -plt.show() diff --git a/docs/docs/tutorials/diffusion_model58y3qb6__nbqa_ipynb.py b/docs/docs/tutorials/diffusion_model58y3qb6__nbqa_ipynb.py deleted file mode 100644 index 3236521c6..000000000 --- a/docs/docs/tutorials/diffusion_model58y3qb6__nbqa_ipynb.py +++ /dev/null @@ -1,53 +0,0 @@ -# %%NBQA-CELL-SEP5932b7 -import matplotlib.pyplot as plt -import numpy as np - -from easydynamics.sample_model import BrownianTranslationalDiffusion - -hash(0x4D38875) - -# %%NBQA-CELL-SEP5932b7 -# Create Brownian Translational Diffusion model -# and plot the model for different Q values. -# Q is in Angstrom^-1 and energy in meV. - -Q = np.linspace(0.5, 2, 7) -energy = np.linspace(-2, 2, 501) -scale = 1.0 -diffusion_coefficient = 2.4e-9 # m^2/s - -diffusion_model = BrownianTranslationalDiffusion( - display_name='DiffusionModel', - scale=scale, - diffusion_coefficient=diffusion_coefficient, -) - -component_collections = diffusion_model.create_component_collections(Q) - - -cmap = plt.cm.jet -nQ = len(component_collections) -plt.figure() -for Q_index in range(len(component_collections)): - color = cmap(Q_index / (nQ - 1)) - y = component_collections[Q_index].evaluate(energy) - plt.plot(energy, y, label=f'Q={Q[Q_index]} Å^-1', color=color) - -plt.legend() -plt.show() -plt.xlabel('Energy (meV)') -plt.ylabel('Intensity (arb. units)') -plt.title('Brownian Translational Diffusion Model') - -# %%NBQA-CELL-SEP5932b7 -# Calculate and plot the half width at half maximum (HWHM) as function -# of Q -Q = np.linspace(0.1, 2, 101) -HWHM = diffusion_model.calculate_width(Q) -plt.figure() -plt.plot(Q, HWHM) -plt.xlabel('Q (Å$^{-1}$)') -plt.ylabel('HWHM (meV)') -plt.xlim(0, 2.5) -plt.ylim(0, max(HWHM) * 1.1) -plt.title('HWHM vs Q for Brownian Translational Diffusion') diff --git a/docs/docs/tutorials/experimentbgw31e06_nbqa_ipynb.py b/docs/docs/tutorials/experimentbgw31e06_nbqa_ipynb.py deleted file mode 100644 index d4d7f3416..000000000 --- a/docs/docs/tutorials/experimentbgw31e06_nbqa_ipynb.py +++ /dev/null @@ -1,29 +0,0 @@ -# %%NBQA-CELL-SEP5932b7 -import pooch - -from easydynamics.experiment import Experiment - -hash(0x3AE3B342) - -# %%NBQA-CELL-SEP5932b7 -# Load and plot example vanadium data -# Load the vanadium data -vanadium_experiment = Experiment('Vanadium') - -file_path = pooch.retrieve( - url='https://github.com/easyscience/dynamics-lib/raw/refs/heads/master/docs/docs/tutorials/data/vanadium_data_example.h5', - known_hash='16cc1b327c303feeb88fb9dda5390dc4880b62396b1793f98c6fef0b27c7b873', -) - -vanadium_experiment.load_hdf5(filename=file_path) - -vanadium_experiment.plot_data() - -# %%NBQA-CELL-SEP5932b7 -# Rebin the data and plot again -vanadium_experiment.rebin({'Q': 5, 'energy': 50}) -vanadium_experiment.plot_data() - -# %%NBQA-CELL-SEP5932b7 -# Plot using the plopp slicer with extra arguments -vanadium_experiment.plot_data(slicer=True, keep='energy', vmin=0, vmax=2.0) diff --git a/docs/docs/tutorials/indexqpz_wmw5_nbqa_ipynb.py b/docs/docs/tutorials/indexqpz_wmw5_nbqa_ipynb.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/docs/docs/tutorials/instrument_modelw4cdd861_nbqa_ipynb.py b/docs/docs/tutorials/instrument_modelw4cdd861_nbqa_ipynb.py deleted file mode 100644 index 9b614b9e0..000000000 --- a/docs/docs/tutorials/instrument_modelw4cdd861_nbqa_ipynb.py +++ /dev/null @@ -1,35 +0,0 @@ -# %%NBQA-CELL-SEP5932b7 -import numpy as np - -from easydynamics.sample_model import Gaussian -from easydynamics.sample_model import Polynomial -from easydynamics.sample_model.background_model import BackgroundModel -from easydynamics.sample_model.instrument_model import InstrumentModel -from easydynamics.sample_model.resolution_model import ResolutionModel - -hash(0x51DBF3) - -# %%NBQA-CELL-SEP5932b7 -# Create a BackgroundModel and a ResolutionModel and add them to an -# InstrumentModel - -Q = np.linspace(0.1, 2.0, 5) - -background_model = BackgroundModel() -background_model.components = Polynomial(coefficients=[1, 0.1, 0.01]) - -resolution_model = ResolutionModel() -resolution_model.append_component(Gaussian(width=0.05)) - -instrument_model = InstrumentModel( - Q=Q, - resolution_model=resolution_model, - background_model=background_model, -) - -# %%NBQA-CELL-SEP5932b7 -instrument_model.get_all_variables(Q_index=1) - -# %%NBQA-CELL-SEP5932b7 -instrument_model.fix_resolution_parameters() -instrument_model.get_all_variables(Q_index=1) diff --git a/docs/docs/tutorials/sample_modelzmrku316_nbqa_ipynb.py b/docs/docs/tutorials/sample_modelzmrku316_nbqa_ipynb.py deleted file mode 100644 index ea977d759..000000000 --- a/docs/docs/tutorials/sample_modelzmrku316_nbqa_ipynb.py +++ /dev/null @@ -1,89 +0,0 @@ -# %%NBQA-CELL-SEP5932b7 -import matplotlib.pyplot as plt -import numpy as np - -from easydynamics.sample_model import BrownianTranslationalDiffusion -from easydynamics.sample_model import ComponentCollection -from easydynamics.sample_model import DampedHarmonicOscillator -from easydynamics.sample_model import Gaussian -from easydynamics.sample_model import Polynomial -from easydynamics.sample_model.background_model import BackgroundModel -from easydynamics.sample_model.resolution_model import ResolutionModel -from easydynamics.sample_model.sample_model import SampleModel - -hash(0xFF4B0398) - -# %%NBQA-CELL-SEP5932b7 -# Create a diffusion_model and components for the SampleModel -Q = np.linspace(0.5, 2, 7) -energy = np.linspace(-2, 2, 501) - -scale = 1.0 -diffusion_coefficient = 2.4e-9 # m^2/s -diffusion_model = BrownianTranslationalDiffusion( - display_name='DiffusionModel', - scale=scale, - diffusion_coefficient=diffusion_coefficient, -) - - -# Creating components -component_collection = ComponentCollection() -gaussian = Gaussian(display_name='Gaussian', width=0.2, area=1, center=1.5) -dho = DampedHarmonicOscillator(display_name='DHO', center=1.0, width=0.3, area=2.0) - -# Adding components to the component collection -component_collection.append_component(gaussian) -component_collection.append_component(dho) - -sample_model = SampleModel( - diffusion_models=diffusion_model, - components=component_collection, - Q=Q, - unit='meV', - display_name='MySampleModel', - temperature=10, -) - -# Calculate the model intensity at the specified Q and energy values -# y_list will be a list of intensity arrays, one for each Q value -sample_intensity = sample_model.evaluate(energy) - -plt.figure() -for i, Q_value in enumerate(Q): - plt.plot(energy, sample_intensity[i], label=f'Q={Q_value:.2f} Å⁻¹') -plt.xlabel('Energy (meV)') -plt.ylabel('Intensity (a.u.)') -plt.title('Sample Model Evaluation at 10 K') -plt.legend() -plt.show() - -# %%NBQA-CELL-SEP5932b7 -# Create a BackgroundModel and show other ways to set Q and components - -background_model = BackgroundModel() -background_model.Q = Q - -background_model.components = Polynomial(coefficients=[1, 0.1, 0.01]) -background = background_model.evaluate(energy) - -# Also create a ResolutionModel. -# It doesn't do anything here, but shows how to set it up. -resolution_model = ResolutionModel() -resolution_model.Q = Q -resolution_model.append_component(Gaussian(width=0.05)) -resolution = resolution_model.evaluate(energy) - -# %%NBQA-CELL-SEP5932b7 -# Plot the three models together -for i, Q_value in enumerate(Q): - plt.figure() - plt.plot(energy, sample_intensity[i], label='Sample model') - plt.plot(energy, background[i], label='Background model') - plt.plot(energy, resolution[i], label='Resolution model') - plt.legend() - plt.xlabel('Energy (meV)') - plt.ylabel('Intensity (a.u.)') - plt.title(f'Sample Model Evaluation at 10 K and Q ={Q_value:.2f} Å⁻¹') - plt.legend() - plt.show() diff --git a/docs/docs/tutorials/tutorial1_browniant5se346o_nbqa_ipynb.py b/docs/docs/tutorials/tutorial1_browniant5se346o_nbqa_ipynb.py deleted file mode 100644 index c9ff23f92..000000000 --- a/docs/docs/tutorials/tutorial1_browniant5se346o_nbqa_ipynb.py +++ /dev/null @@ -1,193 +0,0 @@ -# %%NBQA-CELL-SEP5932b7 -# Imports -import pooch - -from easydynamics.analysis.analysis import Analysis -from easydynamics.experiment import Experiment -from easydynamics.sample_model import BrownianTranslationalDiffusion -from easydynamics.sample_model import ComponentCollection -from easydynamics.sample_model import DeltaFunction -from easydynamics.sample_model import Gaussian -from easydynamics.sample_model import Lorentzian -from easydynamics.sample_model import Polynomial -from easydynamics.sample_model.background_model import BackgroundModel -from easydynamics.sample_model.instrument_model import InstrumentModel -from easydynamics.sample_model.resolution_model import ResolutionModel -from easydynamics.sample_model.sample_model import SampleModel - -# Make the plots interactive -hash(0x69D94788) - -# %%NBQA-CELL-SEP5932b7 -# Load the vanadium data -vanadium_experiment = Experiment('Vanadium') - -file_path = pooch.retrieve( - url='https://github.com/easyscience/dynamics-lib/raw/refs/heads/master/docs/docs/tutorials/data/vanadium_data_example.h5', - known_hash='16cc1b327c303feeb88fb9dda5390dc4880b62396b1793f98c6fef0b27c7b873', -) - - -vanadium_experiment.load_hdf5(filename=file_path) - -# %%NBQA-CELL-SEP5932b7 -vanadium_experiment.plot_data(slicer=False) - -# %%NBQA-CELL-SEP5932b7 -vanadium_experiment.plot_data(slicer=True) - -# %%NBQA-CELL-SEP5932b7 -delta_function = DeltaFunction(display_name='DeltaFunction', area=1) -sample_model = SampleModel(components=delta_function) - -# %%NBQA-CELL-SEP5932b7 -resolution_components = ComponentCollection() -res_gauss = Gaussian(width=0.1, area=1, display_name='Res. Gauss') -res_gauss.area.fixed = True -resolution_components.append_component(res_gauss) -resolution_model = ResolutionModel(components=resolution_components) - -# %%NBQA-CELL-SEP5932b7 -background_model = BackgroundModel(components=Polynomial(coefficients=[0.001])) - -# %%NBQA-CELL-SEP5932b7 -instrument_model = InstrumentModel( - resolution_model=resolution_model, - background_model=background_model, -) - -# %%NBQA-CELL-SEP5932b7 -vanadium_analysis = Analysis( - display_name='Vanadium Full Analysis', - experiment=vanadium_experiment, - sample_model=sample_model, - instrument_model=instrument_model, -) - -# %%NBQA-CELL-SEP5932b7 -fit_result_independent_single_Q = vanadium_analysis.fit(fit_method='independent', Q_index=5) -vanadium_analysis.plot_data_and_model(Q_index=5) - -# %%NBQA-CELL-SEP5932b7 -fit_result_independent_all_Q = vanadium_analysis.fit(fit_method='independent') -vanadium_analysis.plot_data_and_model() - -# %%NBQA-CELL-SEP5932b7 -# Inspect the Parameters as a scipp Dataset -vanadium_analysis.parameters_to_dataset() - -# %%NBQA-CELL-SEP5932b7 -# Plot some of fitted parameters as a function of Q -vanadium_analysis.plot_parameters(names=['DeltaFunction area']) - -# %%NBQA-CELL-SEP5932b7 -vanadium_analysis.plot_parameters(names=['Res. Gauss width']) - -# %%NBQA-CELL-SEP5932b7 -vanadium_analysis.plot_parameters(names=['energy_offset']) - -# %%NBQA-CELL-SEP5932b7 -diffusion_experiment = Experiment('Diffusion') - -file_path = pooch.retrieve( - url='https://github.com/easyscience/dynamics-lib/raw/refs/heads/master/docs/docs/tutorials/data/diffusion_data_example.h5', - known_hash='5fe846b19aacbda8b8b936eb2e5310d025dc56c25b0b353521e7d6b921f229ab', -) - -diffusion_experiment.load_hdf5(filename=file_path) - -# %%NBQA-CELL-SEP5932b7 -diffusion_experiment.plot_data(slicer=True) - -# %%NBQA-CELL-SEP5932b7 -delta_function = DeltaFunction(display_name='DeltaFunction', area=0.2) -lorentzian = Lorentzian(display_name='Lorentzian', area=0.5, width=0.3) -component_collection = ComponentCollection( - components=[delta_function, lorentzian], -) - -sample_model = SampleModel( - components=component_collection, -) - -background_model = BackgroundModel(components=Polynomial(coefficients=[0.001])) - -# %%NBQA-CELL-SEP5932b7 -instrument_model = InstrumentModel( - background_model=background_model, -) - -diffusion_analysis = Analysis( - display_name='Diffusion Analysis', - experiment=diffusion_experiment, - sample_model=sample_model, - instrument_model=instrument_model, -) - -# We need to hack in the resolution model from the vanadium analysis, -# since the setters and getters overwrite the model. This will be fixed -# asap. -diffusion_analysis.instrument_model._resolution_model = ( - vanadium_analysis.instrument_model.resolution_model -) - -# %%NBQA-CELL-SEP5932b7 -# We fix all parameters of the resolution model. -diffusion_analysis.instrument_model.resolution_model.fix_all_parameters() - -# %%NBQA-CELL-SEP5932b7 -diffusion_analysis.plot_data_and_model() - -# %%NBQA-CELL-SEP5932b7 -diffusion_analysis.fit(fit_method='independent') -diffusion_analysis.plot_data_and_model() - -# %%NBQA-CELL-SEP5932b7 -# Let us look at the most interesting fit parameters -diffusion_analysis.plot_parameters(names=['Lorentzian width', 'Lorentzian area']) - -# %%NBQA-CELL-SEP5932b7 -delta_function = DeltaFunction(display_name='DeltaFunction', area=0.2) -component_collection = ComponentCollection( - components=[delta_function], -) -diffusion_model = BrownianTranslationalDiffusion( - display_name='Brownian Translational Diffusion', diffusion_coefficient=2.4e-9, scale=0.5 -) - -sample_model = SampleModel( - components=component_collection, - diffusion_models=diffusion_model, -) - -background_model = BackgroundModel(components=Polynomial(coefficients=[0.001])) - -instrument_model = InstrumentModel( - background_model=background_model, -) - -# %%NBQA-CELL-SEP5932b7 -diffusion_model_analysis = Analysis( - display_name='Diffusion Full Analysis', - experiment=diffusion_experiment, - sample_model=sample_model, - instrument_model=instrument_model, -) - -# We again need to hack in the resolution model from the vanadium -# analysis, since the setters and getters overwrite the model. This will -# be fixed asap. -diffusion_model_analysis.instrument_model._resolution_model = ( - vanadium_analysis.instrument_model.resolution_model -) -diffusion_model_analysis.instrument_model.resolution_model.fix_all_parameters() - -# %%NBQA-CELL-SEP5932b7 -diffusion_model_analysis.plot_data_and_model() - -# %%NBQA-CELL-SEP5932b7 -diffusion_model_analysis.fit(fit_method='simultaneous') -diffusion_model_analysis.plot_data_and_model() - -# %%NBQA-CELL-SEP5932b7 -diffusion_model.get_all_parameters()