diff --git a/.copier-answers.yml b/.copier-answers.yml index 7b414c16..c3886bcb 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-25-ga5301e9 +_commit: v0.10.1-38-g85fbe04 _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/coverage.yml b/.github/workflows/coverage.yml index cd9ff1e0..e1e44d41 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -1,8 +1,10 @@ name: Coverage checks on: - # Trigger the workflow on push + # Trigger the workflow on push to develop push: + branches: + - develop # Do not run on version tags (those are handled by other workflows) tags-ignore: ['v*'] # Trigger the workflow on pull request @@ -15,11 +17,11 @@ permissions: actions: write contents: read -# Allow only one concurrent workflow, skipping runs queued between the run -# in-progress and latest queued. And cancel in-progress runs. +# Allow only one concurrent workflow per PR or branch ref. +# Cancel in-progress runs only for pull requests, but let branch push runs finish. concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true + cancel-in-progress: ${{ github.event_name == 'pull_request' }} # Set the environment variables to be used in all jobs defined in this workflow env: diff --git a/.github/workflows/issues-labels.yml b/.github/workflows/issues-labels.yml index 3a60cdd7..df3c55f1 100644 --- a/.github/workflows/issues-labels.yml +++ b/.github/workflows/issues-labels.yml @@ -13,9 +13,18 @@ permissions: jobs: check-labels: + if: github.actor != 'easyscience[bot]' + runs-on: ubuntu-latest + concurrency: + group: issue-labels-${{ github.event.issue.number }} + cancel-in-progress: true + steps: + - name: Checkout repository + uses: actions/checkout@v5 + - name: Setup easyscience[bot] id: bot uses: ./.github/actions/setup-easyscience-bot diff --git a/.github/workflows/lint-format.yml b/.github/workflows/lint-format.yml index 457d67f4..f1135fa5 100644 --- a/.github/workflows/lint-format.yml +++ b/.github/workflows/lint-format.yml @@ -79,12 +79,6 @@ jobs: shell: bash run: pixi run docstring-lint-check - - 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 @@ -112,7 +106,6 @@ jobs: 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" @@ -125,7 +118,6 @@ jobs: || 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 diff --git a/.gitignore b/.gitignore index 0500ede3..6dc595c7 100644 --- a/.gitignore +++ b/.gitignore @@ -41,5 +41,6 @@ CMakeLists.txt.user* *.dmg # Misc +.cache/ *.log *.zip diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5fa0c2cb..9a3855f4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -39,13 +39,6 @@ repos: 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-nonpy-format-check name: pixi run nonpy-format-check entry: pixi run nonpy-format-check diff --git a/.prettierignore b/.prettierignore index 788ec8fa..a08c3c48 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,3 +1,6 @@ +# Git +.git/ + # Copier .copier-answers*.yml @@ -14,4 +17,16 @@ docs/docs/assets/ .pytest_cache/ # MyPy -.mypy_cache +.mypy_cache/ + +# Ruff +.ruff_cache/ + +# Node +node_modules + +# Misc +.benchmarks +.cache +deps/ +tmp/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 85365779..168c5d97 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -230,7 +230,6 @@ 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 diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index bd73822e..a821a82f 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -148,8 +148,7 @@ plugins: python: paths: ['src'] # Change 'src' to your actual sources directory options: - docstring_style: google - render_markdown: true + docstring_style: numpy group_by_category: false heading_level: 1 show_root_heading: true @@ -183,6 +182,7 @@ nav: - Tutorials: tutorials/index.md - Getting Started: - Tutorial 1. Brownian Diffusion: tutorials/tutorial1_brownian.ipynb + - Tutorial 2. Magnetic nanoparticles: tutorials/tutorial2_nanoparticles.ipynb - Classes and Methods: - Components: tutorials/components.ipynb - Component collection: tutorials/component_collection.ipynb diff --git a/pixi.lock b/pixi.lock index cdb11917..1665d1a6 100644 --- a/pixi.lock +++ b/pixi.lock @@ -83,8 +83,8 @@ environments: - pypi: https://files.pythonhosted.org/packages/c7/a0/5ff05d1919ca249508012cad89f08fdc6cfbdaa15b41651c5fe6dffaf1d3/dfo_ls-1.6.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl - 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/85/d7/9b6ac05350ab7f7d3a730ff143ff3e2cada54514117c37be37e26dc91242/docstripy-0.7.2-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 @@ -92,6 +92,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/cb/a8/20d0723294217e47de6d9e2e40fd4a9d2f7c4b6ef974babd482a59743694/fastjsonschema-2.21.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a4/a5/842ae8f0c08b61d6484b52f99a03510a3a72d23141942d216ebe81fefbce/filelock-3.25.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9b/8a/99c8b3c3888c5c474c08dbfd7c8899786de9604b727fcefb055b42c84bba/fonttools-4.62.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/33/b2/986d1220f6ee931e338d272bc1f3ec02cfe5f9b5fad84e95afdad57f1ebc/format_docstring-0.2.7-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cf/58/8acf1b3e91c58313ce5cb67df61001fc9dcd21be4fadb76c1a2d540e09ed/fqdn-1.5.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl - pypi: https://files.pythonhosted.org/packages/d5/08/c2409cb01d5368dcfedcbaffa7d044cc8957d57a9d0855244a5eb4709d30/funcy-2.0-py2.py3-none-any.whl @@ -124,6 +125,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e2/48/577993f1f99c552f18a0428731a755e06171f9902fa118c379eb7c04ea22/jupyter_events-0.12.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1a/60/1f6cee0c46263de1173894f0fafcb3475ded276c472c14d25e0280c18d6d/jupyter_lsp-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f4/a4/61adb19f3c74b0dc0e411de4f06ebef564b1f179928f9dffcbd4b378f2ef/jupyter_notebook_parser-0.1.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/92/80/a24767e6ca280f5a49525d987bf3e4d7552bf67c8be07e8ccf20271f8568/jupyter_server-2.17.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d1/2d/6674563f71c6320841fc300911a55143925112a72a883e2ca71fba4c618d/jupyter_server_terminals-0.5.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e1/1b/dad6fdcc658ed7af26fdf3841e7394072c9549a8b896c381ab49dd11e2d9/jupyterlab-4.5.6-py3-none-any.whl @@ -250,7 +252,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8f/5e/f1e1dd319e35e962a4e00b33150a8868b6329cc1d19fd533436ba5488f09/uncertainties-3.2.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f7/46/e7cea8159199096e1df52da20a57a6665da80c37fb8aeb848a3e47442c32/untokenize-0.1.1.tar.gz - pypi: https://files.pythonhosted.org/packages/e7/00/3fca040d7cf8a32776d3d81a00c8ee7457e00f80c649f1e4a863c8321ae9/uri_template-1.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b7/ee/e9c95cda829131f71a8dff5ce0406059fd16e591c074414e31ada19ba7c3/validate_pyproject-0.25-py3-none-any.whl @@ -334,8 +335,8 @@ environments: - pypi: https://files.pythonhosted.org/packages/c7/a0/5ff05d1919ca249508012cad89f08fdc6cfbdaa15b41651c5fe6dffaf1d3/dfo_ls-1.6.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl - 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/85/d7/9b6ac05350ab7f7d3a730ff143ff3e2cada54514117c37be37e26dc91242/docstripy-0.7.2-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 @@ -343,6 +344,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/cb/a8/20d0723294217e47de6d9e2e40fd4a9d2f7c4b6ef974babd482a59743694/fastjsonschema-2.21.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a4/a5/842ae8f0c08b61d6484b52f99a03510a3a72d23141942d216ebe81fefbce/filelock-3.25.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/66/9e/a769c8e99b81e5a87ab7e5e7236684de4e96246aae17274e5347d11ebd78/fonttools-4.62.1-cp312-cp312-macosx_10_13_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/33/b2/986d1220f6ee931e338d272bc1f3ec02cfe5f9b5fad84e95afdad57f1ebc/format_docstring-0.2.7-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cf/58/8acf1b3e91c58313ce5cb67df61001fc9dcd21be4fadb76c1a2d540e09ed/fqdn-1.5.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl - pypi: https://files.pythonhosted.org/packages/d5/08/c2409cb01d5368dcfedcbaffa7d044cc8957d57a9d0855244a5eb4709d30/funcy-2.0-py2.py3-none-any.whl @@ -375,6 +377,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e2/48/577993f1f99c552f18a0428731a755e06171f9902fa118c379eb7c04ea22/jupyter_events-0.12.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1a/60/1f6cee0c46263de1173894f0fafcb3475ded276c472c14d25e0280c18d6d/jupyter_lsp-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f4/a4/61adb19f3c74b0dc0e411de4f06ebef564b1f179928f9dffcbd4b378f2ef/jupyter_notebook_parser-0.1.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/92/80/a24767e6ca280f5a49525d987bf3e4d7552bf67c8be07e8ccf20271f8568/jupyter_server-2.17.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d1/2d/6674563f71c6320841fc300911a55143925112a72a883e2ca71fba4c618d/jupyter_server_terminals-0.5.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e1/1b/dad6fdcc658ed7af26fdf3841e7394072c9549a8b896c381ab49dd11e2d9/jupyterlab-4.5.6-py3-none-any.whl @@ -501,7 +504,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8f/5e/f1e1dd319e35e962a4e00b33150a8868b6329cc1d19fd533436ba5488f09/uncertainties-3.2.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f7/46/e7cea8159199096e1df52da20a57a6665da80c37fb8aeb848a3e47442c32/untokenize-0.1.1.tar.gz - pypi: https://files.pythonhosted.org/packages/e7/00/3fca040d7cf8a32776d3d81a00c8ee7457e00f80c649f1e4a863c8321ae9/uri_template-1.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b7/ee/e9c95cda829131f71a8dff5ce0406059fd16e591c074414e31ada19ba7c3/validate_pyproject-0.25-py3-none-any.whl @@ -585,8 +587,8 @@ environments: - pypi: https://files.pythonhosted.org/packages/c7/a0/5ff05d1919ca249508012cad89f08fdc6cfbdaa15b41651c5fe6dffaf1d3/dfo_ls-1.6.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl - 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/85/d7/9b6ac05350ab7f7d3a730ff143ff3e2cada54514117c37be37e26dc91242/docstripy-0.7.2-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 @@ -594,6 +596,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/cb/a8/20d0723294217e47de6d9e2e40fd4a9d2f7c4b6ef974babd482a59743694/fastjsonschema-2.21.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a4/a5/842ae8f0c08b61d6484b52f99a03510a3a72d23141942d216ebe81fefbce/filelock-3.25.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/47/d4/dbacced3953544b9a93088cc10ef2b596d348c983d5c67a404fa41ec51ba/fonttools-4.62.1-cp312-cp312-macosx_10_13_universal2.whl + - pypi: https://files.pythonhosted.org/packages/33/b2/986d1220f6ee931e338d272bc1f3ec02cfe5f9b5fad84e95afdad57f1ebc/format_docstring-0.2.7-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cf/58/8acf1b3e91c58313ce5cb67df61001fc9dcd21be4fadb76c1a2d540e09ed/fqdn-1.5.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/d5/08/c2409cb01d5368dcfedcbaffa7d044cc8957d57a9d0855244a5eb4709d30/funcy-2.0-py2.py3-none-any.whl @@ -626,6 +629,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e2/48/577993f1f99c552f18a0428731a755e06171f9902fa118c379eb7c04ea22/jupyter_events-0.12.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1a/60/1f6cee0c46263de1173894f0fafcb3475ded276c472c14d25e0280c18d6d/jupyter_lsp-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f4/a4/61adb19f3c74b0dc0e411de4f06ebef564b1f179928f9dffcbd4b378f2ef/jupyter_notebook_parser-0.1.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/92/80/a24767e6ca280f5a49525d987bf3e4d7552bf67c8be07e8ccf20271f8568/jupyter_server-2.17.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d1/2d/6674563f71c6320841fc300911a55143925112a72a883e2ca71fba4c618d/jupyter_server_terminals-0.5.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e1/1b/dad6fdcc658ed7af26fdf3841e7394072c9549a8b896c381ab49dd11e2d9/jupyterlab-4.5.6-py3-none-any.whl @@ -752,7 +756,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8f/5e/f1e1dd319e35e962a4e00b33150a8868b6329cc1d19fd533436ba5488f09/uncertainties-3.2.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f7/46/e7cea8159199096e1df52da20a57a6665da80c37fb8aeb848a3e47442c32/untokenize-0.1.1.tar.gz - pypi: https://files.pythonhosted.org/packages/e7/00/3fca040d7cf8a32776d3d81a00c8ee7457e00f80c649f1e4a863c8321ae9/uri_template-1.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b7/ee/e9c95cda829131f71a8dff5ce0406059fd16e591c074414e31ada19ba7c3/validate_pyproject-0.25-py3-none-any.whl @@ -826,8 +829,8 @@ environments: - pypi: https://files.pythonhosted.org/packages/c7/a0/5ff05d1919ca249508012cad89f08fdc6cfbdaa15b41651c5fe6dffaf1d3/dfo_ls-1.6.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl - 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/85/d7/9b6ac05350ab7f7d3a730ff143ff3e2cada54514117c37be37e26dc91242/docstripy-0.7.2-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 @@ -835,6 +838,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/cb/a8/20d0723294217e47de6d9e2e40fd4a9d2f7c4b6ef974babd482a59743694/fastjsonschema-2.21.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a4/a5/842ae8f0c08b61d6484b52f99a03510a3a72d23141942d216ebe81fefbce/filelock-3.25.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/28/b1/0c2ab56a16f409c6c8a68816e6af707827ad5d629634691ff60a52879792/fonttools-4.62.1-cp312-cp312-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/33/b2/986d1220f6ee931e338d272bc1f3ec02cfe5f9b5fad84e95afdad57f1ebc/format_docstring-0.2.7-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cf/58/8acf1b3e91c58313ce5cb67df61001fc9dcd21be4fadb76c1a2d540e09ed/fqdn-1.5.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/d5/08/c2409cb01d5368dcfedcbaffa7d044cc8957d57a9d0855244a5eb4709d30/funcy-2.0-py2.py3-none-any.whl @@ -867,6 +871,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e2/48/577993f1f99c552f18a0428731a755e06171f9902fa118c379eb7c04ea22/jupyter_events-0.12.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1a/60/1f6cee0c46263de1173894f0fafcb3475ded276c472c14d25e0280c18d6d/jupyter_lsp-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f4/a4/61adb19f3c74b0dc0e411de4f06ebef564b1f179928f9dffcbd4b378f2ef/jupyter_notebook_parser-0.1.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/92/80/a24767e6ca280f5a49525d987bf3e4d7552bf67c8be07e8ccf20271f8568/jupyter_server-2.17.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d1/2d/6674563f71c6320841fc300911a55143925112a72a883e2ca71fba4c618d/jupyter_server_terminals-0.5.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e1/1b/dad6fdcc658ed7af26fdf3841e7394072c9549a8b896c381ab49dd11e2d9/jupyterlab-4.5.6-py3-none-any.whl @@ -993,7 +998,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8f/5e/f1e1dd319e35e962a4e00b33150a8868b6329cc1d19fd533436ba5488f09/uncertainties-3.2.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f7/46/e7cea8159199096e1df52da20a57a6665da80c37fb8aeb848a3e47442c32/untokenize-0.1.1.tar.gz - pypi: https://files.pythonhosted.org/packages/e7/00/3fca040d7cf8a32776d3d81a00c8ee7457e00f80c649f1e4a863c8321ae9/uri_template-1.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b7/ee/e9c95cda829131f71a8dff5ce0406059fd16e591c074414e31ada19ba7c3/validate_pyproject-0.25-py3-none-any.whl @@ -1092,8 +1096,8 @@ environments: - pypi: https://files.pythonhosted.org/packages/c7/a0/5ff05d1919ca249508012cad89f08fdc6cfbdaa15b41651c5fe6dffaf1d3/dfo_ls-1.6.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl - 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/85/d7/9b6ac05350ab7f7d3a730ff143ff3e2cada54514117c37be37e26dc91242/docstripy-0.7.2-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 @@ -1101,6 +1105,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/cb/a8/20d0723294217e47de6d9e2e40fd4a9d2f7c4b6ef974babd482a59743694/fastjsonschema-2.21.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a4/a5/842ae8f0c08b61d6484b52f99a03510a3a72d23141942d216ebe81fefbce/filelock-3.25.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cc/a1/40a5c4d8e28b0851d53a8eeeb46fbd73c325a2a9a165f290a5ed90e6c597/fonttools-4.62.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/33/b2/986d1220f6ee931e338d272bc1f3ec02cfe5f9b5fad84e95afdad57f1ebc/format_docstring-0.2.7-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cf/58/8acf1b3e91c58313ce5cb67df61001fc9dcd21be4fadb76c1a2d540e09ed/fqdn-1.5.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/11/b1/71a477adc7c36e5fb628245dfbdea2166feae310757dea848d02bd0689fd/frozenlist-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl - pypi: https://files.pythonhosted.org/packages/d5/08/c2409cb01d5368dcfedcbaffa7d044cc8957d57a9d0855244a5eb4709d30/funcy-2.0-py2.py3-none-any.whl @@ -1133,6 +1138,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e2/48/577993f1f99c552f18a0428731a755e06171f9902fa118c379eb7c04ea22/jupyter_events-0.12.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1a/60/1f6cee0c46263de1173894f0fafcb3475ded276c472c14d25e0280c18d6d/jupyter_lsp-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f4/a4/61adb19f3c74b0dc0e411de4f06ebef564b1f179928f9dffcbd4b378f2ef/jupyter_notebook_parser-0.1.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/92/80/a24767e6ca280f5a49525d987bf3e4d7552bf67c8be07e8ccf20271f8568/jupyter_server-2.17.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d1/2d/6674563f71c6320841fc300911a55143925112a72a883e2ca71fba4c618d/jupyter_server_terminals-0.5.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e1/1b/dad6fdcc658ed7af26fdf3841e7394072c9549a8b896c381ab49dd11e2d9/jupyterlab-4.5.6-py3-none-any.whl @@ -1260,7 +1266,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8f/5e/f1e1dd319e35e962a4e00b33150a8868b6329cc1d19fd533436ba5488f09/uncertainties-3.2.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f7/46/e7cea8159199096e1df52da20a57a6665da80c37fb8aeb848a3e47442c32/untokenize-0.1.1.tar.gz - pypi: https://files.pythonhosted.org/packages/e7/00/3fca040d7cf8a32776d3d81a00c8ee7457e00f80c649f1e4a863c8321ae9/uri_template-1.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b7/ee/e9c95cda829131f71a8dff5ce0406059fd16e591c074414e31ada19ba7c3/validate_pyproject-0.25-py3-none-any.whl @@ -1344,8 +1349,8 @@ environments: - pypi: https://files.pythonhosted.org/packages/c7/a0/5ff05d1919ca249508012cad89f08fdc6cfbdaa15b41651c5fe6dffaf1d3/dfo_ls-1.6.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl - 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/85/d7/9b6ac05350ab7f7d3a730ff143ff3e2cada54514117c37be37e26dc91242/docstripy-0.7.2-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 @@ -1353,6 +1358,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/cb/a8/20d0723294217e47de6d9e2e40fd4a9d2f7c4b6ef974babd482a59743694/fastjsonschema-2.21.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a4/a5/842ae8f0c08b61d6484b52f99a03510a3a72d23141942d216ebe81fefbce/filelock-3.25.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/24/7f/66d3f8a9338a9b67fe6e1739f47e1cd5cee78bd3bc1206ef9b0b982289a5/fonttools-4.62.1-cp311-cp311-macosx_10_9_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/33/b2/986d1220f6ee931e338d272bc1f3ec02cfe5f9b5fad84e95afdad57f1ebc/format_docstring-0.2.7-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cf/58/8acf1b3e91c58313ce5cb67df61001fc9dcd21be4fadb76c1a2d540e09ed/fqdn-1.5.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/df/b5/7610b6bd13e4ae77b96ba85abea1c8cb249683217ef09ac9e0ae93f25a91/frozenlist-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl - pypi: https://files.pythonhosted.org/packages/d5/08/c2409cb01d5368dcfedcbaffa7d044cc8957d57a9d0855244a5eb4709d30/funcy-2.0-py2.py3-none-any.whl @@ -1385,6 +1391,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e2/48/577993f1f99c552f18a0428731a755e06171f9902fa118c379eb7c04ea22/jupyter_events-0.12.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1a/60/1f6cee0c46263de1173894f0fafcb3475ded276c472c14d25e0280c18d6d/jupyter_lsp-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f4/a4/61adb19f3c74b0dc0e411de4f06ebef564b1f179928f9dffcbd4b378f2ef/jupyter_notebook_parser-0.1.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/92/80/a24767e6ca280f5a49525d987bf3e4d7552bf67c8be07e8ccf20271f8568/jupyter_server-2.17.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d1/2d/6674563f71c6320841fc300911a55143925112a72a883e2ca71fba4c618d/jupyter_server_terminals-0.5.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e1/1b/dad6fdcc658ed7af26fdf3841e7394072c9549a8b896c381ab49dd11e2d9/jupyterlab-4.5.6-py3-none-any.whl @@ -1512,7 +1519,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8f/5e/f1e1dd319e35e962a4e00b33150a8868b6329cc1d19fd533436ba5488f09/uncertainties-3.2.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f7/46/e7cea8159199096e1df52da20a57a6665da80c37fb8aeb848a3e47442c32/untokenize-0.1.1.tar.gz - pypi: https://files.pythonhosted.org/packages/e7/00/3fca040d7cf8a32776d3d81a00c8ee7457e00f80c649f1e4a863c8321ae9/uri_template-1.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b7/ee/e9c95cda829131f71a8dff5ce0406059fd16e591c074414e31ada19ba7c3/validate_pyproject-0.25-py3-none-any.whl @@ -1596,8 +1602,8 @@ environments: - pypi: https://files.pythonhosted.org/packages/c7/a0/5ff05d1919ca249508012cad89f08fdc6cfbdaa15b41651c5fe6dffaf1d3/dfo_ls-1.6.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl - 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/85/d7/9b6ac05350ab7f7d3a730ff143ff3e2cada54514117c37be37e26dc91242/docstripy-0.7.2-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 @@ -1605,6 +1611,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/cb/a8/20d0723294217e47de6d9e2e40fd4a9d2f7c4b6ef974babd482a59743694/fastjsonschema-2.21.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a4/a5/842ae8f0c08b61d6484b52f99a03510a3a72d23141942d216ebe81fefbce/filelock-3.25.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/39/23ff32561ec8d45a4d48578b4d241369d9270dc50926c017570e60893701/fonttools-4.62.1-cp311-cp311-macosx_10_9_universal2.whl + - pypi: https://files.pythonhosted.org/packages/33/b2/986d1220f6ee931e338d272bc1f3ec02cfe5f9b5fad84e95afdad57f1ebc/format_docstring-0.2.7-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cf/58/8acf1b3e91c58313ce5cb67df61001fc9dcd21be4fadb76c1a2d540e09ed/fqdn-1.5.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/6e/ef/0e8f1fe32f8a53dd26bdd1f9347efe0778b0fddf62789ea683f4cc7d787d/frozenlist-1.8.0-cp311-cp311-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/d5/08/c2409cb01d5368dcfedcbaffa7d044cc8957d57a9d0855244a5eb4709d30/funcy-2.0-py2.py3-none-any.whl @@ -1637,6 +1644,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e2/48/577993f1f99c552f18a0428731a755e06171f9902fa118c379eb7c04ea22/jupyter_events-0.12.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1a/60/1f6cee0c46263de1173894f0fafcb3475ded276c472c14d25e0280c18d6d/jupyter_lsp-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f4/a4/61adb19f3c74b0dc0e411de4f06ebef564b1f179928f9dffcbd4b378f2ef/jupyter_notebook_parser-0.1.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/92/80/a24767e6ca280f5a49525d987bf3e4d7552bf67c8be07e8ccf20271f8568/jupyter_server-2.17.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d1/2d/6674563f71c6320841fc300911a55143925112a72a883e2ca71fba4c618d/jupyter_server_terminals-0.5.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e1/1b/dad6fdcc658ed7af26fdf3841e7394072c9549a8b896c381ab49dd11e2d9/jupyterlab-4.5.6-py3-none-any.whl @@ -1764,7 +1772,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8f/5e/f1e1dd319e35e962a4e00b33150a8868b6329cc1d19fd533436ba5488f09/uncertainties-3.2.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f7/46/e7cea8159199096e1df52da20a57a6665da80c37fb8aeb848a3e47442c32/untokenize-0.1.1.tar.gz - pypi: https://files.pythonhosted.org/packages/e7/00/3fca040d7cf8a32776d3d81a00c8ee7457e00f80c649f1e4a863c8321ae9/uri_template-1.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b7/ee/e9c95cda829131f71a8dff5ce0406059fd16e591c074414e31ada19ba7c3/validate_pyproject-0.25-py3-none-any.whl @@ -1838,8 +1845,8 @@ environments: - pypi: https://files.pythonhosted.org/packages/c7/a0/5ff05d1919ca249508012cad89f08fdc6cfbdaa15b41651c5fe6dffaf1d3/dfo_ls-1.6.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl - 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/85/d7/9b6ac05350ab7f7d3a730ff143ff3e2cada54514117c37be37e26dc91242/docstripy-0.7.2-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 @@ -1847,6 +1854,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/cb/a8/20d0723294217e47de6d9e2e40fd4a9d2f7c4b6ef974babd482a59743694/fastjsonschema-2.21.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a4/a5/842ae8f0c08b61d6484b52f99a03510a3a72d23141942d216ebe81fefbce/filelock-3.25.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d3/97/bf54c5b3f2be34e1f143e6db838dfdc54f2ffa3e68c738934c82f3b2a08d/fonttools-4.62.1-cp311-cp311-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/33/b2/986d1220f6ee931e338d272bc1f3ec02cfe5f9b5fad84e95afdad57f1ebc/format_docstring-0.2.7-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cf/58/8acf1b3e91c58313ce5cb67df61001fc9dcd21be4fadb76c1a2d540e09ed/fqdn-1.5.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/0a/f5/603d0d6a02cfd4c8f2a095a54672b3cf967ad688a60fb9faf04fc4887f65/frozenlist-1.8.0-cp311-cp311-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/d5/08/c2409cb01d5368dcfedcbaffa7d044cc8957d57a9d0855244a5eb4709d30/funcy-2.0-py2.py3-none-any.whl @@ -1879,6 +1887,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e2/48/577993f1f99c552f18a0428731a755e06171f9902fa118c379eb7c04ea22/jupyter_events-0.12.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1a/60/1f6cee0c46263de1173894f0fafcb3475ded276c472c14d25e0280c18d6d/jupyter_lsp-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f4/a4/61adb19f3c74b0dc0e411de4f06ebef564b1f179928f9dffcbd4b378f2ef/jupyter_notebook_parser-0.1.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/92/80/a24767e6ca280f5a49525d987bf3e4d7552bf67c8be07e8ccf20271f8568/jupyter_server-2.17.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d1/2d/6674563f71c6320841fc300911a55143925112a72a883e2ca71fba4c618d/jupyter_server_terminals-0.5.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e1/1b/dad6fdcc658ed7af26fdf3841e7394072c9549a8b896c381ab49dd11e2d9/jupyterlab-4.5.6-py3-none-any.whl @@ -2006,7 +2015,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8f/5e/f1e1dd319e35e962a4e00b33150a8868b6329cc1d19fd533436ba5488f09/uncertainties-3.2.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f7/46/e7cea8159199096e1df52da20a57a6665da80c37fb8aeb848a3e47442c32/untokenize-0.1.1.tar.gz - pypi: https://files.pythonhosted.org/packages/e7/00/3fca040d7cf8a32776d3d81a00c8ee7457e00f80c649f1e4a863c8321ae9/uri_template-1.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b7/ee/e9c95cda829131f71a8dff5ce0406059fd16e591c074414e31ada19ba7c3/validate_pyproject-0.25-py3-none-any.whl @@ -2105,8 +2113,8 @@ environments: - pypi: https://files.pythonhosted.org/packages/c7/a0/5ff05d1919ca249508012cad89f08fdc6cfbdaa15b41651c5fe6dffaf1d3/dfo_ls-1.6.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl - 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/85/d7/9b6ac05350ab7f7d3a730ff143ff3e2cada54514117c37be37e26dc91242/docstripy-0.7.2-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 @@ -2114,6 +2122,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/cb/a8/20d0723294217e47de6d9e2e40fd4a9d2f7c4b6ef974babd482a59743694/fastjsonschema-2.21.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a4/a5/842ae8f0c08b61d6484b52f99a03510a3a72d23141942d216ebe81fefbce/filelock-3.25.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9b/8a/99c8b3c3888c5c474c08dbfd7c8899786de9604b727fcefb055b42c84bba/fonttools-4.62.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/33/b2/986d1220f6ee931e338d272bc1f3ec02cfe5f9b5fad84e95afdad57f1ebc/format_docstring-0.2.7-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cf/58/8acf1b3e91c58313ce5cb67df61001fc9dcd21be4fadb76c1a2d540e09ed/fqdn-1.5.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl - pypi: https://files.pythonhosted.org/packages/d5/08/c2409cb01d5368dcfedcbaffa7d044cc8957d57a9d0855244a5eb4709d30/funcy-2.0-py2.py3-none-any.whl @@ -2146,6 +2155,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e2/48/577993f1f99c552f18a0428731a755e06171f9902fa118c379eb7c04ea22/jupyter_events-0.12.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1a/60/1f6cee0c46263de1173894f0fafcb3475ded276c472c14d25e0280c18d6d/jupyter_lsp-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f4/a4/61adb19f3c74b0dc0e411de4f06ebef564b1f179928f9dffcbd4b378f2ef/jupyter_notebook_parser-0.1.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/92/80/a24767e6ca280f5a49525d987bf3e4d7552bf67c8be07e8ccf20271f8568/jupyter_server-2.17.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d1/2d/6674563f71c6320841fc300911a55143925112a72a883e2ca71fba4c618d/jupyter_server_terminals-0.5.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e1/1b/dad6fdcc658ed7af26fdf3841e7394072c9549a8b896c381ab49dd11e2d9/jupyterlab-4.5.6-py3-none-any.whl @@ -2272,7 +2282,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8f/5e/f1e1dd319e35e962a4e00b33150a8868b6329cc1d19fd533436ba5488f09/uncertainties-3.2.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f7/46/e7cea8159199096e1df52da20a57a6665da80c37fb8aeb848a3e47442c32/untokenize-0.1.1.tar.gz - pypi: https://files.pythonhosted.org/packages/e7/00/3fca040d7cf8a32776d3d81a00c8ee7457e00f80c649f1e4a863c8321ae9/uri_template-1.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b7/ee/e9c95cda829131f71a8dff5ce0406059fd16e591c074414e31ada19ba7c3/validate_pyproject-0.25-py3-none-any.whl @@ -2356,8 +2365,8 @@ environments: - pypi: https://files.pythonhosted.org/packages/c7/a0/5ff05d1919ca249508012cad89f08fdc6cfbdaa15b41651c5fe6dffaf1d3/dfo_ls-1.6.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl - 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/85/d7/9b6ac05350ab7f7d3a730ff143ff3e2cada54514117c37be37e26dc91242/docstripy-0.7.2-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 @@ -2365,6 +2374,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/cb/a8/20d0723294217e47de6d9e2e40fd4a9d2f7c4b6ef974babd482a59743694/fastjsonschema-2.21.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a4/a5/842ae8f0c08b61d6484b52f99a03510a3a72d23141942d216ebe81fefbce/filelock-3.25.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/66/9e/a769c8e99b81e5a87ab7e5e7236684de4e96246aae17274e5347d11ebd78/fonttools-4.62.1-cp312-cp312-macosx_10_13_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/33/b2/986d1220f6ee931e338d272bc1f3ec02cfe5f9b5fad84e95afdad57f1ebc/format_docstring-0.2.7-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cf/58/8acf1b3e91c58313ce5cb67df61001fc9dcd21be4fadb76c1a2d540e09ed/fqdn-1.5.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl - pypi: https://files.pythonhosted.org/packages/d5/08/c2409cb01d5368dcfedcbaffa7d044cc8957d57a9d0855244a5eb4709d30/funcy-2.0-py2.py3-none-any.whl @@ -2397,6 +2407,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e2/48/577993f1f99c552f18a0428731a755e06171f9902fa118c379eb7c04ea22/jupyter_events-0.12.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1a/60/1f6cee0c46263de1173894f0fafcb3475ded276c472c14d25e0280c18d6d/jupyter_lsp-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f4/a4/61adb19f3c74b0dc0e411de4f06ebef564b1f179928f9dffcbd4b378f2ef/jupyter_notebook_parser-0.1.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/92/80/a24767e6ca280f5a49525d987bf3e4d7552bf67c8be07e8ccf20271f8568/jupyter_server-2.17.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d1/2d/6674563f71c6320841fc300911a55143925112a72a883e2ca71fba4c618d/jupyter_server_terminals-0.5.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e1/1b/dad6fdcc658ed7af26fdf3841e7394072c9549a8b896c381ab49dd11e2d9/jupyterlab-4.5.6-py3-none-any.whl @@ -2523,7 +2534,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8f/5e/f1e1dd319e35e962a4e00b33150a8868b6329cc1d19fd533436ba5488f09/uncertainties-3.2.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f7/46/e7cea8159199096e1df52da20a57a6665da80c37fb8aeb848a3e47442c32/untokenize-0.1.1.tar.gz - pypi: https://files.pythonhosted.org/packages/e7/00/3fca040d7cf8a32776d3d81a00c8ee7457e00f80c649f1e4a863c8321ae9/uri_template-1.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b7/ee/e9c95cda829131f71a8dff5ce0406059fd16e591c074414e31ada19ba7c3/validate_pyproject-0.25-py3-none-any.whl @@ -2607,8 +2617,8 @@ environments: - pypi: https://files.pythonhosted.org/packages/c7/a0/5ff05d1919ca249508012cad89f08fdc6cfbdaa15b41651c5fe6dffaf1d3/dfo_ls-1.6.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl - 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/85/d7/9b6ac05350ab7f7d3a730ff143ff3e2cada54514117c37be37e26dc91242/docstripy-0.7.2-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 @@ -2616,6 +2626,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/cb/a8/20d0723294217e47de6d9e2e40fd4a9d2f7c4b6ef974babd482a59743694/fastjsonschema-2.21.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a4/a5/842ae8f0c08b61d6484b52f99a03510a3a72d23141942d216ebe81fefbce/filelock-3.25.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/47/d4/dbacced3953544b9a93088cc10ef2b596d348c983d5c67a404fa41ec51ba/fonttools-4.62.1-cp312-cp312-macosx_10_13_universal2.whl + - pypi: https://files.pythonhosted.org/packages/33/b2/986d1220f6ee931e338d272bc1f3ec02cfe5f9b5fad84e95afdad57f1ebc/format_docstring-0.2.7-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cf/58/8acf1b3e91c58313ce5cb67df61001fc9dcd21be4fadb76c1a2d540e09ed/fqdn-1.5.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/d5/08/c2409cb01d5368dcfedcbaffa7d044cc8957d57a9d0855244a5eb4709d30/funcy-2.0-py2.py3-none-any.whl @@ -2648,6 +2659,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e2/48/577993f1f99c552f18a0428731a755e06171f9902fa118c379eb7c04ea22/jupyter_events-0.12.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1a/60/1f6cee0c46263de1173894f0fafcb3475ded276c472c14d25e0280c18d6d/jupyter_lsp-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f4/a4/61adb19f3c74b0dc0e411de4f06ebef564b1f179928f9dffcbd4b378f2ef/jupyter_notebook_parser-0.1.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/92/80/a24767e6ca280f5a49525d987bf3e4d7552bf67c8be07e8ccf20271f8568/jupyter_server-2.17.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d1/2d/6674563f71c6320841fc300911a55143925112a72a883e2ca71fba4c618d/jupyter_server_terminals-0.5.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e1/1b/dad6fdcc658ed7af26fdf3841e7394072c9549a8b896c381ab49dd11e2d9/jupyterlab-4.5.6-py3-none-any.whl @@ -2774,7 +2786,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8f/5e/f1e1dd319e35e962a4e00b33150a8868b6329cc1d19fd533436ba5488f09/uncertainties-3.2.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f7/46/e7cea8159199096e1df52da20a57a6665da80c37fb8aeb848a3e47442c32/untokenize-0.1.1.tar.gz - pypi: https://files.pythonhosted.org/packages/e7/00/3fca040d7cf8a32776d3d81a00c8ee7457e00f80c649f1e4a863c8321ae9/uri_template-1.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b7/ee/e9c95cda829131f71a8dff5ce0406059fd16e591c074414e31ada19ba7c3/validate_pyproject-0.25-py3-none-any.whl @@ -2848,8 +2859,8 @@ environments: - pypi: https://files.pythonhosted.org/packages/c7/a0/5ff05d1919ca249508012cad89f08fdc6cfbdaa15b41651c5fe6dffaf1d3/dfo_ls-1.6.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl - 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/85/d7/9b6ac05350ab7f7d3a730ff143ff3e2cada54514117c37be37e26dc91242/docstripy-0.7.2-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 @@ -2857,6 +2868,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/cb/a8/20d0723294217e47de6d9e2e40fd4a9d2f7c4b6ef974babd482a59743694/fastjsonschema-2.21.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a4/a5/842ae8f0c08b61d6484b52f99a03510a3a72d23141942d216ebe81fefbce/filelock-3.25.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/28/b1/0c2ab56a16f409c6c8a68816e6af707827ad5d629634691ff60a52879792/fonttools-4.62.1-cp312-cp312-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/33/b2/986d1220f6ee931e338d272bc1f3ec02cfe5f9b5fad84e95afdad57f1ebc/format_docstring-0.2.7-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cf/58/8acf1b3e91c58313ce5cb67df61001fc9dcd21be4fadb76c1a2d540e09ed/fqdn-1.5.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/d5/08/c2409cb01d5368dcfedcbaffa7d044cc8957d57a9d0855244a5eb4709d30/funcy-2.0-py2.py3-none-any.whl @@ -2889,6 +2901,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e2/48/577993f1f99c552f18a0428731a755e06171f9902fa118c379eb7c04ea22/jupyter_events-0.12.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1a/60/1f6cee0c46263de1173894f0fafcb3475ded276c472c14d25e0280c18d6d/jupyter_lsp-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f4/a4/61adb19f3c74b0dc0e411de4f06ebef564b1f179928f9dffcbd4b378f2ef/jupyter_notebook_parser-0.1.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/92/80/a24767e6ca280f5a49525d987bf3e4d7552bf67c8be07e8ccf20271f8568/jupyter_server-2.17.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d1/2d/6674563f71c6320841fc300911a55143925112a72a883e2ca71fba4c618d/jupyter_server_terminals-0.5.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e1/1b/dad6fdcc658ed7af26fdf3841e7394072c9549a8b896c381ab49dd11e2d9/jupyterlab-4.5.6-py3-none-any.whl @@ -3015,7 +3028,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8f/5e/f1e1dd319e35e962a4e00b33150a8868b6329cc1d19fd533436ba5488f09/uncertainties-3.2.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f7/46/e7cea8159199096e1df52da20a57a6665da80c37fb8aeb848a3e47442c32/untokenize-0.1.1.tar.gz - pypi: https://files.pythonhosted.org/packages/e7/00/3fca040d7cf8a32776d3d81a00c8ee7457e00f80c649f1e4a863c8321ae9/uri_template-1.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b7/ee/e9c95cda829131f71a8dff5ce0406059fd16e591c074414e31ada19ba7c3/validate_pyproject-0.25-py3-none-any.whl @@ -4031,15 +4043,6 @@ packages: name: distlib version: 0.4.0 sha256: 9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16 -- pypi: https://files.pythonhosted.org/packages/dc/b4/a7ec1eaee86761a9dbfd339732b4706db3c6b65e970c12f0f56cfcce3dcf/docformatter-1.7.7-py3-none-any.whl - name: docformatter - version: 1.7.7 - sha256: 7af49f8a46346a77858f6651f431b882c503c2f4442c8b4524b920c863277834 - requires_dist: - - charset-normalizer>=3.0.0,<4.0.0 - - 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 @@ -4051,6 +4054,13 @@ packages: - pydoctor>=25.4.0 ; extra == 'docs' - pytest ; extra == 'test' requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/85/d7/9b6ac05350ab7f7d3a730ff143ff3e2cada54514117c37be37e26dc91242/docstripy-0.7.2-py3-none-any.whl + name: docstripy + version: 0.7.2 + sha256: c4ba35de6c1b1c51f7afad4a46d8953aad55dce1a490d198f7e98c8c63efefda + requires_dist: + - nbformat + requires_python: '>=3.7' - pypi: https://files.pythonhosted.org/packages/87/10/2c7edbf230e5c507d38367af498fa94258ed97205d9b4b6f63a921fe9c49/dunamai-1.26.0-py3-none-any.whl name: dunamai version: 1.26.0 @@ -4061,8 +4071,8 @@ packages: requires_python: '>=3.5' - pypi: ./ name: easydynamics - version: 0.3.0+devdirty13 - sha256: c9fe79448ba34fd24162d77b5501f66b07c86af3dc0e0982ef2ce4d1a597b1d9 + version: 0.4.0+devdirty10 + sha256: bd1d44f7263fe45e52e8b62d2740c303be86c7bcc89e3cbec95ec663568953b1 requires_dist: - darkdetect - easyscience @@ -4077,7 +4087,8 @@ packages: - sympy - build ; extra == 'dev' - copier ; extra == 'dev' - - docformatter ; extra == 'dev' + - docstripy ; extra == 'dev' + - format-docstring ; extra == 'dev' - gitpython ; extra == 'dev' - interrogate ; extra == 'dev' - jinja2 ; extra == 'dev' @@ -4451,6 +4462,15 @@ packages: - skia-pathops>=0.5.0 ; extra == 'all' - uharfbuzz>=0.45.0 ; extra == 'all' requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/33/b2/986d1220f6ee931e338d272bc1f3ec02cfe5f9b5fad84e95afdad57f1ebc/format_docstring-0.2.7-py3-none-any.whl + name: format-docstring + version: 0.2.7 + sha256: c9d50eafebe0f260e3270ca662ff3a0ed4050f64d95e352f8c5f88d9aede42d6 + requires_dist: + - click>=8.0 + - jupyter-notebook-parser>=0.1.4 + - tomli>=1.1.0 ; python_full_version < '3.11' + requires_python: '>=3.10' - pypi: https://files.pythonhosted.org/packages/cf/58/8acf1b3e91c58313ce5cb67df61001fc9dcd21be4fadb76c1a2d540e09ed/fqdn-1.5.1-py3-none-any.whl name: fqdn version: 1.5.1 @@ -5091,6 +5111,11 @@ packages: - jupyter-server>=1.1.2 - importlib-metadata>=4.8.3 ; python_full_version < '3.10' requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/f4/a4/61adb19f3c74b0dc0e411de4f06ebef564b1f179928f9dffcbd4b378f2ef/jupyter_notebook_parser-0.1.4-py2.py3-none-any.whl + name: jupyter-notebook-parser + version: 0.1.4 + sha256: 27b3b67cf898684e646d569f017cb27046774ad23866cb0bdf51d5f76a46476b + requires_python: '>=3.7' - pypi: https://files.pythonhosted.org/packages/92/80/a24767e6ca280f5a49525d987bf3e4d7552bf67c8be07e8ccf20271f8568/jupyter_server-2.17.0-py3-none-any.whl name: jupyter-server version: 2.17.0 @@ -10084,10 +10109,6 @@ packages: - python-docs-theme ; extra == 'doc' - uncertainties[arrays,doc,test] ; extra == 'all' requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/f7/46/e7cea8159199096e1df52da20a57a6665da80c37fb8aeb848a3e47442c32/untokenize-0.1.1.tar.gz - name: untokenize - version: 0.1.1 - sha256: 3865dbbbb8efb4bb5eaa72f1be7f3e0be00ea8b7f125c69cbd1f5fda926f37a2 - pypi: https://files.pythonhosted.org/packages/e7/00/3fca040d7cf8a32776d3d81a00c8ee7457e00f80c649f1e4a863c8321ae9/uri_template-1.3.0-py3-none-any.whl name: uri-template version: 1.3.0 diff --git a/pixi.toml b/pixi.toml index ab15262f..ac6a7e02 100644 --- a/pixi.toml +++ b/pixi.toml @@ -5,7 +5,7 @@ # Platform-independent [activation.env] -PYTHONIOENCODING = "utf-8" +PYTHONIOENCODING = 'utf-8' # Platform-specific @@ -35,7 +35,7 @@ platforms = ['win-64', 'linux-64', 'osx-64', 'osx-arm64'] channels = ['conda-forge'] ##################### -# System requirements +# SYSTEM REQUIREMENTS ##################### [system-requirements] @@ -92,8 +92,8 @@ default = { features = ['default', 'py-max'] } unit-tests = 'python -m pytest tests/unit/ --color=yes -v' integration-tests = 'python -m pytest tests/integration/ --color=yes -n auto -v' -notebook-tests = 'python -m pytest --nbmake docs/docs/tutorials/ --nbmake-timeout=600 --color=yes -n auto -v' script-tests = 'python -m pytest tools/test_scripts.py --color=yes -n auto -v' +notebook-tests = 'python -m pytest --nbmake docs/docs/tutorials/ --nbmake-timeout=600 --color=yes -n auto -v' test = { depends-on = ['unit-tests'] } @@ -103,12 +103,11 @@ test = { depends-on = ['unit-tests'] } pyproject-check = 'python -m validate_pyproject pyproject.toml' 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 ." -nonpy-format-check-modified = "python tools/nonpy_prettier_modified.py" +py-format-check = 'ruff format --check src/ tests/ docs/docs/tutorials/' +nonpy-format-check = 'npx prettier --list-different --config=prettierrc.toml --ignore-unknown .' +nonpy-format-check-modified = 'python tools/nonpy_prettier_modified.py' check = 'pre-commit run --hook-stage manual --all-files' @@ -116,22 +115,23 @@ check = 'pre-commit run --hook-stage manual --all-files' # 🛠️ Fixes ########## -docs-format-fix = 'docformatter --in-place src/ docs/docs/tutorials/' +docstring-transform = 'pixi run docstripy src/ -s=numpy -w' +docstring-format-fix = 'format-docstring src/' notebook-lint-fix = 'nbqa ruff --fix docs/docs/tutorials/' py-lint-fix = 'ruff check --fix src/ tests/ docs/docs/tutorials/' py-lint-fix-unsafe = 'ruff check --fix --unsafe-fixes src/ tests/ docs/docs/tutorials/' -py-format-fix = "ruff format src/ tests/ docs/docs/tutorials/" +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 auto-formatting steps completed successfully!"' +nonpy-format-fix-modified = 'python tools/nonpy_prettier_modified.py --write' +success-message = 'echo "✅ All auto-formatting steps completed successfully!"' fix = { depends-on = [ + 'docstring-format-fix', 'py-format-fix', - 'docs-format-fix', 'py-lint-fix', 'nonpy-format-fix', 'notebook-lint-fix', - 'success-message-fix', + 'success-message', ] } #################### @@ -179,11 +179,11 @@ notebook-prepare = { depends-on = [ ######################## docs-vars = "JUPYTER_PLATFORM_DIRS=1 PYTHONWARNINGS='ignore::RuntimeWarning'" -docs-pre = "pixi run docs-vars python -m mkdocs" -docs-serve = "pixi run docs-pre serve -f docs/mkdocs.yml" -docs-serve-dirty = "pixi run docs-serve --dirty" -docs-build = "pixi run docs-pre build -f docs/mkdocs.yml" -docs-build-local = "pixi run docs-build --no-directory-urls" +docs-pre = 'pixi run docs-vars python -m mkdocs' +docs-serve = 'pixi run docs-pre serve -f docs/mkdocs.yml' +docs-serve-dirty = 'pixi run docs-serve --dirty' +docs-build = 'pixi run docs-pre build -f docs/mkdocs.yml' +docs-build-local = 'pixi run docs-build --no-directory-urls' docs-deploy-pre = 'mike deploy -F docs/mkdocs.yml --push --branch gh-pages --update-aliases --alias-type redirect' docs-set-default-pre = 'mike set-default -F docs/mkdocs.yml --push --branch gh-pages' @@ -194,9 +194,9 @@ docs-update-assets = 'python tools/update_docs_assets.py' # 📦 Template Management Tasks ############################## -copier-copy = "copier copy gh:easyscience/templates . --data-file ../dynamics/.copier-answers.yml --data template_type=lib" -copier-recopy = "copier recopy --data-file ../dynamics/.copier-answers.yml --data template_type=lib" -copier-update = "copier update --data-file ../dynamics/.copier-answers.yml --data template_type=lib" +copier-copy = 'copier copy gh:easyscience/templates . --data-file ../dynamics/.copier-answers.yml --data template_type=lib' +copier-recopy = 'copier recopy --data-file ../dynamics/.copier-answers.yml --data template_type=lib' +copier-update = 'copier update --data-file ../dynamics/.copier-answers.yml --data template_type=lib' ##################### # 🪝 Pre-commit Hooks @@ -243,9 +243,9 @@ github-labels = 'python tools/update_github_labels.py' # ⚖️ SPDX License Headers ######################### -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/' +license-remove = 'python tools/license_headers.py remove src/ tests/ --exclude-from-pyproject-toml tool.ruff.exclude' +license-add = 'python tools/license_headers.py add src/ tests/ --exclude-from-pyproject-toml tool.ruff.exclude' +license-check = 'python tools/license_headers.py check src/ tests/ --exclude-from-pyproject-toml tool.ruff.exclude' #################################### # 🚀 Other Development & Build Tasks diff --git a/pyproject.toml b/pyproject.toml index bcacdf96..49f35979 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,7 +55,8 @@ dev = [ 'jupytext', # Jupyter notebook text format support 'jupyterquiz', # Quizzes in Jupyter notebooks 'pydoclint', # Docstring linter - 'docformatter', # Docstring formatter + 'format-docstring', # Docstring formatter + 'docstripy', # Convert docstrings to other formats 'interrogate', # Docstring coverage checker 'copier', # Template management 'mike', # MkDocs: Versioned documentation support @@ -177,14 +178,14 @@ testpaths = ['tests'] [tool.ruff] exclude = ['tmp'] indent-width = 4 -line-length = 99 +line-length = 99 # See also `max-line-length` in [tool.ruff.lint.pycodestyle] preview = true # Enable new rules that are not yet stable, like DOC # 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 +docstring-code-line-length = 99 # Line length for code snippets in docstrings 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) @@ -195,6 +196,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 + #'D', # https://docs.astral.sh/ruff/rules/#pydocstyle-d '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 @@ -254,15 +256,20 @@ select = [ # 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 + # The following is replaced by 'D'/[tool.ruff.lint.pydocstyle] and [tool.pydoclint] 'DOC', # https://docs.astral.sh/ruff/rules/#pydoclint-doc + # Disable, as [tool.format_docstring] split one-line docstrings into the canonical multi-line layout + 'D200', # https://docs.astral.sh/ruff/rules/unnecessary-multiline-docstring/ ] # Ignore specific rules in certain files or directories [tool.ruff.lint.per-file-ignores] +'*/__init__.py' = [ + 'F401', # re-exports are intentional in __init__.py +] 'tests/**' = [ 'ANN', # https://docs.astral.sh/ruff/rules/#flake8-annotations-ann + 'D', # https://docs.astral.sh/ruff/rules/#pydocstyle-d 'DOC', # https://docs.astral.sh/ruff/rules/#pydoclint-doc 'INP001', # https://docs.astral.sh/ruff/rules/implicit-namespace-package/ 'S101', # https://docs.astral.sh/ruff/rules/assert/ @@ -290,18 +297,13 @@ max-complexity = 10 # PEP 8 line length guidance: # https://peps.python.org/pep-0008/#maximum-line-length # Use 99 characters as the project-wide maximum for regular code lines. -max-line-length = 99 -# allow longer lines so that parameter declarations such as -# `name (Type | Type | None):` remain on a single line. Splitting these -# lines can prevent tools such as MkDocs and IDEs from correctly -# parsing and rendering parameter documentation. -# The descriptive text itself is wrapped more strictly by -# `docformatter` (see the configuration in [tool.docformatter] below) -# whenever it is treated as normal paragraph text. -# The line length for code snippets in docstrings is also more strict, -# as defined in the [tool.ruff.format] section above. +# Use 99 characters for docstrings. +max-line-length = 99 # See also `line-length` in [tool.ruff] max-doc-length = 99 +[tool.ruff.lint.pydocstyle] +convention = 'numpy' + ############################# # Configuration for pydoclint ############################# @@ -317,20 +319,22 @@ max-doc-length = 99 # the parameter declarations in the code (in function's signature). [tool.pydoclint] -style = "google" +#exclude = '\.' # Temporarily disable pydoclint until we are ready +style = 'numpy' check-style-mismatch = true check-arg-defaults = true allow-init-docstring = true -################################ -# Configuration for docformatter -################################ +#################################### +# Configuration for format-docstring +#################################### -# 'docformatter' -- Code formatter for docstrings -# https://docformatter.readthedocs.io/en/latest/ +# 'format-docstring' -- Code formatter for docstrings +# https://github.com/jsh9/format-docstring -[tool.docformatter] -recursive = true -wrap-summaries = 72 -wrap-descriptions = 72 -close-quotes-on-newline = true +[tool.format_docstring] +#exclude = '\.' # Temporarily disable format-docstring until we are ready +docstring_style = 'numpy' +line_length = 99 +fix_rst_backticks = true +verbose = 'default' diff --git a/src/easydynamics/analysis/analysis.py b/src/easydynamics/analysis/analysis.py index 23364709..bb937e34 100644 --- a/src/easydynamics/analysis/analysis.py +++ b/src/easydynamics/analysis/analysis.py @@ -20,11 +20,10 @@ class Analysis(AnalysisBase): - """For analysing two-dimensional data, i.e. intensity as function of - energy and Q. + """ + 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. + Supports independent fits of each Q value and simultaneous fits of all Q. """ def __init__( @@ -36,24 +35,26 @@ def __init__( instrument_model: InstrumentModel | None = None, extra_parameters: Parameter | list[Parameter] | None = None, ) -> None: - """Initialize an Analysis object. - - Args: - 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, default=None): The Experiment associated - with this Analysis. If None, a default Experiment is - created. - sample_model (SampleModel | None, default=None): The SampleModel - associated with this Analysis. If None, a default - SampleModel is created. - 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, default=None): Extra - parameters to be included in the analysis for advanced - users. If None, no extra parameters are added. + """ + Initialize an Analysis object. + + Parameters + ---------- + 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, default=None + The Experiment associated with this Analysis. If None, a default Experiment is created. + sample_model : SampleModel | None, default=None + The SampleModel associated with this Analysis. If None, a default SampleModel is + created. + 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, default=None + Extra parameters to be included in the analysis for advanced users. If None, no extra + parameters are added. """ # Avoid triggering updates before the object is fully @@ -90,28 +91,33 @@ def __init__( @property def analysis_list(self) -> list[Analysis1d]: - """Get the Analysis1d objects associated with this Analysis. + """ + Get the Analysis1d objects associated with this Analysis. - Returns: - list[Analysis1d]: A list of Analysis1d objects, one for - each Q index. + Returns + ------- + list[Analysis1d] + A list of Analysis1d objects, one for each Q index. """ return self._analysis_list @analysis_list.setter def analysis_list(self, _value: list[Analysis1d]) -> None: - """analysis_list is read-only. + """ + Analysis_list is read-only. - To change the analysis list, modify the experiment, sample - model, or instrument model. + 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. + Parameters + ---------- + _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. + Raises + ------ + AttributeError + Always raised, since analysis_list is read-only. """ raise AttributeError( @@ -128,21 +134,24 @@ def calculate( Q_index: int | None = None, energy: sc.Variable | None = None, ) -> list[np.ndarray] | np.ndarray: - """Calculate model data for a specific Q index. If Q_index is - None, calculate for all Q indices and return a list of arrays. - - Args: - 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. + """ + Calculate model data for a specific Q index. + + If Q_index is None, calculate for all Q indices and return a list of arrays. + + Parameters + ---------- + 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. """ if energy is None: energy = self.energy @@ -158,25 +167,29 @@ def fit( fit_method: str = 'independent', Q_index: int | None = None, ) -> FitResults | list[FitResults]: - """Fit the model to the experimental data. - - Args: - 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 | 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 | 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" or if there are no Q values available for fitting. + """ + Fit the model to the experimental data. + + Parameters + ---------- + 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). + 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". + + Raises + ------ + ValueError + If fit_method is not "independent" or "simultaneous" or if there are no Q values + available for fitting. + + Returns + ------- + FitResults | list[FitResults] + A list of FitResults if fitting independently, or a single FitResults object if fitting + simultaneously. """ if self.Q is None: @@ -202,36 +215,42 @@ def plot_data_and_model( energy: sc.Variable | None = None, **kwargs: dict[str, Any], ) -> InteractiveFigure: - """Plot the experimental data and the model prediction. + """ + Plot the experimental data and the model prediction. + Optionally also plot the individual components of the model. Uses Plopp for plotting: https://scipp.github.io/plopp/ - Args: - 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, default=True): Whether to plot the individual - components. Default is True. - add_background (bool, default=True): Whether to add background components - to the sample model components when plotting. Default is - True. - 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: - ValueError: If Q_index is out of bounds, or if there is no - data to plot, or if there are no Q values available for - plotting. - RuntimeError: If not in a Jupyter notebook environment. - TypeError: If plot_components or add_background is not True - or False. - - Returns: - InteractiveFigure: A Plopp InteractiveFigure containing the - plot of the data and model. + Parameters + ---------- + Q_index : int | None, default=None + Index of the Q value to plot. If None, plot all Q values. + plot_components : bool, default=True + Whether to plot the individual components. + add_background : bool, default=True + Whether to add background components to the sample model components when plotting. + Default is True. + 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 + ------ + ValueError + If Q_index is out of bounds, or if there is no data to plot, or if there are no Q + values available for plotting. + RuntimeError + If not in a Jupyter notebook environment. + TypeError + If plot_components or add_background is not True or False. + + Returns + ------- + InteractiveFigure + A Plopp InteractiveFigure containing the plot of the data and model. """ if Q_index is not None: @@ -300,19 +319,21 @@ def plot_data_and_model( return fig def parameters_to_dataset(self) -> sc.Dataset: - """Creates a scipp dataset with copies of the Parameters in the - model. + """ + Creates a scipp dataset with copies of the Parameters in the model. Ensures unit consistency across Q. - Returns: - sc.Dataset: A dataset where each entry is a parameter, with - dimensions "Q" and values corresponding to the parameter - values. + Raises + ------ + UnitError + If there are inconsistent units for the same parameter across different Q values. - Raises: - UnitError: If there are inconsistent units for the same - parameter across different Q values. + Returns + ------- + sc.Dataset + A dataset where each entry is a parameter, with dimensions "Q" and values corresponding + to the parameter values. """ ds = sc.Dataset(coords={'Q': self.Q}) @@ -370,22 +391,28 @@ def plot_parameters( names: str | list[str] | None = None, **kwargs: dict[str, Any], ) -> InteractiveFigure: - """Plot fitted parameters as a function of Q. - - Args: - names (str | list[str] | None, default=None): Name(s) of the parameter(s) - to plot. If None, plots all parameters. - **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. + """ + Plot fitted parameters as a function of Q. + + Parameters + ---------- + names : str | list[str] | None, default=None + Name(s) of the parameter(s) to plot. If None, plots all parameters. + **kwargs : dict[str, Any] + Additional keyword arguments passed to plopp.slicer for customizing the plot (e.g., + title, linestyle, marker, color). + + 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. + + Returns + ------- + InteractiveFigure + A Plopp InteractiveFigure containing the plot of the parameters. """ ds = self.parameters_to_dataset() @@ -420,13 +447,15 @@ def plot_parameters( ) def fix_energy_offset(self, Q_index: int | None = None) -> None: - """Fix the energy offset parameter(s) for a specific Q index, or - for all Q indices if Q_index is None. - - Args: - Q_index (int | None, default=None): Index of the Q value to - fix the energy offset for. If None, fixes the energy - offset for all Q values. Default is None. + """ + Fix the energy offset parameter(s) for a specific Q index, or for all Q indices if Q_index + is None. + + Parameters + ---------- + Q_index : int | None, default=None + Index of the Q value to fix the energy offset for. If None, fixes the energy offset for + all Q values. """ if Q_index is not None: Q_index = self._verify_Q_index(Q_index) @@ -436,13 +465,15 @@ def fix_energy_offset(self, Q_index: int | None = None) -> None: analysis.fix_energy_offset() def free_energy_offset(self, Q_index: int | None = None) -> None: - """Free the energy offset parameter(s) for a specific Q index, - or for all Q indices if Q_index is None. - - Args: - Q_index (int | None, default=None): Index of the Q value to - free the energy offset for. If None, frees the energy - offset for all Q values. Default is None. + """ + Free the energy offset parameter(s) for a specific Q index, or for all Q indices if Q_index + is None. + + Parameters + ---------- + Q_index : int | None, default=None + Index of the Q value to free the energy offset for. If None, frees the energy offset + for all Q values. """ if Q_index is not None: Q_index = self._verify_Q_index(Q_index) @@ -456,8 +487,8 @@ def free_energy_offset(self, Q_index: int | None = None) -> None: ############# def _on_experiment_changed(self) -> None: - """Update the Q values in the sample and instrument models when - the experiment changes. + """ + Update the Q values in the sample and instrument models when the experiment changes. Also update all the Analysis1d objects with the new experiment. """ @@ -467,11 +498,10 @@ def _on_experiment_changed(self) -> None: analysis.experiment = self.experiment def _on_sample_model_changed(self) -> None: - """Update the Q values in the sample model when the sample model - changes. + """ + Update the Q values in the sample model when the sample model changes. - Also update all the Analysis1d objects with the new sample - model. + Also update all the Analysis1d objects with the new sample model. """ if self._call_updaters: super()._on_sample_model_changed() @@ -479,11 +509,10 @@ def _on_sample_model_changed(self) -> None: analysis.sample_model = self.sample_model def _on_instrument_model_changed(self) -> None: - """Update the Q values in the instrument model when the - instrument model changes. + """ + Update the Q values in the instrument model when the instrument model changes. - Also update all the Analysis1d objects with the new instrument - model. + Also update all the Analysis1d objects with the new instrument model. """ if self._call_updaters: super()._on_instrument_model_changed() @@ -495,14 +524,18 @@ def _on_instrument_model_changed(self) -> None: ############# def _fit_single_Q(self, Q_index: int) -> FitResults: - """Fit data for a single Q index. + """ + Fit data for a single Q index. - Args: - Q_index (int): Index of the Q value to fit. + Parameters + ---------- + Q_index : int + Index of the Q value to fit. - Returns: - FitResults: The results of the fit for the specified - Q index. + Returns + ------- + FitResults + The results of the fit for the specified Q index. """ Q_index = self._verify_Q_index(Q_index) @@ -510,20 +543,24 @@ def _fit_single_Q(self, Q_index: int) -> FitResults: return self.analysis_list[Q_index].fit() def _fit_all_Q_independently(self) -> list[FitResults]: - """Fit data for all Q indices independently. + """ + Fit data for all Q indices independently. - Returns: - list[FitResults]: A list of FitResults, one for each Q - index. + Returns + ------- + list[FitResults] + A list of FitResults, one for each Q index. """ return [analysis.fit() for analysis in self.analysis_list] def _fit_all_Q_simultaneously(self) -> FitResults: - """Fit data for all Q indices simultaneously. + """ + Fit data for all Q indices simultaneously. - Returns: - FitResults: The results of the simultaneous fit across all - Q indices. + Returns + ------- + FitResults + The results of the simultaneous fit across all Q indices. """ xs = [] @@ -551,26 +588,30 @@ def _fit_all_Q_simultaneously(self) -> FitResults: ) def get_fit_functions(self) -> list[callable]: - """Get fit functions for all Q indices, which can be used for - simultaneous fitting. + """ + Get fit functions for all Q indices, which can be used for simultaneous fitting. - Returns: - list[callable]: A list of fit functions, one for each - Q index. + Returns + ------- + list[callable] + A list of fit functions, one for each Q index. """ return [analysis.as_fit_function() for analysis in self.analysis_list] 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". + """ + Create a scipp array for the model. + + Parameters + ---------- + 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 @@ -585,23 +626,27 @@ def _create_components_dataset( add_background: bool = True, energy: sc.Variable | None = None, ) -> sc.Dataset: - """Create a scipp dataset containing the individual components - of the model for plotting. - - Args: - add_background (bool, default=True): Whether to add background components - to the sample model components when creating the - 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. - - Returns: - sc.Dataset: A scipp Dataset where each entry is a component - of the model, with dimensions "Q". + """ + Create a scipp dataset containing the individual components of the model for plotting. + + Parameters + ---------- + add_background : bool, default=True + Whether to add background components to the sample model components when creating the + 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. + + Returns + ------- + sc.Dataset + A scipp Dataset where each entry is a component of the model, with dimensions "Q". """ if not isinstance(add_background, bool): raise TypeError('add_background must be True or False.') diff --git a/src/easydynamics/analysis/analysis1d.py b/src/easydynamics/analysis/analysis1d.py index 81f98f55..36cc7c8c 100644 --- a/src/easydynamics/analysis/analysis1d.py +++ b/src/easydynamics/analysis/analysis1d.py @@ -21,11 +21,10 @@ class Analysis1d(AnalysisBase): - """For analysing one-dimensional data, i.e. intensity as function of - energy for a single Q index. + """ + 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. + Is used primarily in the Analysis class, but can also be used on its own for simpler analyses. """ def __init__( @@ -38,27 +37,31 @@ def __init__( Q_index: int | None = None, extra_parameters: Parameter | list[Parameter] | None = None, ) -> None: - """Initialize a Analysis1d. - - Args: - 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, default=None): The Experiment associated - with this Analysis. If None, a default Experiment is - created. - sample_model (SampleModel | None, default=None): The SampleModel - associated with this Analysis. If None, a default - SampleModel is created. - instrument_model (InstrumentModel | None, default=None): The - InstrumentModel associated with this Analysis. If None, - a default InstrumentModel is created. - 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, default=None): Extra - parameters to be included in the analysis for advanced - users. If None, no extra parameters are added. + """ + Initialize a Analysis1d. + + Parameters + ---------- + 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. By + default, None. + experiment : Experiment | None, default=None + The Experiment associated with this Analysis. If None, a default Experiment is created. + + sample_model : SampleModel | None, default=None + The SampleModel associated with this Analysis. If None, a default SampleModel is + created. + instrument_model : InstrumentModel | None, default=None + The InstrumentModel associated with this Analysis. If None, a default InstrumentModel + is created. + 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, default=None + Extra parameters to be included in the analysis for advanced users. If None, no extra + parameters are added. """ super().__init__( display_name=display_name, @@ -89,20 +92,26 @@ def __init__( @property def Q_index(self) -> int | None: - """Get the Q index associated with this Analysis. + """ + Get the Q index associated with this Analysis. - Returns: - int | None: The Q index associated with this Analysis. + Returns + ------- + int | None + The Q index associated with this Analysis. """ return self._Q_index @Q_index.setter def Q_index(self, value: int | None) -> None: - """Set the Q index for single Q analysis. + """ + Set the Q index for single Q analysis. - Args: - value (int | None): The Q index. + Parameters + ---------- + value : int | None + The Q index. """ self._Q_index = self._verify_Q_index(value) @@ -113,16 +122,21 @@ def Q_index(self, value: int | None) -> None: ############# def calculate(self, energy: sc.Variable | None = None) -> np.ndarray: - """Calculate the model prediction for the chosen Q index. Makes - sure the convolver is up to date before calculating. + """ + Calculate the model prediction for the chosen Q index. + + Makes sure the convolver is up to date before calculating. - Args: - energy (sc.Variable | None, default=None): Optional energy grid to use for - calculation. If None, the energy grid from the experiment - is used. + Parameters + ---------- + energy : sc.Variable | None, default=None + Optional energy grid to use for calculation. If None, the energy grid from the + experiment is used. - Returns: - np.ndarray: The calculated model prediction. + Returns + ------- + np.ndarray + The calculated model prediction. """ energy = self._verify_energy(energy) self._convolver = self._create_convolver(energy=energy) @@ -130,16 +144,21 @@ def calculate(self, energy: sc.Variable | None = None) -> np.ndarray: return self._calculate(energy=energy) def _calculate(self, energy: sc.Variable | None = None) -> np.ndarray: - """Calculate the model prediction for the chosen Q index. Does - not check if the convolver is up to date. + """ + Calculate the model prediction for the chosen Q index. + + Does not check if the convolver is up to date. - Args: - energy (sc.Variable | None, default=None): Optional energy grid to use for - calculation. If None, the energy grid from the experiment - is used. + Parameters + ---------- + energy : sc.Variable | None, default=None + Optional energy grid to use for calculation. If None, the energy grid from the + experiment is used. - Returns: - np.ndarray: The calculated model prediction. + Returns + ------- + np.ndarray + The calculated model prediction. """ sample_intensity = self._evaluate_sample(energy=energy) @@ -149,19 +168,21 @@ def _calculate(self, energy: sc.Variable | None = None) -> np.ndarray: return sample_intensity + background_intensity def fit(self) -> FitResults: - """Fit the model to the experimental data for the chosen Q - index. + """ + Fit the model to the experimental data for the chosen Q index. - The energy grid is fixed for the duration of the fit. - Convolution objects are created once and reused during - parameter optimization for performance reasons. + The energy grid is fixed for the duration of the fit. Convolution objects are created once + and reused during parameter optimization for performance reasons. - Returns: - FitResults: The result of the fit. + Raises + ------ + ValueError + If no experiment is associated with this Analysis. - 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.') @@ -188,37 +209,43 @@ def as_fit_function( _x: np.ndarray | sc.Variable | None = None, **kwargs: dict[str, Any], # noqa: ARG002 ) -> callable: - """Return self._calculate as a fit function. - - The EasyScience fitter requires x as input, but - self._calculate() already uses the correct energy from the - experiment. So we ignore the x input and just return the - calculated model. - - Args: - _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. + """ + Return self._calculate as a fit function. + + The EasyScience fitter requires x as input, but self._calculate() already uses the correct + energy from the experiment. So we ignore the x input and just return the calculated model. + + Parameters + ---------- + _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: np.ndarray | sc.Variable | None = None, **kwargs: dict[str, Any], # noqa: ARG001 ) -> np.ndarray: + """Fit function.""" return self._calculate() return fit_function def get_all_variables(self) -> list[DescriptorNumber]: - """Get all variables used in the analysis. + """ + Get all variables used in the analysis. - Returns: - list[DescriptorNumber]: A list of all variables. + Returns + ------- + list[DescriptorNumber] + A list of all variables. """ variables = self.sample_model.get_all_variables(Q_index=self.Q_index) @@ -236,28 +263,34 @@ def plot_data_and_model( energy: sc.Variable | None = None, **kwargs: dict[str, Any], ) -> InteractiveFigure: - """Plot the experimental data and the model prediction for the - chosen Q index. Optionally also plot the individual components - of the model. + """ + Plot the experimental data and the model prediction for the chosen Q index. Optionally also + plot the individual components of the model. Uses Plopp for plotting: https://scipp.github.io/plopp/ - Args: - 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, default=None): Optional energy grid to use for - plotting. If None, the energy grid from the experiment - is used. - **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. + Parameters + ---------- + 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, default=None + Optional energy grid to use for plotting. If None, the energy grid from the experiment + is used. + **kwargs : dict[str, Any] + Keyword arguments to pass to the plotting function. + + Raises + ------ + ValueError + If no data is available to plot. + + Returns + ------- + InteractiveFigure + A plot of the data and model. """ import plopp as pp @@ -317,42 +350,54 @@ def free_energy_offset(self) -> None: ############# def _require_Q_index(self) -> int: - """Get the Q index, ensuring it is set. Raises a ValueError if - the Q index is not set. + """ + Get the Q index, ensuring it is set. + + Raises a ValueError if the Q index is not set. - Returns: - int: The Q index. + Raises + ------ + ValueError + If the Q index is not set. - Raises: - ValueError: If the Q index is not set. + Returns + ------- + int + The Q index. """ if self._Q_index is None: raise ValueError('Q_index must be set.') return self._Q_index def _on_Q_index_changed(self) -> None: - """Handle changes to the Q index. + """ + Handle changes to the Q index. - This method is called whenever the Q index is changed. It - updates the Convolution object for the new Q index and the - masked energy from the experiment for the new Q index. + This method is called whenever the Q index is changed. It updates the Convolution object + for the new Q index and the masked energy from the experiment for the new Q index. """ masked_energy = self.experiment.get_masked_energy(Q_index=self._Q_index) self._masked_energy = masked_energy self._convolver = self._create_convolver() def _verify_energy(self, energy: sc.Variable | None) -> sc.Variable | None: - """Verify that the provided energy is the correct type. - - Args: - energy (sc.Variable | None): The energy to verify. - - 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. + """ + Verify that the provided energy is the correct type. + + Parameters + ---------- + energy : sc.Variable | None + The energy to verify. + + Raises + ------ + TypeError + If energy is not a sc.Variable or None. + + Returns + ------- + sc.Variable | None + The verified energy, or None if no energy is provided. """ if energy is not None and not isinstance(energy, sc.Variable): @@ -364,18 +409,25 @@ def _calculate_energy_with_offset( energy: sc.Variable, energy_offset: Parameter, ) -> sc.Variable: - """Calculate the energy grid with the energy offset applied. - - Args: - energy (sc.Variable): The energy grid to apply the offset to. - energy_offset (Parameter): The energy offset to apply. - - Returns: - sc.Variable: The energy grid with the offset applied. - - Raises: - sc.UnitError: If the energy and energy offset have - incompatible units. + """ + Calculate the energy grid with the energy offset applied. + + Parameters + ---------- + energy : sc.Variable + The energy grid to apply the offset to. + energy_offset : Parameter + The energy offset to apply. + + Raises + ------ + sc.UnitError + If the energy and energy offset have incompatible units. + + Returns + ------- + sc.Variable + The energy grid with the offset applied. """ if energy.unit != energy_offset.unit: @@ -402,33 +454,33 @@ def _evaluate_components( convolve: bool = True, energy: sc.Variable | None = None, ) -> np.ndarray: - """Calculate the contribution of a set of components, optionally - convolving with the resolution. - - If convolve is True and a - Convolution object is provided (for full model evaluation), we - use it to perform the convolution of the components with the + """ + Calculate the contribution of a set of components, optionally convolving with the resolution. - If convolve is True but no Convolution object is - provided, create a new Convolution object for the given - components (for individual components). - If convolve is False, evaluate the components directly without - convolution (for background). - - Args: - components (ComponentCollection | ModelComponent): The - components to evaluate. - 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, default=True): Whether to perform convolution with the - resolution. Default is True. - 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. + + If convolve is True and a Convolution object is provided (for full model evaluation), we + use it to perform the convolution of the components with the resolution. If convolve is + True but no Convolution object is provided, create a new Convolution object for the given + components (for individual components). If convolve is False, evaluate the components + directly without convolution (for background). + + Parameters + ---------- + components : ComponentCollection | ModelComponent + The components to evaluate. + 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, default=True + Whether to perform convolution with the resolution. + 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. """ Q_index = self._require_Q_index() @@ -477,17 +529,21 @@ def _evaluate_sample( self, energy: sc.Variable | None = None, ) -> np.ndarray: - """Evaluate the sample contribution for a given Q index. + """ + Evaluate the sample contribution for a given Q index. Assumes that self._convolver is up to date. - Args: - energy (sc.Variable | None, default=None): Optional energy grid to use for - evaluation. If None, the energy grid from the experiment - is used. + Parameters + ---------- + 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 sample contribution. + Returns + ------- + np.ndarray + The evaluated sample contribution. """ Q_index = self._require_Q_index() components = self.sample_model.get_component_collection(Q_index=Q_index) @@ -503,17 +559,21 @@ def _evaluate_sample_component( component: ModelComponent, energy: sc.Variable | None = None, ) -> np.ndarray: - """Evaluate a single sample component for the chosen Q index. - - Args: - component (ModelComponent): The sample component to - evaluate. - 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 sample component contribution. + """ + Evaluate a single sample component for the chosen Q index. + + Parameters + ---------- + component : ModelComponent + The sample component to evaluate. + 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 sample component contribution. """ return self._evaluate_components( components=component, @@ -523,15 +583,19 @@ def _evaluate_sample_component( ) 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, default=None): Optional energy grid to use for - evaluation. If None, the energy grid from the experiment - is used. - - Returns: - np.ndarray: The evaluated background contribution. + """ + Evaluate the background contribution for the chosen Q index. + + Parameters + ---------- + 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 background contribution. """ Q_index = self._require_Q_index() background_components = self.instrument_model.background_model.get_component_collection( @@ -549,18 +613,21 @@ def _evaluate_background_component( component: ModelComponent, energy: sc.Variable | None = None, ) -> np.ndarray: - """Evaluate a single background component for the chosen Q - index. - - Args: - component (ModelComponent): The background component to - evaluate. - 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 background component contribution. + """ + Evaluate a single background component for the chosen Q index. + + Parameters + ---------- + component : ModelComponent + The background component to evaluate. + 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 background component contribution. """ return self._evaluate_components( @@ -574,18 +641,20 @@ def _create_convolver( self, energy: sc.Variable | None = None, ) -> Convolution | None: - """Initialize and return a Convolution object for the chosen Q - index. If the necessary components for convolution are not - available, return None. - - Args: - energy (sc.Variable | None, default=None): Optional energy grid to use for - convolution. If None, the energy grid from the experiment - is used. - - Returns: - Convolution | None: The initialized Convolution object or - None if not available. + """ + Initialize and return a Convolution object for the chosen Q index. If the necessary + components for convolution are not available, return None. + + Parameters + ---------- + energy : sc.Variable | None, default=None + Optional energy grid to use for convolution. If None, the energy grid from the + experiment is used. + + Returns + ------- + Convolution | None + The initialized Convolution object or None if not available. """ Q_index = self._require_Q_index() @@ -621,19 +690,25 @@ def _create_component_scipp_array( background: np.ndarray | None = None, energy: sc.Variable | None = None, ) -> sc.DataArray: - """Create a scipp DataArray for a single component. Adds the - background if it is not None. - - Args: - component (ModelComponent): The component to evaluate. - background (np.ndarray | None, default=None): Optional background to add - to the component. - energy (sc.Variable | None, default=None): Optional energy grid to use for - evaluation. If None, the energy grid from the experiment - is used. - - Returns: - sc.DataArray: The model calculation of the component. + """ + Create a scipp DataArray for a single component. + + Adds the background if it is not None. + + Parameters + ---------- + component : ModelComponent + The component to evaluate. + background : np.ndarray | None, default=None + Optional background to add to the component. + energy : sc.Variable | None, default=None + Optional energy grid to use for evaluation. If None, the energy grid from the + experiment is used. + + Returns + ------- + sc.DataArray + The model calculation of the component. """ values = self._evaluate_sample_component(component=component, energy=energy) @@ -646,16 +721,21 @@ def _create_background_component_scipp_array( component: ModelComponent, energy: sc.Variable | None = None, ) -> sc.DataArray: - """Create a scipp DataArray for a single background component. - - Args: - component (ModelComponent): The component to evaluate. - energy (sc.Variable | None, default=None): Optional energy grid to use for - evaluation. If None, the energy grid from the experiment - is used. - - Returns: - sc.DataArray: The model calculation of the component. + """ + Create a scipp DataArray for a single background component. + + Parameters + ---------- + component : ModelComponent + The component to evaluate. + energy : sc.Variable | None, default=None + Optional energy grid to use for evaluation. If None, the energy grid from the + experiment is used. + + Returns + ------- + sc.DataArray + The model calculation of the component. """ values = self._evaluate_background_component( @@ -665,17 +745,19 @@ 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: - """Create a scipp DataArray for the full sample model including - background. - - Args: - energy (sc.Variable | None, default=None): Optional energy grid to use for - evaluation. If None, the energy grid from the experiment - is used. - - Returns: - sc.DataArray: The model calculation of the full sample - model. + """ + Create a scipp DataArray for the full sample model including background. + + Parameters + ---------- + energy : sc.Variable | None, default=None + Optional energy grid to use for evaluation. If None, the energy grid from the + experiment is used. + + Returns + ------- + sc.DataArray + The model calculation of the full sample model. """ values = self.calculate(energy=energy) return self._to_scipp_array(values=values, energy=energy) @@ -685,18 +767,21 @@ def _create_components_dataset_single_Q( add_background: bool = True, energy: sc.Variable | None = None, ) -> dict[str, sc.DataArray]: - """Create sc.DataArrays for all sample and background - components. - - Args: - 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. - - Returns: - dict[str, sc.DataArray]: A dictionary of component names to - their corresponding sc.DataArrays. + """ + Create sc.DataArrays for all sample and background components. + + Parameters + ---------- + 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. + + Returns + ------- + dict[str, sc.DataArray] + A dictionary of component names to their corresponding sc.DataArrays. """ scipp_arrays = {} sample_components = self.sample_model.get_component_collection( @@ -727,17 +812,22 @@ def _to_scipp_array( values: np.ndarray, energy: sc.Variable | None = None, ) -> sc.DataArray: - """Convert a numpy array of values to a sc.DataArray with the - correct coordinates for energy and Q. - - 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. + """ + Convert a numpy array of values to a sc.DataArray with the correct coordinates for energy + and Q. + + Parameters + ---------- + 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. """ if energy is None: diff --git a/src/easydynamics/analysis/analysis_base.py b/src/easydynamics/analysis/analysis_base.py index 61247e0e..d249a052 100644 --- a/src/easydynamics/analysis/analysis_base.py +++ b/src/easydynamics/analysis/analysis_base.py @@ -12,14 +12,14 @@ class AnalysisBase(EasyScienceModelBase): - """Base class for analysis in EasyDynamics. This class is not meant - to be used directly. - - An Analysis consists of an Experiment, a SampleModel, and an - InstrumentModel. The Experiment contains the data to be fitted, the - SampleModel contains the model for the sample, and the - InstrumentModel contains the model for the instrument, including - background and resolution + """ + Base class for analysis in EasyDynamics. + + This class is not meant to be used directly. + + An Analysis consists of an Experiment, a SampleModel, and an InstrumentModel. The Experiment + contains the data to be fitted, the SampleModel contains the model for the sample, and the + InstrumentModel contains the model for the instrument, including background and resolution """ def __init__( @@ -31,31 +31,35 @@ def __init__( instrument_model: InstrumentModel | None = None, extra_parameters: Parameter | list[Parameter] | None = None, ) -> None: - """Initialize the AnalysisBase. - - Args: - 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, default=None): The Experiment associated - with this Analysis. If None, a default Experiment is - created. - sample_model (SampleModel | None, default=None): The SampleModel - associated with this Analysis. If None, a default - SampleModel is created. - 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, 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 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. + """ + Initialize the AnalysisBase. + + Parameters + ---------- + 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. By + default, None. + experiment : Experiment | None, default=None + The Experiment associated with this Analysis. If None, a default Experiment is created. + + sample_model : SampleModel | None, default=None + The SampleModel associated with this Analysis. If None, a default SampleModel is + created. + 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, 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 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) @@ -101,23 +105,31 @@ def __init__( @property def experiment(self) -> Experiment: - """Get the Experiment associated with this Analysis. + """ + Get the Experiment associated with this Analysis. - Returns: - Experiment: The Experiment associated with this Analysis. + Returns + ------- + Experiment + The Experiment associated with this Analysis. """ return self._experiment @experiment.setter def experiment(self, value: Experiment) -> None: - """Set the Experiment for this Analysis. + """ + Set the Experiment for this Analysis. - Args: - value (Experiment): The Experiment to set for this Analysis. + Parameters + ---------- + value : Experiment + The Experiment to set for this Analysis. - Raises: - TypeError: if value is not an Experiment. + Raises + ------ + TypeError + If value is not an Experiment. """ if not isinstance(value, Experiment): @@ -127,23 +139,31 @@ def experiment(self, value: Experiment) -> None: @property def sample_model(self) -> SampleModel: - """Get the SampleModel associated with this Analysis. + """ + Get the SampleModel associated with this Analysis. - Returns: - SampleModel: The SampleModel associated with this Analysis. + Returns + ------- + SampleModel + The SampleModel associated with this Analysis. """ return self._sample_model @sample_model.setter def sample_model(self, value: SampleModel) -> None: - """Set the SampleModel for this Analysis. + """ + Set the SampleModel for this Analysis. - Args: - value (SampleModel): The SampleModel to set for this Analysis. + Parameters + ---------- + value : SampleModel + The SampleModel to set for this Analysis. - Raises: - TypeError: if value is not a SampleModel. + Raises + ------ + TypeError + If value is not a SampleModel. """ if not isinstance(value, SampleModel): raise TypeError('sample_model must be an instance of SampleModel') @@ -152,24 +172,30 @@ def sample_model(self, value: SampleModel) -> None: @property def instrument_model(self) -> InstrumentModel: - """Get the InstrumentModel associated with this Analysis. + """ + Get the InstrumentModel associated with this Analysis. - Returns: - InstrumentModel: The InstrumentModel associated with this - Analysis. + Returns + ------- + InstrumentModel + The InstrumentModel associated with this Analysis. """ return self._instrument_model @instrument_model.setter def instrument_model(self, value: InstrumentModel) -> None: - """Set the InstrumentModel for this Analysis. + """ + Set the InstrumentModel for this Analysis. - Args: - value (InstrumentModel): The InstrumentModel to set for this - Analysis. + Parameters + ---------- + value : InstrumentModel + The InstrumentModel to set for this Analysis. - Raises: - TypeError: if value is not an InstrumentModel. + Raises + ------ + TypeError + If value is not an InstrumentModel. """ if not isinstance(value, InstrumentModel): raise TypeError('instrument_model must be an instance of InstrumentModel') @@ -178,105 +204,122 @@ def instrument_model(self, value: InstrumentModel) -> None: @property def Q(self) -> sc.Variable | None: - """Get the Q values from the associated Experiment, if - available. + """ + Get the Q values from the associated Experiment, if available. - Returns: - sc.Variable | None: The Q values from the associated Experiment, - if available, and None if not. + Returns + ------- + 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: sc.Variable) -> None: - """Q cannot be set, as it is a read-only property derived from - the Experiment. + """ + 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. + Parameters + ---------- + _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. + Raises + ------ + AttributeError + If trying to set Q. """ raise AttributeError('Q is a read-only property derived from the Experiment.') @property def energy(self) -> sc.Variable | None: - """Get the energy values from the associated Experiment, if - available. + """ + Get the energy values from the associated Experiment, if available. - Returns: - sc.Variable | None: The energy values from the associated - Experiment, if available, and None if not. + Returns + ------- + sc.Variable | None + The energy values from the associated. """ return self.experiment.energy @energy.setter def energy(self, _value: sc.Variable) -> None: - """Energy cannot be set, as it is a read-only property derived - from the Experiment. + """ + 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. + Parameters + ---------- + _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. + Raises + ------ + AttributeError + If trying to set energy. """ raise AttributeError('energy is a read-only property derived from the Experiment.') @property def temperature(self) -> Parameter | None: - """Get the temperature from the associated SampleModel, if - available. + """ + Get the temperature from the associated SampleModel, if available. - Returns: - Parameter | None: The temperature from the associated SampleModel, - if available, and None if not. + Returns + ------- + 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: 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. + """ + Temperature cannot be set, as it is a read-only property derived from the SampleModel. + + Parameters + ---------- + _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.') @property def extra_parameters(self) -> list[Parameter]: - """Get the extra parameters included in this Analysis. + """ + Get the extra parameters included in this Analysis. - Returns: - list[Parameter]: The extra parameters included in this - Analysis. + Returns + ------- + list[Parameter] + The extra parameters included in this Analysis. """ return self._extra_parameters @extra_parameters.setter def extra_parameters(self, value: Parameter | list[Parameter]) -> None: - """Set the extra parameters for this Analysis. + """ + Set the extra parameters for this Analysis. - Args: - value (Parameter | list[Parameter]): The extra parameters to - include in this Analysis. + Parameters + ---------- + value : Parameter | list[Parameter] + The extra parameters to include in this Analysis. - Raises: - TypeError: If value is not a Parameter, a list of - Parameters, or None. + Raises + ------ + TypeError + If value is not a Parameter, a list of Parameters, or None. """ if isinstance(value, Parameter): self._extra_parameters = [value] @@ -292,11 +335,10 @@ def extra_parameters(self, value: Parameter | list[Parameter]) -> None: ############# def normalize_resolution(self) -> None: - """Normalize the resolution in the InstrumentModel to ensure - that it integrates to 1. + """ + Normalize the resolution in the InstrumentModel to ensure that it integrates to 1. - This is important for accurate fitting and interpretation of the - results. + This is important for accurate fitting and interpretation of the results. """ self.instrument_model.normalize_resolution() @@ -305,36 +347,44 @@ def normalize_resolution(self) -> None: ############# def _on_experiment_changed(self) -> None: - """Update the Q values in the sample and instrument models when - the experiment changes. + """ + Update the Q values in the sample and instrument models when the experiment changes. """ self.sample_model.Q = self.Q self.instrument_model.Q = self.Q def _on_sample_model_changed(self) -> None: - """Update the Q values in the sample model when the sample model - changes. + """ + Update the Q values in the sample model when the sample model changes. """ self.sample_model.Q = self.Q def _on_instrument_model_changed(self) -> None: - """Update the Q values in the instrument model when the - instrument model changes. + """ + Update the Q values in the instrument model when the instrument model changes. """ self.instrument_model.Q = self.Q def _verify_Q_index(self, Q_index: int | None) -> int | None: - """Verify that the Q index is valid. - - Args: - Q_index (int | None): The Q index to verify. - - Returns: - int | None: The verified Q index. - - Raises: - TypeError: If Q_index is not an integer or None. - IndexError: If the Q index is not valid. + """ + Verify that the Q index is valid. + + Parameters + ---------- + Q_index : int | None + The Q index to verify. + + Raises + ------ + TypeError + If Q_index is not an integer or None. + IndexError + If the Q index is not valid. + + Returns + ------- + int | None + The verified Q index. """ if Q_index is None: return None @@ -351,10 +401,13 @@ def _verify_Q_index(self, Q_index: int | None) -> int | None: ############# def __repr__(self) -> str: - """Return a string representation of the Analysis. + """ + Return a string representation of the Analysis. - Returns: - str: A string representation of the Analysis. + Returns + ------- + str + A string representation of the Analysis. """ 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 70ecc318..a835f215 100644 --- a/src/easydynamics/convolution/analytical_convolution.py +++ b/src/easydynamics/convolution/analytical_convolution.py @@ -17,11 +17,11 @@ class AnalyticalConvolution(ConvolutionBase): - """Analytical convolution of a ModelComponent or ComponentCollection - with a ResolutionModel. + """ + 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. @@ -43,19 +43,21 @@ def __init__( resolution_components: ComponentCollection | ModelComponent | None = None, energy_offset: Numeric | Parameter = 0.0, ) -> None: - """Initialize an AnalyticalConvolution. - - Args: - energy (np.ndarray | sc.Variable): 1D array of energy values - where the convolution is evaluated. - 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 | None, default=None): - The resolution model to convolve with. - energy_offset (Numeric | Parameter, default=0.0): An offset to - shift the energy values by. + """ + Initialize an AnalyticalConvolution. + + Parameters + ---------- + energy : np.ndarray | sc.Variable + 1D array of energy values where the convolution is evaluated. + 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 | None, default=None + The resolution model to convolve with. + energy_offset : Numeric | Parameter, default=0.0 + An offset to shift the energy values by. """ super().__init__( energy=energy, @@ -68,14 +70,18 @@ def __init__( def convolution( self, ) -> np.ndarray: - """Convolve sample with resolution analytically if possible. - Accepts ComponentCollection or single ModelComponent for each. - Possible analytical convolutions are any combination of delta - functions, Gaussians, Lorentzians and Voigt profiles. - - Returns: - np.ndarray: The convolution of the sample_components and - resolution_components values evaluated at self.energy. + """ + Convolve sample with resolution analytically if possible. + + Accepts ComponentCollection or single ModelComponent for each. Possible analytical + convolutions are any combination of delta functions, Gaussians, Lorentzians and Voigt + profiles. + + Returns + ------- + np.ndarray + The convolution of the sample_components and resolution_components values evaluated at + self.energy. """ sample_components = self.sample_components.components @@ -100,48 +106,47 @@ def _convolute_analytic_pair( sample_component: ModelComponent, resolution_component: ModelComponent, ) -> np.ndarray: - r"""Analytic convolution for component pair (sample_component, - resolution_component). + r""" + Analytic convolution for component pair (sample_component, resolution_component). - The convolution of two Gaussian components results in another - Gaussian component with width $\sqrt{w_1^2 + w_2^2}$. + The convolution of two Gaussian components results in another Gaussian component with width + $\sqrt{w_1^2 + w_2^2}$. - The convolution of two Lorentzian components results in another - Lorentzian component with width $w_1 + w_2$. + The convolution of two Lorentzian components results in another Lorentzian component with + width $w_1 + w_2$. - The convolution of a Gaussian and a Lorentzian results in a - Voigt profile. + The convolution of a Gaussian and a Lorentzian results in a Voigt profile. - The convolution of a Gaussian and a Voigt profile results in - another Voigt profile, with the Lorentzian width unchanged and - the Gaussian widths summed in quadrature. + The convolution of a Gaussian and a Voigt profile results in another Voigt profile, with + the Lorentzian width unchanged and the Gaussian widths summed in quadrature. - The convolution of a Lorentzian and a Voigt profile results in - another Voigt profile, with the Gaussian width unchanged and the - Lorentzian widths summed. + The convolution of a Lorentzian and a Voigt profile results in another Voigt profile, with + the Gaussian width unchanged and the Lorentzian widths summed. - The convolution of two Voigt profiles results in another Voigt - profile, with the Gaussian widths summed in quadrature and the - Lorentzian widths summed. + The convolution of two Voigt profiles results in another Voigt profile, with the Gaussian + widths summed in quadrature and the Lorentzian widths summed. - The convolution of a delta function with any component or - ComponentCollection results in the same component or - ComponentCollection shifted by the delta center. + The convolution of a delta function with any component or ComponentCollection results in + the same component or ComponentCollection shifted by the delta center. All areas are multiplied in the convolution. - Args: - sample_component (ModelComponent): The sample component to - be convolved. - resolution_component (ModelComponent): The resolution - component to convolve with. - - Returns: - np.ndarray: The convolution result - - Raises: - ValueError: If the component pair cannot be handled - analytically. + Parameters + ---------- + sample_component : ModelComponent + The sample component to be convolved. + resolution_component : ModelComponent + The resolution component to convolve with. + + Raises + ------ + ValueError + If the component pair cannot be handled analytically. + + Returns + ------- + np.ndarray + The convolution result. """ if isinstance(resolution_component, DeltaFunction): @@ -187,19 +192,22 @@ def _convolute_delta_any( 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 + """ + Convolution of delta function with any ModelComponent or ComponentCollection results in the + same component or ComponentCollection shifted by the delta center. The areas are multiplied. - Args: - sample_component (DeltaFunction): The sample component to - be convolved. - resolution_components (ComponentCollection | ModelComponent) - : The resolution model to convolve with. - - Returns: - np.ndarray: The evaluated convolution values at self.energy. + Parameters + ---------- + sample_component : DeltaFunction + The sample component to be convolved. + resolution_components : ComponentCollection | ModelComponent + : The resolution model to convolve with. + + Returns + ------- + np.ndarray + The evaluated convolution values at self.energy. """ return sample_component.area.value * resolution_components.evaluate( self.energy_with_offset.values - sample_component.center.value @@ -210,18 +218,21 @@ def _convolute_gaussian_gaussian( sample_component: Gaussian, resolution_component: Gaussian, ) -> np.ndarray: - r"""Convolution of two Gaussian components results in another - Gaussian component with width $\sqrt{w_1^2 + w_2^2}$. The areas - are multiplied. - - Args: - sample_component (Gaussian): The sample Gaussian component - to be convolved. - resolution_component (Gaussian): The resolution Gaussian - component to convolve with. - - Returns: - np.ndarray: The evaluated convolution values at self.energy. + r""" + Convolution of two Gaussian components results in another Gaussian component with width + $\sqrt{w_1^2 + w_2^2}$. The areas are multiplied. + + Parameters + ---------- + sample_component : Gaussian + The sample Gaussian component to be convolved. + resolution_component : Gaussian + The resolution Gaussian component to convolve with. + + Returns + ------- + np.ndarray + The evaluated convolution values at self.energy. """ width = np.sqrt(sample_component.width.value**2 + resolution_component.width.value**2) @@ -237,17 +248,21 @@ def _convolute_gaussian_lorentzian( sample_component: Gaussian, resolution_component: Lorentzian, ) -> np.ndarray: - """Convolution of a Gaussian and a Lorentzian results in a Voigt - profile. The areas are multiplied. - - Args: - sample_component (Gaussian): The sample Gaussian component - to be convolved. - resolution_component (Lorentzian): The resolution Lorentzian - component to convolve with. + """ + Convolution of a Gaussian and a Lorentzian results in a Voigt profile. The areas are + multiplied. - Returns: - np.ndarray: The evaluated convolution values at self.energy. + Parameters + ---------- + sample_component : Gaussian + The sample Gaussian component to be convolved. + resolution_component : Lorentzian + The resolution Lorentzian component to convolve with. + + Returns + ------- + np.ndarray + The evaluated convolution values at self.energy. """ center = sample_component.center.value + resolution_component.center.value area = sample_component.area.value * resolution_component.area.value @@ -264,19 +279,22 @@ def _convolute_gaussian_voigt( sample_component: Gaussian, resolution_component: Voigt, ) -> np.ndarray: - """Convolution of a Gaussian and a Voigt profile results in - another Voigt profile. The Lorentzian width remains unchanged, - while the Gaussian widths are summed in quadrature. The areas - are multiplied. - - Args: - sample_component (Gaussian): The sample Gaussian component - to be convolved. - resolution_component (Voigt): The resolution Voigt component - to convolve with. - - Returns: - np.ndarray: The evaluated convolution values at self.energy. + """ + Convolution of a Gaussian and a Voigt profile results in another Voigt profile. The + Lorentzian width remains unchanged, while the Gaussian widths are summed in quadrature. The + areas are multiplied. + + Parameters + ---------- + sample_component : Gaussian + The sample Gaussian component to be convolved. + resolution_component : Voigt + The resolution Voigt component to convolve with. + + Returns + ------- + np.ndarray + The evaluated convolution values at self.energy. """ area = sample_component.area.value * resolution_component.area.value @@ -300,18 +318,21 @@ def _convolute_lorentzian_lorentzian( sample_component: Lorentzian, resolution_component: Lorentzian, ) -> np.ndarray: - r"""Convolution of two Lorentzian components results in another - Lorentzian component with width $w_1 + w_2$. The areas are - multiplied. - - Args: - sample_component (Lorentzian): The sample Lorentzian - component to be convolved. - resolution_component (Lorentzian): The resolution Lorentzian - component to convolve with. - - Returns: - np.ndarray: The evaluated convolution values at self.energy. + r""" + Convolution of two Lorentzian components results in another Lorentzian component with width + $w_1 + w_2$. The areas are multiplied. + + Parameters + ---------- + sample_component : Lorentzian + The sample Lorentzian component to be convolved. + resolution_component : Lorentzian + The resolution Lorentzian component to convolve with. + + Returns + ------- + np.ndarray + The evaluated convolution values at self.energy. """ area = sample_component.area.value * resolution_component.area.value @@ -326,21 +347,24 @@ def _convolute_lorentzian_voigt( sample_component: Lorentzian, resolution_component: Voigt, ) -> np.ndarray: - """Convolution of a Lorentzian and a Voigt profile results in - another Voigt profile. + """ + Convolution of a Lorentzian and a Voigt profile results in another Voigt profile. - The Gaussian width remains unchanged, while the Lorentzian - widths are summed. + The Gaussian width remains unchanged, while the Lorentzian widths are summed. The areas are multiplied. - Args: - sample_component (Lorentzian): The sample Lorentzian - component to be convolved. - resolution_component (Voigt): The resolution Voigt component - to convolve with. - Returns: - np.ndarray: The evaluated convolution values at self.energy. + Parameters + ---------- + sample_component : Lorentzian + The sample Lorentzian component to be convolved. + resolution_component : Voigt + The resolution Voigt component to convolve with. + + Returns + ------- + np.ndarray + The evaluated convolution values at self.energy. """ area = sample_component.area.value * resolution_component.area.value @@ -364,21 +388,23 @@ def _convolute_voigt_voigt( sample_component: Voigt, resolution_component: Voigt, ) -> np.ndarray: - """Convolution of two Voigt profiles results in another Voigt - profile. - - The Gaussian widths are summed in quadrature, - while the Lorentzian widths are summed. - The areas are multiplied. - - Args: - sample_component (Voigt): The sample Voigt component to be - convolved. - resolution_component (Voigt): The resolution Voigt component - to convolve with. - - Returns: - np.ndarray: The evaluated convolution values at self.energy. + """ + Convolution of two Voigt profiles results in another Voigt profile. + + The Gaussian widths are summed in quadrature, while the Lorentzian widths are summed. The + areas are multiplied. + + Parameters + ---------- + sample_component : Voigt + The sample Voigt component to be convolved. + resolution_component : Voigt + The resolution Voigt component to convolve with. + + Returns + ------- + np.ndarray + The evaluated convolution values at self.energy. """ area = sample_component.area.value * resolution_component.area.value @@ -404,28 +430,29 @@ def _gaussian_eval( center: float, width: float, ) -> np.ndarray: - r"""Evaluate a Gaussian function. + r""" + Evaluate a Gaussian function. - $$ - I(x) = \frac{A}{\sigma \sqrt{2\pi}} - \exp\left( - -\frac{1}{2} - \left(\frac{x - x_0}{\sigma}\right)^2 - \right) - $$ + $$ I(x) = \frac{A}{\sigma \sqrt{2\pi}} \exp\left( -\frac{1}{2} \left(\frac{x - + x_0}{\sigma}\right)^2 \right) $$ - where $A$ is the area, $x_0$ is the center, and $\sigma$ is the - width. + where $A$ is the area, $x_0$ is the center, and $\sigma$ is the width. All checks are handled in the calling function. - Args: - area (float): The area under the Gaussian curve. - center (float): The center of the Gaussian. - width (float): The width (sigma) of the Gaussian. - - Returns: - np.ndarray: The evaluated Gaussian values at self.energy. + Parameters + ---------- + area : float + The area under the Gaussian curve. + center : float + The center of the Gaussian. + width : float + The width (sigma) of the Gaussian. + + Returns + ------- + np.ndarray + The evaluated Gaussian values at self.energy. """ normalization = 1 / (np.sqrt(2 * np.pi) * width) @@ -437,22 +464,26 @@ def _lorentzian_eval(self, area: float, center: float, width: float) -> np.ndarr r""" Evaluate a Lorentzian function. - $$ - I(x) = \frac{A}{\\pi} \frac{\Gamma}{(x - x_0)^2 + \Gamma^2}, - $$ + $$ I(x) = \frac{A}{\\pi} \frac{\Gamma}{(x - x_0)^2 + \Gamma^2}, $$ - where $A$ is the area, $x_0$ is the center, and $\\Gamma$ is - the half width at half maximum (HWHM). + where $A$ is the area, $x_0$ is the center, and $\\Gamma$ is the half width at half maximum + (HWHM). All checks are handled in the calling function. - Args: - area (float): The area under the Lorentzian. - center (float): The center of the Lorentzian. - width (float): The width (HWHM) of the Lorentzian. - - Returns: - np.ndarray: The evaluated Lorentzian values at self.energy. + Parameters + ---------- + area : float + The area under the Lorentzian. + center : float + The center of the Lorentzian. + width : float + The width (HWHM) of the Lorentzian. + + Returns + ------- + np.ndarray + The evaluated Lorentzian values at self.energy. """ normalization = width / np.pi @@ -467,20 +498,24 @@ def _voigt_eval( gaussian_width: float, lorentzian_width: float, ) -> np.ndarray: - """Evaluate a Voigt profile function using scipy's - voigt_profile. - - Args: - area (float): The area under the Voigt profile. - center (float): The center of the Voigt profile. - gaussian_width (float): The Gaussian width (sigma) of the - Voigt profile. - lorentzian_width (float): The Lorentzian width (HWHM) of the - Voigt profile. - - Returns: - np.ndarray: The evaluated Voigt profile values at - self.energy. + """ + Evaluate a Voigt profile function using scipy's voigt_profile. + + Parameters + ---------- + area : float + The area under the Voigt profile. + center : float + The center of the Voigt profile. + gaussian_width : float + The Gaussian width (sigma) of the Voigt profile. + lorentzian_width : float + The Lorentzian width (HWHM) of the Voigt profile. + + Returns + ------- + np.ndarray + The evaluated Voigt profile values at self.energy. """ return area * voigt_profile( diff --git a/src/easydynamics/convolution/convolution.py b/src/easydynamics/convolution/convolution.py index 3f3f1ef3..32d3fb96 100644 --- a/src/easydynamics/convolution/convolution.py +++ b/src/easydynamics/convolution/convolution.py @@ -18,19 +18,17 @@ class Convolution(NumericalConvolutionBase): - """Convolution class that combines analytical and numerical - convolution methods to efficiently perform convolutions of - 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. - 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 - instabilities if peaks are very wide or very narrow. + """ + Convolution class that combines analytical and numerical convolution methods to efficiently + perform convolutions of 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. 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 instabilities if peaks are very wide or very + narrow. """ # When these attributes are changed, the convolution plan @@ -61,33 +59,31 @@ def __init__( energy_unit: str | sc.Unit = 'meV', normalize_detailed_balance: bool = True, ) -> None: - """Initialize the Convolution class. - - Args: - 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. - 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 (Numeric | None, default=0.2): The factor by which to - extend the input data range before convolution. Default - is 0.2. - temperature (Parameter | Numeric | None, default=None): The - temperature to use for detailed balance correction. - Default is None. - temperature_unit (str | sc.Unit, default='K'): The unit of the - temperature parameter. Default is 'K'. - energy_unit (str | sc.Unit, default='meV'): The unit of the energy. - Default is 'meV'. - normalize_detailed_balance (bool, default=True): Whether to - normalize the detailed balance correction. Default is - True. + """ + Initialize the Convolution class. + + Parameters + ---------- + 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. + 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 : Numeric | None, default=0.2 + The factor by which to extend the input data range before convolution. Default is 0.2. + 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. + 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. Default is True. """ self._convolution_plan_is_valid = False @@ -115,12 +111,14 @@ def __init__( def convolution( self, ) -> np.ndarray: - """Perform convolution using analytical convolutions where - possible, and numerical convolutions for the remaining - components. + """ + Perform convolution using analytical convolutions where possible, and numerical + convolutions for the remaining components. - Returns: - np.ndarray: The convolved values evaluated at energy. + Returns + ------- + np.ndarray + The convolved values evaluated at energy. """ if not self._convolution_plan_is_valid: self._build_convolution_plan() @@ -141,13 +139,14 @@ def convolution( return total def _convolve_delta_functions(self) -> np.ndarray: - """Convolve delta function components of the sample model with - the resolution components. No detailed balance correction is - applied to delta functions. + """ + Convolve delta function components of the sample model with the resolution components. No + detailed balance correction is applied to delta functions. - Returns: - np.ndarray: The convolved values of the delta function c - components evaluated at energy. + Returns + ------- + np.ndarray + The convolved values of the delta function c components evaluated at energy. """ return sum( delta.area.value @@ -162,22 +161,26 @@ def _check_if_pair_is_analytic( sample_component: ModelComponent, resolution_component: ModelComponent, ) -> bool: - """Check if the convolution of the given component pair can be - handled analytically. - - Args: - sample_component (ModelComponent): The sample component to - be convolved. - resolution_component (ModelComponent): The resolution - component to convolve with. - - 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. + """ + Check if the convolution of the given component pair can be handled analytically. + + Parameters + ---------- + sample_component : ModelComponent + The sample component to be convolved. + resolution_component : ModelComponent + The resolution component to convolve with. + + Raises + ------ + TypeError + If either component is not a ModelComponent, or if the resolution component is a + DeltaFunction. + + Returns + ------- + bool + True if the component pair can be handled analytically, False otherwise. """ if not isinstance(sample_component, ModelComponent): @@ -204,8 +207,8 @@ def _check_if_pair_is_analytic( ) def _build_convolution_plan(self) -> None: - """Separate sample model components into analytical pairs, delta - functions, and the rest. + """ + Separate sample model components into analytical pairs, delta functions, and the rest. """ analytical_sample_components = ComponentCollection() @@ -250,11 +253,11 @@ def _build_convolution_plan(self) -> None: self._set_convolvers() def _set_convolvers(self) -> None: - """Initialize analytical and numerical convolvers based on - sample model components. + """ + Initialize analytical and numerical convolvers based on sample model components. - There is no delta function convolver, as delta functions are - handled directly in the convolution method. + There is no delta function convolver, as delta functions are handled directly in the + convolution method. """ if self._analytical_sample_components.components: @@ -284,15 +287,19 @@ def _set_convolvers(self) -> None: # Update some setters so the internal sample models are updated 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. + """ + 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__. + + Parameters + ---------- + name : str + The name of the attribute to set. + value : any + The value to set the attribute to. """ super().__setattr__(name, value) diff --git a/src/easydynamics/convolution/convolution_base.py b/src/easydynamics/convolution/convolution_base.py index 3037c649..d328dbed 100644 --- a/src/easydynamics/convolution/convolution_base.py +++ b/src/easydynamics/convolution/convolution_base.py @@ -11,7 +11,8 @@ class ConvolutionBase: - """Base class for convolutions of sample and resolution models. + """ + Base class for convolutions of sample and resolution models. This base class has no convolution functionality. """ @@ -24,26 +25,29 @@ def __init__( energy_unit: str | sc.Unit = 'meV', energy_offset: Numeric | Parameter = 0.0, ) -> None: - """Initialize the ConvolutionBase. - - Args: - energy (np.ndarray | sc.Variable): 1D array of energy - values where the convolution is evaluated. - sample_components (ComponentCollection | ModelComponent | None, default=None): - The sample model to be convolved. - resolution_components (ComponentCollection | ModelComponent | None, default=None): - The resolution model to convolve with. - 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 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. + """ + Initialize the ConvolutionBase. + + Parameters + ---------- + energy : np.ndarray | sc.Variable + 1D array of energy values where the convolution is evaluated. + sample_components : ComponentCollection | ModelComponent | None, default=None + The sample model to be convolved. + resolution_components : ComponentCollection | ModelComponent | None, default=None + The resolution model to convolve with. + 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. + + Raises + ------ + TypeError + If energy is not a numpy ndarray or a scipp 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)]) @@ -91,23 +95,30 @@ def __init__( @property def energy_offset(self) -> Parameter: - """Get the energy offset. + """ + Get the energy offset. - Returns: - Parameter: The energy offset applied to the convolution. + Returns + ------- + Parameter + The energy offset applied to the convolution. """ return self._energy_offset @energy_offset.setter def energy_offset(self, energy_offset: Numeric | Parameter) -> None: - """Set the energy offset. + """ + Set the energy offset. - Args: - energy_offset (Numeric | Parameter): The energy offset to - apply to the convolution. + Parameters + ---------- + energy_offset : Numeric | Parameter + The energy offset to apply to the convolution. - Raises: - TypeError: If energy_offset is not a number or a Parameter. + 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.') @@ -120,10 +131,13 @@ def energy_offset(self, energy_offset: Numeric | Parameter) -> None: @property def energy_with_offset(self) -> sc.Variable: - """Get the energy with the offset applied. + """ + Get the energy with the offset applied. - Returns: - sc.Variable: The energy values with the offset applied. + Returns + ------- + sc.Variable + The energy values with the offset applied. """ energy_with_offset = self.energy.copy() energy_with_offset.values = self.energy.values - self.energy_offset.value @@ -131,15 +145,18 @@ def energy_with_offset(self) -> sc.Variable: @energy_with_offset.setter def energy_with_offset(self, _value: sc.Variable) -> None: - """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. - Args: - _value (sc.Variable): The value to set (ignored). + Parameters + ---------- + _value : sc.Variable + The value to set (ignored). - Raises: - AttributeError: Always raised since energy_with_offset is - read-only. + 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.' @@ -147,26 +164,31 @@ def energy_with_offset(self, _value: sc.Variable) -> None: @property def energy(self) -> sc.Variable: - """Get the energy. + """ + Get the energy. - Returns: - sc.Variable: The energy values where the convolution is - evaluated. + Returns + ------- + sc.Variable + The energy values where the convolution is evaluated. """ return self._energy @energy.setter def energy(self, energy: np.ndarray | sc.Variable) -> None: - """Set the energy. + """ + Set the energy. - Args: - energy (np.ndarray | sc.Variable): 1D array of energy - values where the convolution is evaluated. + Parameters + ---------- + energy : np.ndarray | sc.Variable + 1D array of energy values where the convolution is evaluated. - Raises: - TypeError: If energy is not a numpy ndarray or a - scipp Variable. + Raises + ------ + TypeError + If energy is not a numpy ndarray or a scipp Variable. """ if isinstance(energy, Numeric): @@ -184,30 +206,39 @@ def energy(self, energy: np.ndarray | sc.Variable) -> None: @property def energy_unit(self) -> str: - """Get the energy unit. + """ + Get the energy unit. - Returns: - str: The unit of the energy. + Returns + ------- + str + The unit of the energy. """ return self._energy_unit @energy_unit.setter def energy_unit(self, _unit_str: str) -> None: + """Energy unit.""" 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.' ) # noqa: E501 def convert_energy_unit(self, energy_unit: str | sc.Unit) -> None: - """Convert the energy and energy_offset to the specified unit. - - Args: - energy_unit (str | sc.Unit): The unit of the energy. - - Raises: - TypeError: If energy_unit is not a string or scipp unit. - Exception: If energy cannot be converted to the specified - unit. + """ + Convert the energy and energy_offset to the specified unit. + + Parameters + ---------- + energy_unit : str | sc.Unit + The unit of the energy. + + Raises + ------ + TypeError + If energy_unit is not a string or scipp unit. + 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.') @@ -230,25 +261,30 @@ def convert_energy_unit(self, energy_unit: str | sc.Unit) -> None: @property def sample_components(self) -> ComponentCollection | ModelComponent: - """Get the sample model. + """ + Get the sample model. - Returns: - ComponentCollection | ModelComponent: The sample model to - be convolved. + Returns + ------- + ComponentCollection | ModelComponent + The sample model to be convolved. """ return self._sample_components @sample_components.setter def sample_components(self, sample_components: ComponentCollection | ModelComponent) -> None: - """Set the sample model. + """ + Set the sample model. - Args: - sample_components (ComponentCollection | ModelComponent): - The sample model to be convolved. + Parameters + ---------- + sample_components : ComponentCollection | ModelComponent + The sample model to be convolved. - Raises: - TypeError: If sample_components is not a ComponentCollection - or ModelComponent. + Raises + ------ + TypeError + If sample_components is not a ComponentCollection or ModelComponent. """ if not isinstance(sample_components, (ComponentCollection, ModelComponent)): raise TypeError( @@ -261,11 +297,13 @@ def sample_components(self, sample_components: ComponentCollection | ModelCompon @property def resolution_components(self) -> ComponentCollection | ModelComponent: - """Get the resolution model. + """ + Get the resolution model. - Returns: - ComponentCollection | ModelComponent: The resolution model - to be convolved. + Returns + ------- + ComponentCollection | ModelComponent + The resolution model to be convolved. """ return self._resolution_components @@ -273,16 +311,19 @@ def resolution_components(self) -> ComponentCollection | ModelComponent: def resolution_components( self, resolution_components: ComponentCollection | ModelComponent ) -> None: - """Set the resolution model. - - Args: - 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 - ComponentCollection or ModelComponent. + """ + Set the resolution model. + + Parameters + ---------- + 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 ComponentCollection or ModelComponent. """ if not isinstance(resolution_components, (ComponentCollection, ModelComponent)): raise TypeError( diff --git a/src/easydynamics/convolution/energy_grid.py b/src/easydynamics/convolution/energy_grid.py index 645a605b..0722aa59 100644 --- a/src/easydynamics/convolution/energy_grid.py +++ b/src/easydynamics/convolution/energy_grid.py @@ -8,20 +8,21 @@ @dataclass(frozen=True) class EnergyGrid: - """Container for the dense energy grid and related metadata. + """ + Container for the dense energy grid and related metadata. - Attributes: - energy_dense (np.ndarray): The upsampled and extended energy - array. - energy_dense_centered (np.ndarray): The centered version of - energy_dense (used for resolution evaluation). - energy_dense_step (float): The spacing of energy_dense - (used for width checks and normalization). - energy_span_dense (float): The total span of energy_dense. - (used for width checks). - energy_even_length_offset (float): The offset to apply if - energy_dense has even length (used for convolution - alignment). + Attributes + ---------- + energy_dense : np.ndarray + The upsampled and extended energy array. + energy_dense_centered : np.ndarray + The centered version of energy_dense (used for resolution evaluation). + energy_dense_step : float + The spacing of energy_dense (used for width checks and normalization). + energy_span_dense : float + The total span of energy_dense. (used for width checks). + energy_even_length_offset : float + The offset to apply if energy_dense has even length (used for convolution alignment). """ energy_dense: np.ndarray diff --git a/src/easydynamics/convolution/numerical_convolution.py b/src/easydynamics/convolution/numerical_convolution.py index 10a55c81..87e6b49e 100644 --- a/src/easydynamics/convolution/numerical_convolution.py +++ b/src/easydynamics/convolution/numerical_convolution.py @@ -14,13 +14,12 @@ class NumericalConvolution(NumericalConvolutionBase): - """Numerical convolution of a ComponentCollection with a - ComponentCollection using FFT. + """ + 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. + 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__( @@ -36,31 +35,31 @@ def __init__( energy_unit: str | sc.Unit = 'meV', normalize_detailed_balance: bool = True, ) -> None: - """Initialize the NumericalConvolution object. - - 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, 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, 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, default=True): Whether to - normalize the detailed balance correction. Default is - True. + """ + Initialize the NumericalConvolution object. + + Parameters + ---------- + 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, 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. + 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. Default is True. """ super().__init__( energy=energy, @@ -78,12 +77,14 @@ def __init__( def convolution( self, ) -> np.ndarray: - """Calculate the convolution of the sample and resolution models - at the values given in energy. Includes detailed balance - correction if temperature is provided. + """ + Calculate the convolution of the sample and resolution models at the values given in + energy. Includes detailed balance correction if temperature is provided. - Returns: - np.ndarray: The convolved values evaluated at energy. + Returns + ------- + np.ndarray + The convolved values evaluated at energy. """ # Give warnings if peaks are very wide or very narrow diff --git a/src/easydynamics/convolution/numerical_convolution_base.py b/src/easydynamics/convolution/numerical_convolution_base.py index ccbdea57..e2d6f5b6 100644 --- a/src/easydynamics/convolution/numerical_convolution_base.py +++ b/src/easydynamics/convolution/numerical_convolution_base.py @@ -25,12 +25,11 @@ class NumericalConvolutionBase(ConvolutionBase): - """Base class for numerical convolutions of sample and resolution - models. + """ + 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 - functionality. + Provides methods to handle upsampling, extension, and detailed balance correction. This base + class has no convolution functionality. """ def __init__( @@ -46,34 +45,38 @@ def __init__( energy_unit: str | sc.Unit = 'meV', normalize_detailed_balance: bool = True, ) -> None: - """Initialize the NumericalConvolutionBase. - - 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. - 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. - 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, 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. + """ + Initialize the NumericalConvolutionBase. + + Parameters + ---------- + 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. + 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. + 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, 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, @@ -104,10 +107,13 @@ def __init__( @ConvolutionBase.energy.setter def energy(self, energy: np.ndarray) -> None: - """Set the energy array and recreate the dense grid. + """ + Set the energy array and recreate the dense grid. - Args: - energy (np.ndarray): The new energy array. + Parameters + ---------- + energy : np.ndarray + The new energy array. """ ConvolutionBase.energy.fset(self, energy) # Recreate dense grid when energy is updated @@ -115,24 +121,33 @@ def energy(self, energy: np.ndarray) -> None: @property def upsample_factor(self) -> Numeric | None: - """Get the upsample factor. + """ + Get the upsample factor. - Returns: - Numeric | None: The upsample factor. + Returns + ------- + Numeric | None + The upsample factor. """ return self._upsample_factor @upsample_factor.setter def upsample_factor(self, factor: Numeric | None) -> None: - """Set the upsample factor and recreate the dense grid. - - Args: - factor (Numeric | None): The new upsample factor. - - Raises: - TypeError: If factor is not a number or None. - ValueError: If factor is not greater than 1. + """ + Set the upsample factor and recreate the dense grid. + + Parameters + ---------- + factor : Numeric | None + The new upsample factor. + + Raises + ------ + TypeError + If factor is not a number or None. + ValueError + If factor is not greater than 1. """ if factor is None: self._upsample_factor = factor @@ -152,15 +167,16 @@ def upsample_factor(self, factor: Numeric | None) -> None: @property def extension_factor(self) -> float: - """Get the extension factor. + """ + Get the extension factor. - The extension factor determines how much the energy range is - extended on both sides before convolution. - 0.2 means extending by 20% of the original energy span - on each side + The extension factor determines how much the energy range is extended on both sides before + convolution. 0.2 means extending by 20% of the original energy span on each side - Returns: - float: The extension factor. + Returns + ------- + float + The extension factor. """ return self._extension_factor @@ -169,17 +185,21 @@ def extension_factor(self) -> float: 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 - extended on both sides before convolution. - 0.2 means extending by 20% of the original energy span - on each side. - Args: - factor (Numeric): The new extension factor. + The extension factor determines how much the energy range is extended on both sides before + convolution. 0.2 means extending by 20% of the original energy span on each side. - Raises: - TypeError: If factor is not a number. - ValueError: If factor is negative. + Parameters + ---------- + factor : Numeric + The new extension factor. + + Raises + ------ + TypeError + If factor is not a number. + ValueError + If factor is negative. """ if not isinstance(factor, Numeric): @@ -193,29 +213,34 @@ def extension_factor(self, factor: Numeric) -> None: @property def temperature(self) -> Parameter | None: - """Get the temperature. + """ + Get the temperature. - Returns: - Parameter | None: The temperature parameter, or None if - detailed balance correction is disabled. + Returns + ------- + Parameter | None + The temperature parameter, or None if detailed balance correction is disabled. """ return self._temperature @temperature.setter def temperature(self, temp: Parameter | Numeric | None) -> None: - """Set the temperature. + """ + Set the temperature. - If None, disables detailed balance - correction and removes the temperature parameter. + If None, disables detailed balance correction and removes the temperature parameter. - Args: - temp (Parameter | Numeric | None): The temperature to set. - The unit will be the same as the existing temperature - parameter if it exists, otherwise 'K'. + Parameters + ---------- + 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 Numeric, Parameter, or None. + Raises + ------ + TypeError + If temp is not a Numeric, Parameter, or None. """ if temp is None: @@ -237,28 +262,35 @@ def temperature(self, temp: Parameter | Numeric | None) -> None: @property def normalize_detailed_balance(self) -> bool: - """Get whether to normalize the detailed balance factor. + """ + Get whether to normalize the detailed balance factor. If True, the detailed balance factor is divided by temperature. - Returns: - bool: Whether to normalize the detailed balance factor. + Returns + ------- + bool + Whether to normalize the detailed balance factor. """ return self._normalize_detailed_balance @normalize_detailed_balance.setter def normalize_detailed_balance(self, normalize: bool) -> None: - """Set whether to normalize the detailed balance factor. + """ + Set whether to normalize the detailed balance factor. If True, the detailed balance factor is divided by temperature. - Args: - normalize (bool): Whether to normalize the detailed balance - factor. + Parameters + ---------- + normalize : bool + Whether to normalize the detailed balance factor. - Raises: - TypeError: If normalize is not a bool. + Raises + ------ + TypeError + If normalize is not a bool. """ if not isinstance(normalize, bool): @@ -269,20 +301,22 @@ def normalize_detailed_balance(self, normalize: bool) -> None: def _create_energy_grid( self, ) -> EnergyGrid: - """Create a dense grid by upsampling and extending the energy - array. + """ + Create a dense grid by upsampling and extending the energy array. - If upsample_factor is None, no upsampling or extension is - performed. - This dense grid is used for convolution to improve accuracy. + If upsample_factor is None, no upsampling or extension is performed. This dense grid is + used for convolution to improve accuracy. - 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. - Raises: - ValueError: If energy array is not uniformly spaced when - upsample_factor is None, or if energy array has less than 2 points. + Returns + ------- + EnergyGrid + The dense grid created by upsampling and extending energy. """ if self.upsample_factor is None: # Check if the array is uniformly spaced. @@ -345,18 +379,19 @@ def _check_width_thresholds( model: ComponentCollection | ModelComponent, model_name: str, ) -> None: - """Helper function to check and warn if components are wide - compared to the span of the data, or narrow compared to the - spacing. + """ + Helper function to check and warn if components are wide compared to the span of the data, + or narrow compared to the spacing. In both cases, the convolution accuracy may be compromised. - Args: - model (ComponentCollection | ModelComponent): The model to - check - model_name (str): A string indicating whether the model is a - 'sample model' or 'resolution model' for warning - messages. + Parameters + ---------- + model : ComponentCollection | ModelComponent + The model to check. + model_name : str + A string indicating whether the model is a 'sample model' or 'resolution model' for + warning messages. """ # Handle ComponentCollection or ModelComponent @@ -386,12 +421,13 @@ def _check_width_thresholds( ) def __repr__(self) -> str: - """Return a string representation of the - NumericalConvolutionBase. + """ + Return a string representation of the NumericalConvolutionBase. - Returns: - str: A string representation of the - NumericalConvolutionBase. + Returns + ------- + str + A string representation of the NumericalConvolutionBase. """ return ( f'{self.__class__.__name__}(' diff --git a/src/easydynamics/experiment/experiment.py b/src/easydynamics/experiment/experiment.py index ad829e37..383f6c06 100644 --- a/src/easydynamics/experiment/experiment.py +++ b/src/easydynamics/experiment/experiment.py @@ -15,11 +15,10 @@ class Experiment(NewBase): - """Holds data from an experiment as a sc.DataArray along with - metadata. + """ + Holds data from an experiment as a sc.DataArray along with metadata. - This is a minimal implementation that will be extended in the - future. + This is a minimal implementation that will be extended in the future. """ def __init__( @@ -28,18 +27,23 @@ def __init__( unique_name: str | None = None, data: sc.DataArray | str | None = None, ) -> None: - """Initialize the Experiment object. - - Args: - 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, 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. + """ + Initialize the Experiment object. + + Parameters + ---------- + 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. None. + 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. """ super().__init__( display_name=display_name, @@ -68,24 +72,30 @@ def __init__( @property def data(self) -> sc.DataArray | None: - """Get the dataset associated with this experiment. + """ + Get the dataset associated with this experiment. - Returns: - sc.DataArray | None: The dataset associated with this - experiment, or None if no data is loaded. + Returns + ------- + sc.DataArray | None + The dataset associated with this experiment, or None if no data is loaded. """ return self._data @data.setter def data(self, value: sc.DataArray) -> None: - """Set the dataset associated with this experiment. + """ + Set the dataset associated with this experiment. - Args: - value (sc.DataArray): The new dataset to associate with this - experiment. + Parameters + ---------- + value : sc.DataArray + The new dataset to associate with this experiment. - Raises: - TypeError: If the value is not a sc.DataArray. + Raises + ------ + 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__}') @@ -97,35 +107,44 @@ def data(self, value: sc.DataArray) -> None: @property def binned_data(self) -> sc.DataArray | None: - """Get the binned dataset associated with this experiment. + """ + Get the binned dataset associated with this experiment. - Returns: - sc.DataArray | None: The binned dataset associated with this - experiment, or None if no data is loaded. + Returns + ------- + sc.DataArray | None + The binned dataset associated with this experiment, or None if no data is loaded. """ return self._binned_data @binned_data.setter def binned_data(self, _value: sc.DataArray) -> None: - """Set the binned dataset associated with this experiment. Read- - only property. Use rebin() to rebin the data instead. + """ + Set the binned dataset associated with this experiment. + + Read- only property. Use rebin() to rebin the data instead. - Args: - _value (sc.DataArray): The new binned dataset to associate - with this experiment (ignored) + Parameters + ---------- + _value : sc.DataArray + The new binned dataset to associate with this experiment (ignored). - Raises: - AttributeError: Always, since binned_data is read-only. + Raises + ------ + AttributeError + Always, since binned_data is read-only. """ raise AttributeError('binned_data is a read-only property. Use rebin() to rebin the data') @property def Q(self) -> sc.Variable | None: - """Get the Q values from the dataset. + """ + Get the Q values from the dataset. - Returns: - sc.Variable | None: The Q values from the dataset, or None - if no data is loaded. + Returns + ------- + sc.Variable | None + The Q values from the dataset, or None if no data is loaded. """ if self._binned_data is None: return None @@ -133,24 +152,32 @@ def Q(self) -> sc.Variable | None: @Q.setter def Q(self, _value: sc.Variable) -> None: - """Set the Q values for the dataset. Q is a read-only property - derived from the data, so this setter raises an error. + """ + Set the Q values for the dataset. - Args: - _value (sc.Variable): The new Q values to set (ignored) + Q is a read-only property derived from the data, so this setter raises an error. - Raises: - AttributeError: Always, since Q is read-only. + Parameters + ---------- + _value : sc.Variable + The new Q values to set (ignored). + + Raises + ------ + AttributeError + Always, since Q is read-only. """ raise AttributeError('Q is a read-only property derived from the data.') @property def energy(self) -> sc.Variable | None: - """Get the energy values from the dataset. + """ + Get the energy values from the dataset. - Returns: - sc.Variable | None: The energy values from the dataset, or - None if no data is loaded. + Returns + ------- + sc.Variable | None + The energy values from the dataset, or None if no data is loaded. """ if self._binned_data is None: return None @@ -158,31 +185,42 @@ def energy(self) -> sc.Variable | None: @energy.setter def energy(self, _value: sc.Variable) -> None: - """Set the energy values for the dataset. Energy is a read-only - property derived from the data, so this setter raises an error. + """ + Set the energy values for the dataset. - Args: - _value (sc.Variable): The new energy values to set (ignored) + Energy is a read-only property derived from the data, so this setter raises an error. - Raises: - AttributeError: Always, since energy is read-only. + Parameters + ---------- + _value : sc.Variable + The new energy values to set (ignored). + + Raises + ------ + AttributeError + Always, since energy is read-only. """ 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 - the y values or variances are NaN or Inf for the given Q index. - - Args: - Q_index (int): The Q index to get the masked energy values - for. - - 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. + """ + Get the energy values from the dataset, removing points where the y values or variances are + NaN or Inf for the given Q index. + + Parameters + ---------- + Q_index : int + The Q index to get the masked energy values for. + + Raises + ------ + IndexError + If Q_index is not a valid index for the Q values. + + Returns + ------- + sc.Variable | None + The masked energy values from the dataset, or None if no data is loaded. """ if self._binned_data is None: return None @@ -205,16 +243,21 @@ def get_masked_energy(self, Q_index: int) -> sc.Variable | None: ########### def load_hdf5(self, filename: str, display_name: str | None = None) -> None: - """Load data from an HDF5 file. - - Args: - filename (str ): Path to the HDF5 file. - 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 or if the loaded data is not a sc.DataArray. + """ + Load data from an HDF5 file. + + Parameters + ---------- + filename : str + Path to the HDF5 file. + 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 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__}') @@ -235,16 +278,21 @@ def load_hdf5(self, filename: str, display_name: str | None = None) -> None: self.data = loaded_data def save_hdf5(self, filename: str | None = None) -> None: - """Save the dataset to HDF5. - - Args: - 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. - - Raises: - TypeError: If filename is not a string or None. - ValueError: If there is no data to save. + """ + Save the dataset to HDF5. + + Parameters + ---------- + 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. + + Raises + ------ + TypeError + If filename is not a string or None. + ValueError + If there is no data to save. """ if filename is None: @@ -266,18 +314,23 @@ def remove_data(self) -> None: self._binned_data = None def rebin(self, dimensions: dict[str, int | sc.Variable]) -> None: - """Rebin the dataset along specified dimensions. - - Args: - dimensions (dict[str, int | sc.Variable]): A dictionary - mapping dimension names to number of bins (int) or bin - edges (sc.Variable). - - 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. + """ + Rebin the dataset along specified dimensions. + + Parameters + ---------- + dimensions : dict[str, int | sc.Variable] + A dictionary mapping dimension names to number of bins (int) or bin edges + (sc.Variable). + + 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): @@ -320,15 +373,22 @@ def rebin(self, dimensions: dict[str, int | sc.Variable]) -> None: ########### def plot_data(self, slicer: bool = False, **kwargs: dict) -> None: - """Plot the dataset using plopp: https://scipp.github.io/plopp/ - - Args: - 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. - RuntimeError: If not in a Jupyter notebook environment. + """ + Plot the dataset using plopp: https://scipp.github.io/plopp/. + + Parameters + ---------- + 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. + RuntimeError + If not in a Jupyter notebook environment. """ if self._binned_data is None: @@ -367,14 +427,20 @@ def plot_data(self, slicer: bool = False, **kwargs: dict) -> None: @staticmethod def _validate_coordinates(data: sc.DataArray) -> None: - """Validate that required coordinates are present in the data. - - Args: - data (sc.DataArray): The data to validate. - - Raises: - TypeError: If data is not a sc.DataArray. - ValueError: If required coordinates are missing. + """ + Validate that required coordinates are present in the data. + + Parameters + ---------- + 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.') @@ -385,13 +451,18 @@ def _validate_coordinates(data: sc.DataArray) -> None: raise ValueError(f"Data is missing required coordinate: '{coord}'") def _convert_to_bin_centers(self, data: sc.DataArray) -> sc.DataArray: - """Convert the coordinates of the data to bin centers. + """ + Convert the coordinates of the data to bin centers. - Args: - data (sc.DataArray): The data to convert. + Parameters + ---------- + data : sc.DataArray + The data to convert. - Returns: - sc.DataArray: The data with coordinates at bin centers. + Returns + ------- + sc.DataArray + The data with coordinates at bin centers. """ for dim in data.dims: coord = data.coords[dim] @@ -401,16 +472,18 @@ def _convert_to_bin_centers(self, data: sc.DataArray) -> sc.DataArray: return data 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. + """ + Extract the x, y, and weights arrays from the experiment for the given Q index. - Args: - Q_index (int): The Q index to extract the data for. + Parameters + ---------- + Q_index : int + The Q index to extract the data for. - Returns: - tuple[np.ndarray, np.ndarray, np.ndarray]: The x, y, and - variances arrays extracted from the experiment for the - given Q index. + Returns + ------- + tuple[np.ndarray, np.ndarray, np.ndarray] + The x, y, and variances arrays extracted from the experiment for the given Q index. """ data = self.binned_data['Q', Q_index] x = data.coords['energy'].values @@ -421,21 +494,26 @@ def _extract_x_y_var(self, Q_index: int) -> tuple[np.ndarray, np.ndarray, np.nda def _extract_x_y_weights_only_finite( self, Q_index: int ) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: - """Extract the x, y, and weights arrays from the experiment for - the given Q index, removing any NaN and Inf values. - - Args: - Q_index (int): The Q index to extract the data for. - - Returns: - tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: The - x, y, weights, and mask arrays extracted from the - experiment for the given Q index, with NaNs and Infs - removed. - - Raises: - ValueError: If any variances are zero after removing NaNs - and Infs, since this would lead to infinite weights. + """ + Extract the x, y, and weights arrays from the experiment for the given Q index, removing + any NaN and Inf values. + + Parameters + ---------- + Q_index : int + The Q index to extract the data for. + + Raises + ------ + ValueError + If any variances are zero after removing NaNs and Infs, since this would lead to + infinite weights. + + Returns + ------- + tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray] + The x, y, weights, and mask arrays extracted from the experiment for the given Q index, + with NaNs and Infs removed. """ x, y, var = self._extract_x_y_var(Q_index) @@ -463,19 +541,25 @@ def _extract_x_y_weights_only_finite( ########### def __repr__(self) -> str: - """Return a string representation of the Experiment object. + """ + Return a string representation of the Experiment object. - Returns: - str: A string representation of the Experiment object. + Returns + ------- + str + A string representation of the Experiment object. """ return f'Experiment `{self.unique_name}` with data: {self._data}' def __copy__(self) -> 'Experiment': - """Return a copy of the object. + """ + Return a copy of the object. - Returns: - Experiment: A copy of the Experiment object. + Returns + ------- + 'Experiment' + A copy of the Experiment object. """ temp = self.to_dict(skip=['unique_name']) new_obj = self.__class__.from_dict(temp) diff --git a/src/easydynamics/sample_model/background_model.py b/src/easydynamics/sample_model/background_model.py index 2f4645f5..81f29b81 100644 --- a/src/easydynamics/sample_model/background_model.py +++ b/src/easydynamics/sample_model/background_model.py @@ -11,8 +11,8 @@ class BackgroundModel(ModelBase): - """BackgroundModel represents a model of the background in an - experiment at various Q. + """ + BackgroundModel represents a model of the background in an experiment at various Q. """ def __init__( @@ -23,19 +23,22 @@ def __init__( components: ModelComponent | ComponentCollection | None = None, Q: Q_type | None = None, ) -> None: - """Initialize the BackgroundModel. + """ + Initialize the BackgroundModel. - Args: - 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, 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, default=None): Q values for the model. If None, Q is not - set. + Parameters + ---------- + 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, 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, default=None + Q values for the model. If None, Q is not set. """ super().__init__( display_name=display_name, diff --git a/src/easydynamics/sample_model/component_collection.py b/src/easydynamics/sample_model/component_collection.py index 5f483091..a805f58f 100644 --- a/src/easydynamics/sample_model/component_collection.py +++ b/src/easydynamics/sample_model/component_collection.py @@ -17,8 +17,8 @@ class ComponentCollection(ModelBase): - """Collection of model components representing a sample, background - or resolution model. + """ + Collection of model components representing a sample, background or resolution model. """ def __init__( @@ -28,20 +28,24 @@ def __init__( unique_name: str | None = None, components: list[ModelComponent] | None = None, ) -> None: - """Initialize a new ComponentCollection. - - Args: - 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. + """ + Initialize a new ComponentCollection. + + Parameters + ---------- + 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. + 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. """ super().__init__(display_name=display_name, unique_name=unique_name) @@ -66,24 +70,31 @@ def __init__( @property def components(self) -> list[ModelComponent]: - """Get the list of components in the collection. + """ + Get the list of components in the collection. - Returns: - list[ModelComponent]: The 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: - """Set the list of components in the collection. + """ + Set the list of components in the collection. - Args: - components (list[ModelComponent]): The new list of - components. + Parameters + ---------- + components : list[ModelComponent] + The new list of components. - Raises: - TypeError: If components is not a list of ModelComponent. + Raises + ------ + TypeError + If components is not a list of ModelComponent. """ if not isinstance(components, list): @@ -99,24 +110,30 @@ def components(self, components: list[ModelComponent]) -> None: @property def is_empty(self) -> bool: - """Check if the ComponentCollection has no components. + """ + Check if the ComponentCollection has no components. - Returns: - bool: True if the collection has no components, - False otherwise. + Returns + ------- + bool + True if the collection has no components, False otherwise. """ return not self._components @is_empty.setter def is_empty(self, _value: bool) -> None: - """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. - Args: - _value (bool): The value to set (ignored). + Parameters + ---------- + _value : bool + The value to set (ignored). - Raises: - AttributeError: Always raised since is_empty is read-only. + Raises + ------ + AttributeError + Always raised since is_empty is read-only. """ raise AttributeError( 'is_empty is a read-only property that indicates ' @@ -125,23 +142,30 @@ def is_empty(self, _value: bool) -> None: @property def unit(self) -> str | sc.Unit | None: - """Get the unit of the ComponentCollection. + """ + Get the unit of the ComponentCollection. - Returns: - str | sc.Unit | None: The unit of the ComponentCollection, - which is the same as the unit of its components. + Returns + ------- + str | sc.Unit | None + The unit of the ComponentCollection, which is the same as the unit of its components. """ return self._unit @unit.setter def unit(self, _unit_str: str) -> None: - """Unit is read-only and cannot be set directly. + """ + Unit is read-only and cannot be set directly. - Args: - _unit_str (str): The unit to set (ignored). + Parameters + ---------- + _unit_str : str + The unit to set (ignored). - Raises: - AttributeError: Always raised since unit is read-only. + Raises + ------ + AttributeError + Always raised since unit is read-only. """ raise AttributeError( @@ -150,16 +174,20 @@ def unit(self, _unit_str: str) -> None: ) # noqa: E501 def convert_unit(self, unit: str | sc.Unit) -> None: - """Convert the unit of the ComponentCollection and all its - components. - - Args: - unit (str | sc.Unit): The target unit to convert to. - - Raises: - TypeError: If unit is not a string or sc.Unit. - Exception: If any component cannot be converted to the - specified unit. + """ + Convert the unit of the ComponentCollection and all its components. + + Parameters + ---------- + unit : str | sc.Unit + The target unit to convert to. + + Raises + ------ + TypeError + If unit is not a string or sc.Unit. + Exception + If any component cannot be converted to the specified unit. """ if not isinstance(unit, (str, sc.Unit)): @@ -185,19 +213,22 @@ def convert_unit(self, unit: str | sc.Unit) -> 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. - - Raises: - TypeError: If component is not a ModelComponent or - ComponentCollection. - ValueError: If a component with the same unique name already - exists in the collection. + """ + Append a model component or the components from another ComponentCollection to this + ComponentCollection. + + Parameters + ---------- + 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 ComponentCollection. + ValueError + If a component with the same unique name already exists in the collection. """ if not isinstance(component, (ModelComponent, ComponentCollection)): raise TypeError( @@ -219,15 +250,20 @@ def append_component(self, component: ModelComponent | ComponentCollection) -> N self._components.append(comp) def remove_component(self, unique_name: str) -> None: - """Remove a component from the collection by its unique name. - - 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 - in the collection. + """ + Remove a component from the collection by its unique name. + + Parameters + ---------- + 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 in the collection. """ if not isinstance(unique_name, str): @@ -246,22 +282,30 @@ def remove_component(self, unique_name: str) -> None: @property def components(self) -> list[ModelComponent]: - """Get the list of components in the collection. + """ + Get the list of components in the collection. - Returns: - list[ModelComponent]: The 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: - """Set the components in the collection. + """ + Set the components in the collection. - Args: - components (list[ModelComponent]): The new components in the collection + Parameters + ---------- + components : list[ModelComponent] + The new components in the collection. - Raises: - TypeError: If components is not a list of ModelComponent + 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.') @@ -276,23 +320,30 @@ def components(self, components: list[ModelComponent]) -> None: @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 + 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. + """ + Is_empty is read-only. - Args: - _value (bool): ignored. + Parameters + ---------- + _value : bool + Ignored. - Raises: - AttributeError: Always raised since is_empty is read-only + Raises + ------ + AttributeError + Always raised since is_empty is read-only. """ raise AttributeError( 'is_empty is a read-only property that indicates ' @@ -300,11 +351,13 @@ def is_empty(self, _value: bool) -> None: ) def list_component_names(self) -> list[str]: - """List the names of all components in the model. + """ + List the names of all components in the model. - Returns: - list[str]: List of unique names of the components in the - collection. + Returns + ------- + list[str] + List of unique names of the components in the collection. """ return [component.unique_name for component in self._components] @@ -314,13 +367,16 @@ def clear_components(self) -> None: self._components.clear() def normalize_area(self) -> None: - """Normalize the areas of all components so they sum to 1. This - is useful for convolutions. + """ + Normalize the areas of all components so they sum to 1. - Raises: - ValueError: If there are no components in the model or - if the total area is zero or not finite, which - would prevent normalization. + This is useful for convolutions. + + Raises + ------ + 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.') @@ -354,23 +410,30 @@ def normalize_area(self) -> None: # ------------------------------------------------------------------ def get_all_variables(self) -> list[DescriptorBase]: - """Get all parameters from the model component. + """ + Get all parameters from the model component. - Returns: - list[DescriptorBase]: List of parameters in the component. + Returns + ------- + list[DescriptorBase] + List of parameters in the component. """ 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: - """Evaluate the sum of all components. + """ + Evaluate the sum of all components. - Args: - x (Numeric | list | np.ndarray | sc.Variable | sc.DataArray): - Energy axis. + Parameters + ---------- + x : Numeric | list | np.ndarray | sc.Variable | sc.DataArray + Energy axis. - Returns: - np.ndarray: Evaluated model values. + Returns + ------- + np.ndarray + Evaluated model values. """ if not self.components: @@ -382,20 +445,29 @@ def evaluate_component( x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray, unique_name: str, ) -> np.ndarray: - """Evaluate a single component by name. - - Args: - x (Numeric | list | np.ndarray | sc.Variable | sc.DataArray): - Energy axis. - unique_name (str): Component unique name. - - Returns: - np.ndarray: Evaluated values for the specified component. - Raises: - ValueError: If there are no components in the model. - TypeError: If unique_name is not a string. - KeyError: If no component with the given unique name exists - in the collection. + """ + Evaluate a single component by name. + + Parameters + ---------- + x : Numeric | list | np.ndarray | sc.Variable | sc.DataArray + Energy axis. + unique_name : str + Component unique name. + + Raises + ------ + ValueError + If there are no components in the model. + TypeError + If unique_name is not a string. + KeyError + If no component with the given unique name exists in the collection. + + Returns + ------- + np.ndarray + Evaluated values for the specified component. """ if not self.components: raise ValueError('No components in the model to evaluate.') @@ -428,15 +500,18 @@ def free_all_parameters(self) -> None: # ------------------------------------------------------------------ def __contains__(self, item: str | ModelComponent) -> bool: - """Check if a component with the given name or instance exists - in the ComponentCollection. + """ + Check if a component with the given name or instance exists in the ComponentCollection. - Args: - item (str | ModelComponent): The component name or instance - to check for. + Parameters + ---------- + item : str | ModelComponent + The component name or instance to check for. - Returns: - bool: True if the component exists, False otherwise. + Returns + ------- + bool + True if the component exists, False otherwise. """ if isinstance(item, str): @@ -448,10 +523,13 @@ def __contains__(self, item: str | ModelComponent) -> bool: return False def __repr__(self) -> str: - """Return a string representation of the ComponentCollection. + """ + Return a string representation of the ComponentCollection. - Returns: - str: String representation of the ComponentCollection. + Returns + ------- + str + String representation of the ComponentCollection. """ comp_names = ', '.join(c.unique_name for c in self.components) or 'No components' diff --git a/src/easydynamics/sample_model/components/damped_harmonic_oscillator.py b/src/easydynamics/sample_model/components/damped_harmonic_oscillator.py index 81a766f7..3bb71a72 100644 --- a/src/easydynamics/sample_model/components/damped_harmonic_oscillator.py +++ b/src/easydynamics/sample_model/components/damped_harmonic_oscillator.py @@ -14,15 +14,11 @@ class DampedHarmonicOscillator(CreateParametersMixin, ModelComponent): - r"""Model of a Damped Harmonic Oscillator (DHO). - - The intensity is given by - $$ - I(x) = \frac{2 A x_0^2 \gamma}{\pi \left( (x^2 - x_0^2)^2 + - (2 \gamma x)^2 \right)}, - $$ - where $A$ is the area, $x_0$ is the center, and $\gamma$ is the - width. + r""" + Model of a Damped Harmonic Oscillator (DHO). + + The intensity is given by $$ I(x) = \frac{2 A x_0^2 \gamma}{\pi \left( (x^2 - x_0^2)^2 + (2 + \gamma x)^2 \right)}, $$ where $A$ is the area, $x_0$ is the center, and $\gamma$ is the width. """ def __init__( @@ -34,19 +30,25 @@ def __init__( display_name: str | None = 'DampedHarmonicOscillator', unique_name: str | None = None, ) -> None: - """Initialize the Damped Harmonic Oscillator. - - Args: - area (Numeric | Parameter, default=1.0): Area under the curve. - center (Numeric | Parameter, default=1.0): Resonance frequency, approximately the - peak position. - width (Numeric | Parameter, default=1.0): Damping constant, approximately the - half width at half max (HWHM) of the peaks. - 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. + """ + Initialize the Damped Harmonic Oscillator. + + Parameters + ---------- + area : Numeric | Parameter, default=1.0 + Area under the curve. + center : Numeric | Parameter, default=1.0 + Resonance frequency, approximately the peak position. + width : Numeric | Parameter, default=1.0 + Damping constant, approximately the half width at half max (HWHM) of the peaks. By + default, 1.0. + 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. By + default, None. """ super().__init__( @@ -73,22 +75,30 @@ def __init__( @property def area(self) -> Parameter: - """Get the area parameter. + """ + Get the area parameter. - Returns: - Parameter: The area parameter. + Returns + ------- + Parameter + The area parameter. """ return self._area @area.setter def area(self, value: Numeric) -> None: - """Set the value of the area parameter. + """ + Set the value of the area parameter. - Args: - value (Numeric): The new value for the area parameter. + Parameters + ---------- + value : Numeric + The new value for the area parameter. - Raises: - TypeError: If the value is not a number. + Raises + ------ + TypeError + If the value is not a number. """ if not isinstance(value, Numeric): raise TypeError('area must be a number') @@ -96,23 +106,32 @@ def area(self, value: Numeric) -> None: @property def center(self) -> Parameter: - """Get the center parameter. + """ + Get the center parameter. - Returns: - Parameter: The center parameter. + Returns + ------- + Parameter + The center parameter. """ return self._center @center.setter def center(self, value: Numeric) -> None: - """Set the value of the center parameter. - - Args: - value (Numeric): The new value for the center parameter. - - Raises: - TypeError: If the value is not a number. - ValueError: If the value is not positive. + """ + Set the value of the center parameter. + + Parameters + ---------- + value : Numeric + The new value for the center parameter. + + Raises + ------ + TypeError + If the value is not a number. + ValueError + If the value is not positive. """ if not isinstance(value, Numeric): raise TypeError('center must be a number') @@ -123,23 +142,32 @@ def center(self, value: Numeric) -> None: @property def width(self) -> Parameter: - """Get the width parameter. + """ + Get the width parameter. - Returns: - Parameter: The width parameter. + Returns + ------- + Parameter + The width parameter. """ return self._width @width.setter def width(self, value: Numeric) -> None: - """Set the value of the width parameter. - - Args: - value (Numeric): The new value for the width parameter. - - Raises: - TypeError: If the value is not a number. - ValueError: If the value is not positive. + """ + Set the value of the width parameter. + + Parameters + ---------- + value : Numeric + The new value for the width parameter. + + Raises + ------ + 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') @@ -150,24 +178,22 @@ def width(self, value: Numeric) -> None: self._width.value = value 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. - - If x is a scipp Variable, the unit of the DHO will be converted - to match x. The intensity is given by - $$ - I(x) = \frac{2 A x_0^2 \gamma}{\pi \left( (x^2 - x_0^2)^2 + - (2 \gamma x)^2 \right)}, - $$ - where $A$ is the area, $x_0$ is the center, and $\gamma$ is the - width. - - Args: - x (Numeric | list | np.ndarray | sc.Variable | sc.DataArray): - The x values at which to evaluate the DHO. - - Returns: - np.ndarray: The intensity of the DHO at the given x values. + r""" + Evaluate the Damped Harmonic Oscillator at the given x values. + + If x is a scipp Variable, the unit of the DHO will be converted to match x. The intensity + is given by $$ I(x) = \frac{2 A x_0^2 \gamma}{\pi \left( (x^2 - x_0^2)^2 + (2 \gamma x)^2 + \right)}, $$ where $A$ is the area, $x_0$ is the center, and $\gamma$ is the width. + + Parameters + ---------- + x : Numeric | list | np.ndarray | sc.Variable | sc.DataArray + The x values at which to evaluate the DHO. + + Returns + ------- + np.ndarray + The intensity of the DHO at the given x values. """ x = self._prepare_x_for_evaluate(x) @@ -179,12 +205,13 @@ def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) return self.area.value * normalization / (denominator) def __repr__(self) -> str: - """Return a string representation of the Damped Harmonic - Oscillator. + """ + Return a string representation of the Damped Harmonic Oscillator. - Returns: - str: A string representation of the Damped Harmonic - Oscillator. + Returns + ------- + str + A string representation of the Damped Harmonic Oscillator. """ return ( f'DampedHarmonicOscillator(display_name = {self.display_name}, unit = {self._unit},\n \ diff --git a/src/easydynamics/sample_model/components/delta_function.py b/src/easydynamics/sample_model/components/delta_function.py index 820a039b..178b4489 100644 --- a/src/easydynamics/sample_model/components/delta_function.py +++ b/src/easydynamics/sample_model/components/delta_function.py @@ -16,12 +16,12 @@ class DeltaFunction(CreateParametersMixin, ModelComponent): - """Delta function. + """ + Delta function. - Evaluates to zero everywhere, except in convolutions, where it acts - 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. + Evaluates to zero everywhere, except in convolutions, where it acts 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. """ def __init__( @@ -32,16 +32,22 @@ def __init__( display_name: str | None = 'DeltaFunction', unique_name: str | None = None, ) -> None: - """Initialize the Delta function. - - Args: - center (Numeric | Parameter | None, default=None): Center of the delta function. - If None, defaults to 0 and is fixed. - 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. + """ + Initialize the Delta function. + + Parameters + ---------- + center : Numeric | Parameter | None, default=None + Center of the delta function. If None. + 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. By + default, None. """ # Validate inputs and create Parameters if not given super().__init__( @@ -61,23 +67,31 @@ def __init__( @property def area(self) -> Parameter: - """Get the area parameter. + """ + Get the area parameter. - Returns: - Parameter: The area parameter. + Returns + ------- + Parameter + The area parameter. """ return self._area @area.setter def area(self, value: Numeric) -> None: - """Set the value of the area parameter. + """ + Set the value of the area parameter. - Args: - value (Numeric): The new value for the area parameter. + Parameters + ---------- + value : Numeric + The new value for the area parameter. - Raises: - TypeError: If the value is not a number. + Raises + ------ + TypeError + If the value is not a number. """ if not isinstance(value, Numeric): @@ -86,24 +100,31 @@ def area(self, value: Numeric) -> None: @property def center(self) -> Parameter: - """Get the center parameter. + """ + Get the center parameter. - Returns: - Parameter: The center parameter. + Returns + ------- + Parameter + The center parameter. """ return self._center @center.setter def center(self, value: Numeric | None) -> None: - """Set the center parameter value. + """ + Set the center parameter value. - Args: - value (Numeric | None): The new value for the center - parameter. If None, defaults to 0 and is fixed. + Parameters + ---------- + value : Numeric | None + The new value for the center parameter. If None, defaults to 0 and is fixed. - Raises: - TypeError: If the value is not a number or None. + Raises + ------ + TypeError + If the value is not a number or None. """ if value is None: @@ -114,19 +135,21 @@ def center(self, value: Numeric | None) -> None: self._center.value = value def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray: - """Evaluate the Delta function at the given x values. + """ + Evaluate the Delta function at the given x values. - The Delta function evaluates to zero everywhere, except at the - center. Its numerical integral is equal to the area. It acts as - an identity in convolutions. + The Delta function evaluates to zero everywhere, except at the center. Its numerical + integral is equal to the area. It acts as an identity in convolutions. - Args: - x (Numeric | list | np.ndarray | sc.Variable | sc.DataArray): - The x values at which to evaluate the Delta function. + Parameters + ---------- + x : Numeric | list | np.ndarray | sc.Variable | sc.DataArray + The x values at which to evaluate the Delta function. - Returns: - np.ndarray: The evaluated Delta function at the given x - values. + Returns + ------- + np.ndarray + The evaluated Delta function at the given x values. """ # x assumed sorted, 1D numpy array @@ -159,10 +182,13 @@ def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) return model def __repr__(self) -> str: - """Return a string representation of the Delta function. + """ + Return a string representation of the Delta function. - Returns: - str: A string representation of the Delta function. + Returns + ------- + str + A string representation of the Delta function. """ return f'DeltaFunction(unique_name = {self.unique_name}, unit = {self._unit},\n \ diff --git a/src/easydynamics/sample_model/components/exponential.py b/src/easydynamics/sample_model/components/exponential.py index 56a72912..7c5e36cb 100644 --- a/src/easydynamics/sample_model/components/exponential.py +++ b/src/easydynamics/sample_model/components/exponential.py @@ -14,14 +14,14 @@ class Exponential(CreateParametersMixin, ModelComponent): - r"""Model of an exponential function. + r""" + Model of an exponential function. The intensity is given by $$ 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. + where $A$ is the amplitude, $x_0$ is the center, and $B$ describes the rate of decay or growth. """ def __init__( @@ -33,24 +33,31 @@ def __init__( display_name: str | None = 'Exponential', unique_name: str | None = None, ) -> None: - """Initialize the Exponential component. - - Args: - 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. - 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. - ValueError: If amplitude, center or rate are not finite numbers + """ + Initialize the Exponential component. + + Parameters + ---------- + 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. + 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. By + default, None. + + Raises + ------ + TypeError + If amplitude, center, or rate are not numbers or Parameters. + ValueError + If amplitude, center or rate are not finite numbers. """ # Validate inputs and create Parameters if not given super().__init__( @@ -89,23 +96,31 @@ def __init__( @property def amplitude(self) -> Parameter: - """Get the amplitude parameter. + """ + Get the amplitude parameter. - Returns: - Parameter: The amplitude parameter. + Returns + ------- + Parameter + The amplitude parameter. """ return self._amplitude @amplitude.setter def amplitude(self, value: Numeric) -> None: - """Set the value of the amplitude parameter. + """ + Set the value of the amplitude parameter. - Args: - value (Numeric): The new value for the amplitude parameter. + Parameters + ---------- + value : Numeric + The new value for the amplitude parameter. - Raises: - TypeError: If the value is not a number. + Raises + ------ + TypeError + If the value is not a number. """ if not isinstance(value, Numeric): @@ -114,24 +129,31 @@ def amplitude(self, value: Numeric) -> None: @property def center(self) -> Parameter: - """Get the center parameter. + """ + Get the center parameter. - Returns: - Parameter: The center parameter. + Returns + ------- + Parameter + The center parameter. """ return self._center @center.setter def center(self, value: Numeric | None) -> None: - """Set the center parameter value. + """ + Set the center parameter value. - Args: - value (Numeric | None): The new value for the center - parameter. + Parameters + ---------- + value : Numeric | None + The new value for the center parameter. - Raises: - TypeError: If the value is not a number. + Raises + ------ + TypeError + If the value is not a number. """ if value is None: @@ -144,23 +166,30 @@ def center(self, value: Numeric | None) -> None: @property def rate(self) -> Parameter: - """Get the rate parameter. + """ + Get the rate parameter. - Returns: - Parameter: The rate parameter. + Returns + ------- + Parameter + The rate parameter. """ return self._rate @rate.setter def rate(self, value: Numeric) -> None: - """Set the rate parameter value. + """ + Set the rate parameter value. - Args: - value (Numeric): The new value for the rate - parameter. + Parameters + ---------- + value : Numeric + The new value for the rate parameter. - Raises: - TypeError: If the value is not a number. + Raises + ------ + TypeError + If the value is not a number. """ if not isinstance(value, Numeric): raise TypeError('rate must be a number') @@ -171,25 +200,23 @@ def evaluate( self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray, ) -> np.ndarray: - r"""Evaluate the Exponential at the given x values. + r""" + Evaluate the Exponential at the given x values. - If x is a scipp Variable, the unit of the Exponential will be - converted to match x. - The intensity is given by - $$ - I(x) = A \exp\left( r (x - x_0) \right) - $$ + If x is a scipp Variable, the unit of the Exponential will be converted to match x. The + intensity is given by $$ I(x) = A \exp\left( r (x - x_0) \right) $$ - where $A$ is the amplitude, $x_0$ is the center, and $r$ is the - rate. + where $A$ is the amplitude, $x_0$ is the center, and $r$ is the rate. - Args: - x (Numeric | list | np.ndarray | sc.Variable | sc.DataArray): - The x values at which to evaluate the Exponential. + Parameters + ---------- + x : Numeric | list | np.ndarray | sc.Variable | sc.DataArray + The x values at which to evaluate the Exponential. - Returns: - np.ndarray: The intensity of the Exponential at the given x - values. + Returns + ------- + np.ndarray + The intensity of the Exponential at the given x values. """ x = self._prepare_x_for_evaluate(x) @@ -198,14 +225,20 @@ def evaluate( return self.amplitude.value * np.exp(exponent) def convert_unit(self, unit: str | sc.Unit) -> None: - """Convert the unit of the Parameters in the component. - - Args: - 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. + """ + Convert the unit of the Parameters in the component. + + Parameters + ---------- + 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)): @@ -229,10 +262,13 @@ def convert_unit(self, unit: str | sc.Unit) -> None: raise e def __repr__(self) -> str: - """Return a string representation of the Exponential. + """ + Return a string representation of the Exponential. - Returns: - str: A string representation of the Exponential. + Returns + ------- + str + A string representation of the Exponential. """ return f'Exponential(unique_name = {self.unique_name}, unit = {self._unit},\n \ diff --git a/src/easydynamics/sample_model/components/expression_component.py b/src/easydynamics/sample_model/components/expression_component.py index dcc9e445..176b4dc5 100644 --- a/src/easydynamics/sample_model/components/expression_component.py +++ b/src/easydynamics/sample_model/components/expression_component.py @@ -14,16 +14,13 @@ class ExpressionComponent(ModelComponent): - """Model component defined by a symbolic expression. + """ + Model component defined by a symbolic expression. - Example: - expr = ExpressionComponent( - "A * exp(-(x - x0)**2 / (2*sigma**2))", - parameters={"A": 10, "x0": 0, "sigma": 1}, - ) + Example: expr = ExpressionComponent( "A * exp(-(x - x0)**2 / (2*sigma**2))", parameters={"A": + 10, "x0": 0, "sigma": 1}, ) - expr.A = 5 - y = expr.evaluate(x) + expr.A = 5 y = expr.evaluate(x) """ # ------------------------- @@ -77,21 +74,28 @@ def __init__( display_name: str | None = 'Expression', unique_name: str | None = None, ) -> None: - """Initialize the ExpressionComponent. - - Args: - expression (str): The symbolic expression as a string. - Must contain 'x' as the independent variable. - parameters (dict[str, Numeric] | None, default=None): - Dictionary of parameter names and their initial values. - Defaults to None (no parameters). - unit (str | sc.Unit, default="meV"): Unit of the output. - display_name (str | None, default="Expression"): Display name for the component. - unique_name (str | None, default=None): Unique name for the component. - - Raises: - ValueError: If the expression is invalid or does not contain 'x'. - TypeError: If any parameter value is not numeric. + """ + Initialize the ExpressionComponent. + + Parameters + ---------- + expression : str + The symbolic expression as a string. Must contain 'x' as the independent variable. + parameters : dict[str, Numeric] | None, default=None + Dictionary of parameter names and their initial values. + unit : str | sc.Unit, default='meV' + Unit of the output. + display_name : str | None, default='Expression' + Display name for the component. + unique_name : str | None, default=None + Unique name for the component. + + Raises + ------ + ValueError + If the expression is invalid or does not contain 'x'. + TypeError + If any parameter value is not numeric. """ super().__init__(unit=unit, display_name=display_name, unique_name=unique_name) @@ -187,13 +191,18 @@ def expression(self) -> str: @expression.setter def expression(self, _new_expr: str) -> None: - """Prevent changing the expression after initialization. + """ + Prevent changing the expression after initialization. - Args: - _new_expr (str): New expression string (ignored). + Parameters + ---------- + _new_expr : str + New expression string (ignored). - Raises: - AttributeError: Always raised to prevent changing the expression. + Raises + ------ + AttributeError + Always raised to prevent changing the expression. """ raise AttributeError('Expression cannot be changed after initialization') @@ -201,14 +210,18 @@ def evaluate( self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray, ) -> np.ndarray: - """Evaluate the expression for given x values. + """ + Evaluate the expression for given x values. - Args: - x (Numeric | list | np.ndarray | sc.Variable | sc.DataArray): - Input values for the independent variable. + Parameters + ---------- + x : Numeric | list | np.ndarray | sc.Variable | sc.DataArray + Input values for the independent variable. - Returns: - np.ndarray: Evaluated results. + Returns + ------- + np.ndarray + Evaluated results. """ x = self._prepare_x_for_evaluate(x) @@ -222,23 +235,31 @@ def evaluate( return self._func(*args) def get_all_variables(self) -> list[Parameter]: - """Return all parameters. + """ + Return all parameters. - Returns: - list[Parameter]: List of all parameters in the expression. + Returns + ------- + list[Parameter] + List of all parameters in the expression. """ return list(self._parameters.values()) def convert_unit(self, _new_unit: str | sc.Unit) -> None: - """Convert the unit of the expression. + """ + Convert the unit of the expression. Unit conversion is not implemented for ExpressionComponent. - Args: - _new_unit (str | sc.Unit): The new unit to convert to (ignored). + Parameters + ---------- + _new_unit : str | sc.Unit + The new unit to convert to (ignored). - Raises: - NotImplementedError: Always raised to indicate unit conversion is not supported. + Raises + ------ + NotImplementedError + Always raised to indicate unit conversion is not supported. """ raise NotImplementedError('Unit conversion is not implemented for ExpressionComponent') @@ -248,30 +269,43 @@ def convert_unit(self, _new_unit: str | sc.Unit) -> None: # ------------------------- def __getattr__(self, name: str) -> Parameter: - """Allow access to parameters as attributes. - - Args: - name (str): Name of the parameter to access. - - Returns: - Parameter: The parameter with the given name. - - Raises: - AttributeError: If the parameter does not exist. + """ + Allow access to parameters as attributes. + + Parameters + ---------- + name : str + Name of the parameter to access. + + Raises + ------ + AttributeError + If the parameter does not exist. + + Returns + ------- + Parameter + The parameter with the given name. """ if '_parameters' in self.__dict__ and name in self._parameters: return self._parameters[name] raise AttributeError(f"{self.__class__.__name__} has no attribute '{name}'") def __setattr__(self, name: str, value: Numeric) -> None: - """Allow setting parameter values as attributes. - - Args: - name (str): Name of the parameter to set. - value (Numeric): New value for the parameter. - - Raises: - TypeError: If the value is not numeric. + """ + Allow setting parameter values as attributes. + + Parameters + ---------- + name : str + Name of the parameter to set. + value : Numeric + New value for the parameter. + + Raises + ------ + TypeError + If the value is not numeric. """ if '_parameters' in self.__dict__ and name in self._parameters: param = self._parameters[name] @@ -285,15 +319,18 @@ def __setattr__(self, name: str, value: Numeric) -> None: super().__setattr__(name, value) def __dir__(self) -> list[str]: - """Include parameter names in dir() output for better IDE - support. + """ + Include parameter names in dir() output for better IDE support. - Returns: - list[str]: List of attribute names, including parameters. + Returns + ------- + list[str] + List of attribute names, including parameters. """ return super().__dir__() + list(self._parameters.keys()) def __repr__(self) -> str: + """Repr function.""" param_str = ', '.join(f'{k}={v.value}' for k, v in self._parameters.items()) return ( f'{self.__class__.__name__}(\n' diff --git a/src/easydynamics/sample_model/components/gaussian.py b/src/easydynamics/sample_model/components/gaussian.py index 892577e1..9c185b2f 100644 --- a/src/easydynamics/sample_model/components/gaussian.py +++ b/src/easydynamics/sample_model/components/gaussian.py @@ -14,23 +14,18 @@ class Gaussian(CreateParametersMixin, ModelComponent): - r"""Model of a Gaussian function. + r""" + Model of a Gaussian function. The intensity is given by - $$ - I(x) = \frac{A}{\sigma \sqrt{2\pi}} - \exp\left( - -\frac{1}{2} - \left(\frac{x - x_0}{\sigma}\right)^2 - \right) - $$ + $$ I(x) = \frac{A}{\sigma \sqrt{2\pi}} \exp\left( -\frac{1}{2} \left(\frac{x - + x_0}{\sigma}\right)^2 \right) $$ - where $A$ is the area, $x_0$ is the center, and $\sigma$ is the - width. + where $A$ is the area, $x_0$ is the center, and $\sigma$ is the width. - If the center is not provided, it will be centered at 0 and - fixed, which is typically what you want in QENS. + If the center is not provided, it will be centered at 0 and fixed, which is typically what you + want in QENS. """ def __init__( @@ -42,17 +37,24 @@ def __init__( display_name: str | None = 'Gaussian', unique_name: str | None = None, ) -> None: - """Initialize the Gaussian component. - - Args: - 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 (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. + """ + Initialize the Gaussian component. + + Parameters + ---------- + area : Numeric | Parameter, default=1.0 + Area of the Gaussian. + center : Numeric | Parameter | None, default=None + Center of the Gaussian. If None. + 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. By + default, None. """ # Validate inputs and create Parameters if not given super().__init__( @@ -74,23 +76,31 @@ def __init__( @property def area(self) -> Parameter: - """Get the area parameter. + """ + Get the area parameter. - Returns: - Parameter: The area parameter. + Returns + ------- + Parameter + The area parameter. """ return self._area @area.setter def area(self, value: Numeric) -> None: - """Set the value of the area parameter. + """ + Set the value of the area parameter. - Args: - value (Numeric): The new value for the area parameter. + Parameters + ---------- + value : Numeric + The new value for the area parameter. - Raises: - TypeError: If the value is not a number. + Raises + ------ + TypeError + If the value is not a number. """ if not isinstance(value, Numeric): @@ -99,24 +109,31 @@ def area(self, value: Numeric) -> None: @property def center(self) -> Parameter: - """Get the center parameter. + """ + Get the center parameter. - Returns: - Parameter: The center parameter. + Returns + ------- + Parameter + The center parameter. """ return self._center @center.setter def center(self, value: Numeric | None) -> None: - """Set the center parameter value. + """ + Set the center parameter value. - Args: - value (Numeric | None): The new value for the center - parameter. If None, defaults to 0 and is fixed. + Parameters + ---------- + value : Numeric | None + The new value for the center parameter. If None, defaults to 0 and is fixed. - Raises: - TypeError: If the value is not a number or None. + Raises + ------ + TypeError + If the value is not a number or None. """ if value is None: @@ -128,24 +145,32 @@ def center(self, value: Numeric | None) -> None: @property def width(self) -> Parameter: - """Get the width parameter (standard deviation). + """ + Get the width parameter (standard deviation). - Returns: - Parameter: The width parameter. + Returns + ------- + Parameter + The width parameter. """ return self._width @width.setter def width(self, value: Numeric) -> None: - """Set the width parameter value. - - Args: - 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. + """ + Set the width parameter value. + + Parameters + ---------- + 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') @@ -159,30 +184,24 @@ def evaluate( self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray, ) -> np.ndarray: - r"""Evaluate the Gaussian at the given x values. + r""" + Evaluate the Gaussian at the given x values. - If x is a scipp Variable, the unit of the Gaussian will be - converted to match x. - The intensity is given by - $$ - I(x) = \frac{A}{\sigma \sqrt{2\pi}} - \exp\left( - -\frac{1}{2} - \left(\frac{x - x_0}{\sigma}\right)^2 - \right) - $$ + If x is a scipp Variable, the unit of the Gaussian will be converted to match x. The + intensity is given by $$ I(x) = \frac{A}{\sigma \sqrt{2\pi}} \exp\left( -\frac{1}{2} + \left(\frac{x - x_0}{\sigma}\right)^2 \right) $$ - where $A$ is the area, $x_0$ is the center, and $\sigma$ is the - width. + where $A$ is the area, $x_0$ is the center, and $\sigma$ is the width. + Parameters + ---------- + x : Numeric | list | np.ndarray | sc.Variable | sc.DataArray + The x values at which to evaluate the Gaussian. - Args: - x (Numeric | list | np.ndarray | sc.Variable | sc.DataArray): - The x values at which to evaluate the Gaussian. - - Returns: - np.ndarray: The intensity of the Gaussian at the given x - values. + Returns + ------- + np.ndarray + The intensity of the Gaussian at the given x values. """ x = self._prepare_x_for_evaluate(x) @@ -193,10 +212,13 @@ def evaluate( return self.area.value * normalization * np.exp(exponent) def __repr__(self) -> str: - """Return a string representation of the Gaussian. + """ + Return a string representation of the Gaussian. - Returns: - str: A string representation of the Gaussian. + Returns + ------- + str + A string representation of the Gaussian. """ return f'Gaussian(unique_name = {self.unique_name}, unit = {self._unit},\n \ diff --git a/src/easydynamics/sample_model/components/lorentzian.py b/src/easydynamics/sample_model/components/lorentzian.py index 8f5697bd..ca3d1a21 100644 --- a/src/easydynamics/sample_model/components/lorentzian.py +++ b/src/easydynamics/sample_model/components/lorentzian.py @@ -14,17 +14,15 @@ class Lorentzian(CreateParametersMixin, ModelComponent): - r"""Model of a Lorentzian function. + r""" + Model of a Lorentzian function. - The intensity is given by - $$ - I(x) = \frac{A}{\pi} \frac{\Gamma}{(x - x_0)^2 + \Gamma^2}, - $$ - where $A$ is the area, $x_0$ is the center, and $\Gamma$ is the - half width at half maximum (HWHM). + The intensity is given by $$ I(x) = \frac{A}{\pi} \frac{\Gamma}{(x - x_0)^2 + \Gamma^2}, $$ + where $A$ is the area, $x_0$ is the center, and $\Gamma$ is the half width at half maximum + (HWHM). - If the center is not provided, it will be centered at 0 - and fixed, which is typically what you want in QENS. + If the center is not provided, it will be centered at 0 and fixed, which is typically what you + want in QENS. """ def __init__( @@ -36,18 +34,24 @@ def __init__( display_name: str | None = 'Lorentzian', unique_name: str | None = None, ) -> None: - """Initialize the Lorentzian component. - - Args: - 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, 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. + """ + Initialize the Lorentzian component. + + Parameters + ---------- + area : Numeric | Parameter, default=1.0 + Area of the Lorentzian. + center : Numeric | Parameter | None, default=None + Center of the Lorentzian. If None. + width : Numeric | Parameter, default=1.0 + Half width at half maximum (HWHM). + 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. By + default, None. """ super().__init__( @@ -69,22 +73,30 @@ def __init__( @property def area(self) -> Parameter: - """Get the area parameter. + """ + Get the area parameter. - Returns: - Parameter: The area parameter. + Returns + ------- + Parameter + The area parameter. """ return self._area @area.setter def area(self, value: Numeric) -> None: - """Set the value of the area parameter. + """ + Set the value of the area parameter. - Args: - value (Numeric): The new value for the area parameter. + Parameters + ---------- + value : Numeric + The new value for the area parameter. - Raises: - TypeError: If the value is not a number. + Raises + ------ + TypeError + If the value is not a number. """ if not isinstance(value, Numeric): raise TypeError('area must be a number') @@ -92,23 +104,30 @@ def area(self, value: Numeric) -> None: @property def center(self) -> Parameter: - """Get the center parameter. + """ + Get the center parameter. - Returns: - Parameter: The center parameter. + Returns + ------- + Parameter + The center parameter. """ return self._center @center.setter def center(self, value: Numeric | None) -> None: - """Set the value of the center parameter. + """ + Set the value of the center parameter. - Args: - value (Numeric | None): The new value for the center - parameter. If None, defaults to 0 and is fixed. + Parameters + ---------- + value : Numeric | None + The new value for the center parameter. If None, defaults to 0 and is fixed. - Raises: - TypeError: If the value is not a number or None. + Raises + ------ + TypeError + If the value is not a number or None. """ if value is None: @@ -120,24 +139,32 @@ def center(self, value: Numeric | None) -> None: @property def width(self) -> Parameter: - """Get the width parameter (HWHM). + """ + Get the width parameter (HWHM). - Returns: - Parameter: The width parameter. + Returns + ------- + Parameter + The width parameter. """ return self._width @width.setter def width(self, value: Numeric) -> None: - """Set the width parameter value (HWHM). - - Args: - value (Numeric): The new value for the width - parameter. - - Raises: - TypeError: If the value is not a number. - ValueError: If the value is not positive. + """ + Set the width parameter value (HWHM). + + Parameters + ---------- + value : Numeric + The new value for the width parameter. + + Raises + ------ + 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') @@ -147,26 +174,26 @@ def width(self, value: Numeric) -> None: self._width.value = value def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray: - r"""Evaluate the Lorentzian at the given x values. + r""" + Evaluate the Lorentzian at the given x values. - If x is a scipp Variable, the unit of the Lorentzian will be - converted to match x. The intensity is given by + If x is a scipp Variable, the unit of the Lorentzian will be converted to match x. The + intensity is given by - $$ - I(x) = \frac{A}{\pi} \frac{\Gamma}{(x - - x_0)^2 + \Gamma^2}, - $$ + $$ I(x) = \frac{A}{\pi} \frac{\Gamma}{(x - x_0)^2 + \Gamma^2}, $$ - where $A$ is the area, $x_0$ is the center, and $\Gamma$ is - the half width at half maximum (HWHM). + where $A$ is the area, $x_0$ is the center, and $\Gamma$ is the half width at half maximum + (HWHM). - Args: - x (Numeric | list | np.ndarray | sc.Variable | sc.DataArray): - The x values at which to evaluate the Lorentzian. + Parameters + ---------- + x : Numeric | list | np.ndarray | sc.Variable | sc.DataArray + The x values at which to evaluate the Lorentzian. - Returns: - np.ndarray: The intensity of the Lorentzian at the given x - values. + Returns + ------- + np.ndarray + The intensity of the Lorentzian at the given x values. """ x = self._prepare_x_for_evaluate(x) @@ -177,10 +204,13 @@ def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) return self.area.value * normalization / denominator def __repr__(self) -> str: - """Return a string representation of the Lorentzian. + """ + Return a string representation of the Lorentzian. - Returns: - str: A string representation of the Lorentzian. + 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})' diff --git a/src/easydynamics/sample_model/components/mixins.py b/src/easydynamics/sample_model/components/mixins.py index b5125767..a80a7200 100644 --- a/src/easydynamics/sample_model/components/mixins.py +++ b/src/easydynamics/sample_model/components/mixins.py @@ -15,12 +15,11 @@ class CreateParametersMixin: - """Provides parameter creation and validation methods for model - components. + """ + Provides parameter creation and validation methods for model components. - This mixin provides methods to create and validate common physics - parameters (area, center, width) with appropriate bounds and type - checking. + This mixin provides methods to create and validate common physics parameters (area, center, + width) with appropriate bounds and type checking. """ def _create_area_parameter( @@ -30,26 +29,34 @@ def _create_area_parameter( unit: str | sc.Unit = 'meV', minimum_area: float = MINIMUM_AREA, ) -> Parameter: - """Validate and convert a number to a Parameter describing the - area of a function. - - If the area is negative, a warning is raised. - If the area is non-negative, its minimum is set to 0 to avoid it - accidentally becoming negative during fitting. - - Args: - area (Numeric | Parameter): The area value or Parameter. - name (str): The name of the model component. - 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. - ValueError: If area is not a finite number or if the area - Parameter has a non-finite value. + """ + Validate and convert a number to a Parameter describing the area of a function. + + If the area is negative, a warning is raised. If the area is non-negative, its minimum is + set to 0 to avoid it accidentally becoming negative during fitting. + + Parameters + ---------- + area : Numeric | Parameter + The area value or Parameter. + name : str + The name of the model component. + unit : str | sc.Unit, default='meV' + The unit of the area Parameter. + minimum_area : float, default=MINIMUM_AREA + The minimum allowed area. + + Raises + ------ + TypeError + If area is not a number or a Parameter. + ValueError + If area is not a finite number or if the area Parameter has a non-finite value. + + Returns + ------- + Parameter + The validated area Parameter. """ if not isinstance(area, (Parameter, Numeric)): raise TypeError('area must be a number or a Parameter.') @@ -79,28 +86,34 @@ def _create_center_parameter( unit: str | sc.Unit = 'meV', enforce_minimum_center: bool = False, ) -> Parameter: - """Validate and convert a number to a Parameter describing the - center of a function. - - Args: - center (Numeric | Parameter | None): The center value or - 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, 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. + """ + Validate and convert a number to a Parameter describing the center of a function. + + Parameters + ---------- + center : Numeric | Parameter | None + The center value or 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, 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. + + 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. + + Returns + ------- + Parameter + The validated center Parameter. """ if center is not None and not isinstance(center, (Numeric, Parameter)): raise TypeError('center must be None, a number, or a Parameter.') @@ -129,23 +142,33 @@ def _create_width_parameter( 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 | Parameter): The width value or Parameter. - name (str): The name of the model component. - 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. - - Raises: - TypeError: If width is not a number or a Parameter. - ValueError: If width is non-positive. + """ + Validate and convert a number to a Parameter describing the width of a function. + + Parameters + ---------- + width : Numeric | Parameter + The width value or Parameter. + name : str + The name of the model component. + 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. + + Raises + ------ + TypeError + If width is not a number or a Parameter. + ValueError + If width is non-positive. + + Returns + ------- + Parameter + The validated width Parameter. """ if not isinstance(width, (Numeric, Parameter)): raise TypeError(f'{param_name} must be a number or a Parameter.') diff --git a/src/easydynamics/sample_model/components/model_component.py b/src/easydynamics/sample_model/components/model_component.py index bfbd7384..f4d0c15e 100644 --- a/src/easydynamics/sample_model/components/model_component.py +++ b/src/easydynamics/sample_model/components/model_component.py @@ -23,14 +23,17 @@ def __init__( display_name: str | None = None, unique_name: str | None = None, ) -> None: - """Initialize the ModelComponent. - - Args: - 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. + """ + Initialize the ModelComponent. + + Parameters + ---------- + 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) @@ -38,24 +41,33 @@ def __init__( @property def unit(self) -> str: - """Get the unit. + """ + Get the unit. - Returns: - str: The unit of the model component. + Returns + ------- + str + The unit of the model component. """ return str(self._unit) @unit.setter def unit(self, _unit_str: str) -> None: - """Unit is read-only. Use convert_unit to change the unit - between allowed types or create a new ModelComponent with the - desired unit. + """ + Unit is read-only. - Args: - _unit_str (str): The new unit to set. + Use convert_unit to change the unit between allowed types or create a new ModelComponent + with the desired unit. - Raises: - AttributeError: Always raised since unit is read-only. + Parameters + ---------- + _unit_str : str + The new unit to set. + + Raises + ------ + AttributeError + Always raised since unit is read-only. """ raise AttributeError( f'Unit is read-only. Use convert_unit to change the unit between allowed types ' @@ -77,21 +89,26 @@ def free_all_parameters(self) -> None: def _prepare_x_for_evaluate( 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 | np.ndarray | sc.Variable | sc.DataArray): - The input data to prepare. - - Returns: - np.ndarray: The prepared input data as a numpy array. - - Raises: - ValueError: If x contains NaN or infinite values, or if a - sc.DataArray has more than one coordinate. - UnitError: If x has incompatible units that cannot be - converted to the component's unit. + """ + Prepare the input x for evaluation by handling units and converting to a numpy array. + + Parameters + ---------- + x : Numeric | list | np.ndarray | sc.Variable | sc.DataArray + The input data to prepare. + + Raises + ------ + ValueError + If x contains NaN or infinite values, or if a sc.DataArray has more than one + coordinate. + UnitError + If x has incompatible units that cannot be converted to the component's unit. + + Returns + ------- + np.ndarray + The prepared input data as a numpy array. """ # Handle units @@ -148,13 +165,18 @@ def _prepare_x_for_evaluate( @staticmethod def validate_unit(unit: str | sc.Unit | None) -> None: - """Validate that the unit is either a string or a scipp Unit. + """ + Validate that the unit is either a string or a scipp Unit. - Args: - unit (str | sc.Unit | None): The unit to validate. + Parameters + ---------- + unit : str | sc.Unit | None + The unit to validate. - Raises: - TypeError: If unit is not a string or scipp Unit. + 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( @@ -162,15 +184,20 @@ def validate_unit(unit: str | sc.Unit | None) -> None: ) def convert_unit(self, unit: str | sc.Unit) -> None: - """Convert the unit of the Parameters in the component. - - Args: - 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. + """ + Convert the unit of the Parameters in the component. + + Parameters + ---------- + 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__}') @@ -193,23 +220,31 @@ def convert_unit(self, unit: str | sc.Unit) -> None: @abstractmethod def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray: - """Abstract method to evaluate the model component at input x. + """ + Abstract method to evaluate the model component at input x. + Must be implemented by subclasses. - Args: - x (Numeric | list | np.ndarray | sc.Variable | sc.DataArray): - The x values at which to evaluate the component. + Parameters + ---------- + x : Numeric | list | np.ndarray | sc.Variable | sc.DataArray + The x values at which to evaluate the component. - Returns: - np.ndarray: Evaluated function values. + Returns + ------- + np.ndarray + Evaluated function values. """ pass def __repr__(self) -> str: - """Return a string representation of the ModelComponent. + """ + Return a string representation of the ModelComponent. - Returns: - str: A string representation of the ModelComponent. + Returns + ------- + str + A string representation of the ModelComponent. """ 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 340ca537..0263d81b 100644 --- a/src/easydynamics/sample_model/components/polynomial.py +++ b/src/easydynamics/sample_model/components/polynomial.py @@ -18,13 +18,11 @@ class Polynomial(ModelComponent): - r"""Polynomial function component. + r""" + Polynomial function component. - The intensity is given by - $$ - I(x) = c_0 + c_1 x + c_2 x^2 + ... + c_N x^N, - $$ - where $C_i$ are the coefficients. + The intensity is given by $$ I(x) = c_0 + c_1 x + c_2 x^2 + ... + c_N x^N, $$ where $C_i$ are + the coefficients. """ def __init__( @@ -34,24 +32,28 @@ def __init__( 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 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. - ValueError: If coefficients is an empty sequence. + """ + Initialize the Polynomial component. + + Parameters + ---------- + 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 | None, default=None + Unique name of the component. If None, a unique_name is automatically generated. By + default, None. + + Raises + ------ + TypeError + If coefficients is not a sequence of numbers or Parameters or if any item in + coefficients is not a number or Parameter. + ValueError + If coefficients is an empty sequence. """ super().__init__(display_name=display_name, unit=unit, unique_name=unique_name) @@ -84,29 +86,35 @@ def __init__( @property def coefficients(self) -> list[Parameter]: - """Get the coefficients of the polynomial as a list of - Parameters. + """ + Get the coefficients of the polynomial as a list of Parameters. - Returns: - list[Parameter]: The coefficients of the polynomial. + Returns + ------- + list[Parameter] + The coefficients of the polynomial. """ return list(self._coefficients) @coefficients.setter def coefficients(self, coeffs: Sequence[Numeric | Parameter]) -> None: - """Set the coefficients of the polynomial. + """ + Set the coefficients of the polynomial. Length must match current number of coefficients. - Args: - coeffs (Sequence[Numeric | Parameter]): New coefficients as - a sequence of numbers or Parameters. - - Raises: - TypeError: If coeffs is not a sequence of numbers or - 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. + Parameters + ---------- + coeffs : Sequence[Numeric | Parameter] + New coefficients as a sequence of numbers or Parameters. + + Raises + ------ + TypeError + If coeffs is not a sequence of numbers or 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. """ if not isinstance(coeffs, (list, tuple, np.ndarray)): raise TypeError( @@ -126,29 +134,32 @@ def coefficients(self, coeffs: Sequence[Numeric | Parameter]) -> None: 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. + """ + Get the coefficients of the polynomial as a list. - Returns: - list[float]: The coefficient values of the polynomial. + Returns + ------- + list[float] + The coefficient values of the polynomial. """ return [param.value for param in self._coefficients] def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray: - r"""Evaluate the Polynomial at the given x values. + r""" + Evaluate the Polynomial at the given x values. - The intensity is given by - $$ - I(x) = c_0 + c_1 x + c_2 x^2 + ... + c_N x^N, - $$ - where $C_i$ are the coefficients. + The intensity is given by $$ I(x) = c_0 + c_1 x + c_2 x^2 + ... + + c_N x^N, $$ where $C_i$ are the coefficients. - Args: - x (Numeric | list | np.ndarray | sc.Variable | sc.DataArray): - The x values at which to evaluate the Polynomial. + Parameters + ---------- + x : Numeric | list | np.ndarray | sc.Variable | sc.DataArray + The x values at which to evaluate the Polynomial. - Returns: - np.ndarray: The evaluated Polynomial at the given x - values. + Returns + ------- + np.ndarray + The evaluated Polynomial at the given x values. """ x = self._prepare_x_for_evaluate(x) @@ -168,24 +179,30 @@ def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) @property def degree(self) -> int: - """Get the degree of the polynomial. + """ + Get the degree of the polynomial. - Returns: - int: The degree of the polynomial. + Returns + ------- + int + The degree of the polynomial. """ return len(self._coefficients) - 1 @degree.setter def degree(self, _value: int) -> None: - """The degree is determined by the number of coefficients and - cannot be set directly. + """ + The degree is determined by the number of coefficients and cannot be set directly. - Args: - _value (int): The new degree of the polynomial. + Parameters + ---------- + _value : int + The new degree of the polynomial. - Raises: - AttributeError: Always raised since degree cannot be set - directly. + Raises + ------ + AttributeError + Always raised since degree cannot be set directly. """ raise AttributeError( 'The degree of the polynomial is determined by the number of coefficients ' @@ -193,21 +210,29 @@ def degree(self, _value: int) -> None: ) def get_all_variables(self) -> list[DescriptorBase]: - """Get all variables from the model component. + """ + Get all variables from the model component. - Returns: - list[DescriptorBase]: List of variables in the component. + Returns + ------- + list[DescriptorBase] + List of variables in the component. """ return list(self._coefficients) def convert_unit(self, unit: str | sc.Unit) -> None: - """Convert the unit of the polynomial. + """ + Convert the unit of the polynomial. - Args: - unit (str | sc.Unit): The target unit to convert to. + Parameters + ---------- + unit : str | sc.Unit + The target unit to convert to. - Raises: - UnitError: If the provided unit is not a string or sc.Unit. + Raises + ------ + UnitError + If the provided unit is not a string or sc.Unit. """ if not isinstance(unit, (str, sc.Unit)): @@ -226,10 +251,13 @@ def convert_unit(self, unit: str | sc.Unit) -> None: self._unit = unit def __repr__(self) -> str: - """Return a string representation of the Polynomial. + """ + Return a string representation of the Polynomial. - Returns: - str: A string representation of the Polynomial. + Returns + ------- + str + A string representation of the Polynomial. """ coeffs_str = ', '.join(f'{param.name}={param.value}' for param in self._coefficients) diff --git a/src/easydynamics/sample_model/components/voigt.py b/src/easydynamics/sample_model/components/voigt.py index d57c91bc..c261b3f2 100644 --- a/src/easydynamics/sample_model/components/voigt.py +++ b/src/easydynamics/sample_model/components/voigt.py @@ -15,9 +15,11 @@ class Voigt(CreateParametersMixin, ModelComponent): - r"""Voigt profile, a convolution of Gaussian and Lorentzian. If the - center is not provided, it will be centered at 0 and fixed, which is - typically what you want in QENS. + r""" + Voigt profile, a convolution of Gaussian and Lorentzian. + + If the center is not provided, it will be centered at 0 and fixed, which is typically what you + want in QENS. Use scipy.special.voigt_profile to evaluate the Voigt profile. """ @@ -32,19 +34,26 @@ def __init__( display_name: str | None = 'Voigt', unique_name: str | None = None, ) -> None: - """Initialize a Voigt component. - - Args: - 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 (Numeric | Parameter, default=1.0): Half width at half max - (HWHM) of the Lorentzian part. - 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. + """ + Initialize a Voigt component. + + Parameters + ---------- + 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 : Numeric | Parameter, default=1.0 + Half width at half max (HWHM) of the Lorentzian part. + 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. By + default, None. """ super().__init__( @@ -78,22 +87,30 @@ def __init__( @property def area(self) -> Parameter: - """Get the area parameter. + """ + Get the area parameter. - Returns: - Parameter: The area parameter. + Returns + ------- + Parameter + The area parameter. """ return self._area @area.setter def area(self, value: Numeric) -> None: - """Set the value of the area parameter. + """ + Set the value of the area parameter. - Args: - value (Numeric): The new value for the area parameter. + Parameters + ---------- + value : Numeric + The new value for the area parameter. - Raises: - TypeError: If the value is not a number. + Raises + ------ + TypeError + If the value is not a number. """ if not isinstance(value, Numeric): raise TypeError('area must be a number') @@ -101,23 +118,30 @@ def area(self, value: Numeric) -> None: @property def center(self) -> Parameter: - """Get the center parameter. + """ + Get the center parameter. - Returns: - Parameter: The center parameter. + Returns + ------- + Parameter + The center parameter. """ return self._center @center.setter def center(self, value: Numeric | None) -> None: - """Set the value of the center parameter. + """ + Set the value of the center parameter. - Args: - value (Numeric | None): The new value for the center - parameter. If None, defaults to 0 and is fixed. + Parameters + ---------- + value : Numeric | None + The new value for the center parameter. If None, defaults to 0 and is fixed. - Raises: - TypeError: If the value is not a number. + Raises + ------ + TypeError + If the value is not a number. """ if value is None: value = 0.0 @@ -128,24 +152,32 @@ def center(self, value: Numeric | None) -> None: @property def gaussian_width(self) -> Parameter: - """Get the Gaussian width parameter. + """ + Get the Gaussian width parameter. - Returns: - Parameter: The Gaussian width parameter. + Returns + ------- + Parameter + The Gaussian width parameter. """ return self._gaussian_width @gaussian_width.setter def gaussian_width(self, value: Numeric) -> None: - """Set the width parameter value. - - Args: - value (Numeric): The new value for the width - parameter. - - Raises: - TypeError: If the value is not a number. - ValueError: If the value is not positive. + """ + Set the width parameter value. + + Parameters + ---------- + value : Numeric + The new value for the width parameter. + + Raises + ------ + 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') @@ -155,24 +187,32 @@ def gaussian_width(self, value: Numeric) -> None: @property def lorentzian_width(self) -> Parameter: - """Get the Lorentzian width parameter (HWHM). + """ + Get the Lorentzian width parameter (HWHM). - Returns: - Parameter: The Lorentzian width parameter. + Returns + ------- + Parameter + The Lorentzian width parameter. """ return self._lorentzian_width @lorentzian_width.setter def lorentzian_width(self, value: Numeric) -> None: - """Set the value of the Lorentzian width parameter. - - Args: - value (Numeric): The new value for the Lorentzian width - parameter. - - Raises: - TypeError: If the value is not a number. - ValueError: If the value is not positive. + """ + Set the value of the Lorentzian width parameter. + + Parameters + ---------- + value : Numeric + The new value for the Lorentzian width parameter. + + Raises + ------ + TypeError + If the value is not a number. + ValueError + If the value is not positive. """ if not isinstance(value, Numeric): raise TypeError('lorentzian_width must be a number') @@ -181,22 +221,22 @@ def lorentzian_width(self, value: Numeric) -> None: self._lorentzian_width.value = value 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 - converted to match x. The Voigt evaluates to the convolution of - a Gaussian with sigma gaussian_width and a Lorentzian with half - width at half max lorentzian_width, centered at center, with - area equal to area. - - - Args: - x (Numeric | list | np.ndarray | sc.Variable | sc.DataArray): - The x values at which to evaluate the Voigt. - - Returns: - np.ndarray: The intensity of the Voigt at the given x - values. + r""" + Evaluate the Voigt at the given x values. + + If x is a scipp Variable, the unit of the Voigt will be converted to match x. The Voigt + evaluates to the convolution of a Gaussian with sigma gaussian_width and a Lorentzian with + half width at half max lorentzian_width, centered at center, with area equal to area. + + Parameters + ---------- + x : Numeric | list | np.ndarray | sc.Variable | sc.DataArray + The x values at which to evaluate the Voigt. + + Returns + ------- + np.ndarray + The intensity of the Voigt at the given x values. """ x = self._prepare_x_for_evaluate(x) @@ -208,10 +248,13 @@ def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) ) def __repr__(self) -> str: - """Return a string representation of the Voigt. + """ + Return a string representation of the Voigt. - Returns: - str: A string representation of the Voigt. + Returns + ------- + str + A string representation of the Voigt. """ return f'Voigt(unique_name = {self.unique_name}, unit = {self._unit},\n \ 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 f9aa17d3..b3872d9a 100644 --- a/src/easydynamics/sample_model/diffusion_model/brownian_translational_diffusion.py +++ b/src/easydynamics/sample_model/diffusion_model/brownian_translational_diffusion.py @@ -18,24 +18,20 @@ class BrownianTranslationalDiffusion(DiffusionModelBase): - r"""Model of Brownian translational diffusion, consisting of a - Lorentzian function for each Q-value, where the width is given by $D - Q^2$, where $D$ is the diffusion coefficient. The area of the - Lorentzians is given by the scale parameter multiplied by the QISF, - which is 1 for this model. The EISF is 0 for this model, so there is - no delta function component. Q is assumed to have units of - 1/angstrom. Creates ComponentCollections with Lorentzian components - for given Q-values. - - Example: - >>>Q=np.linspace(0.5,2,7) - >>>energy=np.linspace(-2, 2, 501) - >>>scale=1.0 + r""" + Model of Brownian translational diffusion, consisting of a Lorentzian function for each + Q-value, where the width is given by $D Q^2$, where $D$ is the diffusion coefficient. The area + of the Lorentzians is given by the scale parameter multiplied by the QISF, which is 1 for this + model. The EISF is 0 for this model, so there is no delta function component. Q is assumed to + have units of 1/angstrom. Creates ComponentCollections with Lorentzian components for given + Q-values. + + Example: >>>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) - See also the tutorials. + >>>component_collections=diffusion_model.create_component_collections(Q) See also the + tutorials. """ def __init__( @@ -46,23 +42,27 @@ def __init__( scale: Numeric = 1.0, diffusion_coefficient: Numeric = 1.0, ) -> None: - """Initialize a new BrownianTranslationalDiffusion model. - - Args: - 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 - 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. + """ + Initialize a new BrownianTranslationalDiffusion model. + + Parameters + ---------- + 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. By + default, None. + 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. """ if not isinstance(scale, Numeric): raise TypeError('scale must be a number.') @@ -93,24 +93,32 @@ def __init__( @property def diffusion_coefficient(self) -> Parameter: - """Get the diffusion coefficient parameter D. + """ + Get the diffusion coefficient parameter D. - Returns: - Parameter: Diffusion coefficient D in m^2/s. + Returns + ------- + Parameter + Diffusion coefficient D in m^2/s. """ return self._diffusion_coefficient @diffusion_coefficient.setter def diffusion_coefficient(self, diffusion_coefficient: Numeric) -> None: - """Set the diffusion coefficient parameter D. - - Args: - diffusion_coefficient (Numeric): The new value for the - diffusion coefficient D in m^2/s. - - Raises: - TypeError: If diffusion_coefficient is not a number. - ValueError: If diffusion_coefficient is negative. + """ + Set the diffusion coefficient parameter D. + + Parameters + ---------- + diffusion_coefficient : Numeric + The new value for the diffusion coefficient D in m^2/s. + + Raises + ------ + TypeError + If diffusion_coefficient is not a number. + ValueError + If diffusion_coefficient is negative. """ if not isinstance(diffusion_coefficient, Numeric): raise TypeError('diffusion_coefficient must be a number.') @@ -124,15 +132,18 @@ def diffusion_coefficient(self, diffusion_coefficient: Numeric) -> None: # ------------------------------------------------------------------ def calculate_width(self, Q: Q_type) -> np.ndarray: - """Calculate the half-width at half-maximum (HWHM) for the - diffusion model. + """ + Calculate the half-width at half-maximum (HWHM) for the diffusion model. - Args: - Q (Q_type): Scattering vector in 1/angstrom + Parameters + ---------- + Q : Q_type + Scattering vector in 1/angstrom. - Returns: - np.ndarray: HWHM values in the unit of the model - (e.g., meV). + Returns + ------- + np.ndarray + HWHM values in the unit of the model (e.g., meV). """ Q = _validate_and_convert_Q(Q) @@ -142,28 +153,36 @@ def calculate_width(self, Q: Q_type) -> np.ndarray: return Q**2 * unit_conversion_factor.value def calculate_EISF(self, Q: Q_type) -> np.ndarray: - """Calculate the Elastic Incoherent Structure Factor (EISF) for - the Brownian translational diffusion model. + """ + Calculate the Elastic Incoherent Structure Factor (EISF) for the Brownian translational + diffusion model. - Args: - Q (Q_type): Scattering - vector in 1/angstrom + Parameters + ---------- + Q : Q_type + Scattering vector in 1/angstrom. - Returns: - np.ndarray: EISF values (dimensionless). + Returns + ------- + np.ndarray + EISF values (dimensionless). """ Q = _validate_and_convert_Q(Q) return np.zeros_like(Q) def calculate_QISF(self, Q: Q_type) -> np.ndarray: - """Calculate the Quasi-Elastic Incoherent Structure Factor - (QISF). + """ + Calculate the Quasi-Elastic Incoherent Structure Factor (QISF). - Args: - Q (Q_type): Scattering vector in 1/angstrom + Parameters + ---------- + Q : Q_type + Scattering vector in 1/angstrom. - Returns: - np.ndarray: QISF values (dimensionless). + Returns + ------- + np.ndarray + QISF values (dimensionless). """ Q = _validate_and_convert_Q(Q) @@ -174,23 +193,28 @@ def create_component_collections( Q: Q_type, component_display_name: str = 'Brownian diffusion', ) -> list[ComponentCollection]: - r"""Create ComponentCollection components for the Brownian - translational diffusion model at given Q values. - - Args: - 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 - Lorentzian components for each Q value. Each Lorentzian - has a width given by $D*Q^2$ and an area given by the - scale parameter multiplied by the QISF (which is 1 for - this model). - - Raises: - TypeError: If component_display_name is not a string. + r""" + Create ComponentCollection components for the Brownian translational diffusion model at + given Q values. + + Parameters + ---------- + Q : Q_type + Scattering vector values. + component_display_name : str, default='Brownian diffusion' + Name of the Lorentzian component. + + Raises + ------ + TypeError + If component_display_name is not a string. + + Returns + ------- + list[ComponentCollection] + List of ComponentCollections with Lorentzian components for each Q value. Each + Lorentzian has a width given by $D*Q^2$ and an area given by the scale parameter + multiplied by the QISF (which is 1 for this model). """ Q = _validate_and_convert_Q(Q) @@ -241,17 +265,24 @@ def create_component_collections( # ------------------------------------------------------------------ def _write_width_dependency_expression(self, Q: float) -> str: - """Write the dependency expression for the width as a function - of Q to make dependent Parameters. + """ + Write the dependency expression for the width as a function of Q to make dependent + Parameters. - Args: - Q (float): Scattering vector in 1/angstrom. + Parameters + ---------- + Q : float + Scattering vector in 1/angstrom. - Returns: - str: Dependency expression for the width. + Raises + ------ + TypeError + If Q is not a float. - Raises: - TypeError: If Q is not a float. + Returns + ------- + str + Dependency expression for the width. """ if not isinstance(Q, (float)): raise TypeError('Q must be a float.') @@ -260,11 +291,13 @@ def _write_width_dependency_expression(self, Q: float) -> str: 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 - Parameters. + """ + Write the dependency map expression to make dependent Parameters. - Returns: - dict[str, DescriptorNumber]: Dependency map for the width. + Returns + ------- + dict[str, DescriptorNumber] + Dependency map for the width. """ return { 'D': self.diffusion_coefficient, @@ -273,17 +306,23 @@ def _write_width_dependency_map_expression(self) -> dict[str, DescriptorNumber]: } def _write_area_dependency_expression(self, QISF: float) -> str: - """Write the dependency expression for the area to make - dependent Parameters. - - Args: - QISF (float): Quasielastic Incoherent Scattering Function. - - Returns: - str: Dependency expression for the area. - - Raises: - TypeError: If QISF is not a float. + """ + Write the dependency expression for the area to make dependent Parameters. + + Parameters + ---------- + QISF : float + Quasielastic Incoherent Scattering Function. + + Raises + ------ + TypeError + If QISF is not a float. + + Returns + ------- + str + Dependency expression for the area. """ if not isinstance(QISF, (float)): raise TypeError('QISF must be a float.') @@ -291,11 +330,13 @@ def _write_area_dependency_expression(self, QISF: float) -> str: return f'{QISF} * scale' def _write_area_dependency_map_expression(self) -> dict[str, DescriptorNumber]: - """Write the dependency map expression to make dependent - Parameters. + """ + Write the dependency map expression to make dependent Parameters. - Returns: - dict[str, DescriptorNumber]: Dependency map for the area. + Returns + ------- + dict[str, DescriptorNumber] + Dependency map for the area. """ return { 'scale': self.scale, @@ -306,12 +347,13 @@ def _write_area_dependency_map_expression(self) -> dict[str, DescriptorNumber]: # ------------------------------------------------------------------ def __repr__(self) -> str: - """String representation of the BrownianTranslationalDiffusion - model. + """ + String representation of the BrownianTranslationalDiffusion model. - Returns: - str: String representation of the - BrownianTranslationalDiffusion model. + Returns + ------- + str + String representation of the BrownianTranslationalDiffusion model. """ return ( f'BrownianTranslationalDiffusion(display_name={self.display_name},' 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 6360ca52..177d5e79 100644 --- a/src/easydynamics/sample_model/diffusion_model/diffusion_model_base.py +++ b/src/easydynamics/sample_model/diffusion_model/diffusion_model_base.py @@ -20,22 +20,27 @@ def __init__( scale: Numeric = 1.0, unit: str | sc.Unit = 'meV', ) -> None: - """Initialize a new DiffusionModel. - - Args: - 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 - 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. - UnitError: If unit is not a string or scipp Unit, or if it - cannot be converted to meV. + """ + Initialize a new DiffusionModel. + + Parameters + ---------- + 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. By + default, None. + 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. + UnitError + If unit is not a string or scipp Unit, or if it cannot be converted to meV. """ try: @@ -61,24 +66,32 @@ def __init__( @property def unit(self) -> str | sc.Unit | None: - """Get the unit of the energy axis of the DiffusionModel. + """ + Get the unit of the energy axis of the DiffusionModel. - Returns: - str | sc.Unit | None: Unit of the DiffusionModel. + Returns + ------- + str | sc.Unit | None + Unit of the DiffusionModel. """ return str(self._unit) @unit.setter def unit(self, _unit_str: str) -> None: - """The unit of the energy axis is read-only. To change the unit, - use convert_unit or create a new DiffusionModel with the desired - unit. + """ + The unit of the energy axis is read-only. - Args: - _unit_str (str): The new unit to set (ignored) + To change the unit, use convert_unit or create a new DiffusionModel with the desired unit. - Raises: - AttributeError: Always, since the unit is read-only. + Parameters + ---------- + _unit_str : str + The new unit to set (ignored). + + Raises + ------ + AttributeError + Always, since the unit is read-only. """ raise AttributeError( f'Unit is read-only. Use convert_unit to change the unit between allowed types ' @@ -87,24 +100,32 @@ def unit(self, _unit_str: str) -> None: @property def scale(self) -> Parameter: - """Get the scale parameter of the diffusion model. + """ + Get the scale parameter of the diffusion model. - Returns: - Parameter: scale parameter of the diffusion model + Returns + ------- + Parameter + Scale parameter of the diffusion model. """ return self._scale @scale.setter def scale(self, scale: Numeric) -> None: - """Set the scale parameter of the diffusion model. - - Args: - scale (Numeric): The new value for the scale parameter. Must - be a non-negative number. - - Raises: - TypeError: If scale is not a number. - ValueError: If scale is negative. + """ + Set the scale parameter of the diffusion model. + + Parameters + ---------- + scale : Numeric + The new value for the scale parameter. Must be a non-negative number. + + Raises + ------ + TypeError + If scale is not a number. + ValueError + If scale is negative. """ if not isinstance(scale, Numeric): raise TypeError('scale must be a number.') @@ -118,9 +139,12 @@ def scale(self, scale: Numeric) -> None: # ------------------------------------------------------------------ def __repr__(self) -> str: - """String representation of the Diffusion model. + """ + String representation of the Diffusion model. - Returns: - str: String representation of the DiffusionModel. + Returns + ------- + str + String representation of the DiffusionModel. """ 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 888a0ad5..346ce152 100644 --- a/src/easydynamics/sample_model/diffusion_model/jump_translational_diffusion.py +++ b/src/easydynamics/sample_model/diffusion_model/jump_translational_diffusion.py @@ -18,29 +18,22 @@ class JumpTranslationalDiffusion(DiffusionModelBase): - r"""Model of Jump translational diffusion. The model consists of a - Lorentzian function for each Q-value, where the width is given by - - $$ - \Gamma(Q) = \frac{Q^2}{1+D t Q^2}. - $$ - - where $D$ is the diffusion coefficient and $t$ is the relaxation - time. Q is assumed to have units of 1/angstrom. Creates - ComponentCollections with Lorentzian components for given Q-values. - - Example: - >>> Q = np.linspace(0.5, 2, 7) - >>> energy = np.linspace(-2, 2, 501) - >>> scale = 1.0 - >>> diffusion_coefficient = 2.4e-9 # m^2/s - >>> relaxation_time = 1.0 # ps - >>> diffusion_model=JumpTranslationalDiffusion( - >>> scale = scale, diffusion_coefficient = (diffusion_coefficient,) - >>> relaxation_time=relaxation_time) - >>> component_collections= - >>> diffusion_model.create_component_collections(Q) - See also the tutorials.. + r""" + Model of Jump translational diffusion. + + The model consists of a Lorentzian function for each Q-value, where the width is given by + + $$ \Gamma(Q) = \frac{Q^2}{1+D t Q^2}. $$ + + where $D$ is the diffusion coefficient and $t$ is the relaxation time. Q is assumed to have + units of 1/angstrom. Creates ComponentCollections with Lorentzian components for given + Q-values. + + Example: >>> Q = np.linspace(0.5, 2, 7) >>> energy = np.linspace(-2, 2, 501) >>> scale = 1.0 + >>> diffusion_coefficient = 2.4e-9 # m^2/s >>> relaxation_time = 1.0 # ps >>> + diffusion_model=JumpTranslationalDiffusion( >>> scale = scale, diffusion_coefficient = + (diffusion_coefficient,) >>> relaxation_time=relaxation_time) >>> component_collections= >>> + diffusion_model.create_component_collections(Q) See also the tutorials.. """ def __init__( @@ -52,24 +45,29 @@ def __init__( diffusion_coefficient: Numeric = 1.0, relaxation_time: Numeric = 1.0, ) -> None: - """Initialize a new JumpTranslationalDiffusion model. - - Args: - 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 - 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. + """ + Initialize a new JumpTranslationalDiffusion model. + + Parameters + ---------- + 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. By + default, None. + 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. """ super().__init__( display_name=display_name, @@ -109,24 +107,32 @@ def __init__( @property def diffusion_coefficient(self) -> Parameter: - """Get the diffusion coefficient parameter D. + """ + Get the diffusion coefficient parameter D. - Returns: - Parameter: Diffusion coefficient D. + Returns + ------- + Parameter + Diffusion coefficient D. """ return self._diffusion_coefficient @diffusion_coefficient.setter def diffusion_coefficient(self, diffusion_coefficient: Numeric) -> None: - """Set the diffusion coefficient parameter D. - - Args: - diffusion_coefficient (Numeric): Diffusion coefficient D in - m^2/s. - - Raises: - TypeError: If diffusion_coefficient is not a number. - ValueError: If diffusion_coefficient is negative. + """ + Set the diffusion coefficient parameter D. + + Parameters + ---------- + diffusion_coefficient : Numeric + Diffusion coefficient D in m^2/s. + + Raises + ------ + TypeError + If diffusion_coefficient is not a number. + ValueError + If diffusion_coefficient is negative. """ if not isinstance(diffusion_coefficient, Numeric): raise TypeError('diffusion_coefficient must be a number.') @@ -136,23 +142,32 @@ def diffusion_coefficient(self, diffusion_coefficient: Numeric) -> None: @property def relaxation_time(self) -> Parameter: - """Get the relaxation time parameter t. + """ + Get the relaxation time parameter t. - Returns: - Parameter: Relaxation time t in ps. + Returns + ------- + Parameter + Relaxation time t in ps. """ return self._relaxation_time @relaxation_time.setter def relaxation_time(self, relaxation_time: Numeric) -> None: - """Set the relaxation time parameter t. - - Args: - relaxation_time (Numeric): Relaxation time t in ps. - - Raises: - TypeError: If relaxation_time is not a number. - ValueError: If relaxation_time is negative. + """ + Set the relaxation time parameter t. + + Parameters + ---------- + relaxation_time : Numeric + Relaxation time t in ps. + + Raises + ------ + TypeError + If relaxation_time is not a number. + ValueError + If relaxation_time is negative. """ if not isinstance(relaxation_time, Numeric): raise TypeError('relaxation_time must be a number.') @@ -166,17 +181,19 @@ def relaxation_time(self, relaxation_time: Numeric) -> None: ################################ def calculate_width(self, Q: Q_type) -> np.ndarray: - r"""Calculate the half-width at half-maximum (HWHM) for the - diffusion model. $\Gamma(Q) = Q^2/(1+D t Q^2)$, where $D$ is the - diffusion coefficient and $t$ is the relaxation time. - - Args: - Q (Q_type): Scattering vector in 1/angstrom. Can be a single - value or an array of values. - - Returns: - np.ndarray: HWHM values in the unit of the model (e.g., - meV). + r""" + Calculate the half-width at half-maximum (HWHM) for the diffusion model. $\Gamma(Q) = + Q^2/(1+D t Q^2)$, where $D$ is the diffusion coefficient and $t$ is the relaxation time. + + Parameters + ---------- + Q : Q_type + Scattering vector in 1/angstrom. Can be a single value or an array of values. + + Returns + ------- + np.ndarray + HWHM values in the unit of the model (e.g., meV). """ Q = _validate_and_convert_Q(Q) @@ -198,27 +215,35 @@ def calculate_width(self, Q: Q_type) -> np.ndarray: return numerator / denominator def calculate_EISF(self, Q: Q_type) -> np.ndarray: - """Calculate the Elastic Incoherent Structure Factor (EISF). + """ + Calculate the Elastic Incoherent Structure Factor (EISF). - Args: - Q (Q_type): Scattering vector in 1/angstrom. Can be a single - value or an array of values. + Parameters + ---------- + Q : Q_type + Scattering vector in 1/angstrom. Can be a single value or an array of values. - Returns: - np.ndarray: EISF values (dimensionless). + Returns + ------- + np.ndarray + EISF values (dimensionless). """ Q = _validate_and_convert_Q(Q) return np.zeros_like(Q) def calculate_QISF(self, Q: Q_type) -> np.ndarray: - """Calculate the Quasi-Elastic Incoherent Structure Factor - (QISF). - - Args: - Q (Q_type): Scattering vector in 1/angstrom. Can be a single - value or an array of values. - Returns: - np.ndarray: QISF values (dimensionless). + """ + Calculate the Quasi-Elastic Incoherent Structure Factor (QISF). + + Parameters + ---------- + Q : Q_type + Scattering vector in 1/angstrom. Can be a single value or an array of values. + + Returns + ------- + np.ndarray + QISF values (dimensionless). """ Q = _validate_and_convert_Q(Q) return np.ones_like(Q) @@ -228,21 +253,25 @@ def create_component_collections( Q: Q_type, component_display_name: str = 'Jump translational diffusion', ) -> list[ComponentCollection]: - """Create ComponentCollection components for the diffusion model - at given Q values. - - 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. - - Returns: - list[ComponentCollection]: List of ComponentCollections with - Jump Diffusion Lorentzian components. - - Raises: - TypeError: If component_display_name is not a string. + """ + Create ComponentCollection components for the diffusion model at given Q values. + + Parameters + ---------- + 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. + + Raises + ------ + TypeError + If component_display_name is not a string. + + Returns + ------- + list[ComponentCollection] + List of ComponentCollections with Jump Diffusion Lorentzian components. """ Q = _validate_and_convert_Q(Q) @@ -293,17 +322,24 @@ def create_component_collections( ################################ def _write_width_dependency_expression(self, Q: float) -> str: - """Write the dependency expression for the width as a function - of Q to make dependent Parameters. + """ + Write the dependency expression for the width as a function of Q to make dependent + Parameters. - Args: - Q (float): Scattering vector in 1/angstrom + Parameters + ---------- + Q : float + Scattering vector in 1/angstrom. - Returns: - str: Dependency expression for the width. + Raises + ------ + TypeError + If Q is not a float. - Raises: - TypeError: If Q is not a float. + Returns + ------- + str + Dependency expression for the width. """ if not isinstance(Q, (float)): raise TypeError('Q must be a float.') @@ -312,11 +348,13 @@ def _write_width_dependency_expression(self, Q: float) -> str: 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 - Parameters. + """ + Write the dependency map expression to make dependent Parameters. - Returns: - dict[str, DescriptorNumber]: Dependency map for the width. + Returns + ------- + dict[str, DescriptorNumber] + Dependency map for the width. """ return { 'D': self._diffusion_coefficient, @@ -326,17 +364,23 @@ def _write_width_dependency_map_expression(self) -> dict[str, DescriptorNumber]: } def _write_area_dependency_expression(self, QISF: float) -> str: - """Write the dependency expression for the area to make - dependent Parameters. - - Args: - QISF (float): Q-dependent intermediate scattering function. - - Returns: - str: Dependency expression for the area. - - Raises: - TypeError: If QISF is not a float. + """ + Write the dependency expression for the area to make dependent Parameters. + + Parameters + ---------- + QISF : float + Q-dependent intermediate scattering function. + + Raises + ------ + TypeError + If QISF is not a float. + + Returns + ------- + str + Dependency expression for the area. """ if not isinstance(QISF, (float)): @@ -345,11 +389,13 @@ def _write_area_dependency_expression(self, QISF: float) -> str: return f'{QISF} * scale' def _write_area_dependency_map_expression(self) -> dict[str, DescriptorNumber]: - """Write the dependency map expression to make dependent - Parameters. + """ + Write the dependency map expression to make dependent Parameters. - Returns: - dict[str, DescriptorNumber]: Dependency map for the area. + Returns + ------- + dict[str, DescriptorNumber] + Dependency map for the area. """ return { 'scale': self._scale, @@ -360,12 +406,13 @@ def _write_area_dependency_map_expression(self) -> dict[str, DescriptorNumber]: ################################ def __repr__(self) -> str: - """String representation of the JumpTranslationalDiffusion - model. + """ + String representation of the JumpTranslationalDiffusion model. - Returns: - str: String representation of the JumpTranslationalDiffusion - model. + Returns + ------- + str + String representation of the JumpTranslationalDiffusion model. """ return ( f'JumpTranslationalDiffusion(display_name={self.display_name}, ' diff --git a/src/easydynamics/sample_model/instrument_model.py b/src/easydynamics/sample_model/instrument_model.py index 78815553..00cf2544 100644 --- a/src/easydynamics/sample_model/instrument_model.py +++ b/src/easydynamics/sample_model/instrument_model.py @@ -17,11 +17,11 @@ class InstrumentModel(NewBase): - """InstrumentModel represents a model of the instrument in an - experiment at various Q. + """ + 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. + It can contain a model of the resolution function for convolutions, of the background and an + offset in the energy axis. """ def __init__( @@ -34,30 +34,34 @@ def __init__( energy_offset: Numeric | None = None, unit: str | sc.Unit = 'meV', ) -> None: - """Initialize an InstrumentModel. - - Args: - display_name (str, default="MyInstrumentModel"): The display name of the - InstrumentModel. Default is "MyInstrumentModel". - 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. - 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. - 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. - unit (str | sc.Unit, default="meV"): The unit of the energy axis. - - Raises: - TypeError: If resolution_model is not a ResolutionModel or - None, or if background_model is not a BackgroundModel or None, or - if energy_offset is not a number or None. + """ + Initialize an InstrumentModel. + + Parameters + ---------- + display_name : str, default='MyInstrumentModel' + The display name of the InstrumentModel. + 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. + 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. + 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. + unit : str | sc.Unit, default='meV' + The unit of the energy axis. + + Raises + ------ + TypeError + If resolution_model is not a ResolutionModel or 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,23 +111,30 @@ def __init__( @property def resolution_model(self) -> ResolutionModel: - """Get the resolution model of the instrument. + """ + Get the resolution model of the instrument. - Returns: - ResolutionModel: The resolution model of the instrument. + Returns + ------- + ResolutionModel + The resolution model of the instrument. """ return self._resolution_model @resolution_model.setter def resolution_model(self, value: ResolutionModel) -> None: - """Set the resolution model of the instrument. + """ + Set the resolution model of the instrument. - Args: - value (ResolutionModel): The new resolution model of the - instrument. + Parameters + ---------- + value : ResolutionModel + The new resolution model of the instrument. - Raises: - TypeError: If value is not a ResolutionModel. + Raises + ------ + TypeError + If value is not a ResolutionModel. """ if not isinstance(value, ResolutionModel): raise TypeError( @@ -134,24 +145,31 @@ def resolution_model(self, value: ResolutionModel) -> None: @property def background_model(self) -> BackgroundModel: - """Get the background model of the instrument. + """ + Get the background model of the instrument. - Returns: - BackgroundModel: The background model of the instrument. + Returns + ------- + BackgroundModel + The background model of the instrument. """ return self._background_model @background_model.setter def background_model(self, value: BackgroundModel) -> None: - """Set the background model of the instrument. + """ + Set the background model of the instrument. - Args: - value (BackgroundModel): The new background model of the - instrument. + Parameters + ---------- + value : BackgroundModel + The new background model of the instrument. - Raises: - TypeError: If value is not a BackgroundModel. + Raises + ------ + TypeError + If value is not a BackgroundModel. """ if not isinstance(value, BackgroundModel): @@ -163,28 +181,34 @@ def background_model(self, value: BackgroundModel) -> None: @property def Q(self) -> np.ndarray | None: - """Get the Q values of the InstrumentModel. + """ + Get the Q values of the InstrumentModel. - Returns: - np.ndarray | None: The Q values of the InstrumentModel, or - None if not set + Returns + ------- + np.ndarray | None + The Q values of the InstrumentModel, or None if not set. """ return self._Q @Q.setter def Q(self, value: Q_type | None) -> None: - """Set the Q values of the InstrumentModel. If Q is already set, - it raises an error if the new Q values are not similar to the - old ones to prevent accidental changes to the background and - resolution models. To change Q values, first run clear_Q(). + """ + Set the Q values of the InstrumentModel. + + If Q is already set, it raises an error if the new Q values are not similar to the old ones + to prevent accidental changes to the background and resolution models. To change Q values, + first run clear_Q(). - Args: - value (Q_type | None): The new Q values to set. - If None, Q values are not changed. + Parameters + ---------- + value : Q_type | None + The new Q values to set. If None, Q values are not changed. - Raises: - ValueError: If the new Q values are not similar to the old - ones when Q is not None + Raises + ------ + ValueError + If the new Q values are not similar to the old ones when Q is not None. """ if value is None: return @@ -204,26 +228,33 @@ def Q(self, value: Q_type | None) -> None: @property def unit(self) -> str | sc.Unit: - """Get the unit of the InstrumentModel. + """ + Get the unit of the InstrumentModel. - Returns: - str | sc.Unit: The unit of the InstrumentModel. + Returns + ------- + str | sc.Unit + The unit of the InstrumentModel. """ return self._unit @unit.setter def unit(self, _unit_str: str) -> None: - """Set the unit of the InstrumentModel. The unit is read-only - and cannot be set directly. Use convert_unit to change the unit - between allowed types or create a new InstrumentModel with the - desired unit. + """ + Set the unit of the InstrumentModel. - Args: - _unit_str (str): The new unit for the InstrumentModel - (ignored) + The unit is read-only and cannot be set directly. Use convert_unit to change the unit + between allowed types or create a new InstrumentModel with the desired unit. - Raises: - AttributeError: Always, as the unit is read-only. + Parameters + ---------- + _unit_str : str + The new unit for the InstrumentModel (ignored). + + Raises + ------ + AttributeError + Always, as the unit is read-only. """ raise AttributeError( f'Unit is read-only. Use convert_unit to change the unit between allowed types ' @@ -232,25 +263,30 @@ def unit(self, _unit_str: str) -> None: @property def energy_offset(self) -> Parameter: - """Get the energy offset template parameter of the instrument - model. + """ + Get the energy offset template parameter of the instrument model. - Returns: - Parameter: The energy offset template parameter of the - instrument model. + Returns + ------- + Parameter + The energy offset template parameter of the instrument model. """ return self._energy_offset @energy_offset.setter def energy_offset(self, value: Numeric) -> None: - """Set the offset parameter of the instrument model. + """ + Set the offset parameter of the instrument model. - Args: - value (Numeric): The new value for the energy offset - parameter. Will be copied to all Q values. + Parameters + ---------- + 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. + 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__}') @@ -263,16 +299,19 @@ def energy_offset(self, value: Numeric) -> None: # -------------------------------------------------------------- def clear_Q(self, confirm: bool = False) -> None: - """Clear the Q values of the InstrumentModel and any associated - ResolutionModel and BackgroundModel, removing all component - collections and their associated Parameters. - - Args: - confirm (bool, default=False): Confirmation to clear Q - values. - - Raises: - ValueError: If confirm is not True. + """ + Clear the Q values of the InstrumentModel and any associated ResolutionModel and + BackgroundModel, removing all component collections and their associated Parameters. + + Parameters + ---------- + confirm : bool, default=False + Confirmation to clear Q values. + + Raises + ------ + ValueError + If confirm is not True. """ if not confirm: raise ValueError( @@ -284,14 +323,18 @@ def clear_Q(self, confirm: bool = False) -> None: self._on_Q_change() def convert_unit(self, unit_str: str | sc.Unit) -> None: - """Convert the unit of the InstrumentModel. + """ + Convert the unit of the InstrumentModel. - Args: - unit_str (str | sc.Unit): The unit to convert to. + Parameters + ---------- + unit_str : str | sc.Unit + The unit to convert to. - Raises: - ValueError: If unit_str is not a valid unit string or - scipp Unit. + Raises + ------ + ValueError + If unit_str is not a valid unit string or scipp Unit. """ unit = _validate_unit(unit_str) if unit is None: @@ -306,23 +349,28 @@ def convert_unit(self, unit_str: str | sc.Unit) -> None: self._unit = unit def get_all_variables(self, Q_index: int | None = None) -> list[Parameter]: - """Get all variables in the InstrumentModel. - - Args: - 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: - list[Parameter]: A list of all variables in the - InstrumentModel. If Q_index is specified, only variables - from the ComponentCollection at the given Q index are - included. Otherwise, all variables in the - InstrumentModel are included. - - Raises: - TypeError: If Q_index is not an int or None. - IndexError: If Q_index is out of bounds for the Q values in - the InstrumentModel. + """ + Get all variables in the InstrumentModel. + + Parameters + ---------- + Q_index : int | None, default=None + The index of the Q value to get variables for. If None, get variables for all Q values. + + + Raises + ------ + TypeError + If Q_index is not an int or None. + IndexError + If Q_index is out of bounds for the Q values in the InstrumentModel. + + Returns + ------- + list[Parameter] + A list of all variables in the InstrumentModel. If Q_index is specified, only variables + from the ComponentCollection at the given Q index are included. Otherwise, all + variables in the InstrumentModel are included. """ if self._Q is None: return [] @@ -359,20 +407,29 @@ def get_energy_offset( self, Q_index: int | None = None, ) -> Parameter | list[Parameter]: - """Get the energy offset Parameter at a specific Q index. - - Args: - Q_index (int | None, default=None): The index of the Q value to get the energy - offset for. If None, get the energy offset for all Q values. - - Returns: - Parameter | list[Parameter]: The energy offset Parameter at the specified Q - index, or a list of Parameters if Q_index is None. - - Raises: - ValueError: If no Q values are set in the InstrumentModel. - IndexError: If Q_index is out of bounds. - TypeError: If Q_index is not an int or None. + """ + Get the energy offset Parameter at a specific Q index. + + Parameters + ---------- + Q_index : int | None, default=None + The index of the Q value to get the energy offset for. If None, get the energy offset + for all Q values. + + Raises + ------ + ValueError + If no Q values are set in the InstrumentModel. + IndexError + If Q_index is out of bounds. + TypeError + If Q_index is not an int or None. + + Returns + ------- + Parameter | list[Parameter] + The energy offset Parameter at the specified Q index, or a list of Parameters if + Q_index is None. """ if self._Q is None: raise ValueError('No Q values are set in the InstrumentModel.') @@ -389,26 +446,32 @@ def get_energy_offset( return self._energy_offsets[Q_index] def fix_energy_offset(self, Q_index: int | None = None) -> None: - """Fix energy offset parameters. If Q_index is specified, only - fix the energy offset for that Q value. If Q_index is None, fix - energy offsets for all Q values. + """ + Fix energy offset parameters. + + If Q_index is specified, only fix the energy offset for that Q value. If Q_index is None, + fix energy offsets for all Q values. - Args: - Q_index (int | None, default=None): The index of the Q value - to fix the energy offset for. If None, fix energy - offsets for all Q values. + Parameters + ---------- + Q_index : int | None, default=None + The index of the Q value to fix the energy offset for. If None, fix energy offsets for + all Q values. """ self._fix_or_free_energy_offset(Q_index, fixed=True) def free_energy_offset(self, Q_index: int | None = None) -> None: - """Free energy offset parameters. If Q_index is specified, only - free the energy offset for that Q value. If Q_index is None, + """ + Free energy offset parameters. + + If Q_index is specified, only free the energy offset for that Q value. If Q_index is None, free energy offsets for all Q values. - Args: - Q_index (int | None, default=None): The index of the Q value - to free the energy offset for. If None, free energy - offsets for all Q values. + Parameters + ---------- + Q_index : int | None, default=None + The index of the Q value to free the energy offset for. If None, free energy offsets + for all Q values. """ self._fix_or_free_energy_offset(Q_index, fixed=False) @@ -416,21 +479,26 @@ def free_energy_offset(self, Q_index: int | None = None) -> None: # Private methods # -------------------------------------------------------------- def _fix_or_free_energy_offset(self, Q_index: int | None = None, fixed: bool = True) -> None: - """Fix or free energy offset parameters. If Q_index is - specified, only fix or free the energy offset for that Q value. - If Q_index is None, fix or free energy offsets for all Q values. - - Args: - Q_index (int | None, default=None): The index of the Q value - to fix or free the energy offset for. If None, fix or - free energy offsets for all Q values. - fixed (bool, default=True): Whether to fix (True) or free - (False) the energy offset. - - Raises: - TypeError: If Q_index is not an int or None. - IndexError: If Q_index is out of bounds for the Q values in - the InstrumentModel. + """ + Fix or free energy offset parameters. + + If Q_index is specified, only fix or free the energy offset for that Q value. If Q_index is + None, fix or free energy offsets for all Q values. + + Parameters + ---------- + Q_index : int | None, default=None + The index of the Q value to fix or free the energy offset for. If None, fix or free + energy offsets for all Q values. + fixed : bool, default=True + Whether to fix (True) or free (False) the energy offset. + + Raises + ------ + TypeError + If Q_index is not an int or None. + IndexError + If Q_index is out of bounds for the Q values in the InstrumentModel. """ if Q_index is None: @@ -478,10 +546,13 @@ def _on_background_model_change(self) -> None: # ------------------------------------------------------------- def __repr__(self) -> str: - """Return a string representation of the InstrumentModel. + """ + Return a string representation of the InstrumentModel. - Returns: - str: A string representation of the InstrumentModel. + Returns + ------- + str + A string representation of the InstrumentModel. """ return ( diff --git a/src/easydynamics/sample_model/model_base.py b/src/easydynamics/sample_model/model_base.py index 0bb95860..1b5d9363 100644 --- a/src/easydynamics/sample_model/model_base.py +++ b/src/easydynamics/sample_model/model_base.py @@ -17,10 +17,10 @@ class ModelBase(EasyScienceModelBase): - """Base class for Sample Models. + """ + 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__( @@ -31,22 +31,27 @@ def __init__( components: ModelComponent | ComponentCollection | None = None, Q: Q_type | None = None, ) -> None: - """Initialize the ModelBase. - - Args: - 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, 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, default=None): Q values for the model. - If None, Q is not set. - - Raises: - TypeError: If components is not a ModelComponent or ComponentCollection. + """ + Initialize the ModelBase. + + Parameters + ---------- + 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, 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, 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, @@ -72,23 +77,26 @@ def __init__( def evaluate( self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray ) -> list[np.ndarray]: - """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. - - Returns: - list[np.ndarray]: A list of numpy arrays containing the - evaluated model values for each Q. The length of the - list will match the number of Q values in the model. - - Raises: - ValueError: If there are no components in the model to - evaluate. + """ + Evaluate the sample model at all Q for the given x values. + + Parameters + ---------- + 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. + + Raises + ------ + ValueError + If there are no components in the model to evaluate. + + Returns + ------- + list[np.ndarray] + A list of numpy arrays containing the evaluated model values for each Q. The length of + the list will match the number of Q values in the model. """ if not self._component_collections: @@ -102,23 +110,25 @@ def evaluate( # Component management # ------------------------------------------------------------------ def append_component(self, component: ModelComponent | ComponentCollection) -> None: - """Append a ModelComponent or ComponentCollection to the - SampleModel. + """ + Append a ModelComponent or ComponentCollection to the SampleModel. - Args: - component (ModelComponent | ComponentCollection): The - ModelComponent or ComponentCollection to append. + Parameters + ---------- + component : ModelComponent | ComponentCollection + The ModelComponent or ComponentCollection to append. """ self._components.append_component(component) self._on_components_change() def remove_component(self, unique_name: str) -> None: - """Remove a ModelComponent from the SampleModel by its unique - name. + """ + Remove a ModelComponent from the SampleModel by its unique name. - Args: - unique_name (str): The unique name of the ModelComponent - to remove. + Parameters + ---------- + unique_name : str + The unique name of the ModelComponent to remove. """ self._components.remove_component(unique_name) self._on_components_change() @@ -134,24 +144,31 @@ def clear_components(self) -> None: @property def unit(self) -> str | sc.Unit | None: - """Get the unit of the ComponentCollection. + """ + Get the unit of the ComponentCollection. - Returns: - str | sc.Unit | None: The unit of the ComponentCollection. + Returns + ------- + str | sc.Unit | None + The unit of the ComponentCollection. """ return self._unit @unit.setter def unit(self, _unit_str: str) -> None: - """Unit is read-only and cannot be set directly. + """ + Unit is read-only and cannot be set directly. - Args: - _unit_str (str): The new unit to set (ignored). + Parameters + ---------- + _unit_str : str + The new unit to set (ignored). - Raises: - AttributeError: Always raised to indicate that the unit is - read-only. + Raises + ------ + AttributeError + Always raised to indicate that the unit is read-only. """ raise AttributeError( f'Unit is read-only. Use convert_unit to change the unit between allowed types ' @@ -159,16 +176,20 @@ def unit(self, _unit_str: str) -> None: ) # noqa: E501 def convert_unit(self, unit: str | sc.Unit) -> None: - """Convert the unit of the ComponentCollection and all its - components. - - Args: - unit (str | sc.Unit): The new unit to convert to. - - Raises: - TypeError: If the provided unit is not a string or sc.Unit. - Exception: If the provided unit is not compatible with the - current unit. + """ + Convert the unit of the ComponentCollection and all its components. + + Parameters + ---------- + unit : str | sc.Unit + The new unit to convert to. + + Raises + ------ + TypeError + If the provided unit is not a string or sc.Unit. + Exception + If the provided unit is not compatible with the current unit. """ old_unit = self._unit @@ -191,24 +212,30 @@ def convert_unit(self, unit: str | sc.Unit) -> None: @property def components(self) -> list[ModelComponent]: - """Get the components of the SampleModel. + """ + Get the components of the SampleModel. - Returns: - list[ModelComponent]: The components of the SampleModel. + Returns + ------- + list[ModelComponent] + The components of the SampleModel. """ return self._components.components @components.setter def components(self, value: ModelComponent | ComponentCollection | None) -> None: - """Set the components of the SampleModel. + """ + Set the components of the SampleModel. - Args: - value (ModelComponent | ComponentCollection | None): The new - components to set. If None, all components will be cleared. + Parameters + ---------- + value : ModelComponent | ComponentCollection | None + The new components to set. If None, all components will be cleared. - Raises: - TypeError: If value is not a ModelComponent, - ComponentCollection, or None. + 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') @@ -219,27 +246,33 @@ def components(self, value: ModelComponent | ComponentCollection | None) -> None @property def Q(self) -> np.ndarray | None: - """Get the Q values of the SampleModel. + """ + Get the Q values of the SampleModel. - Returns: - np.ndarray | None: The Q values of the SampleModel, or None - if not set. + Returns + ------- + np.ndarray | None + The Q values of the SampleModel, or None if not set. """ return self._Q @Q.setter def Q(self, value: Q_type | None) -> None: - """Set the Q values of the SampleModel. If Q is already set, it - throws an error if the new Q values are not similar to the old + """ + Set the Q values of the SampleModel. + + If Q is already set, it throws an error if the new Q values are not similar to the old ones. To change Q values, first run clear_Q(). - Args: - value (Q_type | None): The new Q values to set. - If None, Q values are not changed. + Parameters + ---------- + value : Q_type | None + The new Q values to set. If None, Q values are not changed. - Raises: - ValueError: If the new Q values are not similar to the old - ones when Q is already set. + Raises + ------ + ValueError + If the new Q values are not similar to the old ones when Q is already set. """ if value is None: return @@ -258,14 +291,19 @@ def Q(self, value: Q_type | None) -> None: ) def clear_Q(self, confirm: bool = False) -> None: - """Clear the Q values of the SampleModel, removing all component - collections and their associated Parameters. - - Args: - confirm (bool, default=False): Confirmation to clear Q values. - - Raises: - ValueError: If confirm is not True. + """ + Clear the Q values of the SampleModel, removing all component collections and their + associated Parameters. + + Parameters + ---------- + confirm : bool, default=False + Confirmation to clear Q values. + + Raises + ------ + ValueError + If confirm is not True. """ if not confirm: raise ValueError( @@ -288,24 +326,29 @@ def free_all_parameters(self) -> None: par.fixed = False def get_all_variables(self, Q_index: int | None = None) -> list[Parameter]: - """Get all Parameters and Descriptors from all - ComponentCollections in the ModelBase. Parameters Ignores the - Parameters and Descriptors in self._components as these are just + """ + Get all Parameters and Descriptors from all ComponentCollections in the ModelBase. + Parameters Ignores the Parameters and Descriptors in self._components as these are just templates. - Args: - 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. + Parameters + ---------- + Q_index : int | None, default=None + If None, get variables for all ComponentCollections. If int, get variables for the + ComponentCollection at this index. + + Raises + ------ + TypeError + If Q_index is not an int or None. + IndexError + If Q_index is out of bounds for the number of ComponentCollections. + + Returns + ------- + list[Parameter] + A list of all Parameters and Descriptors from the ComponentCollections in the + ModelBase. """ if Q_index is None: @@ -326,19 +369,25 @@ def get_all_variables(self, Q_index: int | None = None) -> list[Parameter]: return all_vars def get_component_collection(self, Q_index: int) -> ComponentCollection: - """Get the ComponentCollection at the given Q index. - - Args: - Q_index (int): The index of the desired ComponentCollection. - - Returns: - ComponentCollection: The ComponentCollection at the - specified Q index. - - Raises: - TypeError: If Q_index is not an int. - IndexError: If Q_index is out of bounds for the number of - ComponentCollections. + """ + Get the ComponentCollection at the given Q index. + + Parameters + ---------- + Q_index : int + The index of the desired ComponentCollection. + + Raises + ------ + TypeError + If Q_index is not an int. + IndexError + If Q_index is out of bounds for the number of ComponentCollections. + + Returns + ------- + ComponentCollection + The ComponentCollection at the. """ if not isinstance(Q_index, int): raise TypeError(f'Q_index must be an int, got {type(Q_index).__name__}') @@ -382,10 +431,13 @@ def _on_components_change(self) -> None: # ------------------------------------------------------------------ def __repr__(self) -> str: - """Return a string representation of the ModelBase. + """ + Return a string representation of the ModelBase. - Returns: - str: A string representation of the ModelBase. + Returns + ------- + str + A string representation of the ModelBase. """ return ( f'{self.__class__.__name__}(unique_name={self.unique_name}, ' diff --git a/src/easydynamics/sample_model/resolution_model.py b/src/easydynamics/sample_model/resolution_model.py index 639bf8f8..50fae8d0 100644 --- a/src/easydynamics/sample_model/resolution_model.py +++ b/src/easydynamics/sample_model/resolution_model.py @@ -13,8 +13,8 @@ class ResolutionModel(ModelBase): - """ResolutionModel represents a model of the instrment resolution in - an experiment at various Q. + """ + ResolutionModel represents a model of the instrment resolution in an experiment at various Q. """ def __init__( @@ -25,19 +25,22 @@ def __init__( components: ModelComponent | ComponentCollection | None = None, Q: Q_type | None = None, ) -> None: - """Initialize a ResolutionModel. + """ + Initialize a ResolutionModel. - Args: - 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, 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, default=None): Q values for the model. If None, Q is not - set. + Parameters + ---------- + 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, 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, default=None + Q values for the model. If None, Q is not set. """ super().__init__( @@ -49,17 +52,21 @@ def __init__( ) def append_component(self, component: ModelComponent | ComponentCollection) -> None: - """Append a component to the ResolutionModel. + """ + Append a component to the ResolutionModel. - Does not allow DeltaFunction or Polynomial components, as these - are not physical resolution components. + Does not allow DeltaFunction or Polynomial components, as these are not physical resolution + components. - Args: - component (ModelComponent | ComponentCollection): - Component(s) to append. + Parameters + ---------- + component : ModelComponent | ComponentCollection + Component(s) to append. - Raises: - TypeError: If the component is a DeltaFunction or Polynomial + Raises + ------ + TypeError + If the component is a DeltaFunction or Polynomial. """ if isinstance(component, ComponentCollection): components = component.components diff --git a/src/easydynamics/sample_model/sample_model.py b/src/easydynamics/sample_model/sample_model.py index d6c63492..26fad08c 100644 --- a/src/easydynamics/sample_model/sample_model.py +++ b/src/easydynamics/sample_model/sample_model.py @@ -18,9 +18,9 @@ class SampleModel(ModelBase): - """SampleModel represents a model of a sample with components and - diffusion models, parameterized by Q and optionally temperature. - Generates ComponentCollections for each Q value, combining + """ + SampleModel represents a model of a sample with components and diffusion models, parameterized + by Q and optionally temperature. Generates ComponentCollections for each Q value, combining components from the base model and diffusion models. Applies detailed balancing based on temperature if provided. @@ -38,36 +38,39 @@ def __init__( temperature_unit: str | sc.Unit = 'K', divide_by_temperature: bool = True, ) -> None: - """Initialize the SampleModel. - - Args: - 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, default="meV"): Unit of the model. If None, - defaults to "meV". - 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, default=None): - Q values for the model. If None, Q is not set. - 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, default=None): Temperature for detailed - balancing. If None, no detailed balancing is applied. - temperature_unit (str | sc.Unit, default="K"): Unit of the temperature. - Defaults to "K". - 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, or if temperature - is not a number or None, or if divide_by_temperature is - not a bool. - ValueError: If temperature is negative. + """ + Initialize the SampleModel. + + Parameters + ---------- + 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, default='meV' + Unit of the model. If 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 : Q_type | None, default=None + Q values for the model. If None, Q is not set. + 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, default=None + Temperature for detailed balancing. If None, no detailed balancing is applied. By + default, None. + temperature_unit : str | sc.Unit, default='K' + Unit of the temperature. + divide_by_temperature : bool, default=True + Whether to divide the detailed balance factor by temperature. + + 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. + ValueError + If temperature is negative. """ if diffusion_models is None: self._diffusion_models = [] @@ -117,15 +120,18 @@ def __init__( # ------------------------------------------------------------------ def append_diffusion_model(self, diffusion_model: DiffusionModelBase) -> None: - """Append a DiffusionModel to the SampleModel. + """ + Append a DiffusionModel to the SampleModel. - Args: - diffusion_model (DiffusionModelBase): The DiffusionModel - to append. + Parameters + ---------- + diffusion_model : DiffusionModelBase + The DiffusionModel to append. - Raises: - TypeError: If the diffusion_model is not a - DiffusionModelBase + Raises + ------ + TypeError + If the diffusion_model is not a DiffusionModelBase. """ if not isinstance(diffusion_model, DiffusionModelBase): @@ -137,14 +143,18 @@ def append_diffusion_model(self, diffusion_model: DiffusionModelBase) -> None: self._generate_component_collections() def remove_diffusion_model(self, name: 'str') -> None: - """Remove a DiffusionModel from the SampleModel by unique name. + """ + Remove a DiffusionModel from the SampleModel by unique name. - Args: - name (str): The unique name of the DiffusionModel to remove. + Parameters + ---------- + name : 'str' + The unique name of the DiffusionModel to remove. - Raises: - ValueError: If no DiffusionModel with the given unique name - is found. + Raises + ------ + ValueError + If no DiffusionModel with the given unique name is found. """ for i, dm in enumerate(self._diffusion_models): if dm.unique_name == name: @@ -167,11 +177,13 @@ def clear_diffusion_models(self) -> None: @property def diffusion_models(self) -> list[DiffusionModelBase]: - """Get the diffusion models of the SampleModel. + """ + Get the diffusion models of the SampleModel. - Returns: - list[DiffusionModelBase]: The diffusion models of the - SampleModel. + Returns + ------- + list[DiffusionModelBase] + The diffusion models of the SampleModel. """ return self._diffusion_models @@ -179,17 +191,19 @@ def diffusion_models(self) -> list[DiffusionModelBase]: def diffusion_models( self, value: DiffusionModelBase | list[DiffusionModelBase] | None ) -> None: - """Set the diffusion models of the SampleModel. - - Args: - 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. + """ + Set the diffusion models of the SampleModel. + + Parameters + ---------- + 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: @@ -210,25 +224,32 @@ def diffusion_models( @property def temperature(self) -> Parameter | None: - """Get the temperature of the SampleModel. + """ + Get the temperature of the SampleModel. - Returns: - Parameter | None: The temperature Parameter of the - SampleModel, or None if not set. + Returns + ------- + Parameter | None + The temperature Parameter of the SampleModel, or None if not set. """ return self._temperature @temperature.setter def temperature(self, value: Numeric | None) -> None: - """Set the temperature of the SampleModel. - - 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. + """ + Set the temperature of the SampleModel. + + Parameters + ---------- + 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 @@ -253,23 +274,30 @@ def temperature(self, value: Numeric | None) -> None: @property def temperature_unit(self) -> str | sc.Unit: - """Get the temperature unit of the SampleModel. + """ + Get the temperature unit of the SampleModel. - Returns: - str | sc.Unit: The unit of the temperature Parameter. + Returns + ------- + str | sc.Unit + The unit of the temperature Parameter. """ return self._temperature_unit @temperature_unit.setter def temperature_unit(self, _value: str | sc.Unit) -> None: - """The temperature unit of the SampleModel is read-only. + """ + The temperature unit of the SampleModel is read-only. - Args: - _value (str | sc.Unit): The unit to set for the temperature - Parameter. + Parameters + ---------- + _value : str | sc.Unit + The unit to set for the temperature Parameter. - Raises: - AttributeError: Always, as temperature_unit is read-only. + Raises + ------ + AttributeError + Always, as temperature_unit is read-only. """ raise AttributeError( @@ -278,15 +306,20 @@ def temperature_unit(self, _value: str | sc.Unit) -> None: ) # noqa: E501 def convert_temperature_unit(self, unit: str | sc.Unit) -> None: - """Convert the unit of the temperature Parameter. - - Args: - unit (str | sc.Unit): The unit to convert the temperature - Parameter to. - - Raises: - ValueError: If temperature is not set or conversion fails. - Exception: If the provided unit is invalid or cannot be converted. + """ + Convert the unit of the temperature Parameter. + + Parameters + ---------- + unit : str | sc.Unit + The unit to convert the temperature Parameter to. + + 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: @@ -305,26 +338,30 @@ def convert_temperature_unit(self, unit: str | sc.Unit) -> None: @property def divide_by_temperature(self) -> bool: - """Get whether to divide the detailed balance factor by - temperature. + """ + Get whether to divide the detailed balance factor by temperature. - Returns: - bool: True if the detailed balance factor is divided by - temperature, False otherwise. + Returns + ------- + bool + True if the detailed balance factor is divided by temperature, False otherwise. """ return self._divide_by_temperature @divide_by_temperature.setter def divide_by_temperature(self, value: bool) -> None: - """Set whether to divide the detailed balance factor by - temperature. + """ + Set whether to divide the detailed balance factor by temperature. - Args: - value (bool): True to divide the detailed balance factor by - temperature, False otherwise. + Parameters + ---------- + value : bool + True to divide the detailed balance factor by temperature, False otherwise. - Raises: - TypeError: If value is not a bool. + Raises + ------ + TypeError + If value is not a bool. """ if not isinstance(value, bool): raise TypeError('divide_by_temperature must be True or False') @@ -337,15 +374,19 @@ def divide_by_temperature(self, value: bool) -> None: def evaluate( self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray ) -> list[np.ndarray]: - """Evaluate the sample model at all Q for the given x values. - - Args: - x (Numeric | list | np.ndarray | sc.Variable | sc.DataArray): - The x values to evaluate the model at. Can be a number, - list, numpy array, scipp Variable, or scipp DataArray. - - Returns: - list[np.ndarray]: List of evaluated model values for each Q. + """ + Evaluate the sample model at all Q for the given x values. + + Parameters + ---------- + x : Numeric | list | np.ndarray | sc.Variable | sc.DataArray + The x values to evaluate the model at. Can be a number, list, numpy array, scipp + Variable, or scipp DataArray. + + Returns + ------- + list[np.ndarray] + List of evaluated model values for each Q. """ y = super().evaluate(x) @@ -362,22 +403,23 @@ def evaluate( return y def get_all_variables(self, Q_index: int | None = None) -> list[Parameter]: - """Get all Parameters and Descriptors from all - ComponentCollections in the SampleModel. - - Also includes temperature if set and all variables from - diffusion models. Ignores the Parameters and Descriptors in - self._components as these are just templates. - - Args: - 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. + """ + Get all Parameters and Descriptors from all ComponentCollections in the SampleModel. + + Also includes temperature if set and all variables from diffusion models. Ignores the + Parameters and Descriptors in self._components as these are just templates. + + Parameters + ---------- + 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) @@ -394,8 +436,9 @@ def get_all_variables(self, Q_index: int | None = None) -> list[Parameter]: # ------------------------------------------------------------------ def _generate_component_collections(self) -> None: - """Generate ComponentCollections from the DiffusionModels for - each Q and add the components from self._components. + """ + Generate ComponentCollections from the DiffusionModels for each Q and add the components + from self._components. """ # TODO regenerate automatically if Q, diffusion models # or components have changed @@ -422,10 +465,13 @@ def _on_diffusion_models_change(self) -> None: # ------------------------------------------------------------------ def __repr__(self) -> str: - """Return a string representation of the SampleModel. + """ + Return a string representation of the SampleModel. - Returns: - str: A string representation of the SampleModel. + Returns + ------- + str + A string representation of the SampleModel. """ return ( diff --git a/src/easydynamics/utils/detailed_balance.py b/src/easydynamics/utils/detailed_balance.py index cd8ff347..796afaff 100644 --- a/src/easydynamics/utils/detailed_balance.py +++ b/src/easydynamics/utils/detailed_balance.py @@ -30,51 +30,48 @@ def detailed_balance_factor( divide_by_temperature: bool = True, ) -> np.ndarray: r""" - Compute the detailed balance factor (DBF): - $$ - DBF(E, T) = E(n(E)+1)=\frac{E}{(1 - e^{-E / (k_B*T)})}}, - $$ - where $n(E)$ is the Bose-Einstein distribution, $E$ is the energy - transfer, and $T$ is the temperature. $k_B$ is the Boltzmann - constant. - If divide_by_temperature is True, the result is normalized by - $k_B*T$ to have value 1 at $E=0$. - - Args: - energy (int | float | list | np.ndarray | sc.Variable): The energy - transfer. If number, assumed to be in meV unless energy_unit - is set. - 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, default='meV'): Unit for energy if energy is - given as a number or list. Default is 'meV' - temperature_unit (str | sc.Unit, default='K'): Unit for temperature if - temperature is given as a number. Default is 'K' - 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: - np.ndarray: Detailed balance factor evaluated at the - 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. - ZeroDivisionError: If divide_by_temperature is True and temperature is zero. - - Examples: + Compute the detailed balance factor (DBF): $$ DBF(E, T) = E(n(E)+1)=\frac{E}{(1 - e^{-E / + (k_B*T)})}}, $$ where $n(E)$ is the Bose-Einstein distribution, $E$ is the energy transfer, and + $T$ is the temperature. $k_B$ is the Boltzmann constant. If divide_by_temperature is True, the + result is normalized by $k_B*T$ to have value 1 at $E=0$. + + Parameters + ---------- + energy : int | float | list | np.ndarray | sc.Variable + The energy transfer. If number, assumed to be in meV unless energy_unit is set. + 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, default='meV' + Unit for energy if energy is given as a number or list. + temperature_unit : str | sc.Unit, default='K' + Unit for temperature if temperature is given as a number. + 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. By + default, True. + + 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. + + Returns + ------- + np.ndarray + Detailed balance factor evaluated at the given energy and temperature. + + Examples + -------- >>> detailed_balance_factor(1.0, 300) # 1 meV at 300 K >>> detailed_balance_factor( ... energy=[1.0, 2.0], @@ -186,28 +183,32 @@ def _convert_to_scipp_variable( name: str, unit: str | None = None, ) -> sc.Variable: - """Convert various input types to a scipp Variable with proper - 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. - name (str): The name of the variable, used for error messages. - 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. - - Raises: - TypeError: If value is not one of the accepted types, or if unit - is not a string when needed. - UnitError: If the provided unit is invalid. - - Returns: - sc.Variable: The input value converted to a scipp Variable with - appropriate units. + """ + Convert various input types to a scipp Variable with proper units. + + Parameters + ---------- + 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, 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. By + default, None. + + Raises + ------ + TypeError + If value is not one of the accepted types, or if unit is not a string when needed. + UnitError + If the provided unit is invalid. + + Returns + ------- + sc.Variable + The input value converted to a scipp Variable with appropriate units. """ if isinstance(value, sc.Variable): return value diff --git a/src/easydynamics/utils/utils.py b/src/easydynamics/utils/utils.py index 7b9134bc..3d383d9c 100644 --- a/src/easydynamics/utils/utils.py +++ b/src/easydynamics/utils/utils.py @@ -19,20 +19,26 @@ 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 (np.ndarray | Numeric | list | ArrayLike | sc.Variable | None): - Scattering vector values in 1/angstrom. - - Returns: - 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'. + """ + Validate and convert Q to a numpy array. + + Parameters + ---------- + Q : np.ndarray | Numeric | list | ArrayLike | sc.Variable | None + Scattering vector values in 1/angstrom. + + 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'. + + Returns + ------- + np.ndarray | None + Q as a np.ndarray or None if Q is None. """ if Q is None: return None @@ -57,16 +63,23 @@ def _validate_and_convert_Q( def _validate_unit(unit: str | sc.Unit | None) -> sc.Unit | None: - """Validate that the unit is a string or scipp Unit. - - Args: - unit (str | sc.Unit | None): Unit to validate. - - Returns: - sc.Unit | None: Validated unit or None. - - Raises: - TypeError: If unit is not None, a string, or a scipp Unit. + """ + Validate that the unit is a string or scipp Unit. + + Parameters + ---------- + unit : str | sc.Unit | None + Unit to validate. + + Raises + ------ + TypeError + If unit is not None, a string, or a scipp Unit. + + Returns + ------- + sc.Unit | None + Validated unit or None. """ if unit is not None and not isinstance(unit, (str, sc.Unit)): @@ -77,10 +90,13 @@ def _validate_unit(unit: str | sc.Unit | None) -> sc.Unit | None: def _in_notebook() -> bool: - """Check if the code is running in a Jupyter notebook. + """ + Check if the code is running in a Jupyter notebook. - Returns: - bool: True if in a Jupyter notebook, False otherwise. + Returns + ------- + bool + True if in a Jupyter notebook, False otherwise. """ try: from IPython import get_ipython diff --git a/tools/add_license_headers.py b/tools/add_license_headers.py deleted file mode 100644 index 28febde7..00000000 --- a/tools/add_license_headers.py +++ /dev/null @@ -1,151 +0,0 @@ -# SPDX-FileCopyrightText: 2026 EasyScience contributors -# SPDX-License-Identifier: BSD-3-Clause -"""Add SPDX headers to Python files. - -- SPDX-FileCopyrightText with the license holder name and organization - URL from ``pyproject.toml`` as well as the file's creation year. -- SPDX-License-Identifier is taken from the project license value in - ``pyproject.toml``. -""" - -from __future__ import annotations - -import argparse -import tomllib -from datetime import datetime -from pathlib import Path -from typing import Optional -from typing import Union - -from git import Repo -from spdx_headers.core import find_repository_root -from spdx_headers.core import get_copyright_info -from spdx_headers.data import load_license_data -from spdx_headers.operations import add_header_to_single_file - -LICENSE_DATABASE = load_license_data() - - -def load_pyproject(repo_path: Union[str, Path]) -> dict: - """Load and return parsed ``pyproject.toml`` data for the - repository. - """ - repo_root = find_repository_root(repo_path) - pyproject_path = repo_root / 'pyproject.toml' - - with open(pyproject_path, 'rb') as file_handle: - return tomllib.load(file_handle) - - -def get_file_creation_year(file_path: Union[str, Path]) -> str: - """Return the year the file was first added to Git history. - - If the year cannot be determined, fall back to the current year. - """ - file_path = Path(file_path) - - repo = Repo(file_path, search_parent_directories=True) - root = Path(repo.working_tree_dir).resolve() - rel_path = file_path.resolve().relative_to(root) - - rel_path_git = rel_path.as_posix() # IMPORTANT for git pathspec - - # Get the year when the file was first added to Git history. - # NOTE: Do not combine `--reverse` with `--max-count=1` here, as it can - # yield an empty result with some Git versions. Instead, get the full - # filtered output and take the first line. - log_output = repo.git.log( - '--follow', - '--diff-filter=A', - '--reverse', - '--format=%ad', - '--date=format:%Y', - '--', - rel_path_git, - ).strip() - - year = log_output.splitlines()[0].strip() if log_output else '' - - return year or str(datetime.now().year) - - -def get_org_url(repo_path: Union[str, Path]) -> str: - """Return the organization URL derived from the repository source - URL. - """ - pyproject_data = load_pyproject(repo_path) - repo_url = pyproject_data['project']['urls']['Source Code'] - return repo_url.rsplit('/', 1)[0] - - -def get_project_license(repo_path: Union[str, Path]) -> str: - """Return the project license value from ``pyproject.toml``.""" - pyproject_data = load_pyproject(repo_path) - return pyproject_data['project']['license'] - - -def get_copyright_holder(repo_path: Union[str, Path]) -> str: - """Return the repository copyright holder name.""" - _, name, _ = get_copyright_info(repo_path) - return name - - -def add_spdx_header( - target_file: Union[str, Path], - *, - license_key: str, - copyright_holder: str, - org_url: str, -) -> None: - """Add SPDX headers.""" - year = get_file_creation_year(target_file) - - add_header_to_single_file( - filepath=target_file, - license_key=license_key, - license_data=LICENSE_DATABASE, - year=year, - name=copyright_holder, - email=org_url, - ) - - -def build_parser() -> argparse.ArgumentParser: - parser = argparse.ArgumentParser( - description='Add SPDX headers to Python files under the given paths.', - ) - parser.add_argument( - 'paths', - nargs='+', - help='Relative paths to scan (e.g. src tests)', - ) - return parser - - -def main(argv: Optional[list[str]] = None) -> int: - parser = build_parser() - args = parser.parse_args(argv) - - repo_path = Path('.').resolve() - license_key = get_project_license(repo_path) - copyright_holder = get_copyright_holder(repo_path) - org_url = get_org_url(repo_path) - - for base_dir in args.paths: - base_path = Path(base_dir) - if not base_path.exists(): - parser.error(f'Path does not exist: {base_dir}') - - for py_file in base_path.rglob('*.py'): - add_spdx_header( - py_file, - license_key=license_key, - copyright_holder=copyright_holder, - org_url=org_url, - ) - - return 0 - - -if __name__ == '__main__': - raise SystemExit(main()) diff --git a/tools/check_license_headers.py b/tools/check_license_headers.py deleted file mode 100644 index c8a4df21..00000000 --- a/tools/check_license_headers.py +++ /dev/null @@ -1,45 +0,0 @@ -# SPDX-FileCopyrightText: 2026 EasyScience contributors -# SPDX-License-Identifier: BSD-3-Clause -"""Check SPDX headers in Python files.""" - -from __future__ import annotations - -import argparse -from pathlib import Path -from typing import Optional - -from spdx_headers.operations import check_headers - - -def build_parser() -> argparse.ArgumentParser: - parser = argparse.ArgumentParser( - description='Check SPDX headers in Python files under the given paths.', - ) - parser.add_argument( - 'paths', - nargs='+', - help='Relative paths to scan (e.g. src tests)', - ) - return parser - - -def main(argv: Optional[list[str]] = None) -> int: - parser = build_parser() - args = parser.parse_args(argv) - - exit_codes = [] - - for base_dir in args.paths: - base_path = Path(base_dir) - if not base_path.exists(): - parser.error(f'Path does not exist: {base_dir}') - - print('=' * 50) - print(f'Checking SPDX headers in: {base_dir}') - exit_codes.append(check_headers(base_dir)) - - return 0 if all(code == 0 for code in exit_codes) else 1 - - -if __name__ == '__main__': - raise SystemExit(main()) diff --git a/tools/license_headers.py b/tools/license_headers.py new file mode 100644 index 00000000..47d23524 --- /dev/null +++ b/tools/license_headers.py @@ -0,0 +1,315 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Add, remove, or check SPDX headers in Python files.""" + +from __future__ import annotations + +import argparse +import fnmatch +import tomllib +from datetime import datetime +from pathlib import Path +from typing import Any +from typing import Optional +from typing import Union + +from git import Repo +from spdx_headers.core import find_repository_root +from spdx_headers.core import get_copyright_info +from spdx_headers.core import has_spdx_header +from spdx_headers.data import load_license_data +from spdx_headers.operations import add_header_to_single_file +from spdx_headers.operations import remove_header_from_single_file + +LICENSE_DATABASE = load_license_data() + + +def load_pyproject(repo_path: Union[str, Path]) -> dict[str, Any]: + """Load and return parsed ``pyproject.toml`` data for the repository.""" + repo_root = find_repository_root(repo_path) + pyproject_path = repo_root / 'pyproject.toml' + + with pyproject_path.open('rb') as file_handle: + return tomllib.load(file_handle) + + +def get_pyproject_value(pyproject_data: dict[str, Any], dotted_key: str) -> Any: + """Return a nested ``pyproject.toml`` value from a dotted key.""" + value: Any = pyproject_data + for part in dotted_key.split('.'): + if not isinstance(value, dict) or part not in value: + raise KeyError(dotted_key) + value = value[part] + return value + + +def normalize_pattern(pattern: str) -> str: + """Normalize an exclude pattern to a POSIX-style relative path.""" + normalized = Path(pattern).as_posix() + if normalized.startswith('./'): + normalized = normalized[2:] + return normalized.rstrip('/') + + +def get_exclude_patterns( + repo_path: Union[str, Path], + exclude_values: list[str], + exclude_from_pyproject_toml: Optional[str], +) -> list[str]: + """Return normalized exclude patterns from CLI and ``pyproject.toml``.""" + pyproject_data = load_pyproject(repo_path) + patterns: list[str] = [] + + if exclude_from_pyproject_toml: + value = get_pyproject_value(pyproject_data, exclude_from_pyproject_toml) + if not isinstance(value, list) or not all(isinstance(item, str) for item in value): + raise ValueError( + f'{exclude_from_pyproject_toml} in pyproject.toml must be a list of strings.', + ) + patterns.extend(value) + + for item in exclude_values: + try: + value = get_pyproject_value(pyproject_data, item) + except KeyError: + patterns.append(item) + continue + + if not isinstance(value, list) or not all(isinstance(entry, str) for entry in value): + raise ValueError(f'{item} in pyproject.toml must be a list of strings.') + patterns.extend(value) + + normalized_patterns: list[str] = [] + seen: set[str] = set() + for pattern in patterns: + normalized = normalize_pattern(pattern) + if normalized and normalized not in seen: + normalized_patterns.append(normalized) + seen.add(normalized) + + return normalized_patterns + + +def get_file_creation_year(file_path: Union[str, Path]) -> str: + """Return the year the file was first added to Git history. + + If the year cannot be determined, fall back to the current year. + """ + file_path = Path(file_path) + + repo = Repo(file_path, search_parent_directories=True) + root = Path(repo.working_tree_dir).resolve() + rel_path = file_path.resolve().relative_to(root) + + rel_path_git = rel_path.as_posix() + + log_output = repo.git.log( + '--follow', + '--diff-filter=A', + '--reverse', + '--format=%ad', + '--date=format:%Y', + '--', + rel_path_git, + ).strip() + + year = log_output.splitlines()[0].strip() if log_output else '' + + return year or str(datetime.now().year) + + +def get_org_url(repo_path: Union[str, Path]) -> str: + """Return the organization URL derived from the repository source URL.""" + pyproject_data = load_pyproject(repo_path) + repo_url = pyproject_data['project']['urls']['Source Code'] + return repo_url.rsplit('/', 1)[0] + + +def get_project_license(repo_path: Union[str, Path]) -> str: + """Return the project license value from ``pyproject.toml``.""" + pyproject_data = load_pyproject(repo_path) + return pyproject_data['project']['license'] + + +def get_copyright_holder(repo_path: Union[str, Path]) -> str: + """Return the repository copyright holder name.""" + _, name, _ = get_copyright_info(repo_path) + return name + + +def add_spdx_header( + target_file: Union[str, Path], + *, + license_key: str, + copyright_holder: str, + org_url: str, +) -> None: + """Add SPDX headers to one file.""" + year = get_file_creation_year(target_file) + + add_header_to_single_file( + filepath=target_file, + license_key=license_key, + license_data=LICENSE_DATABASE, + year=year, + name=copyright_holder, + email=org_url, + ) + + +def is_excluded(relative_path: str, exclude_patterns: list[str]) -> bool: + """Return whether a relative path should be excluded.""" + for pattern in exclude_patterns: + if fnmatch.fnmatch(relative_path, pattern): + return True + if relative_path == pattern: + return True + if relative_path.startswith(f'{pattern}/'): + return True + return False + + +def iter_python_files( + paths: list[str], + *, + repo_root: Path, + exclude_patterns: list[str], + parser: argparse.ArgumentParser, +) -> list[Path]: + """Collect Python files under the given paths after exclusions.""" + files: list[Path] = [] + seen: set[Path] = set() + + for base_dir in paths: + base_path = Path(base_dir) + if not base_path.exists(): + parser.error(f'Path does not exist: {base_dir}') + + if base_path.is_file(): + candidates = [base_path] if base_path.suffix == '.py' else [] + else: + candidates = sorted(base_path.rglob('*.py')) + + for py_file in candidates: + resolved = py_file.resolve() + try: + relative_path = resolved.relative_to(repo_root).as_posix() + except ValueError: + relative_path = py_file.as_posix() + + if is_excluded(relative_path, exclude_patterns): + continue + + if resolved not in seen: + files.append(py_file) + seen.add(resolved) + + return files + + +def run_add( + files: list[Path], + *, + license_key: str, + copyright_holder: str, + org_url: str, +) -> int: + """Add SPDX headers to all selected files.""" + for py_file in files: + add_spdx_header( + py_file, + license_key=license_key, + copyright_holder=copyright_holder, + org_url=org_url, + ) + return 0 + + +def run_remove(files: list[Path]) -> int: + """Remove SPDX headers from all selected files.""" + for py_file in files: + remove_header_from_single_file(py_file) + return 0 + + +def run_check(files: list[Path]) -> int: + """Check SPDX headers in all selected files.""" + missing_files = [py_file for py_file in files if not has_spdx_header(py_file)] + + if not missing_files: + print('✓ All Python files have valid SPDX headers.') + return 0 + + print('✗ The following files are missing SPDX headers:') + for py_file in missing_files: + print(f' - {py_file.as_posix()}') + print(f'\nFound {len(missing_files)} files without SPDX headers.') + return 1 + + +def build_parser() -> argparse.ArgumentParser: + """Build the CLI argument parser.""" + parser = argparse.ArgumentParser( + description='Add, remove, or check SPDX headers in Python files.', + ) + subparsers = parser.add_subparsers(dest='command', required=True) + + for command_name in ('check', 'remove', 'add'): + command_parser = subparsers.add_parser(command_name) + command_parser.add_argument( + 'paths', + nargs='+', + help='Relative paths to scan (e.g. src tests)', + ) + command_parser.add_argument( + '--exclude', + nargs='*', + default=[], + help='Exclude paths, glob patterns, or pyproject dotted keys.', + ) + command_parser.add_argument( + '--exclude-from-pyproject-toml', + help='Read exclude patterns from a dotted key in pyproject.toml.', + ) + + return parser + + +def main(argv: Optional[list[str]] = None) -> int: + """Run the SPDX header CLI.""" + parser = build_parser() + args = parser.parse_args(argv) + + repo_path = Path('.').resolve() + repo_root = find_repository_root(repo_path).resolve() + exclude_patterns = get_exclude_patterns( + repo_path, + args.exclude, + args.exclude_from_pyproject_toml, + ) + files = iter_python_files( + args.paths, + repo_root=repo_root, + exclude_patterns=exclude_patterns, + parser=parser, + ) + + if args.command == 'check': + return run_check(files) + + if args.command == 'remove': + return run_remove(files) + + license_key = get_project_license(repo_path) + copyright_holder = get_copyright_holder(repo_path) + org_url = get_org_url(repo_path) + return run_add( + files, + license_key=license_key, + copyright_holder=copyright_holder, + org_url=org_url, + ) + + +if __name__ == '__main__': + raise SystemExit(main()) diff --git a/tools/remove_license_headers.py b/tools/remove_license_headers.py deleted file mode 100644 index f5d09da5..00000000 --- a/tools/remove_license_headers.py +++ /dev/null @@ -1,41 +0,0 @@ -# SPDX-FileCopyrightText: 2026 EasyScience contributors -# SPDX-License-Identifier: BSD-3-Clause -"""Remove SPDX headers from Python files.""" - -from __future__ import annotations - -import argparse -from pathlib import Path -from typing import Optional - -from spdx_headers.operations import remove_header_from_py_files - - -def build_parser() -> argparse.ArgumentParser: - parser = argparse.ArgumentParser( - description='Remove SPDX headers from Python files under the given paths.', - ) - parser.add_argument( - 'paths', - nargs='+', - help='Relative paths to scan (e.g. src tests)', - ) - return parser - - -def main(argv: Optional[list[str]] = None) -> int: - parser = build_parser() - args = parser.parse_args(argv) - - for base_dir in args.paths: - base_path = Path(base_dir) - if not base_path.exists(): - parser.error(f'Path does not exist: {base_dir}') - - remove_header_from_py_files(base_dir) - - return 0 - - -if __name__ == '__main__': - raise SystemExit(main())