diff --git a/.badgery.yaml b/.badgery.yaml new file mode 100644 index 00000000..99bcdc1c --- /dev/null +++ b/.badgery.yaml @@ -0,0 +1,72 @@ +default_branch: master +develop_branch: develop + +cards: + - group: Tests + type: gh_action + title: Code/package tests (GitHub) + file: test.yml + enabled: true + - group: Tests + type: gh_action + title: Tutorial tests (GitHub) + file: tutorial-tests.yml + enabled: true + + - group: Tests + type: gh_action + title: Package tests (PyPI) + file: pypi-test.yml + enabled: true + + - group: Code Quality + type: codefactor + title: Code quality (CodeFactor) + enabled: true + + - group: Code Quality + type: radon_mi + title: Maintainability index (radon) + report: reports/{branch}/maintainability-index.json + enabled: true + + - group: Code Quality + type: radon_cc + title: Cyclomatic complexity (radon) + report: reports/{branch}/cyclomatic-complexity.json + enabled: true + + - group: Size + type: radon_loc + title: Source/Logical lines of code (radon) + report: reports/{branch}/raw-metrics.json + enabled: true + + - group: Size + type: radon_ff + title: Functions/Files count (radon) + report: reports/{branch}/cyclomatic-complexity.json + enabled: true + + - group: Coverage + type: codecov + title: Unit test coverage (Codecov) + flag: unittests + enabled: true + + - group: Coverage + type: interrogate + title: Docstring coverage (interrogate) + report: reports/{branch}/coverage-docstring.txt + enabled: true + - group: Build & Release + type: gh_action + title: Publishing (PyPI) + workflow: pypi-publish.yml + enabled: true + + - group: Build & Release + type: gh_action + title: Docs build/deployment + workflow: docs.yml + enabled: true diff --git a/.codecov.yml b/.codecov.yml index 9c9abf0b..e17376e8 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -1,8 +1,8 @@ github_checks: - annotations: true + annotations: true comment: - layout: "reach, diff, flags, files" + layout: 'reach, diff, flags, files' behavior: default - require_changes: false # if true: only post the comment if coverage changes - require_base: no # [yes :: must have a base report to post] - require_head: yes # [yes :: must have a head report to post] + require_changes: false # if true: only post the comment if coverage changes + require_base: no # [yes :: must have a base report to post] + require_head: yes # [yes :: must have a head report to post] diff --git a/.copier-answers.yml b/.copier-answers.yml index 867a00e8..f8fbdb26 100644 --- a/.copier-answers.yml +++ b/.copier-answers.yml @@ -1,11 +1,19 @@ -# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY -_commit: 8bdcedc -_src_path: gh:/EasyScience/EasyProjectTemplate -description: A reflectometry python package built on the EasyScience framework. -max_python: '3.13' -min_python: '3.9' -orgname: EasyScience -packagename: easyreflectometry -prettyname: Easy Reflectometry Library -projectname: easyreflectometry -year: 2024 +# WARNING: Do not edit this file manually. +# Any changes will be overwritten by Copier. +_commit: v0.11.2-3-ge3f42a1 +_src_path: gh:easyscience/templates +lib_docs_url: https://easyscience.github.io/reflectometry-lib +lib_doi: 10.5281/zenodo.18163581 +lib_package_name: easyreflectometry +lib_python_max: '3.13' +lib_python_min: '3.11' +lib_repo_name: reflectometry-lib +project_contact_email: support@easyreflectometry.org +project_copyright_years: 2021-2026 +project_extended_description: A software for performing reflectometry calculations + based on a layer model and refining its parameters against reflectometry data +project_name: EasyReflectometry +project_short_description: Reflectometry data analysis +project_shortcut: ER +project_type: lib +template_type: lib diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..997504b4 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# SCM syntax highlighting & preventing 3-way merges +pixi.lock merge=binary linguist-language=YAML linguist-generated=true -diff diff --git a/.github/actions/download-artifact/action.yml b/.github/actions/download-artifact/action.yml new file mode 100644 index 00000000..d1fff1a0 --- /dev/null +++ b/.github/actions/download-artifact/action.yml @@ -0,0 +1,50 @@ +name: 'Download artifact' +description: 'Wrapper for actions/download-artifact' +inputs: + name: + description: 'Name of the artifact to download' + required: true + + path: + description: 'Destination path' + required: false + default: '.' + + pattern: + description: 'Glob pattern to match artifact names (optional)' + required: false + default: '' + + merge-multiple: + description: 'Merge multiple artifacts into the same directory' + required: false + default: 'false' + + github-token: + description: 'GitHub token for cross-repo download (optional)' + required: false + default: '' + + repository: + description: 'owner/repo for cross-repo download (optional)' + required: false + default: '' + + run-id: + description: 'Workflow run ID for cross-run download (optional)' + required: false + default: '' + +runs: + using: 'composite' + steps: + - name: Download artifact + uses: actions/download-artifact@v8 + with: + name: ${{ inputs.name }} + path: ${{ inputs.path }} + pattern: ${{ inputs.pattern }} + merge-multiple: ${{ inputs.merge-multiple }} + github-token: ${{ inputs.github-token }} + repository: ${{ inputs.repository }} + run-id: ${{ inputs.run-id }} diff --git a/.github/actions/github-script/action.yml b/.github/actions/github-script/action.yml new file mode 100644 index 00000000..50de89b7 --- /dev/null +++ b/.github/actions/github-script/action.yml @@ -0,0 +1,19 @@ +name: 'GitHub Script' +description: 'Wrapper for actions/github-script' +inputs: + script: + description: 'JavaScript to run' + required: true + + github-token: + description: 'GitHub token (defaults to github.token)' + required: false + default: ${{ github.token }} + +runs: + using: 'composite' + steps: + - uses: actions/github-script@v9 + with: + script: ${{ inputs.script }} + github-token: ${{ inputs.github-token }} diff --git a/.github/actions/setup-easyscience-bot/action.yml b/.github/actions/setup-easyscience-bot/action.yml new file mode 100644 index 00000000..e51eb01a --- /dev/null +++ b/.github/actions/setup-easyscience-bot/action.yml @@ -0,0 +1,40 @@ +name: 'Setup EasyScience bot for pushing' +description: 'Create GitHub App token and configure git identity + origin remote' +inputs: + app-id: + description: 'GitHub App ID' + required: true + private-key: + description: 'GitHub App private key (PEM)' + required: true + repositories: + description: 'Additional repositories to grant access to (newline-separated)' + required: false + default: '' + +outputs: + token: + description: 'Installation access token' + value: ${{ steps.app-token.outputs.token }} + +runs: + using: 'composite' + steps: + - name: Create GitHub App installation token + id: app-token + uses: actions/create-github-app-token@v3 + with: + client-id: ${{ inputs.app-id }} + private-key: ${{ inputs.private-key }} + repositories: ${{ inputs.repositories }} + + - name: Configure git for pushing + shell: bash + run: | + git config user.name "easyscience[bot]" + git config user.email "${{ inputs.app-id }}+easyscience[bot]@users.noreply.github.com" + + - name: Configure origin remote to use the bot token + shell: bash + run: | + git remote set-url origin https://x-access-token:${{ steps.app-token.outputs.token }}@github.com/${{ github.repository }}.git diff --git a/.github/actions/setup-pixi/action.yml b/.github/actions/setup-pixi/action.yml new file mode 100644 index 00000000..ec7d7ba7 --- /dev/null +++ b/.github/actions/setup-pixi/action.yml @@ -0,0 +1,44 @@ +name: 'Setup Pixi Environment' +description: 'Wrapper for prefix-dev/setup-pixi' +inputs: + environments: + description: 'Pixi environments to setup' + required: false + default: 'default' + activate-environment: + description: 'Environment to activate' + required: false + default: 'default' + run-install: + description: 'Whether to run pixi install' + required: false + default: 'true' + locked: + description: 'Whether to run pixi install --locked' + required: false + default: 'false' + frozen: + description: 'Whether to run pixi install --frozen' + required: false + default: 'true' + cache: + description: 'Whether to use cache' + required: false + default: 'false' + post-cleanup: + description: 'Whether to run post cleanup' + required: false + default: 'false' + +runs: + using: 'composite' + steps: + - uses: prefix-dev/setup-pixi@v0.9.4 + with: + environments: ${{ inputs.environments }} + activate-environment: ${{ inputs.activate-environment }} + run-install: ${{ inputs.run-install }} + locked: ${{ inputs.locked }} + frozen: ${{ inputs.frozen }} + cache: ${{ inputs.cache }} + post-cleanup: ${{ inputs.post-cleanup }} diff --git a/.github/actions/upload-artifact/action.yml b/.github/actions/upload-artifact/action.yml new file mode 100644 index 00000000..fe8e4680 --- /dev/null +++ b/.github/actions/upload-artifact/action.yml @@ -0,0 +1,49 @@ +name: 'Upload artifact' +description: 'Wrapper for actions/upload-artifact' +inputs: + name: + description: 'Artifact name' + required: true + + path: + description: 'File(s)/dir(s)/glob(s) to upload (newline-separated)' + required: true + + include-hidden-files: + description: 'Include hidden files' + required: false + default: 'true' + + if-no-files-found: + description: 'warn | error | ignore' + required: false + default: 'error' + + compression-level: + description: '0-9 (0 = no compression)' + required: false + default: '0' + + retention-days: + description: 'Retention in days (optional)' + required: false + default: '' + + overwrite: + description: 'Overwrite an existing artifact with the same name' + required: false + default: 'false' + +runs: + using: 'composite' + steps: + - name: Upload artifact + uses: actions/upload-artifact@v7 + with: + name: ${{ inputs.name }} + path: ${{ inputs.path }} + include-hidden-files: ${{ inputs.include-hidden-files }} + if-no-files-found: ${{ inputs.if-no-files-found }} + compression-level: ${{ inputs.compression-level }} + retention-days: ${{ inputs.retention-days }} + overwrite: ${{ inputs.overwrite }} diff --git a/.github/actions/upload-codecov/action.yml b/.github/actions/upload-codecov/action.yml new file mode 100644 index 00000000..0cb15d1f --- /dev/null +++ b/.github/actions/upload-codecov/action.yml @@ -0,0 +1,42 @@ +name: 'Upload coverage to Codecov' +description: 'Wrapper for codecov/codecov-action' + +inputs: + name: + description: 'Codecov upload name' + required: true + + flags: + description: 'Codecov flags' + required: false + default: '' + + files: + description: 'Coverage report files' + required: true + + fail_ci_if_error: + description: 'Fail CI if upload fails' + required: false + default: 'true' + + verbose: + description: 'Enable verbose output' + required: false + default: 'true' + + token: + description: 'Codecov token' + required: true + +runs: + using: composite + steps: + - uses: codecov/codecov-action@v6 + with: + name: ${{ inputs.name }} + flags: ${{ inputs.flags }} + files: ${{ inputs.files }} + fail_ci_if_error: ${{ inputs.fail_ci_if_error }} + verbose: ${{ inputs.verbose }} + token: ${{ inputs.token }} diff --git a/.github/configs/pages-deployment.json b/.github/configs/pages-deployment.json new file mode 100644 index 00000000..c0d3fbee --- /dev/null +++ b/.github/configs/pages-deployment.json @@ -0,0 +1,6 @@ +{ + "source": { + "branch": "gh-pages", + "path": "/" + } +} diff --git a/.github/configs/rulesets-develop.json b/.github/configs/rulesets-develop.json new file mode 100644 index 00000000..04489e52 --- /dev/null +++ b/.github/configs/rulesets-develop.json @@ -0,0 +1,37 @@ +{ + "name": "develop branch", + "target": "branch", + "enforcement": "active", + "conditions": { + "ref_name": { + "include": ["refs/heads/develop"], + "exclude": [] + } + }, + "bypass_actors": [ + { + "actor_id": 2476259, + "actor_type": "Integration", + "bypass_mode": "always" + } + ], + "rules": [ + { + "type": "non_fast_forward" + }, + { + "type": "deletion" + }, + { + "type": "pull_request", + "parameters": { + "allowed_merge_methods": ["squash"], + "dismiss_stale_reviews_on_push": false, + "require_code_owner_review": false, + "require_last_push_approval": false, + "required_approving_review_count": 0, + "required_review_thread_resolution": false + } + } + ] +} diff --git a/.github/configs/rulesets-gh-pages.json b/.github/configs/rulesets-gh-pages.json new file mode 100644 index 00000000..ebf38928 --- /dev/null +++ b/.github/configs/rulesets-gh-pages.json @@ -0,0 +1,19 @@ +{ + "name": "gh-pages branch", + "target": "branch", + "enforcement": "active", + "conditions": { + "ref_name": { + "include": ["refs/heads/gh-pages"], + "exclude": [] + } + }, + "rules": [ + { + "type": "non_fast_forward" + }, + { + "type": "deletion" + } + ] +} diff --git a/.github/configs/rulesets-master.json b/.github/configs/rulesets-master.json new file mode 100644 index 00000000..f658a5c6 --- /dev/null +++ b/.github/configs/rulesets-master.json @@ -0,0 +1,30 @@ +{ + "name": "master branch", + "target": "branch", + "enforcement": "active", + "conditions": { + "ref_name": { + "include": ["~DEFAULT_BRANCH"], + "exclude": [] + } + }, + "rules": [ + { + "type": "non_fast_forward" + }, + { + "type": "deletion" + }, + { + "type": "pull_request", + "parameters": { + "allowed_merge_methods": ["merge"], + "dismiss_stale_reviews_on_push": false, + "require_code_owner_review": false, + "require_last_push_approval": false, + "required_approving_review_count": 0, + "required_review_thread_resolution": false + } + } + ] +} diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 881b2c53..533cae79 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -2,32 +2,40 @@ ## Project Overview -EasyReflectometryLib is a reflectometry Python package built on the EasyScience framework. It provides tools for reflectometry analysis and modeling. +EasyReflectometryLib is a reflectometry Python package built on the +EasyScience framework. It provides tools for reflectometry analysis and +modeling. ## Development Environment - **Python Versions**: 3.11, 3.12 -- **Supported Platforms**: Linux (ubuntu-latest), macOS (macos-latest), Windows (windows-latest) +- **Supported Platforms**: Linux (ubuntu-latest), macOS (macos-latest), + Windows (windows-latest) - **Package Manager**: pip - **Build System**: hatchling with setuptools-git-versioning ## Code Style and Formatting ### Ruff Configuration -- Use **Ruff** for linting and formatting (configured in `pyproject.toml`) + +- Use **Ruff** for linting and formatting (configured in + `pyproject.toml`) - Maximum line length: 127 characters - Quote style: single quotes for strings - Import style: force single-line imports - To fix issues automatically: `python -m ruff . --fix` ### Code Quality Standards + - Follow PEP 8 guidelines - Use type hints where appropriate - Write clear, self-documenting code with meaningful variable names - Maintain consistency with existing code patterns in the repository ### Linting Rules + The project uses Ruff with the following rule sets: + - `E9`, `F63`, `F7`, `F82`: Critical flake8 rules - `E`: pycodestyle errors - `F`: Pyflakes @@ -35,6 +43,7 @@ The project uses Ruff with the following rule sets: - `S`: flake8-bandit (security checks) Special notes: + - Asserts are allowed in test files (`*test_*.py`) - Init module imports are ignored - Exclude `docs` directory from linting @@ -42,12 +51,14 @@ Special notes: ## Testing ### Test Framework + - Use **pytest** for all tests - Test coverage should be tracked with **pytest-cov** - Aim for comprehensive test coverage - Tests are located in the `tests/` directory ### Running Tests + ```bash # Install dev dependencies pip install -e '.[dev]' @@ -61,6 +72,7 @@ tox ``` ### Test Guidelines + - Write unit tests for all new functionality - Include tests when fixing bugs to prevent regression - Test files should match the pattern `test_*.py` @@ -77,11 +89,14 @@ tox ## Documentation ### Docstring Style + - Include docstrings for all public modules, classes, and functions -- Use **Sphinx/reStructuredText style** docstrings (`:param`, `:type`, `:return`, `:rtype`) +- Use **Sphinx/reStructuredText style** docstrings (`:param`, `:type`, + `:return`, `:rtype`) - Use clear, concise descriptions - Document parameters, return values, and exceptions - Example format: + ```python """ Brief description of the function. @@ -94,6 +109,7 @@ tox ``` ### Documentation Build + - Documentation is built using Sphinx (version 8.1.3) - Source files are in the `docs/` directory - Use `myst_parser` (MyST parser) for Markdown support @@ -102,6 +118,7 @@ tox ## Dependencies ### Core Dependencies + - easyscience (EasyScience framework) - scipp (Scientific computing) - refnx, refl1d (Reflectometry calculations) @@ -109,6 +126,7 @@ tox - bumps (Optimization) ### Adding New Dependencies + - Only add dependencies when absolutely necessary - Add to appropriate section in `pyproject.toml`: - `dependencies` for core runtime dependencies @@ -119,13 +137,16 @@ tox ## Git and Version Control ### Commit Messages + - Write clear, descriptive commit messages - Use present tense ("Add feature" not "Added feature") - Reference issue numbers when applicable ### Branch Workflow + - Create feature branches from the main branch -- Use descriptive branch names (e.g., `feature/add-new-calculator`, `bugfix/fix-reflection-calculation`) +- Use descriptive branch names (e.g., `feature/add-new-calculator`, + `bugfix/fix-reflection-calculation`) - Keep changes focused and atomic ## Pull Request Guidelines @@ -157,17 +178,24 @@ docs/ # Documentation source ## Best Practices -1. **Minimal Changes**: Make the smallest possible changes to accomplish the task -2. **Don't Break Existing Code**: Maintain backward compatibility unless explicitly required -3. **Test Before Committing**: Always run tests and linting before pushing -4. **Follow Existing Patterns**: Look at similar code in the repository for guidance -5. **Ask When Uncertain**: If unsure about an approach, ask for clarification +1. **Minimal Changes**: Make the smallest possible changes to accomplish + the task +2. **Don't Break Existing Code**: Maintain backward compatibility unless + explicitly required +3. **Test Before Committing**: Always run tests and linting before + pushing +4. **Follow Existing Patterns**: Look at similar code in the repository + for guidance +5. **Ask When Uncertain**: If unsure about an approach, ask for + clarification ## CI/CD Pipeline The project uses GitHub Actions for continuous integration: + - **Code Consistency**: Runs Ruff linting on all pushes and PRs -- **Code Testing**: Runs pytest across multiple Python versions and platforms +- **Code Testing**: Runs pytest across multiple Python versions and + platforms - **Package Testing**: Validates package building and installation - **Coverage**: Uploads test coverage to Codecov @@ -177,5 +205,6 @@ All CI checks must pass before merging PRs. - The project is part of the EasyScience ecosystem - Built on top of established reflectometry libraries (refnx, refl1d) -- Focuses on providing a user-friendly interface for reflectometry analysis +- Focuses on providing a user-friendly interface for reflectometry + analysis - Maintains compatibility with multiple calculator backends diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml index cc95caf6..4f0d1964 100644 --- a/.github/release-drafter.yml +++ b/.github/release-drafter.yml @@ -4,30 +4,30 @@ name-template: v$NEXT_PATCH_VERSION 🌈 tag-template: v$NEXT_PATCH_VERSION categories: -- title: 🚀 Features - labels: - - feature - - enhancement -- title: 🐛 Bug Fixes - labels: - - fix - - bugfix - - bug -- title: 🧰 Maintenance - labels: - - chore - - documentation + - title: 🚀 Features + labels: + - feature + - enhancement + - title: 🐛 Bug Fixes + labels: + - fix + - bugfix + - bug + - title: 🧰 Maintenance + labels: + - chore + - documentation change-template: '- $TITLE @$AUTHOR (#$NUMBER)' version-resolver: major: labels: - - major + - major minor: labels: - - minor + - minor patch: labels: - - patch + - patch default: patch template: | ## Changes diff --git a/.github/scripts/backmerge-conflict-issue.js b/.github/scripts/backmerge-conflict-issue.js new file mode 100644 index 00000000..f6bd98b5 --- /dev/null +++ b/.github/scripts/backmerge-conflict-issue.js @@ -0,0 +1,69 @@ +module.exports = async ({ github, context, core }) => { + // Repo context + const owner = context.repo.owner + const repo = context.repo.repo + + // Link to the exact workflow run that detected the conflict + const runUrl = `${context.serverUrl}/${owner}/${repo}/actions/runs/${context.runId}` + + // We use a *stable title* so we can find/reuse the same "conflict tracker" issue + // instead of creating a new issue on every failed run. + const title = 'Backmerge conflict: master → develop' + + // Comment/issue body includes the run URL so maintainers can jump straight to logs. + const body = [ + 'Automatic backmerge failed due to merge conflicts.', + '', + `Workflow run: ${runUrl}`, + '', + 'Manual resolution required.', + ].join('\n') + + // Label applied to the tracker issue (assumed to already exist in the repo). + const label = '[bot] backmerge' + + // Search issues by title across *open and closed* issues. + // Why: if the conflict was resolved previously and the issue was closed, + // we prefer to reopen it and append a new comment instead of creating duplicates. + const q = `repo:${owner}/${repo} is:issue in:title "${title}"` + const search = await github.rest.search.issuesAndPullRequests({ + q, + per_page: 10, + }) + + // Pick the first exact-title match (search can return partial matches). + const existing = search.data.items.find((i) => i.title === title) + + if (existing) { + // If a tracker issue exists, reuse it: + // - reopen it if needed + // - add a comment with the new run URL + if (existing.state === 'closed') { + await github.rest.issues.update({ + owner, + repo, + issue_number: existing.number, + state: 'open', + }) + } + + await github.rest.issues.createComment({ + owner, + repo, + issue_number: existing.number, + body, + }) + + core.notice(`Conflict issue updated: #${existing.number}`) + return + } + + // No tracker issue exists yet -> create the first one. + await github.rest.issues.create({ + owner, + repo, + title, + body, + labels: [label], + }) +} diff --git a/.github/scripts/publish-dashboard.sh b/.github/scripts/publish-dashboard.sh new file mode 100644 index 00000000..dcbd0b32 --- /dev/null +++ b/.github/scripts/publish-dashboard.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash + +set -euo pipefail + +remote_repository="${DASHBOARD_REMOTE_REPOSITORY:?}" +publish_branch="${DASHBOARD_PUBLISH_BRANCH:-master}" +source_dir="${DASHBOARD_SOURCE_DIR:?}" +token="${DASHBOARD_TOKEN:?}" +git_user_name="${DASHBOARD_GIT_USER_NAME:-easyscience[bot]}" +git_user_email="${DASHBOARD_GIT_USER_EMAIL:?}" +commit_message="${DASHBOARD_COMMIT_MESSAGE:?}" +max_attempts="${DASHBOARD_PUSH_ATTEMPTS:-3}" +delay_seconds="${DASHBOARD_PUSH_DELAY_SECONDS:-15}" + +workspace_dir="$(mktemp -d)" +repo_dir="${workspace_dir}/dashboard" +remote_url="https://x-access-token:${token}@github.com/${remote_repository}.git" + +cleanup() { + rm -rf "${workspace_dir}" +} + +prepare_worktree() { + if [[ ! -d "${repo_dir}/.git" ]]; then + git clone --branch "${publish_branch}" --depth 1 "${remote_url}" "${repo_dir}" + else + git -C "${repo_dir}" fetch origin "${publish_branch}" + git -C "${repo_dir}" checkout "${publish_branch}" + git -C "${repo_dir}" reset --hard "origin/${publish_branch}" + git -C "${repo_dir}" clean -fd + fi + + git -C "${repo_dir}" config user.name "${git_user_name}" + git -C "${repo_dir}" config user.email "${git_user_email}" +} + +sync_publish_dir() { + cp -R "${source_dir}/." "${repo_dir}/" + git -C "${repo_dir}" add . + + if git -C "${repo_dir}" diff --cached --quiet; then + return 1 + fi + + git -C "${repo_dir}" commit -m "${commit_message}" +} + +trap cleanup EXIT + +prepare_worktree + +if ! sync_publish_dir; then + echo "No dashboard changes to publish." + exit 0 +fi + +for ((attempt = 1; attempt <= max_attempts; attempt += 1)); do + if git -C "${repo_dir}" push origin "HEAD:${publish_branch}"; then + echo "Dashboard published on attempt ${attempt}." + exit 0 + fi + + if ((attempt == max_attempts)); then + echo "Dashboard publish failed after ${max_attempts} attempts." >&2 + exit 1 + fi + + echo "Dashboard push attempt ${attempt} failed. Retrying in ${delay_seconds}s." >&2 + sleep "${delay_seconds}" + + prepare_worktree + + if ! sync_publish_dir; then + echo "Dashboard changes already exist in the target repository." + exit 0 + fi +done \ No newline at end of file diff --git a/.github/workflows/backmerge.yml b/.github/workflows/backmerge.yml new file mode 100644 index 00000000..36ce6f54 --- /dev/null +++ b/.github/workflows/backmerge.yml @@ -0,0 +1,109 @@ +# This workflow automatically merges `master` into `develop` whenever a +# new version release with a tag is published. It can also be triggered +# manually via workflow_dispatch for cases where an automatic backmerge +# is needed outside of the standard release process. +# If a merge conflict occurs, the workflow creates an issue to notify +# maintainers for manual resolution. + +name: Backmerge (master → develop) + +on: + release: + types: [published, prereleased] + workflow_dispatch: + +permissions: + contents: write + issues: write + +concurrency: + group: backmerge-master-into-develop + cancel-in-progress: false + +jobs: + backmerge: + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - name: Checkout repository (for local actions) + uses: actions/checkout@v6 + + - name: Setup easyscience[bot] + id: bot + uses: ./.github/actions/setup-easyscience-bot + with: + app-id: ${{ vars.EASYSCIENCE_APP_ID }} + private-key: ${{ secrets.EASYSCIENCE_APP_KEY }} + repositories: ${{ github.event.repository.name }} + + - name: Checkout repository (with bot token) + uses: actions/checkout@v6 + with: + fetch-depth: 0 + token: ${{ steps.bot.outputs.token }} + + - name: Configure git identity + run: | + git config user.name "easyscience[bot]" + git config user.email "${{ vars.EASYSCIENCE_APP_ID }}+easyscience[bot]@users.noreply.github.com" + + - name: Set merge message + run: | + if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then + MESSAGE="Backmerge: master into develop (manual) [skip ci]" + else + TAG="${{ github.event.release.tag_name }}" + MESSAGE="Backmerge: master (${TAG}) into develop [skip ci]" + fi + + echo "MESSAGE=$MESSAGE" >> "$GITHUB_ENV" + echo "message=$MESSAGE" >> "$GITHUB_OUTPUT" + echo "📝 Merge message: $MESSAGE" | tee -a "$GITHUB_STEP_SUMMARY" + + - name: Prepare branches + run: | + git fetch origin master develop + git checkout -B develop origin/develop + + - name: Check if develop is already up-to-date + id: up_to_date + run: | + if git merge-base --is-ancestor origin/master develop; then + echo "value=true" >> "$GITHUB_OUTPUT" + echo "ℹ️ Develop is already up-to-date with master" | tee -a "$GITHUB_STEP_SUMMARY" + else + echo "value=false" >> "$GITHUB_OUTPUT" + fi + + - name: Try merge master into develop + id: merge + if: steps.up_to_date.outputs.value == 'false' + continue-on-error: true + run: | + if ! git merge origin/master --no-ff -m "${MESSAGE}"; then + echo "conflict=true" >> "$GITHUB_OUTPUT" + echo "❌ Backmerge conflict detected." | tee -a "$GITHUB_STEP_SUMMARY" + git status --porcelain || true + exit 0 + fi + + echo "conflict=false" >> "$GITHUB_OUTPUT" + echo "✅ Merge commit created." | tee -a "$GITHUB_STEP_SUMMARY" + + - name: Push to develop (if merge succeeded) + if: + steps.up_to_date.outputs.value == 'false' && steps.merge.outputs.conflict == + 'false' + run: | + git push origin develop + echo "🚀 Backmerge successful: master → develop" | tee -a "$GITHUB_STEP_SUMMARY" + + - name: Create issue (if merge failed with conflicts) + if: steps.merge.outputs.conflict == 'true' + uses: ./.github/actions/github-script + with: + github-token: ${{ steps.bot.outputs.token }} + script: | + const run = require('./.github/scripts/backmerge-conflict-issue.js') + await run({ github, context, core }) diff --git a/.github/workflows/cleanup.yml b/.github/workflows/cleanup.yml new file mode 100644 index 00000000..21c72b38 --- /dev/null +++ b/.github/workflows/cleanup.yml @@ -0,0 +1,84 @@ +# This workflow will delete old workflow runs based on the input +# parameters. +# https://github.com/Mattraks/delete-workflow-runs + +name: Old workflow runs cleanup + +on: + # Run monthly, at 00:00 on the 1st day of month. + schedule: + - cron: '0 0 1 * *' + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + inputs: + days: + description: 'Number of days.' + required: true + default: '30' + minimum_runs: + description: 'The minimum runs to keep for each workflow.' + required: true + default: '6' + delete_workflow_pattern: + description: + 'The name or filename of the workflow. if not set then it will target all + workflows.' + required: false + delete_workflow_by_state_pattern: + description: + 'Remove workflow by state: active, deleted, disabled_fork, + disabled_inactivity, disabled_manually' + required: true + default: 'All' + type: choice + options: + - 'All' + - active + - deleted + - disabled_inactivity + - disabled_manually + delete_run_by_conclusion_pattern: + description: + 'Remove workflow by conclusion: action_required, cancelled, failure, skipped, + success' + required: true + default: 'All' + type: choice + options: + - 'All' + - action_required + - cancelled + - failure + - skipped + - success + dry_run: + description: 'Only log actions, do not perform any delete operations (dry run).' + required: false + default: 'false' + type: choice + options: + - 'false' + - 'true' + +jobs: + del-runs: + runs-on: ubuntu-latest + + permissions: + actions: write + + steps: + - name: Delete workflow runs + uses: Mattraks/delete-workflow-runs@v2 + with: + token: ${{ github.token }} + repository: ${{ github.repository }} + retain_days: ${{ github.event.inputs.days }} + keep_minimum_runs: ${{ github.event.inputs.minimum_runs }} + delete_workflow_pattern: ${{ github.event.inputs.delete_workflow_pattern }} + delete_workflow_by_state_pattern: + ${{ github.event.inputs.delete_workflow_by_state_pattern }} + delete_run_by_conclusion_pattern: + ${{ github.event.inputs.delete_run_by_conclusion_pattern }} + dry_run: ${{ github.event.inputs.dry_run }} diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index b388f884..238dc354 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -1,11 +1,11 @@ -name: "CodeQL" +name: 'CodeQL' on: push: - branches: [ master, pre-release, develop ] + branches: [master, pre-release, develop] pull_request: # The branches below must be a subset of the branches above - branches: [ master ] + branches: [master] schedule: - cron: '0 16 * * 5' @@ -24,43 +24,43 @@ jobs: # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - # We must fetch at least the immediate parents so that if this is - # a pull request then we can checkout the head. - fetch-depth: 2 + - name: Checkout repository + uses: actions/checkout@v4 + with: + # We must fetch at least the immediate parents so that if this is + # a pull request then we can checkout the head. + fetch-depth: 2 - # If this run was triggered by a pull request event, then checkout - # the head of the pull request instead of the merge commit. - - run: git checkout HEAD^2 - if: ${{ github.event_name == 'pull_request' }} + # If this run was triggered by a pull request event, then checkout + # the head of the pull request instead of the merge commit. + - run: git checkout HEAD^2 + if: ${{ github.event_name == 'pull_request' }} - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v3 - with: - languages: ${{ matrix.language }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - # queries: ./path/to/local/query, your-org/your-repo/queries@master + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@master - # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@v3 + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v3 - # ℹ️ Command-line programs to run using the OS shell. - # 📚 https://git.io/JvXDl + # ℹ️ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl - # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines - # and modify them (or add more) to build your code if your project - # uses a compiled language + # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language - #- run: | - # make bootstrap - # make release + #- run: | + # make bootstrap + # make release - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 \ No newline at end of file + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml new file mode 100644 index 00000000..d96e5b87 --- /dev/null +++ b/.github/workflows/coverage.yml @@ -0,0 +1,94 @@ +name: Coverage checks + +on: + # Trigger the workflow on push to develop + push: + branches: + - develop + # Trigger the workflow on pull request + pull_request: + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# Need permissions to trigger the dashboard build workflow +permissions: + actions: write + contents: read + +# 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: ${{ github.event_name == 'pull_request' }} + +# Set the environment variables to be used in all jobs defined in this workflow +env: + CI_BRANCH: ${{ github.head_ref || github.ref_name }} + +jobs: + # Job 1: Run docstring coverage + docstring-coverage: + runs-on: ubuntu-latest + + steps: + - name: Check-out repository + uses: actions/checkout@v6 + + - name: Set up pixi + uses: ./.github/actions/setup-pixi + + - name: Run docstring coverage + run: pixi run docstring-coverage + + # Job 2: Run unit tests with coverage and upload to Codecov + unit-tests-coverage: + runs-on: ubuntu-latest + + steps: + - name: Check-out repository + uses: actions/checkout@v6 + + - name: Set up pixi + uses: ./.github/actions/setup-pixi + + - name: Run unit tests with coverage + run: pixi run unit-tests-coverage --cov-report=xml:coverage-unit.xml + + - name: Upload unit tests coverage to Codecov + if: ${{ !cancelled() }} + uses: ./.github/actions/upload-codecov + with: + name: unit-tests-job + flags: unittests + files: ./coverage-unit.xml + token: ${{ secrets.CODECOV_TOKEN }} + + # Job 2: Run integration tests with coverage and upload to Codecov + integration-tests-coverage: + runs-on: ubuntu-latest + + steps: + - name: Check-out repository + uses: actions/checkout@v6 + + - name: Set up pixi + uses: ./.github/actions/setup-pixi + + - name: Run integration tests with coverage + run: + pixi run integration-tests-coverage --cov-report=xml:coverage-integration.xml + + - name: Upload integration tests coverage to Codecov + if: ${{ !cancelled() }} + uses: ./.github/actions/upload-codecov + with: + name: integration-tests-job + flags: integration + files: ./coverage-integration.xml + token: ${{ secrets.CODECOV_TOKEN }} + + # Job 4: Build and publish dashboard (reusable workflow) + run-reusable-workflows: + needs: [docstring-coverage, unit-tests-coverage, integration-tests-coverage] # depend on the previous jobs + uses: ./.github/workflows/dashboard.yml + secrets: inherit diff --git a/.github/workflows/dashboard.yml b/.github/workflows/dashboard.yml new file mode 100644 index 00000000..9d1f2b0b --- /dev/null +++ b/.github/workflows/dashboard.yml @@ -0,0 +1,111 @@ +name: Dashboard build and publish + +on: + workflow_dispatch: + workflow_call: + +permissions: + contents: read + +concurrency: + group: dashboard-publish-${{ github.repository }} + cancel-in-progress: false + +# Set the environment variables to be used in all jobs defined in this workflow +env: + CI_BRANCH: ${{ github.head_ref || github.ref_name }} + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + DEVELOP_BRANCH: develop + REPO_OWNER: ${{ github.repository_owner }} + REPO_NAME: ${{ github.event.repository.name }} + +jobs: + dashboard: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Set up pixi + uses: ./.github/actions/setup-pixi + + - name: Install badgery + shell: bash + run: pixi add --pypi --git https://github.com/enhantica/badgery badgery + + - name: Run docstring coverage and code complexity/maintainability checks + run: | + for BRANCH in $DEFAULT_BRANCH $DEVELOP_BRANCH $CI_BRANCH; do + echo + echo "🔹🔸🔹🔸🔹 Processing branch $BRANCH 🔹🔸🔹🔸🔹" + if [ -d "../$BRANCH" ]; then + echo "Branch $BRANCH already processed, skipping" + continue + fi + + git worktree add ../$BRANCH origin/$BRANCH + mkdir -p reports/$BRANCH + + echo "Docstring coverage for branch $BRANCH" + pixi run interrogate -c pyproject.toml --fail-under=0 ../$BRANCH/src > reports/$BRANCH/coverage-docstring.txt + + echo "Cyclomatic complexity for branch $BRANCH" + pixi run radon cc -s -j ../$BRANCH/src > reports/$BRANCH/cyclomatic-complexity.json + + echo "Maintainability index for branch $BRANCH" + pixi run radon mi -j ../$BRANCH/src > reports/$BRANCH/maintainability-index.json + + echo "Raw metrics for branch $BRANCH" + pixi run radon raw -s -j ../$BRANCH/src > reports/$BRANCH/raw-metrics.json + done + + - name: Generate dashboard HTML + run: > + pixi run python -m badgery --config .badgery.yaml --repo ${{ github.repository + }} --branch ${{ env.CI_BRANCH }} --output index.html + + - name: Prepare publish directory + run: | + mkdir -p _dashboard_publish/${{ env.REPO_NAME }}/${{ env.CI_BRANCH }} + cp index.html _dashboard_publish/${{ env.REPO_NAME }}/${{ env.CI_BRANCH }} + + # Create GitHub App token for pushing to external dashboard repo. + # The 'repositories' parameter is required to grant access to repos + # other than the one where the workflow is running. + - name: Setup easyscience[bot] + id: bot + uses: ./.github/actions/setup-easyscience-bot + with: + app-id: ${{ vars.EASYSCIENCE_APP_ID }} + private-key: ${{ secrets.EASYSCIENCE_APP_KEY }} + repositories: | + ${{ github.event.repository.name }} + dashboard + + # Push to external dashboard repository with retry logic. + # Retry is needed to handle transient GitHub API/authentication issues + # that occasionally cause 403 errors when multiple workflows push concurrently. + # Uses personal_token (not github_token) as GITHUB_TOKEN cannot access external repos. + - name: + Push to ${{ env.REPO_OWNER }}/dashboard/${{ env.REPO_NAME }}/${{ env.CI_BRANCH + }} + shell: bash + env: + DASHBOARD_COMMIT_MESSAGE: ${{ env.CI_BRANCH }} + DASHBOARD_GIT_USER_EMAIL: + ${{ vars.EASYSCIENCE_APP_ID }}+easyscience[bot]@users.noreply.github.com + DASHBOARD_PUSH_ATTEMPTS: '3' + DASHBOARD_PUSH_DELAY_SECONDS: '15' + DASHBOARD_PUBLISH_BRANCH: master + DASHBOARD_REMOTE_REPOSITORY: ${{ env.REPO_OWNER }}/dashboard + DASHBOARD_SOURCE_DIR: ./_dashboard_publish + DASHBOARD_TOKEN: ${{ steps.bot.outputs.token }} + run: bash ./.github/scripts/publish-dashboard.sh + + - name: Add dashboard link to summary + run: | + URL="https://${{ env.REPO_OWNER }}.github.io/dashboard/${{ env.REPO_NAME }}/${{ env.CI_BRANCH }}" + echo "Dashboard link: [$URL]($URL)" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 00000000..d8622c29 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,188 @@ +# This workflow builds and deploys documentation for the project. +# +# Overview: +# - Converts tutorial Python scripts to Jupyter notebooks and executes them. +# - Builds the documentation site using MkDocs with the Material theme. +# - Uploads the built site as an artifact for local inspection. +# - Deploys versioned documentation to the gh-pages branch using Mike: +# - For release tags (v*): deploys to a versioned folder (e.g., /0.9.1/) and updates /latest/. +# - For branches: deploys to /dev/. +# +# The action summary page will contain a link to the built artifact for downloading +# and inspecting, as well as a link to the deployed documentation site. + +name: Docs build and deployment + +on: + # Trigger the workflow on push + push: + # Selected branches + branches: [develop] # master and main are already verified in PR + # Runs on creating a new tag starting with 'v', e.g. 'v1.0.3' + tags: ['v*'] + # Trigger the workflow on pull request + pull_request: + # Selected branches + branches: [master, main, develop] + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# - Non-tagged pushes and pull requests all use `docs-dev` group, so +# they cancel each other. +# - Tagged pushes use their own group like docs-v1.2.3, so they do not +# cancel non-tagged runs, and non-tagged runs do not cancel them. +concurrency: + group: >- + ${{ startsWith(github.ref, 'refs/tags/v') + && format('docs-{0}', github.ref_name) + || 'docs-dev' }} + cancel-in-progress: true + +# Set the environment variables to be used in all jobs defined in this workflow +env: + # CI_BRANCH - the branch name (used in mkdocs.yml) + # For PRs: github.head_ref is the source branch + # For pushes: github.ref_name is the branch + # For tags: github.ref_name is the tag name + # GITHUB_REPOSITORY - the repository name (used in mkdocs.yml) + # NOTEBOOKS_DIR - the directory containing the Jupyter notebooks (used in mkdocs.yml) + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + DEVELOP_BRANCH: develop + CI_BRANCH: ${{ github.head_ref || github.ref_name }} + IS_RELEASE_TAG: ${{ startsWith(github.ref, 'refs/tags/v') }} + GITHUB_REPOSITORY: ${{ github.repository }} + NOTEBOOKS_DIR: tutorials + +jobs: + # Single job that builds and deploys documentation. + # Uses macOS runner for consistent Plotly chart rendering. + build-deploy-docs: + runs-on: ubuntu-latest # macos-latest + + permissions: + contents: write # Required for pushing to the gh-pages branch + + steps: + # Setting DOCS_VERSION to be used in mkdocs.yml, and then in the + # main.html template. It defines the versioned docs subfolder name + # in the gh-pages branch. If it's a release tag, use the version + # number without the 'v' prefix, otherwise use 'dev'. + # Setting RELEASE_VERSION to be used in mkdocs.yml to show + # the latest release version in the index.md file. If it's a + # release tag, use the tag name, otherwise use the branch name + # for development builds. + - name: Set extra env variables + shell: bash + run: | + if [[ "${IS_RELEASE_TAG}" == "true" ]]; then + RELEASE_VERSION="${GITHUB_REF_NAME}" + DOCS_VERSION="${RELEASE_VERSION#v}" + else + RELEASE_VERSION="${CI_BRANCH}" + DOCS_VERSION="dev" + fi + echo "RELEASE_VERSION=${RELEASE_VERSION}" >> "$GITHUB_ENV" + echo "DOCS_VERSION=${DOCS_VERSION}" >> "$GITHUB_ENV" + + # Check out the repository source code. + # Note: The gh-pages branch is fetched separately later for mike deployment. + - name: Checkout repository + uses: actions/checkout@v6 + + # Activate dark mode to create documentation with Plotly charts in dark mode + # Need a better solution to automatically switch the chart colour theme based on the mkdocs material switcher + # Something similar to mkdocs_plotly_plugin https://haoda-li.github.io/mkdocs-plotly-plugin/, + # but for generating documentation from notepads + #- name: Activate dark mode + # run: | + # brew install dark-mode + # dark-mode status + # dark-mode on + # dark-mode status + + # Set up the pixi package manager and install dependencies from pixi.toml. + # Uses frozen lockfile to ensure reproducible builds. + - name: Set up pixi + uses: ./.github/actions/setup-pixi + # Pre-import the main package to exclude info messages from the docs + # E.g., Matplotlib may print messages to stdout/stderr when first + # imported. This step allows to avoid "Matplotlib is building the font + # cache" messages during notebook execution. + - name: Pre-build site step + run: pixi run python -c "import easyreflectometry" + + # Prepare the Jupyter notebooks for documentation (strip output, etc.). + - name: Prepare notebooks + run: pixi run notebook-prepare + + # Execute all Jupyter notebooks to generate output cells (plots, tables, etc.). + # Uses multiple cores for parallel execution to speed up the process. + - name: Run notebooks + # if: false # Temporarily disabled to speed up the docs build + run: pixi run notebook-exec + + # Build the static files for the documentation site for local inspection + # Input: docs/ directory containing the Markdown files + # Output: site/ directory containing the generated HTML files + - name: Build site for local check + run: pixi run docs-build-local + + # Upload the static files from the site/ directory to be used for + # local check + - name: Upload built site as artifact + uses: ./.github/actions/upload-artifact + with: + name: site-local_easyreflectometry-lib-${{ env.RELEASE_VERSION }} + path: docs/site/ + + # Create GitHub App token for pushing to gh-pages as easyscience[bot]. + - name: Setup easyscience[bot] + id: bot + uses: ./.github/actions/setup-easyscience-bot + with: + app-id: ${{ vars.EASYSCIENCE_APP_ID }} + private-key: ${{ secrets.EASYSCIENCE_APP_KEY }} + + # Configure git identity and remote URL so mike pushes as easyscience[bot]. + - name: Configure git for pushing + run: | + set -euo pipefail + git config user.name "easyscience[bot]" + git config user.email "${{ vars.EASYSCIENCE_APP_ID }}+easyscience[bot]@users.noreply.github.com" + git remote set-url origin "https://x-access-token:${{ steps.bot.outputs.token }}@github.com/${{ github.repository }}.git" + + # Fetch the gh-pages branch to ensure mike has the latest remote state. + # This is required because the checkout step only fetches the source branch, + # not the gh-pages branch that mike needs to update. + - name: Fetch gh-pages branch + run: | + git fetch origin gh-pages:gh-pages 2>/dev/null || true + + # Deploy versioned documentation using mike (MkDocs plugin for versioning). + # - For release tags (v*): deploys to versioned folder (e.g., /0.9.1/) and aliases to /latest/. + # - For branches: deploys to /dev/. + # The "${RELEASE_VERSION#v}" syntax strips the 'v' prefix (v0.9.1 -> 0.9.1). + # Also sets 'latest' as the default version for the version selector. + - name: Rebuild and deploy docs with mike + run: | + # Exit on error (-e), undefined vars (-u), and pipeline failures (pipefail) + set -euo pipefail + + REPO_NAME="${{ github.event.repository.name }}" + BASE_URL="https://easyscience.github.io/${REPO_NAME}" + + # Deploy the release version and update the "latest" alias + if [[ "${IS_RELEASE_TAG}" == "true" ]]; then + pixi run docs-deploy-pre "${RELEASE_VERSION#v}" latest + pixi run docs-set-default-pre latest + DEPLOYMENT_URL="${BASE_URL}/latest" + + # Deploy/update the "dev" alias (or whatever your convention is) + else + pixi run docs-deploy-pre dev + DEPLOYMENT_URL="${BASE_URL}/dev" + + fi + + # Add links to the action summary page for easy access + echo "🔗 deployment url [${DEPLOYMENT_URL}](${DEPLOYMENT_URL})" >> "${GITHUB_STEP_SUMMARY}" diff --git a/.github/workflows/documentation-build.yml b/.github/workflows/documentation-build.yml index f447b491..b3477bb6 100644 --- a/.github/workflows/documentation-build.yml +++ b/.github/workflows/documentation-build.yml @@ -29,40 +29,40 @@ jobs: # Grant GITHUB_TOKEN the permissions required to make a Pages deployment permissions: - contents: read # to clone the repository - pages: write # to deploy to Pages - id-token: write # to verify the deployment originates from an appropriate source + contents: read # to clone the repository + pages: write # to deploy to Pages + id-token: write # to verify the deployment originates from an appropriate source steps: - - name: Checkout - uses: actions/checkout@master - with: - fetch-depth: 0 # otherwise, you will failed to push refs to dest repo - - name: Upgrade pip - run: | - python -m pip install --upgrade pip - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: 3.12 - - name: Install Pandoc, repo and dependencies - run: | - sudo apt install pandoc - sudo apt install libcairo2-dev - pip install sphinx==8.1.3 - pip install . '.[dev,docs]' + - name: Checkout + uses: actions/checkout@master + with: + fetch-depth: 0 # otherwise, you will failed to push refs to dest repo + - name: Upgrade pip + run: | + python -m pip install --upgrade pip + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: 3.12 + - name: Install Pandoc, repo and dependencies + run: | + sudo apt install pandoc + sudo apt install libcairo2-dev + pip install sphinx==8.1.3 + pip install . '.[dev,docs]' - - name: Install Jupyter kernel - run: | - python -m ipykernel install --user --name=python3 + - name: Install Jupyter kernel + run: | + python -m ipykernel install --user --name=python3 - - name: Build and Commit - uses: sphinx-notes/pages@v3 - with: - sphinx_version: 8.1.3 - documentation_path: docs/src - - name: Push changes - uses: ad-m/github-push-action@master - continue-on-error: true - with: - branch: gh-pages \ No newline at end of file + - name: Build and Commit + uses: sphinx-notes/pages@v3 + with: + sphinx_version: 8.1.3 + documentation_path: docs/src + - name: Push changes + uses: ad-m/github-push-action@master + continue-on-error: true + with: + branch: gh-pages diff --git a/.github/workflows/issues-labels.yml b/.github/workflows/issues-labels.yml new file mode 100644 index 00000000..56ab5b19 --- /dev/null +++ b/.github/workflows/issues-labels.yml @@ -0,0 +1,149 @@ +# Verifies if the current issue has at least one real `[scope]` label and one +# real `[priority]` label. If either is missing, the workflow adds a reminder +# label with a warning emoji. + +name: Issue labels check + +on: + issues: + types: [opened, labeled, unlabeled] + +permissions: + issues: write + +jobs: + check-labels: + runs-on: ubuntu-latest + + concurrency: + group: issue-labels-${{ github.event.issue.number }} + cancel-in-progress: true + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Sync missing-label reminders + uses: ./.github/actions/github-script + with: + script: | + const fs = require('fs'); + + const issueNumber = context.issue.number; + const action = context.payload.action; + const changedLabel = context.payload.label?.name ?? null; + const labels = context.payload.issue.labels.map(({ name }) => name); + const requirements = [ + { + prefix: '[scope] ', + reminder: '[scope] ⚠️ label needed', + }, + { + prefix: '[priority] ', + reminder: '[priority] ⚠️ label needed', + }, + ]; + + const labelsToAdd = []; + const labelsToRemove = []; + const evaluations = []; + + console.log(`::group::Issue label check for #${issueNumber}`); + console.log(`Event action: ${action}`); + if (changedLabel) { + console.log(`Event label: ${changedLabel}`); + } + console.log( + `Current labels: ${labels.length > 0 ? labels.join(', ') : '(none)'}`, + ); + + for (const { prefix, reminder } of requirements) { + const matchingRealLabels = labels.filter( + (name) => name.startsWith(prefix) && name !== reminder, + ); + const hasRealLabel = matchingRealLabels.length > 0; + const hasReminderLabel = labels.includes(reminder); + + evaluations.push({ + prefix, + reminder, + matchingRealLabels, + hasReminderLabel, + }); + + if (hasRealLabel && hasReminderLabel) { + labelsToRemove.push(reminder); + } else if (!hasRealLabel && !hasReminderLabel) { + labelsToAdd.push(reminder); + } + } + + for (const evaluation of evaluations) { + if (evaluation.matchingRealLabels.length > 0) { + console.log( + `Found required ${evaluation.prefix}label(s): ${evaluation.matchingRealLabels.join(', ')}`, + ); + } else { + console.log(`Missing required ${evaluation.prefix}label.`); + } + + if (evaluation.hasReminderLabel) { + console.log(`Reminder label already present: ${evaluation.reminder}`); + } + } + + if (labelsToAdd.length > 0) { + console.log(`Adding reminder labels: ${labelsToAdd.join(', ')}`); + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + labels: labelsToAdd, + }); + } + + for (const name of labelsToRemove) { + console.log(`Removing reminder label: ${name}`); + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + name, + }); + } + + if (labelsToAdd.length === 0 && labelsToRemove.length === 0) { + console.log('No label changes required.'); + } + + console.log('::endgroup::'); + + if (process.env.GITHUB_STEP_SUMMARY) { + const summaryLines = [ + '### Issue Label Check', + `- Issue: #${issueNumber}`, + `- Event: ${action}`, + `- Trigger label: ${changedLabel ?? '(none)'}`, + `- Current labels: ${labels.length > 0 ? labels.join(', ') : '(none)'}`, + '', + '#### Requirement status', + ...evaluations.map((evaluation) => { + const status = + evaluation.matchingRealLabels.length > 0 + ? `found ${evaluation.matchingRealLabels.join(', ')}` + : 'missing'; + const reminder = evaluation.hasReminderLabel + ? `reminder present: ${evaluation.reminder}` + : `reminder absent: ${evaluation.reminder}`; + return `- ${evaluation.prefix}: ${status}; ${reminder}`; + }), + '', + `- Labels to add: ${labelsToAdd.length > 0 ? labelsToAdd.join(', ') : '(none)'}`, + `- Labels to remove: ${labelsToRemove.length > 0 ? labelsToRemove.join(', ') : '(none)'}`, + ]; + + fs.appendFileSync( + process.env.GITHUB_STEP_SUMMARY, + `${summaryLines.join('\n')}\n`, + ); + } diff --git a/.github/workflows/lint-format.yml b/.github/workflows/lint-format.yml new file mode 100644 index 00000000..16dd95c9 --- /dev/null +++ b/.github/workflows/lint-format.yml @@ -0,0 +1,124 @@ +# The workflow checks +# - the validity of pyproject.toml, +# - the presence and correctness of SPDX license headers, +# - linting and formatting of Python code, +# - linting and formatting of docstrings in Python code, +# - formatting of non-Python files (like markdown and toml). +# - linting of Python code in Jupyter notebooks (for library template). +# +# A summary of the checks is added to the GitHub Actions summary. + +name: Lint and format checks + +on: + # Trigger the workflow on push + push: + branches-ignore: [master, main] # Already verified in PR + # Do not run this workflow on creating a new tag starting with + # 'v', e.g. 'v1.0.3' (see publish-pypi.yml) + tags-ignore: ['v*'] + # Trigger the workflow on pull request + pull_request: + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# Allow only one concurrent workflow, skipping runs queued between the run +# in-progress and latest queued. And cancel in-progress runs. +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +# Set the environment variables to be used in all jobs defined in this workflow +env: + CI_BRANCH: ${{ github.head_ref || github.ref_name }} + +jobs: + lint-format: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Set up pixi + uses: ./.github/actions/setup-pixi + + - name: Run post-install developer steps + run: pixi run post-install + + - name: Check validity of pyproject.toml + id: pyproject + continue-on-error: true + shell: bash + run: pixi run pyproject-check + + - name: Check SPDX license headers + id: license_headers + continue-on-error: true + shell: bash + run: pixi run license-check + + - name: Check linting of Python code + id: py_lint + continue-on-error: true + shell: bash + run: pixi run py-lint-check + + - name: Check formatting of Python code + id: py_format + continue-on-error: true + shell: bash + run: pixi run py-format-check + + - name: Check linting of docstrings in Python code + id: docstring_lint + continue-on-error: true + shell: bash + run: pixi run docstring-lint-check + + - name: Check formatting of non-Python files (md, toml, etc.) + id: nonpy_format + continue-on-error: true + shell: bash + run: pixi run nonpy-format-check + + - name: Check linting of Python code in Jupyter notebooks (ipynb) + id: notebook_lint + continue-on-error: true + shell: bash + run: pixi run notebook-lint-check + + # Add summary + - name: Add quality checks summary + if: always() + shell: bash + run: | + { + echo "## 🧪 Checks Summary" + echo "" + echo "| Check | Status |" + echo "|-------|--------|" + echo "| pyproject.toml | ${{ steps.pyproject.outcome == 'success' && '✅' || '❌' }} |" + echo "| license headers | ${{ steps.license_headers.outcome == 'success' && '✅' || '❌' }} |" + echo "| py lint | ${{ steps.py_lint.outcome == 'success' && '✅' || '❌' }} |" + echo "| py format | ${{ steps.py_format.outcome == 'success' && '✅' || '❌' }} |" + echo "| docstring lint | ${{ steps.docstring_lint.outcome == 'success' && '✅' || '❌' }} |" + echo "| nonpy format | ${{ steps.nonpy_format.outcome == 'success' && '✅' || '❌' }} |" + echo "| notebooks lint | ${{ steps.notebook_lint.outcome == 'success' && '✅' || '❌' }} |" + } >> "$GITHUB_STEP_SUMMARY" + + # Fail job if any check failed + - name: Fail job if any check failed + if: | + steps.pyproject.outcome == 'failure' + || steps.license_headers.outcome == 'failure' + || steps.py_lint.outcome == 'failure' + || steps.py_format.outcome == 'failure' + || steps.docstring_lint.outcome == 'failure' + || steps.nonpy_format.outcome == 'failure' + || steps.notebook_lint.outcome == 'failure' + shell: bash + run: exit 1 diff --git a/.github/workflows/ossar-analysis.yml b/.github/workflows/ossar-analysis.yml index a2c77306..94859565 100644 --- a/.github/workflows/ossar-analysis.yml +++ b/.github/workflows/ossar-analysis.yml @@ -3,7 +3,6 @@ # # For more information see: https://github.com/github/ossar-action - name: OSSAR on: @@ -17,35 +16,34 @@ jobs: steps: # Checkout your code repository to scan - - name: Checkout repository - uses: actions/checkout@v4 - with: - # We must fetch at least the immediate parents so that if this is - # a pull request then we can checkout the head. - fetch-depth: 2 - - # If this run was triggered by a pull request event, then checkout - # the head of the pull request instead of the merge commit. - - run: git checkout HEAD^2 - if: ${{ github.event_name == 'pull_request' }} - - # Ensure a compatible version of dotnet is installed. - # The [Microsoft Security Code Analysis CLI](https://aka.ms/mscadocs) is built with dotnet v3.1.201. - # A version greater than or equal to v3.1.201 of dotnet must be installed on the agent in order to run this action. - # Remote agents already have a compatible version of dotnet installed and this step may be skipped. - # For local agents, ensure dotnet version 3.1.201 or later is installed by including this action: - # - name: Install .NET - # uses: actions/setup-dotnet@v1 - # with: - # dotnet-version: '3.1.x' - + - name: Checkout repository + uses: actions/checkout@v4 + with: + # We must fetch at least the immediate parents so that if this is + # a pull request then we can checkout the head. + fetch-depth: 2 + + # If this run was triggered by a pull request event, then checkout + # the head of the pull request instead of the merge commit. + - run: git checkout HEAD^2 + if: ${{ github.event_name == 'pull_request' }} + + # Ensure a compatible version of dotnet is installed. + # The [Microsoft Security Code Analysis CLI](https://aka.ms/mscadocs) is built with dotnet v3.1.201. + # A version greater than or equal to v3.1.201 of dotnet must be installed on the agent in order to run this action. + # Remote agents already have a compatible version of dotnet installed and this step may be skipped. + # For local agents, ensure dotnet version 3.1.201 or later is installed by including this action: + # - name: Install .NET + # uses: actions/setup-dotnet@v1 + # with: + # dotnet-version: '3.1.x' # Run open source static analysis tools - - name: Run OSSAR - uses: github/ossar-action@v2.0.0 - id: ossar - - # Upload results to the Security tab - - name: Upload OSSAR results - uses: github/codeql-action/upload-sarif@v3 - with: - sarif_file: ${{ steps.ossar.outputs.sarifFile }} \ No newline at end of file + - name: Run OSSAR + uses: github/ossar-action@v2.0.0 + id: ossar + + # Upload results to the Security tab + - name: Upload OSSAR results + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: ${{ steps.ossar.outputs.sarifFile }} diff --git a/.github/workflows/pr-labels.yml b/.github/workflows/pr-labels.yml new file mode 100644 index 00000000..25710633 --- /dev/null +++ b/.github/workflows/pr-labels.yml @@ -0,0 +1,63 @@ +# Verifies if a pull request has at least one label from a set of valid +# labels before it can be merged. +# +# NOTE: +# This workflow may be triggered twice in quick succession when a PR is +# created: +# 1) `opened` — when the pull request is initially created +# 2) `labeled` — if labels are added immediately after creation +# (e.g. by manual labeling, another workflow, or GitHub App). +# +# These are separate GitHub events, so two workflow runs can be started. +# The `concurrency` configuration below ensures that only the latest run +# for the same PR remains active, canceling any previous in-progress +# run. + +name: PR labels check + +on: + pull_request_target: + types: [opened, labeled, unlabeled, synchronize] + +concurrency: + group: pr-labels-${{ github.event.pull_request.number }} + cancel-in-progress: true + +permissions: + pull-requests: read + +jobs: + check-labels: + runs-on: ubuntu-latest + + steps: + - name: Check for valid labels + run: | + PR_LABELS=$(echo '${{ toJson(github.event.pull_request.labels.*.name) }}' | jq -r '.[]') + + echo "Current PR labels: $PR_LABELS" + VALID_LABELS=( + "[bot] release" + "[scope] bug" + "[scope] documentation" + "[scope] enhancement" + "[scope] maintenance" + "[scope] significant" + ) + + found=false + for label in "${VALID_LABELS[@]}"; do + if echo "$PR_LABELS" | grep -Fxq "$label"; then + echo "✅ Found valid label: $label" + found=true + break + fi + done + + if [ "$found" = false ]; then + echo "ERROR: PR must have at least one of the following labels:" + for label in "${VALID_LABELS[@]}"; do + echo " - $label" + done + exit 1 + fi diff --git a/.github/workflows/pypi-publish.yml b/.github/workflows/pypi-publish.yml new file mode 100644 index 00000000..0b431717 --- /dev/null +++ b/.github/workflows/pypi-publish.yml @@ -0,0 +1,46 @@ +# Builds a Python package and publish it to PyPI when a new tag is +# created. + +name: PyPI publishing + +on: + # Runs on creating a new tag starting with 'v', e.g. 'v1.0.3' + push: + tags: ['v*'] + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +jobs: + pypi-publish: + runs-on: ubuntu-latest + + permissions: + contents: read + id-token: write # IMPORTANT: this permission is mandatory for trusted publishing + + steps: + - name: Check-out repository + uses: actions/checkout@v6 + with: + fetch-depth: 0 # full history with tags to get the version number by versioningit + + - name: Set up pixi + uses: ./.github/actions/setup-pixi + + # Build the Python package (to dist/ folder) + - name: Create Python package + run: pixi run default-build + + # Publish the package to PyPI (from dist/ folder) + # Instead of publishing with personal access token, we use + # GitHub Actions OIDC to get a short-lived token from PyPI. + # New publisher must be previously configured in PyPI at + # https://pypi.org/manage/project/easyreflectometry/settings/publishing/ + # Use the following data: + # Owner: easyscience + # Repository name: reflectometry-lib + # Workflow name: pypi-publish.yml + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + packages-dir: 'dist' diff --git a/.github/workflows/pypi-test.yml b/.github/workflows/pypi-test.yml new file mode 100644 index 00000000..8bcb4b5e --- /dev/null +++ b/.github/workflows/pypi-test.yml @@ -0,0 +1,82 @@ +name: PyPI package tests + +on: + # Run daily, at 00:00. + schedule: + - cron: '0 0 * * *' + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# Allow only one concurrent workflow, skipping runs queued between the run +# in-progress and latest queued. And cancel in-progress runs. +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +# Set the environment variables to be used in all jobs defined in this workflow +env: + CI_BRANCH: ${{ github.head_ref || github.ref_name }} + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + +jobs: + # Job 1: Test installation from PyPI on multiple OS + pypi-package-tests: + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + + runs-on: ${{ matrix.os }} + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Set up pixi + uses: ./.github/actions/setup-pixi + with: + environments: '' + activate-environment: '' + run-install: false + frozen: false + + - name: Init pixi project + run: pixi init easyreflectometry + + - name: Set the minimum system requirements + working-directory: easyreflectometry + run: pixi project system-requirements add macos 14.0 + + - name: Add Python 3.13 from Conda + working-directory: easyreflectometry + run: pixi add "python=3.13" + + - name: Add other Conda dependencies + working-directory: easyreflectometry + run: pixi add gsl + + - name: Add easyreflectometry (with dev dependencies) from PyPI + working-directory: easyreflectometry + run: pixi add --pypi "easyreflectometry[dev]" + + - name: Run unit tests to verify the installation + working-directory: easyreflectometry + run: pixi run python -m pytest ../tests/unit/ --color=yes -v + + - name: Run functional tests to verify the installation + working-directory: easyreflectometry + run: pixi run python -m pytest ../tests/functional/ --color=yes -v + + - name: Run integration tests to verify the installation + working-directory: easyreflectometry + # No -n auto: concurrent xdist workers race on arviz's daily-warning + # stamp file when they import easyreflectometry. See pixi.toml. + run: pixi run python -m pytest ../tests/integration/ --color=yes + + # Job 2: Build and publish dashboard (reusable workflow) + run-reusable-workflows: + needs: pypi-package-tests # depend on previous job + uses: ./.github/workflows/dashboard.yml + secrets: inherit diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml index 1d33313f..ea40ee70 100644 --- a/.github/workflows/python-ci.yml +++ b/.github/workflows/python-ci.yml @@ -1,4 +1,4 @@ -# This workflow will for a variety of Python versions +# This workflow will for a variety of Python versions # - install the code base # - lint the code base # - test the code base @@ -21,8 +21,8 @@ jobs: - name: Suggestion to fix issues if: ${{ failure() }} run: | - echo "::notice::In project root run 'python.exe -m ruff . --fix' and commit changes to fix issues." - exit 1 + echo "::notice::In project root run 'python.exe -m ruff . --fix' and commit changes to fix issues." + exit 1 Code_Testing: strategy: @@ -35,56 +35,54 @@ jobs: if: "!contains(github.event.head_commit.message, '[ci skip]')" steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - - - name: Upgrade pip - run: | - python -m pip install --upgrade pip + - uses: actions/checkout@v4 - - name: Install dependencies - run: pip install -e '.[dev]' + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} - - name: Test with pytest and coverage - run: | - pip install pytest pytest-cov - pytest --cov=src/easyreflectometry tests --cov-branch --cov-report=xml:coverage-unit.xml + - name: Upgrade pip + run: | + python -m pip install --upgrade pip - - name: Upload coverage reports to Codecov - # only on ubuntu to avoid multiple uploads - if: runner.os == 'Linux' - uses: codecov/codecov-action@v5 - with: - name: unit-tests-job - flags: unittests - files: ./coverage-unit.xml - fail_ci_if_error: false - verbose: true - token: ${{ secrets.CODECOV_TOKEN }} - slug: EasyScience/EasyReflectometryLib + - name: Install dependencies + run: pip install -e '.[dev]' + - name: Test with pytest and coverage + run: | + pip install pytest pytest-cov + pytest --cov=src/easyreflectometry tests --cov-branch --cov-report=xml:coverage-unit.xml + + - name: Upload coverage reports to Codecov + # only on ubuntu to avoid multiple uploads + if: runner.os == 'Linux' + uses: codecov/codecov-action@v5 + with: + name: unit-tests-job + flags: unittests + files: ./coverage-unit.xml + fail_ci_if_error: false + verbose: true + token: ${{ secrets.CODECOV_TOKEN }} + slug: EasyScience/EasyReflectometryLib Package_Testing: - runs-on: ubuntu-latest if: "!contains(github.event.head_commit.message, '[ci skip]')" steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: 3.11 + - uses: actions/setup-python@v5 + with: + python-version: 3.11 - - name: Install dependencies and build - run: | - pip install -e '.[dev]' - python -m build + - name: Install dependencies and build + run: | + pip install -e '.[dev]' + python -m build - - name: Check Build - run: | - cd ./dist - pytest ../ + - name: Check Build + run: | + cd ./dist + pytest ../ diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 3a4d1036..c0717847 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -9,34 +9,34 @@ name: Create Python Package on: push: - branches: [ master, pre-release ] + branches: [master, pre-release] pull_request: - branches: [ master, pre-release ] + branches: [master, pre-release] jobs: build: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.11','3.12','3.13'] + python-version: ['3.11', '3.12', '3.13'] if: "!contains(github.event.head_commit.message, '[ci skip]')" steps: - - uses: actions/checkout@v4 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - - name: Install dependencies and build - run: | - pip install -e '.[dev]' - python -m build - - name: Test with pytest - run: | - cd ./dist - pytest ../ - - uses: actions/upload-artifact@v4 - with: - name: EasyReflectometrys - Python ${{ matrix.python-version }} - path: ${{ github.workspace }}/dist/* - overwrite: true \ No newline at end of file + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies and build + run: | + pip install -e '.[dev]' + python -m build + - name: Test with pytest + run: | + cd ./dist + pytest ../ + - uses: actions/upload-artifact@v4 + with: + name: EasyReflectometrys - Python ${{ matrix.python-version }} + path: ${{ github.workspace }}/dist/* + overwrite: true diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index dce167b2..38133fd1 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -4,7 +4,6 @@ # # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries - name: Publish Python Package # Controls when the workflow will run @@ -19,24 +18,23 @@ on: jobs: deploy: - runs-on: ubuntu-latest permissions: id-token: write - + steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: 3.12 + - uses: actions/setup-python@v5 + with: + python-version: 3.12 - - name: Install dependencies and build - run: | - pip install -e '.[dev]' - python -m build + - name: Install dependencies and build + run: | + pip install -e '.[dev]' + python -m build - - name: Publish distribution 📦 to PyPI - uses: pypa/gh-action-pypi-publish@release/v1 - #with: - # password: ${{ secrets.PYPI_PASSWORD }} + - name: Publish distribution 📦 to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + #with: + # password: ${{ secrets.PYPI_PASSWORD }} diff --git a/.github/workflows/release-drafter-verify-pr-labels.yml b/.github/workflows/release-drafter-verify-pr-labels.yml index b024955b..3a8dbb4b 100644 --- a/.github/workflows/release-drafter-verify-pr-labels.yml +++ b/.github/workflows/release-drafter-verify-pr-labels.yml @@ -5,7 +5,6 @@ # # For more information see: https://github.com/marketplace/actions/release-drafter - name: Verify PR labels on: pull_request_target: @@ -16,11 +15,12 @@ jobs: runs-on: ubuntu-latest name: Verify that the PR has a valid label steps: - - name: Verify PR label action - uses: jesusvasquez333/verify-pr-label-action@v1.4.0 - id: verify-pr-label - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - valid-labels: chore, fix, bugfix, bug, enhancement, feature, dependencies, documentation - pull-request-number: ${{ github.event.pull_request.number }} - disable-reviews: false + - name: Verify PR label action + uses: jesusvasquez333/verify-pr-label-action@v1.4.0 + id: verify-pr-label + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + valid-labels: + chore, fix, bugfix, bug, enhancement, feature, dependencies, documentation + pull-request-number: ${{ github.event.pull_request.number }} + disable-reviews: false diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index 31308447..51cd0d22 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -4,7 +4,6 @@ # Uses the release-drafter.yml configuration file in the .github directory. # https://github.com/marketplace/actions/release-drafter - name: Release Drafter on: diff --git a/.github/workflows/release-notes.yml b/.github/workflows/release-notes.yml new file mode 100644 index 00000000..b6d8205c --- /dev/null +++ b/.github/workflows/release-notes.yml @@ -0,0 +1,71 @@ +# Drafts your next Release notes as pull requests are merged into +# default branch + +name: Release draft update + +on: + # Runs on pushes targeting the default branch (updates the real draft release) + push: + branches: [master, main] + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +jobs: + draft-release-notes: + permissions: + # write permission is required to create a github release + contents: write + + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + fetch-depth: 0 # full history with tags to get the version number + + - name: Set up pixi + uses: ./.github/actions/setup-pixi + + - name: Setup easyscience[bot] + id: bot + uses: ./.github/actions/setup-easyscience-bot + with: + app-id: ${{ vars.EASYSCIENCE_APP_ID }} + private-key: ${{ secrets.EASYSCIENCE_APP_KEY }} + + - name: Drafts the next release notes + id: draft + uses: enhantica/drafterino@v2 + with: + config: | + title: 'easyreflectometry $COMPUTED_VERSION' + tag: 'v$COMPUTED_VERSION' + note-template: '- $PR_TITLE (#$PR_NUMBER)' + + default-bump: post + + major-bump-labels: ['[scope] significant'] + minor-bump-labels: ['[scope] enhancement'] + patch-bump-labels: ['[scope] bug', '[scope] maintenance'] + post-bump-labels: ['[scope] documentation'] + + release-notes: + - title: 'Added' + labels: ['[scope] significant', '[scope] enhancement'] + - title: 'Fixed' + labels: ['[scope] bug'] + - title: 'Changed' + labels: ['[scope] maintenance', '[scope] documentation'] + env: + GITHUB_TOKEN: ${{ steps.bot.outputs.token }} + + - name: Create GitHub draft release + uses: softprops/action-gh-release@v3 + with: + draft: true + tag_name: ${{ steps.draft.outputs.tag_name }} + name: ${{ steps.draft.outputs.release_name }} + body: ${{ steps.draft.outputs.release_notes }} + env: + GITHUB_TOKEN: ${{ steps.bot.outputs.token }} diff --git a/.github/workflows/release-pr.yml b/.github/workflows/release-pr.yml new file mode 100644 index 00000000..3fa073b9 --- /dev/null +++ b/.github/workflows/release-pr.yml @@ -0,0 +1,55 @@ +# This workflow creates an automated release PR from a source branch into the default branch. +# +# Usage: +# - Triggered manually via workflow_dispatch. +# - Creates a PR titled "Release: merge into ". +# - Adds the label "[bot] release" so it is excluded from changelogs. +# - The PR body makes clear that this is automation only (no review needed). + +name: 'Release PR (develop → master)' + +on: + workflow_dispatch: + inputs: + source_branch: + description: 'Source branch to create PR from' + required: false + default: 'develop' + type: string + +permissions: + contents: read + pull-requests: write + +env: + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + SOURCE_BRANCH: ${{ inputs.source_branch || 'develop' }} + +jobs: + create-pull-request: + runs-on: ubuntu-latest + steps: + - name: Checkout ${{ env.SOURCE_BRANCH }} branch + uses: actions/checkout@v6 + with: + ref: ${{ env.SOURCE_BRANCH }} + + - name: Setup easyscience[bot] + id: bot + uses: ./.github/actions/setup-easyscience-bot + with: + app-id: ${{ vars.EASYSCIENCE_APP_ID }} + private-key: ${{ secrets.EASYSCIENCE_APP_KEY }} + + - name: Create PR from ${{ env.SOURCE_BRANCH }} to ${{ env.DEFAULT_BRANCH }} + env: + GH_TOKEN: ${{ steps.bot.outputs.token }} + run: | + gh pr create \ + --base ${{ env.DEFAULT_BRANCH }} \ + --head ${{ env.SOURCE_BRANCH }} \ + --title "🎉 Release: merge ${{ env.SOURCE_BRANCH }} into ${{ env.DEFAULT_BRANCH }}" \ + --label "[bot] release" \ + --body "This PR is created automatically to trigger the release pipeline. It merges the accumulated changes from \`${{ env.SOURCE_BRANCH }}\` into \`${{ env.DEFAULT_BRANCH }}\`. + + ⚠️ It is labeled \`[bot] release\` and is excluded from release notes and version bump logic." diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml new file mode 100644 index 00000000..0f43db35 --- /dev/null +++ b/.github/workflows/security.yml @@ -0,0 +1,93 @@ +# Code scanning (CodeQL) for vulnerabilities and insecure coding patterns. +# +# What this workflow does +# - Runs GitHub CodeQL analysis and uploads results to your repository's Security tab. +# - Triggers on PRs (so findings appear as PR checks) and on pushes to `develop`. +# - Runs on a weekly schedule. +# +# Where to find results on GitHub +# - Repository → Security → Code scanning alerts +# (You can filter by tool = CodeQL and by branch.) +# +# Where to configure on GitHub +# - Repository → Settings → Advanced Security +# Enable "GitHub Advanced Security" (if available) and configure CodeQL there. +# - Repository → Security → Code scanning alerts +# This page shows findings produced by this workflow. +# +# Notes about the scheduled run +# - Scheduled workflows are triggered from the repository's *default branch*. +# If your default branch is `master` but you want the scheduled scan to analyze +# `develop`, this workflow checks out `develop` explicitly for scheduled runs. +# +# References +# - CodeQL Action: https://github.com/github/codeql-action +# - Advanced setup docs: https://docs.github.com/en/code-security/code-scanning + +name: Security scans with CodeQL + +on: + # Run on pull requests so results show up as PR checks and code + # scanning alerts. + pull_request: + branches: [master, main, develop] + + # Run on pushes (e.g., after merging PRs). + push: + branches: [master, main, develop] + + # Run weekly. (Cron is in UTC.) + schedule: + - cron: '0 3 * * 1' + +permissions: + contents: read + security-events: write + +jobs: + codeql: + name: Code scanning + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + # Keep this list tight to avoid noise and speed up runs. + language: [python, actions] + + steps: + # Scheduled workflows run from the default branch. + # We explicitly analyze `develop` on the schedule to keep the scan + # focused on the active dev branch. + - name: Checkout repository (scheduled → develop) + if: ${{ github.event_name == 'schedule' }} + uses: actions/checkout@v6 + with: + ref: develop + + - name: Checkout repository + if: ${{ github.event_name != 'schedule' }} + uses: actions/checkout@v6 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v4 + with: + languages: ${{ matrix.language }} + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v4 + + print-link: + name: Print results link + runs-on: ubuntu-latest + + needs: codeql + permissions: {} # no special perms needed just to print links + + steps: + - name: Add Code Scanning link to job summary + run: | + echo "## 🔎 CodeQL Results" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "View Code Scanning alerts here:" >> $GITHUB_STEP_SUMMARY + echo "${{ github.server_url }}/${{ github.repository }}/security/code-scanning" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/test-trigger.yml b/.github/workflows/test-trigger.yml new file mode 100644 index 00000000..440bae3c --- /dev/null +++ b/.github/workflows/test-trigger.yml @@ -0,0 +1,40 @@ +name: Scheduled code tests trigger + +on: + # Run daily, at 00:00. + schedule: + - cron: '0 0 * * *' + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +permissions: + contents: read + +jobs: + code-tests-trigger: + runs-on: ubuntu-latest + + steps: + - name: Checkout develop branch + uses: actions/checkout@v6 + with: + ref: develop + + - name: Setup easyscience[bot] + id: bot + uses: ./.github/actions/setup-easyscience-bot + with: + app-id: ${{ vars.EASYSCIENCE_APP_ID }} + private-key: ${{ secrets.EASYSCIENCE_APP_KEY }} + + - name: Dispatch code tests workflow + uses: ./.github/actions/github-script + with: + github-token: ${{ steps.bot.outputs.token }} + script: | + await github.rest.actions.createWorkflowDispatch({ + owner: context.repo.owner, + repo: context.repo.repo, + workflow_id: "test.yml", + ref: "develop" + }); diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..530c154f --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,308 @@ +# This is the main workflow for testing the code before and after +# packaging it. +# The workflow is divided into three jobs: +# 1. env-prepare: +# - Prepare the environment for testing +# 2. source-test: +# - Test the code base against the latest code in the repository +# - Create the Python package +# - Upload the Python package for the next job +# 3. package-test: +# - Download the Python package (including extra files) from the previous job +# - Install the downloaded Python package +# - Test the code base against the installed package +# 4. dashboard-build-trigger: +# - Trigger the dashboard build workflow to update the code quality +# metrics on the dashboard + +name: Code and package tests + +on: + # Trigger the workflow on push + push: + branches-ignore: [master, main] # Already verified in PR + # But do not run this workflow on creating a new tag starting with + # 'v', e.g. 'v1.0.3' (see publish-pypi.yml) + tags-ignore: ['v*'] + # Trigger the workflow on pull request + pull_request: + branches: ['**'] + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# Need permissions to trigger the dashboard build workflow +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. +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +# Set the environment variables to be used in all jobs defined in this workflow +env: + CI_BRANCH: ${{ github.head_ref || github.ref_name }} + PY_VERSIONS: '3.11 3.13' + PIXI_ENVS: 'py-311-env py-313-env' + +jobs: + # Job 1: Set up environment variables + env-prepare: + runs-on: [ubuntu-latest] + + outputs: + pytest-marks: ${{ steps.set-mark.outputs.pytest_marks }} + + steps: + # Determine if integration tests should be run fully or only the fast ones + # (to save time on branches other than master and develop) + - name: Set mark for integration tests + id: set-mark + run: | + if [[ "${{ env.CI_BRANCH }}" == "master" || "${{ env.CI_BRANCH }}" == "develop" ]]; then + echo "pytest_marks=" >> $GITHUB_OUTPUT + else + echo "pytest_marks=-m fast" >> $GITHUB_OUTPUT + fi + + # Job 2: Test code + source-test: + needs: env-prepare # depend on previous job + + strategy: + fail-fast: false + matrix: + os: [ubuntu-24.04, macos-15, windows-2022] + + runs-on: ${{ matrix.os }} + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Set up pixi + uses: ./.github/actions/setup-pixi + with: + environments: ${{ env.PIXI_ENVS }} + + - name: Run unit tests + shell: bash + run: | + set -euo pipefail + + for py_ver in $PY_VERSIONS; do + echo + echo "🔹🔸🔹🔸🔹 Python: $py_ver 🔹🔸🔹🔸🔹" + + env="py-$(echo $py_ver | tr -d .)-env" # Converts 3.XX -> py-3XX-env + + echo "Running tests in environment: $env" + pixi run --environment $env unit-tests + done + + - name: Run functional tests + shell: bash + run: | + set -euo pipefail + + for py_ver in $PY_VERSIONS; do + echo + echo "🔹🔸🔹🔸🔹 Python: $py_ver 🔹🔸🔹🔸🔹" + + env="py-$(echo $py_ver | tr -d .)-env" # Converts 3.XX -> py-3XX-env + + echo "Running tests in environment: $env" + pixi run --environment $env functional-tests + done + + - name: Run integration tests ${{ needs.env-prepare.outputs.pytest-marks }} + shell: bash + run: | + set -euo pipefail + + for py_ver in $PY_VERSIONS; do + echo + echo "🔹🔸🔹🔸🔹 Python: $py_ver 🔹🔸🔹🔸🔹" + + env="py-$(echo $py_ver | tr -d .)-env" # Converts 3.XX -> py-3XX-env + + echo "Running tests in environment: $env" + pixi run --environment $env integration-tests ${{ needs.env-prepare.outputs.pytest-marks }} + done + + # Delete all local tags when not on a tagged commit to force versioningit + # to fall back to the configured default-tag, which is '999.0.0' in our case. + # This is needed for testing the package in the next job, as its version + # must be higher than the PyPI version for pip to prefer the local version. + - name: Force using versioningit default tag (non tagged release) + if: startsWith(github.ref , 'refs/tags/v') != true + run: git tag --delete $(git tag) + + - name: Build package wheels for all Python versions + shell: bash + run: | + set -euo pipefail + + for py_ver in $PY_VERSIONS; do + echo + echo "🔹🔸🔹🔸🔹 Python: $py_ver 🔹🔸🔹🔸🔹" + + env="py-$(echo $py_ver | tr -d .)-env" # Converts 3.11 -> py-311-env + + echo "Building wheel in environment: $env" + pixi run --environment $env dist-build + + echo "Moving built wheel to dist/py$py_ver/" + pixi run mkdir -p dist/py$py_ver + pixi run mv dist/*.whl dist/py$py_ver/ + done + + - name: Remove Python cache files before uploading + shell: bash + run: pixi run clean-pycache + + # More than one file/dir need to be specified in 'path', to preserve the + # structure of the dist/ directory, not only its contents. + - name: Upload package (incl. extras) for next job + uses: ./.github/actions/upload-artifact + with: + name: easyreflectometry_${{ matrix.os }}_${{ runner.arch }} + path: dist/ + + # Job 3: Test the package + package-test: + needs: source-test # depend on previous job + + strategy: + fail-fast: false + matrix: + os: [ubuntu-24.04, macos-15, windows-2022] + + runs-on: ${{ matrix.os }} + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Download package (incl. extras) from previous job + uses: ./.github/actions/download-artifact + with: + # name and path should be taken from the upload step of the previous job + name: easyreflectometry_${{ matrix.os }}_${{ runner.arch }} + path: dist/ + + - name: Set up pixi + uses: ./.github/actions/setup-pixi + with: + environments: '' + activate-environment: '' + run-install: false + frozen: false + + - name: Install easyreflectometry from the built wheel + shell: bash + run: | + set -euo pipefail + + for py_ver in $PY_VERSIONS; do + echo + echo "🔹🔸🔹🔸🔹 Python: $py_ver 🔹🔸🔹🔸🔹" + + echo "Initializing pixi project" + pixi init easyreflectometry_py$py_ver + cd easyreflectometry_py$py_ver + + echo "Adding Python $py_ver" + pixi add "python=$py_ver" + + echo "Setting macOS 14.0 as minimum required" + pixi project system-requirements add macos 14.0 + + echo "Looking for wheel in ../dist/py$py_ver/" + ls -l "../dist/py$py_ver/" + + whl_path=(../dist/"py$py_ver"/*.whl) + if [[ ! -f "${whl_path[0]}" ]]; then + echo "❌ No wheel found in ../dist/py$py_ver/" + exit 1 + fi + + # whl_url="file://$(python -c 'import os,sys; print(os.path.abspath(sys.argv[1]))' "${whl_path[0]}")" + # echo "Adding easyreflectometry from: $whl_url" + # pixi add --pypi "easyreflectometry[dev] @ ${whl_url}" + + whl_abs_path="$(python -c 'import os,sys; print(os.path.abspath(sys.argv[1]))' "${whl_path[0]}")" + + echo "Adding easyreflectometry from: $whl_abs_path" + pixi add --pypi "easyreflectometry[dev] @ ${whl_abs_path}" + + echo "Exiting pixi project directory" + cd .. + done + + - name: Run unit tests + shell: bash + run: | + set -euo pipefail + + for py_ver in $PY_VERSIONS; do + echo + echo "🔹🔸🔹🔸🔹 Python: $py_ver 🔹🔸🔹🔸🔹" + + echo "Entering pixi project directory easyreflectometry_py$py_ver" + cd easyreflectometry_py$py_ver + + echo "Running tests" + pixi run python -m pytest ../tests/unit/ --color=yes -v + + echo "Exiting pixi project directory" + cd .. + done + + - name: Run functional tests + shell: bash + run: | + set -euo pipefail + + for py_ver in $PY_VERSIONS; do + echo + echo "🔹🔸🔹🔸🔹 Python: $py_ver 🔹🔸🔹🔸🔹" + + echo "Entering pixi project directory easyreflectometry_py$py_ver" + cd easyreflectometry_py$py_ver + + echo "Running tests" + pixi run python -m pytest ../tests/functional/ --color=yes -v + + echo "Exiting pixi project directory" + cd .. + done + + - name: Run integration tests ${{ needs.env-prepare.outputs.pytest-marks }} + shell: bash + run: | + set -euo pipefail + + for py_ver in $PY_VERSIONS; do + echo + echo "🔹🔸🔹🔸🔹 Python: $py_ver 🔹🔸🔹🔸🔹" + + echo "Entering pixi project directory easyreflectometry_py$py_ver" + cd easyreflectometry_py$py_ver + + echo "Running tests" + # No -n auto: concurrent xdist workers race on arviz's daily-warning + # stamp file when they import easyreflectometry. See pixi.toml. + pixi run python -m pytest ../tests/integration/ --color=yes -v ${{ needs.env-prepare.outputs.pytest-marks }} + + echo "Exiting pixi project directory" + cd .. + done + + # Job 4: Build and publish dashboard (reusable workflow) + run-reusable-workflows: + needs: package-test # depend on previous job + uses: ./.github/workflows/dashboard.yml + secrets: inherit diff --git a/.github/workflows/tutorial-tests-trigger.yml b/.github/workflows/tutorial-tests-trigger.yml new file mode 100644 index 00000000..4b160d87 --- /dev/null +++ b/.github/workflows/tutorial-tests-trigger.yml @@ -0,0 +1,40 @@ +name: Scheduled tutorial tests trigger + +on: + # Run daily, at 00:00. + schedule: + - cron: '0 0 * * *' + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +permissions: + contents: read + +jobs: + tutorial-tests-trigger: + runs-on: ubuntu-latest + + steps: + - name: Checkout develop branch + uses: actions/checkout@v6 + with: + ref: develop + + - name: Setup easyscience[bot] + id: bot + uses: ./.github/actions/setup-easyscience-bot + with: + app-id: ${{ vars.EASYSCIENCE_APP_ID }} + private-key: ${{ secrets.EASYSCIENCE_APP_KEY }} + + - name: Dispatch tutorial tests workflow + uses: ./.github/actions/github-script + with: + github-token: ${{ steps.bot.outputs.token }} + script: | + await github.rest.actions.createWorkflowDispatch({ + owner: context.repo.owner, + repo: context.repo.repo, + workflow_id: "tutorial-tests.yml", + ref: "develop" + }); diff --git a/.github/workflows/tutorial-tests.yml b/.github/workflows/tutorial-tests.yml new file mode 100644 index 00000000..a4fc940c --- /dev/null +++ b/.github/workflows/tutorial-tests.yml @@ -0,0 +1,60 @@ +name: Tutorial tests + +on: + # Trigger the workflow on push + push: + # Selected branches + branches: [develop] # master and main are already verified in PR + # Trigger the workflow on pull request + pull_request: + branches: ['**'] + # Trigger the workflow on workflow_call (to be called from other workflows) + # Needed, as standard schedule triggers the master branch only, but we want + # to run this workflow on develop branch. + workflow_call: + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +permissions: + contents: read + +# Allow only one concurrent workflow, skipping runs queued between the run +# in-progress and latest queued. And cancel in-progress runs. +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +# Set the environment variables to be used in all jobs defined in this workflow +env: + CI_BRANCH: ${{ github.head_ref || github.ref_name }} + +jobs: + # Job 1: Test tutorials as scripts and notebooks on multiple OS + tutorial-tests: + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + + runs-on: ${{ matrix.os }} + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Set up pixi + uses: ./.github/actions/setup-pixi + + - name: Prepare notebooks + shell: bash + run: pixi run notebook-prepare + + - name: Test tutorials as notebooks + shell: bash + run: pixi run notebook-tests + + # Job 2: Build and publish dashboard (reusable workflow) + run-reusable-workflows: + needs: tutorial-tests # depend on previous job + uses: ./.github/workflows/dashboard.yml + secrets: inherit diff --git a/.gitignore b/.gitignore index ff5a50d4..6dc595c7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,49 +1,46 @@ -# QtCreator -*.autosave - -# QtCreator Qml -*.qmlproject.user -*.qmlproject.user.* - -# QtCreator Python -*.pyproject.user -*.pyproject.user.* - -# QtCreator CMake -CMakeLists.txt.user* - # Python -__pycache__ -.venv +__pycache__/ +.venv/ .coverage .pyc -# Poetry -dist -poetry.lock -*.egg-info +# Pixi +.pixi/ # PyInstaller -build +dist/ +build/ *.spec -# Jupyter +# MkDocs +docs/site/ + +# Jupyter Notebooks .ipynb_checkpoints +# Node +node_modules/ + +# QtCreator +*.autosave +*.qmlproject.user +*.qmlproject.user.* +*.pyproject.user +*.pyproject.user.* +CMakeLists.txt.user* + +# PyCharm +.idea/ + +# VS Code +.vscode/ + # macOS .DS_Store *.app *.dmg -# Docs -docs/_build - -# VSCode -.vscode - # Misc -..* +.cache/ *.log *.zip -.ci/ -.idea/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..9a3855f4 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,61 @@ +repos: + - repo: local + hooks: + # ------------- + # Manual checks + # ------------- + - id: pixi-pyproject-check + name: pixi run pyproject-check + entry: pixi run pyproject-check + language: system + pass_filenames: false + stages: [manual] + + - id: license-headers-check + name: pixi run license-check + entry: pixi run license-check + language: system + pass_filenames: false + stages: [manual] + + - id: pixi-py-lint-check + name: pixi run py-lint-check + entry: pixi run py-lint-check + language: system + pass_filenames: false + stages: [manual] + + - id: pixi-py-format-check + name: pixi run py-format-check + entry: pixi run py-format-check + language: system + pass_filenames: false + stages: [manual] + + - id: pixi-docstring-lint-check + name: pixi run docstring-lint-check + entry: pixi run docstring-lint-check + language: system + pass_filenames: false + stages: [manual] + + - id: pixi-nonpy-format-check + name: pixi run nonpy-format-check + entry: pixi run nonpy-format-check + language: system + pass_filenames: false + stages: [manual] + + - id: pixi-notebook-lint-check + name: pixi run notebook-lint-check + entry: pixi run notebook-lint-check + language: system + pass_filenames: false + stages: [manual] + + - id: pixi-unit-tests + name: pixi run unit-tests + entry: pixi run unit-tests + language: system + pass_filenames: false + stages: [manual] diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..a08c3c48 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,32 @@ +# Git +.git/ + +# Copier +.copier-answers*.yml + +# Pixi +.pixi +pixi.lock + +# MkDocs +docs/overrides/ +docs/site/ +docs/docs/assets/ + +# Python +.pytest_cache/ + +# MyPy +.mypy_cache/ + +# Ruff +.ruff_cache/ + +# Node +node_modules + +# Misc +.benchmarks +.cache +deps/ +tmp/ diff --git a/BAYESIAN_IN_ERL.md b/BAYESIAN_IN_ERL.md new file mode 100644 index 00000000..dc99953f --- /dev/null +++ b/BAYESIAN_IN_ERL.md @@ -0,0 +1,361 @@ +# Bayesian Analysis in EasyReflectometryLib — Implementation Plan + +## Current State + +- `BAYESIAN_BUMPS.md` (in `MD/`) describes a 3-phase plan. Phase 1 + (docs/notebook) is complete. +- `bayesian_bumps.py` (attached notebook) works but users must directly + import from `bumps.fitters`, `bumps.names`, `bumps.parameter` — a + leaky abstraction. +- The `Bumps` minimizer in `easyscience` + (`core/src/easyscience/fitting/minimizers/minimizer_bumps.py`) only + exposes **classical optimization** methods (`amoeba`, `newton`, `lm`), + NOT the DREAM/MCMC sampler. +- The `AvailableMinimizers` enum only has `Bumps`, `Bumps_simplex`, + `Bumps_newton`, `Bumps_lm`. This should remain optimizer-focused; + DREAM should be exposed as a sampling workflow, not as another + minimizer choice. +- `MultiFitter` in reflectometry-lib currently only has `fit()` and + `fit_single_data_set_1d()`. + +## Goal + +Users should be able to run Bayesian MCMC sampling with a **clean +high-level API** like: + +```python +fitter = MultiFitter(model) +fitter.switch_minimizer(AvailableMinimizers.Bumps) + +# Classical fit first +analysed = fitter.fit(data) + +# Bayesian sampling +posterior = fitter.sample(data, samples=5000, burn=1000, thin=10) + +# Analyze +from easyreflectometry.analysis.bayesian import plot_corner, posterior_summary +plot_corner(posterior) +print(posterior_summary(posterior)) +``` + +Important API boundary: `fit()` remains classical optimization only. +Bayesian DREAM sampling is exposed through `sample()` so users do not +receive sampler-shaped results from an optimizer-shaped API. + +## Implementation Plan + +### Step 1 — Keep DREAM separate from `AvailableMinimizers` + +**File**: `core/src/easyscience/fitting/available_minimizers.py` + +Do **not** add `Bumps_dream` as a normal `AvailableMinimizers` member. +The enum is currently used to instantiate minimizer backends and to +route calls through `Fitter.fit()`, which expects optimizer-style +`FitResults`. DREAM is an MCMC sampler and returns a sampler state/chain +rather than a best-fit result. + +Use `AvailableMinimizers.Bumps` to select the BUMPS backend, then expose +DREAM through a dedicated `sample()` method. This avoids making +`project.minimizer = AvailableMinimizers.Bumps_dream` look like a valid +classical fitting mode. + +### Step 2 — Add dedicated DREAM sampling support to the Bumps minimizer (core repo) + +**File**: `core/src/easyscience/fitting/minimizers/minimizer_bumps.py` + +**2a.** Keep `supported_methods()` optimizer-only (`amoeba`, `newton`, +`lm`). Do not add `'dream'` there unless the EasyScience fitting +abstraction is later split into optimizer and sampler concepts. + +**2b.** Add a new method `sample()` to the `Bumps` class: + +```python +def sample( + self, + x: np.ndarray, + y: np.ndarray, + weights: np.ndarray, + samples: int = 10000, + burn: int = 2000, + thin: int = 10, + chains: int | None = None, + population: int | None = None, + model: Callable | None = None, + parameters: list | None = None, + progress_callback: Callable | None = None, + seed: int | None = None, + **kwargs, +) -> dict: + """Run Bayesian MCMC sampling using BUMPS DREAM sampler. + + Returns a dict with: + - 'draws': np.ndarray, shape (n_samples, n_params) — posterior samples + - 'param_names': list[str] — parameter names + - 'state': DreamState — raw BUMPS state for save/restore + - 'logp': np.ndarray — log-posterior values + """ +``` + +The implementation would: + +1. Build the `Curve` model + `FitProblem` (reuse `_make_model()` logic) +2. Translate user-friendly aliases to BUMPS DREAM settings and call + `bumps_fit(problem, method='dream', samples=samples, burn=burn, thin=thin, pop=population, ...)` +3. Extract `result.state.draw().points` and return structured dict +4. Preserve and restore the EasyScience global object stack state, + mirroring the existing `fit()` implementation +5. Handle multi-dataset via the same `MultiFitter._precompute_reshaping` + pattern + +**2c.** Do **not** modify `fit()` to handle `method='dream'`, and do +**not** delegate `fit(method='dream')` to `sample()`. `fit()` returns +`FitResults`; `sample()` returns posterior samples and sampler metadata. +Keeping the methods separate prevents incompatible return types from +leaking into `Fitter.fit()` and reflectometry-lib `MultiFitter.fit()`. + +Recommended alias mapping: + +| Public argument | BUMPS DREAM setting | Rationale | +| ---------------- | --------------------------- | ----------------------------------------------------------------------- | +| `samples` | `samples` | Clear user-facing chain length; prefer over optimizer-oriented `steps`. | +| `burn` | `burn` | Matches BUMPS and common MCMC terminology. | +| `thin` | `thin` | Matches BUMPS and common MCMC terminology. | +| `chains` | `pop` | User-friendly MCMC wording; maps to BUMPS population count. | +| `population` | `pop` | BUMPS-aware alias for advanced users. | +| `initialization` | `init` | More readable than `init`, but pass through to BUMPS. | +| `seed` | RNG seeding before sampling | Expose reproducibility without requiring users to know BUMPS internals. | + +If both `chains` and `population` are provided, raise `ValueError` +unless they match. Accept `steps` only as a deprecated alias for +`samples`, with a warning, because `steps` already means optimizer +budget in EasyScience `Bumps.fit()`. + +### Step 3 — Add `sample()` to reflectometry-lib `MultiFitter` + +**File**: `reflectometry-lib/src/easyreflectometry/fitting.py` + +Add a `sample()` method to `MultiFitter`: + +```python +def sample( + self, + data: sc.DataGroup, + samples: int = 10000, + burn: int = 2000, + thin: int = 10, + chains: int | None = None, + population: int | None = None, + seed: int | None = None, + objective: str | None = None, +) -> dict: + """Run Bayesian MCMC sampling on reflectometry data. + + :param data: DataGroup with reflectivity data. + :param samples: Number of retained DREAM samples requested from BUMPS. + :param burn: Burn-in steps. + :param thin: Thinning interval. + :param chains: User-friendly alias for BUMPS DREAM population count. + :param population: BUMPS DREAM population count (`pop`) for advanced users. + :param seed: Random seed for reproducibility. + :param objective: Zero-variance handling strategy. + :return: Dict with posterior samples, parameter names, and sampler state. + """ +``` + +Internally: + +1. Reuse `_prepare_fit_arrays` for data preparation +2. Mirror the EasyScience `Fitter.fit()` lifecycle for reshaping, fit + function wrapping, and restoration +3. Delegate to `self.easy_science_multi_fitter.minimizer.sample(...)` + for the MCMC +4. Handle multi-model / multi-contrast aggregation as one joint + posterior +5. Return structured posterior dict or `PosteriorResults` + +The `MultiFitter` currently stores `self.easy_science_multi_fitter` +which has `.minimizer` — we'll call `sample()` on it when the minimizer +is a `Bumps` instance. + +### Step 4 — Create Bayesian analysis module in reflectometry-lib + +**New file**: +`reflectometry-lib/src/easyreflectometry/analysis/__init__.py` **New +file**: `reflectometry-lib/src/easyreflectometry/analysis/bayesian.py` + +The `bayesian.py` module provides: + +```python +class PosteriorResults: + """Container for Bayesian posterior samples with analysis methods.""" + + draws: np.ndarray # (n_samples, n_params) + param_names: list[str] + logp: np.ndarray | None + sampler_state: Any | None + + def summary(self) -> str: + """Return formatted summary table with mean, sd, HDI for each parameter.""" + + def corner(self, **kwargs) -> None: + """Plot parameter correlation corner plot using the `corner` library.""" + + def credible_interval(self, alpha: float = 0.95) -> dict: + """Return {param_name: (lower, upper)} credible intervals.""" + + def gelman_rubin(self) -> dict: + """Compute R-hat convergence diagnostic.""" + +def posterior_summary(draws, param_names) -> str: ... +def plot_corner(draws, param_names, **kwargs) -> None: ... +def plot_trace(draws, param_names, **kwargs) -> None: ... +def credible_intervals(draws, param_names, alpha=0.95) -> dict: ... +``` + +**Posterior predictive functions** (reflectivity & SLD): + +```python +def posterior_predictive_reflectivity( + draws, param_names, model, q_values, n_samples=200 +) -> tuple[np.ndarray, np.ndarray, np.ndarray]: + """Return (median, lower_95, upper_95) reflectivity arrays.""" + +def posterior_sld_profile( + draws, param_names, model, n_samples=200 +) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: + """Return (z, median, lower_95, upper_95) SLD profile arrays.""" +``` + +Posterior predictive helpers must save original parameter values and +errors before applying any posterior draw, and restore them in a +`finally` block after prediction. Parameter lookup should use +EasyScience parameter `unique_name` values, matching the BUMPS names +after removing the minimizer prefix, rather than display names. This +avoids leaving the model mutated after plotting and avoids collisions +when repeated models or multi-contrast fits contain similarly named +parameters. + +### Step 5 — Dependencies + +Add to `pyproject.toml` in reflectometry-lib: + +```toml +[project.optional-dependencies] +bayesian = ["corner>=2.2", "arviz>=0.18"] +``` + +Make `corner` and `arviz` optional imports — the analysis module should +work with graceful fallbacks when they're not installed. + +### Step 6 — Update exports + +**File**: `reflectometry-lib/src/easyreflectometry/__init__.py` + +Add `PosteriorResults` and analysis functions to the public API if +desired. + +### Step 7 — Update the example notebook + +**File**: +`reflectometry-lib/docs/src/tutorials/advancedfitting/bayesian_bumps.py` + +Replace low-level BUMPS calls with the new high-level API: + +- `fitter.sample(data, samples=500, burn=100, thin=10)` instead of + manual `FitProblem` + `bumps_fit` +- `plot_corner(posterior['draws'], posterior['param_names'])` instead of + manual `corner.corner()` +- `posterior_summary(...)` instead of manual numpy statistics + +### Step 8 — Tests + +**File**: `reflectometry-lib/tests/test_bayesian.py` (new) + +```python +def test_sample_basic(): ... +def test_posterior_summary_format(): ... +def test_corner_plot_does_not_crash(): ... +def test_credible_intervals(): ... +def test_sample_seed_reproducibility(): ... +``` + +Also add a test in `core/tests/` for the `Bumps.sample()` method. + +## Architecture Diagram + +``` +User Code + │ + ├─ fitter.fit(data) ──► classical chi² minimization + ├─ fitter.sample(data, ...) ──► Bayesian DREAM MCMC + │ + ▼ +reflectometry-lib MultiFitter + │ ._prepare_fit_arrays() ← reused from fit() + │ delegates to ↓ + ▼ +easyscience Bumps minimizer + │ .fit(x, y, weights) ──► amoeba/newton/lm + │ .sample(x, y, weights, ...) ──► DREAM MCMC (NEW) + │ + ▼ +bumps.fitters.fit / FitProblem / Curve + └──► reflectivity model evaluation via fit_func + +Post-hoc analysis: + reflectometry-lib analysis.bayesian + ├── PosteriorResults (container) + ├── plot_corner() → corner.corner() + ├── posterior_summary() → numpy stats + └── posterior_predictive_reflectivity() → model.interface.fit_func() +``` + +## Risk Assessment + +| Risk | Mitigation | +| -------------------------------------------- | -------------------------------------------------------------------------------- | +| DREAM may not converge with default settings | Expose `samples`, `burn`, `thin`, `chains`/`population`; document best practices | +| MCMC is 10-100× slower than least-squares | Document that classical fit first is recommended; add progress callback | +| `corner` / `arviz` may not be installed | Make optional dependencies with graceful fallbacks | +| Multi-dataset sampling aggregation | Follow existing `MultiFitter._precompute_reshaping` pattern | +| BUMPS DREAM API changes | Pin bumps version; wrap in our API | +| Reproducibility | Expose `seed` parameter; document how to save/load DreamState | +| Model mutation during posterior prediction | Save/restore original parameter values and map draws by `unique_name` | + +## Files to Create / Modify + +### Create: + +1. `reflectometry-lib/src/easyreflectometry/analysis/__init__.py` +2. `reflectometry-lib/src/easyreflectometry/analysis/bayesian.py` +3. `reflectometry-lib/tests/test_bayesian.py` + +### Modify: + +4. `core/src/easyscience/fitting/available_minimizers.py` — no + `Bumps_dream`; optionally document that samplers are exposed + separately +5. `core/src/easyscience/fitting/minimizers/minimizer_bumps.py` — add + dedicated `sample()` method without adding `'dream'` to optimizer + methods +6. `reflectometry-lib/src/easyreflectometry/fitting.py` — add `sample()` + to `MultiFitter` +7. `reflectometry-lib/src/easyreflectometry/__init__.py` — optional: + export new classes +8. `reflectometry-lib/pyproject.toml` — add optional `bayesian` + dependencies +9. `reflectometry-lib/docs/src/tutorials/advancedfitting/bayesian_bumps.py` + — update to use new API + +## Implementation Order + +1. **core changes** (Steps 1-2): Add `Bumps.sample()` method while + keeping DREAM out of optimizer enum/method dispatch +2. **reflectometry-lib fitting** (Step 3): Add `MultiFitter.sample()` +3. **reflectometry-lib analysis** (Step 4): Create + `analysis/bayesian.py` with corner plot & stats +4. **Dependencies** (Step 5): Add optional deps to pyproject.toml +5. **Exports** (Step 6): Update `__init__.py` +6. **Example update** (Step 7): Update notebook to use new API +7. **Tests** (Step 8): Add test coverage diff --git a/CHANGELOG.md b/CHANGELOG.md index 69077f1c..938f27b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,97 @@ +# Version 1.7.0 (1 Aug 2026) + +Restored the measured per-point resolution on data load (issue #368). + +- Loading data through `Project` (`load_new_experiment`, + `load_experiment_for_model_at_index`, + `load_all_experiments_from_file`) again sets a `Pointwise` resolution + function when the file carries per-point q-resolution (an sQz column + in `.ort` files, or a 4th column in text files). Since PR #293 the + loaders discarded this data and always applied a flat + `PercentageFwhm(5.0)` — a temporary workaround that never got + reverted. Fits of such data were smeared with 5% FWHM regardless of + what the instrument delivered and should be re-run. +- Files without q-resolution data keep the 5% FWHM default. The pre-#293 + fallback that built a `LinearSpline` from the _reflectivity_ error + (`sqrt(ye)`) was not restored: a reflectivity uncertainty is not a + q-width, and that branch produced effectively zero smearing. +- Known limitation (pre-existing): the resolution function lives on the + model, so when several experiments share one model the last-loaded + dataset's resolution wins. + +Fixed inconsistent interpretation of vector resolution functions between +the refnx and refl1d engines (issue #367). + +- **Reflectivity results change for two engine / resolution + combinations.** `LinearSpline` on refl1d previously **over-smeared by + a factor of 2.355** (its FWHM widths were passed to refl1d's + `probe.dQ`, which expects sigma). `Pointwise` on refnx previously + **under-smeared by the same factor** (its sigma widths were passed to + refnx's `x_err`, which expects FWHM). Both are now correct. Fits and + simulations that used either combination will produce different — + previously wrong — results and should be re-run. `PercentageFwhm` on + either engine, `LinearSpline` on refnx, and `Pointwise` on refl1d are + numerically unchanged. +- `ResolutionFunction.smearing()` now returns **sigma** (the Gaussian + standard deviation) for every subclass; each engine wrapper converts + to its backend's convention. This is a behavioural change to a public + method. Most visibly, `PercentageFwhm.smearing(q)` used to return the + _percentage_ itself (e.g. `5.0`) and now returns an absolute sigma + (e.g. `0.00212` at `q=0.1`); `LinearSpline.smearing(q)` returns its + `fwhm_values` divided by `2*sqrt(2*ln2)`. Callers relying on the old + values need to convert. The new `SIGMA_TO_FWHM` constant is exported + from `easyreflectometry.model.resolution_functions`. +- Constructors are **unchanged**: `PercentageFwhm(5)` still means 5% + FWHM and `LinearSpline(q, fwhm_values)` still takes FWHM. Only the + `smearing()` output convention moved, so existing model-building code + needs no edits. +- `PercentageFwhm.smearing(q)` given a scalar `q` now returns a 0-d + numpy scalar rather than a shape-`(1,)` array, matching + `LinearSpline`. `smearing(0.1)[0]` therefore raises `IndexError` where + it previously returned a value. + +Migrated sample / model classes off the deprecated `easyscience.ObjBase` +and `easyscience.CollectionBase` pipeline. + +- `BaseCore` is now built on `ModelBase`; `BaseCollection` on + `EasyList`. `Model`, `Material`, `Layer`, `MaterialMixture`, + `MaterialSolvated`, `LayerAreaPerMolecule`, `Multilayer`, + `RepeatingMultilayer`, `GradientLayer`, `Bilayer`, `SurfactantLayer`, + `BaseAssembly`, `LayerCollection`, `MaterialCollection`, `Sample`, and + `ModelCollection` were all rewritten to use the new bases. +- Properties returning a `Parameter` (`Material.sld`-style) now expose + the `Parameter` object directly across all sample classes, replacing + the inconsistent legacy behaviour where `MaterialMixture.fraction`, + `MaterialSolvated.solvent_fraction`, + `LayerAreaPerMolecule.area_per_molecule`, and + `LayerAreaPerMolecule.solvent_fraction` returned `float`. Read the + value via `.value` (e.g. `material_mixture.fraction.value`). Setters + still accept a float. `MaterialMixture.sld` / `MaterialMixture.isld` + remain `float` — they are derived via constraints, not constructor + arguments. +- `BaseCollection.remove(index)` (the legacy index-based helper) renamed + to `remove_at(index)`. The standard `MutableSequence.remove(value)` is + now inherited unmodified. +- Project files saved by previous versions cannot be read. + `Project.as_dict` writes `file_format=2`; `Project.from_dict` raises a + clear `ValueError` on missing or unsupported markers. +- `model.get_parameters()` / `collection.get_parameters()` still work + (kept as compatibility shims) but new code should use + `get_all_parameters()`. +- No more `DeprecationWarning` from `easyscience.ObjBase` / + `CollectionBase` on construction of any sample / model object. + # Version 1.6.0 (1 May 2026) -Add Mighell-based handling of non-positive-variance points in fitting (issue #256). -Non-positive-variance data points are no longer forcibly discarded; instead, a -hybrid objective applies a Mighell substitution for non-positive-variance points -while using standard weighted least squares for the rest. The previous masking -behavior is available via `objective='legacy_mask'`. New `objective` parameter on -`MultiFitter`, `fit()`, and `fit_single_data_set_1d()`. +Add Mighell-based handling of non-positive-variance points in fitting +(issue #256). Non-positive-variance data points are no longer forcibly +discarded; instead, a hybrid objective applies a Mighell substitution +for non-positive-variance points while using standard weighted least +squares for the rest. The previous masking behavior is available via +`objective='legacy_mask'`. New `objective` parameter on `MultiFitter`, +`fit()`, and `fit_single_data_set_1d()`. # Version 1.3.3 (17 June 2025) -Added Chi^2 and fit status to fitting results. -Added explicit dependency on bumps version. +Added Chi^2 and fit status to fitting results. Added explicit dependency +on bumps version. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..a6ceeebd --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,445 @@ +# Contributing to EasyReflectometry + +Thank you for your interest in contributing to **EasyReflectometry**! + +This guide explains how you can: + +- Report issues +- Contribute code +- Improve documentation +- Suggest enhancements +- Interact with the EasyScience community + +Whether you are an experienced developer or contributing for the first +time, this document walks you through the entire process step by step. + +Please make sure you follow the EasyScience organization-wide +[Code of Conduct](https://github.com/easyscience/.github/blob/master/CODE_OF_CONDUCT.md). + +--- + +## Table of Contents + +- [How to Interact With This Project](#how-to-interact-with-this-project) +- [1. Understanding the Development Model](#1-understanding-the-development-model) +- [2. Getting the Code](#2-getting-the-code) +- [3. Setting Up the Development Environment](#3-setting-up-the-development-environment) +- [4. Creating a Branch](#4-creating-a-branch) +- [5. Implementing Your Changes](#5-implementing-your-changes) +- [6. Code Quality Checks](#6-code-quality-checks) +- [7. Opening a Pull Request](#7-opening-a-pull-request) +- [8. Continuous Integration (CI)](#8-continuous-integration-ci) +- [9. Code Review](#9-code-review) +- [10. Documentation Contributions](#10-documentation-contributions) +- [11. Reporting Issues](#11-reporting-issues) +- [12. Security Issues](#12-security-issues) +- [13. Releases](#13-releases) + +--- + +## How to Interact With This Project + +If you are not planning to contribute code, you may want to: + +- 🐞 Report a bug — see [Reporting Issues](#11-reporting-issues) +- 🛡 Report a security issue — see [Security Issues](#12-security-issues) +- 💬 Ask a question or start a discussion at + [Project Discussions](https://github.com/easyscience/reflectometry-lib/discussions) + +If you plan to contribute code or documentation, continue below. + +--- + +## 1. Understanding the Development Model + +Before you start coding, it is important to understand how development +works in this project. + +### Branching Strategy + +We use the following branches: + +- `master` — stable releases only +- `develop` — active development branch +- Short-lived branches — feature or fix branches created for a single + contribution and deleted after merge + +> [!IMPORTANT] +> +> All normal contributions must target the `develop` branch. +> +> - Do **not** open Pull Requests against `master` +> - Always create your branch from `develop` +> - Always target `develop` when opening a Pull Request + +See ADR easyscience/.github#12 for more details on the branching +strategy. + +--- + +## 2. Getting the Code + +### 2.1. If You Are an External Contributor + +If you are not a core maintainer of this repository, follow these steps. + +1. Open the repository page: + `https://github.com/easyscience/reflectometry-lib` + +2. Click the **Fork** button (top-right corner). This creates your own + copy of the repository. + +3. Clone your fork locally: + + ```bash + git clone https://github.com//reflectometry-lib.git + cd reflectometry-lib + ``` + +4. Add the original repository as `upstream`: + + ```bash + git remote add upstream https://github.com/easyscience/reflectometry-lib.git + ``` + +5. Switch to the `develop` branch and update it: + + ```bash + git fetch upstream + git checkout develop + git pull upstream develop + ``` + +If you have contributed before, make sure your local `develop` branch is +up to date before starting new work. You can update it with: + +```bash +git fetch upstream +git pull upstream develop +``` + +This ensures you are working on the latest version of the project. + +### 2.2. If You Are a Core Team Member + +Core team members can create branches directly in this repository and +therefore do not need to fork it, but the rest of the workflow remains +the same. + +--- + +## 3. Setting Up the Development Environment + +You need: + +- Git +- Pixi + +EasyScience projects use **Pixi** to manage the development environment. + +To install Pixi, follow the official instructions: +https://pixi.prefix.dev/latest/installation/ + +You do **not** need to manually install Python. Pixi automatically: + +- Creates the correct Python environment +- Installs all required dependencies +- Installs development tools (linters, formatters, test tools) + +Set up the environment: + +```bash +pixi install +pixi run post-install # Install additional tooling +``` + +After this step, your development environment is ready. + +See ADR easyscience/.github#63 for more details about using Pixi for +development. + +--- + +## 4. Creating a Branch + +Never work directly on `develop`. + +Create a new branch: + +```bash +git checkout -b my-change develop +``` + +> [!IMPORTANT] +> +> Use a clear and descriptive name, for example: +> +> - `improve-solver-speed` +> - `fix-boundary-condition` +> - `add-tutorial-example` + +Clear branch names make reviews and history easier to understand. + +--- + +## 5. Implementing Your Changes + +While developing, make small, logical commits with clear messages. + +Example: + +```bash +git add . +git commit -m "Improve performance of time integrator for large systems" +``` + +--- + +## 6. Code Quality Checks + +> [!IMPORTANT] +> +> When adding new functionality or making changes, make sure to add or +> update the following as needed: +> +> - 📘 docstrings +> - 🧪 unit tests + +Before opening a Pull Request, always run: + +```bash +pixi run check +``` + +This command: + +- Validates the pyproject.toml file +- Checks for licence headers in code files +- Identifies linting and formatting issues in Python code +- Checks docstring linting and formatting issues in Python code +- Detects formatting issues in non-Python files (MD, YAML, TOML etc.) +- Checks linting issues in Jupyter notebooks (if applicable) +- Runs unit tests + +A successful run should look like this: + +```bash +pixi run pyproject-check.......................Passed +pixi run license-check.........................Passed +pixi run py-lint-check.........................Passed +pixi run py-format-check.......................Passed +pixi run docstring-lint-check..................Passed +pixi run nonpy-format-check....................Passed +pixi run notebook-lint-check...................Passed +pixi run unit-tests............................Passed +``` + +If something fails, read the error message carefully and fix the issue. + +You can run individual checks, for example, to run only unit tests: + +```bash +pixi run unit-tests +``` + +or to run only Python linting checks: + +```bash +pixi run py-lint-check +``` + +Some formatting issues can be fixed automatically: + +```bash +pixi run fix +``` + +If everything is correctly formatted, you will see: + +```text +✅ All auto-formatting steps completed successfully! +``` + +This indicates that the auto-formatting pipeline completed successfully. +If you do not see this message and no error messages appear, try running +the command again. + +If errors are reported, resolve them and re-run: + +```bash +pixi run check +``` + +> [!IMPORTANT] +> +> All checks must pass before your Pull Request can be merged. + +If you are unsure how to fix an issue, ask for help in your Pull Request +discussion. + +--- + +## 7. Opening a Pull Request + +Push your branch: + +```bash +git push origin my-change +``` + +On GitHub: + +- Click **Compare & Pull Request** +- Ensure the base branch is `develop` +- Write a clear and concise title +- Add a description explaining what changed and why +- Add the required `[scope]` label + +### Pull Request Title + +> [!IMPORTANT] +> +> The PR title appears in release notes and changelogs. It should be +> concise and informative. + +Good examples: + +- Improve performance of time integrator for large systems +- Fix incorrect boundary condition handling in solver +- Add adaptive step-size control to ODE solver +- Add tutorial for custom model configuration +- Refactor solver API for improved readability + +### Required `[scope]` Label + +> [!IMPORTANT] +> +> Each Pull Request must include a `[scope]` label, which is used for +> automatically suggesting version bumps when preparing a new release. + +The available scopes are: + +| Label | Description | +| ----------------------- | ----------------------------------------------------------------------- | +| `[scope] bug` | Bug report or fix (major.minor.**PATCH**) | +| `[scope] documentation` | Documentation-only changes (major.minor.patch.**POST**) | +| `[scope] enhancement` | Adds or improves features (major.**MINOR**.patch) | +| `[scope] maintenance` | Code/tooling cleanup without feature or bug fix (major.minor.**PATCH**) | +| `[scope] significant` | Breaking or major changes (**MAJOR**.minor.patch) | + +See ADR easyscience/.github#33 for more details on the standardized +labeling scheme. + +--- + +## 8. Continuous Integration (CI) + +After opening a Pull Request: + +- Automated checks run automatically +- You will see green checkmarks or red crosses + +If checks fail: + +1. Open the failing check +2. Read the logs +3. Fix the issue locally +4. Run `pixi run check` +5. Push your changes + +The Pull Request updates automatically. + +--- + +## 9. Code Review + +All Pull Requests are reviewed by at least one core team member. + +Code review is collaborative and aims to improve quality. + +Do not take comments personally — they are meant to help. + +To update your PR: + +```bash +git add . +git commit -m "Address review comments" +git push +``` + +--- + +## 10. Documentation Contributions + +> [!IMPORTANT] +> +> If your change affects user-facing functionality, update the project +> documentation accordingly — specifically the `nav:` (navigation) +> structure in `mkdocs.yml` and the relevant documentation Markdown +> files in `docs/docs/`. +> +> ```text +> 📁 docs +> ├── 📁 docs - Markdown files for documentation +> │ └── ... +> └── 📄 mkdocs.yml - Configuration file (navigation, theme, etc.) +> ``` + +This may include: + +- API documentation +- Examples +- Tutorials +- Jupyter notebooks + +Preview documentation locally: + +```bash +pixi run docs-serve +``` + +Open the URL shown in the terminal to review your changes. + +--- + +## 11. Reporting Issues + +If you find a bug but cannot work on a fix, please consider opening an +issue. + +When reporting an issue, it helps to: + +- Search existing issues first. +- Provide clear reproduction steps. +- Include logs, screenshots, and environment details. + +Clear and detailed reports help maintainers investigate and resolve +issues more effectively. + +--- + +## 12. Security Issues + +> [!IMPORTANT] +> +> Please do **not** report security vulnerabilities publicly. + +If you discover a potential vulnerability, please contact the +maintainers privately so the issue can be investigated and addressed +responsibly. + +--- + +## 13. Releases + +Once your contribution is merged into `develop`, it will eventually be +included in the next stable release. + +When enough changes have accumulated in `develop`, core team members +merge `develop` into `master` to prepare a new release. The release is +then tagged and published on GitHub and PyPI. + +--- + +Thank you for contributing to EasyReflectometry and the EasyScience +ecosystem! diff --git a/LICENSE b/LICENSE index c1ee0cf3..c4e3e48e 100644 --- a/LICENSE +++ b/LICENSE @@ -1,7 +1,6 @@ BSD 3-Clause License -Copyright (c) 2024, Easyscience contributors (https://github.com/EasyScience) -All rights reserved. +Copyright (c) 2021-2026 EasyScience contributors. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: diff --git a/README.md b/README.md index 1e53cdf4..01da1ff1 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,48 @@ -![Logo](https://github.com/easyScience/EasyReflectometryLib/raw/master/docs/src/_static/logo.png) -[![CI badge](https://github.com/easyScience/EasyReflectometryLib/actions/workflows/python-ci.yml/badge.svg)](https://github.com/easyScience/easyReflectometryLib/actions/workflows/python-ci.yml) -[![PyPI badge](https://img.shields.io/pypi/v/easyreflectometry.svg)](https://pypi.python.org/pypi/easyreflectometry) -[![Quality badge](https://www.codefactor.io/repository/github/easyscience/easyreflectometrylib/badge)](https://www.codefactor.io/repository/github/easyscience/easyreflectometrylib) -[![Docs badge](https://img.shields.io/badge/docs-built-blue)](http://docs.easyreflectometry.org) +

+ + + + + + + EasyReflectometry + +

-# About +**EasyReflectometry** is a software for performing reflectometry +calculations based on a layer model and refining its parameters against +reflectometry data. -A reflectometry python package and an application. + -This repo and documentation is for the `easyreflectometry` Python package that is built on the `easyscience` [framework](https://easyscience.software). -To get more information about the application visit [`easyreflectometry.org`](https://easyreflectometry.org) +**EasyReflectometry** is developed as a Python library. -# Installation +License: +[BSD 3-Clause](https://github.com/easyscience/reflectometry-lib/blob/master/LICENSE) -```sh -python -m pip install easyreflectometry -``` +## Useful Links + +### For Users + +- 📖 + [Documentation](https://easyscience.github.io/reflectometry-lib/latest) +- 🚀 + [Getting Started](https://easyscience.github.io/reflectometry-lib/latest/introduction) +- 🧪 + [Tutorials](https://easyscience.github.io/reflectometry-lib/latest/tutorials) +- 💬 + [Get in Touch](https://easyscience.github.io/reflectometry-lib/latest/introduction/#get-in-touch) +- 🧾 + [Citation](https://easyscience.github.io/reflectometry-lib/latest/introduction/#citation) + +### For Contributors + +- 🧑‍💻 [Source Code](https://github.com/easyscience/reflectometry-lib) +- 🐞 + [Issue Tracker](https://github.com/easyscience/reflectometry-lib/issues) +- 💡 + [Discussions](https://github.com/easyscience/reflectometry-lib/discussions) +- 🤝 + [Contributing Guide](https://github.com/easyscience/reflectometry-lib/blob/master/CONTRIBUTING.md) +- 🛡 + [Code of Conduct](https://github.com/easyscience/.github/blob/master/CODE_OF_CONDUCT.md) diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 00000000..f62b13ab --- /dev/null +++ b/codecov.yml @@ -0,0 +1,13 @@ +# Codecov configuration +# https://docs.codecov.com/docs/codecovyml-reference + +coverage: + status: + project: + default: + # Make project coverage informational (won't block PR) + informational: true + patch: + default: + # Require patch coverage but with threshold + threshold: 1% diff --git a/docs/docs/api-reference/assemblies/gradient_layer.md b/docs/docs/api-reference/assemblies/gradient_layer.md new file mode 100644 index 00000000..b5ee7d48 --- /dev/null +++ b/docs/docs/api-reference/assemblies/gradient_layer.md @@ -0,0 +1 @@ +::: easyreflectometry.sample.assemblies.gradient_layer diff --git a/docs/docs/api-reference/assemblies/multilayer.md b/docs/docs/api-reference/assemblies/multilayer.md new file mode 100644 index 00000000..f693c205 --- /dev/null +++ b/docs/docs/api-reference/assemblies/multilayer.md @@ -0,0 +1 @@ +::: easyreflectometry.sample.assemblies.multilayer diff --git a/docs/docs/api-reference/assemblies/repeating_multilayer.md b/docs/docs/api-reference/assemblies/repeating_multilayer.md new file mode 100644 index 00000000..d2215e46 --- /dev/null +++ b/docs/docs/api-reference/assemblies/repeating_multilayer.md @@ -0,0 +1 @@ +::: easyreflectometry.sample.assemblies.repeating_multilayer diff --git a/docs/docs/api-reference/assemblies/surfactant_layer.md b/docs/docs/api-reference/assemblies/surfactant_layer.md new file mode 100644 index 00000000..19f1f56d --- /dev/null +++ b/docs/docs/api-reference/assemblies/surfactant_layer.md @@ -0,0 +1 @@ +::: easyreflectometry.sample.assemblies.surfactant_layer diff --git a/docs/docs/api-reference/data.md b/docs/docs/api-reference/data.md new file mode 100644 index 00000000..700ec762 --- /dev/null +++ b/docs/docs/api-reference/data.md @@ -0,0 +1 @@ +::: easyreflectometry.data.measurement diff --git a/docs/docs/api-reference/elements/layer.md b/docs/docs/api-reference/elements/layer.md new file mode 100644 index 00000000..c37cd7b4 --- /dev/null +++ b/docs/docs/api-reference/elements/layer.md @@ -0,0 +1 @@ +::: easyreflectometry.sample.elements.layers.layer diff --git a/docs/docs/api-reference/elements/layer_area_per_molecule.md b/docs/docs/api-reference/elements/layer_area_per_molecule.md new file mode 100644 index 00000000..d7245c69 --- /dev/null +++ b/docs/docs/api-reference/elements/layer_area_per_molecule.md @@ -0,0 +1 @@ +::: easyreflectometry.sample.elements.layers.layer_area_per_molecule diff --git a/docs/docs/api-reference/elements/material.md b/docs/docs/api-reference/elements/material.md new file mode 100644 index 00000000..7fba8cf4 --- /dev/null +++ b/docs/docs/api-reference/elements/material.md @@ -0,0 +1 @@ +::: easyreflectometry.sample.elements.materials.material diff --git a/docs/docs/api-reference/elements/material_density.md b/docs/docs/api-reference/elements/material_density.md new file mode 100644 index 00000000..24c4c425 --- /dev/null +++ b/docs/docs/api-reference/elements/material_density.md @@ -0,0 +1 @@ +::: easyreflectometry.sample.elements.materials.material_density diff --git a/docs/docs/api-reference/elements/material_mixture.md b/docs/docs/api-reference/elements/material_mixture.md new file mode 100644 index 00000000..899b28e8 --- /dev/null +++ b/docs/docs/api-reference/elements/material_mixture.md @@ -0,0 +1 @@ +::: easyreflectometry.sample.elements.materials.material_mixture diff --git a/docs/docs/api-reference/elements/material_solvated.md b/docs/docs/api-reference/elements/material_solvated.md new file mode 100644 index 00000000..d89901e6 --- /dev/null +++ b/docs/docs/api-reference/elements/material_solvated.md @@ -0,0 +1 @@ +::: easyreflectometry.sample.elements.materials.material_solvated diff --git a/docs/docs/api-reference/fitting.md b/docs/docs/api-reference/fitting.md new file mode 100644 index 00000000..88b465c9 --- /dev/null +++ b/docs/docs/api-reference/fitting.md @@ -0,0 +1 @@ +::: easyreflectometry.fitting diff --git a/docs/docs/api-reference/index.md b/docs/docs/api-reference/index.md new file mode 100644 index 00000000..85b9e5c7 --- /dev/null +++ b/docs/docs/api-reference/index.md @@ -0,0 +1,72 @@ +--- +icon: material/code-braces-box +--- + +# :material-code-braces-box: API Reference + +This section contains the auto-generated reference detailing the +functions and modules available in EasyReflectometry. + +## Model + +Model is a sample, a background and a resolution. + +- [Model](model.md) + +## Sample + +Sample is built from assemblies. + +- [Sample](sample.md) + +## Project + +Project provides a higher-level interface for managing models, +experiments, and ORSO import. + +- [Project](project.md) + +## Fitting + +Fitting helpers and objective functions. + +- [Fitting](fitting.md) + +## Assemblies + +Assemblies are collections of layers that are used to represent a +specific physical setup. + +- [Multilayer](assemblies/multilayer.md) +- [Repeating Multilayer](assemblies/repeating_multilayer.md) +- [Surfactant Layer](assemblies/surfactant_layer.md) +- [Gradient Layer](assemblies/gradient_layer.md) + +## Elements + +Elements are the building blocks that are required to construct a +sample. + +### Layers + +Layers are basic elements and used to represent a single layer of +material with a thickness and a roughness. + +- [Layer](elements/layer.md) +- [Layer Area Per Molecule](elements/layer_area_per_molecule.md) + +### Materials + +Materials are the most basic elements and are used to represent a +material with given physical properties. + +- [Material](elements/material.md) +- [Material Density](elements/material_density.md) +- [Material Mixture](elements/material_mixture.md) +- [Material Solvated](elements/material_solvated.md) + +## Data + +Collection of helper functions. + +- [Data](data.md) diff --git a/docs/docs/api-reference/model.md b/docs/docs/api-reference/model.md new file mode 100644 index 00000000..cbb1bbf9 --- /dev/null +++ b/docs/docs/api-reference/model.md @@ -0,0 +1 @@ +::: easyreflectometry.model.model diff --git a/docs/docs/api-reference/project.md b/docs/docs/api-reference/project.md new file mode 100644 index 00000000..d778788c --- /dev/null +++ b/docs/docs/api-reference/project.md @@ -0,0 +1 @@ +::: easyreflectometry.project diff --git a/docs/docs/api-reference/sample.md b/docs/docs/api-reference/sample.md new file mode 100644 index 00000000..387cd62c --- /dev/null +++ b/docs/docs/api-reference/sample.md @@ -0,0 +1 @@ +::: easyreflectometry.sample.collections.sample diff --git a/docs/docs/assets/images/favicon.png b/docs/docs/assets/images/favicon.png new file mode 100644 index 00000000..ec9e329e Binary files /dev/null and b/docs/docs/assets/images/favicon.png differ diff --git a/docs/docs/assets/images/logo_dark.svg b/docs/docs/assets/images/logo_dark.svg new file mode 100644 index 00000000..d59c52d2 --- /dev/null +++ b/docs/docs/assets/images/logo_dark.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + Logo + + + + + + + + Text + + + + + \ No newline at end of file diff --git a/docs/docs/assets/images/logo_light.svg b/docs/docs/assets/images/logo_light.svg new file mode 100644 index 00000000..f02ec4a1 --- /dev/null +++ b/docs/docs/assets/images/logo_light.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + Logo + + + + + + + + Text + + + + + \ No newline at end of file diff --git a/docs/docs/assets/javascripts/extra.js b/docs/docs/assets/javascripts/extra.js new file mode 100644 index 00000000..9596ee07 --- /dev/null +++ b/docs/docs/assets/javascripts/extra.js @@ -0,0 +1,27 @@ +;(function () { + 'use strict' + + // Variables + const header = document.getElementsByTagName('header')[0] + + console.log(window.pageYOffset) + + // Hide-show header shadow + function toggleHeaderShadow() { + if (window.pageYOffset <= 0) { + header.classList.remove('md-header--shadow') + } else { + header.classList.add('md-header--shadow') + } + } + + // Onload + window.onload = function () { + toggleHeaderShadow() + } + + // Onscroll + window.onscroll = function () { + toggleHeaderShadow() + } +})() diff --git a/docs/docs/assets/javascripts/mathjax.js b/docs/docs/assets/javascripts/mathjax.js new file mode 100644 index 00000000..333524ec --- /dev/null +++ b/docs/docs/assets/javascripts/mathjax.js @@ -0,0 +1,33 @@ +window.MathJax = { + tex: { + //inlineMath: [['\\(', '\\)']], + //displayMath: [['\\[', '\\]']], + // Add support for $...$ and \(...\) delimiters + inlineMath: [ + ['$', '$'], + ['\\(', '\\)'], + ], + // Add support for $$...$$ and \[...]\ delimiters + displayMath: [ + ['$$', '$$'], + ['\\[', '\\]'], + ], + processEscapes: true, + processEnvironments: true, + }, + options: { + //ignoreHtmlClass: ".*|", + //processHtmlClass: "arithmatex" + // Skip code blocks only + skipHtmlTags: ['script', 'noscript', 'style', 'textarea', 'pre', 'code'], + // Only ignore explicit opt-out + ignoreHtmlClass: 'no-mathjax|tex2jax_ignore', + }, +} + +document$.subscribe(() => { + MathJax.startup.output.clearCache() + MathJax.typesetClear() + MathJax.texReset() + MathJax.typesetPromise() +}) diff --git a/docs/docs/assets/stylesheets/extra.css b/docs/docs/assets/stylesheets/extra.css new file mode 100644 index 00000000..a625be80 --- /dev/null +++ b/docs/docs/assets/stylesheets/extra.css @@ -0,0 +1,359 @@ +/*************/ +/* Variables */ +/*************/ + +:root { + --sz-link-color--lightmode: #0184c7; + --sz-hovered-link-color--lightmode: #37bdf9; + --sz-body-background-color--lightmode: #fafafa; + --sz-body-text-color--lightmode: #525252; + --sz-body-heading-color--lightmode: #434343; + --sz-code-background-color--lightmode: #ececec; + + --sz-link-color--darkmode: #37bdf9; + --sz-hovered-link-color--darkmode: #2890c0; + --sz-body-background-color--darkmode: #262626; + --sz-body-text-color--darkmode: #a3a3a3; + --sz-body-heading-color--darkmode: #e5e5e5; + --sz-code-background-color--darkmode: #212121; +} + +/****************/ +/* Color styles */ +/****************/ + +/* Default styles https://github.com/squidfunk/mkdocs-material/blob/master/src/assets/stylesheets/main/_typeset.scss */ + +/* Light mode */ + +/* Default light mode https://github.com/squidfunk/mkdocs-material/blob/master/src/assets/stylesheets/main/_colors.scss */ + +[data-md-color-scheme="default"] { + + /* Primary color shades */ + --md-primary-fg-color: var(--sz-body-background-color--lightmode); /* Navigation background */ + --md-primary-bg-color: var(--sz-body-text-color--lightmode); /* E.g., Header title and icons */ + --md-primary-bg-color--light: var(--sz-body-text-color--lightmode); /* E.g., Header search */ + + /* Accent color shades */ + --md-accent-fg-color: var(--sz-hovered-link-color--lightmode); /* E.g., Hovered `a` elements & copy icon in code */ + + /* Default color shades */ + --md-default-fg-color: var(--sz-body-text-color--lightmode); + --md-default-fg-color--light: var(--sz-body-heading-color--lightmode); /* E.g., `h1` color & TOC viewed items & `$` in code */ + --md-default-fg-color--lighter: var(--sz-body-text-color--lightmode); /* E.g., `¶` sign near `h1-h8` */ + --md-default-fg-color--lightest: var(--sz-body-text-color--lightmode); /* E.g., Copy icon in code */ + --md-default-bg-color: var(--sz-body-background-color--lightmode); + + /* Code color shades */ + --md-code-bg-color: var(--sz-code-background-color--lightmode); + + /* Typeset color shades */ + --md-typeset-color: var(--sz-body-text-color--lightmode); + + /* Typeset `a` color shades */ + --md-typeset-a-color: var(--sz-link-color--lightmode); + + /* Footer color shades */ + --md-footer-fg-color: var(--sz-body-text-color--lightmode); /* E.g., Next -> */ + --md-footer-fg-color--light: var(--sz-body-text-color--lightmode); /* E.g., © 2022 EasyDiffraction, Material for MkDocs */ + --md-footer-fg-color--lighter: var(--sz-body-text-color--lightmode); /* E.g. Made with */ + --md-footer-bg-color: hsla(0, 0%, 0%, 0.0); /* Space with, e.g., Next -> */ + --md-footer-bg-color--dark: hsla(0, 0%, 0%, 0.0); /* Space with, e.g., © 2022 EasyDiffraction */ + + /* Custom colors */ + --sz-red-color: #F44336; + --sz-blue-color: #03A9F4; + --sz-green-color: #4CAF50; + --sz-orange-color: #FF9800; + + /* Logo display */ + --md-footer-logo-dark-mode: none; + --md-footer-logo-light-mode: block; +} + +/* Dark mode */ + +/* Default dark mode: https://github.com/squidfunk/mkdocs-material/blob/master/src/assets/stylesheets/palette/_scheme.scss */ + +[data-md-color-scheme="slate"] { + + /* Primary color shades */ + --md-primary-fg-color: var(--sz-body-background-color--darkmode); /* Navigation background */ + --md-primary-bg-color: var(--sz-body-text-color--darkmode); /* E.g., Header title and icons */ + --md-primary-bg-color--light: var(--sz-body-text-color--darkmode); /* E.g., Header search */ + + /* Accent color shades */ + --md-accent-fg-color: var(--sz-hovered-link-color--darkmode); /* E.g., Hovered `a` elements & copy icon in code */ + + /* Default color shades */ + --md-default-fg-color: var(--sz-body-text-color--darkmode); + --md-default-fg-color--light: var(--sz-body-heading-color--darkmode); /* E.g., `h1` color & TOC viewed items & `$` in code */ + --md-default-fg-color--lighter: var(--sz-body-text-color--darkmode); /* E.g., `¶` sign near `h1-h8` */ + --md-default-fg-color--lightest: var(--sz-body-text-color--darkmode); /* E.g., Copy icon in code */ + --md-default-bg-color: var(--sz-body-background-color--darkmode); + + /* Code color shades */ + --md-code-bg-color: var(--sz-code-background-color--darkmode); + + /* Typeset color shades */ + --md-typeset-color: var(--sz-body-text-color--darkmode); + + /* Typeset `a` color shades */ + --md-typeset-a-color: var(--sz-link-color--darkmode); + + /* Footer color shades */ + --md-footer-fg-color: var(--sz-body-text-color--darkmode); /* E.g., Next -> */ + --md-footer-fg-color--light: var(--sz-body-text-color--darkmode); /* E.g., © 2022 EasyDiffraction, Material for MkDocs */ + --md-footer-fg-color--lighter: var(--sz-body-text-color--darkmode); /* E.g. Made with */ + --md-footer-bg-color: hsla(0, 0%, 0%, 0.0); /* Space with, e.g., Next -> */ + --md-footer-bg-color--dark: hsla(0, 0%, 0%, 0.0); /* Space with, e.g., © 2022 EasyDiffraction */ + + /* Custom colors */ + --sz-red-color: #EF9A9A; + --sz-blue-color: #81D4FA; + --sz-green-color: #A5D6A7; + --sz-orange-color: #FFCC80; + + /* Logo display */ + --md-footer-logo-dark-mode: block; + --md-footer-logo-light-mode: none; +} + +/*****************/ +/* Custom styles */ +/*****************/ + +/* Logo */ + +#logo_light_mode { + display: var(--md-footer-logo-light-mode); +} + +#logo_dark_mode { + display: var(--md-footer-logo-dark-mode); +} + +/* Customize default styles of MkDocs Material */ + +/* Hide navigation title */ +label.md-nav__title[for="__drawer"] { + height: 0; +} + +/* Hide site title (first topic) while keeping page title and version selector */ +.md-header__topic:first-child .md-ellipsis { + display: none; +} + +/* Increase logo size */ +.md-logo :is(img, svg) { + height: 1.8rem !important; +} + +/* Hide GH repo with counts (top right page corner) */ +.md-header__source { + display: none; +} + +/* Hide GH repo with counts (navigation bar in mobile view) */ +.md-nav__source { + display: none; +} + +/* Ensure all horizontal lines in the navigation list are removed or hidden */ +.md-nav__item { + /* Removes any border starting from the second level */ + border: none !important; + /* Modifies the background color to hide the first horizontal line */ + background-color: var(--md-default-bg-color); +} + +/* Increase TOC (on the right) width */ +.md-nav--secondary { + margin-left: -10px; + margin-right: -4px; +} + +/* */ +.md-nav__item > .md-nav__link { + padding-left: 0.5em; /* Default */ +} + +/* Change line height of the tabel cells */ +.md-typeset td, +.md-typeset th { + line-height: 1.25 !important; +} + +/* Change vertical alignment of the icon inside the tabel cells */ +.md-typeset td .twemoji { + vertical-align: sub !important; +} + +/* Change the width of the primary sidebar */ +/* +.md-sidebar--primary { + width: 240px; +} +*/ + +/* Change the overall width of the page */ +.md-grid { + max-width: 1280px; +} + +/* Needed for mkdocs-jupyter to show download and other buttons on top of the notebook */ +.md-content__button { + position: relative !important; +} + +/* Background color of the search input field */ +.md-search__input { + background-color: var(--md-code-bg-color) !important; +} + +/* Customize default style of mkdocs-jupyter plugin */ + +/* Set the width of the notebook to fill 100% and not reduce by the width of .md-content__button's +Adjust the margins and paddings to fit the defaults in MkDocs Material and do not crop the label in the header +*/ +.jupyter-wrapper { + width: 100% !important; + display: flex !important; +} + +.jp-Notebook { + padding: 0 !important; + margin-top: -3em !important; + + /* Ensure notebook content stretches across the page */ + width: 100% !important; + max-width: 100% !important; + + /* mkdocs-material + some notebook HTML end up as flex */ + align-items: stretch !important; +} + +.jp-Notebook .jp-Cell { + /* Key: flex children often need min-width: 0 to prevent weird shrink */ + width: 100% !important; + max-width: 100% !important; + min-width: 0 !important; + + /* Removes jupyter cell paddings */ + padding-left: 0 !important; +} + +/* Removes jupyter cell prefixes, like In[123]: */ +.prompt, +.jp-InputPrompt, +.jp-OutputPrompt { + display: none !important; +} + +/* Removes jupyter output cell padding to align with input cell text */ +.jp-RenderedText { + padding-left: 0.85em !important; +} + +/* Extra styling the panda dataframes, on top of the style included in the code */ +table.dataframe { + float: left; + margin-left: 0.75em !important; + margin-bottom: 0.5em !important; + font-size: var(--jp-code-font-size) !important; + color: var(--md-primary-bg-color) !important; + /* Allow table cell wrapping in MkDocs-Jupyter outputs */ + /* + table-layout: auto !important; + width: auto !important; + */ +} + +/* Allow wrap for the last column */ +/* +table.dataframe td:last-child, +table.dataframe th:last-child { + white-space: normal !important; + word-break: break-word !important; +} +*/ + +/* Custom styles for the CIF files */ + +.cif { + padding-left: 1em; + padding-right: 1em; + padding-top: 1px; + padding-bottom: 1px; + background-color: var(--md-code-bg-color); + font-size: small; +} +.red { + color: var(--sz-red-color); +} +.green { + color: var(--sz-green-color); +} +.blue { + color: var(--sz-blue-color); +} +.orange { + color: var(--sz-orange-color); +} +.grey { + color: grey; +} + +/**********/ +/* Labels */ +/**********/ + +.label-cif { + padding-top: 0.5ex; + padding-bottom: 0.5ex; + padding-left: 0.9ex; + padding-right: 0.9ex; + border-radius: 1ex; + color: var(--md-default-fg-color) !important; + background-color: var(--md-code-bg-color); +} + +p .label-cif, li .label-cif { + vertical-align: 5%; + font-size: 12px; +} + +.label-cif:hover { + color: white !important; +} + +.label-experiment { + padding-top: 0.25ex; + padding-bottom: 0.6ex; + padding-left: 0.9ex; + padding-right: 0.9ex; + border-radius: 1ex; + color: var(--md-default-fg-color) !important; + background-color: rgba(55, 189, 249, 0.1); +} + +p .label-experiment, li .label-experiment { + vertical-align: 5%; + font-size: 12px; +} + +h1 .label-experiment { + padding-top: 0.05ex; + padding-bottom: 0.4ex; + padding-left: 0.9ex; + padding-right: 0.9ex; + border-radius: 0.75ex; + color: var(--md-default-fg-color) !important; + background-color: rgba(55, 189, 249, 0.1); +} + +.label-experiment:hover { + color: white !important; +} diff --git a/docs/docs/index.md b/docs/docs/index.md new file mode 100644 index 00000000..f11ef87f --- /dev/null +++ b/docs/docs/index.md @@ -0,0 +1,21 @@ +![](assets/images/logo_dark.svg#gh-dark-mode-only)![](assets/images/logo_light.svg#gh-light-mode-only) + +# Reflectometry data analysis + +Here is a brief overview of the main documentation sections: + +- [:material-information-slab-circle: Introduction](introduction/index.md) + – Provides a description of EasyReflectometry, including its purpose, + licensing, latest release details, and contact information. +- [:material-cog-box: Installation & Setup](installation-and-setup/index.md) + – Guides users through system requirements, environment configuration, + and the installation process. +- [:material-book-open-variant: User Guide](user-guide/index.md) – + Covers core concepts, key terminology, workflow steps, and essential + parameters for effective use of EasyReflectometry. +- [:material-school: Tutorials](tutorials/index.md) – Offers practical, + step-by-step examples demonstrating common workflows and data analysis + tasks. +- [:material-code-braces-box: API Reference](api-reference/index.md) – + An auto-generated reference detailing the available functions and + modules in EasyReflectometry. diff --git a/docs/docs/installation-and-setup/index.md b/docs/docs/installation-and-setup/index.md new file mode 100644 index 00000000..4baef822 --- /dev/null +++ b/docs/docs/installation-and-setup/index.md @@ -0,0 +1,282 @@ +--- +icon: material/cog-box +--- + +# :material-cog-box: Installation & Setup + +**EasyReflectometry** is a cross-platform Python library compatible with +**Python 3.11** through **3.13**. + +To install and set up EasyReflectometry, we recommend using +[**Pixi**](https://pixi.prefix.dev), a modern package manager for +Windows, macOS, and Linux. + +??? note "Main benefits of using Pixi" + + - **Ease of use**: Pixi simplifies the installation process, making it + accessible even for users with limited experience in package management. + - **Python version control**: Pixi allows specifying and managing different + Python versions for each project, ensuring compatibility. + - **Isolated environments**: Pixi creates isolated environments for each + project, preventing conflicts between different package versions. + - **PyPI and Conda support**: Pixi can install packages from both PyPI and + Conda repositories, providing access to a wide range of libraries. + +An alternative installation method using the traditional **pip** package +manager is also provided. + +## Installing with Pixi recommended { #installing-with-pixi data-toc-label="Installing with Pixi" } + +This section describes the simplest way to set up EasyReflectometry +using **Pixi**. + +#### Installing Pixi + +- Install Pixi by following the instructions on the + [official Pixi Installation Guide](https://pixi.prefix.dev/latest/installation). + +#### Setting up EasyReflectometry with Pixi + + + +- Choose a project location (local drive recommended). + + ??? warning ":fontawesome-brands-windows: Windows + OneDrive" + + We **do not recommend creating a Pixi project inside OneDrive or other + synced folders**. + + By default, Pixi creates the virtual environment inside the project + directory (in `.pixi/`). On Windows, synced folders such as OneDrive + may cause file‑system issues (e.g., path-length limitations or + restricted link operations), which can lead to unexpected install + errors or environments being recreated. + + Instead, create your project in a **local directory on your drive** + where you have full write permissions. + + + +- Initialize a new Pixi project and navigate into it: + ```txt + pixi init easyreflectometry + cd easyreflectometry + ``` +- Set the Python version for the Pixi environment (e.g., 3.13): + ```txt + pixi add python=3.13 + ``` +- Add EasyReflectometry to the Pixi environment from PyPI: + ```txt + pixi add --pypi easyreflectometry + ``` +- Add a Pixi task to run EasyReflectometry commands easily: + ```txt + pixi task add easyreflectometry "python -m easyreflectometry" + ``` + +#### Updating Pixi and EasyReflectometry + +- To update all packages in the Pixi environment, including + EasyReflectometry: + ```txt + pixi update + ``` +- To update Pixi itself to the latest version: + ```txt + pixi self-update + ``` + +#### Uninstalling Pixi + +- Follow the + [official Pixi Guide](https://pixi.prefix.dev/latest/installation/#uninstall). + +## Classical Installation + +This section describes how to install EasyReflectometry using the +traditional method with **pip**. It is assumed that you are familiar +with Python package management and virtual environments. + +### Environment Setup optional { #environment-setup data-toc-label="Environment Setup" } + +We recommend using a **virtual environment** to isolate dependencies and +avoid conflicts with system-wide packages. If any issues arise, you can +simply delete and recreate the environment. + +#### Creating and Activating a Virtual Environment: + + + +- Create a new virtual environment: + ```txt + python3 -m venv venv + ``` +- Activate the environment: + + === ":material-apple: macOS" + ```txt + . venv/bin/activate + ``` + === ":material-linux: Linux" + ```txt + . venv/bin/activate + ``` + === ":fontawesome-brands-windows: Windows" + ```txt + . venv/Scripts/activate # Windows with Unix-like shells + .\venv\Scripts\activate.bat # Windows with CMD + .\venv\Scripts\activate.ps1 # Windows with PowerShell + ``` + +- The terminal should now show `(venv)`, indicating that the virtual environment + is active. + + + +#### Deactivating and Removing the Virtual Environment: + + + +- Exit the environment: + ```txt + deactivate + ``` +- If this environment is no longer needed, delete it: + + === ":material-apple: macOS" + ```txt + rm -rf venv + ``` + === ":material-linux: Linux" + ```txt + rm -rf venv + ``` + === ":fontawesome-brands-windows: Windows" + ```txt + rmdir /s /q venv + ``` + + + +### Installing from PyPI { #from-pypi } + +EasyReflectometry is available on **PyPI (Python Package Index)** and +can be installed using `pip`. To do so, use the following command: + +```txt +pip install easyreflectometry +``` + +To install a specific version of EasyReflectometry, e.g., 1.0.3: + +```txt +pip install 'easyreflectometry==1.0.3' +``` + +To upgrade to the latest version: + +```txt +pip install --upgrade easyreflectometry +``` + +To upgrade to the latest version and force reinstallation of all +dependencies (useful if files are corrupted): + +```txt +pip install --upgrade --force-reinstall easyreflectometry +``` + +To check the installed version: + +```txt +pip show easyreflectometry +``` + +### Installing from GitHub alternative { #from-github data-toc-label="Installing from GitHub" } + +Installing unreleased versions is generally not recommended but may be +useful for testing. + +To install EasyReflectometry from the `develop` branch of GitHub, for +example: + +```txt +pip install git+https://github.com/easyscience/reflectometry-lib@develop +``` + +To include extra dependencies (e.g., dev): + +```txt +pip install 'easyreflectometry[dev] @ git+https://github.com/easyscience/reflectometry-lib@develop' +``` + +## How to Run Tutorials + +EasyReflectometry includes a collection of **Jupyter Notebook examples** +that demonstrate key functionality. These tutorials serve as +**step-by-step guides** to help users understand the data analysis +workflow. They are available as **static HTML pages** in the +[:material-school: Tutorials](../tutorials/index.md) section. + +In the next sections, we explain how to set up Jupyter and run the +tutorials interactively in two different ways: locally or online via +Google Colab. + +If you decide to run the tutorials locally, you need to download them +first. This can be done individually via the :material-download: +**Download Notebook** button available on each tutorial page, or all at +once using the command line, as shown below. + +### Run Tutorials Locally with Pixi recommended { #running-with-pixi data-toc-label="Run Tutorials Locally with Pixi" } + +- Navigate to your existing Pixi project, created as described in the + [Installing with Pixi](#installing-with-pixi) section. +- Add JupyterLab, Interactive Python shell and the Pixi kernel for + Jupyter: + ```txt + pixi add --pypi jupyterlab ipython pixi-kernel + ``` +- Download all the EasyReflectometry tutorials to the `tutorials/` + directory. +- Start the JupyterLab server in the `tutorials/` directory to access + the notebooks: + ```txt + pixi run jupyter lab tutorials/ + ``` +- Your web browser should open automatically. Click on one of the + `*.ipynb` files and select the `Python (Pixi)` kernel to get started. + +### Classical Run Tutorials Locally + +- Install Jupyter Notebook, Interactive Python shell and the IPython + kernel: + ```txt + pip install notebook ipython ipykernel + ``` +- Add the virtual environment as a Jupyter kernel: + ```txt + python -m ipykernel install --user --name=venv --display-name "EasyReflectometry Python kernel" + ``` +- Download all the EasyReflectometry tutorials to the `tutorials/` + directory. +- Start the Jupyter Notebook server in the `tutorials/` directory to + access the notebooks: + ```txt + jupyter notebook tutorials/ + ``` +- Your web browser should open automatically. Click on one of the + `*.ipynb` files and select the `EasyReflectometry Python kernel` to + get started. + +### Run Tutorials via Google Colab + +**Google Colab** lets you run Jupyter Notebooks in the cloud without any +local installation. This is the fastest way to start experimenting with +EasyReflectometry. + +- Ensure you have a **Google account**. +- Go to the **[:material-school: Tutorials](../tutorials/index.md)** + section. +- Click the :google-colab: **Open in Google Colab** button on any + tutorial. diff --git a/docs/docs/introduction/index.md b/docs/docs/introduction/index.md new file mode 100644 index 00000000..4335dfdf --- /dev/null +++ b/docs/docs/introduction/index.md @@ -0,0 +1,68 @@ +--- +icon: material/information-slab-circle +--- + +# :material-information-slab-circle: Introduction + +## Description + +**EasyReflectometry** is a software for performing reflectometry +calculations based on a layer model and refining its parameters against +reflectometry data. + +**EasyReflectometry** is developed as a Python library. + + + +## License + +**EasyReflectometry** library is released under the +[BSD 3-Clause License](https://raw.githubusercontent.com/easyscience/reflectometry-lib/master/LICENSE). + +## Releases + +The latest version of the **EasyReflectometry** library is +[{{ vars.release_version }}](https://github.com/easyscience/reflectometry-lib/releases/latest). + +For a complete list of new features, bug fixes, and improvements, see +the +[GitHub Releases page](https://github.com/easyscience/reflectometry-lib/releases). + +## Citation + +If you use **EasyReflectometry** library in your work, please cite the +specific version you used. + +All official releases of the **EasyReflectometry** library are archived +on Zenodo, each with a version-specific Digital Object Identifier (DOI). + +Citation details in various styles (e.g., APA, MLA) and formats (e.g., +BibTeX, JSON) are available on the +[Zenodo archive page](https://doi.org/10.5281/zenodo.18163581). + +## Contributing + +We welcome contributions of any kind! + +**EasyReflectometry** is intended to be a community-driven, open-source +project supported by a diverse group of contributors. + +The project is maintained by the +[European Spallation Source (ESS)](https://ess.eu). + +If you would like to report a bug or request a new feature, please use +the +[GitHub Issue Tracker](https://github.com/easyscience/reflectometry-lib/issues) +(A free GitHub account is required.) + +To contribute code, documentation, or tests, please see our +[:material-account-plus: Contributing Guidelines](https://github.com/easyscience/reflectometry-lib/blob/master/CONTRIBUTING.md) +for detailed development instructions. + +## Get in Touch + +For general questions or feedback, please contact us at +[support@easyreflectometry.org](mailto:support@easyreflectometry.org). diff --git a/docs/docs/tutorials/advancedfitting/bayesian_bumps.ipynb b/docs/docs/tutorials/advancedfitting/bayesian_bumps.ipynb new file mode 100644 index 00000000..dcaaa025 --- /dev/null +++ b/docs/docs/tutorials/advancedfitting/bayesian_bumps.ipynb @@ -0,0 +1,777 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "649cf9aa", + "metadata": {}, + "source": [ + "# Bayesian Fitting with BUMPS\n", + "\n", + "This notebook demonstrates the full high-level Bayesian MCMC API in\n", + "EasyReflectometry.\n", + "\n", + "It covers:\n", + "\n", + "- Building a reflectometry model with realistic bounds (critical for MCMC).\n", + "- Classical optimisation first (good starting point for Bayesian sampling).\n", + "- High-level DREAM MCMC sampling via\n", + " ``MultiFitter.mcmc_sample()`` and ``PosteriorResults``.\n", + "- Checking convergence with the Gelman-Rubin R-hat diagnostic, then\n", + " **extending an under-converged chain** with ``fitter.sampler.extend()`` and\n", + " comparing the posterior before and after.\n", + "- Posterior inspection: summary table, marginal distributions, corner plot,\n", + " trace plot, credible intervals.\n", + "- Posterior-predictive checks: reflectivity and SLD profile with 95 %\n", + " credible bands.\n", + "\n", + "The sampling run below is deliberately started short so that the diagnostic\n", + "flags it as not converged — this gives us something real to fix when we extend\n", + "the chain, rather than a cosmetic demonstration.\n", + "\n", + "All posterior plots are rendered as **interactive Plotly figures**, the\n", + "same ones shown by the EasyReflectometryApp Bayesian Posterior tab.\n", + "\n", + "**Note**: Requires ``arviz`` and ``plotly`` (install via\n", + "``pip install easyreflectometry[dev]``)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "61aa83ac", + "metadata": {}, + "outputs": [], + "source": [ + "import warnings\n", + "\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "import pooch\n", + "from easyscience.fitting import AvailableMinimizers\n", + "\n", + "from easyreflectometry.analysis.bayesian import PosteriorResults\n", + "from easyreflectometry.analysis.bayesian import plot_corner\n", + "from easyreflectometry.analysis.bayesian import plot_trace\n", + "from easyreflectometry.analysis.bayesian import posterior_predictive_reflectivity\n", + "from easyreflectometry.analysis.bayesian import posterior_predictive_sld_profile\n", + "from easyreflectometry.calculators import CalculatorFactory\n", + "from easyreflectometry.data.measurement import load\n", + "from easyreflectometry.fitting import MultiFitter\n", + "from easyreflectometry.model import Model\n", + "from easyreflectometry.model import PercentageFwhm\n", + "from easyreflectometry.sample import Layer\n", + "from easyreflectometry.sample import Material\n", + "from easyreflectometry.sample import Multilayer\n", + "from easyreflectometry.sample import Sample\n", + "\n", + "warnings.filterwarnings('ignore')\n", + "\n", + "print('All libraries imported successfully.')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "14986b98", + "metadata": {}, + "outputs": [], + "source": [ + "# ---- Load experimental data -------------------------------------------------\n", + "# Fetch the .ort test data from the easyscience/reflectometry data repository.\n", + "data_path = pooch.retrieve(\n", + " url='https://raw.githubusercontent.com/easyscience/reflectometry/master/data/example.ort',\n", + " known_hash='82d0c95c069092279a799a8131ad3710335f601d9f1080754b387f42e407dfab',\n", + ")\n", + "data = load(data_path)\n", + "print('Data loaded with keys:', list(data.keys()))\n", + "\n", + "# Quick look at the data\n", + "qz = data['coords']['Qz_0'].values\n", + "r_data = data['data']['R_0'].values\n", + "\n", + "plt.figure(figsize=(7, 4))\n", + "plt.semilogy(qz, r_data, 'o', label='Data')\n", + "plt.xlabel('Q / Å⁻¹')\n", + "plt.ylabel('Reflectivity')\n", + "plt.title('Experimental data (example.ort)')\n", + "plt.legend()\n", + "plt.grid(True, alpha=0.3)\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cd56a6eb", + "metadata": {}, + "outputs": [], + "source": [ + "# ---- Create a monolayer model (Si / Film / D₂O) ----------------------------\n", + "si = Material(sld=2.07, isld=0.0, name='Si')\n", + "film = Material(sld=2.0, isld=0.0, name='Film')\n", + "d2o = Material(sld=6.36, isld=0.0, name='D2O')\n", + "\n", + "si_layer = Layer(material=si, thickness=0.0, roughness=3.0, name='Si')\n", + "film_layer = Layer(material=film, thickness=250.0, roughness=3.0, name='Film')\n", + "d2o_layer = Layer(material=d2o, thickness=0.0, roughness=3.0, name='D2O')\n", + "\n", + "sample = Sample(\n", + " Multilayer(si_layer),\n", + " Multilayer(film_layer),\n", + " Multilayer(d2o_layer),\n", + " name='Monolayer Sample',\n", + ")\n", + "\n", + "resolution = PercentageFwhm(0.02)\n", + "model = Model(\n", + " sample=sample,\n", + " scale=1.0,\n", + " background=1e-6,\n", + " resolution_function=resolution,\n", + " name='Monolayer Model',\n", + ")\n", + "\n", + "# ---- Make key parameters free with realistic bounds (essential for MCMC) -----\n", + "film_layer.thickness.fixed = False\n", + "film_layer.thickness.bounds = (100, 400)\n", + "\n", + "film.sld.fixed = False\n", + "film.sld.bounds = (0.5, 4.0)\n", + "\n", + "model.scale.fixed = False\n", + "model.scale.bounds = (0.8, 1.2)\n", + "\n", + "model.background.fixed = False\n", + "model.background.bounds = (1e-7, 1e-5)\n", + "\n", + "print('Model created with the following free parameters:')\n", + "for p in model.get_parameters():\n", + " if not p.fixed:\n", + " print(f' {p.name}: value={p.value}, bounds={p.bounds}')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "991e1169", + "metadata": {}, + "outputs": [], + "source": [ + "# ---- Set up the calculator and fitter ---------------------------------------\n", + "interface = CalculatorFactory()\n", + "interface.switch('refnx') # or 'refl1d'\n", + "model.interface = interface\n", + "\n", + "fitter = MultiFitter(model)\n", + "# Use the BUMPS backend — DREAM sampling is only available through BUMPS.\n", + "fitter.switch_minimizer(AvailableMinimizers.Bumps)\n", + "\n", + "print('Fitter ready with minimizer:', fitter.easy_science_multi_fitter.minimizer.name)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "eb0f989e", + "metadata": {}, + "outputs": [], + "source": [ + "# ---- Classical fit first ----------------------------------------------------\n", + "# A classical optimisation gives a good starting point and a sanity check\n", + "# before launching the (much more expensive) MCMC sampler.\n", + "\n", + "analysed = fitter.fit(data)\n", + "\n", + "print('Classical fit successful:', analysed.get('success', 'N/A'))\n", + "print('Reduced χ² ≈', analysed.get('reduced_chi', 'N/A'))\n", + "\n", + "# Plot the fit\n", + "r_model = analysed['R_0_model'].values\n", + "\n", + "plt.figure(figsize=(8, 5))\n", + "plt.semilogy(qz, r_data, 'o', label='Data', alpha=0.7)\n", + "plt.semilogy(qz, r_model, '-', label='Classical BUMPS fit', linewidth=2)\n", + "plt.xlabel('Q / Å⁻¹')\n", + "plt.ylabel('Reflectivity')\n", + "plt.title('Classical fit before Bayesian sampling')\n", + "plt.legend()\n", + "plt.grid(True, alpha=0.3)\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "20c9b0e0", + "metadata": {}, + "outputs": [], + "source": [ + "# ---- Bayesian MCMC sampling -------------------------------------------------\n", + "# ``MultiFitter.mcmc_sample()`` delegates to the BUMPS DREAM sampler.\n", + "# All keyword arguments are forwarded with user-friendly names:\n", + "# ``samples`` ← total retained samples\n", + "# ``burn`` ← burn-in steps\n", + "# ``thin`` ← thinning interval\n", + "# ``population``← BUMPS-native ``pop`` for advanced users\n", + "#\n", + "# We deliberately start with a *very* short run. It finishes in seconds but is\n", + "# far too short to trust — which is exactly the situation the \"extend the chain\"\n", + "# section below exists to fix. In production you would ask for 20 k+ samples.\n", + "\n", + "posterior_dict = fitter.mcmc_sample(\n", + " data,\n", + " samples=500, # Deliberately too short — extended later in this notebook\n", + " burn=100,\n", + " thin=10,\n", + ")\n", + "\n", + "print('DREAM sampling complete.')\n", + "print(f' Posterior shape : {posterior_dict[\"draws\"].shape}')\n", + "print(f' Parameters : {posterior_dict[\"param_names\"]}')\n", + "print()\n", + "print(\n", + " 'Note: the retained draws are fewer than samples/thin. BUMPS trims the\\n'\n", + " 'chain at a detected burn point and drops outlier chains, so the number of\\n'\n", + " 'rows is not predictable from the arguments — read it off the array.'\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f23934a0", + "metadata": {}, + "outputs": [], + "source": [ + "# ---- Wrap in PosteriorResults -----------------------------------------------\n", + "# ``PosteriorResults`` gives you a convenient object with built-in analysis\n", + "# methods. You can also use the raw ``dict`` with the standalone functions\n", + "# — both styles are shown below.\n", + "\n", + "posterior = PosteriorResults(\n", + " draws=posterior_dict['draws'],\n", + " param_names=posterior_dict['param_names'],\n", + " logp=posterior_dict.get('logp'),\n", + " sampler_state=posterior_dict.get('state'),\n", + ")\n", + "\n", + "print(posterior)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ef300cb1", + "metadata": {}, + "outputs": [], + "source": [ + "# ---- Posterior summary table ------------------------------------------------\n", + "# ``.summary()`` prints mean, standard deviation and 95 % HDI for each parameter.\n", + "\n", + "print(posterior.summary())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "34e3cc3e", + "metadata": {}, + "outputs": [], + "source": [ + "# ---- Gelman-Rubin R-hat convergence diagnostic ------------------------------\n", + "# R-hat compares the variance *between* DREAM's parallel chains with the\n", + "# variance *within* each one. Values close to 1.0 mean the chains have mixed\n", + "# and agree on the same posterior; the usual rule of thumb is R̂ < 1.1.\n", + "#\n", + "# ``posterior.gelman_rubin()`` needs draws pre-split into chains, shape\n", + "# ``(n_chains, n_draws, n_params)``. ``mcmc_sample()`` returns the draws\n", + "# already pooled into a flat ``(n_samples, n_params)`` array, so we ask the\n", + "# BUMPS state itself — it still knows the individual chains.\n", + "\n", + "\n", + "def rhat_table(state, param_names):\n", + " \"\"\"R-hat per parameter, read from the chain-aware BUMPS state.\"\"\"\n", + " return dict(zip(param_names, state.gelman()))\n", + "\n", + "\n", + "rhat_short = rhat_table(posterior.sampler_state, posterior.param_names)\n", + "\n", + "print('Gelman-Rubin R-hat after the short run:')\n", + "for name, r in rhat_short.items():\n", + " flag = ' ✓' if r < 1.1 else ' ⚠ not converged'\n", + " print(f' {name:<30s} R̂ = {r:.3f}{flag}')\n", + "\n", + "# DREAM is stochastic and unseeded, so the exact values differ run to run.\n", + "n_bad = sum(r >= 1.1 for r in rhat_short.values())\n", + "if n_bad:\n", + " print(f'\\n{n_bad} parameter(s) above 1.1 — this chain is not converged and needs more samples.')\n", + "else:\n", + " print(\n", + " f'\\nR̂ passes, but this chain holds only {posterior.draws.shape[0]} draws — '\n", + " 'far too few to rely on,\\nso every posterior estimate from it is coarse. '\n", + " 'Either way, the answer is more samples.'\n", + " )" + ] + }, + { + "cell_type": "markdown", + "id": "223f79f1", + "metadata": {}, + "source": [ + "## Extending the chain\n", + "\n", + "The short run above is not something to draw conclusions from — it usually\n", + "fails the R̂ < 1.1 check outright, and even when it scrapes past it, it holds\n", + "only a few dozen draws. The fix is more samples, but restarting from scratch\n", + "would throw away the work already done and pay the burn-in cost a second time.\n", + "\n", + "Instead, **continue the existing chain**. `MultiFitter.mcmc_sample()` keeps the\n", + "underlying `Sampler` on `fitter.sampler`, and `Sampler.extend()` picks the chain\n", + "up exactly where DREAM left off:\n", + "\n", + "```python\n", + "extended = fitter.sampler.extend(additional_samples=8000, thin=10)\n", + "```\n", + "\n", + "`extend()` runs with `burn=0` — re-burning an already-converged chain would be a\n", + "mistake, and BUMPS forces it to 0 on resume anyway. It also grows DREAM's\n", + "fixed-size ring buffer by exactly `additional_samples`, so none of the existing\n", + "draws are evicted.\n", + "\n", + "### What \"improvement\" does and does not mean here\n", + "\n", + "Two things get better, and one thing deliberately does not:\n", + "\n", + "* **R̂ drops** — the chains mix and settle onto the same posterior.\n", + "* **The Monte-Carlo error shrinks** — with more draws the posterior *estimate*\n", + " (mean, credible interval, histogram shape) stops moving from run to run.\n", + "* **The posterior width does _not_ shrink.** The spread of a parameter is set by\n", + " the data and the model, not by how long you sample. If the credible interval\n", + " collapsed as we added samples, that would be a bug, not a win. What improves is\n", + " how precisely we know that interval.\n", + "\n", + "DREAM is stochastic and this notebook does not seed it, so the exact numbers\n", + "below change on every run. The direction of travel does not." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a5d26166", + "metadata": {}, + "outputs": [], + "source": [ + "# ---- Continue the existing chain --------------------------------------------\n", + "# ``extend()`` mutates the sampler's BUMPS state in place, so snapshot anything\n", + "# we want to compare against first. ``rhat_short`` (plain floats) was already\n", + "# captured above; the draws need an explicit copy.\n", + "\n", + "draws_short = np.array(posterior.draws, copy=True)\n", + "n_short = draws_short.shape[0]\n", + "\n", + "extended_results = fitter.sampler.extend(\n", + " additional_samples=8000, # Added on top of the original 2000\n", + " thin=10,\n", + ")\n", + "\n", + "posterior_extended = PosteriorResults(\n", + " draws=extended_results.draws,\n", + " param_names=extended_results.param_names,\n", + " logp=extended_results.logp,\n", + " sampler_state=extended_results.state,\n", + ")\n", + "\n", + "print('Chain extended.')\n", + "print(f' Retained draws before : {n_short}')\n", + "print(f' Retained draws after : {posterior_extended.draws.shape[0]}')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f6b17af8", + "metadata": {}, + "outputs": [], + "source": [ + "# ---- Did convergence actually improve? --------------------------------------\n", + "# The headline check: R-hat before vs after.\n", + "\n", + "rhat_extended = rhat_table(posterior_extended.sampler_state, posterior_extended.param_names)\n", + "\n", + "print(f'{\"parameter\":<32s} {\"short\":>8s} {\"extended\":>10s}')\n", + "print('-' * 54)\n", + "for name in posterior_extended.param_names:\n", + " before, after = rhat_short[name], rhat_extended[name]\n", + " flag = ' ✓' if after < 1.1 else ' ⚠'\n", + " print(f'{name:<32s} {before:8.3f} {after:10.3f}{flag}')\n", + "\n", + "n_bad_before = sum(r >= 1.1 for r in rhat_short.values())\n", + "n_bad_after = sum(r >= 1.1 for r in rhat_extended.values())\n", + "print(f'\\nParameters failing R̂ < 1.1: {n_bad_before} → {n_bad_after}')\n", + "print(f'Worst R̂ across parameters: {max(rhat_short.values()):.3f} → {max(rhat_extended.values()):.3f}')\n", + "\n", + "if n_bad_after == 0 and n_bad_before > 0:\n", + " print('\\nThe extended chain has converged; the short one had not.')\n", + "elif n_bad_after == 0:\n", + " print('\\nBoth chains pass R̂, but the extended one does so with far more draws behind it.')\n", + "else:\n", + " print('\\nStill above threshold — extend again, or revisit the model and its bounds.')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f620d42d", + "metadata": {}, + "outputs": [], + "source": [ + "# ---- Side-by-side posterior statistics --------------------------------------\n", + "# ``extend()`` returns the parameters in the same order as the original run, so\n", + "# the columns line up — but map by name rather than trusting the index.\n", + "\n", + "col_short = {name: i for i, name in enumerate(posterior.param_names)}\n", + "col_ext = {name: i for i, name in enumerate(posterior_extended.param_names)}\n", + "\n", + "\n", + "def ci_width(column, alpha=0.95):\n", + " lo, hi = np.percentile(column, [100 * (1 - alpha) / 2, 100 * (1 + alpha) / 2])\n", + " return hi - lo\n", + "\n", + "\n", + "header = (\n", + " f'{\"parameter\":<32s} {\"mean (short)\":>13s} {\"mean (ext)\":>13s} {\"shift\":>8s} {\"95% w (short)\":>14s} {\"95% w (ext)\":>13s}'\n", + ")\n", + "print(header)\n", + "print('-' * len(header))\n", + "\n", + "for name in posterior_extended.param_names:\n", + " a = draws_short[:, col_short[name]]\n", + " b = posterior_extended.draws[:, col_ext[name]]\n", + " # Mean shift expressed in extended-posterior standard deviations: how far the\n", + " # short run's answer sits from the better-resolved one, in units that matter.\n", + " shift = abs(np.mean(a) - np.mean(b)) / np.std(b)\n", + " print(f'{name:<32s} {np.mean(a):13.5g} {np.mean(b):13.5g} {shift:7.2f}σ {ci_width(a):14.4g} {ci_width(b):13.4g}')\n", + "\n", + "print(\n", + " '\\nRead this alongside the R̂ table, not on its own. The credible intervals do\\n'\n", + " 'not systematically shrink — they should not, since posterior width is set by\\n'\n", + " 'the data, not by sampling effort. What extending buys is trust: the means\\n'\n", + " 'stop drifting and the marginals below are resolved by many more draws.'\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "67c37350", + "metadata": {}, + "outputs": [], + "source": [ + "# ---- Visual overlay: marginals, short vs extended ----------------------------\n", + "# Same posterior, better resolved. The short run's histogram is ragged because\n", + "# it is built from few draws; the extended one traces a smooth marginal over the\n", + "# same range. Densities are normalised so the two are comparable despite the\n", + "# very different sample counts.\n", + "\n", + "n_params = len(posterior_extended.param_names)\n", + "fig, axes = plt.subplots(1, n_params, figsize=(4 * n_params, 3.2))\n", + "axes = np.atleast_1d(axes)\n", + "\n", + "for ax, name in zip(axes, posterior_extended.param_names):\n", + " a = draws_short[:, col_short[name]]\n", + " b = posterior_extended.draws[:, col_ext[name]]\n", + " bins = np.histogram_bin_edges(np.concatenate([a, b]), bins=30)\n", + " ax.hist(a, bins=bins, density=True, alpha=0.55, label=f'short (n={a.shape[0]})')\n", + " ax.hist(b, bins=bins, density=True, alpha=0.55, label=f'extended (n={b.shape[0]})')\n", + " ax.set_title(name, fontsize=9)\n", + " ax.set_ylabel('density')\n", + " ax.legend(fontsize=8)\n", + " ax.grid(True, alpha=0.3)\n", + "\n", + "fig.suptitle('Marginal posterior: original chain vs extended chain')\n", + "fig.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e46c3dd4", + "metadata": {}, + "outputs": [], + "source": [ + "# ---- Adopt the extended chain for the rest of the notebook -------------------\n", + "# Everything below (corner plots, credible intervals, posterior-predictive\n", + "# checks) now runs on the converged chain. ``posterior_short`` is kept around\n", + "# in case you want to re-run the comparisons above.\n", + "\n", + "posterior_short = posterior\n", + "posterior = posterior_extended\n", + "\n", + "print(posterior)\n", + "print()\n", + "print(posterior.summary())" + ] + }, + { + "cell_type": "markdown", + "id": "d1f0a300", + "metadata": {}, + "source": [ + "## Interactive posterior plots\n", + "\n", + "The next three cells render the same Plotly figures that are shown on the\n", + "Bayesian Posterior tab of the EasyReflectometryApp:\n", + "\n", + "* **Marginals** — per-parameter distribution: histogram + smooth KDE marginal,\n", + " shaded 95% credible interval, and median / best-sample lines\n", + " (`plot_distribution` or `posterior.distribution()`)\n", + "* **Corner plot** — pairwise marginals + 2-D contours + scatter (`plot_corner`)\n", + "* **Traces** — per-chain trace + marginal histogram (`plot_trace`)\n", + "\n", + "The standalone helpers are called with ``return_figure=True`` so they return a\n", + "Plotly ``Figure`` that we render in the notebook with ``fig.show()``; the\n", + "``PosteriorResults`` methods return the figure directly." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5b9d3c2a", + "metadata": {}, + "outputs": [], + "source": [ + "# ---- Marginal posterior distributions ---------------------------------------\n", + "# One panel per free parameter, each overlaying the posterior histogram, a\n", + "# smooth Gaussian-KDE marginal, the shaded 95% credible interval, and the\n", + "# median / best-sample reference lines. Mirrors the \"Marginals\" sub-tab in\n", + "# the EasyReflectometryApp.\n", + "\n", + "# The PosteriorResults method passes ``logp`` through automatically so the\n", + "# best posterior sample (MAP) line is drawn:\n", + "fig = posterior.distribution()\n", + "fig.show()\n", + "\n", + "# Standalone equivalent (pass ``logp`` to get the best-sample line):\n", + "# fig = plot_distribution(\n", + "# posterior.draws,\n", + "# posterior.param_names,\n", + "# logp=posterior.logp,\n", + "# return_figure=True,\n", + "# )\n", + "# fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e759a2a0", + "metadata": {}, + "outputs": [], + "source": [ + "# ---- Corner plot ------------------------------------------------------------\n", + "# Pairwise correlations: marginal density on the diagonal, scatter samples\n", + "# and 2-D contours on the lower triangle. Mirrors the \"Corner Plot\" sub-tab.\n", + "\n", + "fig = plot_corner(posterior.draws, posterior.param_names)\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "593f5ed3", + "metadata": {}, + "outputs": [], + "source": [ + "# ---- Trace plot -------------------------------------------------------------\n", + "# Per-chain trace (left column) and marginal histogram (right column).\n", + "# Mirrors the \"Traces\" sub-tab in the EasyReflectometryApp.\n", + "\n", + "fig = plot_trace(posterior.draws, posterior.param_names, return_figure=True)\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3049e68a", + "metadata": {}, + "outputs": [], + "source": [ + "# ---- Credible intervals -----------------------------------------------------\n", + "# Equal-tailed 95 % credible intervals for each parameter.\n", + "\n", + "ci_95 = posterior.credible_interval(alpha=0.95)\n", + "print('95 % credible intervals:')\n", + "for name, (lo, hi) in ci_95.items():\n", + " print(f' {name:<30s} [{lo:.4f}, {hi:.4f}]')\n", + "\n", + "# You can request narrower intervals too:\n", + "ci_50 = posterior.credible_interval(alpha=0.50)\n", + "print('\\n50 % credible intervals:')\n", + "for name, (lo, hi) in ci_50.items():\n", + " print(f' {name:<30s} [{lo:.4f}, {hi:.4f}]')\n", + "\n", + "# Standalone equivalent:\n", + "# ci = credible_intervals(posterior.draws, posterior.param_names, alpha=0.95)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "806bf47a", + "metadata": {}, + "outputs": [], + "source": [ + "# ---- Pairwise focus: thickness vs SLD ---------------------------------------\n", + "# A 2-parameter corner plot zooms in on the joint posterior for a specific\n", + "# pair. This is the same view the EasyReflectometryApp's \"2D Heatmap\"\n", + "# sub-tab uses to inspect parameter degeneracies.\n", + "\n", + "\n", + "params_of_interest = ['Layer_1_ThicknessParameter_0', 'Material_1_SldParameter_0']\n", + "idx = [posterior.param_names.index(p) for p in params_of_interest]\n", + "subset_draws = np.asarray(posterior.draws)[:, idx]\n", + "\n", + "fig = plot_corner(subset_draws, params_of_interest)\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c7e5816e", + "metadata": {}, + "outputs": [], + "source": [ + "# ---- Posterior-predictive reflectivity --------------------------------------\n", + "# Propagate parameter uncertainty through the model to obtain median\n", + "# reflectivity and 95 % credible band.\n", + "\n", + "r_median, r_lower, r_upper = posterior_predictive_reflectivity(\n", + " posterior.draws,\n", + " posterior.param_names,\n", + " model,\n", + " qz,\n", + " n_samples=200,\n", + ")\n", + "\n", + "plt.figure(figsize=(9, 6))\n", + "plt.semilogy(qz, r_data, 'o', label='Data', alpha=0.6)\n", + "plt.semilogy(qz, r_median, '-', color='tab:orange', label='Posterior median')\n", + "plt.fill_between(\n", + " qz,\n", + " r_lower,\n", + " r_upper,\n", + " color='tab:orange',\n", + " alpha=0.3,\n", + " label='95 % credible interval',\n", + ")\n", + "plt.xlabel('Q / Å⁻¹')\n", + "plt.ylabel('Reflectivity')\n", + "plt.title('Bayesian Posterior-Predictive Check')\n", + "plt.legend()\n", + "plt.grid(True, alpha=0.3)\n", + "plt.show()\n", + "\n", + "print('The 95 % credible interval captures parameter uncertainty propagated through the model.')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ef27b8ec", + "metadata": {}, + "outputs": [], + "source": [ + "# ---- Posterior-predictive SLD profile ---------------------------------------\n", + "# The same idea applied to the scattering-length-density profile.\n", + "\n", + "z, sld_median, sld_lower, sld_upper = posterior_predictive_sld_profile(\n", + " posterior.draws,\n", + " posterior.param_names,\n", + " model,\n", + " n_samples=200,\n", + ")\n", + "\n", + "plt.figure(figsize=(8, 4))\n", + "plt.plot(z, sld_median, label='Posterior median SLD')\n", + "plt.fill_between(\n", + " z,\n", + " sld_lower,\n", + " sld_upper,\n", + " alpha=0.3,\n", + " label='95 % credible interval',\n", + ")\n", + "plt.xlabel('z / Å')\n", + "plt.ylabel('SLD / 10⁻⁶ Å⁻²')\n", + "plt.title('SLD Profile with Bayesian Uncertainty')\n", + "plt.legend()\n", + "plt.grid(True, alpha=0.3)\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "37e78b4e", + "metadata": {}, + "outputs": [], + "source": [ + "# ---- Summary ----------------------------------------------------------------\n", + "print('―' * 60)\n", + "print('Bayesian analysis complete!')\n", + "print('―' * 60)\n", + "print()\n", + "print('API surface demonstrated:')\n", + "print(' MultiFitter.mcmc_sample(data, samples=, burn=, thin=)')\n", + "print(' MultiFitter.sampler — the Sampler behind the last run')\n", + "print(' .extend(additional_samples=, thin=)')\n", + "print(' — continue the chain, no re-burn')\n", + "print(' .state.gelman() — chain-aware R̂ (works on pooled draws)')\n", + "print(' PosteriorResults(draws, param_names, logp=, sampler_state=)')\n", + "print(' .summary() — formatted parameter table')\n", + "print(' .distribution() — per-parameter marginal Plotly figure')\n", + "print(' .corner() — pairwise correlation Plotly figure')\n", + "print(' .trace() — MCMC chain trace plot')\n", + "print(' .credible_interval(alpha) — equal-tailed credible intervals')\n", + "print(' .gelman_rubin() — R̂ for draws pre-split into chains')\n", + "print()\n", + "print(' Standalone functions (work on raw dict; the plot helpers return')\n", + "print(' the same interactive Plotly figures as the App):')\n", + "print(' posterior_summary(draws, names)')\n", + "print(' plot_distribution(draws, names, logp=, return_figure=True)')\n", + "print(' plot_corner(draws, names)')\n", + "print(' plot_trace(draws, names, return_figure=True)')\n", + "print(' credible_intervals(draws, names, alpha)')\n", + "print(' posterior_predictive_reflectivity(draws, names, model, q, n)')\n", + "print(' posterior_predictive_sld_profile(draws, names, model, n)')\n", + "print()\n", + "print('Workflow: fit → sample short → check R̂ → extend until converged → analyse.')\n", + "print('Extending continues the existing chain, so the burn-in is paid only once.')" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "era", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/src/tutorials/fitting/example.ort b/docs/docs/tutorials/advancedfitting/example.ort similarity index 100% rename from docs/src/tutorials/fitting/example.ort rename to docs/docs/tutorials/advancedfitting/example.ort diff --git a/docs/src/tutorials/advancedfitting/multi_contrast.ipynb b/docs/docs/tutorials/advancedfitting/multi_contrast.ipynb similarity index 91% rename from docs/src/tutorials/advancedfitting/multi_contrast.ipynb rename to docs/docs/tutorials/advancedfitting/multi_contrast.ipynb index 51e4e225..1c482386 100644 --- a/docs/src/tutorials/advancedfitting/multi_contrast.ipynb +++ b/docs/docs/tutorials/advancedfitting/multi_contrast.ipynb @@ -20,6 +20,16 @@ "First configure matplotlib to place figures in notebook and import needed modules. Note that the plot function needs installation of `plopp` seperately or installation of `easyreflectometry[dev]`" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "54684688", + "metadata": {}, + "outputs": [], + "source": [ + "%matplotlib inline" + ] + }, { "cell_type": "code", "execution_count": null, @@ -27,29 +37,21 @@ "metadata": {}, "outputs": [], "source": [ - "%matplotlib inline\n", - "\n", - "import refnx\n", "import pooch\n", + "from easyscience.fitting import AvailableMinimizers\n", "\n", - "import easyreflectometry\n", - "\n", + "from easyreflectometry.calculators import CalculatorFactory\n", "from easyreflectometry.data import load\n", + "from easyreflectometry.fitting import MultiFitter\n", + "from easyreflectometry.model import Model\n", + "from easyreflectometry.model import PercentageFwhm\n", "from easyreflectometry.plot import plot\n", - "from easyreflectometry.sample import Material\n", - "from easyreflectometry.sample import SurfactantLayer\n", "from easyreflectometry.sample import Layer\n", - "from easyreflectometry.sample import Multilayer\n", "from easyreflectometry.sample import LayerAreaPerMolecule\n", + "from easyreflectometry.sample import Material\n", + "from easyreflectometry.sample import Multilayer\n", "from easyreflectometry.sample import Sample\n", - "from easyreflectometry.model import Model\n", - "from easyreflectometry.model import PercentageFwhm\n", - "from easyreflectometry.calculators import CalculatorFactory\n", - "from easyreflectometry.fitting import MultiFitter\n", - "from easyscience.fitting import AvailableMinimizers\n", - "\n", - "print(f'easyreflectometry: {easyreflectometry.__version__}')\n", - "print(f'refnx: {refnx.__version__}')" + "from easyreflectometry.sample import SurfactantLayer" ] }, { @@ -70,14 +72,7 @@ "id": "694b4e5e-2d1a-402e-aa3f-a26cc82f7774", "metadata": {}, "outputs": [], - "source": [ - "file_path = pooch.retrieve(\n", - " # URL to one of Pooch's test files\n", - " url=\"https://raw.githubusercontent.com/EasyScience/EasyReflectometryLib/master/docs/src/tutorials/advancedfitting/multiple.ort\",\n", - " known_hash=\"241bcb819cdae47fbbb310a99c2456c7332312719496b936a153dc7dee83e62c\",\n", - ")\n", - "data = load(file_path)" - ] + "source": "file_path = pooch.retrieve(\n # Fetch test data from the easyscience/reflectometry data repository\n url='https://raw.githubusercontent.com/easyscience/reflectometry/master/data/multiple.ort',\n known_hash='241bcb819cdae47fbbb310a99c2456c7332312719496b936a153dc7dee83e62c',\n)\ndata = load(file_path)" }, { "cell_type": "markdown", @@ -130,8 +125,7 @@ "metadata": {}, "outputs": [], "source": [ - "dspc = {'d-head': 'C10D18NO8P', 'd-tail': 'C34D70',\n", - " 'h-head': 'C10H18NO8P', 'h-tail': 'C34H70'}" + "dspc = {'d-head': 'C10D18NO8P', 'd-tail': 'C34D70', 'h-head': 'C10H18NO8P', 'h-tail': 'C34H70'}" ] }, { @@ -235,7 +229,7 @@ " solvent=air,\n", " solvent_fraction=tail_solvent_fraction,\n", " area_per_molecule=area_per_molecule,\n", - " roughness=roughness\n", + " roughness=roughness,\n", ")\n", "head_layer_d13d2o = LayerAreaPerMolecule(\n", " molecular_formula=dspc['d-head'],\n", @@ -243,12 +237,9 @@ " solvent=d2o,\n", " solvent_fraction=head_solvent_fraction,\n", " area_per_molecule=area_per_molecule,\n", - " roughness=roughness\n", - ")\n", - "d13d2o = SurfactantLayer(\n", - " tail_layer=tail_layer_d13d2o,\n", - " head_layer=head_layer_d13d2o\n", + " roughness=roughness,\n", ")\n", + "d13d2o = SurfactantLayer(tail_layer=tail_layer_d13d2o, head_layer=head_layer_d13d2o)\n", "d13d2o.constrain_area_per_molecule = True\n", "d13d2o.conformal_roughness = True\n", "d13d2o.constrain_solvent_roughness(d2o_layer.roughness)" @@ -275,7 +266,7 @@ " solvent=air,\n", " solvent_fraction=tail_solvent_fraction,\n", " area_per_molecule=area_per_molecule,\n", - " roughness=roughness\n", + " roughness=roughness,\n", ")\n", "head_layer_d70d2o = LayerAreaPerMolecule(\n", " molecular_formula=dspc['h-head'],\n", @@ -283,12 +274,9 @@ " solvent=d2o,\n", " solvent_fraction=head_solvent_fraction,\n", " area_per_molecule=area_per_molecule,\n", - " roughness=roughness\n", - ")\n", - "d70d2o = SurfactantLayer(\n", - " tail_layer=tail_layer_d70d2o,\n", - " head_layer=head_layer_d70d2o\n", + " roughness=roughness,\n", ")\n", + "d70d2o = SurfactantLayer(tail_layer=tail_layer_d70d2o, head_layer=head_layer_d70d2o)\n", "d70d2o.constrain_area_per_molecule = True\n", "d70d2o.conformal_roughness = True\n", "d70d2o.constrain_solvent_roughness(d2o_layer.roughness)" @@ -315,7 +303,7 @@ " solvent=air,\n", " solvent_fraction=tail_solvent_fraction,\n", " area_per_molecule=area_per_molecule,\n", - " roughness=roughness\n", + " roughness=roughness,\n", ")\n", "head_layer_d83acmw = LayerAreaPerMolecule(\n", " molecular_formula=dspc['d-head'],\n", @@ -323,12 +311,9 @@ " solvent=acmw,\n", " solvent_fraction=head_solvent_fraction,\n", " area_per_molecule=area_per_molecule,\n", - " roughness=roughness\n", - ")\n", - "d83acmw = SurfactantLayer(\n", - " tail_layer=tail_layer_d83acmw,\n", - " head_layer=head_layer_d83acmw\n", + " roughness=roughness,\n", ")\n", + "d83acmw = SurfactantLayer(tail_layer=tail_layer_d83acmw, head_layer=head_layer_d83acmw)\n", "d83acmw.constrain_area_per_molecule = True\n", "d83acmw.conformal_roughness = True\n", "d83acmw.constrain_solvent_roughness(acmw_layer.roughness)" @@ -420,19 +405,19 @@ " sample=d13d2o_sample,\n", " scale=0.1,\n", " background=data['data']['R_d13DSPC-D2O'].values.min(),\n", - " resolution_function=resolution_function\n", + " resolution_function=resolution_function,\n", ")\n", "d70d2o_model = Model(\n", " sample=d70d2o_sample,\n", " scale=0.1,\n", " background=data['data']['R_d70DSPC-D2O'].values.min(),\n", - " resolution_function=resolution_function\n", + " resolution_function=resolution_function,\n", ")\n", "d83acmw_model = Model(\n", " sample=d83acmw_sample,\n", " scale=0.1,\n", " background=data['data']['R_d83DSPC-ACMW'].values.min(),\n", - " resolution_function=resolution_function\n", + " resolution_function=resolution_function,\n", ")" ] }, @@ -570,4 +555,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} +} \ No newline at end of file diff --git a/docs/src/tutorials/advancedfitting/multiple.ort b/docs/docs/tutorials/advancedfitting/multiple.ort similarity index 100% rename from docs/src/tutorials/advancedfitting/multiple.ort rename to docs/docs/tutorials/advancedfitting/multiple.ort diff --git a/docs/docs/tutorials/basic/assemblies_library.md b/docs/docs/tutorials/basic/assemblies_library.md new file mode 100644 index 00000000..da419f56 --- /dev/null +++ b/docs/docs/tutorials/basic/assemblies_library.md @@ -0,0 +1,71 @@ +# Creating Multilayers and Surfactant Layers + +EasyReflectometry is designed to be used with a broad range of different +assemblies. Assemblies are collective layers behaving as a single +object, for example, a multilayer or a surfactant layer. These +assemblies offer flexibility for the user and enable more powerful +analysis by making chemical and physical constraints available with +limited code. In this page, we will document the assemblies that are +available with simple examples of the constructors that exist. Full API +documentation is also available for the +`easyreflectometry.sample.assemblies` module. + +## Multilayer + +This assembly should be used for a series of layers that should be +thought of as a single object. For example, in the simple fitting +tutorial this assembly type is used to combine the silicon and silicon +dioxide layer that is formed into a single object. All of the separate +layers in these objects will be fitted individually, i.e. there are no +constraints present, however, there is some cognitive benefit to +grouping layers together. + +To create a `Multilayer` object, we use the following construction. + +```python +from easyreflectometry.sample import Layer +from easyreflectometry.sample import Material +from easyreflectometry.sample import Multilayer + +si = Material(sld=2.07, isld=0, name='Si') +sio2 = Material(sld=3.47, isld=0, name='SiO2') +si_layer = Layer(material=si, thickness=0, roughness=0, name='Si layer') +sio2_layer = Layer(material=sio2, thickness=30, roughness=3, name='SiO2 layer') + +subphase = Multilayer(layers=[si_layer, sio2_layer], name='Si/SiO2 subphase') +``` + +This will create a `Multilayer` object named `subphase` which we can use +in some `Structure` for our analysis. + +## RepeatingMultilayer + +The `RepeatingMultilayer` assembly type is an extension of the +`Multilayer` for the analysis of systems with a multilayer that has some +number of repeats. This assembly type imposes some constraints, +specifically that all of the repeats have the exact same structure (i.e. +thicknesses, roughnesses, and scattering length densities), which brings +with it some computational saving as the reflectometry coefficients only +need to be calculated once for this structure and propagated for the +correct number of repeats. There is a tutorial that discusses the +utilisation of this assembly type for a nickel-titanium multilayer +system. + +The creation of a `RepeatingMultilayer` object is very similar to that +for the `Multilayer`, with the addition of a number of repetitions. + +```python +from easyreflectometry.sample import Layer +from easyreflectometry.sample import Material +from easyreflectometry.sample import RepeatingMultilayer + +ti = Material(sld=-1.9493, isld=0, name='Ti') +ni = Material(sld=9.4245, isld=0, name='Ni') +ti_layer = Layer(material=ti, thickness=40, roughness=0, name='Ti Layer') +ni_layer = Layer(material=ni, thickness=70, roughness=0, name='Ni Layer') +ni_ti = RepeatingMultilayer(layers=[ti_layer, ni_layer], repetitions=10, name='Ni/Ti Multilayer') +``` + +The number of repeats is a parameter that can be varied in the +optimisation process, however given this is a value that depends on the +synthesis of the sample this is unlikely to be necessary. diff --git a/docs/docs/tutorials/basic/layer_library.md b/docs/docs/tutorials/basic/layer_library.md new file mode 100644 index 00000000..4a679690 --- /dev/null +++ b/docs/docs/tutorials/basic/layer_library.md @@ -0,0 +1,69 @@ +# Defining Layers + +Similar to a range of different materials, there are a few different +ways that a layer can be defined in EasyReflectometry. + +## Layer + +The `Layer` is the simplest possible type of layer, taking a `Material` +and two floats associated with the thickness and upper (that is closer +to the source of the incident radiation) roughness. So we construct a +`Layer` as follows for a 100 Å thick layer of boron with a roughness of +10 Å. + +```python +from easyreflectometry.sample import Material +from easyreflectometry.sample import Layer + +boron = Material(sld=6.908, isld=-0.278, name='Boron') +boron_layer = Layer(material=boron, thickness=100, roughness=10, name='Boron Layer') +``` + +This type of layer is used extensively in the tutorials. + +To create a semi-infinite layer one needs to set the thickness to 0 and +the roughness to 0. + +```python +from easyreflectometry.sample import Material +from easyreflectometry.sample import Layer + +si = Material(sld=2.07, isld=0, name='Si') +semi_infinite_layer = Layer(material=si, thickness=0, roughness=0, name='Si layer') +``` + +## LayerAreaPerMolecule + +The `LayerAreaPerMolecule` layer type is the foundation of the +`SurfactantLayer` assemblies type (further information on this can be +found in the assemblies library). The purpose of the +`LayerAreaPerMolecule` is to allow a layer to be defined in terms of the +chemical formula of the material and the area per molecule of the layer. +The area per molecule is a common description of surface density in the +surfactant monolayer and bilayer community. + +We can construct a 10 Å thick `LayerAreaPerMolecule` of +phosphatidylcholine, with an area per molecule of 48 Å squared and a +roughness of 3 Å that has 20% solvent surface coverage with D2O using +the following. + +```python +from easyreflectometry.sample import Material +from easyreflectometry.sample import LayerAreaPerMolecule + +d2o = Material(sld=6.36, isld=0, name='D2O') +molecular_formula = 'C10H18NO8P' +pc = LayerAreaPerMolecule( + molecular_formula=molecular_formula, + thickness=10, + solvent=d2o, + solvent_fraction=0.2, + area_per_molecule=48, + roughness=3, + name='PC Layer', +) +``` + +It is expected that the typical user will not interface directly with +the `LayerAreaPerMolecule` assembly type, but instead the +`SurfactantLayer` assemblies library will be used instead. diff --git a/docs/docs/tutorials/basic/material_library.md b/docs/docs/tutorials/basic/material_library.md new file mode 100644 index 00000000..78bdfcf1 --- /dev/null +++ b/docs/docs/tutorials/basic/material_library.md @@ -0,0 +1,78 @@ +# Defining Materials + +In order to support a wide range of applications (and to build complex +assemblies) there are a few different types of material that can be +utilised in EasyReflectometry. These can include constraints or enable +the user to define the material based on chemical or physical +properties. Full API documentation for the +`easyreflectometry.sample.elements.material` module is also available, +but here we will give some simple uses for them. + +## Material + +The simplest type of material that is available is the `Material`. This +allows the user to define a single type of material, with a real and +imaginary component to the scattering length density. The construction +of a `Material` is achieved as shown below. + +```python +from easyreflectometry.sample import Material + +boron = Material(sld=6.908, isld=-0.278, name='Boron') +``` + +The above object will have the properties of `sld` and `isld`, which +will have values of `6.908 1/angstrom^2` and `-0.278 1/angstrom^2` +respectively. As is shown in the tutorials, a material can be used to +construct a `Layer` from which +[slab models](https://www.reflectometry.org/isis_school/3_reflectometry_slab_models/the_slab_model.html) +are created. + +## MaterialDensity + +In addition to defining a material by its scattering length density, it +may be useful to define a material by the mass density and chemical +formula. This is possible with the `MaterialDensity` material type, +which uses the scattering length and atomic mass from the chemical +formula and the density to determine the scattering length density. It +is then possible to vary the density, which defines the scattering +length density in turn. The `MaterialDensity` material can be created as +follows. + +```python +from easyreflectometry.sample import MaterialDensity + +chemical_structure = 'SiO2' +si = MaterialDensity(chemical_structure=chemical_structure, density=2.65, name='SiO2 Material') +``` + +The density should be in units of grams per cubic centimeter and the +scattering length is calculated from `'SiO2'`. + +## MaterialSolvated + +Sometimes it is desirable to have a layer that consists of a material +and a solvent in some ratio. An example of this is shown in the +solvation tutorial, where a polymer film solvated with D2O is modelled. +To produce a material that is described by such a mixture, there is +`MaterialSolvated`. This is constructed from two constituent `Materials` +and the fractional amount of the material in the solvent. So to produce +a `MaterialSolvated` that is 20% D2O in a polymer, the following is +used. + +```python +from easyreflectometry.sample import Material +from easyreflectometry.sample import MaterialSolvated + +polymer = Material(sld=2.0, isld=0.0, name='Polymer') +d2o = Material(sld=6.36, isld=0, name='D2O') + +solvated_polymer = MaterialSolvated(material=polymer, solvent=d2o, solvent_fraction=0.2, name='Solvated Polymer') +``` + +For the `solvated_polymer` object, the `sld` will be +`2.872 1/angstrom^2` (the weighted average of the two scattering length +densities). The `MaterialSolvated` includes a constraint such that if +the value of either constituent scattering length densities (both real +and imaginary components) or the fraction changes, then the resulting +material `sld` and `isld` will change appropriately. diff --git a/docs/docs/tutorials/basic/model.md b/docs/docs/tutorials/basic/model.md new file mode 100644 index 00000000..6b9c76c3 --- /dev/null +++ b/docs/docs/tutorials/basic/model.md @@ -0,0 +1,87 @@ +# Creating a Model + +The main component of an experiment in EasyReflectometry is the `Model`. +This is a description of the `Sample` and the environment in which the +experiment is performed. The `Model` is used to calculate the +reflectivity of the `Sample` at a given set of angles (Q-points). The +resolution functions are used to quantify the experimental uncertainties +in wavelength and angle, allowing the `Model` to accurately describe the +data. + +## Model + +A `Model` instance contains a `Sample` and variables describing +experimental settings. To be able to compute reflectivities it is also +necessary to have a `Calculator` (interface). + +```python +from easyreflectometry.calculators import CalculatorFactory +from easyreflectometry.model import Model +from easyreflectometry.sample import Sample + +default_sample = Sample() +model = Model(sample=default_sample, scale=1.0, background=1e-6) + +interface = CalculatorFactory() +model.interface = interface +``` + +This will create a `Model` instance with the `default_sample` and the +environment variables `scale` factor set to 1.0 and a `background` of +1e-6. Following the `interface` is set to the default calculator that is +`Refnx`. + +## Resolution Functions + +A resolution function enables the EasyReflectometry model to incorporate +the experimental uncertainties in wavelength and incident angle into the +model. In its essence the resolution function controls the smearing to +apply when determining the reflectivity at a given Q-point. For a given +Q-point the smearing to apply is given as a weighted average of the +neighboring Q-point, which weights are by a normal distribution. This +normal distribution is then defined by a Q-point dependent Full Width at +the Half Maximum (FWHM) that is given by the resolution function. + +### PercentageFwhm + +Often we rely on a resolution function that has a simple functional +dependency of the Q-point. By this is understood that the applied +smearing in a Q-point has a FWHM that is simply a percentage of the +value of the Q-point. + +```python +from easyreflectometry.model import Model +from easyreflectometry.model import PercentageFwhm + +resolution_function = PercentageFwhm(1.1) + +m = Model(resolution_function=resolution_function) +``` + +This will create a `Model` instance where the resolution function is +defined as 1.1% of the Q-point value, which again is the FWHM for the +smearing. + +### LinearSpline + +Alternatively the FWHM value might be determined and declared directly +for each measured Q-point. When this is the case the provided Q-points +and the corresponding FWHM values can be used to declare a linear spline +function and thereby enable a determination of the reflectivity at an +arbitrary point within the provided range of discrete Q-points. + +```python +from easyreflectometry.model import Model +from easyreflectometry.model import LinearSpline + +m = Model() + +resolution_function = LinearSpline(q_data_points=[0.01, 0.2, 0.31], fwhm_values=[0.001, 0.043, 0.026]) + +m.resolution_function = resolution_function +``` + +This will create a `Model` instance where the resolution function +defining the FWHM is determined from a linear interpolation. In the +present case the provided data Q-points are (`[0.01, 0.2, 0.31]`) and +the corresponding FWHM function values are (`[0.001, 0.043, 0.026]`). diff --git a/docs/src/tutorials/fitting/d70d2o.ort b/docs/docs/tutorials/fitting/d70d2o.ort similarity index 100% rename from docs/src/tutorials/fitting/d70d2o.ort rename to docs/docs/tutorials/fitting/d70d2o.ort diff --git a/docs/src/tutorials/fitting/dspc.png b/docs/docs/tutorials/fitting/dspc.png similarity index 100% rename from docs/src/tutorials/fitting/dspc.png rename to docs/docs/tutorials/fitting/dspc.png diff --git a/docs/src/tutorials/fitting/eq_monolayer.svg b/docs/docs/tutorials/fitting/eq_monolayer.svg similarity index 100% rename from docs/src/tutorials/fitting/eq_monolayer.svg rename to docs/docs/tutorials/fitting/eq_monolayer.svg diff --git a/docs/src/tutorials/fitting/eq_solvated.svg b/docs/docs/tutorials/fitting/eq_solvated.svg similarity index 100% rename from docs/src/tutorials/fitting/eq_solvated.svg rename to docs/docs/tutorials/fitting/eq_solvated.svg diff --git a/docs/docs/tutorials/fitting/example.ort b/docs/docs/tutorials/fitting/example.ort new file mode 100644 index 00000000..968fa7bb --- /dev/null +++ b/docs/docs/tutorials/fitting/example.ort @@ -0,0 +1,448 @@ +# # ORSO reflectivity data file | 0.1 standard | YAML encoding | https://www.reflectometry.org/ +# data_source: +# owner: +# name: Andrew Nelson +# affiliation: ANSTO +# contact: Andrew.Nelson@ansto.gov.au +# experiment: +# facility: ANSTO +# start_date: 2021-05-12 +# title: Example data file from refnx docs +# instrument: platypus +# probe: neutron +# sample: +# name: Polymer Film +# category: solid / liquid +# composition: Si / SiO2 / Film / D2O +# measurement: +# instrument_settings: +# wavelength: +# magnitude: 12 +# unit: angstrom +# incident_angle: +# magnitude: 3 +# unit: deg +# data_files: +# - Unknown.nxs +# scheme: angle-dispersive +# reduction: +# software: ess +# timestamp: 2022-01-27T15:33:59+01:00 +# corrections: +# - footprint +# - incident intensity +# - detector efficiency +# columns: +# - {name: Qz, unit: 1/angstrom, dimension: WW transfer} +# - {name: R, dimension: reflectivity} +# - {name: sR, dimension: error-reflectivity} +# - {name: sQz, unit: 1/angstrom, dimension: resolution-WW transfer} +## Qz RQz sR sQz +8.060220000e-03 7.095810000e-01 8.506760000e-02 1.407419648e-04 +8.136620000e-03 8.622810000e-01 1.123700000e-01 1.420996057e-04 +8.263750000e-03 9.086470000e-01 7.900470000e-02 1.443588017e-04 +8.370670000e-03 7.732920000e-01 7.927280000e-02 1.462583099e-04 +8.450330000e-03 1.057970000e+00 1.259590000e-01 1.476732801e-04 +8.530830000e-03 1.015660000e+00 1.132950000e-01 1.491031133e-04 +8.612170000e-03 7.347170000e-01 6.115660000e-02 1.505473850e-04 +8.694370000e-03 7.692160000e-01 6.170580000e-02 1.520069445e-04 +8.777430000e-03 1.115740000e+00 1.127300000e-01 1.534813672e-04 +8.861360000e-03 9.723030000e-01 8.971600000e-02 1.549710776e-04 +8.946160000e-03 7.512140000e-01 5.493930000e-02 1.564760759e-04 +9.031850000e-03 7.976490000e-01 5.671220000e-02 1.579963619e-04 +9.118440000e-03 9.221890000e-01 6.858410000e-02 1.595327850e-04 +9.205930000e-03 9.757550000e-01 7.293950000e-02 1.610844959e-04 +9.294320000e-03 8.195040000e-01 5.216170000e-02 1.626523440e-04 +9.383640000e-03 7.883200000e-01 4.794730000e-02 1.642367538e-04 +9.473890000e-03 7.947010000e-01 4.602240000e-02 1.658368761e-04 +9.565080000e-03 8.744000000e-01 5.151640000e-02 1.674535601e-04 +9.657210000e-03 8.396620000e-01 4.742850000e-02 1.690872306e-04 +9.750300000e-03 8.008720000e-01 4.399580000e-02 1.707370382e-04 +9.844360000e-03 1.117100000e+00 7.373300000e-02 1.724042569e-04 +9.939390000e-03 8.884110000e-01 4.954100000e-02 1.740884620e-04 +1.003540000e-02 7.791290000e-01 3.898730000e-02 1.757896536e-04 +1.013240000e-02 7.999680000e-01 3.899740000e-02 1.775086809e-04 +1.023040000e-02 8.431240000e-01 4.159800000e-02 1.792451193e-04 +1.032940000e-02 9.613320000e-01 4.925360000e-02 1.809989689e-04 +1.048680000e-02 8.805440000e-01 2.990830000e-02 1.837851690e-04 +1.063270000e-02 7.557350000e-01 3.208510000e-02 1.863700799e-04 +1.073590000e-02 9.712310000e-01 4.534850000e-02 1.881969711e-04 +1.084010000e-02 8.955490000e-01 3.905420000e-02 1.900425474e-04 +1.094540000e-02 8.625890000e-01 3.580720000e-02 1.919068087e-04 +1.105180000e-02 8.909920000e-01 3.612570000e-02 1.937906045e-04 +1.115930000e-02 9.003480000e-01 3.679240000e-02 1.956930853e-04 +1.126790000e-02 8.459270000e-01 3.218810000e-02 1.976151006e-04 +1.137760000e-02 9.431520000e-01 3.655330000e-02 1.995570749e-04 +1.148840000e-02 9.956310000e-01 3.901160000e-02 2.015185836e-04 +1.160040000e-02 9.695940000e-01 3.636230000e-02 2.034996267e-04 +1.171350000e-02 9.051810000e-01 3.204100000e-02 2.055014781e-04 +1.182780000e-02 8.933810000e-01 3.061190000e-02 2.075232887e-04 +1.194330000e-02 9.196020000e-01 3.146770000e-02 2.095659076e-04 +1.205990000e-02 9.189980000e-01 3.060970000e-02 2.116293349e-04 +1.217770000e-02 7.810560000e-01 2.355420000e-02 2.137135706e-04 +1.229680000e-02 8.649150000e-01 2.720250000e-02 2.158190394e-04 +1.241700000e-02 8.435160000e-01 2.550150000e-02 2.179457412e-04 +1.253850000e-02 9.984180000e-01 3.197220000e-02 2.200945253e-04 +1.266120000e-02 8.812600000e-01 2.602730000e-02 2.222645425e-04 +1.278520000e-02 8.835690000e-01 2.558620000e-02 2.244570667e-04 +1.291050000e-02 9.376700000e-01 2.746510000e-02 2.266716733e-04 +1.303700000e-02 1.019200000e+00 3.051070000e-02 2.289087870e-04 +1.316480000e-02 8.455260000e-01 2.262690000e-02 2.311684076e-04 +1.329390000e-02 8.738040000e-01 2.324680000e-02 2.334509599e-04 +1.342430000e-02 8.659530000e-01 2.249060000e-02 2.357568686e-04 +1.355610000e-02 8.779820000e-01 2.244910000e-02 2.380861337e-04 +1.368920000e-02 9.475450000e-01 2.489850000e-02 2.404387551e-04 +1.382370000e-02 8.881540000e-01 2.209620000e-02 2.428155821e-04 +1.395950000e-02 8.913620000e-01 2.181480000e-02 2.452161902e-04 +1.409670000e-02 8.884560000e-01 2.150920000e-02 2.476410039e-04 +1.423530000e-02 9.137170000e-01 2.225860000e-02 2.500908727e-04 +1.437530000e-02 8.103640000e-01 1.882890000e-02 2.525649471e-04 +1.451680000e-02 7.385480000e-01 1.688270000e-02 2.550645011e-04 +1.465970000e-02 6.865100000e-01 1.597420000e-02 2.575891102e-04 +1.480400000e-02 5.822400000e-01 1.350210000e-02 2.601396235e-04 +1.494980000e-02 4.468550000e-01 1.006260000e-02 2.627156166e-04 +1.509700000e-02 3.924610000e-01 9.155490000e-03 2.653175139e-04 +1.524580000e-02 3.205170000e-01 7.319590000e-03 2.679461649e-04 +1.539610000e-02 2.810060000e-01 6.399090000e-03 2.706011448e-04 +1.554790000e-02 2.401000000e-01 5.442390000e-03 2.732828784e-04 +1.570120000e-02 2.208810000e-01 5.024370000e-03 2.759917903e-04 +1.585610000e-02 1.920330000e-01 4.314410000e-03 2.787278805e-04 +1.601260000e-02 1.798490000e-01 4.051590000e-03 2.814919983e-04 +1.617070000e-02 1.600690000e-01 3.562020000e-03 2.842837190e-04 +1.633030000e-02 1.531290000e-01 3.467770000e-03 2.871038920e-04 +1.649160000e-02 1.342200000e-01 3.016000000e-03 2.899525174e-04 +1.665450000e-02 1.283300000e-01 2.888530000e-03 2.928300196e-04 +1.681900000e-02 1.247940000e-01 2.861820000e-03 2.957363988e-04 +1.698530000e-02 1.091270000e-01 2.483100000e-03 2.986720796e-04 +1.715320000e-02 1.044290000e-01 2.353920000e-03 3.016374867e-04 +1.732280000e-02 9.468300000e-02 2.091620000e-03 3.046326200e-04 +1.758810000e-02 8.969110000e-02 1.439380000e-03 3.093191777e-04 +1.784190000e-02 8.091440000e-02 1.872780000e-03 3.138010489e-04 +1.801850000e-02 7.465440000e-02 1.714220000e-03 3.169189092e-04 +1.819680000e-02 7.036610000e-02 1.634230000e-03 3.200686191e-04 +1.837700000e-02 6.904450000e-02 1.605300000e-03 3.232497539e-04 +1.855890000e-02 6.270550000e-02 1.461460000e-03 3.264631629e-04 +1.874270000e-02 5.939150000e-02 1.400070000e-03 3.297088462e-04 +1.892840000e-02 5.754770000e-02 1.360950000e-03 3.329872283e-04 +1.911590000e-02 5.138330000e-02 1.226680000e-03 3.362987340e-04 +1.930540000e-02 4.922670000e-02 1.178240000e-03 3.396437879e-04 +1.949670000e-02 4.521740000e-02 1.079620000e-03 3.430219654e-04 +1.969000000e-02 4.245560000e-02 1.022250000e-03 3.464349651e-04 +1.988520000e-02 4.126130000e-02 9.947810000e-04 3.498819376e-04 +2.008240000e-02 3.523330000e-02 8.777900000e-04 3.533637323e-04 +2.028150000e-02 3.352710000e-02 8.367470000e-04 3.568803492e-04 +2.048270000e-02 3.326840000e-02 8.339590000e-04 3.604326376e-04 +2.068590000e-02 3.166440000e-02 7.898860000e-04 3.640210222e-04 +2.089120000e-02 2.916000000e-02 7.496130000e-04 3.676450784e-04 +2.109850000e-02 2.652010000e-02 7.063830000e-04 3.713060800e-04 +2.130800000e-02 2.518290000e-02 6.713890000e-04 3.750036024e-04 +2.151950000e-02 2.387570000e-02 6.426930000e-04 3.787384951e-04 +2.173310000e-02 2.289290000e-02 6.330680000e-04 3.825111825e-04 +2.194900000e-02 2.086460000e-02 5.931280000e-04 3.863216648e-04 +2.216690000e-02 2.087710000e-02 5.920100000e-04 3.901703665e-04 +2.238710000e-02 1.822280000e-02 5.292050000e-04 3.940581370e-04 +2.260950000e-02 1.773460000e-02 5.251520000e-04 3.979849764e-04 +2.283410000e-02 1.587140000e-02 4.818770000e-04 4.019513092e-04 +2.306100000e-02 1.432550000e-02 4.516190000e-04 4.059575601e-04 +2.329020000e-02 1.427760000e-02 4.522490000e-04 4.100041538e-04 +2.352170000e-02 1.266240000e-02 4.198450000e-04 4.140919397e-04 +2.375550000e-02 1.221280000e-02 4.137860000e-04 4.182204929e-04 +2.399170000e-02 1.046080000e-02 3.745320000e-04 4.223906630e-04 +2.423020000e-02 1.061330000e-02 3.810520000e-04 4.266016005e-04 +2.447120000e-02 9.879030000e-03 3.641280000e-04 4.308567027e-04 +2.471450000e-02 8.372030000e-03 3.313930000e-04 4.351542710e-04 +2.496030000e-02 7.670480000e-03 3.077050000e-04 4.394943054e-04 +2.520860000e-02 7.344890000e-03 3.049780000e-04 4.438810525e-04 +2.545940000e-02 6.798650000e-03 2.898680000e-04 4.483102657e-04 +2.571270000e-02 5.916300000e-03 2.671780000e-04 4.527819450e-04 +2.596850000e-02 5.344980000e-03 2.515360000e-04 4.573003369e-04 +2.622690000e-02 5.122650000e-03 2.474120000e-04 4.618611950e-04 +2.648800000e-02 4.750310000e-03 2.379530000e-04 4.664730124e-04 +2.675160000e-02 4.307150000e-03 2.238500000e-04 4.711272958e-04 +2.701790000e-02 4.018170000e-03 2.200510000e-04 4.758325386e-04 +2.728680000e-02 3.539150000e-03 2.046530000e-04 4.805802475e-04 +2.755850000e-02 3.818190000e-03 2.079440000e-04 4.853789156e-04 +2.783290000e-02 2.864750000e-03 1.819210000e-04 4.902285431e-04 +2.811000000e-02 2.795800000e-03 1.766910000e-04 4.951206367e-04 +2.839000000e-02 2.621500000e-03 1.750020000e-04 5.000679362e-04 +2.867270000e-02 2.484770000e-03 1.694560000e-04 5.050619484e-04 +2.895830000e-02 2.420090000e-03 1.709250000e-04 5.101069199e-04 +2.924670000e-02 2.359260000e-03 1.700600000e-04 5.151986041e-04 +2.953810000e-02 1.978560000e-03 1.600210000e-04 5.203454942e-04 +2.983230000e-02 1.947200000e-03 1.582820000e-04 5.255475902e-04 +3.012960000e-02 1.735930000e-03 1.527310000e-04 5.307963989e-04 +3.042980000e-02 1.894590000e-03 1.603050000e-04 5.361004136e-04 +3.073300000e-02 1.696680000e-03 1.525740000e-04 5.414596341e-04 +3.103920000e-02 1.793690000e-03 1.592450000e-04 5.468698140e-04 +3.134860000e-02 1.786860000e-03 1.552020000e-04 5.523351998e-04 +3.166100000e-02 1.872010000e-03 1.583950000e-04 5.578557915e-04 +3.197660000e-02 1.688180000e-03 1.499840000e-04 5.634315891e-04 +3.229530000e-02 1.732370000e-03 1.543250000e-04 5.690625926e-04 +3.261730000e-02 1.537600000e-03 1.453960000e-04 5.747530487e-04 +3.294240000e-02 1.541340000e-03 1.497560000e-04 5.804987107e-04 +3.327080000e-02 1.700330000e-03 1.572080000e-04 5.863038252e-04 +3.360260000e-02 2.142240000e-03 1.728870000e-04 5.921683922e-04 +3.393760000e-02 1.944020000e-03 1.686890000e-04 5.980924118e-04 +3.422050000e-02 1.914000000e-03 1.077140000e-04 6.019483327e-04 +3.466450000e-02 1.986410000e-03 1.438280000e-04 6.103523719e-04 +3.502190000e-02 1.849740000e-03 1.279740000e-04 6.165099550e-04 +3.538500000e-02 1.932640000e-03 1.227080000e-04 6.227354838e-04 +3.571620000e-02 2.188990000e-03 1.495980000e-04 6.288506008e-04 +3.607730000e-02 2.314320000e-03 1.503450000e-04 6.351568151e-04 +3.646770000e-02 1.831770000e-03 1.041590000e-04 6.416328939e-04 +3.682630000e-02 1.797150000e-03 1.012920000e-04 6.480155472e-04 +3.716160000e-02 2.278740000e-03 1.417330000e-04 6.543599810e-04 +3.754210000e-02 2.183390000e-03 1.294600000e-04 6.609379784e-04 +3.794040000e-02 1.799000000e-03 9.624980000e-05 6.676221409e-04 +3.831280000e-02 1.828080000e-03 9.734640000e-05 6.742680840e-04 +3.868400000e-02 1.949850000e-03 1.082600000e-04 6.809692330e-04 +3.906920000e-02 2.042140000e-03 1.122450000e-04 6.877680540e-04 +3.947340000e-02 1.659460000e-03 8.395540000e-05 6.946772869e-04 +3.986330000e-02 1.620310000e-03 8.073380000e-05 7.016035062e-04 +4.026580000e-02 1.588620000e-03 7.617690000e-05 7.086189042e-04 +4.066080000e-02 1.582510000e-03 7.829120000e-05 7.156810150e-04 +4.107010000e-02 1.429900000e-03 7.034450000e-05 7.228323046e-04 +4.147810000e-02 1.336120000e-03 6.498820000e-05 7.300472933e-04 +4.187440000e-02 1.721900000e-03 9.200060000e-05 7.373174879e-04 +4.230490000e-02 1.404890000e-03 6.788170000e-05 7.446980943e-04 +4.273170000e-02 1.146480000e-03 5.315150000e-05 7.521381533e-04 +4.315460000e-02 1.030130000e-03 4.872800000e-05 7.596504046e-04 +4.358350000e-02 1.018640000e-03 4.851630000e-05 7.672348483e-04 +4.401550000e-02 1.060250000e-03 5.148880000e-05 7.748999775e-04 +4.445490000e-02 9.115610000e-04 4.517670000e-05 7.826415457e-04 +4.489830000e-02 7.795360000e-04 3.746760000e-05 7.904553063e-04 +4.534700000e-02 5.957570000e-04 2.867780000e-05 7.983539991e-04 +4.579470000e-02 6.476450000e-04 3.286690000e-05 8.063333774e-04 +4.625120000e-02 5.268390000e-04 2.666210000e-05 8.143891946e-04 +4.671240000e-02 4.365460000e-04 2.240950000e-05 8.225299441e-04 +4.717830000e-02 3.696050000e-04 1.929650000e-05 8.307471325e-04 +4.764690000e-02 3.315640000e-04 1.808550000e-05 8.390577463e-04 +4.812080000e-02 2.545840000e-04 1.440620000e-05 8.474405525e-04 +4.859940000e-02 2.090800000e-04 1.312430000e-05 8.559167841e-04 +4.908280000e-02 1.929370000e-04 1.261590000e-05 8.644779478e-04 +4.957100000e-02 1.309220000e-04 9.992220000e-06 8.731240437e-04 +5.006400000e-02 1.098230000e-04 9.080510000e-06 8.818635651e-04 +5.056190000e-02 9.565480000e-05 8.370090000e-06 8.906880186e-04 +5.106550000e-02 7.925720000e-05 7.699130000e-06 8.995974043e-04 +5.157340000e-02 7.956050000e-05 6.898560000e-06 9.086044619e-04 +5.208640000e-02 7.224950000e-05 6.102730000e-06 9.177006984e-04 +5.260450000e-02 7.825130000e-05 6.391360000e-06 9.268903603e-04 +5.312790000e-02 8.637120000e-05 6.561860000e-06 9.361734476e-04 +5.365650000e-02 1.331620000e-04 8.789840000e-06 9.455499603e-04 +5.419050000e-02 1.436320000e-04 8.178840000e-06 9.550241449e-04 +5.472980000e-02 1.586820000e-04 8.565340000e-06 9.645917550e-04 +5.527450000e-02 1.900070000e-04 9.755240000e-06 9.742612837e-04 +5.582470000e-02 2.354200000e-04 1.141720000e-05 9.840284844e-04 +5.638040000e-02 2.339320000e-04 1.034520000e-05 9.938933571e-04 +5.694170000e-02 2.433900000e-04 1.059550000e-05 1.003855902e-03 +5.750870000e-02 2.708580000e-04 1.125130000e-05 1.013924612e-03 +5.808130000e-02 3.109660000e-04 1.233550000e-05 1.024095240e-03 +5.865970000e-02 3.483270000e-04 1.357860000e-05 1.034367788e-03 +5.924390000e-02 3.329570000e-04 1.275590000e-05 1.044750747e-03 +5.983400000e-02 3.546590000e-04 1.322360000e-05 1.055235624e-03 +6.043000000e-02 3.544670000e-04 1.306610000e-05 1.065826667e-03 +6.103190000e-02 3.855150000e-04 1.408930000e-05 1.076523875e-03 +6.164000000e-02 3.325090000e-04 1.201760000e-05 1.087335742e-03 +6.225410000e-02 3.330870000e-04 1.197150000e-05 1.098253773e-03 +6.287440000e-02 3.303070000e-04 1.197230000e-05 1.109286464e-03 +6.350090000e-02 3.224690000e-04 1.162910000e-05 1.120433812e-03 +6.413370000e-02 2.744620000e-04 9.948790000e-06 1.131691573e-03 +6.477280000e-02 2.792430000e-04 1.025370000e-05 1.143063992e-03 +6.541840000e-02 2.389380000e-04 8.960850000e-06 1.154555315e-03 +6.607040000e-02 2.330280000e-04 8.726650000e-06 1.166165544e-03 +6.672900000e-02 1.863430000e-04 7.236140000e-06 1.177890432e-03 +6.739420000e-02 1.649490000e-04 6.619500000e-06 1.189738471e-03 +6.806600000e-02 1.332440000e-04 5.541570000e-06 1.201709662e-03 +6.874460000e-02 1.153760000e-04 4.971620000e-06 1.213799758e-03 +6.943000000e-02 8.250330000e-05 3.951150000e-06 1.226017252e-03 +7.012230000e-02 6.719230000e-05 3.570180000e-06 1.238357898e-03 +7.082150000e-02 5.033220000e-05 2.926230000e-06 1.250825942e-03 +7.152780000e-02 3.336120000e-05 2.509700000e-06 1.263421384e-03 +7.224110000e-02 2.290520000e-05 2.116380000e-06 1.276148471e-03 +7.293380000e-02 1.879440000e-05 1.860530000e-06 1.288068702e-03 +7.366830000e-02 1.553440000e-05 1.798210000e-06 1.301292643e-03 +7.440150000e-02 1.824520000e-05 1.951840000e-06 1.314359459e-03 +7.514050000e-02 2.235620000e-05 1.801000000e-06 1.327506960e-03 +7.587210000e-02 2.773620000e-05 1.890140000e-06 1.340293500e-03 +7.664020000e-02 3.931010000e-05 2.300950000e-06 1.354192651e-03 +7.739640000e-02 4.596690000e-05 2.332400000e-06 1.367582209e-03 +7.810610000e-02 5.541310000e-05 2.459260000e-06 1.379328330e-03 +7.886820000e-02 6.726050000e-05 2.729570000e-06 1.392692408e-03 +7.979530000e-02 7.650640000e-05 2.773090000e-06 1.409143772e-03 +8.072890000e-02 7.794720000e-05 2.857410000e-06 1.425518696e-03 +8.153340000e-02 8.505420000e-05 2.990400000e-06 1.439910454e-03 +8.233180000e-02 9.685690000e-05 3.222700000e-06 1.454408377e-03 +8.316050000e-02 9.462400000e-05 3.166210000e-06 1.469122877e-03 +8.402520000e-02 8.585640000e-05 2.824460000e-06 1.484058201e-03 +8.486480000e-02 8.101440000e-05 2.662540000e-06 1.499057224e-03 +8.573420000e-02 7.655920000e-05 2.549780000e-06 1.514268578e-03 +8.655530000e-02 7.385430000e-05 2.485840000e-06 1.529496917e-03 +8.743720000e-02 6.249710000e-05 2.190920000e-06 1.545009780e-03 +8.831630000e-02 5.495430000e-05 2.001270000e-06 1.560658534e-03 +8.911080000e-02 5.694540000e-05 2.081580000e-06 1.576247836e-03 +9.004160000e-02 4.229200000e-05 1.729710000e-06 1.592304265e-03 +9.095460000e-02 3.367540000e-05 1.458910000e-06 1.608462612e-03 +9.185040000e-02 2.545640000e-05 1.249630000e-06 1.624727124e-03 +9.274350000e-02 2.092210000e-05 1.159760000e-06 1.641140268e-03 +9.362890000e-02 1.531120000e-05 1.060140000e-06 1.657689303e-03 +9.455270000e-02 1.265650000e-05 9.577530000e-07 1.674480395e-03 +9.551780000e-02 9.331240000e-06 8.621390000e-07 1.691509297e-03 +9.647710000e-02 8.901520000e-06 7.828970000e-07 1.708691077e-03 +9.741890000e-02 1.079150000e-05 8.938470000e-07 1.725991763e-03 +9.842590000e-02 1.213680000e-05 9.113080000e-07 1.743589710e-03 +9.942010000e-02 1.540800000e-05 9.077770000e-07 1.761323549e-03 +1.004250000e-01 1.938030000e-05 9.602530000e-07 1.779248486e-03 +1.014340000e-01 2.091160000e-05 1.040890000e-06 1.797347534e-03 +1.024990000e-01 2.519250000e-05 1.078370000e-06 1.815722611e-03 +1.034980000e-01 2.818800000e-05 1.132170000e-06 1.834144401e-03 +1.045460000e-01 2.998100000e-05 1.178530000e-06 1.852837973e-03 +1.056080000e-01 3.056280000e-05 1.150490000e-06 1.871735383e-03 +1.066820000e-01 2.819030000e-05 1.099070000e-06 1.890832384e-03 +1.077580000e-01 2.692630000e-05 1.037400000e-06 1.910111989e-03 +1.088310000e-01 2.541920000e-05 1.012560000e-06 1.929574198e-03 +1.099160000e-01 2.282700000e-05 9.367100000e-07 1.949244491e-03 +1.110400000e-01 1.769170000e-05 7.904500000e-07 1.969169580e-03 +1.121170000e-01 1.547510000e-05 7.625400000e-07 1.989209328e-03 +1.132440000e-01 1.172580000e-05 6.688050000e-07 2.009529352e-03 +1.143190000e-01 1.050270000e-05 6.598050000e-07 2.029959788e-03 +1.154640000e-01 8.121530000e-06 5.847030000e-07 2.050708720e-03 +1.166450000e-01 4.988840000e-06 5.267190000e-07 2.071720941e-03 +1.177620000e-01 4.909990000e-06 5.360960000e-07 2.092830835e-03 +1.189250000e-01 5.304340000e-06 5.240840000e-07 2.114229497e-03 +1.201290000e-01 5.689050000e-06 4.912340000e-07 2.135895696e-03 +1.213460000e-01 6.444710000e-06 5.061070000e-07 2.157799706e-03 +1.225860000e-01 7.456190000e-06 5.058030000e-07 2.179954265e-03 +1.238230000e-01 8.248420000e-06 5.025600000e-07 2.202325401e-03 +1.250910000e-01 1.067490000e-05 5.845390000e-07 2.224964074e-03 +1.263610000e-01 1.131620000e-05 5.722280000e-07 2.247827816e-03 +1.276430000e-01 1.104400000e-05 5.762550000e-07 2.270937863e-03 +1.289320000e-01 1.062580000e-05 5.655340000e-07 2.294294212e-03 +1.302100000e-01 9.337660000e-06 5.254720000e-07 2.317858646e-03 +1.315470000e-01 8.922800000e-06 5.000680000e-07 2.341754314e-03 +1.328540000e-01 6.436110000e-06 4.446880000e-07 2.365841081e-03 +1.341620000e-01 6.181130000e-06 4.556250000e-07 2.390178397e-03 +1.355150000e-01 4.685680000e-06 4.034780000e-07 2.414821469e-03 +1.368240000e-01 4.667540000e-06 3.809290000e-07 2.439659885e-03 +1.382040000e-01 4.262500000e-06 3.763010000e-07 2.464842276e-03 +1.395890000e-01 3.884300000e-06 3.571580000e-07 2.490292204e-03 +1.409890000e-01 3.699480000e-06 3.796620000e-07 2.516022408e-03 +1.424300000e-01 3.659930000e-06 3.543380000e-07 2.542066861e-03 +1.438510000e-01 4.559390000e-06 3.665690000e-07 2.568357617e-03 +1.452630000e-01 4.580880000e-06 3.470620000e-07 2.594911663e-03 +1.467500000e-01 4.829290000e-06 3.503920000e-07 2.621826671e-03 +1.482250000e-01 4.930920000e-06 3.533300000e-07 2.649009215e-03 +1.497100000e-01 4.781000000e-06 3.612560000e-07 2.676489022e-03 +1.512250000e-01 4.644840000e-06 3.397110000e-07 2.704291571e-03 +1.527360000e-01 4.365080000e-06 3.324590000e-07 2.732374397e-03 +1.542610000e-01 3.807060000e-06 3.253780000e-07 2.760775718e-03 +1.558250000e-01 3.544150000e-06 3.053880000e-07 2.789516767e-03 +1.573660000e-01 2.632690000e-06 2.773550000e-07 2.818538093e-03 +1.589410000e-01 2.129680000e-06 2.570610000e-07 2.847903395e-03 +1.605110000e-01 2.509080000e-06 2.684170000e-07 2.877574452e-03 +1.621370000e-01 2.606260000e-06 2.683540000e-07 2.907627704e-03 +1.637550000e-01 2.467960000e-06 2.618110000e-07 2.937986711e-03 +1.654040000e-01 2.437690000e-06 2.457080000e-07 2.968710928e-03 +1.670350000e-01 2.818460000e-06 2.626350000e-07 2.999740899e-03 +1.687360000e-01 3.201910000e-06 2.677220000e-07 3.031182793e-03 +1.704100000e-01 3.020960000e-06 2.548660000e-07 3.062934688e-03 +1.721190000e-01 2.398550000e-06 2.324640000e-07 3.095064532e-03 +1.738390000e-01 2.458240000e-06 2.385460000e-07 3.127555337e-03 +1.755740000e-01 2.033660000e-06 2.231960000e-07 3.160419844e-03 +1.773360000e-01 1.684040000e-06 2.048280000e-07 3.193666546e-03 +1.790870000e-01 1.293830000e-06 2.007650000e-07 3.227265717e-03 +1.809070000e-01 1.439670000e-06 1.882070000e-07 3.261306534e-03 +1.827150000e-01 1.540200000e-06 1.999980000e-07 3.295708314e-03 +1.845350000e-01 1.338490000e-06 1.864820000e-07 3.330500781e-03 +1.863830000e-01 1.615990000e-06 1.863690000e-07 3.365705170e-03 +1.882470000e-01 1.493470000e-06 1.883860000e-07 3.401317233e-03 +1.901260000e-01 2.059680000e-06 1.920700000e-07 3.437345464e-03 +1.920380000e-01 1.690150000e-06 1.767700000e-07 3.473806849e-03 +1.939540000e-01 1.488370000e-06 1.792740000e-07 3.510675908e-03 +1.959000000e-01 1.403820000e-06 1.747280000e-07 3.547990861e-03 +1.978600000e-01 1.346890000e-06 1.705690000e-07 3.585734722e-03 +1.998450000e-01 9.477100000e-07 1.617790000e-07 3.623937217e-03 +2.018330000e-01 1.089550000e-06 1.735020000e-07 3.662568619e-03 +2.038630000e-01 1.246350000e-06 1.654660000e-07 3.701684134e-03 +2.059040000e-01 1.227370000e-06 1.644900000e-07 3.741254037e-03 +2.079590000e-01 1.168960000e-06 1.584560000e-07 3.781286820e-03 +2.100420000e-01 1.161720000e-06 1.521980000e-07 3.821807963e-03 +2.121400000e-01 1.216890000e-06 1.588540000e-07 3.862808973e-03 +2.142570000e-01 1.317570000e-06 1.546070000e-07 3.904298343e-03 +2.163990000e-01 1.032790000e-06 1.472920000e-07 3.946293059e-03 +2.185700000e-01 1.030660000e-06 1.514730000e-07 3.988805862e-03 +2.207810000e-01 5.945350000e-07 1.511720000e-07 4.031853738e-03 +2.229880000e-01 7.279960000e-07 1.429280000e-07 4.075394220e-03 +2.252160000e-01 7.806310000e-07 1.485250000e-07 4.119465528e-03 +2.274670000e-01 1.063390000e-06 1.499300000e-07 4.164076155e-03 +2.297410000e-01 6.526970000e-07 1.296680000e-07 4.209234596e-03 +2.320370000e-01 1.074380000e-06 1.484530000e-07 4.254932355e-03 +2.343560000e-01 8.687630000e-07 1.468620000e-07 4.301220393e-03 +2.366980000e-01 9.218680000e-07 1.450820000e-07 4.348060490e-03 +2.390640000e-01 6.471490000e-07 1.368090000e-07 4.395495113e-03 +2.414540000e-01 5.363930000e-07 1.356880000e-07 4.443524261e-03 +2.438670000e-01 6.337170000e-07 1.304030000e-07 4.492147934e-03 +2.463040000e-01 6.292700000e-07 1.365570000e-07 4.541408598e-03 +2.487660000e-01 6.332920000e-07 1.224530000e-07 4.591263788e-03 +2.512530000e-01 1.037050000e-06 1.354090000e-07 4.641713503e-03 +2.537640000e-01 8.252860000e-07 1.431610000e-07 4.692842675e-03 +2.563010000e-01 6.268250000e-07 1.192570000e-07 4.744608839e-03 +2.588630000e-01 5.252590000e-07 1.227410000e-07 4.797054460e-03 +2.614500000e-01 5.218320000e-07 1.309180000e-07 4.850137073e-03 +2.640640000e-01 3.916590000e-07 1.213700000e-07 4.903941609e-03 +2.667030000e-01 4.724390000e-07 1.399730000e-07 4.958425602e-03 +2.693690000e-01 5.595360000e-07 1.391510000e-07 5.013589053e-03 +2.720620000e-01 6.640700000e-07 1.448670000e-07 5.069474428e-03 +2.747820000e-01 4.593780000e-07 1.514660000e-07 5.126081726e-03 +2.775290000e-01 3.669610000e-07 1.435460000e-07 5.183453413e-03 +2.803030000e-01 5.315310000e-07 1.483080000e-07 5.241589490e-03 +2.831050000e-01 4.289140000e-07 1.361450000e-07 5.300447491e-03 +2.859350000e-01 5.520060000e-07 1.419830000e-07 5.360112348e-03 +2.887930000e-01 5.702640000e-07 1.529000000e-07 5.420541594e-03 +2.916800000e-01 5.047310000e-07 1.364290000e-07 5.481777696e-03 +2.945960000e-01 6.619230000e-07 1.407930000e-07 5.543863119e-03 +2.975410000e-01 7.601320000e-07 1.607210000e-07 5.606755399e-03 +3.005160000e-01 4.685270000e-07 1.348870000e-07 5.670454534e-03 +3.035200000e-01 4.428600000e-07 1.471340000e-07 5.735087923e-03 +3.065540000e-01 4.899790000e-07 1.404300000e-07 5.800528167e-03 +3.096190000e-01 3.601630000e-07 1.337280000e-07 5.866902666e-03 +3.127140000e-01 2.905630000e-07 1.480620000e-07 5.934168953e-03 +3.158410000e-01 5.199630000e-07 1.515620000e-07 6.002327027e-03 +3.189980000e-01 4.666650000e-07 1.562560000e-07 6.071461822e-03 +3.221870000e-01 3.888830000e-07 1.570110000e-07 6.141530870e-03 +3.254080000e-01 4.467780000e-07 1.555120000e-07 6.212576639e-03 +3.286620000e-01 3.238850000e-07 1.525860000e-07 6.284599127e-03 +3.319470000e-01 3.787360000e-07 1.581800000e-07 6.357640802e-03 +3.352660000e-01 3.881990000e-07 1.539110000e-07 6.431701663e-03 +3.386180000e-01 4.428770000e-07 1.490980000e-07 6.506824176e-03 +3.420030000e-01 2.477870000e-07 1.463210000e-07 6.582965876e-03 +3.454230000e-01 2.857690000e-07 1.351350000e-07 6.660211693e-03 +3.488760000e-01 4.759640000e-07 1.536640000e-07 6.738561630e-03 +3.523640000e-01 4.496390000e-07 1.539560000e-07 6.818015684e-03 +3.558870000e-01 2.471470000e-07 1.443220000e-07 6.898573857e-03 +3.594450000e-01 2.167640000e-07 1.491810000e-07 6.980363546e-03 +3.630390000e-01 5.205850000e-07 1.573690000e-07 7.063257354e-03 +3.666690000e-01 5.246970000e-07 1.545410000e-07 7.147425144e-03 +3.703350000e-01 3.644030000e-07 1.550240000e-07 7.232781985e-03 +3.740370000e-01 4.597520000e-07 1.653080000e-07 7.319370343e-03 +3.777770000e-01 5.359220000e-07 1.820430000e-07 7.407232683e-03 +3.815540000e-01 4.268070000e-07 1.770030000e-07 7.496411472e-03 +3.853690000e-01 4.036660000e-07 1.811100000e-07 7.586906710e-03 +3.892220000e-01 2.554540000e-07 1.597290000e-07 7.678718396e-03 +3.931130000e-01 9.269720000e-08 1.654020000e-07 7.771931464e-03 +3.970440000e-01 1.106720000e-07 1.808880000e-07 7.866503446e-03 +4.010140000e-01 4.521630000e-07 1.731910000e-07 7.962519276e-03 +4.050230000e-01 3.780660000e-07 1.515970000e-07 8.059978953e-03 +4.090730000e-01 3.091360000e-07 1.571950000e-07 8.158924942e-03 +4.131630000e-01 3.417740000e-07 1.448180000e-07 8.259357245e-03 +4.172940000e-01 3.449240000e-07 1.595810000e-07 8.361318327e-03 +4.214660000e-01 2.518400000e-07 1.597570000e-07 8.464850655e-03 +4.256800000e-01 4.017370000e-07 1.538140000e-07 8.569954228e-03 +4.299360000e-01 3.172790000e-07 1.691300000e-07 8.676713978e-03 +4.342350000e-01 5.506310000e-07 1.611420000e-07 8.785087440e-03 +4.385770000e-01 5.085100000e-07 1.649900000e-07 8.895159545e-03 +4.429620000e-01 6.025930000e-07 1.738350000e-07 9.006930294e-03 +4.473910000e-01 4.384540000e-07 1.653500000e-07 9.120484618e-03 +4.518650000e-01 3.387570000e-07 1.876390000e-07 9.235822519e-03 +4.563830000e-01 4.358460000e-07 1.978260000e-07 9.352943995e-03 +4.609460000e-01 3.855790000e-07 1.761430000e-07 9.471933979e-03 +4.655550000e-01 3.834150000e-07 1.884540000e-07 9.592834938e-03 diff --git a/docs/src/tutorials/fitting/material_solvated.ipynb b/docs/docs/tutorials/fitting/material_solvated.ipynb similarity index 92% rename from docs/src/tutorials/fitting/material_solvated.ipynb rename to docs/docs/tutorials/fitting/material_solvated.ipynb index 4665e87b..e64f7f7a 100644 --- a/docs/src/tutorials/fitting/material_solvated.ipynb +++ b/docs/docs/tutorials/fitting/material_solvated.ipynb @@ -23,6 +23,16 @@ "First configure matplotlib to place figures in notebook and import needed modules. Note that the plot function needs installation of `plopp` seperately or installation of `easyreflectometry[dev]`" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "0143544a", + "metadata": {}, + "outputs": [], + "source": [ + "%matplotlib inline" + ] + }, { "cell_type": "code", "execution_count": null, @@ -30,26 +40,23 @@ "metadata": {}, "outputs": [], "source": [ - "%matplotlib inline\n", - "\n", "import numpy as np\n", - "import scipp as sc\n", "import pooch\n", "import refnx\n", + "import scipp as sc\n", "\n", "import easyreflectometry\n", - "\n", + "from easyreflectometry.calculators import CalculatorFactory\n", "from easyreflectometry.data import load\n", + "from easyreflectometry.fitting import MultiFitter\n", + "from easyreflectometry.model import Model\n", + "from easyreflectometry.model import PercentageFwhm\n", + "from easyreflectometry.plot import plot\n", "from easyreflectometry.sample import Layer\n", - "from easyreflectometry.sample import Sample\n", "from easyreflectometry.sample import Material\n", "from easyreflectometry.sample import MaterialSolvated\n", "from easyreflectometry.sample import Multilayer\n", - "from easyreflectometry.model import Model\n", - "from easyreflectometry.model import PercentageFwhm\n", - "from easyreflectometry.calculators import CalculatorFactory\n", - "from easyreflectometry.fitting import MultiFitter\n", - "from easyreflectometry.plot import plot" + "from easyreflectometry.sample import Sample" ] }, { @@ -89,14 +96,7 @@ "id": "a95a39dd-d0eb-4029-9dc8-41e6e7918f66", "metadata": {}, "outputs": [], - "source": [ - "file_path = pooch.retrieve(\n", - " # URL to one of Pooch's test files\n", - " url=\"https://raw.githubusercontent.com/EasyScience/EasyReflectometryLib/master/docs/src/tutorials/fitting/example.ort\",\n", - " known_hash=\"82d0c95c069092279a799a8131ad3710335f601d9f1080754b387f42e407dfab\",\n", - ")\n", - "data = load(file_path)" - ] + "source": "file_path = pooch.retrieve(\n # Fetch test data from the easyscience/reflectometry data repository\n url='https://raw.githubusercontent.com/easyscience/reflectometry/master/data/example.ort',\n known_hash='82d0c95c069092279a799a8131ad3710335f601d9f1080754b387f42e407dfab',\n)\ndata = load(file_path)" }, { "cell_type": "markdown", @@ -137,12 +137,7 @@ "metadata": {}, "outputs": [], "source": [ - "solvated_film_material = MaterialSolvated(\n", - " material=film,\n", - " solvent=d2o,\n", - " solvent_fraction=0.25,\n", - " name='Solvated Film'\n", - ")" + "solvated_film_material = MaterialSolvated(material=film, solvent=d2o, solvent_fraction=0.25, name='Solvated Film')" ] }, { @@ -196,13 +191,7 @@ "\n", "resolution_function = PercentageFwhm(0.02)\n", "sample = Sample(superphase, Multilayer(solvated_film), Multilayer(subphase), name='Film Structure')\n", - "model = Model(\n", - " sample=sample,\n", - " scale=1,\n", - " background=1e-6,\n", - " resolution_function=resolution_function,\n", - " name='Film Model'\n", - ")" + "model = Model(sample=sample, scale=1, background=1e-6, resolution_function=resolution_function, name='Film Model')" ] }, { @@ -376,4 +365,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} +} \ No newline at end of file diff --git a/docs/src/tutorials/fitting/monolayer.ipynb b/docs/docs/tutorials/fitting/monolayer.ipynb similarity index 94% rename from docs/src/tutorials/fitting/monolayer.ipynb rename to docs/docs/tutorials/fitting/monolayer.ipynb index d0ef8aa0..a04bba8f 100644 --- a/docs/src/tutorials/fitting/monolayer.ipynb +++ b/docs/docs/tutorials/fitting/monolayer.ipynb @@ -20,6 +20,16 @@ "First configure matplotlib to place figures in notebook and import needed modules. Note that the plot function needs installation of `plopp` seperately or installation of `easyreflectometry[dev]`" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "ece9d352", + "metadata": {}, + "outputs": [], + "source": [ + "%matplotlib inline" + ] + }, { "cell_type": "code", "execution_count": null, @@ -27,27 +37,22 @@ "metadata": {}, "outputs": [], "source": [ - "%matplotlib inline\n", - "\n", - "import refnx\n", "import pooch\n", + "import refnx\n", "\n", "import easyreflectometry\n", - "\n", "from easyreflectometry.calculators import CalculatorFactory\n", "from easyreflectometry.data import load\n", + "from easyreflectometry.fitting import MultiFitter\n", + "from easyreflectometry.model import Model\n", + "from easyreflectometry.model import PercentageFwhm\n", "from easyreflectometry.plot import plot\n", - "from easyreflectometry.sample import Material\n", - "from easyreflectometry.sample import SurfactantLayer\n", - "from easyreflectometry.sample import LayerAreaPerMolecule\n", "from easyreflectometry.sample import Layer\n", + "from easyreflectometry.sample import LayerAreaPerMolecule\n", + "from easyreflectometry.sample import Material\n", "from easyreflectometry.sample import Multilayer\n", "from easyreflectometry.sample import Sample\n", - "from easyreflectometry.model import Model\n", - "from easyreflectometry.model import PercentageFwhm\n", - "from easyreflectometry.fitting import MultiFitter\n", - "from easyreflectometry.plot import plot\n", - "from easyscience.fitting import AvailableMinimizers\n" + "from easyreflectometry.sample import SurfactantLayer" ] }, { @@ -87,15 +92,7 @@ "id": "e392660e-6f02-4f0b-be86-4c8ea78883e0", "metadata": {}, "outputs": [], - "source": [ - "file_path = pooch.retrieve(\n", - " # URL to one of Pooch's test files\n", - " url=\"https://raw.githubusercontent.com/EasyScience/EasyReflectometryLib/master/docs/src/tutorials/fitting/d70d2o.ort\",\n", - " known_hash=\"3e4750536621be8eec493fa21a287e408d384f29cacb113b71d02690d99f0998\",\n", - ")\n", - "data = load(file_path)\n", - "plot(data)" - ] + "source": "file_path = pooch.retrieve(\n # Fetch test data from the easyscience/reflectometry data repository\n url='https://raw.githubusercontent.com/easyscience/reflectometry/master/data/d70d2o.ort',\n known_hash='3e4750536621be8eec493fa21a287e408d384f29cacb113b71d02690d99f0998',\n)\ndata = load(file_path)\nplot(data)" }, { "cell_type": "markdown", @@ -254,22 +251,19 @@ " molecular_formula=tail_formula,\n", " thickness=tail_thickness,\n", " solvent=air,\n", - " solvent_fraction=tail_solvent_fraction, \n", + " solvent_fraction=tail_solvent_fraction,\n", " area_per_molecule=area_per_molecule,\n", - " roughness=roughness\n", + " roughness=roughness,\n", ")\n", "head_layer = LayerAreaPerMolecule(\n", " molecular_formula=head_formula,\n", " thickness=head_thickness,\n", " solvent=d2o,\n", - " solvent_fraction=head_solvent_fraction, \n", + " solvent_fraction=head_solvent_fraction,\n", " area_per_molecule=area_per_molecule,\n", - " roughness=roughness\n", - ")\n", - "dspc = SurfactantLayer(\n", - " tail_layer=tail_layer,\n", - " head_layer=head_layer\n", + " roughness=roughness,\n", ")\n", + "dspc = SurfactantLayer(tail_layer=tail_layer, head_layer=head_layer)\n", "dspc.constrain_area_per_molecule = True\n", "dspc.conformal_roughness = True\n", "dspc" @@ -330,12 +324,7 @@ "source": [ "resolution_function = PercentageFwhm(5)\n", "sample = Sample(Multilayer(air_layer), dspc, Multilayer(d2o_layer))\n", - "model = Model(\n", - " sample=sample,\n", - " scale=1,\n", - " background=data['data']['R_0'].values.min(),\n", - " resolution_function=resolution_function\n", - ")" + "model = Model(sample=sample, scale=1, background=data['data']['R_0'].values.min(), resolution_function=resolution_function)" ] }, { @@ -492,4 +481,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} +} \ No newline at end of file diff --git a/docs/src/tutorials/fitting/monolayer.png b/docs/docs/tutorials/fitting/monolayer.png similarity index 100% rename from docs/src/tutorials/fitting/monolayer.png rename to docs/docs/tutorials/fitting/monolayer.png diff --git a/docs/src/tutorials/fitting/monolayer.svg b/docs/docs/tutorials/fitting/monolayer.svg similarity index 100% rename from docs/src/tutorials/fitting/monolayer.svg rename to docs/docs/tutorials/fitting/monolayer.svg diff --git a/docs/src/tutorials/fitting/polymer_film.png b/docs/docs/tutorials/fitting/polymer_film.png similarity index 100% rename from docs/src/tutorials/fitting/polymer_film.png rename to docs/docs/tutorials/fitting/polymer_film.png diff --git a/docs/src/tutorials/fitting/polymer_film.svg b/docs/docs/tutorials/fitting/polymer_film.svg similarity index 100% rename from docs/src/tutorials/fitting/polymer_film.svg rename to docs/docs/tutorials/fitting/polymer_film.svg diff --git a/docs/src/tutorials/fitting/repeating.ipynb b/docs/docs/tutorials/fitting/repeating.ipynb similarity index 93% rename from docs/src/tutorials/fitting/repeating.ipynb rename to docs/docs/tutorials/fitting/repeating.ipynb index 0ebde2e1..156a1229 100644 --- a/docs/src/tutorials/fitting/repeating.ipynb +++ b/docs/docs/tutorials/fitting/repeating.ipynb @@ -24,6 +24,16 @@ "First configure matplotlib to place figures in notebook and import needed modules. Note that the plot function needs installation of `plopp` seperately or installation of `easyreflectometry[dev]`" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "419add7b", + "metadata": {}, + "outputs": [], + "source": [ + "%matplotlib inline" + ] + }, { "cell_type": "code", "execution_count": null, @@ -31,27 +41,23 @@ "metadata": {}, "outputs": [], "source": [ - "%matplotlib inline\n", - "\n", "import numpy as np\n", - "import scipp as sc\n", "import pooch\n", "import refl1d\n", + "import scipp as sc\n", "\n", "import easyreflectometry\n", - "\n", + "from easyreflectometry.calculators import CalculatorFactory\n", "from easyreflectometry.data import load\n", - "from easyreflectometry.sample import Layer\n", - "from easyreflectometry.sample import Sample\n", - "from easyreflectometry.sample import Material\n", - "from easyreflectometry.sample import RepeatingMultilayer\n", - "from easyreflectometry.sample import Multilayer\n", + "from easyreflectometry.fitting import MultiFitter\n", "from easyreflectometry.model import Model\n", "from easyreflectometry.model import PercentageFwhm\n", - "from easyreflectometry.calculators import CalculatorFactory\n", - "from easyreflectometry.fitting import MultiFitter\n", "from easyreflectometry.plot import plot\n", - "from easyscience.fitting import AvailableMinimizers" + "from easyreflectometry.sample import Layer\n", + "from easyreflectometry.sample import Material\n", + "from easyreflectometry.sample import Multilayer\n", + "from easyreflectometry.sample import RepeatingMultilayer\n", + "from easyreflectometry.sample import Sample" ] }, { @@ -92,14 +98,7 @@ "id": "7121c7e9", "metadata": {}, "outputs": [], - "source": [ - "file_path = pooch.retrieve(\n", - " # URL to one of Pooch's test files\n", - " url=\"https://raw.githubusercontent.com/EasyScience/EasyReflectometryLib/master/docs/src/tutorials/fitting/repeating_layers.ort\",\n", - " known_hash=\"a5ffca9fd24f1d362266251723aec7ce9f34f123e39a38dfc4d829c758e6bf90\",\n", - ")\n", - "data = load(file_path)" - ] + "source": "file_path = pooch.retrieve(\n # Fetch test data from the easyscience/reflectometry data repository\n url='https://raw.githubusercontent.com/easyscience/reflectometry/master/data/repeating_layers.ort',\n known_hash='a5ffca9fd24f1d362266251723aec7ce9f34f123e39a38dfc4d829c758e6bf90',\n)\ndata = load(file_path)" }, { "cell_type": "markdown", @@ -203,13 +202,7 @@ "source": [ "resolution_function = PercentageFwhm(0)\n", "sample = Sample(Multilayer(superphase), rep_multilayer, Multilayer(subphase), name='Multilayer Structure')\n", - "model = Model(\n", - " sample=sample,\n", - " scale=1,\n", - " background=0,\n", - " resolution_function=resolution_function,\n", - " name='Multilayer Model'\n", - ")" + "model = Model(sample=sample, scale=1, background=0, resolution_function=resolution_function, name='Multilayer Model')" ] }, { @@ -346,4 +339,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} +} \ No newline at end of file diff --git a/docs/src/tutorials/fitting/repeating.png b/docs/docs/tutorials/fitting/repeating.png similarity index 100% rename from docs/src/tutorials/fitting/repeating.png rename to docs/docs/tutorials/fitting/repeating.png diff --git a/docs/src/tutorials/fitting/repeating.svg b/docs/docs/tutorials/fitting/repeating.svg similarity index 100% rename from docs/src/tutorials/fitting/repeating.svg rename to docs/docs/tutorials/fitting/repeating.svg diff --git a/docs/src/tutorials/fitting/repeating_layers.ort b/docs/docs/tutorials/fitting/repeating_layers.ort similarity index 100% rename from docs/src/tutorials/fitting/repeating_layers.ort rename to docs/docs/tutorials/fitting/repeating_layers.ort diff --git a/docs/src/tutorials/fitting/simple_fitting.ipynb b/docs/docs/tutorials/fitting/simple_fitting.ipynb similarity index 95% rename from docs/src/tutorials/fitting/simple_fitting.ipynb rename to docs/docs/tutorials/fitting/simple_fitting.ipynb index ea761e75..27d1e485 100644 --- a/docs/src/tutorials/fitting/simple_fitting.ipynb +++ b/docs/docs/tutorials/fitting/simple_fitting.ipynb @@ -21,6 +21,16 @@ "Note that the plot function needs installation of `plopp` seperately or installation of `easyreflectometry[dev]`" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "38b12098", + "metadata": {}, + "outputs": [], + "source": [ + "%matplotlib inline" + ] + }, { "cell_type": "code", "execution_count": null, @@ -28,25 +38,22 @@ "metadata": {}, "outputs": [], "source": [ - "%matplotlib inline\n", - "\n", "import matplotlib.pyplot as plt\n", "import pooch\n", "import refl1d\n", "import refnx\n", "\n", "import easyreflectometry\n", - "\n", + "from easyreflectometry.calculators import CalculatorFactory\n", "from easyreflectometry.data import load\n", + "from easyreflectometry.fitting import MultiFitter\n", + "from easyreflectometry.model import Model\n", + "from easyreflectometry.model import PercentageFwhm\n", + "from easyreflectometry.plot import plot\n", "from easyreflectometry.sample import Layer\n", - "from easyreflectometry.sample import Sample\n", "from easyreflectometry.sample import Material\n", "from easyreflectometry.sample import Multilayer\n", - "from easyreflectometry.model import Model\n", - "from easyreflectometry.model import PercentageFwhm\n", - "from easyreflectometry.calculators import CalculatorFactory\n", - "from easyreflectometry.fitting import MultiFitter\n", - "from easyreflectometry.plot import plot" + "from easyreflectometry.sample import Sample" ] }, { @@ -87,14 +94,7 @@ "id": "7d851064-605c-4f80-a510-197bcdbff2ea", "metadata": {}, "outputs": [], - "source": [ - "file_path = pooch.retrieve(\n", - " # URL to one of Pooch's test files\n", - " url=\"https://raw.githubusercontent.com/EasyScience/EasyReflectometryLib/master/docs/src/tutorials/fitting/example.ort\",\n", - " known_hash=\"82d0c95c069092279a799a8131ad3710335f601d9f1080754b387f42e407dfab\",\n", - ")\n", - "data = load(file_path)" - ] + "source": "file_path = pooch.retrieve(\n # Fetch test data from the easyscience/reflectometry data repository\n url='https://raw.githubusercontent.com/easyscience/reflectometry/master/data/example.ort',\n known_hash='82d0c95c069092279a799a8131ad3710335f601d9f1080754b387f42e407dfab',\n)\ndata = load(file_path)" }, { "cell_type": "markdown", @@ -311,13 +311,7 @@ "outputs": [], "source": [ "resolution_function = PercentageFwhm(0.02)\n", - "model = Model(\n", - " sample=sample,\n", - " scale=1,\n", - " background=1e-6,\n", - " resolution_function=resolution_function,\n", - " name='Film Model'\n", - ")" + "model = Model(sample=sample, scale=1, background=1e-6, resolution_function=resolution_function, name='Film Model')" ] }, { @@ -552,11 +546,7 @@ "\n", "resolution_function_refl1d = PercentageFwhm(0.02)\n", "model_refl1d = Model(\n", - " sample=sample_refl1d,\n", - " scale=1,\n", - " background=1e-6,\n", - " resolution_function=resolution_function_refl1d,\n", - " name='Film Model (Refl1D)'\n", + " sample=sample_refl1d, scale=1, background=1e-6, resolution_function=resolution_function_refl1d, name='Film Model (Refl1D)'\n", ")\n", "\n", "sio2_layer_refl1d.thickness.bounds = (15, 50)\n", @@ -598,7 +588,7 @@ "\n", "qz = data['coords']['Qz_0'].values\n", "reflectivity = data['data']['R_0'].values\n", - "uncertainty = data['data']['R_0'].variances**0.5\n", + "uncertainty = data['data']['R_0'].variances ** 0.5\n", "\n", "plt.figure(figsize=(8, 5))\n", "plt.errorbar(qz, reflectivity, yerr=uncertainty, fmt='o', markersize=3, color='black', alpha=0.6, label='Data')\n", @@ -648,4 +638,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} +} \ No newline at end of file diff --git a/docs/docs/tutorials/index.md b/docs/docs/tutorials/index.md new file mode 100644 index 00000000..7b59c331 --- /dev/null +++ b/docs/docs/tutorials/index.md @@ -0,0 +1,56 @@ +--- +icon: material/school +--- + +# :material-school: Tutorials + +This section presents a collection of **Jupyter Notebook** tutorials +that demonstrate how to use EasyReflectometry for various tasks. These +tutorials serve as self-contained, step-by-step **guides** to help users +grasp the workflow of data analysis using EasyReflectometry. + +Instructions on how to run the tutorials are provided in the +[:material-cog-box: Installation & Setup](../installation-and-setup/index.md#how-to-run-tutorials) +section of the documentation. + +## Getting Started + +- [Creating a Model](basic/model.md) – Learn how to define a + reflectometry model with sample, scale, background, and resolution + functions. +- [Defining Materials](basic/material_library.md) – Explore different + material types: `Material`, `MaterialDensity`, `MaterialSolvated`, and + `MaterialMixture`. +- [Defining Layers](basic/layer_library.md) – Understand layer types + including `Layer` and `LayerAreaPerMolecule`. +- [Creating Assemblies](basic/assemblies_library.md) – Build complex + structures with `Multilayer`, `RepeatingMultilayer`, and + `SurfactantLayer`. + +## Simulation + +These are basic simulation examples using the EasyReflectometry library. + +- [Bilayer Simulation](simulation/bilayer.ipynb) +- [Magnetism Simulation](simulation/magnetism.ipynb) +- [Resolution Functions](simulation/resolution_functions.ipynb) + +## Fitting + +These are basic fitting examples using the EasyReflectometry library. + +- [Simple Fitting](fitting/simple_fitting.ipynb) +- [Repeating Multilayer Fitting](fitting/repeating.ipynb) +- [Monolayer Fitting](fitting/monolayer.ipynb) +- [Solvated Material Fitting](fitting/material_solvated.ipynb) + +## Advanced Fitting + +These are advanced fitting examples using the EasyReflectometry library. + +- [Multi-Contrast Fitting](advancedfitting/multi_contrast.ipynb) + +## Extra + +Additional examples and supplementary material using the +EasyReflectometry library. diff --git a/docs/src/tutorials/simulation/bilayer.ipynb b/docs/docs/tutorials/simulation/bilayer.ipynb similarity index 94% rename from docs/src/tutorials/simulation/bilayer.ipynb rename to docs/docs/tutorials/simulation/bilayer.ipynb index 3d7faccd..5f37b010 100644 --- a/docs/src/tutorials/simulation/bilayer.ipynb +++ b/docs/docs/tutorials/simulation/bilayer.ipynb @@ -32,6 +32,16 @@ "First, we import the necessary modules and configure matplotlib for inline plotting." ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "7d213b99", + "metadata": {}, + "outputs": [], + "source": [ + "%matplotlib inline" + ] + }, { "cell_type": "code", "execution_count": null, @@ -39,21 +49,18 @@ "metadata": {}, "outputs": [], "source": [ - "%matplotlib inline\n", - "\n", - "import numpy as np\n", "import matplotlib.pyplot as plt\n", + "import numpy as np\n", "\n", - "import easyreflectometry\n", "from easyreflectometry.calculators import CalculatorFactory\n", + "from easyreflectometry.model import Model\n", + "from easyreflectometry.model import PercentageFwhm\n", "from easyreflectometry.sample import Bilayer\n", + "from easyreflectometry.sample import Layer\n", "from easyreflectometry.sample import LayerAreaPerMolecule\n", "from easyreflectometry.sample import Material\n", - "from easyreflectometry.sample import Layer\n", "from easyreflectometry.sample import Multilayer\n", - "from easyreflectometry.sample import Sample\n", - "from easyreflectometry.model import Model\n", - "from easyreflectometry.model import PercentageFwhm" + "from easyreflectometry.sample import Sample" ] }, { @@ -105,7 +112,7 @@ " solvent_fraction=0.3, # 30% solvent in head region\n", " area_per_molecule=48.2,\n", " roughness=3.0,\n", - " name='DPPC Head'\n", + " name='DPPC Head',\n", ")\n", "\n", "# Create a tail layer for the bilayer\n", @@ -117,7 +124,7 @@ " solvent_fraction=0.0, # No solvent in the tail region\n", " area_per_molecule=48.2,\n", " roughness=3.0,\n", - " name='DPPC Tail'\n", + " name='DPPC Tail',\n", ")" ] }, @@ -145,9 +152,9 @@ "bilayer = Bilayer(\n", " front_head_layer=head_layer,\n", " front_tail_layer=front_tail_layer,\n", - " constrain_heads=True, # Head layers share thickness and area per molecule\n", + " constrain_heads=True, # Head layers share thickness and area per molecule\n", " conformal_roughness=True, # All layers share the same roughness\n", - " name='DPPC Bilayer'\n", + " name='DPPC Bilayer',\n", ")\n", "\n", "print(bilayer)" @@ -195,7 +202,7 @@ "# Access key structural parameters\n", "print(f'Head thickness: {bilayer.front_head_layer.thickness.value:.2f} Å')\n", "print(f'Tail thickness: {bilayer.front_tail_layer.thickness.value:.2f} Å')\n", - "print(f'Area per molecule: {bilayer.front_head_layer.area_per_molecule:.2f} Ų')" + "print(f'Area per molecule: {bilayer.front_head_layer.area_per_molecule.value:.2f} Ų')" ] }, { @@ -217,14 +224,14 @@ "source": [ "# Head layers share thickness and area per molecule via constrain_heads=True,\n", "# but solvent fraction is independent and can be set separately for each side.\n", - "print(f'Front head solvent fraction: {bilayer.front_head_layer.solvent_fraction:.2f}')\n", - "print(f'Back head solvent fraction: {bilayer.back_head_layer.solvent_fraction:.2f}')\n", + "print(f'Front head solvent fraction: {bilayer.front_head_layer.solvent_fraction.value:.2f}')\n", + "print(f'Back head solvent fraction: {bilayer.back_head_layer.solvent_fraction.value:.2f}')\n", "\n", "# We can set them independently\n", "bilayer.back_head_layer.solvent_fraction = 0.5\n", - "print(f'\\nAfter setting back head solvent fraction to 0.5:')\n", - "print(f'Front head solvent fraction: {bilayer.front_head_layer.solvent_fraction:.2f}')\n", - "print(f'Back head solvent fraction: {bilayer.back_head_layer.solvent_fraction:.2f}')" + "print('\\nAfter setting back head solvent fraction to 0.5:')\n", + "print(f'Front head solvent fraction: {bilayer.front_head_layer.solvent_fraction.value:.2f}')\n", + "print(f'Back head solvent fraction: {bilayer.back_head_layer.solvent_fraction.value:.2f}')" ] }, { @@ -290,7 +297,7 @@ " Multilayer(sio2_layer, name='SiO2'),\n", " bilayer,\n", " Multilayer(d2o_subphase, name='D2O Subphase'),\n", - " name='Bilayer on Si/SiO2'\n", + " name='Bilayer on Si/SiO2',\n", ")\n", "\n", "print(sample)" @@ -314,13 +321,7 @@ "outputs": [], "source": [ "# Create the model\n", - "model = Model(\n", - " sample=sample,\n", - " scale=1.0,\n", - " background=1e-7,\n", - " resolution_function=PercentageFwhm(5),\n", - " name='Bilayer Model'\n", - ")\n", + "model = Model(sample=sample, scale=1.0, background=1e-7, resolution_function=PercentageFwhm(5), name='Bilayer Model')\n", "\n", "# Set up the calculator\n", "interface = CalculatorFactory()\n", @@ -451,7 +452,7 @@ "\n", "# Now set asymmetric hydration (common in supported bilayers)\n", "bilayer.front_head_layer.solvent_fraction = 0.1 # Substrate side - less hydrated\n", - "bilayer.back_head_layer.solvent_fraction = 0.4 # Solution side - more hydrated\n", + "bilayer.back_head_layer.solvent_fraction = 0.4 # Solution side - more hydrated\n", "\n", "# Get asymmetric SLD profile\n", "z_asym, sld_asym = model.interface().sld_profile(model.unique_name)\n", @@ -502,7 +503,7 @@ " solvent_fraction=0.3,\n", " area_per_molecule=48.2,\n", " roughness=3.0,\n", - " name='DPPC Head H2O'\n", + " name='DPPC Head H2O',\n", ")\n", "\n", "# Create tail layer for H2O contrast (same deuterated lipid, different solvent)\n", @@ -513,7 +514,7 @@ " solvent_fraction=0.0,\n", " area_per_molecule=48.2,\n", " roughness=3.0,\n", - " name='DPPC Tail H2O'\n", + " name='DPPC Tail H2O',\n", ")\n", "\n", "# Create H2O bilayer\n", @@ -522,7 +523,7 @@ " front_tail_layer=tail_layer_h2o,\n", " constrain_heads=True,\n", " conformal_roughness=True,\n", - " name='DPPC Bilayer H2O'\n", + " name='DPPC Bilayer H2O',\n", ")" ] }, @@ -568,16 +569,12 @@ " Multilayer(sio2_layer, name='SiO2'),\n", " bilayer_h2o,\n", " Multilayer(h2o_subphase, name='H2O Subphase'),\n", - " name='Bilayer on Si/SiO2 in H2O'\n", + " name='Bilayer on Si/SiO2 in H2O',\n", ")\n", "\n", "# Create model for H2O contrast\n", "model_h2o = Model(\n", - " sample=sample_h2o,\n", - " scale=1.0,\n", - " background=1e-7,\n", - " resolution_function=PercentageFwhm(5),\n", - " name='Bilayer Model H2O'\n", + " sample=sample_h2o, scale=1.0, background=1e-7, resolution_function=PercentageFwhm(5), name='Bilayer Model H2O'\n", ")\n", "model_h2o.interface = interface" ] @@ -688,7 +685,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.12" + "version": "3.12.11" } }, "nbformat": 4, diff --git a/docs/src/tutorials/simulation/magnetism.ipynb b/docs/docs/tutorials/simulation/magnetism.ipynb similarity index 87% rename from docs/src/tutorials/simulation/magnetism.ipynb rename to docs/docs/tutorials/simulation/magnetism.ipynb index 1f35fdf1..8efdb9e4 100644 --- a/docs/src/tutorials/simulation/magnetism.ipynb +++ b/docs/docs/tutorials/simulation/magnetism.ipynb @@ -20,6 +20,16 @@ "First configure matplotlib to place figures in notebook and import needed modules" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "644e53e3", + "metadata": {}, + "outputs": [], + "source": [ + "%matplotlib inline" + ] + }, { "cell_type": "code", "execution_count": null, @@ -27,24 +37,21 @@ "metadata": {}, "outputs": [], "source": [ - "%matplotlib inline\n", - "\n", "import matplotlib.pyplot as plt\n", "import numpy as np\n", - "import scipp as sc\n", "import refl1d\n", "import refl1d.names\n", + "import scipp as sc\n", "\n", "import easyreflectometry\n", - "\n", "from easyreflectometry.calculators import CalculatorFactory\n", + "from easyreflectometry.calculators.refl1d.wrapper import _get_polarized_probe\n", "from easyreflectometry.model import Model\n", "from easyreflectometry.model import PercentageFwhm\n", "from easyreflectometry.sample import Layer\n", "from easyreflectometry.sample import Material\n", "from easyreflectometry.sample import Multilayer\n", - "from easyreflectometry.sample import Sample\n", - "from easyreflectometry.calculators.refl1d.wrapper import _get_polarized_probe" + "from easyreflectometry.sample import Sample" ] }, { @@ -150,18 +157,13 @@ "metadata": {}, "outputs": [], "source": [ - "refl1d_sld_4 = refl1d.names.SLD(name=\"Sld 4\", rho=4.0, irho=0)\n", - "refl1d_sld_8 = refl1d.names.SLD(name=\"Sld 8\", rho=8.0, irho=0)\n", - "refl1d_vacuum = refl1d.names.SLD(name=\"Vacuum\", rho=0, irho=0)\n", - "refl1d_si = refl1d.names.SLD(name=\"Si\", rho=2.047, irho=0)\n", + "refl1d_sld_4 = refl1d.names.SLD(name='Sld 4', rho=4.0, irho=0)\n", + "refl1d_sld_8 = refl1d.names.SLD(name='Sld 8', rho=8.0, irho=0)\n", + "refl1d_vacuum = refl1d.names.SLD(name='Vacuum', rho=0, irho=0)\n", + "refl1d_si = refl1d.names.SLD(name='Si', rho=2.047, irho=0)\n", "\n", "# Refl1d model is inverted as compared to EasyReflectometry, so the order of the layers is reversed\n", - "refl1d_sample = (\n", - " refl1d_si(0, 0) | \n", - " refl1d_sld_8(150, 0) |\n", - " refl1d_sld_4(100, 0) | \n", - " refl1d_vacuum(0, 0)\n", - ") " + "refl1d_sample = refl1d_si(0, 0) | refl1d_sld_8(150, 0) | refl1d_sld_4(100, 0) | refl1d_vacuum(0, 0)" ] }, { @@ -206,6 +208,7 @@ " num=1000,\n", ")\n", "\n", + "\n", "def plot_apply_makeup():\n", " ax = plt.gca()\n", " ax.set_xlim([-0.01, 0.35])\n", @@ -233,11 +236,11 @@ "source": [ "# Refl1d\n", "probe = refl1d.names.QProbe(\n", - " Q=model_coords,\n", - " dQ=np.zeros(len(model_coords)),\n", - " intensity=1,\n", - " background=0,\n", - " )\n", + " Q=model_coords,\n", + " dQ=np.zeros(len(model_coords)),\n", + " intensity=1,\n", + " background=0,\n", + ")\n", "experiment = refl1d.names.Experiment(probe=probe, sample=refl1d_sample)\n", "model_data_no_magnetism_ref1d_raw = experiment.reflectivity()[1]\n", "\n", @@ -253,7 +256,9 @@ " model_coords,\n", " model.unique_name,\n", ")\n", - "plt.plot(model_coords, model_data_no_magnetism_ref1d_easy, 'r-', label=f'EasyReflectometry ({model_interface.name})', linewidth=2)\n", + "plt.plot(\n", + " model_coords, model_data_no_magnetism_ref1d_easy, 'r-', label=f'EasyReflectometry ({model_interface.name})', linewidth=2\n", + ")\n", "\n", "plot_apply_makeup()" ] @@ -290,8 +295,12 @@ "model.interface = interface\n", "model_interface = model.interface()\n", "model_interface.include_magnetism = True\n", - "model_interface._wrapper.update_layer(list(model_interface._wrapper.storage['layer'].keys())[1], magnetism_rhoM=10, magnetism_thetaM=70)\n", - "model_interface._wrapper.update_layer(list(model_interface._wrapper.storage['layer'].keys())[2], magnetism_rhoM=5, magnetism_thetaM=175)\n", + "model_interface._wrapper.update_layer(\n", + " list(model_interface._wrapper.storage['layer'].keys())[1], magnetism_rhoM=10, magnetism_thetaM=70\n", + ")\n", + "model_interface._wrapper.update_layer(\n", + " list(model_interface._wrapper.storage['layer'].keys())[2], magnetism_rhoM=5, magnetism_thetaM=175\n", + ")\n", "model_data_magnetism_layer_1 = model.interface().reflectity_profile(\n", " model_coords,\n", " model.unique_name,\n", @@ -327,21 +336,19 @@ "source": [ "# Refl1d model is inverted as compared to EasyReflectometry, so the order of the layers is reversed\n", "refl1d_sample = (\n", - " refl1d_si(0, 0) | \n", - " refl1d_sld_8(150, 0, magnetism=refl1d.names.Magnetism(rhoM=5, thetaM=175)) |\n", - " refl1d_sld_4(100, 0, magnetism=refl1d.names.Magnetism(rhoM=10, thetaM=70)) | \n", - " refl1d_vacuum(0, 0)\n", - ") \n", + " refl1d_si(0, 0)\n", + " | refl1d_sld_8(150, 0, magnetism=refl1d.names.Magnetism(rhoM=5, thetaM=175))\n", + " | refl1d_sld_4(100, 0, magnetism=refl1d.names.Magnetism(rhoM=10, thetaM=70))\n", + " | refl1d_vacuum(0, 0)\n", + ")\n", "model_name = model.unique_name\n", "storage = {'model': {model_name: {}}}\n", "storage['model'][model_name]['scale'] = 10.0\n", "storage['model'][model_name]['bkg'] = 20.0\n", "\n", "polarized_probe = _get_polarized_probe(\n", - " q_array=model_coords,\n", - " dq_array=np.zeros(len(model_coords)),\n", - " model_name=model_name,\n", - " storage=storage)\n", + " q_array=model_coords, dq_array=np.zeros(len(model_coords)), model_name=model_name, storage=storage\n", + ")\n", "\n", "experiment = refl1d.names.Experiment(probe=polarized_probe, sample=refl1d_sample)\n", "model_data_magnetism_ref1d = experiment.reflectivity()[0][1]\n", @@ -352,8 +359,12 @@ "model.interface = interface\n", "model_interface = model.interface()\n", "model_interface.include_magnetism = True\n", - "model_interface._wrapper.update_layer(list(model_interface._wrapper.storage['layer'].keys())[1], magnetism_rhoM=10, magnetism_thetaM=70)\n", - "model_interface._wrapper.update_layer(list(model_interface._wrapper.storage['layer'].keys())[2], magnetism_rhoM=5, magnetism_thetaM=175)\n", + "model_interface._wrapper.update_layer(\n", + " list(model_interface._wrapper.storage['layer'].keys())[1], magnetism_rhoM=10, magnetism_thetaM=70\n", + ")\n", + "model_interface._wrapper.update_layer(\n", + " list(model_interface._wrapper.storage['layer'].keys())[2], magnetism_rhoM=5, magnetism_thetaM=175\n", + ")\n", "model_data_magnetism_easy = model.interface().reflectity_profile(\n", " model_coords,\n", " model.unique_name,\n", @@ -406,15 +417,14 @@ "metadata": {}, "outputs": [], "source": [ - "# The magnetism is set to 8. \n", + "# The magnetism is set to 8.\n", "# This would double (pp) and cancel out (mm) the magnitude of the reflectivity oscillations when its angle is set to 90.\n", "# This would give the strongest spin-flipping (pm and mp) when its angle is set to 0.\n", - "# However we set the angle to 45, so the reflectivity oscillations are not doubled or cancelled out, and the spin-flipping is not maximized.\n", + "# However we set the angle to 45, so the reflectivity oscillations are not doubled or cancelled out,\n", + "# and the spin-flipping is not maximized.\n", "refl1d_sample = (\n", - " refl1d_si(0, 0) | \n", - " refl1d_sld_8(150, 0, magnetism=refl1d.names.Magnetism(rhoM=8, thetaM=45)) |\n", - " refl1d_vacuum(0, 0)\n", - ") \n", + " refl1d_si(0, 0) | refl1d_sld_8(150, 0, magnetism=refl1d.names.Magnetism(rhoM=8, thetaM=45)) | refl1d_vacuum(0, 0)\n", + ")\n", "\n", "model_name = model.unique_name\n", "storage = {'model': {model_name: {}}}\n", @@ -422,13 +432,10 @@ "storage['model'][model_name]['bkg'] = 0.0\n", "\n", "polarized_probe = _get_polarized_probe(\n", - " q_array=model_coords,\n", - " dq_array=np.zeros(len(model_coords)),\n", - " model_name=model_name,\n", - " storage=storage,\n", - " all_polarizations=True)\n", + " q_array=model_coords, dq_array=np.zeros(len(model_coords)), model_name=model_name, storage=storage, all_polarizations=True\n", + ")\n", "\n", - "experiment = refl1d.names.Experiment(probe=polarized_probe, sample=refl1d_sample)\n" + "experiment = refl1d.names.Experiment(probe=polarized_probe, sample=refl1d_sample)" ] }, { diff --git a/docs/src/tutorials/simulation/mod_pointwise_two_layer_sample_dq-0.0.ort b/docs/docs/tutorials/simulation/mod_pointwise_two_layer_sample_dq-0.0.ort similarity index 100% rename from docs/src/tutorials/simulation/mod_pointwise_two_layer_sample_dq-0.0.ort rename to docs/docs/tutorials/simulation/mod_pointwise_two_layer_sample_dq-0.0.ort diff --git a/docs/src/tutorials/simulation/mod_pointwise_two_layer_sample_dq-1.0.ort b/docs/docs/tutorials/simulation/mod_pointwise_two_layer_sample_dq-1.0.ort similarity index 100% rename from docs/src/tutorials/simulation/mod_pointwise_two_layer_sample_dq-1.0.ort rename to docs/docs/tutorials/simulation/mod_pointwise_two_layer_sample_dq-1.0.ort diff --git a/docs/src/tutorials/simulation/mod_pointwise_two_layer_sample_dq-10.0.ort b/docs/docs/tutorials/simulation/mod_pointwise_two_layer_sample_dq-10.0.ort similarity index 100% rename from docs/src/tutorials/simulation/mod_pointwise_two_layer_sample_dq-10.0.ort rename to docs/docs/tutorials/simulation/mod_pointwise_two_layer_sample_dq-10.0.ort diff --git a/docs/src/tutorials/simulation/resolution_functions.ipynb b/docs/docs/tutorials/simulation/resolution_functions.ipynb similarity index 84% rename from docs/src/tutorials/simulation/resolution_functions.ipynb rename to docs/docs/tutorials/simulation/resolution_functions.ipynb index 4fc42ad2..d46a76cb 100644 --- a/docs/src/tutorials/simulation/resolution_functions.ipynb +++ b/docs/docs/tutorials/simulation/resolution_functions.ipynb @@ -24,6 +24,16 @@ "First configure matplotlib to place figures in notebook and import needed modules. Note that the plot function needs installation of `plopp` seperately or installation of `easyreflectometry[dev]`" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "deb2db0a", + "metadata": {}, + "outputs": [], + "source": [ + "%matplotlib inline" + ] + }, { "cell_type": "code", "execution_count": null, @@ -31,27 +41,24 @@ "metadata": {}, "outputs": [], "source": [ - "%matplotlib inline\n", - "\n", "import matplotlib.pyplot as plt\n", "import numpy as np\n", - "import scipp as sc\n", - "import refnx\n", "import pooch\n", + "import refnx\n", + "import scipp as sc\n", "\n", "import easyreflectometry\n", - "\n", "from easyreflectometry.calculators import CalculatorFactory\n", "from easyreflectometry.data import load\n", - "from easyreflectometry.model import Model\n", "from easyreflectometry.model import LinearSpline\n", + "from easyreflectometry.model import Model\n", "from easyreflectometry.model import PercentageFwhm\n", "from easyreflectometry.model import Pointwise\n", + "from easyreflectometry.plot import plot\n", "from easyreflectometry.sample import Layer\n", "from easyreflectometry.sample import Material\n", "from easyreflectometry.sample import Multilayer\n", - "from easyreflectometry.sample import Sample\n", - "from easyreflectometry.plot import plot" + "from easyreflectometry.sample import Sample" ] }, { @@ -94,27 +101,7 @@ "id": "609174e5-1371-412d-a29f-cb05bfe36df0", "metadata": {}, "outputs": [], - "source": [ - "file_path_0 = pooch.retrieve(\n", - " # URL to one of Pooch's test files\n", - " url=\"https://raw.githubusercontent.com/EasyScience/EasyReflectometryLib/master/docs/src/tutorials/simulation/mod_pointwise_two_layer_sample_dq-0.0.ort\",\n", - " known_hash=\"f8a3e7007b83f0de4e2c761134e7d1c55027f0099528bd56f746b50349369f50\",\n", - ")\n", - "file_path_1 = pooch.retrieve(\n", - " # URL to one of Pooch's test files\n", - " url=\"https://raw.githubusercontent.com/EasyScience/EasyReflectometryLib/master/docs/src/tutorials/simulation/mod_pointwise_two_layer_sample_dq-1.0.ort\",\n", - " known_hash=\"9d81a512cbe45f923806ad307e476b27535614b2e08a2bf0f4559ab608a34f7a\",\n", - ")\n", - "file_path_10 = pooch.retrieve(\n", - " # URL to one of Pooch's test files\n", - " url=\"https://raw.githubusercontent.com/EasyScience/EasyReflectometryLib/master/docs/src/tutorials/simulation/mod_pointwise_two_layer_sample_dq-10.0.ort\",\n", - " known_hash=\"991395c0b6a91bf60c12d234c645143dcac1cab929944fc4e452020d44b787ad\",\n", - ")\n", - "dict_reference = {}\n", - "dict_reference['0'] = load(file_path_0)\n", - "dict_reference['1'] = load(file_path_1)\n", - "dict_reference['10'] = load(file_path_10)" - ] + "source": "file_path_0 = pooch.retrieve(\n # Fetch test data from the easyscience/reflectometry data repository\n url='https://raw.githubusercontent.com/easyscience/reflectometry/master/data/mod_pointwise_two_layer_sample_dq-0.0.ort',\n known_hash='f8a3e7007b83f0de4e2c761134e7d1c55027f0099528bd56f746b50349369f50',\n)\nfile_path_1 = pooch.retrieve(\n # Fetch test data from the easyscience/reflectometry data repository\n url='https://raw.githubusercontent.com/easyscience/reflectometry/master/data/mod_pointwise_two_layer_sample_dq-1.0.ort',\n known_hash='9d81a512cbe45f923806ad307e476b27535614b2e08a2bf0f4559ab608a34f7a',\n)\nfile_path_10 = pooch.retrieve(\n # Fetch test data from the easyscience/reflectometry data repository\n url='https://raw.githubusercontent.com/easyscience/reflectometry/master/data/mod_pointwise_two_layer_sample_dq-10.0.ort',\n known_hash='991395c0b6a91bf60c12d234c645143dcac1cab929944fc4e452020d44b787ad',\n)\ndict_reference = {}\ndict_reference['0'] = load(file_path_0)\ndict_reference['1'] = load(file_path_1)\ndict_reference['10'] = load(file_path_10)" }, { "cell_type": "code", @@ -328,7 +315,7 @@ " model.unique_name,\n", " )\n", " plt.plot(model_coords, model_data, 'k-', label=f'Resolution: {key}%')\n", - " plt.plot(reference_coords, reference_data, 'rx', label=f'Reference')\n", + " plt.plot(reference_coords, reference_data, 'rx', label='Reference')\n", " ax = plt.gca()\n", " ax.set_xlim([-0.01, 0.45])\n", " ax.set_ylim([1e-10, 2.5])\n", @@ -353,7 +340,10 @@ "## Afterthoughts\n", "As a last task we will compare the reflectivity determined using a percentage resolution function and a point-wise function.\n", "We should recall that the \"experimental\" data was generated using `Refnx`.\n", - "By comparing the reflectivities determined using a resolution function with a FWHM of 1.0% and the point-wise FHWN constructed from data in a `.ort` file it is apparent that this reference data also was constructed using a resolution function of 1.0%." + "\n", + "The `Pointwise` resolution function derives a per-point resolution width directly from the data: it takes the `[Qz, R, sQz]` triple, where `sQz` is the variance of `Qz` (`Qz_0.variances`), and uses `sqrt(sQz)` as the resolution width at each measured point, linearly interpolating onto the requested `q` (exactly as `LinearSpline` does for explicitly provided widths).\n", + "\n", + "By comparing the reflectivities determined using a resolution function with a FWHM of 1.0% and the point-wise width constructed from the data in a `.ort` file, it is apparent that this reference data also was constructed using a resolution function of 1.0%." ] }, { @@ -377,14 +367,14 @@ " model_coords,\n", " model.unique_name,\n", ")\n", - "plt.plot(model_coords, model_data, 'k-', label=f'Variable', linewidth=5)\n", + "plt.plot(model_coords, model_data, 'k-', label='Variable', linewidth=5)\n", "\n", "model.resolution_function = PercentageFwhm(1.0)\n", "model_data = model.interface().reflectity_profile(\n", " model_coords,\n", " model.unique_name,\n", ")\n", - "plt.plot(model_coords, model_data, 'r-', label=f'Percentage')\n", + "plt.plot(model_coords, model_data, 'r-', label='Percentage')\n", "\n", "ax = plt.gca()\n", "ax.set_xlim([-0.01, 0.45])\n", @@ -416,17 +406,21 @@ " model_coords,\n", " model.unique_name,\n", ")\n", - "plt.plot(model_coords, model_data, 'k-', label=f'Variable', linewidth=5)\n", + "plt.plot(model_coords, model_data, 'k-', label='Variable', linewidth=5)\n", + "\n", + "# The Pointwise resolution is built from the data triple [Qz, R, sQz],\n", + "# where sQz is the variance of Qz. The width sqrt(sQz) is interpolated onto q.\n", "data_points = []\n", - "data_points.append(reference_coords) # Qz\n", - "data_points.append(reference_data) # R\n", - "data_points.append(reference_variances) # sQz\n", + "data_points.append(reference_coords) # Qz\n", + "data_points.append(reference_data) # R\n", + "data_points.append(reference_variances) # sQz (variance of Qz)\n", "model.resolution_function = Pointwise(q_data_points=data_points)\n", "model_data = model.interface().reflectity_profile(\n", " model_coords,\n", " model.unique_name,\n", ")\n", - "plt.plot(model_coords, model_data, 'r-', label=f'Pointwise')\n", + "plt.plot(model_coords, model_data, 'r-', label='Pointwise')\n", + "plt.plot(reference_coords, reference_data, 'bx', label='Reference', markersize=4)\n", "\n", "ax = plt.gca()\n", "ax.set_xlim([-0.01, 0.45])\n", @@ -458,4 +452,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} +} \ No newline at end of file diff --git a/docs/src/tutorials/simulation/two_layers.png b/docs/docs/tutorials/simulation/two_layers.png similarity index 100% rename from docs/src/tutorials/simulation/two_layers.png rename to docs/docs/tutorials/simulation/two_layers.png diff --git a/docs/src/tutorials/simulation/two_layers.svg b/docs/docs/tutorials/simulation/two_layers.svg similarity index 100% rename from docs/src/tutorials/simulation/two_layers.svg rename to docs/docs/tutorials/simulation/two_layers.svg diff --git a/docs/docs/user-guide/index.md b/docs/docs/user-guide/index.md new file mode 100644 index 00000000..fbdec8b4 --- /dev/null +++ b/docs/docs/user-guide/index.md @@ -0,0 +1,211 @@ +--- +icon: material/book-open-variant +--- + +# :material-book-open-variant: User Guide + +This section provides an overview of the **core concepts**, **key +parameters** and **workflow steps** required for using EasyReflectometry +effectively. + +## Glossary + +The following serves to clarify what we mean by the terms we use in this +project. + +### Sample + +A sample is an ideal representation of the full physical setup. This +includes the layer(s) under investigation, the surrounding superphase, +and the subphase. + +### Calculator + +A calculator is the physics engine which calculates the reflectivity +curve from our inputted sample parameters. We rely on third party +software to provide the necessary calculators. Different calculators +might have different capabilities and limitations. + +Currently, EasyReflectometry can offer two different calculation +engines: + +- [**refnx**](https://refnx.readthedocs.io/) +- [**Refl1D**](https://refl1d.readthedocs.io/en/latest/) + +And we are working to add more, in particular +[**BornAgain**](https://www.bornagainproject.org) and +[**GenX**](https://aglavic.github.io/genx/doc/). + +### Model + +A model combines a sample and calculator. The model is also responsible +for including instrumental effects such as background, scale, and +resolution. + +### Assemblies + +Assemblies are collections of layers that are used to represent a +specific physical setup. Examples include: + +- **Multilayer** – A series of layers grouped as a single object +- **RepeatingMultilayer** – A multilayer with a fixed number of repeats +- **SurfactantLayer** – A layer defined by area per molecule and + chemical formula +- **GradientLayer** – A layer with a graded scattering length density + profile + +### Elements + +Elements are the building blocks that are required to construct a +sample. + +**Layers** are basic elements used to represent a single layer of +material with a thickness and a roughness: + +- `Layer` – Standard layer with material, thickness, and roughness +- `LayerAreaPerMolecule` – Layer defined by area per molecule and + chemical formula + +**Materials** are the most basic elements and are used to represent a +material with given physical properties: + +- `Material` – Simple material defined by SLD (real and imaginary) +- `MaterialDensity` – Material defined by mass density and chemical + formula +- `MaterialSolvated` – Material mixed with a solvent in a given ratio +- `MaterialMixture` – Mixture of two materials + +### Fitting + +Fitting helpers and objective functions. The `MultiFitter` supports +several objective modes for handling reflectometry data during fitting, +especially when measured variances are non-positive. + +## Getting Started + +To use EasyReflectometry in a project: + +```python +import easyreflectometry +from easyreflectometry.sample import Material, Layer +from easyreflectometry.model import Model +from easyreflectometry.fitting import MultiFitter +from easyreflectometry.plot import plot + +# Define your Material +material = Material(...) + +# Create a Layer +layer = Layer(material=material, ...) + +# Make a Sample out of the Layer +sample = Sample(layer, ...) + +# Define a Model of the experiment +model = Model( + sample=sample, + scale=1, + background=1e-6, + ... +) + +# Set parameter bounds for fit +... + +# Perform the fit and plot +fitter = MultiFitter(model) +analysed = fitter.fit(data) + +plot(analysed) +``` + +Details of specific usage of EasyReflectometry can be found in the +[Tutorials](../tutorials/index.md). + +## Objective Functions and Non-Positive Variance Handling + +`MultiFitter` supports several objective modes for handling +reflectometry data during fitting, especially when measured variances +are non-positive. + +The default objective is `hybrid`. This uses ordinary weighted least +squares for points with positive variance and applies a Mighell-style +substitution only to points whose variance is non-positive. The older +`legacy_mask` mode drops non-positive-variance points before fitting. +The `mighell` mode applies the Mighell transform to every point. + +### Mighell Objective + +The full `mighell` objective follows the algebraic form of the +$\chi^2_\gamma$ statistic described by Mighell for Poisson-distributed +count data: + +$$ +\chi^2_\gamma = +\sum_i \frac{[n_i + \min(n_i, 1) - m_i]^2}{n_i + 1} +$$ + +where $n_i$ are observed counts and $m_i$ are model values. + +In EasyReflectometry this is implemented as a weighted least-squares +problem. For each observed value $y_i$ the fitted target is shifted to + +$$ +y_{\mathrm{eff},i} = y_i + \min(y_i, 1) +$$ + +and the effective uncertainty is + +$$ +\sigma_i = \sqrt{y_i + 1} +$$ + +so the minimized objective is + +$$ +\sum_i \left(\frac{y_{\mathrm{eff},i} - f_i}{\sigma_i}\right)^2 = +\sum_i \frac{[y_i + \min(y_i, 1) - f_i]^2}{y_i + 1} +$$ + +### Scope and Interpretation + +Mighell's statistic was derived for Poisson-distributed count data. In +reflectometry workflows, the fitted values are usually normalized +reflectivities or intensities rather than raw counts. They may already +have been processed, scaled, background-corrected, or otherwise +transformed before they reach the fitter. + +This distinction matters when interpreting the result. The full +`mighell` objective is not only a reweighting of residuals; it also +changes the fitted target from $y$ to $y + \min(y, 1)$. For values +between zero and one, this can substantially increase the target value. +A fit can therefore have a good Mighell objective value while looking +poorer against the originally plotted reflectivity curve, or while +having a worse classical chi-square. + +For reflectometry data, `hybrid` is generally the recommended +compromise: it preserves ordinary weighted least-squares behavior where +positive variances are available, while still allowing +non-positive-variance points to contribute through the Mighell-style +substitution. + +### Objective Modes + +- **`hybrid`** (default): Use standard weighted least squares for points + with positive variance and apply the Mighell substitution only where + variance is non-positive. +- **`mighell`**: Apply the Mighell transform to all points. The reported + objective chi-square is evaluated in transformed objective space and + should not be interpreted as a classical chi-square against the + original reflectivity values. +- **`legacy_mask`**: Remove non-positive-variance points before fitting + and use standard weighted least squares for the remaining points. +- **`auto`**: Alias for `hybrid`. + +### Fit Metrics + +The fitter exposes both objective-space and classical fit metrics after +fitting. `objective_chi2` and `objective_reduced_chi` describe the +minimized objective value, while `classical_chi2` and +`classical_reduced_chi` describe the fit quality against the original +reflectivity values (with non-positive-variance points excluded). diff --git a/docs/includes/abbreviations.md b/docs/includes/abbreviations.md new file mode 100644 index 00000000..682f16ff --- /dev/null +++ b/docs/includes/abbreviations.md @@ -0,0 +1,15 @@ + + +*[CIF]: Crystallographic Information File. +*[curl]: Command-line tool for transferring data with URLs. +*[GitHub]: A web-based platform for version control and collaboration. +*[Google Colab]: Cloud service that allows you to run Jupyter Notebooks in the cloud. +*[IUCr]: International Union of Crystallography. +*[Jupyter Notebook]: An open-source web application that allows you to create and share documents that contain live code, equations, visualizations, and narrative text. +*[JupyterLab]: Web-based interactive development environment for notebooks, code, and data. +*[pip]: Package installer for Python. +*[PyPI]: The Python Package Index is a repository of software for the Python programming language. +*[Conda]: Conda is a cross-platform, language-agnostic binary package manager. +*[Pixi]: A modern package manager for Windows, macOS, and Linux. + + diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml new file mode 100644 index 00000000..1cd19cde --- /dev/null +++ b/docs/mkdocs.yml @@ -0,0 +1,218 @@ +# Project information +site_name: EasyReflectometry Library +site_url: https://easyscience.github.io/reflectometry-lib + +# Repository +repo_url: https://github.com/easyscience/reflectometry-lib +edit_uri: edit/develop/docs/ + +# Copyright +copyright: © 2021-2026 EasyReflectometry + +# Sets the theme and theme-specific configuration +theme: + name: material + custom_dir: overrides + features: + #- content.action.edit # Temporary disable edit button (until decided on which branch to use and where to host the notebooks) + #- content.action.view + - content.code.annotate + - content.code.copy # Auto generated button to copy a code block's content + - content.tooltips + - navigation.footer + - navigation.indexes + #- navigation.instant # Instant loading, but it causes issues with rendering equations + #- navigation.sections + - navigation.top # Back-to-top button + - navigation.tracking # Anchor tracking + - search.highlight + - search.share + - search.suggest + - toc.follow + palette: + # Palette toggle for light mode + - media: '(prefers-color-scheme: light)' + scheme: default + primary: custom + toggle: + icon: fontawesome/solid/sun + name: Switch to dark mode + # Palette toggle for dark mode + - media: '(prefers-color-scheme: dark)' + scheme: slate + primary: custom + toggle: + icon: fontawesome/solid/moon + name: Switch to light mode + font: + text: Mulish + code: Roboto Mono + icon: + edit: material/file-edit-outline + favicon: assets/images/favicon.png + logo_dark_mode: assets/images/logo_dark.svg + logo_light_mode: assets/images/logo_light.svg + +# A set of key-value pairs, where the values can be any valid YAML +# construct, that will be passed to the template +extra: + generator: false # Disable `Made with Material for MkDocs` (bottom left) + social: # Extra icons in the bottom right corner + - icon: easyscience # File: overrides/.icons/easyscience.svg + link: https://easyscience.org + name: EasyScience Framework Webpage + - icon: easyreflectometry # File: overrides/.icons/easyreflectometry.svg + link: https://easyscience.github.io/reflectometry + name: EasyReflectometry Main Webpage + - icon: fontawesome/brands/github # Name as in Font Awesome + link: https://github.com/easyscience/reflectometry-lib + name: EasyReflectometry Library Source Code on GitHub + # Set custom variables to be used in Markdown and HTML files + vars: + ci_branch: !ENV CI_BRANCH + github_repository: !ENV GITHUB_REPOSITORY + release_version: !ENV RELEASE_VERSION + docs_version: !ENV DOCS_VERSION + notebooks_dir: !ENV NOTEBOOKS_DIR + # Renders a version selector in the header + version: + provider: mike + +# Customization to be included by the theme +extra_css: + - assets/stylesheets/extra.css + +extra_javascript: + - assets/javascripts/extra.js + # MathJax for rendering mathematical expressions + - assets/javascripts/mathjax.js # Custom MathJax config to ensure compatibility with mkdocs-jupyter + - https://unpkg.com/mathjax@3/es5/tex-mml-chtml.js # Official MathJax CDN + +# A list of extensions beyond the ones that MkDocs uses by default (meta, toc, tables, and fenced_code) +markdown_extensions: + - abbr + - admonition + - attr_list + - def_list + - footnotes + - pymdownx.arithmatex: # rendering of equations and integrates with MathJax or KaTeX + generic: true + - pymdownx.blocks.caption + - pymdownx.details + - pymdownx.emoji: + emoji_index: !!python/name:material.extensions.emoji.twemoji + emoji_generator: !!python/name:material.extensions.emoji.to_svg + options: + custom_icons: + - docs/overrides/.icons + - pymdownx.highlight: # whether highlighting should be carried out during build time by Pygments + use_pygments: true + pygments_lang_class: true + - pymdownx.snippets: + auto_append: + - docs/includes/abbreviations.md + - pymdownx.superfences: # whether highlighting should be carried out during build time by Pygments + custom_fences: + - name: mermaid + class: mermaid + format: !!python/name:pymdownx.superfences.fence_code_format + - pymdownx.tabbed: # enables content tabs + alternate_style: true + - pymdownx.tasklist: + custom_checkbox: true + - toc: + toc_depth: 3 + +# A list of plugins (with optional configuration settings) to use when building the site +plugins: + - autorefs + - inline-svg + - markdownextradata # Plugin that injects the mkdocs.yml extra variables into the Markdown template + - mike # Plugin that makes it easy to deploy multiple versions of the docs + - mkdocs-jupyter: + include: ['*.ipynb'] # Default: ['*.py', '*.ipynb'] + execute: false # Do not execute notebooks during build. They are expected to be pre-executed. + allow_errors: false + include_source: true + include_requirejs: true # Required for Plotly + #custom_mathjax_url: 'https://unpkg.com/mathjax@3/es5/tex-mml-chtml.js' # See 'extra_javascript' above + ignore_h1_titles: true # Use titles defined in the nav section below + remove_tag_config: + remove_input_tags: + - hide-in-docs + - mkdocstrings: + handlers: + python: + paths: ['src'] # Change 'src' to your actual sources directory + options: + annotations_path: source + docstring_style: numpy + group_by_category: true + members: true + members_order: source + heading_level: 1 + show_inheritance: true + show_root_heading: true + show_root_full_path: false + show_submodules: true + show_source: true + - search + +# Determines additional directories to watch when running mkdocs serve +watch: + - includes + - overrides + - ../src + +# Exclude files and folders from the global navigation +not_in_nav: | + index.md + +# Format and layout of the global navigation for the site +nav: + - Introduction: + - Introduction: introduction/index.md + - Installation & Setup: + - Installation & Setup: installation-and-setup/index.md + - User Guide: + - User Guide: user-guide/index.md + - Tutorials: + - Tutorials: tutorials/index.md + - Getting Started: + - Creating a Model: tutorials/basic/model.md + - Defining Materials: tutorials/basic/material_library.md + - Defining Layers: tutorials/basic/layer_library.md + - Creating Assemblies: tutorials/basic/assemblies_library.md + - Simulation: + - Bilayer Simulation: tutorials/simulation/bilayer.ipynb + - Magnetism Simulation: tutorials/simulation/magnetism.ipynb + - Resolution Functions: tutorials/simulation/resolution_functions.ipynb + - Fitting: + - Simple Fitting: tutorials/fitting/simple_fitting.ipynb + - Repeating Multilayer Fitting: tutorials/fitting/repeating.ipynb + - Monolayer Fitting: tutorials/fitting/monolayer.ipynb + - Solvated Material Fitting: tutorials/fitting/material_solvated.ipynb + - Advanced Fitting: + - Multi-Contrast Fitting: tutorials/advancedfitting/multi_contrast.ipynb + - API Reference: + - API Reference: api-reference/index.md + - Model: api-reference/model.md + - Sample: api-reference/sample.md + - Project: api-reference/project.md + - Fitting: api-reference/fitting.md + - Assemblies: + - Multilayer: api-reference/assemblies/multilayer.md + - Repeating Multilayer: api-reference/assemblies/repeating_multilayer.md + - Surfactant Layer: api-reference/assemblies/surfactant_layer.md + - Gradient Layer: api-reference/assemblies/gradient_layer.md + - Elements: + - Layers: + - Layer: api-reference/elements/layer.md + - Layer Area Per Molecule: + api-reference/elements/layer_area_per_molecule.md + - Materials: + - Material: api-reference/elements/material.md + - Material Density: api-reference/elements/material_density.md + - Material Mixture: api-reference/elements/material_mixture.md + - Material Solvated: api-reference/elements/material_solvated.md + - Data: api-reference/data.md diff --git a/docs/overrides/.icons/app.svg b/docs/overrides/.icons/app.svg new file mode 100644 index 00000000..b4fdd4f3 --- /dev/null +++ b/docs/overrides/.icons/app.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/docs/overrides/.icons/easyreflectometry.svg b/docs/overrides/.icons/easyreflectometry.svg new file mode 100644 index 00000000..8a2e3087 --- /dev/null +++ b/docs/overrides/.icons/easyreflectometry.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + Logo + + + + + \ No newline at end of file diff --git a/docs/overrides/.icons/easyscience.svg b/docs/overrides/.icons/easyscience.svg new file mode 100644 index 00000000..fb514912 --- /dev/null +++ b/docs/overrides/.icons/easyscience.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/overrides/.icons/google-colab.svg b/docs/overrides/.icons/google-colab.svg new file mode 100644 index 00000000..9cd9d1b0 --- /dev/null +++ b/docs/overrides/.icons/google-colab.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/docs/overrides/main.html b/docs/overrides/main.html new file mode 100644 index 00000000..2e146827 --- /dev/null +++ b/docs/overrides/main.html @@ -0,0 +1,39 @@ +{% extends "base.html" %} + +{% block content %} + +{% if page.nb_url %} + {# Parse notebook path/URL #} + {% set parts = page.nb_url.split('/') %} + {% set tutorial_name = parts[-2] %} + {% set filename = parts[-1] %} + + {# Colab url #} + {% set base_colab_url = "https://colab.research.google.com/github/" %} + {% set colab_url = + base_colab_url ~ config.extra.vars.github_repository ~ + "/blob/gh-pages/" ~ config.extra.vars.docs_version ~ + "/tutorials/" ~ tutorial_name ~ "/" ~ filename + %} + + {# Download link: relative to the current page #} + {% set file_url = filename %} + + {# Open in Colab (absolute GitHub URL; works anywhere) #} + + {% include ".icons/google-colab.svg" %} + + + {# Download: use a RELATIVE link to the file next to this page #} + + {% include ".icons/material/download.svg" %} + +{% endif %} + +{{ super() }} +{% endblock content %} diff --git a/docs/overrides/partials/logo.html b/docs/overrides/partials/logo.html new file mode 100644 index 00000000..78fa69ca --- /dev/null +++ b/docs/overrides/partials/logo.html @@ -0,0 +1,15 @@ +{% if ( config.theme.logo_light_mode and config.theme.logo_dark_mode ) %} +logo +logo +{% elif config.theme.logo %} +logo +{% else %} {% set icon = config.theme.icon.logo or "material/library" %} {% +include ".icons/" ~ icon ~ ".svg" %} {% endif %} diff --git a/docs/src/conf.py b/docs/src/conf.py index 36235949..127779c9 100644 --- a/docs/src/conf.py +++ b/docs/src/conf.py @@ -20,9 +20,10 @@ import datetime import os import sys -import toml from pathlib import Path +import toml + import easyreflectometry sys.path.insert(0, os.path.abspath('../src')) @@ -54,7 +55,7 @@ 'sphinx_autodoc_typehints', 'sphinx_copybutton', 'nbsphinx', - 'myst_parser' + 'myst_parser', ] # Add any paths that contain templates here, relative to this directory. @@ -71,8 +72,8 @@ # General information about the project. project = 'EasyReflectometry' -copyright = f"{datetime.date.today().year}, EasyReflectometry" -author = "EasyReflectometry" +copyright = f'{datetime.date.today().year}, EasyReflectometry' +author = 'EasyReflectometry' # The version info for the project you're documenting, acts as replacement # for |version| and |release|, also used in various other places throughout @@ -105,7 +106,7 @@ autoclass_content = 'class' autodoc_member_order = 'bysource' autodoc_typehints = 'signature' -autodoc_class_signature = "separated" +autodoc_class_signature = 'separated' # -- Options for HTML output ------------------------------------------- @@ -116,7 +117,7 @@ html_logo = os.path.join('_static', 'logo.png') html_favicon = os.path.join('_static', 'favicon.ico') html_theme_options = { -# 'logo_only': True, + # 'logo_only': True, 'navigation_with_keys': True } html_baseurl = 'https://docs.easyreflectometry.org' @@ -132,7 +133,7 @@ html_static_path = ['_static'] nbsphinx_execute_arguments = [ - "--Session.metadata=scipp_docs_build=True", + '--Session.metadata=scipp_docs_build=True', ] @@ -148,15 +149,12 @@ # The paper size ('letterpaper' or 'a4paper'). # # 'papersize': 'letterpaper', - # The font size ('10pt', '11pt' or '12pt'). # # 'pointsize': '10pt', - # Additional stuff for the LaTeX preamble. # # 'preamble': '', - # Latex figure (float) alignment # # 'figure_align': 'htbp', @@ -166,9 +164,7 @@ # (source start file, target name, title, author, documentclass # [howto, manual, or own class]). latex_documents = [ - (master_doc, 'EasyReflectometry.tex', - 'EasyReflectometry Documentation', - 'Andrew R. McCluskey', 'manual'), + (master_doc, 'EasyReflectometry.tex', 'EasyReflectometry Documentation', 'Andrew R. McCluskey', 'manual'), ] @@ -176,11 +172,7 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [ - (master_doc, 'EasyReflectometry', - 'EasyReflectometry Documentation', - [author], 1) -] +man_pages = [(master_doc, 'EasyReflectometry', 'EasyReflectometry Documentation', [author], 1)] # -- Options for Texinfo output ---------------------------------------- @@ -189,11 +181,13 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - (master_doc, 'EasyReflectometry', - 'EasyReflectometry Documentation', - author, - 'EasyReflectometry', - 'One line description of project.', - 'Miscellaneous'), + ( + master_doc, + 'EasyReflectometry', + 'EasyReflectometry Documentation', + author, + 'EasyReflectometry', + 'One line description of project.', + 'Miscellaneous', + ), ] - diff --git a/docs/src/tutorials/advancedfitting/advancedfitting.rst b/docs/src/tutorials/advancedfitting/advancedfitting.rst deleted file mode 100644 index 8c12ba54..00000000 --- a/docs/src/tutorials/advancedfitting/advancedfitting.rst +++ /dev/null @@ -1,9 +0,0 @@ -Advanced Fitting -================ - -These are advanced fitting examples using the :py:mod:`easyreflectometry` library, to get an understanding of the possibilities. - -.. toctree:: - :maxdepth: 1 - - multi_contrast.ipynb \ No newline at end of file diff --git a/docs/src/tutorials/basic/assemblies_library.rst b/docs/src/tutorials/basic/assemblies_library.rst deleted file mode 100644 index d72201a9..00000000 --- a/docs/src/tutorials/basic/assemblies_library.rst +++ /dev/null @@ -1,249 +0,0 @@ -Creating multilayers and surfactant layers -=========================================== - -:py:mod:`easyreflectometry` is designed to be used with a broad range of different assemblies. -Assemblies are collective layers behaving as a single object, for example, a multilayer or a surfactant layer. -These assemblies offer flexibility for the user and enable more powerful analysis by making chemical and physical constraints available with limited code. -In this page, we will document the assemblies that are available with simple examples of the constructors that exist. -Full API documentation is also available for the :py:mod:`easyreflectometry.sample.assemblies` module. - -:py:class:`Multilayer` ----------------------- - -This assembly should be used for a series of layers that should be thought of as a single object. -For example, in the `simple fitting tutorial`_ this assembly type is used to combine the silicon and silicon dioxide layer that as formed into a single object. -All of the separate layers in these objects will be fitted individually, i.e. there is no constraints present, however, there is some cognative benefit to grouping layers together. - -To create a :py:class:`Multilayer` object, we use the following construction. - -.. code-block:: python - - from easyreflectometry.sample import Layer - from easyreflectometry.sample import Material - from easyreflectometry.sample import Multilayer - - si = Material( - sld=2.07, - isld=0, - name='Si' - ) - sio2 = Material( - sld=3.47, - isld=0, - name='SiO2' - ) - si_layer = Layer( - material=si, - thickness=0, - roughness=0, - name='Si layer' - ) - sio2_layer = Layer( - material=sio2, - thickness=30, - roughness=3, - name='SiO2 layer' - ) - - subphase = Multilayer( - layers=[si_layer, sio2_layer], - name='Si/SiO2 subphase' - ) - -This will create a :py:class:`Multilayer` object named :code:`subphase` which we can use in some :py:class:`Structure` for our analysis. - -:py:class:`RepeatingMultilayer` -------------------------------- - -The :py:class:`RepeatingMultilayer` assembly type is an extension of the :py:class:`Multilayer` for the analysis of systems with a multilayer that has some number of repeats. -This assembly type imposes some constraints, specifically that all of the repeats have the exact same structure (i.e. thicknesses, roughnesses, and scattering length densities), -which brings with it some computational saving as the reflectometry coefficients only needs to be calculated once for this structure and propagated for the correct number of repeats. -There is a `tutorial`_ that discusses the utilisation of this assembly type for a nickel-titanium multilayer system. - -The creation of a :py:class:`RepeatingMultilayer` object is very similar to that for the :py:class:`Multilayer`, with the addition of a number of repetitions. - -.. code-block:: python - - from easyreflectometry.sample import Layer - from easyreflectometry.sample import Material - from easyreflectometry.sample import RepeatingMultilayer - - ti = Material( - sld=-1.9493, - isld=0, - name='Ti' - ) - ni = Material( - sld=9.4245, - isld=0, - name='Ni' - ) - ti_layer = Layer( - material=ti, - thickness=40, - roughness=0, - name='Ti Layer' - ) - ni_layer = Layer( - material=ni, - thickness=70, - roughness=0, - name='Ni Layer' - ) - ni_ti = RepeatingMultilayer( - layers=[ti_layer, ni_layer], - repetitions=10, - name='Ni/Ti Multilayer' - ) - -The number of repeats is a parameter that can be varied in the optimisation process, however given this is a value that depends on the synthesis of the sample this is unlikely to be necessary. - -:py:class:`SurfactantLayer` ---------------------------- - -The :py:class:`SurfactantLayer` assembly type allows for the creating of a model to describe a monolayer of surfactant at some interface. -Using this assembly, we can define our surfactant in terms of the chemistry of the head and tail groups and be confident that the constraints are present to ensure the number density if kept constant. -The `surfactant monolayer tutorial`_ looks in detail at the definition of the scattering length density in the :py:class:`SurfactantLayer`. -However, it is founded on the chemical formula for the head and tail group and the area per molecule that these groups occupy. - -The creation of a :py:class:`SurfactantLayer` object is shown below. - -.. code-block:: python - - from easyreflectometry.sample import LayerAreaPerMolecule - from easyreflectometry.sample import Material - from easyreflectometry.sample import SurfactantLayer - - area_per_molecule = 48 - roughness = 3.3 - subphase = Material( - sld=6.36, - isld=0.0, - name='D2O' - ) - superphase = Material( - sld=0.0, - isld=0.0, - name='Air' - ) - tail_layer = LayerAreaPerMolecule( - molecular_formula='C30D64', - thickness=16.0, - solvent=superphase, - solvent_fraction=0.0, - area_per_molecule=area_per_molecule, - roughness=roughness - ) - head_layer = LayerAreaPerMolecule( - molecular_formula='C10H18NO8P', - thickness=10.0, - solvent=subphase, - solvent_fraction=0.2, - area_per_molecule=area_per_molecule, - roughness=roughness - ) - dspc = SurfactantLayer( - tail_layer=tail_layer, - head_layer=head_layer - ) - -On creation, the area per molecule and roughness above both the head and tail layers can be constrained to be the same. -These constraints can be addded by setting :code:`dppc.constrain_area_per_molecule = True` or :code:`dppc.conformal_roughness = True`. -Furthermore, as shown in the `surfactant monolayer tutorial`_ the conformal roughness can be defined by that of the subphase. - -The use of the :py:class:`SurfactantLayer` in multiple contrast data analysis is shown in a `multiple contrast tutorial`_. - -:py:class:`Bilayer` -------------------- - -The :py:class:`Bilayer` assembly type represents a phospholipid bilayer at an interface. -It consists of two surfactant layers where one is inverted, creating the structure: - -.. code-block:: text - - Head₁ - Tail₁ - Tail₂ - Head₂ - -This assembly is particularly useful for studying supported lipid bilayers and membrane systems. -The bilayer comes pre-populated with physically meaningful constraints: - -- Both tail layers share the same structural parameters (thickness, area per molecule) -- Head layers share thickness and area per molecule (different hydration/solvent fraction allowed) -- A single roughness parameter applies to all interfaces (conformal roughness) - -These default constraints can be enabled or disabled as needed for specific analyses. - -The creation of a :py:class:`Bilayer` object is shown below. - -.. code-block:: python - - from easyreflectometry.sample import Bilayer - from easyreflectometry.sample import LayerAreaPerMolecule - from easyreflectometry.sample import Material - - # Create materials for solvents - d2o = Material(sld=6.36, isld=0.0, name='D2O') - air = Material(sld=0.0, isld=0.0, name='Air') - - # Create head layer (used for front, back head will be auto-created with constraints) - head = LayerAreaPerMolecule( - molecular_formula='C10H18NO8P', - thickness=10.0, - solvent=d2o, - solvent_fraction=0.3, - area_per_molecule=48.2, - roughness=3.0, - name='DPPC Head' - ) - - # Create tail layer (both tail positions will share these parameters) - tail = LayerAreaPerMolecule( - molecular_formula='C32D64', - thickness=16.0, - solvent=air, - solvent_fraction=0.0, - area_per_molecule=48.2, - roughness=3.0, - name='DPPC Tail' - ) - - # Create bilayer with default constraints - bilayer = Bilayer( - front_head_layer=head, - tail_layer=tail, - constrain_heads=True, - conformal_roughness=True, - name='DPPC Bilayer' - ) - -The head layers can have different solvent fractions (hydration) even when constrained, -enabling the modeling of asymmetric bilayers at interfaces where the two sides of the -bilayer may have different solvent exposure. - -The constraints can be controlled at runtime: - -.. code-block:: python - - # Disable head constraints to allow different head layer structures - bilayer.constrain_heads = False - - # Disable conformal roughness to allow different roughness values - bilayer.conformal_roughness = False - -Individual layers can be accessed via properties: - -.. code-block:: python - - # Access the four layers - bilayer.front_head_layer # First head layer - bilayer.front_tail_layer # First tail layer - bilayer.back_tail_layer # Second tail layer (constrained to front tail) - bilayer.back_head_layer # Second head layer - -For more detailed examples including simulation and parameter access, see the `bilayer tutorial`_. - - -.. _`simple fitting tutorial`: ../tutorials/simple_fitting.html -.. _`tutorial`: ../tutorials/repeating.html -.. _`surfactant monolayer tutorial`: ../tutorials/monolayer.html -.. _`multiple contrast tutorial`: ../tutorials/multi_contrast.html -.. _`bilayer tutorial`: ../tutorials/simulation/bilayer.html \ No newline at end of file diff --git a/docs/src/tutorials/basic/basic.rst b/docs/src/tutorials/basic/basic.rst deleted file mode 100644 index bcb94f58..00000000 --- a/docs/src/tutorials/basic/basic.rst +++ /dev/null @@ -1,13 +0,0 @@ -Basic -===== - -The :py:mod:`easyreflectometry` package is focused on making easy to use functionality for specific modelling approaches. -In order to achieve this, we have a sample library with different functionality. - -.. toctree:: - :maxdepth: 1 - - model - material_library - layer_library - assemblies_library \ No newline at end of file diff --git a/docs/src/tutorials/basic/layer_library.rst b/docs/src/tutorials/basic/layer_library.rst deleted file mode 100644 index 76c75ad4..00000000 --- a/docs/src/tutorials/basic/layer_library.rst +++ /dev/null @@ -1,84 +0,0 @@ -Defining Layers -=============== - -Similar to a range of different `materials`_, there are a few different ways that a layer can be defined in :py:mod:`easyreflectometry`. - -:py:class:`Layer` ------------------ - -The :py:class:`Layer` is the simplest possible type of layer, taking a :py:class:`Material` and two floats associated with the thickness and upper (that is closer to the source of the incident radiation) roughness. -So we construct a :py:class:`Layer` as follows for a 100 Å thick layer of boron with a roughness of 10 Å. - -.. code-block:: python - - from easyreflectometry.sample import Material - from easyreflectometry.sample import Layer - - boron = Material( - sld=6.908, - isld=-0.278, - name='Boron' - ) - boron_layer = Layer( - material=boron, - thickness=100, - roughness=10, - name='Boron Layer' - ) - -This type of layer is used extensively in the `tutorials`_ - -To create a semi-infinite layer one needs to set the thickness to 0 and the roughness to 0. - -.. code-block:: python - - from easyreflectometry.sample import Material - from easyreflectometry.sample import Layer - - si = Material( - sld=2.07, - isld=0, - name='Si' - ) - semi_infinite_layer = Layer( - material=si, - thickness=0, - roughness=0, - name='Si layer' - ) - -:py:class:`LayerAreaPerMolecule` --------------------------------- - -The :py:class:`LayerAreaPerMolecule` layer type is the fundation of the :py:class:`SurfactantLayer` assemblies type (further information on this can be found in the `assemblies library`_). -The purpose of the :py:class:`LayerAreaPerMolecule` is to allow a layer to be defined in terms of the chemical formula of the material and the area per molecule of the layer. -The area per molecule is a common description of surface density in the surfactant monolayer and bilayer community. - -We can construct a 10 Å thick :py:class:`LayerAreaPerMolecule` of phosphatidylcholine, with an area per molecule of 48 Å squared and a roughness of 3 Å that has 20 % solvent surface coverage with D2O using the following. - -.. code-block:: python - - from easyreflectometry.sample import Material - from easyreflectometry.sample import LayerAreaPerMolecule - - d2o = Material( - sld=6.36, - isld=0, - name='D2O' - ) - molecular_formula = 'C10H18NO8P' - pc = LayerAreaPerMolecule( - molecular_formula=molecular_formula, - thickness=10, - solvent=d2o, - solvent_fraction=.2, - area_per_molecule=48, - roughness=3, - name='PC Layer' - ) - -It is expected that the typical user will not interface directly with the :py:class:`LayerAreaPerMolecule` assembly type, but instead the :py:class:`SurfactantLayer` `assemblies library`_ will be used instead. - -.. _`materials`: ./material_library.html -.. _`tutorials`: ../tutorials/tutorials.html -.. _`assemblies library`: ./assemblies_library.html \ No newline at end of file diff --git a/docs/src/tutorials/basic/material_library.rst b/docs/src/tutorials/basic/material_library.rst deleted file mode 100644 index 2aaf0911..00000000 --- a/docs/src/tutorials/basic/material_library.rst +++ /dev/null @@ -1,87 +0,0 @@ -Defining materials -================== - -In order to support a wide range of applications (and to build complex `assemblies`_) there are a few different types of material that can be utilised in :py:mod:`easyreflectometry`. -These can include constraints or enable the user to define the material based on chemical or physical properties. -Full API documentation for the :py:mod:`easyreflectometry.sample.elements.material` module is also available, but here we will give some simple uses for them. - -:py:class:`Material` --------------------- - -The simplest type of material that is available is the :py:class:`Material`. -This allows the user to define a single type of material, with a real and imaginary component to the scattering length density. -The construction of a :py:class:`Material` is achieved as shown below. - -.. code-block:: python - - from easyreflectometry.sample import Material - - boron = Material( - sld=6.908, - isld=-0.278, - name='Boron' - ) - -The above object will have the properties of :py:attr:`sld` and :py:attr:`isld`, which will have values of :code:`6.908 1/angstrom^2` and :code:`-0.278 1/angstrom^2` respectively. -As is shown in the `tutorials`_, a material can be used to construct a :py:class:`Layer` from which `slab models`_ are created. - -:py:class:`MaterialDensity` ---------------------------- - -In addition to defining a material by its scattering length density, it may be useful to define a material by the mass density and chemical formula. -This is possible with the :py:class:`MaterialDensity` material type, which uses the scattering length and atomic mass from the chemical formula and the density to determine the scattering length density. -It is then possible to vary the density, which defines the scattering length density in turn. -The :py:class:`MaterialDensity` material can be create as follows. - -.. code-block:: python - - from easyreflectometry.sample import MaterialDensity - - chemical_structure = 'SiO2' - si = MaterialDensity( - chemical_structure=chemical_structure, - density=2.65, - name='SiO2 Material' - ) - -The density should be in units of grams per cubic centimeter and the scattering length is calculated from :code:`'SiO2'`. - -:py:class:`MaterialSolvated` ----------------------------- - -Sometimes it is desirable to have a layer that consists of a material and a solvent in some ratio. -An example of this is shown in the `solvation tutorial`_, where a polymer film solvated with D2O is modelled. -To produce a material that is described by such a mixture, there is :py:class:`MaterialSolvated`. -This is constructed from two constituent :py:class:`Materials` and the fractional amount of the material in the solvent. -So to produce a :py:class:`MaterialSolvated` that is 20 % D2O in a polymer, the following is used. - -.. code-block:: python - - from easyreflectometry.sample import Material - from easyreflectometry.sample import MaterialSolvated - - polymer = Material( - sld=2., - isld=0., - name='Polymer' - ) - d2o = Material( - sld=6.36, - isld=0, - name='D2O' - ) - - solvated_polymer = MaterialSolvated( - material=polymer, - solvent=d2o, - solvent_fraction=0.2, - name='Solvated Polymer' - ) - -For the :py:attr:`solvated_polymer` object, the :py:attr:`sld` will be :code:`2.872 1/angstrom^2` (the weighted average of the two scattering length densities). -The :py:class:`MaterialSolvated` includes a constraint such that if the value of either constituent scattering length densities (both real and imaginary components) or the fraction changes, then the resulting material :py:attr:`sld` and :py:attr:`isld` will change appropriately. - -.. _`assemblies`: ./assemblies_library.html -.. _`tutorials`: ../tutorials/tutorials.html -.. _`slab models`: https://www.reflectometry.org/isis_school/3_reflectometry_slab_models/the_slab_model.html -.. _`solvation tutorial`: ../tutorials/solvation.html \ No newline at end of file diff --git a/docs/src/tutorials/basic/model.rst b/docs/src/tutorials/basic/model.rst deleted file mode 100644 index 47134397..00000000 --- a/docs/src/tutorials/basic/model.rst +++ /dev/null @@ -1,80 +0,0 @@ -Creating a model -================ - -The main component of an experiment in :py:mod:`easyreflectometry` is the :py:class:`Model`. -This is a description of the :py:class:`Sample` and the environment in which the experiment is performed. -The :py:class:`Model` is used to calculate the reflectivity of the :py:class:`Sample` at a given set of angles (Q-points). -The :py:func:`resolution_functions` are used to quantify the experimental uncertainties in wavelength and angle, allowing the :py:class:`Model` to accurately describe the data. - -:py:class:`Model` ------------------ - -A :py:class:`Model` instance contains a :py:class:`Sample` and variables describing experimental settings. -To be able to compute reflectivities it is also necessary to have a :py:class:`Calculator` (interface). - -.. code-block:: python - - from easyreflectometry.calculators import CalculatorFactory - from easyreflectometry.model import Model - from easyreflectometry.sample import Sample - - default_sample = Sample() - model = Model( - sample=default_sample, - scale=1.0, - background=1e-6 - ) - - interface = CalculatorFactory() - model.interface = interface - -This will create a :py:class:`Model` instance with the :py:attr:`default_sample` and the environment variables :py:attr:`scale` factor set to 1.0 and a :py:attr:`background` of 1e-6. -Following the :py:attr:`interface` is set to the default calculator that is :py:class:`Refnx`. - - -:py:mod:`resolution_functions` ------------------------------- -A resolution function enables the :py:mod:`easyreflectometry` model to incorporate the experimental uncertainties in wavelength and incident angle into the model. -In its essence the resolution function controls the smearing to apply when determing the reflectivtiy at a given Q-point. -For a given Q-point the smearing to apply is given as a weighted average of the neighboring Q-point, which weigths are by a normal distribution. -This normal distribution is then defined by a Q-point dependent Full Width at the Half Maximum (FWHM) that is given by the resolution function. - -:py:class:`PercentageFwhm` -Often we rely on a resolution function that has a simple functional dependecy of the Q-point. -By this is understood that the applied smearing in an Q-point has a FWHM that is simply a percentage of the value of the Q-point. - -.. code-block:: python - - from easyreflectometry.model import Model - from easyreflectometry.model import PercentageFwhm - - resolution_function = PercentageFwhm(1.1) - - m = Model( - resolution_function=resolution_function - ) - -This will create a :py:class:`Model` instance where the resolution function is defined as 1.1% of the Q-point value, which again is the FWHM for the smearing. - - -:py:func:`LinearSpline` -Alternatively the FWHM value might be determined and declared directly for each measured Q-point. -When this is the case the provided Q-points and the corresponding FWHM values can be used to declare a linear spline function -and thereby enable a determination of the reflectivity at an arbitrary point within the provided range of discrete Q-points. - -.. code-block:: python - - from easyreflectometry.model import Model - from easyreflectometry.model import LinearSpline - - m = Model() - - resolution_function = LinearSpline( - q_data_points=[0.01, 0.2, 0.31], - fwhm_values=[0.001, 0.043, 0.026] - ) - - m.resolution_function = resolution_function - -This will create a :py:class:`Model` instance where the resolution function defining the FWHM is determined from a linear interpolation. -In the present case the provided data Q-points are (`[0.01, 0.2, 0.31]`) and the corresponding FWHM function values are (`[0.001, 0.043, 0.026]`). diff --git a/docs/src/tutorials/extra/extra.rst b/docs/src/tutorials/extra/extra.rst deleted file mode 100644 index 9d68b33a..00000000 --- a/docs/src/tutorials/extra/extra.rst +++ /dev/null @@ -1,7 +0,0 @@ -Extra -===== - -These are extra examples using the :py:mod:`easyreflectometry` library, to get an understanding of the possibilities. - -.. toctree:: - :maxdepth: 1 \ No newline at end of file diff --git a/docs/src/tutorials/fitting/fitting.rst b/docs/src/tutorials/fitting/fitting.rst deleted file mode 100644 index 52b6c14f..00000000 --- a/docs/src/tutorials/fitting/fitting.rst +++ /dev/null @@ -1,12 +0,0 @@ -Fitting -======= - -These are basic fitting examples using the :py:mod:`easyreflectometry` library, to get an understanding of the possibilities. - -.. toctree:: - :maxdepth: 1 - - simple_fitting.ipynb - repeating.ipynb - monolayer.ipynb - material_solvated.ipynb \ No newline at end of file diff --git a/docs/src/tutorials/simulation/simulation.rst b/docs/src/tutorials/simulation/simulation.rst deleted file mode 100644 index 5cf9ced4..00000000 --- a/docs/src/tutorials/simulation/simulation.rst +++ /dev/null @@ -1,11 +0,0 @@ -Simulation -========== - -These are basic simulation examples using the :py:mod:`easyreflectometry` library, to get an understanding of the possibilities. - -.. toctree:: - :maxdepth: 1 - - bilayer.ipynb - magnetism.ipynb - resolution_functions.ipynb \ No newline at end of file diff --git a/docs/src/tutorials/tutorials.rst b/docs/src/tutorials/tutorials.rst deleted file mode 100644 index b73e6666..00000000 --- a/docs/src/tutorials/tutorials.rst +++ /dev/null @@ -1,49 +0,0 @@ -========== -How to use -========== - -Dictionary -========== -The following serves to clarify what we mean by the terms we use in this project. - -Sample ------- -A sample is an ideal representation of a the full physical setup. -This includes the layer(s) under investigation, the surrounding superphase, and the subphase. - -Calculator ----------- -A calculator is the physics engine which calculates the reflectivity curve from our inputted sample parameters. -We rely on third party software to provide the necessary calculators. -Different calculators might have different capabilities and limitations. - -Model ------ -A model combines a sample and calculator. -The model is also responsible for including instrumental effects such as background, scale, and resolution. - - -Calculators & Optimisation -========================== - -:py:mod:`easyreflectometry` is built on the :py:mod:`easyscience` framework which facilities the use of a range of different reflectometry calculation engines and optimiser solutions. -Currently, :py:mod:`easyreflectometry` can offer two different calculation engines, namely: - -* `refnx`_ -* `Refl1D`_ - -And we are working to add more, in particular `bornagain`_ and `GenX`_. - -.. _`refnx`: https://refnx.readthedocs.io/ -.. _`Refl1D`: https://refl1d.readthedocs.io/en/latest/ -.. _`BornAgain`: https://www.bornagainproject.org -.. _`GenX`: https://aglavic.github.io/genx/doc/ - -.. toctree:: - :maxdepth: 2 - - basic/basic - simulation/simulation - fitting/fitting - advancedfitting/advancedfitting - extra/extra diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..1b7139b3 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,65 @@ +{ + "name": "reflectometry-lib", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "devDependencies": { + "prettier": "^3.8.3", + "prettier-plugin-toml": "^2.0.6" + } + }, + "node_modules/@taplo/core": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@taplo/core/-/core-0.2.0.tgz", + "integrity": "sha512-r8bl54Zj1In3QLkiW/ex694bVzpPJ9EhwqT9xkcUVODnVUGirdB1JTsmiIv0o1uwqZiwhi8xNnTOQBRQCpizrQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@taplo/lib": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@taplo/lib/-/lib-0.5.0.tgz", + "integrity": "sha512-+xIqpQXJco3T+VGaTTwmhxLa51qpkQxCjRwezjFZgr+l21ExlywJFcDfTrNmL6lG6tqb0h8GyJKO3UPGPtSCWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@taplo/core": "^0.2.0" + } + }, + "node_modules/prettier": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", + "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-plugin-toml": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/prettier-plugin-toml/-/prettier-plugin-toml-2.0.6.tgz", + "integrity": "sha512-12N/wBuHa9jd/KVy9pRP20NMKxQfQLMseQCt66lIbLaPLItvGUcSIryE1eZZMJ7loSws6Ig3M2Elc2EreNh76w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@taplo/lib": "^0.5.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + }, + "peerDependencies": { + "prettier": "^3.0.3" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..80724263 --- /dev/null +++ b/package.json @@ -0,0 +1,6 @@ +{ + "devDependencies": { + "prettier": "^3.8.3", + "prettier-plugin-toml": "^2.0.6" + } +} diff --git a/pixi.lock b/pixi.lock new file mode 100644 index 00000000..16dba96f --- /dev/null +++ b/pixi.lock @@ -0,0 +1,13575 @@ +version: 7 +platforms: +- name: linux-64 + virtual-packages: + - __unix=0=0 + - __linux=4.18 + - __glibc=2.28 + - __archspec=0=x86_64 +- name: p1 + subdir: osx-arm64 + virtual-packages: + - __osx=14.0 + - __unix=0=0 + - __archspec=0=m1 +- name: win-64 + virtual-packages: + - __win=10.0 + - __archspec=0=x86_64 +environments: + default: + channels: + - url: https://conda.anaconda.org/nodefaults/ + - url: https://conda.anaconda.org/conda-forge/ + indexes: + - https://pypi.org/simple + packages: + linux-64: + - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/argon2-cffi-bindings-25.1.0-py313h07c4f96_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/backports.zstd-1.4.0-py313h18e8e13_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/bcrypt-5.0.0-py313h843e2db_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.2.0-py313hf159716_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_9.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/c-ares-1.34.6-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cffi-2.0.0-py313hf46b229_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cryptography-48.0.0-py313heb322e3_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/debugpy-1.8.20-py313h5d5ffb9_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/icu-78.3-h33c6efd_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.3-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.22.2-ha1258a1_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45.1-default_hbd61a6d_102.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libabseil-20260107.1-cxx17_h7b12aa8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlicommon-1.2.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlidec-1.2.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlienc-1.2.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20250104-pl5321h7949ede_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libev-4.33-hd590300_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.8.0-hecca717_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h3435931_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-he0feb66_18.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.2.0-h69a702a_18.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-he0feb66_18.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.3-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libnghttp2-1.68.1-h877daf1_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsodium-1.0.21-h280c20c_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.53.1-h0c1763c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h934c35e_18.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.42-h5347b49_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libuv-1.51.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.2-h25fd6f3_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.3-py313h3dea7bd_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/msgspec-0.21.1-py313h07c4f96_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.6-hdb14827_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/nodejs-25.8.2-he4ff34a_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.2-h35e630c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/psutil-7.2.2-py313h54dd161_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pydantic-core-2.46.4-py313h843e2db_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pynacl-1.6.2-py313h5008379_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.13.13-h6add32d_100_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.3-py313h3dea7bd_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pyzmq-27.1.0-py312hda471dd_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.3-h853b02a_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/rpds-py-0.30.0-py313h843e2db_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ruff-0.15.12-h994f30f_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h366c992_103.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tornado-6.5.5-py313h07c4f96_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h280c20c_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zeromq-4.3.5-h41580af_10.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/annotated-types-0.7.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.13.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/argon2-cffi-25.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/arrow-1.4.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.18.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.14.3-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.3.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-with-css-6.3.0-hbca2aae_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.4.22-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cached-property-1.5.2-hd8ed1ab_1.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/cached_property-1.5.2-pyha770c72_1.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2026.4.22-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.7-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.3-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/copier-9.15.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.13.13-py313hd8ed1ab_100.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/decorator-5.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/defusedxml-0.7.1-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/dunamai-1.26.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/eval-type-backport-0.2.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/eval_type_backport-0.2.2-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fqdn-1.5.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/funcy-2.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h11-0.16.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/httpcore-1.0.9-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/httpx-0.28.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.13-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.8.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib_resources-7.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/invoke-3.0.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.2.0-pyha191276_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.13.0-pyh53cf698_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython_pygments_lexers-1.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/isoduration-20.11.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jedi-0.19.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-ansible-filters-1.3.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/json5-0.14.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonpointer-3.1.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.26.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.9.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-with-format-nongpl-4.26.0-hcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter-lsp-2.3.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.8.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.9.1-pyhc90fa1f_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_events-0.12.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.18.2-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server_terminals-0.5.4-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.5.7-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_pygments-0.3.0-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_server-2.28.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/lark-1.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mistune-3.2.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbclient-0.10.4-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbconvert-core-7.17.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbformat-5.10.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/notebook-shim-0.2.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/overrides-7.7.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.2-pyhc364b38_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pandocfilters-1.5.0-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/paramiko-4.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.7-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pathspec-1.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pexpect-4.9.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pixi-kernel-0.7.1-pyhbbac1ac_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.9.6-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/plumbum-1.10.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prometheus_client-0.25.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.51-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prompt_toolkit-3.0.51-hd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ptyprocess-0.7.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pydantic-2.13.4-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.21.2-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.13.13-h4df99d1_100.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-json-logger-3.2.1-pyh332efcf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2026.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pywin32-on-windows-0.1.0-pyh1179c8e_3.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/questionary-2.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.37.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.33.1-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/returns-0.27.0-pyhc364b38_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3339-validator-0.1.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3986-validator-0.1.1-pyh9f0ad1d_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3987-syntax-1.1.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/send2trash-2.1.0-pyha191276_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-82.0.1-pyh332efcf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sniffio-1.3.1-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.8.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyhc90fa1f_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tinycss2-1.4.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.15.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.15.0-h396c80c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-inspection-0.4.2-pyhcf101f3_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_utils-0.1.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/uri-template-1.3.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.6.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.7.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/webcolors-25.10.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/webencodings-0.5.1-pyhd8ed1ab_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/websocket-client-1.9.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.1-pyhcf101f3_0.conda + - pypi: ./ + - pypi: https://files.pythonhosted.org/packages/00/bb/90ba423612b6aa0adccc6b1874bcd4a9b44b660c0c16f346611e00f64ac3/backrefs-7.0-py313-none-any.whl + - pypi: https://files.pythonhosted.org/packages/01/7c/fa07d3da2b6253eb8474be16eab2eadf670460e364ccc895ca7ff388ee30/oscrypto-1.3.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/07/c7/deb8c5e604404dbf10a3808a858946ca3547692ff6316b698945bb72177e/python_socketio-5.16.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0c/d6/1d5c60cc17bbdf37c1552d9c03862fc6d32c5836732a0415b2d637edc2d0/pypdf-6.10.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0d/1f/d398de1612f7a611e22d743280339c9af4903675635e41be3370091c704b/arviz_stats-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0d/fe/6bea5c9162869c5beba5d9c8abbed835ec85bf1ec1fba05a3822325c45f3/build-1.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/11/8c/c9138d881c79aa0ea9ed83cbd58d5ca75624378b38cee225dcf5c42cc91f/griffelib-2.0.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/12/c5/cbb1ffefb20a93d3f0e1fdcda699fb84976210d411b008f97f48bf6ce27e/pandas-3.0.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/12/c9/6869a1dcf4aaf309b9543ec070be3ec3adebee7c9bec9af8c230494134b9/interrogate-1.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/13/95/cf3f7fe4910cf0365fa8ea0c731f4b8a624d97cd76ea777913ac8d0868e2/mkdocs_jupyter-0.26.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/16/5a/736dd2f4535dbf3bf26523f9158c011389ef88dd06ec2eef67fd744f1c7b/jupytext-1.19.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/19/d4/225027a913621a879b429a043674aa35220e6ce67785acad4f7bd0c4ff33/xarray_einstats-0.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1c/13/bc43591a54dd38ac5c19e7e849f0311d879737a0d07e032e5be79849a5fb/reportlab-4.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1c/59/964ecb8008722d27d8a835baea81f56a91cea8e097b3be992bc6ccde6367/versioningit-3.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/20/16/e777eadfa0c0305878c36fae1d5e6db474fbb15dae202b9ec378809dfb4d/nbstripout-0.9.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/20/7a/1c6e3562dfd8950adbb11ffbc65d21e7c89d01a6e4f137fa981056de25c5/gitpython-3.1.50-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/21/0e/8459ca4413e1a21a06c97d134bfaf18adfd27cea068813dc0faae06cbf00/cssselect2-0.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/28/88/4789719fbbe166d12d345b3ac66b96105f10001b16e00a9765ba29261a21/nbqa-1.9.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/28/de/a3e710469772c6a89595fc52816da05c1e164b4c866a89e3cb82fb1b67c5/mkdocs_autorefs-1.4.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2a/2d/d4bf65e47cea8ff2c794a600c4fd1273a7902f268757c531e0ee9f18aa58/pooch-1.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2b/0a/7b98e1e119878a27ba8618ca1e18b14f992ff1eda40f47bccccf4de44121/kiwisolver-1.5.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/2c/01/bc663630c510822c95c47a66af9fa7a443c295b47d5f041e5e6ae62ef659/mkdocs_material-9.7.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2e/75/5604f4d17ab607510d4702f156329194d8edfff7e29644ca9200b085e9a2/scipp-26.3.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/2f/c8/005d1de3af80f54411703d1263a0b9d31276411ec9f273d9432c59b17299/arviz_plots-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/30/d4/24d543ab8b8158b7f5a97113c831205f5c900c92c8762b1e7f44b7ea0405/python_discovery-1.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/31/b8/69f5565f1a280d032525878a86511eebed0645818492feeb169dfb20ae8e/llvmlite-0.47.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/32/28/79f0f8de97cce916d5ae88a7bee1ad724855e83e6019c0b4d5b3fabc80f3/mkdocstrings_python-2.0.3-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/33/b2/986d1220f6ee931e338d272bc1f3ec02cfe5f9b5fad84e95afdad57f1ebc/format_docstring-0.2.7-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/33/f0/3fe8c6e69135a845f4106f2ff8b6805638d4e85c264e70114e8126689587/tokenize_rt-6.2.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/38/7e/7b91c89a4cf0f543a83be978657afb20c86af6d725253e319589dcc4ce52/lmfit-1.3.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3a/eb/fea4d1d51c49832120f7f285d07306db3960f423a2612c6057caf3e8196f/pip-26.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3e/17/1f31d8562e6f970d64911f1abc330d233bc0c0601411cf7e19c1292be6da/spdx_headers-1.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/44/0d/f494a2ec62ab56ffd44da4f236db374421fe0cd1e39d9ebc9785751eb432/periodictable-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4a/1a/bea014b55c45c98c63dffd34d2dd01eed0a73ce5b175bd7b1ba798d13558/pyhanko-0.35.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4b/32/e0f13a1c5b0f8572d0ec6ae2f6c677b7991fafd95da523159c19eff0696a/contourpy-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/52/59/0782e51887ac6b07ffd1570e0364cf901ebc36345fea669969d2084baebb/simple_websocket-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/56/5b/53ca0fd447f73423c7dc59d34e523530ef434481a3d18808ff7537ad33ec/svglib-1.5.1.tar.gz + - pypi: https://files.pythonhosted.org/packages/5a/38/f1efc72736fb39e75802efcb5e2ba8eb02c7ee3687940047234bc607e3a5/python_bidi-0.6.9-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5b/69/93b34728cc386efdde0c342f8c680b9187dea7beb7adaf6b58a0713be101/mpld3-0.5.12-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5d/ba/459f18c16f2b3fc1a1ca871f72f07d70c07bf768ad0a507a698b8052ac58/msgpack-1.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/64/a8/c09fbe44b12fa919c5bfe0afb71e60d1231a7dc93405e54c30496c57c9d3/arviz-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/66/fe/b1e10b08d287f518994f1e2ff9b6d26f0adeecd8dd7d533b01bab29a3eda/yarl-1.23.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/67/ee/21d4e8536afd1a328f01b359b4d3997b291ffd35a237c877b331c1c3b71c/pillow-12.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/69/d1/705e6c19b437a4105bf3b9ae7945fcfc3ad2abb73d14bae0a3f2d58b305b/arviz_base-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6c/dd/a834df6482147d48e225a49515aabc28974ad5a4ca3215c18a882565b028/html5lib-1.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6e/7d/77752d819ede528c664ecf4de974b17b049e054a14537b74b53c8568014f/arabic_reshaper-3.0.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6e/94/be70f8ee9c45f2f62b39a1f0e9303bc20e138a8f3b8e50ffd89498e177e1/mkdocstrings-1.0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/76/8e/56ccb09c7232a55403a7637caa21922f3b65901a37f5e8bdb405d0de0946/mike-2.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7b/67/b1944235474aac3f0b0e1b232ce49547f9f9461ca4b943df1b88da5d3f1d/bumps-1.0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-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/6f/cc2b231dc78d8c3aaa674a676db190b8f8071c50134af8f8cf39b9b8e8df/pydoclint-0.8.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8a/17/4402d0d14ccf1dfc70932600b68097fbbf9c898a4871d2cbbe79c7801a32/matplotlib-3.10.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/8a/a1/8d812e53a5da1687abb10445275d41a8b13adb781bbf7196ddbcf8d88505/lazy_loader-0.5-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/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/90/ad/cba91b3bcf04073e4d1655a5c1710ef3f457f56f7d1b79dcc3d72f4dd912/plotly-6.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/93/ca/d53764f0534ff857239595f090f4cb83b599d226cc326c7de5eb3d802715/xhtml2pdf-0.2.17-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/93/f7/d00d9b4a0313a6be3a3e0818e6375e15da6d7076f4ae47d1324e7ca986a1/radon-6.0.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/99/31/6cf181011dc738c33bf6ba7aea2e8e1d3c1f71b7dab1942f3054f66f6202/asteval-1.0.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/99/37/e8730c3587a65eb5645d4aba2d27aae48e8003614d6aaf15dda67f702f1f/bidict-0.23.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9e/43/53afb8ba17218f19b77c7834128566c5bbb100a0ad9ba2e8e89d089d7079/autopep8-2.3.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a0/22/b8d873f6466b20aa563fc9b33acd48dec89a07803ddaa2f1c8ca1cd33126/numba-0.65.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a4/ce/3b6fee91c85626eaf769d617f1be9d2e15c1cca027bbdeb2e0d751469355/verspec-0.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a8/4e/c09876f08fa9faaa5e1178f3d77b7af3f343258689bd6f3b72593b2f74e3/mkdocs_markdownextradata_plugin-0.2.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a8/b1/3338e121cbd4c8a126b8ccb1061170c2ce51a53f678c502793ea49c6fd6d/chardet-7.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/aa/54/0cce26da03a981f949bb8449c9778537f75f5917c172e1d2992ff25cb57d/python_engineio-4.13.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ac/68/1666e3a4462f8202d836920114fa7a5ee9275d1fa45366d336c551a162dd/coverage-7.13.5-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/af/78/bc3850efda66712370ebb2a52130111b5cbbdee7a5b9d1672aadfc73234f/orsopy-1.2.3-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b0/73/6e1b01cbeb458807aa0831742232dbdd1fa92bfa33f52a3f176b4ff3dc11/multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/b1/4f/f71e641e504111a5a74e3a20bc52d01bd86788b22699dd3fee1c63253cf6/virtualenv-21.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/52/bc858b1665d0dec3a2511f4e6f5c18ea85c0977563d624d597c95d6d0fd7/jupyterquiz-2.9.6.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/b7/ee/e9c95cda829131f71a8dff5ce0406059fd16e591c074414e31ada19ba7c3/validate_pyproject-0.25-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bd/98/ef2b6fe2903e377cbe870c3b2800d62552f1e3dbe81ce49e1923c53d1c5c/h5py-3.16.0-cp313-cp313-manylinux_2_28_x86_64.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/c1/d4/59e74daffcb57a07668852eeeb6035af9f32cbfd7a1d2511f17d2fe6a738/smmap-5.0.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c2/14/e2a54fabd4f08cd7af1c07030603c3356b74da07f7cc056e600436edfa17/tzlocal-5.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c6/3d/020a6b6248c3d4a37797db068256f0b3f15b01bc481327ba888c50309aa8/mkdocs_plugin_inline_svg-0.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c7/a0/5ff05d1919ca249508012cad89f08fdc6cfbdaa15b41651c5fe6dffaf1d3/dfo_ls-1.6.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c9/7f/09065fd9e27da0eda08b4d6897f1c13535066174cc023af248fc2a8d5e5a/asn1crypto-1.5.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d0/69/f24d3d1c38ad69e256138b4ec2452a8c7cf66be49dc214771ae99dd4f0a0/narwhals-2.20.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d1/73/a9d864e42a01896bb5974475438f16086be9ba1f0d19d0bb7a07427c4a8b/numpy-2.4.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/d2/f0/834e479e47e499b6478e807fb57b31cc2db696c4db30557bb6f5aea4a90b/mando-0.7.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/d6/bb/fbdc4e57731efb86b4209ffdd2519520782bf27b3c961beac3e5c20d2b87/trove_classifiers-2026.4.28.13-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/dc/83/6d810a8a9ebc9c307989b418840c20e46907c74d707beb67ab566773e6fc/xarray-2026.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e2/98/8b1e801939839d405f1f122e7d175cebe9aeb4e114f95bfc45e3152af9a7/fonttools-4.62.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/e4/bc/daa30c02069eeac5b9198985ba42f5d65ca71bed6705b18329e51d352b7c/plopp-26.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e6/90/90a65e6ae1b6e66183b48874d32509fc306c994beecc7924a6fa3d9f8955/easyscience-2.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/eb/be/b257e12f9710819fde40adc972578bee6b72c5992da1bc8369bef2597756/nbmake-1.5.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ee/b8/ead7c10efff731738c72e59ed6eb5791854879fbed7ae98781a12006263a/lxml-6.1.0-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/f0/d2/e2f77eef1acb7111405433c707dc735e63f67a56e176e72e9e7a2cd3f493/aiohttp-3.13.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/f1/8b/544bc867e24e1bd48f3118cecd3b05c694e160a168478fa28770f22fd094/propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/f4/99/722e90c586455733fa860d092722ef074f3a65449fe9787b7b7e2dafcd1f/pyhanko_certvalidator-0.31.1-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/f5/5f/f17563f28ff03c7b6799c50d01d5d856a1d55f2676f537ca8d28c7f627cd/scipy-1.17.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/f6/f0/10642828a8dfb741e5f3fbaac830550a518a775c7fff6f04a007259b0548/py-1.11.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f7/27/a2fc51a4a122dfd1015e921ae9d22fee3d20b0b8080d9a704578bf9deece/pymdown_extensions-10.21.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f7/ee/d0348d8dd27ac7f001e9583c004f1b245826b04cc23b5ce6b7867c4a1cc6/refnx-0.1.63-cp311-abi3-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/86/dd6e5db36df29e76c7a7699123569a4a18c1623ce68d826ed96c62643cae/mdit_py_plugins-0.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/d9/8f612389178e1e1800aaed85537b024ebe28b9a82fff6a015825e51a8877/refl1d-1.0.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fd/20/2870f2e63da2b74eda896053809065769b256646bb3f4b74e06d013ad590/uritools-6.1.0-py3-none-any.whl + p1: + - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/annotated-types-0.7.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.13.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/appnope-0.1.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/argon2-cffi-25.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/arrow-1.4.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.18.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.14.3-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.3.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-with-css-6.3.0-hbca2aae_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.4.22-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cached-property-1.5.2-hd8ed1ab_1.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/cached_property-1.5.2-pyha770c72_1.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2026.4.22-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.7-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.3-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/copier-9.15.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.13.13-py313hd8ed1ab_100.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/decorator-5.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/defusedxml-0.7.1-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/dunamai-1.26.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/eval-type-backport-0.2.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/eval_type_backport-0.2.2-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fqdn-1.5.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/funcy-2.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h11-0.16.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/httpcore-1.0.9-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/httpx-0.28.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.13-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.8.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib_resources-7.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/invoke-3.0.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.2.0-pyh5552912_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.13.0-pyh53cf698_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython_pygments_lexers-1.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/isoduration-20.11.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jedi-0.19.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-ansible-filters-1.3.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/json5-0.14.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonpointer-3.1.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.26.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.9.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-with-format-nongpl-4.26.0-hcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter-lsp-2.3.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.8.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.9.1-pyhc90fa1f_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_events-0.12.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.18.2-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server_terminals-0.5.4-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.5.7-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_pygments-0.3.0-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_server-2.28.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/lark-1.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mistune-3.2.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbclient-0.10.4-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbconvert-core-7.17.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbformat-5.10.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/notebook-shim-0.2.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/overrides-7.7.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.2-pyhc364b38_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pandocfilters-1.5.0-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/paramiko-4.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.7-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pathspec-1.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pexpect-4.9.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pixi-kernel-0.7.1-pyhbbac1ac_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.9.6-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/plumbum-1.10.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prometheus_client-0.25.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.51-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prompt_toolkit-3.0.51-hd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ptyprocess-0.7.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pydantic-2.13.4-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.21.2-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.13.13-h4df99d1_100.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-json-logger-3.2.1-pyh332efcf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2026.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pywin32-on-windows-0.1.0-pyh1179c8e_3.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/questionary-2.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.37.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.33.1-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/returns-0.27.0-pyhc364b38_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3339-validator-0.1.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3986-validator-0.1.1-pyh9f0ad1d_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3987-syntax-1.1.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/send2trash-2.1.0-pyh5552912_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-82.0.1-pyh332efcf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sniffio-1.3.1-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.8.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyhc90fa1f_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tinycss2-1.4.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.15.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.15.0-h396c80c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-inspection-0.4.2-pyhcf101f3_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_utils-0.1.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/uri-template-1.3.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.6.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.7.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/webcolors-25.10.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/webencodings-0.5.1-pyhd8ed1ab_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/websocket-client-1.9.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/argon2-cffi-bindings-25.1.0-py313h6535dbc_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/backports.zstd-1.4.0-py313h7208f8c_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bcrypt-5.0.0-py313h2c089d5_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-python-1.2.0-py313hde1f3bb_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-hd037594_9.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/c-ares-1.34.6-hc919400_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cffi-2.0.0-py313h224173a_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cryptography-48.0.0-py313hda5ae78_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/debugpy-1.8.20-py313h1188861_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/icu-78.3-hef89b57_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/krb5-1.22.2-h385eeb1_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libabseil-20260107.1-cxx17_h2062a1b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlicommon-1.2.0-hc919400_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlidec-1.2.0-hc919400_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlienc-1.2.0-hc919400_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-22.1.5-h55c6f16_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libedit-3.1.20250104-pl5321hafb1f1b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libev-4.33-h93a5062_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.8.0-hf6b4638_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.5.2-hcf2aa1b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.3-h8088a28_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libmpdec-4.0.0-h84a0fba_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libnghttp2-1.68.1-h8f3e76b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsodium-1.0.21-h1a92334_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.53.1-h1b79a29_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libuv-1.51.0-h6caf38d_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.2-h8088a28_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/markupsafe-3.0.3-py313h65a2061_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/msgspec-0.21.1-py313h0997733_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.6-h1d4f5a5_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/nodejs-25.8.2-h7039424_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.6.2-hd24854e_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/psutil-7.2.2-py313h6688731_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pydantic-core-2.46.4-py313h212e517_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pynacl-1.6.2-py313h6940bce_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyobjc-core-12.1-py313h40b429f_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyobjc-framework-cocoa-12.1-py313hcc5defa_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.13.13-h20e6be0_100_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyyaml-6.0.3-py313h65a2061_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyzmq-27.1.0-py312h022ad19_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.3-h46df422_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/rpds-py-0.30.0-py313h2c089d5_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ruff-0.15.12-hbd3f8a3_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h010d191_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tornado-6.5.5-py313h0997733_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/yaml-0.2.5-h925e9cb_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zeromq-4.3.5-h4818236_10.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hbf9d68e_6.conda + - pypi: ./ + - pypi: https://files.pythonhosted.org/packages/00/bb/90ba423612b6aa0adccc6b1874bcd4a9b44b660c0c16f346611e00f64ac3/backrefs-7.0-py313-none-any.whl + - pypi: https://files.pythonhosted.org/packages/01/7c/fa07d3da2b6253eb8474be16eab2eadf670460e364ccc895ca7ff388ee30/oscrypto-1.3.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/07/c7/deb8c5e604404dbf10a3808a858946ca3547692ff6316b698945bb72177e/python_socketio-5.16.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/08/03/69347590f1cf4a6d5a4944bb6099e6d37f334784f16062234e1f892fdb1d/lxml-6.1.0-cp313-cp313-macosx_10_13_universal2.whl + - pypi: https://files.pythonhosted.org/packages/0b/77/dbc82ff2fb0e63c6564356682bf201edff0ba16c98630d21a1fb312a8182/pandas-3.0.2-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/0c/c9/44fb661c55062f0818a6ffd2685c67aa30816200d5f2817543717d4b92eb/coverage-7.13.5-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/0c/d6/1d5c60cc17bbdf37c1552d9c03862fc6d32c5836732a0415b2d637edc2d0/pypdf-6.10.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0d/1f/d398de1612f7a611e22d743280339c9af4903675635e41be3370091c704b/arviz_stats-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0d/fe/6bea5c9162869c5beba5d9c8abbed835ec85bf1ec1fba05a3822325c45f3/build-1.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/11/8c/c9138d881c79aa0ea9ed83cbd58d5ca75624378b38cee225dcf5c42cc91f/griffelib-2.0.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/12/c9/6869a1dcf4aaf309b9543ec070be3ec3adebee7c9bec9af8c230494134b9/interrogate-1.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/13/95/cf3f7fe4910cf0365fa8ea0c731f4b8a624d97cd76ea777913ac8d0868e2/mkdocs_jupyter-0.26.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/16/5a/736dd2f4535dbf3bf26523f9158c011389ef88dd06ec2eef67fd744f1c7b/jupytext-1.19.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/19/d4/225027a913621a879b429a043674aa35220e6ce67785acad4f7bd0c4ff33/xarray_einstats-0.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1c/13/bc43591a54dd38ac5c19e7e849f0311d879737a0d07e032e5be79849a5fb/reportlab-4.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1c/59/964ecb8008722d27d8a835baea81f56a91cea8e097b3be992bc6ccde6367/versioningit-3.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/20/16/e777eadfa0c0305878c36fae1d5e6db474fbb15dae202b9ec378809dfb4d/nbstripout-0.9.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/20/7a/1c6e3562dfd8950adbb11ffbc65d21e7c89d01a6e4f137fa981056de25c5/gitpython-3.1.50-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/21/0e/8459ca4413e1a21a06c97d134bfaf18adfd27cea068813dc0faae06cbf00/cssselect2-0.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/28/88/4789719fbbe166d12d345b3ac66b96105f10001b16e00a9765ba29261a21/nbqa-1.9.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/28/de/a3e710469772c6a89595fc52816da05c1e164b4c866a89e3cb82fb1b67c5/mkdocs_autorefs-1.4.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2a/2d/d4bf65e47cea8ff2c794a600c4fd1273a7902f268757c531e0ee9f18aa58/pooch-1.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2c/01/bc663630c510822c95c47a66af9fa7a443c295b47d5f041e5e6ae62ef659/mkdocs_material-9.7.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2f/c8/005d1de3af80f54411703d1263a0b9d31276411ec9f273d9432c59b17299/arviz_plots-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/30/d4/24d543ab8b8158b7f5a97113c831205f5c900c92c8762b1e7f44b7ea0405/python_discovery-1.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/32/28/79f0f8de97cce916d5ae88a7bee1ad724855e83e6019c0b4d5b3fabc80f3/mkdocstrings_python-2.0.3-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/33/b2/986d1220f6ee931e338d272bc1f3ec02cfe5f9b5fad84e95afdad57f1ebc/format_docstring-0.2.7-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/33/f0/3fe8c6e69135a845f4106f2ff8b6805638d4e85c264e70114e8126689587/tokenize_rt-6.2.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/38/7e/7b91c89a4cf0f543a83be978657afb20c86af6d725253e319589dcc4ce52/lmfit-1.3.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3a/eb/fea4d1d51c49832120f7f285d07306db3960f423a2612c6057caf3e8196f/pip-26.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3b/56/6f389de21c49555553d6a5aeed5ac9767631497ac836c4f076273d15bd72/fonttools-4.62.1-cp313-cp313-macosx_10_13_universal2.whl + - pypi: https://files.pythonhosted.org/packages/3e/17/1f31d8562e6f970d64911f1abc330d233bc0c0601411cf7e19c1292be6da/spdx_headers-1.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/44/0d/f494a2ec62ab56ffd44da4f236db374421fe0cd1e39d9ebc9785751eb432/periodictable-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4a/1a/bea014b55c45c98c63dffd34d2dd01eed0a73ce5b175bd7b1ba798d13558/pyhanko-0.35.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/52/59/0782e51887ac6b07ffd1570e0364cf901ebc36345fea669969d2084baebb/simple_websocket-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/55/5f/25bdec773905bff0ff6cf35ca73b17bd05593b4f87bd8c5fa43705f7167d/chardet-7.4.3-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/56/5b/53ca0fd447f73423c7dc59d34e523530ef434481a3d18808ff7537ad33ec/svglib-1.5.1.tar.gz + - pypi: https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5b/58/3ce96251560107b381cbd6e8413c483bbb1228a6b919fa8652b0d4090e7f/scipy-1.17.1-cp313-cp313-macosx_14_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/5b/69/93b34728cc386efdde0c342f8c680b9187dea7beb7adaf6b58a0713be101/mpld3-0.5.12-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/63/d9/9e14bc7564bf92d5ffa801ae5fac819ce74b925dfb55e3ebde61a3bbad3e/matplotlib-3.10.9-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/64/a8/c09fbe44b12fa919c5bfe0afb71e60d1231a7dc93405e54c30496c57c9d3/arviz-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/69/d1/705e6c19b437a4105bf3b9ae7945fcfc3ad2abb73d14bae0a3f2d58b305b/arviz_base-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6c/dd/a834df6482147d48e225a49515aabc28974ad5a4ca3215c18a882565b028/html5lib-1.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6e/7d/77752d819ede528c664ecf4de974b17b049e054a14537b74b53c8568014f/arabic_reshaper-3.0.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6e/94/be70f8ee9c45f2f62b39a1f0e9303bc20e138a8f3b8e50ffd89498e177e1/mkdocstrings-1.0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/71/43/905a14a8b17fdb1ccb58d282454490662d2cb89a6bfec26af6d3520da5ec/pillow-12.2.0-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/76/8e/56ccb09c7232a55403a7637caa21922f3b65901a37f5e8bdb405d0de0946/mike-2.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/77/6f/4615353e016799f80fa52ccb270a843c413b22361fadda2589b2922fb9b0/llvmlite-0.47.0-cp313-cp313-macosx_12_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/79/37/14a4579049c1eb673afd0de0cb4842982acd55b9ce2643e763db858bcea0/numba-0.65.1-cp313-cp313-macosx_12_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/79/76/55cd7186f498ed080a18440c9013011eb548f77ae1b297206d030eb1180a/multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/79/fe/b14d806894cf05178f1e77d0d619f071db50cf698bc654c54f9241223bcf/scipp-26.3.1-cp313-cp313-macosx_14_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/7b/67/b1944235474aac3f0b0e1b232ce49547f9f9461ca4b943df1b88da5d3f1d/bumps-1.0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-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/6f/cc2b231dc78d8c3aaa674a676db190b8f8071c50134af8f8cf39b9b8e8df/pydoclint-0.8.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/89/a4/92380f7ca60f99ebae761936bc48a72a639e8a47b29050615eef757cb2a7/propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/8a/a1/8d812e53a5da1687abb10445275d41a8b13adb781bbf7196ddbcf8d88505/lazy_loader-0.5-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/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/90/ad/cba91b3bcf04073e4d1655a5c1710ef3f457f56f7d1b79dcc3d72f4dd912/plotly-6.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/90/df/cbd75ba033d712a7bbe8efff2a764c15a3904003f78ed3c19f259c91a4a4/refnx-0.1.63-cp311-abi3-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/92/dc/c385f38f2c2433333345a82926c6bfa5ecfff3ef787201614317b58dd8be/msgpack-1.1.2-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/93/ca/d53764f0534ff857239595f090f4cb83b599d226cc326c7de5eb3d802715/xhtml2pdf-0.2.17-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/93/f7/d00d9b4a0313a6be3a3e0818e6375e15da6d7076f4ae47d1324e7ca986a1/radon-6.0.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/95/95/685e5a31f6c4cc24826d3a0df9e6a4bcf13d00f90d0c1f1060f13f5c16f8/python_bidi-0.6.9-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/96/e4/7adcd9c8362745b2210728f209bfbcf7d91ba868a2c5f40d8b58f54c509b/contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/99/31/6cf181011dc738c33bf6ba7aea2e8e1d3c1f71b7dab1942f3054f66f6202/asteval-1.0.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/99/37/e8730c3587a65eb5645d4aba2d27aae48e8003614d6aaf15dda67f702f1f/bidict-0.23.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9c/f5/0fb20fb49f8efdcdce6cd8127604ad2c503e754a8f139f5e02b01626523f/aiohttp-3.13.5-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9e/43/53afb8ba17218f19b77c7834128566c5bbb100a0ad9ba2e8e89d089d7079/autopep8-2.3.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a4/ce/3b6fee91c85626eaf769d617f1be9d2e15c1cca027bbdeb2e0d751469355/verspec-0.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a8/3a/d0a972b34e1c63e2409413104216cd1caa02c5a37cb668d1687d466c1c45/kiwisolver-1.5.0-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/a8/4e/c09876f08fa9faaa5e1178f3d77b7af3f343258689bd6f3b72593b2f74e3/mkdocs_markdownextradata_plugin-0.2.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/aa/54/0cce26da03a981f949bb8449c9778537f75f5917c172e1d2992ff25cb57d/python_engineio-4.13.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ae/50/06d511cc4b8e0360d3c94af051a768e84b755c5eb031b12adaaab6dec6e5/yarl-1.23.0-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/af/78/bc3850efda66712370ebb2a52130111b5cbbdee7a5b9d1672aadfc73234f/orsopy-1.2.3-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b0/65/5e088a45d0f43cd814bc5bec521c051d42005a472e804b1a36c48dada09b/h5py-3.16.0-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/b1/4f/f71e641e504111a5a74e3a20bc52d01bd86788b22699dd3fee1c63253cf6/virtualenv-21.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/52/bc858b1665d0dec3a2511f4e6f5c18ea85c0977563d624d597c95d6d0fd7/jupyterquiz-2.9.6.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/ee/e9c95cda829131f71a8dff5ce0406059fd16e591c074414e31ada19ba7c3/validate_pyproject-0.25-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-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/c1/d4/59e74daffcb57a07668852eeeb6035af9f32cbfd7a1d2511f17d2fe6a738/smmap-5.0.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c2/14/e2a54fabd4f08cd7af1c07030603c3356b74da07f7cc056e600436edfa17/tzlocal-5.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c6/3d/020a6b6248c3d4a37797db068256f0b3f15b01bc481327ba888c50309aa8/mkdocs_plugin_inline_svg-0.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c7/a0/5ff05d1919ca249508012cad89f08fdc6cfbdaa15b41651c5fe6dffaf1d3/dfo_ls-1.6.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c9/7f/09065fd9e27da0eda08b4d6897f1c13535066174cc023af248fc2a8d5e5a/asn1crypto-1.5.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d0/69/f24d3d1c38ad69e256138b4ec2452a8c7cf66be49dc214771ae99dd4f0a0/narwhals-2.20.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d2/f0/834e479e47e499b6478e807fb57b31cc2db696c4db30557bb6f5aea4a90b/mando-0.7.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d6/bb/fbdc4e57731efb86b4209ffdd2519520782bf27b3c961beac3e5c20d2b87/trove_classifiers-2026.4.28.13-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/dc/83/6d810a8a9ebc9c307989b418840c20e46907c74d707beb67ab566773e6fc/xarray-2026.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e4/bc/daa30c02069eeac5b9198985ba42f5d65ca71bed6705b18329e51d352b7c/plopp-26.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e5/87/499737bfba066b4a3bebff24a8f1c5b2dee410b209bc6668c9be692580f0/numpy-2.4.4-cp313-cp313-macosx_14_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/e6/90/90a65e6ae1b6e66183b48874d32509fc306c994beecc7924a6fa3d9f8955/easyscience-2.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/eb/be/b257e12f9710819fde40adc972578bee6b72c5992da1bc8369bef2597756/nbmake-1.5.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f4/99/722e90c586455733fa860d092722ef074f3a65449fe9787b7b7e2dafcd1f/pyhanko_certvalidator-0.31.1-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/f6/f0/10642828a8dfb741e5f3fbaac830550a518a775c7fff6f04a007259b0548/py-1.11.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f7/27/a2fc51a4a122dfd1015e921ae9d22fee3d20b0b8080d9a704578bf9deece/pymdown_extensions-10.21.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/86/dd6e5db36df29e76c7a7699123569a4a18c1623ce68d826ed96c62643cae/mdit_py_plugins-0.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/d9/8f612389178e1e1800aaed85537b024ebe28b9a82fff6a015825e51a8877/refl1d-1.0.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fd/20/2870f2e63da2b74eda896053809065769b256646bb3f4b74e06d013ad590/uritools-6.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl + win-64: + - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/annotated-types-0.7.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.13.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/argon2-cffi-25.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/arrow-1.4.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.18.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.14.3-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.3.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-with-css-6.3.0-hbca2aae_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.4.22-h4c7d964_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cached-property-1.5.2-hd8ed1ab_1.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/cached_property-1.5.2-pyha770c72_1.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2026.4.22-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.7-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.3-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/copier-9.15.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.13.13-py313hd8ed1ab_100.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/decorator-5.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/defusedxml-0.7.1-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/dunamai-1.26.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/eval-type-backport-0.2.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/eval_type_backport-0.2.2-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fqdn-1.5.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/funcy-2.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h11-0.16.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/httpcore-1.0.9-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/httpx-0.28.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.13-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.8.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib_resources-7.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/invoke-3.0.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.2.0-pyh6dadd2b_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.13.0-pyhe2676ad_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython_pygments_lexers-1.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/isoduration-20.11.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jedi-0.19.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-ansible-filters-1.3.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/json5-0.14.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonpointer-3.1.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.26.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.9.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-with-format-nongpl-4.26.0-hcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter-lsp-2.3.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.8.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.9.1-pyh6dadd2b_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_events-0.12.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.18.2-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server_terminals-0.5.4-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.5.7-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_pygments-0.3.0-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_server-2.28.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/lark-1.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mistune-3.2.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbclient-0.10.4-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbconvert-core-7.17.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbformat-5.10.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/notebook-shim-0.2.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/overrides-7.7.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.2-pyhc364b38_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pandocfilters-1.5.0-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/paramiko-4.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.7-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pathspec-1.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pixi-kernel-0.7.1-pyhbbac1ac_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.9.6-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/plumbum-1.10.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prometheus_client-0.25.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.51-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prompt_toolkit-3.0.51-hd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pydantic-2.13.4-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyh09c184e_7.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.21.2-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.13.13-h4df99d1_100.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-json-logger-3.2.1-pyh332efcf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2026.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pywin32-on-windows-0.1.0-pyh07e9846_2.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/questionary-2.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.37.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.33.1-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/returns-0.27.0-pyhc364b38_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3339-validator-0.1.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3986-validator-0.1.1-pyh9f0ad1d_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3987-syntax-1.1.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/send2trash-2.1.0-pyh6dadd2b_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-82.0.1-pyh332efcf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sniffio-1.3.1-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.8.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyh6dadd2b_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tinycss2-1.4.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.15.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.15.0-h396c80c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-inspection-0.4.2-pyhcf101f3_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_utils-0.1.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/uri-template-1.3.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.6.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.7.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/webcolors-25.10.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/webencodings-0.5.1-pyhd8ed1ab_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/websocket-client-1.9.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/win_inet_pton-1.1.0-pyh7428d3b_8.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/argon2-cffi-bindings-25.1.0-py313h5ea7bf4_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/backports.zstd-1.4.0-py313h2a31948_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/bcrypt-5.0.0-py313hfbe8231_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/brotli-python-1.2.0-py313h3ebfc14_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h0ad9c76_9.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/cffi-2.0.0-py313h5ea7bf4_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/cryptography-48.0.0-py313hf5c5e30_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/debugpy-1.8.20-py313h927ade5_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/krb5-1.22.2-h0ea6238_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.8.0-hac47afa_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libffi-3.5.2-h3d046cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-5.8.3-hfd05255_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libmpdec-4.0.0-hfd05255_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libsodium-1.0.21-h6a83c73_3.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.53.1-hf5d6505_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libzlib-1.3.2-hfd05255_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/markupsafe-3.0.3-py313hd650c13_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/msgspec-0.21.1-py313h5ea7bf4_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/nodejs-25.8.2-h80d1838_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.6.2-hf411b9b_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/psutil-7.2.2-py313h5fd188c_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pydantic-core-2.46.4-py313hfbe8231_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pynacl-1.6.2-py313hed4ef92_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/python-3.13.13-h09917c8_100_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pywin32-311-py313h40c08fc_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pywinpty-2.0.15-py313h5813708_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pyyaml-6.0.3-py313hd650c13_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pyzmq-27.1.0-py312h343a6d4_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/rpds-py-0.30.0-py313hfbe8231_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/ruff-0.15.12-hd7ccaa8_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h6ed50ae_3.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/tornado-6.5.5-py313h5ea7bf4_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.26100.0-h57928b3_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-h41ae7f8_34.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.44.35208-h818238b_34.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.44.35208-h818238b_34.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/winpty-0.4.3-4.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/win-64/yaml-0.2.5-h6a83c73_3.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/zeromq-4.3.5-h507cc87_10.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_6.conda + - pypi: ./ + - pypi: https://files.pythonhosted.org/packages/00/bb/90ba423612b6aa0adccc6b1874bcd4a9b44b660c0c16f346611e00f64ac3/backrefs-7.0-py313-none-any.whl + - pypi: https://files.pythonhosted.org/packages/01/7c/fa07d3da2b6253eb8474be16eab2eadf670460e364ccc895ca7ff388ee30/oscrypto-1.3.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/07/c7/deb8c5e604404dbf10a3808a858946ca3547692ff6316b698945bb72177e/python_socketio-5.16.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0c/d6/1d5c60cc17bbdf37c1552d9c03862fc6d32c5836732a0415b2d637edc2d0/pypdf-6.10.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0d/1f/d398de1612f7a611e22d743280339c9af4903675635e41be3370091c704b/arviz_stats-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0d/fe/6bea5c9162869c5beba5d9c8abbed835ec85bf1ec1fba05a3822325c45f3/build-1.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/11/8c/c9138d881c79aa0ea9ed83cbd58d5ca75624378b38cee225dcf5c42cc91f/griffelib-2.0.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/12/c9/6869a1dcf4aaf309b9543ec070be3ec3adebee7c9bec9af8c230494134b9/interrogate-1.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/13/95/cf3f7fe4910cf0365fa8ea0c731f4b8a624d97cd76ea777913ac8d0868e2/mkdocs_jupyter-0.26.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/16/5a/736dd2f4535dbf3bf26523f9158c011389ef88dd06ec2eef67fd744f1c7b/jupytext-1.19.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/18/0b/0098c214843213759692cc638fce7de5c289200a830e5035d1791d7a2338/contourpy-1.3.3-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/19/d4/225027a913621a879b429a043674aa35220e6ce67785acad4f7bd0c4ff33/xarray_einstats-0.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1b/b3/5d0e77ea774bd3224321c248880ea0c0379000ac5c2bb6d77609549de247/chardet-7.4.3-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/1c/13/bc43591a54dd38ac5c19e7e849f0311d879737a0d07e032e5be79849a5fb/reportlab-4.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1c/59/964ecb8008722d27d8a835baea81f56a91cea8e097b3be992bc6ccde6367/versioningit-3.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/20/16/e777eadfa0c0305878c36fae1d5e6db474fbb15dae202b9ec378809dfb4d/nbstripout-0.9.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/20/7a/1c6e3562dfd8950adbb11ffbc65d21e7c89d01a6e4f137fa981056de25c5/gitpython-3.1.50-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/21/0e/8459ca4413e1a21a06c97d134bfaf18adfd27cea068813dc0faae06cbf00/cssselect2-0.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/28/88/4789719fbbe166d12d345b3ac66b96105f10001b16e00a9765ba29261a21/nbqa-1.9.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/28/de/a3e710469772c6a89595fc52816da05c1e164b4c866a89e3cb82fb1b67c5/mkdocs_autorefs-1.4.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2a/2d/d4bf65e47cea8ff2c794a600c4fd1273a7902f268757c531e0ee9f18aa58/pooch-1.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2c/01/bc663630c510822c95c47a66af9fa7a443c295b47d5f041e5e6ae62ef659/mkdocs_material-9.7.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2f/c8/005d1de3af80f54411703d1263a0b9d31276411ec9f273d9432c59b17299/arviz_plots-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/30/d4/24d543ab8b8158b7f5a97113c831205f5c900c92c8762b1e7f44b7ea0405/python_discovery-1.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/32/28/79f0f8de97cce916d5ae88a7bee1ad724855e83e6019c0b4d5b3fabc80f3/mkdocstrings_python-2.0.3-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/33/b2/986d1220f6ee931e338d272bc1f3ec02cfe5f9b5fad84e95afdad57f1ebc/format_docstring-0.2.7-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/33/f0/3fe8c6e69135a845f4106f2ff8b6805638d4e85c264e70114e8126689587/tokenize_rt-6.2.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/35/e5/d6d0e51fc888f692a35134336866341c08655d92614f492c6860dc45bb2c/scipy-1.17.1-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/37/fd/22621d3ee9e3ee87ef4c89b63bba55b265ab85039b3c1ba88ed2380a24c1/scipp-26.3.1-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/38/60/35186529de1db3c01f5ad625bde07c1f576305eab6d86bbda4c58445f721/fonttools-4.62.1-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/38/7e/7b91c89a4cf0f543a83be978657afb20c86af6d725253e319589dcc4ce52/lmfit-1.3.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3a/eb/fea4d1d51c49832120f7f285d07306db3960f423a2612c6057caf3e8196f/pip-26.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3c/0c/c73042d7ae043f4e3ff51406821ad70904249708a6bc4f0dc94f5f78ce9b/python_bidi-0.6.9.tar.gz + - pypi: https://files.pythonhosted.org/packages/3e/17/1f31d8562e6f970d64911f1abc330d233bc0c0601411cf7e19c1292be6da/spdx_headers-1.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3f/82/8a3739a5e470b3c6cbb1d21d315800d8e16bff503d1f16b03a4ec3212786/pillow-12.2.0-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/43/34/7d27a333c558d6ac16dbc12a35061d389735e99e494ee4effa4ec6d99bed/pycairo-1.29.0-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/44/0d/f494a2ec62ab56ffd44da4f236db374421fe0cd1e39d9ebc9785751eb432/periodictable-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/46/9f/4898b44e4042c60fafcb1162dfb7014f6f15b1ec19bf29cfea6bf26df90d/llvmlite-0.47.0-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/4a/1a/bea014b55c45c98c63dffd34d2dd01eed0a73ce5b175bd7b1ba798d13558/pyhanko-0.35.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/52/59/0782e51887ac6b07ffd1570e0364cf901ebc36345fea669969d2084baebb/simple_websocket-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/57/fb/48649b4971cde70d817cf97a2a2fdc0b4d8308569f1dd2f2611959d2e0cf/numpy-2.4.4-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5b/69/93b34728cc386efdde0c342f8c680b9187dea7beb7adaf6b58a0713be101/mpld3-0.5.12-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5e/52/42855bf626868413f761addd574acc6195880ae247a5346477a4361c3acb/pandas-3.0.2-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/64/a8/c09fbe44b12fa919c5bfe0afb71e60d1231a7dc93405e54c30496c57c9d3/arviz-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/66/40/7732d648ab9d069a46e686043241f01206348e2bbf128daea85be4d6414b/coverage-7.13.5-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/69/d1/705e6c19b437a4105bf3b9ae7945fcfc3ad2abb73d14bae0a3f2d58b305b/arviz_base-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6a/00/d26b61e82163a59334e9f2ae31ba95ac01922da3792f090e11762d4422e8/rlpycairo-0.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6c/dd/a834df6482147d48e225a49515aabc28974ad5a4ca3215c18a882565b028/html5lib-1.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6d/93/01273a1b8d8454d45f2e18b3d6098c7be13a0864a55fbd0ebda7815c201a/svglib-1.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6e/7d/77752d819ede528c664ecf4de974b17b049e054a14537b74b53c8568014f/arabic_reshaper-3.0.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6e/94/be70f8ee9c45f2f62b39a1f0e9303bc20e138a8f3b8e50ffd89498e177e1/mkdocstrings-1.0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/74/07/1ed8277f8653c40ebc65985180b007879f6a836c525b3885dcc6448ae6cb/msgpack-1.1.2-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/76/8e/56ccb09c7232a55403a7637caa21922f3b65901a37f5e8bdb405d0de0946/mike-2.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7a/84/266e8da36879c6edcd37b02b547e2d9ecdfea776be49598e75696e3316e1/yarl-1.23.0-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/7b/67/b1944235474aac3f0b0e1b232ce49547f9f9461ca4b943df1b88da5d3f1d/bumps-1.0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-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/6f/cc2b231dc78d8c3aaa674a676db190b8f8071c50134af8f8cf39b9b8e8df/pydoclint-0.8.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8a/46/7e2d150dd574039c2f8d09c4e102bb90a6cb6634be4040fc29f212fc42bf/refnx-0.1.63-cp311-abi3-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/8a/a1/8d812e53a5da1687abb10445275d41a8b13adb781bbf7196ddbcf8d88505/lazy_loader-0.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8e/63/981401c5680c1eb30893f00a19641ac80db5d1e7086c62cb4b13ed813038/lxml-6.1.0-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/8f/5e/f1e1dd319e35e962a4e00b33150a8868b6329cc1d19fd533436ba5488f09/uncertainties-3.2.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/90/ad/cba91b3bcf04073e4d1655a5c1710ef3f457f56f7d1b79dcc3d72f4dd912/plotly-6.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/93/6e/bd7fbfacca077bc6f34f1a1109800a2c41ab50f4704d3a0507ba41009915/freetype_py-2.5.1-py3-none-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/93/ca/d53764f0534ff857239595f090f4cb83b599d226cc326c7de5eb3d802715/xhtml2pdf-0.2.17-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/93/f7/d00d9b4a0313a6be3a3e0818e6375e15da6d7076f4ae47d1324e7ca986a1/radon-6.0.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/99/31/6cf181011dc738c33bf6ba7aea2e8e1d3c1f71b7dab1942f3054f66f6202/asteval-1.0.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/99/37/e8730c3587a65eb5645d4aba2d27aae48e8003614d6aaf15dda67f702f1f/bidict-0.23.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9e/43/53afb8ba17218f19b77c7834128566c5bbb100a0ad9ba2e8e89d089d7079/autopep8-2.3.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a4/ce/3b6fee91c85626eaf769d617f1be9d2e15c1cca027bbdeb2e0d751469355/verspec-0.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a8/4e/c09876f08fa9faaa5e1178f3d77b7af3f343258689bd6f3b72593b2f74e3/mkdocs_markdownextradata_plugin-0.2.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/aa/54/0cce26da03a981f949bb8449c9778537f75f5917c172e1d2992ff25cb57d/python_engineio-4.13.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/af/78/bc3850efda66712370ebb2a52130111b5cbbdee7a5b9d1672aadfc73234f/orsopy-1.2.3-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b1/4f/f71e641e504111a5a74e3a20bc52d01bd86788b22699dd3fee1c63253cf6/virtualenv-21.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b2/35/e994121b0e90e46134673422dd564623f93304614f5d11886b1b3e06f503/multidict-6.7.1-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/52/bc858b1665d0dec3a2511f4e6f5c18ea85c0977563d624d597c95d6d0fd7/jupyterquiz-2.9.6.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/ee/e9c95cda829131f71a8dff5ce0406059fd16e591c074414e31ada19ba7c3/validate_pyproject-0.25-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/be/8a/be60e3bbcf513cc5a50f4a3e88e1dcecebb79c1ad607a7222877becaa101/kiwisolver-1.5.0-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/bf/50/98b146aea0f1cd7531d25f12bea69fa9ce8d1662124f93fb30dc4511b65e/docstring_parser_fork-0.0.14-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c0/1b/ac685a8882896acf0f6b31d689e3792199cfe7aba37969fa91da63a7fa27/aiohttp-3.13.5-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/c1/d4/59e74daffcb57a07668852eeeb6035af9f32cbfd7a1d2511f17d2fe6a738/smmap-5.0.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c2/14/e2a54fabd4f08cd7af1c07030603c3356b74da07f7cc056e600436edfa17/tzlocal-5.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c3/d9/a27997f84341fc0dfcdd1fe4179b6ba6c32a7aa880fdb8c514d4dad6fba3/h5py-3.16.0-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/c5/e0/0840fd2f93da988ec660b8ad1984abe9f25d2aed22a5e394ff1c68c88307/matplotlib-3.10.9-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/c6/3d/020a6b6248c3d4a37797db068256f0b3f15b01bc481327ba888c50309aa8/mkdocs_plugin_inline_svg-0.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c7/a0/5ff05d1919ca249508012cad89f08fdc6cfbdaa15b41651c5fe6dffaf1d3/dfo_ls-1.6.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c9/7f/09065fd9e27da0eda08b4d6897f1c13535066174cc023af248fc2a8d5e5a/asn1crypto-1.5.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d0/69/f24d3d1c38ad69e256138b4ec2452a8c7cf66be49dc214771ae99dd4f0a0/narwhals-2.20.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d2/f0/834e479e47e499b6478e807fb57b31cc2db696c4db30557bb6f5aea4a90b/mando-0.7.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d6/bb/fbdc4e57731efb86b4209ffdd2519520782bf27b3c961beac3e5c20d2b87/trove_classifiers-2026.4.28.13-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/dc/83/6d810a8a9ebc9c307989b418840c20e46907c74d707beb67ab566773e6fc/xarray-2026.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e4/bc/daa30c02069eeac5b9198985ba42f5d65ca71bed6705b18329e51d352b7c/plopp-26.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e6/90/90a65e6ae1b6e66183b48874d32509fc306c994beecc7924a6fa3d9f8955/easyscience-2.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/eb/be/b257e12f9710819fde40adc972578bee6b72c5992da1bc8369bef2597756/nbmake-1.5.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f4/99/722e90c586455733fa860d092722ef074f3a65449fe9787b7b7e2dafcd1f/pyhanko_certvalidator-0.31.1-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/f5/ab/f76ec3c3627c883215b5c8080debb4394ef5a7a29be811f786415fc1e6fd/propcache-0.4.1-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/f6/f0/10642828a8dfb741e5f3fbaac830550a518a775c7fff6f04a007259b0548/py-1.11.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f7/27/a2fc51a4a122dfd1015e921ae9d22fee3d20b0b8080d9a704578bf9deece/pymdown_extensions-10.21.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/86/dd6e5db36df29e76c7a7699123569a4a18c1623ce68d826ed96c62643cae/mdit_py_plugins-0.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/d9/8f612389178e1e1800aaed85537b024ebe28b9a82fff6a015825e51a8877/refl1d-1.0.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fd/20/2870f2e63da2b74eda896053809065769b256646bb3f4b74e06d013ad590/uritools-6.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fd/a5/03c970d57f4c1741354837353ce39fb5206952ae1dba8922d29c86f64805/numba-0.65.1-cp313-cp313-win_amd64.whl + py-311-env: + channels: + - url: https://conda.anaconda.org/nodefaults/ + - url: https://conda.anaconda.org/conda-forge/ + indexes: + - https://pypi.org/simple + packages: + linux-64: + - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/argon2-cffi-bindings-25.1.0-py311h49ec1c0_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/backports.zstd-1.4.0-py311h6b1f9c4_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/bcrypt-5.0.0-py311h902ca64_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.2.0-py311h66f275b_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_9.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/c-ares-1.34.6-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cffi-2.0.0-py311h03d9500_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cryptography-48.0.0-py311h2005dd1_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/debugpy-1.8.20-py311hc665b79_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/icu-78.3-h33c6efd_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.3-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.22.2-ha1258a1_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45.1-default_hbd61a6d_102.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libabseil-20260107.1-cxx17_h7b12aa8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlicommon-1.2.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlidec-1.2.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlienc-1.2.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20250104-pl5321h7949ede_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libev-4.33-hd590300_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.8.0-hecca717_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h3435931_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-he0feb66_18.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.2.0-h69a702a_18.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-he0feb66_18.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.3-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libnghttp2-1.68.1-h877daf1_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hb9d3cd8_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsodium-1.0.21-h280c20c_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.53.1-h0c1763c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h934c35e_18.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.42-h5347b49_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libuv-1.51.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.2-h25fd6f3_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.3-py311h3778330_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/msgspec-0.21.1-py311h49ec1c0_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.6-hdb14827_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/nodejs-25.8.2-he4ff34a_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.2-h35e630c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/psutil-7.2.2-py311haee01d2_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pydantic-core-2.46.4-py311h902ca64_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pynacl-1.6.2-py311hdf6b40b_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.11.15-hd63d673_0_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.3-py311h3778330_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pyzmq-27.1.0-py311h57d2397_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.3-h853b02a_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/rpds-py-0.30.0-py311h902ca64_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ruff-0.15.12-h994f30f_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h366c992_103.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tornado-6.5.5-py311h49ec1c0_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h280c20c_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zeromq-4.3.5-h41580af_10.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/annotated-types-0.7.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.13.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/argon2-cffi-25.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/arrow-1.4.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.18.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.14.3-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.3.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-with-css-6.3.0-hbca2aae_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.4.22-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cached-property-1.5.2-hd8ed1ab_1.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/cached_property-1.5.2-pyha770c72_1.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2026.4.22-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.7-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.3-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/copier-9.15.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/decorator-5.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/defusedxml-0.7.1-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/dunamai-1.26.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/eval-type-backport-0.2.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/eval_type_backport-0.2.2-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fqdn-1.5.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/funcy-2.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h11-0.16.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/httpcore-1.0.9-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/httpx-0.28.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.13-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.8.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib_resources-7.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/invoke-3.0.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.2.0-pyha191276_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.13.0-pyh53cf698_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython_pygments_lexers-1.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/isoduration-20.11.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jedi-0.19.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-ansible-filters-1.3.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/json5-0.14.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonpointer-3.1.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.26.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.9.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-with-format-nongpl-4.26.0-hcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter-lsp-2.3.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.8.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.9.1-pyhc90fa1f_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_events-0.12.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.18.2-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server_terminals-0.5.4-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.5.7-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_pygments-0.3.0-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_server-2.28.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/lark-1.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mistune-3.2.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbclient-0.10.4-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbconvert-core-7.17.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbformat-5.10.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/notebook-shim-0.2.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/overrides-7.7.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.2-pyhc364b38_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pandocfilters-1.5.0-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/paramiko-4.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.7-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pathspec-1.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pexpect-4.9.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pixi-kernel-0.7.1-pyhbbac1ac_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.9.6-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/plumbum-1.10.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prometheus_client-0.25.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.51-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prompt_toolkit-3.0.51-hd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ptyprocess-0.7.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pydantic-2.13.4-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.21.2-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-json-logger-3.2.1-pyh332efcf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2026.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.11-8_cp311.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pywin32-on-windows-0.1.0-pyh1179c8e_3.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/questionary-2.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.37.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.33.1-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/returns-0.27.0-pyhc364b38_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3339-validator-0.1.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3986-validator-0.1.1-pyh9f0ad1d_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3987-syntax-1.1.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/send2trash-2.1.0-pyha191276_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-82.0.1-pyh332efcf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sniffio-1.3.1-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.8.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyhc90fa1f_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tinycss2-1.4.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.15.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.15.0-h396c80c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-inspection-0.4.2-pyhcf101f3_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_utils-0.1.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/uri-template-1.3.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.6.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.7.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/webcolors-25.10.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/webencodings-0.5.1-pyhd8ed1ab_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/websocket-client-1.9.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.1-pyhcf101f3_0.conda + - pypi: ./ + - pypi: https://files.pythonhosted.org/packages/01/7c/fa07d3da2b6253eb8474be16eab2eadf670460e364ccc895ca7ff388ee30/oscrypto-1.3.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/06/8a/5e156e31ba656ce93c1cc895dd8f051ec351cb382940dca655aaec475005/python_bidi-0.6.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/07/c7/deb8c5e604404dbf10a3808a858946ca3547692ff6316b698945bb72177e/python_socketio-5.16.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/09/7d/af933f0f6e0767995b4e2d705a0665e454d1c19402aa7e895de3951ebb04/scipy-1.17.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/0c/d6/1d5c60cc17bbdf37c1552d9c03862fc6d32c5836732a0415b2d637edc2d0/pypdf-6.10.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0d/fe/6bea5c9162869c5beba5d9c8abbed835ec85bf1ec1fba05a3822325c45f3/build-1.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/11/8c/c9138d881c79aa0ea9ed83cbd58d5ca75624378b38cee225dcf5c42cc91f/griffelib-2.0.2-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/12/c9/6869a1dcf4aaf309b9543ec070be3ec3adebee7c9bec9af8c230494134b9/interrogate-1.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/13/95/cf3f7fe4910cf0365fa8ea0c731f4b8a624d97cd76ea777913ac8d0868e2/mkdocs_jupyter-0.26.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/16/5a/736dd2f4535dbf3bf26523f9158c011389ef88dd06ec2eef67fd744f1c7b/jupytext-1.19.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/17/8b/ed2f0f49385c3d7739cd4699954add26e8f09a372a0c3f04f2bde32fcea2/xarray_einstats-0.9.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1c/13/bc43591a54dd38ac5c19e7e849f0311d879737a0d07e032e5be79849a5fb/reportlab-4.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1c/59/964ecb8008722d27d8a835baea81f56a91cea8e097b3be992bc6ccde6367/versioningit-3.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/20/16/e777eadfa0c0305878c36fae1d5e6db474fbb15dae202b9ec378809dfb4d/nbstripout-0.9.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/20/17/ec40d981705654853726e7ac9aea9ddbb4a5d9cf54d8472222f4f3de06c2/pandas-3.0.2-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/20/7a/1c6e3562dfd8950adbb11ffbc65d21e7c89d01a6e4f137fa981056de25c5/gitpython-3.1.50-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/21/0e/8459ca4413e1a21a06c97d134bfaf18adfd27cea068813dc0faae06cbf00/cssselect2-0.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/28/88/4789719fbbe166d12d345b3ac66b96105f10001b16e00a9765ba29261a21/nbqa-1.9.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/28/de/a3e710469772c6a89595fc52816da05c1e164b4c866a89e3cb82fb1b67c5/mkdocs_autorefs-1.4.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2a/2d/d4bf65e47cea8ff2c794a600c4fd1273a7902f268757c531e0ee9f18aa58/pooch-1.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2c/01/bc663630c510822c95c47a66af9fa7a443c295b47d5f041e5e6ae62ef659/mkdocs_material-9.7.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/30/d4/24d543ab8b8158b7f5a97113c831205f5c900c92c8762b1e7f44b7ea0405/python_discovery-1.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/32/28/79f0f8de97cce916d5ae88a7bee1ad724855e83e6019c0b4d5b3fabc80f3/mkdocstrings_python-2.0.3-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/33/b2/986d1220f6ee931e338d272bc1f3ec02cfe5f9b5fad84e95afdad57f1ebc/format_docstring-0.2.7-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/33/f0/3fe8c6e69135a845f4106f2ff8b6805638d4e85c264e70114e8126689587/tokenize_rt-6.2.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/38/7e/7b91c89a4cf0f543a83be978657afb20c86af6d725253e319589dcc4ce52/lmfit-1.3.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3a/eb/fea4d1d51c49832120f7f285d07306db3960f423a2612c6057caf3e8196f/pip-26.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3e/17/1f31d8562e6f970d64911f1abc330d233bc0c0601411cf7e19c1292be6da/spdx_headers-1.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/44/0b/0615dbedb98f5b32a35a53290fbdc6e22306968109278d7e58df82d7a9f6/numba-0.65.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/44/0d/f494a2ec62ab56ffd44da4f236db374421fe0cd1e39d9ebc9785751eb432/periodictable-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/44/1f/227f9cb7edcd3e14ab05928f3db00e9d595c0f269c87bf35f565ce44941b/arviz-0.23.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/46/27/5799b020e4cdfb25a7c951c06a96397c135efcdc21b78d853bbd9c814c7d/llvmlite-0.47.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/4a/1a/bea014b55c45c98c63dffd34d2dd01eed0a73ce5b175bd7b1ba798d13558/pyhanko-0.35.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/52/59/0782e51887ac6b07ffd1570e0364cf901ebc36345fea669969d2084baebb/simple_websocket-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/52/6a/57f43e054fb3d3a56ac9fc532bc684fc6169a26c75c353e65425b3e56eef/propcache-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/52/a0/c1f604538ff6db22a0690be2dc44ab59178e115f63c917794e529356ab23/h5py-3.16.0-cp311-cp311-manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/56/5b/53ca0fd447f73423c7dc59d34e523530ef434481a3d18808ff7537ad33ec/svglib-1.5.1.tar.gz + - pypi: https://files.pythonhosted.org/packages/5a/56/21b27c560c13822ed93133f08aa6372c53a8e067f11fbed37b4adcdac922/multidict-6.7.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5b/69/93b34728cc386efdde0c342f8c680b9187dea7beb7adaf6b58a0713be101/mpld3-0.5.12-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5f/4b/6157f24ca425b89fe2eb7e7be642375711ab671135be21e6faa100f7448c/contourpy-1.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/6c/dd/a834df6482147d48e225a49515aabc28974ad5a4ca3215c18a882565b028/html5lib-1.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6e/7d/77752d819ede528c664ecf4de974b17b049e054a14537b74b53c8568014f/arabic_reshaper-3.0.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6e/94/be70f8ee9c45f2f62b39a1f0e9303bc20e138a8f3b8e50ffd89498e177e1/mkdocstrings-1.0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/76/8e/56ccb09c7232a55403a7637caa21922f3b65901a37f5e8bdb405d0de0946/mike-2.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/79/db/e28c1b83e3680740aa78925f5fb2ae4d16207207419ad75ea9fe604f8676/matplotlib-3.10.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/7b/67/b1944235474aac3f0b0e1b232ce49547f9f9461ca4b943df1b88da5d3f1d/bumps-1.0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7e/a5/0521aa32c1ddf3aa1e71dcc466be0b7db2771907a13f18cddaa45967d97b/aiohttp-3.13.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/80/46/bddc13df6c2a40741e0cc7865bb1c9ed4796b6760bd04ce5fae3928ef917/kiwisolver-1.5.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-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/6f/cc2b231dc78d8c3aaa674a676db190b8f8071c50134af8f8cf39b9b8e8df/pydoclint-0.8.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8a/a1/8d812e53a5da1687abb10445275d41a8b13adb781bbf7196ddbcf8d88505/lazy_loader-0.5-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/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/90/ad/cba91b3bcf04073e4d1655a5c1710ef3f457f56f7d1b79dcc3d72f4dd912/plotly-6.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/92/be/b1afb692be85b947f3401375851484496134c5554e67e822c35f28bf2fbc/coverage-7.13.5-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/93/ca/d53764f0534ff857239595f090f4cb83b599d226cc326c7de5eb3d802715/xhtml2pdf-0.2.17-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/93/f7/d00d9b4a0313a6be3a3e0818e6375e15da6d7076f4ae47d1324e7ca986a1/radon-6.0.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/99/31/6cf181011dc738c33bf6ba7aea2e8e1d3c1f71b7dab1942f3054f66f6202/asteval-1.0.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/99/37/e8730c3587a65eb5645d4aba2d27aae48e8003614d6aaf15dda67f702f1f/bidict-0.23.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9a/64/c53487d9f4968045b8afa51aed7ca44f58b2589e772f32745f3744476c82/yarl-1.23.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9e/43/53afb8ba17218f19b77c7834128566c5bbb100a0ad9ba2e8e89d089d7079/autopep8-2.3.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a4/ce/3b6fee91c85626eaf769d617f1be9d2e15c1cca027bbdeb2e0d751469355/verspec-0.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a8/4e/c09876f08fa9faaa5e1178f3d77b7af3f343258689bd6f3b72593b2f74e3/mkdocs_markdownextradata_plugin-0.2.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/aa/54/0cce26da03a981f949bb8449c9778537f75f5917c172e1d2992ff25cb57d/python_engineio-4.13.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/af/78/bc3850efda66712370ebb2a52130111b5cbbdee7a5b9d1672aadfc73234f/orsopy-1.2.3-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b1/10/31932775c94a86814f76b41c4a772b52abfb0e6125324f32c6da1196c297/chardet-7.4.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/b1/4f/f71e641e504111a5a74e3a20bc52d01bd86788b22699dd3fee1c63253cf6/virtualenv-21.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b1/8b/88f16936a8e8070a83d36239555227ecd91728f9ef222c5382cda07e0fd6/h5netcdf-1.8.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/52/bc858b1665d0dec3a2511f4e6f5c18ea85c0977563d624d597c95d6d0fd7/jupyterquiz-2.9.6.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/b7/ee/e9c95cda829131f71a8dff5ce0406059fd16e591c074414e31ada19ba7c3/validate_pyproject-0.25-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-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/c1/d4/59e74daffcb57a07668852eeeb6035af9f32cbfd7a1d2511f17d2fe6a738/smmap-5.0.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c2/14/e2a54fabd4f08cd7af1c07030603c3356b74da07f7cc056e600436edfa17/tzlocal-5.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c2/20/053aa10bdc39747e1e923ce2d45413075e84f70a136045bb09e5eaca41d3/lxml-6.1.0-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/c6/3d/020a6b6248c3d4a37797db068256f0b3f15b01bc481327ba888c50309aa8/mkdocs_plugin_inline_svg-0.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c7/a0/5ff05d1919ca249508012cad89f08fdc6cfbdaa15b41651c5fe6dffaf1d3/dfo_ls-1.6.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c9/7f/09065fd9e27da0eda08b4d6897f1c13535066174cc023af248fc2a8d5e5a/asn1crypto-1.5.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c9/b5/9302644225ba7dfa934a2ff2b9c7bb85701313a90dddb3dfaf693fa5bae2/backrefs-7.0-py311-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-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/cf/c5/9fcb7e0e69cef59cf10c746b84f7d58b08bc66a6b7d459783c5a4f6101a6/numpy-2.4.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/d0/69/f24d3d1c38ad69e256138b4ec2452a8c7cf66be49dc214771ae99dd4f0a0/narwhals-2.20.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d2/f0/834e479e47e499b6478e807fb57b31cc2db696c4db30557bb6f5aea4a90b/mando-0.7.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d4/06/19ff1efd58b85906149ce83dfddce23252cea5bec7e0fa5f834336cfe836/scipp-26.3.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d6/bb/fbdc4e57731efb86b4209ffdd2519520782bf27b3c961beac3e5c20d2b87/trove_classifiers-2026.4.28.13-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/da/e0/6cc2e852837cd6086fe7d8406af4294e66827a60a4cf60b86575a4a65ca8/msgpack-1.1.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/dc/83/6d810a8a9ebc9c307989b418840c20e46907c74d707beb67ab566773e6fc/xarray-2026.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e4/bc/daa30c02069eeac5b9198985ba42f5d65ca71bed6705b18329e51d352b7c/plopp-26.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e6/90/90a65e6ae1b6e66183b48874d32509fc306c994beecc7924a6fa3d9f8955/easyscience-2.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e9/bd/e51a61b1054f09437acfbc2ff9106c30d1eb76bc1453d428399946781253/pillow-12.2.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/eb/be/b257e12f9710819fde40adc972578bee6b72c5992da1bc8369bef2597756/nbmake-1.5.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f4/99/722e90c586455733fa860d092722ef074f3a65449fe9787b7b7e2dafcd1f/pyhanko_certvalidator-0.31.1-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/f6/f0/10642828a8dfb741e5f3fbaac830550a518a775c7fff6f04a007259b0548/py-1.11.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f7/27/a2fc51a4a122dfd1015e921ae9d22fee3d20b0b8080d9a704578bf9deece/pymdown_extensions-10.21.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f7/ee/d0348d8dd27ac7f001e9583c004f1b245826b04cc23b5ce6b7867c4a1cc6/refnx-0.1.63-cp311-abi3-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/86/dd6e5db36df29e76c7a7699123569a4a18c1623ce68d826ed96c62643cae/mdit_py_plugins-0.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/d9/8f612389178e1e1800aaed85537b024ebe28b9a82fff6a015825e51a8877/refl1d-1.0.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fd/20/2870f2e63da2b74eda896053809065769b256646bb3f4b74e06d013ad590/uritools-6.1.0-py3-none-any.whl + p1: + - conda: https://conda.anaconda.org/conda-forge/noarch/annotated-types-0.7.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.13.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/appnope-0.1.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/argon2-cffi-25.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/arrow-1.4.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.18.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.14.3-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.3.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-with-css-6.3.0-hbca2aae_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.4.22-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cached-property-1.5.2-hd8ed1ab_1.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/cached_property-1.5.2-pyha770c72_1.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2026.4.22-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.7-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.3-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/copier-9.15.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/decorator-5.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/defusedxml-0.7.1-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/dunamai-1.26.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/eval-type-backport-0.2.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/eval_type_backport-0.2.2-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fqdn-1.5.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/funcy-2.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h11-0.16.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/httpcore-1.0.9-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/httpx-0.28.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.13-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.8.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib_resources-7.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/invoke-3.0.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.2.0-pyh5552912_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.13.0-pyh53cf698_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython_pygments_lexers-1.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/isoduration-20.11.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jedi-0.19.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-ansible-filters-1.3.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/json5-0.14.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonpointer-3.1.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.26.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.9.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-with-format-nongpl-4.26.0-hcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter-lsp-2.3.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.8.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.9.1-pyhc90fa1f_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_events-0.12.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.18.2-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server_terminals-0.5.4-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.5.7-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_pygments-0.3.0-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_server-2.28.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/lark-1.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mistune-3.2.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbclient-0.10.4-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbconvert-core-7.17.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbformat-5.10.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/notebook-shim-0.2.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/overrides-7.7.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.2-pyhc364b38_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pandocfilters-1.5.0-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/paramiko-4.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.7-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pathspec-1.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pexpect-4.9.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pixi-kernel-0.7.1-pyhbbac1ac_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.9.6-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/plumbum-1.10.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prometheus_client-0.25.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.51-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prompt_toolkit-3.0.51-hd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ptyprocess-0.7.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pydantic-2.13.4-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.21.2-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-json-logger-3.2.1-pyh332efcf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2026.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.11-8_cp311.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pywin32-on-windows-0.1.0-pyh1179c8e_3.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/questionary-2.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.37.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.33.1-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/returns-0.27.0-pyhc364b38_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3339-validator-0.1.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3986-validator-0.1.1-pyh9f0ad1d_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3987-syntax-1.1.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/send2trash-2.1.0-pyh5552912_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-82.0.1-pyh332efcf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sniffio-1.3.1-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.8.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyhc90fa1f_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tinycss2-1.4.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.15.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.15.0-h396c80c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-inspection-0.4.2-pyhcf101f3_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_utils-0.1.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/uri-template-1.3.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.6.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.7.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/webcolors-25.10.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/webencodings-0.5.1-pyhd8ed1ab_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/websocket-client-1.9.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/argon2-cffi-bindings-25.1.0-py311h9408147_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/backports.zstd-1.4.0-py311h36d4fbb_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bcrypt-5.0.0-py311h71babbd_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-python-1.2.0-py311hdc60ec4_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-hd037594_9.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/c-ares-1.34.6-hc919400_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cffi-2.0.0-py311hd10dc20_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cryptography-48.0.0-py311hbb43c59_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/debugpy-1.8.20-py311h8948835_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/icu-78.3-hef89b57_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/krb5-1.22.2-h385eeb1_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libabseil-20260107.1-cxx17_h2062a1b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlicommon-1.2.0-hc919400_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlidec-1.2.0-hc919400_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlienc-1.2.0-hc919400_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-22.1.5-h55c6f16_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libedit-3.1.20250104-pl5321hafb1f1b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libev-4.33-h93a5062_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.8.0-hf6b4638_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.5.2-hcf2aa1b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.3-h8088a28_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libnghttp2-1.68.1-h8f3e76b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsodium-1.0.21-h1a92334_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.53.1-h1b79a29_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libuv-1.51.0-h6caf38d_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.2-h8088a28_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/markupsafe-3.0.3-py311hc290fe0_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/msgspec-0.21.1-py311hc949640_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.6-h1d4f5a5_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/nodejs-25.8.2-h7039424_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.6.2-hd24854e_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/psutil-7.2.2-py311he363849_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pydantic-core-2.46.4-py311hf7c400d_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pynacl-1.6.2-py311h38a6bc0_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyobjc-core-12.1-py311hce6e4fa_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyobjc-framework-cocoa-12.1-py311h9049b8e_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.11.15-h8561d8f_0_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyyaml-6.0.3-py311hc290fe0_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyzmq-27.1.0-py311h745ac33_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.3-h46df422_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/rpds-py-0.30.0-py311h71babbd_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ruff-0.15.12-hbd3f8a3_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h010d191_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tornado-6.5.5-py311hc949640_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/yaml-0.2.5-h925e9cb_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zeromq-4.3.5-h4818236_10.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hbf9d68e_6.conda + - pypi: ./ + - pypi: https://files.pythonhosted.org/packages/01/7c/fa07d3da2b6253eb8474be16eab2eadf670460e364ccc895ca7ff388ee30/oscrypto-1.3.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/07/c7/deb8c5e604404dbf10a3808a858946ca3547692ff6316b698945bb72177e/python_socketio-5.16.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0a/aa/510dc933d87767584abfe03efa445889996c70c2990f6f87c3ebaa0a18c5/kiwisolver-1.5.0-cp311-cp311-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/0c/d6/1d5c60cc17bbdf37c1552d9c03862fc6d32c5836732a0415b2d637edc2d0/pypdf-6.10.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0d/44/c4b0b6095fef4dc9c420e041799591e3b63e9619e3044f7f4f6c21c0ab24/contourpy-1.3.3-cp311-cp311-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/0d/fe/6bea5c9162869c5beba5d9c8abbed835ec85bf1ec1fba05a3822325c45f3/build-1.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/11/8c/c9138d881c79aa0ea9ed83cbd58d5ca75624378b38cee225dcf5c42cc91f/griffelib-2.0.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/12/c9/6869a1dcf4aaf309b9543ec070be3ec3adebee7c9bec9af8c230494134b9/interrogate-1.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/13/95/cf3f7fe4910cf0365fa8ea0c731f4b8a624d97cd76ea777913ac8d0868e2/mkdocs_jupyter-0.26.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/14/4b/d3c79495dee4831b8bebca2790e72cb90f0c5849c940570a7c7e5b70b952/chardet-7.4.3-cp311-cp311-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/16/5a/736dd2f4535dbf3bf26523f9158c011389ef88dd06ec2eef67fd744f1c7b/jupytext-1.19.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/17/8b/ed2f0f49385c3d7739cd4699954add26e8f09a372a0c3f04f2bde32fcea2/xarray_einstats-0.9.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1c/13/bc43591a54dd38ac5c19e7e849f0311d879737a0d07e032e5be79849a5fb/reportlab-4.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1c/59/964ecb8008722d27d8a835baea81f56a91cea8e097b3be992bc6ccde6367/versioningit-3.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/20/16/e777eadfa0c0305878c36fae1d5e6db474fbb15dae202b9ec378809dfb4d/nbstripout-0.9.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/20/7a/1c6e3562dfd8950adbb11ffbc65d21e7c89d01a6e4f137fa981056de25c5/gitpython-3.1.50-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/21/0e/8459ca4413e1a21a06c97d134bfaf18adfd27cea068813dc0faae06cbf00/cssselect2-0.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/28/88/4789719fbbe166d12d345b3ac66b96105f10001b16e00a9765ba29261a21/nbqa-1.9.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/28/de/a3e710469772c6a89595fc52816da05c1e164b4c866a89e3cb82fb1b67c5/mkdocs_autorefs-1.4.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2a/2d/d4bf65e47cea8ff2c794a600c4fd1273a7902f268757c531e0ee9f18aa58/pooch-1.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2c/01/bc663630c510822c95c47a66af9fa7a443c295b47d5f041e5e6ae62ef659/mkdocs_material-9.7.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/30/d4/24d543ab8b8158b7f5a97113c831205f5c900c92c8762b1e7f44b7ea0405/python_discovery-1.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/32/28/79f0f8de97cce916d5ae88a7bee1ad724855e83e6019c0b4d5b3fabc80f3/mkdocstrings_python-2.0.3-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/33/b2/986d1220f6ee931e338d272bc1f3ec02cfe5f9b5fad84e95afdad57f1ebc/format_docstring-0.2.7-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/33/f0/3fe8c6e69135a845f4106f2ff8b6805638d4e85c264e70114e8126689587/tokenize_rt-6.2.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/34/0b/b9d1911cfefa61399821dfb37f486d83e0f42630a8d12f7194270c417002/llvmlite-0.47.0-cp311-cp311-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/35/8b/cd129b0ca4afe886a6ce9d183c44d8301acbd4ef248622e7c49a23145605/coverage-7.13.5-cp311-cp311-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/38/7e/7b91c89a4cf0f543a83be978657afb20c86af6d725253e319589dcc4ce52/lmfit-1.3.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3a/eb/fea4d1d51c49832120f7f285d07306db3960f423a2612c6057caf3e8196f/pip-26.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3e/17/1f31d8562e6f970d64911f1abc330d233bc0c0601411cf7e19c1292be6da/spdx_headers-1.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3e/9a/a9383ff12698d5306522122e7007d9fd58f903e00a7715506e3ff5be1678/python_bidi-0.6.9-cp311-cp311-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/44/0d/f494a2ec62ab56ffd44da4f236db374421fe0cd1e39d9ebc9785751eb432/periodictable-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/44/1f/227f9cb7edcd3e14ab05928f3db00e9d595c0f269c87bf35f565ce44941b/arviz-0.23.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/47/a1/d5ff69e747374c33a3b53b9f98cca7889fce1fd03d79cdc4e1bccc6c5a87/pillow-12.2.0-cp311-cp311-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/4a/1a/bea014b55c45c98c63dffd34d2dd01eed0a73ce5b175bd7b1ba798d13558/pyhanko-0.35.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/51/18/325cd32ece1120d1da51cc4e4294c6580190699490183fc2fe8cb6d61ec5/matplotlib-3.10.9-cp311-cp311-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/52/59/0782e51887ac6b07ffd1570e0364cf901ebc36345fea669969d2084baebb/simple_websocket-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/56/5b/53ca0fd447f73423c7dc59d34e523530ef434481a3d18808ff7537ad33ec/svglib-1.5.1.tar.gz + - pypi: https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5b/69/93b34728cc386efdde0c342f8c680b9187dea7beb7adaf6b58a0713be101/mpld3-0.5.12-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5e/5d/3bccad330292946f97962df9d5f2d3ae129cce6e212732a781e856b91e07/lxml-6.1.0-cp311-cp311-macosx_10_9_universal2.whl + - pypi: https://files.pythonhosted.org/packages/60/54/5011adb56853caabfd90686c2e543d1e3c76a8ef2755809b7e12e3f3583b/scipp-26.3.1-cp311-cp311-macosx_14_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/6c/dd/a834df6482147d48e225a49515aabc28974ad5a4ca3215c18a882565b028/html5lib-1.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6d/ee/18146b7757ed4976276b9c9819108adbc73c5aad636e5353e20746b73069/scipy-1.17.1-cp311-cp311-macosx_14_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/6e/7d/77752d819ede528c664ecf4de974b17b049e054a14537b74b53c8568014f/arabic_reshaper-3.0.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6e/94/be70f8ee9c45f2f62b39a1f0e9303bc20e138a8f3b8e50ffd89498e177e1/mkdocstrings-1.0.4-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/76/8e/56ccb09c7232a55403a7637caa21922f3b65901a37f5e8bdb405d0de0946/mike-2.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7b/67/b1944235474aac3f0b0e1b232ce49547f9f9461ca4b943df1b88da5d3f1d/bumps-1.0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/83/04/28a41024ccbd67467380b6fb440ae916c1e4f25e2cd4c63abe6835ac566e/msgpack-1.1.2-cp311-cp311-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/85/d7/9b6ac05350ab7f7d3a730ff143ff3e2cada54514117c37be37e26dc91242/docstripy-0.7.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/87/6f/cc2b231dc78d8c3aaa674a676db190b8f8071c50134af8f8cf39b9b8e8df/pydoclint-0.8.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/87/ec/e38ce072e724fd7add6243613f8d1810da084f54175353d25ccf9f9c7e5a/aiohttp-3.13.5-cp311-cp311-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.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/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8a/a1/8d812e53a5da1687abb10445275d41a8b13adb781bbf7196ddbcf8d88505/lazy_loader-0.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8a/dc/df98c095978fa6ee7b9a9387d1d58cbb3d232d0e69ad169a4ce784bde4fd/numpy-2.4.4-cp311-cp311-macosx_14_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/8f/5e/f1e1dd319e35e962a4e00b33150a8868b6329cc1d19fd533436ba5488f09/uncertainties-3.2.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/90/ad/cba91b3bcf04073e4d1655a5c1710ef3f457f56f7d1b79dcc3d72f4dd912/plotly-6.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/90/df/cbd75ba033d712a7bbe8efff2a764c15a3904003f78ed3c19f259c91a4a4/refnx-0.1.63-cp311-abi3-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/93/ca/d53764f0534ff857239595f090f4cb83b599d226cc326c7de5eb3d802715/xhtml2pdf-0.2.17-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/93/f7/d00d9b4a0313a6be3a3e0818e6375e15da6d7076f4ae47d1324e7ca986a1/radon-6.0.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/96/b3/650500c2eab4534d98e9166f4298e0f3c69c742afdf24e6eabccd1f16ad8/numba-0.65.1-cp311-cp311-macosx_12_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/99/31/6cf181011dc738c33bf6ba7aea2e8e1d3c1f71b7dab1942f3054f66f6202/asteval-1.0.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/99/37/e8730c3587a65eb5645d4aba2d27aae48e8003614d6aaf15dda67f702f1f/bidict-0.23.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9e/43/53afb8ba17218f19b77c7834128566c5bbb100a0ad9ba2e8e89d089d7079/autopep8-2.3.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a4/ce/3b6fee91c85626eaf769d617f1be9d2e15c1cca027bbdeb2e0d751469355/verspec-0.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a8/4e/c09876f08fa9faaa5e1178f3d77b7af3f343258689bd6f3b72593b2f74e3/mkdocs_markdownextradata_plugin-0.2.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/aa/54/0cce26da03a981f949bb8449c9778537f75f5917c172e1d2992ff25cb57d/python_engineio-4.13.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/af/78/bc3850efda66712370ebb2a52130111b5cbbdee7a5b9d1672aadfc73234f/orsopy-1.2.3-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b1/4f/f71e641e504111a5a74e3a20bc52d01bd86788b22699dd3fee1c63253cf6/virtualenv-21.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b1/8b/88f16936a8e8070a83d36239555227ecd91728f9ef222c5382cda07e0fd6/h5netcdf-1.8.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b2/0d/71ceabc14c146ba8ee3804ca7b3d42b1664c8440439de5214d366fec7d3a/yarl-1.23.0-cp311-cp311-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/52/bc858b1665d0dec3a2511f4e6f5c18ea85c0977563d624d597c95d6d0fd7/jupyterquiz-2.9.6.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/ee/e9c95cda829131f71a8dff5ce0406059fd16e591c074414e31ada19ba7c3/validate_pyproject-0.25-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bf/3b/38ff88b347c3e346cda1d3fc1b65a7aa75d40632228d8b8a5d7b58508c24/h5py-3.16.0-cp311-cp311-macosx_11_0_arm64.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/c1/d4/59e74daffcb57a07668852eeeb6035af9f32cbfd7a1d2511f17d2fe6a738/smmap-5.0.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c2/14/e2a54fabd4f08cd7af1c07030603c3356b74da07f7cc056e600436edfa17/tzlocal-5.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c4/d3/b7da1d5d7dbdc5ef52ed7debd2b484313b832982266905315dad5a0bf0b1/pandas-3.0.2-cp311-cp311-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/c6/3d/020a6b6248c3d4a37797db068256f0b3f15b01bc481327ba888c50309aa8/mkdocs_plugin_inline_svg-0.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c7/a0/5ff05d1919ca249508012cad89f08fdc6cfbdaa15b41651c5fe6dffaf1d3/dfo_ls-1.6.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c9/7f/09065fd9e27da0eda08b4d6897f1c13535066174cc023af248fc2a8d5e5a/asn1crypto-1.5.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c9/b5/9302644225ba7dfa934a2ff2b9c7bb85701313a90dddb3dfaf693fa5bae2/backrefs-7.0-py311-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d0/69/f24d3d1c38ad69e256138b4ec2452a8c7cf66be49dc214771ae99dd4f0a0/narwhals-2.20.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/d2/f0/834e479e47e499b6478e807fb57b31cc2db696c4db30557bb6f5aea4a90b/mando-0.7.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d3/1d/11605e99ac8ea9435651ee71ab4cb4bf03f0949586246476a25aadfec54a/propcache-0.4.1-cp311-cp311-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d6/bb/fbdc4e57731efb86b4209ffdd2519520782bf27b3c961beac3e5c20d2b87/trove_classifiers-2026.4.28.13-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/dc/83/6d810a8a9ebc9c307989b418840c20e46907c74d707beb67ab566773e6fc/xarray-2026.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/dd/a4/d45caf2b97b035c57267791ecfaafbd59c68212004b3842830954bb4b02e/multidict-6.7.1-cp311-cp311-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e4/bc/daa30c02069eeac5b9198985ba42f5d65ca71bed6705b18329e51d352b7c/plopp-26.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e6/90/90a65e6ae1b6e66183b48874d32509fc306c994beecc7924a6fa3d9f8955/easyscience-2.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/eb/be/b257e12f9710819fde40adc972578bee6b72c5992da1bc8369bef2597756/nbmake-1.5.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f4/99/722e90c586455733fa860d092722ef074f3a65449fe9787b7b7e2dafcd1f/pyhanko_certvalidator-0.31.1-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/f6/f0/10642828a8dfb741e5f3fbaac830550a518a775c7fff6f04a007259b0548/py-1.11.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f7/27/a2fc51a4a122dfd1015e921ae9d22fee3d20b0b8080d9a704578bf9deece/pymdown_extensions-10.21.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/86/dd6e5db36df29e76c7a7699123569a4a18c1623ce68d826ed96c62643cae/mdit_py_plugins-0.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/d9/8f612389178e1e1800aaed85537b024ebe28b9a82fff6a015825e51a8877/refl1d-1.0.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fd/20/2870f2e63da2b74eda896053809065769b256646bb3f4b74e06d013ad590/uritools-6.1.0-py3-none-any.whl + win-64: + - conda: https://conda.anaconda.org/conda-forge/noarch/annotated-types-0.7.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.13.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/argon2-cffi-25.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/arrow-1.4.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.18.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.14.3-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.3.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-with-css-6.3.0-hbca2aae_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.4.22-h4c7d964_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cached-property-1.5.2-hd8ed1ab_1.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/cached_property-1.5.2-pyha770c72_1.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2026.4.22-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.7-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.3-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/copier-9.15.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/decorator-5.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/defusedxml-0.7.1-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/dunamai-1.26.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/eval-type-backport-0.2.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/eval_type_backport-0.2.2-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fqdn-1.5.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/funcy-2.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h11-0.16.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/httpcore-1.0.9-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/httpx-0.28.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.13-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.8.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib_resources-7.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/invoke-3.0.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.2.0-pyh6dadd2b_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.13.0-pyhe2676ad_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython_pygments_lexers-1.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/isoduration-20.11.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jedi-0.19.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-ansible-filters-1.3.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/json5-0.14.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonpointer-3.1.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.26.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.9.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-with-format-nongpl-4.26.0-hcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter-lsp-2.3.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.8.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.9.1-pyh6dadd2b_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_events-0.12.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.18.2-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server_terminals-0.5.4-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.5.7-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_pygments-0.3.0-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_server-2.28.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/lark-1.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mistune-3.2.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbclient-0.10.4-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbconvert-core-7.17.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbformat-5.10.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/notebook-shim-0.2.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/overrides-7.7.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.2-pyhc364b38_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pandocfilters-1.5.0-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/paramiko-4.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.7-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pathspec-1.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pixi-kernel-0.7.1-pyhbbac1ac_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.9.6-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/plumbum-1.10.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prometheus_client-0.25.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.51-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prompt_toolkit-3.0.51-hd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pydantic-2.13.4-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyh09c184e_7.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.21.2-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-json-logger-3.2.1-pyh332efcf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2026.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.11-8_cp311.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pywin32-on-windows-0.1.0-pyh07e9846_2.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/questionary-2.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.37.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.33.1-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/returns-0.27.0-pyhc364b38_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3339-validator-0.1.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3986-validator-0.1.1-pyh9f0ad1d_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3987-syntax-1.1.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/send2trash-2.1.0-pyh6dadd2b_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-82.0.1-pyh332efcf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sniffio-1.3.1-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.8.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyh6dadd2b_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tinycss2-1.4.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.15.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.15.0-h396c80c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-inspection-0.4.2-pyhcf101f3_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_utils-0.1.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/uri-template-1.3.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.6.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.7.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/webcolors-25.10.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/webencodings-0.5.1-pyhd8ed1ab_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/websocket-client-1.9.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/win_inet_pton-1.1.0-pyh7428d3b_8.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/argon2-cffi-bindings-25.1.0-py311h3485c13_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/backports.zstd-1.4.0-py311h71c1bcc_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/bcrypt-5.0.0-py311hf51aa87_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/brotli-python-1.2.0-py311hc5da9e4_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h0ad9c76_9.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/cffi-2.0.0-py311h3485c13_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/cryptography-48.0.0-py311h2098ed6_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/debugpy-1.8.20-py311h5dfdfe8_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/krb5-1.22.2-h0ea6238_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.8.0-hac47afa_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libffi-3.5.2-h3d046cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-5.8.3-hfd05255_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libsodium-1.0.21-h6a83c73_3.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.53.1-hf5d6505_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libzlib-1.3.2-hfd05255_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/markupsafe-3.0.3-py311h3f79411_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/msgspec-0.21.1-py311h3485c13_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/nodejs-25.8.2-h80d1838_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.6.2-hf411b9b_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/psutil-7.2.2-py311hf893f09_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pydantic-core-2.46.4-py311hf51aa87_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pynacl-1.6.2-py311hcd7b28f_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/python-3.11.15-h0159041_0_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pywin32-311-py311hefeebc8_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pywinpty-2.0.15-py311hda3d55a_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pyyaml-6.0.3-py311h3f79411_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pyzmq-27.1.0-py311hff25285_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/rpds-py-0.30.0-py311hf51aa87_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/ruff-0.15.12-hd7ccaa8_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h6ed50ae_3.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/tornado-6.5.5-py311h3485c13_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.26100.0-h57928b3_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-h41ae7f8_34.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.44.35208-h818238b_34.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.44.35208-h818238b_34.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/winpty-0.4.3-4.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/win-64/yaml-0.2.5-h6a83c73_3.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/zeromq-4.3.5-h507cc87_10.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_6.conda + - pypi: ./ + - pypi: https://files.pythonhosted.org/packages/01/7c/fa07d3da2b6253eb8474be16eab2eadf670460e364ccc895ca7ff388ee30/oscrypto-1.3.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/02/05/d60c732b56da5085175c07c74b2df4e6d181b0c9a61e1691474f06ef4b39/lxml-6.1.0-cp311-cp311-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/07/c7/deb8c5e604404dbf10a3808a858946ca3547692ff6316b698945bb72177e/python_socketio-5.16.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/0c/d6/1d5c60cc17bbdf37c1552d9c03862fc6d32c5836732a0415b2d637edc2d0/pypdf-6.10.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0d/fe/6bea5c9162869c5beba5d9c8abbed835ec85bf1ec1fba05a3822325c45f3/build-1.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/11/8c/c9138d881c79aa0ea9ed83cbd58d5ca75624378b38cee225dcf5c42cc91f/griffelib-2.0.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/12/c9/6869a1dcf4aaf309b9543ec070be3ec3adebee7c9bec9af8c230494134b9/interrogate-1.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/13/95/cf3f7fe4910cf0365fa8ea0c731f4b8a624d97cd76ea777913ac8d0868e2/mkdocs_jupyter-0.26.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/16/5a/736dd2f4535dbf3bf26523f9158c011389ef88dd06ec2eef67fd744f1c7b/jupytext-1.19.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/17/8b/ed2f0f49385c3d7739cd4699954add26e8f09a372a0c3f04f2bde32fcea2/xarray_einstats-0.9.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1c/13/bc43591a54dd38ac5c19e7e849f0311d879737a0d07e032e5be79849a5fb/reportlab-4.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1c/59/964ecb8008722d27d8a835baea81f56a91cea8e097b3be992bc6ccde6367/versioningit-3.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/20/16/e777eadfa0c0305878c36fae1d5e6db474fbb15dae202b9ec378809dfb4d/nbstripout-0.9.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/20/7a/1c6e3562dfd8950adbb11ffbc65d21e7c89d01a6e4f137fa981056de25c5/gitpython-3.1.50-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/21/0e/8459ca4413e1a21a06c97d134bfaf18adfd27cea068813dc0faae06cbf00/cssselect2-0.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/28/88/4789719fbbe166d12d345b3ac66b96105f10001b16e00a9765ba29261a21/nbqa-1.9.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/28/de/a3e710469772c6a89595fc52816da05c1e164b4c866a89e3cb82fb1b67c5/mkdocs_autorefs-1.4.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2a/2d/d4bf65e47cea8ff2c794a600c4fd1273a7902f268757c531e0ee9f18aa58/pooch-1.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2a/79/309d0e637f6f37e83c711f547308b91af02b72d2326ddd860b966080ef29/msgpack-1.1.2-cp311-cp311-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/2c/01/bc663630c510822c95c47a66af9fa7a443c295b47d5f041e5e6ae62ef659/mkdocs_material-9.7.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/30/d4/24d543ab8b8158b7f5a97113c831205f5c900c92c8762b1e7f44b7ea0405/python_discovery-1.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/32/28/79f0f8de97cce916d5ae88a7bee1ad724855e83e6019c0b4d5b3fabc80f3/mkdocstrings_python-2.0.3-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/33/b2/986d1220f6ee931e338d272bc1f3ec02cfe5f9b5fad84e95afdad57f1ebc/format_docstring-0.2.7-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/33/f0/3fe8c6e69135a845f4106f2ff8b6805638d4e85c264e70114e8126689587/tokenize_rt-6.2.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/38/7e/7b91c89a4cf0f543a83be978657afb20c86af6d725253e319589dcc4ce52/lmfit-1.3.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3a/eb/fea4d1d51c49832120f7f285d07306db3960f423a2612c6057caf3e8196f/pip-26.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3e/17/1f31d8562e6f970d64911f1abc330d233bc0c0601411cf7e19c1292be6da/spdx_headers-1.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/44/0d/f494a2ec62ab56ffd44da4f236db374421fe0cd1e39d9ebc9785751eb432/periodictable-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/44/1f/227f9cb7edcd3e14ab05928f3db00e9d595c0f269c87bf35f565ce44941b/arviz-0.23.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/44/a0/97a6339859d4acb2536efb24feb6708e82f7d33b2ed7e036f2983fcced82/pandas-3.0.2-cp311-cp311-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/4a/1a/bea014b55c45c98c63dffd34d2dd01eed0a73ce5b175bd7b1ba798d13558/pyhanko-0.35.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/52/59/0782e51887ac6b07ffd1570e0364cf901ebc36345fea669969d2084baebb/simple_websocket-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5b/69/93b34728cc386efdde0c342f8c680b9187dea7beb7adaf6b58a0713be101/mpld3-0.5.12-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5d/a6/e9b8f8a3e99602792b01fa7d0a731737615ab56d8bfd0b52935a0ef88b85/chardet-7.4.3-cp311-cp311-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/69/42/836b6f3cd7f3e5fa10a1f1a5420447c17966044c8fbf589cc0452d5502db/pillow-12.2.0-cp311-cp311-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/6a/00/d26b61e82163a59334e9f2ae31ba95ac01922da3792f090e11762d4422e8/rlpycairo-0.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6c/dd/a834df6482147d48e225a49515aabc28974ad5a4ca3215c18a882565b028/html5lib-1.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6d/93/01273a1b8d8454d45f2e18b3d6098c7be13a0864a55fbd0ebda7815c201a/svglib-1.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6e/7d/77752d819ede528c664ecf4de974b17b049e054a14537b74b53c8568014f/arabic_reshaper-3.0.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6e/94/be70f8ee9c45f2f62b39a1f0e9303bc20e138a8f3b8e50ffd89498e177e1/mkdocstrings-1.0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/76/8e/56ccb09c7232a55403a7637caa21922f3b65901a37f5e8bdb405d0de0946/mike-2.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7b/67/b1944235474aac3f0b0e1b232ce49547f9f9461ca4b943df1b88da5d3f1d/bumps-1.0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7e/0d/271aace3342157c64700c9ff4c59c7b392f3dbab393692e8db6fbe7ab96c/matplotlib-3.10.9-cp311-cp311-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-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/6f/cc2b231dc78d8c3aaa674a676db190b8f8071c50134af8f8cf39b9b8e8df/pydoclint-0.8.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8a/46/7e2d150dd574039c2f8d09c4e102bb90a6cb6634be4040fc29f212fc42bf/refnx-0.1.63-cp311-abi3-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/8a/a1/8d812e53a5da1687abb10445275d41a8b13adb781bbf7196ddbcf8d88505/lazy_loader-0.5-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/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/90/ad/cba91b3bcf04073e4d1655a5c1710ef3f457f56f7d1b79dcc3d72f4dd912/plotly-6.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/93/22/b85eca6fa2ad9491af48c973e4c8cf6b103a73dbb271fe3346949449fca0/yarl-1.23.0-cp311-cp311-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/93/6e/bd7fbfacca077bc6f34f1a1109800a2c41ab50f4704d3a0507ba41009915/freetype_py-2.5.1-py3-none-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/93/ca/d53764f0534ff857239595f090f4cb83b599d226cc326c7de5eb3d802715/xhtml2pdf-0.2.17-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/93/f7/d00d9b4a0313a6be3a3e0818e6375e15da6d7076f4ae47d1324e7ca986a1/radon-6.0.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/95/da/0d1df507cf574b3f224ccc3d45244c9a1d732c81dcb26b1e8a766ae271a8/scipy-1.17.1-cp311-cp311-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/98/4b/9bd370b004b5c9d8045c6c33cf65bae018b27aca550a3f657cdc99acdbd8/contourpy-1.3.3-cp311-cp311-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/99/31/6cf181011dc738c33bf6ba7aea2e8e1d3c1f71b7dab1942f3054f66f6202/asteval-1.0.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/99/37/e8730c3587a65eb5645d4aba2d27aae48e8003614d6aaf15dda67f702f1f/bidict-0.23.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9e/43/53afb8ba17218f19b77c7834128566c5bbb100a0ad9ba2e8e89d089d7079/autopep8-2.3.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a2/50/59227d06bdc96e23322713c381af4e77420949d8cd8a042c79e0043096cc/llvmlite-0.47.0-cp311-cp311-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/a4/ce/3b6fee91c85626eaf769d617f1be9d2e15c1cca027bbdeb2e0d751469355/verspec-0.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a8/4e/c09876f08fa9faaa5e1178f3d77b7af3f343258689bd6f3b72593b2f74e3/mkdocs_markdownextradata_plugin-0.2.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/aa/54/0cce26da03a981f949bb8449c9778537f75f5917c172e1d2992ff25cb57d/python_engineio-4.13.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ac/a6/1ca2d290381911c17975ba00353671217bb9ed6c56f395871b274435b020/python_bidi-0.6.9-cp311-cp311-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/af/78/bc3850efda66712370ebb2a52130111b5cbbdee7a5b9d1672aadfc73234f/orsopy-1.2.3-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/af/7f/4cd8a92531253f9d7c1bbecd9fa1b472907fb54446ca768c59b531248dc5/coverage-7.13.5-cp311-cp311-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/b1/4f/f71e641e504111a5a74e3a20bc52d01bd86788b22699dd3fee1c63253cf6/virtualenv-21.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b1/8b/88f16936a8e8070a83d36239555227ecd91728f9ef222c5382cda07e0fd6/h5netcdf-1.8.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/52/bc858b1665d0dec3a2511f4e6f5c18ea85c0977563d624d597c95d6d0fd7/jupyterquiz-2.9.6.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/ee/e9c95cda829131f71a8dff5ce0406059fd16e591c074414e31ada19ba7c3/validate_pyproject-0.25-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bd/63/05d193dbb4b5eec1eca73822d80da98b511f8328ad4ae3ca4caf0f4db91d/numpy-2.4.4-cp311-cp311-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/bd/9a/af61ec03b3116c161fd7a06b9e8a265729a8718458333e8ffbb06d9a3978/numba-0.65.1-cp311-cp311-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/be/6c/28f17390b62b8f2f520e2915095b3c94d88681ecf0041e75389d9667f202/kiwisolver-1.5.0-cp311-cp311-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/bf/50/98b146aea0f1cd7531d25f12bea69fa9ce8d1662124f93fb30dc4511b65e/docstring_parser_fork-0.0.14-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c0/43/8c7163a596dab4f8be12c190cf467a1e07e4734cf90eebb39f7f5d53fc6a/aiohttp-3.13.5-cp311-cp311-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/c1/d4/59e74daffcb57a07668852eeeb6035af9f32cbfd7a1d2511f17d2fe6a738/smmap-5.0.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c2/14/e2a54fabd4f08cd7af1c07030603c3356b74da07f7cc056e600436edfa17/tzlocal-5.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c6/3d/020a6b6248c3d4a37797db068256f0b3f15b01bc481327ba888c50309aa8/mkdocs_plugin_inline_svg-0.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c7/a0/5ff05d1919ca249508012cad89f08fdc6cfbdaa15b41651c5fe6dffaf1d3/dfo_ls-1.6.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c9/68/f16a3a8ba6f7b6dc92a1f19669c0810bd2c43fc5a02da13b1cbf8e253845/multidict-6.7.1-cp311-cp311-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/c9/7f/09065fd9e27da0eda08b4d6897f1c13535066174cc023af248fc2a8d5e5a/asn1crypto-1.5.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c9/b5/9302644225ba7dfa934a2ff2b9c7bb85701313a90dddb3dfaf693fa5bae2/backrefs-7.0-py311-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d0/69/f24d3d1c38ad69e256138b4ec2452a8c7cf66be49dc214771ae99dd4f0a0/narwhals-2.20.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d2/f0/834e479e47e499b6478e807fb57b31cc2db696c4db30557bb6f5aea4a90b/mando-0.7.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d3/97/bf54c5b3f2be34e1f143e6db838dfdc54f2ffa3e68c738934c82f3b2a08d/fonttools-4.62.1-cp311-cp311-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d6/bb/fbdc4e57731efb86b4209ffdd2519520782bf27b3c961beac3e5c20d2b87/trove_classifiers-2026.4.28.13-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/db/09/a0ab6a246a7ede89e817d749a941df34f27a74bedf15551da51e86ae105e/pycairo-1.29.0-cp311-cp311-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/dc/83/6d810a8a9ebc9c307989b418840c20e46907c74d707beb67ab566773e6fc/xarray-2026.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e4/bc/daa30c02069eeac5b9198985ba42f5d65ca71bed6705b18329e51d352b7c/plopp-26.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e6/0d/8882a4c7a5ebe59a46b709e82411d9c730d67250d41a2e11bc4bcd4d431d/scipp-26.3.1-cp311-cp311-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/e6/90/90a65e6ae1b6e66183b48874d32509fc306c994beecc7924a6fa3d9f8955/easyscience-2.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/eb/be/b257e12f9710819fde40adc972578bee6b72c5992da1bc8369bef2597756/nbmake-1.5.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f4/78/6cce448e2098e9f3bfc91bb877f06aa24b6ccace872e39c53b2f707c4648/propcache-0.4.1-cp311-cp311-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/f4/99/722e90c586455733fa860d092722ef074f3a65449fe9787b7b7e2dafcd1f/pyhanko_certvalidator-0.31.1-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/f6/f0/10642828a8dfb741e5f3fbaac830550a518a775c7fff6f04a007259b0548/py-1.11.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f7/20/e6c0ff62ca2ad1a396a34f4380bafccaaf8791ff8fccf3d995a1fc12d417/h5py-3.16.0-cp311-cp311-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/f7/27/a2fc51a4a122dfd1015e921ae9d22fee3d20b0b8080d9a704578bf9deece/pymdown_extensions-10.21.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/86/dd6e5db36df29e76c7a7699123569a4a18c1623ce68d826ed96c62643cae/mdit_py_plugins-0.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/d9/8f612389178e1e1800aaed85537b024ebe28b9a82fff6a015825e51a8877/refl1d-1.0.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fd/20/2870f2e63da2b74eda896053809065769b256646bb3f4b74e06d013ad590/uritools-6.1.0-py3-none-any.whl + py-313-env: + channels: + - url: https://conda.anaconda.org/nodefaults/ + - url: https://conda.anaconda.org/conda-forge/ + indexes: + - https://pypi.org/simple + packages: + linux-64: + - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/argon2-cffi-bindings-25.1.0-py313h07c4f96_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/backports.zstd-1.4.0-py313h18e8e13_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/bcrypt-5.0.0-py313h843e2db_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.2.0-py313hf159716_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_9.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/c-ares-1.34.6-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cffi-2.0.0-py313hf46b229_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cryptography-48.0.0-py313heb322e3_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/debugpy-1.8.20-py313h5d5ffb9_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/icu-78.3-h33c6efd_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.3-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.22.2-ha1258a1_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45.1-default_hbd61a6d_102.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libabseil-20260107.1-cxx17_h7b12aa8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlicommon-1.2.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlidec-1.2.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlienc-1.2.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20250104-pl5321h7949ede_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libev-4.33-hd590300_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.8.0-hecca717_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h3435931_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-he0feb66_18.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.2.0-h69a702a_18.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-he0feb66_18.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.3-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libnghttp2-1.68.1-h877daf1_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsodium-1.0.21-h280c20c_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.53.1-h0c1763c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h934c35e_18.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.42-h5347b49_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libuv-1.51.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.2-h25fd6f3_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.3-py313h3dea7bd_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/msgspec-0.21.1-py313h07c4f96_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.6-hdb14827_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/nodejs-25.8.2-he4ff34a_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.2-h35e630c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/psutil-7.2.2-py313h54dd161_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pydantic-core-2.46.4-py313h843e2db_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pynacl-1.6.2-py313h5008379_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.13.13-h6add32d_100_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.3-py313h3dea7bd_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pyzmq-27.1.0-py312hda471dd_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.3-h853b02a_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/rpds-py-0.30.0-py313h843e2db_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ruff-0.15.12-h994f30f_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h366c992_103.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tornado-6.5.5-py313h07c4f96_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h280c20c_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zeromq-4.3.5-h41580af_10.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/annotated-types-0.7.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.13.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/argon2-cffi-25.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/arrow-1.4.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.18.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.14.3-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.3.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-with-css-6.3.0-hbca2aae_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.4.22-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cached-property-1.5.2-hd8ed1ab_1.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/cached_property-1.5.2-pyha770c72_1.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2026.4.22-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.7-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.3-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/copier-9.15.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.13.13-py313hd8ed1ab_100.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/decorator-5.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/defusedxml-0.7.1-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/dunamai-1.26.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/eval-type-backport-0.2.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/eval_type_backport-0.2.2-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fqdn-1.5.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/funcy-2.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h11-0.16.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/httpcore-1.0.9-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/httpx-0.28.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.13-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.8.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib_resources-7.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/invoke-3.0.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.2.0-pyha191276_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.13.0-pyh53cf698_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython_pygments_lexers-1.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/isoduration-20.11.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jedi-0.19.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-ansible-filters-1.3.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/json5-0.14.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonpointer-3.1.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.26.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.9.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-with-format-nongpl-4.26.0-hcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter-lsp-2.3.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.8.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.9.1-pyhc90fa1f_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_events-0.12.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.18.2-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server_terminals-0.5.4-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.5.7-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_pygments-0.3.0-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_server-2.28.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/lark-1.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mistune-3.2.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbclient-0.10.4-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbconvert-core-7.17.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbformat-5.10.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/notebook-shim-0.2.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/overrides-7.7.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.2-pyhc364b38_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pandocfilters-1.5.0-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/paramiko-4.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.7-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pathspec-1.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pexpect-4.9.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pixi-kernel-0.7.1-pyhbbac1ac_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.9.6-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/plumbum-1.10.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prometheus_client-0.25.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.51-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prompt_toolkit-3.0.51-hd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ptyprocess-0.7.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pydantic-2.13.4-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.21.2-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.13.13-h4df99d1_100.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-json-logger-3.2.1-pyh332efcf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2026.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pywin32-on-windows-0.1.0-pyh1179c8e_3.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/questionary-2.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.37.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.33.1-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/returns-0.27.0-pyhc364b38_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3339-validator-0.1.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3986-validator-0.1.1-pyh9f0ad1d_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3987-syntax-1.1.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/send2trash-2.1.0-pyha191276_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-82.0.1-pyh332efcf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sniffio-1.3.1-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.8.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyhc90fa1f_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tinycss2-1.4.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.15.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.15.0-h396c80c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-inspection-0.4.2-pyhcf101f3_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_utils-0.1.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/uri-template-1.3.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.6.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.7.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/webcolors-25.10.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/webencodings-0.5.1-pyhd8ed1ab_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/websocket-client-1.9.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.1-pyhcf101f3_0.conda + - pypi: ./ + - pypi: https://files.pythonhosted.org/packages/00/bb/90ba423612b6aa0adccc6b1874bcd4a9b44b660c0c16f346611e00f64ac3/backrefs-7.0-py313-none-any.whl + - pypi: https://files.pythonhosted.org/packages/01/7c/fa07d3da2b6253eb8474be16eab2eadf670460e364ccc895ca7ff388ee30/oscrypto-1.3.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/07/c7/deb8c5e604404dbf10a3808a858946ca3547692ff6316b698945bb72177e/python_socketio-5.16.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0c/d6/1d5c60cc17bbdf37c1552d9c03862fc6d32c5836732a0415b2d637edc2d0/pypdf-6.10.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0d/1f/d398de1612f7a611e22d743280339c9af4903675635e41be3370091c704b/arviz_stats-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0d/fe/6bea5c9162869c5beba5d9c8abbed835ec85bf1ec1fba05a3822325c45f3/build-1.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/11/8c/c9138d881c79aa0ea9ed83cbd58d5ca75624378b38cee225dcf5c42cc91f/griffelib-2.0.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/12/c5/cbb1ffefb20a93d3f0e1fdcda699fb84976210d411b008f97f48bf6ce27e/pandas-3.0.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/12/c9/6869a1dcf4aaf309b9543ec070be3ec3adebee7c9bec9af8c230494134b9/interrogate-1.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/13/95/cf3f7fe4910cf0365fa8ea0c731f4b8a624d97cd76ea777913ac8d0868e2/mkdocs_jupyter-0.26.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/16/5a/736dd2f4535dbf3bf26523f9158c011389ef88dd06ec2eef67fd744f1c7b/jupytext-1.19.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/19/d4/225027a913621a879b429a043674aa35220e6ce67785acad4f7bd0c4ff33/xarray_einstats-0.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1c/13/bc43591a54dd38ac5c19e7e849f0311d879737a0d07e032e5be79849a5fb/reportlab-4.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1c/59/964ecb8008722d27d8a835baea81f56a91cea8e097b3be992bc6ccde6367/versioningit-3.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/20/16/e777eadfa0c0305878c36fae1d5e6db474fbb15dae202b9ec378809dfb4d/nbstripout-0.9.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/20/7a/1c6e3562dfd8950adbb11ffbc65d21e7c89d01a6e4f137fa981056de25c5/gitpython-3.1.50-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/21/0e/8459ca4413e1a21a06c97d134bfaf18adfd27cea068813dc0faae06cbf00/cssselect2-0.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/28/88/4789719fbbe166d12d345b3ac66b96105f10001b16e00a9765ba29261a21/nbqa-1.9.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/28/de/a3e710469772c6a89595fc52816da05c1e164b4c866a89e3cb82fb1b67c5/mkdocs_autorefs-1.4.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2a/2d/d4bf65e47cea8ff2c794a600c4fd1273a7902f268757c531e0ee9f18aa58/pooch-1.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2b/0a/7b98e1e119878a27ba8618ca1e18b14f992ff1eda40f47bccccf4de44121/kiwisolver-1.5.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/2c/01/bc663630c510822c95c47a66af9fa7a443c295b47d5f041e5e6ae62ef659/mkdocs_material-9.7.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2e/75/5604f4d17ab607510d4702f156329194d8edfff7e29644ca9200b085e9a2/scipp-26.3.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/2f/c8/005d1de3af80f54411703d1263a0b9d31276411ec9f273d9432c59b17299/arviz_plots-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/30/d4/24d543ab8b8158b7f5a97113c831205f5c900c92c8762b1e7f44b7ea0405/python_discovery-1.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/31/b8/69f5565f1a280d032525878a86511eebed0645818492feeb169dfb20ae8e/llvmlite-0.47.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/32/28/79f0f8de97cce916d5ae88a7bee1ad724855e83e6019c0b4d5b3fabc80f3/mkdocstrings_python-2.0.3-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/33/b2/986d1220f6ee931e338d272bc1f3ec02cfe5f9b5fad84e95afdad57f1ebc/format_docstring-0.2.7-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/33/f0/3fe8c6e69135a845f4106f2ff8b6805638d4e85c264e70114e8126689587/tokenize_rt-6.2.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/38/7e/7b91c89a4cf0f543a83be978657afb20c86af6d725253e319589dcc4ce52/lmfit-1.3.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3a/eb/fea4d1d51c49832120f7f285d07306db3960f423a2612c6057caf3e8196f/pip-26.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3e/17/1f31d8562e6f970d64911f1abc330d233bc0c0601411cf7e19c1292be6da/spdx_headers-1.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/44/0d/f494a2ec62ab56ffd44da4f236db374421fe0cd1e39d9ebc9785751eb432/periodictable-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4a/1a/bea014b55c45c98c63dffd34d2dd01eed0a73ce5b175bd7b1ba798d13558/pyhanko-0.35.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4b/32/e0f13a1c5b0f8572d0ec6ae2f6c677b7991fafd95da523159c19eff0696a/contourpy-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/52/59/0782e51887ac6b07ffd1570e0364cf901ebc36345fea669969d2084baebb/simple_websocket-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/56/5b/53ca0fd447f73423c7dc59d34e523530ef434481a3d18808ff7537ad33ec/svglib-1.5.1.tar.gz + - pypi: https://files.pythonhosted.org/packages/5a/38/f1efc72736fb39e75802efcb5e2ba8eb02c7ee3687940047234bc607e3a5/python_bidi-0.6.9-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5b/69/93b34728cc386efdde0c342f8c680b9187dea7beb7adaf6b58a0713be101/mpld3-0.5.12-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5d/ba/459f18c16f2b3fc1a1ca871f72f07d70c07bf768ad0a507a698b8052ac58/msgpack-1.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/64/a8/c09fbe44b12fa919c5bfe0afb71e60d1231a7dc93405e54c30496c57c9d3/arviz-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/66/fe/b1e10b08d287f518994f1e2ff9b6d26f0adeecd8dd7d533b01bab29a3eda/yarl-1.23.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/67/ee/21d4e8536afd1a328f01b359b4d3997b291ffd35a237c877b331c1c3b71c/pillow-12.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/69/d1/705e6c19b437a4105bf3b9ae7945fcfc3ad2abb73d14bae0a3f2d58b305b/arviz_base-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6c/dd/a834df6482147d48e225a49515aabc28974ad5a4ca3215c18a882565b028/html5lib-1.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6e/7d/77752d819ede528c664ecf4de974b17b049e054a14537b74b53c8568014f/arabic_reshaper-3.0.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6e/94/be70f8ee9c45f2f62b39a1f0e9303bc20e138a8f3b8e50ffd89498e177e1/mkdocstrings-1.0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/76/8e/56ccb09c7232a55403a7637caa21922f3b65901a37f5e8bdb405d0de0946/mike-2.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7b/67/b1944235474aac3f0b0e1b232ce49547f9f9461ca4b943df1b88da5d3f1d/bumps-1.0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-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/6f/cc2b231dc78d8c3aaa674a676db190b8f8071c50134af8f8cf39b9b8e8df/pydoclint-0.8.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8a/17/4402d0d14ccf1dfc70932600b68097fbbf9c898a4871d2cbbe79c7801a32/matplotlib-3.10.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/8a/a1/8d812e53a5da1687abb10445275d41a8b13adb781bbf7196ddbcf8d88505/lazy_loader-0.5-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/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/90/ad/cba91b3bcf04073e4d1655a5c1710ef3f457f56f7d1b79dcc3d72f4dd912/plotly-6.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/93/ca/d53764f0534ff857239595f090f4cb83b599d226cc326c7de5eb3d802715/xhtml2pdf-0.2.17-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/93/f7/d00d9b4a0313a6be3a3e0818e6375e15da6d7076f4ae47d1324e7ca986a1/radon-6.0.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/99/31/6cf181011dc738c33bf6ba7aea2e8e1d3c1f71b7dab1942f3054f66f6202/asteval-1.0.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/99/37/e8730c3587a65eb5645d4aba2d27aae48e8003614d6aaf15dda67f702f1f/bidict-0.23.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9e/43/53afb8ba17218f19b77c7834128566c5bbb100a0ad9ba2e8e89d089d7079/autopep8-2.3.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a0/22/b8d873f6466b20aa563fc9b33acd48dec89a07803ddaa2f1c8ca1cd33126/numba-0.65.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a4/ce/3b6fee91c85626eaf769d617f1be9d2e15c1cca027bbdeb2e0d751469355/verspec-0.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a8/4e/c09876f08fa9faaa5e1178f3d77b7af3f343258689bd6f3b72593b2f74e3/mkdocs_markdownextradata_plugin-0.2.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a8/b1/3338e121cbd4c8a126b8ccb1061170c2ce51a53f678c502793ea49c6fd6d/chardet-7.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/aa/54/0cce26da03a981f949bb8449c9778537f75f5917c172e1d2992ff25cb57d/python_engineio-4.13.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ac/68/1666e3a4462f8202d836920114fa7a5ee9275d1fa45366d336c551a162dd/coverage-7.13.5-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/af/78/bc3850efda66712370ebb2a52130111b5cbbdee7a5b9d1672aadfc73234f/orsopy-1.2.3-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b0/73/6e1b01cbeb458807aa0831742232dbdd1fa92bfa33f52a3f176b4ff3dc11/multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/b1/4f/f71e641e504111a5a74e3a20bc52d01bd86788b22699dd3fee1c63253cf6/virtualenv-21.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/52/bc858b1665d0dec3a2511f4e6f5c18ea85c0977563d624d597c95d6d0fd7/jupyterquiz-2.9.6.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/b7/ee/e9c95cda829131f71a8dff5ce0406059fd16e591c074414e31ada19ba7c3/validate_pyproject-0.25-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bd/98/ef2b6fe2903e377cbe870c3b2800d62552f1e3dbe81ce49e1923c53d1c5c/h5py-3.16.0-cp313-cp313-manylinux_2_28_x86_64.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/c1/d4/59e74daffcb57a07668852eeeb6035af9f32cbfd7a1d2511f17d2fe6a738/smmap-5.0.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c2/14/e2a54fabd4f08cd7af1c07030603c3356b74da07f7cc056e600436edfa17/tzlocal-5.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c6/3d/020a6b6248c3d4a37797db068256f0b3f15b01bc481327ba888c50309aa8/mkdocs_plugin_inline_svg-0.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c7/a0/5ff05d1919ca249508012cad89f08fdc6cfbdaa15b41651c5fe6dffaf1d3/dfo_ls-1.6.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c9/7f/09065fd9e27da0eda08b4d6897f1c13535066174cc023af248fc2a8d5e5a/asn1crypto-1.5.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d0/69/f24d3d1c38ad69e256138b4ec2452a8c7cf66be49dc214771ae99dd4f0a0/narwhals-2.20.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d1/73/a9d864e42a01896bb5974475438f16086be9ba1f0d19d0bb7a07427c4a8b/numpy-2.4.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/d2/f0/834e479e47e499b6478e807fb57b31cc2db696c4db30557bb6f5aea4a90b/mando-0.7.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/d6/bb/fbdc4e57731efb86b4209ffdd2519520782bf27b3c961beac3e5c20d2b87/trove_classifiers-2026.4.28.13-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/dc/83/6d810a8a9ebc9c307989b418840c20e46907c74d707beb67ab566773e6fc/xarray-2026.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e2/98/8b1e801939839d405f1f122e7d175cebe9aeb4e114f95bfc45e3152af9a7/fonttools-4.62.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/e4/bc/daa30c02069eeac5b9198985ba42f5d65ca71bed6705b18329e51d352b7c/plopp-26.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e6/90/90a65e6ae1b6e66183b48874d32509fc306c994beecc7924a6fa3d9f8955/easyscience-2.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/eb/be/b257e12f9710819fde40adc972578bee6b72c5992da1bc8369bef2597756/nbmake-1.5.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ee/b8/ead7c10efff731738c72e59ed6eb5791854879fbed7ae98781a12006263a/lxml-6.1.0-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/f0/d2/e2f77eef1acb7111405433c707dc735e63f67a56e176e72e9e7a2cd3f493/aiohttp-3.13.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/f1/8b/544bc867e24e1bd48f3118cecd3b05c694e160a168478fa28770f22fd094/propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/f4/99/722e90c586455733fa860d092722ef074f3a65449fe9787b7b7e2dafcd1f/pyhanko_certvalidator-0.31.1-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/f5/5f/f17563f28ff03c7b6799c50d01d5d856a1d55f2676f537ca8d28c7f627cd/scipy-1.17.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/f6/f0/10642828a8dfb741e5f3fbaac830550a518a775c7fff6f04a007259b0548/py-1.11.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f7/27/a2fc51a4a122dfd1015e921ae9d22fee3d20b0b8080d9a704578bf9deece/pymdown_extensions-10.21.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f7/ee/d0348d8dd27ac7f001e9583c004f1b245826b04cc23b5ce6b7867c4a1cc6/refnx-0.1.63-cp311-abi3-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/86/dd6e5db36df29e76c7a7699123569a4a18c1623ce68d826ed96c62643cae/mdit_py_plugins-0.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/d9/8f612389178e1e1800aaed85537b024ebe28b9a82fff6a015825e51a8877/refl1d-1.0.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fd/20/2870f2e63da2b74eda896053809065769b256646bb3f4b74e06d013ad590/uritools-6.1.0-py3-none-any.whl + p1: + - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/annotated-types-0.7.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.13.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/appnope-0.1.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/argon2-cffi-25.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/arrow-1.4.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.18.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.14.3-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.3.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-with-css-6.3.0-hbca2aae_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.4.22-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cached-property-1.5.2-hd8ed1ab_1.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/cached_property-1.5.2-pyha770c72_1.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2026.4.22-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.7-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.3-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/copier-9.15.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.13.13-py313hd8ed1ab_100.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/decorator-5.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/defusedxml-0.7.1-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/dunamai-1.26.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/eval-type-backport-0.2.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/eval_type_backport-0.2.2-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fqdn-1.5.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/funcy-2.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h11-0.16.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/httpcore-1.0.9-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/httpx-0.28.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.13-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.8.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib_resources-7.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/invoke-3.0.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.2.0-pyh5552912_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.13.0-pyh53cf698_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython_pygments_lexers-1.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/isoduration-20.11.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jedi-0.19.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-ansible-filters-1.3.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/json5-0.14.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonpointer-3.1.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.26.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.9.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-with-format-nongpl-4.26.0-hcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter-lsp-2.3.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.8.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.9.1-pyhc90fa1f_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_events-0.12.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.18.2-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server_terminals-0.5.4-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.5.7-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_pygments-0.3.0-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_server-2.28.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/lark-1.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mistune-3.2.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbclient-0.10.4-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbconvert-core-7.17.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbformat-5.10.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/notebook-shim-0.2.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/overrides-7.7.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.2-pyhc364b38_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pandocfilters-1.5.0-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/paramiko-4.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.7-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pathspec-1.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pexpect-4.9.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pixi-kernel-0.7.1-pyhbbac1ac_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.9.6-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/plumbum-1.10.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prometheus_client-0.25.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.51-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prompt_toolkit-3.0.51-hd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ptyprocess-0.7.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pydantic-2.13.4-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.21.2-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.13.13-h4df99d1_100.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-json-logger-3.2.1-pyh332efcf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2026.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pywin32-on-windows-0.1.0-pyh1179c8e_3.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/questionary-2.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.37.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.33.1-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/returns-0.27.0-pyhc364b38_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3339-validator-0.1.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3986-validator-0.1.1-pyh9f0ad1d_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3987-syntax-1.1.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/send2trash-2.1.0-pyh5552912_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-82.0.1-pyh332efcf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sniffio-1.3.1-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.8.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyhc90fa1f_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tinycss2-1.4.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.15.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.15.0-h396c80c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-inspection-0.4.2-pyhcf101f3_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_utils-0.1.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/uri-template-1.3.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.6.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.7.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/webcolors-25.10.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/webencodings-0.5.1-pyhd8ed1ab_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/websocket-client-1.9.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/argon2-cffi-bindings-25.1.0-py313h6535dbc_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/backports.zstd-1.4.0-py313h7208f8c_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bcrypt-5.0.0-py313h2c089d5_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-python-1.2.0-py313hde1f3bb_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-hd037594_9.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/c-ares-1.34.6-hc919400_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cffi-2.0.0-py313h224173a_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cryptography-48.0.0-py313hda5ae78_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/debugpy-1.8.20-py313h1188861_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/icu-78.3-hef89b57_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/krb5-1.22.2-h385eeb1_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libabseil-20260107.1-cxx17_h2062a1b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlicommon-1.2.0-hc919400_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlidec-1.2.0-hc919400_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlienc-1.2.0-hc919400_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-22.1.5-h55c6f16_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libedit-3.1.20250104-pl5321hafb1f1b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libev-4.33-h93a5062_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.8.0-hf6b4638_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.5.2-hcf2aa1b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.3-h8088a28_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libmpdec-4.0.0-h84a0fba_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libnghttp2-1.68.1-h8f3e76b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsodium-1.0.21-h1a92334_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.53.1-h1b79a29_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libuv-1.51.0-h6caf38d_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.2-h8088a28_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/markupsafe-3.0.3-py313h65a2061_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/msgspec-0.21.1-py313h0997733_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.6-h1d4f5a5_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/nodejs-25.8.2-h7039424_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.6.2-hd24854e_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/psutil-7.2.2-py313h6688731_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pydantic-core-2.46.4-py313h212e517_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pynacl-1.6.2-py313h6940bce_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyobjc-core-12.1-py313h40b429f_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyobjc-framework-cocoa-12.1-py313hcc5defa_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.13.13-h20e6be0_100_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyyaml-6.0.3-py313h65a2061_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyzmq-27.1.0-py312h022ad19_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.3-h46df422_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/rpds-py-0.30.0-py313h2c089d5_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ruff-0.15.12-hbd3f8a3_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h010d191_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tornado-6.5.5-py313h0997733_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/yaml-0.2.5-h925e9cb_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zeromq-4.3.5-h4818236_10.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hbf9d68e_6.conda + - pypi: ./ + - pypi: https://files.pythonhosted.org/packages/00/bb/90ba423612b6aa0adccc6b1874bcd4a9b44b660c0c16f346611e00f64ac3/backrefs-7.0-py313-none-any.whl + - pypi: https://files.pythonhosted.org/packages/01/7c/fa07d3da2b6253eb8474be16eab2eadf670460e364ccc895ca7ff388ee30/oscrypto-1.3.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/07/c7/deb8c5e604404dbf10a3808a858946ca3547692ff6316b698945bb72177e/python_socketio-5.16.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/08/03/69347590f1cf4a6d5a4944bb6099e6d37f334784f16062234e1f892fdb1d/lxml-6.1.0-cp313-cp313-macosx_10_13_universal2.whl + - pypi: https://files.pythonhosted.org/packages/0b/77/dbc82ff2fb0e63c6564356682bf201edff0ba16c98630d21a1fb312a8182/pandas-3.0.2-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/0c/c9/44fb661c55062f0818a6ffd2685c67aa30816200d5f2817543717d4b92eb/coverage-7.13.5-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/0c/d6/1d5c60cc17bbdf37c1552d9c03862fc6d32c5836732a0415b2d637edc2d0/pypdf-6.10.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0d/1f/d398de1612f7a611e22d743280339c9af4903675635e41be3370091c704b/arviz_stats-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0d/fe/6bea5c9162869c5beba5d9c8abbed835ec85bf1ec1fba05a3822325c45f3/build-1.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/11/8c/c9138d881c79aa0ea9ed83cbd58d5ca75624378b38cee225dcf5c42cc91f/griffelib-2.0.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/12/c9/6869a1dcf4aaf309b9543ec070be3ec3adebee7c9bec9af8c230494134b9/interrogate-1.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/13/95/cf3f7fe4910cf0365fa8ea0c731f4b8a624d97cd76ea777913ac8d0868e2/mkdocs_jupyter-0.26.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/16/5a/736dd2f4535dbf3bf26523f9158c011389ef88dd06ec2eef67fd744f1c7b/jupytext-1.19.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/19/d4/225027a913621a879b429a043674aa35220e6ce67785acad4f7bd0c4ff33/xarray_einstats-0.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1c/13/bc43591a54dd38ac5c19e7e849f0311d879737a0d07e032e5be79849a5fb/reportlab-4.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1c/59/964ecb8008722d27d8a835baea81f56a91cea8e097b3be992bc6ccde6367/versioningit-3.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/20/16/e777eadfa0c0305878c36fae1d5e6db474fbb15dae202b9ec378809dfb4d/nbstripout-0.9.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/20/7a/1c6e3562dfd8950adbb11ffbc65d21e7c89d01a6e4f137fa981056de25c5/gitpython-3.1.50-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/21/0e/8459ca4413e1a21a06c97d134bfaf18adfd27cea068813dc0faae06cbf00/cssselect2-0.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/28/88/4789719fbbe166d12d345b3ac66b96105f10001b16e00a9765ba29261a21/nbqa-1.9.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/28/de/a3e710469772c6a89595fc52816da05c1e164b4c866a89e3cb82fb1b67c5/mkdocs_autorefs-1.4.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2a/2d/d4bf65e47cea8ff2c794a600c4fd1273a7902f268757c531e0ee9f18aa58/pooch-1.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2c/01/bc663630c510822c95c47a66af9fa7a443c295b47d5f041e5e6ae62ef659/mkdocs_material-9.7.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2f/c8/005d1de3af80f54411703d1263a0b9d31276411ec9f273d9432c59b17299/arviz_plots-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/30/d4/24d543ab8b8158b7f5a97113c831205f5c900c92c8762b1e7f44b7ea0405/python_discovery-1.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/32/28/79f0f8de97cce916d5ae88a7bee1ad724855e83e6019c0b4d5b3fabc80f3/mkdocstrings_python-2.0.3-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/33/b2/986d1220f6ee931e338d272bc1f3ec02cfe5f9b5fad84e95afdad57f1ebc/format_docstring-0.2.7-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/33/f0/3fe8c6e69135a845f4106f2ff8b6805638d4e85c264e70114e8126689587/tokenize_rt-6.2.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/38/7e/7b91c89a4cf0f543a83be978657afb20c86af6d725253e319589dcc4ce52/lmfit-1.3.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3a/eb/fea4d1d51c49832120f7f285d07306db3960f423a2612c6057caf3e8196f/pip-26.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3b/56/6f389de21c49555553d6a5aeed5ac9767631497ac836c4f076273d15bd72/fonttools-4.62.1-cp313-cp313-macosx_10_13_universal2.whl + - pypi: https://files.pythonhosted.org/packages/3e/17/1f31d8562e6f970d64911f1abc330d233bc0c0601411cf7e19c1292be6da/spdx_headers-1.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/44/0d/f494a2ec62ab56ffd44da4f236db374421fe0cd1e39d9ebc9785751eb432/periodictable-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4a/1a/bea014b55c45c98c63dffd34d2dd01eed0a73ce5b175bd7b1ba798d13558/pyhanko-0.35.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/52/59/0782e51887ac6b07ffd1570e0364cf901ebc36345fea669969d2084baebb/simple_websocket-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/55/5f/25bdec773905bff0ff6cf35ca73b17bd05593b4f87bd8c5fa43705f7167d/chardet-7.4.3-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/56/5b/53ca0fd447f73423c7dc59d34e523530ef434481a3d18808ff7537ad33ec/svglib-1.5.1.tar.gz + - pypi: https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5b/58/3ce96251560107b381cbd6e8413c483bbb1228a6b919fa8652b0d4090e7f/scipy-1.17.1-cp313-cp313-macosx_14_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/5b/69/93b34728cc386efdde0c342f8c680b9187dea7beb7adaf6b58a0713be101/mpld3-0.5.12-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/63/d9/9e14bc7564bf92d5ffa801ae5fac819ce74b925dfb55e3ebde61a3bbad3e/matplotlib-3.10.9-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/64/a8/c09fbe44b12fa919c5bfe0afb71e60d1231a7dc93405e54c30496c57c9d3/arviz-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/69/d1/705e6c19b437a4105bf3b9ae7945fcfc3ad2abb73d14bae0a3f2d58b305b/arviz_base-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6c/dd/a834df6482147d48e225a49515aabc28974ad5a4ca3215c18a882565b028/html5lib-1.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6e/7d/77752d819ede528c664ecf4de974b17b049e054a14537b74b53c8568014f/arabic_reshaper-3.0.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6e/94/be70f8ee9c45f2f62b39a1f0e9303bc20e138a8f3b8e50ffd89498e177e1/mkdocstrings-1.0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/71/43/905a14a8b17fdb1ccb58d282454490662d2cb89a6bfec26af6d3520da5ec/pillow-12.2.0-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/76/8e/56ccb09c7232a55403a7637caa21922f3b65901a37f5e8bdb405d0de0946/mike-2.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/77/6f/4615353e016799f80fa52ccb270a843c413b22361fadda2589b2922fb9b0/llvmlite-0.47.0-cp313-cp313-macosx_12_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/79/37/14a4579049c1eb673afd0de0cb4842982acd55b9ce2643e763db858bcea0/numba-0.65.1-cp313-cp313-macosx_12_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/79/76/55cd7186f498ed080a18440c9013011eb548f77ae1b297206d030eb1180a/multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/79/fe/b14d806894cf05178f1e77d0d619f071db50cf698bc654c54f9241223bcf/scipp-26.3.1-cp313-cp313-macosx_14_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/7b/67/b1944235474aac3f0b0e1b232ce49547f9f9461ca4b943df1b88da5d3f1d/bumps-1.0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-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/6f/cc2b231dc78d8c3aaa674a676db190b8f8071c50134af8f8cf39b9b8e8df/pydoclint-0.8.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/89/a4/92380f7ca60f99ebae761936bc48a72a639e8a47b29050615eef757cb2a7/propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/8a/a1/8d812e53a5da1687abb10445275d41a8b13adb781bbf7196ddbcf8d88505/lazy_loader-0.5-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/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/90/ad/cba91b3bcf04073e4d1655a5c1710ef3f457f56f7d1b79dcc3d72f4dd912/plotly-6.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/90/df/cbd75ba033d712a7bbe8efff2a764c15a3904003f78ed3c19f259c91a4a4/refnx-0.1.63-cp311-abi3-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/92/dc/c385f38f2c2433333345a82926c6bfa5ecfff3ef787201614317b58dd8be/msgpack-1.1.2-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/93/ca/d53764f0534ff857239595f090f4cb83b599d226cc326c7de5eb3d802715/xhtml2pdf-0.2.17-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/93/f7/d00d9b4a0313a6be3a3e0818e6375e15da6d7076f4ae47d1324e7ca986a1/radon-6.0.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/95/95/685e5a31f6c4cc24826d3a0df9e6a4bcf13d00f90d0c1f1060f13f5c16f8/python_bidi-0.6.9-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/96/e4/7adcd9c8362745b2210728f209bfbcf7d91ba868a2c5f40d8b58f54c509b/contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/99/31/6cf181011dc738c33bf6ba7aea2e8e1d3c1f71b7dab1942f3054f66f6202/asteval-1.0.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/99/37/e8730c3587a65eb5645d4aba2d27aae48e8003614d6aaf15dda67f702f1f/bidict-0.23.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9c/f5/0fb20fb49f8efdcdce6cd8127604ad2c503e754a8f139f5e02b01626523f/aiohttp-3.13.5-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9e/43/53afb8ba17218f19b77c7834128566c5bbb100a0ad9ba2e8e89d089d7079/autopep8-2.3.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a4/ce/3b6fee91c85626eaf769d617f1be9d2e15c1cca027bbdeb2e0d751469355/verspec-0.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a8/3a/d0a972b34e1c63e2409413104216cd1caa02c5a37cb668d1687d466c1c45/kiwisolver-1.5.0-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/a8/4e/c09876f08fa9faaa5e1178f3d77b7af3f343258689bd6f3b72593b2f74e3/mkdocs_markdownextradata_plugin-0.2.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/aa/54/0cce26da03a981f949bb8449c9778537f75f5917c172e1d2992ff25cb57d/python_engineio-4.13.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ae/50/06d511cc4b8e0360d3c94af051a768e84b755c5eb031b12adaaab6dec6e5/yarl-1.23.0-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/af/78/bc3850efda66712370ebb2a52130111b5cbbdee7a5b9d1672aadfc73234f/orsopy-1.2.3-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b0/65/5e088a45d0f43cd814bc5bec521c051d42005a472e804b1a36c48dada09b/h5py-3.16.0-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/b1/4f/f71e641e504111a5a74e3a20bc52d01bd86788b22699dd3fee1c63253cf6/virtualenv-21.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/52/bc858b1665d0dec3a2511f4e6f5c18ea85c0977563d624d597c95d6d0fd7/jupyterquiz-2.9.6.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/ee/e9c95cda829131f71a8dff5ce0406059fd16e591c074414e31ada19ba7c3/validate_pyproject-0.25-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-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/c1/d4/59e74daffcb57a07668852eeeb6035af9f32cbfd7a1d2511f17d2fe6a738/smmap-5.0.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c2/14/e2a54fabd4f08cd7af1c07030603c3356b74da07f7cc056e600436edfa17/tzlocal-5.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c6/3d/020a6b6248c3d4a37797db068256f0b3f15b01bc481327ba888c50309aa8/mkdocs_plugin_inline_svg-0.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c7/a0/5ff05d1919ca249508012cad89f08fdc6cfbdaa15b41651c5fe6dffaf1d3/dfo_ls-1.6.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c9/7f/09065fd9e27da0eda08b4d6897f1c13535066174cc023af248fc2a8d5e5a/asn1crypto-1.5.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d0/69/f24d3d1c38ad69e256138b4ec2452a8c7cf66be49dc214771ae99dd4f0a0/narwhals-2.20.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d2/f0/834e479e47e499b6478e807fb57b31cc2db696c4db30557bb6f5aea4a90b/mando-0.7.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d6/bb/fbdc4e57731efb86b4209ffdd2519520782bf27b3c961beac3e5c20d2b87/trove_classifiers-2026.4.28.13-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/dc/83/6d810a8a9ebc9c307989b418840c20e46907c74d707beb67ab566773e6fc/xarray-2026.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e4/bc/daa30c02069eeac5b9198985ba42f5d65ca71bed6705b18329e51d352b7c/plopp-26.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e5/87/499737bfba066b4a3bebff24a8f1c5b2dee410b209bc6668c9be692580f0/numpy-2.4.4-cp313-cp313-macosx_14_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/e6/90/90a65e6ae1b6e66183b48874d32509fc306c994beecc7924a6fa3d9f8955/easyscience-2.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/eb/be/b257e12f9710819fde40adc972578bee6b72c5992da1bc8369bef2597756/nbmake-1.5.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f4/99/722e90c586455733fa860d092722ef074f3a65449fe9787b7b7e2dafcd1f/pyhanko_certvalidator-0.31.1-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/f6/f0/10642828a8dfb741e5f3fbaac830550a518a775c7fff6f04a007259b0548/py-1.11.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f7/27/a2fc51a4a122dfd1015e921ae9d22fee3d20b0b8080d9a704578bf9deece/pymdown_extensions-10.21.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/86/dd6e5db36df29e76c7a7699123569a4a18c1623ce68d826ed96c62643cae/mdit_py_plugins-0.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/d9/8f612389178e1e1800aaed85537b024ebe28b9a82fff6a015825e51a8877/refl1d-1.0.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fd/20/2870f2e63da2b74eda896053809065769b256646bb3f4b74e06d013ad590/uritools-6.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl + win-64: + - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/annotated-types-0.7.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.13.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/argon2-cffi-25.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/arrow-1.4.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.18.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.14.3-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.3.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-with-css-6.3.0-hbca2aae_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.4.22-h4c7d964_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cached-property-1.5.2-hd8ed1ab_1.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/cached_property-1.5.2-pyha770c72_1.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2026.4.22-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.7-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.3-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/copier-9.15.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.13.13-py313hd8ed1ab_100.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/decorator-5.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/defusedxml-0.7.1-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/dunamai-1.26.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/eval-type-backport-0.2.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/eval_type_backport-0.2.2-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fqdn-1.5.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/funcy-2.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h11-0.16.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/httpcore-1.0.9-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/httpx-0.28.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.13-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.8.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib_resources-7.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/invoke-3.0.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.2.0-pyh6dadd2b_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.13.0-pyhe2676ad_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython_pygments_lexers-1.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/isoduration-20.11.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jedi-0.19.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-ansible-filters-1.3.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/json5-0.14.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonpointer-3.1.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.26.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.9.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-with-format-nongpl-4.26.0-hcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter-lsp-2.3.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.8.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.9.1-pyh6dadd2b_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_events-0.12.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.18.2-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server_terminals-0.5.4-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.5.7-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_pygments-0.3.0-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_server-2.28.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/lark-1.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mistune-3.2.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbclient-0.10.4-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbconvert-core-7.17.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbformat-5.10.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/notebook-shim-0.2.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/overrides-7.7.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.2-pyhc364b38_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pandocfilters-1.5.0-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/paramiko-4.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.7-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pathspec-1.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pixi-kernel-0.7.1-pyhbbac1ac_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.9.6-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/plumbum-1.10.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prometheus_client-0.25.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.51-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prompt_toolkit-3.0.51-hd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pydantic-2.13.4-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyh09c184e_7.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.21.2-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.13.13-h4df99d1_100.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-json-logger-3.2.1-pyh332efcf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2026.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pywin32-on-windows-0.1.0-pyh07e9846_2.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/questionary-2.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.37.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.33.1-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/returns-0.27.0-pyhc364b38_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3339-validator-0.1.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3986-validator-0.1.1-pyh9f0ad1d_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3987-syntax-1.1.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/send2trash-2.1.0-pyh6dadd2b_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-82.0.1-pyh332efcf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sniffio-1.3.1-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.8.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyh6dadd2b_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tinycss2-1.4.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.15.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.15.0-h396c80c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-inspection-0.4.2-pyhcf101f3_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_utils-0.1.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/uri-template-1.3.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.6.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.7.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/webcolors-25.10.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/webencodings-0.5.1-pyhd8ed1ab_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/websocket-client-1.9.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/win_inet_pton-1.1.0-pyh7428d3b_8.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/argon2-cffi-bindings-25.1.0-py313h5ea7bf4_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/backports.zstd-1.4.0-py313h2a31948_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/bcrypt-5.0.0-py313hfbe8231_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/brotli-python-1.2.0-py313h3ebfc14_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h0ad9c76_9.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/cffi-2.0.0-py313h5ea7bf4_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/cryptography-48.0.0-py313hf5c5e30_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/debugpy-1.8.20-py313h927ade5_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/krb5-1.22.2-h0ea6238_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.8.0-hac47afa_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libffi-3.5.2-h3d046cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-5.8.3-hfd05255_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libmpdec-4.0.0-hfd05255_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libsodium-1.0.21-h6a83c73_3.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.53.1-hf5d6505_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libzlib-1.3.2-hfd05255_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/markupsafe-3.0.3-py313hd650c13_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/msgspec-0.21.1-py313h5ea7bf4_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/nodejs-25.8.2-h80d1838_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.6.2-hf411b9b_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/psutil-7.2.2-py313h5fd188c_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pydantic-core-2.46.4-py313hfbe8231_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pynacl-1.6.2-py313hed4ef92_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/python-3.13.13-h09917c8_100_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pywin32-311-py313h40c08fc_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pywinpty-2.0.15-py313h5813708_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pyyaml-6.0.3-py313hd650c13_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pyzmq-27.1.0-py312h343a6d4_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/rpds-py-0.30.0-py313hfbe8231_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/ruff-0.15.12-hd7ccaa8_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h6ed50ae_3.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/tornado-6.5.5-py313h5ea7bf4_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.26100.0-h57928b3_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-h41ae7f8_34.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.44.35208-h818238b_34.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.44.35208-h818238b_34.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/winpty-0.4.3-4.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/win-64/yaml-0.2.5-h6a83c73_3.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/zeromq-4.3.5-h507cc87_10.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_6.conda + - pypi: ./ + - pypi: https://files.pythonhosted.org/packages/00/bb/90ba423612b6aa0adccc6b1874bcd4a9b44b660c0c16f346611e00f64ac3/backrefs-7.0-py313-none-any.whl + - pypi: https://files.pythonhosted.org/packages/01/7c/fa07d3da2b6253eb8474be16eab2eadf670460e364ccc895ca7ff388ee30/oscrypto-1.3.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/07/c7/deb8c5e604404dbf10a3808a858946ca3547692ff6316b698945bb72177e/python_socketio-5.16.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0c/d6/1d5c60cc17bbdf37c1552d9c03862fc6d32c5836732a0415b2d637edc2d0/pypdf-6.10.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0d/1f/d398de1612f7a611e22d743280339c9af4903675635e41be3370091c704b/arviz_stats-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0d/fe/6bea5c9162869c5beba5d9c8abbed835ec85bf1ec1fba05a3822325c45f3/build-1.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/11/8c/c9138d881c79aa0ea9ed83cbd58d5ca75624378b38cee225dcf5c42cc91f/griffelib-2.0.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/12/c9/6869a1dcf4aaf309b9543ec070be3ec3adebee7c9bec9af8c230494134b9/interrogate-1.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/13/95/cf3f7fe4910cf0365fa8ea0c731f4b8a624d97cd76ea777913ac8d0868e2/mkdocs_jupyter-0.26.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/16/5a/736dd2f4535dbf3bf26523f9158c011389ef88dd06ec2eef67fd744f1c7b/jupytext-1.19.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/18/0b/0098c214843213759692cc638fce7de5c289200a830e5035d1791d7a2338/contourpy-1.3.3-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/19/d4/225027a913621a879b429a043674aa35220e6ce67785acad4f7bd0c4ff33/xarray_einstats-0.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1b/b3/5d0e77ea774bd3224321c248880ea0c0379000ac5c2bb6d77609549de247/chardet-7.4.3-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/1c/13/bc43591a54dd38ac5c19e7e849f0311d879737a0d07e032e5be79849a5fb/reportlab-4.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1c/59/964ecb8008722d27d8a835baea81f56a91cea8e097b3be992bc6ccde6367/versioningit-3.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/20/16/e777eadfa0c0305878c36fae1d5e6db474fbb15dae202b9ec378809dfb4d/nbstripout-0.9.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/20/7a/1c6e3562dfd8950adbb11ffbc65d21e7c89d01a6e4f137fa981056de25c5/gitpython-3.1.50-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/21/0e/8459ca4413e1a21a06c97d134bfaf18adfd27cea068813dc0faae06cbf00/cssselect2-0.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/28/88/4789719fbbe166d12d345b3ac66b96105f10001b16e00a9765ba29261a21/nbqa-1.9.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/28/de/a3e710469772c6a89595fc52816da05c1e164b4c866a89e3cb82fb1b67c5/mkdocs_autorefs-1.4.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2a/2d/d4bf65e47cea8ff2c794a600c4fd1273a7902f268757c531e0ee9f18aa58/pooch-1.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2c/01/bc663630c510822c95c47a66af9fa7a443c295b47d5f041e5e6ae62ef659/mkdocs_material-9.7.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2f/c8/005d1de3af80f54411703d1263a0b9d31276411ec9f273d9432c59b17299/arviz_plots-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/30/d4/24d543ab8b8158b7f5a97113c831205f5c900c92c8762b1e7f44b7ea0405/python_discovery-1.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/32/28/79f0f8de97cce916d5ae88a7bee1ad724855e83e6019c0b4d5b3fabc80f3/mkdocstrings_python-2.0.3-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/33/b2/986d1220f6ee931e338d272bc1f3ec02cfe5f9b5fad84e95afdad57f1ebc/format_docstring-0.2.7-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/33/f0/3fe8c6e69135a845f4106f2ff8b6805638d4e85c264e70114e8126689587/tokenize_rt-6.2.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/35/e5/d6d0e51fc888f692a35134336866341c08655d92614f492c6860dc45bb2c/scipy-1.17.1-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/37/fd/22621d3ee9e3ee87ef4c89b63bba55b265ab85039b3c1ba88ed2380a24c1/scipp-26.3.1-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/38/60/35186529de1db3c01f5ad625bde07c1f576305eab6d86bbda4c58445f721/fonttools-4.62.1-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/38/7e/7b91c89a4cf0f543a83be978657afb20c86af6d725253e319589dcc4ce52/lmfit-1.3.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3a/eb/fea4d1d51c49832120f7f285d07306db3960f423a2612c6057caf3e8196f/pip-26.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3c/0c/c73042d7ae043f4e3ff51406821ad70904249708a6bc4f0dc94f5f78ce9b/python_bidi-0.6.9.tar.gz + - pypi: https://files.pythonhosted.org/packages/3e/17/1f31d8562e6f970d64911f1abc330d233bc0c0601411cf7e19c1292be6da/spdx_headers-1.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3f/82/8a3739a5e470b3c6cbb1d21d315800d8e16bff503d1f16b03a4ec3212786/pillow-12.2.0-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/43/34/7d27a333c558d6ac16dbc12a35061d389735e99e494ee4effa4ec6d99bed/pycairo-1.29.0-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/44/0d/f494a2ec62ab56ffd44da4f236db374421fe0cd1e39d9ebc9785751eb432/periodictable-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/46/9f/4898b44e4042c60fafcb1162dfb7014f6f15b1ec19bf29cfea6bf26df90d/llvmlite-0.47.0-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/4a/1a/bea014b55c45c98c63dffd34d2dd01eed0a73ce5b175bd7b1ba798d13558/pyhanko-0.35.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/52/59/0782e51887ac6b07ffd1570e0364cf901ebc36345fea669969d2084baebb/simple_websocket-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/57/fb/48649b4971cde70d817cf97a2a2fdc0b4d8308569f1dd2f2611959d2e0cf/numpy-2.4.4-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5b/69/93b34728cc386efdde0c342f8c680b9187dea7beb7adaf6b58a0713be101/mpld3-0.5.12-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5e/52/42855bf626868413f761addd574acc6195880ae247a5346477a4361c3acb/pandas-3.0.2-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/64/a8/c09fbe44b12fa919c5bfe0afb71e60d1231a7dc93405e54c30496c57c9d3/arviz-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/66/40/7732d648ab9d069a46e686043241f01206348e2bbf128daea85be4d6414b/coverage-7.13.5-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/69/d1/705e6c19b437a4105bf3b9ae7945fcfc3ad2abb73d14bae0a3f2d58b305b/arviz_base-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6a/00/d26b61e82163a59334e9f2ae31ba95ac01922da3792f090e11762d4422e8/rlpycairo-0.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6c/dd/a834df6482147d48e225a49515aabc28974ad5a4ca3215c18a882565b028/html5lib-1.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6d/93/01273a1b8d8454d45f2e18b3d6098c7be13a0864a55fbd0ebda7815c201a/svglib-1.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6e/7d/77752d819ede528c664ecf4de974b17b049e054a14537b74b53c8568014f/arabic_reshaper-3.0.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6e/94/be70f8ee9c45f2f62b39a1f0e9303bc20e138a8f3b8e50ffd89498e177e1/mkdocstrings-1.0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/74/07/1ed8277f8653c40ebc65985180b007879f6a836c525b3885dcc6448ae6cb/msgpack-1.1.2-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/76/8e/56ccb09c7232a55403a7637caa21922f3b65901a37f5e8bdb405d0de0946/mike-2.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7a/84/266e8da36879c6edcd37b02b547e2d9ecdfea776be49598e75696e3316e1/yarl-1.23.0-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/7b/67/b1944235474aac3f0b0e1b232ce49547f9f9461ca4b943df1b88da5d3f1d/bumps-1.0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-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/6f/cc2b231dc78d8c3aaa674a676db190b8f8071c50134af8f8cf39b9b8e8df/pydoclint-0.8.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8a/46/7e2d150dd574039c2f8d09c4e102bb90a6cb6634be4040fc29f212fc42bf/refnx-0.1.63-cp311-abi3-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/8a/a1/8d812e53a5da1687abb10445275d41a8b13adb781bbf7196ddbcf8d88505/lazy_loader-0.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8e/63/981401c5680c1eb30893f00a19641ac80db5d1e7086c62cb4b13ed813038/lxml-6.1.0-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/8f/5e/f1e1dd319e35e962a4e00b33150a8868b6329cc1d19fd533436ba5488f09/uncertainties-3.2.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/90/ad/cba91b3bcf04073e4d1655a5c1710ef3f457f56f7d1b79dcc3d72f4dd912/plotly-6.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/93/6e/bd7fbfacca077bc6f34f1a1109800a2c41ab50f4704d3a0507ba41009915/freetype_py-2.5.1-py3-none-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/93/ca/d53764f0534ff857239595f090f4cb83b599d226cc326c7de5eb3d802715/xhtml2pdf-0.2.17-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/93/f7/d00d9b4a0313a6be3a3e0818e6375e15da6d7076f4ae47d1324e7ca986a1/radon-6.0.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/99/31/6cf181011dc738c33bf6ba7aea2e8e1d3c1f71b7dab1942f3054f66f6202/asteval-1.0.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/99/37/e8730c3587a65eb5645d4aba2d27aae48e8003614d6aaf15dda67f702f1f/bidict-0.23.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9e/43/53afb8ba17218f19b77c7834128566c5bbb100a0ad9ba2e8e89d089d7079/autopep8-2.3.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a4/ce/3b6fee91c85626eaf769d617f1be9d2e15c1cca027bbdeb2e0d751469355/verspec-0.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a8/4e/c09876f08fa9faaa5e1178f3d77b7af3f343258689bd6f3b72593b2f74e3/mkdocs_markdownextradata_plugin-0.2.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/aa/54/0cce26da03a981f949bb8449c9778537f75f5917c172e1d2992ff25cb57d/python_engineio-4.13.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/af/78/bc3850efda66712370ebb2a52130111b5cbbdee7a5b9d1672aadfc73234f/orsopy-1.2.3-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b1/4f/f71e641e504111a5a74e3a20bc52d01bd86788b22699dd3fee1c63253cf6/virtualenv-21.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b2/35/e994121b0e90e46134673422dd564623f93304614f5d11886b1b3e06f503/multidict-6.7.1-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/52/bc858b1665d0dec3a2511f4e6f5c18ea85c0977563d624d597c95d6d0fd7/jupyterquiz-2.9.6.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/ee/e9c95cda829131f71a8dff5ce0406059fd16e591c074414e31ada19ba7c3/validate_pyproject-0.25-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/be/8a/be60e3bbcf513cc5a50f4a3e88e1dcecebb79c1ad607a7222877becaa101/kiwisolver-1.5.0-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/bf/50/98b146aea0f1cd7531d25f12bea69fa9ce8d1662124f93fb30dc4511b65e/docstring_parser_fork-0.0.14-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c0/1b/ac685a8882896acf0f6b31d689e3792199cfe7aba37969fa91da63a7fa27/aiohttp-3.13.5-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/c1/d4/59e74daffcb57a07668852eeeb6035af9f32cbfd7a1d2511f17d2fe6a738/smmap-5.0.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c2/14/e2a54fabd4f08cd7af1c07030603c3356b74da07f7cc056e600436edfa17/tzlocal-5.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c3/d9/a27997f84341fc0dfcdd1fe4179b6ba6c32a7aa880fdb8c514d4dad6fba3/h5py-3.16.0-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/c5/e0/0840fd2f93da988ec660b8ad1984abe9f25d2aed22a5e394ff1c68c88307/matplotlib-3.10.9-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/c6/3d/020a6b6248c3d4a37797db068256f0b3f15b01bc481327ba888c50309aa8/mkdocs_plugin_inline_svg-0.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c7/a0/5ff05d1919ca249508012cad89f08fdc6cfbdaa15b41651c5fe6dffaf1d3/dfo_ls-1.6.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c9/7f/09065fd9e27da0eda08b4d6897f1c13535066174cc023af248fc2a8d5e5a/asn1crypto-1.5.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d0/69/f24d3d1c38ad69e256138b4ec2452a8c7cf66be49dc214771ae99dd4f0a0/narwhals-2.20.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d2/f0/834e479e47e499b6478e807fb57b31cc2db696c4db30557bb6f5aea4a90b/mando-0.7.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d6/bb/fbdc4e57731efb86b4209ffdd2519520782bf27b3c961beac3e5c20d2b87/trove_classifiers-2026.4.28.13-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/dc/83/6d810a8a9ebc9c307989b418840c20e46907c74d707beb67ab566773e6fc/xarray-2026.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e4/bc/daa30c02069eeac5b9198985ba42f5d65ca71bed6705b18329e51d352b7c/plopp-26.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e6/90/90a65e6ae1b6e66183b48874d32509fc306c994beecc7924a6fa3d9f8955/easyscience-2.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/eb/be/b257e12f9710819fde40adc972578bee6b72c5992da1bc8369bef2597756/nbmake-1.5.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f4/99/722e90c586455733fa860d092722ef074f3a65449fe9787b7b7e2dafcd1f/pyhanko_certvalidator-0.31.1-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/f5/ab/f76ec3c3627c883215b5c8080debb4394ef5a7a29be811f786415fc1e6fd/propcache-0.4.1-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/f6/f0/10642828a8dfb741e5f3fbaac830550a518a775c7fff6f04a007259b0548/py-1.11.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f7/27/a2fc51a4a122dfd1015e921ae9d22fee3d20b0b8080d9a704578bf9deece/pymdown_extensions-10.21.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/86/dd6e5db36df29e76c7a7699123569a4a18c1623ce68d826ed96c62643cae/mdit_py_plugins-0.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/d9/8f612389178e1e1800aaed85537b024ebe28b9a82fff6a015825e51a8877/refl1d-1.0.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fd/20/2870f2e63da2b74eda896053809065769b256646bb3f4b74e06d013ad590/uritools-6.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fd/a5/03c970d57f4c1741354837353ce39fb5206952ae1dba8922d29c86f64805/numba-0.65.1-cp313-cp313-win_amd64.whl + user: + channels: + - url: https://conda.anaconda.org/nodefaults/ + - url: https://conda.anaconda.org/conda-forge/ + indexes: + - https://pypi.org/simple + packages: + linux-64: + - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/argon2-cffi-bindings-25.1.0-py313h07c4f96_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/backports.zstd-1.4.0-py313h18e8e13_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/bcrypt-5.0.0-py313h843e2db_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.2.0-py313hf159716_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_9.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cffi-2.0.0-py313hf46b229_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cryptography-48.0.0-py313heb322e3_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/debugpy-1.8.20-py313h5d5ffb9_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.3-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.22.2-ha1258a1_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45.1-default_hbd61a6d_102.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20250104-pl5321h7949ede_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.8.0-hecca717_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h3435931_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-he0feb66_18.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-he0feb66_18.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.3-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsodium-1.0.21-h280c20c_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.53.1-h0c1763c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h934c35e_18.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.42-h5347b49_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.2-h25fd6f3_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.3-py313h3dea7bd_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/msgspec-0.21.1-py313h07c4f96_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.6-hdb14827_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.2-h35e630c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/psutil-7.2.2-py313h54dd161_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pydantic-core-2.46.4-py313h843e2db_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pynacl-1.6.2-py313h5008379_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.13.13-h6add32d_100_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.3-py313h3dea7bd_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pyzmq-27.1.0-py312hda471dd_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.3-h853b02a_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/rpds-py-0.30.0-py313h843e2db_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ruff-0.15.12-h994f30f_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h366c992_103.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tornado-6.5.5-py313h07c4f96_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h280c20c_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zeromq-4.3.5-h41580af_10.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/annotated-types-0.7.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.13.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/argon2-cffi-25.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/arrow-1.4.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.18.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.14.3-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.3.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-with-css-6.3.0-hbca2aae_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.4.22-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cached-property-1.5.2-hd8ed1ab_1.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/cached_property-1.5.2-pyha770c72_1.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2026.4.22-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.7-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.3-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/copier-9.15.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.13.13-py313hd8ed1ab_100.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/decorator-5.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/defusedxml-0.7.1-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/dunamai-1.26.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/eval-type-backport-0.2.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/eval_type_backport-0.2.2-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fqdn-1.5.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/funcy-2.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h11-0.16.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/httpcore-1.0.9-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/httpx-0.28.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.13-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.8.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib_resources-7.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/invoke-3.0.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.2.0-pyha191276_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.13.0-pyh53cf698_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython_pygments_lexers-1.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/isoduration-20.11.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jedi-0.19.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-ansible-filters-1.3.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/json5-0.14.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonpointer-3.1.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.26.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.9.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-with-format-nongpl-4.26.0-hcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter-lsp-2.3.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.8.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.9.1-pyhc90fa1f_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_events-0.12.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.18.2-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server_terminals-0.5.4-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.5.7-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_pygments-0.3.0-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_server-2.28.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/lark-1.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mistune-3.2.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbclient-0.10.4-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbconvert-core-7.17.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbformat-5.10.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/notebook-shim-0.2.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/overrides-7.7.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.2-pyhc364b38_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pandocfilters-1.5.0-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/paramiko-4.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.7-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pathspec-1.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pexpect-4.9.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pixi-kernel-0.7.1-pyhbbac1ac_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.9.6-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/plumbum-1.10.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prometheus_client-0.25.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.51-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prompt_toolkit-3.0.51-hd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ptyprocess-0.7.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pydantic-2.13.4-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.21.2-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.13.13-h4df99d1_100.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-json-logger-3.2.1-pyh332efcf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2026.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pywin32-on-windows-0.1.0-pyh1179c8e_3.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/questionary-2.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.37.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.33.1-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/returns-0.27.0-pyhc364b38_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3339-validator-0.1.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3986-validator-0.1.1-pyh9f0ad1d_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3987-syntax-1.1.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/send2trash-2.1.0-pyha191276_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-82.0.1-pyh332efcf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sniffio-1.3.1-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.8.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyhc90fa1f_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tinycss2-1.4.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.15.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.15.0-h396c80c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-inspection-0.4.2-pyhcf101f3_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_utils-0.1.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/uri-template-1.3.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.6.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.7.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/webcolors-25.10.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/webencodings-0.5.1-pyhd8ed1ab_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/websocket-client-1.9.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.1-pyhcf101f3_0.conda + - pypi: https://files.pythonhosted.org/packages/07/c7/deb8c5e604404dbf10a3808a858946ca3547692ff6316b698945bb72177e/python_socketio-5.16.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/12/aa/fb2a0649fdeef5ab7072d221e8f4df164098792c813af6c87e2581cfa860/mpltoolbox-26.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/12/c5/cbb1ffefb20a93d3f0e1fdcda699fb84976210d411b008f97f48bf6ce27e/pandas-3.0.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/25/7d/cea3531f77df694ac7f169378250d85f19f69b09a5f4fa45f650837ae7cc/py3dmol-2.5.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2a/2d/d4bf65e47cea8ff2c794a600c4fd1273a7902f268757c531e0ee9f18aa58/pooch-1.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2b/0a/7b98e1e119878a27ba8618ca1e18b14f992ff1eda40f47bccccf4de44121/kiwisolver-1.5.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/2e/75/5604f4d17ab607510d4702f156329194d8edfff7e29644ca9200b085e9a2/scipp-26.3.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/33/eb/f9f1ded8e4db9638f9530c3782eb01f5ab04945f4cb9e597a51c203fa4c5/diffpy_pdffit2-1.6.0.tar.gz + - pypi: https://files.pythonhosted.org/packages/38/7e/7b91c89a4cf0f543a83be978657afb20c86af6d725253e319589dcc4ce52/lmfit-1.3.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/38/8b/7ec325b4e9e78beefc2d025b01ee8a2fde771ef7c957c3bff99b9e1fbffa/xraydb-4.5.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3a/eb/fea4d1d51c49832120f7f285d07306db3960f423a2612c6057caf3e8196f/pip-26.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3f/0e/fa3b193432cfc60c93b42f3be03365f5f909d2b3ea410295cf36df739e31/widgetsnbextension-4.0.15-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3f/d0/7b958df957e4827837b590944008f0b28078f552b451f7407b4b3d54f574/asciichartpy-1.5.25-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/42/d9/27b13bc9419bf5dae02905b348f16ca827646cd76244ddd326f1a8139a6a/cyclebane-24.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4a/f3/00bb1e867fba351e2d784170955713bee200c43ea306c59f30bd7e748192/dask-2026.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4b/32/e0f13a1c5b0f8572d0ec6ae2f6c677b7991fafd95da523159c19eff0696a/contourpy-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/52/59/0782e51887ac6b07ffd1570e0364cf901ebc36345fea669969d2084baebb/simple_websocket-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/53/01/1c0485ae02e645bc517bf5d5a6ca674f62c97e247890b954cbfe85c64dae/spglib-2.6.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/56/6d/0d9848617b9f753b87f214f1c682592f7ca42de085f564352f10f0843026/ipywidgets-8.1.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5b/69/93b34728cc386efdde0c342f8c680b9187dea7beb7adaf6b58a0713be101/mpld3-0.5.12-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5c/01/6cb4d63c6b6933be4b7945b2f64638336420f04ea71ca5b9a7539c008bc5/scippnexus-26.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5c/a5/a8c7562ec39f2647245b52ea4aeb13b5b125b3f48c0c152e9ebce7047a0a/pycifrw-5.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/5d/ba/459f18c16f2b3fc1a1ca871f72f07d70c07bf768ad0a507a698b8052ac58/msgpack-1.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/61/2b/e260d50e64690d2a9e405d52ccd18a63c286c5088937dd0107cb23eb3195/diffpy_utils-3.7.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/64/2e/ba6b404a8a0e7c954cdf0fdd2b21723dc41def60f16b69b9b1cbb2e22d91/crysfml-0.6.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/66/fe/b1e10b08d287f518994f1e2ff9b6d26f0adeecd8dd7d533b01bab29a3eda/yarl-1.23.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/67/ee/21d4e8536afd1a328f01b359b4d3997b291ffd35a237c877b331c1c3b71c/pillow-12.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/6c/25/4f103d1bedb3593718713b3f743df7b3ff3fc68d36d6666c30265ef59c8a/ase-3.28.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/78/3c/2a612b95ddbb9a6bdcb47b7a93c4884f74c6ff22356b2f7b213b16e65c35/pycifstar-0.3.0.tar.gz + - pypi: https://files.pythonhosted.org/packages/7b/67/b1944235474aac3f0b0e1b232ce49547f9f9461ca4b943df1b88da5d3f1d/bumps-1.0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/82/9b/cf7e6f157c53d2c8bc0165580350c53939aea3b2a8390ec8e07f0adb82de/scippneutron-26.4.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8a/06/2c1bd1ee9eee3e65b0b6395dcf960e71b11576995eae3b4ab9ec63d89bea/essreduce-26.4.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8a/17/4402d0d14ccf1dfc70932600b68097fbbf9c898a4871d2cbbe79c7801a32/matplotlib-3.10.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/8a/17/a3918541fd0ddefe024a69de6d16aa7b46d36ac19562adaa63c7fa180eff/greenlet-3.5.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/8a/a1/8d812e53a5da1687abb10445275d41a8b13adb781bbf7196ddbcf8d88505/lazy_loader-0.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8d/c0/fdf9d3ee103ce66a55f0532835ad5e154226c5222423c6636ba049dc42fc/traittypes-0.2.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/90/ad/cba91b3bcf04073e4d1655a5c1710ef3f457f56f7d1b79dcc3d72f4dd912/plotly-6.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/91/4c/e0ce1ef95d4000ebc1c11801f9b944fa5910ecc15b5e351865763d8657f8/graphviz-0.21-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/91/88/b55b3117287a8540b76dbdd87733808d4d01c8067a3b339408c250bb3600/typeguard-4.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/96/5d/0c59079aa7ef34980a5925a06a90ad2b7c94e486c194b3527d557cabb042/cryspy-0.11.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/99/31/6cf181011dc738c33bf6ba7aea2e8e1d3c1f71b7dab1942f3054f66f6202/asteval-1.0.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/99/37/e8730c3587a65eb5645d4aba2d27aae48e8003614d6aaf15dda67f702f1f/bidict-0.23.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a3/58/35da89ee790598a0700ea49b2a66594140f44dec458c07e8e3d4979137fc/ply-3.11-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a3/8c/db8e79c4c744ebae1dcf25f7dbcc5d7df912cdbcdf7221e761479e8bd04b/gemmi-0.7.5-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/a4/39/3a0ae5b0edb66e61bb0e8bc53a503495cba5892297ae21faf6ba0525e681/varname-1.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a8/88/802c82060c54bc7dde21eb0033e337838b8181a1323254aa9ec41cbfc3d1/markdown_it_py-4.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/aa/54/0cce26da03a981f949bb8449c9778537f75f5917c172e1d2992ff25cb57d/python_engineio-4.13.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ab/b5/36c712098e6191d1b4e349304ef73a8d06aed77e56ceaac8c0a306c7bda1/jupyterlab_widgets-3.0.16-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/af/46/661159ad844034ba8b3f4e0516215c41e4ee17db4213d13a82227670764f/sciline-25.11.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b0/73/6e1b01cbeb458807aa0831742232dbdd1fa92bfa33f52a3f176b4ff3dc11/multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b4/5e/bf11645aebb9af7d8d35927c40d3855816a0855c799e8156eeca8d632c90/diffpy_structure-3.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bd/98/ef2b6fe2903e377cbe870c3b2800d62552f1e3dbe81ce49e1923c53d1c5c/h5py-3.16.0-cp313-cp313-manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/bf/e1/9e8e09ab8fc5c77f228e3271188dc4f569d12692548c42bff81b1fbba5e1/easydiffraction-0.16.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c4/6d/82e65254354ba651dc966775270a9bbc02414a3eb3f1704e6c87dab2ea83/essdiffraction-26.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c7/a0/5ff05d1919ca249508012cad89f08fdc6cfbdaa15b41651c5fe6dffaf1d3/dfo_ls-1.6.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/cd/f5/038741f5e747a5f6ea3e72487211579d8cbea5eb9827a9cbd61d0108c4bd/sqlalchemy-2.0.49-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/d0/69/f24d3d1c38ad69e256138b4ec2452a8c7cf66be49dc214771ae99dd4f0a0/narwhals-2.20.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d1/73/a9d864e42a01896bb5974475438f16086be9ba1f0d19d0bb7a07427c4a8b/numpy-2.4.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/d5/0c/043d5e551459da400957a1395e0febbf771446ff34291afcbe3d8be2a279/fsspec-2026.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/d8/8b/e2bbeb42068f0c48899e8eddd34902afc0f7429d4d2a152d2dc2670dc661/pythreejs-2.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/db/bc/83e112abc66cd466c6b83f99118035867cecd41802f8d044638aa78a106e/locket-1.0.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/dc/b7/901d837999a9350a7773289f7760cb473d4ba01fdc6ebae0ff2553d269ac/ncrystal_python-4.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e2/98/8b1e801939839d405f1f122e7d175cebe9aeb4e114f95bfc45e3152af9a7/fonttools-4.62.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/e4/bc/daa30c02069eeac5b9198985ba42f5d65ca71bed6705b18329e51d352b7c/plopp-26.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ee/8c/83087ebc47ab0396ce092363001fa37c17153119ee282700c0713a195853/prettytable-3.17.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f0/d2/e2f77eef1acb7111405433c707dc735e63f67a56e176e72e9e7a2cd3f493/aiohttp-3.13.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/f1/2c/3850985d4c64048dec7b826f8a803e135b52b11b4c81c9cd4326b1ca15ab/ncrystal_core-4.4.2-py3-none-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/f1/5b/e63c877c4c94382b66de5045e08ec8cd960e8a4d22f0d62a4dfb1f9e5ac6/ipydatawidgets-4.3.5-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f1/8b/544bc867e24e1bd48f3118cecd3b05c694e160a168478fa28770f22fd094/propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/f2/f2/728f041460f1b9739b85ee23b45fa5a505962ea11fd85bdbe2a02b021373/darkdetect-0.8.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f5/57/2a154a69d6642860300bf8eb205d13131104991f2b1065bbb9075ac5c32e/tof-26.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f5/5f/f17563f28ff03c7b6799c50d01d5d856a1d55f2676f537ca8d28c7f627cd/scipy-1.17.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/f7/00/bbca25f8a2372465cdf93138c1e1e38dff045fb0afef1488f395d0afcb3b/ncrystal-4.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl + p1: + - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/annotated-types-0.7.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.13.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/appnope-0.1.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/argon2-cffi-25.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/arrow-1.4.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.18.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.14.3-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.3.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-with-css-6.3.0-hbca2aae_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.4.22-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cached-property-1.5.2-hd8ed1ab_1.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/cached_property-1.5.2-pyha770c72_1.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2026.4.22-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.7-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.3-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/copier-9.15.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.13.13-py313hd8ed1ab_100.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/decorator-5.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/defusedxml-0.7.1-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/dunamai-1.26.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/eval-type-backport-0.2.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/eval_type_backport-0.2.2-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fqdn-1.5.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/funcy-2.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h11-0.16.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/httpcore-1.0.9-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/httpx-0.28.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.13-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.8.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib_resources-7.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/invoke-3.0.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.2.0-pyh5552912_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.13.0-pyh53cf698_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython_pygments_lexers-1.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/isoduration-20.11.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jedi-0.19.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-ansible-filters-1.3.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/json5-0.14.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonpointer-3.1.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.26.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.9.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-with-format-nongpl-4.26.0-hcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter-lsp-2.3.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.8.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.9.1-pyhc90fa1f_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_events-0.12.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.18.2-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server_terminals-0.5.4-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.5.7-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_pygments-0.3.0-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_server-2.28.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/lark-1.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mistune-3.2.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbclient-0.10.4-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbconvert-core-7.17.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbformat-5.10.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/notebook-shim-0.2.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/overrides-7.7.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.2-pyhc364b38_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pandocfilters-1.5.0-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/paramiko-4.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.7-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pathspec-1.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pexpect-4.9.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pixi-kernel-0.7.1-pyhbbac1ac_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.9.6-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/plumbum-1.10.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prometheus_client-0.25.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.51-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prompt_toolkit-3.0.51-hd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ptyprocess-0.7.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pydantic-2.13.4-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.21.2-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.13.13-h4df99d1_100.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-json-logger-3.2.1-pyh332efcf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2026.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pywin32-on-windows-0.1.0-pyh1179c8e_3.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/questionary-2.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.37.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.33.1-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/returns-0.27.0-pyhc364b38_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3339-validator-0.1.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3986-validator-0.1.1-pyh9f0ad1d_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3987-syntax-1.1.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/send2trash-2.1.0-pyh5552912_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-82.0.1-pyh332efcf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sniffio-1.3.1-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.8.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyhc90fa1f_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tinycss2-1.4.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.15.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.15.0-h396c80c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-inspection-0.4.2-pyhcf101f3_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_utils-0.1.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/uri-template-1.3.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.6.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.7.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/webcolors-25.10.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/webencodings-0.5.1-pyhd8ed1ab_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/websocket-client-1.9.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/argon2-cffi-bindings-25.1.0-py313h6535dbc_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/backports.zstd-1.4.0-py313h7208f8c_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bcrypt-5.0.0-py313h2c089d5_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-python-1.2.0-py313hde1f3bb_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-hd037594_9.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cffi-2.0.0-py313h224173a_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cryptography-48.0.0-py313hda5ae78_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/debugpy-1.8.20-py313h1188861_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/krb5-1.22.2-h385eeb1_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-22.1.5-h55c6f16_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libedit-3.1.20250104-pl5321hafb1f1b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.8.0-hf6b4638_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.5.2-hcf2aa1b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.3-h8088a28_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libmpdec-4.0.0-h84a0fba_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsodium-1.0.21-h1a92334_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.53.1-h1b79a29_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.2-h8088a28_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/markupsafe-3.0.3-py313h65a2061_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/msgspec-0.21.1-py313h0997733_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.6-h1d4f5a5_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.6.2-hd24854e_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/psutil-7.2.2-py313h6688731_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pydantic-core-2.46.4-py313h212e517_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pynacl-1.6.2-py313h6940bce_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyobjc-core-12.1-py313h40b429f_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyobjc-framework-cocoa-12.1-py313hcc5defa_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.13.13-h20e6be0_100_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyyaml-6.0.3-py313h65a2061_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyzmq-27.1.0-py312h022ad19_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.3-h46df422_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/rpds-py-0.30.0-py313h2c089d5_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ruff-0.15.12-hbd3f8a3_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h010d191_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tornado-6.5.5-py313h0997733_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/yaml-0.2.5-h925e9cb_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zeromq-4.3.5-h4818236_10.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hbf9d68e_6.conda + - pypi: https://files.pythonhosted.org/packages/07/c7/deb8c5e604404dbf10a3808a858946ca3547692ff6316b698945bb72177e/python_socketio-5.16.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0b/77/dbc82ff2fb0e63c6564356682bf201edff0ba16c98630d21a1fb312a8182/pandas-3.0.2-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/12/aa/fb2a0649fdeef5ab7072d221e8f4df164098792c813af6c87e2581cfa860/mpltoolbox-26.2.0-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/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/25/7d/cea3531f77df694ac7f169378250d85f19f69b09a5f4fa45f650837ae7cc/py3dmol-2.5.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2a/2d/d4bf65e47cea8ff2c794a600c4fd1273a7902f268757c531e0ee9f18aa58/pooch-1.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2d/47/634fe8323c6c2bfa86e10eb41ebfe410db5e6231aa1727a31ce4f002480f/spglib-2.6.0-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/35/8c/a917ef8741729ef8b3228f815240f4859717e600f9498359a6c411ed6992/crysfml-0.6.2-cp313-cp313-macosx_14_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/38/7e/7b91c89a4cf0f543a83be978657afb20c86af6d725253e319589dcc4ce52/lmfit-1.3.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/38/8b/7ec325b4e9e78beefc2d025b01ee8a2fde771ef7c957c3bff99b9e1fbffa/xraydb-4.5.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3a/eb/fea4d1d51c49832120f7f285d07306db3960f423a2612c6057caf3e8196f/pip-26.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3b/56/6f389de21c49555553d6a5aeed5ac9767631497ac836c4f076273d15bd72/fonttools-4.62.1-cp313-cp313-macosx_10_13_universal2.whl + - pypi: https://files.pythonhosted.org/packages/3f/0e/fa3b193432cfc60c93b42f3be03365f5f909d2b3ea410295cf36df739e31/widgetsnbextension-4.0.15-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3f/d0/7b958df957e4827837b590944008f0b28078f552b451f7407b4b3d54f574/asciichartpy-1.5.25-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/42/d9/27b13bc9419bf5dae02905b348f16ca827646cd76244ddd326f1a8139a6a/cyclebane-24.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4a/f3/00bb1e867fba351e2d784170955713bee200c43ea306c59f30bd7e748192/dask-2026.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/52/59/0782e51887ac6b07ffd1570e0364cf901ebc36345fea669969d2084baebb/simple_websocket-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/56/6d/0d9848617b9f753b87f214f1c682592f7ca42de085f564352f10f0843026/ipywidgets-8.1.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5b/58/3ce96251560107b381cbd6e8413c483bbb1228a6b919fa8652b0d4090e7f/scipy-1.17.1-cp313-cp313-macosx_14_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/5b/69/93b34728cc386efdde0c342f8c680b9187dea7beb7adaf6b58a0713be101/mpld3-0.5.12-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5c/01/6cb4d63c6b6933be4b7945b2f64638336420f04ea71ca5b9a7539c008bc5/scippnexus-26.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/61/2b/e260d50e64690d2a9e405d52ccd18a63c286c5088937dd0107cb23eb3195/diffpy_utils-3.7.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/63/d9/9e14bc7564bf92d5ffa801ae5fac819ce74b925dfb55e3ebde61a3bbad3e/matplotlib-3.10.9-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/6c/25/4f103d1bedb3593718713b3f743df7b3ff3fc68d36d6666c30265ef59c8a/ase-3.28.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/71/43/905a14a8b17fdb1ccb58d282454490662d2cb89a6bfec26af6d3520da5ec/pillow-12.2.0-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/78/3c/2a612b95ddbb9a6bdcb47b7a93c4884f74c6ff22356b2f7b213b16e65c35/pycifstar-0.3.0.tar.gz + - pypi: https://files.pythonhosted.org/packages/79/76/55cd7186f498ed080a18440c9013011eb548f77ae1b297206d030eb1180a/multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/79/fe/b14d806894cf05178f1e77d0d619f071db50cf698bc654c54f9241223bcf/scipp-26.3.1-cp313-cp313-macosx_14_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/7b/67/b1944235474aac3f0b0e1b232ce49547f9f9461ca4b943df1b88da5d3f1d/bumps-1.0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/82/9b/cf7e6f157c53d2c8bc0165580350c53939aea3b2a8390ec8e07f0adb82de/scippneutron-26.4.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/83/81/bdd4bfabe70b7c9a8c0716a722ced4ebd27311afd1f4800cd405d3229c1b/pycifrw-5.0.1-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/89/a4/92380f7ca60f99ebae761936bc48a72a639e8a47b29050615eef757cb2a7/propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/8a/06/2c1bd1ee9eee3e65b0b6395dcf960e71b11576995eae3b4ab9ec63d89bea/essreduce-26.4.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8a/a1/8d812e53a5da1687abb10445275d41a8b13adb781bbf7196ddbcf8d88505/lazy_loader-0.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8d/c0/fdf9d3ee103ce66a55f0532835ad5e154226c5222423c6636ba049dc42fc/traittypes-0.2.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/90/ad/cba91b3bcf04073e4d1655a5c1710ef3f457f56f7d1b79dcc3d72f4dd912/plotly-6.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/91/4c/e0ce1ef95d4000ebc1c11801f9b944fa5910ecc15b5e351865763d8657f8/graphviz-0.21-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/91/88/b55b3117287a8540b76dbdd87733808d4d01c8067a3b339408c250bb3600/typeguard-4.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/92/dc/c385f38f2c2433333345a82926c6bfa5ecfff3ef787201614317b58dd8be/msgpack-1.1.2-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/96/5d/0c59079aa7ef34980a5925a06a90ad2b7c94e486c194b3527d557cabb042/cryspy-0.11.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/96/e4/7adcd9c8362745b2210728f209bfbcf7d91ba868a2c5f40d8b58f54c509b/contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/99/31/6cf181011dc738c33bf6ba7aea2e8e1d3c1f71b7dab1942f3054f66f6202/asteval-1.0.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/99/37/e8730c3587a65eb5645d4aba2d27aae48e8003614d6aaf15dda67f702f1f/bidict-0.23.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9c/f5/0fb20fb49f8efdcdce6cd8127604ad2c503e754a8f139f5e02b01626523f/aiohttp-3.13.5-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a3/58/35da89ee790598a0700ea49b2a66594140f44dec458c07e8e3d4979137fc/ply-3.11-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a4/39/3a0ae5b0edb66e61bb0e8bc53a503495cba5892297ae21faf6ba0525e681/varname-1.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a8/3a/d0a972b34e1c63e2409413104216cd1caa02c5a37cb668d1687d466c1c45/kiwisolver-1.5.0-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/a8/88/802c82060c54bc7dde21eb0033e337838b8181a1323254aa9ec41cbfc3d1/markdown_it_py-4.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/aa/54/0cce26da03a981f949bb8449c9778537f75f5917c172e1d2992ff25cb57d/python_engineio-4.13.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ab/b5/36c712098e6191d1b4e349304ef73a8d06aed77e56ceaac8c0a306c7bda1/jupyterlab_widgets-3.0.16-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ae/50/06d511cc4b8e0360d3c94af051a768e84b755c5eb031b12adaaab6dec6e5/yarl-1.23.0-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/ae/81/81755f50eb2478eaf2049728491d4ea4f416c1eb013338682173259efa09/sqlalchemy-2.0.49-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/af/46/661159ad844034ba8b3f4e0516215c41e4ee17db4213d13a82227670764f/sciline-25.11.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b0/65/5e088a45d0f43cd814bc5bec521c051d42005a472e804b1a36c48dada09b/h5py-3.16.0-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b4/5e/bf11645aebb9af7d8d35927c40d3855816a0855c799e8156eeca8d632c90/diffpy_structure-3.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bf/e1/9e8e09ab8fc5c77f228e3271188dc4f569d12692548c42bff81b1fbba5e1/easydiffraction-0.16.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c1/9c/1236dd7d22ed48527286b613c84e3376ea731b65e6734b6e6a0b4d03744c/gemmi-0.7.5-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/c4/6d/82e65254354ba651dc966775270a9bbc02414a3eb3f1704e6c87dab2ea83/essdiffraction-26.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c7/a0/5ff05d1919ca249508012cad89f08fdc6cfbdaa15b41651c5fe6dffaf1d3/dfo_ls-1.6.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c7/ea/7988934c8e3e3418aa043f70421817df28d06aef50bfd85f5ad3ec6e70f1/ncrystal_core-4.4.2-py3-none-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/d0/69/f24d3d1c38ad69e256138b4ec2452a8c7cf66be49dc214771ae99dd4f0a0/narwhals-2.20.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d3/80/722402571443031989e87a5861f26f2e897778768906dff1f998b42f235d/diffpy_pdffit2-1.6.0-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/d5/0c/043d5e551459da400957a1395e0febbf771446ff34291afcbe3d8be2a279/fsspec-2026.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d8/8b/e2bbeb42068f0c48899e8eddd34902afc0f7429d4d2a152d2dc2670dc661/pythreejs-2.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/db/bc/83e112abc66cd466c6b83f99118035867cecd41802f8d044638aa78a106e/locket-1.0.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/dc/b7/901d837999a9350a7773289f7760cb473d4ba01fdc6ebae0ff2553d269ac/ncrystal_python-4.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e4/bc/daa30c02069eeac5b9198985ba42f5d65ca71bed6705b18329e51d352b7c/plopp-26.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e5/87/499737bfba066b4a3bebff24a8f1c5b2dee410b209bc6668c9be692580f0/numpy-2.4.4-cp313-cp313-macosx_14_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ee/8c/83087ebc47ab0396ce092363001fa37c17153119ee282700c0713a195853/prettytable-3.17.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f1/5b/e63c877c4c94382b66de5045e08ec8cd960e8a4d22f0d62a4dfb1f9e5ac6/ipydatawidgets-4.3.5-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f2/f2/728f041460f1b9739b85ee23b45fa5a505962ea11fd85bdbe2a02b021373/darkdetect-0.8.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f5/57/2a154a69d6642860300bf8eb205d13131104991f2b1065bbb9075ac5c32e/tof-26.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f7/00/bbca25f8a2372465cdf93138c1e1e38dff045fb0afef1488f395d0afcb3b/ncrystal-4.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl + win-64: + - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/annotated-types-0.7.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.13.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/argon2-cffi-25.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/arrow-1.4.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.18.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.14.3-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.3.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-with-css-6.3.0-hbca2aae_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.4.22-h4c7d964_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cached-property-1.5.2-hd8ed1ab_1.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/cached_property-1.5.2-pyha770c72_1.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2026.4.22-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.7-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.3-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/copier-9.15.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.13.13-py313hd8ed1ab_100.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/decorator-5.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/defusedxml-0.7.1-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/dunamai-1.26.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/eval-type-backport-0.2.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/eval_type_backport-0.2.2-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fqdn-1.5.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/funcy-2.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h11-0.16.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/httpcore-1.0.9-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/httpx-0.28.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.13-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.8.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib_resources-7.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/invoke-3.0.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.2.0-pyh6dadd2b_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.13.0-pyhe2676ad_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython_pygments_lexers-1.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/isoduration-20.11.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jedi-0.19.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-ansible-filters-1.3.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/json5-0.14.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonpointer-3.1.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.26.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.9.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-with-format-nongpl-4.26.0-hcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter-lsp-2.3.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.8.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.9.1-pyh6dadd2b_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_events-0.12.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.18.2-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server_terminals-0.5.4-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.5.7-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_pygments-0.3.0-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_server-2.28.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/lark-1.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mistune-3.2.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbclient-0.10.4-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbconvert-core-7.17.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbformat-5.10.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/notebook-shim-0.2.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/overrides-7.7.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.2-pyhc364b38_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pandocfilters-1.5.0-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/paramiko-4.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.7-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pathspec-1.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pixi-kernel-0.7.1-pyhbbac1ac_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.9.6-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/plumbum-1.10.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prometheus_client-0.25.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.51-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prompt_toolkit-3.0.51-hd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pydantic-2.13.4-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyh09c184e_7.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.21.2-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.13.13-h4df99d1_100.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-json-logger-3.2.1-pyh332efcf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2026.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pywin32-on-windows-0.1.0-pyh07e9846_2.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/questionary-2.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.37.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.33.1-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/returns-0.27.0-pyhc364b38_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3339-validator-0.1.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3986-validator-0.1.1-pyh9f0ad1d_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3987-syntax-1.1.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/send2trash-2.1.0-pyh6dadd2b_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-82.0.1-pyh332efcf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sniffio-1.3.1-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.8.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyh6dadd2b_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tinycss2-1.4.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.15.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.15.0-h396c80c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-inspection-0.4.2-pyhcf101f3_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_utils-0.1.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/uri-template-1.3.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.6.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.7.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/webcolors-25.10.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/webencodings-0.5.1-pyhd8ed1ab_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/websocket-client-1.9.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/win_inet_pton-1.1.0-pyh7428d3b_8.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/argon2-cffi-bindings-25.1.0-py313h5ea7bf4_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/backports.zstd-1.4.0-py313h2a31948_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/bcrypt-5.0.0-py313hfbe8231_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/brotli-python-1.2.0-py313h3ebfc14_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h0ad9c76_9.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/cffi-2.0.0-py313h5ea7bf4_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/cryptography-48.0.0-py313hf5c5e30_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/debugpy-1.8.20-py313h927ade5_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/krb5-1.22.2-h0ea6238_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.8.0-hac47afa_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libffi-3.5.2-h3d046cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-5.8.3-hfd05255_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libmpdec-4.0.0-hfd05255_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libsodium-1.0.21-h6a83c73_3.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.53.1-hf5d6505_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libzlib-1.3.2-hfd05255_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/markupsafe-3.0.3-py313hd650c13_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/msgspec-0.21.1-py313h5ea7bf4_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.6.2-hf411b9b_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/psutil-7.2.2-py313h5fd188c_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pydantic-core-2.46.4-py313hfbe8231_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pynacl-1.6.2-py313hed4ef92_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/python-3.13.13-h09917c8_100_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pywin32-311-py313h40c08fc_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pywinpty-2.0.15-py313h5813708_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pyyaml-6.0.3-py313hd650c13_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pyzmq-27.1.0-py312h343a6d4_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/rpds-py-0.30.0-py313hfbe8231_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/ruff-0.15.12-hd7ccaa8_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h6ed50ae_3.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/tornado-6.5.5-py313h5ea7bf4_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.26100.0-h57928b3_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-h41ae7f8_34.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.44.35208-h818238b_34.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.44.35208-h818238b_34.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/winpty-0.4.3-4.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/win-64/yaml-0.2.5-h6a83c73_3.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/zeromq-4.3.5-h507cc87_10.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_6.conda + - pypi: https://files.pythonhosted.org/packages/07/c7/deb8c5e604404dbf10a3808a858946ca3547692ff6316b698945bb72177e/python_socketio-5.16.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/12/aa/fb2a0649fdeef5ab7072d221e8f4df164098792c813af6c87e2581cfa860/mpltoolbox-26.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/15/fe/b426215b6843ee70285521e2dd52f08a32096fe1d68e5b92fbdaffdf8ca3/diffpy_pdffit2-1.6.0-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/16/56/a31e8d3c9e8d21100b83bbe1c1f3f7c94db317393a229e193461e5e6d2a4/spglib-2.6.0-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/17/3a/7215b1b7d6d49dc9a87211be44562077f5f04f9bb5a59552c1c8e2d98173/sqlalchemy-2.0.49-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/18/0b/0098c214843213759692cc638fce7de5c289200a830e5035d1791d7a2338/contourpy-1.3.3-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/25/7d/cea3531f77df694ac7f169378250d85f19f69b09a5f4fa45f650837ae7cc/py3dmol-2.5.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2a/2d/d4bf65e47cea8ff2c794a600c4fd1273a7902f268757c531e0ee9f18aa58/pooch-1.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/35/e5/d6d0e51fc888f692a35134336866341c08655d92614f492c6860dc45bb2c/scipy-1.17.1-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/37/fd/22621d3ee9e3ee87ef4c89b63bba55b265ab85039b3c1ba88ed2380a24c1/scipp-26.3.1-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/38/60/35186529de1db3c01f5ad625bde07c1f576305eab6d86bbda4c58445f721/fonttools-4.62.1-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/38/7e/7b91c89a4cf0f543a83be978657afb20c86af6d725253e319589dcc4ce52/lmfit-1.3.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/38/8b/7ec325b4e9e78beefc2d025b01ee8a2fde771ef7c957c3bff99b9e1fbffa/xraydb-4.5.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3a/eb/fea4d1d51c49832120f7f285d07306db3960f423a2612c6057caf3e8196f/pip-26.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3f/0e/fa3b193432cfc60c93b42f3be03365f5f909d2b3ea410295cf36df739e31/widgetsnbextension-4.0.15-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3f/82/8a3739a5e470b3c6cbb1d21d315800d8e16bff503d1f16b03a4ec3212786/pillow-12.2.0-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/3f/d0/7b958df957e4827837b590944008f0b28078f552b451f7407b4b3d54f574/asciichartpy-1.5.25-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/42/d9/27b13bc9419bf5dae02905b348f16ca827646cd76244ddd326f1a8139a6a/cyclebane-24.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4a/f3/00bb1e867fba351e2d784170955713bee200c43ea306c59f30bd7e748192/dask-2026.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/52/59/0782e51887ac6b07ffd1570e0364cf901ebc36345fea669969d2084baebb/simple_websocket-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/56/6d/0d9848617b9f753b87f214f1c682592f7ca42de085f564352f10f0843026/ipywidgets-8.1.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/57/fb/48649b4971cde70d817cf97a2a2fdc0b4d8308569f1dd2f2611959d2e0cf/numpy-2.4.4-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/5b/69/93b34728cc386efdde0c342f8c680b9187dea7beb7adaf6b58a0713be101/mpld3-0.5.12-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5c/01/6cb4d63c6b6933be4b7945b2f64638336420f04ea71ca5b9a7539c008bc5/scippnexus-26.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5e/52/42855bf626868413f761addd574acc6195880ae247a5346477a4361c3acb/pandas-3.0.2-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/61/2b/e260d50e64690d2a9e405d52ccd18a63c286c5088937dd0107cb23eb3195/diffpy_utils-3.7.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6c/25/4f103d1bedb3593718713b3f743df7b3ff3fc68d36d6666c30265ef59c8a/ase-3.28.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/74/07/1ed8277f8653c40ebc65985180b007879f6a836c525b3885dcc6448ae6cb/msgpack-1.1.2-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/78/3c/2a612b95ddbb9a6bdcb47b7a93c4884f74c6ff22356b2f7b213b16e65c35/pycifstar-0.3.0.tar.gz + - pypi: https://files.pythonhosted.org/packages/7a/84/266e8da36879c6edcd37b02b547e2d9ecdfea776be49598e75696e3316e1/yarl-1.23.0-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/7b/67/b1944235474aac3f0b0e1b232ce49547f9f9461ca4b943df1b88da5d3f1d/bumps-1.0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/82/9b/cf7e6f157c53d2c8bc0165580350c53939aea3b2a8390ec8e07f0adb82de/scippneutron-26.4.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8a/06/2c1bd1ee9eee3e65b0b6395dcf960e71b11576995eae3b4ab9ec63d89bea/essreduce-26.4.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8a/a1/8d812e53a5da1687abb10445275d41a8b13adb781bbf7196ddbcf8d88505/lazy_loader-0.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8d/c0/fdf9d3ee103ce66a55f0532835ad5e154226c5222423c6636ba049dc42fc/traittypes-0.2.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/90/ad/cba91b3bcf04073e4d1655a5c1710ef3f457f56f7d1b79dcc3d72f4dd912/plotly-6.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/91/4c/e0ce1ef95d4000ebc1c11801f9b944fa5910ecc15b5e351865763d8657f8/graphviz-0.21-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/91/88/b55b3117287a8540b76dbdd87733808d4d01c8067a3b339408c250bb3600/typeguard-4.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/96/5d/0c59079aa7ef34980a5925a06a90ad2b7c94e486c194b3527d557cabb042/cryspy-0.11.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/99/31/6cf181011dc738c33bf6ba7aea2e8e1d3c1f71b7dab1942f3054f66f6202/asteval-1.0.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/99/37/e8730c3587a65eb5645d4aba2d27aae48e8003614d6aaf15dda67f702f1f/bidict-0.23.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9f/9b/50835e8fd86073fa7aa921df61b4cebc1f0ff400e4338541675cb72b5507/pycifrw-5.0.1-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a3/58/35da89ee790598a0700ea49b2a66594140f44dec458c07e8e3d4979137fc/ply-3.11-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a4/39/3a0ae5b0edb66e61bb0e8bc53a503495cba5892297ae21faf6ba0525e681/varname-1.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a8/88/802c82060c54bc7dde21eb0033e337838b8181a1323254aa9ec41cbfc3d1/markdown_it_py-4.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/aa/54/0cce26da03a981f949bb8449c9778537f75f5917c172e1d2992ff25cb57d/python_engineio-4.13.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ab/b5/36c712098e6191d1b4e349304ef73a8d06aed77e56ceaac8c0a306c7bda1/jupyterlab_widgets-3.0.16-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/af/46/661159ad844034ba8b3f4e0516215c41e4ee17db4213d13a82227670764f/sciline-25.11.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b2/35/e994121b0e90e46134673422dd564623f93304614f5d11886b1b3e06f503/multidict-6.7.1-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/b2/8f/22bf9df92bbff0eb07842b60f7e63bf7675a9742df628437a9f02d09137f/greenlet-3.5.0-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b4/5e/bf11645aebb9af7d8d35927c40d3855816a0855c799e8156eeca8d632c90/diffpy_structure-3.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/be/8a/be60e3bbcf513cc5a50f4a3e88e1dcecebb79c1ad607a7222877becaa101/kiwisolver-1.5.0-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/bf/e1/9e8e09ab8fc5c77f228e3271188dc4f569d12692548c42bff81b1fbba5e1/easydiffraction-0.16.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c0/1b/ac685a8882896acf0f6b31d689e3792199cfe7aba37969fa91da63a7fa27/aiohttp-3.13.5-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/c3/d9/a27997f84341fc0dfcdd1fe4179b6ba6c32a7aa880fdb8c514d4dad6fba3/h5py-3.16.0-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/c4/6d/82e65254354ba651dc966775270a9bbc02414a3eb3f1704e6c87dab2ea83/essdiffraction-26.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c5/e0/0840fd2f93da988ec660b8ad1984abe9f25d2aed22a5e394ff1c68c88307/matplotlib-3.10.9-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/c7/6b/6c02f55c2ce2f137ccca0986be7dd89bea31d5bee4346b4377fa3b8586df/ncrystal_core-4.4.2-py3-none-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/c7/a0/5ff05d1919ca249508012cad89f08fdc6cfbdaa15b41651c5fe6dffaf1d3/dfo_ls-1.6.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d0/69/f24d3d1c38ad69e256138b4ec2452a8c7cf66be49dc214771ae99dd4f0a0/narwhals-2.20.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d5/0c/043d5e551459da400957a1395e0febbf771446ff34291afcbe3d8be2a279/fsspec-2026.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d8/8b/e2bbeb42068f0c48899e8eddd34902afc0f7429d4d2a152d2dc2670dc661/pythreejs-2.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/db/bc/83e112abc66cd466c6b83f99118035867cecd41802f8d044638aa78a106e/locket-1.0.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/dc/b7/901d837999a9350a7773289f7760cb473d4ba01fdc6ebae0ff2553d269ac/ncrystal_python-4.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e4/bc/daa30c02069eeac5b9198985ba42f5d65ca71bed6705b18329e51d352b7c/plopp-26.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ed/01/e1da2f721c3a8feec311a16924c42fb28c37445528df0747f47dec5232d5/crysfml-0.6.2-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/ee/8c/83087ebc47ab0396ce092363001fa37c17153119ee282700c0713a195853/prettytable-3.17.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ee/ab/7d7463cda94f8b68b969ea97aaad679655a0e436efd6a643e528a8de114e/gemmi-0.7.5-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/f1/5b/e63c877c4c94382b66de5045e08ec8cd960e8a4d22f0d62a4dfb1f9e5ac6/ipydatawidgets-4.3.5-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f2/f2/728f041460f1b9739b85ee23b45fa5a505962ea11fd85bdbe2a02b021373/darkdetect-0.8.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f5/57/2a154a69d6642860300bf8eb205d13131104991f2b1065bbb9075ac5c32e/tof-26.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f5/ab/f76ec3c3627c883215b5c8080debb4394ef5a7a29be811f786415fc1e6fd/propcache-0.4.1-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/f7/00/bbca25f8a2372465cdf93138c1e1e38dff045fb0afef1488f395d0afcb3b/ncrystal-4.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl +packages: +- conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda + build_number: 20 + sha256: 1dd3fffd892081df9726d7eb7e0dea6198962ba775bd88842135a4ddb4deb3c9 + md5: a9f577daf3de00bca7c3c76c0ecbd1de + depends: + - __glibc >=2.17,<3.0.a0 + - libgomp >=7.5.0 + constrains: + - openmp_impl <0.0a0 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 28948 + timestamp: 1770939786096 +- conda: https://conda.anaconda.org/conda-forge/linux-64/argon2-cffi-bindings-25.1.0-py311h49ec1c0_2.conda + sha256: b81f852f13a1d148f6ad7e2a29ab375eb1558b73c9bfa38792d98ea7fb414cff + md5: 6e36e9d2b535c3fbe2e093108df26695 + depends: + - __glibc >=2.17,<3.0.a0 + - cffi >=1.0.1 + - libgcc >=14 + - python >=3.11,<3.12.0a0 + - python_abi 3.11.* *_cp311 + license: MIT + license_family: MIT + purls: + - pkg:pypi/argon2-cffi-bindings?source=hash-mapping + size: 35831 + timestamp: 1762509453632 +- conda: https://conda.anaconda.org/conda-forge/linux-64/argon2-cffi-bindings-25.1.0-py313h07c4f96_2.conda + sha256: ad188ccc06a06c633dc124b09e9e06fb9df4c32ffc38acc96ecc86e506062090 + md5: 27bbec9f2f3a15d32b60ec5734f5b41c + depends: + - __glibc >=2.17,<3.0.a0 + - cffi >=1.0.1 + - libgcc >=14 + - python >=3.13,<3.14.0a0 + - python_abi 3.13.* *_cp313 + license: MIT + license_family: MIT + purls: + - pkg:pypi/argon2-cffi-bindings?source=hash-mapping + size: 35943 + timestamp: 1762509452935 +- conda: https://conda.anaconda.org/conda-forge/linux-64/backports.zstd-1.4.0-py311h6b1f9c4_0.conda + sha256: a23717f9dc06924b988b92ba8dca0f4cb9154cac954457a40adb2f19d57896de + md5: aa8c3009fd8903bebdcb22fbcb4c0dea + depends: + - python + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - python_abi 3.11.* *_cp311 + - zstd >=1.5.7,<1.6.0a0 + license: BSD-3-Clause AND MIT AND EPL-2.0 + purls: + - pkg:pypi/backports-zstd?source=compressed-mapping + size: 245341 + timestamp: 1777848716685 +- conda: https://conda.anaconda.org/conda-forge/linux-64/backports.zstd-1.4.0-py313h18e8e13_0.conda + sha256: f92ccaef33713bb66d563e8e829dcdb643e1de8c0d29246a999943c68bb8eb6e + md5: cfe95f3eab50bc3dbd4c3a03f2674bd8 + depends: + - python + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - python_abi 3.13.* *_cp313 + - zstd >=1.5.7,<1.6.0a0 + license: BSD-3-Clause AND MIT AND EPL-2.0 + purls: + - pkg:pypi/backports-zstd?source=compressed-mapping + size: 241034 + timestamp: 1777848719564 +- conda: https://conda.anaconda.org/conda-forge/linux-64/bcrypt-5.0.0-py311h902ca64_1.conda + sha256: 978ce415ca2868f8415ce54a9c33fe2d2c9efd6a78dc1a6fe79a4bc3d055fe66 + md5: 28b014780cae10a8435d012ef08c0202 + depends: + - python + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - python_abi 3.11.* *_cp311 + constrains: + - __glibc >=2.17 + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/bcrypt?source=hash-mapping + size: 292775 + timestamp: 1762497725251 +- conda: https://conda.anaconda.org/conda-forge/linux-64/bcrypt-5.0.0-py313h843e2db_1.conda + sha256: cdf7496797af275f8bb5edd270aa06303dde623c7dd3a5941b0ce085d8f4cdc5 + md5: b59ec3796cba93d0db5f71e361176f27 + depends: + - python + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - python_abi 3.13.* *_cp313 + constrains: + - __glibc >=2.17 + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/bcrypt?source=hash-mapping + size: 292710 + timestamp: 1762497710166 +- conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.2.0-py311h66f275b_1.conda + sha256: c36eb061d9ead85f97644cfb740d485dba9b8823357f35c17851078e95e975c1 + md5: 86daecb8e4ed1042d5dc6efbe0152590 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libstdcxx >=14 + - python >=3.11,<3.12.0a0 + - python_abi 3.11.* *_cp311 + constrains: + - libbrotlicommon 1.2.0 hb03c661_1 + license: MIT + license_family: MIT + purls: + - pkg:pypi/brotli?source=hash-mapping + size: 367573 + timestamp: 1764017405384 +- conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.2.0-py313hf159716_1.conda + sha256: dadec2879492adede0a9af0191203f9b023f788c18efd45ecac676d424c458ae + md5: 6c4d3597cf43f3439a51b2b13e29a4ba + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libstdcxx >=14 + - python >=3.13,<3.14.0a0 + - python_abi 3.13.* *_cp313 + constrains: + - libbrotlicommon 1.2.0 hb03c661_1 + license: MIT + license_family: MIT + purls: + - pkg:pypi/brotli?source=hash-mapping + size: 367721 + timestamp: 1764017371123 +- conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_9.conda + sha256: 0b75d45f0bba3e95dc693336fa51f40ea28c980131fec438afb7ce6118ed05f6 + md5: d2ffd7602c02f2b316fd921d39876885 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: bzip2-1.0.6 + license_family: BSD + purls: [] + size: 260182 + timestamp: 1771350215188 +- conda: https://conda.anaconda.org/conda-forge/linux-64/c-ares-1.34.6-hb03c661_0.conda + sha256: cc9accf72fa028d31c2a038460787751127317dcfa991f8d1f1babf216bb454e + md5: 920bb03579f15389b9e512095ad995b7 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: MIT + license_family: MIT + purls: [] + size: 207882 + timestamp: 1765214722852 +- conda: https://conda.anaconda.org/conda-forge/linux-64/cffi-2.0.0-py311h03d9500_1.conda + sha256: 3ad13377356c86d3a945ae30e9b8c8734300925ef81a3cb0a9db0d755afbe7bb + md5: 3912e4373de46adafd8f1e97e4bd166b + depends: + - __glibc >=2.17,<3.0.a0 + - libffi >=3.5.2,<3.6.0a0 + - libgcc >=14 + - pycparser + - python >=3.11,<3.12.0a0 + - python_abi 3.11.* *_cp311 + license: MIT + license_family: MIT + purls: + - pkg:pypi/cffi?source=hash-mapping + size: 303338 + timestamp: 1761202960110 +- conda: https://conda.anaconda.org/conda-forge/linux-64/cffi-2.0.0-py313hf46b229_1.conda + sha256: 2162a91819945c826c6ef5efe379e88b1df0fe9a387eeba23ddcf7ebeacd5bd6 + md5: d0616e7935acab407d1543b28c446f6f + depends: + - __glibc >=2.17,<3.0.a0 + - libffi >=3.5.2,<3.6.0a0 + - libgcc >=14 + - pycparser + - python >=3.13,<3.14.0a0 + - python_abi 3.13.* *_cp313 + license: MIT + license_family: MIT + purls: + - pkg:pypi/cffi?source=hash-mapping + size: 298357 + timestamp: 1761202966461 +- conda: https://conda.anaconda.org/conda-forge/linux-64/cryptography-48.0.0-py311h2005dd1_0.conda + sha256: 838cde68e70f4e0dc6458613316fa0e50eee5cf86cafb1cee4b7ca1a701a8436 + md5: 96f6e551c7ff30c95c1c8b4d2d1c5c9f + depends: + - __glibc >=2.17,<3.0.a0 + - cffi >=2.0 + - libgcc >=14 + - openssl >=3.5.6,<4.0a0 + - python >=3.11,<3.12.0a0 + - python_abi 3.11.* *_cp311 + constrains: + - __glibc >=2.17 + license: Apache-2.0 AND BSD-3-Clause AND PSF-2.0 AND MIT + license_family: BSD + purls: + - pkg:pypi/cryptography?source=compressed-mapping + size: 1913519 + timestamp: 1777966158377 +- conda: https://conda.anaconda.org/conda-forge/linux-64/cryptography-48.0.0-py313heb322e3_0.conda + sha256: 7c10cdde9f46d4b990e9c90ffb72bfb672b56d2ecae243d95582c9a9fc417ebf + md5: 2310978ddde7f742d0b7a642d4894388 + depends: + - __glibc >=2.17,<3.0.a0 + - cffi >=2.0 + - libgcc >=14 + - openssl >=3.5.6,<4.0a0 + - python >=3.13,<3.14.0a0 + - python_abi 3.13.* *_cp313 + constrains: + - __glibc >=2.17 + license: Apache-2.0 AND BSD-3-Clause AND PSF-2.0 AND MIT + license_family: BSD + purls: + - pkg:pypi/cryptography?source=hash-mapping + size: 1916864 + timestamp: 1777966204947 +- conda: https://conda.anaconda.org/conda-forge/linux-64/debugpy-1.8.20-py311hc665b79_0.conda + sha256: e69be2be543c4d4898895d8aebe758bc683c5a1198583ad676f5719782a07131 + md5: 400e4667a12884216df869cad5fb004b + depends: + - python + - libgcc >=14 + - libstdcxx >=14 + - __glibc >=2.17,<3.0.a0 + - python_abi 3.11.* *_cp311 + license: MIT + license_family: MIT + purls: + - pkg:pypi/debugpy?source=hash-mapping + size: 2733654 + timestamp: 1769744984842 +- conda: https://conda.anaconda.org/conda-forge/linux-64/debugpy-1.8.20-py313h5d5ffb9_0.conda + sha256: 8d76d4eeb5a8e3c5666880b465593fdf4a44f47fbbe89ff5b8f9abbe43026326 + md5: e94dbbec2589f3b1d821f43a4bf2ab45 + depends: + - python + - libstdcxx >=14 + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - python_abi 3.13.* *_cp313 + license: MIT + license_family: MIT + purls: + - pkg:pypi/debugpy?source=hash-mapping + size: 2872698 + timestamp: 1769744980407 +- conda: https://conda.anaconda.org/conda-forge/linux-64/icu-78.3-h33c6efd_0.conda + sha256: fbf86c4a59c2ed05bbffb2ba25c7ed94f6185ec30ecb691615d42342baa1a16a + md5: c80d8a3b84358cb967fa81e7075fbc8a + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libstdcxx >=14 + license: MIT + license_family: MIT + purls: [] + size: 12723451 + timestamp: 1773822285671 +- conda: https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.3-hb9d3cd8_0.conda + sha256: 0960d06048a7185d3542d850986d807c6e37ca2e644342dd0c72feefcf26c2a4 + md5: b38117a3c920364aff79f870c984b4a3 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + license: LGPL-2.1-or-later + purls: [] + size: 134088 + timestamp: 1754905959823 +- conda: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.22.2-ha1258a1_0.conda + sha256: 3e307628ca3527448dd1cb14ad7bb9d04d1d28c7d4c5f97ba196ae984571dd25 + md5: fb53fb07ce46a575c5d004bbc96032c2 + depends: + - __glibc >=2.17,<3.0.a0 + - keyutils >=1.6.3,<2.0a0 + - libedit >=3.1.20250104,<3.2.0a0 + - libedit >=3.1.20250104,<4.0a0 + - libgcc >=14 + - libstdcxx >=14 + - openssl >=3.5.5,<4.0a0 + license: MIT + license_family: MIT + purls: [] + size: 1386730 + timestamp: 1769769569681 +- conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45.1-default_hbd61a6d_102.conda + sha256: 3d584956604909ff5df353767f3a2a2f60e07d070b328d109f30ac40cd62df6c + md5: 18335a698559cdbcd86150a48bf54ba6 + depends: + - __glibc >=2.17,<3.0.a0 + - zstd >=1.5.7,<1.6.0a0 + constrains: + - binutils_impl_linux-64 2.45.1 + license: GPL-3.0-only + license_family: GPL + purls: [] + size: 728002 + timestamp: 1774197446916 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libabseil-20260107.1-cxx17_h7b12aa8_0.conda + sha256: a7a4481a4d217a3eadea0ec489826a69070fcc3153f00443aa491ed21527d239 + md5: 6f7b4302263347698fd24565fbf11310 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libstdcxx >=14 + constrains: + - libabseil-static =20260107.1=cxx17* + - abseil-cpp =20260107.1 + license: Apache-2.0 + license_family: Apache + purls: [] + size: 1384817 + timestamp: 1770863194876 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlicommon-1.2.0-hb03c661_1.conda + sha256: 318f36bd49ca8ad85e6478bd8506c88d82454cc008c1ac1c6bf00a3c42fa610e + md5: 72c8fd1af66bd67bf580645b426513ed + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: MIT + license_family: MIT + purls: [] + size: 79965 + timestamp: 1764017188531 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlidec-1.2.0-hb03c661_1.conda + sha256: 12fff21d38f98bc446d82baa890e01fd82e3b750378fedc720ff93522ffb752b + md5: 366b40a69f0ad6072561c1d09301c886 + depends: + - __glibc >=2.17,<3.0.a0 + - libbrotlicommon 1.2.0 hb03c661_1 + - libgcc >=14 + license: MIT + license_family: MIT + purls: [] + size: 34632 + timestamp: 1764017199083 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlienc-1.2.0-hb03c661_1.conda + sha256: a0c15c79997820bbd3fbc8ecf146f4fe0eca36cc60b62b63ac6cf78857f1dd0d + md5: 4ffbb341c8b616aa2494b6afb26a0c5f + depends: + - __glibc >=2.17,<3.0.a0 + - libbrotlicommon 1.2.0 hb03c661_1 + - libgcc >=14 + license: MIT + license_family: MIT + purls: [] + size: 298378 + timestamp: 1764017210931 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20250104-pl5321h7949ede_0.conda + sha256: d789471216e7aba3c184cd054ed61ce3f6dac6f87a50ec69291b9297f8c18724 + md5: c277e0a4d549b03ac1e9d6cbbe3d017b + depends: + - ncurses + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - ncurses >=6.5,<7.0a0 + license: BSD-2-Clause + license_family: BSD + purls: [] + size: 134676 + timestamp: 1738479519902 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libev-4.33-hd590300_2.conda + sha256: 1cd6048169fa0395af74ed5d8f1716e22c19a81a8a36f934c110ca3ad4dd27b4 + md5: 172bf1cd1ff8629f2b1179945ed45055 + depends: + - libgcc-ng >=12 + license: BSD-2-Clause + license_family: BSD + purls: [] + size: 112766 + timestamp: 1702146165126 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.8.0-hecca717_0.conda + sha256: ea33c40977ea7a2c3658c522230058395bc2ee0d89d99f0711390b6a1ee80d12 + md5: a3b390520c563d78cc58974de95a03e5 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + constrains: + - expat 2.8.0.* + license: MIT + license_family: MIT + purls: [] + size: 77241 + timestamp: 1777846112704 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h3435931_0.conda + sha256: 31f19b6a88ce40ebc0d5a992c131f57d919f73c0b92cd1617a5bec83f6e961e6 + md5: a360c33a5abe61c07959e449fa1453eb + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: MIT + license_family: MIT + purls: [] + size: 58592 + timestamp: 1769456073053 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-he0feb66_18.conda + sha256: faf7d2017b4d718951e3a59d081eb09759152f93038479b768e3d612688f83f5 + md5: 0aa00f03f9e39fb9876085dee11a85d4 + depends: + - __glibc >=2.17,<3.0.a0 + - _openmp_mutex >=4.5 + constrains: + - libgcc-ng ==15.2.0=*_18 + - libgomp 15.2.0 he0feb66_18 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 1041788 + timestamp: 1771378212382 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.2.0-h69a702a_18.conda + sha256: e318a711400f536c81123e753d4c797a821021fb38970cebfb3f454126016893 + md5: d5e96b1ed75ca01906b3d2469b4ce493 + depends: + - libgcc 15.2.0 he0feb66_18 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 27526 + timestamp: 1771378224552 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-he0feb66_18.conda + sha256: 21337ab58e5e0649d869ab168d4e609b033509de22521de1bfed0c031bfc5110 + md5: 239c5e9546c38a1e884d69effcf4c882 + depends: + - __glibc >=2.17,<3.0.a0 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 603262 + timestamp: 1771378117851 +- conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.3-hb03c661_0.conda + sha256: ec30e52a3c1bf7d0425380a189d209a52baa03f22fb66dd3eb587acaa765bd6d + md5: b88d90cad08e6bc8ad540cb310a761fb + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + constrains: + - xz 5.8.3.* + license: 0BSD + purls: [] + size: 113478 + timestamp: 1775825492909 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb03c661_1.conda + sha256: fe171ed5cf5959993d43ff72de7596e8ac2853e9021dec0344e583734f1e0843 + md5: 2c21e66f50753a083cbe6b80f38268fa + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: BSD-2-Clause + license_family: BSD + purls: [] + size: 92400 + timestamp: 1769482286018 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libnghttp2-1.68.1-h877daf1_0.conda + sha256: 663444d77a42f2265f54fb8b48c5450bfff4388d9c0f8253dd7855f0d993153f + md5: 2a45e7f8af083626f009645a6481f12d + depends: + - __glibc >=2.17,<3.0.a0 + - c-ares >=1.34.6,<2.0a0 + - libev >=4.33,<4.34.0a0 + - libev >=4.33,<5.0a0 + - libgcc >=14 + - libstdcxx >=14 + - libzlib >=1.3.1,<2.0a0 + - openssl >=3.5.5,<4.0a0 + license: MIT + license_family: MIT + purls: [] + size: 663344 + timestamp: 1773854035739 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hb9d3cd8_1.conda + sha256: 927fe72b054277cde6cb82597d0fcf6baf127dcbce2e0a9d8925a68f1265eef5 + md5: d864d34357c3b65a4b731f78c0801dc4 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + license: LGPL-2.1-only + license_family: GPL + purls: [] + size: 33731 + timestamp: 1750274110928 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libsodium-1.0.21-h280c20c_3.conda + sha256: 64e5c80cbce4680a2d25179949739a6def695d72c40ca28f010711764e372d97 + md5: 7af961ef4aa2c1136e11dd43ded245ab + depends: + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + license: ISC + purls: [] + size: 277661 + timestamp: 1772479381288 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.53.1-h0c1763c_0.conda + sha256: 54cdcd3214313b62c2a8ee277e6f42150d9b748264c1b70d958bf735e420ef8d + md5: 7dc38adcbf71e6b38748e919e16e0dce + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libzlib >=1.3.2,<2.0a0 + license: blessing + purls: [] + size: 954962 + timestamp: 1777986471789 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h934c35e_18.conda + sha256: 78668020064fdaa27e9ab65cd2997e2c837b564ab26ce3bf0e58a2ce1a525c6e + md5: 1b08cd684f34175e4514474793d44bcb + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc 15.2.0 he0feb66_18 + constrains: + - libstdcxx-ng ==15.2.0=*_18 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 5852330 + timestamp: 1771378262446 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.42-h5347b49_0.conda + sha256: bc1b08c92626c91500fd9f26f2c797f3eb153b627d53e9c13cd167f1e12b2829 + md5: 38ffe67b78c9d4de527be8315e5ada2c + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 40297 + timestamp: 1775052476770 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libuv-1.51.0-hb03c661_1.conda + sha256: c180f4124a889ac343fc59d15558e93667d894a966ec6fdb61da1604481be26b + md5: 0f03292cc56bf91a077a134ea8747118 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: MIT + license_family: MIT + purls: [] + size: 895108 + timestamp: 1753948278280 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda + sha256: 6ae68e0b86423ef188196fff6207ed0c8195dd84273cb5623b85aa08033a410c + md5: 5aa797f8787fe7a17d1b0821485b5adc + depends: + - libgcc-ng >=12 + license: LGPL-2.1-or-later + purls: [] + size: 100393 + timestamp: 1702724383534 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.2-h25fd6f3_2.conda + sha256: 55044c403570f0dc26e6364de4dc5368e5f3fc7ff103e867c487e2b5ab2bcda9 + md5: d87ff7921124eccd67248aa483c23fec + depends: + - __glibc >=2.17,<3.0.a0 + constrains: + - zlib 1.3.2 *_2 + license: Zlib + license_family: Other + purls: [] + size: 63629 + timestamp: 1774072609062 +- conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.3-py311h3778330_1.conda + sha256: 710e207b2e91308a34bcfe547c60ad86c1fa294827266ba18548c1fe1a9d8333 + md5: f9efdf9b0f3d0cc309d56af6edf2a6b0 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - python >=3.11,<3.12.0a0 + - python_abi 3.11.* *_cp311 + constrains: + - jinja2 >=3.0.0 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/markupsafe?source=hash-mapping + size: 26756 + timestamp: 1772445078834 +- conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.3-py313h3dea7bd_1.conda + sha256: 72ed7c0216541d65a17b171bf2eec4a3b81e9158d8ed48e59e1ecd3ae302d263 + md5: aeb9b9da79fd0258b3db091d1fefcd71 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - python >=3.13,<3.14.0a0 + - python_abi 3.13.* *_cp313 + constrains: + - jinja2 >=3.0.0 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/markupsafe?source=hash-mapping + size: 26100 + timestamp: 1772445154165 +- conda: https://conda.anaconda.org/conda-forge/linux-64/msgspec-0.21.1-py311h49ec1c0_0.conda + sha256: 59123ba4af69c1e7ca6b1b8ffcc40f2aff9a635814495bda3f4f555b3b929165 + md5: 0347d2804701019ac005528df9eb502c + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - python >=3.11,<3.12.0a0 + - python_abi 3.11.* *_cp311 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/msgspec?source=hash-mapping + size: 217556 + timestamp: 1776337480302 +- conda: https://conda.anaconda.org/conda-forge/linux-64/msgspec-0.21.1-py313h07c4f96_0.conda + sha256: a44d23085117672e4e19629ea3e6c53f516984322513c4bfdd052b1c7c6fc8ad + md5: 15c679f7133347d68058b688f3519cea + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - python >=3.13,<3.14.0a0 + - python_abi 3.13.* *_cp313 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/msgspec?source=hash-mapping + size: 219767 + timestamp: 1776337423071 +- conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.6-hdb14827_0.conda + sha256: fc89f74bbe362fb29fa3c037697a89bec140b346a2469a90f7936d1d7ea4d8a3 + md5: fc21868a1a5aacc937e7a18747acb8a5 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: X11 AND BSD-3-Clause + purls: [] + size: 918956 + timestamp: 1777422145199 +- conda: https://conda.anaconda.org/conda-forge/linux-64/nodejs-25.8.2-he4ff34a_0.conda + sha256: d1a673d1418d9e956b6e4e46c23e72a511c5c1d45dc5519c947457427036d5e2 + md5: baffb1570b3918c784d4490babc52fbf + depends: + - libgcc >=14 + - libstdcxx >=14 + - __glibc >=2.28,<3.0.a0 + - libnghttp2 >=1.68.1,<2.0a0 + - libuv >=1.51.0,<2.0a0 + - c-ares >=1.34.6,<2.0a0 + - openssl >=3.5.5,<4.0a0 + - libsqlite >=3.52.0,<4.0a0 + - icu >=78.3,<79.0a0 + - libzlib >=1.3.2,<2.0a0 + - libabseil >=20260107.1,<20260108.0a0 + - libabseil * cxx17* + - zstd >=1.5.7,<1.6.0a0 + - libbrotlicommon >=1.2.0,<1.3.0a0 + - libbrotlienc >=1.2.0,<1.3.0a0 + - libbrotlidec >=1.2.0,<1.3.0a0 + license: MIT + license_family: MIT + purls: [] + size: 18829340 + timestamp: 1774514313036 +- conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.2-h35e630c_0.conda + sha256: c0ef482280e38c71a08ad6d71448194b719630345b0c9c60744a2010e8a8e0cb + md5: da1b85b6a87e141f5140bb9924cecab0 + depends: + - __glibc >=2.17,<3.0.a0 + - ca-certificates + - libgcc >=14 + license: Apache-2.0 + license_family: Apache + purls: [] + size: 3167099 + timestamp: 1775587756857 +- conda: https://conda.anaconda.org/conda-forge/linux-64/psutil-7.2.2-py311haee01d2_0.conda + sha256: 8d9325af538a8f56013e42bbb91a4dc6935aece34476e20bafacf6007b571e86 + md5: 2ed8f6fe8b51d8e19f7621941f7bb95f + depends: + - python + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - python_abi 3.11.* *_cp311 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/psutil?source=hash-mapping + size: 231786 + timestamp: 1769678156460 +- conda: https://conda.anaconda.org/conda-forge/linux-64/psutil-7.2.2-py313h54dd161_0.conda + sha256: f19fd682d874689dfde20bf46d7ec1a28084af34583e0405685981363af47c91 + md5: 25fe6e02c2083497b3239e21b49d8093 + depends: + - python + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - python_abi 3.13.* *_cp313 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/psutil?source=hash-mapping + size: 228663 + timestamp: 1769678153829 +- conda: https://conda.anaconda.org/conda-forge/linux-64/pydantic-core-2.46.4-py311h902ca64_0.conda + sha256: ac3faa31154a85f4b1a4618957b4a06623ff2c6a85f8eac20efe9dbac5757d91 + md5: b6cfa054aec29586d383f9e666bd0e9a + depends: + - python + - typing-extensions >=4.6.0,!=4.7.0 + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - python_abi 3.11.* *_cp311 + constrains: + - __glibc >=2.17 + license: MIT + license_family: MIT + purls: + - pkg:pypi/pydantic-core?source=hash-mapping + size: 1884122 + timestamp: 1778084220486 +- conda: https://conda.anaconda.org/conda-forge/linux-64/pydantic-core-2.46.4-py313h843e2db_0.conda + sha256: d6705962afa2655cc22edcd075ce1f7e67c4407c005137d6d63c0dc5eaa76f47 + md5: ef0f9efec6ec28b17e22f787c7672c67 + depends: + - python + - typing-extensions >=4.6.0,!=4.7.0 + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - python_abi 3.13.* *_cp313 + constrains: + - __glibc >=2.17 + license: MIT + license_family: MIT + purls: + - pkg:pypi/pydantic-core?source=hash-mapping + size: 1893749 + timestamp: 1778084222642 +- conda: https://conda.anaconda.org/conda-forge/linux-64/pynacl-1.6.2-py311hdf6b40b_1.conda + sha256: 4b89f502dc58df0abb27d4677ca4b0ef5230f5c28e24b80eef3dc0c9c968e225 + md5: fb7e00fed4fddee97e6cf0bd3ae9a27e + depends: + - __glibc >=2.17,<3.0.a0 + - cffi >=1.4.1 + - libgcc >=14 + - libsodium >=1.0.21,<1.0.22.0a0 + - python >=3.11,<3.12.0a0 + - python_abi 3.11.* *_cp311 + - six + license: Apache-2.0 + license_family: Apache + purls: + - pkg:pypi/pynacl?source=hash-mapping + size: 1151915 + timestamp: 1772171237221 +- conda: https://conda.anaconda.org/conda-forge/linux-64/pynacl-1.6.2-py313h5008379_1.conda + sha256: 51e80a7bef95025ad47a92acb69ee0e78f01e107655c86fe76abcde2ac688166 + md5: c4426edfc5514a2c9be6871557bce52b + depends: + - __glibc >=2.17,<3.0.a0 + - cffi >=1.4.1 + - libgcc >=14 + - libsodium >=1.0.21,<1.0.22.0a0 + - python >=3.13,<3.14.0a0 + - python_abi 3.13.* *_cp313 + - six + license: Apache-2.0 + license_family: Apache + purls: + - pkg:pypi/pynacl?source=hash-mapping + size: 1191901 + timestamp: 1772171244923 +- conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.11.15-hd63d673_0_cpython.conda + sha256: bf6a32c69889d38482436a786bea32276756cedf0e9805cc856ffd088e8d00f0 + md5: a5ebcefec0c12a333bcd6d7bf3bddc1f + depends: + - __glibc >=2.17,<3.0.a0 + - bzip2 >=1.0.8,<2.0a0 + - ld_impl_linux-64 >=2.36.1 + - libexpat >=2.7.4,<3.0a0 + - libffi >=3.5.2,<3.6.0a0 + - libgcc >=14 + - liblzma >=5.8.2,<6.0a0 + - libnsl >=2.0.1,<2.1.0a0 + - libsqlite >=3.51.2,<4.0a0 + - libuuid >=2.41.3,<3.0a0 + - libxcrypt >=4.4.36 + - libzlib >=1.3.1,<2.0a0 + - ncurses >=6.5,<7.0a0 + - openssl >=3.5.5,<4.0a0 + - readline >=8.3,<9.0a0 + - tk >=8.6.13,<8.7.0a0 + - tzdata + constrains: + - python_abi 3.11.* *_cp311 + license: Python-2.0 + purls: [] + size: 30949404 + timestamp: 1772730362552 +- conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.13.13-h6add32d_100_cp313.conda + build_number: 100 + sha256: 7f77eb57648f545c1f58e10035d0d9d66b0a0efb7c4b58d3ed89ec7269afdde1 + md5: 05051be49267378d2fcd12931e319ac3 + depends: + - __glibc >=2.17,<3.0.a0 + - bzip2 >=1.0.8,<2.0a0 + - ld_impl_linux-64 >=2.36.1 + - libexpat >=2.7.5,<3.0a0 + - libffi >=3.5.2,<3.6.0a0 + - libgcc >=14 + - liblzma >=5.8.2,<6.0a0 + - libmpdec >=4.0.0,<5.0a0 + - libsqlite >=3.52.0,<4.0a0 + - libuuid >=2.42,<3.0a0 + - libzlib >=1.3.2,<2.0a0 + - ncurses >=6.5,<7.0a0 + - openssl >=3.5.6,<4.0a0 + - python_abi 3.13.* *_cp313 + - readline >=8.3,<9.0a0 + - tk >=8.6.13,<8.7.0a0 + - tzdata + license: Python-2.0 + purls: [] + size: 37358322 + timestamp: 1775614712638 + python_site_packages_path: lib/python3.13/site-packages +- conda: https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.3-py311h3778330_1.conda + sha256: c9a6cd2c290d7c3d2b30ea34a0ccda30f770e8ddb2937871f2c404faf60d0050 + md5: a24add9a3bababee946f3bc1c829acfe + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - python >=3.11,<3.12.0a0 + - python_abi 3.11.* *_cp311 + - yaml >=0.2.5,<0.3.0a0 + license: MIT + license_family: MIT + purls: + - pkg:pypi/pyyaml?source=hash-mapping + size: 206190 + timestamp: 1770223702917 +- conda: https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.3-py313h3dea7bd_1.conda + sha256: ef7df29b38ef04ec67a8888a4aa039973eaa377e8c4b59a7be0a1c50cd7e4ac6 + md5: f256753e840c3cd3766488c9437a8f8b + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - python >=3.13,<3.14.0a0 + - python_abi 3.13.* *_cp313 + - yaml >=0.2.5,<0.3.0a0 + license: MIT + license_family: MIT + purls: + - pkg:pypi/pyyaml?source=compressed-mapping + size: 201616 + timestamp: 1770223543730 +- conda: https://conda.anaconda.org/conda-forge/linux-64/pyzmq-27.1.0-py311h57d2397_2.conda + sha256: 4137226663b353919396f8cf239971cfa54107cd077cf3b65e979ba3e1f8a037 + md5: 759edfe34f07c5c4565b6c5ec0c7fb17 + depends: + - python + - libgcc >=14 + - libstdcxx >=14 + - __glibc >=2.17,<3.0.a0 + - zeromq >=4.3.5,<4.4.0a0 + - python_abi 3.11.* *_cp311 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/pyzmq?source=hash-mapping + size: 383627 + timestamp: 1771716979818 +- conda: https://conda.anaconda.org/conda-forge/linux-64/pyzmq-27.1.0-py312hda471dd_2.conda + noarch: python + sha256: be66c1f85c3b48137200d62c12d918f4f8ad329423daef04fed292818efd3c28 + md5: 082985717303dab433c976986c674b35 + depends: + - python + - libgcc >=14 + - libstdcxx >=14 + - __glibc >=2.17,<3.0.a0 + - zeromq >=4.3.5,<4.4.0a0 + - _python_abi3_support 1.* + - cpython >=3.12 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/pyzmq?source=hash-mapping + size: 211567 + timestamp: 1771716961404 +- conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.3-h853b02a_0.conda + sha256: 12ffde5a6f958e285aa22c191ca01bbd3d6e710aa852e00618fa6ddc59149002 + md5: d7d95fc8287ea7bf33e0e7116d2b95ec + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - ncurses >=6.5,<7.0a0 + license: GPL-3.0-only + license_family: GPL + purls: [] + size: 345073 + timestamp: 1765813471974 +- conda: https://conda.anaconda.org/conda-forge/linux-64/rpds-py-0.30.0-py311h902ca64_0.conda + sha256: bf5e6197fb08b8c6e421ca0126e966b7c3ae62b84d7b98523356b4fd5ae6f8ae + md5: 3893f7b40738f9fe87510cb4468cdda5 + depends: + - python + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - python_abi 3.11.* *_cp311 + constrains: + - __glibc >=2.17 + license: MIT + license_family: MIT + purls: + - pkg:pypi/rpds-py?source=hash-mapping + size: 383153 + timestamp: 1764543197251 +- conda: https://conda.anaconda.org/conda-forge/linux-64/rpds-py-0.30.0-py313h843e2db_0.conda + sha256: 076d26e51c62c8ecfca6eb19e3c1febdd7632df1990a7aa53da5df5e54482b1c + md5: 779e3307a0299518713765b83a36f4b1 + depends: + - python + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - python_abi 3.13.* *_cp313 + constrains: + - __glibc >=2.17 + license: MIT + license_family: MIT + purls: + - pkg:pypi/rpds-py?source=hash-mapping + size: 383230 + timestamp: 1764543223529 +- conda: https://conda.anaconda.org/conda-forge/linux-64/ruff-0.15.12-h994f30f_0.conda + noarch: python + sha256: 98c771464ed93a2733bae460c39cfa9640feb20972d2e651b44e85db296942eb + md5: 3c8f229055ad244fa3a97c6c45b77099 + depends: + - python + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + constrains: + - __glibc >=2.17 + license: MIT + license_family: MIT + purls: + - pkg:pypi/ruff?source=compressed-mapping + size: 9266480 + timestamp: 1778119386343 +- conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h366c992_103.conda + sha256: cafeec44494f842ffeca27e9c8b0c27ed714f93ac77ddadc6aaf726b5554ebac + md5: cffd3bdd58090148f4cfcd831f4b26ab + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libzlib >=1.3.1,<2.0a0 + constrains: + - xorg-libx11 >=1.8.12,<2.0a0 + license: TCL + license_family: BSD + purls: [] + size: 3301196 + timestamp: 1769460227866 +- conda: https://conda.anaconda.org/conda-forge/linux-64/tornado-6.5.5-py311h49ec1c0_0.conda + sha256: cd8bc08ca661291cfbb05581b79f7e6a6971a072a54a335abcea5460c1230880 + md5: 73b44a114241e564deb5846e7394bf19 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - python >=3.11,<3.12.0a0 + - python_abi 3.11.* *_cp311 + license: Apache-2.0 + license_family: Apache + purls: + - pkg:pypi/tornado?source=compressed-mapping + size: 876817 + timestamp: 1774358035290 +- conda: https://conda.anaconda.org/conda-forge/linux-64/tornado-6.5.5-py313h07c4f96_0.conda + sha256: 9e8497e1ecca77d03c6be2d3b5f901dfe0ab99686af4fb94ab418b7d449ac547 + md5: 6c0b0ae017b5bfd9c8d718217efd8f14 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - python >=3.13,<3.14.0a0 + - python_abi 3.13.* *_cp313 + license: Apache-2.0 + license_family: Apache + purls: + - pkg:pypi/tornado?source=hash-mapping + size: 882996 + timestamp: 1774358035145 +- conda: https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h280c20c_3.conda + sha256: 6d9ea2f731e284e9316d95fa61869fe7bbba33df7929f82693c121022810f4ad + md5: a77f85f77be52ff59391544bfe73390a + depends: + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + license: MIT + license_family: MIT + purls: [] + size: 85189 + timestamp: 1753484064210 +- conda: https://conda.anaconda.org/conda-forge/linux-64/zeromq-4.3.5-h41580af_10.conda + sha256: 325d370b28e2b9cc1f765c5b4cdb394c91a5d958fbd15da1a14607a28fee09f6 + md5: 755b096086851e1193f3b10347415d7c + depends: + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - libstdcxx >=14 + - krb5 >=1.22.2,<1.23.0a0 + - libsodium >=1.0.21,<1.0.22.0a0 + license: MPL-2.0 + license_family: MOZILLA + purls: [] + size: 311150 + timestamp: 1772476812121 +- conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda + sha256: 68f0206ca6e98fea941e5717cec780ed2873ffabc0e1ed34428c061e2c6268c7 + md5: 4a13eeac0b5c8e5b8ab496e6c4ddd829 + depends: + - __glibc >=2.17,<3.0.a0 + - libzlib >=1.3.1,<2.0a0 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 601375 + timestamp: 1764777111296 +- conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda + sha256: a3967b937b9abf0f2a99f3173fa4630293979bd1644709d89580e7c62a544661 + md5: aaa2a381ccc56eac91d63b6c1240312f + depends: + - cpython + - python-gil + license: MIT + license_family: MIT + purls: [] + size: 8191 + timestamp: 1744137672556 +- conda: https://conda.anaconda.org/conda-forge/noarch/annotated-types-0.7.0-pyhd8ed1ab_1.conda + sha256: e0ea1ba78fbb64f17062601edda82097fcf815012cf52bb704150a2668110d48 + md5: 2934f256a8acfe48f6ebb4fce6cde29c + depends: + - python >=3.9 + - typing-extensions >=4.0.0 + license: MIT + license_family: MIT + purls: + - pkg:pypi/annotated-types?source=hash-mapping + size: 18074 + timestamp: 1733247158254 +- conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.13.0-pyhcf101f3_0.conda + sha256: f09aed24661cd45ba54a43772504f05c0698248734f9ae8cd289d314ac89707e + md5: af2df4b9108808da3dc76710fe50eae2 + depends: + - exceptiongroup >=1.0.2 + - idna >=2.8 + - python >=3.10 + - typing_extensions >=4.5 + - python + constrains: + - trio >=0.32.0 + - uvloop >=0.22.1 + - winloop >=0.2.3 + license: MIT + license_family: MIT + purls: + - pkg:pypi/anyio?source=compressed-mapping + size: 146764 + timestamp: 1774359453364 +- conda: https://conda.anaconda.org/conda-forge/noarch/appnope-0.1.4-pyhd8ed1ab_1.conda + sha256: 8f032b140ea4159806e4969a68b4a3c0a7cab1ad936eb958a2b5ffe5335e19bf + md5: 54898d0f524c9dee622d44bbb081a8ab + depends: + - python >=3.9 + license: BSD-2-Clause + license_family: BSD + purls: + - pkg:pypi/appnope?source=hash-mapping + size: 10076 + timestamp: 1733332433806 +- conda: https://conda.anaconda.org/conda-forge/noarch/argon2-cffi-25.1.0-pyhd8ed1ab_0.conda + sha256: bea62005badcb98b1ae1796ec5d70ea0fc9539e7d59708ac4e7d41e2f4bb0bad + md5: 8ac12aff0860280ee0cff7fa2cf63f3b + depends: + - argon2-cffi-bindings + - python >=3.9 + - typing-extensions + constrains: + - argon2_cffi ==999 + license: MIT + license_family: MIT + purls: + - pkg:pypi/argon2-cffi?source=hash-mapping + size: 18715 + timestamp: 1749017288144 +- conda: https://conda.anaconda.org/conda-forge/noarch/arrow-1.4.0-pyhcf101f3_0.conda + sha256: 792da8131b1b53ff667bd6fc617ea9087b570305ccb9913deb36b8e12b3b5141 + md5: 85c4f19f377424eafc4ed7911b291642 + depends: + - python >=3.10 + - python-dateutil >=2.7.0 + - python-tzdata + - python + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/arrow?source=hash-mapping + size: 113854 + timestamp: 1760831179410 +- conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.1-pyhd8ed1ab_0.conda + sha256: ee4da0f3fe9d59439798ee399ef3e482791e48784873d546e706d0935f9ff010 + md5: 9673a61a297b00016442e022d689faa6 + depends: + - python >=3.10 + constrains: + - astroid >=2,<5 + license: Apache-2.0 + license_family: Apache + purls: + - pkg:pypi/asttokens?source=hash-mapping + size: 28797 + timestamp: 1763410017955 +- conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.3.0-pyhcf101f3_0.conda + sha256: ea8486637cfb89dc26dc9559921640cd1d5fd37e5e02c33d85c94572139f2efe + md5: b85e84cb64c762569cc1a760c2327e0a + depends: + - python >=3.10 + - typing_extensions >=4.0.0 + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/async-lru?source=hash-mapping + size: 22949 + timestamp: 1773926359134 +- conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda + sha256: 1b6124230bb4e571b1b9401537ecff575b7b109cc3a21ee019f65e083b8399ab + md5: c6b0543676ecb1fb2d7643941fe375f2 + depends: + - python >=3.10 + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/attrs?source=hash-mapping + size: 64927 + timestamp: 1773935801332 +- conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.18.0-pyhcf101f3_1.conda + sha256: a14a9ad02101aab25570543a59c5193043b73dc311a25650134ed9e6cb691770 + md5: f1976ce927373500cc19d3c0b2c85177 + depends: + - python >=3.10 + - python + constrains: + - pytz >=2015.7 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/babel?source=compressed-mapping + size: 7684321 + timestamp: 1772555330347 +- conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.14.3-pyha770c72_0.conda + sha256: bf1e71c3c0a5b024e44ff928225a0874fc3c3356ec1a0b6fe719108e6d1288f6 + md5: 5267bef8efea4127aacd1f4e1f149b6e + depends: + - python >=3.10 + - soupsieve >=1.2 + - typing-extensions + license: MIT + license_family: MIT + purls: + - pkg:pypi/beautifulsoup4?source=hash-mapping + size: 90399 + timestamp: 1764520638652 +- conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.3.0-pyhcf101f3_1.conda + sha256: f8ff1f98423674278964a46c93a1766f9e91960d44efd91c6c3ed56a33813f46 + md5: 7c5ebdc286220e8021bf55e6384acd67 + depends: + - python >=3.10 + - webencodings + - python + constrains: + - tinycss2 >=1.1.0,<1.5 + license: Apache-2.0 AND MIT + purls: + - pkg:pypi/bleach?source=hash-mapping + size: 142008 + timestamp: 1770719370680 +- conda: https://conda.anaconda.org/conda-forge/noarch/bleach-with-css-6.3.0-hbca2aae_1.conda + sha256: 7c07a865e5e4cca233cc4e0eb3f0f5ff6c90776461687b4fb0b1764133e1fd61 + md5: f11a319b9700b203aa14c295858782b6 + depends: + - bleach ==6.3.0 pyhcf101f3_1 + - tinycss2 + license: Apache-2.0 AND MIT + purls: [] + size: 4409 + timestamp: 1770719370682 +- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.4.22-h4c7d964_0.conda + sha256: 6f4ff81534c19e76acf52fcabf4a258088a932b8f1ac56e9a59e98f6051f8e46 + md5: 56fb2c6c73efc627b40c77d14caecfba + depends: + - __win + license: ISC + purls: [] + size: 131388 + timestamp: 1776865633471 +- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.4.22-hbd8a1cb_0.conda + sha256: c9dbcc8039a52023660d6d1bbf87594a93dd69c6ac5a2a44323af2c92976728d + md5: e18ad67cf881dcadee8b8d9e2f8e5f73 + depends: + - __unix + license: ISC + purls: [] + size: 131039 + timestamp: 1776865545798 +- conda: https://conda.anaconda.org/conda-forge/noarch/cached-property-1.5.2-hd8ed1ab_1.tar.bz2 + noarch: python + sha256: 561e6660f26c35d137ee150187d89767c988413c978e1b712d53f27ddf70ea17 + md5: 9b347a7ec10940d3f7941ff6c460b551 + depends: + - cached_property >=1.5.2,<1.5.3.0a0 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 4134 + timestamp: 1615209571450 +- conda: https://conda.anaconda.org/conda-forge/noarch/cached_property-1.5.2-pyha770c72_1.tar.bz2 + sha256: 6dbf7a5070cc43d90a1e4c2ec0c541c69d8e30a0e25f50ce9f6e4a432e42c5d7 + md5: 576d629e47797577ab0f1b351297ef4a + depends: + - python >=3.6 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/cached-property?source=hash-mapping + size: 11065 + timestamp: 1615209567874 +- conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2026.4.22-pyhd8ed1ab_0.conda + sha256: 989db6e5957c4b44fa600c68c681ec2f36a55e48f7c7f1c073d5e91caa8cd878 + md5: 929471569c93acefb30282a22060dcd5 + depends: + - python >=3.10 + license: ISC + purls: + - pkg:pypi/certifi?source=compressed-mapping + size: 135656 + timestamp: 1776866680878 +- conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.7-pyhd8ed1ab_0.conda + sha256: 3f9483d62ce24ecd063f8a5a714448445dc8d9e201147c46699fc0033e824457 + md5: a9167b9571f3baa9d448faa2139d1089 + depends: + - python >=3.10 + license: MIT + license_family: MIT + purls: + - pkg:pypi/charset-normalizer?source=compressed-mapping + size: 58872 + timestamp: 1775127203018 +- conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda + sha256: ab29d57dc70786c1269633ba3dff20288b81664d3ff8d21af995742e2bb03287 + md5: 962b9857ee8e7018c22f2776ffa0b2d7 + depends: + - python >=3.9 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/colorama?source=hash-mapping + size: 27011 + timestamp: 1733218222191 +- conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.3-pyhe01879c_0.conda + sha256: 576a44729314ad9e4e5ebe055fbf48beb8116b60e58f9070278985b2b634f212 + md5: 2da13f2b299d8e1995bafbbe9689a2f7 + depends: + - python >=3.9 + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/comm?source=hash-mapping + size: 14690 + timestamp: 1753453984907 +- conda: https://conda.anaconda.org/conda-forge/noarch/copier-9.15.0-pyhcf101f3_0.conda + sha256: 2ceb1f7edb5e5943f7af69e4d6b3339b2e2580e7c9eef76efed4d18d05ae49db + md5: 12baf87bfa62ac066dbe2526f5d89de1 + depends: + - python >=3.10 + - colorama >=0.4.6 + - dunamai >=1.7.0 + - funcy >=1.17 + - jinja2 >=3.1.5 + - jinja2-ansible-filters >=1.3.1 + - packaging >=23.0 + - pathspec >=0.9.0 + - plumbum >=1.6.9 + - prompt-toolkit <3.0.52 + - pydantic >=2.4.2 + - pygments >=2.7.1 + - pyyaml >=5.3.1 + - questionary >=1.8.1 + - eval-type-backport >=0.1.3,<0.3.0 + - platformdirs >=4.3.6 + - typing_extensions >=4.0.0,<5.0.0 + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/copier?source=hash-mapping + size: 58609 + timestamp: 1777562504238 +- conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.13.13-py313hd8ed1ab_100.conda + noarch: generic + sha256: 836b92c209d4b6b9fb28bd51bd788b22a0c5492ae95eec2724e65a15ed4ab2e1 + md5: 3a8a8b87e72f95b54689fb588e154ec9 + depends: + - python >=3.13,<3.14.0a0 + - python_abi * *_cp313 + license: Python-2.0 + purls: [] + size: 48530 + timestamp: 1775613723457 +- conda: https://conda.anaconda.org/conda-forge/noarch/decorator-5.2.1-pyhd8ed1ab_0.conda + sha256: c17c6b9937c08ad63cb20a26f403a3234088e57d4455600974a0ce865cb14017 + md5: 9ce473d1d1be1cc3810856a48b3fab32 + depends: + - python >=3.9 + license: BSD-2-Clause + license_family: BSD + purls: + - pkg:pypi/decorator?source=hash-mapping + size: 14129 + timestamp: 1740385067843 +- conda: https://conda.anaconda.org/conda-forge/noarch/defusedxml-0.7.1-pyhd8ed1ab_0.tar.bz2 + sha256: 9717a059677553562a8f38ff07f3b9f61727bd614f505658b0a5ecbcf8df89be + md5: 961b3a227b437d82ad7054484cfa71b2 + depends: + - python >=3.6 + license: PSF-2.0 + license_family: PSF + purls: + - pkg:pypi/defusedxml?source=hash-mapping + size: 24062 + timestamp: 1615232388757 +- conda: https://conda.anaconda.org/conda-forge/noarch/dunamai-1.26.1-pyhd8ed1ab_0.conda + sha256: 930f21584babebdabc8310f894557d032211f6cb427f7028c7c92cab6fe6cc1f + md5: dbb824a3a87ac1e2ce02f8227be67e74 + depends: + - importlib-metadata >=1.6.0 + - packaging >=20.9 + - python >=3.10 + license: MIT + license_family: MIT + purls: + - pkg:pypi/dunamai?source=hash-mapping + size: 30866 + timestamp: 1775597880526 +- conda: https://conda.anaconda.org/conda-forge/noarch/eval-type-backport-0.2.2-pyhd8ed1ab_0.conda + sha256: 05ffdcb83903c159bfbb78a07fbcce6fd6dda41df9c55ed75e0eb1db5528048f + md5: 879479fda1dddb002fdc4885cea33740 + depends: + - eval_type_backport >=0.2.2,<0.2.3.0a0 + - python >=3.9 + license: MIT + license_family: MIT + purls: [] + size: 6662 + timestamp: 1734857849281 +- conda: https://conda.anaconda.org/conda-forge/noarch/eval_type_backport-0.2.2-pyha770c72_0.conda + sha256: 2d721421a60676216e10837a240c75e2190e093920a4016a469fa9a62c95ab5f + md5: 8681d7f876da5e66a1c7fce424509383 + depends: + - python >=3.9 + constrains: + - eval-type-backport >=0.2.2,<0.2.3.0a0 + license: MIT + license_family: MIT + purls: + - pkg:pypi/eval-type-backport?source=hash-mapping + size: 11520 + timestamp: 1734857840035 +- conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda + sha256: ee6cf346d017d954255bbcbdb424cddea4d14e4ed7e9813e429db1d795d01144 + md5: 8e662bd460bda79b1ea39194e3c4c9ab + depends: + - python >=3.10 + - typing_extensions >=4.6.0 + license: MIT and PSF-2.0 + purls: + - pkg:pypi/exceptiongroup?source=hash-mapping + size: 21333 + timestamp: 1763918099466 +- conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.1-pyhd8ed1ab_0.conda + sha256: 210c8165a58fdbf16e626aac93cc4c14dbd551a01d1516be5ecad795d2422cad + md5: ff9efb7f7469aed3c4a8106ffa29593c + depends: + - python >=3.10 + license: MIT + license_family: MIT + purls: + - pkg:pypi/executing?source=hash-mapping + size: 30753 + timestamp: 1756729456476 +- conda: https://conda.anaconda.org/conda-forge/noarch/fqdn-1.5.1-pyhd8ed1ab_1.conda + sha256: 2509992ec2fd38ab27c7cdb42cf6cadc566a1cc0d1021a2673475d9fa87c6276 + md5: d3549fd50d450b6d9e7dddff25dd2110 + depends: + - cached-property >=1.3.0 + - python >=3.9,<4 + license: MPL-2.0 + license_family: MOZILLA + purls: + - pkg:pypi/fqdn?source=hash-mapping + size: 16705 + timestamp: 1733327494780 +- conda: https://conda.anaconda.org/conda-forge/noarch/funcy-2.0-pyhd8ed1ab_1.conda + sha256: 4a3e3e86b7b49aaa2a0faa57da2bcf39f7ee858499e8335132f83bfeed79191e + md5: 84f8955e99a8944fdee49da39edb0add + depends: + - python >=3.9 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/funcy?source=hash-mapping + size: 30249 + timestamp: 1734381235500 +- conda: https://conda.anaconda.org/conda-forge/noarch/h11-0.16.0-pyhcf101f3_1.conda + sha256: 96cac6573fd35ae151f4d6979bab6fbc90cb6b1fb99054ba19eb075da9822fcb + md5: b8993c19b0c32a2f7b66cbb58ca27069 + depends: + - python >=3.10 + - typing_extensions + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/h11?source=compressed-mapping + size: 39069 + timestamp: 1767729720872 +- conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda + sha256: 84c64443368f84b600bfecc529a1194a3b14c3656ee2e832d15a20e0329b6da3 + md5: 164fc43f0b53b6e3a7bc7dce5e4f1dc9 + depends: + - python >=3.10 + - hyperframe >=6.1,<7 + - hpack >=4.1,<5 + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/h2?source=hash-mapping + size: 95967 + timestamp: 1756364871835 +- conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda + sha256: 6ad78a180576c706aabeb5b4c8ceb97c0cb25f1e112d76495bff23e3779948ba + md5: 0a802cb9888dd14eeefc611f05c40b6e + depends: + - python >=3.9 + license: MIT + license_family: MIT + purls: + - pkg:pypi/hpack?source=hash-mapping + size: 30731 + timestamp: 1737618390337 +- conda: https://conda.anaconda.org/conda-forge/noarch/httpcore-1.0.9-pyh29332c3_0.conda + sha256: 04d49cb3c42714ce533a8553986e1642d0549a05dc5cc48e0d43ff5be6679a5b + md5: 4f14640d58e2cc0aa0819d9d8ba125bb + depends: + - python >=3.9 + - h11 >=0.16 + - h2 >=3,<5 + - sniffio 1.* + - anyio >=4.0,<5.0 + - certifi + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/httpcore?source=hash-mapping + size: 49483 + timestamp: 1745602916758 +- conda: https://conda.anaconda.org/conda-forge/noarch/httpx-0.28.1-pyhd8ed1ab_0.conda + sha256: cd0f1de3697b252df95f98383e9edb1d00386bfdd03fdf607fa42fe5fcb09950 + md5: d6989ead454181f4f9bc987d3dc4e285 + depends: + - anyio + - certifi + - httpcore 1.* + - idna + - python >=3.9 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/httpx?source=hash-mapping + size: 63082 + timestamp: 1733663449209 +- conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda + sha256: 77af6f5fe8b62ca07d09ac60127a30d9069fdc3c68d6b256754d0ffb1f7779f8 + md5: 8e6923fc12f1fe8f8c4e5c9f343256ac + depends: + - python >=3.9 + license: MIT + license_family: MIT + purls: + - pkg:pypi/hyperframe?source=hash-mapping + size: 17397 + timestamp: 1737618427549 +- conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.13-pyhcf101f3_0.conda + sha256: 9ab620e6f64bb67737bd7bc1ad6f480770124e304c6710617aba7fe60b089f48 + md5: fb7130c190f9b4ec91219840a05ba3ac + depends: + - python >=3.10 + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/idna?source=compressed-mapping + size: 59038 + timestamp: 1776947141407 +- conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.8.0-pyhcf101f3_0.conda + sha256: 82ab2a0d91ca1e7e63ab6a4939356667ef683905dea631bc2121aa534d347b16 + md5: 080594bf4493e6bae2607e65390c520a + depends: + - python >=3.10 + - zipp >=3.20 + - python + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/importlib-metadata?source=compressed-mapping + size: 34387 + timestamp: 1773931568510 +- conda: https://conda.anaconda.org/conda-forge/noarch/importlib_resources-7.1.0-pyhd8ed1ab_0.conda + sha256: a563a51aa522998172838e867e6dedcf630bc45796e8612f5a1f6d73e9c8125a + md5: 0ba6225c279baf7ea9473a62ea0ec9ae + depends: + - python >=3.10 + - zipp >=3.1.0 + constrains: + - importlib-resources >=7.1.0,<7.1.1.0a0 + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/importlib-resources?source=compressed-mapping + size: 34809 + timestamp: 1776068839274 +- conda: https://conda.anaconda.org/conda-forge/noarch/invoke-3.0.3-pyhd8ed1ab_0.conda + sha256: d7421c54944dec04d2671e23196a15583d48f309d06fbcf995b5dfa6d586a5e9 + md5: 4dee387356d58bdc4b686ae3424fbd9e + depends: + - python >=3.10 + license: BSD-2-Clause + license_family: BSD + purls: + - pkg:pypi/invoke?source=hash-mapping + size: 133811 + timestamp: 1775579645825 +- conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.2.0-pyh5552912_1.conda + sha256: 5c1f3e874adaf603449f2b135d48f168c5d510088c78c229bda0431268b43b27 + md5: 4b53d436f3fbc02ce3eeaf8ae9bebe01 + depends: + - appnope + - __osx + - comm >=0.1.1 + - debugpy >=1.6.5 + - ipython >=7.23.1 + - jupyter_client >=8.8.0 + - jupyter_core >=5.1,!=6.0.* + - matplotlib-inline >=0.1 + - nest-asyncio >=1.4 + - packaging >=22 + - psutil >=5.7 + - python >=3.10 + - pyzmq >=25 + - tornado >=6.4.1 + - traitlets >=5.4.0 + - python + constrains: + - appnope >=0.1.2 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/ipykernel?source=hash-mapping + size: 132260 + timestamp: 1770566135697 +- conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.2.0-pyh6dadd2b_1.conda + sha256: 9cdadaeef5abadca4113f92f5589db19f8b7df5e1b81cb0225f7024a3aedefa3 + md5: b3a7d5842f857414d9ae831a799444dd + depends: + - __win + - comm >=0.1.1 + - debugpy >=1.6.5 + - ipython >=7.23.1 + - jupyter_client >=8.8.0 + - jupyter_core >=5.1,!=6.0.* + - matplotlib-inline >=0.1 + - nest-asyncio >=1.4 + - packaging >=22 + - psutil >=5.7 + - python >=3.10 + - pyzmq >=25 + - tornado >=6.4.1 + - traitlets >=5.4.0 + - python + constrains: + - appnope >=0.1.2 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/ipykernel?source=hash-mapping + size: 132382 + timestamp: 1770566174387 +- conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.2.0-pyha191276_1.conda + sha256: b77ed58eb235e5ad80e742b03caeed4bbc2a2ef064cb9a2deee3b75dfae91b2a + md5: 8b267f517b81c13594ed68d646fd5dcb + depends: + - __linux + - comm >=0.1.1 + - debugpy >=1.6.5 + - ipython >=7.23.1 + - jupyter_client >=8.8.0 + - jupyter_core >=5.1,!=6.0.* + - matplotlib-inline >=0.1 + - nest-asyncio >=1.4 + - packaging >=22 + - psutil >=5.7 + - python >=3.10 + - pyzmq >=25 + - tornado >=6.4.1 + - traitlets >=5.4.0 + - python + constrains: + - appnope >=0.1.2 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/ipykernel?source=hash-mapping + size: 133644 + timestamp: 1770566133040 +- conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.13.0-pyh53cf698_0.conda + sha256: a0af49948a1842dfd15a0b0b2fd56c94ddbd07e07a6c8b4bc70d43015eafaff0 + md5: 73e9657cd19605740d21efb14d8d0cb9 + depends: + - __unix + - decorator >=5.1.0 + - ipython_pygments_lexers >=1.0.0 + - jedi >=0.18.2 + - matplotlib-inline >=0.1.6 + - prompt-toolkit >=3.0.41,<3.1.0 + - psutil >=7 + - pygments >=2.14.0 + - python >=3.11 + - stack_data >=0.6.0 + - traitlets >=5.13.0 + - typing_extensions >=4.6 + - pexpect >4.6 + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/ipython?source=compressed-mapping + size: 651632 + timestamp: 1777038396606 +- conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.13.0-pyhe2676ad_0.conda + sha256: f252ec33597115ff21cbb31051f6f9be34ca36cbbbf3d266b597660d8d8edde9 + md5: 5631ab99e902463d9dd4221e5b4eab6d + depends: + - __win + - decorator >=5.1.0 + - ipython_pygments_lexers >=1.0.0 + - jedi >=0.18.2 + - matplotlib-inline >=0.1.6 + - prompt-toolkit >=3.0.41,<3.1.0 + - psutil >=7 + - pygments >=2.14.0 + - python >=3.11 + - stack_data >=0.6.0 + - traitlets >=5.13.0 + - typing_extensions >=4.6 + - colorama >=0.4.4 + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/ipython?source=compressed-mapping + size: 650593 + timestamp: 1777038425499 +- conda: https://conda.anaconda.org/conda-forge/noarch/ipython_pygments_lexers-1.1.1-pyhd8ed1ab_0.conda + sha256: 894682a42a7d659ae12878dbcb274516a7031bbea9104e92f8e88c1f2765a104 + md5: bd80ba060603cc228d9d81c257093119 + depends: + - pygments + - python >=3.9 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/ipython-pygments-lexers?source=hash-mapping + size: 13993 + timestamp: 1737123723464 +- conda: https://conda.anaconda.org/conda-forge/noarch/isoduration-20.11.0-pyhd8ed1ab_1.conda + sha256: 08e838d29c134a7684bca0468401d26840f41c92267c4126d7b43a6b533b0aed + md5: 0b0154421989637d424ccf0f104be51a + depends: + - arrow >=0.15.0 + - python >=3.9 + license: MIT + license_family: MIT + purls: + - pkg:pypi/isoduration?source=hash-mapping + size: 19832 + timestamp: 1733493720346 +- conda: https://conda.anaconda.org/conda-forge/noarch/jedi-0.19.2-pyhd8ed1ab_1.conda + sha256: 92c4d217e2dc68983f724aa983cca5464dcb929c566627b26a2511159667dba8 + md5: a4f4c5dc9b80bc50e0d3dc4e6e8f1bd9 + depends: + - parso >=0.8.3,<0.9.0 + - python >=3.9 + license: Apache-2.0 AND MIT + purls: + - pkg:pypi/jedi?source=hash-mapping + size: 843646 + timestamp: 1733300981994 +- conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda + sha256: fc9ca7348a4f25fed2079f2153ecdcf5f9cf2a0bc36c4172420ca09e1849df7b + md5: 04558c96691bed63104678757beb4f8d + depends: + - markupsafe >=2.0 + - python >=3.10 + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/jinja2?source=compressed-mapping + size: 120685 + timestamp: 1764517220861 +- conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-ansible-filters-1.3.2-pyhd8ed1ab_1.conda + sha256: a36789229ca9ce5315265b9d425abce9acb4691f5864aea69e935020545a9acb + md5: 974c5b3e353f031cfcf2365c9d375926 + depends: + - jinja2 + - python >=3.9 + - pyyaml + license: BSD-2-Clause + license_family: BSD + purls: + - pkg:pypi/jinja2-ansible-filters?source=hash-mapping + size: 20315 + timestamp: 1734906203051 +- conda: https://conda.anaconda.org/conda-forge/noarch/json5-0.14.0-pyhd8ed1ab_0.conda + sha256: 9daa95bd164c8fa23b3ab196e906ef806141d749eddce2a08baa064f722d25fa + md5: 1269891272187518a0a75c286f7d0bbf + depends: + - python >=3.10 + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/json5?source=compressed-mapping + size: 34731 + timestamp: 1774655440045 +- conda: https://conda.anaconda.org/conda-forge/noarch/jsonpointer-3.1.1-pyhcf101f3_0.conda + sha256: a3d10301b6ff399ba1f3d39e443664804a3d28315a4fb81e745b6817845f70ae + md5: 89bf346df77603055d3c8fe5811691e6 + depends: + - python >=3.10 + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/jsonpointer?source=hash-mapping + size: 14190 + timestamp: 1774311356147 +- conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.26.0-pyhcf101f3_0.conda + sha256: db973a37d75db8e19b5f44bbbdaead0c68dde745407f281e2a7fe4db74ec51d7 + md5: ada41c863af263cc4c5fcbaff7c3e4dc + depends: + - attrs >=22.2.0 + - jsonschema-specifications >=2023.3.6 + - python >=3.10 + - referencing >=0.28.4 + - rpds-py >=0.25.0 + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/jsonschema?source=hash-mapping + size: 82356 + timestamp: 1767839954256 +- conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.9.1-pyhcf101f3_0.conda + sha256: 0a4f3b132f0faca10c89fdf3b60e15abb62ded6fa80aebfc007d05965192aa04 + md5: 439cd0f567d697b20a8f45cb70a1005a + depends: + - python >=3.10 + - referencing >=0.31.0 + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/jsonschema-specifications?source=hash-mapping + size: 19236 + timestamp: 1757335715225 +- conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-with-format-nongpl-4.26.0-hcf101f3_0.conda + sha256: 6886fc61e4e4edd38fd38729976b134e8bd2143f7fce56cc80d7ac7bac99bce1 + md5: 8368d58342d0825f0843dc6acdd0c483 + depends: + - jsonschema >=4.26.0,<4.26.1.0a0 + - fqdn + - idna + - isoduration + - jsonpointer >1.13 + - rfc3339-validator + - rfc3986-validator >0.1.0 + - rfc3987-syntax >=1.1.0 + - uri-template + - webcolors >=24.6.0 + license: MIT + license_family: MIT + purls: [] + size: 4740 + timestamp: 1767839954258 +- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter-lsp-2.3.1-pyhcf101f3_0.conda + sha256: 3766e2ae59641c172cec8a821528bfa6bf9543ffaaeb8b358bfd5259dcf18e4e + md5: 0c3b465ceee138b9c39279cc02e5c4a0 + depends: + - importlib-metadata >=4.8.3 + - jupyter_server >=1.1.2 + - python >=3.10 + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/jupyter-lsp?source=compressed-mapping + size: 61633 + timestamp: 1775136333147 +- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.8.0-pyhcf101f3_0.conda + sha256: e402bd119720862a33229624ec23645916a7d47f30e1711a4af9e005162b84f3 + md5: 8a3d6d0523f66cf004e563a50d9392b3 + depends: + - jupyter_core >=5.1 + - python >=3.10 + - python-dateutil >=2.8.2 + - pyzmq >=25.0 + - tornado >=6.4.1 + - traitlets >=5.3 + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/jupyter-client?source=hash-mapping + size: 112785 + timestamp: 1767954655912 +- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.9.1-pyh6dadd2b_0.conda + sha256: ed709a6c25b731e01563521ef338b93986cd14b5bc17f35e9382000864872ccc + md5: a8db462b01221e9f5135be466faeb3e0 + depends: + - __win + - pywin32 + - platformdirs >=2.5 + - python >=3.10 + - traitlets >=5.3 + - python + constrains: + - pywin32 >=300 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/jupyter-core?source=hash-mapping + size: 64679 + timestamp: 1760643889625 +- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.9.1-pyhc90fa1f_0.conda + sha256: 1d34b80e5bfcd5323f104dbf99a2aafc0e5d823019d626d0dce5d3d356a2a52a + md5: b38fe4e78ee75def7e599843ef4c1ab0 + depends: + - __unix + - python + - platformdirs >=2.5 + - python >=3.10 + - traitlets >=5.3 + - python + constrains: + - pywin32 >=300 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/jupyter-core?source=hash-mapping + size: 65503 + timestamp: 1760643864586 +- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_events-0.12.1-pyhcf101f3_0.conda + sha256: c7edb5682c6316a95ad781dccb1b6589cd2ec0bf94f23c21152974eb0363b5d7 + md5: bf42ee94c750c0b2e7e998b79ac299ea + depends: + - jsonschema-with-format-nongpl >=4.18.0 + - packaging + - python >=3.10 + - python-json-logger >=2.0.4 + - pyyaml >=5.3 + - referencing + - rfc3339-validator + - rfc3986-validator >=0.1.1 + - traitlets >=5.3 + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/jupyter-events?source=compressed-mapping + size: 24002 + timestamp: 1776861872237 +- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.18.2-pyhcf101f3_0.conda + sha256: 04fb8ea7749f67abaf76df6257bf86688e1389ceed55eb4fb0176fd2e882dbd6 + md5: 5ee7945accf0f215ddd6055d25d7cd83 + depends: + - anyio >=3.1.0 + - argon2-cffi >=21.1 + - jinja2 >=3.0.3 + - jupyter_client >=7.4.4 + - jupyter_core >=4.12,!=5.0.* + - jupyter_events >=0.11.0 + - jupyter_server_terminals >=0.4.4 + - nbconvert-core >=6.4.4 + - nbformat >=5.3.0 + - overrides >=5.0 + - packaging >=22.0 + - prometheus_client >=0.9 + - python >=3.10 + - pyzmq >=24 + - send2trash >=1.8.2 + - terminado >=0.8.3 + - tornado >=6.2.0 + - traitlets >=5.6.0 + - websocket-client >=1.7 + - python + license: BSD-3-Clause + purls: + - pkg:pypi/jupyter-server?source=compressed-mapping + size: 360522 + timestamp: 1778060967727 +- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server_terminals-0.5.4-pyhcf101f3_0.conda + sha256: 5eda79ed9f53f590031d29346abd183051263227dd9ee667b5ca1133ce297654 + md5: 7b8bace4943e0dc345fc45938826f2b8 + depends: + - python >=3.10 + - terminado >=0.8.3 + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/jupyter-server-terminals?source=hash-mapping + size: 22052 + timestamp: 1768574057200 +- conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.5.7-pyhd8ed1ab_0.conda + sha256: b85befad5ba1f50c0cc042a2ffb26441d13ffc2f18572dc20d3541476da0c7b9 + md5: 2ffe77234070324e763a6eddabb5f467 + depends: + - async-lru >=1.0.0 + - httpx >=0.25.0,<1 + - ipykernel >=6.5.0,!=6.30.0 + - jinja2 >=3.0.3 + - jupyter-lsp >=2.0.0 + - jupyter_core + - jupyter_server >=2.4.0,<3 + - jupyterlab_server >=2.28.0,<3 + - notebook-shim >=0.2 + - packaging + - python >=3.10 + - setuptools >=41.1.0 + - tomli >=1.2.2 + - tornado >=6.2.0 + - traitlets + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/jupyterlab?source=compressed-mapping + size: 8861204 + timestamp: 1777483115382 +- conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_pygments-0.3.0-pyhd8ed1ab_2.conda + sha256: dc24b900742fdaf1e077d9a3458fd865711de80bca95fe3c6d46610c532c6ef0 + md5: fd312693df06da3578383232528c468d + depends: + - pygments >=2.4.1,<3 + - python >=3.9 + constrains: + - jupyterlab >=4.0.8,<5.0.0 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/jupyterlab-pygments?source=hash-mapping + size: 18711 + timestamp: 1733328194037 +- conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_server-2.28.0-pyhcf101f3_0.conda + sha256: 381d2d6a259a3be5f38a69463e0f6c5dcf1844ae113058007b51c3bef13a7cee + md5: a63877cb23de826b1620d3adfccc4014 + depends: + - babel >=2.10 + - jinja2 >=3.0.3 + - json5 >=0.9.0 + - jsonschema >=4.18 + - jupyter_server >=1.21,<3 + - packaging >=21.3 + - python >=3.10 + - requests >=2.31 + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/jupyterlab-server?source=hash-mapping + size: 51621 + timestamp: 1761145478692 +- conda: https://conda.anaconda.org/conda-forge/noarch/lark-1.3.1-pyhd8ed1ab_0.conda + sha256: 49570840fb15f5df5d4b4464db8ee43a6d643031a2bc70ef52120a52e3809699 + md5: 9b965c999135d43a3d0f7bd7d024e26a + depends: + - python >=3.10 + license: MIT + license_family: MIT + purls: + - pkg:pypi/lark?source=hash-mapping + size: 94312 + timestamp: 1761596921009 +- conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.2.1-pyhd8ed1ab_0.conda + sha256: 9d690334de0cd1d22c51bc28420663f4277cfa60d34fa5cad1ce284a13f1d603 + md5: 00e120ce3e40bad7bfc78861ce3c4a25 + depends: + - python >=3.10 + - traitlets + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/matplotlib-inline?source=hash-mapping + size: 15175 + timestamp: 1761214578417 +- conda: https://conda.anaconda.org/conda-forge/noarch/mistune-3.2.1-pyhcf101f3_0.conda + sha256: b52dc6c78fbbe7a3008535cb8bfd87d70d8053e9250bbe16e387470a9df07070 + md5: b97e84d1553b4a1c765b87fff83453ad + depends: + - python >=3.10 + - typing_extensions + - python + license: BSD-3-Clause + purls: + - pkg:pypi/mistune?source=compressed-mapping + size: 74567 + timestamp: 1777824616382 +- conda: https://conda.anaconda.org/conda-forge/noarch/nbclient-0.10.4-pyhd8ed1ab_0.conda + sha256: 1b66960ee06874ddceeebe375d5f17fb5f393d025a09e15b830ad0c4fffb585b + md5: 00f5b8dafa842e0c27c1cd7296aa4875 + depends: + - jupyter_client >=6.1.12 + - jupyter_core >=4.12,!=5.0.* + - nbformat >=5.1 + - python >=3.8 + - traitlets >=5.4 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/nbclient?source=compressed-mapping + size: 28473 + timestamp: 1766485646962 +- conda: https://conda.anaconda.org/conda-forge/noarch/nbconvert-core-7.17.1-pyhcf101f3_0.conda + sha256: ab2ac79c5892c5434d50b3542d96645bdaa06d025b6e03734be29200de248ac2 + md5: 2bce0d047658a91b99441390b9b27045 + depends: + - beautifulsoup4 + - bleach-with-css !=5.0.0 + - defusedxml + - importlib-metadata >=3.6 + - jinja2 >=3.0 + - jupyter_core >=4.7 + - jupyterlab_pygments + - markupsafe >=2.0 + - mistune >=2.0.3,<4 + - nbclient >=0.5.0 + - nbformat >=5.7 + - packaging + - pandocfilters >=1.4.1 + - pygments >=2.4.1 + - python >=3.10 + - traitlets >=5.1 + - python + constrains: + - pandoc >=2.9.2,<4.0.0 + - nbconvert ==7.17.1 *_0 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/nbconvert?source=compressed-mapping + size: 202229 + timestamp: 1775615493260 +- conda: https://conda.anaconda.org/conda-forge/noarch/nbformat-5.10.4-pyhd8ed1ab_1.conda + sha256: 7a5bd30a2e7ddd7b85031a5e2e14f290898098dc85bea5b3a5bf147c25122838 + md5: bbe1963f1e47f594070ffe87cdf612ea + depends: + - jsonschema >=2.6 + - jupyter_core >=4.12,!=5.0.* + - python >=3.9 + - python-fastjsonschema >=2.15 + - traitlets >=5.1 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/nbformat?source=hash-mapping + size: 100945 + timestamp: 1733402844974 +- conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_1.conda + sha256: bb7b21d7fd0445ddc0631f64e66d91a179de4ba920b8381f29b9d006a42788c0 + md5: 598fd7d4d0de2455fb74f56063969a97 + depends: + - python >=3.9 + license: BSD-2-Clause + license_family: BSD + purls: + - pkg:pypi/nest-asyncio?source=hash-mapping + size: 11543 + timestamp: 1733325673691 +- conda: https://conda.anaconda.org/conda-forge/noarch/notebook-shim-0.2.4-pyhd8ed1ab_1.conda + sha256: 7b920e46b9f7a2d2aa6434222e5c8d739021dbc5cc75f32d124a8191d86f9056 + md5: e7f89ea5f7ea9401642758ff50a2d9c1 + depends: + - jupyter_server >=1.8,<3 + - python >=3.9 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/notebook-shim?source=hash-mapping + size: 16817 + timestamp: 1733408419340 +- conda: https://conda.anaconda.org/conda-forge/noarch/overrides-7.7.0-pyhd8ed1ab_1.conda + sha256: 1840bd90d25d4930d60f57b4f38d4e0ae3f5b8db2819638709c36098c6ba770c + md5: e51f1e4089cad105b6cac64bd8166587 + depends: + - python >=3.9 + - typing_utils + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/overrides?source=hash-mapping + size: 30139 + timestamp: 1734587755455 +- conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.2-pyhc364b38_0.conda + sha256: 3906abfb6511a3bb309e39b9b1b7bc38f50a723971de2395489fd1f379255890 + md5: 4c06a92e74452cfa53623a81592e8934 + depends: + - python >=3.8 + - python + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/packaging?source=compressed-mapping + size: 91574 + timestamp: 1777103621679 +- conda: https://conda.anaconda.org/conda-forge/noarch/pandocfilters-1.5.0-pyhd8ed1ab_0.tar.bz2 + sha256: 2bb9ba9857f4774b85900c2562f7e711d08dd48e2add9bee4e1612fbee27e16f + md5: 457c2c8c08e54905d6954e79cb5b5db9 + depends: + - python !=3.0,!=3.1,!=3.2,!=3.3 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/pandocfilters?source=hash-mapping + size: 11627 + timestamp: 1631603397334 +- conda: https://conda.anaconda.org/conda-forge/noarch/paramiko-4.0.0-pyhd8ed1ab_0.conda + sha256: ce76d5a1fc6c7ef636cbdbf14ce2d601a1bfa0dd8d286507c1fd02546fccb94b + md5: 1a884d2b1ea21abfb73911dcdb8342e4 + depends: + - bcrypt >=3.2 + - cryptography >=3.3 + - invoke >=2.0 + - pynacl >=1.5 + - python >=3.9 + license: LGPL-2.1-or-later + license_family: LGPL + purls: + - pkg:pypi/paramiko?source=hash-mapping + size: 159896 + timestamp: 1755102147074 +- conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.7-pyhcf101f3_0.conda + sha256: 611882f7944b467281c46644ffde6c5145d1a7730388bcde26e7e86819b0998e + md5: 39894c952938276405a1bd30e4ce2caf + depends: + - python >=3.10 + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/parso?source=compressed-mapping + size: 82472 + timestamp: 1777722955579 +- conda: https://conda.anaconda.org/conda-forge/noarch/pathspec-1.1.1-pyhd8ed1ab_0.conda + sha256: 6eaee417d33f298db79bc7185ab1208604c0e6cf51dade34cd513c6f9db9c6f3 + md5: 11adc78451c998c0fd162584abfa3559 + depends: + - python >=3.10 + license: MPL-2.0 + license_family: MOZILLA + purls: + - pkg:pypi/pathspec?source=compressed-mapping + size: 56559 + timestamp: 1777271601895 +- conda: https://conda.anaconda.org/conda-forge/noarch/pexpect-4.9.0-pyhd8ed1ab_1.conda + sha256: 202af1de83b585d36445dc1fda94266697341994d1a3328fabde4989e1b3d07a + md5: d0d408b1f18883a944376da5cf8101ea + depends: + - ptyprocess >=0.5 + - python >=3.9 + license: ISC + purls: + - pkg:pypi/pexpect?source=hash-mapping + size: 53561 + timestamp: 1733302019362 +- conda: https://conda.anaconda.org/conda-forge/noarch/pixi-kernel-0.7.1-pyhbbac1ac_0.conda + sha256: 506c9330b8dc5ae98f4c32629fa59fa40e6bdd42a681c48d2f9554693dd01156 + md5: d57ef7cb7ad6b5d62cef8b9bdf1d400b + depends: + - ipykernel >=6 + - jupyter_client >=7 + - jupyter_server >=2.4 + - msgspec >=0.18 + - python >=3.10 + - returns >=0.23 + - tomli >=2 + license: MIT + license_family: MIT + purls: + - pkg:pypi/pixi-kernel?source=hash-mapping + size: 39509 + timestamp: 1764156429044 +- conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.9.6-pyhcf101f3_0.conda + sha256: 8f29915c172f1f7f4f7c9391cd5dac3ebf5d13745c8b7c8006032615246345a5 + md5: 89c0b6d1793601a2a3a3f7d2d3d8b937 + depends: + - python >=3.10 + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/platformdirs?source=compressed-mapping + size: 25862 + timestamp: 1775741140609 +- conda: https://conda.anaconda.org/conda-forge/noarch/plumbum-1.10.0-pyhcf101f3_0.conda + sha256: 972e0c1c9f0b1763d482e885732baef7f9a0cbe8686f5e2c6bdd88838619a59a + md5: 7fd2e38f4e608f5ebd80f871352c5495 + depends: + - python >=3.10 + - pywin32-on-windows + - paramiko + - importlib_resources + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/plumbum?source=hash-mapping + size: 103911 + timestamp: 1761911487086 +- conda: https://conda.anaconda.org/conda-forge/noarch/prometheus_client-0.25.0-pyhd8ed1ab_0.conda + sha256: 4d7ec90d4f9c1f3b4a50623fefe4ebba69f651b102b373f7c0e9dbbfa43d495c + md5: a11ab1f31af799dd93c3a39881528884 + depends: + - python >=3.10 + license: Apache-2.0 + license_family: Apache + purls: + - pkg:pypi/prometheus-client?source=compressed-mapping + size: 57113 + timestamp: 1775771465170 +- conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.51-pyha770c72_0.conda + sha256: ebc1bb62ac612af6d40667da266ff723662394c0ca78935340a5b5c14831227b + md5: d17ae9db4dc594267181bd199bf9a551 + depends: + - python >=3.9 + - wcwidth + constrains: + - prompt_toolkit 3.0.51 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/prompt-toolkit?source=hash-mapping + size: 271841 + timestamp: 1744724188108 +- conda: https://conda.anaconda.org/conda-forge/noarch/prompt_toolkit-3.0.51-hd8ed1ab_0.conda + sha256: 936189f0373836c1c77cd2d6e71ba1e583e2d3920bf6d015e96ee2d729b5e543 + md5: 1e61ab85dd7c60e5e73d853ea035dc29 + depends: + - prompt-toolkit >=3.0.51,<3.0.52.0a0 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 7182 + timestamp: 1744724189376 +- conda: https://conda.anaconda.org/conda-forge/noarch/ptyprocess-0.7.0-pyhd8ed1ab_1.conda + sha256: a7713dfe30faf17508ec359e0bc7e0983f5d94682492469bd462cdaae9c64d83 + md5: 7d9daffbb8d8e0af0f769dbbcd173a54 + depends: + - python >=3.9 + license: ISC + purls: + - pkg:pypi/ptyprocess?source=hash-mapping + size: 19457 + timestamp: 1733302371990 +- conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda + sha256: 71bd24600d14bb171a6321d523486f6a06f855e75e547fa0cb2a0953b02047f0 + md5: 3bfdfb8dbcdc4af1ae3f9a8eb3948f04 + depends: + - python >=3.9 + license: MIT + license_family: MIT + purls: + - pkg:pypi/pure-eval?source=hash-mapping + size: 16668 + timestamp: 1733569518868 +- conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda + sha256: 79db7928d13fab2d892592223d7570f5061c192f27b9febd1a418427b719acc6 + md5: 12c566707c80111f9799308d9e265aef + depends: + - python >=3.9 + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/pycparser?source=hash-mapping + size: 110100 + timestamp: 1733195786147 +- conda: https://conda.anaconda.org/conda-forge/noarch/pydantic-2.13.4-pyhcf101f3_0.conda + sha256: 69700e31165df070e9716315e042196aa92525dae5deb5107785847ab9f4189f + md5: 729843edafc0899b3348bd3f19525b9d + depends: + - typing-inspection >=0.4.2 + - typing_extensions >=4.14.1 + - python >=3.10 + - annotated-types >=0.6.0 + - pydantic-core ==2.46.4 + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/pydantic?source=compressed-mapping + size: 346511 + timestamp: 1778103405862 +- conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda + sha256: cf70b2f5ad9ae472b71235e5c8a736c9316df3705746de419b59d442e8348e86 + md5: 16c18772b340887160c79a6acc022db0 + depends: + - python >=3.10 + license: BSD-2-Clause + license_family: BSD + purls: + - pkg:pypi/pygments?source=compressed-mapping + size: 893031 + timestamp: 1774796815820 +- conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyh09c184e_7.conda + sha256: d016e04b0e12063fbee4a2d5fbb9b39a8d191b5a0042f0b8459188aedeabb0ca + md5: e2fd202833c4a981ce8a65974fe4abd1 + depends: + - __win + - python >=3.9 + - win_inet_pton + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/pysocks?source=hash-mapping + size: 21784 + timestamp: 1733217448189 +- conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda + sha256: ba3b032fa52709ce0d9fd388f63d330a026754587a2f461117cac9ab73d8d0d8 + md5: 461219d1a5bd61342293efa2c0c90eac + depends: + - __unix + - python >=3.9 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/pysocks?source=hash-mapping + size: 21085 + timestamp: 1733217331982 +- conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda + sha256: d6a17ece93bbd5139e02d2bd7dbfa80bee1a4261dced63f65f679121686bf664 + md5: 5b8d21249ff20967101ffa321cab24e8 + depends: + - python >=3.9 + - six >=1.5 + - python + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/python-dateutil?source=hash-mapping + size: 233310 + timestamp: 1751104122689 +- conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.21.2-pyhe01879c_0.conda + sha256: df9aa74e9e28e8d1309274648aac08ec447a92512c33f61a8de0afa9ce32ebe8 + md5: 23029aae904a2ba587daba708208012f + depends: + - python >=3.9 + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/fastjsonschema?source=hash-mapping + size: 244628 + timestamp: 1755304154927 +- conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.13.13-h4df99d1_100.conda + sha256: b2e51d83e5ebeb7e9fde1cde822a60e8564cc9dabd786ad853056afbf708a466 + md5: fd00e4b24ea88093c93f5c9bad27b52f + depends: + - cpython 3.13.13.* + - python_abi * *_cp313 + license: Python-2.0 + purls: [] + size: 48536 + timestamp: 1775613791711 +- conda: https://conda.anaconda.org/conda-forge/noarch/python-json-logger-3.2.1-pyh332efcf_0.conda + sha256: 1c55116c22512cef7b01d55ae49697707f2c1fd829407183c19817e2d300fd8d + md5: 1cd2f3e885162ee1366312bd1b1677fd + depends: + - python >=3.10 + - typing_extensions + license: BSD-2-Clause + license_family: BSD + purls: + - pkg:pypi/python-json-logger?source=compressed-mapping + size: 18969 + timestamp: 1777318679482 +- conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2026.2-pyhd8ed1ab_0.conda + sha256: e943f9c15a6bdba2e1b9f423ab913b3f6b02197b0ef9f8e6b7464d78b59965b9 + md5: f6ad7450fc21e00ecc23812baed6d2e4 + depends: + - python >=3.10 + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/tzdata?source=compressed-mapping + size: 146639 + timestamp: 1777068997932 +- conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.11-8_cp311.conda + build_number: 8 + sha256: fddf123692aa4b1fc48f0471e346400d9852d96eeed77dbfdd746fa50a8ff894 + md5: 8fcb6b0e2161850556231336dae58358 + constrains: + - python 3.11.* *_cpython + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 7003 + timestamp: 1752805919375 +- conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda + build_number: 8 + sha256: 210bffe7b121e651419cb196a2a63687b087497595c9be9d20ebe97dd06060a7 + md5: 94305520c52a4aa3f6c2b1ff6008d9f8 + constrains: + - python 3.13.* *_cp313 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 7002 + timestamp: 1752805902938 +- conda: https://conda.anaconda.org/conda-forge/noarch/pywin32-on-windows-0.1.0-pyh07e9846_2.tar.bz2 + sha256: 09803b75cccc16d8586d2f41ea890658d165f4afc359973fa1c7904a2c140eae + md5: 91733394059b880d9cc0d010c20abda0 + depends: + - python >=2.7 + - pywin32 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 5282 + timestamp: 1646866839398 +- conda: https://conda.anaconda.org/conda-forge/noarch/pywin32-on-windows-0.1.0-pyh1179c8e_3.tar.bz2 + sha256: 6502696aaef571913b22a808b15c185bd8ea4aabb952685deb29e6a6765761cb + md5: 2807a0becd1d986fe1ef9b7f8135f215 + depends: + - __unix + - python >=2.7 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 4856 + timestamp: 1646866525560 +- conda: https://conda.anaconda.org/conda-forge/noarch/questionary-2.1.1-pyhd8ed1ab_0.conda + sha256: 0604c6dff3af5f53e34fceb985395d08287137f220450108a795bcd1959caf14 + md5: 34fa231b5c5927684b03bb296bb94ddc + depends: + - prompt_toolkit >=2.0,<4.0 + - python >=3.10 + license: MIT + license_family: MIT + purls: + - pkg:pypi/questionary?source=hash-mapping + size: 31257 + timestamp: 1757356458097 +- conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.37.0-pyhcf101f3_0.conda + sha256: 0577eedfb347ff94d0f2fa6c052c502989b028216996b45c7f21236f25864414 + md5: 870293df500ca7e18bedefa5838a22ab + depends: + - attrs >=22.2.0 + - python >=3.10 + - rpds-py >=0.7.0 + - typing_extensions >=4.4.0 + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/referencing?source=hash-mapping + size: 51788 + timestamp: 1760379115194 +- conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.33.1-pyhcf101f3_1.conda + sha256: 7f2c24dd3bd3c104a1d2c9a10ead5ed6758b0976b74f972cfe9c19884ccc4241 + md5: 9659f587a8ceacc21864260acd02fc67 + depends: + - python >=3.10 + - certifi >=2023.5.7 + - charset-normalizer >=2,<4 + - idna >=2.5,<4 + - urllib3 >=1.26,<3 + - python + constrains: + - chardet >=3.0.2,<8 + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/requests?source=compressed-mapping + size: 63728 + timestamp: 1777030058920 +- conda: https://conda.anaconda.org/conda-forge/noarch/returns-0.27.0-pyhc364b38_0.conda + sha256: 3b45efeae771f1a20307b36ecdb3a8911a89c05382836b50c62b0a99d8d3dfd8 + md5: da94ff04d97ec5efc42cbe5da3c43a84 + depends: + - python >=3.11 + - typing_extensions >=4.0,<5.0 + - python + license: BSD-2-Clause + license_family: BSD + purls: + - pkg:pypi/returns?source=hash-mapping + size: 100559 + timestamp: 1776176903101 +- conda: https://conda.anaconda.org/conda-forge/noarch/rfc3339-validator-0.1.4-pyhd8ed1ab_1.conda + sha256: 2e4372f600490a6e0b3bac60717278448e323cab1c0fecd5f43f7c56535a99c5 + md5: 36de09a8d3e5d5e6f4ee63af49e59706 + depends: + - python >=3.9 + - six + license: MIT + license_family: MIT + purls: + - pkg:pypi/rfc3339-validator?source=hash-mapping + size: 10209 + timestamp: 1733600040800 +- conda: https://conda.anaconda.org/conda-forge/noarch/rfc3986-validator-0.1.1-pyh9f0ad1d_0.tar.bz2 + sha256: 2a5b495a1de0f60f24d8a74578ebc23b24aa53279b1ad583755f223097c41c37 + md5: 912a71cc01012ee38e6b90ddd561e36f + depends: + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/rfc3986-validator?source=hash-mapping + size: 7818 + timestamp: 1598024297745 +- conda: https://conda.anaconda.org/conda-forge/noarch/rfc3987-syntax-1.1.0-pyhe01879c_1.conda + sha256: 70001ac24ee62058557783d9c5a7bbcfd97bd4911ef5440e3f7a576f9e43bc92 + md5: 7234f99325263a5af6d4cd195035e8f2 + depends: + - python >=3.9 + - lark >=1.2.2 + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/rfc3987-syntax?source=hash-mapping + size: 22913 + timestamp: 1752876729969 +- conda: https://conda.anaconda.org/conda-forge/noarch/send2trash-2.1.0-pyh5552912_1.conda + sha256: 8fc024bf1a7b99fc833b131ceef4bef8c235ad61ecb95a71a6108be2ccda63e8 + md5: b70e2d44e6aa2beb69ba64206a16e4c6 + depends: + - __osx + - pyobjc-framework-cocoa + - python >=3.10 + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/send2trash?source=hash-mapping + size: 22519 + timestamp: 1770937603551 +- conda: https://conda.anaconda.org/conda-forge/noarch/send2trash-2.1.0-pyh6dadd2b_1.conda + sha256: 305446a0b018f285351300463653d3d3457687270e20eda37417b12ee386ef76 + md5: 6ac53f3fff2c416d63511843a04646fa + depends: + - __win + - pywin32 + - python >=3.10 + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/send2trash?source=hash-mapping + size: 22864 + timestamp: 1770937641143 +- conda: https://conda.anaconda.org/conda-forge/noarch/send2trash-2.1.0-pyha191276_1.conda + sha256: 59656f6b2db07229351dfb3a859c35e57cc8e8bcbc86d4e501bff881a6f771f1 + md5: 28eb91468df04f655a57bcfbb35fc5c5 + depends: + - __linux + - python >=3.10 + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/send2trash?source=hash-mapping + size: 24108 + timestamp: 1770937597662 +- conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-82.0.1-pyh332efcf_0.conda + sha256: 82088a6e4daa33329a30bc26dc19a98c7c1d3f05c0f73ce9845d4eab4924e9e1 + md5: 8e194e7b992f99a5015edbd4ebd38efd + depends: + - python >=3.10 + license: MIT + license_family: MIT + purls: + - pkg:pypi/setuptools?source=hash-mapping + size: 639697 + timestamp: 1773074868565 +- conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda + sha256: 458227f759d5e3fcec5d9b7acce54e10c9e1f4f4b7ec978f3bfd54ce4ee9853d + md5: 3339e3b65d58accf4ca4fb8748ab16b3 + depends: + - python >=3.9 + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/six?source=hash-mapping + size: 18455 + timestamp: 1753199211006 +- conda: https://conda.anaconda.org/conda-forge/noarch/sniffio-1.3.1-pyhd8ed1ab_2.conda + sha256: dce518f45e24cd03f401cb0616917773159a210c19d601c5f2d4e0e5879d30ad + md5: 03fe290994c5e4ec17293cfb6bdce520 + depends: + - python >=3.10 + license: Apache-2.0 + license_family: Apache + purls: + - pkg:pypi/sniffio?source=hash-mapping + size: 15698 + timestamp: 1762941572482 +- conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.8.3-pyhd8ed1ab_0.conda + sha256: 23b71ecf089967d2900126920e7f9ff18cdcef82dbff3e2f54ffa360243a17ac + md5: 18de09b20462742fe093ba39185d9bac + depends: + - python >=3.10 + license: MIT + license_family: MIT + purls: + - pkg:pypi/soupsieve?source=hash-mapping + size: 38187 + timestamp: 1769034509657 +- conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda + sha256: 570da295d421661af487f1595045760526964f41471021056e993e73089e9c41 + md5: b1b505328da7a6b246787df4b5a49fbc + depends: + - asttokens + - executing + - pure_eval + - python >=3.9 + license: MIT + license_family: MIT + purls: + - pkg:pypi/stack-data?source=hash-mapping + size: 26988 + timestamp: 1733569565672 +- conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyh6dadd2b_1.conda + sha256: b375e8df0d5710717c31e7c8e93c025c37fa3504aea325c7a55509f64e5d4340 + md5: e43ca10d61e55d0a8ec5d8c62474ec9e + depends: + - __win + - pywinpty >=1.1.0 + - python >=3.10 + - tornado >=6.1.0 + - python + license: BSD-2-Clause + license_family: BSD + purls: + - pkg:pypi/terminado?source=hash-mapping + size: 23665 + timestamp: 1766513806974 +- conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyhc90fa1f_1.conda + sha256: 6b6727a13d1ca6a23de5e6686500d0669081a117736a87c8abf444d60c1e40eb + md5: 17b43cee5cc84969529d5d0b0309b2cb + depends: + - __unix + - ptyprocess + - python >=3.10 + - tornado >=6.1.0 + - python + license: BSD-2-Clause + license_family: BSD + purls: + - pkg:pypi/terminado?source=hash-mapping + size: 24749 + timestamp: 1766513766867 +- conda: https://conda.anaconda.org/conda-forge/noarch/tinycss2-1.4.0-pyhd8ed1ab_0.conda + sha256: cad582d6f978276522f84bd209a5ddac824742fe2d452af6acf900f8650a73a2 + md5: f1acf5fdefa8300de697982bcb1761c9 + depends: + - python >=3.5 + - webencodings >=0.4 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/tinycss2?source=hash-mapping + size: 28285 + timestamp: 1729802975370 +- conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.1-pyhcf101f3_0.conda + sha256: 91cafdb64268e43e0e10d30bd1bef5af392e69f00edd34dfaf909f69ab2da6bd + md5: b5325cf06a000c5b14970462ff5e4d58 + depends: + - python >=3.10 + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/tomli?source=hash-mapping + size: 21561 + timestamp: 1774492402955 +- conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.15.0-pyhcf101f3_0.conda + sha256: dfb681579be59c2e790c95f7f49b7529a9b0511d6385ad276e3c8988cbd54d2c + md5: 4bada6a6d908a27262af8ebddf4f7492 + depends: + - python >=3.10 + - python + license: BSD-3-Clause + purls: + - pkg:pypi/traitlets?source=compressed-mapping + size: 115165 + timestamp: 1778074251714 +- conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.15.0-h396c80c_0.conda + sha256: 7c2df5721c742c2a47b2c8f960e718c930031663ac1174da67c1ed5999f7938c + md5: edd329d7d3a4ab45dcf905899a7a6115 + depends: + - typing_extensions ==4.15.0 pyhcf101f3_0 + license: PSF-2.0 + license_family: PSF + purls: [] + size: 91383 + timestamp: 1756220668932 +- conda: https://conda.anaconda.org/conda-forge/noarch/typing-inspection-0.4.2-pyhcf101f3_2.conda + sha256: 8b90d2f19f9458b8c58a55e1fcdc1d90c1603a847a47654d8a454549413ba60a + md5: 53f5409c5cfd6c5a66417d68e3f0a864 + depends: + - python >=3.10 + - typing_extensions >=4.12.0 + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/typing-inspection?source=compressed-mapping + size: 20935 + timestamp: 1777105465795 +- conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda + sha256: 032271135bca55aeb156cee361c81350c6f3fb203f57d024d7e5a1fc9ef18731 + md5: 0caa1af407ecff61170c9437a808404d + depends: + - python >=3.10 + - python + license: PSF-2.0 + license_family: PSF + purls: + - pkg:pypi/typing-extensions?source=hash-mapping + size: 51692 + timestamp: 1756220668932 +- conda: https://conda.anaconda.org/conda-forge/noarch/typing_utils-0.1.0-pyhd8ed1ab_1.conda + sha256: 3088d5d873411a56bf988eee774559335749aed6f6c28e07bf933256afb9eb6c + md5: f6d7aa696c67756a650e91e15e88223c + depends: + - python >=3.9 + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/typing-utils?source=hash-mapping + size: 15183 + timestamp: 1733331395943 +- conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + sha256: 1d30098909076af33a35017eed6f2953af1c769e273a0626a04722ac4acaba3c + md5: ad659d0a2b3e47e38d829aa8cad2d610 + license: LicenseRef-Public-Domain + purls: [] + size: 119135 + timestamp: 1767016325805 +- conda: https://conda.anaconda.org/conda-forge/noarch/uri-template-1.3.0-pyhd8ed1ab_1.conda + sha256: e0eb6c8daf892b3056f08416a96d68b0a358b7c46b99c8a50481b22631a4dfc0 + md5: e7cb0f5745e4c5035a460248334af7eb + depends: + - python >=3.9 + license: MIT + license_family: MIT + purls: + - pkg:pypi/uri-template?source=hash-mapping + size: 23990 + timestamp: 1733323714454 +- conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.6.3-pyhd8ed1ab_0.conda + sha256: af641ca7ab0c64525a96fd9ad3081b0f5bcf5d1cbb091afb3f6ed5a9eee6111a + md5: 9272daa869e03efe68833e3dc7a02130 + depends: + - backports.zstd >=1.0.0 + - brotli-python >=1.2.0 + - h2 >=4,<5 + - pysocks >=1.5.6,<2.0,!=1.5.7 + - python >=3.10 + license: MIT + license_family: MIT + purls: + - pkg:pypi/urllib3?source=hash-mapping + size: 103172 + timestamp: 1767817860341 +- conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.7.0-pyhd8ed1ab_0.conda + sha256: 1ee2d8384972ecbf8630ce8a3ea9d16858358ad3e8566675295e66996d5352da + md5: eb9538b8e55069434a18547f43b96059 + depends: + - python >=3.10 + license: MIT + license_family: MIT + purls: + - pkg:pypi/wcwidth?source=compressed-mapping + size: 82917 + timestamp: 1777744489106 +- conda: https://conda.anaconda.org/conda-forge/noarch/webcolors-25.10.0-pyhd8ed1ab_0.conda + sha256: 21f6c8a20fe050d09bfda3fb0a9c3493936ce7d6e1b3b5f8b01319ee46d6c6f6 + md5: 6639b6b0d8b5a284f027a2003669aa65 + depends: + - python >=3.10 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/webcolors?source=hash-mapping + size: 18987 + timestamp: 1761899393153 +- conda: https://conda.anaconda.org/conda-forge/noarch/webencodings-0.5.1-pyhd8ed1ab_3.conda + sha256: 19ff205e138bb056a46f9e3839935a2e60bd1cf01c8241a5e172a422fed4f9c6 + md5: 2841eb5bfc75ce15e9a0054b98dcd64d + depends: + - python >=3.9 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/webencodings?source=hash-mapping + size: 15496 + timestamp: 1733236131358 +- conda: https://conda.anaconda.org/conda-forge/noarch/websocket-client-1.9.0-pyhd8ed1ab_0.conda + sha256: 42a2b61e393e61cdf75ced1f5f324a64af25f347d16c60b14117393a98656397 + md5: 2f1ed718fcd829c184a6d4f0f2e07409 + depends: + - python >=3.10 + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/websocket-client?source=hash-mapping + size: 61391 + timestamp: 1759928175142 +- conda: https://conda.anaconda.org/conda-forge/noarch/win_inet_pton-1.1.0-pyh7428d3b_8.conda + sha256: 93807369ab91f230cf9e6e2a237eaa812492fe00face5b38068735858fba954f + md5: 46e441ba871f524e2b067929da3051c2 + depends: + - __win + - python >=3.9 + license: LicenseRef-Public-Domain + purls: + - pkg:pypi/win-inet-pton?source=hash-mapping + size: 9555 + timestamp: 1733130678956 +- conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.1-pyhcf101f3_0.conda + sha256: 523616c0530d305d2216c2b4a8dfd3872628b60083255b89c5e0d8c42e738cca + md5: e1c36c6121a7c9c76f2f148f1e83b983 + depends: + - python >=3.10 + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/zipp?source=compressed-mapping + size: 24461 + timestamp: 1776131454755 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/argon2-cffi-bindings-25.1.0-py311h9408147_2.conda + sha256: c15fc1aa6184185f1b31670a16b76d59f484d3efa25b467f28d59088cf0085c0 + md5: 501c2a606787bcd55b9ddee776208333 + depends: + - __osx >=11.0 + - cffi >=1.0.1 + - python >=3.11,<3.12.0a0 + - python >=3.11,<3.12.0a0 *_cpython + - python_abi 3.11.* *_cp311 + license: MIT + license_family: MIT + purls: + - pkg:pypi/argon2-cffi-bindings?source=hash-mapping + size: 34403 + timestamp: 1762509963182 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/argon2-cffi-bindings-25.1.0-py313h6535dbc_2.conda + sha256: 05ea6fa7109235cfb4fc24526bae1fe82d88bbb5e697ab3945c313f5f041af5b + md5: e23e087109b2096db4cf9a3985bab329 + depends: + - __osx >=11.0 + - cffi >=1.0.1 + - python >=3.13,<3.14.0a0 + - python >=3.13,<3.14.0a0 *_cp313 + - python_abi 3.13.* *_cp313 + license: MIT + license_family: MIT + purls: + - pkg:pypi/argon2-cffi-bindings?source=hash-mapping + size: 33947 + timestamp: 1762510144907 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/backports.zstd-1.4.0-py311h36d4fbb_0.conda + sha256: 2e6399a3663e7e20134a208cd09befc44ce1e9efe94d1d36d8911db04f186270 + md5: 3ee7e8ae532366221658b80baee36dc0 + depends: + - python + - __osx >=11.0 + - python_abi 3.11.* *_cp311 + - zstd >=1.5.7,<1.6.0a0 + license: BSD-3-Clause AND MIT AND EPL-2.0 + purls: + - pkg:pypi/backports-zstd?source=compressed-mapping + size: 244743 + timestamp: 1777848723707 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/backports.zstd-1.4.0-py313h7208f8c_0.conda + sha256: 257b234caffc760e48c8d7e642d6d0d8bac4341ca19c7df098421358eff636a1 + md5: 84e922fe79295051f6925d7f8d71c98c + depends: + - python + - __osx >=11.0 + - python_abi 3.13.* *_cp313 + - zstd >=1.5.7,<1.6.0a0 + license: BSD-3-Clause AND MIT AND EPL-2.0 + purls: + - pkg:pypi/backports-zstd?source=compressed-mapping + size: 242188 + timestamp: 1777848729476 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/bcrypt-5.0.0-py311h71babbd_1.conda + sha256: 79594899d1f2472a6d95c3d489268bb9103ba6bab043d9faca379d989074aeb3 + md5: 00ad8eed3ec75b6ec425526968c72e61 + depends: + - python + - python 3.11.* *_cpython + - __osx >=11.0 + - python_abi 3.11.* *_cp311 + constrains: + - __osx >=11.0 + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/bcrypt?source=hash-mapping + size: 268519 + timestamp: 1762497846273 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/bcrypt-5.0.0-py313h2c089d5_1.conda + sha256: c8e3b95ad2665dc16ac37888fb91dd481fa759bad1fbf799451d12d6bfaec600 + md5: 4b14cba28eaf98170c2ddd7e900efa07 + depends: + - python + - __osx >=11.0 + - python 3.13.* *_cp313 + - python_abi 3.13.* *_cp313 + constrains: + - __osx >=11.0 + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/bcrypt?source=hash-mapping + size: 267465 + timestamp: 1762497730829 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-python-1.2.0-py311hdc60ec4_1.conda + sha256: 617545ec0e97d35ed2ff7852f2581a20c0dda80b366d0c42a43706687f971ba8 + md5: 150cbf381febcf0a5e470a8d066e1bc0 + depends: + - __osx >=11.0 + - libcxx >=19 + - python >=3.11,<3.12.0a0 + - python >=3.11,<3.12.0a0 *_cpython + - python_abi 3.11.* *_cp311 + constrains: + - libbrotlicommon 1.2.0 hc919400_1 + license: MIT + license_family: MIT + purls: + - pkg:pypi/brotli?source=hash-mapping + size: 359588 + timestamp: 1764018467340 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-python-1.2.0-py313hde1f3bb_1.conda + sha256: 2e21dccccd68bedd483300f9ab87a425645f6776e6e578e10e0dd98c946e1be9 + md5: b03732afa9f4f54634d94eb920dfb308 + depends: + - __osx >=11.0 + - libcxx >=19 + - python >=3.13,<3.14.0a0 + - python >=3.13,<3.14.0a0 *_cp313 + - python_abi 3.13.* *_cp313 + constrains: + - libbrotlicommon 1.2.0 hc919400_1 + license: MIT + license_family: MIT + purls: + - pkg:pypi/brotli?source=hash-mapping + size: 359568 + timestamp: 1764018359470 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-hd037594_9.conda + sha256: 540fe54be35fac0c17feefbdc3e29725cce05d7367ffedfaaa1bdda234b019df + md5: 620b85a3f45526a8bc4d23fd78fc22f0 + depends: + - __osx >=11.0 + license: bzip2-1.0.6 + license_family: BSD + purls: [] + size: 124834 + timestamp: 1771350416561 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/c-ares-1.34.6-hc919400_0.conda + sha256: 2995f2aed4e53725e5efbc28199b46bf311c3cab2648fc4f10c2227d6d5fa196 + md5: bcb3cba70cf1eec964a03b4ba7775f01 + depends: + - __osx >=11.0 + license: MIT + license_family: MIT + purls: [] + size: 180327 + timestamp: 1765215064054 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/cffi-2.0.0-py311hd10dc20_1.conda + sha256: 1ffde698463d6e7ed571bdb85cb17bfc1d3a107c026d20047995512dcc2749ec + md5: 4d7f6780e36f18e7601811dddf3bbec5 + depends: + - __osx >=11.0 + - libffi >=3.5.2,<3.6.0a0 + - pycparser + - python >=3.11,<3.12.0a0 + - python >=3.11,<3.12.0a0 *_cpython + - python_abi 3.11.* *_cp311 + license: MIT + license_family: MIT + purls: + - pkg:pypi/cffi?source=hash-mapping + size: 294848 + timestamp: 1761203196617 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/cffi-2.0.0-py313h224173a_1.conda + sha256: 1fa69651f5e81c25d48ac42064db825ed1a3e53039629db69f86b952f5ce603c + md5: 050374657d1c7a4f2ea443c0d0cbd9a0 + depends: + - __osx >=11.0 + - libffi >=3.5.2,<3.6.0a0 + - pycparser + - python >=3.13,<3.14.0a0 + - python >=3.13,<3.14.0a0 *_cp313 + - python_abi 3.13.* *_cp313 + license: MIT + license_family: MIT + purls: + - pkg:pypi/cffi?source=hash-mapping + size: 291376 + timestamp: 1761203583358 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/cryptography-48.0.0-py311hbb43c59_0.conda + sha256: 6e59412439c8b51ee5f05b3c50b05b43991fa544a895226168fab63be9ba28f9 + md5: cbc7fb4eb2049a4a6cd2c452afd2f7bd + depends: + - __osx >=11.0 + - cffi >=2.0 + - openssl >=3.5.6,<4.0a0 + - python >=3.11,<3.12.0a0 + - python_abi 3.11.* *_cp311 + constrains: + - __osx >=11.0 + license: Apache-2.0 AND BSD-3-Clause AND PSF-2.0 AND MIT + license_family: BSD + purls: + - pkg:pypi/cryptography?source=hash-mapping + size: 1855763 + timestamp: 1777966249758 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/cryptography-48.0.0-py313hda5ae78_0.conda + sha256: 2adca86f420558eaaad8b8a7ecffadc4a23f980c02716d0fd5bee3f51879e76b + md5: 730fd8d2542ece52b71bc4fbb80ceb10 + depends: + - __osx >=11.0 + - cffi >=2.0 + - openssl >=3.5.6,<4.0a0 + - python >=3.13,<3.14.0a0 + - python_abi 3.13.* *_cp313 + constrains: + - __osx >=11.0 + license: Apache-2.0 AND BSD-3-Clause AND PSF-2.0 AND MIT + license_family: BSD + purls: + - pkg:pypi/cryptography?source=hash-mapping + size: 1854514 + timestamp: 1777966313532 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/debugpy-1.8.20-py311h8948835_0.conda + sha256: 093b015e9abf27fb4d3b4f7e52417d35cd69a99fab8b95ec5c6c3983275c46ba + md5: 150c921424bc9f08c0378f8a6ae58d05 + depends: + - python + - __osx >=11.0 + - libcxx >=19 + - python 3.11.* *_cpython + - python_abi 3.11.* *_cp311 + license: MIT + license_family: MIT + purls: + - pkg:pypi/debugpy?source=hash-mapping + size: 2668163 + timestamp: 1769745020016 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/debugpy-1.8.20-py313h1188861_0.conda + sha256: 372464b345220758769f49e76d125933008abec7048df4077a851adcc79da1e8 + md5: b3a832c19cfa5dfcce7575750ef693ed + depends: + - python + - libcxx >=19 + - python 3.13.* *_cp313 + - __osx >=11.0 + - python_abi 3.13.* *_cp313 + license: MIT + license_family: MIT + purls: + - pkg:pypi/debugpy?source=hash-mapping + size: 2761021 + timestamp: 1769744996428 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/icu-78.3-hef89b57_0.conda + sha256: 3a7907a17e9937d3a46dfd41cffaf815abad59a569440d1e25177c15fd0684e5 + md5: f1182c91c0de31a7abd40cedf6a5ebef + depends: + - __osx >=11.0 + license: MIT + license_family: MIT + purls: [] + size: 12361647 + timestamp: 1773822915649 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/krb5-1.22.2-h385eeb1_0.conda + sha256: c0a0bf028fe7f3defcdcaa464e536cf1b202d07451e18ad83fdd169d15bef6ed + md5: e446e1822f4da8e5080a9de93474184d + depends: + - __osx >=11.0 + - libcxx >=19 + - libedit >=3.1.20250104,<3.2.0a0 + - libedit >=3.1.20250104,<4.0a0 + - openssl >=3.5.5,<4.0a0 + license: MIT + license_family: MIT + purls: [] + size: 1160828 + timestamp: 1769770119811 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libabseil-20260107.1-cxx17_h2062a1b_0.conda + sha256: 756611fbb8d2957a5b4635d9772bd8432cb6ddac05580a6284cca6fdc9b07fca + md5: bb65152e0d7c7178c0f1ee25692c9fd1 + depends: + - __osx >=11.0 + - libcxx >=19 + constrains: + - abseil-cpp =20260107.1 + - libabseil-static =20260107.1=cxx17* + license: Apache-2.0 + license_family: Apache + purls: [] + size: 1229639 + timestamp: 1770863511331 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlicommon-1.2.0-hc919400_1.conda + sha256: a7cb9e660531cf6fbd4148cff608c85738d0b76f0975c5fc3e7d5e92840b7229 + md5: 006e7ddd8a110771134fcc4e1e3a6ffa + depends: + - __osx >=11.0 + license: MIT + license_family: MIT + purls: [] + size: 79443 + timestamp: 1764017945924 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlidec-1.2.0-hc919400_1.conda + sha256: 2eae444039826db0454b19b52a3390f63bfe24f6b3e63089778dd5a5bf48b6bf + md5: 079e88933963f3f149054eec2c487bc2 + depends: + - __osx >=11.0 + - libbrotlicommon 1.2.0 hc919400_1 + license: MIT + license_family: MIT + purls: [] + size: 29452 + timestamp: 1764017979099 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlienc-1.2.0-hc919400_1.conda + sha256: 01436c32bb41f9cb4bcf07dda647ce4e5deb8307abfc3abdc8da5317db8189d1 + md5: b2b7c8288ca1a2d71ff97a8e6a1e8883 + depends: + - __osx >=11.0 + - libbrotlicommon 1.2.0 hc919400_1 + license: MIT + license_family: MIT + purls: [] + size: 290754 + timestamp: 1764018009077 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-22.1.5-h55c6f16_0.conda + sha256: 76c3d3c702593b6ef57dc2f99c082193a99835526634b844f4a78ae3aecdb6c8 + md5: d0a2e921c5a89df73118cff1afed5213 + depends: + - __osx >=11.0 + license: Apache-2.0 WITH LLVM-exception + license_family: Apache + purls: [] + size: 570968 + timestamp: 1778110367423 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libedit-3.1.20250104-pl5321hafb1f1b_0.conda + sha256: 66aa216a403de0bb0c1340a88d1a06adaff66bae2cfd196731aa24db9859d631 + md5: 44083d2d2c2025afca315c7a172eab2b + depends: + - ncurses + - __osx >=11.0 + - ncurses >=6.5,<7.0a0 + license: BSD-2-Clause + license_family: BSD + purls: [] + size: 107691 + timestamp: 1738479560845 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libev-4.33-h93a5062_2.conda + sha256: 95cecb3902fbe0399c3a7e67a5bed1db813e5ab0e22f4023a5e0f722f2cc214f + md5: 36d33e440c31857372a72137f78bacf5 + license: BSD-2-Clause + license_family: BSD + purls: [] + size: 107458 + timestamp: 1702146414478 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.8.0-hf6b4638_0.conda + sha256: f4b1cafc59afaede8fa0a2d9cf376840f1c553001acd72f6ead18bbc8ac8c49c + md5: 65466e82c09e888ca7560c11a97d5450 + depends: + - __osx >=11.0 + constrains: + - expat 2.8.0.* + license: MIT + license_family: MIT + purls: [] + size: 68789 + timestamp: 1777846180142 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.5.2-hcf2aa1b_0.conda + sha256: 6686a26466a527585e6a75cc2a242bf4a3d97d6d6c86424a441677917f28bec7 + md5: 43c04d9cb46ef176bb2a4c77e324d599 + depends: + - __osx >=11.0 + license: MIT + license_family: MIT + purls: [] + size: 40979 + timestamp: 1769456747661 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.3-h8088a28_0.conda + sha256: 34878d87275c298f1a732c6806349125cebbf340d24c6c23727268184bba051e + md5: b1fd823b5ae54fbec272cea0811bd8a9 + depends: + - __osx >=11.0 + constrains: + - xz 5.8.3.* + license: 0BSD + purls: [] + size: 92472 + timestamp: 1775825802659 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libmpdec-4.0.0-h84a0fba_1.conda + sha256: 1089c7f15d5b62c622625ec6700732ece83be8b705da8c6607f4dabb0c4bd6d2 + md5: 57c4be259f5e0b99a5983799a228ae55 + depends: + - __osx >=11.0 + license: BSD-2-Clause + license_family: BSD + purls: [] + size: 73690 + timestamp: 1769482560514 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libnghttp2-1.68.1-h8f3e76b_0.conda + sha256: 2bc7bc3978066f2c274ebcbf711850cc9ab92e023e433b9631958a098d11e10a + md5: 6ea18834adbc3b33df9bd9fb45eaf95b + depends: + - __osx >=11.0 + - c-ares >=1.34.6,<2.0a0 + - libcxx >=19 + - libev >=4.33,<4.34.0a0 + - libev >=4.33,<5.0a0 + - libzlib >=1.3.1,<2.0a0 + - openssl >=3.5.5,<4.0a0 + license: MIT + license_family: MIT + purls: [] + size: 576526 + timestamp: 1773854624224 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsodium-1.0.21-h1a92334_3.conda + sha256: df603472ea1ebd8e7d4fb71e4360fe48d10b11c240df51c129de1da2ff9e8227 + md5: 7cc5247987e6d115134ebab15186bc13 + depends: + - __osx >=11.0 + license: ISC + purls: [] + size: 248039 + timestamp: 1772479570912 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.53.1-h1b79a29_0.conda + sha256: 49daec7c83e70d4efc17b813547824bc2bcf2f7256d84061d24fbfe537da9f74 + md5: 6681822ea9d362953206352371b6a904 + depends: + - __osx >=11.0 + - libzlib >=1.3.2,<2.0a0 + license: blessing + purls: [] + size: 920047 + timestamp: 1777987051643 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libuv-1.51.0-h6caf38d_1.conda + sha256: 042c7488ad97a5629ec0a991a8b2a3345599401ecc75ad6a5af73b60e6db9689 + md5: c0d87c3c8e075daf1daf6c31b53e8083 + depends: + - __osx >=11.0 + license: MIT + license_family: MIT + purls: [] + size: 421195 + timestamp: 1753948426421 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.2-h8088a28_2.conda + sha256: 361415a698514b19a852f5d1123c5da746d4642139904156ddfca7c922d23a05 + md5: bc5a5721b6439f2f62a84f2548136082 + depends: + - __osx >=11.0 + constrains: + - zlib 1.3.2 *_2 + license: Zlib + license_family: Other + purls: [] + size: 47759 + timestamp: 1774072956767 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/markupsafe-3.0.3-py311hc290fe0_1.conda + sha256: d635f2b1d9e19e8e68c5d33150f7e4f62df08ef2ef0e85977f743e81939afc01 + md5: ff068874356bbc7f9bd2d793f809f44b + depends: + - __osx >=11.0 + - python >=3.11,<3.12.0a0 + - python >=3.11,<3.12.0a0 *_cpython + - python_abi 3.11.* *_cp311 + constrains: + - jinja2 >=3.0.0 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/markupsafe?source=compressed-mapping + size: 26511 + timestamp: 1772445369187 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/markupsafe-3.0.3-py313h65a2061_1.conda + sha256: f62892a42948c61aa0a13d9a36ff811651f0a1102331223594aecf3cc042bece + md5: 0195d558b0c0ab8f4af3089af83067c5 + depends: + - __osx >=11.0 + - python >=3.13,<3.14.0a0 + - python >=3.13,<3.14.0a0 *_cp313 + - python_abi 3.13.* *_cp313 + constrains: + - jinja2 >=3.0.0 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/markupsafe?source=hash-mapping + size: 26009 + timestamp: 1772445537524 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/msgspec-0.21.1-py311hc949640_0.conda + sha256: 55ce08c77b6e87fcc5cca61a516e81fdcc1cd0969e0204fce0a32ca84118a148 + md5: 24707aa446fa7babd806397b8c5349a2 + depends: + - __osx >=11.0 + - python >=3.11,<3.12.0a0 + - python >=3.11,<3.12.0a0 *_cpython + - python_abi 3.11.* *_cp311 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/msgspec?source=hash-mapping + size: 207945 + timestamp: 1776338655947 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/msgspec-0.21.1-py313h0997733_0.conda + sha256: 63cdc0052bd638f1f3df6de438e69275b7b273b4b77dbd1ac34ce25d98cf973c + md5: 2dd8d3b25e88780abc058559bc7975c6 + depends: + - __osx >=11.0 + - python >=3.13,<3.14.0a0 + - python >=3.13,<3.14.0a0 *_cp313 + - python_abi 3.13.* *_cp313 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/msgspec?source=hash-mapping + size: 214497 + timestamp: 1776338358818 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.6-h1d4f5a5_0.conda + sha256: 4ea6c620b87bd1d42bb2ccc2c87cd2483fa2d7f9e905b14c223f11ff3f4c455d + md5: 343d10ed5b44030a2f67193905aea159 + depends: + - __osx >=11.0 + license: X11 AND BSD-3-Clause + purls: [] + size: 805509 + timestamp: 1777423252320 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/nodejs-25.8.2-h7039424_0.conda + sha256: 4782b172b3b8a557b60bf5f591821cf100e2092ba7a5494ce047dfa41626de26 + md5: ca8277c52fdface8bb8ebff7cd9a6f56 + depends: + - libcxx >=19 + - __osx >=11.0 + - icu >=78.3,<79.0a0 + - libbrotlicommon >=1.2.0,<1.3.0a0 + - libbrotlienc >=1.2.0,<1.3.0a0 + - libbrotlidec >=1.2.0,<1.3.0a0 + - libnghttp2 >=1.68.1,<2.0a0 + - libuv >=1.51.0,<2.0a0 + - libsqlite >=3.52.0,<4.0a0 + - libzlib >=1.3.2,<2.0a0 + - openssl >=3.5.5,<4.0a0 + - zstd >=1.5.7,<1.6.0a0 + - c-ares >=1.34.6,<2.0a0 + - libabseil >=20260107.1,<20260108.0a0 + - libabseil * cxx17* + license: MIT + license_family: MIT + purls: [] + size: 17101803 + timestamp: 1774517834028 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.6.2-hd24854e_0.conda + sha256: c91bf510c130a1ea1b6ff023e28bac0ccaef869446acd805e2016f69ebdc49ea + md5: 25dcccd4f80f1638428613e0d7c9b4e1 + depends: + - __osx >=11.0 + - ca-certificates + license: Apache-2.0 + license_family: Apache + purls: [] + size: 3106008 + timestamp: 1775587972483 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/psutil-7.2.2-py311he363849_0.conda + sha256: 2b774e8f4ccaac8783d1908f281e4bb20b7036d6708dc913ee7811b070f5b4b5 + md5: 7cff50265141513c960f00ba586780ea + depends: + - python + - python 3.11.* *_cpython + - __osx >=11.0 + - python_abi 3.11.* *_cp311 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/psutil?source=hash-mapping + size: 245203 + timestamp: 1769678306347 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/psutil-7.2.2-py313h6688731_0.conda + sha256: 1d2a6039fb71d61134b1d6816202529f2f6286c83b59bc1491fd288f5c08046e + md5: ba2d89e51a855963c767648f44c03871 + depends: + - python + - __osx >=11.0 + - python 3.13.* *_cp313 + - python_abi 3.13.* *_cp313 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/psutil?source=hash-mapping + size: 242596 + timestamp: 1769678288893 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pydantic-core-2.46.4-py311hf7c400d_0.conda + sha256: 194e8872dcda301703c8f1427e480fee6421e3901803ffc863b68eb886372eaf + md5: 80d6864b30e0697b652a9bd580b2d351 + depends: + - python + - typing-extensions >=4.6.0,!=4.7.0 + - __osx >=11.0 + - python 3.11.* *_cpython + - python_abi 3.11.* *_cp311 + constrains: + - __osx >=11.0 + license: MIT + license_family: MIT + purls: + - pkg:pypi/pydantic-core?source=hash-mapping + size: 1730463 + timestamp: 1778084304998 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pydantic-core-2.46.4-py313h212e517_0.conda + sha256: 5bcc20f2c21d8a20d8f2821caf31d8c0ef2fa3bcdaf7a9bdce62e37be819f1d7 + md5: 32bb0d4dbb1be2064c06248a1190aa96 + depends: + - python + - typing-extensions >=4.6.0,!=4.7.0 + - python 3.13.* *_cp313 + - __osx >=11.0 + - python_abi 3.13.* *_cp313 + constrains: + - __osx >=11.0 + license: MIT + license_family: MIT + purls: + - pkg:pypi/pydantic-core?source=hash-mapping + size: 1720475 + timestamp: 1778084300413 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pynacl-1.6.2-py311h38a6bc0_1.conda + sha256: 503b3a2543577f2fb70ddc44113a4da7ecdfdfb2bf51ecd1203adcb93e7257df + md5: 64d4dd58b1eb87d7e6ba084c7dc9259b + depends: + - __osx >=11.0 + - cffi >=1.4.1 + - libsodium >=1.0.21,<1.0.22.0a0 + - python >=3.11,<3.12.0a0 + - python >=3.11,<3.12.0a0 *_cpython + - python_abi 3.11.* *_cp311 + - six + license: Apache-2.0 + license_family: Apache + purls: + - pkg:pypi/pynacl?source=hash-mapping + size: 1188895 + timestamp: 1772171487639 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pynacl-1.6.2-py313h6940bce_1.conda + sha256: ef6ab85953ebf81b3b0c6d0b6377eb1a318b07823e76d2c8e850365e60d9ca5c + md5: fca94b9ddea6d5c63ccb6b5ad6d8e232 + depends: + - __osx >=11.0 + - cffi >=1.4.1 + - libsodium >=1.0.21,<1.0.22.0a0 + - python >=3.13,<3.14.0a0 + - python >=3.13,<3.14.0a0 *_cp313 + - python_abi 3.13.* *_cp313 + - six + license: Apache-2.0 + license_family: Apache + purls: + - pkg:pypi/pynacl?source=hash-mapping + size: 1192924 + timestamp: 1772171603602 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyobjc-core-12.1-py311hce6e4fa_0.conda + sha256: f6dced0ea4f220abc1278f6217e22ca1c631f6154dd086b0a7a2866589d6b78c + md5: 812e22c5c7859e719ec225ece0c6f2c1 + depends: + - __osx >=11.0 + - libffi >=3.5.2,<3.6.0a0 + - python >=3.11,<3.12.0a0 + - python >=3.11,<3.12.0a0 *_cpython + - python_abi 3.11.* *_cp311 + - setuptools + license: MIT + license_family: MIT + purls: + - pkg:pypi/pyobjc-core?source=hash-mapping + size: 481434 + timestamp: 1763151508974 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyobjc-core-12.1-py313h40b429f_0.conda + sha256: 307ca29ebf2317bd2561639b1ee0290fd8c03c3450fa302b9f9437d8df6a5280 + md5: 31a0a72f3466682d0ea2ebcbd7d319b8 + depends: + - __osx >=11.0 + - libffi >=3.5.2,<3.6.0a0 + - python >=3.13,<3.14.0a0 + - python >=3.13,<3.14.0a0 *_cp313 + - python_abi 3.13.* *_cp313 + - setuptools + license: MIT + license_family: MIT + purls: + - pkg:pypi/pyobjc-core?source=hash-mapping + size: 481508 + timestamp: 1763152124940 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyobjc-framework-cocoa-12.1-py311h9049b8e_0.conda + sha256: 67e209a5e9ebaf08aa36e2a4b9139ff91a7ecee7c17408d0a3a1ea1dc3a67681 + md5: b215a767b5ef6f16347cacebc409e66b + depends: + - __osx >=11.0 + - libffi >=3.5.2,<3.6.0a0 + - pyobjc-core 12.1.* + - python >=3.11,<3.12.0a0 + - python >=3.11,<3.12.0a0 *_cpython + - python_abi 3.11.* *_cp311 + license: MIT + license_family: MIT + purls: + - pkg:pypi/pyobjc-framework-cocoa?source=hash-mapping + size: 383445 + timestamp: 1763160541362 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyobjc-framework-cocoa-12.1-py313hcc5defa_0.conda + sha256: 194e188d8119befc952d04157079733e2041a7a502d50340ddde632658799fdc + md5: a6d28c8fc266a3d3c3dae183e25c4d31 + depends: + - __osx >=11.0 + - libffi >=3.5.2,<3.6.0a0 + - pyobjc-core 12.1.* + - python >=3.13,<3.14.0a0 + - python >=3.13,<3.14.0a0 *_cp313 + - python_abi 3.13.* *_cp313 + license: MIT + license_family: MIT + purls: + - pkg:pypi/pyobjc-framework-cocoa?source=hash-mapping + size: 376136 + timestamp: 1763160678792 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.11.15-h8561d8f_0_cpython.conda + sha256: 9a846065863925b2562126a5c6fecd7a972e84aaa4de9e686ad3715ca506acfa + md5: 49c7d96c58b969585cf09fb01d74e08e + depends: + - __osx >=11.0 + - bzip2 >=1.0.8,<2.0a0 + - libexpat >=2.7.4,<3.0a0 + - libffi >=3.5.2,<3.6.0a0 + - liblzma >=5.8.2,<6.0a0 + - libsqlite >=3.51.2,<4.0a0 + - libzlib >=1.3.1,<2.0a0 + - ncurses >=6.5,<7.0a0 + - openssl >=3.5.5,<4.0a0 + - readline >=8.3,<9.0a0 + - tk >=8.6.13,<8.7.0a0 + - tzdata + constrains: + - python_abi 3.11.* *_cp311 + license: Python-2.0 + purls: [] + size: 14753109 + timestamp: 1772730203101 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.13.13-h20e6be0_100_cp313.conda + build_number: 100 + sha256: d0fffc5fde21d1ae350da545dfb9e115a8c53bed8a9c5761f9efd4a5581853c1 + md5: 9991a930e81d3873eba7a299ba783ec4 + depends: + - __osx >=11.0 + - bzip2 >=1.0.8,<2.0a0 + - libexpat >=2.7.5,<3.0a0 + - libffi >=3.5.2,<3.6.0a0 + - liblzma >=5.8.2,<6.0a0 + - libmpdec >=4.0.0,<5.0a0 + - libsqlite >=3.52.0,<4.0a0 + - libzlib >=1.3.2,<2.0a0 + - ncurses >=6.5,<7.0a0 + - openssl >=3.5.6,<4.0a0 + - python_abi 3.13.* *_cp313 + - readline >=8.3,<9.0a0 + - tk >=8.6.13,<8.7.0a0 + - tzdata + license: Python-2.0 + purls: [] + size: 12966447 + timestamp: 1775615694085 + python_site_packages_path: lib/python3.13/site-packages +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyyaml-6.0.3-py311hc290fe0_1.conda + sha256: 984e73d7957460689e10533059de8adb38a308853d298900a37acc58edd84cec + md5: e4b908da7cd496b3fa6798c0f60a2a19 + depends: + - __osx >=11.0 + - python >=3.11,<3.12.0a0 + - python >=3.11,<3.12.0a0 *_cpython + - python_abi 3.11.* *_cp311 + - yaml >=0.2.5,<0.3.0a0 + license: MIT + license_family: MIT + purls: + - pkg:pypi/pyyaml?source=hash-mapping + size: 192948 + timestamp: 1770223655988 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyyaml-6.0.3-py313h65a2061_1.conda + sha256: 950725516f67c9691d81bb8dde8419581c5332c5da3da10c9ba8cbb1698b825d + md5: 5d0c8b92128c93027632ca8f8dc1190f + depends: + - __osx >=11.0 + - python >=3.13,<3.14.0a0 + - python >=3.13,<3.14.0a0 *_cp313 + - python_abi 3.13.* *_cp313 + - yaml >=0.2.5,<0.3.0a0 + license: MIT + license_family: MIT + purls: + - pkg:pypi/pyyaml?source=hash-mapping + size: 188763 + timestamp: 1770224094408 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyzmq-27.1.0-py311h745ac33_2.conda + sha256: c5d65e1192241d764a68eb0e95ef3ec4800a7e8584260aa2bdcfad0a2d769609 + md5: 9648524ed194922a084f0c2c2c2ada6a + depends: + - python + - __osx >=11.0 + - python 3.11.* *_cpython + - libcxx >=19 + - zeromq >=4.3.5,<4.4.0a0 + - python_abi 3.11.* *_cp311 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/pyzmq?source=hash-mapping + size: 359869 + timestamp: 1771717160649 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyzmq-27.1.0-py312h022ad19_2.conda + noarch: python + sha256: 2f31f799a46ed75518fae0be75ecc8a1b84360dbfd55096bc2fe8bd9c797e772 + md5: 2f6b79700452ef1e91f45a99ab8ffe5a + depends: + - python + - libcxx >=19 + - __osx >=11.0 + - _python_abi3_support 1.* + - cpython >=3.12 + - zeromq >=4.3.5,<4.4.0a0 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/pyzmq?source=hash-mapping + size: 191641 + timestamp: 1771717073430 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.3-h46df422_0.conda + sha256: a77010528efb4b548ac2a4484eaf7e1c3907f2aec86123ed9c5212ae44502477 + md5: f8381319127120ce51e081dce4865cf4 + depends: + - __osx >=11.0 + - ncurses >=6.5,<7.0a0 + license: GPL-3.0-only + license_family: GPL + purls: [] + size: 313930 + timestamp: 1765813902568 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/rpds-py-0.30.0-py311h71babbd_0.conda + sha256: 15873755f078583cea046f7ca7fc0d5348d1f29a16a30b73bdb53dd62f2ba379 + md5: 4408829b022e8e0d19365c0c00be00c4 + depends: + - python + - python 3.11.* *_cpython + - __osx >=11.0 + - python_abi 3.11.* *_cp311 + constrains: + - __osx >=11.0 + license: MIT + license_family: MIT + purls: + - pkg:pypi/rpds-py?source=hash-mapping + size: 357600 + timestamp: 1764543142990 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/rpds-py-0.30.0-py313h2c089d5_0.conda + sha256: db63344f91e8bfe77703c6764aa9eeafb44d165e286053214722814eabda0264 + md5: 190c2d0d4e98ec97df48cdb74caf44d8 + depends: + - python + - __osx >=11.0 + - python 3.13.* *_cp313 + - python_abi 3.13.* *_cp313 + constrains: + - __osx >=11.0 + license: MIT + license_family: MIT + purls: + - pkg:pypi/rpds-py?source=hash-mapping + size: 358961 + timestamp: 1764543165314 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/ruff-0.15.12-hbd3f8a3_0.conda + noarch: python + sha256: 91992a557456cbe9980e8ffb0f0d1200f65f741adeefc7ecf568d1935690a7bc + md5: 7151a0851207b88c0031f388a7825e9b + depends: + - python + - __osx >=11.0 + constrains: + - __osx >=11.0 + license: MIT + license_family: MIT + purls: + - pkg:pypi/ruff?source=compressed-mapping + size: 8485007 + timestamp: 1778119519471 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h010d191_3.conda + sha256: 799cab4b6cde62f91f750149995d149bc9db525ec12595e8a1d91b9317f038b3 + md5: a9d86bc62f39b94c4661716624eb21b0 + depends: + - __osx >=11.0 + - libzlib >=1.3.1,<2.0a0 + license: TCL + license_family: BSD + purls: [] + size: 3127137 + timestamp: 1769460817696 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/tornado-6.5.5-py311hc949640_0.conda + sha256: 572923958ec987f3f68e228dfec243c89ab94b50398731215d36e2e040db8703 + md5: 6f12d9ff025ab1875dff67ad779ca29d + depends: + - __osx >=11.0 + - python >=3.11,<3.12.0a0 + - python >=3.11,<3.12.0a0 *_cpython + - python_abi 3.11.* *_cp311 + license: Apache-2.0 + license_family: Apache + purls: + - pkg:pypi/tornado?source=hash-mapping + size: 875022 + timestamp: 1774358570256 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/tornado-6.5.5-py313h0997733_0.conda + sha256: c5b0ee042d8a0b88a3823226dc95b794c042c498aee330aa9b4d78bfad01d099 + md5: 303333dd882dfeb303cc8bfac178464b + depends: + - __osx >=11.0 + - python >=3.13,<3.14.0a0 + - python >=3.13,<3.14.0a0 *_cp313 + - python_abi 3.13.* *_cp313 + license: Apache-2.0 + license_family: Apache + purls: + - pkg:pypi/tornado?source=hash-mapping + size: 883472 + timestamp: 1774358832451 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/yaml-0.2.5-h925e9cb_3.conda + sha256: b03433b13d89f5567e828ea9f1a7d5c5d697bf374c28a4168d71e9464f5dafac + md5: 78a0fe9e9c50d2c381e8ee47e3ea437d + depends: + - __osx >=11.0 + license: MIT + license_family: MIT + purls: [] + size: 83386 + timestamp: 1753484079473 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/zeromq-4.3.5-h4818236_10.conda + sha256: 2705360c72d4db8de34291493379ffd13b09fd594d0af20c9eefa8a3f060d868 + md5: e85dcd3bde2b10081cdcaeae15797506 + depends: + - __osx >=11.0 + - libcxx >=19 + - krb5 >=1.22.2,<1.23.0a0 + - libsodium >=1.0.21,<1.0.22.0a0 + license: MPL-2.0 + license_family: MOZILLA + purls: [] + size: 245246 + timestamp: 1772476886668 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hbf9d68e_6.conda + sha256: 9485ba49e8f47d2b597dd399e88f4802e100851b27c21d7525625b0b4025a5d9 + md5: ab136e4c34e97f34fb621d2592a393d8 + depends: + - __osx >=11.0 + - libzlib >=1.3.1,<2.0a0 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 433413 + timestamp: 1764777166076 +- conda: https://conda.anaconda.org/conda-forge/win-64/argon2-cffi-bindings-25.1.0-py311h3485c13_2.conda + sha256: 787e9a5f0a5624fee2af4f58c823bd7933b8ca560e794dd9a821e990b31d2d49 + md5: 5fde0926a521a37d454a5e3162cb6e51 + depends: + - cffi >=1.0.1 + - python >=3.11,<3.12.0a0 + - python_abi 3.11.* *_cp311 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: MIT + license_family: MIT + purls: + - pkg:pypi/argon2-cffi-bindings?source=hash-mapping + size: 38502 + timestamp: 1762509668838 +- conda: https://conda.anaconda.org/conda-forge/win-64/argon2-cffi-bindings-25.1.0-py313h5ea7bf4_2.conda + sha256: 3f8a1affdfeb2be5289d709e365fc6e386d734773895215cf8cbc5100fa6af9a + md5: eabb4b677b54874d7d6ab775fdaa3d27 + depends: + - cffi >=1.0.1 + - python >=3.13,<3.14.0a0 + - python_abi 3.13.* *_cp313 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: MIT + license_family: MIT + purls: + - pkg:pypi/argon2-cffi-bindings?source=hash-mapping + size: 38779 + timestamp: 1762509796090 +- conda: https://conda.anaconda.org/conda-forge/win-64/backports.zstd-1.4.0-py311h71c1bcc_0.conda + sha256: f551f6d7f1a62be18e4165772d300d21e0bd925956bcd9c2a605cabf9a2835d0 + md5: a0b8a2723108680aa6bb85fe3e413e76 + depends: + - python + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - zstd >=1.5.7,<1.6.0a0 + - python_abi 3.11.* *_cp311 + license: BSD-3-Clause AND MIT AND EPL-2.0 + purls: + - pkg:pypi/backports-zstd?source=hash-mapping + size: 243538 + timestamp: 1777848744191 +- conda: https://conda.anaconda.org/conda-forge/win-64/backports.zstd-1.4.0-py313h2a31948_0.conda + sha256: b9c1465c03cb6ff79fb4f2feb21955b9b89783f868c900cad45dbcb8d81ab429 + md5: ef16917e1231df5c6f2ba245c8fe35e4 + depends: + - python + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - zstd >=1.5.7,<1.6.0a0 + - python_abi 3.13.* *_cp313 + license: BSD-3-Clause AND MIT AND EPL-2.0 + purls: + - pkg:pypi/backports-zstd?source=compressed-mapping + size: 240632 + timestamp: 1777848740331 +- conda: https://conda.anaconda.org/conda-forge/win-64/bcrypt-5.0.0-py311hf51aa87_1.conda + sha256: b4cc5ee88f5d19381923519b1f6a1bbc73752f5b1528e4a1df0a3a802e497d13 + md5: fd25556009eae0faab9fc6286150d477 + depends: + - python + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - python_abi 3.11.* *_cp311 + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/bcrypt?source=hash-mapping + size: 170642 + timestamp: 1762497737769 +- conda: https://conda.anaconda.org/conda-forge/win-64/bcrypt-5.0.0-py313hfbe8231_1.conda + sha256: 21baf1316a8160fd3b83e7128803735b107f47c58ba5d5305a372fd6c679d42b + md5: 2a664b5cc93fbc9d7ca595b206a299f0 + depends: + - python + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - python_abi 3.13.* *_cp313 + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/bcrypt?source=hash-mapping + size: 170702 + timestamp: 1762497739995 +- conda: https://conda.anaconda.org/conda-forge/win-64/brotli-python-1.2.0-py311hc5da9e4_1.conda + sha256: 1803c838946d79ef6485ae8c7dafc93e28722c5999b059a34118ef758387a4c9 + md5: b0c459f98ac5ea504a9d9df6242f7ee1 + depends: + - python >=3.11,<3.12.0a0 + - python_abi 3.11.* *_cp311 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + constrains: + - libbrotlicommon 1.2.0 hfd05255_1 + license: MIT + license_family: MIT + purls: + - pkg:pypi/brotli?source=hash-mapping + size: 335333 + timestamp: 1764018370925 +- conda: https://conda.anaconda.org/conda-forge/win-64/brotli-python-1.2.0-py313h3ebfc14_1.conda + sha256: 3558006cd6e836de8dff53cbe5f0b9959f96ea6a6776b4e14f1c524916dd956c + md5: 916a39a0261621b8c33e9db2366dd427 + depends: + - python >=3.13,<3.14.0a0 + - python_abi 3.13.* *_cp313 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + constrains: + - libbrotlicommon 1.2.0 hfd05255_1 + license: MIT + license_family: MIT + purls: + - pkg:pypi/brotli?source=hash-mapping + size: 335605 + timestamp: 1764018132514 +- conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h0ad9c76_9.conda + sha256: 76dfb71df5e8d1c4eded2dbb5ba15bb8fb2e2b0fe42d94145d5eed4c75c35902 + md5: 4cb8e6b48f67de0b018719cdf1136306 + depends: + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: bzip2-1.0.6 + license_family: BSD + purls: [] + size: 56115 + timestamp: 1771350256444 +- conda: https://conda.anaconda.org/conda-forge/win-64/cffi-2.0.0-py311h3485c13_1.conda + sha256: c9caca6098e3d92b1a269159b759d757518f2c477fbbb5949cb9fee28807c1f1 + md5: f02335db0282d5077df5bc84684f7ff9 + depends: + - pycparser + - python >=3.11,<3.12.0a0 + - python_abi 3.11.* *_cp311 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: MIT + license_family: MIT + purls: + - pkg:pypi/cffi?source=hash-mapping + size: 297941 + timestamp: 1761203850323 +- conda: https://conda.anaconda.org/conda-forge/win-64/cffi-2.0.0-py313h5ea7bf4_1.conda + sha256: f867a11f42bb64a09b232e3decf10f8a8fe5194d7e3a216c6bac9f40483bd1c6 + md5: 55b44664f66a2caf584d72196aa98af9 + depends: + - pycparser + - python >=3.13,<3.14.0a0 + - python_abi 3.13.* *_cp313 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: MIT + license_family: MIT + purls: + - pkg:pypi/cffi?source=hash-mapping + size: 292681 + timestamp: 1761203203673 +- conda: https://conda.anaconda.org/conda-forge/win-64/cryptography-48.0.0-py311h2098ed6_0.conda + sha256: a6fca5c54f019724d08fc3d9e54c60ca8c0c8aa8b54571fec3e713bbb64a55c3 + md5: 27def055167d830e4ce6c6821eaee591 + depends: + - cffi >=2.0 + - openssl >=3.5.6,<4.0a0 + - python >=3.11,<3.12.0a0 + - python_abi 3.11.* *_cp311 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: Apache-2.0 AND BSD-3-Clause AND PSF-2.0 AND MIT + license_family: BSD + purls: + - pkg:pypi/cryptography?source=hash-mapping + size: 1645132 + timestamp: 1777966325193 +- conda: https://conda.anaconda.org/conda-forge/win-64/cryptography-48.0.0-py313hf5c5e30_0.conda + sha256: 1fbf66094059379d72f06d3cbb9df5277352a95d25a68d55d1dd60fa6acca5c6 + md5: c216777102e0b49ed307e181df3903d2 + depends: + - cffi >=2.0 + - openssl >=3.5.6,<4.0a0 + - python >=3.13,<3.14.0a0 + - python_abi 3.13.* *_cp313 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: Apache-2.0 AND BSD-3-Clause AND PSF-2.0 AND MIT + license_family: BSD + purls: + - pkg:pypi/cryptography?source=hash-mapping + size: 1646298 + timestamp: 1777966340031 +- conda: https://conda.anaconda.org/conda-forge/win-64/debugpy-1.8.20-py311h5dfdfe8_0.conda + sha256: 661e5c582b1f853a46a78d4bb6e55f2bfdac66e68d015e111f1580a11c28abbf + md5: 683be2cd10e80a367790b3083ce529b7 + depends: + - python + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - python_abi 3.11.* *_cp311 + license: MIT + license_family: MIT + purls: + - pkg:pypi/debugpy?source=hash-mapping + size: 3940002 + timestamp: 1769745017274 +- conda: https://conda.anaconda.org/conda-forge/win-64/debugpy-1.8.20-py313h927ade5_0.conda + sha256: f6fe9cbd9e3d3c09f173eeb43676a58bc6169e97da0d190e0201e40828a3a62b + md5: 75eb3091b05924429a3a8d2a9bbdfac2 + depends: + - python + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - python_abi 3.13.* *_cp313 + license: MIT + license_family: MIT + purls: + - pkg:pypi/debugpy?source=hash-mapping + size: 4003589 + timestamp: 1769745018248 +- conda: https://conda.anaconda.org/conda-forge/win-64/krb5-1.22.2-h0ea6238_0.conda + sha256: eb60f1ad8b597bcf95dee11bc11fe71a8325bc1204cf51d2bb1f2120ffd77761 + md5: 4432f52dc0c8eb6a7a6abc00a037d93c + depends: + - openssl >=3.5.5,<4.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: MIT + license_family: MIT + purls: [] + size: 751055 + timestamp: 1769769688841 +- conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.8.0-hac47afa_0.conda + sha256: 2d81d647c1f01108803457cac999b947456f44dd0a3c2325395677feacaeca67 + md5: 264e350e035092b5135a2147c238aec4 + depends: + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + constrains: + - expat 2.8.0.* + license: MIT + license_family: MIT + purls: [] + size: 71094 + timestamp: 1777846223617 +- conda: https://conda.anaconda.org/conda-forge/win-64/libffi-3.5.2-h3d046cb_0.conda + sha256: 59d01f2dfa8b77491b5888a5ab88ff4e1574c9359f7e229da254cdfe27ddc190 + md5: 720b39f5ec0610457b725eb3f396219a + depends: + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: MIT + license_family: MIT + purls: [] + size: 45831 + timestamp: 1769456418774 +- conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-5.8.3-hfd05255_0.conda + sha256: d636d1a25234063642f9c531a7bb58d84c1c496411280a36ea000bd122f078f1 + md5: 8f83619ab1588b98dd99c90b0bfc5c6d + depends: + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + constrains: + - xz 5.8.3.* + license: 0BSD + purls: [] + size: 106486 + timestamp: 1775825663227 +- conda: https://conda.anaconda.org/conda-forge/win-64/libmpdec-4.0.0-hfd05255_1.conda + sha256: 40dcd0b9522a6e0af72a9db0ced619176e7cfdb114855c7a64f278e73f8a7514 + md5: e4a9fc2bba3b022dad998c78856afe47 + depends: + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: BSD-2-Clause + license_family: BSD + purls: [] + size: 89411 + timestamp: 1769482314283 +- conda: https://conda.anaconda.org/conda-forge/win-64/libsodium-1.0.21-h6a83c73_3.conda + sha256: d915f4fa8ebbf237c7a6e511ed458f2cfdc7c76843a924740318a15d0dd33d6d + md5: da2aa614d16a795b3007b6f4a1318a81 + depends: + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + license: ISC + purls: [] + size: 276860 + timestamp: 1772479407566 +- conda: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.53.1-hf5d6505_0.conda + sha256: e70562450332ca8954bc16f3455468cca5ef3695c7d7187ecc87f8fc3c70e9eb + md5: 7fea434a17c323256acc510a041b80d7 + depends: + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: blessing + purls: [] + size: 1304178 + timestamp: 1777986510497 +- conda: https://conda.anaconda.org/conda-forge/win-64/libzlib-1.3.2-hfd05255_2.conda + sha256: 88609816e0cc7452bac637aaf65783e5edf4fee8a9f8e22bdc3a75882c536061 + md5: dbabbd6234dea34040e631f87676292f + depends: + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + constrains: + - zlib 1.3.2 *_2 + license: Zlib + license_family: Other + purls: [] + size: 58347 + timestamp: 1774072851498 +- conda: https://conda.anaconda.org/conda-forge/win-64/markupsafe-3.0.3-py311h3f79411_1.conda + sha256: 3d37fb1900e31131f84549560e7a4bfea5f39aa3ecd73345fef1f33975cf0baa + md5: f55de41c947bdd2ff9bbeffedf8089f7 + depends: + - python >=3.11,<3.12.0a0 + - python_abi 3.11.* *_cp311 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + constrains: + - jinja2 >=3.0.0 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/markupsafe?source=hash-mapping + size: 29362 + timestamp: 1772445178723 +- conda: https://conda.anaconda.org/conda-forge/win-64/markupsafe-3.0.3-py313hd650c13_1.conda + sha256: 9dc626b6c00bc2dbd2494df689876ff675b93d92636ba5df8e37b99040a1f6bc + md5: 5cc690ddf943700e0ef50a265df31f03 + depends: + - python >=3.13,<3.14.0a0 + - python_abi 3.13.* *_cp313 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + constrains: + - jinja2 >=3.0.0 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/markupsafe?source=hash-mapping + size: 28992 + timestamp: 1772445161959 +- conda: https://conda.anaconda.org/conda-forge/win-64/msgspec-0.21.1-py311h3485c13_0.conda + sha256: d096ad02a47a61aeeade2ad3639cc300adf950790cca097717040d46ca5bff52 + md5: 6e6fd9a9012358254bebee33eb3d7f71 + depends: + - python >=3.11,<3.12.0a0 + - python_abi 3.11.* *_cp311 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/msgspec?source=hash-mapping + size: 199272 + timestamp: 1776337711230 +- conda: https://conda.anaconda.org/conda-forge/win-64/msgspec-0.21.1-py313h5ea7bf4_0.conda + sha256: 4e70a7b9bee6b9dadd1f038da6fcf1627cc53581d1cc01355b66d638ae55c0e3 + md5: 96575971ffc81497b83bf321f0b9e945 + depends: + - python >=3.13,<3.14.0a0 + - python_abi 3.13.* *_cp313 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/msgspec?source=hash-mapping + size: 201017 + timestamp: 1776337591817 +- conda: https://conda.anaconda.org/conda-forge/win-64/nodejs-25.8.2-h80d1838_0.conda + sha256: 5e38e51da1aa4bc352db9b4cec1c3e25811de0f4408edaa24e009a64de6dbfdf + md5: e626ee7934e4b7cb21ce6b721cff8677 + license: MIT + license_family: MIT + purls: [] + size: 31271315 + timestamp: 1774517904472 +- conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.6.2-hf411b9b_0.conda + sha256: feb5815125c60f2be4a411e532db1ed1cd2d7261a6a43c54cb6ae90724e2e154 + md5: 05c7d624cff49dbd8db1ad5ba537a8a3 + depends: + - ca-certificates + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: Apache-2.0 + license_family: Apache + purls: [] + size: 9410183 + timestamp: 1775589779763 +- conda: https://conda.anaconda.org/conda-forge/win-64/psutil-7.2.2-py311hf893f09_0.conda + sha256: 32da17824abadd1f5b46faedfa4964c7b1817b11887c2e8bb4e48628da51b93a + md5: fd968cdacc7967efd0ff5ef1805b812c + depends: + - python + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - python_abi 3.11.* *_cp311 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/psutil?source=hash-mapping + size: 249478 + timestamp: 1769678166841 +- conda: https://conda.anaconda.org/conda-forge/win-64/psutil-7.2.2-py313h5fd188c_0.conda + sha256: 3ec3373748f83069bef93b540de416e637ee30231b222d5df8f712e93f2f9195 + md5: 761b299a6289c77459defea3563f8fc0 + depends: + - python + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - python_abi 3.13.* *_cp313 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/psutil?source=hash-mapping + size: 246062 + timestamp: 1769678176886 +- conda: https://conda.anaconda.org/conda-forge/win-64/pydantic-core-2.46.4-py311hf51aa87_0.conda + sha256: 4cf860418a6a1a678bcc23bc29a83f4f0916682fd6ab1ff209b4e5ee3dc5e15f + md5: 85efdb748cbec9210e718c4a8860749c + depends: + - python + - typing-extensions >=4.6.0,!=4.7.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - python_abi 3.11.* *_cp311 + license: MIT + license_family: MIT + purls: + - pkg:pypi/pydantic-core?source=hash-mapping + size: 1898998 + timestamp: 1778084255268 +- conda: https://conda.anaconda.org/conda-forge/win-64/pydantic-core-2.46.4-py313hfbe8231_0.conda + sha256: 6466b7e441adb71e29308de2a87c9787d5d0100601ede2d02ed4f85c251699d6 + md5: 9981bc46be6341306a5473b290b2f366 + depends: + - python + - typing-extensions >=4.6.0,!=4.7.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - python_abi 3.13.* *_cp313 + license: MIT + license_family: MIT + purls: + - pkg:pypi/pydantic-core?source=hash-mapping + size: 1888029 + timestamp: 1778084253466 +- conda: https://conda.anaconda.org/conda-forge/win-64/pynacl-1.6.2-py311hcd7b28f_1.conda + sha256: cd35ad681895b45f578bac8fd8dafc87116c2db42489dda9bef76ff3518bbc73 + md5: e2197089006b3cda7e03c7a51fa7050e + depends: + - cffi >=1.4.1 + - libsodium >=1.0.21,<1.0.22.0a0 + - python >=3.11,<3.12.0a0 + - python_abi 3.11.* *_cp311 + - six + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: Apache-2.0 + license_family: Apache + purls: + - pkg:pypi/pynacl?source=hash-mapping + size: 1147862 + timestamp: 1772171327674 +- conda: https://conda.anaconda.org/conda-forge/win-64/pynacl-1.6.2-py313hed4ef92_1.conda + sha256: 0960e727d0bd70e07bca0c049c4c4afa0b41bcea050135db49f54c1c0c02ed7f + md5: 99b7617a643bed6a403fc908c1e167f0 + depends: + - cffi >=1.4.1 + - libsodium >=1.0.21,<1.0.22.0a0 + - python >=3.13,<3.14.0a0 + - python_abi 3.13.* *_cp313 + - six + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: Apache-2.0 + license_family: Apache + purls: + - pkg:pypi/pynacl?source=hash-mapping + size: 1186552 + timestamp: 1772171311072 +- conda: https://conda.anaconda.org/conda-forge/win-64/python-3.11.15-h0159041_0_cpython.conda + sha256: a1f1031088ce69bc99c82b95980c1f54e16cbd5c21f042e9c1ea25745a8fc813 + md5: d09dbf470b41bca48cbe6a78ba1e009b + depends: + - bzip2 >=1.0.8,<2.0a0 + - libexpat >=2.7.4,<3.0a0 + - libffi >=3.5.2,<3.6.0a0 + - liblzma >=5.8.2,<6.0a0 + - libsqlite >=3.51.2,<4.0a0 + - libzlib >=1.3.1,<2.0a0 + - openssl >=3.5.5,<4.0a0 + - tk >=8.6.13,<8.7.0a0 + - tzdata + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + constrains: + - python_abi 3.11.* *_cp311 + license: Python-2.0 + purls: [] + size: 18416208 + timestamp: 1772728847666 +- conda: https://conda.anaconda.org/conda-forge/win-64/python-3.13.13-h09917c8_100_cp313.conda + build_number: 100 + sha256: b8108d7f83f71fb15fbb4a263406c2065a8990b3d7eba2cbd7a3075b9a6392ba + md5: 7065f7067762c4c2bda1912f18d16239 + depends: + - bzip2 >=1.0.8,<2.0a0 + - libexpat >=2.7.5,<3.0a0 + - libffi >=3.5.2,<3.6.0a0 + - liblzma >=5.8.2,<6.0a0 + - libmpdec >=4.0.0,<5.0a0 + - libsqlite >=3.52.0,<4.0a0 + - libzlib >=1.3.2,<2.0a0 + - openssl >=3.5.6,<4.0a0 + - python_abi 3.13.* *_cp313 + - tk >=8.6.13,<8.7.0a0 + - tzdata + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: Python-2.0 + purls: [] + size: 16618694 + timestamp: 1775613654892 + python_site_packages_path: Lib/site-packages +- conda: https://conda.anaconda.org/conda-forge/win-64/pywin32-311-py311hefeebc8_1.conda + sha256: e3ef7e0cc53111ab81b8a9dd3eabc1374d7420d4c9fce3c8631e73310203ad55 + md5: c1cfe9f5d8e278cc4d2d4c7b0126634d + depends: + - python + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - python_abi 3.11.* *_cp311 + license: PSF-2.0 + license_family: PSF + purls: + - pkg:pypi/pywin32?source=hash-mapping + size: 6729388 + timestamp: 1756487145061 +- conda: https://conda.anaconda.org/conda-forge/win-64/pywin32-311-py313h40c08fc_1.conda + sha256: 87eaeb79b5961e0f216aa840bc35d5f0b9b123acffaecc4fda4de48891901f20 + md5: 1ce4f826332dca56c76a5b0cc89fb19e + depends: + - python + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - python_abi 3.13.* *_cp313 + license: PSF-2.0 + license_family: PSF + purls: + - pkg:pypi/pywin32?source=hash-mapping + size: 6695114 + timestamp: 1756487139550 +- conda: https://conda.anaconda.org/conda-forge/win-64/pywinpty-2.0.15-py311hda3d55a_1.conda + sha256: b1f6b3a907e36f7af486faf3892f47fab42993c13c934cc19855bbae227f2b18 + md5: e5dd9afed138ff193d4593f1b15a388b + depends: + - python >=3.11,<3.12.0a0 + - python_abi 3.11.* *_cp311 + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + - winpty + license: MIT + license_family: MIT + purls: + - pkg:pypi/pywinpty?source=hash-mapping + size: 215911 + timestamp: 1759557817579 +- conda: https://conda.anaconda.org/conda-forge/win-64/pywinpty-2.0.15-py313h5813708_1.conda + sha256: d34a7cd0a4a7dc79662cb6005e01d630245d9a942e359eb4d94b2fb464ed2552 + md5: 8f01ed27e2baa455e753301218e054fd + depends: + - python >=3.13,<3.14.0a0 + - python_abi 3.13.* *_cp313 + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + - winpty + license: MIT + license_family: MIT + purls: + - pkg:pypi/pywinpty?source=hash-mapping + size: 216075 + timestamp: 1759556799508 +- conda: https://conda.anaconda.org/conda-forge/win-64/pyyaml-6.0.3-py311h3f79411_1.conda + sha256: 301c3ba100d25cd5ae37895988ee3ab986210d4d972aa58efed948fbe857773d + md5: a0153c033dc55203e11d1cac8f6a9cf2 + depends: + - python >=3.11,<3.12.0a0 + - python_abi 3.11.* *_cp311 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - yaml >=0.2.5,<0.3.0a0 + license: MIT + license_family: MIT + purls: + - pkg:pypi/pyyaml?source=hash-mapping + size: 187108 + timestamp: 1770223467913 +- conda: https://conda.anaconda.org/conda-forge/win-64/pyyaml-6.0.3-py313hd650c13_1.conda + sha256: dfaed50de8ee72a51096163b87631921688851001e38c78a841eba1ae8b35889 + md5: c1bdb8dd255c79fb9c428ad25cc6ee54 + depends: + - python >=3.13,<3.14.0a0 + - python_abi 3.13.* *_cp313 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - yaml >=0.2.5,<0.3.0a0 + license: MIT + license_family: MIT + purls: + - pkg:pypi/pyyaml?source=hash-mapping + size: 180992 + timestamp: 1770223457761 +- conda: https://conda.anaconda.org/conda-forge/win-64/pyzmq-27.1.0-py311hff25285_2.conda + sha256: d37e831618e8df0506d32e16820adab550b74b36d039da753f7543bb967b700d + md5: 0ad824e928af5d6200a97649d810ab2b + depends: + - python + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - python_abi 3.11.* *_cp311 + - zeromq >=4.3.5,<4.3.6.0a0 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/pyzmq?source=hash-mapping + size: 363314 + timestamp: 1771716977369 +- conda: https://conda.anaconda.org/conda-forge/win-64/pyzmq-27.1.0-py312h343a6d4_2.conda + noarch: python + sha256: d84bcc19a945ca03d1fd794be3e9896ab6afc9f691d58d9c2da514abe584d4df + md5: eb1ec67a70b4d479f7dd76e6c8fe7575 + depends: + - python + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - zeromq >=4.3.5,<4.3.6.0a0 + - _python_abi3_support 1.* + - cpython >=3.12 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/pyzmq?source=hash-mapping + size: 183235 + timestamp: 1771716967192 +- conda: https://conda.anaconda.org/conda-forge/win-64/rpds-py-0.30.0-py311hf51aa87_0.conda + sha256: 6edeab1412def450e72f0e96a5d8bb31a2a0b4e56624699c916d3bafd4d9b475 + md5: 43ab63451a9df29f2c499da524665de9 + depends: + - python + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - python_abi 3.11.* *_cp311 + license: MIT + license_family: MIT + purls: + - pkg:pypi/rpds-py?source=hash-mapping + size: 241288 + timestamp: 1764543026991 +- conda: https://conda.anaconda.org/conda-forge/win-64/rpds-py-0.30.0-py313hfbe8231_0.conda + sha256: 27bd383787c0df7a0a926b11014fd692d60d557398dcf1d50c55aa2378507114 + md5: 58ae648b12cfa6df3923b5fd219931cb + depends: + - python + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - python_abi 3.13.* *_cp313 + license: MIT + license_family: MIT + purls: + - pkg:pypi/rpds-py?source=hash-mapping + size: 243419 + timestamp: 1764543047271 +- conda: https://conda.anaconda.org/conda-forge/win-64/ruff-0.15.12-hd7ccaa8_0.conda + noarch: python + sha256: 9b47c39449fd0102315bb62bd632707a754d32cf6c6f08d2a9c74b0a476f2282 + md5: 2db0d3419f3e6fc05516da561f81cda5 + depends: + - python + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + license: MIT + license_family: MIT + purls: + - pkg:pypi/ruff?source=compressed-mapping + size: 9719930 + timestamp: 1778119435755 +- conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h6ed50ae_3.conda + sha256: 0e79810fae28f3b69fe7391b0d43f5474d6bd91d451d5f2bde02f55ae481d5e3 + md5: 0481bfd9814bf525bd4b3ee4b51494c4 + depends: + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: TCL + license_family: BSD + purls: [] + size: 3526350 + timestamp: 1769460339384 +- conda: https://conda.anaconda.org/conda-forge/win-64/tornado-6.5.5-py311h3485c13_0.conda + sha256: 71f58fe09f7729ad82c8eb942e775ddeffcef454483911d19d1041ac5d81eec3 + md5: b004afcc680af88cb877978e71d42667 + depends: + - python >=3.11,<3.12.0a0 + - python_abi 3.11.* *_cp311 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: Apache-2.0 + license_family: Apache + purls: + - pkg:pypi/tornado?source=hash-mapping + size: 878544 + timestamp: 1774358187588 +- conda: https://conda.anaconda.org/conda-forge/win-64/tornado-6.5.5-py313h5ea7bf4_0.conda + sha256: b97fab804ab457cf4157103289317e3619e801a77410e756bb35c6223418cc6e + md5: 7d53f0d25ad5fd7d6962ce4eb385fb07 + depends: + - python >=3.13,<3.14.0a0 + - python_abi 3.13.* *_cp313 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: Apache-2.0 + license_family: Apache + purls: + - pkg:pypi/tornado?source=hash-mapping + size: 883689 + timestamp: 1774358224157 +- conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.26100.0-h57928b3_0.conda + sha256: 3005729dce6f3d3f5ec91dfc49fc75a0095f9cd23bab49efb899657297ac91a5 + md5: 71b24316859acd00bdb8b38f5e2ce328 + constrains: + - vc14_runtime >=14.29.30037 + - vs2015_runtime >=14.29.30037 + license: LicenseRef-MicrosoftWindowsSDK10 + purls: [] + size: 694692 + timestamp: 1756385147981 +- conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-h41ae7f8_34.conda + sha256: 9dc40c2610a6e6727d635c62cced5ef30b7b30123f5ef67d6139e23d21744b3a + md5: 1e610f2416b6acdd231c5f573d754a0f + depends: + - vc14_runtime >=14.44.35208 + track_features: + - vc14 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 19356 + timestamp: 1767320221521 +- conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.44.35208-h818238b_34.conda + sha256: 02732f953292cce179de9b633e74928037fa3741eb5ef91c3f8bae4f761d32a5 + md5: 37eb311485d2d8b2c419449582046a42 + depends: + - ucrt >=10.0.20348.0 + - vcomp14 14.44.35208 h818238b_34 + constrains: + - vs2015_runtime 14.44.35208.* *_34 + license: LicenseRef-MicrosoftVisualCpp2015-2022Runtime + license_family: Proprietary + purls: [] + size: 683233 + timestamp: 1767320219644 +- conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.44.35208-h818238b_34.conda + sha256: 878d5d10318b119bd98ed3ed874bd467acbe21996e1d81597a1dbf8030ea0ce6 + md5: 242d9f25d2ae60c76b38a5e42858e51d + depends: + - ucrt >=10.0.20348.0 + constrains: + - vs2015_runtime 14.44.35208.* *_34 + license: LicenseRef-MicrosoftVisualCpp2015-2022Runtime + license_family: Proprietary + purls: [] + size: 115235 + timestamp: 1767320173250 +- conda: https://conda.anaconda.org/conda-forge/win-64/winpty-0.4.3-4.tar.bz2 + sha256: 9df10c5b607dd30e05ba08cbd940009305c75db242476f4e845ea06008b0a283 + md5: 1cee351bf20b830d991dbe0bc8cd7dfe + license: MIT + license_family: MIT + purls: [] + size: 1176306 +- conda: https://conda.anaconda.org/conda-forge/win-64/yaml-0.2.5-h6a83c73_3.conda + sha256: 80ee68c1e7683a35295232ea79bcc87279d31ffeda04a1665efdb43cbd50a309 + md5: 433699cba6602098ae8957a323da2664 + depends: + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + license: MIT + license_family: MIT + purls: [] + size: 63944 + timestamp: 1753484092156 +- conda: https://conda.anaconda.org/conda-forge/win-64/zeromq-4.3.5-h507cc87_10.conda + sha256: b8568dfde46edf3455458912ea6ffb760e4456db8230a0cf34ecbc557d3c275f + md5: 1ab0237036bfb14e923d6107473b0021 + depends: + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - libsodium >=1.0.21,<1.0.22.0a0 + - krb5 >=1.22.2,<1.23.0a0 + license: MPL-2.0 + license_family: MOZILLA + purls: [] + size: 265665 + timestamp: 1772476832995 +- conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_6.conda + sha256: 368d8628424966fd8f9c8018326a9c779e06913dd39e646cf331226acc90e5b2 + md5: 053b84beec00b71ea8ff7a4f84b55207 + depends: + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - libzlib >=1.3.1,<2.0a0 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 388453 + timestamp: 1764777142545 +- pypi: ./ + name: easyreflectometry + requires_dist: + - bumps + - easyscience + - orsopy + - plotly + - pooch + - refl1d>=1.0.0 + - refnx + - scipp + - svglib<1.6 ; sys_platform == 'darwin' or sys_platform == 'linux' + - xhtml2pdf + - arviz ; extra == 'dev' + - build ; extra == 'dev' + - copier ; extra == 'dev' + - docstripy ; extra == 'dev' + - format-docstring ; extra == 'dev' + - gitpython ; extra == 'dev' + - interrogate ; extra == 'dev' + - jinja2 ; extra == 'dev' + - jupyterquiz ; extra == 'dev' + - jupytext ; extra == 'dev' + - mike ; extra == 'dev' + - mkdocs ; extra == 'dev' + - mkdocs-autorefs ; extra == 'dev' + - mkdocs-jupyter ; extra == 'dev' + - mkdocs-markdownextradata-plugin ; extra == 'dev' + - mkdocs-material ; extra == 'dev' + - mkdocs-plugin-inline-svg ; extra == 'dev' + - mkdocstrings-python ; extra == 'dev' + - nbmake ; extra == 'dev' + - nbqa ; extra == 'dev' + - nbstripout ; extra == 'dev' + - plopp ; extra == 'dev' + - plotly ; extra == 'dev' + - pre-commit ; extra == 'dev' + - pydoclint ; extra == 'dev' + - pytest ; extra == 'dev' + - pytest-cov ; extra == 'dev' + - pytest-xdist ; extra == 'dev' + - pyyaml ; extra == 'dev' + - radon ; extra == 'dev' + - ruff ; extra == 'dev' + - spdx-headers ; extra == 'dev' + - validate-pyproject[all] ; extra == 'dev' + - versioningit ; extra == 'dev' + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/00/bb/90ba423612b6aa0adccc6b1874bcd4a9b44b660c0c16f346611e00f64ac3/backrefs-7.0-py313-none-any.whl + name: backrefs + version: '7.0' + sha256: f2c52955d631b9e1ac4cd56209f0a3a946d592b98e7790e77699339ae01c102a + requires_dist: + - regex ; extra == 'extras' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/01/7c/fa07d3da2b6253eb8474be16eab2eadf670460e364ccc895ca7ff388ee30/oscrypto-1.3.0-py2.py3-none-any.whl + name: oscrypto + version: 1.3.0 + sha256: 2b2f1d2d42ec152ca90ccb5682f3e051fb55986e1b170ebde472b133713e7085 + requires_dist: + - asn1crypto>=1.5.1 +- pypi: https://files.pythonhosted.org/packages/02/05/d60c732b56da5085175c07c74b2df4e6d181b0c9a61e1691474f06ef4b39/lxml-6.1.0-cp311-cp311-win_amd64.whl + name: lxml + version: 6.1.0 + sha256: 183bfb45a493081943be7ea2b5adfc2b611e1cf377cefa8b8a8be404f45ef9a7 + requires_dist: + - cssselect>=0.7 ; extra == 'cssselect' + - html5lib ; extra == 'html5' + - beautifulsoup4 ; extra == 'htmlsoup' + - lxml-html-clean ; extra == 'html-clean' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl + name: pyyaml-env-tag + version: '1.1' + sha256: 17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04 + requires_dist: + - pyyaml + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/06/8a/5e156e31ba656ce93c1cc895dd8f051ec351cb382940dca655aaec475005/python_bidi-0.6.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + name: python-bidi + version: 0.6.9 + sha256: 29e40e02bf2bddec225730eb3ba9f4ca948dced9877358458b74d7a447d936ac + requires_dist: + - nox ; extra == 'dev' + - pytest ; extra == 'dev' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/07/c7/deb8c5e604404dbf10a3808a858946ca3547692ff6316b698945bb72177e/python_socketio-5.16.1-py3-none-any.whl + name: python-socketio + version: 5.16.1 + sha256: a3eb1702e92aa2f2b5d3ba00261b61f062cce51f1cfb6900bf3ab4d1934d2d35 + requires_dist: + - bidict>=0.21.0 + - python-engineio>=4.11.0 + - requests>=2.21.0 ; extra == 'client' + - websocket-client>=0.54.0 ; extra == 'client' + - aiohttp>=3.4 ; extra == 'asyncio-client' + - tox ; extra == 'dev' + - sphinx ; extra == 'docs' + - furo ; extra == 'docs' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/08/03/69347590f1cf4a6d5a4944bb6099e6d37f334784f16062234e1f892fdb1d/lxml-6.1.0-cp313-cp313-macosx_10_13_universal2.whl + name: lxml + version: 6.1.0 + sha256: a0092f2b107b69601adf562a57c956fbb596e05e3e6651cabd3054113b007e45 + requires_dist: + - cssselect>=0.7 ; extra == 'cssselect' + - html5lib ; extra == 'html5' + - beautifulsoup4 ; extra == 'htmlsoup' + - lxml-html-clean ; extra == 'html-clean' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/09/7d/af933f0f6e0767995b4e2d705a0665e454d1c19402aa7e895de3951ebb04/scipy-1.17.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + name: scipy + version: 1.17.1 + sha256: 43af8d1f3bea642559019edfe64e9b11192a8978efbd1539d7bc2aaa23d92de4 + requires_dist: + - numpy>=1.26.4,<2.7 + - pytest>=8.0.0 ; extra == 'test' + - pytest-cov ; extra == 'test' + - pytest-timeout ; extra == 'test' + - pytest-xdist ; extra == 'test' + - asv ; extra == 'test' + - mpmath ; extra == 'test' + - gmpy2 ; extra == 'test' + - threadpoolctl ; extra == 'test' + - scikit-umfpack ; extra == 'test' + - pooch ; extra == 'test' + - hypothesis>=6.30 ; extra == 'test' + - array-api-strict>=2.3.1 ; extra == 'test' + - cython ; extra == 'test' + - meson ; extra == 'test' + - ninja ; sys_platform != 'emscripten' and extra == 'test' + - sphinx>=5.0.0,<8.2.0 ; extra == 'doc' + - intersphinx-registry ; extra == 'doc' + - pydata-sphinx-theme>=0.15.2 ; extra == 'doc' + - sphinx-copybutton ; extra == 'doc' + - sphinx-design>=0.4.0 ; extra == 'doc' + - matplotlib>=3.5 ; extra == 'doc' + - numpydoc ; extra == 'doc' + - jupytext ; extra == 'doc' + - myst-nb>=1.2.0 ; extra == 'doc' + - pooch ; extra == 'doc' + - jupyterlite-sphinx>=0.19.1 ; extra == 'doc' + - jupyterlite-pyodide-kernel ; extra == 'doc' + - linkify-it-py ; extra == 'doc' + - tabulate ; extra == 'doc' + - click<8.3.0 ; extra == 'dev' + - spin ; extra == 'dev' + - mypy==1.10.0 ; extra == 'dev' + - typing-extensions ; extra == 'dev' + - types-psutil ; extra == 'dev' + - pycodestyle ; extra == 'dev' + - ruff>=0.12.0 ; extra == 'dev' + - cython-lint>=0.12.2 ; extra == 'dev' + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/0a/aa/510dc933d87767584abfe03efa445889996c70c2990f6f87c3ebaa0a18c5/kiwisolver-1.5.0-cp311-cp311-macosx_11_0_arm64.whl + name: kiwisolver + version: 1.5.0 + sha256: 0df54df7e686afa55e6f21fb86195224a6d9beb71d637e8d7920c95cf0f89aac + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/0a/f5/603d0d6a02cfd4c8f2a095a54672b3cf967ad688a60fb9faf04fc4887f65/frozenlist-1.8.0-cp311-cp311-win_amd64.whl + name: frozenlist + version: 1.8.0 + sha256: ac913f8403b36a2c8610bbfd25b8013488533e71e62b4b4adce9c86c8cea905b + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/0b/77/dbc82ff2fb0e63c6564356682bf201edff0ba16c98630d21a1fb312a8182/pandas-3.0.2-cp313-cp313-macosx_11_0_arm64.whl + name: pandas + version: 3.0.2 + sha256: d606a041c89c0a474a4702d532ab7e73a14fe35c8d427b972a625c8e46373668 + requires_dist: + - numpy>=1.26.0 ; python_full_version < '3.14' + - numpy>=2.3.3 ; python_full_version >= '3.14' + - python-dateutil>=2.8.2 + - tzdata ; sys_platform == 'win32' + - tzdata ; sys_platform == 'emscripten' + - hypothesis>=6.116.0 ; extra == 'test' + - pytest>=8.3.4 ; extra == 'test' + - pytest-xdist>=3.6.1 ; extra == 'test' + - pyarrow>=13.0.0 ; extra == 'pyarrow' + - bottleneck>=1.4.2 ; extra == 'performance' + - numba>=0.60.0 ; extra == 'performance' + - numexpr>=2.10.2 ; extra == 'performance' + - scipy>=1.14.1 ; extra == 'computation' + - xarray>=2024.10.0 ; extra == 'computation' + - fsspec>=2024.10.0 ; extra == 'fss' + - s3fs>=2024.10.0 ; extra == 'aws' + - gcsfs>=2024.10.0 ; extra == 'gcp' + - odfpy>=1.4.1 ; extra == 'excel' + - openpyxl>=3.1.5 ; extra == 'excel' + - python-calamine>=0.3.0 ; extra == 'excel' + - pyxlsb>=1.0.10 ; extra == 'excel' + - xlrd>=2.0.1 ; extra == 'excel' + - xlsxwriter>=3.2.0 ; extra == 'excel' + - pyarrow>=13.0.0 ; extra == 'parquet' + - pyarrow>=13.0.0 ; extra == 'feather' + - pyiceberg>=0.8.1 ; extra == 'iceberg' + - tables>=3.10.1 ; extra == 'hdf5' + - pyreadstat>=1.2.8 ; extra == 'spss' + - sqlalchemy>=2.0.36 ; extra == 'postgresql' + - psycopg2>=2.9.10 ; extra == 'postgresql' + - adbc-driver-postgresql>=1.2.0 ; extra == 'postgresql' + - sqlalchemy>=2.0.36 ; extra == 'mysql' + - pymysql>=1.1.1 ; extra == 'mysql' + - sqlalchemy>=2.0.36 ; extra == 'sql-other' + - adbc-driver-postgresql>=1.2.0 ; extra == 'sql-other' + - adbc-driver-sqlite>=1.2.0 ; extra == 'sql-other' + - beautifulsoup4>=4.12.3 ; extra == 'html' + - html5lib>=1.1 ; extra == 'html' + - lxml>=5.3.0 ; extra == 'html' + - lxml>=5.3.0 ; extra == 'xml' + - matplotlib>=3.9.3 ; extra == 'plot' + - jinja2>=3.1.5 ; extra == 'output-formatting' + - tabulate>=0.9.0 ; extra == 'output-formatting' + - pyqt5>=5.15.9 ; extra == 'clipboard' + - qtpy>=2.4.2 ; extra == 'clipboard' + - zstandard>=0.23.0 ; extra == 'compression' + - pytz>=2024.2 ; extra == 'timezone' + - adbc-driver-postgresql>=1.2.0 ; extra == 'all' + - adbc-driver-sqlite>=1.2.0 ; extra == 'all' + - beautifulsoup4>=4.12.3 ; extra == 'all' + - bottleneck>=1.4.2 ; extra == 'all' + - fastparquet>=2024.11.0 ; extra == 'all' + - fsspec>=2024.10.0 ; extra == 'all' + - gcsfs>=2024.10.0 ; extra == 'all' + - html5lib>=1.1 ; extra == 'all' + - hypothesis>=6.116.0 ; extra == 'all' + - jinja2>=3.1.5 ; extra == 'all' + - lxml>=5.3.0 ; extra == 'all' + - matplotlib>=3.9.3 ; extra == 'all' + - numba>=0.60.0 ; extra == 'all' + - numexpr>=2.10.2 ; extra == 'all' + - odfpy>=1.4.1 ; extra == 'all' + - openpyxl>=3.1.5 ; extra == 'all' + - psycopg2>=2.9.10 ; extra == 'all' + - pyarrow>=13.0.0 ; extra == 'all' + - pyiceberg>=0.8.1 ; extra == 'all' + - pymysql>=1.1.1 ; extra == 'all' + - pyqt5>=5.15.9 ; extra == 'all' + - pyreadstat>=1.2.8 ; extra == 'all' + - pytest>=8.3.4 ; extra == 'all' + - pytest-xdist>=3.6.1 ; extra == 'all' + - python-calamine>=0.3.0 ; extra == 'all' + - pytz>=2024.2 ; extra == 'all' + - pyxlsb>=1.0.10 ; extra == 'all' + - qtpy>=2.4.2 ; extra == 'all' + - scipy>=1.14.1 ; extra == 'all' + - s3fs>=2024.10.0 ; extra == 'all' + - sqlalchemy>=2.0.36 ; extra == 'all' + - tables>=3.10.1 ; extra == 'all' + - tabulate>=0.9.0 ; extra == 'all' + - xarray>=2024.10.0 ; extra == 'all' + - xlrd>=2.0.1 ; extra == 'all' + - xlsxwriter>=3.2.0 ; extra == 'all' + - zstandard>=0.23.0 ; extra == 'all' + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl + name: frozenlist + version: 1.8.0 + sha256: f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/0c/c9/44fb661c55062f0818a6ffd2685c67aa30816200d5f2817543717d4b92eb/coverage-7.13.5-cp313-cp313-macosx_11_0_arm64.whl + name: coverage + version: 7.13.5 + sha256: 941617e518602e2d64942c88ec8499f7fbd49d3f6c4327d3a71d43a1973032f3 + requires_dist: + - tomli ; python_full_version <= '3.11' and extra == 'toml' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/0c/d6/1d5c60cc17bbdf37c1552d9c03862fc6d32c5836732a0415b2d637edc2d0/pypdf-6.10.2-py3-none-any.whl + name: pypdf + version: 6.10.2 + sha256: aa53be9826655b51c96741e5d7983ca224d898ac0a77896e64636810517624aa + requires_dist: + - typing-extensions>=4.0 ; python_full_version < '3.11' + - cryptography ; extra == 'crypto' + - pycryptodome ; extra == 'cryptodome' + - flit ; extra == 'dev' + - pip-tools ; extra == 'dev' + - pre-commit ; extra == 'dev' + - pytest-cov ; extra == 'dev' + - pytest-socket ; extra == 'dev' + - pytest-timeout ; extra == 'dev' + - pytest-xdist ; extra == 'dev' + - wheel ; extra == 'dev' + - myst-parser ; extra == 'docs' + - sphinx ; extra == 'docs' + - sphinx-rtd-theme ; extra == 'docs' + - cryptography ; extra == 'full' + - pillow>=8.0.0 ; extra == 'full' + - pillow>=8.0.0 ; extra == 'image' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/0d/1f/d398de1612f7a611e22d743280339c9af4903675635e41be3370091c704b/arviz_stats-1.1.0-py3-none-any.whl + name: arviz-stats + version: 1.1.0 + sha256: ed47334ccff8670a0b90a50e1a37e7257268084eb3436e6b7b15e623f1001947 + requires_dist: + - numpy>=2 + - scipy>=1.13 + - sphinx-book-theme ; extra == 'doc' + - myst-parser[linkify] ; extra == 'doc' + - myst-nb ; extra == 'doc' + - sphinx-copybutton ; extra == 'doc' + - numpydoc ; extra == 'doc' + - sphinx<9 ; extra == 'doc' + - sphinx-design ; extra == 'doc' + - jupyter-sphinx ; extra == 'doc' + - h5netcdf[h5py] ; extra == 'doc' + - sphinx-autosummary-accessors ; extra == 'doc' + - numba ; extra == 'numba' + - xarray-einstats[einops,numba] ; extra == 'numba' + - pytest ; extra == 'test' + - pytest-cov ; extra == 'test' + - pytest ; extra == 'test-xarray' + - pytest-cov ; extra == 'test-xarray' + - h5netcdf[h5py] ; extra == 'test-xarray' + - arviz-base>=1.1,<1.2 ; extra == 'xarray' + - xarray-einstats ; extra == 'xarray' + - xarray>=2024.11.0 ; extra == 'xarray' + requires_python: '>=3.12' +- pypi: https://files.pythonhosted.org/packages/0d/44/c4b0b6095fef4dc9c420e041799591e3b63e9619e3044f7f4f6c21c0ab24/contourpy-1.3.3-cp311-cp311-macosx_11_0_arm64.whl + name: contourpy + version: 1.3.3 + sha256: 23416f38bfd74d5d28ab8429cc4d63fa67d5068bd711a85edb1c3fb0c3e2f381 + requires_dist: + - numpy>=1.25 + - furo ; extra == 'docs' + - sphinx>=7.2 ; extra == 'docs' + - sphinx-copybutton ; extra == 'docs' + - bokeh ; extra == 'bokeh' + - selenium ; extra == 'bokeh' + - contourpy[bokeh,docs] ; extra == 'mypy' + - bokeh ; extra == 'mypy' + - docutils-stubs ; extra == 'mypy' + - mypy==1.17.0 ; extra == 'mypy' + - types-pillow ; extra == 'mypy' + - contourpy[test-no-images] ; extra == 'test' + - matplotlib ; extra == 'test' + - pillow ; extra == 'test' + - pytest ; extra == 'test-no-images' + - pytest-cov ; extra == 'test-no-images' + - pytest-rerunfailures ; extra == 'test-no-images' + - pytest-xdist ; extra == 'test-no-images' + - wurlitzer ; extra == 'test-no-images' + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/0d/fe/6bea5c9162869c5beba5d9c8abbed835ec85bf1ec1fba05a3822325c45f3/build-1.5.0-py3-none-any.whl + name: build + version: 1.5.0 + sha256: 13f3eecb844759ab66efec90ca17639bbf14dc06cb2fdf37a9010322d9c50a6f + requires_dist: + - packaging>=24.0 + - pyproject-hooks + - colorama ; os_name == 'nt' + - importlib-metadata>=4.6 ; python_full_version < '3.10.2' + - tomli>=1.1.0 ; python_full_version < '3.11' + - keyring ; extra == 'keyring' + - uv>=0.1.18 ; extra == 'uv' + - virtualenv>=20.17 ; python_full_version >= '3.10' and python_full_version < '3.14' and extra == 'virtualenv' + - virtualenv>=20.31 ; python_full_version >= '3.14' and extra == 'virtualenv' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl + name: aiohappyeyeballs + version: 2.6.1 + sha256: f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl + name: pyparsing + version: 3.3.2 + sha256: 850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d + requires_dist: + - railroad-diagrams ; extra == 'diagrams' + - jinja2 ; extra == 'diagrams' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl + name: blinker + version: 1.9.0 + sha256: ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/11/8c/c9138d881c79aa0ea9ed83cbd58d5ca75624378b38cee225dcf5c42cc91f/griffelib-2.0.2-py3-none-any.whl + name: griffelib + version: 2.0.2 + sha256: 925c857658fb1ba40c0772c37acbc2ab650bd794d9c1b9726922e36ea4117ea1 + requires_dist: + - pip>=24.0 ; extra == 'pypi' + - platformdirs>=4.2 ; extra == 'pypi' + - wheel>=0.42 ; extra == 'pypi' + requires_python: '>=3.10' +- 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 + name: frozenlist + version: 1.8.0 + sha256: 2552f44204b744fba866e573be4c1f9048d6a324dfe14475103fd51613eb1d1f + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/12/aa/fb2a0649fdeef5ab7072d221e8f4df164098792c813af6c87e2581cfa860/mpltoolbox-26.2.0-py3-none-any.whl + name: mpltoolbox + version: 26.2.0 + sha256: cd2668db4216fc4d7c2ba37974961aa61445f1517527b645b6082930e35ba7f0 + requires_dist: + - matplotlib + - ipympl ; extra == 'test' + - pytest>=8.0 ; extra == 'test' + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/12/c5/cbb1ffefb20a93d3f0e1fdcda699fb84976210d411b008f97f48bf6ce27e/pandas-3.0.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl + name: pandas + version: 3.0.2 + sha256: 5d3cfe227c725b1f3dff4278b43d8c784656a42a9325b63af6b1492a8232209e + requires_dist: + - numpy>=1.26.0 ; python_full_version < '3.14' + - numpy>=2.3.3 ; python_full_version >= '3.14' + - python-dateutil>=2.8.2 + - tzdata ; sys_platform == 'win32' + - tzdata ; sys_platform == 'emscripten' + - hypothesis>=6.116.0 ; extra == 'test' + - pytest>=8.3.4 ; extra == 'test' + - pytest-xdist>=3.6.1 ; extra == 'test' + - pyarrow>=13.0.0 ; extra == 'pyarrow' + - bottleneck>=1.4.2 ; extra == 'performance' + - numba>=0.60.0 ; extra == 'performance' + - numexpr>=2.10.2 ; extra == 'performance' + - scipy>=1.14.1 ; extra == 'computation' + - xarray>=2024.10.0 ; extra == 'computation' + - fsspec>=2024.10.0 ; extra == 'fss' + - s3fs>=2024.10.0 ; extra == 'aws' + - gcsfs>=2024.10.0 ; extra == 'gcp' + - odfpy>=1.4.1 ; extra == 'excel' + - openpyxl>=3.1.5 ; extra == 'excel' + - python-calamine>=0.3.0 ; extra == 'excel' + - pyxlsb>=1.0.10 ; extra == 'excel' + - xlrd>=2.0.1 ; extra == 'excel' + - xlsxwriter>=3.2.0 ; extra == 'excel' + - pyarrow>=13.0.0 ; extra == 'parquet' + - pyarrow>=13.0.0 ; extra == 'feather' + - pyiceberg>=0.8.1 ; extra == 'iceberg' + - tables>=3.10.1 ; extra == 'hdf5' + - pyreadstat>=1.2.8 ; extra == 'spss' + - sqlalchemy>=2.0.36 ; extra == 'postgresql' + - psycopg2>=2.9.10 ; extra == 'postgresql' + - adbc-driver-postgresql>=1.2.0 ; extra == 'postgresql' + - sqlalchemy>=2.0.36 ; extra == 'mysql' + - pymysql>=1.1.1 ; extra == 'mysql' + - sqlalchemy>=2.0.36 ; extra == 'sql-other' + - adbc-driver-postgresql>=1.2.0 ; extra == 'sql-other' + - adbc-driver-sqlite>=1.2.0 ; extra == 'sql-other' + - beautifulsoup4>=4.12.3 ; extra == 'html' + - html5lib>=1.1 ; extra == 'html' + - lxml>=5.3.0 ; extra == 'html' + - lxml>=5.3.0 ; extra == 'xml' + - matplotlib>=3.9.3 ; extra == 'plot' + - jinja2>=3.1.5 ; extra == 'output-formatting' + - tabulate>=0.9.0 ; extra == 'output-formatting' + - pyqt5>=5.15.9 ; extra == 'clipboard' + - qtpy>=2.4.2 ; extra == 'clipboard' + - zstandard>=0.23.0 ; extra == 'compression' + - pytz>=2024.2 ; extra == 'timezone' + - adbc-driver-postgresql>=1.2.0 ; extra == 'all' + - adbc-driver-sqlite>=1.2.0 ; extra == 'all' + - beautifulsoup4>=4.12.3 ; extra == 'all' + - bottleneck>=1.4.2 ; extra == 'all' + - fastparquet>=2024.11.0 ; extra == 'all' + - fsspec>=2024.10.0 ; extra == 'all' + - gcsfs>=2024.10.0 ; extra == 'all' + - html5lib>=1.1 ; extra == 'all' + - hypothesis>=6.116.0 ; extra == 'all' + - jinja2>=3.1.5 ; extra == 'all' + - lxml>=5.3.0 ; extra == 'all' + - matplotlib>=3.9.3 ; extra == 'all' + - numba>=0.60.0 ; extra == 'all' + - numexpr>=2.10.2 ; extra == 'all' + - odfpy>=1.4.1 ; extra == 'all' + - openpyxl>=3.1.5 ; extra == 'all' + - psycopg2>=2.9.10 ; extra == 'all' + - pyarrow>=13.0.0 ; extra == 'all' + - pyiceberg>=0.8.1 ; extra == 'all' + - pymysql>=1.1.1 ; extra == 'all' + - pyqt5>=5.15.9 ; extra == 'all' + - pyreadstat>=1.2.8 ; extra == 'all' + - pytest>=8.3.4 ; extra == 'all' + - pytest-xdist>=3.6.1 ; extra == 'all' + - python-calamine>=0.3.0 ; extra == 'all' + - pytz>=2024.2 ; extra == 'all' + - pyxlsb>=1.0.10 ; extra == 'all' + - qtpy>=2.4.2 ; extra == 'all' + - scipy>=1.14.1 ; extra == 'all' + - s3fs>=2024.10.0 ; extra == 'all' + - sqlalchemy>=2.0.36 ; extra == 'all' + - tables>=3.10.1 ; extra == 'all' + - tabulate>=0.9.0 ; extra == 'all' + - xarray>=2024.10.0 ; extra == 'all' + - xlrd>=2.0.1 ; extra == 'all' + - xlsxwriter>=3.2.0 ; extra == 'all' + - zstandard>=0.23.0 ; extra == 'all' + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/12/c9/6869a1dcf4aaf309b9543ec070be3ec3adebee7c9bec9af8c230494134b9/interrogate-1.7.0-py3-none-any.whl + name: interrogate + version: 1.7.0 + sha256: b13ff4dd8403369670e2efe684066de9fcb868ad9d7f2b4095d8112142dc9d12 + requires_dist: + - attrs + - click>=7.1 + - colorama + - py + - tabulate + - tomli ; python_full_version < '3.11' + - cairosvg ; extra == 'dev' + - sphinx ; extra == 'dev' + - sphinx-autobuild ; extra == 'dev' + - pytest ; extra == 'dev' + - pytest-cov ; extra == 'dev' + - pytest-mock ; extra == 'dev' + - coverage[toml] ; extra == 'dev' + - wheel ; extra == 'dev' + - pre-commit ; extra == 'dev' + - sphinx ; extra == 'docs' + - sphinx-autobuild ; extra == 'docs' + - cairosvg ; extra == 'png' + - pytest ; extra == 'tests' + - pytest-cov ; extra == 'tests' + - pytest-mock ; extra == 'tests' + - coverage[toml] ; extra == 'tests' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/13/95/cf3f7fe4910cf0365fa8ea0c731f4b8a624d97cd76ea777913ac8d0868e2/mkdocs_jupyter-0.26.3-py3-none-any.whl + name: mkdocs-jupyter + version: 0.26.3 + sha256: cd6644fb578131157194d750fd4d10fc2fd8f1e84e00036ee62df3b5b4b84c82 + requires_dist: + - ipykernel>6.0.0,<8 + - jupytext>1.13.8,<2 + - mkdocs-material>9.0.0 + - mkdocs>=1.4.0,<2 + - nbconvert>=7.2.9,<8 + - pygments>2.12.0 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/14/4b/d3c79495dee4831b8bebca2790e72cb90f0c5849c940570a7c7e5b70b952/chardet-7.4.3-cp311-cp311-macosx_11_0_arm64.whl + name: chardet + version: 7.4.3 + sha256: 7005c88da26fd95d8abb8acbe6281d833e9a9181b03cf49b4546c4555389bd97 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/15/fe/b426215b6843ee70285521e2dd52f08a32096fe1d68e5b92fbdaffdf8ca3/diffpy_pdffit2-1.6.0-cp313-cp313-win_amd64.whl + name: diffpy-pdffit2 + version: 1.6.0 + sha256: f1bba407eac8ff8ef1264ad5a1d3dff840e2d0a5f71e9fb0c0b9b06d5a8eb139 + requires_dist: + - diffpy-structure + requires_python: '>=3.12,<3.15' +- pypi: https://files.pythonhosted.org/packages/16/56/a31e8d3c9e8d21100b83bbe1c1f3f7c94db317393a229e193461e5e6d2a4/spglib-2.6.0-cp313-cp313-win_amd64.whl + name: spglib + version: 2.6.0 + sha256: ff1632524f6ac0031423474e48d6b69f4932ecb7eb4446a501f59619e2b5cbc9 + requires_dist: + - numpy>=1.20,<3 + - importlib-resources ; python_full_version < '3.10' + - typing-extensions>=4.9.0 ; python_full_version < '3.13' + - pytest ; extra == 'test' + - pyyaml ; extra == 'test' + - sphinx>=7.0 ; extra == 'docs' + - sphinxcontrib-bibtex>=2.5 ; extra == 'docs' + - sphinx-book-theme ; extra == 'docs' + - sphinx-autodoc-typehints ; extra == 'docs' + - myst-parser>=2.0 ; extra == 'docs' + - linkify-it-py ; extra == 'docs' + - sphinx-tippy ; extra == 'docs' + - spglib[test] ; extra == 'test-cov' + - pytest-cov ; extra == 'test-cov' + - spglib[test] ; extra == 'test-benchmark' + - pytest-benchmark ; extra == 'test-benchmark' + - spglib[test] ; extra == 'dev' + - pre-commit ; extra == 'dev' + - spglib[docs] ; extra == 'doc' + - spglib[test] ; extra == 'testing' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/16/5a/736dd2f4535dbf3bf26523f9158c011389ef88dd06ec2eef67fd744f1c7b/jupytext-1.19.1-py3-none-any.whl + name: jupytext + version: 1.19.1 + sha256: d8975035155d034bdfde5c0c37891425314b7ea8d3a6c4b5d18c294348714cd9 + requires_dist: + - markdown-it-py>=1.0 + - mdit-py-plugins + - nbformat + - packaging + - pyyaml + - tomli ; python_full_version < '3.11' + - autopep8 ; extra == 'dev' + - black ; extra == 'dev' + - flake8 ; extra == 'dev' + - gitpython ; extra == 'dev' + - ipykernel ; extra == 'dev' + - isort ; extra == 'dev' + - jupyter-fs[fs]>=1.0 ; extra == 'dev' + - jupyter-server!=2.11 ; extra == 'dev' + - marimo>=0.17.6,<=0.19.4 ; extra == 'dev' + - nbconvert ; extra == 'dev' + - pre-commit ; extra == 'dev' + - pytest ; extra == 'dev' + - pytest-asyncio ; extra == 'dev' + - pytest-cov>=2.6.1 ; extra == 'dev' + - pytest-randomly ; extra == 'dev' + - pytest-xdist ; extra == 'dev' + - sphinx ; extra == 'dev' + - sphinx-gallery>=0.8 ; extra == 'dev' + - myst-parser ; extra == 'docs' + - sphinx ; extra == 'docs' + - sphinx-copybutton ; extra == 'docs' + - sphinx-rtd-theme ; extra == 'docs' + - pytest ; extra == 'test' + - pytest-asyncio ; extra == 'test' + - pytest-randomly ; extra == 'test' + - pytest-xdist ; extra == 'test' + - black ; extra == 'test-cov' + - ipykernel ; extra == 'test-cov' + - jupyter-server!=2.11 ; extra == 'test-cov' + - nbconvert ; extra == 'test-cov' + - pytest ; extra == 'test-cov' + - pytest-asyncio ; extra == 'test-cov' + - pytest-cov>=2.6.1 ; extra == 'test-cov' + - pytest-randomly ; extra == 'test-cov' + - pytest-xdist ; extra == 'test-cov' + - autopep8 ; extra == 'test-external' + - black ; extra == 'test-external' + - flake8 ; extra == 'test-external' + - gitpython ; extra == 'test-external' + - ipykernel ; extra == 'test-external' + - isort ; extra == 'test-external' + - jupyter-fs[fs]>=1.0 ; extra == 'test-external' + - jupyter-server!=2.11 ; extra == 'test-external' + - marimo>=0.17.6,<=0.19.4 ; extra == 'test-external' + - nbconvert ; extra == 'test-external' + - pre-commit ; extra == 'test-external' + - pytest ; extra == 'test-external' + - pytest-asyncio ; extra == 'test-external' + - pytest-randomly ; extra == 'test-external' + - pytest-xdist ; extra == 'test-external' + - sphinx ; extra == 'test-external' + - sphinx-gallery>=0.8 ; extra == 'test-external' + - black ; extra == 'test-functional' + - pytest ; extra == 'test-functional' + - pytest-asyncio ; extra == 'test-functional' + - pytest-randomly ; extra == 'test-functional' + - pytest-xdist ; extra == 'test-functional' + - black ; extra == 'test-integration' + - ipykernel ; extra == 'test-integration' + - jupyter-server!=2.11 ; extra == 'test-integration' + - nbconvert ; extra == 'test-integration' + - pytest ; extra == 'test-integration' + - pytest-asyncio ; extra == 'test-integration' + - pytest-randomly ; extra == 'test-integration' + - pytest-xdist ; extra == 'test-integration' + - bash-kernel ; extra == 'test-ui' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/17/3a/7215b1b7d6d49dc9a87211be44562077f5f04f9bb5a59552c1c8e2d98173/sqlalchemy-2.0.49-cp313-cp313-win_amd64.whl + name: sqlalchemy + version: 2.0.49 + sha256: 12b04d1db2663b421fe072d638a138460a51d5a862403295671c4f3987fb9148 + requires_dist: + - importlib-metadata ; python_full_version < '3.8' + - greenlet>=1 ; platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64' + - typing-extensions>=4.6.0 + - greenlet>=1 ; extra == 'asyncio' + - mypy>=0.910 ; extra == 'mypy' + - pyodbc ; extra == 'mssql' + - pymssql ; extra == 'mssql-pymssql' + - pyodbc ; extra == 'mssql-pyodbc' + - mysqlclient>=1.4.0 ; extra == 'mysql' + - mysql-connector-python ; extra == 'mysql-connector' + - mariadb>=1.0.1,!=1.1.2,!=1.1.5,!=1.1.10 ; extra == 'mariadb-connector' + - cx-oracle>=8 ; extra == 'oracle' + - oracledb>=1.0.1 ; extra == 'oracle-oracledb' + - psycopg2>=2.7 ; extra == 'postgresql' + - pg8000>=1.29.1 ; extra == 'postgresql-pg8000' + - greenlet>=1 ; extra == 'postgresql-asyncpg' + - asyncpg ; extra == 'postgresql-asyncpg' + - psycopg2-binary ; extra == 'postgresql-psycopg2binary' + - psycopg2cffi ; extra == 'postgresql-psycopg2cffi' + - psycopg>=3.0.7 ; extra == 'postgresql-psycopg' + - psycopg[binary]>=3.0.7 ; extra == 'postgresql-psycopgbinary' + - pymysql ; extra == 'pymysql' + - greenlet>=1 ; extra == 'aiomysql' + - aiomysql>=0.2.0 ; extra == 'aiomysql' + - greenlet>=1 ; extra == 'aioodbc' + - aioodbc ; extra == 'aioodbc' + - greenlet>=1 ; extra == 'asyncmy' + - asyncmy>=0.2.3,!=0.2.4,!=0.2.6 ; extra == 'asyncmy' + - greenlet>=1 ; extra == 'aiosqlite' + - aiosqlite ; extra == 'aiosqlite' + - typing-extensions!=3.10.0.1 ; extra == 'aiosqlite' + - sqlcipher3-binary ; extra == 'sqlcipher' + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/17/8b/ed2f0f49385c3d7739cd4699954add26e8f09a372a0c3f04f2bde32fcea2/xarray_einstats-0.9.1-py3-none-any.whl + name: xarray-einstats + version: 0.9.1 + sha256: 777339524e85d066f2ef9ed1e3a3fb63aead4c1065fd1406f30dfa4de58ce063 + requires_dist: + - numpy>=1.25 + - scipy>=1.11 + - xarray>=2023.6.0 + - furo ; extra == 'doc' + - myst-parser[linkify] ; extra == 'doc' + - myst-nb ; extra == 'doc' + - sphinx-copybutton ; extra == 'doc' + - numpydoc ; extra == 'doc' + - sphinx>=5 ; extra == 'doc' + - jupyter-sphinx ; extra == 'doc' + - sphinx-design ; extra == 'doc' + - watermark ; extra == 'doc' + - matplotlib ; extra == 'doc' + - sphinx-togglebutton ; extra == 'doc' + - einops ; extra == 'einops' + - numba>=0.55 ; extra == 'numba' + - hypothesis ; extra == 'test' + - pytest ; extra == 'test' + - pytest-cov ; extra == 'test' + - packaging ; extra == 'test' + - scipy>=1.15 ; extra == 'test' + - preliz>=0.19 ; extra == 'test' + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/18/0b/0098c214843213759692cc638fce7de5c289200a830e5035d1791d7a2338/contourpy-1.3.3-cp313-cp313-win_amd64.whl + name: contourpy + version: 1.3.3 + sha256: 1cadd8b8969f060ba45ed7c1b714fe69185812ab43bd6b86a9123fe8f99c3263 + requires_dist: + - numpy>=1.25 + - furo ; extra == 'docs' + - sphinx>=7.2 ; extra == 'docs' + - sphinx-copybutton ; extra == 'docs' + - bokeh ; extra == 'bokeh' + - selenium ; extra == 'bokeh' + - contourpy[bokeh,docs] ; extra == 'mypy' + - bokeh ; extra == 'mypy' + - docutils-stubs ; extra == 'mypy' + - mypy==1.17.0 ; extra == 'mypy' + - types-pillow ; extra == 'mypy' + - contourpy[test-no-images] ; extra == 'test' + - matplotlib ; extra == 'test' + - pillow ; extra == 'test' + - pytest ; extra == 'test-no-images' + - pytest-cov ; extra == 'test-no-images' + - pytest-rerunfailures ; extra == 'test-no-images' + - pytest-xdist ; extra == 'test-no-images' + - wurlitzer ; extra == 'test-no-images' + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/19/d4/225027a913621a879b429a043674aa35220e6ce67785acad4f7bd0c4ff33/xarray_einstats-0.10.0-py3-none-any.whl + name: xarray-einstats + version: 0.10.0 + sha256: fa3169b46cee29092db820d8bbc203148bada4fc970ee75e62cbf3dd7c5a8945 + requires_dist: + - numpy>=2.0 + - scipy>=1.13 + - xarray>=2024.2.0 + - furo ; extra == 'doc' + - myst-parser[linkify] ; extra == 'doc' + - myst-nb ; extra == 'doc' + - sphinx-copybutton ; extra == 'doc' + - numpydoc ; extra == 'doc' + - sphinx>=5 ; extra == 'doc' + - jupyter-sphinx ; extra == 'doc' + - sphinx-design ; extra == 'doc' + - watermark ; extra == 'doc' + - matplotlib ; extra == 'doc' + - sphinx-togglebutton ; extra == 'doc' + - einops ; extra == 'einops' + - numba>=0.55 ; extra == 'numba' + - hypothesis ; extra == 'test' + - pytest ; extra == 'test' + - pytest-cov ; extra == 'test' + - packaging ; extra == 'test' + - scipy>=1.15 ; extra == 'test' + - preliz>=0.19 ; extra == 'test' + requires_python: '>=3.12' +- pypi: https://files.pythonhosted.org/packages/1b/b3/5d0e77ea774bd3224321c248880ea0c0379000ac5c2bb6d77609549de247/chardet-7.4.3-cp313-cp313-win_amd64.whl + name: chardet + version: 7.4.3 + sha256: e1b98790c284ff813f18f7cf7de5f05ea2435a080030c7f1a8318f3a4f80b131 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/1c/13/bc43591a54dd38ac5c19e7e849f0311d879737a0d07e032e5be79849a5fb/reportlab-4.5.0-py3-none-any.whl + name: reportlab + version: 4.5.0 + sha256: b8cc8996947d84e805368b47b2376070966f091d029351a0d8a1f238984c2c7f + requires_dist: + - pillow>=9.0.0 + - charset-normalizer + - rl-accel>=0.9.0,<1.1 ; extra == 'accel' + - rl-renderpm>=4.0.3,<4.1 ; extra == 'renderpm' + - rlpycairo>=0.2.0,<1 ; extra == 'pycairo' + - freetype-py>=2.3.0,<2.4 ; extra == 'pycairo' + - rlbidi ; extra == 'bidi' + - uharfbuzz ; extra == 'shaping' + requires_python: '>=3.9,<4' +- pypi: https://files.pythonhosted.org/packages/1c/59/964ecb8008722d27d8a835baea81f56a91cea8e097b3be992bc6ccde6367/versioningit-3.3.0-py3-none-any.whl + name: versioningit + version: 3.3.0 + sha256: 23b1db3c4756cded9bd6b0ddec6643c261e3d0c471707da3e0b230b81ce53e4b + requires_dist: + - importlib-metadata>=3.6 ; python_full_version < '3.10' + - packaging>=17.1 + - tomli>=1.2,<3.0 ; python_full_version < '3.11' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl + name: dill + version: 0.4.1 + sha256: 1e1ce33e978ae97fcfcff5638477032b801c46c7c65cf717f95fbc2248f79a9d + requires_dist: + - objgraph>=1.7.2 ; extra == 'graph' + - gprof2dot>=2022.7.29 ; extra == 'profile' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl + name: annotated-doc + version: 0.0.4 + sha256: 571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320 + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/20/16/e777eadfa0c0305878c36fae1d5e6db474fbb15dae202b9ec378809dfb4d/nbstripout-0.9.1-py3-none-any.whl + name: nbstripout + version: 0.9.1 + sha256: ca027ee45742ee77e4f8e9080254f9a707f1161ba11367b82fdf4a29892c759e + requires_dist: + - nbformat + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/20/17/ec40d981705654853726e7ac9aea9ddbb4a5d9cf54d8472222f4f3de06c2/pandas-3.0.2-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl + name: pandas + version: 3.0.2 + sha256: 61c2fd96d72b983a9891b2598f286befd4ad262161a609c92dc1652544b46b76 + requires_dist: + - numpy>=1.26.0 ; python_full_version < '3.14' + - numpy>=2.3.3 ; python_full_version >= '3.14' + - python-dateutil>=2.8.2 + - tzdata ; sys_platform == 'win32' + - tzdata ; sys_platform == 'emscripten' + - hypothesis>=6.116.0 ; extra == 'test' + - pytest>=8.3.4 ; extra == 'test' + - pytest-xdist>=3.6.1 ; extra == 'test' + - pyarrow>=13.0.0 ; extra == 'pyarrow' + - bottleneck>=1.4.2 ; extra == 'performance' + - numba>=0.60.0 ; extra == 'performance' + - numexpr>=2.10.2 ; extra == 'performance' + - scipy>=1.14.1 ; extra == 'computation' + - xarray>=2024.10.0 ; extra == 'computation' + - fsspec>=2024.10.0 ; extra == 'fss' + - s3fs>=2024.10.0 ; extra == 'aws' + - gcsfs>=2024.10.0 ; extra == 'gcp' + - odfpy>=1.4.1 ; extra == 'excel' + - openpyxl>=3.1.5 ; extra == 'excel' + - python-calamine>=0.3.0 ; extra == 'excel' + - pyxlsb>=1.0.10 ; extra == 'excel' + - xlrd>=2.0.1 ; extra == 'excel' + - xlsxwriter>=3.2.0 ; extra == 'excel' + - pyarrow>=13.0.0 ; extra == 'parquet' + - pyarrow>=13.0.0 ; extra == 'feather' + - pyiceberg>=0.8.1 ; extra == 'iceberg' + - tables>=3.10.1 ; extra == 'hdf5' + - pyreadstat>=1.2.8 ; extra == 'spss' + - sqlalchemy>=2.0.36 ; extra == 'postgresql' + - psycopg2>=2.9.10 ; extra == 'postgresql' + - adbc-driver-postgresql>=1.2.0 ; extra == 'postgresql' + - sqlalchemy>=2.0.36 ; extra == 'mysql' + - pymysql>=1.1.1 ; extra == 'mysql' + - sqlalchemy>=2.0.36 ; extra == 'sql-other' + - adbc-driver-postgresql>=1.2.0 ; extra == 'sql-other' + - adbc-driver-sqlite>=1.2.0 ; extra == 'sql-other' + - beautifulsoup4>=4.12.3 ; extra == 'html' + - html5lib>=1.1 ; extra == 'html' + - lxml>=5.3.0 ; extra == 'html' + - lxml>=5.3.0 ; extra == 'xml' + - matplotlib>=3.9.3 ; extra == 'plot' + - jinja2>=3.1.5 ; extra == 'output-formatting' + - tabulate>=0.9.0 ; extra == 'output-formatting' + - pyqt5>=5.15.9 ; extra == 'clipboard' + - qtpy>=2.4.2 ; extra == 'clipboard' + - zstandard>=0.23.0 ; extra == 'compression' + - pytz>=2024.2 ; extra == 'timezone' + - adbc-driver-postgresql>=1.2.0 ; extra == 'all' + - adbc-driver-sqlite>=1.2.0 ; extra == 'all' + - beautifulsoup4>=4.12.3 ; extra == 'all' + - bottleneck>=1.4.2 ; extra == 'all' + - fastparquet>=2024.11.0 ; extra == 'all' + - fsspec>=2024.10.0 ; extra == 'all' + - gcsfs>=2024.10.0 ; extra == 'all' + - html5lib>=1.1 ; extra == 'all' + - hypothesis>=6.116.0 ; extra == 'all' + - jinja2>=3.1.5 ; extra == 'all' + - lxml>=5.3.0 ; extra == 'all' + - matplotlib>=3.9.3 ; extra == 'all' + - numba>=0.60.0 ; extra == 'all' + - numexpr>=2.10.2 ; extra == 'all' + - odfpy>=1.4.1 ; extra == 'all' + - openpyxl>=3.1.5 ; extra == 'all' + - psycopg2>=2.9.10 ; extra == 'all' + - pyarrow>=13.0.0 ; extra == 'all' + - pyiceberg>=0.8.1 ; extra == 'all' + - pymysql>=1.1.1 ; extra == 'all' + - pyqt5>=5.15.9 ; extra == 'all' + - pyreadstat>=1.2.8 ; extra == 'all' + - pytest>=8.3.4 ; extra == 'all' + - pytest-xdist>=3.6.1 ; extra == 'all' + - python-calamine>=0.3.0 ; extra == 'all' + - pytz>=2024.2 ; extra == 'all' + - pyxlsb>=1.0.10 ; extra == 'all' + - qtpy>=2.4.2 ; extra == 'all' + - scipy>=1.14.1 ; extra == 'all' + - s3fs>=2024.10.0 ; extra == 'all' + - sqlalchemy>=2.0.36 ; extra == 'all' + - tables>=3.10.1 ; extra == 'all' + - tabulate>=0.9.0 ; extra == 'all' + - xarray>=2024.10.0 ; extra == 'all' + - xlrd>=2.0.1 ; extra == 'all' + - xlsxwriter>=3.2.0 ; extra == 'all' + - zstandard>=0.23.0 ; extra == 'all' + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/20/7a/1c6e3562dfd8950adbb11ffbc65d21e7c89d01a6e4f137fa981056de25c5/gitpython-3.1.50-py3-none-any.whl + name: gitpython + version: 3.1.50 + sha256: d352abe2908d07355014abdd21ddf798c2a961469239afec4962e9da884858f9 + requires_dist: + - gitdb>=4.0.1,<5 + - typing-extensions>=3.10.0.2 ; python_full_version < '3.10' + - coverage[toml] ; extra == 'test' + - ddt>=1.1.1,!=1.4.3 ; extra == 'test' + - mock ; python_full_version < '3.8' and extra == 'test' + - mypy==1.18.2 ; python_full_version >= '3.9' and extra == 'test' + - pre-commit ; extra == 'test' + - pytest>=7.3.1 ; extra == 'test' + - pytest-cov ; extra == 'test' + - pytest-instafail ; extra == 'test' + - pytest-mock ; extra == 'test' + - pytest-sugar ; extra == 'test' + - typing-extensions ; python_full_version < '3.11' and extra == 'test' + - sphinx>=7.4.7,<8 ; extra == 'doc' + - sphinx-rtd-theme ; extra == 'doc' + - sphinx-autodoc-typehints ; extra == 'doc' + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/21/0e/8459ca4413e1a21a06c97d134bfaf18adfd27cea068813dc0faae06cbf00/cssselect2-0.9.0-py3-none-any.whl + name: cssselect2 + version: 0.9.0 + sha256: 6a99e5f91f9a016a304dd929b0966ca464bcfda15177b6fb4a118fc0fb5d9563 + requires_dist: + - tinycss2 + - webencodings + - sphinx ; extra == 'doc' + - furo ; extra == 'doc' + - pytest ; extra == 'test' + - ruff ; extra == 'test' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl + name: mkdocs + version: 1.6.1 + sha256: db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e + requires_dist: + - click>=7.0 + - colorama>=0.4 ; sys_platform == 'win32' + - ghp-import>=1.0 + - importlib-metadata>=4.4 ; python_full_version < '3.10' + - jinja2>=2.11.1 + - markdown>=3.3.6 + - markupsafe>=2.0.1 + - mergedeep>=1.3.4 + - mkdocs-get-deps>=0.2.0 + - packaging>=20.5 + - pathspec>=0.11.1 + - pyyaml-env-tag>=0.1 + - pyyaml>=5.1 + - watchdog>=2.0 + - babel>=2.9.0 ; extra == 'i18n' + - babel==2.9.0 ; extra == 'min-versions' + - click==7.0 ; extra == 'min-versions' + - colorama==0.4 ; sys_platform == 'win32' and extra == 'min-versions' + - ghp-import==1.0 ; extra == 'min-versions' + - importlib-metadata==4.4 ; python_full_version < '3.10' and extra == 'min-versions' + - jinja2==2.11.1 ; extra == 'min-versions' + - markdown==3.3.6 ; extra == 'min-versions' + - markupsafe==2.0.1 ; extra == 'min-versions' + - mergedeep==1.3.4 ; extra == 'min-versions' + - mkdocs-get-deps==0.2.0 ; extra == 'min-versions' + - packaging==20.5 ; extra == 'min-versions' + - pathspec==0.11.1 ; extra == 'min-versions' + - pyyaml-env-tag==0.1 ; extra == 'min-versions' + - pyyaml==5.1 ; extra == 'min-versions' + - watchdog==2.0 ; extra == 'min-versions' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/25/7d/cea3531f77df694ac7f169378250d85f19f69b09a5f4fa45f650837ae7cc/py3dmol-2.5.4-py2.py3-none-any.whl + name: py3dmol + version: 2.5.4 + sha256: 32806726b5310524a2b5bfee320737f7feef635cafc945c991062806daa9e43a + requires_dist: + - ipython ; extra == 'ipython' +- pypi: https://files.pythonhosted.org/packages/28/88/4789719fbbe166d12d345b3ac66b96105f10001b16e00a9765ba29261a21/nbqa-1.9.1-py3-none-any.whl + name: nbqa + version: 1.9.1 + sha256: 95552d2f6c2c038136252a805aa78d85018aef922586270c3a074332737282e5 + requires_dist: + - autopep8>=1.5 + - ipython>=7.8.0 + - tokenize-rt>=3.2.0 + - tomli + - black ; extra == 'toolchain' + - blacken-docs ; extra == 'toolchain' + - flake8 ; extra == 'toolchain' + - isort ; extra == 'toolchain' + - jupytext ; extra == 'toolchain' + - mypy ; extra == 'toolchain' + - pylint ; extra == 'toolchain' + - pyupgrade ; extra == 'toolchain' + - ruff ; extra == 'toolchain' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/28/de/a3e710469772c6a89595fc52816da05c1e164b4c866a89e3cb82fb1b67c5/mkdocs_autorefs-1.4.4-py3-none-any.whl + name: mkdocs-autorefs + version: 1.4.4 + sha256: 834ef5408d827071ad1bc69e0f39704fa34c7fc05bc8e1c72b227dfdc5c76089 + requires_dist: + - markdown>=3.3 + - markupsafe>=2.0.1 + - mkdocs>=1.1 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/2a/2d/d4bf65e47cea8ff2c794a600c4fd1273a7902f268757c531e0ee9f18aa58/pooch-1.9.0-py3-none-any.whl + name: pooch + version: 1.9.0 + sha256: f265597baa9f760d25ceb29d0beb8186c243d6607b0f60b83ecf14078dbc703b + requires_dist: + - platformdirs>=2.5.0 + - packaging>=20.0 + - requests>=2.19.0 + - tqdm>=4.41.0,<5.0.0 ; extra == 'progress' + - paramiko>=2.7.0 ; extra == 'sftp' + - xxhash>=1.4.3 ; extra == 'xxhash' + - pytest-httpserver ; extra == 'test' + - pytest-localftpserver ; extra == 'test' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/2a/79/309d0e637f6f37e83c711f547308b91af02b72d2326ddd860b966080ef29/msgpack-1.1.2-cp311-cp311-win_amd64.whl + name: msgpack + version: 1.1.2 + sha256: d198d275222dc54244bf3327eb8cbe00307d220241d9cec4d306d49a44e85f68 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/2b/0a/7b98e1e119878a27ba8618ca1e18b14f992ff1eda40f47bccccf4de44121/kiwisolver-1.5.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + name: kiwisolver + version: 1.5.0 + sha256: 332b4f0145c30b5f5ad9374881133e5aa64320428a57c2c2b61e9d891a51c2f3 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/2c/01/bc663630c510822c95c47a66af9fa7a443c295b47d5f041e5e6ae62ef659/mkdocs_material-9.7.6-py3-none-any.whl + name: mkdocs-material + version: 9.7.6 + sha256: 71b84353921b8ea1ba84fe11c50912cc512da8fe0881038fcc9a0761c0e635ba + requires_dist: + - babel>=2.10 + - backrefs>=5.7.post1 + - colorama>=0.4 + - jinja2>=3.1 + - markdown>=3.2 + - mkdocs-material-extensions>=1.3 + - mkdocs>=1.6,<2 + - paginate>=0.5 + - pygments>=2.16 + - pymdown-extensions>=10.2 + - requests>=2.30 + - mkdocs-git-committers-plugin-2>=1.1 ; extra == 'git' + - mkdocs-git-revision-date-localized-plugin>=1.2.4 ; extra == 'git' + - cairosvg>=2.6 ; extra == 'imaging' + - pillow>=10.2 ; extra == 'imaging' + - mkdocs-minify-plugin>=0.7 ; extra == 'recommended' + - mkdocs-redirects>=1.2 ; extra == 'recommended' + - mkdocs-rss-plugin>=1.6 ; extra == 'recommended' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl + name: mergedeep + version: 1.3.4 + sha256: 70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307 + requires_python: '>=3.6' +- pypi: https://files.pythonhosted.org/packages/2d/47/634fe8323c6c2bfa86e10eb41ebfe410db5e6231aa1727a31ce4f002480f/spglib-2.6.0-cp313-cp313-macosx_11_0_arm64.whl + name: spglib + version: 2.6.0 + sha256: 12db7a0d6ad84c55e61eda67590a438edeb48e57ffd5df868cd931b57fb8c630 + requires_dist: + - numpy>=1.20,<3 + - importlib-resources ; python_full_version < '3.10' + - typing-extensions>=4.9.0 ; python_full_version < '3.13' + - pytest ; extra == 'test' + - pyyaml ; extra == 'test' + - sphinx>=7.0 ; extra == 'docs' + - sphinxcontrib-bibtex>=2.5 ; extra == 'docs' + - sphinx-book-theme ; extra == 'docs' + - sphinx-autodoc-typehints ; extra == 'docs' + - myst-parser>=2.0 ; extra == 'docs' + - linkify-it-py ; extra == 'docs' + - sphinx-tippy ; extra == 'docs' + - spglib[test] ; extra == 'test-cov' + - pytest-cov ; extra == 'test-cov' + - spglib[test] ; extra == 'test-benchmark' + - pytest-benchmark ; extra == 'test-benchmark' + - spglib[test] ; extra == 'dev' + - pre-commit ; extra == 'dev' + - spglib[docs] ; extra == 'doc' + - spglib[test] ; extra == 'testing' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/2e/75/5604f4d17ab607510d4702f156329194d8edfff7e29644ca9200b085e9a2/scipp-26.3.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + name: scipp + version: 26.3.1 + sha256: 993706e7c31f0317be2db5f528f9142ba67b2e52d7af174fcad195f702e1d6c7 + requires_dist: + - numpy>=2 + - pytest ; extra == 'test' + - matplotlib ; extra == 'test' + - beautifulsoup4 ; extra == 'test' + - ipython ; extra == 'test' + - h5py ; extra == 'extra' + - scipy>=1.7.0 ; extra == 'extra' + - graphviz ; extra == 'extra' + - pooch ; extra == 'extra' + - plopp ; extra == 'extra' + - matplotlib ; extra == 'extra' + - scipp[extra] ; extra == 'all' + - ipympl ; extra == 'all' + - ipython ; extra == 'all' + - ipywidgets ; extra == 'all' + - jupyterlab ; extra == 'all' + - jupyterlab-widgets ; extra == 'all' + - jupyter-nbextensions-configurator ; extra == 'all' + - nodejs ; extra == 'all' + - pythreejs ; extra == 'all' + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/2f/c8/005d1de3af80f54411703d1263a0b9d31276411ec9f273d9432c59b17299/arviz_plots-1.1.0-py3-none-any.whl + name: arviz-plots + version: 1.1.0 + sha256: 5c7ab5b0c7c29cda6ddb5e04c699c70285fe68a76d2b1b42302c69a85742adde + requires_dist: + - arviz-base>=1.1,<1.2 + - arviz-stats[xarray]>=1.1,<1.2 + - bokeh>=3.4 ; extra == 'bokeh' + - sphinx-book-theme ; extra == 'doc' + - myst-parser[linkify] ; extra == 'doc' + - myst-nb ; extra == 'doc' + - sphinx-copybutton ; extra == 'doc' + - numpydoc ; extra == 'doc' + - sphinx>=6 ; extra == 'doc' + - sphinx-design ; extra == 'doc' + - jupyter-sphinx ; extra == 'doc' + - h5netcdf[h5py] ; extra == 'doc' + - plotly<6 ; extra == 'doc' + - matplotlib>=3.9 ; extra == 'matplotlib' + - plotly>=5.19 ; extra == 'plotly' + - webcolors ; extra == 'plotly' + - hypothesis ; extra == 'test' + - pytest ; extra == 'test' + - pytest-cov ; extra == 'test' + - h5netcdf[h5py] ; extra == 'test' + - kaleido ; extra == 'test' + requires_python: '>=3.12' +- pypi: https://files.pythonhosted.org/packages/30/d4/24d543ab8b8158b7f5a97113c831205f5c900c92c8762b1e7f44b7ea0405/python_discovery-1.3.0-py3-none-any.whl + name: python-discovery + version: 1.3.0 + sha256: 441d9ced3dfce36e113beb35ca302c71c7ef06f3c0f9c227a0b9bb3bd49b9e9f + requires_dist: + - filelock>=3.15.4 + - platformdirs>=4.3.6,<5 + - furo>=2025.12.19 ; extra == 'docs' + - sphinx-autodoc-typehints>=3.6.3 ; extra == 'docs' + - sphinx>=9.1 ; extra == 'docs' + - sphinxcontrib-mermaid>=2 ; extra == 'docs' + - covdefaults>=2.3 ; extra == 'testing' + - coverage>=7.5.4 ; extra == 'testing' + - pytest-mock>=3.14 ; extra == 'testing' + - pytest>=8.3.5 ; extra == 'testing' + - setuptools>=75.1 ; extra == 'testing' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/31/b8/69f5565f1a280d032525878a86511eebed0645818492feeb169dfb20ae8e/llvmlite-0.47.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + name: llvmlite + version: 0.47.0 + sha256: 2699a74321189e812d476a43d6d7f652f51811e7b5aad9d9bba842a1c7927acb + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/32/28/79f0f8de97cce916d5ae88a7bee1ad724855e83e6019c0b4d5b3fabc80f3/mkdocstrings_python-2.0.3-py3-none-any.whl + name: mkdocstrings-python + version: 2.0.3 + sha256: 0b83513478bdfd803ff05aa43e9b1fca9dd22bcd9471f09ca6257f009bc5ee12 + requires_dist: + - mkdocstrings>=0.30 + - mkdocs-autorefs>=1.4 + - griffelib>=2.0 + - typing-extensions>=4.0 ; python_full_version < '3.11' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl + name: distlib + version: 0.4.0 + sha256: 9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16 +- 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/33/eb/f9f1ded8e4db9638f9530c3782eb01f5ab04945f4cb9e597a51c203fa4c5/diffpy_pdffit2-1.6.0.tar.gz + name: diffpy-pdffit2 + version: 1.6.0 + sha256: 11a65466f8790f5ac7ae45f2f3fc0d5d116d156d274bcfc079df653123d080e2 + requires_dist: + - diffpy-structure + requires_python: '>=3.12,<3.15' +- pypi: https://files.pythonhosted.org/packages/33/f0/3fe8c6e69135a845f4106f2ff8b6805638d4e85c264e70114e8126689587/tokenize_rt-6.2.0-py2.py3-none-any.whl + name: tokenize-rt + version: 6.2.0 + sha256: a152bf4f249c847a66497a4a95f63376ed68ac6abf092a2f7cfb29d044ecff44 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/34/0b/b9d1911cfefa61399821dfb37f486d83e0f42630a8d12f7194270c417002/llvmlite-0.47.0-cp311-cp311-macosx_11_0_arm64.whl + name: llvmlite + version: 0.47.0 + sha256: 74090f0dcfd6f24ebbef3f21f11e38111c4d7e6919b54c4416e1e357c3446b07 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/35/8b/cd129b0ca4afe886a6ce9d183c44d8301acbd4ef248622e7c49a23145605/coverage-7.13.5-cp311-cp311-macosx_11_0_arm64.whl + name: coverage + version: 7.13.5 + sha256: 145ede53ccbafb297c1c9287f788d1bc3efd6c900da23bf6931b09eafc931587 + requires_dist: + - tomli ; python_full_version <= '3.11' and extra == 'toml' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/35/8c/a917ef8741729ef8b3228f815240f4859717e600f9498359a6c411ed6992/crysfml-0.6.2-cp313-cp313-macosx_14_0_arm64.whl + name: crysfml + version: 0.6.2 + sha256: 0010858646240f6c64e2711c33ccfffd94ee6602859babd1186785e28d6c5c11 + requires_dist: + - numpy + requires_python: '>=3.11,<3.15' +- pypi: https://files.pythonhosted.org/packages/35/e5/d6d0e51fc888f692a35134336866341c08655d92614f492c6860dc45bb2c/scipy-1.17.1-cp313-cp313-win_amd64.whl + name: scipy + version: 1.17.1 + sha256: 37425bc9175607b0268f493d79a292c39f9d001a357bebb6b88fdfaff13f6448 + requires_dist: + - numpy>=1.26.4,<2.7 + - pytest>=8.0.0 ; extra == 'test' + - pytest-cov ; extra == 'test' + - pytest-timeout ; extra == 'test' + - pytest-xdist ; extra == 'test' + - asv ; extra == 'test' + - mpmath ; extra == 'test' + - gmpy2 ; extra == 'test' + - threadpoolctl ; extra == 'test' + - scikit-umfpack ; extra == 'test' + - pooch ; extra == 'test' + - hypothesis>=6.30 ; extra == 'test' + - array-api-strict>=2.3.1 ; extra == 'test' + - cython ; extra == 'test' + - meson ; extra == 'test' + - ninja ; sys_platform != 'emscripten' and extra == 'test' + - sphinx>=5.0.0,<8.2.0 ; extra == 'doc' + - intersphinx-registry ; extra == 'doc' + - pydata-sphinx-theme>=0.15.2 ; extra == 'doc' + - sphinx-copybutton ; extra == 'doc' + - sphinx-design>=0.4.0 ; extra == 'doc' + - matplotlib>=3.5 ; extra == 'doc' + - numpydoc ; extra == 'doc' + - jupytext ; extra == 'doc' + - myst-nb>=1.2.0 ; extra == 'doc' + - pooch ; extra == 'doc' + - jupyterlite-sphinx>=0.19.1 ; extra == 'doc' + - jupyterlite-pyodide-kernel ; extra == 'doc' + - linkify-it-py ; extra == 'doc' + - tabulate ; extra == 'doc' + - click<8.3.0 ; extra == 'dev' + - spin ; extra == 'dev' + - mypy==1.10.0 ; extra == 'dev' + - typing-extensions ; extra == 'dev' + - types-psutil ; extra == 'dev' + - pycodestyle ; extra == 'dev' + - ruff>=0.12.0 ; extra == 'dev' + - cython-lint>=0.12.2 ; extra == 'dev' + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/37/fd/22621d3ee9e3ee87ef4c89b63bba55b265ab85039b3c1ba88ed2380a24c1/scipp-26.3.1-cp313-cp313-win_amd64.whl + name: scipp + version: 26.3.1 + sha256: 03c6dbf8936a2ed62587c5abe8ab5266a5098834a0709321ce799bd1328eb3e6 + requires_dist: + - numpy>=2 + - pytest ; extra == 'test' + - matplotlib ; extra == 'test' + - beautifulsoup4 ; extra == 'test' + - ipython ; extra == 'test' + - h5py ; extra == 'extra' + - scipy>=1.7.0 ; extra == 'extra' + - graphviz ; extra == 'extra' + - pooch ; extra == 'extra' + - plopp ; extra == 'extra' + - matplotlib ; extra == 'extra' + - scipp[extra] ; extra == 'all' + - ipympl ; extra == 'all' + - ipython ; extra == 'all' + - ipywidgets ; extra == 'all' + - jupyterlab ; extra == 'all' + - jupyterlab-widgets ; extra == 'all' + - jupyter-nbextensions-configurator ; extra == 'all' + - nodejs ; extra == 'all' + - pythreejs ; extra == 'all' + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/38/60/35186529de1db3c01f5ad625bde07c1f576305eab6d86bbda4c58445f721/fonttools-4.62.1-cp313-cp313-win_amd64.whl + name: fonttools + version: 4.62.1 + sha256: 7aa21ff53e28a9c2157acbc44e5b401149d3c9178107130e82d74ceb500e5056 + requires_dist: + - lxml>=4.0 ; extra == 'lxml' + - brotli>=1.0.1 ; platform_python_implementation == 'CPython' and extra == 'woff' + - brotlicffi>=0.8.0 ; platform_python_implementation != 'CPython' and extra == 'woff' + - zopfli>=0.1.4 ; extra == 'woff' + - unicodedata2>=17.0.0 ; python_full_version < '3.15' and extra == 'unicode' + - lz4>=1.7.4.2 ; extra == 'graphite' + - scipy ; platform_python_implementation != 'PyPy' and extra == 'interpolatable' + - munkres ; platform_python_implementation == 'PyPy' and extra == 'interpolatable' + - pycairo ; extra == 'interpolatable' + - matplotlib ; extra == 'plot' + - sympy ; extra == 'symfont' + - xattr ; sys_platform == 'darwin' and extra == 'type1' + - skia-pathops>=0.5.0 ; extra == 'pathops' + - uharfbuzz>=0.45.0 ; extra == 'repacker' + - lxml>=4.0 ; extra == 'all' + - brotli>=1.0.1 ; platform_python_implementation == 'CPython' and extra == 'all' + - brotlicffi>=0.8.0 ; platform_python_implementation != 'CPython' and extra == 'all' + - zopfli>=0.1.4 ; extra == 'all' + - unicodedata2>=17.0.0 ; python_full_version < '3.15' and extra == 'all' + - lz4>=1.7.4.2 ; extra == 'all' + - scipy ; platform_python_implementation != 'PyPy' and extra == 'all' + - munkres ; platform_python_implementation == 'PyPy' and extra == 'all' + - pycairo ; extra == 'all' + - matplotlib ; extra == 'all' + - sympy ; extra == 'all' + - xattr ; sys_platform == 'darwin' and extra == 'all' + - skia-pathops>=0.5.0 ; extra == 'all' + - uharfbuzz>=0.45.0 ; extra == 'all' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/38/7e/7b91c89a4cf0f543a83be978657afb20c86af6d725253e319589dcc4ce52/lmfit-1.3.4-py3-none-any.whl + name: lmfit + version: 1.3.4 + sha256: afce1593b42324d37ae2908249b0c55445e2f4c1a0474ff706a8e2f7b5d949fa + requires_dist: + - asteval>=1.0 + - numpy>=1.24 + - scipy>=1.10.0 + - uncertainties>=3.2.2 + - dill>=0.3.4 + - build ; extra == 'dev' + - check-wheel-contents ; extra == 'dev' + - flake8-pyproject ; extra == 'dev' + - pre-commit ; extra == 'dev' + - twine ; extra == 'dev' + - cairosvg ; extra == 'doc' + - corner ; extra == 'doc' + - emcee>=3.0.0 ; extra == 'doc' + - ipykernel ; extra == 'doc' + - jupyter-sphinx>=0.2.4 ; extra == 'doc' + - matplotlib ; extra == 'doc' + - numdifftools ; extra == 'doc' + - pandas ; extra == 'doc' + - pillow ; extra == 'doc' + - pycairo ; sys_platform == 'win32' and extra == 'doc' + - sphinx ; extra == 'doc' + - sphinx-gallery>=0.10 ; extra == 'doc' + - sphinxcontrib-svg2pdfconverter ; extra == 'doc' + - sympy ; extra == 'doc' + - coverage ; extra == 'test' + - flaky ; extra == 'test' + - pytest ; extra == 'test' + - pytest-cov ; extra == 'test' + - lmfit[dev,doc,test] ; extra == 'all' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/38/8b/7ec325b4e9e78beefc2d025b01ee8a2fde771ef7c957c3bff99b9e1fbffa/xraydb-4.5.8-py3-none-any.whl + name: xraydb + version: 4.5.8 + sha256: 2215baafa6a03d00d0254a94525aafc6493c8c285e4ac4477fbd6271b25e6a51 + requires_dist: + - numpy>=1.19 + - scipy>=1.6 + - sqlalchemy>=2.0.1 + - platformdirs + - build ; extra == 'dev' + - twine ; extra == 'dev' + - sphinx ; extra == 'doc' + - pytest ; extra == 'test' + - pytest-cov ; extra == 'test' + - coverage ; extra == 'test' + - xraydb[dev,doc,test] ; extra == 'all' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/3a/eb/fea4d1d51c49832120f7f285d07306db3960f423a2612c6057caf3e8196f/pip-26.1.1-py3-none-any.whl + name: pip + version: 26.1.1 + sha256: 99cb1c2899893b075ff56e4ed0af55669a955b49ad7fb8d8603ecdaf4ed653fb + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/3b/56/6f389de21c49555553d6a5aeed5ac9767631497ac836c4f076273d15bd72/fonttools-4.62.1-cp313-cp313-macosx_10_13_universal2.whl + name: fonttools + version: 4.62.1 + sha256: c22b1014017111c401469e3acc5433e6acf6ebcc6aa9efb538a533c800971c79 + requires_dist: + - lxml>=4.0 ; extra == 'lxml' + - brotli>=1.0.1 ; platform_python_implementation == 'CPython' and extra == 'woff' + - brotlicffi>=0.8.0 ; platform_python_implementation != 'CPython' and extra == 'woff' + - zopfli>=0.1.4 ; extra == 'woff' + - unicodedata2>=17.0.0 ; python_full_version < '3.15' and extra == 'unicode' + - lz4>=1.7.4.2 ; extra == 'graphite' + - scipy ; platform_python_implementation != 'PyPy' and extra == 'interpolatable' + - munkres ; platform_python_implementation == 'PyPy' and extra == 'interpolatable' + - pycairo ; extra == 'interpolatable' + - matplotlib ; extra == 'plot' + - sympy ; extra == 'symfont' + - xattr ; sys_platform == 'darwin' and extra == 'type1' + - skia-pathops>=0.5.0 ; extra == 'pathops' + - uharfbuzz>=0.45.0 ; extra == 'repacker' + - lxml>=4.0 ; extra == 'all' + - brotli>=1.0.1 ; platform_python_implementation == 'CPython' and extra == 'all' + - brotlicffi>=0.8.0 ; platform_python_implementation != 'CPython' and extra == 'all' + - zopfli>=0.1.4 ; extra == 'all' + - unicodedata2>=17.0.0 ; python_full_version < '3.15' and extra == 'all' + - lz4>=1.7.4.2 ; extra == 'all' + - scipy ; platform_python_implementation != 'PyPy' and extra == 'all' + - munkres ; platform_python_implementation == 'PyPy' and extra == 'all' + - pycairo ; extra == 'all' + - matplotlib ; extra == 'all' + - sympy ; extra == 'all' + - xattr ; sys_platform == 'darwin' and extra == 'all' + - skia-pathops>=0.5.0 ; extra == 'all' + - uharfbuzz>=0.45.0 ; extra == 'all' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/3c/0c/c73042d7ae043f4e3ff51406821ad70904249708a6bc4f0dc94f5f78ce9b/python_bidi-0.6.9.tar.gz + name: python-bidi + version: 0.6.9 + sha256: 001c1769893fd859216b0dc39dd4679c7260bf292c3637b727a928402a254322 + requires_dist: + - nox ; extra == 'dev' + - pytest ; extra == 'dev' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/3e/17/1f31d8562e6f970d64911f1abc330d233bc0c0601411cf7e19c1292be6da/spdx_headers-1.5.1-py3-none-any.whl + name: spdx-headers + version: 1.5.1 + sha256: 73bcb1ed087824b55ccaa497d03d8f0f0b0eaf30e5f0f7d5bbd29d2c4fe78fcf + requires_dist: + - chardet>=5.2.0 + - requests>=2.32.3 + - black>=23.0.0 ; extra == 'dev' + - build>=0.10.0 ; extra == 'dev' + - hatch>=1.9.0 ; extra == 'dev' + - isort>=5.12.0 ; extra == 'dev' + - mypy>=1.0.0 ; extra == 'dev' + - pre-commit>=4.3.0 ; extra == 'dev' + - pytest-cov>=4.0.0 ; extra == 'dev' + - pytest>=7.0.0 ; extra == 'dev' + - ruff>=0.5.0 ; extra == 'dev' + - twine>=4.0.0 ; extra == 'dev' + - types-requests>=2.31.0.6 ; extra == 'dev' + - pytest-cov>=4.0.0 ; extra == 'test' + - pytest-mock>=3.10.0 ; extra == 'test' + - pytest>=7.0.0 ; extra == 'test' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/3e/9a/a9383ff12698d5306522122e7007d9fd58f903e00a7715506e3ff5be1678/python_bidi-0.6.9-cp311-cp311-macosx_11_0_arm64.whl + name: python-bidi + version: 0.6.9 + sha256: c3554a3cb9b95e503bc348a8c461b330bf1812c500f51bc9bbd789544fc4a8aa + requires_dist: + - nox ; extra == 'dev' + - pytest ; extra == 'dev' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/3f/0e/fa3b193432cfc60c93b42f3be03365f5f909d2b3ea410295cf36df739e31/widgetsnbextension-4.0.15-py3-none-any.whl + name: widgetsnbextension + version: 4.0.15 + sha256: 8156704e4346a571d9ce73b84bee86a29906c9abfd7223b7228a28899ccf3366 + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/3f/82/8a3739a5e470b3c6cbb1d21d315800d8e16bff503d1f16b03a4ec3212786/pillow-12.2.0-cp313-cp313-win_amd64.whl + name: pillow + version: 12.2.0 + sha256: 390ede346628ccc626e5730107cde16c42d3836b89662a115a921f28440e6a3b + requires_dist: + - furo ; extra == 'docs' + - olefile ; extra == 'docs' + - sphinx>=8.2 ; extra == 'docs' + - sphinx-autobuild ; extra == 'docs' + - sphinx-copybutton ; extra == 'docs' + - sphinx-inline-tabs ; extra == 'docs' + - sphinxext-opengraph ; extra == 'docs' + - olefile ; extra == 'fpx' + - olefile ; extra == 'mic' + - arro3-compute ; extra == 'test-arrow' + - arro3-core ; extra == 'test-arrow' + - nanoarrow ; extra == 'test-arrow' + - pyarrow ; extra == 'test-arrow' + - check-manifest ; extra == 'tests' + - coverage>=7.4.2 ; extra == 'tests' + - defusedxml ; extra == 'tests' + - markdown2 ; extra == 'tests' + - olefile ; extra == 'tests' + - packaging ; extra == 'tests' + - pyroma>=5 ; extra == 'tests' + - pytest ; extra == 'tests' + - pytest-cov ; extra == 'tests' + - pytest-timeout ; extra == 'tests' + - pytest-xdist ; extra == 'tests' + - trove-classifiers>=2024.10.12 ; extra == 'tests' + - defusedxml ; extra == 'xmp' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/3f/d0/7b958df957e4827837b590944008f0b28078f552b451f7407b4b3d54f574/asciichartpy-1.5.25-py2.py3-none-any.whl + name: asciichartpy + version: 1.5.25 + sha256: 33c417a3c8ef7d0a11b98eb9ea6dd9b2c1b17559e539b207a17d26d4302d0258 + requires_dist: + - setuptools + - flake8 ; extra == 'qa' +- pypi: https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl + name: typer + version: 0.25.1 + sha256: 75caa44ed46a03fb2dab8808753ffacdbfea88495e74c85a28c5eefcf5f39c89 + requires_dist: + - click>=8.2.1 + - shellingham>=1.3.0 + - rich>=13.8.0 + - annotated-doc>=0.0.2 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/42/d9/27b13bc9419bf5dae02905b348f16ca827646cd76244ddd326f1a8139a6a/cyclebane-24.10.0-py3-none-any.whl + name: cyclebane + version: 24.10.0 + sha256: 902dd318667e4a222afc270cc5bc72c67d5d6047d2e0e1c36018885fb80f5e5d + requires_dist: + - networkx + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/43/34/7d27a333c558d6ac16dbc12a35061d389735e99e494ee4effa4ec6d99bed/pycairo-1.29.0-cp313-cp313-win_amd64.whl + name: pycairo + version: 1.29.0 + sha256: 91114e4b3fbf4287c2b0788f83e1f566ce031bda49cf1c3c3c19c3e986e95c38 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl + name: mpmath + version: 1.3.0 + sha256: a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c + requires_dist: + - pytest>=4.6 ; extra == 'develop' + - pycodestyle ; extra == 'develop' + - pytest-cov ; extra == 'develop' + - codecov ; extra == 'develop' + - wheel ; extra == 'develop' + - sphinx ; extra == 'docs' + - gmpy2>=2.1.0a4 ; platform_python_implementation != 'PyPy' and extra == 'gmpy' + - pytest>=4.6 ; extra == 'tests' +- pypi: https://files.pythonhosted.org/packages/44/0b/0615dbedb98f5b32a35a53290fbdc6e22306968109278d7e58df82d7a9f6/numba-0.65.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + name: numba + version: 0.65.1 + sha256: f80ed83774b5173abd6581cd8d2165d1d38e13d2e5c8155c0c0b421784745420 + requires_dist: + - llvmlite>=0.47.0.dev0,<0.48 + - numpy>=1.22 + - numpy>=1.22,<2.5 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/44/0d/f494a2ec62ab56ffd44da4f236db374421fe0cd1e39d9ebc9785751eb432/periodictable-2.1.0-py3-none-any.whl + name: periodictable + version: 2.1.0 + sha256: e9155d2bf5ac10050abeff2f99096d4f04312c0c8a6bb432e28744367c5064b3 + requires_dist: + - pyparsing>=3.0.0 + - numpy + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/44/1f/227f9cb7edcd3e14ab05928f3db00e9d595c0f269c87bf35f565ce44941b/arviz-0.23.4-py3-none-any.whl + name: arviz + version: 0.23.4 + sha256: c46c7faf8a06abadc9b5b64000584062ecbc20c2298e2bd6dfba04bb01a684ca + requires_dist: + - setuptools>=60.0.0 + - matplotlib>=3.8 + - numpy>=1.26.0 + - scipy>=1.11.0 + - packaging + - pandas>=2.1.0 + - xarray>=2023.7.0 + - h5netcdf>=1.0.2 + - h5py + - typing-extensions>=4.1.0 + - xarray-einstats>=0.3 + - platformdirs + - numba ; extra == 'all' + - netcdf4 ; extra == 'all' + - bokeh>=3 ; extra == 'all' + - contourpy ; extra == 'all' + - ujson ; extra == 'all' + - dask[distributed] ; extra == 'all' + - zarr>=2.5.0,<3 ; extra == 'all' + - xarray>=2024.11.0 ; extra == 'all' + - dm-tree>=0.1.8 ; extra == 'all' + - arviz-base[h5netcdf] ; extra == 'preview' + - arviz-stats[xarray] ; extra == 'preview' + - arviz-plots ; extra == 'preview' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/44/a0/97a6339859d4acb2536efb24feb6708e82f7d33b2ed7e036f2983fcced82/pandas-3.0.2-cp311-cp311-win_amd64.whl + name: pandas + version: 3.0.2 + sha256: ed72cb3f45190874eb579c64fa92d9df74e98fd63e2be7f62bce5ace0ade61df + requires_dist: + - numpy>=1.26.0 ; python_full_version < '3.14' + - numpy>=2.3.3 ; python_full_version >= '3.14' + - python-dateutil>=2.8.2 + - tzdata ; sys_platform == 'win32' + - tzdata ; sys_platform == 'emscripten' + - hypothesis>=6.116.0 ; extra == 'test' + - pytest>=8.3.4 ; extra == 'test' + - pytest-xdist>=3.6.1 ; extra == 'test' + - pyarrow>=13.0.0 ; extra == 'pyarrow' + - bottleneck>=1.4.2 ; extra == 'performance' + - numba>=0.60.0 ; extra == 'performance' + - numexpr>=2.10.2 ; extra == 'performance' + - scipy>=1.14.1 ; extra == 'computation' + - xarray>=2024.10.0 ; extra == 'computation' + - fsspec>=2024.10.0 ; extra == 'fss' + - s3fs>=2024.10.0 ; extra == 'aws' + - gcsfs>=2024.10.0 ; extra == 'gcp' + - odfpy>=1.4.1 ; extra == 'excel' + - openpyxl>=3.1.5 ; extra == 'excel' + - python-calamine>=0.3.0 ; extra == 'excel' + - pyxlsb>=1.0.10 ; extra == 'excel' + - xlrd>=2.0.1 ; extra == 'excel' + - xlsxwriter>=3.2.0 ; extra == 'excel' + - pyarrow>=13.0.0 ; extra == 'parquet' + - pyarrow>=13.0.0 ; extra == 'feather' + - pyiceberg>=0.8.1 ; extra == 'iceberg' + - tables>=3.10.1 ; extra == 'hdf5' + - pyreadstat>=1.2.8 ; extra == 'spss' + - sqlalchemy>=2.0.36 ; extra == 'postgresql' + - psycopg2>=2.9.10 ; extra == 'postgresql' + - adbc-driver-postgresql>=1.2.0 ; extra == 'postgresql' + - sqlalchemy>=2.0.36 ; extra == 'mysql' + - pymysql>=1.1.1 ; extra == 'mysql' + - sqlalchemy>=2.0.36 ; extra == 'sql-other' + - adbc-driver-postgresql>=1.2.0 ; extra == 'sql-other' + - adbc-driver-sqlite>=1.2.0 ; extra == 'sql-other' + - beautifulsoup4>=4.12.3 ; extra == 'html' + - html5lib>=1.1 ; extra == 'html' + - lxml>=5.3.0 ; extra == 'html' + - lxml>=5.3.0 ; extra == 'xml' + - matplotlib>=3.9.3 ; extra == 'plot' + - jinja2>=3.1.5 ; extra == 'output-formatting' + - tabulate>=0.9.0 ; extra == 'output-formatting' + - pyqt5>=5.15.9 ; extra == 'clipboard' + - qtpy>=2.4.2 ; extra == 'clipboard' + - zstandard>=0.23.0 ; extra == 'compression' + - pytz>=2024.2 ; extra == 'timezone' + - adbc-driver-postgresql>=1.2.0 ; extra == 'all' + - adbc-driver-sqlite>=1.2.0 ; extra == 'all' + - beautifulsoup4>=4.12.3 ; extra == 'all' + - bottleneck>=1.4.2 ; extra == 'all' + - fastparquet>=2024.11.0 ; extra == 'all' + - fsspec>=2024.10.0 ; extra == 'all' + - gcsfs>=2024.10.0 ; extra == 'all' + - html5lib>=1.1 ; extra == 'all' + - hypothesis>=6.116.0 ; extra == 'all' + - jinja2>=3.1.5 ; extra == 'all' + - lxml>=5.3.0 ; extra == 'all' + - matplotlib>=3.9.3 ; extra == 'all' + - numba>=0.60.0 ; extra == 'all' + - numexpr>=2.10.2 ; extra == 'all' + - odfpy>=1.4.1 ; extra == 'all' + - openpyxl>=3.1.5 ; extra == 'all' + - psycopg2>=2.9.10 ; extra == 'all' + - pyarrow>=13.0.0 ; extra == 'all' + - pyiceberg>=0.8.1 ; extra == 'all' + - pymysql>=1.1.1 ; extra == 'all' + - pyqt5>=5.15.9 ; extra == 'all' + - pyreadstat>=1.2.8 ; extra == 'all' + - pytest>=8.3.4 ; extra == 'all' + - pytest-xdist>=3.6.1 ; extra == 'all' + - python-calamine>=0.3.0 ; extra == 'all' + - pytz>=2024.2 ; extra == 'all' + - pyxlsb>=1.0.10 ; extra == 'all' + - qtpy>=2.4.2 ; extra == 'all' + - scipy>=1.14.1 ; extra == 'all' + - s3fs>=2024.10.0 ; extra == 'all' + - sqlalchemy>=2.0.36 ; extra == 'all' + - tables>=3.10.1 ; extra == 'all' + - tabulate>=0.9.0 ; extra == 'all' + - xarray>=2024.10.0 ; extra == 'all' + - xlrd>=2.0.1 ; extra == 'all' + - xlsxwriter>=3.2.0 ; extra == 'all' + - zstandard>=0.23.0 ; extra == 'all' + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/46/27/5799b020e4cdfb25a7c951c06a96397c135efcdc21b78d853bbd9c814c7d/llvmlite-0.47.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + name: llvmlite + version: 0.47.0 + sha256: ca14f02e29134e837982497959a8e2193d6035235de1cb41a9cb2bd6da4eedbb + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/46/9f/4898b44e4042c60fafcb1162dfb7014f6f15b1ec19bf29cfea6bf26df90d/llvmlite-0.47.0-cp313-cp313-win_amd64.whl + name: llvmlite + version: 0.47.0 + sha256: c2e9adf8698d813a9a5efb2d4370caf344dbc1e145019851fee6a6f319ba760e + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/47/a1/d5ff69e747374c33a3b53b9f98cca7889fce1fd03d79cdc4e1bccc6c5a87/pillow-12.2.0-cp311-cp311-macosx_11_0_arm64.whl + name: pillow + version: 12.2.0 + sha256: 71cde9a1e1551df7d34a25462fc60325e8a11a82cc2e2f54578e5e9a1e153d65 + requires_dist: + - furo ; extra == 'docs' + - olefile ; extra == 'docs' + - sphinx>=8.2 ; extra == 'docs' + - sphinx-autobuild ; extra == 'docs' + - sphinx-copybutton ; extra == 'docs' + - sphinx-inline-tabs ; extra == 'docs' + - sphinxext-opengraph ; extra == 'docs' + - olefile ; extra == 'fpx' + - olefile ; extra == 'mic' + - arro3-compute ; extra == 'test-arrow' + - arro3-core ; extra == 'test-arrow' + - nanoarrow ; extra == 'test-arrow' + - pyarrow ; extra == 'test-arrow' + - check-manifest ; extra == 'tests' + - coverage>=7.4.2 ; extra == 'tests' + - defusedxml ; extra == 'tests' + - markdown2 ; extra == 'tests' + - olefile ; extra == 'tests' + - packaging ; extra == 'tests' + - pyroma>=5 ; extra == 'tests' + - pytest ; extra == 'tests' + - pytest-cov ; extra == 'tests' + - pytest-timeout ; extra == 'tests' + - pytest-xdist ; extra == 'tests' + - trove-classifiers>=2024.10.12 ; extra == 'tests' + - defusedxml ; extra == 'xmp' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/4a/1a/bea014b55c45c98c63dffd34d2dd01eed0a73ce5b175bd7b1ba798d13558/pyhanko-0.35.1-py3-none-any.whl + name: pyhanko + version: 0.35.1 + sha256: 57c085f29d7f87334be2a5fbd38a1ca3c987f40f51aea38bde628497d6d9ef2e + requires_dist: + - asn1crypto>=1.5.1 + - tzlocal>=4.3 + - pyhanko-certvalidator>=0.31.1,<0.32 + - requests>=2.31.0 + - pyyaml>=6.0 + - cryptography>=48.0.0 + - lxml>=5.4.0 + - fonttools>=4.33.3 ; extra == 'opentype' + - uharfbuzz>=0.25.0,<0.55.0 ; extra == 'opentype' + - qrcode>=7.3.1 ; extra == 'qr' + - pillow>=7.2.0 ; extra == 'image-support' + - python-barcode==0.16.1 ; extra == 'image-support' + - python-pkcs11~=0.9.3 ; extra == 'pkcs11' + - aiohttp>=3.9,<3.14 ; extra == 'async-http' + - xsdata>=24.4,<27.0 ; extra == 'etsi' + - signxml>=4.2.0 ; extra == 'etsi' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/4a/f3/00bb1e867fba351e2d784170955713bee200c43ea306c59f30bd7e748192/dask-2026.3.0-py3-none-any.whl + name: dask + version: 2026.3.0 + sha256: be614b9242b0b38288060fb2d7696125946469c98a1c30e174883fd199e0428d + requires_dist: + - click>=8.1 + - cloudpickle>=3.0.0 + - fsspec>=2021.9.0 + - packaging>=20.0 + - partd>=1.4.0 + - pyyaml>=5.3.1 + - toolz>=0.12.0 + - importlib-metadata>=4.13.0 ; python_full_version < '3.12' + - numpy>=1.24 ; extra == 'array' + - dask[array] ; extra == 'dataframe' + - pandas>=2.0 ; extra == 'dataframe' + - pyarrow>=16.0 ; extra == 'dataframe' + - distributed>=2026.3.0,<2026.3.1 ; extra == 'distributed' + - bokeh>=3.1.0 ; extra == 'diagnostics' + - jinja2>=2.10.3 ; extra == 'diagnostics' + - dask[array,dataframe,diagnostics,distributed] ; extra == 'complete' + - pyarrow>=16.0 ; extra == 'complete' + - lz4>=4.3.2 ; extra == 'complete' + - pandas[test] ; extra == 'test' + - pytest ; extra == 'test' + - pytest-cov ; extra == 'test' + - pytest-mock ; extra == 'test' + - pytest-rerunfailures ; extra == 'test' + - pytest-timeout ; extra == 'test' + - pytest-xdist ; extra == 'test' + - pre-commit ; extra == 'test' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/4b/32/e0f13a1c5b0f8572d0ec6ae2f6c677b7991fafd95da523159c19eff0696a/contourpy-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + name: contourpy + version: 1.3.3 + sha256: 4debd64f124ca62069f313a9cb86656ff087786016d76927ae2cf37846b006c9 + requires_dist: + - numpy>=1.25 + - furo ; extra == 'docs' + - sphinx>=7.2 ; extra == 'docs' + - sphinx-copybutton ; extra == 'docs' + - bokeh ; extra == 'bokeh' + - selenium ; extra == 'bokeh' + - contourpy[bokeh,docs] ; extra == 'mypy' + - bokeh ; extra == 'mypy' + - docutils-stubs ; extra == 'mypy' + - mypy==1.17.0 ; extra == 'mypy' + - types-pillow ; extra == 'mypy' + - contourpy[test-no-images] ; extra == 'test' + - matplotlib ; extra == 'test' + - pillow ; extra == 'test' + - pytest ; extra == 'test-no-images' + - pytest-cov ; extra == 'test-no-images' + - pytest-rerunfailures ; extra == 'test-no-images' + - pytest-xdist ; extra == 'test-no-images' + - wurlitzer ; extra == 'test-no-images' + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/51/18/325cd32ece1120d1da51cc4e4294c6580190699490183fc2fe8cb6d61ec5/matplotlib-3.10.9-cp311-cp311-macosx_11_0_arm64.whl + name: matplotlib + version: 3.10.9 + sha256: dfca0129678bd56379db26c52b5d77ed7de314c047492fbdc763aa7501710cfb + requires_dist: + - contourpy>=1.0.1 + - cycler>=0.10 + - fonttools>=4.22.0 + - kiwisolver>=1.3.1 + - numpy>=1.23 + - packaging>=20.0 + - pillow>=8 + - pyparsing>=3 + - python-dateutil>=2.7 + - meson-python>=0.13.1,<0.17.0 ; extra == 'dev' + - pybind11>=2.13.2,!=2.13.3 ; extra == 'dev' + - setuptools-scm>=7,<10 ; extra == 'dev' + - setuptools>=64 ; extra == 'dev' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/52/59/0782e51887ac6b07ffd1570e0364cf901ebc36345fea669969d2084baebb/simple_websocket-1.1.0-py3-none-any.whl + name: simple-websocket + version: 1.1.0 + sha256: 4af6069630a38ed6c561010f0e11a5bc0d4ca569b36306eb257cd9a192497c8c + requires_dist: + - wsproto + - tox ; extra == 'dev' + - flake8 ; extra == 'dev' + - pytest ; extra == 'dev' + - pytest-cov ; extra == 'dev' + - sphinx ; extra == 'docs' + requires_python: '>=3.6' +- pypi: https://files.pythonhosted.org/packages/52/6a/57f43e054fb3d3a56ac9fc532bc684fc6169a26c75c353e65425b3e56eef/propcache-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + name: propcache + version: 0.4.1 + sha256: fd6f30fdcf9ae2a70abd34da54f18da086160e4d7d9251f81f3da0ff84fc5a48 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/52/a0/c1f604538ff6db22a0690be2dc44ab59178e115f63c917794e529356ab23/h5py-3.16.0-cp311-cp311-manylinux_2_28_x86_64.whl + name: h5py + version: 3.16.0 + sha256: fb1720028d99040792bb2fb31facb8da44a6f29df7697e0b84f0d79aff2e9bd3 + requires_dist: + - numpy>=1.21.2 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/53/01/1c0485ae02e645bc517bf5d5a6ca674f62c97e247890b954cbfe85c64dae/spglib-2.6.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + name: spglib + version: 2.6.0 + sha256: cc9d856d7cc936dc73b6b303aaf8b2fb4230a8527659373450c6e1139cbb2a5c + requires_dist: + - numpy>=1.20,<3 + - importlib-resources ; python_full_version < '3.10' + - typing-extensions>=4.9.0 ; python_full_version < '3.13' + - pytest ; extra == 'test' + - pyyaml ; extra == 'test' + - sphinx>=7.0 ; extra == 'docs' + - sphinxcontrib-bibtex>=2.5 ; extra == 'docs' + - sphinx-book-theme ; extra == 'docs' + - sphinx-autodoc-typehints ; extra == 'docs' + - myst-parser>=2.0 ; extra == 'docs' + - linkify-it-py ; extra == 'docs' + - sphinx-tippy ; extra == 'docs' + - spglib[test] ; extra == 'test-cov' + - pytest-cov ; extra == 'test-cov' + - spglib[test] ; extra == 'test-benchmark' + - pytest-benchmark ; extra == 'test-benchmark' + - spglib[test] ; extra == 'dev' + - pre-commit ; extra == 'dev' + - spglib[docs] ; extra == 'doc' + - spglib[test] ; extra == 'testing' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl + name: pluggy + version: 1.6.0 + sha256: e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746 + requires_dist: + - pre-commit ; extra == 'dev' + - tox ; extra == 'dev' + - pytest ; extra == 'testing' + - pytest-benchmark ; extra == 'testing' + - coverage ; extra == 'testing' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/55/5f/25bdec773905bff0ff6cf35ca73b17bd05593b4f87bd8c5fa43705f7167d/chardet-7.4.3-cp313-cp313-macosx_11_0_arm64.whl + name: chardet + version: 7.4.3 + sha256: 4fbff1907925b0c5a1064cffb5e040cd5e338585c9c552625f30de6bc2f3107a + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/56/5b/53ca0fd447f73423c7dc59d34e523530ef434481a3d18808ff7537ad33ec/svglib-1.5.1.tar.gz + name: svglib + version: 1.5.1 + sha256: 3ae765d3a9409ee60c0fb4d24c2deb6a80617aa927054f5bcd7fc98f0695e587 + requires_dist: + - reportlab + - lxml + - tinycss2>=0.6.0 + - cssselect2>=0.2.0 + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/56/6d/0d9848617b9f753b87f214f1c682592f7ca42de085f564352f10f0843026/ipywidgets-8.1.8-py3-none-any.whl + name: ipywidgets + version: 8.1.8 + sha256: ecaca67aed704a338f88f67b1181b58f821ab5dc89c1f0f5ef99db43c1c2921e + requires_dist: + - comm>=0.1.3 + - ipython>=6.1.0 + - traitlets>=4.3.1 + - widgetsnbextension~=4.0.14 + - jupyterlab-widgets~=3.0.15 + - jsonschema ; extra == 'test' + - ipykernel ; extra == 'test' + - pytest>=3.6.0 ; extra == 'test' + - pytest-cov ; extra == 'test' + - pytz ; extra == 'test' + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/57/fb/48649b4971cde70d817cf97a2a2fdc0b4d8308569f1dd2f2611959d2e0cf/numpy-2.4.4-cp313-cp313-win_amd64.whl + name: numpy + version: 2.4.4 + sha256: 5c70f1cc1c4efbe316a572e2d8b9b9cc44e89b95f79ca3331553fbb63716e2bf + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/5a/38/f1efc72736fb39e75802efcb5e2ba8eb02c7ee3687940047234bc607e3a5/python_bidi-0.6.9-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + name: python-bidi + version: 0.6.9 + sha256: 6cf6941062f0589291a396b3d81636b7e35a99105098bc147c48e077c0fb4b45 + requires_dist: + - nox ; extra == 'dev' + - pytest ; extra == 'dev' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/5a/56/21b27c560c13822ed93133f08aa6372c53a8e067f11fbed37b4adcdac922/multidict-6.7.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + name: multidict + version: 6.7.1 + sha256: 439cbebd499f92e9aa6793016a8acaa161dfa749ae86d20960189f5398a19144 + requires_dist: + - typing-extensions>=4.1.0 ; python_full_version < '3.11' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl + name: mkdocs-material-extensions + version: 1.3.1 + sha256: adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31 + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/5b/58/3ce96251560107b381cbd6e8413c483bbb1228a6b919fa8652b0d4090e7f/scipy-1.17.1-cp313-cp313-macosx_14_0_arm64.whl + name: scipy + version: 1.17.1 + sha256: 7ff200bf9d24f2e4d5dc6ee8c3ac64d739d3a89e2326ba68aaf6c4a2b838fd7d + requires_dist: + - numpy>=1.26.4,<2.7 + - pytest>=8.0.0 ; extra == 'test' + - pytest-cov ; extra == 'test' + - pytest-timeout ; extra == 'test' + - pytest-xdist ; extra == 'test' + - asv ; extra == 'test' + - mpmath ; extra == 'test' + - gmpy2 ; extra == 'test' + - threadpoolctl ; extra == 'test' + - scikit-umfpack ; extra == 'test' + - pooch ; extra == 'test' + - hypothesis>=6.30 ; extra == 'test' + - array-api-strict>=2.3.1 ; extra == 'test' + - cython ; extra == 'test' + - meson ; extra == 'test' + - ninja ; sys_platform != 'emscripten' and extra == 'test' + - sphinx>=5.0.0,<8.2.0 ; extra == 'doc' + - intersphinx-registry ; extra == 'doc' + - pydata-sphinx-theme>=0.15.2 ; extra == 'doc' + - sphinx-copybutton ; extra == 'doc' + - sphinx-design>=0.4.0 ; extra == 'doc' + - matplotlib>=3.5 ; extra == 'doc' + - numpydoc ; extra == 'doc' + - jupytext ; extra == 'doc' + - myst-nb>=1.2.0 ; extra == 'doc' + - pooch ; extra == 'doc' + - jupyterlite-sphinx>=0.19.1 ; extra == 'doc' + - jupyterlite-pyodide-kernel ; extra == 'doc' + - linkify-it-py ; extra == 'doc' + - tabulate ; extra == 'doc' + - click<8.3.0 ; extra == 'dev' + - spin ; extra == 'dev' + - mypy==1.10.0 ; extra == 'dev' + - typing-extensions ; extra == 'dev' + - types-psutil ; extra == 'dev' + - pycodestyle ; extra == 'dev' + - ruff>=0.12.0 ; extra == 'dev' + - cython-lint>=0.12.2 ; extra == 'dev' + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/5b/69/93b34728cc386efdde0c342f8c680b9187dea7beb7adaf6b58a0713be101/mpld3-0.5.12-py3-none-any.whl + name: mpld3 + version: 0.5.12 + sha256: bea31799a4041029a906f53f2662bbf1c49903e0c0bc712b412354158ec7cf54 + requires_dist: + - jinja2 + - matplotlib +- pypi: https://files.pythonhosted.org/packages/5c/01/6cb4d63c6b6933be4b7945b2f64638336420f04ea71ca5b9a7539c008bc5/scippnexus-26.1.1-py3-none-any.whl + name: scippnexus + version: 26.1.1 + sha256: 899a0a5e71291b7809d902c17b6c74addf5a805397eabcec557491ff74eead12 + requires_dist: + - scipp>=24.2.0 + - scipy>=1.10.0 + - h5py>=3.12 + - pytest>=7.0 ; extra == 'test' + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/5c/a5/a8c7562ec39f2647245b52ea4aeb13b5b125b3f48c0c152e9ebce7047a0a/pycifrw-5.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl + name: pycifrw + version: 5.0.1 + sha256: 65a90ee34e46e6be20834493382585277d12daaa8d1a145cb158c163b3d9fc1c + requires_dist: + - prettytable + - ply + - numpy +- pypi: https://files.pythonhosted.org/packages/5d/a6/e9b8f8a3e99602792b01fa7d0a731737615ab56d8bfd0b52935a0ef88b85/chardet-7.4.3-cp311-cp311-win_amd64.whl + name: chardet + version: 7.4.3 + sha256: ccc1f83ab4bcfb901cf39e0c4ba6bc6e726fc6264735f10e24ceb5cb47387578 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/5d/ba/459f18c16f2b3fc1a1ca871f72f07d70c07bf768ad0a507a698b8052ac58/msgpack-1.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + name: msgpack + version: 1.1.2 + sha256: fac4be746328f90caa3cd4bc67e6fe36ca2bf61d5c6eb6d895b6527e3f05071e + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/5e/52/42855bf626868413f761addd574acc6195880ae247a5346477a4361c3acb/pandas-3.0.2-cp313-cp313-win_amd64.whl + name: pandas + version: 3.0.2 + sha256: 07a10f5c36512eead51bc578eb3354ad17578b22c013d89a796ab5eee90cd991 + requires_dist: + - numpy>=1.26.0 ; python_full_version < '3.14' + - numpy>=2.3.3 ; python_full_version >= '3.14' + - python-dateutil>=2.8.2 + - tzdata ; sys_platform == 'win32' + - tzdata ; sys_platform == 'emscripten' + - hypothesis>=6.116.0 ; extra == 'test' + - pytest>=8.3.4 ; extra == 'test' + - pytest-xdist>=3.6.1 ; extra == 'test' + - pyarrow>=13.0.0 ; extra == 'pyarrow' + - bottleneck>=1.4.2 ; extra == 'performance' + - numba>=0.60.0 ; extra == 'performance' + - numexpr>=2.10.2 ; extra == 'performance' + - scipy>=1.14.1 ; extra == 'computation' + - xarray>=2024.10.0 ; extra == 'computation' + - fsspec>=2024.10.0 ; extra == 'fss' + - s3fs>=2024.10.0 ; extra == 'aws' + - gcsfs>=2024.10.0 ; extra == 'gcp' + - odfpy>=1.4.1 ; extra == 'excel' + - openpyxl>=3.1.5 ; extra == 'excel' + - python-calamine>=0.3.0 ; extra == 'excel' + - pyxlsb>=1.0.10 ; extra == 'excel' + - xlrd>=2.0.1 ; extra == 'excel' + - xlsxwriter>=3.2.0 ; extra == 'excel' + - pyarrow>=13.0.0 ; extra == 'parquet' + - pyarrow>=13.0.0 ; extra == 'feather' + - pyiceberg>=0.8.1 ; extra == 'iceberg' + - tables>=3.10.1 ; extra == 'hdf5' + - pyreadstat>=1.2.8 ; extra == 'spss' + - sqlalchemy>=2.0.36 ; extra == 'postgresql' + - psycopg2>=2.9.10 ; extra == 'postgresql' + - adbc-driver-postgresql>=1.2.0 ; extra == 'postgresql' + - sqlalchemy>=2.0.36 ; extra == 'mysql' + - pymysql>=1.1.1 ; extra == 'mysql' + - sqlalchemy>=2.0.36 ; extra == 'sql-other' + - adbc-driver-postgresql>=1.2.0 ; extra == 'sql-other' + - adbc-driver-sqlite>=1.2.0 ; extra == 'sql-other' + - beautifulsoup4>=4.12.3 ; extra == 'html' + - html5lib>=1.1 ; extra == 'html' + - lxml>=5.3.0 ; extra == 'html' + - lxml>=5.3.0 ; extra == 'xml' + - matplotlib>=3.9.3 ; extra == 'plot' + - jinja2>=3.1.5 ; extra == 'output-formatting' + - tabulate>=0.9.0 ; extra == 'output-formatting' + - pyqt5>=5.15.9 ; extra == 'clipboard' + - qtpy>=2.4.2 ; extra == 'clipboard' + - zstandard>=0.23.0 ; extra == 'compression' + - pytz>=2024.2 ; extra == 'timezone' + - adbc-driver-postgresql>=1.2.0 ; extra == 'all' + - adbc-driver-sqlite>=1.2.0 ; extra == 'all' + - beautifulsoup4>=4.12.3 ; extra == 'all' + - bottleneck>=1.4.2 ; extra == 'all' + - fastparquet>=2024.11.0 ; extra == 'all' + - fsspec>=2024.10.0 ; extra == 'all' + - gcsfs>=2024.10.0 ; extra == 'all' + - html5lib>=1.1 ; extra == 'all' + - hypothesis>=6.116.0 ; extra == 'all' + - jinja2>=3.1.5 ; extra == 'all' + - lxml>=5.3.0 ; extra == 'all' + - matplotlib>=3.9.3 ; extra == 'all' + - numba>=0.60.0 ; extra == 'all' + - numexpr>=2.10.2 ; extra == 'all' + - odfpy>=1.4.1 ; extra == 'all' + - openpyxl>=3.1.5 ; extra == 'all' + - psycopg2>=2.9.10 ; extra == 'all' + - pyarrow>=13.0.0 ; extra == 'all' + - pyiceberg>=0.8.1 ; extra == 'all' + - pymysql>=1.1.1 ; extra == 'all' + - pyqt5>=5.15.9 ; extra == 'all' + - pyreadstat>=1.2.8 ; extra == 'all' + - pytest>=8.3.4 ; extra == 'all' + - pytest-xdist>=3.6.1 ; extra == 'all' + - python-calamine>=0.3.0 ; extra == 'all' + - pytz>=2024.2 ; extra == 'all' + - pyxlsb>=1.0.10 ; extra == 'all' + - qtpy>=2.4.2 ; extra == 'all' + - scipy>=1.14.1 ; extra == 'all' + - s3fs>=2024.10.0 ; extra == 'all' + - sqlalchemy>=2.0.36 ; extra == 'all' + - tables>=3.10.1 ; extra == 'all' + - tabulate>=0.9.0 ; extra == 'all' + - xarray>=2024.10.0 ; extra == 'all' + - xlrd>=2.0.1 ; extra == 'all' + - xlsxwriter>=3.2.0 ; extra == 'all' + - zstandard>=0.23.0 ; extra == 'all' + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/5e/5d/3bccad330292946f97962df9d5f2d3ae129cce6e212732a781e856b91e07/lxml-6.1.0-cp311-cp311-macosx_10_9_universal2.whl + name: lxml + version: 6.1.0 + sha256: cec05be8c876f92a5aa07b01d60bbb4d11cfbdd654cad0561c0d7b5c043a61b9 + requires_dist: + - cssselect>=0.7 ; extra == 'cssselect' + - html5lib ; extra == 'html5' + - beautifulsoup4 ; extra == 'htmlsoup' + - lxml-html-clean ; extra == 'html-clean' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/5f/4b/6157f24ca425b89fe2eb7e7be642375711ab671135be21e6faa100f7448c/contourpy-1.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + name: contourpy + version: 1.3.3 + sha256: 51e79c1f7470158e838808d4a996fa9bac72c498e93d8ebe5119bc1e6becb0db + requires_dist: + - numpy>=1.25 + - furo ; extra == 'docs' + - sphinx>=7.2 ; extra == 'docs' + - sphinx-copybutton ; extra == 'docs' + - bokeh ; extra == 'bokeh' + - selenium ; extra == 'bokeh' + - contourpy[bokeh,docs] ; extra == 'mypy' + - bokeh ; extra == 'mypy' + - docutils-stubs ; extra == 'mypy' + - mypy==1.17.0 ; extra == 'mypy' + - types-pillow ; extra == 'mypy' + - contourpy[test-no-images] ; extra == 'test' + - matplotlib ; extra == 'test' + - pillow ; extra == 'test' + - pytest ; extra == 'test-no-images' + - pytest-cov ; extra == 'test-no-images' + - pytest-rerunfailures ; extra == 'test-no-images' + - pytest-xdist ; extra == 'test-no-images' + - wurlitzer ; extra == 'test-no-images' + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/60/54/5011adb56853caabfd90686c2e543d1e3c76a8ef2755809b7e12e3f3583b/scipp-26.3.1-cp311-cp311-macosx_14_0_arm64.whl + name: scipp + version: 26.3.1 + sha256: 67d275fc83b062053df9aa7ce3af4d2205109c2bc3ab22467bcd73ceb0a83df2 + requires_dist: + - numpy>=2 + - pytest ; extra == 'test' + - matplotlib ; extra == 'test' + - beautifulsoup4 ; extra == 'test' + - ipython ; extra == 'test' + - h5py ; extra == 'extra' + - scipy>=1.7.0 ; extra == 'extra' + - graphviz ; extra == 'extra' + - pooch ; extra == 'extra' + - plopp ; extra == 'extra' + - matplotlib ; extra == 'extra' + - scipp[extra] ; extra == 'all' + - ipympl ; extra == 'all' + - ipython ; extra == 'all' + - ipywidgets ; extra == 'all' + - jupyterlab ; extra == 'all' + - jupyterlab-widgets ; extra == 'all' + - jupyter-nbextensions-configurator ; extra == 'all' + - nodejs ; extra == 'all' + - pythreejs ; extra == 'all' + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/61/2b/e260d50e64690d2a9e405d52ccd18a63c286c5088937dd0107cb23eb3195/diffpy_utils-3.7.2-py3-none-any.whl + name: diffpy-utils + version: 3.7.2 + sha256: 6100600736791a8e4638e3dd476704f4dabe3cab75bcb5c60c83c16a2032519a + requires_dist: + - numpy + - xraydb + - scipy + requires_python: '>=3.10,<3.15' +- pypi: https://files.pythonhosted.org/packages/63/d9/9e14bc7564bf92d5ffa801ae5fac819ce74b925dfb55e3ebde61a3bbad3e/matplotlib-3.10.9-cp313-cp313-macosx_11_0_arm64.whl + name: matplotlib + version: 3.10.9 + sha256: b1b745c489cd1a77a0dc1120a05dc87af9798faebc913601feb8c73d89bf2d1e + requires_dist: + - contourpy>=1.0.1 + - cycler>=0.10 + - fonttools>=4.22.0 + - kiwisolver>=1.3.1 + - numpy>=1.23 + - packaging>=20.0 + - pillow>=8 + - pyparsing>=3 + - python-dateutil>=2.7 + - meson-python>=0.13.1,<0.17.0 ; extra == 'dev' + - pybind11>=2.13.2,!=2.13.3 ; extra == 'dev' + - setuptools-scm>=7,<10 ; extra == 'dev' + - setuptools>=64 ; extra == 'dev' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/64/2e/ba6b404a8a0e7c954cdf0fdd2b21723dc41def60f16b69b9b1cbb2e22d91/crysfml-0.6.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + name: crysfml + version: 0.6.2 + sha256: 734e2180b2c427e77cc9cf9cee4a3651ad563adab28dfe79051f004489f8546c + requires_dist: + - numpy + requires_python: '>=3.11,<3.15' +- pypi: https://files.pythonhosted.org/packages/64/a8/c09fbe44b12fa919c5bfe0afb71e60d1231a7dc93405e54c30496c57c9d3/arviz-1.1.0-py3-none-any.whl + name: arviz + version: 1.1.0 + sha256: 87ebd21ce052f30d21f932b4166fc31b91c0bc7443f8da7fed3518b342267010 + requires_dist: + - arviz-base>=1.1.0,<1.2.0 + - arviz-stats[xarray]>=1.1.0,<1.2.0 + - arviz-plots>=1.1.0,<1.2.0 + - arviz-plots[bokeh] ; extra == 'bokeh' + - build ; extra == 'check' + - pre-commit ; extra == 'check' + - h5netcdf ; extra == 'doc' + - h5py ; extra == 'doc' + - jupyter-sphinx ; extra == 'doc' + - matplotlib ; extra == 'doc' + - myst-parser[linkify] ; extra == 'doc' + - myst-nb ; extra == 'doc' + - pydata-sphinx-theme>=0.13 ; extra == 'doc' + - sphinx>=5 ; extra == 'doc' + - sphinx-copybutton ; extra == 'doc' + - sphinx-design ; extra == 'doc' + - sphinx-notfound-page ; extra == 'doc' + - sphinxcontrib-youtube ; extra == 'doc' + - sphinx-togglebutton ; extra == 'doc' + - sphobjinv ; extra == 'doc' + - arviz-base[h5netcdf] ; extra == 'h5netcdf' + - arviz-plots[matplotlib] ; extra == 'matplotlib' + - arviz-base[netcdf4] ; extra == 'netcdf4' + - arviz-plots[plotly] ; extra == 'plotly' + - pytest ; extra == 'test' + - arviz-base[zarr] ; extra == 'zarr' + requires_python: '>=3.12' +- pypi: https://files.pythonhosted.org/packages/66/40/7732d648ab9d069a46e686043241f01206348e2bbf128daea85be4d6414b/coverage-7.13.5-cp313-cp313-win_amd64.whl + name: coverage + version: 7.13.5 + sha256: 631efb83f01569670a5e866ceb80fe483e7c159fac6f167e6571522636104a0b + requires_dist: + - tomli ; python_full_version <= '3.11' and extra == 'toml' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/66/fe/b1e10b08d287f518994f1e2ff9b6d26f0adeecd8dd7d533b01bab29a3eda/yarl-1.23.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + name: yarl + version: 1.23.0 + sha256: 34b6cf500e61c90f305094911f9acc9c86da1a05a7a3f5be9f68817043f486e4 + requires_dist: + - idna>=2.0 + - multidict>=4.0 + - propcache>=0.2.1 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/67/ee/21d4e8536afd1a328f01b359b4d3997b291ffd35a237c877b331c1c3b71c/pillow-12.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + name: pillow + version: 12.2.0 + sha256: eedf4b74eda2b5a4b2b2fb4c006d6295df3bf29e459e198c90ea48e130dc75c3 + requires_dist: + - furo ; extra == 'docs' + - olefile ; extra == 'docs' + - sphinx>=8.2 ; extra == 'docs' + - sphinx-autobuild ; extra == 'docs' + - sphinx-copybutton ; extra == 'docs' + - sphinx-inline-tabs ; extra == 'docs' + - sphinxext-opengraph ; extra == 'docs' + - olefile ; extra == 'fpx' + - olefile ; extra == 'mic' + - arro3-compute ; extra == 'test-arrow' + - arro3-core ; extra == 'test-arrow' + - nanoarrow ; extra == 'test-arrow' + - pyarrow ; extra == 'test-arrow' + - check-manifest ; extra == 'tests' + - coverage>=7.4.2 ; extra == 'tests' + - defusedxml ; extra == 'tests' + - markdown2 ; extra == 'tests' + - olefile ; extra == 'tests' + - packaging ; extra == 'tests' + - pyroma>=5 ; extra == 'tests' + - pytest ; extra == 'tests' + - pytest-cov ; extra == 'tests' + - pytest-timeout ; extra == 'tests' + - pytest-xdist ; extra == 'tests' + - trove-classifiers>=2024.10.12 ; extra == 'tests' + - defusedxml ; extra == 'xmp' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/69/42/836b6f3cd7f3e5fa10a1f1a5420447c17966044c8fbf589cc0452d5502db/pillow-12.2.0-cp311-cp311-win_amd64.whl + name: pillow + version: 12.2.0 + sha256: 6e6b2a0c538fc200b38ff9eb6628228b77908c319a005815f2dde585a0664b60 + requires_dist: + - furo ; extra == 'docs' + - olefile ; extra == 'docs' + - sphinx>=8.2 ; extra == 'docs' + - sphinx-autobuild ; extra == 'docs' + - sphinx-copybutton ; extra == 'docs' + - sphinx-inline-tabs ; extra == 'docs' + - sphinxext-opengraph ; extra == 'docs' + - olefile ; extra == 'fpx' + - olefile ; extra == 'mic' + - arro3-compute ; extra == 'test-arrow' + - arro3-core ; extra == 'test-arrow' + - nanoarrow ; extra == 'test-arrow' + - pyarrow ; extra == 'test-arrow' + - check-manifest ; extra == 'tests' + - coverage>=7.4.2 ; extra == 'tests' + - defusedxml ; extra == 'tests' + - markdown2 ; extra == 'tests' + - olefile ; extra == 'tests' + - packaging ; extra == 'tests' + - pyroma>=5 ; extra == 'tests' + - pytest ; extra == 'tests' + - pytest-cov ; extra == 'tests' + - pytest-timeout ; extra == 'tests' + - pytest-xdist ; extra == 'tests' + - trove-classifiers>=2024.10.12 ; extra == 'tests' + - defusedxml ; extra == 'xmp' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/69/d1/705e6c19b437a4105bf3b9ae7945fcfc3ad2abb73d14bae0a3f2d58b305b/arviz_base-1.1.0-py3-none-any.whl + name: arviz-base + version: 1.1.0 + sha256: 4f97016f697751038f45d144331a1830c921f0ebc2739d5df343120fba453e83 + requires_dist: + - numpy>=2 + - xarray>=2024.11.0 + - typing-extensions>=3.10 + - lazy-loader>=0.4 + - build ; extra == 'check' + - pre-commit ; extra == 'check' + - docstub==0.4 ; extra == 'check' + - mypy ; extra == 'check' + - pre-commit ; extra == 'ci' + - cloudpickle ; extra == 'ci' + - sphinx-book-theme ; extra == 'doc' + - myst-parser[linkify] ; extra == 'doc' + - myst-nb ; extra == 'doc' + - sphinx-copybutton ; extra == 'doc' + - numpydoc ; extra == 'doc' + - sphinx>=5 ; extra == 'doc' + - sphinx-design ; extra == 'doc' + - jupyter-sphinx ; extra == 'doc' + - h5netcdf[h5py] ; extra == 'doc' + - h5netcdf[h5py] ; extra == 'h5netcdf' + - netcdf4 ; extra == 'netcdf4' + - xarray!=2025.8.0 ; extra == 'test' + - pytest ; extra == 'test' + - pytest-cov ; extra == 'test' + - scipy ; extra == 'test' + - zarr ; extra == 'zarr' + requires_python: '>=3.12' +- pypi: https://files.pythonhosted.org/packages/6a/00/d26b61e82163a59334e9f2ae31ba95ac01922da3792f090e11762d4422e8/rlpycairo-0.4.0-py3-none-any.whl + name: rlpycairo + version: 0.4.0 + sha256: 3ce83825d5761c03bc3571c7db12a336ad51417e63189e3512d11b8922576aa9 + requires_dist: + - pycairo>=1.20.0 + - freetype-py>=2.3 + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/6c/25/4f103d1bedb3593718713b3f743df7b3ff3fc68d36d6666c30265ef59c8a/ase-3.28.0-py3-none-any.whl + name: ase + version: 3.28.0 + sha256: 0e24056302d7307b7247f90de281de15e3031c14cf400bedb1116c3b0d0e50b8 + requires_dist: + - numpy>=1.21.6 + - scipy>=1.8.1 + - matplotlib>=3.5.2 + - sphinx ; extra == 'docs' + - sphinx-book-theme ; extra == 'docs' + - sphinxcontrib-video ; extra == 'docs' + - sphinx-gallery ; extra == 'docs' + - pillow ; extra == 'docs' + - pytest>=7.4.0 ; extra == 'test' + - pytest-xdist>=3.2.0 ; extra == 'test' + - spglib>=1.9 ; extra == 'spglib' + - mypy ; extra == 'lint' + - ruff ; extra == 'lint' + - types-docutils ; extra == 'lint' + - types-pymysql ; extra == 'lint' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/6c/dd/a834df6482147d48e225a49515aabc28974ad5a4ca3215c18a882565b028/html5lib-1.1-py2.py3-none-any.whl + name: html5lib + version: '1.1' + sha256: 0d78f8fde1c230e99fe37986a60526d7049ed4bf8a9fadbad5f00e22e58e041d + requires_dist: + - six>=1.9 + - webencodings + - genshi ; extra == 'all' + - chardet>=2.2 ; extra == 'all' + - lxml ; platform_python_implementation == 'CPython' and extra == 'all' + - chardet>=2.2 ; extra == 'chardet' + - genshi ; extra == 'genshi' + - lxml ; platform_python_implementation == 'CPython' and extra == 'lxml' + requires_python: '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*' +- pypi: https://files.pythonhosted.org/packages/6d/93/01273a1b8d8454d45f2e18b3d6098c7be13a0864a55fbd0ebda7815c201a/svglib-1.6.0-py3-none-any.whl + name: svglib + version: 1.6.0 + sha256: 9aea8e2e81cbbf9c844460e4c7dc90e0a06aea7983bc201975ccd279d7b2d194 + requires_dist: + - cssselect2>=0.2.0 + - lxml>=6.0.0 + - reportlab>=4.4.3 + - rlpycairo>=0.4.0 + - tinycss2>=0.6.0 + - mypy>=1.18.1 ; extra == 'dev' + - pre-commit>=4.3.0 ; extra == 'dev' + - pytest-cov>=7.0.0 ; extra == 'dev' + - pytest-runner>=6.0.1 ; extra == 'dev' + - pytest>=8.3.5 ; extra == 'dev' + - ruff>=0.13.0 ; extra == 'dev' + - tox>=4.30.2 ; extra == 'dev' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/6d/ee/18146b7757ed4976276b9c9819108adbc73c5aad636e5353e20746b73069/scipy-1.17.1-cp311-cp311-macosx_14_0_arm64.whl + name: scipy + version: 1.17.1 + sha256: a3472cfbca0a54177d0faa68f697d8ba4c80bbdc19908c3465556d9f7efce9ee + requires_dist: + - numpy>=1.26.4,<2.7 + - pytest>=8.0.0 ; extra == 'test' + - pytest-cov ; extra == 'test' + - pytest-timeout ; extra == 'test' + - pytest-xdist ; extra == 'test' + - asv ; extra == 'test' + - mpmath ; extra == 'test' + - gmpy2 ; extra == 'test' + - threadpoolctl ; extra == 'test' + - scikit-umfpack ; extra == 'test' + - pooch ; extra == 'test' + - hypothesis>=6.30 ; extra == 'test' + - array-api-strict>=2.3.1 ; extra == 'test' + - cython ; extra == 'test' + - meson ; extra == 'test' + - ninja ; sys_platform != 'emscripten' and extra == 'test' + - sphinx>=5.0.0,<8.2.0 ; extra == 'doc' + - intersphinx-registry ; extra == 'doc' + - pydata-sphinx-theme>=0.15.2 ; extra == 'doc' + - sphinx-copybutton ; extra == 'doc' + - sphinx-design>=0.4.0 ; extra == 'doc' + - matplotlib>=3.5 ; extra == 'doc' + - numpydoc ; extra == 'doc' + - jupytext ; extra == 'doc' + - myst-nb>=1.2.0 ; extra == 'doc' + - pooch ; extra == 'doc' + - jupyterlite-sphinx>=0.19.1 ; extra == 'doc' + - jupyterlite-pyodide-kernel ; extra == 'doc' + - linkify-it-py ; extra == 'doc' + - tabulate ; extra == 'doc' + - click<8.3.0 ; extra == 'dev' + - spin ; extra == 'dev' + - mypy==1.10.0 ; extra == 'dev' + - typing-extensions ; extra == 'dev' + - types-psutil ; extra == 'dev' + - pycodestyle ; extra == 'dev' + - ruff>=0.12.0 ; extra == 'dev' + - cython-lint>=0.12.2 ; extra == 'dev' + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/6e/7d/77752d819ede528c664ecf4de974b17b049e054a14537b74b53c8568014f/arabic_reshaper-3.0.1-py3-none-any.whl + name: arabic-reshaper + version: 3.0.1 + sha256: 41c5adc2420f85758eada7e880251c4b6a2adbd83377bd27e5d4eba71f648bc7 + requires_dist: + - fonttools>=4.0 ; extra == 'with-fonttools' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/6e/94/be70f8ee9c45f2f62b39a1f0e9303bc20e138a8f3b8e50ffd89498e177e1/mkdocstrings-1.0.4-py3-none-any.whl + name: mkdocstrings + version: 1.0.4 + sha256: 63464b4b29053514f32a1dbbf604e52876d5e638111b0c295ab7ed3cac73ca9b + requires_dist: + - jinja2>=3.1 + - markdown>=3.6 + - markupsafe>=1.1 + - mkdocs>=1.6 + - mkdocs-autorefs>=1.4 + - pymdown-extensions>=6.3 + - mkdocstrings-crystal>=0.3.4 ; extra == 'crystal' + - mkdocstrings-python-legacy>=0.2.1 ; extra == 'python-legacy' + - mkdocstrings-python>=1.16.2 ; extra == 'python' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/6e/ef/0e8f1fe32f8a53dd26bdd1f9347efe0778b0fddf62789ea683f4cc7d787d/frozenlist-1.8.0-cp311-cp311-macosx_11_0_arm64.whl + name: frozenlist + version: 1.8.0 + sha256: fa47e444b8ba08fffd1c18e8cdb9a75db1b6a27f17507522834ad13ed5922b93 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/71/43/905a14a8b17fdb1ccb58d282454490662d2cb89a6bfec26af6d3520da5ec/pillow-12.2.0-cp313-cp313-macosx_11_0_arm64.whl + name: pillow + version: 12.2.0 + sha256: 56b25336f502b6ed02e889f4ece894a72612fe885889a6e8c4c80239ff6e5f5f + requires_dist: + - furo ; extra == 'docs' + - olefile ; extra == 'docs' + - sphinx>=8.2 ; extra == 'docs' + - sphinx-autobuild ; extra == 'docs' + - sphinx-copybutton ; extra == 'docs' + - sphinx-inline-tabs ; extra == 'docs' + - sphinxext-opengraph ; extra == 'docs' + - olefile ; extra == 'fpx' + - olefile ; extra == 'mic' + - arro3-compute ; extra == 'test-arrow' + - arro3-core ; extra == 'test-arrow' + - nanoarrow ; extra == 'test-arrow' + - pyarrow ; extra == 'test-arrow' + - check-manifest ; extra == 'tests' + - coverage>=7.4.2 ; extra == 'tests' + - defusedxml ; extra == 'tests' + - markdown2 ; extra == 'tests' + - olefile ; extra == 'tests' + - packaging ; extra == 'tests' + - pyroma>=5 ; extra == 'tests' + - pytest ; extra == 'tests' + - pytest-cov ; extra == 'tests' + - pytest-timeout ; extra == 'tests' + - pytest-xdist ; extra == 'tests' + - trove-classifiers>=2024.10.12 ; extra == 'tests' + - defusedxml ; extra == 'xmp' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-py3-none-any.whl + name: partd + version: 1.4.2 + sha256: 978e4ac767ec4ba5b86c6eaa52e5a2a3bc748a2ca839e8cc798f1cc6ce6efb0f + requires_dist: + - locket + - toolz + - numpy>=1.20.0 ; extra == 'complete' + - pandas>=1.3 ; extra == 'complete' + - pyzmq ; extra == 'complete' + - blosc ; extra == 'complete' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/74/07/1ed8277f8653c40ebc65985180b007879f6a836c525b3885dcc6448ae6cb/msgpack-1.1.2-cp313-cp313-win_amd64.whl + name: msgpack + version: 1.1.2 + sha256: a465f0dceb8e13a487e54c07d04ae3ba131c7c5b95e2612596eafde1dccf64a9 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/76/8e/56ccb09c7232a55403a7637caa21922f3b65901a37f5e8bdb405d0de0946/mike-2.2.0-py3-none-any.whl + name: mike + version: 2.2.0 + sha256: e1f4981c1152eec7c2490a3401142292cc47d686194188416db2648fdfe1d040 + requires_dist: + - jinja2>=2.7 + - mkdocs~=1.0 + - pyparsing>=3.0 + - pyyaml>=5.1 + - pyyaml-env-tag + - verspec + - importlib-metadata ; python_full_version < '3.10' + - importlib-resources ; python_full_version < '3.10' + - coverage ; extra == 'dev' + - flake8-quotes ; extra == 'dev' + - flake8>=3.0 ; extra == 'dev' + - shtab ; extra == 'dev' + - coverage ; extra == 'test' + - flake8-quotes ; extra == 'test' + - flake8>=3.0 ; extra == 'test' + - shtab ; extra == 'test' +- pypi: https://files.pythonhosted.org/packages/77/6f/4615353e016799f80fa52ccb270a843c413b22361fadda2589b2922fb9b0/llvmlite-0.47.0-cp313-cp313-macosx_12_0_arm64.whl + name: llvmlite + version: 0.47.0 + sha256: a3c6a735d4e1041808434f9d440faa3d78d9b4af2ee64d05a66f351883b6ceec + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/78/3c/2a612b95ddbb9a6bdcb47b7a93c4884f74c6ff22356b2f7b213b16e65c35/pycifstar-0.3.0.tar.gz + name: pycifstar + version: 0.3.0 + sha256: 5892fdf16c83372ee5f32557127d5f36e14b0bbe520883a4e2e70365382f70ed +- pypi: https://files.pythonhosted.org/packages/79/37/14a4579049c1eb673afd0de0cb4842982acd55b9ce2643e763db858bcea0/numba-0.65.1-cp313-cp313-macosx_12_0_arm64.whl + name: numba + version: 0.65.1 + sha256: 1735c15c1134a5108b4d6a5c77fc0947924ea066a738dc09a52008c13df9cad3 + requires_dist: + - llvmlite>=0.47.0.dev0,<0.48 + - numpy>=1.22 + - numpy>=1.22,<2.5 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/79/76/55cd7186f498ed080a18440c9013011eb548f77ae1b297206d030eb1180a/multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl + name: multidict + version: 6.7.1 + sha256: 935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445 + requires_dist: + - typing-extensions>=4.1.0 ; python_full_version < '3.11' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/79/db/e28c1b83e3680740aa78925f5fb2ae4d16207207419ad75ea9fe604f8676/matplotlib-3.10.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + name: matplotlib + version: 3.10.9 + sha256: 8e436d155fa8a3399dc62683f8f5d0e2e50d25d0144a73edd73f82eec8f4abfb + requires_dist: + - contourpy>=1.0.1 + - cycler>=0.10 + - fonttools>=4.22.0 + - kiwisolver>=1.3.1 + - numpy>=1.23 + - packaging>=20.0 + - pillow>=8 + - pyparsing>=3 + - python-dateutil>=2.7 + - meson-python>=0.13.1,<0.17.0 ; extra == 'dev' + - pybind11>=2.13.2,!=2.13.3 ; extra == 'dev' + - setuptools-scm>=7,<10 ; extra == 'dev' + - setuptools>=64 ; extra == 'dev' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/79/fe/b14d806894cf05178f1e77d0d619f071db50cf698bc654c54f9241223bcf/scipp-26.3.1-cp313-cp313-macosx_14_0_arm64.whl + name: scipp + version: 26.3.1 + sha256: 8dfe8adedb5cba05acaaea15e3b6fe1820ac2f497e87c1e581ba4be9d82c53bb + requires_dist: + - numpy>=2 + - pytest ; extra == 'test' + - matplotlib ; extra == 'test' + - beautifulsoup4 ; extra == 'test' + - ipython ; extra == 'test' + - h5py ; extra == 'extra' + - scipy>=1.7.0 ; extra == 'extra' + - graphviz ; extra == 'extra' + - pooch ; extra == 'extra' + - plopp ; extra == 'extra' + - matplotlib ; extra == 'extra' + - scipp[extra] ; extra == 'all' + - ipympl ; extra == 'all' + - ipython ; extra == 'all' + - ipywidgets ; extra == 'all' + - jupyterlab ; extra == 'all' + - jupyterlab-widgets ; extra == 'all' + - jupyter-nbextensions-configurator ; extra == 'all' + - nodejs ; extra == 'all' + - pythreejs ; extra == 'all' + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/7a/84/266e8da36879c6edcd37b02b547e2d9ecdfea776be49598e75696e3316e1/yarl-1.23.0-cp313-cp313-win_amd64.whl + name: yarl + version: 1.23.0 + sha256: baaf55442359053c7d62f6f8413a62adba3205119bcb6f49594894d8be47e5e3 + requires_dist: + - idna>=2.0 + - multidict>=4.0 + - propcache>=0.2.1 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/7b/67/b1944235474aac3f0b0e1b232ce49547f9f9461ca4b943df1b88da5d3f1d/bumps-1.0.4-py3-none-any.whl + name: bumps + version: 1.0.4 + sha256: 78b8cfaf9fbcbf2fd77f6d4a2f8c906b0e03a794804ba6caf64d56d6f6cce4d4 + requires_dist: + - numpy + - scipy + - h5py + - dill + - cloudpickle + - matplotlib + - blinker + - aiohttp + - python-socketio + - plotly + - mpld3 + - msgpack + - uncertainties + - build ; extra == 'dev' + - pre-commit ; extra == 'dev' + - pytest ; extra == 'dev' + - pytest-cov ; extra == 'dev' + - ruff ; extra == 'dev' + - wheel ; extra == 'dev' + - setuptools ; extra == 'dev' + - sphinx ; extra == 'dev' + - versioningit ; extra == 'dev' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/7e/0d/271aace3342157c64700c9ff4c59c7b392f3dbab393692e8db6fbe7ab96c/matplotlib-3.10.9-cp311-cp311-win_amd64.whl + name: matplotlib + version: 3.10.9 + sha256: d730e984eddf56974c3e72b6129c7ca462ac38dc624338f4b0b23eb23ecba00f + requires_dist: + - contourpy>=1.0.1 + - cycler>=0.10 + - fonttools>=4.22.0 + - kiwisolver>=1.3.1 + - numpy>=1.23 + - packaging>=20.0 + - pillow>=8 + - pyparsing>=3 + - python-dateutil>=2.7 + - meson-python>=0.13.1,<0.17.0 ; extra == 'dev' + - pybind11>=2.13.2,!=2.13.3 ; extra == 'dev' + - setuptools-scm>=7,<10 ; extra == 'dev' + - setuptools>=64 ; extra == 'dev' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/7e/a5/0521aa32c1ddf3aa1e71dcc466be0b7db2771907a13f18cddaa45967d97b/aiohttp-3.13.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + name: aiohttp + version: 3.13.5 + sha256: 3a807cabd5115fb55af198b98178997a5e0e57dead43eb74a93d9c07d6d4a7dc + requires_dist: + - aiohappyeyeballs>=2.5.0 + - aiosignal>=1.4.0 + - async-timeout>=4.0,<6.0 ; python_full_version < '3.11' + - attrs>=17.3.0 + - frozenlist>=1.1.1 + - multidict>=4.5,<7.0 + - propcache>=0.2.0 + - yarl>=1.17.0,<2.0 + - aiodns>=3.3.0 ; extra == 'speedups' + - brotli>=1.2 ; platform_python_implementation == 'CPython' and extra == 'speedups' + - brotlicffi>=1.2 ; platform_python_implementation != 'CPython' and extra == 'speedups' + - backports-zstd ; python_full_version < '3.14' and platform_python_implementation == 'CPython' and extra == 'speedups' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/80/46/bddc13df6c2a40741e0cc7865bb1c9ed4796b6760bd04ce5fae3928ef917/kiwisolver-1.5.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + name: kiwisolver + version: 1.5.0 + sha256: 2517e24d7315eb51c10664cdb865195df38ab74456c677df67bb47f12d088a27 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl + name: pre-commit + version: 4.6.0 + sha256: e2cf246f7299edcabcf15f9b0571fdce06058527f0a06535068a86d38089f29b + requires_dist: + - cfgv>=2.0.0 + - identify>=1.0.0 + - nodeenv>=0.11.1 + - pyyaml>=5.1 + - virtualenv>=20.10.0 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl + name: filelock + version: 3.29.0 + sha256: 96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl + name: rich + version: 15.0.0 + sha256: 33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb + requires_dist: + - ipywidgets>=7.5.1,<9 ; extra == 'jupyter' + - markdown-it-py>=2.2.0 + - pygments>=2.13.0,<3.0.0 + requires_python: '>=3.9.0' +- pypi: https://files.pythonhosted.org/packages/82/9b/cf7e6f157c53d2c8bc0165580350c53939aea3b2a8390ec8e07f0adb82de/scippneutron-26.4.1-py3-none-any.whl + name: scippneutron + version: 26.4.1 + sha256: 3b9865ebdb7923eb25739b7cda624555d45275341000f099c1dcf371b1dd7e35 + requires_dist: + - python-dateutil>=2.8 + - email-validator>=2 + - h5py>=3.12 + - lazy-loader>=0.4 + - mpltoolbox>=24.6.0 + - numpy>=1.20 + - plopp>=26.4.1 + - pydantic>=2 + - scipp>=24.7.0 + - scippnexus>=23.11.0 + - scipy>=1.7.0 + - scipp[all]>=23.7.0 ; extra == 'all' + - pooch>=1.5 ; extra == 'all' + - hypothesis>=6.100 ; extra == 'test' + - ipympl>0.9.0 ; extra == 'test' + - ipykernel>6.30 ; extra == 'test' + - pace-neutrons>=0.3 ; extra == 'test' + - pooch>=1.5 ; extra == 'test' + - psutil>=5.0 ; extra == 'test' + - pytest>=7.0 ; extra == 'test' + - pytest-xdist>=3.0 ; extra == 'test' + - pythreejs>=2.4.1 ; extra == 'test' + - sciline>=25.1.0 ; extra == 'test' + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/83/04/28a41024ccbd67467380b6fb440ae916c1e4f25e2cd4c63abe6835ac566e/msgpack-1.1.2-cp311-cp311-macosx_11_0_arm64.whl + name: msgpack + version: 1.1.2 + sha256: 283ae72fc89da59aa004ba147e8fc2f766647b1251500182fac0350d8af299c0 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/83/81/bdd4bfabe70b7c9a8c0716a722ced4ebd27311afd1f4800cd405d3229c1b/pycifrw-5.0.1-cp313-cp313-macosx_11_0_arm64.whl + name: pycifrw + version: 5.0.1 + sha256: e9ad2fdb4fca6398ed5ae50c19908bf6238434893b68d0125bda79a54a03d708 + requires_dist: + - prettytable + - ply + - numpy +- 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/6f/cc2b231dc78d8c3aaa674a676db190b8f8071c50134af8f8cf39b9b8e8df/pydoclint-0.8.3-py3-none-any.whl + name: pydoclint + version: 0.8.3 + sha256: 5fc9b82d0d515afce0908cb70e8ff695a68b19042785c248c4f227ad66b4a164 + requires_dist: + - click>=8.1.0 + - docstring-parser-fork>=0.0.12 + - tomli>=2.0.1 ; python_full_version < '3.11' + - flake8>=4 ; extra == 'flake8' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/87/ec/e38ce072e724fd7add6243613f8d1810da084f54175353d25ccf9f9c7e5a/aiohttp-3.13.5-cp311-cp311-macosx_11_0_arm64.whl + name: aiohttp + version: 3.13.5 + sha256: e7d2f8616f0ff60bd332022279011776c3ac0faa0f1b463f7bb12326fbc97a1c + requires_dist: + - aiohappyeyeballs>=2.5.0 + - aiosignal>=1.4.0 + - async-timeout>=4.0,<6.0 ; python_full_version < '3.11' + - attrs>=17.3.0 + - frozenlist>=1.1.1 + - multidict>=4.5,<7.0 + - propcache>=0.2.0 + - yarl>=1.17.0,<2.0 + - aiodns>=3.3.0 ; extra == 'speedups' + - brotli>=1.2 ; platform_python_implementation == 'CPython' and extra == 'speedups' + - brotlicffi>=1.2 ; platform_python_implementation != 'CPython' and extra == 'speedups' + - backports-zstd ; python_full_version < '3.14' and platform_python_implementation == 'CPython' and extra == 'speedups' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl + name: mkdocs-get-deps + version: 0.2.2 + sha256: e7878cbeac04860b8b5e0ca31d3abad3df9411a75a32cde82f8e44b6c16ff650 + requires_dist: + - importlib-metadata>=4.3 ; python_full_version < '3.10' + - mergedeep>=1.3.4 + - platformdirs>=2.2.0 + - pyyaml>=5.1 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/88/39/23ff32561ec8d45a4d48578b4d241369d9270dc50926c017570e60893701/fonttools-4.62.1-cp311-cp311-macosx_10_9_universal2.whl + name: fonttools + version: 4.62.1 + sha256: 40975849bac44fb0b9253d77420c6d8b523ac4dcdcefeff6e4d706838a5b80f7 + requires_dist: + - lxml>=4.0 ; extra == 'lxml' + - brotli>=1.0.1 ; platform_python_implementation == 'CPython' and extra == 'woff' + - brotlicffi>=0.8.0 ; platform_python_implementation != 'CPython' and extra == 'woff' + - zopfli>=0.1.4 ; extra == 'woff' + - unicodedata2>=17.0.0 ; python_full_version < '3.15' and extra == 'unicode' + - lz4>=1.7.4.2 ; extra == 'graphite' + - scipy ; platform_python_implementation != 'PyPy' and extra == 'interpolatable' + - munkres ; platform_python_implementation == 'PyPy' and extra == 'interpolatable' + - pycairo ; extra == 'interpolatable' + - matplotlib ; extra == 'plot' + - sympy ; extra == 'symfont' + - xattr ; sys_platform == 'darwin' and extra == 'type1' + - skia-pathops>=0.5.0 ; extra == 'pathops' + - uharfbuzz>=0.45.0 ; extra == 'repacker' + - lxml>=4.0 ; extra == 'all' + - brotli>=1.0.1 ; platform_python_implementation == 'CPython' and extra == 'all' + - brotlicffi>=0.8.0 ; platform_python_implementation != 'CPython' and extra == 'all' + - zopfli>=0.1.4 ; extra == 'all' + - unicodedata2>=17.0.0 ; python_full_version < '3.15' and extra == 'all' + - lz4>=1.7.4.2 ; extra == 'all' + - scipy ; platform_python_implementation != 'PyPy' and extra == 'all' + - munkres ; platform_python_implementation == 'PyPy' and extra == 'all' + - pycairo ; extra == 'all' + - matplotlib ; extra == 'all' + - sympy ; extra == 'all' + - xattr ; sys_platform == 'darwin' and extra == 'all' + - skia-pathops>=0.5.0 ; extra == 'all' + - uharfbuzz>=0.45.0 ; extra == 'all' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl + name: cloudpickle + version: 3.1.2 + sha256: 9acb47f6afd73f60dc1df93bb801b472f05ff42fa6c84167d25cb206be1fbf4a + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl + name: nodeenv + version: 1.10.0 + sha256: 5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827 + requires_python: '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*' +- pypi: https://files.pythonhosted.org/packages/89/a4/92380f7ca60f99ebae761936bc48a72a639e8a47b29050615eef757cb2a7/propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl + name: propcache + version: 0.4.1 + sha256: cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/8a/06/2c1bd1ee9eee3e65b0b6395dcf960e71b11576995eae3b4ab9ec63d89bea/essreduce-26.4.1-py3-none-any.whl + name: essreduce + version: 26.4.1 + sha256: 1758a18fffca9c7c2a6fa9547cf87bf45f9d52fc3ccbdffcf7524f71bc060424 + requires_dist: + - sciline>=25.11.0 + - scipp>=26.3.1 + - scippneutron>=25.11.1 + - scippnexus>=25.6.0 + - graphviz>=0.20 ; extra == 'test' + - ipywidgets>=8.1 ; extra == 'test' + - matplotlib>=3.10.7 ; extra == 'test' + - numba>=0.63 ; extra == 'test' + - pooch>=1.9.0 ; extra == 'test' + - pytest>=7.0 ; extra == 'test' + - scipy>=1.14 ; extra == 'test' + - tof>=25.12.0 ; extra == 'test' + - autodoc-pydantic ; extra == 'docs' + - graphviz>=0.20 ; extra == 'docs' + - ipykernel ; extra == 'docs' + - ipython!=8.7.0 ; extra == 'docs' + - ipywidgets>=8.1 ; extra == 'docs' + - myst-parser ; extra == 'docs' + - nbsphinx ; extra == 'docs' + - numba>=0.63 ; extra == 'docs' + - plopp ; extra == 'docs' + - pydata-sphinx-theme>=0.14 ; extra == 'docs' + - sphinx>=7 ; extra == 'docs' + - sphinx-autodoc-typehints ; extra == 'docs' + - sphinx-copybutton ; extra == 'docs' + - sphinx-design ; extra == 'docs' + - tof>=25.12.0 ; extra == 'docs' + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/8a/17/4402d0d14ccf1dfc70932600b68097fbbf9c898a4871d2cbbe79c7801a32/matplotlib-3.10.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + name: matplotlib + version: 3.10.9 + sha256: 8f3bcac1ca5ed000a6f4337d47ba67dfddf37ed6a46c15fd7f014997f7bf865f + requires_dist: + - contourpy>=1.0.1 + - cycler>=0.10 + - fonttools>=4.22.0 + - kiwisolver>=1.3.1 + - numpy>=1.23 + - packaging>=20.0 + - pillow>=8 + - pyparsing>=3 + - python-dateutil>=2.7 + - meson-python>=0.13.1,<0.17.0 ; extra == 'dev' + - pybind11>=2.13.2,!=2.13.3 ; extra == 'dev' + - setuptools-scm>=7,<10 ; extra == 'dev' + - setuptools>=64 ; extra == 'dev' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/8a/17/a3918541fd0ddefe024a69de6d16aa7b46d36ac19562adaa63c7fa180eff/greenlet-3.5.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl + name: greenlet + version: 3.5.0 + sha256: 2094acd54b272cb6eae8c03dd87b3fa1820a4cef18d6889c378d503500a1dc13 + requires_dist: + - sphinx ; extra == 'docs' + - furo ; extra == 'docs' + - objgraph ; extra == 'test' + - psutil ; extra == 'test' + - setuptools ; extra == 'test' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/8a/46/7e2d150dd574039c2f8d09c4e102bb90a6cb6634be4040fc29f212fc42bf/refnx-0.1.63-cp311-abi3-win_amd64.whl + name: refnx + version: 0.1.63 + sha256: 3a12accac6e469fd2c0ef49357301299f6b2e4a9ecdfdfb90b592dad59214eb9 + requires_dist: + - numpy>=2.0 + - scipy + - orsopy + - ipython ; extra == 'all' + - ipywidgets ; extra == 'all' + - traitlets ; extra == 'all' + - matplotlib ; extra == 'all' + - xlrd ; extra == 'all' + - h5py ; extra == 'all' + - jupyter ; extra == 'all' + - tqdm ; extra == 'all' + - pymc ; extra == 'all' + - pytensor ; extra == 'all' + - attrs ; extra == 'all' + - pandas ; extra == 'all' + - pyparsing ; extra == 'all' + - periodictable ; extra == 'all' + - pyqt6 ; extra == 'all' + - qtpy ; extra == 'all' + - corner ; extra == 'all' + - numba ; extra == 'all' + - pytest ; extra == 'test' + - uncertainties ; extra == 'test' + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/8a/a1/8d812e53a5da1687abb10445275d41a8b13adb781bbf7196ddbcf8d88505/lazy_loader-0.5-py3-none-any.whl + name: lazy-loader + version: '0.5' + sha256: ab0ea149e9c554d4ffeeb21105ac60bed7f3b4fd69b1d2360a4add51b170b005 + requires_dist: + - packaging + - pytest>=8.0 ; extra == 'test' + - pytest-cov>=5.0 ; extra == 'test' + - coverage[toml]>=7.2 ; extra == 'test' + - pre-commit==4.3.0 ; extra == 'lint' + - changelist==0.5 ; extra == 'dev' + - spin==0.15 ; extra == 'dev' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/8a/dc/df98c095978fa6ee7b9a9387d1d58cbb3d232d0e69ad169a4ce784bde4fd/numpy-2.4.4-cp311-cp311-macosx_14_0_arm64.whl + name: numpy + version: 2.4.4 + sha256: 86b6f55f5a352b48d7fbfd2dbc3d5b780b2d79f4d3c121f33eb6efb22e9a2015 + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/8d/c0/fdf9d3ee103ce66a55f0532835ad5e154226c5222423c6636ba049dc42fc/traittypes-0.2.3-py2.py3-none-any.whl + name: traittypes + version: 0.2.3 + sha256: 49016082ce740d6556d9bb4672ee2d899cd14f9365f17cbb79d5d96b47096d4e + requires_dist: + - traitlets>=4.2.2 + - numpy ; extra == 'test' + - pandas ; extra == 'test' + - xarray ; extra == 'test' + - pytest ; extra == 'test' +- pypi: https://files.pythonhosted.org/packages/8e/63/981401c5680c1eb30893f00a19641ac80db5d1e7086c62cb4b13ed813038/lxml-6.1.0-cp313-cp313-win_amd64.whl + name: lxml + version: 6.1.0 + sha256: 4a1503c56e4e2b38dc76f2f2da7bae69670c0f1933e27cfa34b2fa5876410b16 + requires_dist: + - cssselect>=0.7 ; extra == 'cssselect' + - html5lib ; extra == 'html5' + - beautifulsoup4 ; extra == 'htmlsoup' + - lxml-html-clean ; extra == 'html-clean' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/8f/5e/f1e1dd319e35e962a4e00b33150a8868b6329cc1d19fd533436ba5488f09/uncertainties-3.2.3-py3-none-any.whl + name: uncertainties + version: 3.2.3 + sha256: 313353900d8f88b283c9bad81e7d2b2d3d4bcc330cbace35403faaed7e78890a + requires_dist: + - numpy ; extra == 'arrays' + - pytest ; extra == 'test' + - pytest-codspeed ; extra == 'test' + - pytest-cov ; extra == 'test' + - scipy ; extra == 'test' + - sphinx ; extra == 'doc' + - sphinx-copybutton ; extra == 'doc' + - python-docs-theme ; extra == 'doc' + - uncertainties[arrays,doc,test] ; extra == 'all' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl + name: paginate + version: 0.5.7 + sha256: b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591 + requires_dist: + - pytest ; extra == 'dev' + - tox ; extra == 'dev' + - black ; extra == 'lint' +- pypi: https://files.pythonhosted.org/packages/90/ad/cba91b3bcf04073e4d1655a5c1710ef3f457f56f7d1b79dcc3d72f4dd912/plotly-6.7.0-py3-none-any.whl + name: plotly + version: 6.7.0 + sha256: ac8aca1c25c663a59b5b9140a549264a5badde2e057d79b8c772ae2920e32ff0 + requires_dist: + - narwhals>=1.15.1 + - packaging + - anywidget ; extra == 'dev' + - build ; extra == 'dev' + - colorcet ; extra == 'dev' + - fiona<=1.9.6 ; python_full_version < '3.9' and extra == 'dev' + - geopandas ; extra == 'dev' + - inflect ; extra == 'dev' + - jupyterlab ; extra == 'dev' + - kaleido>=1.1.0 ; extra == 'dev' + - numpy>=1.22 ; extra == 'dev' + - orjson ; extra == 'dev' + - pandas ; extra == 'dev' + - pdfrw ; extra == 'dev' + - pillow ; extra == 'dev' + - plotly-geo ; extra == 'dev' + - polars[timezone] ; extra == 'dev' + - pyarrow ; extra == 'dev' + - pyshp ; extra == 'dev' + - pytest ; extra == 'dev' + - pytz ; extra == 'dev' + - requests ; extra == 'dev' + - ruff==0.11.12 ; extra == 'dev' + - scikit-image ; extra == 'dev' + - scipy ; extra == 'dev' + - shapely ; extra == 'dev' + - statsmodels ; extra == 'dev' + - vaex ; python_full_version < '3.10' and extra == 'dev' + - xarray ; extra == 'dev' + - build ; extra == 'dev-build' + - jupyterlab ; extra == 'dev-build' + - pytest ; extra == 'dev-build' + - requests ; extra == 'dev-build' + - ruff==0.11.12 ; extra == 'dev-build' + - pytest ; extra == 'dev-core' + - requests ; extra == 'dev-core' + - ruff==0.11.12 ; extra == 'dev-core' + - anywidget ; extra == 'dev-optional' + - build ; extra == 'dev-optional' + - colorcet ; extra == 'dev-optional' + - fiona<=1.9.6 ; python_full_version < '3.9' and extra == 'dev-optional' + - geopandas ; extra == 'dev-optional' + - inflect ; extra == 'dev-optional' + - jupyterlab ; extra == 'dev-optional' + - kaleido>=1.1.0 ; extra == 'dev-optional' + - numpy>=1.22 ; extra == 'dev-optional' + - orjson ; extra == 'dev-optional' + - pandas ; extra == 'dev-optional' + - pdfrw ; extra == 'dev-optional' + - pillow ; extra == 'dev-optional' + - plotly-geo ; extra == 'dev-optional' + - polars[timezone] ; extra == 'dev-optional' + - pyarrow ; extra == 'dev-optional' + - pyshp ; extra == 'dev-optional' + - pytest ; extra == 'dev-optional' + - pytz ; extra == 'dev-optional' + - requests ; extra == 'dev-optional' + - ruff==0.11.12 ; extra == 'dev-optional' + - scikit-image ; extra == 'dev-optional' + - scipy ; extra == 'dev-optional' + - shapely ; extra == 'dev-optional' + - statsmodels ; extra == 'dev-optional' + - vaex ; python_full_version < '3.10' and extra == 'dev-optional' + - xarray ; extra == 'dev-optional' + - numpy>=1.22 ; extra == 'express' + - kaleido>=1.1.0 ; extra == 'kaleido' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/90/df/cbd75ba033d712a7bbe8efff2a764c15a3904003f78ed3c19f259c91a4a4/refnx-0.1.63-cp311-abi3-macosx_11_0_arm64.whl + name: refnx + version: 0.1.63 + sha256: 9bc88eba956e97892805d2fb18a7268780f943c5d74feb5d1c58f5ae90699fa2 + requires_dist: + - numpy>=2.0 + - scipy + - orsopy + - ipython ; extra == 'all' + - ipywidgets ; extra == 'all' + - traitlets ; extra == 'all' + - matplotlib ; extra == 'all' + - xlrd ; extra == 'all' + - h5py ; extra == 'all' + - jupyter ; extra == 'all' + - tqdm ; extra == 'all' + - pymc ; extra == 'all' + - pytensor ; extra == 'all' + - attrs ; extra == 'all' + - pandas ; extra == 'all' + - pyparsing ; extra == 'all' + - periodictable ; extra == 'all' + - pyqt6 ; extra == 'all' + - qtpy ; extra == 'all' + - corner ; extra == 'all' + - numba ; extra == 'all' + - pytest ; extra == 'test' + - uncertainties ; extra == 'test' + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/91/4c/e0ce1ef95d4000ebc1c11801f9b944fa5910ecc15b5e351865763d8657f8/graphviz-0.21-py3-none-any.whl + name: graphviz + version: '0.21' + sha256: 54f33de9f4f911d7e84e4191749cac8cc5653f815b06738c54db9a15ab8b1e42 + requires_dist: + - build ; extra == 'dev' + - wheel ; extra == 'dev' + - twine ; extra == 'dev' + - flake8 ; extra == 'dev' + - flake8-pyproject ; extra == 'dev' + - pep8-naming ; extra == 'dev' + - tox>=3 ; extra == 'dev' + - pytest>=7,<8.1 ; extra == 'test' + - pytest-mock>=3 ; extra == 'test' + - pytest-cov ; extra == 'test' + - coverage ; extra == 'test' + - sphinx>=5,<7 ; extra == 'docs' + - sphinx-autodoc-typehints ; extra == 'docs' + - sphinx-rtd-theme>=0.2.5 ; extra == 'docs' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/91/88/b55b3117287a8540b76dbdd87733808d4d01c8067a3b339408c250bb3600/typeguard-4.5.1-py3-none-any.whl + name: typeguard + version: 4.5.1 + sha256: 44d2bf329d49a244110a090b55f5f91aa82d9a9834ebfd30bcc73651e4a8cc40 + requires_dist: + - importlib-metadata>=3.6 ; python_full_version < '3.10' + - typing-extensions>=4.14.0 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/92/be/b1afb692be85b947f3401375851484496134c5554e67e822c35f28bf2fbc/coverage-7.13.5-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl + name: coverage + version: 7.13.5 + sha256: ec10e2a42b41c923c2209b846126c6582db5e43a33157e9870ba9fb70dc7854b + requires_dist: + - tomli ; python_full_version <= '3.11' and extra == 'toml' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/92/dc/c385f38f2c2433333345a82926c6bfa5ecfff3ef787201614317b58dd8be/msgpack-1.1.2-cp313-cp313-macosx_11_0_arm64.whl + name: msgpack + version: 1.1.2 + sha256: 42eefe2c3e2af97ed470eec850facbe1b5ad1d6eacdbadc42ec98e7dcf68b4b7 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/93/22/b85eca6fa2ad9491af48c973e4c8cf6b103a73dbb271fe3346949449fca0/yarl-1.23.0-cp311-cp311-win_amd64.whl + name: yarl + version: 1.23.0 + sha256: bf49a3ae946a87083ef3a34c8f677ae4243f5b824bfc4c69672e72b3d6719d46 + requires_dist: + - idna>=2.0 + - multidict>=4.0 + - propcache>=0.2.1 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/93/6e/bd7fbfacca077bc6f34f1a1109800a2c41ab50f4704d3a0507ba41009915/freetype_py-2.5.1-py3-none-win_amd64.whl + name: freetype-py + version: 2.5.1 + sha256: 0b7f8e0342779f65ca13ef8bc103938366fecade23e6bb37cb671c2b8ad7f124 + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/93/ca/d53764f0534ff857239595f090f4cb83b599d226cc326c7de5eb3d802715/xhtml2pdf-0.2.17-py3-none-any.whl + name: xhtml2pdf + version: 0.2.17 + sha256: 61a7ecac829fed518f7dbcb916e9d56bea6e521e02e54644b3d0ca33f0658315 + requires_dist: + - arabic-reshaper>=3.0.0 + - html5lib>=1.1 + - pillow>=8.1.1 + - pyhanko>=0.12.1 + - pyhanko-certvalidator>=0.19.5 + - pypdf>=3.1.0 + - python-bidi>=0.5.0 + - reportlab>=4.0.4,<5 + - svglib>=1.2.1 + - reportlab[pycairo]>=4.0.4,<5 ; extra == 'pycairo' + - reportlab[renderpm]>=4.0.4,<5 ; extra == 'renderpm' + - tomli>=2.0.1 ; python_full_version < '3.11' and extra == 'test' + - tox ; extra == 'test' + - coverage>=5.3 ; extra == 'test' + - sphinx>=6 ; extra == 'docs' + - sphinx-rtd-theme>=0.5.0 ; extra == 'docs' + - sphinx-reredirects>=0.1.3 ; extra == 'docs' + - build ; extra == 'release' + - twine ; extra == 'release' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/93/f7/d00d9b4a0313a6be3a3e0818e6375e15da6d7076f4ae47d1324e7ca986a1/radon-6.0.1-py2.py3-none-any.whl + name: radon + version: 6.0.1 + sha256: 632cc032364a6f8bb1010a2f6a12d0f14bc7e5ede76585ef29dc0cecf4cd8859 + requires_dist: + - mando>=0.6,<0.8 + - colorama==0.4.1 ; python_full_version < '3.5' + - colorama>=0.4.1 ; python_full_version >= '3.5' + - tomli>=2.0.1 ; extra == 'toml' +- pypi: https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl + name: identify + version: 2.6.19 + sha256: 20e6a87f786f768c092a721ad107fc9df0eb89347be9396cadf3f4abbd1fb78a + requires_dist: + - ukkonen ; extra == 'license' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/95/95/685e5a31f6c4cc24826d3a0df9e6a4bcf13d00f90d0c1f1060f13f5c16f8/python_bidi-0.6.9-cp313-cp313-macosx_11_0_arm64.whl + name: python-bidi + version: 0.6.9 + sha256: c3e4445f0fd1e2c16d3efdb28622a38f57ab4b971eb47803a334be477cee173e + requires_dist: + - nox ; extra == 'dev' + - pytest ; extra == 'dev' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/95/da/0d1df507cf574b3f224ccc3d45244c9a1d732c81dcb26b1e8a766ae271a8/scipy-1.17.1-cp311-cp311-win_amd64.whl + name: scipy + version: 1.17.1 + sha256: d30e57c72013c2a4fe441c2fcb8e77b14e152ad48b5464858e07e2ad9fbfceff + requires_dist: + - numpy>=1.26.4,<2.7 + - pytest>=8.0.0 ; extra == 'test' + - pytest-cov ; extra == 'test' + - pytest-timeout ; extra == 'test' + - pytest-xdist ; extra == 'test' + - asv ; extra == 'test' + - mpmath ; extra == 'test' + - gmpy2 ; extra == 'test' + - threadpoolctl ; extra == 'test' + - scikit-umfpack ; extra == 'test' + - pooch ; extra == 'test' + - hypothesis>=6.30 ; extra == 'test' + - array-api-strict>=2.3.1 ; extra == 'test' + - cython ; extra == 'test' + - meson ; extra == 'test' + - ninja ; sys_platform != 'emscripten' and extra == 'test' + - sphinx>=5.0.0,<8.2.0 ; extra == 'doc' + - intersphinx-registry ; extra == 'doc' + - pydata-sphinx-theme>=0.15.2 ; extra == 'doc' + - sphinx-copybutton ; extra == 'doc' + - sphinx-design>=0.4.0 ; extra == 'doc' + - matplotlib>=3.5 ; extra == 'doc' + - numpydoc ; extra == 'doc' + - jupytext ; extra == 'doc' + - myst-nb>=1.2.0 ; extra == 'doc' + - pooch ; extra == 'doc' + - jupyterlite-sphinx>=0.19.1 ; extra == 'doc' + - jupyterlite-pyodide-kernel ; extra == 'doc' + - linkify-it-py ; extra == 'doc' + - tabulate ; extra == 'doc' + - click<8.3.0 ; extra == 'dev' + - spin ; extra == 'dev' + - mypy==1.10.0 ; extra == 'dev' + - typing-extensions ; extra == 'dev' + - types-psutil ; extra == 'dev' + - pycodestyle ; extra == 'dev' + - ruff>=0.12.0 ; extra == 'dev' + - cython-lint>=0.12.2 ; extra == 'dev' + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/96/5d/0c59079aa7ef34980a5925a06a90ad2b7c94e486c194b3527d557cabb042/cryspy-0.11.0-py3-none-any.whl + name: cryspy + version: 0.11.0 + sha256: 0b650655a0fbdc3cfcb28826c2ab9fbc5f491e32e1ea9a47d9b75976cd43f26f + requires_dist: + - numpy + - scipy + - pycifstar + - matplotlib +- pypi: https://files.pythonhosted.org/packages/96/b3/650500c2eab4534d98e9166f4298e0f3c69c742afdf24e6eabccd1f16ad8/numba-0.65.1-cp311-cp311-macosx_12_0_arm64.whl + name: numba + version: 0.65.1 + sha256: 7020d74b19cdb8cff16506542fdd510756e28c5e7f3bd0b7f574f0f42272fcd9 + requires_dist: + - llvmlite>=0.47.0.dev0,<0.48 + - numpy>=1.22 + - numpy>=1.22,<2.5 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/96/e4/7adcd9c8362745b2210728f209bfbcf7d91ba868a2c5f40d8b58f54c509b/contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl + name: contourpy + version: 1.3.3 + sha256: d002b6f00d73d69333dac9d0b8d5e84d9724ff9ef044fd63c5986e62b7c9e1b1 + requires_dist: + - numpy>=1.25 + - furo ; extra == 'docs' + - sphinx>=7.2 ; extra == 'docs' + - sphinx-copybutton ; extra == 'docs' + - bokeh ; extra == 'bokeh' + - selenium ; extra == 'bokeh' + - contourpy[bokeh,docs] ; extra == 'mypy' + - bokeh ; extra == 'mypy' + - docutils-stubs ; extra == 'mypy' + - mypy==1.17.0 ; extra == 'mypy' + - types-pillow ; extra == 'mypy' + - contourpy[test-no-images] ; extra == 'test' + - matplotlib ; extra == 'test' + - pillow ; extra == 'test' + - pytest ; extra == 'test-no-images' + - pytest-cov ; extra == 'test-no-images' + - pytest-rerunfailures ; extra == 'test-no-images' + - pytest-xdist ; extra == 'test-no-images' + - wurlitzer ; extra == 'test-no-images' + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/98/4b/9bd370b004b5c9d8045c6c33cf65bae018b27aca550a3f657cdc99acdbd8/contourpy-1.3.3-cp311-cp311-win_amd64.whl + name: contourpy + version: 1.3.3 + sha256: 3519428f6be58431c56581f1694ba8e50626f2dd550af225f82fb5f5814d2a42 + requires_dist: + - numpy>=1.25 + - furo ; extra == 'docs' + - sphinx>=7.2 ; extra == 'docs' + - sphinx-copybutton ; extra == 'docs' + - bokeh ; extra == 'bokeh' + - selenium ; extra == 'bokeh' + - contourpy[bokeh,docs] ; extra == 'mypy' + - bokeh ; extra == 'mypy' + - docutils-stubs ; extra == 'mypy' + - mypy==1.17.0 ; extra == 'mypy' + - types-pillow ; extra == 'mypy' + - contourpy[test-no-images] ; extra == 'test' + - matplotlib ; extra == 'test' + - pillow ; extra == 'test' + - pytest ; extra == 'test-no-images' + - pytest-cov ; extra == 'test-no-images' + - pytest-rerunfailures ; extra == 'test-no-images' + - pytest-xdist ; extra == 'test-no-images' + - wurlitzer ; extra == 'test-no-images' + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/99/31/6cf181011dc738c33bf6ba7aea2e8e1d3c1f71b7dab1942f3054f66f6202/asteval-1.0.8-py3-none-any.whl + name: asteval + version: 1.0.8 + sha256: 6c64385c6ff859a474953c124987c7ee8354d781c76509b2c598741c4d1d28e9 + requires_dist: + - build ; extra == 'dev' + - twine ; extra == 'dev' + - sphinx ; extra == 'doc' + - pytest ; extra == 'test' + - pytest-cov ; extra == 'test' + - coverage ; extra == 'test' + - asteval[dev,doc,test] ; extra == 'all' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/99/37/e8730c3587a65eb5645d4aba2d27aae48e8003614d6aaf15dda67f702f1f/bidict-0.23.1-py3-none-any.whl + name: bidict + version: 0.23.1 + sha256: 5dae8d4d79b552a71cbabc7deb25dfe8ce710b17ff41711e13010ead2abfc3e5 + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl + name: tabulate + version: 0.10.0 + sha256: f0b0622e567335c8fabaaa659f1b33bcb6ddfe2e496071b743aa113f8774f2d3 + requires_dist: + - wcwidth ; extra == 'widechars' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/9a/64/c53487d9f4968045b8afa51aed7ca44f58b2589e772f32745f3744476c82/yarl-1.23.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + name: yarl + version: 1.23.0 + sha256: 99c8a9ed30f4164bc4c14b37a90208836cbf50d4ce2a57c71d0f52c7fb4f7598 + requires_dist: + - idna>=2.0 + - multidict>=4.0 + - propcache>=0.2.1 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/9c/f5/0fb20fb49f8efdcdce6cd8127604ad2c503e754a8f139f5e02b01626523f/aiohttp-3.13.5-cp313-cp313-macosx_11_0_arm64.whl + name: aiohttp + version: 3.13.5 + sha256: a84792f8631bf5a94e52d9cc881c0b824ab42717165a5579c760b830d9392ac9 + requires_dist: + - aiohappyeyeballs>=2.5.0 + - aiosignal>=1.4.0 + - async-timeout>=4.0,<6.0 ; python_full_version < '3.11' + - attrs>=17.3.0 + - frozenlist>=1.1.1 + - multidict>=4.5,<7.0 + - propcache>=0.2.0 + - yarl>=1.17.0,<2.0 + - aiodns>=3.3.0 ; extra == 'speedups' + - brotli>=1.2 ; platform_python_implementation == 'CPython' and extra == 'speedups' + - brotlicffi>=1.2 ; platform_python_implementation != 'CPython' and extra == 'speedups' + - backports-zstd ; python_full_version < '3.14' and platform_python_implementation == 'CPython' and extra == 'speedups' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl + name: pytest-cov + version: 7.1.0 + sha256: a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678 + requires_dist: + - coverage[toml]>=7.10.6 + - pluggy>=1.2 + - pytest>=7 + - process-tests ; extra == 'testing' + - pytest-xdist ; extra == 'testing' + - virtualenv ; extra == 'testing' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/9e/43/53afb8ba17218f19b77c7834128566c5bbb100a0ad9ba2e8e89d089d7079/autopep8-2.3.2-py2.py3-none-any.whl + name: autopep8 + version: 2.3.2 + sha256: ce8ad498672c845a0c3de2629c15b635ec2b05ef8177a6e7c91c74f3e9b51128 + requires_dist: + - pycodestyle>=2.12.0 + - tomli ; python_full_version < '3.11' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl + name: networkx + version: 3.6.1 + sha256: d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762 + requires_dist: + - asv ; extra == 'benchmarking' + - virtualenv ; extra == 'benchmarking' + - numpy>=1.25 ; extra == 'default' + - scipy>=1.11.2 ; extra == 'default' + - matplotlib>=3.8 ; extra == 'default' + - pandas>=2.0 ; extra == 'default' + - pre-commit>=4.1 ; extra == 'developer' + - mypy>=1.15 ; extra == 'developer' + - sphinx>=8.0 ; extra == 'doc' + - pydata-sphinx-theme>=0.16 ; extra == 'doc' + - sphinx-gallery>=0.18 ; extra == 'doc' + - numpydoc>=1.8.0 ; extra == 'doc' + - pillow>=10 ; extra == 'doc' + - texext>=0.6.7 ; extra == 'doc' + - myst-nb>=1.1 ; extra == 'doc' + - intersphinx-registry ; extra == 'doc' + - osmnx>=2.0.0 ; extra == 'example' + - momepy>=0.7.2 ; extra == 'example' + - contextily>=1.6 ; extra == 'example' + - seaborn>=0.13 ; extra == 'example' + - cairocffi>=1.7 ; extra == 'example' + - igraph>=0.11 ; extra == 'example' + - scikit-learn>=1.5 ; extra == 'example' + - iplotx>=0.9.0 ; extra == 'example' + - lxml>=4.6 ; extra == 'extra' + - pygraphviz>=1.14 ; extra == 'extra' + - pydot>=3.0.1 ; extra == 'extra' + - sympy>=1.10 ; extra == 'extra' + - build>=0.10 ; extra == 'release' + - twine>=4.0 ; extra == 'release' + - wheel>=0.40 ; extra == 'release' + - changelist==0.5 ; extra == 'release' + - pytest>=7.2 ; extra == 'test' + - pytest-cov>=4.0 ; extra == 'test' + - pytest-xdist>=3.0 ; extra == 'test' + - pytest-mpl ; extra == 'test-extras' + - pytest-randomly ; extra == 'test-extras' + requires_python: '>=3.11,!=3.14.1' +- pypi: https://files.pythonhosted.org/packages/9f/9b/50835e8fd86073fa7aa921df61b4cebc1f0ff400e4338541675cb72b5507/pycifrw-5.0.1-cp313-cp313-win_amd64.whl + name: pycifrw + version: 5.0.1 + sha256: 8632df76b99932408ab432e09b30aa9a6c390df884a5f34f1e1f76201e625156 + requires_dist: + - prettytable + - ply + - numpy +- pypi: https://files.pythonhosted.org/packages/a0/22/b8d873f6466b20aa563fc9b33acd48dec89a07803ddaa2f1c8ca1cd33126/numba-0.65.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + name: numba + version: 0.65.1 + sha256: c09f49117ef255e1f1c6dad0c7a1ed39868243862a73be5706793241a3755f1b + requires_dist: + - llvmlite>=0.47.0.dev0,<0.48 + - numpy>=1.22 + - numpy>=1.22,<2.5 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl + name: gitdb + version: 4.0.12 + sha256: 67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf + requires_dist: + - smmap>=3.0.1,<6 + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl + name: sympy + version: 1.14.0 + sha256: e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5 + requires_dist: + - mpmath>=1.1.0,<1.4 + - pytest>=7.1.0 ; extra == 'dev' + - hypothesis>=6.70.0 ; extra == 'dev' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/a2/50/59227d06bdc96e23322713c381af4e77420949d8cd8a042c79e0043096cc/llvmlite-0.47.0-cp311-cp311-win_amd64.whl + name: llvmlite + version: 0.47.0 + sha256: c37d6eb7aaabfa83ab9c2ff5b5cdb95a5e6830403937b2c588b7490724e05327 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/a3/58/35da89ee790598a0700ea49b2a66594140f44dec458c07e8e3d4979137fc/ply-3.11-py2.py3-none-any.whl + name: ply + version: '3.11' + sha256: 096f9b8350b65ebd2fd1346b12452efe5b9607f7482813ffca50c22722a807ce +- pypi: https://files.pythonhosted.org/packages/a3/8c/db8e79c4c744ebae1dcf25f7dbcc5d7df912cdbcdf7221e761479e8bd04b/gemmi-0.7.5-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + name: gemmi + version: 0.7.5 + sha256: 750b4d9751aaf1460ac4f0f45308ddced25f47bcf7a30355eb3b1f779f03952a + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/a4/39/3a0ae5b0edb66e61bb0e8bc53a503495cba5892297ae21faf6ba0525e681/varname-1.0.0-py3-none-any.whl + name: varname + version: 1.0.0 + sha256: 1125bfe981c3bbbe56988f5cb85fdcd7cad923b153283c2d464aea8b4c833d51 + requires_dist: + - executing>=2.1 + - typing-extensions>=4.13 ; python_full_version < '3.10' + - asttokens==3.* ; extra == 'all' + - pure-eval==0.* ; extra == 'all' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/a4/ce/3b6fee91c85626eaf769d617f1be9d2e15c1cca027bbdeb2e0d751469355/verspec-0.1.0-py3-none-any.whl + name: verspec + version: 0.1.0 + sha256: 741877d5633cc9464c45a469ae2a31e801e6dbbaa85b9675d481cda100f11c31 + requires_dist: + - coverage ; extra == 'test' + - flake8>=3.7 ; extra == 'test' + - mypy ; extra == 'test' + - pretend ; extra == 'test' + - pytest ; extra == 'test' +- pypi: https://files.pythonhosted.org/packages/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-py3-none-any.whl + name: wsproto + version: 1.3.2 + sha256: 61eea322cdf56e8cc904bd3ad7573359a242ba65688716b0710a5eb12beab584 + requires_dist: + - h11>=0.16.0,<1 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/a8/3a/d0a972b34e1c63e2409413104216cd1caa02c5a37cb668d1687d466c1c45/kiwisolver-1.5.0-cp313-cp313-macosx_11_0_arm64.whl + name: kiwisolver + version: 1.5.0 + sha256: dda366d548e89a90d88a86c692377d18d8bd64b39c1fb2b92cb31370e2896bbd + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/a8/4e/c09876f08fa9faaa5e1178f3d77b7af3f343258689bd6f3b72593b2f74e3/mkdocs_markdownextradata_plugin-0.2.6-py3-none-any.whl + name: mkdocs-markdownextradata-plugin + version: 0.2.6 + sha256: 34dd40870781784c75809596b2d8d879da783815b075336d541de1f150c94242 + requires_dist: + - mkdocs + - pyyaml + requires_python: '>=3.6' +- pypi: https://files.pythonhosted.org/packages/a8/88/802c82060c54bc7dde21eb0033e337838b8181a1323254aa9ec41cbfc3d1/markdown_it_py-4.1.0-py3-none-any.whl + name: markdown-it-py + version: 4.1.0 + sha256: d4939a62a2dd0cd9cb80a191a711ba1d39bac8ed5ef9e9966895b0171c01c46d + requires_dist: + - mdurl~=0.1 + - psutil ; extra == 'benchmarking' + - pytest ; extra == 'benchmarking' + - pytest-benchmark ; extra == 'benchmarking' + - commonmark~=0.9 ; extra == 'compare' + - markdown~=3.4 ; extra == 'compare' + - mistletoe~=1.0 ; extra == 'compare' + - mistune~=3.0 ; extra == 'compare' + - panflute~=2.3 ; extra == 'compare' + - markdown-it-pyrs ; extra == 'compare' + - linkify-it-py>=1,<3 ; extra == 'linkify' + - mdit-py-plugins>=0.5.0 ; extra == 'plugins' + - gprof2dot ; extra == 'profiling' + - mdit-py-plugins>=0.5.0 ; extra == 'rtd' + - myst-parser ; extra == 'rtd' + - pyyaml ; extra == 'rtd' + - sphinx ; extra == 'rtd' + - sphinx-copybutton ; extra == 'rtd' + - sphinx-design ; extra == 'rtd' + - sphinx-book-theme~=1.0 ; extra == 'rtd' + - jupyter-sphinx ; extra == 'rtd' + - ipykernel ; extra == 'rtd' + - coverage ; extra == 'testing' + - pytest ; extra == 'testing' + - pytest-cov ; extra == 'testing' + - pytest-regressions ; extra == 'testing' + - pytest-timeout ; extra == 'testing' + - requests ; extra == 'testing' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/a8/b1/3338e121cbd4c8a126b8ccb1061170c2ce51a53f678c502793ea49c6fd6d/chardet-7.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + name: chardet + version: 7.4.3 + sha256: bfc134b70c846c21ead8e43ada3ae1a805fff732f6922f8abcf2ff27b8f6493d + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/aa/54/0cce26da03a981f949bb8449c9778537f75f5917c172e1d2992ff25cb57d/python_engineio-4.13.1-py3-none-any.whl + name: python-engineio + version: 4.13.1 + sha256: f32ad10589859c11053ad7d9bb3c9695cdf862113bfb0d20bc4d890198287399 + requires_dist: + - simple-websocket>=0.10.0 + - requests>=2.21.0 ; extra == 'client' + - websocket-client>=0.54.0 ; extra == 'client' + - aiohttp>=3.11 ; extra == 'asyncio-client' + - tox ; extra == 'dev' + - sphinx ; extra == 'docs' + - furo ; extra == 'docs' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl + name: execnet + version: 2.1.2 + sha256: 67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec + requires_dist: + - hatch ; extra == 'testing' + - pre-commit ; extra == 'testing' + - pytest ; extra == 'testing' + - tox ; extra == 'testing' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/ab/b5/36c712098e6191d1b4e349304ef73a8d06aed77e56ceaac8c0a306c7bda1/jupyterlab_widgets-3.0.16-py3-none-any.whl + name: jupyterlab-widgets + version: 3.0.16 + sha256: 45fa36d9c6422cf2559198e4db481aa243c7a32d9926b500781c830c80f7ecf8 + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/ac/68/1666e3a4462f8202d836920114fa7a5ee9275d1fa45366d336c551a162dd/coverage-7.13.5-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl + name: coverage + version: 7.13.5 + sha256: 78e696e1cc714e57e8b25760b33a8b1026b7048d270140d25dafe1b0a1ee05a3 + requires_dist: + - tomli ; python_full_version <= '3.11' and extra == 'toml' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/ac/a6/1ca2d290381911c17975ba00353671217bb9ed6c56f395871b274435b020/python_bidi-0.6.9-cp311-cp311-win_amd64.whl + name: python-bidi + version: 0.6.9 + sha256: be28092eef062fad59e7e0402bf2c1db797ccadedd866a53bbd7ddb97e42cac1 + requires_dist: + - nox ; extra == 'dev' + - pytest ; extra == 'dev' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl + name: click + version: 8.3.3 + sha256: a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613 + requires_dist: + - colorama ; sys_platform == 'win32' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/ae/50/06d511cc4b8e0360d3c94af051a768e84b755c5eb031b12adaaab6dec6e5/yarl-1.23.0-cp313-cp313-macosx_11_0_arm64.whl + name: yarl + version: 1.23.0 + sha256: 7c6b9461a2a8b47c65eef63bb1c76a4f1c119618ffa99ea79bc5bb1e46c5821b + requires_dist: + - idna>=2.0 + - multidict>=4.0 + - propcache>=0.2.1 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/ae/81/81755f50eb2478eaf2049728491d4ea4f416c1eb013338682173259efa09/sqlalchemy-2.0.49-cp313-cp313-macosx_11_0_arm64.whl + name: sqlalchemy + version: 2.0.49 + sha256: df2d441bacf97022e81ad047e1597552eb3f83ca8a8f1a1fdd43cd7fe3898120 + requires_dist: + - importlib-metadata ; python_full_version < '3.8' + - greenlet>=1 ; platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64' + - typing-extensions>=4.6.0 + - greenlet>=1 ; extra == 'asyncio' + - mypy>=0.910 ; extra == 'mypy' + - pyodbc ; extra == 'mssql' + - pymssql ; extra == 'mssql-pymssql' + - pyodbc ; extra == 'mssql-pyodbc' + - mysqlclient>=1.4.0 ; extra == 'mysql' + - mysql-connector-python ; extra == 'mysql-connector' + - mariadb>=1.0.1,!=1.1.2,!=1.1.5,!=1.1.10 ; extra == 'mariadb-connector' + - cx-oracle>=8 ; extra == 'oracle' + - oracledb>=1.0.1 ; extra == 'oracle-oracledb' + - psycopg2>=2.7 ; extra == 'postgresql' + - pg8000>=1.29.1 ; extra == 'postgresql-pg8000' + - greenlet>=1 ; extra == 'postgresql-asyncpg' + - asyncpg ; extra == 'postgresql-asyncpg' + - psycopg2-binary ; extra == 'postgresql-psycopg2binary' + - psycopg2cffi ; extra == 'postgresql-psycopg2cffi' + - psycopg>=3.0.7 ; extra == 'postgresql-psycopg' + - psycopg[binary]>=3.0.7 ; extra == 'postgresql-psycopgbinary' + - pymysql ; extra == 'pymysql' + - greenlet>=1 ; extra == 'aiomysql' + - aiomysql>=0.2.0 ; extra == 'aiomysql' + - greenlet>=1 ; extra == 'aioodbc' + - aioodbc ; extra == 'aioodbc' + - greenlet>=1 ; extra == 'asyncmy' + - asyncmy>=0.2.3,!=0.2.4,!=0.2.6 ; extra == 'asyncmy' + - greenlet>=1 ; extra == 'aiosqlite' + - aiosqlite ; extra == 'aiosqlite' + - typing-extensions!=3.10.0.1 ; extra == 'aiosqlite' + - sqlcipher3-binary ; extra == 'sqlcipher' + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/af/46/661159ad844034ba8b3f4e0516215c41e4ee17db4213d13a82227670764f/sciline-25.11.1-py3-none-any.whl + name: sciline + version: 25.11.1 + sha256: 13c378287b8157e819b9b67d7e973c65bc6bdc545a3602d18204c365b0c336f9 + requires_dist: + - cyclebane>=24.6.0 + - pytest ; extra == 'test' + - pytest-randomly>=3 ; extra == 'test' + - dask ; extra == 'test' + - graphviz ; extra == 'test' + - jsonschema ; extra == 'test' + - numpy ; extra == 'test' + - pandas ; extra == 'test' + - pydantic ; extra == 'test' + - rich ; extra == 'test' + - rich ; extra == 'progress' + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/af/78/bc3850efda66712370ebb2a52130111b5cbbdee7a5b9d1672aadfc73234f/orsopy-1.2.3-py2.py3-none-any.whl + name: orsopy + version: 1.2.3 + sha256: 11d3ee9a1185467ba4363b24d6b98b268956ee0fb528aeff7ff106b49f363180 + requires_dist: + - pyyaml>=5.4.1 + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/af/7f/4cd8a92531253f9d7c1bbecd9fa1b472907fb54446ca768c59b531248dc5/coverage-7.13.5-cp311-cp311-win_amd64.whl + name: coverage + version: 7.13.5 + sha256: 258354455f4e86e3e9d0d17571d522e13b4e1e19bf0f8596bcf9476d61e7d8a9 + requires_dist: + - tomli ; python_full_version <= '3.11' and extra == 'toml' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/b0/65/5e088a45d0f43cd814bc5bec521c051d42005a472e804b1a36c48dada09b/h5py-3.16.0-cp313-cp313-macosx_11_0_arm64.whl + name: h5py + version: 3.16.0 + sha256: 42108e93326c50c2810025aade9eac9d6827524cdccc7d4b75a546e5ab308edb + requires_dist: + - numpy>=1.21.2 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/b0/73/6e1b01cbeb458807aa0831742232dbdd1fa92bfa33f52a3f176b4ff3dc11/multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + name: multidict + version: 6.7.1 + sha256: 9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429 + requires_dist: + - typing-extensions>=4.1.0 ; python_full_version < '3.11' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/b1/10/31932775c94a86814f76b41c4a772b52abfb0e6125324f32c6da1196c297/chardet-7.4.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + name: chardet + version: 7.4.3 + sha256: 4c3da294de1a681097848ab58bd3f2771a674f8039d2d87a5538b28856b815e9 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/b1/4f/f71e641e504111a5a74e3a20bc52d01bd86788b22699dd3fee1c63253cf6/virtualenv-21.3.1-py3-none-any.whl + name: virtualenv + version: 21.3.1 + sha256: d1a71cf58f2f9228fff23a1f6ec15d39785c6b32e03658d104974247145edd35 + requires_dist: + - distlib>=0.3.7,<1 + - filelock>=3.24.2,<4 ; python_full_version >= '3.10' + - filelock>=3.16.1,<=3.19.1 ; python_full_version < '3.10' + - importlib-metadata>=6.6 ; python_full_version < '3.8' + - platformdirs>=3.9.1,<5 + - python-discovery>=1.2.2 + - typing-extensions>=4.13.2 ; python_full_version < '3.11' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/b1/8b/88f16936a8e8070a83d36239555227ecd91728f9ef222c5382cda07e0fd6/h5netcdf-1.8.1-py3-none-any.whl + name: h5netcdf + version: 1.8.1 + sha256: a76ed7cfc9b8a8908ea7057c4e57e27307acff1049b7f5ed52db6c2247636879 + requires_dist: + - packaging + - numpy + - h5py ; extra == 'h5py' + - pyfive>=1.0.0 ; extra == 'pyfive' + - h5pyd ; extra == 'h5pyd' + - h5py ; extra == 'test' + - netcdf4 ; extra == 'test' + - pyfive>=1.0.0 ; extra == 'test' + - pytest ; extra == 'test' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/b2/0d/71ceabc14c146ba8ee3804ca7b3d42b1664c8440439de5214d366fec7d3a/yarl-1.23.0-cp311-cp311-macosx_11_0_arm64.whl + name: yarl + version: 1.23.0 + sha256: dc52310451fc7c629e13c4e061cbe2dd01684d91f2f8ee2821b083c58bd72432 + requires_dist: + - idna>=2.0 + - multidict>=4.0 + - propcache>=0.2.1 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/b2/35/e994121b0e90e46134673422dd564623f93304614f5d11886b1b3e06f503/multidict-6.7.1-cp313-cp313-win_amd64.whl + name: multidict + version: 6.7.1 + sha256: 960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5 + requires_dist: + - typing-extensions>=4.1.0 ; python_full_version < '3.11' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/b2/8f/22bf9df92bbff0eb07842b60f7e63bf7675a9742df628437a9f02d09137f/greenlet-3.5.0-cp313-cp313-win_amd64.whl + name: greenlet + version: 3.5.0 + sha256: 728d9667d8f2f586644b748dbd9bb67e50d6a9381767d1357714ea6825bb3bf5 + requires_dist: + - sphinx ; extra == 'docs' + - furo ; extra == 'docs' + - objgraph ; extra == 'test' + - psutil ; extra == 'test' + - setuptools ; extra == 'test' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl + name: mdurl + version: 0.1.2 + sha256: 84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/b3/52/bc858b1665d0dec3a2511f4e6f5c18ea85c0977563d624d597c95d6d0fd7/jupyterquiz-2.9.6.4-py2.py3-none-any.whl + name: jupyterquiz + version: 2.9.6.4 + sha256: f8c4418f6c827454523fc882a30d744b585cb58ac1ae277769c3059d04fc272b +- pypi: https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl + name: markdown-it-py + version: 4.2.0 + sha256: 9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a + requires_dist: + - mdurl~=0.1 + - psutil ; extra == 'benchmarking' + - pytest ; extra == 'benchmarking' + - pytest-benchmark ; extra == 'benchmarking' + - commonmark~=0.9 ; extra == 'compare' + - markdown~=3.4 ; extra == 'compare' + - mistletoe~=1.0 ; extra == 'compare' + - mistune~=3.0 ; extra == 'compare' + - panflute~=2.3 ; extra == 'compare' + - markdown-it-pyrs ; extra == 'compare' + - linkify-it-py>=1,<3 ; extra == 'linkify' + - mdit-py-plugins>=0.5.0 ; extra == 'plugins' + - gprof2dot ; extra == 'profiling' + - mdit-py-plugins>=0.5.0 ; extra == 'rtd' + - myst-parser ; extra == 'rtd' + - pyyaml ; extra == 'rtd' + - sphinx ; extra == 'rtd' + - sphinx-copybutton ; extra == 'rtd' + - sphinx-design ; extra == 'rtd' + - sphinx-book-theme~=1.0 ; extra == 'rtd' + - jupyter-sphinx ; extra == 'rtd' + - ipykernel ; extra == 'rtd' + - coverage ; extra == 'testing' + - pytest ; extra == 'testing' + - pytest-cov ; extra == 'testing' + - pytest-regressions ; extra == 'testing' + - pytest-timeout ; extra == 'testing' + - requests ; extra == 'testing' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/b4/5e/bf11645aebb9af7d8d35927c40d3855816a0855c799e8156eeca8d632c90/diffpy_structure-3.4.0-py3-none-any.whl + name: diffpy-structure + version: 3.4.0 + sha256: bd0f06a96635d80316f51ebc0a08003bdeb2cb48c9bb18cbed1455ac60645e48 + requires_dist: + - numpy + - pycifrw + - diffpy-utils + requires_python: '>=3.12,<3.15' +- pypi: https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl + name: watchdog + version: 6.0.0 + sha256: 20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2 + requires_dist: + - pyyaml>=3.10 ; extra == 'watchmedo' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/b7/ee/e9c95cda829131f71a8dff5ce0406059fd16e591c074414e31ada19ba7c3/validate_pyproject-0.25-py3-none-any.whl + name: validate-pyproject + version: '0.25' + sha256: f9d05e2686beff82f9ea954f582306b036ced3d3feb258c1110f2c2a495b1981 + requires_dist: + - fastjsonschema>=2.16.2,<=3 + - packaging>=24.2 ; extra == 'all' + - trove-classifiers>=2021.10.20 ; extra == 'all' + - tomli>=1.2.1 ; python_full_version < '3.11' and extra == 'all' + - validate-pyproject-schema-store ; extra == 'store' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl + name: dnspython + version: 2.8.0 + sha256: 01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af + requires_dist: + - black>=25.1.0 ; extra == 'dev' + - coverage>=7.0 ; extra == 'dev' + - flake8>=7 ; extra == 'dev' + - hypercorn>=0.17.0 ; extra == 'dev' + - mypy>=1.17 ; extra == 'dev' + - pylint>=3 ; extra == 'dev' + - pytest-cov>=6.2.0 ; extra == 'dev' + - pytest>=8.4 ; extra == 'dev' + - quart-trio>=0.12.0 ; extra == 'dev' + - sphinx-rtd-theme>=3.0.0 ; extra == 'dev' + - sphinx>=8.2.0 ; extra == 'dev' + - twine>=6.1.0 ; extra == 'dev' + - wheel>=0.45.0 ; extra == 'dev' + - cryptography>=45 ; extra == 'dnssec' + - h2>=4.2.0 ; extra == 'doh' + - httpcore>=1.0.0 ; extra == 'doh' + - httpx>=0.28.0 ; extra == 'doh' + - aioquic>=1.2.0 ; extra == 'doq' + - idna>=3.10 ; extra == 'idna' + - trio>=0.30 ; extra == 'trio' + - wmi>=1.5.1 ; sys_platform == 'win32' and extra == 'wmi' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl + name: pyproject-hooks + version: 1.2.0 + sha256: 9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913 + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/bd/63/05d193dbb4b5eec1eca73822d80da98b511f8328ad4ae3ca4caf0f4db91d/numpy-2.4.4-cp311-cp311-win_amd64.whl + name: numpy + version: 2.4.4 + sha256: 6bbe4eb67390b0a0265a2c25458f6b90a409d5d069f1041e6aff1e27e3d9a79e + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/bd/98/ef2b6fe2903e377cbe870c3b2800d62552f1e3dbe81ce49e1923c53d1c5c/h5py-3.16.0-cp313-cp313-manylinux_2_28_x86_64.whl + name: h5py + version: 3.16.0 + sha256: 9300ad32dea9dfc5171f94d5f6948e159ed93e4701280b0f508773b3f582f402 + requires_dist: + - numpy>=1.21.2 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/bd/9a/af61ec03b3116c161fd7a06b9e8a265729a8718458333e8ffbb06d9a3978/numba-0.65.1-cp311-cp311-win_amd64.whl + name: numba + version: 0.65.1 + sha256: df40a5028a975b9ea66f6a2a3f7abbdbd541a863070e34ed367aff21141248e4 + requires_dist: + - llvmlite>=0.47.0.dev0,<0.48 + - numpy>=1.22 + - numpy>=1.22,<2.5 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/be/6c/28f17390b62b8f2f520e2915095b3c94d88681ecf0041e75389d9667f202/kiwisolver-1.5.0-cp311-cp311-win_amd64.whl + name: kiwisolver + version: 1.5.0 + sha256: beb7f344487cdcb9e1efe4b7a29681b74d34c08f0043a327a74da852a6749e7b + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/be/8a/be60e3bbcf513cc5a50f4a3e88e1dcecebb79c1ad607a7222877becaa101/kiwisolver-1.5.0-cp313-cp313-win_amd64.whl + name: kiwisolver + version: 1.5.0 + sha256: 0bf3acf1419fa93064a4c2189ac0b58e3be7872bf6ee6177b0d4c63dc4cea276 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/bf/3b/38ff88b347c3e346cda1d3fc1b65a7aa75d40632228d8b8a5d7b58508c24/h5py-3.16.0-cp311-cp311-macosx_11_0_arm64.whl + name: h5py + version: 3.16.0 + sha256: c3f0a0e136f2e95dd0b67146abb6668af4f1a69c81ef8651a2d316e8e01de447 + requires_dist: + - numpy>=1.21.2 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/bf/50/98b146aea0f1cd7531d25f12bea69fa9ce8d1662124f93fb30dc4511b65e/docstring_parser_fork-0.0.14-py3-none-any.whl + name: docstring-parser-fork + version: 0.0.14 + sha256: 4c544f234ef2cc2749a3df32b70c437d77888b1099143a1ad5454452c574b9af + requires_dist: + - docstring-parser[docs] ; extra == 'dev' + - docstring-parser[test] ; extra == 'dev' + - pre-commit>=2.16.0 ; python_full_version >= '3.9' and extra == 'dev' + - pydoctor>=25.4.0 ; extra == 'docs' + - pytest ; extra == 'test' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/bf/e1/9e8e09ab8fc5c77f228e3271188dc4f569d12692548c42bff81b1fbba5e1/easydiffraction-0.16.0-py3-none-any.whl + name: easydiffraction + version: 0.16.0 + sha256: 2691a1e175974ca79e0ec3c219d92b77f277c38fb3b0b8d25f6f7e99696bf70f + requires_dist: + - asciichartpy + - asteval + - bumps + - colorama + - crysfml + - cryspy + - darkdetect + - dfo-ls + - diffpy-pdffit2 + - diffpy-utils + - essdiffraction + - gemmi + - lmfit + - numpy + - pandas + - plotly + - pooch + - py3dmol + - rich + - scipy + - sympy + - tabulate + - typeguard + - typer + - uncertainties + - varname + - build ; extra == 'dev' + - copier ; extra == 'dev' + - docstripy ; extra == 'dev' + - format-docstring ; extra == 'dev' + - gitpython ; extra == 'dev' + - interrogate ; extra == 'dev' + - jinja2 ; extra == 'dev' + - jupyterquiz ; extra == 'dev' + - jupytext ; extra == 'dev' + - mike ; extra == 'dev' + - mkdocs ; extra == 'dev' + - mkdocs-autorefs ; extra == 'dev' + - mkdocs-jupyter ; extra == 'dev' + - mkdocs-markdownextradata-plugin ; extra == 'dev' + - mkdocs-material ; extra == 'dev' + - mkdocs-plugin-inline-svg ; extra == 'dev' + - mkdocstrings-python ; extra == 'dev' + - nbmake ; extra == 'dev' + - nbqa ; extra == 'dev' + - nbstripout ; extra == 'dev' + - pre-commit ; extra == 'dev' + - pydoclint ; extra == 'dev' + - pytest ; extra == 'dev' + - pytest-benchmark ; extra == 'dev' + - pytest-cov ; extra == 'dev' + - pytest-xdist ; extra == 'dev' + - pyyaml ; extra == 'dev' + - radon ; extra == 'dev' + - ruff ; extra == 'dev' + - spdx-headers ; extra == 'dev' + - validate-pyproject[all] ; extra == 'dev' + - versioningit ; extra == 'dev' + requires_python: '>=3.12' +- pypi: https://files.pythonhosted.org/packages/c0/1b/ac685a8882896acf0f6b31d689e3792199cfe7aba37969fa91da63a7fa27/aiohttp-3.13.5-cp313-cp313-win_amd64.whl + name: aiohttp + version: 3.13.5 + sha256: 69f571de7500e0557801c0b51f4780482c0ec5fe2ac851af5a92cfce1af1cb83 + requires_dist: + - aiohappyeyeballs>=2.5.0 + - aiosignal>=1.4.0 + - async-timeout>=4.0,<6.0 ; python_full_version < '3.11' + - attrs>=17.3.0 + - frozenlist>=1.1.1 + - multidict>=4.5,<7.0 + - propcache>=0.2.0 + - yarl>=1.17.0,<2.0 + - aiodns>=3.3.0 ; extra == 'speedups' + - brotli>=1.2 ; platform_python_implementation == 'CPython' and extra == 'speedups' + - brotlicffi>=1.2 ; platform_python_implementation != 'CPython' and extra == 'speedups' + - backports-zstd ; python_full_version < '3.14' and platform_python_implementation == 'CPython' and extra == 'speedups' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/c0/43/8c7163a596dab4f8be12c190cf467a1e07e4734cf90eebb39f7f5d53fc6a/aiohttp-3.13.5-cp311-cp311-win_amd64.whl + name: aiohttp + version: 3.13.5 + sha256: d3a4834f221061624b8887090637db9ad4f61752001eae37d56c52fddade2dc8 + requires_dist: + - aiohappyeyeballs>=2.5.0 + - aiosignal>=1.4.0 + - async-timeout>=4.0,<6.0 ; python_full_version < '3.11' + - attrs>=17.3.0 + - frozenlist>=1.1.1 + - multidict>=4.5,<7.0 + - propcache>=0.2.0 + - yarl>=1.17.0,<2.0 + - aiodns>=3.3.0 ; extra == 'speedups' + - brotli>=1.2 ; platform_python_implementation == 'CPython' and extra == 'speedups' + - brotlicffi>=1.2 ; platform_python_implementation != 'CPython' and extra == 'speedups' + - backports-zstd ; python_full_version < '3.14' and platform_python_implementation == 'CPython' and extra == 'speedups' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/c1/9c/1236dd7d22ed48527286b613c84e3376ea731b65e6734b6e6a0b4d03744c/gemmi-0.7.5-cp313-cp313-macosx_11_0_arm64.whl + name: gemmi + version: 0.7.5 + sha256: c7d8b08c33fe6ba375223306149092440c69cbfbd55c3d3e3436e5fb315a225d + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/c1/d4/59e74daffcb57a07668852eeeb6035af9f32cbfd7a1d2511f17d2fe6a738/smmap-5.0.3-py3-none-any.whl + name: smmap + version: 5.0.3 + sha256: c106e05d5a61449cf6ba9a1e650227ecfb141590d2a98412103ff35d89fc7b2f + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/c2/14/e2a54fabd4f08cd7af1c07030603c3356b74da07f7cc056e600436edfa17/tzlocal-5.3.1-py3-none-any.whl + name: tzlocal + version: 5.3.1 + sha256: eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d + requires_dist: + - tzdata ; sys_platform == 'win32' + - pytest>=4.3 ; extra == 'devenv' + - pytest-mock>=3.3 ; extra == 'devenv' + - pytest-cov ; extra == 'devenv' + - check-manifest ; extra == 'devenv' + - zest-releaser ; extra == 'devenv' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/c2/20/053aa10bdc39747e1e923ce2d45413075e84f70a136045bb09e5eaca41d3/lxml-6.1.0-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl + name: lxml + version: 6.1.0 + sha256: 363e47283bde87051b821826e71dde47f107e08614e1aa312ba0c5711e77738c + requires_dist: + - cssselect>=0.7 ; extra == 'cssselect' + - html5lib ; extra == 'html5' + - beautifulsoup4 ; extra == 'htmlsoup' + - lxml-html-clean ; extra == 'html-clean' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/c3/d9/a27997f84341fc0dfcdd1fe4179b6ba6c32a7aa880fdb8c514d4dad6fba3/h5py-3.16.0-cp313-cp313-win_amd64.whl + name: h5py + version: 3.16.0 + sha256: 18f2bbcd545e6991412253b98727374c356d67caa920e68dc79eab36bf5fedad + requires_dist: + - numpy>=1.21.2 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/c4/6d/82e65254354ba651dc966775270a9bbc02414a3eb3f1704e6c87dab2ea83/essdiffraction-26.5.1-py3-none-any.whl + name: essdiffraction + version: 26.5.1 + sha256: 8a6c779078c71be250714619214069221ab7968a69580d4e4d3f4b3e9a1a53ad + requires_dist: + - dask>=2022.1.0 + - essreduce>=26.4.0 + - graphviz + - numpy>=2 + - plopp>=26.2.0 + - pythreejs>=2.4.1 + - sciline>=25.4.1 + - scipp>=25.11.0 + - scippneutron>=26.3.0 + - scippnexus>=23.12.0 + - tof>=25.12.0 + - ncrystal[cif]>=4.1.0 + - spglib!=2.7 + - pandas>=2.1.2 ; extra == 'test' + - pooch>=1.5 ; extra == 'test' + - pytest>=7.0 ; extra == 'test' + - ipywidgets>=8.1.7 ; extra == 'test' + - autodoc-pydantic ; extra == 'docs' + - ipykernel ; extra == 'docs' + - ipympl ; extra == 'docs' + - ipython!=8.7.0 ; extra == 'docs' + - myst-parser ; extra == 'docs' + - nbsphinx ; extra == 'docs' + - pandas ; extra == 'docs' + - pooch ; extra == 'docs' + - pydata-sphinx-theme>=0.14 ; extra == 'docs' + - sphinx ; extra == 'docs' + - sphinx-autodoc-typehints ; extra == 'docs' + - sphinx-copybutton ; extra == 'docs' + - sphinx-design ; extra == 'docs' + - sphinxcontrib-bibtex ; extra == 'docs' + - pyarrow ; extra == 'docs' + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/c4/d3/b7da1d5d7dbdc5ef52ed7debd2b484313b832982266905315dad5a0bf0b1/pandas-3.0.2-cp311-cp311-macosx_11_0_arm64.whl + name: pandas + version: 3.0.2 + sha256: dbbd4aa20ca51e63b53bbde6a0fa4254b1aaabb74d2f542df7a7959feb1d760c + requires_dist: + - numpy>=1.26.0 ; python_full_version < '3.14' + - numpy>=2.3.3 ; python_full_version >= '3.14' + - python-dateutil>=2.8.2 + - tzdata ; sys_platform == 'win32' + - tzdata ; sys_platform == 'emscripten' + - hypothesis>=6.116.0 ; extra == 'test' + - pytest>=8.3.4 ; extra == 'test' + - pytest-xdist>=3.6.1 ; extra == 'test' + - pyarrow>=13.0.0 ; extra == 'pyarrow' + - bottleneck>=1.4.2 ; extra == 'performance' + - numba>=0.60.0 ; extra == 'performance' + - numexpr>=2.10.2 ; extra == 'performance' + - scipy>=1.14.1 ; extra == 'computation' + - xarray>=2024.10.0 ; extra == 'computation' + - fsspec>=2024.10.0 ; extra == 'fss' + - s3fs>=2024.10.0 ; extra == 'aws' + - gcsfs>=2024.10.0 ; extra == 'gcp' + - odfpy>=1.4.1 ; extra == 'excel' + - openpyxl>=3.1.5 ; extra == 'excel' + - python-calamine>=0.3.0 ; extra == 'excel' + - pyxlsb>=1.0.10 ; extra == 'excel' + - xlrd>=2.0.1 ; extra == 'excel' + - xlsxwriter>=3.2.0 ; extra == 'excel' + - pyarrow>=13.0.0 ; extra == 'parquet' + - pyarrow>=13.0.0 ; extra == 'feather' + - pyiceberg>=0.8.1 ; extra == 'iceberg' + - tables>=3.10.1 ; extra == 'hdf5' + - pyreadstat>=1.2.8 ; extra == 'spss' + - sqlalchemy>=2.0.36 ; extra == 'postgresql' + - psycopg2>=2.9.10 ; extra == 'postgresql' + - adbc-driver-postgresql>=1.2.0 ; extra == 'postgresql' + - sqlalchemy>=2.0.36 ; extra == 'mysql' + - pymysql>=1.1.1 ; extra == 'mysql' + - sqlalchemy>=2.0.36 ; extra == 'sql-other' + - adbc-driver-postgresql>=1.2.0 ; extra == 'sql-other' + - adbc-driver-sqlite>=1.2.0 ; extra == 'sql-other' + - beautifulsoup4>=4.12.3 ; extra == 'html' + - html5lib>=1.1 ; extra == 'html' + - lxml>=5.3.0 ; extra == 'html' + - lxml>=5.3.0 ; extra == 'xml' + - matplotlib>=3.9.3 ; extra == 'plot' + - jinja2>=3.1.5 ; extra == 'output-formatting' + - tabulate>=0.9.0 ; extra == 'output-formatting' + - pyqt5>=5.15.9 ; extra == 'clipboard' + - qtpy>=2.4.2 ; extra == 'clipboard' + - zstandard>=0.23.0 ; extra == 'compression' + - pytz>=2024.2 ; extra == 'timezone' + - adbc-driver-postgresql>=1.2.0 ; extra == 'all' + - adbc-driver-sqlite>=1.2.0 ; extra == 'all' + - beautifulsoup4>=4.12.3 ; extra == 'all' + - bottleneck>=1.4.2 ; extra == 'all' + - fastparquet>=2024.11.0 ; extra == 'all' + - fsspec>=2024.10.0 ; extra == 'all' + - gcsfs>=2024.10.0 ; extra == 'all' + - html5lib>=1.1 ; extra == 'all' + - hypothesis>=6.116.0 ; extra == 'all' + - jinja2>=3.1.5 ; extra == 'all' + - lxml>=5.3.0 ; extra == 'all' + - matplotlib>=3.9.3 ; extra == 'all' + - numba>=0.60.0 ; extra == 'all' + - numexpr>=2.10.2 ; extra == 'all' + - odfpy>=1.4.1 ; extra == 'all' + - openpyxl>=3.1.5 ; extra == 'all' + - psycopg2>=2.9.10 ; extra == 'all' + - pyarrow>=13.0.0 ; extra == 'all' + - pyiceberg>=0.8.1 ; extra == 'all' + - pymysql>=1.1.1 ; extra == 'all' + - pyqt5>=5.15.9 ; extra == 'all' + - pyreadstat>=1.2.8 ; extra == 'all' + - pytest>=8.3.4 ; extra == 'all' + - pytest-xdist>=3.6.1 ; extra == 'all' + - python-calamine>=0.3.0 ; extra == 'all' + - pytz>=2024.2 ; extra == 'all' + - pyxlsb>=1.0.10 ; extra == 'all' + - qtpy>=2.4.2 ; extra == 'all' + - scipy>=1.14.1 ; extra == 'all' + - s3fs>=2024.10.0 ; extra == 'all' + - sqlalchemy>=2.0.36 ; extra == 'all' + - tables>=3.10.1 ; extra == 'all' + - tabulate>=0.9.0 ; extra == 'all' + - xarray>=2024.10.0 ; extra == 'all' + - xlrd>=2.0.1 ; extra == 'all' + - xlsxwriter>=3.2.0 ; extra == 'all' + - zstandard>=0.23.0 ; extra == 'all' + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/c5/e0/0840fd2f93da988ec660b8ad1984abe9f25d2aed22a5e394ff1c68c88307/matplotlib-3.10.9-cp313-cp313-win_amd64.whl + name: matplotlib + version: 3.10.9 + sha256: f4399f64b3e94cd500195490972ae1ee81170df1636fa15364d157d5bdd7b921 + requires_dist: + - contourpy>=1.0.1 + - cycler>=0.10 + - fonttools>=4.22.0 + - kiwisolver>=1.3.1 + - numpy>=1.23 + - packaging>=20.0 + - pillow>=8 + - pyparsing>=3 + - python-dateutil>=2.7 + - meson-python>=0.13.1,<0.17.0 ; extra == 'dev' + - pybind11>=2.13.2,!=2.13.3 ; extra == 'dev' + - setuptools-scm>=7,<10 ; extra == 'dev' + - setuptools>=64 ; extra == 'dev' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/c6/3d/020a6b6248c3d4a37797db068256f0b3f15b01bc481327ba888c50309aa8/mkdocs_plugin_inline_svg-0.1.0-py3-none-any.whl + name: mkdocs-plugin-inline-svg + version: 0.1.0 + sha256: a5aab2d98a19b24019f8e650f54fc647c2f31e7d0e36fc5cf2d2161acc0ea49a + requires_dist: + - mkdocs + requires_python: '>=3.5' +- pypi: https://files.pythonhosted.org/packages/c7/6b/6c02f55c2ce2f137ccca0986be7dd89bea31d5bee4346b4377fa3b8586df/ncrystal_core-4.4.2-py3-none-win_amd64.whl + name: ncrystal-core + version: 4.4.2 + sha256: 9b28a90b63849e6a3a807a0a59f7c2ee57e4c64f5643b2dcb6a798ac8ccf666a + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/c7/a0/5ff05d1919ca249508012cad89f08fdc6cfbdaa15b41651c5fe6dffaf1d3/dfo_ls-1.6.5-py3-none-any.whl + name: dfo-ls + version: 1.6.5 + sha256: d147d42e471e240f9abf8bc38351a88f555ea6a8fcfd83119bbbf93c36f75ab2 + requires_dist: + - setuptools + - numpy + - scipy>=1.11 + - pandas + - pytest ; extra == 'dev' + - sphinx ; extra == 'dev' + - sphinx-rtd-theme ; extra == 'dev' + - trustregion>=1.1 ; extra == 'trustregion' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/c7/ea/7988934c8e3e3418aa043f70421817df28d06aef50bfd85f5ad3ec6e70f1/ncrystal_core-4.4.2-py3-none-macosx_11_0_arm64.whl + name: ncrystal-core + version: 4.4.2 + sha256: b7e6101a6850aa18cf441825214381614db444ffcba648de8266fe1c4d1024ce + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/c9/68/f16a3a8ba6f7b6dc92a1f19669c0810bd2c43fc5a02da13b1cbf8e253845/multidict-6.7.1-cp311-cp311-win_amd64.whl + name: multidict + version: 6.7.1 + sha256: bdbf9f3b332abd0cdb306e7c2113818ab1e922dc84b8f8fd06ec89ed2a19ab8b + requires_dist: + - typing-extensions>=4.1.0 ; python_full_version < '3.11' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/c9/7f/09065fd9e27da0eda08b4d6897f1c13535066174cc023af248fc2a8d5e5a/asn1crypto-1.5.1-py2.py3-none-any.whl + name: asn1crypto + version: 1.5.1 + sha256: db4e40728b728508912cbb3d44f19ce188f218e9eba635821bb4b68564f8fd67 +- pypi: https://files.pythonhosted.org/packages/c9/b5/9302644225ba7dfa934a2ff2b9c7bb85701313a90dddb3dfaf693fa5bae2/backrefs-7.0-py311-none-any.whl + name: backrefs + version: '7.0' + sha256: a0fa7360c63509e9e077e174ef4e6d3c21c8db94189b9d957289ae6d794b9475 + requires_dist: + - regex ; extra == 'extras' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl + name: pytest-xdist + version: 3.8.0 + sha256: 202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88 + requires_dist: + - execnet>=2.1 + - pytest>=7.0.0 + - filelock ; extra == 'testing' + - psutil>=3.0 ; extra == 'psutil' + - setproctitle ; extra == 'setproctitle' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl + name: iniconfig + version: 2.3.0 + sha256: f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/cc/a1/40a5c4d8e28b0851d53a8eeeb46fbd73c325a2a9a165f290a5ed90e6c597/fonttools-4.62.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + name: fonttools + version: 4.62.1 + sha256: 1c5c25671ce8805e0d080e2ffdeca7f1e86778c5cbfbeae86d7f866d8830517b + requires_dist: + - lxml>=4.0 ; extra == 'lxml' + - brotli>=1.0.1 ; platform_python_implementation == 'CPython' and extra == 'woff' + - brotlicffi>=0.8.0 ; platform_python_implementation != 'CPython' and extra == 'woff' + - zopfli>=0.1.4 ; extra == 'woff' + - unicodedata2>=17.0.0 ; python_full_version < '3.15' and extra == 'unicode' + - lz4>=1.7.4.2 ; extra == 'graphite' + - scipy ; platform_python_implementation != 'PyPy' and extra == 'interpolatable' + - munkres ; platform_python_implementation == 'PyPy' and extra == 'interpolatable' + - pycairo ; extra == 'interpolatable' + - matplotlib ; extra == 'plot' + - sympy ; extra == 'symfont' + - xattr ; sys_platform == 'darwin' and extra == 'type1' + - skia-pathops>=0.5.0 ; extra == 'pathops' + - uharfbuzz>=0.45.0 ; extra == 'repacker' + - lxml>=4.0 ; extra == 'all' + - brotli>=1.0.1 ; platform_python_implementation == 'CPython' and extra == 'all' + - brotlicffi>=0.8.0 ; platform_python_implementation != 'CPython' and extra == 'all' + - zopfli>=0.1.4 ; extra == 'all' + - unicodedata2>=17.0.0 ; python_full_version < '3.15' and extra == 'all' + - lz4>=1.7.4.2 ; extra == 'all' + - scipy ; platform_python_implementation != 'PyPy' and extra == 'all' + - munkres ; platform_python_implementation == 'PyPy' and extra == 'all' + - pycairo ; extra == 'all' + - matplotlib ; extra == 'all' + - sympy ; extra == 'all' + - xattr ; sys_platform == 'darwin' and extra == 'all' + - skia-pathops>=0.5.0 ; extra == 'all' + - uharfbuzz>=0.45.0 ; extra == 'all' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/cd/f5/038741f5e747a5f6ea3e72487211579d8cbea5eb9827a9cbd61d0108c4bd/sqlalchemy-2.0.49-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + name: sqlalchemy + version: 2.0.49 + sha256: 47604cb2159f8bbd5a1ab48a714557156320f20871ee64d550d8bf2683d980d3 + requires_dist: + - importlib-metadata ; python_full_version < '3.8' + - greenlet>=1 ; platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64' + - typing-extensions>=4.6.0 + - greenlet>=1 ; extra == 'asyncio' + - mypy>=0.910 ; extra == 'mypy' + - pyodbc ; extra == 'mssql' + - pymssql ; extra == 'mssql-pymssql' + - pyodbc ; extra == 'mssql-pyodbc' + - mysqlclient>=1.4.0 ; extra == 'mysql' + - mysql-connector-python ; extra == 'mysql-connector' + - mariadb>=1.0.1,!=1.1.2,!=1.1.5,!=1.1.10 ; extra == 'mariadb-connector' + - cx-oracle>=8 ; extra == 'oracle' + - oracledb>=1.0.1 ; extra == 'oracle-oracledb' + - psycopg2>=2.7 ; extra == 'postgresql' + - pg8000>=1.29.1 ; extra == 'postgresql-pg8000' + - greenlet>=1 ; extra == 'postgresql-asyncpg' + - asyncpg ; extra == 'postgresql-asyncpg' + - psycopg2-binary ; extra == 'postgresql-psycopg2binary' + - psycopg2cffi ; extra == 'postgresql-psycopg2cffi' + - psycopg>=3.0.7 ; extra == 'postgresql-psycopg' + - psycopg[binary]>=3.0.7 ; extra == 'postgresql-psycopgbinary' + - pymysql ; extra == 'pymysql' + - greenlet>=1 ; extra == 'aiomysql' + - aiomysql>=0.2.0 ; extra == 'aiomysql' + - greenlet>=1 ; extra == 'aioodbc' + - aioodbc ; extra == 'aioodbc' + - greenlet>=1 ; extra == 'asyncmy' + - asyncmy>=0.2.3,!=0.2.4,!=0.2.6 ; extra == 'asyncmy' + - greenlet>=1 ; extra == 'aiosqlite' + - aiosqlite ; extra == 'aiosqlite' + - typing-extensions!=3.10.0.1 ; extra == 'aiosqlite' + - sqlcipher3-binary ; extra == 'sqlcipher' + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/cf/c5/9fcb7e0e69cef59cf10c746b84f7d58b08bc66a6b7d459783c5a4f6101a6/numpy-2.4.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + name: numpy + version: 2.4.4 + sha256: df3775294accfdd75f32c74ae39fcba920c9a378a2fc18a12b6820aa8c1fb502 + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/d0/69/f24d3d1c38ad69e256138b4ec2452a8c7cf66be49dc214771ae99dd4f0a0/narwhals-2.20.0-py3-none-any.whl + name: narwhals + version: 2.20.0 + sha256: 16e750ea5507d4ba6e8d03455b5f93a535e0405976561baea235bca5dc9f475d + requires_dist: + - cudf-cu12>=24.10.0 ; extra == 'cudf' + - dask[dataframe]>=2024.8 ; extra == 'dask' + - duckdb>=1.1 ; extra == 'duckdb' + - ibis-framework>=6.0.0 ; extra == 'ibis' + - packaging ; extra == 'ibis' + - pyarrow-hotfix ; extra == 'ibis' + - rich ; extra == 'ibis' + - modin ; extra == 'modin' + - pandas>=1.1.3 ; extra == 'pandas' + - polars>=0.20.4 ; extra == 'polars' + - pyarrow>=13.0.0 ; extra == 'pyarrow' + - pyspark>=3.5.0 ; extra == 'pyspark' + - pyspark[connect]>=3.5.0 ; extra == 'pyspark-connect' + - duckdb>=1.1 ; extra == 'sql' + - sqlparse ; extra == 'sql' + - sqlframe>=3.22.0,!=3.39.3 ; extra == 'sqlframe' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl + name: watchdog + version: 6.0.0 + sha256: afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c + requires_dist: + - pyyaml>=3.10 ; extra == 'watchmedo' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/d1/73/a9d864e42a01896bb5974475438f16086be9ba1f0d19d0bb7a07427c4a8b/numpy-2.4.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + name: numpy + version: 2.4.4 + sha256: c901b15172510173f5cb310eae652908340f8dede90fff9e3bf6c0d8dfd92f83 + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/d2/f0/834e479e47e499b6478e807fb57b31cc2db696c4db30557bb6f5aea4a90b/mando-0.7.1-py2.py3-none-any.whl + name: mando + version: 0.7.1 + sha256: 26ef1d70928b6057ee3ca12583d73c63e05c49de8972d620c278a7b206581a8a + requires_dist: + - six + - argparse ; python_full_version < '2.7' + - funcsigs ; python_full_version < '3.3' + - rst2ansi ; extra == 'restructuredtext' +- pypi: https://files.pythonhosted.org/packages/d3/1d/11605e99ac8ea9435651ee71ab4cb4bf03f0949586246476a25aadfec54a/propcache-0.4.1-cp311-cp311-macosx_11_0_arm64.whl + name: propcache + version: 0.4.1 + sha256: 6918ecbd897443087a3b7cd978d56546a812517dcaaca51b49526720571fa93e + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/d3/80/722402571443031989e87a5861f26f2e897778768906dff1f998b42f235d/diffpy_pdffit2-1.6.0-cp313-cp313-macosx_11_0_arm64.whl + name: diffpy-pdffit2 + version: 1.6.0 + sha256: 8837088196ddabd688deb77740c3f03ddc33f0df6964a8e3507701c32b67b8b2 + requires_dist: + - diffpy-structure + requires_python: '>=3.12,<3.15' +- pypi: https://files.pythonhosted.org/packages/d3/97/bf54c5b3f2be34e1f143e6db838dfdc54f2ffa3e68c738934c82f3b2a08d/fonttools-4.62.1-cp311-cp311-win_amd64.whl + name: fonttools + version: 4.62.1 + sha256: e8514f4924375f77084e81467e63238b095abda5107620f49421c368a6017ed2 + requires_dist: + - lxml>=4.0 ; extra == 'lxml' + - brotli>=1.0.1 ; platform_python_implementation == 'CPython' and extra == 'woff' + - brotlicffi>=0.8.0 ; platform_python_implementation != 'CPython' and extra == 'woff' + - zopfli>=0.1.4 ; extra == 'woff' + - unicodedata2>=17.0.0 ; python_full_version < '3.15' and extra == 'unicode' + - lz4>=1.7.4.2 ; extra == 'graphite' + - scipy ; platform_python_implementation != 'PyPy' and extra == 'interpolatable' + - munkres ; platform_python_implementation == 'PyPy' and extra == 'interpolatable' + - pycairo ; extra == 'interpolatable' + - matplotlib ; extra == 'plot' + - sympy ; extra == 'symfont' + - xattr ; sys_platform == 'darwin' and extra == 'type1' + - skia-pathops>=0.5.0 ; extra == 'pathops' + - uharfbuzz>=0.45.0 ; extra == 'repacker' + - lxml>=4.0 ; extra == 'all' + - brotli>=1.0.1 ; platform_python_implementation == 'CPython' and extra == 'all' + - brotlicffi>=0.8.0 ; platform_python_implementation != 'CPython' and extra == 'all' + - zopfli>=0.1.4 ; extra == 'all' + - unicodedata2>=17.0.0 ; python_full_version < '3.15' and extra == 'all' + - lz4>=1.7.4.2 ; extra == 'all' + - scipy ; platform_python_implementation != 'PyPy' and extra == 'all' + - munkres ; platform_python_implementation == 'PyPy' and extra == 'all' + - pycairo ; extra == 'all' + - matplotlib ; extra == 'all' + - sympy ; extra == 'all' + - xattr ; sys_platform == 'darwin' and extra == 'all' + - skia-pathops>=0.5.0 ; extra == 'all' + - uharfbuzz>=0.45.0 ; extra == 'all' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/d4/06/19ff1efd58b85906149ce83dfddce23252cea5bec7e0fa5f834336cfe836/scipp-26.3.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + name: scipp + version: 26.3.1 + sha256: ab5859a24b3150b588dd2c67e68b0c7f07c9444eae501f3b6326d6b4a34cbf10 + requires_dist: + - numpy>=2 + - pytest ; extra == 'test' + - matplotlib ; extra == 'test' + - beautifulsoup4 ; extra == 'test' + - ipython ; extra == 'test' + - h5py ; extra == 'extra' + - scipy>=1.7.0 ; extra == 'extra' + - graphviz ; extra == 'extra' + - pooch ; extra == 'extra' + - plopp ; extra == 'extra' + - matplotlib ; extra == 'extra' + - scipp[extra] ; extra == 'all' + - ipympl ; extra == 'all' + - ipython ; extra == 'all' + - ipywidgets ; extra == 'all' + - jupyterlab ; extra == 'all' + - jupyterlab-widgets ; extra == 'all' + - jupyter-nbextensions-configurator ; extra == 'all' + - nodejs ; extra == 'all' + - pythreejs ; extra == 'all' + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl + name: pytest + version: 9.0.3 + sha256: 2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9 + requires_dist: + - colorama>=0.4 ; sys_platform == 'win32' + - exceptiongroup>=1 ; python_full_version < '3.11' + - iniconfig>=1.0.1 + - packaging>=22 + - pluggy>=1.5,<2 + - pygments>=2.7.2 + - tomli>=1 ; python_full_version < '3.11' + - argcomplete ; extra == 'dev' + - attrs>=19.2 ; extra == 'dev' + - hypothesis>=3.56 ; extra == 'dev' + - mock ; extra == 'dev' + - requests ; extra == 'dev' + - setuptools ; extra == 'dev' + - xmlschema ; extra == 'dev' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/d5/0c/043d5e551459da400957a1395e0febbf771446ff34291afcbe3d8be2a279/fsspec-2026.4.0-py3-none-any.whl + name: fsspec + version: 2026.4.0 + sha256: 11ef7bb35dab8a394fde6e608221d5cf3e8499401c249bebaeaad760a1a8dec2 + requires_dist: + - adlfs ; extra == 'abfs' + - adlfs ; extra == 'adl' + - pyarrow>=1 ; extra == 'arrow' + - dask ; extra == 'dask' + - distributed ; extra == 'dask' + - pre-commit ; extra == 'dev' + - ruff>=0.5 ; extra == 'dev' + - numpydoc ; extra == 'doc' + - sphinx ; extra == 'doc' + - sphinx-design ; extra == 'doc' + - sphinx-rtd-theme ; extra == 'doc' + - yarl ; extra == 'doc' + - dropbox ; extra == 'dropbox' + - dropboxdrivefs ; extra == 'dropbox' + - requests ; extra == 'dropbox' + - adlfs ; extra == 'full' + - aiohttp!=4.0.0a0,!=4.0.0a1 ; extra == 'full' + - dask ; extra == 'full' + - distributed ; extra == 'full' + - dropbox ; extra == 'full' + - dropboxdrivefs ; extra == 'full' + - fusepy ; extra == 'full' + - gcsfs>2024.2.0 ; extra == 'full' + - libarchive-c ; extra == 'full' + - ocifs ; extra == 'full' + - panel ; extra == 'full' + - paramiko ; extra == 'full' + - pyarrow>=1 ; extra == 'full' + - pygit2 ; extra == 'full' + - requests ; extra == 'full' + - s3fs>2024.2.0 ; extra == 'full' + - smbprotocol ; extra == 'full' + - tqdm ; extra == 'full' + - fusepy ; extra == 'fuse' + - gcsfs>2024.2.0 ; extra == 'gcs' + - pygit2 ; extra == 'git' + - requests ; extra == 'github' + - gcsfs ; extra == 'gs' + - panel ; extra == 'gui' + - pyarrow>=1 ; extra == 'hdfs' + - aiohttp!=4.0.0a0,!=4.0.0a1 ; extra == 'http' + - libarchive-c ; extra == 'libarchive' + - ocifs ; extra == 'oci' + - s3fs>2024.2.0 ; extra == 's3' + - paramiko ; extra == 'sftp' + - smbprotocol ; extra == 'smb' + - paramiko ; extra == 'ssh' + - aiohttp!=4.0.0a0,!=4.0.0a1 ; extra == 'test' + - numpy ; extra == 'test' + - pytest ; extra == 'test' + - pytest-asyncio!=0.22.0 ; extra == 'test' + - pytest-benchmark ; extra == 'test' + - pytest-cov ; extra == 'test' + - pytest-mock ; extra == 'test' + - pytest-recording ; extra == 'test' + - pytest-rerunfailures ; extra == 'test' + - requests ; extra == 'test' + - aiobotocore>=2.5.4,<3.0.0 ; extra == 'test-downstream' + - dask[dataframe,test] ; extra == 'test-downstream' + - moto[server]>4,<5 ; extra == 'test-downstream' + - pytest-timeout ; extra == 'test-downstream' + - xarray ; extra == 'test-downstream' + - adlfs ; extra == 'test-full' + - aiohttp!=4.0.0a0,!=4.0.0a1 ; extra == 'test-full' + - backports-zstd ; python_full_version < '3.14' and extra == 'test-full' + - cloudpickle ; extra == 'test-full' + - dask ; extra == 'test-full' + - distributed ; extra == 'test-full' + - dropbox ; extra == 'test-full' + - dropboxdrivefs ; extra == 'test-full' + - fastparquet ; extra == 'test-full' + - fusepy ; extra == 'test-full' + - gcsfs ; extra == 'test-full' + - jinja2 ; extra == 'test-full' + - kerchunk ; extra == 'test-full' + - libarchive-c ; extra == 'test-full' + - lz4 ; extra == 'test-full' + - notebook ; extra == 'test-full' + - numpy ; extra == 'test-full' + - ocifs ; extra == 'test-full' + - pandas<3.0.0 ; extra == 'test-full' + - panel ; extra == 'test-full' + - paramiko ; extra == 'test-full' + - pyarrow ; extra == 'test-full' + - pyarrow>=1 ; extra == 'test-full' + - pyftpdlib ; extra == 'test-full' + - pygit2 ; extra == 'test-full' + - pytest ; extra == 'test-full' + - pytest-asyncio!=0.22.0 ; extra == 'test-full' + - pytest-benchmark ; extra == 'test-full' + - pytest-cov ; extra == 'test-full' + - pytest-mock ; extra == 'test-full' + - pytest-recording ; extra == 'test-full' + - pytest-rerunfailures ; extra == 'test-full' + - python-snappy ; extra == 'test-full' + - requests ; extra == 'test-full' + - smbprotocol ; extra == 'test-full' + - tqdm ; extra == 'test-full' + - urllib3 ; extra == 'test-full' + - zarr ; extra == 'test-full' + - zstandard ; python_full_version < '3.14' and extra == 'test-full' + - tqdm ; extra == 'tqdm' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl + name: frozenlist + version: 1.8.0 + sha256: fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/d6/bb/fbdc4e57731efb86b4209ffdd2519520782bf27b3c961beac3e5c20d2b87/trove_classifiers-2026.4.28.13-py3-none-any.whl + name: trove-classifiers + version: 2026.4.28.13 + sha256: 8f4b1eb4e16296b57d612965444f87a83861cc989a0451ac97fe4265ddef03b8 +- pypi: https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl + name: pycodestyle + version: 2.14.0 + sha256: dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/d8/8b/e2bbeb42068f0c48899e8eddd34902afc0f7429d4d2a152d2dc2670dc661/pythreejs-2.4.2-py3-none-any.whl + name: pythreejs + version: 2.4.2 + sha256: 8418807163ad91f4df53b58c4e991b26214852a1236f28f1afeaadf99d095818 + requires_dist: + - ipywidgets>=7.2.1 + - ipydatawidgets>=1.1.1 + - numpy + - traitlets + - sphinx>=1.5 ; extra == 'docs' + - nbsphinx>=0.2.13 ; extra == 'docs' + - nbsphinx-link ; extra == 'docs' + - sphinx-rtd-theme ; extra == 'docs' + - scipy ; extra == 'examples' + - matplotlib ; extra == 'examples' + - scikit-image ; extra == 'examples' + - ipywebrtc ; extra == 'examples' + - nbval ; extra == 'test' + - pytest-check-links ; extra == 'test' + - numpy>=1.14 ; extra == 'test' + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl + name: frozenlist + version: 1.8.0 + sha256: 878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/da/e0/6cc2e852837cd6086fe7d8406af4294e66827a60a4cf60b86575a4a65ca8/msgpack-1.1.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + name: msgpack + version: 1.1.2 + sha256: 454e29e186285d2ebe65be34629fa0e8605202c60fbc7c4c650ccd41870896ef + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/db/09/a0ab6a246a7ede89e817d749a941df34f27a74bedf15551da51e86ae105e/pycairo-1.29.0-cp311-cp311-win_amd64.whl + name: pycairo + version: 1.29.0 + sha256: 3391532db03f9601c1cee9ebfa15b7d1db183c6020f3e75c1348cee16825934f + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl + name: cfgv + version: 3.5.0 + sha256: a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/db/bc/83e112abc66cd466c6b83f99118035867cecd41802f8d044638aa78a106e/locket-1.0.0-py2.py3-none-any.whl + name: locket + version: 1.0.0 + sha256: b6c819a722f7b6bd955b80781788e4a66a55628b858d347536b7e81325a3a5e3 + requires_python: '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*' +- pypi: https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl + name: watchdog + version: 6.0.0 + sha256: cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680 + requires_dist: + - pyyaml>=3.10 ; extra == 'watchmedo' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/dc/83/6d810a8a9ebc9c307989b418840c20e46907c74d707beb67ab566773e6fc/xarray-2026.4.0-py3-none-any.whl + name: xarray + version: 2026.4.0 + sha256: d43751d9fb4a90f9249c30431684f00c41bc874f1edccd862631a40cbc0edf08 + requires_dist: + - numpy>=1.26 + - packaging>=24.2 + - pandas>=2.2 + - scipy>=1.15 ; extra == 'accel' + - bottleneck ; extra == 'accel' + - numbagg>=0.9 ; extra == 'accel' + - numba>=0.62 ; extra == 'accel' + - flox>=0.10 ; extra == 'accel' + - opt-einsum ; extra == 'accel' + - xarray[accel,etc,io,parallel,viz] ; extra == 'complete' + - netcdf4>=1.6.0 ; extra == 'io' + - h5netcdf[h5py]>=1.5.0 ; extra == 'io' + - pydap ; extra == 'io' + - scipy>=1.15 ; extra == 'io' + - zarr>=3.0 ; extra == 'io' + - fsspec ; extra == 'io' + - cftime ; extra == 'io' + - pooch ; extra == 'io' + - sparse>=0.15 ; extra == 'etc' + - dask[complete] ; extra == 'parallel' + - cartopy>=0.24 ; extra == 'viz' + - matplotlib>=3.10 ; extra == 'viz' + - nc-time-axis ; extra == 'viz' + - seaborn ; extra == 'viz' + - pandas-stubs ; extra == 'types' + - scipy-stubs ; extra == 'types' + - types-colorama ; extra == 'types' + - types-decorator ; extra == 'types' + - types-defusedxml ; extra == 'types' + - types-docutils ; extra == 'types' + - types-networkx ; extra == 'types' + - types-openpyxl ; extra == 'types' + - types-pexpect ; extra == 'types' + - types-psutil ; extra == 'types' + - types-pycurl ; extra == 'types' + - types-pygments ; extra == 'types' + - types-python-dateutil ; extra == 'types' + - types-pytz ; extra == 'types' + - types-pyyaml ; extra == 'types' + - types-requests ; extra == 'types' + - types-setuptools ; extra == 'types' + - types-xlrd ; extra == 'types' + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/dc/b7/901d837999a9350a7773289f7760cb473d4ba01fdc6ebae0ff2553d269ac/ncrystal_python-4.4.2-py3-none-any.whl + name: ncrystal-python + version: 4.4.2 + sha256: f419318d088fade6bcff1e39e15baf6fe69fcf5306dd681fca1106d1f63a89ce + requires_dist: + - numpy>=1.22 + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/dd/a4/d45caf2b97b035c57267791ecfaafbd59c68212004b3842830954bb4b02e/multidict-6.7.1-cp311-cp311-macosx_11_0_arm64.whl + name: multidict + version: 6.7.1 + sha256: f2a0a924d4c2e9afcd7ec64f9de35fcd96915149b2216e1cb2c10a56df483855 + requires_dist: + - typing-extensions>=4.1.0 ; python_full_version < '3.11' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl + name: email-validator + version: 2.3.0 + sha256: 80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4 + requires_dist: + - dnspython>=2.0.0 + - idna>=2.0.0 + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl + name: markdown + version: 3.10.2 + sha256: e91464b71ae3ee7afd3017d9f358ef0baf158fd9a298db92f1d4761133824c36 + requires_dist: + - coverage ; extra == 'testing' + - pyyaml ; extra == 'testing' + - mkdocs>=1.6 ; extra == 'docs' + - mkdocs-nature>=0.6 ; extra == 'docs' + - mdx-gh-links>=0.2 ; extra == 'docs' + - mkdocstrings[python]>=0.28.3 ; extra == 'docs' + - mkdocs-gen-files ; extra == 'docs' + - mkdocs-section-index ; extra == 'docs' + - mkdocs-literate-nav ; extra == 'docs' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl + name: shellingham + version: 1.5.4 + sha256: 7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686 + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/e2/98/8b1e801939839d405f1f122e7d175cebe9aeb4e114f95bfc45e3152af9a7/fonttools-4.62.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + name: fonttools + version: 4.62.1 + sha256: 6706d1cb1d5e6251a97ad3c1b9347505c5615c112e66047abbef0f8545fa30d1 + requires_dist: + - lxml>=4.0 ; extra == 'lxml' + - brotli>=1.0.1 ; platform_python_implementation == 'CPython' and extra == 'woff' + - brotlicffi>=0.8.0 ; platform_python_implementation != 'CPython' and extra == 'woff' + - zopfli>=0.1.4 ; extra == 'woff' + - unicodedata2>=17.0.0 ; python_full_version < '3.15' and extra == 'unicode' + - lz4>=1.7.4.2 ; extra == 'graphite' + - scipy ; platform_python_implementation != 'PyPy' and extra == 'interpolatable' + - munkres ; platform_python_implementation == 'PyPy' and extra == 'interpolatable' + - pycairo ; extra == 'interpolatable' + - matplotlib ; extra == 'plot' + - sympy ; extra == 'symfont' + - xattr ; sys_platform == 'darwin' and extra == 'type1' + - skia-pathops>=0.5.0 ; extra == 'pathops' + - uharfbuzz>=0.45.0 ; extra == 'repacker' + - lxml>=4.0 ; extra == 'all' + - brotli>=1.0.1 ; platform_python_implementation == 'CPython' and extra == 'all' + - brotlicffi>=0.8.0 ; platform_python_implementation != 'CPython' and extra == 'all' + - zopfli>=0.1.4 ; extra == 'all' + - unicodedata2>=17.0.0 ; python_full_version < '3.15' and extra == 'all' + - lz4>=1.7.4.2 ; extra == 'all' + - scipy ; platform_python_implementation != 'PyPy' and extra == 'all' + - munkres ; platform_python_implementation == 'PyPy' and extra == 'all' + - pycairo ; extra == 'all' + - matplotlib ; extra == 'all' + - sympy ; extra == 'all' + - xattr ; sys_platform == 'darwin' and extra == 'all' + - skia-pathops>=0.5.0 ; extra == 'all' + - uharfbuzz>=0.45.0 ; extra == 'all' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/e4/bc/daa30c02069eeac5b9198985ba42f5d65ca71bed6705b18329e51d352b7c/plopp-26.4.2-py3-none-any.whl + name: plopp + version: 26.4.2 + sha256: 5cab99bb0905ce08a1d1d7d82f0f64cee7d594269ec1bd01a8a361bd14ab7bff + requires_dist: + - lazy-loader>=0.4 + - matplotlib>=3.8 + - scipp>=25.8.0 ; extra == 'scipp' + - plopp[scipp] ; extra == 'all' + - ipympl>0.8.4 ; extra == 'all' + - pythreejs>=2.4.1 ; extra == 'all' + - mpltoolbox>=24.6.0 ; extra == 'all' + - ipywidgets>=8.1.0 ; extra == 'all' + - graphviz>=0.20.3 ; extra == 'all' + - plopp[scipp] ; extra == 'test' + - graphviz>=0.20.3 ; extra == 'test' + - h5py>=3.12 ; extra == 'test' + - ipympl>=0.8.4 ; extra == 'test' + - ipywidgets>=8.1.0 ; extra == 'test' + - ipykernel>=6.26,<7 ; extra == 'test' + - mpltoolbox>=24.6.0 ; extra == 'test' + - pandas>=2.2.2 ; extra == 'test' + - plotly>=5.15.0 ; extra == 'test' + - pooch>=1.5 ; extra == 'test' + - pyarrow>=13.0.0 ; extra == 'test' + - pytest>=8.0 ; extra == 'test' + - pythreejs>=2.4.1 ; extra == 'test' + - scipy>=1.10.0 ; extra == 'test' + - xarray>=2024.5.0 ; extra == 'test' + - anywidget>=0.9.0 ; extra == 'test' + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/e5/87/499737bfba066b4a3bebff24a8f1c5b2dee410b209bc6668c9be692580f0/numpy-2.4.4-cp313-cp313-macosx_14_0_arm64.whl + name: numpy + version: 2.4.4 + sha256: 4a19d9dba1a76618dd86b164d608566f393f8ec6ac7c44f0cc879011c45e65af + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/e6/0d/8882a4c7a5ebe59a46b709e82411d9c730d67250d41a2e11bc4bcd4d431d/scipp-26.3.1-cp311-cp311-win_amd64.whl + name: scipp + version: 26.3.1 + sha256: 37877cf07b4f54f224d5465c265d6a1e591d605d0c23dd350a4b48d95c26ab7b + requires_dist: + - numpy>=2 + - pytest ; extra == 'test' + - matplotlib ; extra == 'test' + - beautifulsoup4 ; extra == 'test' + - ipython ; extra == 'test' + - h5py ; extra == 'extra' + - scipy>=1.7.0 ; extra == 'extra' + - graphviz ; extra == 'extra' + - pooch ; extra == 'extra' + - plopp ; extra == 'extra' + - matplotlib ; extra == 'extra' + - scipp[extra] ; extra == 'all' + - ipympl ; extra == 'all' + - ipython ; extra == 'all' + - ipywidgets ; extra == 'all' + - jupyterlab ; extra == 'all' + - jupyterlab-widgets ; extra == 'all' + - jupyter-nbextensions-configurator ; extra == 'all' + - nodejs ; extra == 'all' + - pythreejs ; extra == 'all' + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/e6/90/90a65e6ae1b6e66183b48874d32509fc306c994beecc7924a6fa3d9f8955/easyscience-2.5.1-py3-none-any.whl + name: easyscience + version: 2.5.1 + sha256: e44c3efb10f8e040ba88523c6ba58e131e3174721f24801d036fdc5a24e622f2 + requires_dist: + - asteval + - bumps>=1.0.4 + - dfo-ls + - lmfit + - numpy + - scipp + - build ; extra == 'dev' + - copier ; extra == 'dev' + - docstripy ; extra == 'dev' + - format-docstring ; extra == 'dev' + - gitpython ; extra == 'dev' + - interrogate ; extra == 'dev' + - ipykernel ; extra == 'dev' + - ipympl ; extra == 'dev' + - ipython ; extra == 'dev' + - ipywidgets ; extra == 'dev' + - jinja2 ; extra == 'dev' + - jupyterlab ; extra == 'dev' + - jupyterquiz ; extra == 'dev' + - jupytext ; extra == 'dev' + - matplotlib ; extra == 'dev' + - mike ; extra == 'dev' + - mkdocs ; extra == 'dev' + - mkdocs-autorefs ; extra == 'dev' + - mkdocs-jupyter ; extra == 'dev' + - mkdocs-markdownextradata-plugin ; extra == 'dev' + - mkdocs-material ; extra == 'dev' + - mkdocs-plugin-inline-svg ; extra == 'dev' + - mkdocstrings-python ; extra == 'dev' + - nbmake ; extra == 'dev' + - nbqa ; extra == 'dev' + - nbstripout ; extra == 'dev' + - pooch ; extra == 'dev' + - pre-commit ; extra == 'dev' + - pydoclint ; extra == 'dev' + - pytest ; extra == 'dev' + - pytest-cov ; extra == 'dev' + - pytest-xdist ; extra == 'dev' + - pyyaml ; extra == 'dev' + - radon ; extra == 'dev' + - ruff ; extra == 'dev' + - spdx-headers ; extra == 'dev' + - validate-pyproject[all] ; extra == 'dev' + - versioningit ; extra == 'dev' + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl + name: cycler + version: 0.12.1 + sha256: 85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30 + requires_dist: + - ipython ; extra == 'docs' + - matplotlib ; extra == 'docs' + - numpydoc ; extra == 'docs' + - sphinx ; extra == 'docs' + - pytest ; extra == 'tests' + - pytest-cov ; extra == 'tests' + - pytest-xdist ; extra == 'tests' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/e9/bd/e51a61b1054f09437acfbc2ff9106c30d1eb76bc1453d428399946781253/pillow-12.2.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + name: pillow + version: 12.2.0 + sha256: e74473c875d78b8e9d5da2a70f7099549f9eb37ded4e2f6a463e60125bccd176 + requires_dist: + - furo ; extra == 'docs' + - olefile ; extra == 'docs' + - sphinx>=8.2 ; extra == 'docs' + - sphinx-autobuild ; extra == 'docs' + - sphinx-copybutton ; extra == 'docs' + - sphinx-inline-tabs ; extra == 'docs' + - sphinxext-opengraph ; extra == 'docs' + - olefile ; extra == 'fpx' + - olefile ; extra == 'mic' + - arro3-compute ; extra == 'test-arrow' + - arro3-core ; extra == 'test-arrow' + - nanoarrow ; extra == 'test-arrow' + - pyarrow ; extra == 'test-arrow' + - check-manifest ; extra == 'tests' + - coverage>=7.4.2 ; extra == 'tests' + - defusedxml ; extra == 'tests' + - markdown2 ; extra == 'tests' + - olefile ; extra == 'tests' + - packaging ; extra == 'tests' + - pyroma>=5 ; extra == 'tests' + - pytest ; extra == 'tests' + - pytest-cov ; extra == 'tests' + - pytest-timeout ; extra == 'tests' + - pytest-xdist ; extra == 'tests' + - trove-classifiers>=2024.10.12 ; extra == 'tests' + - defusedxml ; extra == 'xmp' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/eb/be/b257e12f9710819fde40adc972578bee6b72c5992da1bc8369bef2597756/nbmake-1.5.5-py3-none-any.whl + name: nbmake + version: 1.5.5 + sha256: c6fbe6e48b60cacac14af40b38bf338a3b88f47f085c54ac5b8639ff0babaf4b + requires_dist: + - ipykernel>=5.4.0 + - nbclient>=0.6.6 + - nbformat>=5.0.4 + - pygments>=2.7.3 + - pytest>=6.1.0 + requires_python: '>=3.8.0' +- pypi: https://files.pythonhosted.org/packages/ed/01/e1da2f721c3a8feec311a16924c42fb28c37445528df0747f47dec5232d5/crysfml-0.6.2-cp313-cp313-win_amd64.whl + name: crysfml + version: 0.6.2 + sha256: b32ddbf4010ce61a535182d58c12c410ed4a0f5de2259e7ef605e7fbcce40be3 + requires_dist: + - numpy + requires_python: '>=3.11,<3.15' +- pypi: https://files.pythonhosted.org/packages/ee/8c/83087ebc47ab0396ce092363001fa37c17153119ee282700c0713a195853/prettytable-3.17.0-py3-none-any.whl + name: prettytable + version: 3.17.0 + sha256: aad69b294ddbe3e1f95ef8886a060ed1666a0b83018bbf56295f6f226c43d287 + requires_dist: + - wcwidth + - pytest ; extra == 'tests' + - pytest-cov ; extra == 'tests' + - pytest-lazy-fixtures ; extra == 'tests' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/ee/ab/7d7463cda94f8b68b969ea97aaad679655a0e436efd6a643e528a8de114e/gemmi-0.7.5-cp313-cp313-win_amd64.whl + name: gemmi + version: 0.7.5 + sha256: ad1f72ffa24adbfaf259e11471f6f071a668667f6ca846051f3bfea024fd337d + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/ee/b8/ead7c10efff731738c72e59ed6eb5791854879fbed7ae98781a12006263a/lxml-6.1.0-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl + name: lxml + version: 6.1.0 + sha256: e69aa6805905807186eb00e66c6d97a935c928275182eb02ee40ba00da9623b2 + requires_dist: + - cssselect>=0.7 ; extra == 'cssselect' + - html5lib ; extra == 'html5' + - beautifulsoup4 ; extra == 'htmlsoup' + - lxml-html-clean ; extra == 'html-clean' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/f0/d2/e2f77eef1acb7111405433c707dc735e63f67a56e176e72e9e7a2cd3f493/aiohttp-3.13.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + name: aiohttp + version: 3.13.5 + sha256: 3df334e39d4c2f899a914f1dba283c1aadc311790733f705182998c6f7cae665 + requires_dist: + - aiohappyeyeballs>=2.5.0 + - aiosignal>=1.4.0 + - async-timeout>=4.0,<6.0 ; python_full_version < '3.11' + - attrs>=17.3.0 + - frozenlist>=1.1.1 + - multidict>=4.5,<7.0 + - propcache>=0.2.0 + - yarl>=1.17.0,<2.0 + - aiodns>=3.3.0 ; extra == 'speedups' + - brotli>=1.2 ; platform_python_implementation == 'CPython' and extra == 'speedups' + - brotlicffi>=1.2 ; platform_python_implementation != 'CPython' and extra == 'speedups' + - backports-zstd ; python_full_version < '3.14' and platform_python_implementation == 'CPython' and extra == 'speedups' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/f1/2c/3850985d4c64048dec7b826f8a803e135b52b11b4c81c9cd4326b1ca15ab/ncrystal_core-4.4.2-py3-none-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + name: ncrystal-core + version: 4.4.2 + sha256: d0d9c47cd017b7cefc52dde50546d7c151bfdd75d345e42e2b3e74ab5fe83c62 + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/f1/5b/e63c877c4c94382b66de5045e08ec8cd960e8a4d22f0d62a4dfb1f9e5ac6/ipydatawidgets-4.3.5-py2.py3-none-any.whl + name: ipydatawidgets + version: 4.3.5 + sha256: d590cdb7c364f2f6ab346f20b9d2dd661d27a834ef7845bc9d7113118f05ec87 + requires_dist: + - ipywidgets>=7.0.0 + - numpy + - traittypes>=0.2.0 + - sphinx ; extra == 'docs' + - recommonmark ; extra == 'docs' + - sphinx-rtd-theme ; extra == 'docs' + - pytest>=4 ; extra == 'test' + - pytest-cov ; extra == 'test' + - nbval>=0.9.2 ; extra == 'test' + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/f1/8b/544bc867e24e1bd48f3118cecd3b05c694e160a168478fa28770f22fd094/propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + name: propcache + version: 0.4.1 + sha256: d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/f2/f2/728f041460f1b9739b85ee23b45fa5a505962ea11fd85bdbe2a02b021373/darkdetect-0.8.0-py3-none-any.whl + name: darkdetect + version: 0.8.0 + sha256: a7509ccf517eaad92b31c214f593dbcf138ea8a43b2935406bbd565e15527a85 + requires_dist: + - pyobjc-framework-cocoa ; sys_platform == 'darwin' and extra == 'macos-listener' + requires_python: '>=3.6' +- pypi: https://files.pythonhosted.org/packages/f4/78/6cce448e2098e9f3bfc91bb877f06aa24b6ccace872e39c53b2f707c4648/propcache-0.4.1-cp311-cp311-win_amd64.whl + name: propcache + version: 0.4.1 + sha256: 364426a62660f3f699949ac8c621aad6977be7126c5807ce48c0aeb8e7333ea6 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/f4/99/722e90c586455733fa860d092722ef074f3a65449fe9787b7b7e2dafcd1f/pyhanko_certvalidator-0.31.1-py3-none-any.whl + name: pyhanko-certvalidator + version: 0.31.1 + sha256: 0aabe25b536ed555f3b18e5eb2c62ff46adeff9d15e27f0c9d5d81952d956f45 + requires_dist: + - asn1crypto>=1.5.1 + - oscrypto>=1.1.0 + - cryptography>=48.0.0 + - uritools>=3.0.1 + - requests>=2.31.0 + - aiohttp>=3.9,<3.14 ; extra == 'async-http' + requires_python: '>=3.10' +- 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/f5/57/2a154a69d6642860300bf8eb205d13131104991f2b1065bbb9075ac5c32e/tof-26.3.0-py3-none-any.whl + name: tof + version: 26.3.0 + sha256: e89783a072b05fdb53d9e76fbf919dc8935e75e118fdaf17ca5cc33727ef002b + requires_dist: + - plopp>=23.10.0 + - pooch>=1.5.0 + - scipp>=25.1.0 + - lazy-loader>=0.3 + - pytest>=8.0 ; extra == 'test' + - scippneutron>=24.12.0 ; extra == 'test' + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/f5/5f/f17563f28ff03c7b6799c50d01d5d856a1d55f2676f537ca8d28c7f627cd/scipy-1.17.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + name: scipy + version: 1.17.1 + sha256: 581b2264fc0aa555f3f435a5944da7504ea3a065d7029ad60e7c3d1ae09c5464 + requires_dist: + - numpy>=1.26.4,<2.7 + - pytest>=8.0.0 ; extra == 'test' + - pytest-cov ; extra == 'test' + - pytest-timeout ; extra == 'test' + - pytest-xdist ; extra == 'test' + - asv ; extra == 'test' + - mpmath ; extra == 'test' + - gmpy2 ; extra == 'test' + - threadpoolctl ; extra == 'test' + - scikit-umfpack ; extra == 'test' + - pooch ; extra == 'test' + - hypothesis>=6.30 ; extra == 'test' + - array-api-strict>=2.3.1 ; extra == 'test' + - cython ; extra == 'test' + - meson ; extra == 'test' + - ninja ; sys_platform != 'emscripten' and extra == 'test' + - sphinx>=5.0.0,<8.2.0 ; extra == 'doc' + - intersphinx-registry ; extra == 'doc' + - pydata-sphinx-theme>=0.15.2 ; extra == 'doc' + - sphinx-copybutton ; extra == 'doc' + - sphinx-design>=0.4.0 ; extra == 'doc' + - matplotlib>=3.5 ; extra == 'doc' + - numpydoc ; extra == 'doc' + - jupytext ; extra == 'doc' + - myst-nb>=1.2.0 ; extra == 'doc' + - pooch ; extra == 'doc' + - jupyterlite-sphinx>=0.19.1 ; extra == 'doc' + - jupyterlite-pyodide-kernel ; extra == 'doc' + - linkify-it-py ; extra == 'doc' + - tabulate ; extra == 'doc' + - click<8.3.0 ; extra == 'dev' + - spin ; extra == 'dev' + - mypy==1.10.0 ; extra == 'dev' + - typing-extensions ; extra == 'dev' + - types-psutil ; extra == 'dev' + - pycodestyle ; extra == 'dev' + - ruff>=0.12.0 ; extra == 'dev' + - cython-lint>=0.12.2 ; extra == 'dev' + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/f5/ab/f76ec3c3627c883215b5c8080debb4394ef5a7a29be811f786415fc1e6fd/propcache-0.4.1-cp313-cp313-win_amd64.whl + name: propcache + version: 0.4.1 + sha256: 381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/f6/f0/10642828a8dfb741e5f3fbaac830550a518a775c7fff6f04a007259b0548/py-1.11.0-py2.py3-none-any.whl + name: py + version: 1.11.0 + sha256: 607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378 + requires_python: '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*' +- pypi: https://files.pythonhosted.org/packages/f7/00/bbca25f8a2372465cdf93138c1e1e38dff045fb0afef1488f395d0afcb3b/ncrystal-4.4.2-py3-none-any.whl + name: ncrystal + version: 4.4.2 + sha256: e02fa7d743addc3fbea23287737a88b8d01192450fdca51554d3f9032fe4617c + requires_dist: + - ncrystal-core==4.4.2 + - ncrystal-python==4.4.2 + - spglib>=2.1.0 ; extra == 'composer' + - ase>=3.23.0 ; extra == 'cif' + - gemmi>=0.6.1 ; extra == 'cif' + - spglib>=2.1.0 ; extra == 'cif' + - endf-parserpy>=0.14.3 ; extra == 'endf' + - matplotlib>=3.6.0 ; extra == 'plot' + - ase>=3.23.0 ; extra == 'all' + - endf-parserpy>=0.14.3 ; extra == 'all' + - gemmi>=0.6.1 ; extra == 'all' + - matplotlib>=3.6.0 ; extra == 'all' + - spglib>=2.1.0 ; extra == 'all' + - pyyaml>=6.0.0 ; extra == 'devel' + - ase>=3.23.0 ; extra == 'devel' + - cppcheck ; extra == 'devel' + - endf-parserpy>=0.14.3 ; extra == 'devel' + - gemmi>=0.6.1 ; extra == 'devel' + - matplotlib>=3.6.0 ; extra == 'devel' + - mpmath>=1.3.0 ; extra == 'devel' + - numpy>=1.22 ; extra == 'devel' + - pybind11>=2.11.0 ; extra == 'devel' + - ruff>=0.8.1 ; extra == 'devel' + - simple-build-system>=1.6.0 ; extra == 'devel' + - spglib>=2.1.0 ; extra == 'devel' + - tomli>=2.0.0 ; python_full_version < '3.11' and extra == 'devel' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/f7/20/e6c0ff62ca2ad1a396a34f4380bafccaaf8791ff8fccf3d995a1fc12d417/h5py-3.16.0-cp311-cp311-win_amd64.whl + name: h5py + version: 3.16.0 + sha256: 17d1f1630f92ad74494a9a7392ab25982ce2b469fc62da6074c0ce48366a2999 + requires_dist: + - numpy>=1.21.2 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/f7/27/a2fc51a4a122dfd1015e921ae9d22fee3d20b0b8080d9a704578bf9deece/pymdown_extensions-10.21.2-py3-none-any.whl + name: pymdown-extensions + version: 10.21.2 + sha256: 5c0fd2a2bea14eb39af8ff284f1066d898ab2187d81b889b75d46d4348c01638 + requires_dist: + - markdown>=3.6 + - pyyaml + - pygments>=2.19.1 ; extra == 'extra' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl + name: ghp-import + version: 2.1.0 + sha256: 8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619 + requires_dist: + - python-dateutil>=2.8.1 + - twine ; extra == 'dev' + - markdown ; extra == 'dev' + - flake8 ; extra == 'dev' + - wheel ; extra == 'dev' +- pypi: https://files.pythonhosted.org/packages/f7/ee/d0348d8dd27ac7f001e9583c004f1b245826b04cc23b5ce6b7867c4a1cc6/refnx-0.1.63-cp311-abi3-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl + name: refnx + version: 0.1.63 + sha256: 4f397b0d6e18ac1bb793372ec92c9f4a122383387a706dea456df5a2c90dff5e + requires_dist: + - numpy>=2.0 + - scipy + - orsopy + - ipython ; extra == 'all' + - ipywidgets ; extra == 'all' + - traitlets ; extra == 'all' + - matplotlib ; extra == 'all' + - xlrd ; extra == 'all' + - h5py ; extra == 'all' + - jupyter ; extra == 'all' + - tqdm ; extra == 'all' + - pymc ; extra == 'all' + - pytensor ; extra == 'all' + - attrs ; extra == 'all' + - pandas ; extra == 'all' + - pyparsing ; extra == 'all' + - periodictable ; extra == 'all' + - pyqt6 ; extra == 'all' + - qtpy ; extra == 'all' + - corner ; extra == 'all' + - numba ; extra == 'all' + - pytest ; extra == 'test' + - uncertainties ; extra == 'test' + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl + name: toolz + version: 1.1.0 + sha256: 15ccc861ac51c53696de0a5d6d4607f99c210739caf987b5d2054f3efed429d8 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl + name: aiosignal + version: 1.4.0 + sha256: 053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e + requires_dist: + - frozenlist>=1.1.0 + - typing-extensions>=4.2 ; python_full_version < '3.13' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/fb/86/dd6e5db36df29e76c7a7699123569a4a18c1623ce68d826ed96c62643cae/mdit_py_plugins-0.5.0-py3-none-any.whl + name: mdit-py-plugins + version: 0.5.0 + sha256: 07a08422fc1936a5d26d146759e9155ea466e842f5ab2f7d2266dd084c8dab1f + requires_dist: + - markdown-it-py>=2.0.0,<5.0.0 + - pre-commit ; extra == 'code-style' + - myst-parser ; extra == 'rtd' + - sphinx-book-theme ; extra == 'rtd' + - coverage ; extra == 'testing' + - pytest ; extra == 'testing' + - pytest-cov ; extra == 'testing' + - pytest-regressions ; extra == 'testing' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/fb/d9/8f612389178e1e1800aaed85537b024ebe28b9a82fff6a015825e51a8877/refl1d-1.0.1-py3-none-any.whl + name: refl1d + version: 1.0.1 + sha256: 8cc52121790df465e3a659d0fb0298f2501256c6f2d7fca26085927297112eb1 + requires_dist: + - bumps==1.0.4 + - matplotlib + - numba + - numpy + - periodictable + - scipy + - orsopy + - ipython ; extra == 'dev' + - matplotlib ; extra == 'dev' + - nbsphinx ; extra == 'dev' + - pre-commit ; extra == 'dev' + - pydantic ; extra == 'dev' + - pytest ; extra == 'dev' + - pytest-cov ; extra == 'dev' + - ruff ; extra == 'dev' + - sphinx<8.2 ; extra == 'dev' + - versioningit ; extra == 'dev' + - wxpython ; extra == 'full' + - ipython ; extra == 'full' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/fd/20/2870f2e63da2b74eda896053809065769b256646bb3f4b74e06d013ad590/uritools-6.1.0-py3-none-any.whl + name: uritools + version: 6.1.0 + sha256: f1b699052459711f1dd03721ca79341dc345054e6286f47313f8bce777782415 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/fd/a5/03c970d57f4c1741354837353ce39fb5206952ae1dba8922d29c86f64805/numba-0.65.1-cp313-cp313-win_amd64.whl + name: numba + version: 0.65.1 + sha256: 85be74c0d036842699a30058f82fb88fc5ffdc59f7615cab5792ea92914c9b62 + requires_dist: + - llvmlite>=0.47.0.dev0,<0.48 + - numpy>=1.22 + - numpy>=1.22,<2.5 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl + name: watchdog + version: 6.0.0 + sha256: a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b + requires_dist: + - pyyaml>=3.10 ; extra == 'watchmedo' + requires_python: '>=3.9' diff --git a/pixi.toml b/pixi.toml new file mode 100644 index 00000000..f0023fba --- /dev/null +++ b/pixi.toml @@ -0,0 +1,290 @@ +########### +# WORKSPACE +########### + +[workspace] + +# Supported platforms for the lock file (pixi.lock) +platforms = ['win-64', 'linux-64', 'osx-arm64'] + +# Channels for fetching packages +channels = ['nodefaults', 'conda-forge'] + +########## +# FEATURES +########## + +# Default features: + +[activation.env] +PYTHONIOENCODING = 'utf-8' + +[system-requirements] +# Set minimum supported version for macOS to be 14.0 to ensure packages +# like `scipp` that only have wheels for macOS 14.0+ (macosx_14_0_arm64) +# are used instead of building from source. This is a workaround for +# Pixi, see https://github.com/prefix-dev/pixi/issues/5667 +macos = '14.0' + +# Non-default features: + +# Set specific Python versions to be used in CI testing. + +[feature.py-min.dependencies] +python = '3.11.*' + +[feature.py-max.dependencies] +python = '3.13.*' + +# Development dependencies for local development and testing with +# editable installations. + +[feature.dev.dependencies] +nodejs = '*' # Required for Prettier (non-Python formatting) +jupyterlab = '*' # Jupyter notebooks +ipython = '*' # Interactive Python shell +pixi-kernel = '*' # Pixi Jupyter kernel + +[feature.dev.pypi-dependencies] +pip = '*' +easyreflectometry = { path = '.', editable = true, extras = ['dev'] } + +# User-like behavior for testing with pip-installed dependencies instead +# of editable installations. + +[feature.user.dependencies] +jupyterlab = '*' # Jupyter notebooks +ipython = '*' # Interactive Python shell +pixi-kernel = '*' # Pixi Jupyter kernel + +[feature.user.pypi-dependencies] +pip = '*' +easydiffraction = '*' + +############## +# ENVIRONMENTS +############## + +[environments] + +# The `default` feature is always included in all environments. +# Additional features can be specified per environment. + +# Specific environments for CI testing with different Python versions. +py-311-env = { features = ['py-min', 'dev'] } +py-313-env = { features = ['py-max', 'dev'] } + +# The `default` environment is developer-oriented for local development +# and testing with editable installation of the current package. +default = { features = ['py-max', 'dev'] } + +# The `user` environment allows testing with pip-installed dependencies +# instead of editable installation of the current package. +user = { features = ['py-max', 'user'] } + +####### +# TASKS +####### + +[tasks] + +################## +# 🧪 Testing Tasks +################## + +# The bulk of the unit-test suite still lives at the top level of tests/ +# (pending migration into tests/unit/), so run the whole tree minus the +# functional and integration subtrees -- otherwise CI coverage only sees +# the handful of files under tests/unit/. +unit-tests = 'python -m pytest tests/ --ignore=tests/functional --ignore=tests/integration --color=yes -v' +functional-tests = 'python -m pytest tests/functional/ --color=yes -v' +# No -n auto: importing easyreflectometry pulls in arviz, and arviz 0.23.4 +# (py-311-env) writes a "warn once per day" stamp file on import via a +# _atomic_write_text() that is not atomic -- every process writes the same +# fixed `daily_warning.tmp` and renames it onto `daily_warning`. Concurrent +# xdist workers therefore race on that rename: FileNotFoundError on Linux, +# PermissionError (WinError 32) on Windows. The real fix is +# to stop importing arviz in easyreflectometry/__init__.py, after which +# xdist can come back. +integration-tests = 'python -m pytest tests/integration/ --color=yes -v' +notebook-tests = 'python -m pytest --nbmake docs/docs/tutorials/**/ --nbmake-timeout=1200 --color=yes -n auto -v' + +test = { depends-on = ['unit-tests'] } + +########### +# ✔️ Checks +########### + +pyproject-check = 'python -m validate_pyproject pyproject.toml' +docstring-lint-check = 'pydoclint --quiet 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' + +check = 'pre-commit run --hook-stage manual --all-files' + +########## +# 🛠️ Fixes +########## + +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/' +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 = 'echo "✅ All auto-formatting steps completed successfully!"' + +fix = { depends-on = [ + 'docstring-format-fix', + 'py-format-fix', + 'py-lint-fix', + 'nonpy-format-fix', + 'notebook-lint-fix', + 'success-message', +] } + +#################### +# 🧮 Code Complexity +#################### + +complexity-check = 'radon cc -s src/' +complexity-check-json = 'radon cc -s -j src/' +maintainability-check = 'radon mi src/' +maintainability-check-json = 'radon mi -j src/' +raw-metrics = 'radon raw -s src/' +raw-metrics-json = 'radon raw -s -j src/' + +############# +# 📊 Coverage +############# + +unit-tests-coverage = 'pixi run unit-tests --cov=src/easyreflectometry --cov-report=term-missing' +functional-tests-coverage = 'pixi run functional-tests --cov=src/easyreflectometry --cov-report=term-missing' +integration-tests-coverage = 'pixi run integration-tests --cov=src/easyreflectometry --cov-report=term-missing' +docstring-coverage = 'interrogate -c pyproject.toml src/easyreflectometry' + +cov = { depends-on = [ + 'docstring-coverage', + 'unit-tests-coverage', + 'integration-tests-coverage', +] } + +######################## +# 📓 Notebook Management +######################## + +notebook-convert = 'jupytext docs/docs/tutorials/*.py --from py:percent --to ipynb' +notebook-strip = 'nbstripout docs/docs/tutorials/**/*.ipynb' +notebook-tweak = 'python tools/tweak_notebooks.py docs/docs/tutorials/**/*.ipynb' +notebook-exec = 'python -m pytest --nbmake docs/docs/tutorials/ --nbmake-timeout=1200 --overwrite --color=yes -n auto -v' + +notebook-prepare = { depends-on = [ + #'notebook-convert', + 'notebook-strip', + #'notebook-tweak', +] } + +######################## +# 📚 Documentation Tasks +######################## + +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-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' + +docs-update-assets = 'python tools/update_docs_assets.py' + +############################## +# 📦 Template Management Tasks +############################## + +copier-copy = 'copier copy gh:easyscience/templates . --data-file .copier-answers.yml --data template_type=lib' +copier-recopy = 'copier recopy --data-file .copier-answers.yml --data template_type=lib' +copier-update = 'copier update --data-file .copier-answers.yml --data template_type=lib' + +##################### +# 🪝 Pre-commit Hooks +##################### + +pre-commit-clean = 'pre-commit clean' +pre-commit-install = 'pre-commit install --hook-type pre-commit --hook-type pre-push --overwrite' +pre-commit-uninstall = 'pre-commit uninstall --hook-type pre-commit --hook-type pre-push' +pre-commit-setup = { depends-on = [ + 'pre-commit-clean', + 'pre-commit-uninstall', + 'pre-commit-install', +] } + +################# +# 🐙️ GitHub Tasks +################# + +repo-wiki = 'gh api -X PATCH repos/easyscience/reflectometry-lib -f has_wiki=false' +repo-discussions = 'gh api -X PATCH repos/easyscience/reflectometry-lib -f has_discussions=true' +repo-description = "gh api -X PATCH repos/easyscience/reflectometry-lib -f description='Reflectometry data analysis'" +repo-homepage = "gh api -X PATCH repos/easyscience/reflectometry-lib -f homepage='https://easyscience.github.io/reflectometry-lib'" +repo-config = { depends-on = [ + 'repo-wiki', + 'repo-discussions', + 'repo-description', + 'repo-homepage', +] } + +master-protection = 'gh api -X POST repos/easyscience/reflectometry-lib/rulesets --input .github/configs/rulesets-master.json' +develop-protection = 'gh api -X POST repos/easyscience/reflectometry-lib/rulesets --input .github/configs/rulesets-develop.json' +gh-pages-protection = 'gh api -X POST repos/easyscience/reflectometry-lib/rulesets --input .github/configs/rulesets-gh-pages.json' +branch-protection = { depends-on = [ + 'master-protection', + 'develop-protection', + 'gh-pages-protection', +] } + +pages-deployment = 'gh api -X POST repos/easyscience/reflectometry-lib/pages --input .github/configs/pages-deployment.json' + +github-labels = 'python tools/update_github_labels.py' + +######################### +# ⚖️ SPDX License Headers +######################### + +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 +#################################### + +default-build = 'python -m build' +dist-build = 'python -m build --wheel --outdir dist' + +npm-config = 'npm config set registry https://registry.npmjs.org/' +prettier-install = 'npm install --no-save --no-audit --no-fund prettier prettier-plugin-toml' + +clean-pycache = "find . -type d -name '__pycache__' -prune -exec rm -rf '{}' +" + +post-install = { depends-on = [ + 'npm-config', + 'prettier-install', + #'pre-commit-setup', +] } + +########################## +# 🔗 Main Package Shortcut +########################## +easyreflectometry = 'python -m easyreflectometry' + +[dependencies] +ruff = ">=0.15.12,<0.16" +copier = ">=9.15.0,<10" diff --git a/prettierrc.toml b/prettierrc.toml new file mode 100644 index 00000000..b98c86eb --- /dev/null +++ b/prettierrc.toml @@ -0,0 +1,22 @@ +plugins = [ + "prettier-plugin-toml", # use the TOML plugin +] + +endOfLine = 'lf' # change line endings to LF +proseWrap = 'always' # change wrapping in Markdown files +semi = false # remove semicolons +singleQuote = true # use single quotes instead of double quotes +tabWidth = 2 # change tab width to 2 spaces +useTabs = false # use spaces instead of tabs + +printWidth = 79 # wrap lines at 79 characters + +[[overrides]] +files = ["*.md"] +[overrides.options] +printWidth = 72 # wrap Markdown files at 72 characters + +[[overrides]] +files = ["*.yml", "*.yaml"] +[overrides.options] +printWidth = 88 # wrap YAML files at 88 characters diff --git a/pyproject.toml b/pyproject.toml index ec3df724..9f90457f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,164 +1,348 @@ -[build-system] -requires = [ - "hatchling<=1.21.0", - "setuptools-git-versioning", -] -build-backend = "hatchling.build" - -[tool.setuptools-git-versioning] -enabled = true +############################### +# Configuration for the project +############################### [project] -name = "easyreflectometry" -dynamic = ["version"] -description = "A reflectometry python package built on the EasyScience framework." -readme = "README.md" -authors = [{name = "EasyScience contributors"}] -license = { file = "LICENSE" } +name = 'easyreflectometry' +dynamic = ['version'] # Use versioningit to manage the version +description = 'Reflectometry data analysis' +authors = [{ name = 'EasyScience contributors' }] +readme = 'README.md' +license = 'BSD-3-Clause' +license-files = ['LICENSE'] classifiers = [ - "License :: OSI Approved :: BSD License", - "Operating System :: OS Independent", - "Topic :: Scientific/Engineering", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: 3.13", - "Development Status :: 3 - Alpha" + 'Intended Audience :: Science/Research', + 'Topic :: Scientific/Engineering', + 'License :: OSI Approved :: BSD License', + 'Operating System :: OS Independent', + 'Programming Language :: Python :: 3 :: Only', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', + 'Programming Language :: Python :: 3.13', ] - -requires-python = ">=3.11,<3.14" - +requires-python = '>=3.11' dependencies = [ - "easyscience", - "scipp", - "refnx", - "refl1d>=1.0.0", - "orsopy", - "svglib<1.6 ; platform_system=='Linux' or sys_platform == 'darwin'", - "xhtml2pdf", - "bumps", + 'easyscience', + 'scipp', + 'refnx', + 'refl1d>=1.0.0', + 'orsopy', + 'svglib<1.6 ; platform_system=="Linux" or sys_platform == "darwin"', + 'xhtml2pdf', + 'bumps', + 'pooch', + 'plotly', ] [project.optional-dependencies] dev = [ - "build", - "codecov", - "coverage", - "coveralls", - "flake8", - "ipykernel", - "jupyter", - "jupyterlab", - "plopp", - "pooch", - "pytest", - "pytest-cov", - "ruff", - "toml", - "yapf", -] - -docs = [ - "myst_parser", - "nbsphinx", - "plopp", - "sphinx<=8.1.3", - "sphinx_autodoc_typehints", - "sphinx_book_theme", - "sphinx-copybutton", - "toml" + 'GitPython', # Interact with Git repositories + 'build', # Building the package + 'pre-commit', # Pre-commit hooks + 'jinja2', # Templating + 'nbmake', # Building notebooks + 'nbstripout', # Strip output from notebooks + 'nbqa', # Linting and formatting notebooks + 'pytest', # Testing + 'pytest-cov', # Test coverage + 'pytest-xdist', # Enable parallel testing + 'ruff', # Linting and formatting code + 'radon', # Code complexity and maintainability + 'validate-pyproject[all]', # Validate pyproject.toml + 'versioningit', # Automatic versioning from git tags + 'jupytext', # Jupyter notebook text format support + 'jupyterquiz', # Quizzes in Jupyter notebooks + 'plopp', # Plotting in Jupyter notebooks + 'pydoclint', # Docstring linter + 'format-docstring', # Docstring formatter + 'docstripy', # Convert docstrings to other formats + 'interrogate', # Docstring coverage checker + 'copier', # Template management + 'mike', # MkDocs: Versioned documentation support + 'mkdocs', # Static site generator + 'mkdocs-material', # Documentation framework on top of MkDocs + 'mkdocs-autorefs', # MkDocs: Auto-references support + 'mkdocs-jupyter', # MkDocs: Jupyter notebook support + 'mkdocs-plugin-inline-svg', # MkDocs: Inline SVG support + 'mkdocs-markdownextradata-plugin', # MkDocs: Markdown extra data support, such as global variables + 'mkdocstrings-python', # MkDocs: Python docstring support + 'pyyaml', # YAML parser + 'spdx-headers', # SPDX license header validation + 'arviz', # Bayesian analysis and visualization + 'plotly', # Interactive plotting ] [project.urls] -homepage = "https://docs.easyreflectometry.org" -documentation = "https://docs.easyreflectometry.org" +Documentation = 'https://easyscience.github.io/reflectometry-lib' +'Release Notes' = 'https://github.com/easyscience/reflectometry-lib/releases' +'Source Code' = 'https://github.com/easyscience/reflectometry-lib' +'Issue Tracker' = 'https://github.com/easyscience/reflectometry-lib/issues' -[tool.hatch.version] -path = "src/easyreflectometry/__version__.py" +############################ +# Build system configuration +############################ + +[build-system] +build-backend = 'hatchling.build' +requires = ['hatchling', 'versioningit'] + +############################# +# Configuration for hatchling +############################# + +# 'hatch' -- Build system for Python +# https://hatch.pypa.io/ + +[tool.hatch.build.targets.wheel] +packages = ['src/easyreflectometry'] [tool.hatch.metadata] allow-direct-references = true -[tool.hatch.build.targets.sdist] -packages = ["src"] +[tool.hatch.version] +source = 'versioningit' # Use versioningit to manage the version -[tool.hatch.build.targets.wheel] -packages = ["src/easyreflectometry"] +################################ +# Configuration for versioningit +################################ + +# 'versioningit' -- Versioning from git tags +# https://versioningit.readthedocs.io/ + +# Versioningit generates versions from git tags, so we don't need to +# either specify them statically in pyproject.toml or save them in the +# source code. Do not use {distance} in the version format, as it +# forces a version bump for every commit, which triggers unnecessary +# pixi.lock update without any changes to the source code. + +[tool.versioningit.format] +distance = '{base_version}+dev{distance}' # example: 1.2.3.post4+dev3 +dirty = '{base_version}+dirty{distance}' # example: 0.5.8+dirty3 +distance-dirty = '{base_version}+devdirty{distance}' # example: 0.5.8+devdirty3 + +# Configure how versioningit detects versions from Git +# - 'match' ensures it only considers tags starting with 'v' +# - 'default-tag' is used as a fallback when no matching tag is found +[tool.versioningit.vcs] +method = 'git' +match = ['v*'] +default-tag = 'v999.0.0' + +################################ +# Configuration for interrogate +################################ + +# 'interrogate' -- Docstring coverage checker +# https://interrogate.readthedocs.io/en/latest/ + +[tool.interrogate] +fail-under = 0 # Minimum docstring coverage percentage to pass +verbose = 1 +#exclude = ['src/**/__init__.py'] + +####################################### +# Configuration for coverage/pytest-cov +####################################### + +# 'coverage' -- Code coverage measurement tool +# https://coverage.readthedocs.io/en/latest/ [tool.coverage.run] -source = ["src/easyreflectometry"] +branch = true # Measure branch coverage as well +source = ['src'] # Limit coverage to the source code directory + +[tool.coverage.report] +show_missing = true # Show missing lines +skip_covered = false # Skip files with 100% coverage in the report +fail_under = 0 # Minimum coverage percentage to pass + +########################## +# Configuration for pytest +########################## -[tool.github.info] -organization = 'easyScience' -repo = "easyreflectometry" +# 'pytest' -- Testing framework +# https://docs.pytest.org/en/stable/ + +[tool.pytest.ini_options] +addopts = '--import-mode=importlib' +markers = ['fast: mark test as fast (should be run on every push)'] +testpaths = ['tests'] + +######################## +# Configuration for ruff +######################## + +# 'ruff' -- Python linter and code formatter +# https://docs.astral.sh/ruff/rules/ [tool.ruff] -line-length = 127 -exclude = [ - "docs", -] +exclude = ['tmp'] +indent-width = 4 +# line-length = 99 # See also `max-line-length` in [tool.ruff.lint.pycodestyle] +line-length = 128 # 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] -quote-style = "single" +docstring-code-format = true # Whether to format code snippets in docstrings +docstring-code-line-length = 99 # Line length for code snippets in docstrings +# docstring-code-line-length = 72 # Line length for code snippets in docstrings +indent-style = 'space' # PEP 8 recommends using spaces over tabs +quote-style = 'single' # But double quotes in docstrings (PEP 8, PEP 257) -[tool.ruff.lint.per-file-ignores] -# allow asserts in test files -"*test_*.py" = ["S101"] +# Linting rules to use with Ruff [tool.ruff.lint] select = [ - # flake8 settings from existing CI setup - "E9", "F63", "F7", "F82", - # Code should be polished to fulfill all cases below - # https://docs.astral.sh/ruff/rules/ - # pycodestyle - "E", - # Pyflakes - "F", - # pyupgrade -# "UP", - # flake8-bugbear -# "B", - # flake8-simplify -# "SIM", - # isort - "I", - # flake8-bandit - "S", + # 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 + 'I', # https://docs.astral.sh/ruff/rules/#isort-i + #'N', # https://docs.astral.sh/ruff/rules/#pep8-naming-n + #'NPY', # https://docs.astral.sh/ruff/rules/#numpy-specific-rules-npy + #'PGH', # https://docs.astral.sh/ruff/rules/#pygrep-hooks-pgh + #'PERF', # https://docs.astral.sh/ruff/rules/#perflint-perf + #'RUF', # https://docs.astral.sh/ruff/rules/#ruff-specific-rules-ruf + #'TRY', # https://docs.astral.sh/ruff/rules/#tryceratops-try + #'UP', # https://docs.astral.sh/ruff/rules/#pyupgrade-up + # pycodestyle (E, W) rules + 'E', # https://docs.astral.sh/ruff/rules/#error-e + 'W', # https://docs.astral.sh/ruff/rules/#warning-w + # Pylint (PL) rules + #'PLC', # https://docs.astral.sh/ruff/rules/#convention-plc + #'PLE', # https://docs.astral.sh/ruff/rules/#error-ple + #'PLR', # https://docs.astral.sh/ruff/rules/#refactor-plr + #'PLW', # https://docs.astral.sh/ruff/rules/#warning-plw + # flake8 rules + #'A', # https://docs.astral.sh/ruff/rules/#flake8-builtins-a + #'ANN', # https://docs.astral.sh/ruff/rules/#flake8-annotations-ann + #'ARG', # https://docs.astral.sh/ruff/rules/#flake8-unused-arguments-arg + #'ASYNC', # https://docs.astral.sh/ruff/rules/#flake8-async-async + #'B', # https://docs.astral.sh/ruff/rules/#flake8-bugbear-b + #'BLE', # https://docs.astral.sh/ruff/rules/#flake8-blind-except-ble + #'C4', # https://docs.astral.sh/ruff/rules/#flake8-comprehensions-c4 + #'COM', # https://docs.astral.sh/ruff/rules/#flake8-commas-com + #'DTZ', # https://docs.astral.sh/ruff/rules/#flake8-datetimez-dtz + #'EM', # https://docs.astral.sh/ruff/rules/#flake8-errmsg-em + #'FA', # https://docs.astral.sh/ruff/rules/#flake8-future-annotations-fa + #'FBT', # https://docs.astral.sh/ruff/rules/#flake8-boolean-trap-fbt + #'FIX', # https://docs.astral.sh/ruff/rules/#flake8-fixme-fix + #'G', # https://docs.astral.sh/ruff/rules/#flake8-logging-format-g + #'ICN', # https://docs.astral.sh/ruff/rules/#flake8-import-conventions-icn + #'INP', # https://docs.astral.sh/ruff/rules/#flake8-no-pep420-inp + #'ISC', # https://docs.astral.sh/ruff/rules/#flake8-implicit-str-concat-isc + #'LOG', # https://docs.astral.sh/ruff/rules/#flake8-logging-log + #'PIE', # https://docs.astral.sh/ruff/rules/#flake8-pie-pie + #'PT', # https://docs.astral.sh/ruff/rules/#flake8-pytest-style-pt + #'PTH', # https://docs.astral.sh/ruff/rules/#flake8-use-pathlib-pth + #'PYI', # https://docs.astral.sh/ruff/rules/#flake8-pyi-pyi + #'RET', # https://docs.astral.sh/ruff/rules/#flake8-return-ret + #'RSE', # https://docs.astral.sh/ruff/rules/#flake8-raise-rse + 'S', # https://docs.astral.sh/ruff/rules/#flake8-bandit-s + #'SIM', # https://docs.astral.sh/ruff/rules/#flake8-simplify-sim + #'SLF', # https://docs.astral.sh/ruff/rules/#flake8-self-slf + #'SLOT', # https://docs.astral.sh/ruff/rules/#flake8-slots-slot + #'T20', # https://docs.astral.sh/ruff/rules/#flake8-print-t20 + #'TC', # https://docs.astral.sh/ruff/rules/#flake8-type-checking-tc + #'TD', # https://docs.astral.sh/ruff/rules/#flake8-todos-td + #'TID', # https://docs.astral.sh/ruff/rules/#flake8-tidy-imports-tid +] + +# Exceptions to the linting rules + +# Ignore specific rules globally +ignore = [ + 'COM812', # https://docs.astral.sh/ruff/rules/missing-trailing-comma/ + # The following 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/ ] +'docs/**' = [ + 'INP001', # https://docs.astral.sh/ruff/rules/implicit-namespace-package/ + 'T201', # https://docs.astral.sh/ruff/rules/print/ +] + +# Specific options for certain rules + +[tool.ruff.lint.flake8-tidy-imports] +# Disallow all relative imports +ban-relative-imports = 'all' [tool.ruff.lint.isort] +# Forces all from imports to appear on their own line force-single-line = true -[tool.tox] -legacy_tox_ini = """ -[tox] -isolated_build = True -envlist = py{3.11,3.12,3.13} -[gh-actions] -python = - 3.11: py311 - 3.12: py312 - 3.13: py313 -[gh-actions:env] -PLATFORM = - ubuntu-latest: linux - macos-latest: macos - windows-latest: 2022 -[testenv] -passenv = - CI - GITHUB_ACTIONS - GITHUB_ACTION - GITHUB_REF - GITHUB_REPOSITORY - GITHUB_HEAD_REF - GITHUB_RUN_ID - GITHUB_SHA - COVERAGE_FILE -deps = coverage -commands = - pip install -e '.[dev]' - pytest --cov --cov-report=xml -""" +[tool.ruff.lint.mccabe] +# Cyclomatic complexity threshold (default is 10) +max-complexity = 10 + +[tool.ruff.lint.pycodestyle] +# 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. +# Use 72 characters for docstrings. +max-line-length = 128 # See also `line-length` in [tool.ruff] +max-doc-length = 128 +# max-doc-length = 72 + +[tool.ruff.lint.pydocstyle] +convention = 'numpy' + +[tool.ruff.lint.pylint] +# Ruff counts `self`/`cls` in max-args; traditional pylint does not. +# Setting 6 here matches pylint's default of 5 (excluding self). +max-args = 6 +max-positional-args = 6 + +############################# +# Configuration for pydoclint +############################# + +# 'pydoclint' -- Docstring linter, a faster alternative to +# 'darglint' or 'darglint2'. +# https://pypi.org/project/pydoclint/ + +# This is a more advanced docstring linter compared to Ruff's built-in +# docstring check rules D or DOC. For example, among many other things, +# it can check that arguments in the docstring, which are used by MkDocs +# and IDEs to render parameter documentation, remain synchronized with +# the parameter declarations in the code (in function's signature). + +[tool.pydoclint] +exclude = '\.' # Temporarily disable pydoclint until we are ready +style = 'numpy' +check-style-mismatch = true +check-arg-defaults = true +allow-init-docstring = true + +#################################### +# Configuration for format-docstring +#################################### + +# 'format-docstring' -- Code formatter for docstrings +# https://github.com/jsh9/format-docstring + +[tool.format_docstring] +exclude = '\.' # Temporarily disable format-docstring until we are ready +docstring_style = 'numpy' +line_length = 72 +fix_rst_backticks = true +verbose = 'default' diff --git a/src/easyreflectometry/__init__.py b/src/easyreflectometry/__init__.py index 6de988ac..a18fee06 100644 --- a/src/easyreflectometry/__init__.py +++ b/src/easyreflectometry/__init__.py @@ -1,5 +1,11 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + +"""EasyReflectometry library.""" + from importlib import metadata +from .analysis.bayesian import PosteriorResults from .project import Project try: @@ -8,6 +14,7 @@ __version__ = '0.0.0' __all__ = [ - Project, - __version__, + 'Project', + 'PosteriorResults', + '__version__', ] diff --git a/src/easyreflectometry/__version__.py b/src/easyreflectometry/__version__.py deleted file mode 100644 index bcd8d54e..00000000 --- a/src/easyreflectometry/__version__.py +++ /dev/null @@ -1 +0,0 @@ -__version__ = '1.6.0' diff --git a/src/easyreflectometry/analysis/__init__.py b/src/easyreflectometry/analysis/__init__.py new file mode 100644 index 00000000..9f086835 --- /dev/null +++ b/src/easyreflectometry/analysis/__init__.py @@ -0,0 +1,23 @@ +# SPDX-FileCopyrightText: 2026 EasyReflectometry contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Post-hoc analysis utilities for reflectometry fitting results.""" + +from easyreflectometry.analysis.bayesian import PosteriorResults +from easyreflectometry.analysis.bayesian import credible_intervals +from easyreflectometry.analysis.bayesian import plot_corner +from easyreflectometry.analysis.bayesian import plot_distribution +from easyreflectometry.analysis.bayesian import plot_trace +from easyreflectometry.analysis.bayesian import posterior_predictive_reflectivity +from easyreflectometry.analysis.bayesian import posterior_predictive_sld_profile +from easyreflectometry.analysis.bayesian import posterior_summary + +__all__ = [ + 'PosteriorResults', + 'plot_corner', + 'plot_distribution', + 'plot_trace', + 'posterior_summary', + 'credible_intervals', + 'posterior_predictive_reflectivity', + 'posterior_predictive_sld_profile', +] diff --git a/src/easyreflectometry/analysis/bayesian.py b/src/easyreflectometry/analysis/bayesian.py new file mode 100644 index 00000000..b5bdfb43 --- /dev/null +++ b/src/easyreflectometry/analysis/bayesian.py @@ -0,0 +1,1384 @@ +# SPDX-FileCopyrightText: 2026 EasyReflectometry contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Bayesian posterior analysis for reflectometry fitting results.""" + +from __future__ import annotations + +import hashlib +import json +import warnings +from typing import Any + +import numpy as np +from easyscience.fitting.minimizers.minimizer_base import MINIMIZER_PARAMETER_PREFIX + +try: + import arviz as _arviz + + _HAS_ARVIZ = True +except ImportError: + _HAS_ARVIZ = False + + +def _require_arviz(): + if not _HAS_ARVIZ: + raise ImportError('The ``arviz`` library is required for trace plots and R-hat. Install it with ``pip install arviz``.') + + +def _require_plotly(): + try: + import plotly # noqa: F401 + except ImportError as exc: + raise ImportError( + 'The ``plotly`` library is required for posterior plots. Install it with ``pip install plotly``.' + ) from exc + + +def _wrap_pair_label(name: str, max_len: int = 16) -> str: + """Insert ``
`` line breaks so long parameter labels don't overlap. + + Dotted names (e.g. ``layer1.thickness``) break on dots; plain names + word-wrap on spaces at roughly *max_len* characters per line. + """ + name = name.strip() + if not name: + return name + if '.' in name: + parts = [p.strip() for p in name.split('.') if p.strip()] + if parts: + return '
'.join([*(f'{p}.' for p in parts[:-1]), parts[-1]]) + if len(name) <= max_len: + return name + words = name.split() + if len(words) == 1: + return name + lines: list[str] = [] + current = '' + for word in words: + if current and len(current) + 1 + len(word) > max_len: + lines.append(current) + current = word + else: + current = f'{current} {word}' if current else word + if current: + lines.append(current) + return '
'.join(lines) + + +def _to_arviz_data(draws: np.ndarray, param_names: list[str]): + """Convert posterior draws to an arviz InferenceData object. + + :param draws: Posterior samples, shape ``(n_samples, n_params)`` or + ``(n_chains, n_draws, n_params)``. + :type draws: np.ndarray + :param param_names: Parameter names (one per column). + :type param_names: list[str] + :return: arviz InferenceData object. + """ + draws = np.asarray(draws, dtype=np.float64) + if draws.ndim == 2: + draws = draws[np.newaxis, ...] # (1, n_samples, n_params) + + # Build a dict of {param_name: (chain, draw) array} + posterior_dict = {} + for i, name in enumerate(param_names): + posterior_dict[name] = draws[:, :, i] + + # arviz < 1.0 takes the posterior variables as a keyword argument; arviz + # >= 1.0 removed it in favour of a single {group: {var: array}} mapping. + # The 1.x call cannot go first: 0.x accepts the mapping without error but + # misreads it as one variable named 'posterior'. + try: + return _arviz.from_dict(posterior=posterior_dict) + except TypeError: + return _arviz.from_dict({'posterior': posterior_dict}) + + +class PosteriorResults: + """Container for Bayesian posterior samples with analysis methods. + + :param draws: Posterior samples, shape ``(n_samples, n_params)``. + :type draws: np.ndarray + :param param_names: Parameter names (one per column of ``draws``). + :type param_names: list[str] + :param logp: Log-posterior values, shape ``(n_samples,)``, or ``None``. + :type logp: np.ndarray | None + :param sampler_state: Raw sampler state object (e.g. BUMPS ``DreamState``), or ``None``. + :type sampler_state: Any | None + """ + + def __init__( + self, + draws: np.ndarray, + param_names: list[str], + logp: np.ndarray | None = None, + sampler_state: Any | None = None, + ): + self.draws = np.asarray(draws) + self.param_names = list(param_names) + self.logp = np.asarray(logp) if logp is not None else None + self.sampler_state = sampler_state + + def __repr__(self) -> str: + n_samples, n_params = self.draws.shape + return f'PosteriorResults(n_samples={n_samples}, n_params={n_params}, param_names={self.param_names})' + + def summary(self) -> str: + """Return a formatted summary table with mean, sd, and equal-tailed 95% credible interval for each parameter. + + :return: Formatted summary table as a string. + :rtype: str + """ + return posterior_summary(self.draws, self.param_names) + + def corner(self) -> Any: + """Return the parameter-correlation corner plot as a Plotly Figure. + + Requires the ``plotly`` library. + + :return: Plotly Figure. + """ + return plot_corner(self.draws, self.param_names) + + def trace(self, **kwargs) -> None: + """Plot MCMC trace plot. + + Requires the ``arviz`` library. + + :param kwargs: Additional keyword arguments passed to ``arviz.plot_trace``. + """ + plot_trace(self.draws, self.param_names, **kwargs) + + def distribution(self) -> Any: + """Return the per-parameter marginal posterior distributions as a Plotly Figure. + + Each panel overlays the posterior histogram, a smooth KDE marginal, the + 95% credible interval, the median, and the best posterior sample (when + ``logp`` is available). Requires the ``plotly`` library. + + :return: Plotly Figure. + """ + return plot_distribution(self.draws, self.param_names, logp=self.logp, return_figure=True) + + def credible_interval(self, alpha: float = 0.95) -> dict: + """Compute equal-tailed credible intervals for each parameter. + + :param alpha: Credible interval width (e.g. 0.95 for 95%). + :type alpha: float + :return: Dictionary mapping parameter name to ``(lower, upper)``. + :rtype: dict + """ + return credible_intervals(self.draws, self.param_names, alpha=alpha) + + def save(self, path: str) -> None: + """Persist this posterior trace to disk. + + Convenience wrapper around :func:`save_posterior`. + + :param path: File path prefix (see :func:`save_posterior`). + :type path: str + """ + save_posterior(self, path) + + def gelman_rubin(self) -> dict | None: + """Compute the Gelman-Rubin R-hat convergence diagnostic. + + Requires the ``arviz`` library and posterior draws with at least + two chains, i.e. shape ``(n_chains, n_draws, n_params)`` with + ``n_chains >= 2``. R-hat is undefined for a single chain. + + :return: Dictionary mapping parameter name to R-hat value, or ``None`` + if ``arviz`` is not available. + :rtype: dict | None + :raises ValueError: If ``self.draws`` does not contain at least two + chains. + """ + if not _HAS_ARVIZ: + warnings.warn( + 'The ``arviz`` library is required for Gelman-Rubin R-hat. Install it with ``pip install arviz``.', + UserWarning, + ) + return None + if self.draws.ndim < 3 or self.draws.shape[0] < 2: + raise ValueError( + 'Gelman-Rubin R-hat requires posterior draws with at least 2 chains ' + '(shape ``(n_chains, n_draws, n_params)`` with ``n_chains >= 2``).' + ) + data = _to_arviz_data(self.draws, self.param_names) + rhat = _arviz.rhat(data) + return {name: float(rhat[name].values) for name in self.param_names} + + +def posterior_summary(draws: np.ndarray, param_names: list[str]) -> str: + """Return a formatted summary table with mean, sd, and the equal-tailed + 2.5%/97.5% posterior quantiles for each parameter. + + The reported interval is the equal-tailed 95% credible interval; it is + not a highest-density interval (HDI) and the two coincide only for + symmetric unimodal posteriors. + + :param draws: Posterior samples, shape ``(n_samples, n_params)``. + :type draws: np.ndarray + :param param_names: Parameter names (one per column). + :type param_names: list[str] + :return: Formatted summary table as a string. + :rtype: str + """ + draws = np.asarray(draws) + lines = [f'{"parameter":<30s} {"mean":>10s} {"sd":>10s} {"q2.5%":>10s} {"q97.5%":>10s}'] + for i, name in enumerate(param_names): + col = draws[:, i] + lo, hi = np.percentile(col, [2.5, 97.5]) + lines.append(f'{name:<30s} {col.mean():>10.4f} {col.std():>10.4f} {lo:>10.4f} {hi:>10.4f}') + return '\n'.join(lines) + + +# Posterior pair-plot palette and styling, mirroring easydiffraction so the +# corner plots match across the two libraries. +_POSTERIOR_PAIR_MIN_SAMPLE_COUNT = 2 +_POSTERIOR_PAIR_COVARIANCE_RANK = 2 +_POSTERIOR_PAIR_SCATTER_MAX_POINTS = 1500 +_POSTERIOR_PAIR_CONTOUR_GRID_SIZE = 80 +_POSTERIOR_PAIR_DENSITY_GRID_SIZE = 256 + +_POSTERIOR_PAIR_MARGINAL_LINE_COLOR = 'rgb(44, 160, 44)' +_POSTERIOR_PAIR_MARGINAL_FILL_COLOR = 'rgba(44, 160, 44, 0.22)' +_POSTERIOR_PAIR_SCATTER_COLOR = 'rgba(140, 140, 140, 0.22)' +_POSTERIOR_HISTOGRAM_FILL_COLOR = 'rgba(120, 120, 120, 0.38)' +_POSTERIOR_HISTOGRAM_LINE_COLOR = 'rgba(120, 120, 120, 0.24)' +_POSTERIOR_INTERVAL_95_FILL_COLOR = 'rgba(214, 39, 40, 0.14)' +_POSTERIOR_MEDIAN_LINE_COLOR = 'rgb(80, 80, 80)' +_POSTERIOR_POINT_ESTIMATE_LINE_COLOR = 'rgb(214, 39, 40)' +_POSTERIOR_CONTOUR_FILL_COLORSCALE = [ + [0.0, 'rgba(224, 233, 255, 0.62)'], + [0.35, 'rgba(183, 203, 255, 0.70)'], + [0.60, 'rgba(138, 169, 252, 0.78)'], + [0.82, 'rgba(96, 131, 242, 0.84)'], + [1.0, 'rgba(58, 86, 224, 0.90)'], +] +_POSTERIOR_NEGATIVE_CONTOUR_FILL_COLORSCALE = [ + [0.0, 'rgba(255, 224, 224, 0.62)'], + [0.35, 'rgba(250, 188, 188, 0.70)'], + [0.60, 'rgba(245, 148, 148, 0.78)'], + [0.82, 'rgba(237, 104, 104, 0.84)'], + [1.0, 'rgba(215, 48, 39, 0.90)'], +] +_POSTERIOR_CONTOUR_LINE_COLORSCALE = [ + [0.0, 'rgba(183, 203, 255, 0.94)'], + [0.35, 'rgba(183, 203, 255, 0.94)'], + [0.35, 'rgba(138, 169, 252, 0.95)'], + [0.60, 'rgba(138, 169, 252, 0.95)'], + [0.60, 'rgba(96, 131, 242, 0.96)'], + [0.82, 'rgba(96, 131, 242, 0.96)'], + [0.82, 'rgba(58, 86, 224, 0.98)'], + [1.0, 'rgba(58, 86, 224, 0.98)'], +] +_POSTERIOR_NEGATIVE_CONTOUR_LINE_COLORSCALE = [ + [0.0, 'rgba(250, 188, 188, 0.94)'], + [0.35, 'rgba(250, 188, 188, 0.94)'], + [0.35, 'rgba(245, 148, 148, 0.95)'], + [0.60, 'rgba(245, 148, 148, 0.95)'], + [0.60, 'rgba(237, 104, 104, 0.96)'], + [0.82, 'rgba(237, 104, 104, 0.96)'], + [0.82, 'rgba(215, 48, 39, 0.98)'], + [1.0, 'rgba(215, 48, 39, 0.98)'], +] + + +def _posterior_axis_bounds(values: np.ndarray) -> tuple[float, float] | None: + """Return padded ``(lower, upper)`` plotting bounds for one posterior axis. + + :param values: Posterior samples for a single parameter. + :type values: np.ndarray + :return: Padded bounds, or ``None`` if there are no finite samples. + :rtype: tuple[float, float] | None + """ + data = np.asarray(values, dtype=float) + data = data[np.isfinite(data)] + if data.size == 0: + return None + data_min = float(np.min(data)) + data_max = float(np.max(data)) + data_range = data_max - data_min + padding = 0.05 * data_range if data_range > 0 else max(abs(data_min), 1.0) * 0.05 + if padding == 0: + padding = 1e-6 + return data_min - padding, data_max + padding + + +def _posterior_density_curve( + values: np.ndarray, + grid_size: int = _POSTERIOR_PAIR_DENSITY_GRID_SIZE, +) -> tuple[np.ndarray, np.ndarray] | None: + """Estimate a 1-D Gaussian-KDE marginal density normalised to unit area. + + :param values: Posterior samples for a single parameter. + :type values: np.ndarray + :param grid_size: Number of grid points at which to evaluate the density. + :type grid_size: int + :return: ``(grid, density)`` arrays, or ``None`` if a smooth density could + not be estimated (e.g. ``scipy`` missing or degenerate samples). + :rtype: tuple[np.ndarray, np.ndarray] | None + """ + try: + from scipy.stats import gaussian_kde + except ImportError: + return None + + data = np.asarray(values, dtype=float) + data = data[np.isfinite(data)] + if data.size < _POSTERIOR_PAIR_MIN_SAMPLE_COUNT: + return None + + bounds = _posterior_axis_bounds(data) + if bounds is None: + return None + grid = np.linspace(bounds[0], bounds[1], num=grid_size) + + if np.allclose(data, data[0]): + bandwidth = max(abs(data[0]) * 0.01, 1e-6) + density = np.exp(-0.5 * ((grid - data[0]) / bandwidth) ** 2) + density /= bandwidth * np.sqrt(2.0 * np.pi) + else: + try: + density = np.asarray(gaussian_kde(data)(grid), dtype=float) + except (np.linalg.LinAlgError, ValueError): + return None + + area = np.trapezoid(density, grid) + if area <= 0: + return None + return grid, density / area + + +def _posterior_density_surface( + x_values: np.ndarray, + y_values: np.ndarray, + grid_size: int = _POSTERIOR_PAIR_CONTOUR_GRID_SIZE, +) -> tuple[np.ndarray, np.ndarray, np.ndarray] | None: + """Estimate a 2-D Gaussian-KDE density surface for one pair panel. + + :param x_values: Posterior samples for the x-axis parameter. + :type x_values: np.ndarray + :param y_values: Posterior samples for the y-axis parameter. + :type y_values: np.ndarray + :param grid_size: Number of grid points per axis. + :type grid_size: int + :return: ``(x_grid, y_grid, density)``, or ``None`` if a smooth surface + could not be estimated (e.g. ``scipy`` missing or degenerate samples). + :rtype: tuple[np.ndarray, np.ndarray, np.ndarray] | None + """ + try: + from scipy.stats import gaussian_kde + except ImportError: + return None + + x_data = np.asarray(x_values, dtype=float) + y_data = np.asarray(y_values, dtype=float) + mask = np.isfinite(x_data) & np.isfinite(y_data) + x_data = x_data[mask] + y_data = y_data[mask] + if x_data.size < _POSTERIOR_PAIR_MIN_SAMPLE_COUNT or y_data.size < _POSTERIOR_PAIR_MIN_SAMPLE_COUNT: + return None + if np.allclose(x_data, x_data[0]) and np.allclose(y_data, y_data[0]): + return None + + pair_data = np.vstack([x_data, y_data]) + covariance = np.cov(pair_data) + if np.linalg.matrix_rank(covariance) < _POSTERIOR_PAIR_COVARIANCE_RANK: + return None + + x_bounds = _posterior_axis_bounds(x_data) + y_bounds = _posterior_axis_bounds(y_data) + if x_bounds is None or y_bounds is None: + return None + x_grid = np.linspace(x_bounds[0], x_bounds[1], num=grid_size) + y_grid = np.linspace(y_bounds[0], y_bounds[1], num=grid_size) + mesh_x, mesh_y = np.meshgrid(x_grid, y_grid) + try: + kde = gaussian_kde(pair_data) + density = np.asarray(kde(np.vstack([mesh_x.ravel(), mesh_y.ravel()])), dtype=float) + except (np.linalg.LinAlgError, ValueError): + return None + density = density.reshape(mesh_x.shape) + if not np.any(np.isfinite(density)): + return None + return x_grid, y_grid, density + + +def _posterior_contour_colorscales( + x_values: np.ndarray, + y_values: np.ndarray, +) -> tuple[list, list]: + """Return sign-aware fill and line contour palettes for one pair panel. + + Negatively correlated parameter pairs use a red palette; everything else + uses blue. + + :param x_values: Posterior samples for the x-axis parameter. + :type x_values: np.ndarray + :param y_values: Posterior samples for the y-axis parameter. + :type y_values: np.ndarray + :return: ``(fill_colorscale, line_colorscale)``. + :rtype: tuple[list, list] + """ + x_data = np.asarray(x_values, dtype=float) + y_data = np.asarray(y_values, dtype=float) + mask = np.isfinite(x_data) & np.isfinite(y_data) + if np.count_nonzero(mask) >= _POSTERIOR_PAIR_MIN_SAMPLE_COUNT: + correlation = float(np.corrcoef(x_data[mask], y_data[mask])[0, 1]) + if np.isfinite(correlation) and correlation < 0: + return _POSTERIOR_NEGATIVE_CONTOUR_FILL_COLORSCALE, _POSTERIOR_NEGATIVE_CONTOUR_LINE_COLORSCALE + return _POSTERIOR_CONTOUR_FILL_COLORSCALE, _POSTERIOR_CONTOUR_LINE_COLORSCALE + + +def _add_corner_marginal( + fig: Any, + go: Any, + *, + values: np.ndarray, + row: int, + col: int, + show_legend: bool, +) -> None: + """Add a diagonal marginal-density panel (smooth KDE, histogram fallback). + + :param fig: The Plotly Figure being built. + :param go: The ``plotly.graph_objects`` module. + :param values: Posterior samples for the diagonal parameter. + :type values: np.ndarray + :param row: 1-based subplot row. + :type row: int + :param col: 1-based subplot column. + :type col: int + :param show_legend: Whether this trace should add the legend entry. + :type show_legend: bool + """ + curve = _posterior_density_curve(values) + if curve is not None: + grid, density = curve + fig.add_trace( + go.Scatter( + x=grid, + y=density, + mode='lines', + line=dict(color=_POSTERIOR_PAIR_MARGINAL_LINE_COLOR, width=1), + fill='tozeroy', + fillcolor=_POSTERIOR_PAIR_MARGINAL_FILL_COLOR, + name='Marginal density', + legendgroup='marginal', + showlegend=show_legend, + hoverinfo='skip', + ), + row=row, + col=col, + ) + return + # scipy unavailable or degenerate samples: fall back to a histogram. + fig.add_trace( + go.Histogram( + x=values, + nbinsx=40, + histnorm='probability density', + marker=dict( + color=_POSTERIOR_PAIR_MARGINAL_FILL_COLOR, + line=dict(color=_POSTERIOR_PAIR_MARGINAL_LINE_COLOR, width=1), + ), + name='Marginal density', + legendgroup='marginal', + showlegend=show_legend, + hoverinfo='skip', + ), + row=row, + col=col, + ) + + +def _corner_contour_traces( + go: Any, + x_values: np.ndarray, + y_values: np.ndarray, +) -> tuple[Any, Any] | None: + """Build filled and line 2-D KDE contour traces for one pair panel. + + :param go: The ``plotly.graph_objects`` module. + :param x_values: Posterior samples for the x-axis parameter. + :type x_values: np.ndarray + :param y_values: Posterior samples for the y-axis parameter. + :type y_values: np.ndarray + :return: ``(fill_trace, line_trace)``, or ``None`` if no smooth surface + could be estimated. + :rtype: tuple[Any, Any] | None + """ + surface = _posterior_density_surface(x_values, y_values) + if surface is None: + return None + x_grid, y_grid, density = surface + + fill_colorscale, line_colorscale = _posterior_contour_colorscales(x_values, y_values) + density_max = float(np.max(density)) + contour_start = density_max * 0.20 + contour_end = density_max * 0.95 + contour_size = density_max * 0.15 + + fill_density = np.array(density, copy=True) + fill_density[fill_density < contour_start] = np.nan + fill_trace = go.Contour( + x=x_grid, + y=y_grid, + z=fill_density, + contours=dict( + coloring='fill', + showlabels=False, + showlines=False, + start=contour_start, + end=contour_end, + size=contour_size, + ), + colorscale=fill_colorscale, + zmin=contour_start, + zmax=contour_end, + connectgaps=False, + hoverinfo='skip', + showscale=False, + zorder=1, + ) + line_trace = go.Contour( + x=x_grid, + y=y_grid, + z=density, + contours=dict( + coloring='lines', + showlabels=False, + start=contour_start, + end=contour_end, + size=contour_size, + ), + colorscale=line_colorscale, + zmin=contour_start, + zmax=contour_end, + line=dict(width=0.9), + hoverinfo='skip', + showscale=False, + zorder=2, + ) + return fill_trace, line_trace + + +def plot_corner(draws: np.ndarray, param_names: list[str]) -> Any: + """Build a parameter-correlation corner plot as a Plotly Figure. + + Smooth Gaussian-KDE marginal densities on the diagonal, a posterior + scatter overlay with filled 2-D KDE contours on the lower triangle, and a + hidden upper triangle. Contours are coloured blue for positively + correlated pairs and red for negatively correlated ones. This mirrors the + posterior pair plot in ``easydiffraction``. Requires ``plotly`` (and + ``scipy`` for the KDE smoothing; without it the diagonal falls back to a + histogram and the contours are omitted). + + :param draws: Posterior samples, shape ``(n_samples, n_params)`` or + ``(n_chains, n_draws, n_params)``. + :type draws: np.ndarray + :param param_names: Parameter names (one per column). + :type param_names: list[str] + :return: Plotly Figure. + """ + _require_plotly() + import plotly.graph_objects as go + from plotly.subplots import make_subplots + + draws = np.asarray(draws) + if draws.ndim == 3: + draws = draws.reshape(-1, draws.shape[-1]) + n_params = len(param_names) + + wrapped_labels = [_wrap_pair_label(name) for name in param_names] + + # Full draws drive the smooth KDE surfaces; the scatter overlay is thinned + # so large posteriors stay responsive to render and pan. + n_samples = draws.shape[0] + if n_samples > _POSTERIOR_PAIR_SCATTER_MAX_POINTS: + stride = max(1, n_samples // _POSTERIOR_PAIR_SCATTER_MAX_POINTS) + scatter_draws = draws[::stride] + else: + scatter_draws = draws + + fig = make_subplots( + rows=n_params, + cols=n_params, + horizontal_spacing=0.02, + vertical_spacing=0.02, + ) + + # Track which trace types have already been added to the legend. + legend_shown = {'marginal': False, 'scatter': False, 'contour': False} + + for row in range(n_params): + for col in range(n_params): + r, c = row + 1, col + 1 + if col > row: + fig.update_xaxes(visible=False, row=r, col=c) + fig.update_yaxes(visible=False, row=r, col=c) + continue + if col == row: + _add_corner_marginal( + fig, + go, + values=draws[:, row], + row=r, + col=c, + show_legend=not legend_shown['marginal'], + ) + legend_shown['marginal'] = True + else: + fig.add_trace( + go.Scatter( + x=scatter_draws[:, col], + y=scatter_draws[:, row], + mode='markers', + marker=dict(size=4, color=_POSTERIOR_PAIR_SCATTER_COLOR), + name='Posterior samples', + legendgroup='scatter', + showlegend=not legend_shown['scatter'], + hoverinfo='skip', + zorder=0, + ), + row=r, + col=c, + ) + legend_shown['scatter'] = True + contour_traces = _corner_contour_traces(go, draws[:, col], draws[:, row]) + if contour_traces is not None: + fill_trace, line_trace = contour_traces + fill_trace.name = 'Posterior contours' + fill_trace.legendgroup = 'contour' + fill_trace.showlegend = not legend_shown['contour'] + line_trace.legendgroup = 'contour' + line_trace.showlegend = False + fig.add_trace(fill_trace, row=r, col=c) + fig.add_trace(line_trace, row=r, col=c) + legend_shown['contour'] = True + + # Axis labels: outer edges only (bottom row x-axes, leftmost column y-axes, + # including the top-left diagonal cell so its parameter is identifiable). + for i, label in enumerate(wrapped_labels): + fig.update_xaxes(title_text=label, title_font=dict(size=10), row=n_params, col=i + 1) + fig.update_yaxes(title_text=label, title_font=dict(size=10), row=i + 1, col=1) + # Diagonal y-axes are probability density — hide their tick labels (except the + # top-left, where ticks would be the only cue about the density scale). + for i in range(1, n_params): + fig.update_yaxes(showticklabels=False, row=i + 1, col=i + 1) + + fig.update_layout( + height=max(450, 180 * n_params), + width=max(550, 180 * n_params + 140), + showlegend=True, + legend=dict( + orientation='v', + yanchor='top', + y=1.0, + xanchor='left', + x=1.02, + font=dict(size=11), + itemsizing='constant', + ), + plot_bgcolor='white', + margin=dict(l=80, r=160, t=30, b=60), + ) + fig.update_xaxes(showgrid=False, zeroline=False, ticks='outside') + fig.update_yaxes(showgrid=False, zeroline=False, ticks='outside') + return fig + + +def plot_trace(draws: np.ndarray, param_names: list[str], return_figure: bool = False, **kwargs) -> Any: + """Plot MCMC trace plot. + + When *return_figure* is ``True`` a Plotly ``Figure`` is returned instead of + being displayed inline; the caller is responsible for rendering it. This + requires the ``plotly`` package. + + :param draws: Posterior samples, shape ``(n_chains, n_draws, n_params)`` or + ``(n_draws, n_params)``. + :type draws: np.ndarray + :param param_names: Parameter names (one per column). + :type param_names: list[str] + :param return_figure: Return a Plotly Figure instead of rendering inline. + :type return_figure: bool + :param kwargs: Additional keyword arguments passed to ``arviz.plot_trace`` + when *return_figure* is ``False``. + :return: Plotly Figure when *return_figure* is ``True``, otherwise ``None``. + """ + draws = np.asarray(draws) + if draws.ndim == 2: + draws = draws[np.newaxis, ...] # (1, n_draws, n_params) + # draws shape: (n_chains, n_draws, n_params) + + if return_figure: + try: + import plotly.graph_objects as go + from plotly.subplots import make_subplots + except ImportError: + warnings.warn( + 'The ``plotly`` library is required to build the trace figure. Install it with ``pip install plotly``.', + UserWarning, + stacklevel=2, + ) + return None + + n_params = len(param_names) + n_chains = draws.shape[0] + fig = make_subplots( + rows=n_params, + cols=2, + column_widths=[0.6, 0.4], + ) + colors = ['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728'] + for i, name in enumerate(param_names): + row = i + 1 + show_legend = i == 0 + for c in range(n_chains): + chain_draws = draws[c, :, i] + color = colors[c % len(colors)] + fig.add_trace( + go.Scatter( + y=chain_draws, + mode='lines', + line=dict(color=color, width=1), + name=f'chain {c}', + legendgroup=f'chain {c}', + showlegend=show_legend, + ), + row=row, + col=1, + ) + fig.add_trace( + go.Histogram( + x=chain_draws, + marker_color=color, + opacity=0.6, + name=f'chain {c}', + legendgroup=f'chain {c}', + showlegend=show_legend, + nbinsx=40, + ), + row=row, + col=2, + ) + fig.update_yaxes(title_text=name, title_font=dict(size=10), row=row, col=1) + fig.update_xaxes(title_text=name, title_font=dict(size=10), row=row, col=2) + fig.update_yaxes(title_text='Count', title_font=dict(size=10), row=row, col=2) + fig.update_xaxes(title_text='Draw index', title_font=dict(size=10), row=n_params, col=1) + fig.update_layout( + height=max(300, 200 * n_params), + barmode='overlay', + legend=dict(orientation='h', yanchor='bottom', y=1.02, xanchor='right', x=1, font=dict(size=10)), + ) + return fig + + _require_arviz() + idata = _to_arviz_data(draws, param_names) + _arviz.plot_trace(idata, var_names=param_names, **kwargs) + return None + + +def _posterior_marginal_y_range( + values: np.ndarray, + density_curve: tuple[np.ndarray, np.ndarray] | None, +) -> tuple[float, float] | None: + """Return a ``(0, max)`` y-axis range covering histogram and KDE density. + + The range spans the larger of the histogram (probability-density normalised) + and smooth-KDE peaks so credible-interval bands and reference lines, which + are drawn as full-height traces, reach the top of the panel. + + :param values: Posterior samples for a single parameter. + :type values: np.ndarray + :param density_curve: ``(grid, density)`` from :func:`_posterior_density_curve`, or ``None``. + :type density_curve: tuple[np.ndarray, np.ndarray] | None + :return: ``(0.0, padded_max)`` range, or ``None`` if no density is available. + :rtype: tuple[float, float] | None + """ + data = np.asarray(values, dtype=float) + data = data[np.isfinite(data)] + maxima: list[float] = [] + if data.size: + hist, _ = np.histogram(data, bins=40, density=True) + if hist.size: + maxima.append(float(np.max(hist))) + if density_curve is not None: + maxima.append(float(np.max(density_curve[1]))) + if not maxima: + return None + y_max = max(maxima) + if y_max <= 0: + return None + return 0.0, y_max * 1.08 + + +def _posterior_interval_band_trace( + go: Any, + *, + x0: float, + x1: float, + y_range: tuple[float, float], + name: str, + color: str, + show_legend: bool, +) -> Any: + """Return a filled rectangle marking a credible interval. + + :param go: The ``plotly.graph_objects`` module. + :param x0: Lower interval bound. + :type x0: float + :param x1: Upper interval bound. + :type x1: float + :param y_range: Panel y-axis range the band should span. + :type y_range: tuple[float, float] + :param name: Legend/trace name. + :type name: str + :param color: Fill colour. + :type color: str + :param show_legend: Whether this trace adds the legend entry. + :type show_legend: bool + :return: A Plotly Scatter trace. + """ + return go.Scatter( + x=[x0, x1, x1, x0, x0], + y=[y_range[0], y_range[0], y_range[1], y_range[1], y_range[0]], + mode='lines', + fill='toself', + fillcolor=color, + line=dict(color=color, width=0), + name=name, + legendgroup=name, + showlegend=show_legend, + hoverinfo='skip', + ) + + +def _posterior_reference_line_trace( + go: Any, + *, + x_value: float, + y_range: tuple[float, float], + name: str, + color: str, + dash: str, + show_legend: bool, +) -> Any: + """Return a vertical reference line for a posterior marginal panel. + + :param go: The ``plotly.graph_objects`` module. + :param x_value: Parameter value at which to draw the line. + :type x_value: float + :param y_range: Panel y-axis range the line should span. + :type y_range: tuple[float, float] + :param name: Legend/trace name. + :type name: str + :param color: Line colour. + :type color: str + :param dash: Plotly dash style (e.g. ``'dash'``, ``'dot'``). + :type dash: str + :param show_legend: Whether this trace adds the legend entry. + :type show_legend: bool + :return: A Plotly Scatter trace. + """ + return go.Scatter( + x=[x_value, x_value], + y=[y_range[0], y_range[1]], + mode='lines', + line=dict(color=color, width=2, dash=dash), + name=name, + legendgroup=name, + showlegend=show_legend, + hovertemplate=f'{name}: %{{x:.4f}}', + ) + + +def plot_distribution( + draws: np.ndarray, + param_names: list[str], + logp: np.ndarray | None = None, + return_figure: bool = False, + **kwargs, +) -> Any: + """Plot marginal posterior distributions for each parameter. + + Each panel overlays, mirroring ``easydiffraction``'s + ``project.display.posterior.distribution()``: + + * the posterior **histogram** (probability-density normalised), + * a smooth **Gaussian-KDE marginal** density curve (requires ``scipy``; + omitted if unavailable or the samples are degenerate), + * a shaded **95% credible interval** (equal-tailed 2.5/97.5 percentiles), + * a dashed **median** line, and + * a dotted **best posterior sample** line when *logp* is supplied. + + When *return_figure* is ``True`` a Plotly ``Figure`` is returned. + + :param draws: Posterior samples, shape ``(n_samples, n_params)`` or + ``(n_chains, n_draws, n_params)``. + :type draws: np.ndarray + :param param_names: Parameter names (one per column). + :type param_names: list[str] + :param logp: Log-posterior values, shape ``(n_samples,)``. When given, the + draw with the largest value marks the best posterior sample. + :type logp: np.ndarray | None + :param return_figure: Return a Plotly Figure instead of rendering inline. + :type return_figure: bool + :param kwargs: Additional keyword arguments (currently unused). + :return: Plotly Figure when *return_figure* is ``True``, otherwise ``None``. + """ + draws = np.asarray(draws) + if draws.ndim == 3: + draws = draws.reshape(-1, draws.shape[-1]) + # draws shape: (n_samples, n_params) + + best_index: int | None = None + if logp is not None: + logp = np.asarray(logp).reshape(-1) + if logp.size == draws.shape[0] and np.any(np.isfinite(logp)): + best_index = int(np.nanargmax(logp)) + + if not return_figure: + return None + + try: + import plotly.graph_objects as go + from plotly.subplots import make_subplots + except ImportError: + warnings.warn( + 'The ``plotly`` library is required to build the distribution figure. Install it with ``pip install plotly``.', + UserWarning, + stacklevel=2, + ) + return None + + n_params = len(param_names) + n_cols = min(3, n_params) + n_rows = (n_params + n_cols - 1) // n_cols + fig = make_subplots(rows=n_rows, cols=n_cols) + + # Each trace type contributes a single shared legend entry. + legend_shown = { + 'histogram': False, + 'marginal': False, + 'interval': False, + 'median': False, + 'best': False, + } + + for i, name in enumerate(param_names): + row = i // n_cols + 1 + col = i % n_cols + 1 + values = draws[:, i] + + density_curve = _posterior_density_curve(values) + y_range = _posterior_marginal_y_range(values, density_curve) + lower, upper = (float(v) for v in np.percentile(values, [2.5, 97.5])) + + # Credible-interval band first so it sits behind the density traces. + if y_range is not None: + fig.add_trace( + _posterior_interval_band_trace( + go, + x0=lower, + x1=upper, + y_range=y_range, + name='95% credible interval', + color=_POSTERIOR_INTERVAL_95_FILL_COLOR, + show_legend=not legend_shown['interval'], + ), + row=row, + col=col, + ) + legend_shown['interval'] = True + + fig.add_trace( + go.Histogram( + x=values, + histnorm='probability density', + marker=dict( + color=_POSTERIOR_HISTOGRAM_FILL_COLOR, + line=dict(color=_POSTERIOR_HISTOGRAM_LINE_COLOR, width=1), + ), + opacity=0.82, + nbinsx=40, + name='Posterior histogram', + legendgroup='histogram', + showlegend=not legend_shown['histogram'], + hovertemplate='sample=%{x:.4f}
density: %{y:.2f}', + ), + row=row, + col=col, + ) + legend_shown['histogram'] = True + + if density_curve is not None: + grid, density = density_curve + fig.add_trace( + go.Scatter( + x=grid, + y=density, + mode='lines', + line=dict(color=_POSTERIOR_PAIR_MARGINAL_LINE_COLOR, width=2), + fill='tozeroy', + fillcolor=_POSTERIOR_PAIR_MARGINAL_FILL_COLOR, + name='Marginal density', + legendgroup='marginal', + showlegend=not legend_shown['marginal'], + hovertemplate=f'{name}: %{{x:.4f}}
density: %{{y:.4f}}', + ), + row=row, + col=col, + ) + legend_shown['marginal'] = True + + if y_range is not None: + fig.add_trace( + _posterior_reference_line_trace( + go, + x_value=float(np.median(values)), + y_range=y_range, + name='Median', + color=_POSTERIOR_MEDIAN_LINE_COLOR, + dash='dash', + show_legend=not legend_shown['median'], + ), + row=row, + col=col, + ) + legend_shown['median'] = True + if best_index is not None: + fig.add_trace( + _posterior_reference_line_trace( + go, + x_value=float(values[best_index]), + y_range=y_range, + name='Best posterior sample', + color=_POSTERIOR_POINT_ESTIMATE_LINE_COLOR, + dash='dot', + show_legend=not legend_shown['best'], + ), + row=row, + col=col, + ) + legend_shown['best'] = True + fig.update_yaxes(range=list(y_range), row=row, col=col) + + if density_curve is not None: + fig.update_xaxes( + range=[float(density_curve[0][0]), float(density_curve[0][-1])], + row=row, + col=col, + ) + fig.update_xaxes(title_text=name, title_font=dict(size=11), row=row, col=col) + fig.update_yaxes(title_text='Probability density', title_font=dict(size=11), row=row, col=col) + + fig.update_layout( + height=max(300, 250 * n_rows), + barmode='overlay', + showlegend=True, + legend=dict(font=dict(size=10)), + plot_bgcolor='white', + ) + fig.update_xaxes(showgrid=False, zeroline=False, ticks='outside') + fig.update_yaxes(showgrid=False, zeroline=False, ticks='outside') + return fig + + +def credible_intervals( + draws: np.ndarray, + param_names: list[str], + alpha: float = 0.95, +) -> dict: + """Compute equal-tailed credible intervals for each parameter. + + :param draws: Posterior samples, shape ``(n_samples, n_params)``. + :type draws: np.ndarray + :param param_names: Parameter names (one per column). + :type param_names: list[str] + :param alpha: Credible interval width (e.g. 0.95 for 95%). + :type alpha: float + :return: Dictionary mapping parameter name to ``(lower, upper)``. + :rtype: dict + """ + draws = np.asarray(draws) + tail = (1.0 - alpha) / 2.0 + lo_pct = tail * 100 + hi_pct = (1.0 - tail) * 100 + result = {} + for i, name in enumerate(param_names): + col = draws[:, i] + lo, hi = np.percentile(col, [lo_pct, hi_pct]) + result[name] = (float(lo), float(hi)) + return result + + +def _save_parameter_state(model) -> dict: + """Save the current values and errors of all free parameters in a model. + + :param model: A reflectometry model with ``get_parameters()``. + :return: Dictionary mapping ``unique_name`` to ``(value, error)``. + :rtype: dict + """ + state = {} + for param in model.get_parameters(): + state[param.unique_name] = (param.value, param.error) + return state + + +def _restore_parameter_state(model, state: dict) -> None: + """Restore parameter values and errors from a saved state. + + :param model: A reflectometry model with ``get_parameters()``. + :param state: Dictionary mapping ``unique_name`` to ``(value, error)``. + """ + for param in model.get_parameters(): + if param.unique_name in state: + param.value = state[param.unique_name][0] + param.error = state[param.unique_name][1] + + +def _apply_draw(model, draws: np.ndarray, param_names: list[str], row: int) -> None: + """Apply a single posterior draw to the model parameters. + + Parameter lookup uses ``unique_name``, matching the BUMPS names after + removing the minimizer prefix, which avoids collisions when repeated models + or multi-contrast fits contain similarly named parameters. + + :param model: A reflectometry model with ``get_parameters()``. + :param draws: Posterior samples array. + :param param_names: Parameter names matching the columns of ``draws``. + :param row: Index of the draw to apply. + """ + param_lookup = {p.unique_name: p for p in model.get_parameters()} + for j, name in enumerate(param_names): + if name in param_lookup: + param_lookup[name].value = float(draws[row, j]) + + +def posterior_predictive_reflectivity( + draws: np.ndarray, + param_names: list[str], + model, + q_values: np.ndarray, + n_samples: int = 200, +) -> tuple[np.ndarray, np.ndarray, np.ndarray]: + """Compute the posterior predictive reflectivity with credible intervals. + + Parameter values and errors are saved before applying any posterior draw + and restored in a ``finally`` block, so the model is not left mutated. + + :param draws: Posterior samples, shape ``(n_samples_posterior, n_params)``. + :type draws: np.ndarray + :param param_names: Parameter names matching the columns of ``draws``. + :type param_names: list[str] + :param model: A reflectometry model with ``interface.fit_func``. + :param q_values: Q values at which to evaluate reflectivity. + :type q_values: np.ndarray + :param n_samples: Number of posterior draws to use (last ``n_samples``). + :type n_samples: int + :return: Tuple of ``(median, lower_95, upper_95)`` reflectivity arrays. + :rtype: tuple[np.ndarray, np.ndarray, np.ndarray] + """ + draws = np.asarray(draws) + q_values = np.asarray(q_values) + + n_total = draws.shape[0] + n_use = min(n_samples, n_total) + sample_indices = range(n_total - n_use, n_total) + + saved_state = _save_parameter_state(model) + try: + reflectivity_samples = [] + for i in sample_indices: + _apply_draw(model, draws, param_names, i) + r_calc = model.interface.fit_func(q_values, model.unique_name) + reflectivity_samples.append(np.asarray(r_calc)) + finally: + _restore_parameter_state(model, saved_state) + + reflectivity_samples = np.array(reflectivity_samples) + median = np.median(reflectivity_samples, axis=0) + lower = np.percentile(reflectivity_samples, 2.5, axis=0) + upper = np.percentile(reflectivity_samples, 97.5, axis=0) + return median, lower, upper + + +def posterior_predictive_sld_profile( + draws: np.ndarray, + param_names: list[str], + model, + n_samples: int = 200, +) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: + """Compute the posterior predictive SLD profile with credible intervals. + + Parameter values and errors are saved before applying any posterior draw + and restored in a ``finally`` block, so the model is not left mutated. + + :param draws: Posterior samples, shape ``(n_samples_posterior, n_params)``. + :type draws: np.ndarray + :param param_names: Parameter names matching the columns of ``draws``. + :type param_names: list[str] + :param model: A reflectometry model with ``interface.sld_profile``. + :param n_samples: Number of posterior draws to use (last ``n_samples``). + :type n_samples: int + :return: Tuple of ``(z, median, lower_95, upper_95)`` SLD profile arrays. + :rtype: tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray] + """ + draws = np.asarray(draws) + + n_total = draws.shape[0] + n_use = min(n_samples, n_total) + sample_indices = range(n_total - n_use, n_total) + + saved_state = _save_parameter_state(model) + try: + sld_samples = [] + z_shared = None + for i in sample_indices: + _apply_draw(model, draws, param_names, i) + z, sld = model.interface.sld_profile(model.unique_name) + if z_shared is None: + z_shared = np.asarray(z) + sld_samples.append(np.asarray(sld)) + finally: + _restore_parameter_state(model, saved_state) + + sld_samples = np.array(sld_samples) + median = np.median(sld_samples, axis=0) + lower = np.percentile(sld_samples, 2.5, axis=0) + upper = np.percentile(sld_samples, 97.5, axis=0) + return z_shared, median, lower, upper + + +# =================================================================== +# Persistence helpers — save / load a posterior trace to / from disk +# =================================================================== + +_SIDECAR_SCHEMA_VERSION = 1 + + +def _easyreflectometry_version() -> str: + """Return the installed easyreflectometry version string.""" + try: + from importlib.metadata import version as _v + + return _v('easyreflectometry') + except Exception: + return 'unknown' + + +def _data_fingerprint( + x_list: list[np.ndarray], + y_list: list[np.ndarray], + w_list: list[np.ndarray], +) -> str | None: + """Return a SHA-256 hex digest of concatenated (x|y|weights), or None.""" + try: + h = hashlib.sha256() + for arr in list(x_list) + list(y_list) + list(w_list): + h.update(np.ascontiguousarray(arr, dtype=np.float64).tobytes()) + return h.hexdigest() + except Exception: + return None + + +def save_posterior(results: 'PosteriorResults', path: str) -> None: + """Persist a sampling trace to disk using BUMPS' native state files. + + Writes ``-*.mc`` (BUMPS ``save_state`` output) plus a sidecar + ``.params.json`` holding parameter names and metadata so that + :func:`load_posterior` can reconstruct a fully populated + :class:`PosteriorResults` without re-deriving names from the model. + + Note that ``save_state`` writes **multiple** files (one per DREAM + component: chain, point, and stats). The ``path`` argument is a + prefix; the actual files will be ``-chain.mc``, + ``-point.mc``, and ``-stats.mc``. + + :param results: The posterior results to persist. Must have a + non-``None`` ``sampler_state``. + :type results: PosteriorResults + :param path: File path prefix. BUMPS appends its own suffixes. + :type path: str + :raises ValueError: If ``results.sampler_state`` is ``None``. + :raises TypeError: If ``results.sampler_state`` is not a BUMPS + ``MCMCDraw`` object. + """ + from bumps.dream.state import MCMCDraw + from bumps.dream.state import save_state + + if results.sampler_state is None: + raise ValueError( + 'This PosteriorResults has no sampler_state, so the chain ' + 'cannot be saved or resumed. Re-run sample() and wrap the ' + "returned dict's 'state' value into PosteriorResults." + ) + if not isinstance(results.sampler_state, MCMCDraw): + raise TypeError( + f'sampler_state must be a BUMPS MCMCDraw object, got ' + f'{type(results.sampler_state).__name__}. Only BUMPS DREAM ' + 'traces can be persisted with save_posterior.' + ) + + save_state(results.sampler_state, path) + + # Write the sidecar JSON + sidecar = { + 'schema_version': _SIDECAR_SCHEMA_VERSION, + 'param_names': results.param_names, + 'easyreflectometry_version': _easyreflectometry_version(), + } + with open(f'{path}.params.json', 'w') as f: + json.dump(sidecar, f, indent=2) + + +def load_posterior(path: str, skip: int = 0) -> 'PosteriorResults': + """Reload a trace saved by :func:`save_posterior` into a + :class:`PosteriorResults`. + + The returned object's ``sampler_state`` can be fed back into the core + ``Sampler`` (via ``Sampler.load_state(...)`` / ``Sampler.extend(...)``) + to extend the chain. + + :param path: File path prefix used in :func:`save_posterior`. + :type path: str + :param skip: Discard the first ``skip`` saved generations on load, + forwarded to ``bumps.dream.state.load_state(path, skip=skip)``. + Useful for trimming additional burn-in without re-sampling. + :type skip: int + :return: A fully populated :class:`PosteriorResults`. + :rtype: PosteriorResults + """ + from bumps.dream.state import load_state + + state = load_state(path, skip=skip) + _draw = state.draw() + draws = _draw.points + logp = _draw.logp # .logp is on the Draw object, NOT state.logp + + # Restore param_names: prefer the sidecar; fall back to state.labels + param_names: list[str] | None = None + try: + with open(f'{path}.params.json', 'r') as f: + sidecar = json.load(f) + if sidecar.get('schema_version') == _SIDECAR_SCHEMA_VERSION: + param_names = sidecar.get('param_names') + except (FileNotFoundError, json.JSONDecodeError, KeyError): + pass + + if param_names is None: + # Fallback: strip BUMPS 'p' prefix from state.labels + param_names = [ + lbl[len(MINIMIZER_PARAMETER_PREFIX) :] if lbl.startswith(MINIMIZER_PARAMETER_PREFIX) else lbl + for lbl in state.labels + ] + + return PosteriorResults( + draws=draws, + param_names=param_names, + logp=logp, + sampler_state=state, + ) diff --git a/src/easyreflectometry/calculators/__init__.py b/src/easyreflectometry/calculators/__init__.py index 2411a89f..9f7bde45 100644 --- a/src/easyreflectometry/calculators/__init__.py +++ b/src/easyreflectometry/calculators/__init__.py @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + import traceback from .calculator_base import CalculatorBase @@ -28,4 +31,4 @@ traceback.print_exc() print('Warning: refl1d is not installed') -__all__ = [CalculatorBase, CalculatorFactory] + imported_calculators +__all__ = ['CalculatorBase', 'CalculatorFactory'] + [c.__name__ for c in imported_calculators] diff --git a/src/easyreflectometry/calculators/bornagain/calculator.py b/src/easyreflectometry/calculators/bornagain/calculator.py index 06d86986..2ebb2c08 100644 --- a/src/easyreflectometry/calculators/bornagain/calculator.py +++ b/src/easyreflectometry/calculators/bornagain/calculator.py @@ -1,4 +1,6 @@ -__author__ = 'github.com/arm61' +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + from easyscience.fitting.calculators.interface_factory import ItemContainer @@ -18,9 +20,7 @@ class BornAgain(CalculatorBase): - """ - Calculator for BornAgain - """ + """Calculator for BornAgain.""" name = 'BornAgain' @@ -45,23 +45,26 @@ class BornAgain(CalculatorBase): } def __init__(self): + """Init function.""" super().__init__() self._wrapper = BornAgainWrapper() def reset_storage(self) -> None: - """ - Reset the storage area of the calculator - """ + """Reset the storage area of the calculator.""" self._wrapper.reset_storage() def create(self, model: Material | Layer | Multilayer | Model) -> list[ItemContainer]: - """ - Creation function + """Creation function. - :param model: Object to be created - :type model: Union[Material, Layer, Item, Model] - :return: Item containers of the objects - :rtype: List[ItemContainer] + Parameters + ---------- + model : Material | Layer | Multilayer | Model + Object to be created. + + Returns + ------- + List[ItemContainer] + Item containers of the objects. """ r_list = [] t_ = type(model) @@ -130,52 +133,61 @@ def create(self, model: Material | Layer | Multilayer | Model) -> list[ItemConta return r_list def assign_material_to_layer(self, material_id: int, layer_id: int) -> None: - """ - Assign a material to a layer. - - :param material_name: The material name - :type material_name: str - :param layer_name: The layer name - :type layer_name: str + """Assign a material to a layer. + + Parameters + ---------- + layer_id : int + material_id : int + material_name : str + The material name. + layer_name : str + The layer name. """ self._wrapper.assign_material_to_layer(material_id, layer_id) def add_layer_to_item(self, layer_id: int, item_id: int) -> None: - """ - Add a layer to the item stack - - :param item_id: The item id - :type item_id: int - :param layer_id: The layer id - :type layer_id: int + """Add a layer to the item stack. + + Parameters + ---------- + item_id : int + The item id. + layer_id : int + The layer id. """ self._wrapper.add_layer_to_item(layer_id, item_id) def remove_layer_from_item(self, layer_id: int, item_id: int) -> None: - """ - Remove a layer from an item stack - - :param item_id: The item id - :param layer_id: The layer id + """Remove a layer from an item stack. + + Parameters + ---------- + item_id : int + The item id. + layer_id : int + The layer id. """ self._wrapper.remove_layer_from_item(layer_id, item_id) def add_item_to_model(self, item_id: int) -> None: - """ - Add a layer to the item stack + """Add a layer to the item stack. - :param item_id: The item id - :type item_id: int + Parameters + ---------- + item_id : int + The item id. """ self._wrapper.add_item(item_id) def remove_item_from_model(self, item_id: int) -> None: - """ - Remove a layer from the item stack - - :param item_id: The item id - :type item_id: int - :param layer_id: The layer id - :type layer_id: int + """Remove a layer from the item stack. + + Parameters + ---------- + item_id : int + The item id. + layer_id : int + The layer id. """ self._wrapper.remove_item(item_id) diff --git a/src/easyreflectometry/calculators/bornagain/wrapper.py b/src/easyreflectometry/calculators/bornagain/wrapper.py index 5ad3b985..b0baf9a9 100644 --- a/src/easyreflectometry/calculators/bornagain/wrapper.py +++ b/src/easyreflectometry/calculators/bornagain/wrapper.py @@ -1,4 +1,6 @@ -__author__ = 'github.com/arm61' +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + import bornagain as ba import numpy as np @@ -14,6 +16,7 @@ class BornAgainWrapper(WrapperBase): def __init__(self): + """Init function.""" super().__init__() self.storage = { 'layer_material': {}, @@ -24,9 +27,7 @@ def __init__(self): } def reset_storage(self): - """ - Reset the storage area to blank. - """ + """Reset the storage area to blank.""" super().reset_storage() self.storage = { 'layer_material': {}, @@ -37,20 +38,23 @@ def reset_storage(self): } def create_material(self, name): - """ - Create a material using SLD. + """Create a material using SLD. - :param name: The name of the material - :type name: str + Parameters + ---------- + name : str + The name of the material. """ self.storage['material'][name] = ba.MaterialBySLD(str(name), 0.0, 0.0) def update_material(self, name, **kwargs): - """ - Update a material. + """Update a material. - :param name: The name of the material - :type name: str + Parameters + ---------- + **kwargs : + name : str + The name of the material. """ current_value = self.storage['material'][name].materialData() real = current_value.real @@ -64,40 +68,48 @@ def update_material(self, name, **kwargs): self.storage['material'][name] = ba.MaterialBySLD(str(name), real, imag) def get_material_value(self, name, key): - """ - A function to get a given material value - - :param name: The material name - :type name: str - :param key: The given value keys - :type name: str - :return: The desired value - :rtype: float + """A function to get a given material value. + + Parameters + ---------- + name : str + The material name. + key : str + The given value keys. + + Returns + ------- + float + The desired value. """ current_value = self.storage['material'][name].materialData() return getattr(current_value, key) / 1e-6 def create_layer(self, name): - """ - Create a layer using Slab. + """Create a layer using Slab. - :param name: The name of the layer - :type name: str + Parameters + ---------- + name : str + The name of the layer. """ self.storage['layer'][name] = ba.Layer(ba.MaterialBySLD('A', 0, 0)) self.storage['roughness'][name] = ba.LayerRoughness() def update_layer(self, name, **kwargs): - """ - Update a layer in a given item. + """Update a layer in a given item. - :param name: The layer name - :type name: str + Parameters + ---------- + **kwargs : + name : str + The layer name. """ if 'thickness' in kwargs.keys(): thickness = kwargs['thickness'] self.storage['layer'][name] = ba.Layer( - self.storage['material'][self.storage['layer_material'][name]], thickness * ba.angstrom + self.storage['material'][self.storage['layer_material'][name]], + thickness * ba.angstrom, ) if 'sigma' in kwargs.keys(): sigma = kwargs['sigma'] @@ -105,15 +117,19 @@ def update_layer(self, name, **kwargs): self.storage['roughness'][name].setSigma(sigma * ba.angstrom) def get_layer_value(self, name, key): - """ - A function to get a given layer value - - :param name: The layer name - :type name: str - :param key: The given value keys - :type name: str - :return: The desired value - :rtype: float + """A function to get a given layer value. + + Parameters + ---------- + name : str + The layer name. + key : str + The given value keys. + + Returns + ------- + float + The desired value. """ layer = self.storage['layer'][name] roughness = self.storage['roughness'][name] @@ -123,43 +139,48 @@ def get_layer_value(self, name, key): return roughness.getSigma() / ba.angstrom def create_item(self, name): - """ - Create an item. + """Create an item. - :param name: The name of the item - :type name: str + Parameters + ---------- + name : str + The name of the item. """ self.storage['item'][name] = [] self.storage['item_repeats'][name] = 1 def update_item(self, name, **kwargs): - """ - Update a layer. + """Update a layer. - :param name: The item name - :type name: str + Parameters + ---------- + **kwargs : + name : str + The item name. """ if 'repeats' in kwargs.keys(): self.storage['item_repeats'][name] = kwargs['repeats'] def get_item_value(self, name, key): - """ - A function to get a given item value - - :param name: The item name - :type name: str - :param key: The given value keys - :type name: str - :return: The desired value - :rtype: float + """A function to get a given item value. + + Parameters + ---------- + name : str + The item name. + key : str + The given value keys. + + Returns + ------- + float + The desired value. """ if key == 'repeats': return self.storage['item_repeats'][name] def create_model(self): - """ - Create a model for analysis - """ + """Create a model for analysis.""" self.storage['model'] = ba.Multilayer() self.storage['model'].setRoughnessModel(ba.RoughnessModel.NEVOT_CROCE) self.storage['model_items'] = [] @@ -168,65 +189,72 @@ def create_model(self): self.storage['model_parameters']['resolution'] = 0 def update_model(self, name, **kwargs): - """ - Update the non-structural parameters of the model - """ + """Update the non-structural parameters of the model.""" model = self.storage[name + '_parameters'] for key in kwargs.keys(): model[key] = kwargs[key] def get_model_value(self, name, key): - """ - A function to get a given model value + """A function to get a given model value. + + Parameters + ---------- + name : + key : str + The given value keys. - :param key: The given value keys - :type name: str - :return: The desired value - :rtype: float + Returns + ------- + float + The desired value. """ model = self.storage[name + '_parameters'] return model[key] def assign_material_to_layer(self, material_name, layer_name): - """ - Assign a material to a layer. + """Assign a material to a layer. - :param material_name: The material name - :type material_name: str - :param layer_name: The layer name - :type layer_name: str + Parameters + ---------- + material_name : str + The material name. + layer_name : str + The layer name. """ self.storage['layer_material'][layer_name] = material_name def add_layer_to_item(self, layer_name, item_name): - """ - Create a layer from the material of the same name, in a given item. + """Create a layer from the material of the same name, in a given item. - :param layer_name: The layer name - :type layer_name: int - :param item_name: The item name - :type item_name: int + Parameters + ---------- + layer_name : int + The layer name. + item_name : int + The item name. """ item = self.storage['item'][item_name] item.append(layer_name) def add_item(self, item_name): - """ - Add an item to the model. + """Add an item to the model. - :param item_name: items to add to model - :type item_name: str + Parameters + ---------- + item_name : str + Items to add to model. """ self.storage['model_items'].append(item_name) def remove_layer_from_item(self, layer_name, item_name): - """ - Remove a layer in a given item. + """Remove a layer in a given item. - :param layer_name: The layer name - :type layer_name: int - :param item_name: The item name - :type item_name: int + Parameters + ---------- + layer_name : int + The layer name. + item_name : int + The item name. """ layers_idx = self.storage['item'][item_name].index(layer_name) del self.storage['layer'][layer_name] @@ -234,11 +262,12 @@ def remove_layer_from_item(self, layer_name, item_name): del self.storage['layer_material'][layer_name] def remove_item(self, item_name): - """ - Remove a given item. + """Remove a given item. - :param item_name: The item name - :type item_name: int + Parameters + ---------- + item_name : int + The item name. """ item_idx = self.storage['model_items'].index(item_name) del self.storage['model_items'][item_idx] @@ -250,9 +279,17 @@ def remove_item(self, item_name): def calculate(self, q_array: np.ndarray) -> np.ndarray: """For a given q array calculate the corresponding reflectivity. - :param q_array: array of data points to be calculated - :param model_name: the model name - :return: reflectivity calculated at q + Parameters + ---------- + q_array : np.ndarray + Array of data points to be calculated. + model_name : + The model name. + + Returns + ------- + np.ndarray + Reflectivity calculated at q. """ # 3.5 sigma to sync with refnx n_sig = 3.5 @@ -261,7 +298,8 @@ def calculate(self, q_array: np.ndarray) -> np.ndarray: scan = ba.QSpecScan(q_array / ba.angstrom) scan.setAbsoluteQResolution( - distr, q_array / ba.angstrom * (self.storage['model_parameters']['resolution'] * 0.5 / 100) + distr, + q_array / ba.angstrom * (self.storage['model_parameters']['resolution'] * 0.5 / 100), ) simulation = ba.SpecularSimulation() @@ -286,13 +324,14 @@ def calculate(self, q_array: np.ndarray) -> np.ndarray: ) def sld_profile(self) -> np.ndarray: - """ - Return the scattering length density profile. + """Return the scattering length density profile. This is borrowed from the refnx implementation of the scattering length density. - :return: z and sld(z) - :rtype: tuple[np.ndarray, np.ndarray] + Returns + ------- + tuple[np.ndarray, np.ndarray] + Z and sld(z). """ number_of_layers = 0 for i in self.storage['model_items']: @@ -331,6 +370,7 @@ def sld_profile(self) -> np.ndarray: # use erf for roughness function, but step if the roughness is zero def step(z, scale=1, loc=0): + """Step function.""" new_z = z - loc f = np.ones_like(new_z) * 0.5 f[new_z <= -scale] = 0 diff --git a/src/easyreflectometry/calculators/calculator_base.py b/src/easyreflectometry/calculators/calculator_base.py index 7d1314cd..e2a92804 100644 --- a/src/easyreflectometry/calculators/calculator_base.py +++ b/src/easyreflectometry/calculators/calculator_base.py @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + from __future__ import annotations from abc import ABCMeta @@ -19,9 +22,7 @@ class CalculatorBase(SerializerComponent, metaclass=ABCMeta): - """ - This class is a template and defines all properties that a calculator should have. - """ + """This class is a template and defines all properties that a calculator should have.""" _calculators: list[CalculatorBase] = [] # class variable to store all calculators _material_link: dict[str, str] @@ -30,27 +31,36 @@ class CalculatorBase(SerializerComponent, metaclass=ABCMeta): _model_link: dict[str, str] def __init_subclass__(cls, is_abstract: bool = False, **kwargs) -> None: - r"""Initialise all subclasses so that they can be created in the factory - - :param is_abstract: Is this a subclass which shouldn't be dded - :param kwargs: key word arguments + r"""Initialise all subclasses so that they can be created in the factory. + + Parameters + ---------- + cls : + is_abstract : bool, optional + Is this a subclass which shouldn't be dded. By default, False. + **kwargs : + Key word arguments. """ super().__init_subclass__(**kwargs) if not is_abstract: cls._calculators.append(cls) def __init__(self): + """Init function.""" self._namespace = {} self._wrapper: WrapperBase def reset_storage(self) -> None: - """Reset the storage area of the calculator""" + r"""Reset the storage area of the calculator.""" self._wrapper.reset_storage() def create(self, model: Material | Layer | Multilayer | Model) -> list[ItemContainer]: - """Creation function + """Creation function. - :param model: Object to be created + Parameters + ---------- + model : Material | Layer | Multilayer | Model + Object to be created. """ r_list = [] t_ = type(model) @@ -122,72 +132,106 @@ def create(self, model: Material | Layer | Multilayer | Model) -> list[ItemConta def assign_material_to_layer(self, material_id: str, layer_id: str) -> None: """Assign a material to a layer. - :param material_id: The material name - :param layer_id: The layer name + Parameters + ---------- + material_id : str + The material name. + layer_id : str + The layer name. """ self._wrapper.assign_material_to_layer(material_id, layer_id) def add_layer_to_item(self, layer_id: str, item_id: str) -> None: - """Add a layer to the item stack - - :param item_id: The item id - :param layer_id: The layer id + """Add a layer to the item stack. + + Parameters + ---------- + item_id : str + The item id. + layer_id : str + The layer id. """ self._wrapper.add_layer_to_item(layer_id, item_id) def remove_layer_from_item(self, layer_id: str, item_id: str) -> None: - """Remove a layer from an item stack - - :param item_id: The item id - :param layer_id: The layer id + """Remove a layer from an item stack. + + Parameters + ---------- + item_id : str + The item id. + layer_id : str + The layer id. """ self._wrapper.remove_layer_from_item(layer_id, item_id) def add_item_to_model(self, item_id: str, model_id: str) -> None: - """Add a layer to the item stack - - :param item_id: The item id - :param model_id: The model id + """Add a layer to the item stack. + + Parameters + ---------- + item_id : str + The item id. + model_id : str + The model id. """ self._wrapper.add_item(item_id, model_id) def remove_item_from_model(self, item_id: str, model_id: str) -> None: - """Remove an item from the model - - :param item_id: The item id - :param model_id: The model id + """Remove an item from the model. + + Parameters + ---------- + item_id : str + The item id. + model_id : str + The model id. """ self._wrapper.remove_item(item_id, model_id) def reflectity_profile(self, x_array: np.ndarray, model_id: str) -> np.ndarray: """Determines the reflectivity profile for the given range and model. - :param x_array: points to be calculated at - :param model_id: The model id + Parameters + ---------- + x_array : np.ndarray + Points to be calculated at. + model_id : str + The model id. """ return self._wrapper.calculate(x_array, model_id) def sld_profile(self, model_id: str) -> tuple[np.ndarray, np.ndarray]: - """ - Return the scattering length density profile. + """Return the scattering length density profile. - :param model_id: The model id - :return: z and sld(z) + Parameters + ---------- + model_id : str + The model id. + + Returns + ------- + tuple[np.ndarray, np.ndarray] + z and sld(z). """ return self._wrapper.sld_profile(model_id) def set_resolution_function(self, resolution_function: Callable[[np.array], np.array]) -> None: + """Set resolution function.""" return self._wrapper.set_resolution_function(resolution_function) @property def include_magnetism(self): + """Include magnetism.""" return self._wrapper.magnetism @include_magnetism.setter def include_magnetism(self, magnetism: bool): - """ - Set the magnetism flag for the calculator + """Set the magnetism flag for the calculator. - :param magnetism: True if the calculator should include magnetism + Parameters + ---------- + magnetism : bool + True if the calculator should include magnetism. """ self._wrapper.magnetism = magnetism diff --git a/src/easyreflectometry/calculators/factory.py b/src/easyreflectometry/calculators/factory.py index 15e996fd..c3e1479c 100644 --- a/src/easyreflectometry/calculators/factory.py +++ b/src/easyreflectometry/calculators/factory.py @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + __author__ = 'github.com/wardsimon' from typing import Callable @@ -8,16 +11,20 @@ class CalculatorFactory(InterfaceFactoryTemplate): def __init__(self): + """Init function.""" super().__init__(interface_list=CalculatorBase._calculators) def reset_storage(self) -> None: + """Reset storage.""" return self().reset_storage() def sld_profile(self, model_id: str) -> tuple: + """Sld profile.""" return self().sld_profile(model_id) @property def fit_func(self) -> Callable: + """Fit func.""" """ Pass through to the underlying interfaces fitting function. @@ -32,6 +39,7 @@ def fit_func(self) -> Callable: #""" def __fit_func(*args, **kwargs): + """Fit func.""" return self().reflectity_profile(*args, **kwargs) return __fit_func diff --git a/src/easyreflectometry/calculators/refl1d/calculator.py b/src/easyreflectometry/calculators/refl1d/calculator.py index a472b7b5..2f5068de 100644 --- a/src/easyreflectometry/calculators/refl1d/calculator.py +++ b/src/easyreflectometry/calculators/refl1d/calculator.py @@ -1,13 +1,13 @@ -__author__ = 'github.com/arm61' +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + from ..calculator_base import CalculatorBase from .wrapper import Refl1dWrapper class Refl1d(CalculatorBase): - """ - Calculator for refl1 - """ + """Calculator for refl1.""" name = 'refl1d' @@ -31,5 +31,6 @@ class Refl1d(CalculatorBase): } def __init__(self): + """Init function.""" super().__init__() self._wrapper = Refl1dWrapper() diff --git a/src/easyreflectometry/calculators/refl1d/wrapper.py b/src/easyreflectometry/calculators/refl1d/wrapper.py index e47cf052..6985d47c 100644 --- a/src/easyreflectometry/calculators/refl1d/wrapper.py +++ b/src/easyreflectometry/calculators/refl1d/wrapper.py @@ -1,4 +1,6 @@ -__author__ = 'github.com/arm61' +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + from typing import Tuple @@ -6,8 +8,6 @@ from refl1d import names from refl1d.sample.layers import Repeat -from easyreflectometry.model import PercentageFwhm - from ..wrapper_base import WrapperBase RESOLUTION_PADDING = 3.5 @@ -17,18 +17,22 @@ class Refl1dWrapper(WrapperBase): def create_material(self, name: str): - """ - Create a material using SLD. + """Create a material using SLD. - :param name: The name of the material + Parameters + ---------- + name : str + The name of the material. """ self.storage['material'][name] = names.SLD(str(name)) def create_layer(self, name: str): - """ - Create a layer using Slab. + """Create a layer using Slab. - :param name: The name of the layer + Parameters + ---------- + name : str + The name of the layer. """ if self._magnetism: magnetism = names.Magnetism(rhoM=0.0, thetaM=0.0) @@ -37,10 +41,12 @@ def create_layer(self, name: str): self.storage['layer'][name] = names.Slab(name=str(name), magnetism=magnetism) def create_item(self, name: str): - """ - Create an item using Repeat. + """Create an item using Repeat. - :param name: The name of the item + Parameters + ---------- + name : str + The name of the item. """ self.storage['item'][name] = Repeat(names.Stack(names.Slab(names.SLD(), thickness=0, interface=0)), name=str(name)) del self.storage['item'][name].stack[0] @@ -48,8 +54,11 @@ def create_item(self, name: str): def update_layer(self, name: str, **kwargs): """Update a layer in a given item. - :param name: The layer name. - :param kwargs: + Parameters + ---------- + name : str + The layer name. + **kwargs : """ kwargs_no_magnetism = {k: v for k, v in kwargs.items() if k != 'magnetism_rhoM' and k != 'magnetism_thetaM'} super().update_layer(name, **kwargs_no_magnetism) @@ -58,10 +67,14 @@ def update_layer(self, name: str, **kwargs): self.storage['layer'][name].magnetism = magnetism def get_layer_value(self, name: str, key: str) -> float: - """A function to get a given layer value - - :param name: The layer name - :param key: The given value keys + """A function to get a given layer value. + + Parameters + ---------- + name : str + The layer name. + key : str + The given value keys. """ if key in ['magnetism_rhoM', 'magnetism_thetaM']: return getattr( @@ -70,78 +83,105 @@ def get_layer_value(self, name: str, key: str) -> float: return super().get_layer_value(name, key) def create_model(self, name: str): - """ - Create a model for analysis + """Create a model for analysis. - :param name: Name for the model + Parameters + ---------- + name : str + Name for the model. """ self.storage['model'][name] = {'scale': 1, 'bkg': 0, 'items': []} def update_model(self, name: str, **kwargs): - """ - Update the non-structural parameters of the model + """Update the non-structural parameters of the model. - :param name: Name of the model + Parameters + ---------- + **kwargs : + name : str + Name of the model. """ model = self.storage['model'][name] for key in kwargs.keys(): model[key] = kwargs[key] def get_model_value(self, name: str, key: str) -> float: - """ - A function to get a given model value - - :param name: Name of the model - :param key: The given value keys - :return: The desired value + """A function to get a given model value. + + Parameters + ---------- + name : str + Name of the model. + key : str + The given value keys. + + Returns + ------- + float + The desired value. """ model = self.storage['model'][name] return model[key] def assign_material_to_layer(self, material_name: str, layer_name: str): - """ - Assign a material to a layer. - - :param material_name: The material name - :param layer_name: The layer name + """Assign a material to a layer. + + Parameters + ---------- + material_name : str + The material name. + layer_name : str + The layer name. """ self.storage['layer'][layer_name].material = self.storage['material'][material_name] def add_layer_to_item(self, layer_name: str, item_name: str): - """ - Create a layer from the material of the same name, in a given item. - - :param layer_name: The layer name - :param item_name: The item name + """Create a layer from the material of the same name, in a given item. + + Parameters + ---------- + layer_name : str + The layer name. + item_name : str + The item name. """ item = self.storage['item'][item_name] item.stack.add(self.storage['layer'][layer_name]) def add_item(self, item_name: str, model_name: str): - """ - Add an item to the model. - - :param item_name: items to add to model - :param model_name: name for the model + """Add an item to the model. + + Parameters + ---------- + item_name : str + Items to add to model. + model_name : str + Name for the model. """ self.storage['model'][model_name]['items'].append(self.storage['item'][item_name]) def remove_layer_from_item(self, layer_name: str, item_name: str): - """ - Remove a layer in a given item. - - :param layer_name: The layer name - :param item_name: The item name + """Remove a layer in a given item. + + Parameters + ---------- + layer_name : str + The layer name. + item_name : str + The item name. """ layer_idx = list(self.storage['item'][item_name].stack).index(self.storage['layer'][layer_name]) del self.storage['item'][item_name].stack[layer_idx] def remove_item(self, item_name: str, model_name: str): - """ - Remove a given item. - - :param item_name: The item name - :param model_name: The model name + """Remove a given item. + + Parameters + ---------- + item_name : str + The item name. + model_name : str + The model name. """ item_idx = self.storage['model'][model_name]['items'].index(self.storage['item'][item_name]) del self.storage['model'][model_name]['items'][item_idx] @@ -150,17 +190,22 @@ def remove_item(self, item_name: str, model_name: str): def calculate(self, q_array: np.ndarray, model_name: str) -> np.ndarray: """For a given q array calculate the corresponding reflectivity. - :param q_array: array of data points to be calculated - :param model_name: the model name - :return: reflectivity calculated at q + Parameters + ---------- + q_array : np.ndarray + Array of data points to be calculated. + model_name : str + The model name. + + Returns + ------- + np.ndarray + Reflectivity calculated at q. """ sample = _build_sample(self.storage, model_name) + # smearing() returns sigma, which is exactly what refl1d's probe.dQ expects. dq_array = self._resolution_function.smearing(q_array) - if isinstance(self._resolution_function, PercentageFwhm): - # Get percentage of Q and change from sigma to FWHM - dq_array = dq_array * q_array / 100 / (2 * np.sqrt(2 * np.log(2))) - if not self._magnetism: probe = _get_probe( q_array=q_array, @@ -197,11 +242,17 @@ def calculate(self, q_array: np.ndarray, model_name: str) -> np.ndarray: return reflectivity def sld_profile(self, model_name: str) -> Tuple[np.ndarray, np.ndarray]: - """ - Return the scattering length density profile. + """Return the scattering length density profile. + + Parameters + ---------- + model_name : str + The model name. + + Returns + ------- - :param model_name: the model name - :return: z and sld(z) + Z and sld(z). """ sample = _build_sample(self.storage, model_name) probe = _get_probe( @@ -216,6 +267,7 @@ def sld_profile(self, model_name: str) -> Tuple[np.ndarray, np.ndarray]: def _get_oversampling_q(q_array: np.ndarray, dq_array: np.ndarray, oversampling_factor: int) -> np.ndarray: + """Get oversampling q.""" argmin = np.argmin(q_array) # index of the smallest q element argmax = np.argmax(q_array) # index of the largest q element return np.linspace( @@ -233,6 +285,7 @@ def _get_probe( oversampling_factor: int = 1, magnetism: bool = False, ) -> names.QProbe: + """Get probe.""" probe = names.QProbe( Q=q_array, dQ=dq_array, @@ -258,6 +311,7 @@ def _get_polarized_probe( oversampling_factor: int = 1, all_polarizations: bool = False, ) -> names.PolarizedNeutronQProbe: + """Get polarized probe.""" four_probes = [] for i in range(4): if i == 0 or all_polarizations: @@ -281,6 +335,7 @@ def _get_polarized_probe( def _build_sample(storage: dict, model_name: str) -> names.Stack: + """Build sample.""" sample = names.Stack() # -1 to reverse the order for i in storage['model'][model_name]['items'][::-1]: diff --git a/src/easyreflectometry/calculators/refnx/calculator.py b/src/easyreflectometry/calculators/refnx/calculator.py index 2a5b45b0..1362bad2 100644 --- a/src/easyreflectometry/calculators/refnx/calculator.py +++ b/src/easyreflectometry/calculators/refnx/calculator.py @@ -1,13 +1,13 @@ -__author__ = 'github.com/arm61' +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + from ..calculator_base import CalculatorBase from .wrapper import RefnxWrapper class Refnx(CalculatorBase): - """ - Calculator for refnx - """ + """Calculator for refnx.""" name = 'refnx' @@ -31,5 +31,6 @@ class Refnx(CalculatorBase): } def __init__(self): + """Init function.""" super().__init__() self._wrapper = RefnxWrapper() diff --git a/src/easyreflectometry/calculators/refnx/wrapper.py b/src/easyreflectometry/calculators/refnx/wrapper.py index d2290877..65dc8662 100644 --- a/src/easyreflectometry/calculators/refnx/wrapper.py +++ b/src/easyreflectometry/calculators/refnx/wrapper.py @@ -1,4 +1,6 @@ -__author__ = 'github.com/arm61' +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + from typing import Tuple @@ -6,6 +8,7 @@ from refnx import reflect from easyreflectometry.model import PercentageFwhm +from easyreflectometry.model.resolution_functions import SIGMA_TO_FWHM from ..wrapper_base import WrapperBase @@ -13,53 +16,68 @@ class RefnxWrapper(WrapperBase): @property def include_magnetism(self) -> bool: + """Include magnetism.""" return self._magnetism @include_magnetism.setter def include_magnetism(self, magnetism: bool) -> None: """Set the magnetism flag. - :param magnetism: The magnetism flag + Parameters + ---------- + magnetism : bool + The magnetism flag. """ raise NotImplementedError('Magnetism is not supported by refnx') def create_material(self, name: str): - """ - Create a material using SLD. + """Create a material using SLD. - :param name: The name of the material + Parameters + ---------- + name : str + The name of the material. """ self.storage['material'][name] = reflect.SLD(0, name=name) def create_layer(self, name: str): - """ - Create a layer using Slab. + """Create a layer using Slab. - :param name: The name of the layer + Parameters + ---------- + name : str + The name of the layer. """ self.storage['layer'][name] = reflect.Slab(0, 0, 0, name=name) def create_item(self, name: str): - """ - Create an item using Stack. + """Create an item using Stack. - :param name: The name of the item + Parameters + ---------- + name : str + The name of the item. """ self.storage['item'][name] = reflect.Stack(name=name) def create_model(self, name: str): - """ - Create a model for analysis + """Create a model for analysis. - :param name: Name for the model + Parameters + ---------- + name : str + Name for the model. """ self.storage['model'][name] = reflect.ReflectModel(reflect.Structure()) def update_model(self, name: str, **kwargs): - """ - Update the non-structural parameters of the model + """Update the non-structural parameters of the model. - :param name: Name for the model + Parameters + ---------- + **kwargs : + name : str + Name for the model. """ model = self.storage['model'][name] for key in kwargs.keys(): @@ -67,61 +85,83 @@ def update_model(self, name: str, **kwargs): setattr(item, 'value', kwargs[key]) def get_model_value(self, name: str, key: str) -> float: - """ - A function to get a given model value + """A function to get a given model value. + + Parameters + ---------- + name : str + Name for the model. + key : str + The given value keys. - :param name: Name for the model - :param key: The given value keys - :return: The desired value + Returns + ------- + float + The desired value. """ model = self.storage['model'][name] item = getattr(model, key) return getattr(item, 'value') def assign_material_to_layer(self, material_name: str, layer_name: str): - """ - Assign a material to a layer. + """Assign a material to a layer. - :param material_name: The material name - :param layer_name: The layer name + Parameters + ---------- + material_name : str + The material name. + layer_name : str + The layer name. """ self.storage['layer'][layer_name].sld = self.storage['material'][material_name] def add_layer_to_item(self, layer_name: str, item_name: str): - """ - Create a layer from the material of the same name, in a given item. + """Create a layer from the material of the same name, in a given item. - :param layer_name: The layer name - :param item_name: The item name + Parameters + ---------- + layer_name : str + The layer name. + item_name : str + The item name. """ item = self.storage['item'][item_name] item.append(self.storage['layer'][layer_name]) def add_item(self, item_name: str, model_name: str): - """ - Add an item to the model. + """Add an item to the model. - :param item_name: items to add to model - :param model_name: Name for the model + Parameters + ---------- + item_name : str + Items to add to model. + model_name : str + Name for the model. """ self.storage['model'][model_name].structure.components.append(self.storage['item'][item_name]) def remove_layer_from_item(self, layer_name: str, item_name: str): - """ - Remove a layer in a given item. + """Remove a layer in a given item. - :param layer_name: The layer name - :param item_name: The item name + Parameters + ---------- + layer_name : str + The layer name. + item_name : str + The item name. """ layer_idx = self.storage['item'][item_name].components.index(self.storage['layer'][layer_name]) del self.storage['item'][item_name].components[layer_idx] def remove_item(self, item_name: str, model_name: str): - """ - Remove a given item. + """Remove a given item. - :param item_name: The item name - :param model_name: Name of the model + Parameters + ---------- + item_name : str + The item name. + model_name : str + Name of the model. """ item_idx = self.storage['model'][model_name].structure.components.index(self.storage['item'][item_name]) del self.storage['model'][model_name].structure.components[item_idx] @@ -130,9 +170,17 @@ def remove_item(self, item_name: str, model_name: str): def calculate(self, q_array: np.ndarray, model_name: str) -> np.ndarray: """For a given q array calculate the corresponding reflectivity. - :param q_array: array of data points to be calculated - :param model_name: the model name - :return: reflectivity calculated at q + Parameters + ---------- + q_array : np.ndarray + Array of data points to be calculated. + model_name : str + The model name. + + Returns + ------- + np.ndarray + Reflectivity calculated at q. """ structure = _remove_unecessary_stacks(self.storage['model'][model_name].structure) model = reflect.ReflectModel( @@ -144,29 +192,43 @@ def calculate(self, q_array: np.ndarray, model_name: str) -> np.ndarray: dq_vector = self._resolution_function.smearing(q_array) if isinstance(self._resolution_function, PercentageFwhm): - # FWHM Percentage resolution is constant given as - # For a constant resolution percentage refnx supports to pass a scalar value rather than a vector - dq_vector = dq_vector[0] + # refnx interprets a scalar x_err as a constant dq/q (FWHM percentage), + # so pass the percentage directly rather than a per-point vector. + dq_vector = self._resolution_function.constant + else: + # smearing() returns sigma; refnx expects the FWHM at each point. + dq_vector = dq_vector * SIGMA_TO_FWHM return model(x=q_array, x_err=dq_vector) def sld_profile(self, model_name: str) -> Tuple[np.ndarray, np.ndarray]: - """ - Return the scattering length density profile. + """Return the scattering length density profile. + + Parameters + ---------- + model_name : str + Name for the model. + + Returns + ------- - :param model_name: Name for the model - :return: z and sld(z) + Z and sld(z). """ return _remove_unecessary_stacks(self.storage['model'][model_name].structure).sld_profile() def _remove_unecessary_stacks(current_structure: reflect.Structure) -> reflect.Structure: - """ - Removed unnecessary reflect.Stack objects from the structure. + """Removed unnecessary reflect.Stack objects from the structure. + + Parameters + ---------- + current_structure : reflect.Structure + The current structure. - :param current_structure: The current structure - :return: The structre without the unnecessary Stacks - :rtype: reflect.structure + Returns + ------- + reflect.structure + The structre without the unnecessary Stacks. """ structure = [] for i in current_structure.components: diff --git a/src/easyreflectometry/calculators/wrapper_base.py b/src/easyreflectometry/calculators/wrapper_base.py index 0755ff64..dc53ceca 100644 --- a/src/easyreflectometry/calculators/wrapper_base.py +++ b/src/easyreflectometry/calculators/wrapper_base.py @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + from abc import abstractmethod import numpy as np @@ -31,7 +34,10 @@ def reset_storage(self): def create_material(self, name: str): """Create a material using SLD. - :param name: The name of the material + Parameters + ---------- + name : str + The name of the material. """ ... @@ -39,7 +45,10 @@ def create_material(self, name: str): def create_layer(self, name: str): """Create a layer using Slab. - :param name: The name of the layer + Parameters + ---------- + name : str + The name of the layer. """ ... @@ -47,34 +56,46 @@ def create_layer(self, name: str): def create_item(self, name: str): """Create an item using Stack. - :param name: The name of the item + Parameters + ---------- + name : str + The name of the item. """ ... @abstractmethod def create_model(self, name: str): - """Create a model for analysis + """Create a model for analysis. - :param name: Name for the model + Parameters + ---------- + name : str + Name for the model. """ ... @abstractmethod def update_model(self, name: str, **kwargs): - """Update the non-structural parameters of the model - - :param name: Name for the model - :param kwargs: + """Update the non-structural parameters of the model. + Parameters + ---------- + name : str + Name for the model. + **kwargs : """ ... @abstractmethod def get_model_value(self, name: str, key: str) -> float: - """A function to get a given model value - - :param name: Name for the model - :param key: The given value keys + """A function to get a given model value. + + Parameters + ---------- + name : str + Name for the model. + key : str + The given value keys. """ ... @@ -82,8 +103,12 @@ def get_model_value(self, name: str, key: str) -> float: def assign_material_to_layer(self, material_name: str, layer_name: str): """Assign a material to a layer. - :param material_name: The material name - :param layer_name: The layer name + Parameters + ---------- + material_name : str + The material name. + layer_name : str + The layer name. """ ... @@ -91,8 +116,12 @@ def assign_material_to_layer(self, material_name: str, layer_name: str): def add_layer_to_item(self, layer_name: str, item_name: str): """Create a layer from the material of the same name, in a given item. - :param layer_name: The layer name - :param item_name: The item name + Parameters + ---------- + layer_name : str + The layer name. + item_name : str + The item name. """ ... @@ -100,8 +129,12 @@ def add_layer_to_item(self, layer_name: str, item_name: str): def add_item(self, item_name: str, model_name: str): """Add an item to the model. - :param item_name: items to add to model - :param model_name: Name for the model + Parameters + ---------- + item_name : str + Items to add to model. + model_name : str + Name for the model. """ ... @@ -109,8 +142,12 @@ def add_item(self, item_name: str, model_name: str): def remove_layer_from_item(self, layer_name: str, item_name: str): """Remove a layer in a given item. - :param layer_name: The layer name - :param item_name: The item name + Parameters + ---------- + layer_name : str + The layer name. + item_name : str + The item name. """ ... @@ -118,8 +155,12 @@ def remove_layer_from_item(self, layer_name: str, item_name: str): def remove_item(self, item_name: str, model_name: str): """Remove a given item. - :param item_name: The item name - :param model_name: Name of the model + Parameters + ---------- + item_name : str + The item name. + model_name : str + Name of the model. """ ... @@ -127,9 +168,17 @@ def remove_item(self, item_name: str, model_name: str): def calculate(self, q_array: np.ndarray, model_name: str) -> np.ndarray: """For a given q array calculate the corresponding reflectivity. - :param q_array: array of data points to be calculated - :param model_name: the model name - :return: reflectivity calculated at q + Parameters + ---------- + q_array : np.ndarray + Array of data points to be calculated. + model_name : str + The model name. + + Returns + ------- + np.ndarray + Reflectivity calculated at q. """ ... @@ -137,15 +186,27 @@ def calculate(self, q_array: np.ndarray, model_name: str) -> np.ndarray: def sld_profile(self, model_name: str) -> tuple[np.ndarray, np.ndarray]: """Return the scattering length density profile. - :param model_name: Name for the model - :return: z and sld(z) + Parameters + ---------- + model_name : str + Name for the model. + + Returns + ------- + tuple[np.ndarray, np.ndarray] + Z and sld(z). """ ... def update_material(self, name: str, **kwargs): """Update a material. - :param name: The name of the material + Parameters + ---------- + name : str + The name of the material. + **kwargs : + Key-value pairs of attributes to update. """ material = self.storage['material'][name] for key in kwargs.keys(): @@ -153,11 +214,19 @@ def update_material(self, name: str, **kwargs): setattr(item, 'value', kwargs[key]) def get_material_value(self, name: str, key: str) -> float: - """A function to get a given material value - - :param name: The material name - :param key: The given value keys - :return: The desired value + """A function to get a given material value. + + Parameters + ---------- + name : str + The material name. + key : str + The given value keys. + + Returns + ------- + float + The desired value. """ material = self.storage['material'][name] item = getattr(material, key) @@ -166,8 +235,11 @@ def get_material_value(self, name: str, key: str) -> float: def update_layer(self, name: str, **kwargs): """Update a layer in a given item. - :param name: The layer name. - :param kwargs: + Parameters + ---------- + name : str + The layer name. + **kwargs : """ layer = self.storage['layer'][name] for key in kwargs.keys(): @@ -175,10 +247,14 @@ def update_layer(self, name: str, **kwargs): setattr(ii, 'value', kwargs[key]) def get_layer_value(self, name: str, key: str) -> float: - """A function to get a given layer value - - :param name: The layer name - :param key: The given value keys + """A function to get a given layer value. + + Parameters + ---------- + name : str + The layer name. + key : str + The given value keys. """ layer = self.storage['layer'][name] ii = getattr(layer, key) @@ -187,7 +263,11 @@ def get_layer_value(self, name: str, key: str) -> float: def update_item(self, name: str, **kwargs): """Update a layer. - :param name: The item name + Parameters + ---------- + **kwargs : + name : str + The item name. """ item = self.storage['item'][name] for key in kwargs.keys(): @@ -195,11 +275,19 @@ def update_item(self, name: str, **kwargs): setattr(ii, 'value', kwargs[key]) def get_item_value(self, name: str, key: str) -> float: - """A function to get a given item value - - :param name: The item name - :param key: The given value keys - :return: The desired value + """A function to get a given item value. + + Parameters + ---------- + name : str + The item name. + key : str + The given value keys. + + Returns + ------- + float + The desired value. """ item = self.storage['item'][name] item = getattr(item, key) @@ -208,18 +296,25 @@ def get_item_value(self, name: str, key: str) -> float: def set_resolution_function(self, resolution_function: ResolutionFunction) -> None: """Set the resolution function for the calculator. - :param resolution_function: The resolution function + Parameters + ---------- + resolution_function : ResolutionFunction + The resolution function. """ self._resolution_function = resolution_function @property def magnetism(self) -> bool: + """Magnetism function.""" return self._magnetism @magnetism.setter def magnetism(self, magnetism: bool) -> None: """Set the magnetism flag. - :param magnetism: The magnetism flag + Parameters + ---------- + magnetism : bool + The magnetism flag. """ self._magnetism = magnetism diff --git a/src/easyreflectometry/data/__init__.py b/src/easyreflectometry/data/__init__.py index 194f0d31..0d058120 100644 --- a/src/easyreflectometry/data/__init__.py +++ b/src/easyreflectometry/data/__init__.py @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2024 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + from .data_store import DataSet1D from .data_store import ProjectData from .measurement import load diff --git a/src/easyreflectometry/data/data_store.py b/src/easyreflectometry/data/data_store.py index 948382d7..b0cf001f 100644 --- a/src/easyreflectometry/data/data_store.py +++ b/src/easyreflectometry/data/data_store.py @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2024 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + __author__ = 'github.com/wardsimon' from collections.abc import Sequence @@ -16,6 +19,7 @@ class ProjectData(SerializerComponent): def __init__(self, name='DataStore', exp_data=None, sim_data=None): + """Init function.""" self.name = name if exp_data is None: exp_data = DataStore(name='Exp Datastore') @@ -27,31 +31,39 @@ def __init__(self, name='DataStore', exp_data=None, sim_data=None): class DataStore(Sequence, SerializerComponent): def __init__(self, *args, name='DataStore'): + """Init function.""" self.name = name self.items = list(args) self.show_legend = False def __getitem__(self, i: int) -> T: + """Getitem function.""" return self.items.__getitem__(i) def __len__(self) -> int: + """Len function.""" return len(self.items) def __setitem__(self, key, value): + """Setitem function.""" self.items[key] = value def __delitem__(self, key): + """Delitem function.""" del self.items[key] def append(self, *args): + """Append function.""" self.items.append(*args) def as_dict(self, skip: list = []) -> dict: + """As dict.""" this_dict = super(DataStore, self).as_dict(self, skip=skip) this_dict['items'] = [item.as_dict() for item in self.items if hasattr(item, 'as_dict')] @classmethod def from_dict(cls, d): + """From dict.""" items = d['items'] del d['items'] obj = cls.from_dict(d) @@ -61,10 +73,12 @@ def from_dict(cls, d): @property def experiments(self): + """Experiments function.""" return [self[idx] for idx in range(len(self)) if self[idx].is_experiment] @property def simulations(self): + """Simulations function.""" return [self[idx] for idx in range(len(self)) if self[idx].is_simulation] @@ -81,6 +95,7 @@ def __init__( y_label: str = 'y', auto_background: bool = True, ): + """Init function.""" self._model = model if y is not None and model is not None and auto_background: self._model.background = max(np.min(y), 1e-10) @@ -119,22 +134,28 @@ def __init__( @property def model(self) -> 'Model': # delay type checking until runtime (quotes) + """Model function.""" return self._model @model.setter def model(self, new_model: 'Model') -> None: + """Model function.""" self._model = new_model @property def is_experiment(self) -> bool: + """Is experiment.""" return self._model is not None @property def is_simulation(self) -> bool: + """Is simulation.""" return self._model is None def data_points(self) -> tuple[float, float, float, float]: + """Data points.""" return zip(self.x, self.y, self.ye, self.xe) def __repr__(self) -> str: + """Repr function.""" return "1D DataStore of '{:s}' Vs '{:s}' with {} data points".format(self.x_label, self.y_label, len(self.x)) diff --git a/src/easyreflectometry/data/measurement.py b/src/easyreflectometry/data/measurement.py index df4064b6..12117162 100644 --- a/src/easyreflectometry/data/measurement.py +++ b/src/easyreflectometry/data/measurement.py @@ -1,4 +1,6 @@ -__author__ = 'github.com/arm61' +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + import os from typing import TextIO @@ -14,7 +16,10 @@ def load(fname: Union[TextIO, str]) -> sc.DataGroup: """Load data from an ORSO .ort file. - :param fname: The file to be read. + Parameters + ---------- + fname : Union[TextIO, str] + The file to be read. """ try: return load_data_from_orso_file(fname) @@ -40,6 +45,7 @@ def load_as_dataset(fname: Union[TextIO, str]) -> DataSet1D: def extract_orso_title(data_group: sc.DataGroup, data_name: str) -> str | None: + """Extract orso title.""" try: header = data_group['attrs'][data_name]['orso_header'] title = header.values.get('data_source', {}).get('experiment', {}).get('title') @@ -54,7 +60,10 @@ def extract_orso_title(data_group: sc.DataGroup, data_name: str) -> str | None: def _load_txt(fname: Union[TextIO, str]) -> sc.DataGroup: """Load data from a simple txt file. - :param fname: The path for the file to be read. + Parameters + ---------- + fname : Union[TextIO, str] + The path for the file to be read. """ # fname can have either a space or a comma as delimiter # Determine the delimiter used in the file diff --git a/src/easyreflectometry/fitting.py b/src/easyreflectometry/fitting.py index 0750beb5..0efaaaa5 100644 --- a/src/easyreflectometry/fitting.py +++ b/src/easyreflectometry/fitting.py @@ -1,11 +1,16 @@ -__author__ = 'github.com/arm61' +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + import warnings +from typing import Any +from typing import Callable import numpy as np import scipp as sc from easyscience.fitting import AvailableMinimizers from easyscience.fitting import FitResults +from easyscience.fitting import Sampler from easyscience.fitting.multi_fitter import MultiFitter as EasyScienceMultiFitter from easyreflectometry.data import DataSet1D @@ -18,11 +23,20 @@ def _validate_objective(objective: str) -> str: """Validate and resolve the objective string. - :param objective: The objective mode string. - :type objective: str - :return: Resolved objective string ('auto' becomes 'hybrid'). - :rtype: str - :raises ValueError: If the objective is not one of the valid options. + Parameters + ---------- + objective : str + The objective mode string. + + Raises + ------ + ValueError : + If the objective is not one of the valid options. + + Returns + ------- + str + Resolved objective string ('auto' becomes 'hybrid'). """ if objective not in _VALID_OBJECTIVES: raise ValueError(f'Unknown objective {objective!r}. Valid options: {_VALID_OBJECTIVES}') @@ -46,17 +60,22 @@ def _prepare_fit_arrays( Note: ``variances`` here means σ² (the scipp convention), not σ. - :param x_vals: Independent variable values. - :type x_vals: np.ndarray - :param y_vals: Observed dependent variable values. - :type y_vals: np.ndarray - :param variances: Variance (σ²) of each observed point. - :type variances: np.ndarray - :param objective: One of 'legacy_mask', 'hybrid', 'mighell'. - :type objective: str - :return: Tuple of (x_out, y_eff, weights, stats) where stats is a dict - with keys 'valid', 'mighell_substituted', 'masked'. - :rtype: tuple[np.ndarray, np.ndarray, np.ndarray, dict] + Parameters + ---------- + x_vals : np.ndarray + Independent variable values. + y_vals : np.ndarray + Observed dependent variable values. + variances : np.ndarray + Variance (σ²) of each observed point. + objective : str + One of 'legacy_mask', 'hybrid', 'mighell'. + + Returns + ------- + tuple[np.ndarray, np.ndarray, np.ndarray, dict] + Tuple of (x_out, y_eff, weights, stats) where stats is a dict + with keys 'valid', 'mighell_substituted', 'masked'. """ n = len(y_vals) zero_mask = variances <= 0.0 @@ -71,7 +90,12 @@ def _prepare_fit_arrays( weights = 1.0 / np.sqrt(variances[valid]) else: weights = np.array([]) - stats = {'valid': n_valid, 'mighell_substituted': 0, 'masked': n_zero, 'transformed_all_points': False} + stats = { + 'valid': n_valid, + 'mighell_substituted': 0, + 'masked': n_zero, + 'transformed_all_points': False, + } return x_out, y_eff, weights, stats # hybrid or mighell @@ -143,18 +167,24 @@ def __init__(self, *args: Model, objective: str = 'hybrid'): which will populate the :py:class:`sc.DataGroup` appropriately after the fitting is performed. - :param args: Reflectometry model(s). - :param objective: Zero-variance handling strategy. One of + Parameters + ---------- + *args : Model + Reflectometry model(s). + objective : str, optional + Zero-variance handling strategy. One of ``'hybrid'`` (default, Mighell for zero-variance, WLS otherwise), ``'mighell'`` (Mighell transform for all points), ``'legacy_mask'`` (drop zero-variance points), - ``'auto'`` (alias for ``'hybrid'``). - :type objective: str + ``'auto'`` (alias for ``'hybrid'``). By default, 'hybrid'. """ # This lets the unique_name be passed with the fit_func. def func_wrapper(func, unique_name): + """Func wrapper.""" + def wrapped(*args, **kwargs): + """Wrapped function.""" return func(*args, unique_name, **kwargs) return wrapped @@ -165,25 +195,25 @@ def wrapped(*args, **kwargs): self._fit_results: list[FitResults] | None = None self._classical_fit_metrics: list[dict] | None = None self._objective = _validate_objective(objective) + self._sampler: Sampler | None = None def fit(self, data: sc.DataGroup, id: int = 0, objective: str | None = None) -> sc.DataGroup: """Perform the fitting and populate the DataGroups with the result. - :param data: DataGroup to be fitted to and populated. - :type data: sc.DataGroup - :param id: Unused parameter kept for backward compatibility. - :type id: int - :param objective: Per-call override for the zero-variance objective. - If ``None``, uses the instance default set at construction. - :type objective: str or None - :return: A new DataGroup with fitted model curves, SLD profiles, and fit statistics. - :rtype: sc.DataGroup - - :note: Under the ``mighell`` objective all points are transformed, - so ``reduced_chi`` is not a classical chi-square statistic. - Under ``hybrid``, only zero-variance points are transformed; - when they are a small fraction of the data the chi-square - remains approximately classical. + Parameters + ---------- + data : sc.DataGroup + DataGroup to be fitted to and populated. + id : int, optional + Unused parameter kept for backward compatibility. By default, 0. + objective : str | None, optional + Per-call override for the zero-variance objective. + If ``None``, uses the instance default set at construction. By default, None. + + Returns + ------- + sc.DataGroup + A new DataGroup with fitted model curves, SLD profiles, and fit statistics. """ obj = _validate_objective(objective) if objective is not None else self._objective @@ -236,7 +266,9 @@ def fit(self, data: sc.DataGroup, id: int = 0, objective: str | None = None) -> if 'attrs' in new_data: new_data['attrs'][f'R_{id}_model'] = {'model': sc.scalar(self._models[i].as_dict())} new_data['coords'][f'z_{id}'] = sc.array( - dims=[f'z_{id}'], values=sld_profile[0], unit=(1 / new_data['coords'][f'Qz_{id}'].unit).unit + dims=[f'z_{id}'], + values=sld_profile[0], + unit=(1 / new_data['coords'][f'Qz_{id}'].unit).unit, ) original = original_arrays[i] sigma_classical = np.sqrt(np.clip(original['variances'], 0.0, None)) @@ -246,15 +278,13 @@ def fit(self, data: sc.DataGroup, id: int = 0, objective: str | None = None) -> objective_chi2 = float(result[i].chi2) objective_reduced_chi = _fit_result_reduced_chi(result[i], np.size(result[i].x)) - self._classical_fit_metrics.append( - { - 'classical_chi2': classical_chi2, - 'classical_reduced_chi': classical_reduced_chi, - 'objective_chi2': objective_chi2, - 'objective_reduced_chi': objective_reduced_chi, - 'n_classical_points': n_classical_points, - } - ) + self._classical_fit_metrics.append({ + 'classical_chi2': classical_chi2, + 'classical_reduced_chi': classical_reduced_chi, + 'objective_chi2': objective_chi2, + 'objective_reduced_chi': objective_reduced_chi, + 'n_classical_points': n_classical_points, + }) new_data['objective_chi2'] = objective_chi2 new_data['objective_reduced_chi'] = objective_reduced_chi @@ -267,14 +297,19 @@ def fit(self, data: sc.DataGroup, id: int = 0, objective: str | None = None) -> def fit_single_data_set_1d(self, data: DataSet1D, objective: str | None = None) -> FitResults: """Perform fitting on a single 1D dataset. - :param data: The 1D dataset to fit. Note that ``data.ye`` stores + Parameters + ---------- + data : DataSet1D + The 1D dataset to fit. Note that ``data.ye`` stores variances (σ²), not standard deviations. - :type data: DataSet1D - :param objective: Per-call override for the zero-variance objective. - If ``None``, uses the instance default set at construction. - :type objective: str or None - :return: Fit results from the minimizer. - :rtype: FitResults + objective : str | None, optional + Per-call override for the zero-variance objective. + If ``None``, uses the instance default set at construction. By default, None. + + Returns + ------- + FitResults + Fit results from the minimizer. """ obj = _validate_objective(objective) if objective is not None else self._objective @@ -322,6 +357,142 @@ def fit_single_data_set_1d(self, data: DataSet1D, objective: str | None = None) ] return result + def mcmc_sample( + self, + data: sc.DataGroup, + samples: int = 10000, + burn: int = 2000, + thin: int = 10, + population: int | None = None, + objective: str | None = None, + initializer: str | None = None, + progress_callback: Callable[..., Any] | None = None, + abort_test: Callable[[], bool] | None = None, + ) -> dict: + """Run Bayesian MCMC sampling on reflectometry data using the DREAM sampler. + + Requires that the minimizer is a BUMPS instance (i.e. the minimizer was + switched to ``AvailableMinimizers.Bumps``). + + :param data: DataGroup with reflectivity data. + :param samples: Number of retained DREAM samples requested from BUMPS. + :param burn: Burn-in steps. + :param thin: Thinning interval. + :param population: BUMPS DREAM population count for advanced users. + :param objective: Zero-variance handling strategy. + :param initializer: DREAM population initializer. One of ``'eps'``, + ``'cov'``, ``'lhs'``, or ``'random'``. By default, None (BUMPS + uses ``'eps'``). + :param progress_callback: Optional callback for progress updates during + sampling. Forwarded to the core MultiFitter. + :return: Dictionary with keys ``'draws'``, ``'param_names'``, ``'state'``, + and ``'logp'``. + :raises RuntimeError: If the current minimizer is not a BUMPS instance. + + The underlying :class:`~easyscience.fitting.Sampler` is retained on + :attr:`sampler`, so the chain can be continued without re-running the + burn-in:: + + fitter.mcmc_sample(data, samples=2000, burn=500, thin=10) + extended = fitter.sampler.extend(additional_samples=8000, thin=10) + """ + minimizer = self.easy_science_multi_fitter.minimizer + if not (hasattr(minimizer, 'package') and minimizer.package == 'bumps'): + raise RuntimeError( + 'Bayesian sampling requires a BUMPS minimizer. ' + 'Use ``fitter.switch_minimizer(AvailableMinimizers.Bumps)`` first.' + ) + + obj = _validate_objective(objective) if objective is not None else self._objective + + refl_nums = [k[3:] for k in data['coords'].keys() if k.startswith('Qz_')] + x = [] + y = [] + dy = [] + + # Process each reflectivity dataset + for i in refl_nums: + x_vals = data['coords'][f'Qz_{i}'].values + y_vals = data['data'][f'R_{i}'].values + variances = data['data'][f'R_{i}'].variances + + if obj != 'mighell' and np.all(np.asarray(variances) <= 0.0): + raise ValueError( + f'Cannot run Bayesian sampling on reflectivity {i}: all points have zero variance. ' + 'The likelihood is undefined without measurement uncertainties. Supply uncertainties, ' + "or explicitly opt in to the Mighell transform with objective='mighell' " + '(a chi-square bias correction, not a true likelihood).' + ) + + x_out, y_eff, weights, stats = _prepare_fit_arrays(x_vals, y_vals, variances, obj) + + if stats['masked'] > 0: + warnings.warn( + f'Masked {stats["masked"]} data point(s) in reflectivity {i} due to zero variance during sampling.', + UserWarning, + ) + if stats.get('transformed_all_points'): + warnings.warn( + f'Applied Mighell transform to all {len(y_vals)} point(s) in reflectivity {i} during sampling. ' + 'The Mighell transform is a chi-square bias correction, not a true likelihood; ' + 'posterior widths may be unreliable.', + UserWarning, + ) + elif stats['mighell_substituted'] > 0: + warnings.warn( + f'Applied Mighell substitution to {stats["mighell_substituted"]} ' + f'zero-variance point(s) in reflectivity {i} during sampling. ' + 'The Mighell transform is a chi-square bias correction, not a true likelihood; ' + 'posterior widths may be unreliable.', + UserWarning, + ) + x.append(x_out) + y.append(y_eff) + dy.append(weights) + + # Delegate the actual BUMPS/DREAM sampling to the core ``Sampler``. + # The core API moved from ``MultiFitter.mcmc_sample()`` to a dedicated + # ``Sampler`` class: construct it with the configured fitter and the + # bound data, then call ``sample()``. ``Sampler`` handles the + # multi-dataset reshaping internally. + sampler_kwargs = {} + if initializer is not None: + sampler_kwargs['init'] = initializer + + sampler = Sampler( + self.easy_science_multi_fitter, + x=x, + y=y, + weights=dy, + ) + # Retained so the chain can be continued afterwards via ``self.sampler.extend()``. + self._sampler = sampler + results = sampler.sample( + samples=samples, + burn=burn, + thin=thin, + population=population, + sampler_kwargs=sampler_kwargs or None, + progress_callback=progress_callback, + abort_test=abort_test, + ) + return { + 'draws': results.draws, + 'param_names': results.param_names, + 'state': results.state, + 'logp': results.logp, + } + + @property + def sampler(self) -> Sampler | None: + """The ``Sampler`` behind the most recent :meth:`mcmc_sample` call, or None. + + Holds the live BUMPS chain state, so the sampling run can be continued + with ``fitter.sampler.extend(additional_samples=...)`` instead of + starting a fresh chain. + """ + return self._sampler + @property def chi2(self) -> float | None: """Total chi-squared across all fitted datasets, or None if no fit has been performed.""" @@ -372,20 +543,27 @@ def objective_reduced_chi(self) -> float | None: return self.reduced_chi def switch_minimizer(self, minimizer: AvailableMinimizers) -> None: - """ - Switch the minimizer for the fitting. + """Switch the minimizer for the fitting. - :param minimizer: Minimizer to be switched to + Parameters + ---------- + minimizer : AvailableMinimizers + Minimizer to be switched to. """ self.easy_science_multi_fitter.switch_minimizer(minimizer) def _flatten_list(this_list: list) -> list: - """ - Flatten nested lists. + """Flatten nested lists. - :param this_list: List to be flattened + Parameters + ---------- + this_list : list + List to be flattened. - :return: Flattened list + Returns + ------- + list + Flattened list. """ return np.array([item for sublist in this_list for item in sublist]) diff --git a/src/easyreflectometry/limits.py b/src/easyreflectometry/limits.py index 691f86e3..001bba64 100644 --- a/src/easyreflectometry/limits.py +++ b/src/easyreflectometry/limits.py @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + import numpy as np from easyscience.variable import Parameter @@ -9,10 +12,12 @@ def apply_default_limits(parameter: Parameter, kind: str) -> None: """Apply default min/max to a parameter if current bounds are infinite. - :param parameter: The parameter to adjust. - :type parameter: Parameter - :param kind: One of 'thickness', 'roughness', 'sld', 'isld', 'scale'. - :type kind: str + Parameters + ---------- + parameter : Parameter + The parameter to adjust. + kind : str + One of 'thickness', 'roughness', 'sld', 'isld', 'scale'. """ if not parameter.independent: return diff --git a/src/easyreflectometry/main.py b/src/easyreflectometry/main.py index 043d72c1..2f7737e0 100644 --- a/src/easyreflectometry/main.py +++ b/src/easyreflectometry/main.py @@ -1,7 +1,11 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + from easyreflectometry.calculators import CalculatorFactory def main(): + """Main function.""" factory = CalculatorFactory() print(f'Available calculators: {factory.available_interfaces}') diff --git a/src/easyreflectometry/model/__init__.py b/src/easyreflectometry/model/__init__.py index 6246b2d8..698b5a0c 100644 --- a/src/easyreflectometry/model/__init__.py +++ b/src/easyreflectometry/model/__init__.py @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + from .model import Model from .model_collection import ModelCollection from .resolution_functions import LinearSpline diff --git a/src/easyreflectometry/model/model.py b/src/easyreflectometry/model/model.py index 7f651fa2..da1396d0 100644 --- a/src/easyreflectometry/model/model.py +++ b/src/easyreflectometry/model/model.py @@ -1,6 +1,7 @@ -from __future__ import annotations +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause -__author__ = 'github.com/arm61' +from __future__ import annotations import copy from numbers import Number @@ -8,15 +9,14 @@ from typing import Union import numpy as np -from easyscience import ObjBase as BaseObj from easyscience import global_object from easyscience.variable import Parameter from easyreflectometry.limits import apply_default_limits from easyreflectometry.sample import BaseAssembly from easyreflectometry.sample import Sample +from easyreflectometry.sample.base_core import BaseCore from easyreflectometry.utils import get_as_parameter -from easyreflectometry.utils import yaml_dump from .resolution_functions import PercentageFwhm from .resolution_functions import ResolutionFunction @@ -43,20 +43,26 @@ }, } -COLORS = ['#0173B2', '#DE8F05', '#029E73', '#D55E00', '#CC78BC', '#CA9161', '#FBAFE4', '#949494', '#ECE133', '#56B4E9'] - - -class Model(BaseObj): +COLORS = [ + '#0173B2', + '#DE8F05', + '#029E73', + '#D55E00', + '#CC78BC', + '#CA9161', + '#FBAFE4', + '#949494', + '#ECE133', + '#56B4E9', +] + + +class Model(BaseCore): """Model is the class that represents the experiment. + It is used to store the information about the experiment and to perform the calculations. """ - # Added in super().__init__ - name: str - sample: Sample - scale: Parameter - background: Parameter - def __init__( self, sample: Union[Sample, None] = None, @@ -70,13 +76,24 @@ def __init__( ): """Constructor. - :param sample: The sample being modelled. - :param scale: Scaling factor of profile. - :param background: Linear background magnitude. - :param name: Name of the model, defaults to 'Model'. - :param resolution_function: Resolution function, defaults to PercentageFwhm. - :param interface: Calculator interface, defaults to `None`. - + Parameters + ---------- + unique_name : Optional[str], optional + By default, None. + color : str, optional + By default, COLORS[0]. + sample : Union[Sample, None], optional + The sample being modelled. By default, None. + scale : Union[Parameter, Number, None], optional + Scaling factor of profile. By default, None. + background : Union[Parameter, Number, None], optional + Linear background magnitude. By default, None. + name : str, optional + Name of the model. By default, 'Model'. + resolution_function : Union[ResolutionFunction, None], optional + Resolution function. By default, None. + interface : + Calculator interface. By default, None. """ if unique_name is None: unique_name = global_object.generate_unique_name(self.__class__.__name__) @@ -91,23 +108,53 @@ def __init__( background = get_as_parameter('background', background, DEFAULTS) self.color = color self._is_default = False + self._resolution_function = resolution_function + + super().__init__(name=name, unique_name=unique_name) + self._sample = sample + self._scale = scale + self._background = background + + # Set interface last — propagates to children via BaseCore.generate_bindings + # and then sets the resolution function on the calculator (see setter). + if interface is not None: + self.interface = interface + + # ----- @property accessors for serialization round-trip ----- + + @property + def sample(self) -> Sample: + return self._sample + + @sample.setter + def sample(self, value: Sample) -> None: + self._sample = value + + @property + def scale(self) -> Parameter: + return self._scale + + @scale.setter + def scale(self, value: float) -> None: + self._scale.value = value + + @property + def background(self) -> Parameter: + return self._background - super().__init__( - name=name, - unique_name=unique_name, - sample=sample, - scale=scale, - background=background, - ) - self.resolution_function = resolution_function + @background.setter + def background(self, value: float) -> None: + self._background.value = value - # Must be set after resolution function - self.interface = interface + # ----- assembly management ----- def add_assemblies(self, *assemblies: list[BaseAssembly]) -> None: """Add assemblies to the model sample. - :param assemblies: Assemblies to add to model sample. + Parameters + ---------- + *assemblies : list[BaseAssembly] + Assemblies to add to model sample. """ if not assemblies: self.sample.add_assembly() @@ -125,7 +172,11 @@ def add_assemblies(self, *assemblies: list[BaseAssembly]) -> None: def duplicate_assembly(self, index: int) -> None: """Duplicate a given item or layer in a sample. - :param idx: Index of the item or layer to duplicate + Parameters + ---------- + index : int + idx : + Index of the item or layer to duplicate. """ self.sample.duplicate_assembly(index) if self.interface is not None: @@ -134,7 +185,11 @@ def duplicate_assembly(self, index: int) -> None: def remove_assembly(self, index: int) -> None: """Remove an assembly from the model. - :param idx: Index of the item to remove. + Parameters + ---------- + index : int + idx : + Index of the item to remove. """ assembly_unique_name = self.sample[index].unique_name self.sample.remove_assembly(index) @@ -148,13 +203,11 @@ def is_default(self) -> bool: @is_default.setter def is_default(self, value: bool) -> None: - """Set whether this model is a default placeholder. - - :param value: True if the model is a default placeholder. - :type value: bool - """ + """Set whether this model is a default placeholder.""" self._is_default = value + # ----- resolution function ----- + @property def resolution_function(self) -> ResolutionFunction: """Return the resolution function.""" @@ -167,23 +220,20 @@ def resolution_function(self, resolution_function: ResolutionFunction) -> None: if self.interface is not None: self.interface().set_resolution_function(self._resolution_function) - @property - def interface(self): - """ - Get the current interface of the object - """ - return self._interface + # ----- interface (override BaseCore's to add resolution-function side effect) ----- - @interface.setter + @BaseCore.interface.setter def interface(self, new_interface) -> None: - """Set the interface for the model.""" - # From super class - self._interface = new_interface + """Set the interface; runs `generate_bindings` and then refreshes the + calculator's resolution function. + """ + # Call BaseCore.interface.setter for the binding propagation. + BaseCore.interface.fset(self, new_interface) if new_interface is not None: - self.generate_bindings() - self._interface().set_resolution_function(self._resolution_function) + new_interface().set_resolution_function(self._resolution_function) + + # ----- representation ----- - # Representation @property def _dict_repr(self) -> dict[str, dict[str, str]]: """A simplified dict representation.""" @@ -203,20 +253,15 @@ def _dict_repr(self) -> dict[str, dict[str, str]]: } } - def __repr__(self) -> str: - """String representation of the layer.""" - return yaml_dump(self._dict_repr) - - def as_dict(self, skip: Optional[list[str]] = None) -> dict: - """Produces a cleaned dict using a custom as_dict method to skip necessary things. - The resulting dict matches the parameters in __init__ + # ----- serialization (custom because resolution_function + interface need special handling) ----- - :param skip: List of keys to skip, defaults to `None`. - """ + def to_dict(self, skip: Optional[list[str]] = None) -> dict: + """Serialize the model, encoding the resolution function and interface name.""" if skip is None: skip = [] - skip.extend(['sample', 'resolution_function', 'interface']) - this_dict = super().as_dict(skip=skip) + # Sample/resolution_function/interface get bespoke encoding below. + skip_for_super = list(skip) + ['sample', 'resolution_function', 'interface'] + this_dict = super().to_dict(skip=skip_for_super) this_dict['sample'] = self.sample.as_dict(skip=skip) this_dict['resolution_function'] = self.resolution_function.as_dict(skip=skip) if self.interface is None: @@ -225,28 +270,23 @@ def as_dict(self, skip: Optional[list[str]] = None) -> dict: this_dict['interface'] = self.interface().name return this_dict + def as_dict(self, skip: Optional[list[str]] = None) -> dict: + """Compatibility alias for :meth:`to_dict`.""" + return self.to_dict(skip=skip) + def as_orso(self) -> dict: """Convert the model to a dictionary suitable for ORSO.""" - this_dict = self.as_dict() - - return this_dict + return self.as_dict() @classmethod def from_dict(cls, passed_dict: dict) -> Model: - """ - Create a Model from a dictionary. - - :param this_dict: dictionary of the Model - :return: Model - """ - # Causes circular import if imported at the top + """Create a Model from a dictionary.""" + # Circular import if hoisted to module-top. from easyreflectometry.calculators import CalculatorFactory this_dict = copy.deepcopy(passed_dict) - resolution_function = ResolutionFunction.from_dict(this_dict['resolution_function']) - del this_dict['resolution_function'] - interface_name = this_dict['interface'] - del this_dict['interface'] + resolution_function = ResolutionFunction.from_dict(this_dict.pop('resolution_function')) + interface_name = this_dict.pop('interface') if interface_name is not None: interface = CalculatorFactory() interface.switch(interface_name) diff --git a/src/easyreflectometry/model/model_collection.py b/src/easyreflectometry/model/model_collection.py index b3c0bd2d..817fc5f0 100644 --- a/src/easyreflectometry/model/model_collection.py +++ b/src/easyreflectometry/model/model_collection.py @@ -1,6 +1,8 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + from __future__ import annotations -from typing import List from typing import Optional from typing import Tuple @@ -12,6 +14,7 @@ # Needs to be a function, elements are added to the global_object.map def DEFAULT_ELEMENTS(interface): + """Default elements.""" return (Model(interface),) @@ -26,30 +29,47 @@ def __init__( next_color_index: Optional[int] = None, **kwargs, ): + """Init function.""" if not models: if populate_if_none: models = DEFAULT_ELEMENTS(interface) else: models = [] - # Needed to ensure an empty list is created when saving and instatiating the object as_dict -> from_dict - # Else collisions might occur in global_object.map - self.populate_if_none = False + + # `_next_color_index` must exist before super().__init__ because each + # `append` during construction routes through `_append_internal` → + # `_advance_color_index`, which reads the attribute. self._next_color_index = next_color_index - super().__init__(name, interface, *models, unique_name=unique_name, **kwargs) + super().__init__( + name, + interface, + *models, + unique_name=unique_name, + populate_if_none=False, + **kwargs, + ) color_count = len(COLORS) if color_count == 0: self._next_color_index = 0 - elif self._next_color_index is None: + elif next_color_index is None: self._next_color_index = len(self) % color_count else: - self._next_color_index %= color_count + self._next_color_index = next_color_index % color_count + + @property + def next_color_index(self) -> Optional[int]: + """Index of the next colour to assign — kept around so it round-trips.""" + return self._next_color_index def add_model(self, model: Optional[Model] = None): """Add a model to the collection. - :param model: Model to add. + Parameters + ---------- + model : Optional[Model], optional + Model to add. By default, None. """ if model is None: model = Model(name='Model', interface=self.interface, color=self._current_color()) @@ -58,38 +78,34 @@ def add_model(self, model: Optional[Model] = None): def duplicate_model(self, index: int): """Duplicate a model in the collection. - :param index: Model to duplicate. + Parameters + ---------- + index : int + Model to duplicate. """ to_be_duplicated = self[index] duplicate = Model.from_dict(to_be_duplicated.as_dict(skip=['unique_name'])) duplicate.name = duplicate.name + ' duplicate' self.append(duplicate) - def as_dict(self, skip: List[str] | None = None) -> dict: - this_dict = super().as_dict(skip=skip) - this_dict['populate_if_none'] = self.populate_if_none - this_dict['next_color_index'] = self._next_color_index - return this_dict - @classmethod def from_dict(cls, this_dict: dict) -> ModelCollection: - """ - Create an instance of a collection from a dictionary. - - :param data: The dictionary for the collection - """ - collection_dict = this_dict.copy() - # We need to call from_dict on the base class to get the models - dict_data = collection_dict.pop('data') + """Create an instance of a collection from a dictionary.""" + collection_dict = dict(this_dict) + dict_data = collection_dict.pop('data', []) next_color_index = collection_dict.pop('next_color_index', None) - collection = super().from_dict(collection_dict) # type: ModelCollection + # Reconstruct empty collection via EasyList.from_dict (handles + # protected_types and assigns name/unique_name/populate_if_none). + collection = super().from_dict(collection_dict) + # Append each model without advancing the colour index — the saved + # `next_color_index` below is the source of truth. for model_data in dict_data: collection._append_internal(Model.from_dict(model_data), advance=False) - if len(collection) != len(this_dict['data']): - raise ValueError(f'Expected {len(collection)} models, got {len(this_dict["data"])}') + if len(collection) != len(dict_data): + raise ValueError(f'Expected {len(dict_data)} models, got {len(collection)}') color_count = len(COLORS) if color_count == 0: @@ -102,14 +118,24 @@ def from_dict(cls, this_dict: dict) -> ModelCollection: return collection def append(self, model: Model) -> None: # type: ignore[override] + """Append function.""" self._append_internal(model, advance=True) def _append_internal(self, model: Model, advance: bool) -> None: - super().append(model) + """Append internal.""" + # Bypass our own `append` override and go straight to EasyList's + # `MutableSequence.append` → `insert` path. Calling `super().append` + # would dispatch back to `ModelCollection.append` because Python + # resolves `append` via MRO from MutableSequence which doesn't + # define it on a class higher than ModelCollection. + from collections.abc import MutableSequence + + MutableSequence.append(self, model) if advance: self._advance_color_index() def _advance_color_index(self) -> None: + """Advance color index.""" if not COLORS: self._next_color_index = 0 return @@ -119,6 +145,7 @@ def _advance_color_index(self) -> None: self._next_color_index = (self._next_color_index + 1) % len(COLORS) def _current_color(self) -> str: + """Current color.""" if not COLORS: raise ValueError('No colors defined for models.') if self._next_color_index is None: diff --git a/src/easyreflectometry/model/resolution_functions.py b/src/easyreflectometry/model/resolution_functions.py index 2a6e5c8c..9579ad66 100644 --- a/src/easyreflectometry/model/resolution_functions.py +++ b/src/easyreflectometry/model/resolution_functions.py @@ -1,8 +1,19 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + """Resolution functions for the resolution of the experiment. When a percentage is provided we assume that the resolution is a Gaussian distribution with a FWHM of the percentage of the q value. To convert from a sigma value to a FWHM value we use the formula FWHM = 2.35 * sigma [2 * np.sqrt(2 * np.log(2)) * sigma]. + +The :meth:`ResolutionFunction.smearing` contract returns **sigma** +(the standard deviation of the Gaussian resolution) for every resolution +type. This matches the ``sQz`` convention used by data reduction and the +natural output of :class:`Pointwise`. Each calculation engine wrapper is +responsible for converting sigma to the width convention of its backend +(FWHM for refnx, sigma for refl1d), so that vector resolutions are +interpreted consistently across engines (see GitHub issue #367). """ from __future__ import annotations @@ -16,107 +27,134 @@ DEFAULT_RESOLUTION_FWHM_PERCENTAGE = 5.0 +# Conversion factor between sigma and FWHM for a Gaussian: FWHM = SIGMA_TO_FWHM * sigma. +SIGMA_TO_FWHM = 2 * np.sqrt(2 * np.log(2)) + class ResolutionFunction: @abstractmethod - def smearing(self, q: Union[np.array, float]) -> np.array: ... + def smearing(self, q: Union[np.array, float]) -> np.array: + """Return the resolution as sigma (standard deviation) at each ``q``.""" + ... @abstractmethod def as_dict(self, skip: Optional[List[str]] = None) -> dict: ... @classmethod def from_dict(cls, data: dict) -> ResolutionFunction: + """Smearing function.""" if data['smearing'] == 'PercentageFwhm': return PercentageFwhm(data['constant']) if data['smearing'] == 'LinearSpline': return LinearSpline(data['q_data_points'], data['fwhm_values']) if data['smearing'] == 'Pointwise': - return Pointwise([data['q_data_points'], data['R_data_points'], data['sQz_data_points']]) + return Pointwise([ + data['q_data_points'], + data['R_data_points'], + data['sQz_data_points'], + ]) raise ValueError('Unknown resolution function type') class PercentageFwhm(ResolutionFunction): def __init__(self, constant: Union[None, float] = None): + """Init function.""" if constant is None: constant = DEFAULT_RESOLUTION_FWHM_PERCENTAGE self.constant = constant def smearing(self, q: Union[np.array, float]) -> np.array: - return np.ones(np.array(q).size) * self.constant + """Return per-point sigma values from the constant FWHM percentage. + + ``constant`` is a FWHM percentage of ``q``; it is converted to an + absolute sigma so the smearing() contract is sigma for all types. + """ + q_array = np.asarray(q, dtype=float) + fwhm = (self.constant / 100.0) * q_array + return fwhm / SIGMA_TO_FWHM def as_dict( self, skip: Optional[List[str]] = None ) -> dict[str, str]: # skip is kept for consistency of the as_dict signature + """As dict.""" return {'smearing': 'PercentageFwhm', 'constant': self.constant} class LinearSpline(ResolutionFunction): def __init__(self, q_data_points: np.array, fwhm_values: np.array): + """Init function.""" self.q_data_points = q_data_points self.fwhm_values = fwhm_values def smearing(self, q: Union[np.array, float]) -> np.array: - return np.interp(q, self.q_data_points, self.fwhm_values) + """Return per-point sigma values from the FWHM knots. + + The stored ``fwhm_values`` are FWHM widths; they are interpolated + onto ``q`` and converted to sigma to satisfy the smearing() contract. + """ + fwhm = np.interp(np.asarray(q, dtype=float), self.q_data_points, self.fwhm_values) + return fwhm / SIGMA_TO_FWHM def as_dict( self, skip: Optional[List[str]] = None ) -> dict[str, str]: # skip is kept for consistency of the as_dict signature - return {'smearing': 'LinearSpline', 'q_data_points': list(self.q_data_points), 'fwhm_values': list(self.fwhm_values)} + """As dict.""" + return { + 'smearing': 'LinearSpline', + 'q_data_points': list(self.q_data_points), + 'fwhm_values': list(self.fwhm_values), + } -# add pointwise smearing funtion class Pointwise(ResolutionFunction): - def __init__(self, q_data_points: list[np.ndarray]): + """Pointwise resolution defined by a per-point resolution provided with the data. + + The resolution is supplied as the variance of the Qz values (``sQz``) at the + measured Qz data points, which is the form produced by data reduction (e.g. + ``Qz_0.variances``). The resolution width at each point is ``sqrt(sQz)``. + For a requested ``q`` the width is obtained by linearly interpolating onto + ``q``, exactly as :class:`LinearSpline` does for explicitly provided widths. + + This is a convenience wrapper around :class:`LinearSpline` that derives the + widths from the ``[Qz, R, sQz]`` triple loaded from a data file; the returned + widths are consumed by the calculators (refnx ``x_err`` / refl1d ``dq``), + which perform the actual convolution against the model. + """ + + def __init__(self, q_data_points: List[np.ndarray]): + """Init function. + + Parameters + ---------- + q_data_points : List[np.ndarray] + ``[Qz, R, sQz]`` where ``Qz`` are the measured Qz values, ``R`` the + measured reflectivity (kept only for serialization round-trips) and + ``sQz`` the variance of ``Qz`` at each point. + """ self.q_data_points = q_data_points - self.q = None - - def smearing(self, q: Union[np.ndarray, float] = None) -> np.ndarray: - Qz = self.q_data_points[0] - R = self.q_data_points[1] - sQz = self.q_data_points[2] - if q is None: - q = self.q_data_points[0] - self.q = q - sQzs = np.sqrt(sQz) - if isinstance(Qz, float): - Qz = np.array(Qz) - - smeared = self.apply_smooth_smearing(Qz, R, sQzs) - return smeared + + def smearing(self, q: Optional[Union[np.ndarray, float]] = None) -> np.ndarray: + """Return the resolution sigma interpolated onto ``q``. + + ``sQz`` is the variance of ``Qz``, so the sigma at each data point is + ``sqrt(sQz)``; values are linearly interpolated onto the requested + ``q``. This already satisfies the sigma smearing() contract, so no + FWHM conversion is applied. When ``q`` is ``None`` the sigma values + are returned at the stored data points. + """ + Qz = np.asarray(self.q_data_points[0], dtype=float) + sQz = np.asarray(self.q_data_points[2], dtype=float) + q_eval = Qz if q is None else np.asarray(q, dtype=float) + widths = np.sqrt(sQz) + return np.asarray(np.interp(q_eval, Qz, widths)) def as_dict( self, skip: Optional[List[str]] = None ) -> dict[str, str]: # skip is kept for consistency of the as_dict signature + """As dict.""" return { 'smearing': 'Pointwise', 'q_data_points': list(self.q_data_points[0]), 'R_data_points': list(self.q_data_points[1]), 'sQz_data_points': list(self.q_data_points[2]), } - - def gaussian_smearing(self, qt, Qz, R, sQz): - weights = np.exp(-0.5 * ((qt - Qz) / sQz) ** 2) - if np.sum(weights) == 0 or not np.isfinite(np.sum(weights)): - return np.sum(R) - weights /= sQz * np.sqrt(2 * np.pi) - return np.sum(R * weights) / np.sum(weights) - - def apply_smooth_smearing(self, Qz, R, sQzs): - """ - Apply smooth resolution smearing using convolution with Gaussian kernel. - """ - if self.q is None: - R_smeared = np.zeros_like(Qz) - else: - R_smeared = np.zeros_like(self.q) - - if not isinstance(Qz, np.ndarray): - Qz = np.array(Qz) - if not isinstance(R, np.ndarray): - R = np.array(R) - R_smeared = np.zeros_like(self.q) - - for i, qt in enumerate(self.q): - R_smeared[i] = self.gaussian_smearing(qt, Qz, R, sQzs) - - return R_smeared diff --git a/src/easyreflectometry/orso_utils.py b/src/easyreflectometry/orso_utils.py index 494ed248..aa320933 100644 --- a/src/easyreflectometry/orso_utils.py +++ b/src/easyreflectometry/orso_utils.py @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2025 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + import logging import warnings @@ -49,8 +52,7 @@ def load_data_from_orso_file(fname: str) -> sc.DataGroup: def load_orso_model(orso_data) -> Sample: - """ - Load a model from an ORSO file and return a Sample object. + """Load a model from an ORSO file and return a Sample object. The ORSO file .ort contains information about the sample, saved as a simple "stack" string, e.g. 'air | m1 | SiO2 | Si'. @@ -61,11 +63,20 @@ def load_orso_model(orso_data) -> Sample: - Middle layers -> 'Loaded layer' Multilayer assembly (parameters enabled) - Last layer -> Subphase assembly (thickness=0 fixed, roughness enabled) - :param orso_data: Parsed ORSO dataset list (as returned by ``orso.load_orso``). - :type orso_data: list - :return: An EasyReflectometry Sample object. - :rtype: Sample - :raises ValueError: If ORSO layers could not be resolved or fewer than 2 layers. + Parameters + ---------- + orso_data : list + Parsed ORSO dataset list (as returned by ``orso.load_orso``). + + Raises + ------ + ValueError : + If ORSO layers could not be resolved or fewer than 2 layers. + + Returns + ------- + Sample + An EasyReflectometry Sample object. """ # Extract stack string and layer definitions from ORSO sample model sample_model = orso_data[0].info.data_source.sample.model @@ -134,7 +145,7 @@ def load_orso_model(orso_data) -> Sample: def _convert_orso_layer_to_erl(layer): - """Helper function to convert an ORSO layer to an EasyReflectometry layer""" + r"""Helper function to convert an ORSO layer to an EasyReflectometry laye.""" material = layer.material # Prefer original_name for material name, fall back to formula if available m_name = layer.original_name if layer.original_name is not None else material.formula @@ -157,7 +168,7 @@ def _get_sld_values(material, material_name): Note: ORSO stores SLD in absolute units (A^-2), but the internal representation uses 10^-6 A^-2. When reading directly from ORSO, we multiply by 1e6 to convert. - When calculating from mass density, MaterialDensity already returns the correct units. + When calculating from mass density, MaterialDensity already returns the correct units.. """ if material.sld is None and material.mass_density is not None: # Calculate SLD from mass density @@ -196,10 +207,15 @@ def _get_sld_values(material, material_name): def load_orso_data(orso_data) -> DataSet1D: """Convert parsed ORSO dataset objects into a scipp DataGroup. - :param orso_data: Parsed ORSO dataset list (as returned by ``orso.load_orso``). - :type orso_data: list - :return: A scipp DataGroup with data, coords, and attrs. - :rtype: sc.DataGroup + Parameters + ---------- + orso_data : list + Parsed ORSO dataset list (as returned by ``orso.load_orso``). + + Returns + ------- + sc.DataGroup + A scipp DataGroup with data, coords, and attrs. """ data = {} coords = {} diff --git a/src/easyreflectometry/plot.py b/src/easyreflectometry/plot.py index a88a89bf..3a0aaa26 100644 --- a/src/easyreflectometry/plot.py +++ b/src/easyreflectometry/plot.py @@ -1,4 +1,6 @@ -__author__ = 'github.com/arm61' +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + import matplotlib.pyplot as plt import scipp as sc @@ -8,10 +10,12 @@ def plot(data: sc.DataGroup) -> None: - """ - A general plotting function for easyreflectometry. + """A general plotting function for easyreflectometry. - :param data: the DataGroup to be plotted. + Parameters + ---------- + data : sc.DataGroup + The DataGroup to be plotted. """ if len([i for i in list(data.keys()) if 'SLD' in i]) == 0: plot_sld = False @@ -41,7 +45,14 @@ def plot(data: sc.DataGroup) -> None: ) plot_model_data.data *= sc.scalar(10.0**i, unit=plot_model_data.unit) plot_model_data.coords[f'Qz_{refl_num}'].variances = None - sc.plot(plot_model_data, ax=ax1, norm='log', linestyle='--', color=color_cycle[i], marker='') + sc.plot( + plot_model_data, + ax=ax1, + norm='log', + linestyle='--', + color=color_cycle[i], + marker='', + ) except KeyError: pass ax1.autoscale(True) diff --git a/src/easyreflectometry/project.py b/src/easyreflectometry/project.py index 7e1ab3c9..aa0eacf6 100644 --- a/src/easyreflectometry/project.py +++ b/src/easyreflectometry/project.py @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2024 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + import datetime import json import logging @@ -25,6 +28,7 @@ from easyreflectometry.model import Model from easyreflectometry.model import ModelCollection from easyreflectometry.model import PercentageFwhm +from easyreflectometry.model import Pointwise from easyreflectometry.sample import Layer from easyreflectometry.sample import Material from easyreflectometry.sample import MaterialCollection @@ -43,6 +47,7 @@ class Project: def __init__(self): + """Init function.""" self._info = self._default_info() self._path_project_parent = Path(os.path.expanduser('~')) self._models = ModelCollection(populate_if_none=False, unique_name='project_models') @@ -68,6 +73,7 @@ def __init__(self): self._with_experiments = False def reset(self): + """Reset function.""" del self._models del self._materials global_object.map._clear() @@ -85,7 +91,7 @@ def parameters(self) -> List[Parameter]: seen_ids: set[int] = set() if self._models is not None: for model in self._models: - for param in model.get_parameters(): + for param in model.get_all_parameters(): pid = id(param) if pid not in seen_ids: seen_ids.add(pid) @@ -140,40 +146,48 @@ def _sync_layer_parameter_state(self, parameter: Parameter, kind: str, disabled_ @property def q_min(self): + """Q min.""" if self._q_min is None: return Q_MIN return self._q_min @q_min.setter def q_min(self, value: float) -> None: + """Q min.""" self._q_min = value @property def q_max(self): + """Q max.""" if self._q_max is None: return Q_MAX return self._q_max @q_max.setter def q_max(self, value: float) -> None: + """Q max.""" self._q_max = value @property def q_resolution(self): + """Q resolution.""" if self._q_resolution is None: return Q_RESOLUTION return self._q_resolution @q_resolution.setter def q_resolution(self, value: int) -> None: + """Q resolution.""" self._q_resolution = value @property def current_material_index(self) -> Optional[int]: + """Current material index.""" return self._current_material_index @current_material_index.setter def current_material_index(self, value: int) -> None: + """Current material index.""" if value < 0 or value >= len(self._materials): raise ValueError(f'Index {value} out of range') if self._current_material_index != value: @@ -181,10 +195,12 @@ def current_material_index(self, value: int) -> None: @property def current_model_index(self) -> Optional[int]: + """Current model index.""" return self._current_model_index @current_model_index.setter def current_model_index(self, value: int) -> None: + """Current model index.""" if value < 0 or value >= len(self._models): raise ValueError(f'Index {value} out of range') if self._current_model_index != value: @@ -194,10 +210,12 @@ def current_model_index(self, value: int) -> None: @property def current_assembly_index(self) -> Optional[int]: + """Current assembly index.""" return self._current_assembly_index @current_assembly_index.setter def current_assembly_index(self, value: int) -> None: + """Current assembly index.""" if value < 0 or value >= len(self._models[self._current_model_index].sample): raise ValueError(f'Index {value} out of range') if self._current_assembly_index != value: @@ -206,10 +224,12 @@ def current_assembly_index(self, value: int) -> None: @property def current_layer_index(self) -> Optional[int]: + """Current layer index.""" return self._current_layer_index @current_layer_index.setter def current_layer_index(self, value: int) -> None: + """Current layer index.""" if value < 0 or value >= len(self._models[self._current_model_index].sample[self._current_assembly_index].layers): raise ValueError(f'Index {value} out of range') if self._current_layer_index != value: @@ -217,10 +237,12 @@ def current_layer_index(self, value: int) -> None: @property def current_experiment_index(self) -> Optional[int]: + """Current experiment index.""" return self._current_experiment_index @current_experiment_index.setter def current_experiment_index(self, value: int) -> None: + """Current experiment index.""" if value < 0 or value >= len(self._experiments): raise ValueError(f'Index {value} out of range') if self._current_experiment_index != value: @@ -230,21 +252,26 @@ def current_experiment_index(self, value: int) -> None: @property def created(self) -> bool: + """Created function.""" return self._created @property def path(self): + """Path function.""" return self._path_project_parent / self._info['name'] def set_path_project_parent(self, path: Union[Path, str]): + """Set path project parent.""" self._path_project_parent = Path(path) @property def models(self) -> ModelCollection: + """Models function.""" return self._models @models.setter def models(self, models: ModelCollection) -> None: + """Models function.""" self._replace_collection(models, self._models) # Use setter to update indicies for current model, assembly and layer self.current_model_index = 0 @@ -255,6 +282,7 @@ def models(self, models: ModelCollection) -> None: @property def fitter(self) -> MultiFitter: + """Fitter function.""" if len(self._models): if (self._fitter is None) or (self._fitter_model_index != self._current_model_index): self._fitter = MultiFitter(self._models[self._current_model_index]) @@ -264,10 +292,12 @@ def fitter(self) -> MultiFitter: @property def calculator(self) -> str: + """Calculator function.""" return self._calculator.current_interface_name @calculator.setter def calculator(self, calculator: str) -> None: + """Calculator function.""" if calculator == self._calculator.current_interface_name: return @@ -282,47 +312,61 @@ def calculator(self, calculator: str) -> None: @property def minimizer(self) -> AvailableMinimizers: + """Minimizer function.""" if self._fitter is not None: return self._fitter.easy_science_multi_fitter.minimizer.enum return self._minimizer_selection @minimizer.setter def minimizer(self, minimizer: AvailableMinimizers) -> None: + """Minimizer function.""" old_name = getattr(self._minimizer_selection, 'name', str(self._minimizer_selection)) new_name = getattr(minimizer, 'name', str(minimizer)) - logger.info('Minimizer changed from %s to %s (fitter active: %s)', old_name, new_name, self._fitter is not None) + logger.info( + 'Minimizer changed from %s to %s (fitter active: %s)', + old_name, + new_name, + self._fitter is not None, + ) self._minimizer_selection = minimizer if self._fitter is not None: self._fitter.easy_science_multi_fitter.switch_minimizer(minimizer) @property def experiments(self) -> Dict[int, DataSet1D]: + """Experiments function.""" return self._experiments @experiments.setter def experiments(self, experiments: Dict[int, DataSet1D]) -> None: + """Experiments function.""" self._experiments = experiments @property def path_json(self): + """Path json.""" return self.path / 'project.json' def get_index_air(self) -> int: + """Get index air.""" if 'Air' not in [material.name for material in self._materials]: self._materials.add_material(Material(name='Air', sld=0.0, isld=0.0)) return [material.name for material in self._materials].index('Air') def get_index_si(self) -> int: + """Get index si.""" if 'Si' not in [material.name for material in self._materials]: self._materials.add_material(Material(name='Si', sld=2.07, isld=0.0)) return [material.name for material in self._materials].index('Si') def get_index_sio2(self) -> int: + """Get index sio2.""" if 'SiO2' not in [material.name for material in self._materials]: self._materials.add_material(Material(name='SiO2', sld=3.47, isld=0.0)) return [material.name for material in self._materials].index('SiO2') def get_index_d2o(self) -> int: + """Get index d2o.""" if 'D2O' not in [material.name for material in self._materials]: self._materials.add_material(Material(name='D2O', sld=6.36, isld=0.0)) return [material.name for material in self._materials].index('D2O') @@ -351,10 +395,15 @@ def set_sample_from_orso(self, sample: Sample) -> None: This is a convenience helper for the ORSO import pipeline where a complete :class:`~easyreflectometry.sample.Sample` is constructed elsewhere. - :param sample: Sample to set as the project's (single) model. - :type sample: easyreflectometry.sample.Sample - :return: ``None``. - :rtype: None + Parameters + ---------- + sample : Sample + Sample to set as the project's (single) model. + + Returns + ------- + None + ``None``. """ model = Model(sample=sample) self.models = ModelCollection([model]) @@ -369,10 +418,15 @@ def add_sample_from_orso(self, sample: Sample) -> None: After adding the model, :attr:`current_model_index` is updated to point to the newly added model. - :param sample: Sample to add as a new model. - :type sample: easyreflectometry.sample.Sample - :return: ``None``. - :rtype: None + Parameters + ---------- + sample : Sample + Sample to add as a new model. + + Returns + ------- + None + ``None``. """ if sample is None: raise ValueError('The ORSO file does not contain a valid sample model definition.') @@ -393,10 +447,15 @@ def replace_models_from_orso(self, sample: Sample) -> None: model is created from *sample*, assigned to the project's calculator, and the material collection is rebuilt from the new model only. - :param sample: Sample to set as the project's only model. - :type sample: easyreflectometry.sample.Sample - :return: ``None``. - :rtype: None + Parameters + ---------- + sample : Sample + Sample to set as the project's only model. + + Returns + ------- + None + ``None``. """ if sample is None: raise ValueError('The ORSO file does not contain a valid sample model definition.') @@ -427,11 +486,18 @@ def _apply_experiment_metadata( ) -> None: """Set experiment name from ORSO title and configure the resolution function. - :param path: Path to the experiment data file. - :param experiment: The loaded experiment dataset to configure. - :param fallback_name: Name to use when no ORSO title is available. - :param data_group: Pre-loaded scipp DataGroup (avoids reloading the file). - :param data_key: Specific dataset key to use for title extraction (e.g. ``'R_1'``). + Parameters + ---------- + path : Union[Path, str] + Path to the experiment data file. + experiment : DataSet1D + The loaded experiment dataset to configure. + fallback_name : str + Name to use when no ORSO title is available. + data_group : + Pre-loaded scipp DataGroup (avoids reloading the file). By default, None. + data_key : Optional[str], optional + Specific dataset key to use for title extraction (e.g. ``'R_1'``). By default, None. """ # Prefer ORSO title when available (keeps UI descriptive) title = None @@ -456,10 +522,21 @@ def _apply_resolution_function( ) -> None: """Set the resolution function on *model* based on variance data in *experiment*. - :param experiment: The experiment whose variance data drives the choice. - :param model: The model whose resolution function is set. + Uses the measured per-point q-resolution (``Pointwise``) when the + experiment carries q-variance data (``xe``, i.e. sQz²); otherwise + falls back to the default 5% FWHM percentage resolution. + + Parameters + ---------- + experiment : DataSet1D + The experiment whose variance data drives the choice. + model : Model + The model whose resolution function is set. """ - model.resolution_function = PercentageFwhm(5.0) + if experiment.xe is not None and np.any(experiment.xe): + model.resolution_function = Pointwise(q_data_points=[experiment.x, experiment.y, experiment.xe]) + else: + model.resolution_function = PercentageFwhm(5.0) @staticmethod def _auto_set_background(experiment: DataSet1D) -> None: @@ -468,6 +545,7 @@ def _auto_set_background(experiment: DataSet1D) -> None: experiment.model.background = max(np.min(experiment.y), 1e-10) def load_new_experiment(self, path: Union[Path, str]) -> None: + """Load new experiment.""" new_experiment = load_as_dataset(str(path)) new_index = len(self._experiments) @@ -485,8 +563,15 @@ def load_new_experiment(self, path: Union[Path, str]) -> None: def count_datasets_in_file(self, path: Union[Path, str]) -> int: """Return the number of datasets contained in the file at *path*. - :param path: Path to the data file. - :return: Number of datasets found; 1 if the file cannot be introspected. + Parameters + ---------- + path : Union[Path, str] + Path to the data file. + + Returns + ------- + int + Number of datasets found; 1 if the file cannot be introspected. """ try: data_group = load_data_from_orso_file(str(path)) @@ -502,8 +587,15 @@ def load_all_experiments_from_file(self, path: Union[Path, str]) -> int: currently selected. Falls back to :meth:`load_new_experiment` for single-dataset files or on any loading error. - :param path: Path to the data file. - :return: Number of experiments that were added. + Parameters + ---------- + path : Union[Path, str] + Path to the data file. + + Returns + ------- + int + Number of experiments that were added. """ try: data_group = load_data_from_orso_file(str(path)) @@ -547,6 +639,7 @@ def load_all_experiments_from_file(self, path: Union[Path, str]) -> int: return len(data_keys) def load_experiment_for_model_at_index(self, path: Union[Path, str], index: Optional[int] = 0) -> None: + """Load experiment for model at index.""" experiment = load_as_dataset(str(path)) self._apply_experiment_metadata(path, experiment, f'Experiment {index}') @@ -557,6 +650,7 @@ def load_experiment_for_model_at_index(self, path: Union[Path, str], index: Opti self._apply_resolution_function(experiment, self._models[index]) def sld_data_for_model_at_index(self, index: int = 0) -> DataSet1D: + """Sld data for model at index.""" self.models[index].interface = self._calculator sld = self.models[index].interface().sld_profile(self._models[index].unique_name) return DataSet1D( @@ -566,6 +660,7 @@ def sld_data_for_model_at_index(self, index: int = 0) -> DataSet1D: ) def sample_data_for_model_at_index(self, index: int = 0, q_range: Optional[np.array] = None) -> DataSet1D: + """Sample data for model at index.""" original_resolution_function = self.models[index].resolution_function self.models[index].resolution_function = PercentageFwhm(0) reflectivity_data = self.model_data_for_model_at_index(index, q_range) @@ -574,6 +669,7 @@ def sample_data_for_model_at_index(self, index: int = 0, q_range: Optional[np.ar return reflectivity_data def model_data_for_model_at_index(self, index: int = 0, q_range: Optional[np.array] = None) -> DataSet1D: + """Model data for model at index.""" if q_range is None: q_range = np.linspace(self.q_min, self.q_max, self.q_resolution) self.models[index].interface = self._calculator @@ -585,18 +681,38 @@ def model_data_for_model_at_index(self, index: int = 0, q_range: Optional[np.arr ) def experimental_data_for_model_at_index(self, index: int = 0) -> DataSet1D: + """Experimental data for model at index.""" if index in self._experiments.keys(): return self._experiments[index] else: raise IndexError(f'No experiment data for model at index {index}') def default_model(self): + """Default model.""" self._replace_collection(MaterialCollection(interface=self._calculator), self._materials) layers = [ - Layer(material=self._materials[0], thickness=0.0, roughness=0.0, name='Vacuum Layer', interface=self._calculator), - Layer(material=self._materials[1], thickness=100.0, roughness=3.0, name='D2O Layer', interface=self._calculator), - Layer(material=self._materials[2], thickness=0.0, roughness=1.2, name='Si Layer', interface=self._calculator), + Layer( + material=self._materials[0], + thickness=0.0, + roughness=0.0, + name='Vacuum Layer', + interface=self._calculator, + ), + Layer( + material=self._materials[1], + thickness=100.0, + roughness=3.0, + name='D2O Layer', + interface=self._calculator, + ), + Layer( + material=self._materials[2], + thickness=0.0, + roughness=1.2, + name='Si Layer', + interface=self._calculator, + ), ] assemblies = [ Multilayer(layers[0], name='Superphase', interface=self._calculator), @@ -611,10 +727,15 @@ def default_model(self): def is_default_model(self, index: int) -> bool: """Check if the model at the given index is a default model. - :param index: Index of the model to check. - :type index: int - :return: True if the model was created as a default placeholder. - :rtype: bool + Parameters + ---------- + index : int + Index of the model to check. + + Returns + ------- + bool + True if the model was created as a default placeholder. """ if index < 0 or index >= len(self._models): return False @@ -630,10 +751,17 @@ def remove_model_at_index(self, index: int) -> None: Adjusts the current model index if necessary. - :param index: Index of the model to remove. - :type index: int - :raises IndexError: If the index is out of range. - :raises ValueError: If trying to remove the last remaining model. + Parameters + ---------- + index : int + Index of the model to remove. + + Raises + ------ + IndexError : + If the index is out of range. + ValueError : + If trying to remove the last remaining model. """ if index < 0 or index >= len(self._models): raise IndexError(f'Model index {index} out of range') @@ -668,18 +796,21 @@ def remove_model_at_index(self, index: int) -> None: self._current_layer_index = 0 def add_material(self, material: MaterialCollection) -> None: + """Add material.""" if material in self._materials: print(f'WARNING: Material {material} is already in material collection') else: self._materials.append(material) def remove_material(self, index: int) -> None: + """Remove material.""" if self._materials[index] in self._get_materials_in_models(): print(f'ERROR: Material {self._materials[index]} is used in models') else: self._materials.pop(index) def _default_info(self): + """Default info.""" return dict( name='DefaultEasyReflectometryProject', short_description='Reflectometry, 1D', @@ -687,6 +818,7 @@ def _default_info(self): ) def create(self): + """Create function.""" if not os.path.exists(self.path): os.makedirs(self.path) os.makedirs(self.path / 'experiments') @@ -696,6 +828,7 @@ def create(self): print(f'ERROR: Directory {self.path} already exists') def save_as_json(self, overwrite=False): + """Save as json.""" if self.path_json.exists() and overwrite: print(f'File already exists {self.path_json}. Overwriting...') self.path_json.unlink() @@ -708,6 +841,7 @@ def save_as_json(self, overwrite=False): print(exception) def load_from_json(self, path: Optional[Union[Path, str]] = None): + """Load from json.""" if path is None: path = self.path_json path = Path(path) @@ -721,8 +855,18 @@ def load_from_json(self, path: Optional[Union[Path, str]] = None): else: print(f'ERROR: File {path} does not exist') + #: Schema version embedded in every serialized project. Bumped from 1 → 2 + #: when the sample/model classes migrated from the legacy + #: ``easyscience.ObjBase``/``CollectionBase`` pipeline to + #: ``ModelBase``/``EasyList``. The on-disk shape of nested objects (Layer, + #: Material, MaterialMixture, MaterialSolvated, LayerAreaPerMolecule, etc.) + #: changed in a way that is not backward-compatible with v1 files. + FILE_FORMAT = 2 + def as_dict(self, include_materials_not_in_model=False): + """As dict.""" project_dict = {} + project_dict['file_format'] = self.FILE_FORMAT project_dict['info'] = self._info project_dict['with_experiments'] = self._with_experiments if self._models is not None: @@ -743,6 +887,7 @@ def as_dict(self, include_materials_not_in_model=False): return project_dict def _as_dict_add_materials_not_in_model_dict(self, project_dict: dict): + """As dict add materials not in model dict.""" materials_not_in_model = [] for material in self._materials: if material not in self._get_materials_in_models(): @@ -751,19 +896,44 @@ def _as_dict_add_materials_not_in_model_dict(self, project_dict: dict): project_dict['materials_not_in_model'] = MaterialCollection(materials_not_in_model).as_dict(skip=['interface']) def _as_dict_add_experiments(self, project_dict: dict): + """As dict add experiments.""" project_dict['experiments'] = {} project_dict['experiments_models'] = {} project_dict['experiments_names'] = {} for key, experiment in self._experiments.items(): - project_dict['experiments'][key] = [list(experiment.x), list(experiment.y), list(experiment.ye)] + project_dict['experiments'][key] = [ + list(experiment.x), + list(experiment.y), + list(experiment.ye), + ] if experiment.xe is not None: project_dict['experiments'][key].append(list(experiment.xe)) project_dict['experiments_models'][key] = experiment.model.name project_dict['experiments_names'][key] = experiment.name def from_dict(self, project_dict: dict): + """From dict.""" keys = list(project_dict.keys()) + # Validate file format. v1 files were written by the legacy + # `ObjBase`/`CollectionBase` pipeline; their inner shapes (Layer, + # Material, MaterialMixture, …) are not compatible with the v2 + # `ModelBase`/`EasyList` deserializer. Older files must be re-created. + file_format = project_dict.get('file_format') + if file_format is None: + raise ValueError( + 'This project file predates file_format=2 and cannot be loaded by ' + 'this version of easyreflectometry. The serialization format changed ' + 'when the sample/model classes migrated from the legacy ObjBase / ' + 'CollectionBase pipeline. Please re-create the project from its ' + 'underlying data using the current API.' + ) + if file_format != self.FILE_FORMAT: + raise ValueError( + f'Unsupported project file_format={file_format!r}; this version of ' + f'easyreflectometry only reads file_format={self.FILE_FORMAT}. Please ' + 'either update easyreflectometry or re-create the project.' + ) self._info = project_dict['info'] self._with_experiments = project_dict['with_experiments'] if 'calculator' in keys: @@ -786,6 +956,7 @@ def from_dict(self, project_dict: dict): resolve_all_parameter_dependencies(self) def _from_dict_extract_experiments(self, project_dict: dict) -> Dict[int, DataSet1D]: + """From dict extract experiments.""" experiments = {} for key in project_dict['experiments'].keys(): experiments[int(key)] = DataSet1D( @@ -800,6 +971,7 @@ def _from_dict_extract_experiments(self, project_dict: dict) -> Dict[int, DataSe return experiments def _get_materials_in_models(self) -> MaterialCollection: + """Get materials in models.""" materials_in_model = MaterialCollection(populate_if_none=False) for model in self._models: for assembly in model.sample: @@ -808,6 +980,7 @@ def _get_materials_in_models(self) -> MaterialCollection: return materials_in_model def _replace_collection(self, src_collection: BaseCollection, dst_collection: BaseCollection) -> None: + """Replace collection.""" # Clear the destination collection for i in range(len(dst_collection)): dst_collection.pop(0) @@ -816,4 +989,5 @@ def _replace_collection(self, src_collection: BaseCollection, dst_collection: Ba dst_collection.append(element) def _timestamp_modification(self): + """Timestamp modification.""" self._info['modified'] = datetime.datetime.now().strftime('%d.%m.%Y %H:%M') diff --git a/src/easyreflectometry/sample/__init__.py b/src/easyreflectometry/sample/__init__.py index 4991b975..e6f347ad 100644 --- a/src/easyreflectometry/sample/__init__.py +++ b/src/easyreflectometry/sample/__init__.py @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + from .assemblies.base_assembly import BaseAssembly from .assemblies.bilayer import Bilayer from .assemblies.gradient_layer import GradientLayer diff --git a/src/easyreflectometry/sample/assemblies/__init__.py b/src/easyreflectometry/sample/assemblies/__init__.py new file mode 100644 index 00000000..4e798e20 --- /dev/null +++ b/src/easyreflectometry/sample/assemblies/__init__.py @@ -0,0 +1,2 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause diff --git a/src/easyreflectometry/sample/assemblies/base_assembly.py b/src/easyreflectometry/sample/assemblies/base_assembly.py index 68486805..fcb2f06d 100644 --- a/src/easyreflectometry/sample/assemblies/base_assembly.py +++ b/src/easyreflectometry/sample/assemblies/base_assembly.py @@ -1,4 +1,6 @@ -from typing import Any +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + from typing import Optional from ..base_core import BaseCore @@ -8,36 +10,43 @@ class BaseAssembly(BaseCore): """Assembly of layers. + The front layer (front_layer) is the layer the neutron beam starts in, it has an index of 0. The back layer (back_layer) is the final layer from which the unreflected neutron beam is transmitted, its index number depends on the number of finite layers in the system, but it might be accessed at index -1. """ - # Added in super().__init__ - #: Name of the assembly. - name: str - #: Layers in the assembly. - layers: LayerCollection - #: Interface to the calculator. - interface: Any - def __init__( self, name: str, type: str, interface, - **layers: LayerCollection, + layers: LayerCollection, + unique_name: Optional[str] = None, ): - super().__init__(name=name, interface=interface, **layers) + super().__init__(name=name, unique_name=unique_name) + self._layers = layers # Type is needed when fitting in easyscience self._type = type self._roughness_constraints_setup = False self._thickness_constraints_setup = False + if interface is not None: + self.interface = interface + + @property + def layers(self) -> LayerCollection: + return self._layers + + @layers.setter + def layers(self, value: LayerCollection) -> None: + self._layers = value + @property def type(self) -> str: """Get type of the assembly. + Needed by the GUI. """ return self._type @@ -53,7 +62,10 @@ def front_layer(self) -> Optional[Layer]: def front_layer(self, layer: Layer) -> None: """Set the front layer in the assembly. - :param layer: Layer to set as the front layer. + Parameters + ---------- + layer : Layer + Layer to set as the front layer. """ if len(self.layers) == 0: self.layers.append(layer) @@ -72,7 +84,10 @@ def back_layer(self) -> Optional[Layer]: def back_layer(self, layer: Layer) -> None: """Set the back layer in the assembly. - :param layer: Layer to set as the back layer. + Parameters + ---------- + layer : Layer + Layer to set as the back layer. """ if len(self.layers) == 0: @@ -83,18 +98,14 @@ def back_layer(self, layer: Layer) -> None: self.layers[-1] = layer def _setup_thickness_constraints(self) -> None: - """ - Setup thickness constraint, front layer is the deciding layer - """ + """Setup thickness constraint, front layer is the deciding layer.""" independent_param = self.front_layer.thickness for i in range(1, len(self.layers)): self.layers[i].thickness.make_dependent_on(dependency_expression='a', dependency_map={'a': independent_param}) self._thickness_constraints_setup = True def _enable_thickness_constraints(self): - """ - Enable the thickness constraint. - """ + """Enable the thickness constraint.""" if self._thickness_constraints_setup: # Make sure that the thickness constraint is enabled self._setup_thickness_constraints() @@ -103,9 +114,7 @@ def _enable_thickness_constraints(self): raise Exception('Thickness constraints not setup') def _disable_thickness_constraints(self): - """ - Disable the thickness constraint. - """ + """Disable the thickness constraint.""" if self._thickness_constraints_setup: for i in range(1, len(self.layers)): self.layers[i].thickness.make_independent() @@ -113,25 +122,19 @@ def _disable_thickness_constraints(self): raise Exception('Thickness constraints not setup') def _setup_roughness_constraints(self) -> None: - """ - Setup roughness constraint, front layer is the deciding layer - """ + """Setup roughness constraint, front layer is the deciding layer.""" independent_parameter = self.front_layer.roughness for i in range(1, len(self.layers)): self.layers[i].roughness.make_dependent_on(dependency_expression='a', dependency_map={'a': independent_parameter}) self._roughness_constraints_setup = True def _enable_roughness_constraints(self): - """ - Enable the roughness constraint. - """ + """Enable the roughness constraint.""" independent_parameter = self.front_layer.roughness for i in range(1, len(self.layers)): self.layers[i].roughness.make_dependent_on(dependency_expression='a', dependency_map={'a': independent_parameter}) def _disable_roughness_constraints(self): - """ - Disable the roughness constraint. - """ + """Disable the roughness constraint.""" for i in range(1, len(self.layers)): self.layers[i].roughness.make_independent() diff --git a/src/easyreflectometry/sample/assemblies/bilayer.py b/src/easyreflectometry/sample/assemblies/bilayer.py index 21c428f6..995128ed 100644 --- a/src/easyreflectometry/sample/assemblies/bilayer.py +++ b/src/easyreflectometry/sample/assemblies/bilayer.py @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + from __future__ import annotations from typing import Any @@ -56,6 +59,7 @@ def __init__( front_head_layer: LayerAreaPerMolecule | None = None, front_tail_layer: LayerAreaPerMolecule | None = None, back_head_layer: LayerAreaPerMolecule | None = None, + back_tail_layer: LayerAreaPerMolecule | None = None, name: str = 'EasyBilayer', unique_name: str | None = None, constrain_heads: bool = True, @@ -64,19 +68,35 @@ def __init__( ): """Constructor. - :param front_head_layer: Layer representing the front head part of the bilayer. - :param front_tail_layer: Layer representing the front tail part of the bilayer. - A back tail layer is created internally with its thickness, area per molecule, - and solvent fraction constrained to match this layer. - :param back_head_layer: Layer representing the back head part of the bilayer. - :param name: Name for bilayer, defaults to 'EasyBilayer'. - :param unique_name: Unique name for internal object tracking, defaults to `None`. - :param constrain_heads: When `True`, the back head layer thickness and area per + Parameters + ---------- + front_head_layer : LayerAreaPerMolecule | None, optional + Layer representing the front head part of the bilayer. By default, None. + front_tail_layer : LayerAreaPerMolecule | None, optional + Layer representing the front tail part of the bilayer. + The back tail layer's thickness, area per molecule, and solvent fraction are + constrained to match this layer. By default, None. + back_head_layer : LayerAreaPerMolecule | None, optional + Layer representing the back head part of the bilayer. By default, None. + back_tail_layer : LayerAreaPerMolecule | None, optional + Layer representing the back tail part of the bilayer. If omitted, a back tail + is created from the front tail (same molecular_formula, solvent, roughness, etc.). + Independent state (solvent, molecular_formula, name, roughness when + ``conformal_roughness`` is False) is preserved across serialization; the + structural parameters listed above are derived from the front tail. By default, None. + name : str, optional + Name for bilayer. By default, 'EasyBilayer'. + unique_name : str | None, optional + Unique name for internal object tracking. By default, None. + constrain_heads : bool, optional + When `True`, the back head layer thickness and area per molecule are constrained to match the front head layer. Solvent fraction - (hydration) remains independent on each side. Defaults to `True`. - :param conformal_roughness: When `True`, all four layer interfaces share - the same roughness value, controlled by the front head layer. Defaults to `True`. - :param interface: Calculator interface, defaults to `None`. + (hydration) remains independent on each side. By default, True. + conformal_roughness : bool, optional + When `True`, all four layer interfaces share + the same roughness value, controlled by the front head layer. By default, True. + interface : Any, optional + Calculator interface. By default, None. """ # Generate unique name for nested objects if unique_name is None: @@ -96,13 +116,16 @@ def __init__( interface=interface, ) - # Create back tail layer with initial values copied from the front tail. - # Its parameters will be constrained to the front tail after construction. - back_tail_layer = self._create_back_tail_layer( - front_tail_layer=front_tail_layer, - unique_name=unique_name, - interface=interface, - ) + # If no back tail is supplied, derive one from the front tail. The structural + # parameters (thickness, area_per_molecule, solvent_fraction) get constrained to + # the front tail in `_setup_tail_constraints` below regardless of which path + # produced this layer. + if back_tail_layer is None: + back_tail_layer = self._create_back_tail_layer( + front_tail_layer=front_tail_layer, + unique_name=unique_name, + interface=interface, + ) if back_head_layer is None: back_head_layer = self._create_default_head_layer( @@ -130,7 +153,6 @@ def __init__( interface=interface, ) - self.interface = interface self._conformal_roughness = False self._constrain_heads = False self._tail_constraints_setup = False @@ -154,10 +176,19 @@ def _create_default_head_layer( ) -> LayerAreaPerMolecule: """Create a default head layer with DPPC head group parameters. - :param unique_name: Base unique name for internal object tracking. - :param name_suffix: Suffix for layer name ('Front' or 'Back'). - :param interface: Calculator interface, defaults to `None`. - :return: A new LayerAreaPerMolecule for the head group. + Parameters + ---------- + unique_name : str + Base unique name for internal object tracking. + name_suffix : str + Suffix for layer name ('Front' or 'Back'). + interface : Any, optional + Calculator interface. By default, None. + + Returns + ------- + LayerAreaPerMolecule + A new LayerAreaPerMolecule for the head group. """ solvent = Material( sld=DEFAULTS['solvent']['sld'], @@ -185,9 +216,17 @@ def _create_default_tail_layer( ) -> LayerAreaPerMolecule: """Create a default tail layer with DPPC tail group parameters. - :param unique_name: Base unique name for internal object tracking. - :param interface: Calculator interface, defaults to `None`. - :return: A new LayerAreaPerMolecule for the tail group. + Parameters + ---------- + unique_name : str + Base unique name for internal object tracking. + interface : Any, optional + Calculator interface. By default, None. + + Returns + ------- + LayerAreaPerMolecule + A new LayerAreaPerMolecule for the tail group. """ solvent = Material( sld=DEFAULTS['solvent']['sld'], @@ -216,10 +255,19 @@ def _create_back_tail_layer( ) -> LayerAreaPerMolecule: """Create a back tail layer with initial values copied from the front tail layer. - :param front_tail_layer: The front tail layer to copy initial values from. - :param unique_name: Base unique name for internal object tracking. - :param interface: Calculator interface, defaults to `None`. - :return: A new LayerAreaPerMolecule for the back tail. + Parameters + ---------- + front_tail_layer : LayerAreaPerMolecule + The front tail layer to copy initial values from. + unique_name : str + Base unique name for internal object tracking. + interface : Any, optional + Calculator interface. By default, None. + + Returns + ------- + LayerAreaPerMolecule + A new LayerAreaPerMolecule for the back tail. """ solvent = Material( sld=DEFAULTS['solvent']['sld'], @@ -232,8 +280,8 @@ def _create_back_tail_layer( molecular_formula=front_tail_layer.molecular_formula, thickness=front_tail_layer.thickness.value, solvent=solvent, - solvent_fraction=front_tail_layer.solvent_fraction, - area_per_molecule=front_tail_layer.area_per_molecule, + solvent_fraction=front_tail_layer.solvent_fraction.value, + area_per_molecule=front_tail_layer.area_per_molecule.value, roughness=front_tail_layer.roughness.value, name=front_tail_layer.name + ' Back', unique_name=unique_name + '_LayerAreaPerMoleculeBackTail', @@ -312,7 +360,10 @@ def constrain_heads(self, status: bool) -> None: are constrained to match the front head layer. Solvent fraction (hydration) remains independent. - :param status: Boolean for the constraint status. + Parameters + ---------- + status : bool + Boolean for the constraint status. """ if status: self._enable_head_constraints() @@ -354,7 +405,10 @@ def conformal_roughness(self, status: bool) -> None: When enabled, all layers share the same roughness parameter (controlled by the front head layer). - :param status: Boolean for the constraint status. + Parameters + ---------- + status : bool + Boolean for the constraint status. """ if status: self._setup_roughness_constraints() @@ -367,7 +421,10 @@ def conformal_roughness(self, status: bool) -> None: def constrain_solvent_roughness(self, solvent_roughness: Parameter) -> None: """Add the constraint to the solvent roughness. - :param solvent_roughness: The solvent roughness parameter. + Parameters + ---------- + solvent_roughness : Parameter + The solvent roughness parameter. """ if not self.conformal_roughness: raise ValueError('Roughness must be conformal to use this function.') @@ -395,16 +452,28 @@ def constrain_multiple_contrast( Makes this bilayer's parameters dependent on another_contrast's parameters, so that changes to another_contrast propagate to this bilayer. - :param another_contrast: The bilayer to constrain to. - :param front_head_thickness: Constrain front head thickness. - :param back_head_thickness: Constrain back head thickness. - :param tail_thickness: Constrain tail thickness. - :param front_head_area_per_molecule: Constrain front head area per molecule. - :param back_head_area_per_molecule: Constrain back head area per molecule. - :param tail_area_per_molecule: Constrain tail area per molecule. - :param front_head_fraction: Constrain front head solvent fraction. - :param back_head_fraction: Constrain back head solvent fraction. - :param tail_fraction: Constrain tail solvent fraction. + Parameters + ---------- + another_contrast : Bilayer + The bilayer to constrain to. + front_head_thickness : bool, optional + Constrain front head thickness. By default, True. + back_head_thickness : bool, optional + Constrain back head thickness. By default, True. + tail_thickness : bool, optional + Constrain tail thickness. By default, True. + front_head_area_per_molecule : bool, optional + Constrain front head area per molecule. By default, True. + back_head_area_per_molecule : bool, optional + Constrain back head area per molecule. By default, True. + tail_area_per_molecule : bool, optional + Constrain tail area per molecule. By default, True. + front_head_fraction : bool, optional + Constrain front head solvent fraction. By default, True. + back_head_fraction : bool, optional + Constrain back head solvent fraction. By default, True. + tail_fraction : bool, optional + Constrain tail solvent fraction. By default, True. """ if front_head_thickness: self.front_head_layer.thickness.make_dependent_on( @@ -474,18 +543,17 @@ def _dict_repr(self) -> dict: } } - def as_dict(self, skip: list[str] | None = None) -> dict: - """Produce a cleaned dict using a custom as_dict method. - - The resulting dict matches the parameters in __init__ + def to_dict(self, skip: list[str] | None = None) -> dict: + """Serialize, dropping derived fields. - :param skip: List of keys to skip, defaults to `None`. + The `back_tail_layer` and the underlying `layers` collection are + derived in ``__init__`` from the front head / front tail / back head + constructor arguments, so they are not part of the persisted state. """ - this_dict = super().as_dict(skip=skip) - this_dict['front_head_layer'] = self.front_head_layer.as_dict(skip=skip) - this_dict['front_tail_layer'] = self.front_tail_layer.as_dict(skip=skip) - this_dict['back_head_layer'] = self.back_head_layer.as_dict(skip=skip) - this_dict['constrain_heads'] = self.constrain_heads - this_dict['conformal_roughness'] = self.conformal_roughness - del this_dict['layers'] + this_dict = super().to_dict(skip=skip) + this_dict.pop('layers', None) return this_dict + + def as_dict(self, skip: list[str] | None = None) -> dict: + """Compatibility alias for :meth:`to_dict`.""" + return self.to_dict(skip=skip) diff --git a/src/easyreflectometry/sample/assemblies/gradient_layer.py b/src/easyreflectometry/sample/assemblies/gradient_layer.py index 38771e80..fa38879c 100644 --- a/src/easyreflectometry/sample/assemblies/gradient_layer.py +++ b/src/easyreflectometry/sample/assemblies/gradient_layer.py @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + from typing import Optional from easyscience import global_object @@ -11,8 +14,8 @@ class GradientLayer(BaseAssembly): """A set of discrete gradient layers changing from the front to the back material. - The front layer is where the neutron beam starts in, it has an index of 0. + The front layer is where the neutron beam starts in, it has an index of 0. """ def __init__( @@ -28,26 +31,34 @@ def __init__( ): """Constructor. - :param front_material: Material of front of the layer - :param back_material: Material of back of the layer - :param thickness: Thicknkess of the layer - :param roughness: Roughness of the layer - :param discretisation_elements: Number of discrete layers - :param name: Name for gradient layer, defaults to 'EasyGradienLayer'. - :param interface: Calculator interface, defaults to `None`. + Parameters + ---------- + unique_name : Optional[str], optional + By default, None. + front_material : Optional[Material], optional + Material of front of the layer. By default, None. + back_material : Optional[Material], optional + Material of back of the layer. By default, None. + thickness : Optional[float], optional + Thicknkess of the layer. By default, 2.0. + roughness : Optional[float], optional + Roughness of the layer. By default, 0.2. + discretisation_elements : int, optional + Number of discrete layers. By default, 10. + name : str, optional + Name for gradient layer. By default, 'EasyGradienLayer'. + interface : + Calculator interface. By default, None. """ if front_material is None: front_material = Material(0.0, 0.0, 'Air') - self._front_material = front_material if back_material is None: back_material = Material(6.36, 0.0, 'D2O') - self._back_material = back_material if discretisation_elements < 2: raise ValueError('Discretisation elements must be greater than 2.') - self._discretisation_elements = discretisation_elements gradient_layers = _prepare_gradient_layers( front_material=front_material, @@ -60,9 +71,12 @@ def __init__( layers=gradient_layers, name=name, unique_name=unique_name, - interface=interface, + interface=None, type='Gradient-layer', ) + self._front_material = front_material + self._back_material = back_material + self._discretisation_elements = discretisation_elements self._setup_thickness_constraints() self._enable_thickness_constraints() @@ -73,6 +87,21 @@ def __init__( self.thickness = thickness self.roughness = roughness + if interface is not None: + self.interface = interface + + @property + def front_material(self) -> Material: + return self._front_material + + @property + def back_material(self) -> Material: + return self._back_material + + @property + def discretisation_elements(self) -> int: + return self._discretisation_elements + @property def thickness(self) -> float: """Get the thickness of the gradient layer in Angstrom.""" @@ -82,7 +111,10 @@ def thickness(self) -> float: def thickness(self, thickness: float) -> None: """Set the thickness of the gradient layer. - :param thickness: Thickness of the gradient layer in Angstroms. + Parameters + ---------- + thickness : float + Thickness of the gradient layer in Angstroms. """ self.front_layer.thickness.value = thickness / self._discretisation_elements @@ -95,7 +127,10 @@ def roughness(self) -> float: def roughness(self, roughness: float) -> None: """Set the roughness of the gradient layer. - :param roughness: Roughness of the gradient layer in Angstroms. + Parameters + ---------- + roughness : float + Roughness of the gradient layer in Angstroms. """ self.front_layer.roughness.value = roughness @@ -103,29 +138,44 @@ def roughness(self, roughness: float) -> None: def _dict_repr(self) -> dict[str, str]: """A simplified dict representation.""" return { - 'thickness': float(self.thickness), # Conversion to float is necessary to prevent property reference in dict + 'thickness': float(self.thickness), # Conversion to float is necessary to prevent property reference in dict 'discretisation_elements': int(self._discretisation_elements), # Same as above 'back_layer': self.back_layer._dict_repr, 'front_layer': self.front_layer._dict_repr, } - def as_dict(self, skip: Optional[list[str]] = None) -> dict: - """Produces a cleaned dict using a custom as_dict method to skip necessary things. - The resulting dict matches the parameters in __init__ + def to_dict(self, skip: Optional[list[str]] = None) -> dict: + """Produces a cleaned dict using a custom to_dict method to skip necessary things. + + The resulting dict matches the parameters in __init__: layers are derived + in ``__init__`` from ``front_material``/``back_material``/``discretisation_elements`` + so they are excluded from the serialized representation. - :param skip: List of keys to skip, defaults to `None`. + Parameters + ---------- + skip : Optional[list[str]], optional + List of keys to skip. By default, None. """ - this_dict = super().as_dict(skip=skip) + this_dict = super().to_dict(skip=skip) # Determined in __init__ - del this_dict['layers'] + this_dict.pop('layers', None) + # `thickness` / `roughness` are read-only float views; the serialized + # constructor args are the floats themselves. + this_dict['thickness'] = float(self.thickness) + this_dict['roughness'] = float(self.roughness) return this_dict + def as_dict(self, skip: Optional[list[str]] = None) -> dict: + """Compatibility alias for :meth:`to_dict`.""" + return self.to_dict(skip=skip) + def _linear_gradient( front_value: float, back_value: float, discretisation_elements: int, ) -> list[float]: + """Linear gradient.""" discrete_step = (back_value - front_value) / discretisation_elements if discrete_step != 0: # Both front and back values are included @@ -141,6 +191,7 @@ def _prepare_gradient_layers( discretisation_elements: int, interface=None, ) -> LayerCollection: + """Prepare gradient layers.""" gradient_sld = _linear_gradient( front_value=front_material.sld.value, back_value=back_material.sld.value, diff --git a/src/easyreflectometry/sample/assemblies/multilayer.py b/src/easyreflectometry/sample/assemblies/multilayer.py index 360db10a..db02592d 100644 --- a/src/easyreflectometry/sample/assemblies/multilayer.py +++ b/src/easyreflectometry/sample/assemblies/multilayer.py @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + from __future__ import annotations from typing import Optional @@ -10,6 +13,7 @@ class Multilayer(BaseAssembly): """A multi layer is build from a single or a list of `Layer` or `LayerCollection`. + The multi layer will arrange the layers as slabs, allowing the reflectometry to be determined from them. The front layer is where the neutron beam starts in, it has an index of 0. @@ -30,10 +34,20 @@ def __init__( ): """Constructor. - :param layers: The layers that make up the multi-layer. - :param name: Name for multi layer, defaults to 'EasyMultilayer'. - :param interface: Calculator interface, defaults to `None`. - :param type: Type of the constructed instance, defaults to 'Multi-layer' + Parameters + ---------- + populate_if_none : Optional[bool], optional + By default, True. + unique_name : Optional[str], optional + By default, None. + layers : Union[Layer, list[Layer], LayerCollection, None], optional + The layers that make up the multi-layer. By default, None. + name : str, optional + Name for multi layer. By default, 'EasyMultilayer'. + interface : + Calculator interface. By default, None. + type : str, optional + Type of the constructed instance. By default, 'Multi-layer'. """ if layers is None: if populate_if_none: @@ -48,12 +62,21 @@ def __init__( # Else collisions might occur in global_object.map self.populate_if_none = False - super().__init__(name, unique_name=unique_name, layers=layers, type=type, interface=interface) + super().__init__( + name=name, + type=type, + interface=interface, + layers=layers, + unique_name=unique_name, + ) def add_layer(self, *layers: tuple[Layer]) -> None: """Add a layer to the multi layer. - :param layers: Layers to add to the multi layer. + Parameters + ---------- + *layers : tuple[Layer] + Layers to add to the multi layer. """ for arg in layers: if issubclass(arg.__class__, Layer): @@ -64,8 +87,10 @@ def add_layer(self, *layers: tuple[Layer]) -> None: def duplicate_layer(self, idx: int) -> None: """Duplicate a given layer. - :param idx: index of layer to duplicate. - :type idx: int + Parameters + ---------- + idx : int + Index of layer to duplicate. """ to_duplicate = self.layers[idx] duplicate_layer = Layer( @@ -79,7 +104,10 @@ def duplicate_layer(self, idx: int) -> None: def remove_layer(self, idx: int) -> None: """Remove a layer from the item. - :param idx: index of layer to remove + Parameters + ---------- + idx : int + Index of layer to remove. """ if self.interface is not None: self.interface().remove_layer_from_item(self.layers[idx].unique_name, self.unique_name) @@ -93,11 +121,6 @@ def _dict_repr(self) -> dict: @classmethod def from_dict(cls, data: dict) -> Multilayer: - """ - Create a Multilayer from a dictionary. - - :param data: dictionary of the Multilayer - :return: Multilayer - """ + """Create a Multilayer from a dictionary.""" multilayer = super().from_dict(data) return multilayer diff --git a/src/easyreflectometry/sample/assemblies/repeating_multilayer.py b/src/easyreflectometry/sample/assemblies/repeating_multilayer.py index 7c4ecbcc..79eab4fa 100644 --- a/src/easyreflectometry/sample/assemblies/repeating_multilayer.py +++ b/src/easyreflectometry/sample/assemblies/repeating_multilayer.py @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + from typing import Optional from typing import Union @@ -22,8 +25,7 @@ class RepeatingMultilayer(Multilayer): - """ - A repeating multi layer is build from a `Multilayer` and which it repeats + """A repeating multi layer is build from a `Multilayer` and which it repeats for a given number of times. This enables a computational efficiency in many reflectometry engines as the operation can be performed for a single `Multilayer` and cheaply combined for the appropriate number of @@ -46,10 +48,20 @@ def __init__( ): """Constructor. - :param layers: The layers that make up the multi-layer that will be repeated. - :param repetitions: Number of repetitions of the given series of layers - :param name: Name for the repeating multi layer, defaults to 'EasyRepeatingMultilayer'. - :param interface: Calculator interface, defaults to `None`. + Parameters + ---------- + populate_if_none : bool, optional + By default, True. + unique_name : Optional[str], optional + By default, None. + layers : Union[LayerCollection, Layer, list[Layer], None], optional + The layers that make up the multi-layer that will be repeated. By default, None. + repetitions : Union[Parameter, int, None], optional + Number of repetitions of the given series of layers. By default, None. + name : str, optional + Name for the repeating multi layer. By default, 'EasyRepeatingMultilayer'. + interface : + Calculator interface. By default, None. """ if unique_name is None: unique_name = global_object.generate_unique_name(self.__class__.__name__) @@ -63,9 +75,6 @@ def __init__( layers = LayerCollection(layers, name=layers.name) elif isinstance(layers, list): layers = LayerCollection(*layers, name='/'.join([layer.name for layer in layers])) - # Needed to ensure an empty list is created when saving and instatiating the object as_dict -> from_dict - # Else collisions might occur in global_object.map - self.populate_if_none = False repetitions = get_as_parameter( name='repetitions', @@ -77,11 +86,23 @@ def __init__( super().__init__( layers=layers, name=name, - interface=interface, + unique_name=unique_name, + interface=None, type='Repeating Multi-layer', + populate_if_none=False, ) - self._add_component('repetitions', repetitions) - self.interface = interface + self._repetitions = repetitions + + if interface is not None: + self.interface = interface + + @property + def repetitions(self) -> Parameter: + return self._repetitions + + @repetitions.setter + def repetitions(self, value) -> None: + self._repetitions.value = value # Representation @property diff --git a/src/easyreflectometry/sample/assemblies/surfactant_layer.py b/src/easyreflectometry/sample/assemblies/surfactant_layer.py index 6cbd2c6b..8e81e097 100644 --- a/src/easyreflectometry/sample/assemblies/surfactant_layer.py +++ b/src/easyreflectometry/sample/assemblies/surfactant_layer.py @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + from __future__ import annotations from typing import Optional @@ -37,14 +40,23 @@ def __init__( ): """Constructor. - :param tail_layer: Layer representing the tail part of the surfactant layer. - :param head_layer: Layer representing the head part of the surfactant layer. - :param name: Name for surfactant layer, defaults to 'EasySurfactantLayer'. - :param constrain_area_per_molecule: Constrain the area per molecule, defaults to `False`. - :param conformal_roughness: Constrain the roughness to be the same for both layers, defaults to `False`. - :param interface: Calculator interface, defaults to `None`. + Parameters + ---------- + unique_name : Optional[str], optional + By default, None. + tail_layer : Optional[LayerAreaPerMolecule], optional + Layer representing the tail part of the surfactant layer. By default, None. + head_layer : Optional[LayerAreaPerMolecule], optional + Layer representing the head part of the surfactant layer. By default, None. + name : str, optional + Name for surfactant layer. By default, 'EasySurfactantLayer'. + constrain_area_per_molecule : bool, optional + Constrain the area per molecule. By default, False. + conformal_roughness : bool, optional + Constrain the roughness to be the same for both layers. By default, False. + interface : + Calculator interface. By default, None. """ - # We need to generate a unique name to create the nested objects if unique_name is None: unique_name = global_object.generate_unique_name(self.__class__.__name__) @@ -101,11 +113,13 @@ def __init__( interface=interface, ) - self.interface = interface self.conformal = False + if constrain_area_per_molecule: + self.constrain_area_per_molecule = True if conformal_roughness: self._enable_roughness_constraints() + self.conformal = True @property def tail_layer(self) -> Optional[LayerAreaPerMolecule]: @@ -138,7 +152,10 @@ def constrain_area_per_molecule(self, status: bool): """Set the status for the area per molecule constraint such that the head and tail layers have the same area per molecule. - :param status: Boolean description the wanted of the constraint. + Parameters + ---------- + status : bool + Boolean description the wanted of the constraint. """ if status: independent_param = self.tail_layer._area_per_molecule @@ -158,7 +175,10 @@ def conformal_roughness(self) -> bool: def conformal_roughness(self, status: bool): """Set the status for the roughness to be the same for both layers. - :param status: Boolean description the wanted of the constraint. + Parameters + ---------- + status : bool + Boolean description the wanted of the constraint. """ if status: self._enable_roughness_constraints() @@ -170,7 +190,10 @@ def conformal_roughness(self, status: bool): def constrain_solvent_roughness(self, solvent_roughness: Parameter): """Add the constraint to the solvent roughness. - :param solvent_roughness: The solvent roughness parameter. + Parameters + ---------- + solvent_roughness : Parameter + The solvent roughness parameter. """ if not self.conformal_roughness: raise ValueError('Roughness must be conformal to use this function.') @@ -189,7 +212,22 @@ def constrain_multiple_contrast( ): """Constrain structural parameters between surfactant layer objects. - :param another_contrast: The surfactant layer to constrain + Parameters + ---------- + tail_layer_fraction : bool, optional + By default, True. + head_layer_fraction : bool, optional + By default, True. + tail_layer_area_per_molecule : bool, optional + By default, True. + head_layer_area_per_molecule : bool, optional + By default, True. + tail_layer_thickness : bool, optional + By default, True. + head_layer_thickness : bool, optional + By default, True. + another_contrast : SurfactantLayer + The surfactant layer to constrain. """ if head_layer_thickness: self.head_layer.thickness.make_dependent_on( @@ -239,16 +277,13 @@ def _dict_repr(self) -> dict: } } - def as_dict(self, skip: Optional[list[str]] = None) -> dict: - """Produces a cleaned dict using a custom as_dict method to skip necessary things. - The resulting dict matches the parameters in __init__ - - :param skip: List of keys to skip, defaults to `None`. + def to_dict(self, skip: Optional[list[str]] = None) -> dict: + """Serialize, dropping the derived ``layers`` field (it is rebuilt + from ``tail_layer`` and ``head_layer`` in ``__init__``). """ - this_dict = super().as_dict(skip=skip) - this_dict['tail_layer'] = self.tail_layer.as_dict(skip=skip) - this_dict['head_layer'] = self.head_layer.as_dict(skip=skip) - this_dict['constrain_area_per_molecule'] = self.constrain_area_per_molecule - this_dict['conformal_roughness'] = self.conformal_roughness - del this_dict['layers'] + this_dict = super().to_dict(skip=skip) + this_dict.pop('layers', None) return this_dict + + def as_dict(self, skip: Optional[list[str]] = None) -> dict: + return self.to_dict(skip=skip) diff --git a/src/easyreflectometry/sample/base_core.py b/src/easyreflectometry/sample/base_core.py index b4c3f3ed..611665cd 100644 --- a/src/easyreflectometry/sample/base_core.py +++ b/src/easyreflectometry/sample/base_core.py @@ -1,44 +1,240 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + +from __future__ import annotations + from abc import abstractmethod +from typing import Any +from typing import Optional -from easyscience import ObjBase as BaseObj +from easyscience.base_classes import ModelBase from easyreflectometry.utils import yaml_dump -class BaseCore(BaseObj): +class BaseCore(ModelBase): + """Local base class for sample-tree objects (Material, Layer, assemblies). + + Built on top of `easyscience.base_classes.ModelBase` (the replacement for + the deprecated `ObjBase`). On top of `ModelBase` this class adds: + + - a `name` property + - an `interface` property whose setter propagates the calculator interface + to child objects and (re)generates bindings + - a yaml-formatted `__repr__` driven by an abstract `_dict_repr` + - an `_get_linkable_attributes` compatibility shim used by the calculator's + `InterfaceFactoryTemplate.generate_bindings` + - an `as_dict` alias for `to_dict` + + Subclass `__init__` convention: + 1. Build child Parameters / sub-objects. + 2. Call ``super().__init__(name=..., unique_name=...)``. + 3. Assign children to backing fields (``self._sld = sld`` etc.) or pass + them as ``**kwargs`` to this base class (transitional path; each + kwarg is stored as a plain instance attribute). + 4. Last: ``self.interface = interface`` (triggers ``generate_bindings``). + """ + def __init__( self, name: str, - interface, - **kwargs, + interface: Any = None, + unique_name: Optional[str] = None, + display_name: Optional[str] = None, + **kwargs: Any, ): - super().__init__(name=name, **kwargs) + super().__init__(unique_name=unique_name, display_name=display_name) + self._name = name + self._interface = None + # `user_data` is part of the legacy `BasedBase` API — a free-form dict + # callers stash arbitrary metadata in. Kept for back-compat with code + # like `Project.replace_models_from_orso` which stores the ORSO sample + # name on the model. + self.user_data: dict = {} - # Updates interface using property in base object - self.interface = interface + # Transitional path: subclasses still pass parameter / child objects via + # **kwargs (legacy `ObjBase` accepted them and stashed in `_kwargs`). + # Here we simply store each one as a plain instance attribute so + # `obj.` keeps working. Step 2 of the migration replaces this with + # explicit assignments in each subclass. + for key, value in kwargs.items(): + setattr(self, key, value) - @abstractmethod - def _dict_repr(self) -> dict[str, str]: ... + # Assign interface LAST so children exist when generate_bindings runs. + if interface is not None: + self.interface = interface - def __repr__(self) -> str: + # ----- name ----- + + @property + def name(self) -> str: + """Common (display-friendly) name.""" + return self._name + + @name.setter + def name(self, value: str) -> None: + self._name = value + + # ----- interface ----- + + @property + def interface(self) -> Any: + """The calculator interface attached to this object (may be None).""" + return self._interface + + @interface.setter + def interface(self, new_interface: Any) -> None: + self._interface = new_interface + if new_interface is not None: + self.generate_bindings() + + def generate_bindings(self) -> None: + """Propagate the interface to child objects, then bind via the calculator. + + We propagate to any child whose class advertises an ``interface`` property + with a setter. That includes both the new `BaseCore`-based children and + legacy `BasedBase`-derived collections (which extend `SerializerComponent`, + not `NewBase`). """ - String representation of the layer. + if self._interface is None: + raise AttributeError('Interface error for generating bindings. `interface` has to be set.') + for attr in self._iter_public_children(): + if self._has_interface_setter(type(attr)): + attr.interface = self._interface + self._interface.generate_bindings(self) + + def _iter_public_children(self): + """Yield public child objects from both class-level (dir) and instance-level (__dict__) attrs. - :return: a string representation of the layer - :rtype: str + `NewBase.__dir__` exposes only class attributes, which means plain instance + attributes (the transitional `setattr(self, key, value)` path in + `__init__`) are invisible to a pure `dir()` scan. To bridge both worlds — + legacy subclasses that still use plain attrs, and migrated subclasses that + expose children via `@property` accessors — this helper unions the two. + Once all subclasses migrate to `@property`-backed children with `_field` + backing storage, the `__dict__` branch becomes a no-op (private names are + skipped). """ - return yaml_dump(self._dict_repr) + seen_ids = {id(self)} + # Class-level (properties, methods named like sld/isld/material). + for attr_name in dir(self): + if attr_name.startswith('_') or attr_name in ('interface', 'name'): + continue + try: + attr = getattr(self, attr_name, None) + except AttributeError: + # A subclass @property may legitimately raise AttributeError + # mid-construction (the `_field` backing isn't set yet); skip + # those entries silently. Other exceptions should propagate. + continue + if attr is None or id(attr) in seen_ids: + continue + seen_ids.add(id(attr)) + yield attr + # Instance-level (plain attrs set via the transitional kwargs path). + for attr_name, attr in list(self.__dict__.items()): + if attr_name.startswith('_') or attr_name in ('interface', 'name'): + continue + if attr is None or id(attr) in seen_ids: + continue + seen_ids.add(id(attr)) + yield attr + + @staticmethod + def _has_interface_setter(obj_type: type) -> bool: + for klass in obj_type.__mro__: + prop = klass.__dict__.get('interface') + if isinstance(prop, property): + return prop.fset is not None + return False + + # ----- compatibility shims ----- - # For classes with special serialization needs one must adopt the dict produced by super - # def as_dict(self, skip: list = None) -> dict: - # """Should produce a cleaned dict that matches the parameters in __init__ - # - # :param skip: List of keys to skip, defaults to `None`. - # """ - # if skip is None: - # skip = [] - # this_dict = super().as_dict(skip=skip) - # ... - # Correct the dict here - # ... - # return this_dict + def _get_linkable_attributes(self): + """Used by `easyscience.fitting.calculators.interface_factory.generate_bindings`. + + Returns the same set as :meth:`get_all_variables` (the modern API on + :class:`ModelBase`). Kept under the legacy name because the calculator + in `easyscience` core has not yet been updated. + """ + return self.get_all_variables() + + def get_parameters(self): + """Compatibility shim for legacy callers; prefer :meth:`get_all_parameters`.""" + return self.get_all_parameters() + + def _add_component(self, key: str, component: Any) -> None: + """Compatibility shim for legacy `ObjBase._add_component`. + + Legacy callers (e.g. `LayerAreaPerMolecule`) used this to register an + additional child after `super().__init__`. In the new world the + equivalent is simply setting an attribute; we do that here so the + existing call sites keep working until Step 2 removes them. + """ + setattr(self, key, component) + + def get_all_variables(self): + """Discover Parameters/Descriptors across both class-level and instance-level attrs. + + `ModelBase.get_all_variables` walks `dir(self)`, which `NewBase` restricts + to class attributes only. During the transition some subclasses still + store child Parameters as plain instance attributes (see the kwargs path + in :meth:`__init__`); those are invisible to `dir()`. We therefore also + scan `self.__dict__` for `DescriptorBase` instances and for child + ModelBase objects whose own `get_all_variables` we recurse into. + """ + from easyscience.variable.descriptor_base import DescriptorBase + + out: list = [] + seen_param_ids: set[int] = set() + for attr in self._iter_public_children(): + if isinstance(attr, DescriptorBase): + if id(attr) not in seen_param_ids: + seen_param_ids.add(id(attr)) + out.append(attr) + elif hasattr(attr, 'get_all_variables'): + for v in attr.get_all_variables(): + if id(v) not in seen_param_ids: + seen_param_ids.add(id(v)) + out.append(v) + return out + + def to_dict(self, skip: Optional[list[str]] = None) -> dict[str, Any]: + """Serialize, skipping the calculator interface and unique_name by default. + + The calculator (`CalculatorFactory`) is not serializable and is not part + of the model's persistent state — round-trip code that needs it back + reattaches it after `from_dict`. The legacy `ObjBase`-based pipeline + achieved the same by never including `interface` in its `_kwargs` + encoding; we replicate that here. + + `unique_name` is also stripped by default, matching the legacy + `BasedBase.as_dict` contract. The installed `SerializerBase` does *not* + propagate per-object `_default_unique_name` to nested children — if + we leave it in, child Parameters end up with explicit unique_names in + the dict (e.g. `Parameter_0`) that subsequently collide on reload when + the global counter restarts from 0. + + Pass a *copy* of `skip` to super since `NewBase.to_dict` mutates the + list (appends `unique_name` / `display_name` if those are default). + """ + skip = list(skip or []) + if 'interface' not in skip: + skip.append('interface') + if 'unique_name' not in skip: + skip.append('unique_name') + return super().to_dict(skip=list(skip)) + + def as_dict(self, skip: Optional[list[str]] = None) -> dict[str, Any]: + """Compatibility alias for :meth:`to_dict`.""" + return self.to_dict(skip=skip) + + # ----- repr ----- + + @property + @abstractmethod + def _dict_repr(self) -> dict[str, Any]: ... + + def __repr__(self) -> str: + """Yaml-formatted multi-line string built from :attr:`_dict_repr`.""" + return yaml_dump(self._dict_repr) diff --git a/src/easyreflectometry/sample/collections/__init__.py b/src/easyreflectometry/sample/collections/__init__.py new file mode 100644 index 00000000..4e798e20 --- /dev/null +++ b/src/easyreflectometry/sample/collections/__init__.py @@ -0,0 +1,2 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause diff --git a/src/easyreflectometry/sample/collections/base_collection.py b/src/easyreflectometry/sample/collections/base_collection.py index 53d16b51..59f7038e 100644 --- a/src/easyreflectometry/sample/collections/base_collection.py +++ b/src/easyreflectometry/sample/collections/base_collection.py @@ -1,95 +1,331 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + +from __future__ import annotations + +from typing import Any from typing import List from typing import Optional -from easyscience import global_object -from easyscience.base_classes import CollectionBase as EasyBaseCollection +from easyscience.base_classes import EasyList +from easyscience.base_classes.new_base import NewBase +from easyscience.variable import Parameter from easyreflectometry.utils import yaml_dump -class BaseCollection(EasyBaseCollection): +class BaseCollection(EasyList): + """Local base for sample-tree collections (Material/Layer/Assembly/Model collections). + + Built on top of `easyscience.base_classes.EasyList` (the replacement for + the deprecated `CollectionBase`). On top of `EasyList` this class adds: + + - a `name` property + - an `interface` property whose setter propagates the calculator interface + to every contained item + - propagation of the current `interface` to newly inserted items + - a `populate_if_none` flag preserved for serialization round-trip + - convenience helpers `names`, `move_up`, `move_down`, `remove_at` + - a yaml-formatted `__repr__` driven by `_dict_repr` + - an `as_dict` alias for `to_dict` with `skip=` support and `interface` + excluded by default + + Subclasses (`LayerCollection`, `MaterialCollection`, `Sample`, + `ModelCollection`) keep their existing constructor shape — they pass + `name` and `interface` positionally to this class, items as `*args`, and + additional configuration as kwargs. + """ + def __init__( self, name: str, - interface, - *args, + interface: Any = None, + *args: Any, unique_name: Optional[str] = None, - **kwargs, + populate_if_none: bool = False, + **kwargs: Any, ): - if unique_name is None: - unique_name = global_object.generate_unique_name(self.__class__.__name__) + # `_interface` must exist before `super().__init__` because `EasyList` + # calls `self.append(item)` for each positional arg, which routes + # through our `insert` override and reads `self._interface`. + self._interface = None + self._name = name + # Legacy `CollectionBase` accepted items either positionally or as a + # list-valued keyword (e.g. ``LayerCollection(layers=[a, b])``). Pull + # any list-valued kwarg into the positional stream so callers using + # that older pattern keep working. + extra_items = [] + for key in list(kwargs.keys()): + if isinstance(kwargs[key], list) and kwargs[key] and key != 'data': + extra_items.extend(kwargs.pop(key)) + if extra_items: + args = tuple(args) + tuple(extra_items) + super().__init__(*args, unique_name=unique_name, **kwargs) + # `populate_if_none` is a control flag, not state. It is serialized so + # `from_dict` knows whether the original construction filled in + # defaults; the value should be `False` once the data is restored. + self.populate_if_none = populate_if_none - super().__init__(name, unique_name=unique_name, *args, **kwargs) - self.interface = interface + # Assign interface LAST — propagates to all contained items. + if interface is not None: + self.interface = interface - # Needed to ensure an empty list is created when saving and instatiating the object as_dict -> from_dict - # Else collisions might occur in global_object.map - self.populate_if_none = False + # ----- name ----- - def __repr__(self) -> str: + @property + def name(self) -> str: + return self._name + + @name.setter + def name(self, value: str) -> None: + self._name = value + + # ----- interface ----- + + @property + def interface(self) -> Any: + return self._interface + + @interface.setter + def interface(self, new_interface: Any) -> None: + self._interface = new_interface + if new_interface is None: + return + # Propagate to existing items. + for item in self._data: + if self._has_interface_setter(type(item)): + item.interface = new_interface + # Tell the calculator to bind to self (matches the legacy CollectionBase + # behavior which called `interface.generate_bindings(self)` once per + # collection). + if hasattr(new_interface, 'generate_bindings'): + new_interface.generate_bindings(self) + + def _get_key(self, obj): + """Use the item's `name` for string-indexed lookups. + + Matches the legacy `CollectionBase.__getitem__` behaviour which + searched by `item.name`. `EasyList` defaults to `unique_name`; the + existing `Project` code (and callers) look items up by their pretty + name (e.g. `materials['Air']`). """ - String representation of the collection. + return getattr(obj, 'name', None) or obj.unique_name + + @staticmethod + def _has_interface_setter(obj_type: type) -> bool: + for klass in obj_type.__mro__: + prop = klass.__dict__.get('interface') + if isinstance(prop, property): + return prop.fset is not None + return False + + # ----- mutable-sequence overrides that propagate interface ----- - :return: a string representation of the collection + def insert(self, index: int, value: Any) -> None: + """Insert and (if an interface is set) propagate it to the new item. + + Legacy `CollectionBase.insert` did `value.interface = self.interface` + after registering the item; we replicate the same behaviour here so + downstream calculator state stays in sync when items are appended + after the collection's interface was already set. + + The type check from `EasyList.insert` is bypassed: each subclass + accepts a single item type (Layer / Material / BaseAssembly / Model) + and enforces it elsewhere, while the EasyList check would require + every item to be a `NewBase` subclass — which was historically not + guaranteed and forces an extra coupling we don't need. """ - return yaml_dump(self._dict_repr) + if not isinstance(index, int): + raise TypeError('Index must be an integer') + # Skip the EasyList protected-types check; mimic the rest of its insert + # (duplicate-name warning + append-to-_data). + import warnings as _warnings + + if value in self: + _warnings.warn(f'Item with unique name "{self._get_key(value)}" already in collection, it will be ignored') + return + self._data.insert(index, value) + if self._interface is not None and self._has_interface_setter(type(value)): + value.interface = self._interface + + # ----- helpers ----- @property def names(self) -> list: - """ - :returns: list of names for the elements in the collection. - """ - return [i.name for i in self] + """List of item names.""" + return [getattr(item, 'name', None) for item in self._data] - def move_up(self, index: int): - """Move the element at the given index up in the collection. + @property + def data(self) -> list: + """Read-only view of the underlying item list. - :param index: Index of the element to move up. + Provided for compatibility with code (and tests) written against the + legacy `CollectionBase` shape, which exposed items via `.data`. """ + return list(self._data) + + def move_up(self, index: int) -> None: + """Move the element at the given index up in the collection.""" if index == 0: return self.insert(index - 1, self.pop(index)) - def move_down(self, index: int): - """Move the element at the given index down in the collection. - - :param index: Index of the element to move down. - """ + def move_down(self, index: int) -> None: + """Move the element at the given index down in the collection.""" if index == len(self) - 1: return self.insert(index + 1, self.pop(index)) - def remove(self, index: int): - """ - Remove an element from the elements. + def remove_at(self, index: int) -> None: + """Remove the item at *index* from the collection. - :param index: Index of the element to remove + Renamed from the legacy `BaseCollection.remove(index)` which shadowed + `MutableSequence.remove(value)` (remove-by-value, inherited from + `EasyList`). Callers that meant "remove by index" should use this; the + standard `remove(value)` is still available with its usual semantics. """ self.pop(index) + # ----- compatibility shims (kept until call sites migrate) ----- + + def get_parameters(self) -> List[Parameter]: + """Compatibility alias for legacy callers; prefer `get_all_parameters`.""" + return self.get_all_parameters() + + def get_all_variables(self) -> List: + """Flat list of every Parameter/Descriptor across all items. + + Walks each item in the collection and unions whatever each item + exposes via its own `get_all_variables` (for `ModelBase` / + `BaseCore` children) or, for objects that lack that hook, + unions any direct `DescriptorBase` attributes. + """ + from easyscience.variable.descriptor_base import DescriptorBase + + out: list = [] + seen: set[int] = set() + for item in self._data: + if hasattr(item, 'get_all_variables'): + for v in item.get_all_variables(): + if id(v) not in seen: + seen.add(id(v)) + out.append(v) + elif isinstance(item, DescriptorBase): + if id(item) not in seen: + seen.add(id(item)) + out.append(item) + return out + + def get_all_parameters(self) -> List[Parameter]: + return [v for v in self.get_all_variables() if isinstance(v, Parameter)] + + def get_free_parameters(self) -> List[Parameter]: + return [p for p in self.get_all_parameters() if p.independent and not p.fixed] + + def get_fit_parameters(self) -> List[Parameter]: + """Alias kept for the minimizer; matches `ModelBase.get_fit_parameters`.""" + return self.get_free_parameters() + + def _get_linkable_attributes(self) -> List[Parameter]: + """Bridge for `easyscience.fitting.calculators.interface_factory.generate_bindings`.""" + return self.get_all_variables() + + # ----- repr ----- + @property def _dict_repr(self) -> dict: - """ - A simplified dict representation. + """A simplified dict representation.""" + return {self.name: [getattr(i, '_dict_repr', repr(i)) for i in self._data]} - :return: Simple dictionary - """ - return {self.name: [i._dict_repr for i in self]} + def __repr__(self) -> str: + try: + return yaml_dump(self._dict_repr) + except Exception: + return super().__repr__() - def as_dict(self, skip: Optional[List[str]] = None) -> dict: - """ - Create a dictionary representation of the collection. + # ----- serialization ----- - :return: A dictionary representation of the collection + def _convert_to_dict(self, d: dict, encoder=None, skip: Optional[List[str]] = None, **kwargs) -> dict: + """Serializer hook used when this collection is encoded as a *child* + attribute (e.g. `Multilayer.layers`). + + `SerializerBase._convert_to_dict` iterates `_arg_spec` to populate the + base dict and then calls `obj._convert_to_dict(d, ...)` if defined. + Because `data` is supplied via `*args` (VAR_POSITIONAL — not part of + `_arg_spec`), without this hook the nested encoding would miss the + items entirely and round-trip would reconstruct an empty collection. """ if skip is None: skip = [] - this_dict = super().as_dict(skip=skip) - this_dict['data'] = [] - for collection_element in self: - this_dict['data'].append(collection_element.as_dict(skip=skip)) - this_dict['populate_if_none'] = self.populate_if_none - return this_dict + if self._protected_types != [NewBase] and 'protected_types' not in d: + d['protected_types'] = [{'@module': c.__module__, '@class': c.__name__} for c in self._protected_types] + # Encode each item. Defer to the encoder's recursive walk so nested + # ModelBase / NewBase items get properly serialized. + item_skip = list(skip) + items: list = [] + for item in self._data: + if encoder is not None and hasattr(encoder, '_recursive_encoder'): + items.append(encoder._recursive_encoder(item, skip=item_skip, encoder=encoder, full_encode=False)) + elif hasattr(item, 'to_dict'): + try: + items.append(item.to_dict(skip=list(item_skip))) + except TypeError: + items.append(item.to_dict()) + else: + items.append(item) + d['data'] = items + return d + + def to_dict(self, skip: Optional[List[str]] = None) -> dict: + """Serialize with `skip` support; `interface` excluded by default. + + `EasyList.to_dict` doesn't accept a `skip` argument and is hard-wired + to dump `data` plus the parent's `_arg_spec` view. We reimplement here + so existing callers (`Project`, `Model.as_dict`, etc.) can keep + passing `skip=['unique_name']` or similar. + + ``NewBase.to_dict`` mutates the ``skip`` list in-place (e.g. it + appends ``'unique_name'`` when the collection's own unique_name is + default-generated). We therefore pass a *copy* to it, otherwise the + mutation would leak into the per-item serialization below and force + every item Parameter dict to drop its ``unique_name`` — breaking the + from_dict round-trip. + """ + skip = list(skip or []) + if 'interface' not in skip: + skip.append('interface') + # Matches legacy `BasedBase.as_dict`: drop unique_name from the + # serialized form so nested Parameters don't get explicit names that + # would collide with the auto-generated names produced when the global + # counter restarts during reconstruction. + if 'unique_name' not in skip: + skip.append('unique_name') + dict_repr = NewBase.to_dict(self, skip=list(skip)) + if self._protected_types != [NewBase]: + dict_repr['protected_types'] = [{'@module': c.__module__, '@class': c.__name__} for c in self._protected_types] + dict_repr['data'] = [] + for item in self._data: + # Items that are ModelBase / BaseCore subclasses accept `skip`; + # other shapes use their no-arg `to_dict`/`as_dict`. + if hasattr(item, 'to_dict'): + try: + dict_repr['data'].append(item.to_dict(skip=list(skip))) + except TypeError: + dict_repr['data'].append(item.to_dict()) + else: + dict_repr['data'].append(item.as_dict(skip=list(skip))) + return dict_repr + + def as_dict(self, skip: Optional[List[str]] = None) -> dict: + """Compatibility alias for :meth:`to_dict`.""" + return self.to_dict(skip=skip) def __deepcopy__(self, memo): + """Round-trip via dict-skip-unique to get a fresh copy. + + `NewBase.__copy__` already does this; the override is kept (rather + than deleted) to mirror the legacy `BaseCollection.__deepcopy__` + semantics — callers that relied on `copy.deepcopy(collection)` still + get a clone built from `from_dict(as_dict(skip=['unique_name']))`. + """ return self.from_dict(self.as_dict(skip=['unique_name'])) diff --git a/src/easyreflectometry/sample/collections/layer_collection.py b/src/easyreflectometry/sample/collections/layer_collection.py index 0761f861..a3ae0f19 100644 --- a/src/easyreflectometry/sample/collections/layer_collection.py +++ b/src/easyreflectometry/sample/collections/layer_collection.py @@ -1,4 +1,6 @@ -__author__ = 'github.com/arm61' +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + from typing import Optional @@ -13,18 +15,29 @@ def __init__( name: str = 'EasyLayerCollection', interface=None, unique_name: Optional[str] = None, - populate_if_none: bool = True, # Needed to match as_dict signature from BaseCollection + populate_if_none: bool = True, **kwargs, ): + """Init function.""" if not layers: layers = [] - super().__init__(name, interface, unique_name=unique_name, *layers, **kwargs) + super().__init__( + name, + interface, + *layers, + unique_name=unique_name, + populate_if_none=populate_if_none, + **kwargs, + ) def add_layer(self, layer: Optional[Layer] = None): """Add a layer to the collection. - :param layer: Layer to add. + Parameters + ---------- + layer : Optional[Layer], optional + Layer to add. By default, None. """ if layer is None: layer = Layer( @@ -36,7 +49,11 @@ def add_layer(self, layer: Optional[Layer] = None): def duplicate_layer(self, index: int): """Duplicate a layer in the collection. - :param layer: Assembly to add. + Parameters + ---------- + index : int + layer : + Assembly to add. """ to_be_duplicated = self[index] duplicate = Layer.from_dict(to_be_duplicated.as_dict(skip=['unique_name'])) diff --git a/src/easyreflectometry/sample/collections/material_collection.py b/src/easyreflectometry/sample/collections/material_collection.py index a97f89d9..726678b2 100644 --- a/src/easyreflectometry/sample/collections/material_collection.py +++ b/src/easyreflectometry/sample/collections/material_collection.py @@ -1,4 +1,7 @@ -__author__ = 'github.com/arm61' +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + + from typing import Optional from typing import Tuple @@ -8,6 +11,7 @@ # Needs to be a function, elements are added to the global_object.map def DEFAULT_ELEMENTS(interface): + """Default elements.""" return ( Material(sld=0.0, isld=0.0, name='Air', interface=interface), Material(sld=6.335, isld=0.0, name='D2O', interface=interface), @@ -25,7 +29,8 @@ def __init__( populate_if_none: bool = True, **kwargs, ): - if not materials: # Empty tuple if no materials are provided + """Init function.""" + if not materials: if populate_if_none: materials = DEFAULT_ELEMENTS(interface) else: @@ -34,15 +39,19 @@ def __init__( super().__init__( name, interface, - unique_name=unique_name, *materials, + unique_name=unique_name, + populate_if_none=False, **kwargs, ) def add_material(self, material: Optional[Material] = None): """Add a material to the collection. - :param material: Material to add. + Parameters + ---------- + material : Optional[Material], optional + Material to add. By default, None. """ if material is None: material = Material(sld=0.0, isld=0.0, name='Material added') @@ -52,7 +61,11 @@ def add_material(self, material: Optional[Material] = None): def duplicate_material(self, index: int): """Duplicate a material in the collection. - :param material: Assembly to add. + Parameters + ---------- + index : int + material : + Assembly to add. """ to_be_duplicated = self[index] duplicate = Material.from_dict(to_be_duplicated.as_dict(skip=['unique_name'])) diff --git a/src/easyreflectometry/sample/collections/sample.py b/src/easyreflectometry/sample/collections/sample.py index 65c2a76b..384626e8 100644 --- a/src/easyreflectometry/sample/collections/sample.py +++ b/src/easyreflectometry/sample/collections/sample.py @@ -1,6 +1,7 @@ -from __future__ import annotations +# SPDX-FileCopyrightText: 2024 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause -__author__ = 'github.com/arm61' +from __future__ import annotations from typing import List from typing import Optional @@ -15,7 +16,7 @@ # Needs to be a function, elements are added to the global_object.map def DEFAULT_ELEMENTS(interface): - """:meta private:""" + """:meta private:.""" return ( Multilayer(interface=interface), Multilayer(interface=interface), @@ -36,10 +37,26 @@ def __init__( ): """Constructor. - :param args: The assemblies in the sample. - :param name: Name of the sample, defaults to 'EasySample'. - :param interface: Calculator interface, defaults to `None`. + Parameters + ---------- + **kwargs : + populate_if_none : bool, optional + By default, True. + unique_name : Optional[str], optional + By default, None. + *assemblies : Optional[List[BaseAssembly]] + args : + The assemblies in the sample. + name : str, optional + Name of the sample. By default, 'EasySample'. + interface : + Calculator interface. By default, None. """ + # `from_dict` (via `EasyList.from_dict`) passes the items as a single + # list-positional arg; unpack that so validation and super() agree. + if len(assemblies) == 1 and isinstance(assemblies[0], list): + assemblies = tuple(assemblies[0]) + if not assemblies: if populate_if_none: assemblies = DEFAULT_ELEMENTS(interface) @@ -49,12 +66,22 @@ def __init__( for assembly in assemblies: if not issubclass(type(assembly), BaseAssembly): raise ValueError('The elements must be an Assembly.') - super().__init__(name, interface, unique_name=unique_name, *assemblies, **kwargs) + super().__init__( + name, + interface, + *assemblies, + unique_name=unique_name, + populate_if_none=populate_if_none, + **kwargs, + ) def add_assembly(self, assembly: Optional[BaseAssembly] = None): """Add an assembly to the sample. - :param assembly: Assembly to add. + Parameters + ---------- + assembly : Optional[BaseAssembly], optional + Assembly to add. By default, None. """ if assembly is None: assembly = Multilayer( @@ -66,36 +93,55 @@ def add_assembly(self, assembly: Optional[BaseAssembly] = None): def duplicate_assembly(self, index: int): """Add an assembly to the sample. - :param assembly: Assembly to add. + Parameters + ---------- + index : int + assembly : + Assembly to add. """ + # Order matters: RepeatingMultilayer and SurfactantLayer are subclasses of + # BaseAssembly but not Multilayer; however a RepeatingMultilayer IS a + # Multilayer, so the most-specific check must come first to avoid + # serialising it through the wrong `from_dict`. to_be_duplicated = self[index] - if isinstance(to_be_duplicated, Multilayer): - duplicate = Multilayer.from_dict(to_be_duplicated.as_dict(skip=['unique_name'])) - elif isinstance(to_be_duplicated, RepeatingMultilayer): + if isinstance(to_be_duplicated, RepeatingMultilayer): duplicate = RepeatingMultilayer.from_dict(to_be_duplicated.as_dict(skip=['unique_name'])) elif isinstance(to_be_duplicated, SurfactantLayer): duplicate = SurfactantLayer.from_dict(to_be_duplicated.as_dict(skip=['unique_name'])) + elif isinstance(to_be_duplicated, Multilayer): + duplicate = Multilayer.from_dict(to_be_duplicated.as_dict(skip=['unique_name'])) + else: + raise TypeError(f'Cannot duplicate assembly of type {type(to_be_duplicated).__name__}') duplicate.name = duplicate.name + ' duplicate' self.append(duplicate) def move_up(self, index: int): """Move the assembly at the given index up in the sample. - :param index: Index of the assembly to move up. + Parameters + ---------- + index : int + Index of the assembly to move up. """ super().move_up(index) def move_down(self, index: int): """Move the assembly at the given index down in the sample. - :param index: Index of the assembly to move down. + Parameters + ---------- + index : int + Index of the assembly to move down. """ super().move_down(index) def remove_assembly(self, index: int): """Remove the assembly at the given index from the sample. - :param index: Index of the assembly to remove. + Parameters + ---------- + index : int + Index of the assembly to remove. """ self.pop(index) @@ -112,14 +158,3 @@ def subphase(self) -> Layer: return self[-1].front_layer else: return self[-1].back_layer - - # Representation - def as_dict(self, skip: Optional[List[str]] = None) -> dict: - """Produces a cleaned dict using a custom as_dict method to skip necessary things. - The resulting dict matches the parameters in __init__ - - :param skip: List of keys to skip, defaults to `None`. - """ - this_dict = super().as_dict(skip=skip) - this_dict['populate_if_none'] = self.populate_if_none - return this_dict diff --git a/src/easyreflectometry/sample/elements/__init__.py b/src/easyreflectometry/sample/elements/__init__.py new file mode 100644 index 00000000..4e798e20 --- /dev/null +++ b/src/easyreflectometry/sample/elements/__init__.py @@ -0,0 +1,2 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause diff --git a/src/easyreflectometry/sample/elements/layers/__init__.py b/src/easyreflectometry/sample/elements/layers/__init__.py new file mode 100644 index 00000000..4e798e20 --- /dev/null +++ b/src/easyreflectometry/sample/elements/layers/__init__.py @@ -0,0 +1,2 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause diff --git a/src/easyreflectometry/sample/elements/layers/layer.py b/src/easyreflectometry/sample/elements/layers/layer.py index 513b18f9..7eea9872 100644 --- a/src/easyreflectometry/sample/elements/layers/layer.py +++ b/src/easyreflectometry/sample/elements/layers/layer.py @@ -1,4 +1,7 @@ -__author__ = 'github.com/arm61' +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + + from typing import Optional from typing import Union @@ -34,14 +37,6 @@ class Layer(BaseCore): - # Added in super().__init__ - #: Material that makes up the layer. - material: Material - #: Thickness of the layer in Angstrom. - thickness: Parameter - #: Roughness of the layer in Angstrom. - roughness: Parameter - def __init__( self, material: Union[Material, None] = None, @@ -53,11 +48,20 @@ def __init__( ): """Constructor. - :param material: The material for the layer. - :param thickness: Layer thickness in Angstrom. - :param roughness: Upper roughness on the layer in Angstrom. - :param name: Name of the layer, defaults to 'EasyLayer' - :param interface: Interface object, defaults to `None` + Parameters + ---------- + unique_name : Optional[str], optional + By default, None. + material : Union[Material, None], optional + The material for the layer. By default, None. + thickness : Union[Parameter, float, None], optional + Layer thickness in Angstrom. By default, None. + roughness : Union[Parameter, float, None], optional + Upper roughness on the layer in Angstrom. By default, None. + name : str, optional + Name of the layer. By default, 'EasyLayer'. + interface : + Interface object. By default, None. """ if material is None: material = Material(interface=interface) @@ -83,21 +87,47 @@ def __init__( ) roughness.default_limits_pending = not isinstance(roughness_value, Parameter) - super().__init__( - name=name, - interface=interface, - material=material, - thickness=thickness, - roughness=roughness, - unique_name=unique_name, - ) + super().__init__(name=name, unique_name=unique_name) + self._material = material + self._thickness = thickness + self._roughness = roughness + + if interface is not None: + self.interface = interface + + @property + def material(self) -> Material: + return self._material + + @material.setter + def material(self, value: Material) -> None: + self._material = value + + @property + def thickness(self) -> Parameter: + return self._thickness + + @thickness.setter + def thickness(self, value: float) -> None: + self._thickness.value = value + + @property + def roughness(self) -> Parameter: + return self._roughness + + @roughness.setter + def roughness(self, value: float) -> None: + self._roughness.value = value def assign_material(self, material: Material) -> None: """Assign a material to the layer interface. - :param material: The material to assign to the layer. + Parameters + ---------- + material : Material + The material to assign to the layer. """ - self.material = material + self._material = material if self.interface is not None: self.interface().assign_material_to_layer(self.material.unique_name, self.unique_name) diff --git a/src/easyreflectometry/sample/elements/layers/layer_area_per_molecule.py b/src/easyreflectometry/sample/elements/layers/layer_area_per_molecule.py index dbda1473..32161dcc 100644 --- a/src/easyreflectometry/sample/elements/layers/layer_area_per_molecule.py +++ b/src/easyreflectometry/sample/elements/layers/layer_area_per_molecule.py @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + from typing import Optional from typing import Union @@ -50,20 +53,8 @@ class LayerAreaPerMolecule(Layer): """The `LayerAreaPerMolecule` class allows a layer to be defined in terms of some molecular formula an area per molecule, and a solvent. - """ - # Added in __init__ - #: Real part of the scattering length. - _scattering_length_real: Parameter - #: Imaginary part of the scattering length. - _scattering_length_imag: Parameter - #: Area per molecule in the layer in Anstrom^2. - _area_per_molecule: Parameter - - # Other typer than in __init__.super() - material: MaterialSolvated - def __init__( self, molecular_formula: Union[str, None] = None, @@ -78,14 +69,26 @@ def __init__( ): """Constructor. - :param molecular_formula: Formula for the molecule in the layer. - :param thickness: Layer thickness in Angstrom. - :param solvent: Solvent containing the molecule. - :param solvent_fraction: Fraction of solvent in layer. Fx solvation or surface coverage. - :param area_per_molecule: Area per molecule in the layer - :param roughness: Upper roughness on the layer in Angstrom. - :param name: Name of the layer, defaults to "EasyLayerAreaPerMolecule" - :param interface: Interface object, defaults to `None` + Parameters + ---------- + unique_name : Optional[str], optional + By default, None. + molecular_formula : Union[str, None], optional + Formula for the molecule in the layer. By default, None. + thickness : Union[Parameter, float, None], optional + Layer thickness in Angstrom. By default, None. + solvent : Union[Material, None], optional + Solvent containing the molecule. By default, None. + solvent_fraction : Union[Parameter, float, None], optional + Fraction of solvent in layer. Fx solvation or surface coverage. By default, None. + area_per_molecule : Union[Parameter, float, None], optional + Area per molecule in the layer. By default, None. + roughness : Union[Parameter, float, None], optional + Upper roughness on the layer in Angstrom. By default, None. + name : str, optional + Name of the layer. By default, 'EasyLayerAreaPerMolecule'. + interface : + Interface object. By default, None. """ if unique_name is None: unique_name = global_object.generate_unique_name(self.__class__.__name__) @@ -99,7 +102,6 @@ def __init__( interface=interface, ) - # Create the solvated molecule and corresponding constraints if molecular_formula is None: molecular_formula = DEFAULTS['molecular_formula'] molecule_material = Material( @@ -116,40 +118,37 @@ def __init__( default_dict=DEFAULTS, unique_name_prefix=f'{unique_name}_Thickness', ) - _area_per_molecule = get_as_parameter( + area_per_molecule_param = get_as_parameter( name='area_per_molecule', value=area_per_molecule, default_dict=DEFAULTS, unique_name_prefix=f'{unique_name}_AreaPerMolecule', ) - _scattering_length_real = get_as_parameter( + scattering_length_real = get_as_parameter( name='scattering_length_real', value=0.0, default_dict=DEFAULTS['sl'], unique_name_prefix=f'{unique_name}_Sl', ) - _scattering_length_imag = get_as_parameter( + scattering_length_imag = get_as_parameter( name='scattering_length_imag', value=0.0, default_dict=DEFAULTS['isl'], unique_name_prefix=f'{unique_name}_Isl', ) - # Constrain the real part of the sld value for the molecule - dependency_expression = 'scattering_length / (thickness * area_per_molecule) * 1e6' - dependency_map = { - 'scattering_length': _scattering_length_real, - 'thickness': thickness, - 'area_per_molecule': _area_per_molecule, - } - molecule_material.sld.make_dependent_on(dependency_expression=dependency_expression, dependency_map=dependency_map) - - # # Constrain the real part of the sld value for the molecule - dependency_expression = 'a / (b*p) * 1e6' - dependency_map = {'a': _scattering_length_real, 'b': thickness, 'p': _area_per_molecule} - molecule_material.sld.make_dependent_on(dependency_expression=dependency_expression, dependency_map=dependency_map) - dependency_map = {'a': _scattering_length_imag, 'b': thickness, 'p': _area_per_molecule} - molecule_material.isld.make_dependent_on(dependency_expression=dependency_expression, dependency_map=dependency_map) + # Constrain molecule.sld / .isld to scattering_length / (thickness * area_per_molecule). + # `_setup_sld_constraints` rebuilds the same expression after from_dict, so keep the + # variable names (`a`, `b`, `p`) consistent with that path. + dependency_expression = 'a / (b*p) * 1e6' + molecule_material.sld.make_dependent_on( + dependency_expression=dependency_expression, + dependency_map={'a': scattering_length_real, 'b': thickness, 'p': area_per_molecule_param}, + ) + molecule_material.isld.make_dependent_on( + dependency_expression=dependency_expression, + dependency_map={'a': scattering_length_imag, 'b': thickness, 'p': area_per_molecule_param}, + ) solvated_molecule_material = MaterialSolvated( material=molecule_material, @@ -164,17 +163,85 @@ def __init__( roughness=roughness, name=name, unique_name=unique_name, - interface=interface, + interface=None, ) - self._add_component('_scattering_length_real', _scattering_length_real) - self._add_component('_scattering_length_imag', _scattering_length_imag) - self._add_component('_area_per_molecule', _area_per_molecule) + self._area_per_molecule = area_per_molecule_param + self._scattering_length_real = scattering_length_real + self._scattering_length_imag = scattering_length_imag scattering_length = neutron_scattering_length(molecular_formula) self._scattering_length_real.value = scattering_length.real self._scattering_length_imag.value = scattering_length.imag self._molecular_formula = molecular_formula - self.interface = interface + + if interface is not None: + self.interface = interface + + # ----- constraint plumbing ----- + + def _setup_sld_constraints(self) -> None: + """Wire the inner molecule material's ``sld`` / ``isld`` to depend on + the current scattering-length, thickness, and area-per-molecule + parameters. + + Idempotent — called once from ``__init__`` and again from + ``from_dict`` after the saved Parameter objects replace the + constructor-time temporaries. + """ + molecule_material = self.material.material + for derived in (molecule_material.sld, molecule_material.isld): + if not derived.independent: + derived.make_independent() + + dependency_expression = 'a / (b*p) * 1e6' + molecule_material.sld.make_dependent_on( + dependency_expression=dependency_expression, + dependency_map={ + 'a': self._scattering_length_real, + 'b': self._thickness, + 'p': self._area_per_molecule, + }, + ) + molecule_material.isld.make_dependent_on( + dependency_expression=dependency_expression, + dependency_map={ + 'a': self._scattering_length_imag, + 'b': self._thickness, + 'p': self._area_per_molecule, + }, + ) + + # ----- deserialization ----- + + @classmethod + def from_dict(cls, obj_dict: dict) -> 'LayerAreaPerMolecule': + """Re-route the saved ``solvent_fraction`` Parameter and rebuild the + molecule-SLD constraint chain after :class:`ModelBase.from_dict` + swaps in the persisted Parameter objects. + + `ModelBase.from_dict` writes the deserialized ``solvent_fraction`` + Parameter to ``self._solvent_fraction`` (orphan — the live property + delegates to ``self.material.solvent_fraction``, which is + ``self.material._fraction``). It also reassigns ``self._thickness`` + and ``self._area_per_molecule``, but the constraint graph built in + ``__init__`` still references the temporary Parameters created from + the float kwargs. We fix both here. + """ + instance = super().from_dict(obj_dict) + + saved_solvent_fraction = instance.__dict__.pop('_solvent_fraction', None) + if saved_solvent_fraction is not None: + mixture = instance.material + old = mixture._fraction + mixture._fraction = saved_solvent_fraction + try: + instance._global_object.map.prune(old.unique_name) + except (AttributeError, KeyError): + pass + mixture._materials_constraints() + + instance._setup_sld_constraints() + return instance @property def area_per_molecule_parameter(self) -> Parameter: @@ -182,19 +249,15 @@ def area_per_molecule_parameter(self) -> Parameter: return self._area_per_molecule @property - def area_per_molecule(self) -> float: - """Get the area per molecule.""" - return self._area_per_molecule.value + def area_per_molecule(self) -> Parameter: + """The Parameter that controls area per molecule.""" + return self._area_per_molecule @area_per_molecule.setter - def area_per_molecule(self, new_area_per_molecule: float) -> None: - """Set the area per molecule. - - :param new_area_per_molecule: New area per molecule. - """ - if new_area_per_molecule < 0: - raise ValueError('new_area_per_molecule must be greater than 0.0.') - self._area_per_molecule.value = new_area_per_molecule + def area_per_molecule(self, value: float) -> None: + if value < 0: + raise ValueError('area_per_molecule must be greater than 0.0.') + self._area_per_molecule.value = value @property def molecule(self) -> Material: @@ -208,32 +271,21 @@ def solvent(self) -> Material: @solvent.setter def solvent(self, new_solvent: Material) -> None: - """Set the solvent material. - - :param new_solvent: New solvent material. - """ self.material.solvent = new_solvent @property - def solvent_fraction_parameter(self) -> float: + def solvent_fraction_parameter(self) -> Parameter: """Get parameter for the fraction of the layer occupied by the solvent.""" return self.material.solvent_fraction_parameter @property - def solvent_fraction(self) -> float: - """Get the fraction of the layer occupied by the solvent. - This could be a result of either water solvating the molecule, or incomplete surface coverage of the molecules. - """ + def solvent_fraction(self) -> Parameter: + """The Parameter for the fraction of the layer occupied by the solvent.""" return self.material.solvent_fraction @solvent_fraction.setter - def solvent_fraction(self, solvent_fraction: float) -> None: - """Set the fraction of the layer occupied by the solvent. - This could be a result of either water solvating the molecule, or incomplete surface coverage of the molecules. - - :param solvent_fraction: Fraction of layer described by the solvent. - """ - self.material.solvent_fraction = solvent_fraction + def solvent_fraction(self, value: float) -> None: + self.material.solvent_fraction = value @property def molecular_formula(self) -> str: @@ -242,13 +294,8 @@ def molecular_formula(self) -> str: @molecular_formula.setter def molecular_formula(self, formula_string: str) -> None: - """Set the formula of the molecule in the material. - - :param formula_string: String that defines the molecular formula. - """ self._molecular_formula = formula_string scattering_length = neutron_scattering_length(formula_string) - # The molecule is also being updated through the constraints self._scattering_length_real.value = scattering_length.real self._scattering_length_imag.value = scattering_length.imag @@ -257,23 +304,8 @@ def molecular_formula(self, formula_string: str) -> None: @property def _dict_repr(self) -> dict[str, str]: - """Dictionary representation of the `area_per_molecule` object. Produces a simple dictionary""" + """Dictionary representation of the `area_per_molecule` object.""" dict_repr = super()._dict_repr dict_repr['molecular_formula'] = self._molecular_formula - dict_repr['area_per_molecule'] = f'{self.area_per_molecule:.2f} {self._area_per_molecule.unit}' + dict_repr['area_per_molecule'] = f'{self._area_per_molecule.value:.2f} {self._area_per_molecule.unit}' return dict_repr - - def as_dict(self, skip: Optional[list[str]] = None) -> dict[str, str]: - """Produces a cleaned dict using a custom as_dict method to skip necessary things. - The resulting dict matches the parameters in __init__ - - :param skip: List of keys to skip, defaults to `None`. - """ - this_dict = super().as_dict(skip=skip) - this_dict['solvent_fraction'] = self.material._fraction.as_dict(skip=skip) - this_dict['area_per_molecule'] = self._area_per_molecule.as_dict(skip=skip) - this_dict['solvent'] = self.solvent.as_dict(skip=skip) - del this_dict['material'] - del this_dict['_scattering_length_real'] - del this_dict['_scattering_length_imag'] - return this_dict diff --git a/src/easyreflectometry/sample/elements/materials/__init__.py b/src/easyreflectometry/sample/elements/materials/__init__.py new file mode 100644 index 00000000..4e798e20 --- /dev/null +++ b/src/easyreflectometry/sample/elements/materials/__init__.py @@ -0,0 +1,2 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause diff --git a/src/easyreflectometry/sample/elements/materials/material.py b/src/easyreflectometry/sample/elements/materials/material.py index 8c030031..f072a639 100644 --- a/src/easyreflectometry/sample/elements/materials/material.py +++ b/src/easyreflectometry/sample/elements/materials/material.py @@ -1,4 +1,6 @@ -__author__ = 'github.com/arm61' +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + from typing import Optional from typing import Union @@ -35,10 +37,6 @@ class Material(BaseCore): - # Added in super().__init__ - sld: Parameter - isld: Parameter - def __init__( self, sld: Union[Parameter, float, None] = None, @@ -49,10 +47,18 @@ def __init__( ): """Constructor. - :param sld: Real scattering length density. - :param isld: Imaginary scattering length density. - :param name: Name of the material, defaults to 'EasyMaterial'. - :param interface: Calculator interface, defaults to `None`. + Parameters + ---------- + unique_name : Optional[str], optional + By default, None. + sld : Union[Parameter, float, None], optional + Real scattering length density. By default, None. + isld : Union[Parameter, float, None], optional + Imaginary scattering length density. By default, None. + name : str, optional + Name of the material. By default, 'EasyMaterial'. + interface : + Calculator interface. By default, None. """ if unique_name is None: unique_name = global_object.generate_unique_name(self.__class__.__name__) @@ -73,13 +79,28 @@ def __init__( ) apply_default_limits(isld, 'isld') - super().__init__( - name=name, - sld=sld, - isld=isld, - interface=interface, - unique_name=unique_name, - ) + super().__init__(name=name, unique_name=unique_name) + self._sld = sld + self._isld = isld + + if interface is not None: + self.interface = interface + + @property + def sld(self) -> Parameter: + return self._sld + + @sld.setter + def sld(self, value: float) -> None: + self._sld.value = value + + @property + def isld(self) -> Parameter: + return self._isld + + @isld.setter + def isld(self, value: float) -> None: + self._isld.value = value # Representation @property @@ -87,7 +108,7 @@ def _dict_repr(self) -> dict[str, str]: """A simplified dict representation.""" return { self.name: { - 'sld': f'{self.sld.value:.3f}e-6 {self.sld.unit}', - 'isld': f'{self.isld.value:.3f}e-6 {self.isld.unit}', + 'sld': f'{self._sld.value:.3f}e-6 {self._sld.unit}', + 'isld': f'{self._isld.value:.3f}e-6 {self._isld.unit}', } } diff --git a/src/easyreflectometry/sample/elements/materials/material_density.py b/src/easyreflectometry/sample/elements/materials/material_density.py index 85a3bf1b..5be1cf9d 100644 --- a/src/easyreflectometry/sample/elements/materials/material_density.py +++ b/src/easyreflectometry/sample/elements/materials/material_density.py @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + from typing import Optional from typing import Union @@ -38,12 +41,6 @@ class MaterialDensity(Material): - # Added in __init__ - scattering_length_real: Parameter - scattering_length_imag: Parameter - molecular_weight: Parameter - density: Parameter - def __init__( self, chemical_structure: Union[str, None] = None, @@ -54,10 +51,18 @@ def __init__( ): """Constructor. - :param chemical_structure: Chemical formula for the material. - :param density: Mass density for the material. - :param name: Identifier, defaults to `EasyMaterialDensity`. - :param interface: Interface object, defaults to `None`. + Parameters + ---------- + unique_name : Optional[str], optional + By default, None. + chemical_structure : Union[str, None], optional + Chemical formula for the material. By default, None. + density : Union[Parameter, float, None], optional + Mass density for the material. By default, None. + name : str, optional + Identifier. By default, 'EasyMaterialDensity'. + interface : + Interface object. By default, None. """ if unique_name is None: unique_name = global_object.generate_unique_name(self.__class__.__name__) @@ -112,14 +117,59 @@ def __init__( dependency_map = {'d': density, 'sl': scattering_length_imag, 'mw': mw} isld.make_dependent_on(dependency_expression=dependency_expression, dependency_map=dependency_map) - super().__init__(sld, isld, name=name, interface=interface) + super().__init__(sld=sld, isld=isld, name=name, unique_name=unique_name, interface=None) - self._add_component('scattering_length_real', scattering_length_real) - self._add_component('scattering_length_imag', scattering_length_imag) - self._add_component('molecular_weight', mw) - self._add_component('density', density) + self._scattering_length_real = scattering_length_real + self._scattering_length_imag = scattering_length_imag + self._molecular_weight = mw + self._density = density self._chemical_structure = chemical_structure - self.interface = interface + + if interface is not None: + self.interface = interface + + def _setup_sld_constraints(self) -> None: + """Wire the derived `sld` / `isld` to depend on the current density and + scattering-length Parameters. + + Idempotent — invoked once from `__init__` and again from `from_dict` + after :class:`ModelBase` has swapped in the saved Parameter objects. + """ + for derived in (self._sld, self._isld): + if not derived.independent: + derived.make_independent() + + dependency_expression = '1e-23*(0.602214076e6 * d * sl) / mw' + self._sld.make_dependent_on( + dependency_expression=dependency_expression, + dependency_map={ + 'd': self._density, + 'sl': self._scattering_length_real, + 'mw': self._molecular_weight, + }, + ) + self._isld.make_dependent_on( + dependency_expression=dependency_expression, + dependency_map={ + 'd': self._density, + 'sl': self._scattering_length_imag, + 'mw': self._molecular_weight, + }, + ) + + @classmethod + def from_dict(cls, obj_dict: dict) -> 'MaterialDensity': + """Re-attach sld/isld dependencies after deserialization. + + :class:`ModelBase.from_dict` re-points `self._density` at the + deserialized Parameter (because `density` is a constructor argument); + the constraint graph built in `__init__` still references the + temporary Parameter created from the float kwarg. Rebuild here so + `q.density = X` propagates to the derived SLDs. + """ + instance = super().from_dict(obj_dict) + instance._setup_sld_constraints() + return instance @property def chemical_structure(self) -> str: @@ -130,12 +180,35 @@ def chemical_structure(self) -> str: def chemical_structure(self, structure_string: str) -> None: """Set the chemical structure string. - :param structure_string: String that defines the chemical structure. + Parameters + ---------- + structure_string : str + String that defines the chemical structure. """ self._chemical_structure = structure_string scattering_length = neutron_scattering_length(structure_string) - self.scattering_length_real.value = scattering_length.real - self.scattering_length_imag.value = scattering_length.imag + self._scattering_length_real.value = scattering_length.real + self._scattering_length_imag.value = scattering_length.imag + + @property + def density(self) -> Parameter: + return self._density + + @density.setter + def density(self, value: float) -> None: + self._density.value = value + + @property + def molecular_weight(self) -> Parameter: + return self._molecular_weight + + @property + def scattering_length_real(self) -> Parameter: + return self._scattering_length_real + + @property + def scattering_length_imag(self) -> Parameter: + return self._scattering_length_imag @property def _dict_repr(self) -> dict[str, str]: @@ -144,19 +217,3 @@ def _dict_repr(self) -> dict[str, str]: mat_dict['chemical_structure'] = self._chemical_structure mat_dict['density'] = f'{self.density.value:.2e} {self.density.unit}' return mat_dict - - def as_dict(self, skip: list = []) -> dict[str, str]: - """Produces a cleaned dict using a custom as_dict method to skip necessary things. - The resulting dict matches the parameters in __init__ - - :param skip: List of keys to skip, defaults to `None`. - """ - this_dict = super().as_dict(skip=skip) - # From Material - del this_dict['sld'] - del this_dict['isld'] - # Determined in __init__ - del this_dict['scattering_length_real'] - del this_dict['scattering_length_imag'] - del this_dict['molecular_weight'] - return this_dict diff --git a/src/easyreflectometry/sample/elements/materials/material_mixture.py b/src/easyreflectometry/sample/elements/materials/material_mixture.py index a54f3d9d..1ab76ddc 100644 --- a/src/easyreflectometry/sample/elements/materials/material_mixture.py +++ b/src/easyreflectometry/sample/elements/materials/material_mixture.py @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + from typing import Optional from typing import Union @@ -25,11 +28,6 @@ class MaterialMixture(BaseCore): - # Added in super().__init__ - _material_a: Material - _material_b: Material - _fraction: Parameter - def __init__( self, material_a: Union[Material, None] = None, @@ -41,11 +39,20 @@ def __init__( ): """Constructor. - :param material_a: The first material. - :param material_b: The second material. - :param fraction: The fraction of material_b in material_a. - :param name: Name of the material, defaults to None that causes the name to be constructed. - :param interface: Calculator interface, defaults to `None`. + Parameters + ---------- + unique_name : Optional[str], optional + By default, None. + material_a : Union[Material, None], optional + The first material. By default, None. + material_b : Union[Material, None], optional + The second material. By default, None. + fraction : Union[Parameter, float, None], optional + The fraction of material_b in material_a. By default, None. + name : Union[str, None], optional + Name of the material. By default, None. + interface : + Calculator interface. By default, None. """ if unique_name is None: unique_name = global_object.generate_unique_name(self.__class__.__name__) @@ -62,90 +69,57 @@ def __init__( unique_name_prefix=f'{unique_name}_Fraction', ) - sld = weighted_average( + sld_value = weighted_average( a=material_a.sld.value, b=material_b.sld.value, p=fraction.value, ) - isld = weighted_average( + isld_value = weighted_average( a=material_a.isld.value, b=material_b.isld.value, p=fraction.value, ) - self._sld = get_as_parameter( + sld = get_as_parameter( name='sld', - value=sld, + value=sld_value, default_dict=DEFAULTS, unique_name_prefix=f'{unique_name}_Sld', ) - self._isld = get_as_parameter( + isld = get_as_parameter( name='isld', - value=isld, + value=isld_value, default_dict=DEFAULTS, unique_name_prefix=f'{unique_name}_Isld', ) - # To avoid problems when setting the interface - # self._sld and self._isld need to be declared before calling the super constructor - super().__init__( - name, - _material_a=material_a, - _material_b=material_b, - _fraction=fraction, - interface=interface, - ) + # `name` may be None to signal "derive from material names"; resolve + # before super().__init__ since BaseCore stores `_name` directly. if name is None: - self._update_name() + resolved_name = material_a.name + '/' + material_b.name + else: + resolved_name = name - self._materials_constraints() - self.interface = interface + super().__init__(name=resolved_name, unique_name=unique_name) + self._material_a = material_a + self._material_b = material_b + self._fraction = fraction + self._sld = sld + self._isld = isld - def _get_linkable_attributes(self): - return [self._sld, self._isld] - - @property - def sld(self) -> float: - return self._sld.value - - @property - def isld(self) -> float: - return self._isld.value - - def _materials_constraints(self): - dependency_expression = 'a * (1 - p) + b * p' - dependency_map = {'a': self._material_a.sld, 'b': self._material_b.sld, 'p': self._fraction} - self._sld.make_dependent_on(dependency_expression=dependency_expression, dependency_map=dependency_map) - - dependency_map = {'a': self._material_a.isld, 'b': self._material_b.isld, 'p': self._fraction} - self._isld.make_dependent_on(dependency_expression=dependency_expression, dependency_map=dependency_map) - - @property - def fraction(self) -> float: - """Get the fraction of material_b.""" - return self._fraction.value + self._materials_constraints() - @fraction.setter - def fraction(self, fraction: float) -> None: - """Setter for fraction of material_b. + if interface is not None: + self.interface = interface - :param fraction: The fraction of material_b in material_a. - """ - if not isinstance(fraction, float): - raise ValueError('fraction must be a float') - self._fraction.value = fraction + # ----- constructor-arg accessors ----- @property def material_a(self) -> Material: - """Getter for material_a.""" return self._material_a @material_a.setter def material_a(self, new_material_a: Material) -> None: - """Setter for material_a - - :param new_material_a: New Material for material_a - """ self._material_a = new_material_a self._materials_constraints() if self.interface is not None: @@ -154,24 +128,108 @@ def material_a(self, new_material_a: Material) -> None: @property def material_b(self) -> Material: - """Getter for material_b.""" return self._material_b @material_b.setter def material_b(self, new_material_b: Material) -> None: - """Setter for material_b - - :param new_material_b: New Materialfor material_b - """ self._material_b = new_material_b self._materials_constraints() if self.interface is not None: self.interface.generate_bindings(self) self._update_name() + @property + def fraction(self) -> Parameter: + """The Parameter that controls the mixing fraction of material_b in material_a.""" + return self._fraction + + @fraction.setter + def fraction(self, value: float) -> None: + if not isinstance(value, (int, float)): + raise ValueError('fraction must be a float') + self._fraction.value = value + + # ----- derived sld / isld parameters (shared shape with Material) ----- + # + # These are *derived* via the constraints set up in `_materials_constraints` + # (not constructor arguments) so we expose them as floats to match the + # legacy MaterialMixture API. The underlying Parameter objects remain + # available as `self._sld` / `self._isld`. + + @property + def sld(self) -> float: + return self._sld.value + + @property + def isld(self) -> float: + return self._isld.value + + # ----- calculator binding ----- + + def _get_linkable_attributes(self): + """Return the *mixed* sld / isld parameters for calculator binding. + + Override of the inherited `BaseCore._get_linkable_attributes`, which + walks `get_all_variables()` and would otherwise expose the **child** + materials' sld/isld (because our own `sld` / `isld` are floats, not + Parameters). The calculator's `InterfaceFactoryTemplate.generate_bindings` + matches by parameter `name`; without this override it binds to + `material_a.sld` and reflectivity is computed off the wrong SLD. + """ + return [self._sld, self._isld] + + # ----- internal helpers ----- + + def _materials_constraints(self): + """Wire the mixed `_sld` / `_isld` to depend on the current child + material parameters and the current `_fraction`. Idempotent: callers + invoke this once from ``__init__`` and again from ``from_dict`` after + the saved Parameters have been reattached (so the dependency graph + points at the right objects, not the temporary constructor params).""" + # Detach any existing dependency before rebuilding so make_dependent_on + # doesn't chain on top of stale references. + for derived in (self._sld, self._isld): + if not derived.independent: + derived.make_independent() + + dependency_expression = 'a * (1 - p) + b * p' + dependency_map = { + 'a': self._material_a.sld, + 'b': self._material_b.sld, + 'p': self._fraction, + } + self._sld.make_dependent_on(dependency_expression=dependency_expression, dependency_map=dependency_map) + + dependency_map = { + 'a': self._material_a.isld, + 'b': self._material_b.isld, + 'p': self._fraction, + } + self._isld.make_dependent_on(dependency_expression=dependency_expression, dependency_map=dependency_map) + def _update_name(self) -> None: + """Update name.""" self.name = self._material_a.name + '/' + self._material_b.name + # ----- deserialization ----- + + @classmethod + def from_dict(cls, obj_dict: dict) -> 'MaterialMixture': + """Re-attach mixed-sld dependencies after :class:`ModelBase` swaps in + the saved ``_fraction`` Parameter. + + :class:`ModelBase.from_dict` runs ``__init__`` (which builds the + ``_sld`` / ``_isld`` constraints against the *temporary* ``_fraction`` + created from the float kwargs) and then re-points ``self._fraction`` + at the persisted Parameter. The constraint graph still references the + temporary object, so subsequent ``mm.fraction = X`` mutations don't + propagate to ``_sld`` / ``_isld``. Re-running ``_materials_constraints`` + here points the graph at the live objects. + """ + instance = super().from_dict(obj_dict) + instance._materials_constraints() + return instance + # Representation @property def _dict_repr(self) -> dict[str, str]: @@ -185,15 +243,3 @@ def _dict_repr(self) -> dict[str, str]: 'material_b': self._material_b._dict_repr, } } - - def as_dict(self, skip: Optional[list[str]] = None) -> dict[str, str]: - """Produces a cleaned dict using a custom as_dict method to skip necessary things. - The resulting dict matches the parameters in __init__ - - :param skip: List of keys to skip, defaults to `None`. - """ - this_dict = super().as_dict(skip=skip) - this_dict['material_a'] = self._material_a.as_dict(skip=skip) - this_dict['material_b'] = self._material_b.as_dict(skip=skip) - this_dict['fraction'] = self._fraction.as_dict(skip=skip) - return this_dict diff --git a/src/easyreflectometry/sample/elements/materials/material_solvated.py b/src/easyreflectometry/sample/elements/materials/material_solvated.py index 563e3550..d6121d10 100644 --- a/src/easyreflectometry/sample/elements/materials/material_solvated.py +++ b/src/easyreflectometry/sample/elements/materials/material_solvated.py @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + from typing import Optional from typing import Union @@ -33,11 +36,20 @@ def __init__( ): """Constructor. - :param material: The material being solvated. - :param solvent: The solvent material. - :param solvent_fraction: Fraction of solvent in layer. E.g. solvation or surface coverage. - :param name: Name of the material, defaults to None that causes the name to be constructed. - :param interface: Calculator interface, defaults to `None`. + Parameters + ---------- + unique_name : Optional[str], optional + By default, None. + material : Union[Material, None], optional + The material being solvated. By default, None. + solvent : Union[Material, None], optional + The solvent material. By default, None. + solvent_fraction : Union[Parameter, float, None], optional + Fraction of solvent in layer. E.g. solvation or surface coverage. By default, None. + name : + Name of the material. By default, None. + interface : + Calculator interface. By default, None. """ if unique_name is None: unique_name = global_object.generate_unique_name(self.__class__.__name__) @@ -60,6 +72,7 @@ def __init__( material_b=solvent, fraction=solvent_fraction, name=name, + unique_name=unique_name, interface=interface, ) if name is None: @@ -72,10 +85,7 @@ def material(self) -> Material: @material.setter def material(self, new_material: Material) -> None: - """Set the material. - - :param new_material: Matrerial to be useed. - """ + """Set the material.""" self.material_a = new_material @property @@ -85,10 +95,7 @@ def solvent(self) -> Material: @solvent.setter def solvent(self, new_solvent: Material) -> None: - """Set the solvent. - - :param new_solvent: Solvent to be used. - """ + """Set the solvent.""" self.material_b = new_solvent @property @@ -97,33 +104,58 @@ def solvent_fraction_parameter(self) -> Parameter: return self._fraction @property - def solvent_fraction(self) -> float: - """Get the fraction of layer described by the solvent. - This might be fraction of: - Solvation where solvent is within the layer - Patches of solvent in the layer where no material is present. + def solvent_fraction(self) -> Parameter: + """The Parameter for the fraction of the layer described by the solvent. + + This might be the fraction of: + - solvation where solvent is within the layer, or + - patches of solvent in the layer where no material is present. """ - return self.fraction + return self._fraction @solvent_fraction.setter def solvent_fraction(self, solvent_fraction: float) -> None: - """Set the fraction of layer covered by the material. - This might be fraction of: - Solvation where solvent is within the layer - Patches of solvent in the layer where no material is present. - - :param solvent_fraction : Fraction of layer described by the solvent. - """ - try: - self.fraction = solvent_fraction - if solvent_fraction < 0 or solvent_fraction > 1: - raise ValueError('solvent_fraction must be between 0 and 1') - except ValueError: + """Set the fraction of layer covered by the material.""" + if not isinstance(solvent_fraction, (int, float)): raise ValueError('solvent_fraction must be a float between 0 and 1') + if solvent_fraction < 0 or solvent_fraction > 1: + raise ValueError('solvent_fraction must be between 0 and 1') + self._fraction.value = solvent_fraction def _update_name(self) -> None: + """Update name.""" self.name = self._material_a.name + ' in ' + self._material_b.name + # ----- deserialization ----- + + @classmethod + def from_dict(cls, obj_dict: dict) -> 'MaterialSolvated': + """Re-route the saved ``solvent_fraction`` Parameter onto ``_fraction``. + + :class:`ModelBase.from_dict` writes the saved Parameter to + ``_solvent_fraction`` because that's the constructor-arg name, but + the live `solvent_fraction` property returns ``self._fraction`` + (the field MaterialMixture maintains). Without this override the + saved fit metadata (fixed/bounds/etc.) is stranded on the unused + ``_solvent_fraction`` attribute and the active parameter keeps the + defaults from `__init__`. + + Also re-runs `_materials_constraints` so the parent MaterialMixture's + mixed `_sld` / `_isld` depend on the live `_fraction`, not the + temporary Parameter created from the float kwarg. + """ + instance = super().from_dict(obj_dict) + saved = instance.__dict__.pop('_solvent_fraction', None) + if saved is not None: + old = instance._fraction + instance._fraction = saved + try: + instance._global_object.map.prune(old.unique_name) + except (AttributeError, KeyError): + pass + instance._materials_constraints() + return instance + # Representation @property def _dict_repr(self) -> dict[str, str]: @@ -137,24 +169,3 @@ def _dict_repr(self) -> dict[str, str]: 'solvent': self.solvent._dict_repr, } } - - def as_dict(self, skip: Optional[list[str]] = None) -> dict[str, str]: - """Produces a cleaned dict using a custom as_dict method to skip necessary things. - The resulting dict matches the parameters in __init__ - - :param skip: List of keys to skip, defaults to `None`. - """ - this_dict = super().as_dict(skip=skip) - this_dict['material'] = self.material.as_dict(skip=skip) - this_dict['solvent'] = self.solvent.as_dict(skip=skip) - this_dict['solvent_fraction'] = self._fraction.as_dict(skip=skip) - # Property and protected varible from material_mixture - del this_dict['material_a'] - del this_dict['_material_a'] - # Property and protected varible from material_mixture - del this_dict['material_b'] - del this_dict['_material_b'] - # Property and protected varible from material_mixture - del this_dict['fraction'] - del this_dict['_fraction'] - return this_dict diff --git a/src/easyreflectometry/special/calculations.py b/src/easyreflectometry/special/calculations.py index f7ea9068..07844d3c 100644 --- a/src/easyreflectometry/special/calculations.py +++ b/src/easyreflectometry/special/calculations.py @@ -1,4 +1,6 @@ -__author__ = 'github.com/arm61' +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + import periodictable as pt @@ -6,23 +8,37 @@ def weighted_average(a: float, b: float, p: float) -> float: - """ - Determine the weighted average for a and b, where p is the weight. - - :param a: First value - :param b: Second value - :param p: Weight - :return: Weighted average + """Determine the weighted average for a and b, where p is the weight. + + Parameters + ---------- + a : float + First value. + b : float + Second value. + p : float + Weight. + + Returns + ------- + float + Weighted average. """ return a * (1 - p) + b * p def neutron_scattering_length(formula: str) -> complex: - """ - Determine the neutron scattering length for a chemical formula. + """Determine the neutron scattering length for a chemical formula. + + Parameters + ---------- + formula : str + Chemical formula. - :param formula: Chemical formula. - :return: Real and imaginary descriptors for the scattering length in angstrom. + Returns + ------- + complex + Real and imaginary descriptors for the scattering length in angstrom. """ formula_as_dict = parse_formula(formula) scattering_length = 0 + 0j @@ -37,11 +53,17 @@ def neutron_scattering_length(formula: str) -> complex: def molecular_weight(formula: str) -> float: - """ - Determine the molecular weight for a chemical formula. + """Determine the molecular weight for a chemical formula. - :param formula: Chemical formula - :return: Molecular weight of the material in kilograms. + Parameters + ---------- + formula : str + Chemical formula. + + Returns + ------- + float + Molecular weight of the material in kilograms. """ formula_as_dict = parse_formula(formula) mw = 0 @@ -55,25 +77,41 @@ def area_per_molecule_to_scattering_length_density( thickness: float, area_per_molecule: float, ) -> float: - """ - Find the scattering length density for a given area per molecule. - - :param scattering_length: Scattering length of component, in angstrom. - :param thickness: Thickness of component, in angstrom. - :param area_per_molecule: Area per molecule, in angstrom^2. - :return: Scattering length density of layer in e-6 1/angstrom^2. + """Find the scattering length density for a given area per molecule. + + Parameters + ---------- + scattering_length : float + Scattering length of component, in angstrom. + thickness : float + Thickness of component, in angstrom. + area_per_molecule : float + Area per molecule, in angstrom^2. + + Returns + ------- + float + Scattering length density of layer in e-6 1/angstrom^2. """ return scattering_length / (thickness * area_per_molecule) * 1e6 def density_to_sld(scattering_length: float, molecular_weight: float, density: float) -> float: - """ - Find the scattering length density from the mass density of a material. - - :param scattering_length: Scattering length of component, in angstrom. - :param molecular_weight: Molecular weight of component, in u. - :param density: Mass density of the component, in gram centimeter^-3. - :return: Scattering length density of layer in e-6 1/angstrom^2. + """Find the scattering length density from the mass density of a material. + + Parameters + ---------- + scattering_length : float + Scattering length of component, in angstrom. + molecular_weight : float + Molecular weight of component, in u. + density : float + Mass density of the component, in gram centimeter^-3. + + Returns + ------- + float + Scattering length density of layer in e-6 1/angstrom^2. """ # 0.602214076 is avogadros constant times 1e-24 return 0.602214076e6 * density * scattering_length / molecular_weight diff --git a/src/easyreflectometry/special/parsing.py b/src/easyreflectometry/special/parsing.py index 0d52195e..fd0cd576 100644 --- a/src/easyreflectometry/special/parsing.py +++ b/src/easyreflectometry/special/parsing.py @@ -1,4 +1,6 @@ -__author__ = 'github.com/arm61' +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + import re from typing import Tuple @@ -9,9 +11,17 @@ def _dictify(tuples: Tuple[Tuple[str, str]]) -> dict: - """ - :param tuples: tuples of tuples with atom and occurance. - :return: Dict of atoms and occurance. + """Dictify function. + + Parameters + ---------- + tuples : Tuple[Tuple[str, str]] + Tuples of tuples with atom and occurance. + + Returns + ------- + dict + Dict of atoms and occurance. """ res = dict() for atom, n in tuples: @@ -23,19 +33,37 @@ def _dictify(tuples: Tuple[Tuple[str, str]]) -> dict: def _fuse(mol1: dict, mol2: dict, w: int = 1) -> dict: - """ - :param mol1: First dict to fuse - :param mol2: Second dict to fuse - :param w: Weight for dicts - :return: Fused dictionaries + """Fuse function. + + Parameters + ---------- + mol1 : dict + First dict to fuse. + mol2 : dict + Second dict to fuse. + w : int, optional + Weight for dicts. By default, 1. + + Returns + ------- + dict + Fused dictionaries. """ return {atom: (mol1.get(atom, 0) + mol2.get(atom, 0)) * w for atom in set(mol1) | set(mol2)} def _parse(formula: str) -> Tuple[dict, int]: - """ - :param formula: Chemical formula as a string - :return: Tuple containing; formula as a dictwith occurences + """Parse function. + + Parameters + ---------- + formula : str + Chemical formula as a string. + + Returns + ------- + + Tuple containing; formula as a dictwith occurences of each atom and an iterator. """ token_list = [] @@ -73,8 +101,16 @@ def _parse(formula: str) -> Tuple[dict, int]: def parse_formula(formula: str) -> dict: - """ - :param formula: Chemical formula as a string - :return: Formula as a dict with occurences of each atom. + """Parse formula. + + Parameters + ---------- + formula : str + Chemical formula as a string. + + Returns + ------- + dict + Formula as a dict with occurences of each atom. """ return _parse(formula)[0] diff --git a/src/easyreflectometry/summary/__init__.py b/src/easyreflectometry/summary/__init__.py index af9d5fa4..7f333368 100644 --- a/src/easyreflectometry/summary/__init__.py +++ b/src/easyreflectometry/summary/__init__.py @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2024 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + from .summary import Summary -__all__ = [Summary] +__all__ = ['Summary'] diff --git a/src/easyreflectometry/summary/html_templates.py b/src/easyreflectometry/summary/html_templates.py index dc8afc0e..9df780aa 100644 --- a/src/easyreflectometry/summary/html_templates.py +++ b/src/easyreflectometry/summary/html_templates.py @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2024 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + HTML_TEMPLATE = """ @@ -39,7 +42,7 @@ experiments_section - +

Refinement

@@ -49,7 +52,7 @@ figures_section - + """ @@ -75,18 +78,18 @@ HTML_PARAMETER_HEADER_TEMPLATE = """ - parameter_name + parameter_name parameter_value - parameter_unit + parameter_unit parameter_error """ HTML_PARAMETER_TEMPLATE = """ - parameter_name + parameter_name parameter_value - parameter_unit + parameter_unit parameter_error """ @@ -120,10 +123,10 @@ Minimization engine minimization_engine - - - - + + Goodness-of-fit: reduced χ2 + goodness_of_fit + No. of parameters: num_total_params @@ -147,3 +150,13 @@
Fit experiment plot """ + +# Interactive figures for HTML reports. The plotly ``
``s carry their own +# JavaScript so the report stays interactive (zoom, pan, hover) when opened in a +# browser. The plotly.js library is embedded inline in the first figure, which +# keeps the saved report self-contained and working offline. +HTML_INTERACTIVE_FIGURES_TEMPLATE = """ +sld_plot_div +
+fit_experiment_plot_div +""" diff --git a/src/easyreflectometry/summary/summary.py b/src/easyreflectometry/summary/summary.py index 34da2974..f40f23ad 100644 --- a/src/easyreflectometry/summary/summary.py +++ b/src/easyreflectometry/summary/summary.py @@ -1,3 +1,14 @@ +# SPDX-FileCopyrightText: 2024 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + +import contextlib +import io +import logging +from html import escape +from importlib.metadata import PackageNotFoundError +from importlib.metadata import version +from urllib.parse import quote + import matplotlib.pyplot as plt import numpy as np from easyscience import global_object @@ -7,6 +18,7 @@ from .html_templates import HTML_DATA_COLLECTION_TEMPLATE from .html_templates import HTML_FIGURES_TEMPLATE +from .html_templates import HTML_INTERACTIVE_FIGURES_TEMPLATE from .html_templates import HTML_PARAMETER_HEADER_TEMPLATE from .html_templates import HTML_PARAMETER_TEMPLATE from .html_templates import HTML_PROJECT_INFORMATION_TEMPLATE @@ -14,11 +26,108 @@ from .html_templates import HTML_TEMPLATE +@contextlib.contextmanager +def _silence_pdf_converter(): + """Silence xhtml2pdf's verbose output during PDF conversion. + + xhtml2pdf emits a large amount of ``log.debug`` output (image tags, file + objects, column widths, parsed schemes, ...) and a few stray ``print`` + statements while rendering. When the host application has configured logging + at DEBUG level this floods stdout. For the duration of the conversion we + raise the ``xhtml2pdf`` logger level so its debug records are dropped, and + redirect stdout to swallow the stray prints. Both are restored afterwards. + """ + xhtml2pdf_logger = logging.getLogger('xhtml2pdf') + previous_level = xhtml2pdf_logger.level + xhtml2pdf_logger.setLevel(logging.WARNING) + try: + with contextlib.redirect_stdout(io.StringIO()): + yield + finally: + xhtml2pdf_logger.setLevel(previous_level) + + +_NAME_MAX_LEN = 20 +# Custom href scheme used to pass the full name to QML via TextEdit.hoveredLink. +_TOOLTIP_SCHEME = 'nametooltip' + +# URLs for known calculation engines and minimizer packages. +_ENGINE_URLS: dict[str, str] = { + 'refnx': 'https://refnx.readthedocs.io', + 'refl1d': 'https://refl1d.readthedocs.io', + 'bornagain': 'https://www.bornagainproject.org', + 'lm': 'https://lmfit.github.io/lmfit-py/', + 'bumps': 'https://bumps.readthedocs.io', + 'dfo': 'https://github.com/fitbenchmarking/dfo-ls', +} + + +def _engine_link(name: str, package: str | None = None) -> str: + """Return an HTML hyperlink for an engine, including its version. + + Falls back to plain text when no URL is known for the engine. + """ + url = _ENGINE_URLS.get(name) or _ENGINE_URLS.get(package or '') + display = escape(name) + if package: + try: + ver = version(package) + except PackageNotFoundError: + ver = None + if ver: + display = f'{display} (v{ver})' + + if url: + return f'{display}' + return display + + +def _format_value(value: float, sig_figs: int) -> str: + """Format a numeric value for summary display. + + *sig_figs* significant figures; fall back to 1-decimal exponential when + the formatted string is too long. Zero is shown as '0.0'. + """ + if value == 0.0: + return '0.0' + s = f'{value:.{sig_figs}g}' + if len(s) <= sig_figs + 4: + return s + return f'{value:.1e}' + + +def _truncate_name(name: str, max_len: int = _NAME_MAX_LEN) -> str: + """Return an HTML snippet with a truncated name and tooltip for the full text. + + Browsers use the ``title`` attribute; QML reads the ``href`` via + ``TextEdit.hoveredLink`` and shows a native ToolTip. + """ + safe = escape(name) + if len(name) <= max_len: + return safe + short = escape(name[:max_len].rstrip()) + encoded = quote(name, safe='') + return f'{short}…' + + class Summary: def __init__(self, project: Project): + """Init function.""" self._project = project - def compile_html_summary(self, figures: bool = False) -> str: + def compile_html_summary(self, figures: bool = False, interactive: bool = True) -> str: + """Compile html summary. + + Parameters + ---------- + figures + Whether to render the figures section. + interactive + When ``True`` (the default) the figures are rendered as interactive + plotly charts suitable for an HTML report. Set to ``False`` to fall + back to static images, e.g. when the html is handed to the PDF + converter, which cannot run the embedded JavaScript. + """ html = HTML_TEMPLATE html = html.replace('project_information_section', self._project_information_section()) @@ -33,30 +142,36 @@ def compile_html_summary(self, figures: bool = False) -> str: html = html.replace('refinement_section', self._refinement_section()) if figures: - html = html.replace('figures_section', self._figures_section()) + html = html.replace('figures_section', self._figures_section(interactive=interactive)) else: html = html.replace('figures_section', '') return html def save_html_summary(self, filename: str) -> None: - html = self.compile_html_summary(figures=True) - with open(filename, 'w') as f: + """Save html summary.""" + html = self.compile_html_summary(figures=True, interactive=True) + with open(filename, 'w', encoding='utf-8') as f: f.write(html) def save_pdf_summary(self, filename: str) -> None: - html = self.compile_html_summary(figures=True) + """Save pdf summary.""" + # The PDF converter (xhtml2pdf) cannot execute the JavaScript that powers + # the interactive plotly charts, so embed static images instead. + html = self.compile_html_summary(figures=True, interactive=False) with open(filename, 'w+b') as result_file: - pisa_status = pisa.CreatePDF( - html, - dest=result_file, - ) + with _silence_pdf_converter(): + pisa_status = pisa.CreatePDF( + html, + dest=result_file, + ) if pisa_status.err: print('An error occured when generating PDF summary!') def save_sld_plot(self, filename: str) -> None: + """Save sld plot.""" fig = plt.figure() ax = fig.add_subplot(1, 1, 1) @@ -70,6 +185,7 @@ def save_sld_plot(self, filename: str) -> None: plt.close() def save_fit_experiment_plot(self, filename: str) -> None: + """Save fit experiment plot.""" fig = plt.figure() ax = fig.add_subplot(1, 1, 1) legends = [] @@ -92,6 +208,7 @@ def save_fit_experiment_plot(self, filename: str) -> None: plt.close() def _project_information_section(self) -> str: + """Project information section.""" html_project = HTML_PROJECT_INFORMATION_TEMPLATE name = self._project._info['name'] @@ -102,6 +219,7 @@ def _project_information_section(self) -> str: return html_project def _sample_section(self) -> str: + """Sample section.""" html_parameters = [] html_parameter = HTML_PARAMETER_HEADER_TEMPLATE @@ -113,7 +231,7 @@ def _sample_section(self) -> str: # Get parameters directly from the model instead of using project.parameters model = self._project._models[self._project.current_model_index] - parameters = model.get_parameters() + parameters = model.get_all_parameters() for parameter in parameters: path = global_object.map.find_path(model.unique_name, parameter.unique_name) @@ -127,9 +245,10 @@ def _sample_section(self) -> str: html_parameter = HTML_PARAMETER_TEMPLATE html_parameter = html_parameter.replace('parameter_name', f'{name}') - html_parameter = html_parameter.replace('parameter_value', f'{value}') + html_parameter = html_parameter.replace('parameter_value', _format_value(value, 3)) html_parameter = html_parameter.replace('parameter_unit', f'{unit}') - html_parameter = html_parameter.replace('parameter_error', f'{error}') + error_str = _format_value(error, 2) + html_parameter = html_parameter.replace('parameter_error', error_str) html_parameters.append(html_parameter) html_parameters_str = '\n'.join(html_parameters) @@ -137,6 +256,7 @@ def _sample_section(self) -> str: return html_parameters_str def _experiments_section(self) -> str: + """Experiments section.""" html_experiments = [] for idx, experiment in self._project.experiments.items(): @@ -150,9 +270,9 @@ def _experiments_section(self) -> str: range_max = max(experiment.y) range_units = 'Å⁻¹' html_experiment = HTML_DATA_COLLECTION_TEMPLATE - html_experiment = html_experiment.replace('experiment_name', f'{experiment_name}') - html_experiment = html_experiment.replace('range_min', f'{range_min}') - html_experiment = html_experiment.replace('range_max', f'{range_max}') + html_experiment = html_experiment.replace('experiment_name', _truncate_name(experiment_name)) + html_experiment = html_experiment.replace('range_min', _format_value(range_min, 2)) + html_experiment = html_experiment.replace('range_max', _format_value(range_max, 2)) html_experiment = html_experiment.replace('range_units', f'{range_units}') html_experiment = html_experiment.replace('num_data_points', f'{num_data_points}') html_experiment = html_experiment.replace('resolution_function', f'{resolution_function}') @@ -163,29 +283,63 @@ def _experiments_section(self) -> str: return html_experiments_str def _refinement_section(self) -> str: + """Refinement section.""" html_refinement = HTML_REFINEMENT_TEMPLATE # Get parameters directly from the model model = self._project._models[self._project.current_model_index] - parameters = model.get_parameters() + parameters = model.get_all_parameters() num_free_params = sum(1 for parameter in parameters if parameter.free) num_fixed_params = sum(1 for parameter in parameters if not parameter.free) num_params = num_free_params + num_fixed_params - # goodness_of_fit = self._project.status.goodnessOfFit - # goodness_of_fit = goodness_of_fit.split(' → ')[-1] num_constraints = sum(1 for parameter in parameters if not parameter.independent) - html_refinement = html_refinement.replace('calculation_engine', f'{self._project._calculator.current_interface_name}') - html_refinement = html_refinement.replace('minimization_engine', f'{self._project.minimizer.name}') - # html = html.replace('goodness_of_fit', f'{goodness_of_fit}') + goodness_of_fit = self._compute_goodness_of_fit() + + html_refinement = html_refinement.replace( + 'calculation_engine', + _engine_link(self._project._calculator.current_interface_name), + ) + html_refinement = html_refinement.replace( + 'minimization_engine', + _engine_link(self._project.minimizer.name, self._project.minimizer.package), + ) + html_refinement = html_refinement.replace('goodness_of_fit', goodness_of_fit) html_refinement = html_refinement.replace('num_total_params', f'{num_params}') html_refinement = html_refinement.replace('num_free_params', f'{num_free_params}') html_refinement = html_refinement.replace('num_fixed_params', f'{num_fixed_params}') html_refinement = html_refinement.replace('num_constriants', f'{num_constraints}') return html_refinement - def _figures_section(self) -> None: + def _compute_goodness_of_fit(self) -> str: + """Return reduced chi² as a formatted string, or 'N/A' if no fit has been run.""" + last_fit_results = getattr(self._project, '_last_fit_results', None) + if not last_fit_results: + return 'N/A' + try: + if len(last_fit_results) == 1: + gof = float(last_fit_results[0].reduced_chi2) + else: + total_chi2 = sum(float(r.chi2) for r in last_fit_results) + total_points = sum(len(r.x) for r in last_fit_results) + n_pars = last_fit_results[0].n_pars + dof = total_points - n_pars + gof = total_chi2 / dof if dof > 0 else 0.0 + return f'{gof:.4g}' + except (AttributeError, TypeError, ValueError, ZeroDivisionError): + return 'N/A' + + def _figures_section(self, interactive: bool = True) -> str: + """Figures section. + + When *interactive* is ``True`` the figures are rendered as interactive + plotly charts embedded directly in the html. Otherwise static images are + written to disk and referenced (used for the PDF report). + """ + if interactive: + return self._interactive_figures_section() + html_figures = HTML_FIGURES_TEMPLATE path_sld = self._project.path / 'sld_plot.jpg' path_fit_experiment = self._project.path / 'fit_experiment_plot.jpg' @@ -196,3 +350,98 @@ def _figures_section(self) -> None: html_figures = html_figures.replace('path_sld_plot', str(path_sld)) html_figures = html_figures.replace('path_fit_experiment_plot', str(path_fit_experiment)) return html_figures + + def _interactive_figures_section(self) -> str: + """Build the interactive (plotly) figures section for the html report.""" + import plotly.io as pio + + fig_sld = self._sld_plotly_figure() + fig_fit_experiment = self._fit_experiment_plotly_figure() + + # Embed plotly.js inline once (with the first figure) so the saved report + # is self-contained and renders without an internet connection. + sld_div = pio.to_html( + fig_sld, + include_plotlyjs=True, + full_html=False, + default_width='640px', + default_height='480px', + ) + fit_experiment_div = pio.to_html( + fig_fit_experiment, + include_plotlyjs=False, + full_html=False, + default_width='640px', + default_height='480px', + ) + + html_figures = HTML_INTERACTIVE_FIGURES_TEMPLATE + html_figures = html_figures.replace('sld_plot_div', sld_div) + html_figures = html_figures.replace('fit_experiment_plot_div', fit_experiment_div) + return html_figures + + def _sld_plotly_figure(self): + """Interactive SLD profile figure.""" + import plotly.graph_objects as go + + sld = self._project.sld_data_for_model_at_index(0) + + fig = go.Figure() + fig.add_trace( + go.Scatter( + x=np.asarray(sld.x), + y=np.asarray(sld.y), + mode='lines', + name='SLD', + line={'color': 'blue'}, + ) + ) + fig.update_layout( + xaxis_title='z (Å)', + yaxis_title='SLD (Å⁻²)', + template='simple_white', + margin={'l': 70, 'r': 20, 't': 30, 'b': 50}, + legend={'x': 0.99, 'xanchor': 'right', 'y': 0.99, 'yanchor': 'top'}, + ) + return fig + + def _fit_experiment_plotly_figure(self): + """Interactive reflectivity (model vs. experiment) figure.""" + import plotly.graph_objects as go + + fig = go.Figure() + + model = self._project.model_data_for_model_at_index(0) + fig.add_trace( + go.Scatter( + x=np.asarray(model.x), + y=np.asarray(model.y), + mode='lines', + name='Model', + line={'color': 'blue'}, + ) + ) + + try: + experiment = self._project.experimental_data_for_model_at_index(0) + fig.add_trace( + go.Scatter( + x=np.asarray(experiment.x), + y=np.asarray(experiment.y), + mode='markers', + name='Experiment', + marker={'color': 'red', 'size': 4}, + ) + ) + except IndexError: + pass + + fig.update_layout( + xaxis_title='Q (Å⁻¹)', + yaxis_title='Reflectivity', + yaxis_type='log', + template='simple_white', + margin={'l': 70, 'r': 20, 't': 30, 'b': 50}, + legend={'x': 0.99, 'xanchor': 'right', 'y': 0.99, 'yanchor': 'top'}, + ) + return fig diff --git a/src/easyreflectometry/utils.py b/src/easyreflectometry/utils.py index 43be7821..43ac58d8 100644 --- a/src/easyreflectometry/utils.py +++ b/src/easyreflectometry/utils.py @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + from copy import deepcopy from numbers import Number from typing import Optional @@ -14,15 +17,16 @@ def get_as_parameter( default_dict: dict, unique_name_prefix: Optional[str] = None, ) -> Parameter: - """ - This function creates a parameter for the variable `name`. A parameter has a value and metadata. + """This function creates a parameter for the variable `name`. + + A parameter has a value and metadata. If the value already is a parameter, it is returned. - If the value is a number, a parameter is created with this value and metadata from the dictionary. - If the value is None, a parameter is created with the default value and metadata from the dictionary. + If the value is a number, a parameter is created with this value and metadata from the dictionary. + If the value is None, a parameter is created with the default value and metadata from the dictionary. - param value: The value to use for the parameter. If None, the default value in the dictionary is used. - param name: The name of the parameter - param default_dict: Dictionary with entry for `name` containing the default value and metadata for the parameter + param value: The value to use for the parameter. If None, the default value in the dictionary is used. + param name: The name of the parameter + param default_dict: Dictionary with entry for `name` containing the default value and metadata for the parameter """ # This is a parameter, return it if isinstance(value, Parameter): @@ -51,17 +55,17 @@ def get_as_parameter( def yaml_dump(dict_repr: dict) -> str: + """Yaml dump.""" return yaml.dump(dict_repr, sort_keys=False, allow_unicode=True) def collect_unique_names_from_dict(structure_dict: dict, unique_names: Optional[list[str]] = None) -> list[str]: - """ - This function returns a list with the 'unique_name' found the input dictionary. - """ + """This function returns a list with the 'unique_name' found the input dictionary.""" if unique_names is None: unique_names = [] def _collect(item): + """Collect function.""" if isinstance(item, dict): if 'unique_name' in item: unique_names.append(item['unique_name']) @@ -76,12 +80,15 @@ def _collect(item): def count_free_parameters(project) -> int: + """Count free parameters.""" return sum(1 for parameter in project.parameters if parameter.free) def count_fixed_parameters(project) -> int: + """Count fixed parameters.""" return sum(1 for parameter in project.parameters if not parameter.free) def count_parameter_user_constraints(project) -> int: + """Count parameter user constraints.""" return sum(1 for parameter in project.parameters if not parameter.independent) diff --git a/tests/_static/amor_reduced_iofq.ort b/tests/_static/amor_reduced_iofq.ort new file mode 100644 index 00000000..534097d2 --- /dev/null +++ b/tests/_static/amor_reduced_iofq.ort @@ -0,0 +1,236 @@ +# # ORSO reflectivity data file | 1.1 standard | YAML encoding | https://www.reflectometry.org/ +# data_source: +# owner: +# name: A. Luchini +# affiliation: null +# contact: '' +# experiment: +# title: ESS solid-liquid cell test +# instrument: Amor +# start_date: 2024-09-12T03:32:57 +# probe: neutron +# facility: SINQ +# sample: +# name: ESS004 Si|dmpc+ZipAdiscs|D2O +# model: +# stack: Si | SiO2 | pc_head | d54dm_tail | d54dm_tail | pc_head | D2O +# measurement: +# instrument_settings: +# incident_angle: {min: 0.006719518471565493, max: 0.013002693060859875, unit: rad} +# wavelength: {min: 3.0031411323142234, max: 12.499958254247156, unit: angstrom} +# polarization: null +# data_files: +# - file: amor2024n004079.hdf +# - file: amor2024n004080.hdf +# - file: amor2024n004081.hdf +# additional_files: +# - file: amor2024n004152.hdf +# comment: supermirror +# reduction: +# software: {name: ess.reflectometry, version: 0.1.dev1+g180c20f, platform: Linux} +# timestamp: 2025-06-16T03:13:22.094956+00:00 +# creator: +# name: Max Mustermann +# affiliation: European Spallation Source ERIC +# contact: max.mustermann@ess.eu +# corrections: +# - chopper ToF correction +# - footprint correction +# - supermirror calibration +# data_set: 0 +# columns: +# - {name: Qz, unit: 1/angstrom, physical_quantity: wavevector transfer} +# - {name: R, physical_quantity: reflectivity} +# - {name: sR, physical_quantity: standard deviation of reflectivity} +# - {name: sQz, unit: 1/angstrom, physical_quantity: standard deviation of wavevector +# transfer resolution} +# # Qz (1/angstrom) R sR sQz (1/angstrom) +1.0044995237394449e-02 9.8603449665594300e-01 7.8179479714162482e-03 4.1363725159788978e-04 +1.0135390626460987e-02 9.8245104333478350e-01 7.5570347164487421e-03 4.1670106126586831e-04 +1.0226599487925606e-02 9.7696417593495455e-01 7.3448109829999759e-03 4.1828901426624527e-04 +1.0318629142265039e-02 9.8564334802005227e-01 7.3906865714743432e-03 4.1962414384512657e-04 +1.0411486975833342e-02 9.5169127761079275e-01 6.9599717030842763e-03 4.2310566259624898e-04 +1.0505180441454711e-02 9.7407382190188407e-01 6.8907576938003132e-03 4.2645004390068738e-04 +1.0599717059021668e-02 9.8426912609602146e-01 6.7562375631018429e-03 4.2928423579183424e-04 +1.0695104416098604e-02 9.7450248676141815e-01 6.5034923168526652e-03 4.3253452179161566e-04 +1.0791350168530762e-02 9.8118888188357278e-01 6.3716833404347311e-03 4.3480071560078745e-04 +1.0888462041058696e-02 9.8887990521784652e-01 6.2638375299793724e-03 4.3927220128707435e-04 +1.0986447827938273e-02 1.0106569599007325e+00 6.2384780198697382e-03 4.4223603304064865e-04 +1.1085315393566239e-02 1.0015005572426214e+00 6.0240780687788762e-03 4.4527232322051240e-04 +1.1185072673111411e-02 1.0177740726436633e+00 5.9882664258192365e-03 4.4820858865534128e-04 +1.1285727673151575e-02 9.8921638764095243e-01 5.6963317802609156e-03 4.5265854416738833e-04 +1.1387288472316090e-02 1.0201869446242222e+00 5.7196396022441455e-03 4.5581306060731361e-04 +1.1489763221934280e-02 1.0008249942362757e+00 5.4809677965727164e-03 4.5871481927092051e-04 +1.1593160146689669e-02 1.0025584609428606e+00 5.4013279001621436e-03 4.6292087220993082e-04 +1.1697487545280105e-02 1.0263729247237372e+00 5.3895056224825362e-03 4.6600726482663279e-04 +1.1802753791083805e-02 1.0193693802271575e+00 5.2267157416687896e-03 4.6970776586345347e-04 +1.1908967332831416e-02 1.0103354333924730e+00 5.0871137990220350e-03 4.7319692527195552e-04 +1.2016136695284119e-02 1.0094491046259855e+00 4.9950652800967200e-03 4.7648929966986469e-04 +1.2124270479917813e-02 1.0409814363689365e+00 5.0293613302694624e-03 4.7972371645923123e-04 +1.2233377365613496e-02 1.0223059605490794e+00 4.8607306388079325e-03 4.8407536387068818e-04 +1.2343466109353831e-02 1.0252532931453100e+00 4.7993351510761256e-03 4.8695863655647632e-04 +1.2454545546925976e-02 1.0128499829761819e+00 4.6788897952293471e-03 4.9180902124788242e-04 +1.2566624593630759e-02 9.9854962639967948e-01 4.5570537317499698e-03 4.9464614477104059e-04 +1.2679712244998228e-02 1.0054021277720633e+00 4.4881193266977145e-03 4.9880191982438712e-04 +1.2793817577509629e-02 1.0093717851660171e+00 4.4560096498985390e-03 5.0251488214466983e-04 +1.2908949749325898e-02 1.0010082832213376e+00 4.3674046766402144e-03 5.0611872242520696e-04 +1.3025118001022684e-02 9.9997502865166965e-01 4.3246987974745249e-03 5.1087145503655442e-04 +1.3142331656332034e-02 1.0100273480052027e+00 4.2936439281344493e-03 5.1496622270002569e-04 +1.3260600122890685e-02 9.9862071869848812e-01 4.2029742286579241e-03 5.1907625237436288e-04 +1.3379932892995151e-02 1.0164823930731817e+00 4.2357430748032766e-03 5.2294295070626966e-04 +1.3500339544363577e-02 9.7996155603147039e-01 4.0449708337456014e-03 5.2793717559712617e-04 +1.3621829740904442e-02 9.9247214969196795e-01 4.0645114477996080e-03 5.3257311502360278e-04 +1.3744413233492195e-02 1.0037016435356869e+00 4.0682004028257997e-03 5.3633920455711120e-04 +1.3868099860749876e-02 9.8685070242340178e-01 3.9754464443643002e-03 5.4020499389089403e-04 +1.3992899549838754e-02 9.7696514386858824e-01 3.9134752355823154e-03 5.4470166910886920e-04 +1.4118822317255090e-02 9.7178241076301930e-01 3.8866934076358353e-03 5.4976238836404348e-04 +1.4245878269634081e-02 9.2250010832578311e-01 3.7445142123583479e-03 5.5305864996756332e-04 +1.4374077604561006e-02 8.2214390672146609e-01 3.4677481882741967e-03 5.5843124972314361e-04 +1.4503430611389697e-02 7.1353253457907029e-01 3.2166390980214502e-03 5.6155042910128778e-04 +1.4633947672068381e-02 6.0030958518931332e-01 2.9309926173895426e-03 5.6437947346971672e-04 +1.4765639261972916e-02 4.9166627882772374e-01 2.6392357491950885e-03 5.6950953682093216e-04 +1.4898515950747572e-02 3.9563596998583606e-01 2.3544433153957511e-03 5.7345285137804425e-04 +1.5032588403153347e-02 3.3008462019887669e-01 2.1288969526833288e-03 5.7872465889241900e-04 +1.5167867379923934e-02 2.8348628624360400e-01 1.9645569907311548e-03 5.8367885330035823e-04 +1.5304363738629378e-02 2.4971863206251596e-01 1.8334842472432855e-03 5.8770342136830621e-04 +1.5442088434547505e-02 2.2246216998804652e-01 1.7261069959473119e-03 5.9304201275462809e-04 +1.5581052521543216e-02 1.9665563555021481e-01 1.6004802471234396e-03 5.9843253042245518e-04 +1.5721267152955666e-02 1.7930778808508194e-01 1.5280211872099028e-03 6.0297178899375997e-04 +1.5862743582493424e-02 1.6235463616506585e-01 1.4414618539511184e-03 6.0854894037747011e-04 +1.6005493165137739e-02 1.4508570827061523e-01 1.3449372866973191e-03 6.1344256859332798e-04 +1.6149527358053868e-02 1.3326095171286856e-01 1.2771658081253238e-03 6.1789422621403027e-04 +1.6294857721510625e-02 1.2721780987752765e-01 1.2567160206851790e-03 6.2282221685751999e-04 +1.6441495919808258e-02 1.1360640589351807e-01 1.1729308180692003e-03 6.2841066232835951e-04 +1.6589453722214591e-02 1.0356366919550507e-01 1.1152902778779911e-03 6.3321849045516547e-04 +1.6738743003909644e-02 9.6586762699090237e-02 1.0726325643774977e-03 6.3892800485394416e-04 +1.6889375746938759e-02 8.9077992838027684e-02 1.0185548898229684e-03 6.4389516784577148e-04 +1.7041364041174276e-02 8.2719027886160049e-02 9.8358774221977280e-04 6.4844692107349829e-04 +1.7194720085285855e-02 7.6563380615004603e-02 9.4034688430919878e-04 6.5481558273970013e-04 +1.7349456187719568e-02 7.2490220764900221e-02 9.1415942677435956e-04 6.5926814059131156e-04 +1.7505584767685781e-02 6.4251252027250003e-02 8.4532581653434444e-04 6.6511620008951754e-04 +1.7663118356155916e-02 6.1368551284949335e-02 8.3045196505704150e-04 6.7038946412950238e-04 +1.7822069596868211e-02 5.5794193605098193e-02 7.9049687607934710e-04 6.7542025368706586e-04 +1.7982451247342511e-02 5.3244900196045324e-02 7.6950986679186933e-04 6.8159065116678449e-04 +1.8144276179904172e-02 4.9895039079214679e-02 7.4183354352460121e-04 6.8673563730519630e-04 +1.8307557382717225e-02 4.6369745695198222e-02 7.0676877881829467e-04 6.9215160152456118e-04 +1.8472307960826800e-02 4.2676736819516385e-02 6.7850307559999860e-04 6.9707095735638193e-04 +1.8638541137210956e-02 4.0374548477077950e-02 6.6411546231271486e-04 7.0383181466090615e-04 +1.8806270253841958e-02 3.6999978648876108e-02 6.3332951956318324e-04 7.0895990372178978e-04 +1.8975508772757121e-02 3.4568332978177671e-02 6.0650056249067518e-04 7.1427816321714709e-04 +1.9146270277139256e-02 3.4072934265176318e-02 6.0590467866145038e-04 7.1957920659550430e-04 +1.9318568472406893e-02 3.0046042729325925e-02 5.6472505072440728e-04 7.2611474421825758e-04 +1.9492417187314270e-02 2.8365919147004797e-02 5.4500253028234424e-04 7.3184171000717979e-04 +1.9667830375061243e-02 2.6058834561471629e-02 5.2179425869031267e-04 7.3775340044820266e-04 +1.9844822114413171e-02 2.4746345854375979e-02 5.1107964466043712e-04 7.4155194956120908e-04 +2.0023406610830900e-02 2.3851195066905306e-02 4.9916964513846960e-04 7.4830622458566356e-04 +2.0203598197610889e-02 2.1253241483291857e-02 4.6980003523848648e-04 7.5524727677442024e-04 +2.0385411337035607e-02 2.0999497560799319e-02 4.6455494741044496e-04 7.6137631518134383e-04 +2.0568860621534294e-02 1.9438222062193970e-02 4.5148435621545892e-04 7.6715006979192818e-04 +2.0753960774854155e-02 1.7911668785073356e-02 4.3347108302645740e-04 7.7250543628190973e-04 +2.0940726653242085e-02 1.7063681872190845e-02 4.2249940053998666e-04 7.7959601825903526e-04 +2.1129173246637063e-02 1.5751821495561726e-02 4.0609429536334887e-04 7.8544440633751956e-04 +2.1319315679873243e-02 1.5256503430763645e-02 4.0096295989391071e-04 7.9077472093173310e-04 +2.1511169213893868e-02 1.5247551172600692e-02 3.9817006524973524e-04 7.9725169368577713e-04 +2.1704749246976133e-02 1.4738797155940621e-02 3.9569829441703268e-04 8.0363974559300474e-04 +2.1900071315967076e-02 1.2265720162206365e-02 3.6170484268039949e-04 8.1043237181891976e-04 +2.2097151097530542e-02 1.1435145048888439e-02 3.4884949178788980e-04 8.1613315421321807e-04 +2.2296004409405420e-02 1.1758574408129640e-02 3.5407895511419332e-04 8.2250218837096848e-04 +2.2496647211675196e-02 1.1148262991366597e-02 3.4624925624656392e-04 8.2946886150455192e-04 +2.2699095608048902e-02 1.1134293056936589e-02 3.4897470337361412e-04 8.3654975749666795e-04 +2.2903365847153599e-02 8.9691322016261406e-03 3.0681202709152125e-04 8.4252256109452312e-04 +2.3109474323838523e-02 9.4329068912666313e-03 3.1750543304164013e-04 8.4935410242671769e-04 +2.3317437580490936e-02 8.2774928771431680e-03 2.9965588979169108e-04 8.5682822910394178e-04 +2.3527272308363826e-02 7.8557806942254529e-03 2.9447717006224908e-04 8.6341605251930736e-04 +2.3738995348915562e-02 8.0295695579126886e-03 3.0015229767897395e-04 8.6942632367731127e-04 +2.3952623695161605e-02 6.5062795212043603e-03 2.6699673709128122e-04 8.7638665668172511e-04 +2.4168174493038350e-02 6.5390651424043775e-03 2.7155371818985959e-04 8.8312396312905305e-04 +2.4385665042779302e-02 6.4617205382751352e-03 2.7014672755448684e-04 8.8970775253373595e-04 +2.4605112800303586e-02 6.1242708291963897e-03 2.6232612988914415e-04 8.9720927091643278e-04 +2.4826535378616954e-02 5.6793862370787991e-03 2.5669323342458771e-04 9.0347421490645648e-04 +2.5049950549225465e-02 5.1137524674573992e-03 2.4744685919525034e-04 9.1013595608878698e-04 +2.5275376243561798e-02 5.0715281507510640e-03 2.4661133778676296e-04 9.1864323525257569e-04 +2.5502830554424420e-02 5.0378112186348443e-03 2.4704513388133591e-04 9.2548657511316824e-04 +2.5732331737429767e-02 4.5523100036128760e-03 2.3919340850292169e-04 9.3173438829395562e-04 +2.5963898212477448e-02 4.0075887730517063e-03 2.2259605356545533e-04 9.4002091588991140e-04 +2.6197548565228601e-02 4.3538688398406869e-03 2.3396091018105050e-04 9.4666848305364950e-04 +2.6433301548597618e-02 4.5772645541576765e-03 2.4115707990622463e-04 9.5422910073401305e-04 +2.6671176084257259e-02 3.7595255345007038e-03 2.1931146799267310e-04 9.6341084984366619e-04 +2.6911191264157329e-02 3.4687751333548331e-03 2.1130706788376251e-04 9.7025365591636725e-04 +2.7153366352056972e-02 3.4280294251800374e-03 2.1117047636182585e-04 9.7603991937129345e-04 +2.7397720785070828e-02 3.0988098018301588e-03 2.0353253305151864e-04 9.8405212963986570e-04 +2.7644274175229053e-02 3.0631097653504601e-03 2.0025279738740751e-04 9.9071870063870832e-04 +2.7893046311051387e-02 2.6189507808044949e-03 1.8972012884168663e-04 9.9848441599317676e-04 +2.8144057159135413e-02 2.7670387551085546e-03 1.9431921443520219e-04 1.0072117386581893e-03 +2.8397326865759069e-02 2.5745698827701236e-03 1.8831868298433725e-04 1.0106453741284542e-03 +2.8652875758497592e-02 2.7905681789483642e-03 1.9735568291361536e-04 1.0206108783355101e-03 +2.8910724347855053e-02 2.2579498616464998e-03 1.8144168545649193e-04 1.0247286452242205e-03 +2.9170893328910506e-02 2.2718397646608662e-03 1.8507254277329277e-04 1.0284513133486230e-03 +2.9433403582979002e-02 2.0913335375318648e-03 1.7730493232883838e-04 1.0347765664821713e-03 +2.9698276179287553e-02 2.0037579736302677e-03 1.7397641847780328e-04 1.0407227158837906e-03 +2.9965532376666153e-02 2.0053082414414943e-03 1.8148425060825760e-04 1.0471416121277033e-03 +3.0235193625253991e-02 1.8892278671415810e-03 1.7691505421149438e-04 1.0538028705339444e-03 +3.0507281568221079e-02 1.6455571663803511e-03 1.6438306782146591e-04 1.0586732890518317e-03 +3.0781818043505368e-02 2.0152345002365500e-03 1.8668060953165212e-04 1.0651681484471396e-03 +3.1058825085565428e-02 1.4399014212171631e-03 1.6141275114956683e-04 1.0740831689914773e-03 +3.1338324927148969e-02 1.7036160263742333e-03 1.7395184489677181e-04 1.0788999220164093e-03 +3.1620340001077260e-02 1.1217298551618442e-03 1.4542794260526752e-04 1.0852190174050710e-03 +3.1904892942045598e-02 1.6760876765263965e-03 1.8329924377565878e-04 1.0936302747601016e-03 +3.2192006588439973e-02 1.4541359774942839e-03 1.7112578883105698e-04 1.1006572678515826e-03 +3.2481703984170079e-02 1.2875872140922125e-03 1.5948329784556213e-04 1.1074818246421499e-03 +3.2774008380518888e-02 1.0770163414102337e-03 1.4904188037172727e-04 1.1115157833241759e-03 +3.3068943238008706e-02 1.0239128298757724e-03 1.4722523545267687e-04 1.1210080748372785e-03 +3.3366532228284257e-02 1.3048175654721734e-03 1.7133632525039643e-04 1.1281079743235417e-03 +3.3666799236012505e-02 7.8081362277807726e-04 1.3505100592578191e-04 1.1355989313283469e-03 +3.3969768360799638e-02 8.8962543191568346e-04 1.4347746193817939e-04 1.1448622805880508e-03 +3.4275463919125379e-02 5.8492135802313194e-04 1.1805992819084637e-04 1.1518871164758397e-03 +3.4583910446294566e-02 1.0037626448930780e-03 1.6184361791870929e-04 1.1590405080415585e-03 +3.4895132698406453e-02 9.7543464716695215e-04 1.5700708673043159e-04 1.1668900430769433e-03 +3.5209155654341573e-02 8.2897549415443853e-04 1.4987388458057805e-04 1.1754731914395710e-03 +3.5526004517766607e-02 5.7239932166676844e-04 1.2530670507033206e-04 1.1835062045421057e-03 +3.5845704719157254e-02 6.0909326959586723e-04 1.3776867520875599e-04 1.1899729621591166e-03 +3.6168281917839239e-02 8.8693445320398674e-04 1.6875462952929237e-04 1.2003910698861150e-03 +3.6493762004047754e-02 6.2866063571478806e-04 1.3770801746981355e-04 1.2069214028710526e-03 +3.6822171101005483e-02 7.2420676686636156e-04 1.5522563136825894e-04 1.2167722868909683e-03 +3.7153535567019239e-02 6.0486200606272840e-04 1.4335868693002909e-04 1.2240064922834871e-03 +3.7487881997595465e-02 4.9446798541633616e-04 1.3838616735761618e-04 1.2336892832317771e-03 +3.7825237227574859e-02 5.8215581368609181e-04 1.4606820653814031e-04 1.2427557293899095e-03 +3.8165628333286086e-02 4.8799461496087536e-04 1.4205352618575168e-04 1.2515238172612596e-03 +3.8509082634719000e-02 6.7630231365165602e-04 1.6475492676418794e-04 1.2596638429171289e-03 +3.8855627697717332e-02 3.8561873170820743e-04 1.2952940536673275e-04 1.2691388444112553e-03 +3.9205291336191161e-02 4.1438409638717576e-04 1.3132903286872680e-04 1.2796078996929498e-03 +3.9558101614349250e-02 3.1830759029369065e-04 1.2110370479995612e-04 1.2920115664439001e-03 +3.9914086848951529e-02 5.6771195393483416e-04 1.6471361196844844e-04 1.3001318282913159e-03 +4.0273275611581782e-02 2.1375692518747785e-04 1.0705467399039935e-04 1.3082872057334817e-03 +4.0635696730940848e-02 4.5930344831629293e-04 1.5326244166342880e-04 1.3154555503861233e-03 +4.1001379295160359e-02 4.2122812388616657e-04 1.4899195442700177e-04 1.3261958685364048e-03 +4.1370352654137479e-02 5.0335754311600276e-04 1.6788562790991915e-04 1.3332333052413590e-03 +4.1742646421890482e-02 2.3558363811391621e-04 1.1804725653138095e-04 1.3435013294740446e-03 +4.2118290478935588e-02 6.0136228364515883e-04 2.0124632978611175e-04 1.3516355545853918e-03 +4.2497314974685209e-02 3.4973579458410191e-04 1.5666145956338512e-04 1.3607785943180531e-03 +4.2879750329867759e-02 3.8041576943784828e-04 1.7040036063109117e-04 1.3704800464320716e-03 +4.3265627238969193e-02 1.4956829106560899e-04 1.0577627788060906e-04 1.3811467445739348e-03 +4.3654976672696626e-02 5.2457236348972601e-04 2.1438329194891435e-04 1.3893380463826904e-03 +4.4047829880464046e-02 3.5772420454280963e-04 1.7907266100853451e-04 1.3986637127843060e-03 +4.4444218392900400e-02 1.9663401121448377e-04 1.3931442465193813e-04 1.4087204853440356e-03 +4.4844174024380276e-02 6.3375339418640501e-04 2.5910150759841130e-04 1.4171719936303924e-03 +4.5247728875577285e-02 1.9911713936503233e-04 1.4079740927647961e-04 1.4297107977552753e-03 +4.5654915336040547e-02 3.5358306852108451e-04 2.0460404177313448e-04 1.4388379325034240e-03 +4.6065766086794288e-02 3.5432656021929464e-04 2.0464934542952718e-04 1.4487444072325503e-03 +4.6480314102960781e-02 2.8770257297080278e-04 2.0345856667102854e-04 1.4587365148404951e-03 +4.6898592656407043e-02 0.0000000000000000e+00 0.0000000000000000e+00 1.4689492392730790e-03 +4.7320635318415155e-02 1.7033604000696743e-04 1.7033604000696740e-04 1.4785079554828448e-03 +4.7746475962376814e-02 5.6604272028629654e-04 3.2689578616413662e-04 1.4892584671520415e-03 +4.8176148766511939e-02 1.9739099846253179e-04 1.9739099846253182e-04 1.4994645342843837e-03 +4.8609688216611859e-02 0.0000000000000000e+00 0.0000000000000000e+00 1.5106514637331777e-03 +4.9047129108807180e-02 0.0000000000000000e+00 0.0000000000000000e+00 1.5225005846333374e-03 +4.9488506552360575e-02 0.0000000000000000e+00 0.0000000000000000e+00 1.5342463017077742e-03 +4.9933855972484620e-02 4.0777098120311518e-04 4.0777098120311508e-04 1.5465102660743115e-03 +5.0383213113185062e-02 9.6712777624285962e-04 6.8386581951956297e-04 1.5578312155554435e-03 +5.0836614040129652e-02 0.0000000000000000e+00 0.0000000000000000e+00 1.5687794057255237e-03 +5.1294095143542787e-02 0.0000000000000000e+00 0.0000000000000000e+00 1.5766818520382530e-03 +5.1755693141126241e-02 0.0000000000000000e+00 0.0000000000000000e+00 1.5865994314325019e-03 +5.2221445081006082e-02 1.6425834058987397e-03 1.6425834058987399e-03 1.5961999224356393e-03 +5.2691388344706297e-02 0.0000000000000000e+00 0.0000000000000000e+00 1.6063526892327405e-03 +5.3165560650148949e-02 0.0000000000000000e+00 0.0000000000000000e+00 1.6164111412755033e-03 +5.3644000054681451e-02 0.0000000000000000e+00 0.0000000000000000e+00 1.6284421571881489e-03 +5.4126744958131107e-02 0.0000000000000000e+00 0.0000000000000000e+00 1.6364407191857197e-03 diff --git a/tests/calculators/bornagain/test_bornagain_calculator.py b/tests/calculators/bornagain/test_bornagain_calculator.py index e16cdc70..6a7c1aa9 100644 --- a/tests/calculators/bornagain/test_bornagain_calculator.py +++ b/tests/calculators/bornagain/test_bornagain_calculator.py @@ -1,9 +1,10 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + """ Tests for BornAgain calculator. """ -__author__ = 'github.com/arm61' -__version__ = '0.0.1' # import os # import unittest diff --git a/tests/calculators/bornagain/test_bornagain_wrapper.py b/tests/calculators/bornagain/test_bornagain_wrapper.py index 84b2a2d7..c52c084c 100644 --- a/tests/calculators/bornagain/test_bornagain_wrapper.py +++ b/tests/calculators/bornagain/test_bornagain_wrapper.py @@ -1,10 +1,10 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + """ Tests for the BornAgain wrapper. """ -__author__ = 'github.com/arm61' -__version__ = '0.0.1' - # import unittest # import numpy as np diff --git a/tests/calculators/refl1d/test_refl1d_calculator.py b/tests/calculators/refl1d/test_refl1d_calculator.py index ba8c8d35..50de2db4 100644 --- a/tests/calculators/refl1d/test_refl1d_calculator.py +++ b/tests/calculators/refl1d/test_refl1d_calculator.py @@ -1,10 +1,10 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + """ Tests for Refnx calculator. """ -__author__ = 'github.com/arm61' -__version__ = '0.0.1' - import unittest import numpy as np diff --git a/tests/calculators/refl1d/test_refl1d_wrapper.py b/tests/calculators/refl1d/test_refl1d_wrapper.py index e19dfe42..57c43ee8 100644 --- a/tests/calculators/refl1d/test_refl1d_wrapper.py +++ b/tests/calculators/refl1d/test_refl1d_wrapper.py @@ -1,10 +1,10 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + """ Tests for Refl1d wrapper. """ -__author__ = 'github.com/arm61' -__version__ = '0.0.1' - import unittest from unittest.mock import MagicMock from unittest.mock import patch diff --git a/tests/calculators/refnx/test_refnx_calculator.py b/tests/calculators/refnx/test_refnx_calculator.py index 27283c10..baeb9296 100644 --- a/tests/calculators/refnx/test_refnx_calculator.py +++ b/tests/calculators/refnx/test_refnx_calculator.py @@ -1,10 +1,10 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + """ Tests for Refnx calculator. """ -__author__ = 'github.com/arm61' -__version__ = '0.0.1' - import unittest import numpy as np diff --git a/tests/calculators/refnx/test_refnx_wrapper.py b/tests/calculators/refnx/test_refnx_wrapper.py index 74b50ec9..bb99d633 100644 --- a/tests/calculators/refnx/test_refnx_wrapper.py +++ b/tests/calculators/refnx/test_refnx_wrapper.py @@ -1,11 +1,10 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + """ Tests for Refnx wrapper. """ -__author__ = 'github.com/arm61' -__version__ = '0.0.1' - - import unittest import numpy as np @@ -290,7 +289,7 @@ def test_sld_profile(self): assert_almost_equal(p.sld_profile('MyModel')[1][0], 0) assert_almost_equal(p.sld_profile('MyModel')[1][-1], 4) - ### Tests from https://github.com/reflectivity/analysis/tree/master/validation/test/unpolarised + # Tests from https://github.com/reflectivity/analysis/tree/master/validation/test/unpolarised def test_calculate_github_test0(self): p = RefnxWrapper() p.create_material('Material1') @@ -327,15 +326,13 @@ def test_calculate_github_test0(self): p.add_item('Item4', 'MyModel') p.set_resolution_function(PercentageFwhm(0)) p.update_model('MyModel', bkg=0) - q = np.array( - [ - 5.000000000000000104e-03, - 3.717499999999999971e-02, - 5.449999999999999983e-02, - 1.005349999999999994e-01, - 2.955650000000000222e-01, - ] - ) + q = np.array([ + 5.000000000000000104e-03, + 3.717499999999999971e-02, + 5.449999999999999983e-02, + 1.005349999999999994e-01, + 2.955650000000000222e-01, + ]) expected = [ 9.665000503913141472e-01, 3.486325360684768590e-04, @@ -365,15 +362,13 @@ def test_calculate_github_test2(self): p.add_item('Item2', 'MyModel') p.set_resolution_function(PercentageFwhm(0)) p.update_model('MyModel', bkg=0) - q = np.array( - [ - 5.000000000000000104e-03, - 7.564500000000000390e-02, - 1.433050000000000157e-01, - 2.368350000000000177e-01, - 5.920499999999999652e-01, - ] - ) + q = np.array([ + 5.000000000000000104e-03, + 7.564500000000000390e-02, + 1.433050000000000157e-01, + 2.368350000000000177e-01, + 5.920499999999999652e-01, + ]) expected = [ 1.000000000000000222e00, 1.964576414578978456e-04, @@ -456,7 +451,7 @@ def test_calculate_github_test4_spline_resolution(self): p.add_item('Item3', 'MyModel') p.add_item('Item4', 'MyModel') p.update_model('MyModel', bkg=0) - sigma_to_fwhm = 2.355 + sigma_to_fwhm = 2.0 * np.sqrt(2.0 * np.log(2.0)) p.set_resolution_function(LinearSpline(test4_dat[:, 0], sigma_to_fwhm * test4_dat[:, 3])) assert_allclose(p.calculate(test4_dat[:, 0], 'MyModel'), test4_dat[:, 1], rtol=0.03) diff --git a/tests/calculators/test_resolution_conventions.py b/tests/calculators/test_resolution_conventions.py new file mode 100644 index 00000000..5b7c7fb7 --- /dev/null +++ b/tests/calculators/test_resolution_conventions.py @@ -0,0 +1,207 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + +"""Absolute checks on the width convention each wrapper hands to its backend. + +``ResolutionFunction.smearing()`` returns sigma for every resolution type; each +wrapper is then responsible for converting to what its backend expects: + +* refnx -- ``x_err`` is the **FWHM** at each q (a scalar ``x_err`` is instead a + constant dQ/Q FWHM *percentage*). +* refl1d -- ``QProbe.dQ`` is **sigma**. + +The cross-engine tests in ``tests/integration/test_cross_engine_resolution.py`` +only pin the engines against each other, so they cannot catch an error applied +consistently to both. These tests intercept the value at each engine boundary +and assert the exact numbers, which pins the convention absolutely. In +particular this is the only absolute check on the refl1d resolution path. + +See GitHub issue #367 for background. +""" + +import numpy as np +import pytest +from numpy.testing import assert_allclose +from refl1d import names +from refnx import reflect + +from easyreflectometry.calculators.refl1d.wrapper import Refl1dWrapper +from easyreflectometry.calculators.refnx.wrapper import RefnxWrapper +from easyreflectometry.model.resolution_functions import SIGMA_TO_FWHM +from easyreflectometry.model.resolution_functions import LinearSpline +from easyreflectometry.model.resolution_functions import PercentageFwhm +from easyreflectometry.model.resolution_functions import Pointwise + +Q = np.linspace(0.01, 0.3, 20) + +Q_KNOTS = np.linspace(0.001, 0.5, 10) +FWHM_KNOTS = 0.02 * Q_KNOTS + 0.001 + +QZ = np.linspace(0.001, 0.5, 50) +SIGMA_POINTS = 0.01 * QZ + 0.0005 +SQZ = SIGMA_POINTS**2 + + +def _build_refnx(): + wrapper = RefnxWrapper() + wrapper.reset_storage() + wrapper.create_material('Substrate') + wrapper.update_material('Substrate', real=2.07, imag=0.0) + wrapper.create_material('Film') + wrapper.update_material('Film', real=3.45, imag=0.0) + wrapper.create_layer('SubstrateLayer') + wrapper.assign_material_to_layer('Substrate', 'SubstrateLayer') + wrapper.create_layer('FilmLayer') + wrapper.assign_material_to_layer('Film', 'FilmLayer') + wrapper.update_layer('FilmLayer', thick=100.0, rough=3.0) + wrapper.create_item('Item') + wrapper.add_layer_to_item('FilmLayer', 'Item') + wrapper.add_layer_to_item('SubstrateLayer', 'Item') + wrapper.create_model('MyModel') + wrapper.add_item('Item', 'MyModel') + wrapper.update_model('MyModel', bkg=0.0) + return wrapper + + +def _build_refl1d(): + wrapper = Refl1dWrapper() + wrapper.reset_storage() + wrapper.create_material('Substrate') + wrapper.update_material('Substrate', rho=2.07, irho=0.0) + wrapper.create_material('Film') + wrapper.update_material('Film', rho=3.45, irho=0.0) + wrapper.create_layer('SubstrateLayer') + wrapper.assign_material_to_layer('Substrate', 'SubstrateLayer') + wrapper.create_layer('FilmLayer') + wrapper.assign_material_to_layer('Film', 'FilmLayer') + wrapper.update_layer('FilmLayer', thickness=100.0, interface=3.0) + wrapper.create_item('Item') + wrapper.add_layer_to_item('FilmLayer', 'Item') + wrapper.add_layer_to_item('SubstrateLayer', 'Item') + wrapper.create_model('MyModel') + wrapper.add_item('Item', 'MyModel') + wrapper.update_model('MyModel', bkg=0.0) + return wrapper + + +def _capture_refnx_x_err(monkeypatch, resolution_function): + """Run RefnxWrapper.calculate and return the x_err handed to refnx.""" + captured = {} + real_call = reflect.ReflectModel.__call__ + + def spy(self, x, p=None, x_err=None): + captured['x_err'] = x_err + return real_call(self, x, p=p, x_err=x_err) + + monkeypatch.setattr(reflect.ReflectModel, '__call__', spy) + + wrapper = _build_refnx() + wrapper.set_resolution_function(resolution_function) + wrapper.calculate(Q, 'MyModel') + return captured['x_err'] + + +def _capture_refl1d_dq(monkeypatch, resolution_function): + """Run Refl1dWrapper.calculate and return the dQ handed to refl1d's QProbe.""" + captured = {} + real_qprobe = names.QProbe + + def spy(**kwargs): + captured['dQ'] = np.asarray(kwargs['dQ'], dtype=float) + return real_qprobe(**kwargs) + + monkeypatch.setattr(names, 'QProbe', spy) + + wrapper = _build_refl1d() + wrapper.set_resolution_function(resolution_function) + wrapper.calculate(Q, 'MyModel') + return captured['dQ'] + + +# ----- the constant itself ----- + + +@pytest.mark.fast +def test_sigma_to_fwhm_is_the_gaussian_ratio(): + """Pin SIGMA_TO_FWHM against a literal. + + Every other test in this module imports SIGMA_TO_FWHM -- the same constant + the production code uses -- so a wrong value would cancel out on both sides + of the assertion and stay invisible. This is the one place the constant is + checked against an external fact: the FWHM/sigma ratio of a Gaussian, + 2*sqrt(2*ln2). + """ + assert SIGMA_TO_FWHM == pytest.approx(2.3548200450309493) + + +# ----- refnx expects FWHM ----- + + +@pytest.mark.fast +def test_refnx_receives_fwhm_for_linear_spline(monkeypatch): + x_err = _capture_refnx_x_err(monkeypatch, LinearSpline(Q_KNOTS, FWHM_KNOTS)) + + expected_fwhm = np.interp(Q, Q_KNOTS, FWHM_KNOTS) + assert_allclose(x_err, expected_fwhm) + + +@pytest.mark.fast +def test_refnx_receives_fwhm_for_pointwise(monkeypatch): + x_err = _capture_refnx_x_err(monkeypatch, Pointwise([QZ, np.ones_like(QZ), SQZ])) + + expected_sigma = np.interp(Q, QZ, np.sqrt(SQZ)) + assert_allclose(x_err, expected_sigma * SIGMA_TO_FWHM) + + +@pytest.mark.fast +def test_refnx_receives_scalar_percentage_for_percentage_fwhm(monkeypatch): + x_err = _capture_refnx_x_err(monkeypatch, PercentageFwhm(5.0)) + + # refnx reads a scalar x_err as a constant dQ/Q FWHM percentage, so the + # percentage is passed through verbatim -- not converted to a width. + assert np.isscalar(x_err) or np.ndim(x_err) == 0 + assert_allclose(x_err, 5.0) + + +# ----- refl1d expects sigma ----- + + +@pytest.mark.fast +def test_refl1d_receives_sigma_for_linear_spline(monkeypatch): + dq = _capture_refl1d_dq(monkeypatch, LinearSpline(Q_KNOTS, FWHM_KNOTS)) + + expected_fwhm = np.interp(Q, Q_KNOTS, FWHM_KNOTS) + assert_allclose(dq, expected_fwhm / SIGMA_TO_FWHM) + + +@pytest.mark.fast +def test_refl1d_receives_sigma_for_pointwise(monkeypatch): + dq = _capture_refl1d_dq(monkeypatch, Pointwise([QZ, np.ones_like(QZ), SQZ])) + + expected_sigma = np.interp(Q, QZ, np.sqrt(SQZ)) + assert_allclose(dq, expected_sigma) + + +@pytest.mark.fast +def test_refl1d_receives_sigma_for_percentage_fwhm(monkeypatch): + dq = _capture_refl1d_dq(monkeypatch, PercentageFwhm(5.0)) + + expected_sigma = (5.0 / 100.0) * Q / SIGMA_TO_FWHM + assert_allclose(dq, expected_sigma) + + +# ----- the two backends must receive widths that differ by exactly SIGMA_TO_FWHM ----- + + +@pytest.mark.fast +def test_engines_receive_widths_differing_by_sigma_to_fwhm(monkeypatch): + """The whole point of issue #367, stated directly. + + Catches a common-mode error that the cross-engine reflectivity comparison + cannot see: whatever the widths are, refnx's must be exactly SIGMA_TO_FWHM + times refl1d's. + """ + x_err = _capture_refnx_x_err(monkeypatch, LinearSpline(Q_KNOTS, FWHM_KNOTS)) + dq = _capture_refl1d_dq(monkeypatch, LinearSpline(Q_KNOTS, FWHM_KNOTS)) + + assert_allclose(x_err, dq * SIGMA_TO_FWHM) diff --git a/tests/data/test_data_store.py b/tests/data/test_data_store.py index ea66f8ff..66ba9b04 100644 --- a/tests/data/test_data_store.py +++ b/tests/data/test_data_store.py @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2024 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + from unittest.mock import Mock import numpy as np @@ -29,7 +32,13 @@ def test_constructor_default_values(self): def test_constructor_with_values(self): # When data = DataSet1D( - x=[1, 2, 3], y=[4, 5, 6], ye=[7, 8, 9], xe=[10, 11, 12], x_label='label_x', y_label='label_y', name='MyDataSet1D' + x=[1, 2, 3], + y=[4, 5, 6], + ye=[7, 8, 9], + xe=[10, 11, 12], + x_label='label_x', + y_label='label_y', + name='MyDataSet1D', ) # Then diff --git a/tests/functional/test_dummy.py b/tests/functional/test_dummy.py new file mode 100644 index 00000000..6927fe89 --- /dev/null +++ b/tests/functional/test_dummy.py @@ -0,0 +1,8 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + + +def test_dummy(): + calculated = 2 + 2 + expected = 4 + assert calculated == expected diff --git a/tests/integration/fitting/test_dummy.py b/tests/integration/fitting/test_dummy.py new file mode 100644 index 00000000..2256189c --- /dev/null +++ b/tests/integration/fitting/test_dummy.py @@ -0,0 +1,17 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + +import pytest + + +@pytest.mark.fast +def test_dummy_fast(): + calculated = 2 + 2 + expected = 4 + assert calculated == expected + + +def test_dummy_slow(): + calculated = sum(i * j for i in range(10000) for j in range(10000)) + expected = 2499500025000000 + assert calculated == expected diff --git a/tests/integration/scipp-analysis/test_dummy.py b/tests/integration/scipp-analysis/test_dummy.py new file mode 100644 index 00000000..2256189c --- /dev/null +++ b/tests/integration/scipp-analysis/test_dummy.py @@ -0,0 +1,17 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + +import pytest + + +@pytest.mark.fast +def test_dummy_fast(): + calculated = 2 + 2 + expected = 4 + assert calculated == expected + + +def test_dummy_slow(): + calculated = sum(i * j for i in range(10000) for j in range(10000)) + expected = 2499500025000000 + assert calculated == expected diff --git a/tests/integration/test_cross_engine_resolution.py b/tests/integration/test_cross_engine_resolution.py new file mode 100644 index 00000000..6da7be16 --- /dev/null +++ b/tests/integration/test_cross_engine_resolution.py @@ -0,0 +1,187 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + +"""Cross-engine consistency checks for resolution function width conventions. + +The same model + resolution function should produce broadly the same +reflectivity on refnx and refl1d. A width-convention error at one engine +boundary shows up as a systematic disagreement between them. + +.. warning:: + + These are **smoke tests, not the regression tests for issue #367.** They + compare the engines against each other, so they are blind to any error + applied consistently to both, and -- measured, not assumed -- they are only + sensitive enough to catch *one* of the two bugs #367 fixed: + + * ``LinearSpline``: the pre-fix refl1d code **over**-smeared by 2.355x, + which moves the curve enough to be caught here (measured separation ~2x). + * ``Pointwise``: the pre-fix refnx code **under**-smeared by 2.355x from an + already-small width. That barely moves the curve -- measured separation + ~1.1x, against a baseline engine disagreement of the same size -- so **no + tolerance can catch it here.** The Pointwise test below is a consistency + check only. + + The actual, exact regression tests for both conventions live in + ``tests/calculators/test_resolution_conventions.py``, which intercepts the + widths handed to each backend and asserts them to floating-point precision. + Fix that file first if these ever conflict. + +.. note:: + + Reflectivity spans several decades and the engines' different resolution + algorithms (refnx: pointwise convolution; refl1d: oversampling) disagree + most at fringe minima, where R is tiny and *relative* differences explode. + The comparison is therefore made on ``log10(R)``, and the tolerances are + measured values with roughly 1.5x headroom rather than round numbers. + +See GitHub issue #367 for background. +""" + +import numpy as np +import pytest + +from easyreflectometry.calculators.refl1d.wrapper import Refl1dWrapper +from easyreflectometry.calculators.refnx.wrapper import RefnxWrapper +from easyreflectometry.model.resolution_functions import SIGMA_TO_FWHM +from easyreflectometry.model.resolution_functions import LinearSpline +from easyreflectometry.model.resolution_functions import PercentageFwhm +from easyreflectometry.model.resolution_functions import Pointwise + +Q = np.geomspace(0.005, 0.3, 100) + + +def _build_simple_model_refnx(wrapper): + """Build an ambient | 100 A film | substrate model on a refnx wrapper. + + The ambient layer is not optional decoration: both engines treat the first + layer as the semi-infinite superphase and ignore its thickness. Without it + the "film" becomes the ambient, leaving a bare interface with no Kiessig + fringes -- and resolution smearing acts almost entirely on fringes, so the + model would be insensitive to the very thing under test. + """ + wrapper.reset_storage() + wrapper.create_material('Ambient') + wrapper.update_material('Ambient', real=0.0, imag=0.0) + wrapper.create_material('Film') + wrapper.update_material('Film', real=3.45, imag=0.0) + wrapper.create_material('Substrate') + wrapper.update_material('Substrate', real=2.07, imag=0.0) + wrapper.create_layer('AmbientLayer') + wrapper.assign_material_to_layer('Ambient', 'AmbientLayer') + wrapper.create_layer('FilmLayer') + wrapper.assign_material_to_layer('Film', 'FilmLayer') + wrapper.update_layer('FilmLayer', thick=100.0, rough=3.0) + wrapper.create_layer('SubstrateLayer') + wrapper.assign_material_to_layer('Substrate', 'SubstrateLayer') + wrapper.update_layer('SubstrateLayer', rough=3.0) + wrapper.create_item('Item') + wrapper.add_layer_to_item('AmbientLayer', 'Item') + wrapper.add_layer_to_item('FilmLayer', 'Item') + wrapper.add_layer_to_item('SubstrateLayer', 'Item') + wrapper.create_model('MyModel') + wrapper.add_item('Item', 'MyModel') + wrapper.update_model('MyModel', bkg=0.0) + + +def _build_simple_model_refl1d(wrapper): + """Build the same ambient | 100 A film | substrate model on refl1d.""" + wrapper.reset_storage() + wrapper.create_material('Ambient') + wrapper.update_material('Ambient', rho=0.0, irho=0.0) + wrapper.create_material('Film') + wrapper.update_material('Film', rho=3.45, irho=0.0) + wrapper.create_material('Substrate') + wrapper.update_material('Substrate', rho=2.07, irho=0.0) + wrapper.create_layer('AmbientLayer') + wrapper.assign_material_to_layer('Ambient', 'AmbientLayer') + wrapper.create_layer('FilmLayer') + wrapper.assign_material_to_layer('Film', 'FilmLayer') + wrapper.update_layer('FilmLayer', thickness=100.0, interface=3.0) + wrapper.create_layer('SubstrateLayer') + wrapper.assign_material_to_layer('Substrate', 'SubstrateLayer') + wrapper.update_layer('SubstrateLayer', interface=3.0) + wrapper.create_item('Item') + wrapper.add_layer_to_item('AmbientLayer', 'Item') + wrapper.add_layer_to_item('FilmLayer', 'Item') + wrapper.add_layer_to_item('SubstrateLayer', 'Item') + wrapper.create_model('MyModel') + wrapper.add_item('Item', 'MyModel') + wrapper.update_model('MyModel', bkg=0.0) + + +def _both_engines(resolution_function): + """Return (refnx_reflectivity, refl1d_reflectivity) for one resolution.""" + refnx_w = RefnxWrapper() + _build_simple_model_refnx(refnx_w) + refnx_w.set_resolution_function(resolution_function) + refnx_r = refnx_w.calculate(Q, 'MyModel') + + refl1d_w = Refl1dWrapper() + _build_simple_model_refl1d(refl1d_w) + refl1d_w.set_resolution_function(resolution_function) + refl1d_r = refl1d_w.calculate(Q, 'MyModel') + + return refnx_r, refl1d_r + + +def _assert_log_close(refnx_r, refl1d_r, atol): + """Assert the engines agree to `atol` decades of R at every q.""" + deviation = np.abs(np.log10(refnx_r) - np.log10(refl1d_r)) + assert deviation.max() <= atol, ( + f'engines disagree by {deviation.max():.4f} decades ' + f'(factor {10 ** deviation.max():.2f}) at q={Q[np.argmax(deviation)]:.4f}, tolerance {atol}' + ) + + +@pytest.mark.fast +@pytest.mark.parametrize(('resolution_pct', 'atol'), [(1.0, 0.07), (5.0, 0.23), (10.0, 0.29)]) +def test_percentage_fwhm_consistent_across_engines(resolution_pct, atol): + """PercentageFwhm gives consistent results across engines. + + Measured disagreement grows with the width (0.041 / 0.149 / 0.191 decades + at 1% / 5% / 10% dQ/Q), so the tolerance is parametrized with it rather + than set to one blanket value. This combination was correct both before + and after issue #367; the test guards against regression. + """ + refnx_r, refl1d_r = _both_engines(PercentageFwhm(resolution_pct)) + _assert_log_close(refnx_r, refl1d_r, atol=atol) + + +@pytest.mark.fast +def test_linear_spline_consistent_across_engines(): + """LinearSpline gives consistent results across engines. + + This one does earn its keep: pre-fix, refl1d read the FWHM knots as sigma + and over-smeared by 2.355x. Measured max |dlog10(R)|: 0.085 with the fix, + 0.182 without it, so atol=0.13 separates them with ~1.5x headroom either + way. + """ + q_knots = np.linspace(0.001, 0.5, 10) + fwhm_knots = 0.02 * q_knots + 0.001 + + refnx_r, refl1d_r = _both_engines(LinearSpline(q_knots, fwhm_knots)) + _assert_log_close(refnx_r, refl1d_r, atol=0.13) + + +@pytest.mark.fast +def test_pointwise_consistent_across_engines(): + """Pointwise (sigma from sQz) is consistent across engines. + + Consistency check only. Pre-fix, refnx under-smeared these widths by + 2.355x, but measured max |dlog10(R)| is 0.092 pre-fix versus 0.085 with + the fix -- indistinguishable, because under-smearing an already-small + width barely moves the curve. Do not add a tolerance here expecting it to + catch that bug; ``tests/calculators/test_resolution_conventions.py`` is + what actually pins it. + + The sQz values mirror the LinearSpline knots, so the applied smearing -- + and hence the measured agreement -- matches that test. + """ + qz = np.linspace(0.001, 0.5, 50) + r = np.ones_like(qz) # only kept for serialization round-trips + sigma = (0.02 * qz + 0.001) / SIGMA_TO_FWHM + sqz = sigma**2 + + refnx_r, refl1d_r = _both_engines(Pointwise([qz, r, sqz])) + _assert_log_close(refnx_r, refl1d_r, atol=0.13) diff --git a/tests/model/test_model.py b/tests/model/test_model.py index 49ce60fc..a2dd46ea 100644 --- a/tests/model/test_model.py +++ b/tests/model/test_model.py @@ -1,10 +1,10 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + """ Tests for Model class. """ -__author__ = 'github.com/arm61' -__version__ = '0.0.1' - import unittest from unittest.mock import MagicMock @@ -45,8 +45,9 @@ def test_default(self): assert_equal(p.background.min, 0.0) assert_equal(p.background.max, np.inf) assert_equal(p.background.fixed, True) - assert p._resolution_function.smearing([1]) == 5.0 - assert p._resolution_function.smearing([100]) == 5.0 + sigma_to_fwhm = 2.0 * np.sqrt(2.0 * np.log(2.0)) + assert np.allclose(p._resolution_function.smearing([1]), 5.0 / 100.0 * 1.0 / sigma_to_fwhm) + assert np.allclose(p._resolution_function.smearing([100]), 5.0 / 100.0 * 100.0 / sigma_to_fwhm) def test_from_pars(self): m1 = Material(6.908, -0.278, 'Boron') @@ -81,8 +82,9 @@ def test_from_pars(self): assert_equal(mod.background.min, 0.0) assert_equal(mod.background.max, np.inf) assert_equal(mod.background.fixed, True) - assert mod._resolution_function.smearing([1]) == 2.0 - assert mod._resolution_function.smearing([100]) == 2.0 + sigma_to_fwhm = 2.0 * np.sqrt(2.0 * np.log(2.0)) + assert np.allclose(mod._resolution_function.smearing([1]), 2.0 / 100.0 * 1.0 / sigma_to_fwhm) + assert np.allclose(mod._resolution_function.smearing([100]), 2.0 / 100.0 * 100.0 / sigma_to_fwhm) def test_add_assemblies(self): m1 = Material(6.908, -0.278, 'Boron') @@ -430,3 +432,121 @@ def test_dict_round_trip(interface): model.interface().reflectity_profile([0.3], model.unique_name), model_from_dict.interface().reflectity_profile([0.3], model_from_dict.unique_name), ) + + +class TestModelPropertyAccessors: + """Tests for the new @property accessors introduced in the ModelBase/EasyList migration.""" + + def test_scale_setter_updates_value(self): + model = Model() + model.scale = 3.0 + assert model.scale.value == 3.0 + + def test_scale_getter_returns_parameter(self): + model = Model(scale=2.5) + from easyscience.variable import Parameter + + assert isinstance(model.scale, Parameter) + assert model.scale.value == 2.5 + + def test_background_setter_updates_value(self): + model = Model() + model.background = 1e-6 + assert model.background.value == 1e-6 + + def test_background_getter_returns_parameter(self): + model = Model(background=5e-6) + from easyscience.variable import Parameter + + assert isinstance(model.background, Parameter) + assert model.background.value == 5e-6 + + def test_sample_setter(self): + model = Model() + new_sample = Sample(name='NewSample') + model.sample = new_sample + assert model.sample.name == 'NewSample' + + def test_to_dict_includes_sample_and_resolution(self): + model = Model() + d = model.to_dict() + assert 'sample' in d + assert 'resolution_function' in d + assert 'interface' in d # interface is None, encoded as None + assert 'name' in d + + def test_to_dict_with_interface_name(self): + interface = CalculatorFactory() + model = Model(interface=interface) + d = model.to_dict() + assert d['interface'] == 'refnx' + + def test_to_dict_excludes_derived_fields(self): + model = Model() + d = model.to_dict() + # sample, resolution_function, interface are handled separately + assert 'sample' in d + # The super().to_dict() skip prevents these from being top-level + assert 'resolution_function' in d + assert 'interface' in d + + def test_as_dict_alias(self): + model = Model() + assert model.as_dict() == model.to_dict() + + def test_is_default_property(self): + model = Model() + assert model.is_default is False + model.is_default = True + assert model.is_default is True + + +class TestModelRoundTrip: + """Tests verifying serialization round-trip for the Model class.""" + + def test_basic_round_trip_preserves_name(self): + global_object.map._clear() + model = Model(name='MyModel') + d = model.as_dict() + global_object.map._clear() + restored = Model.from_dict(d) + assert restored.name == 'MyModel' + + def test_round_trip_preserves_scale_and_background(self): + global_object.map._clear() + model = Model(scale=2.0, background=1e-7) + d = model.as_dict() + global_object.map._clear() + restored = Model.from_dict(d) + assert restored.scale.value == 2.0 + assert restored.background.value == 1e-7 + + def test_round_trip_preserves_resolution_function(self): + global_object.map._clear() + model = Model(resolution_function=PercentageFwhm(3.0)) + d = model.as_dict() + global_object.map._clear() + restored = Model.from_dict(d) + sigma_to_fwhm = 2.0 * np.sqrt(2.0 * np.log(2.0)) + assert np.allclose(restored._resolution_function.smearing(100), 3.0 / 100.0 * 100.0 / sigma_to_fwhm) + + def test_round_trip_preserves_interface(self): + global_object.map._clear() + interface = CalculatorFactory() + model = Model(interface=interface) + d = model.as_dict() + global_object.map._clear() + restored = Model.from_dict(d) + assert restored.interface().name == 'refnx' + + def test_round_trip_preserves_is_default(self): + global_object.map._clear() + model = Model() + model.is_default = True + d = model.as_dict() + global_object.map._clear() + restored = Model.from_dict(d) + # Note: is_default is a runtime flag that may not survive round-trip + # because from_dict reconstructs via __init__ which resets _is_default. + # This test documents the current behaviour. + assert restored.is_default is False diff --git a/tests/model/test_model_collection.py b/tests/model/test_model_collection.py index cc98534d..dda554b1 100644 --- a/tests/model/test_model_collection.py +++ b/tests/model/test_model_collection.py @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + from easyscience import global_object from easyreflectometry.model.model import COLORS @@ -62,7 +65,7 @@ def test_add_model_color_cycle(self): collection.add_model() assert collection[1].color == COLORS[1] - collection.remove(0) + collection.remove_at(0) collection.add_model() assert collection[0].color == COLORS[1] @@ -98,7 +101,7 @@ def test_delete_model(self): # Then collection = ModelCollection(model_1, model_2) - collection.remove(0) + collection.remove_at(0) # Expect assert len(collection) == 1 @@ -113,7 +116,10 @@ def test_as_dict(self): dict_repr = collection.as_dict() # Expect - assert dict_repr['data'][0]['resolution_function'] == {'smearing': 'PercentageFwhm', 'constant': 5.0} + assert dict_repr['data'][0]['resolution_function'] == { + 'smearing': 'PercentageFwhm', + 'constant': 5.0, + } def test_dict_round_trip(self): # When @@ -157,3 +163,42 @@ def test_legacy_from_dict_sets_color_index(self): restored.add_model() assert [model.color for model in restored] == [COLORS[0], COLORS[1]] + + def test_next_color_index_property(self): + """next_color_index should be accessible as a property for serialization.""" + collection = ModelCollection(populate_if_none=False) + collection.add_model() + idx = collection.next_color_index + assert isinstance(idx, int) + assert idx >= 0 + + def test_next_color_index_none_when_no_colors(self): + """When COLORS is empty, next_color_index returns 0.""" + # We can test the None case when COLORS has entries, it wraps + collection = ModelCollection(populate_if_none=False) + # Without adding models, the index should still be accessible + assert collection.next_color_index is not None + + def test_from_dict_preserves_data_count(self): + """from_dict should reconstruct the exact number of models.""" + global_object.map._clear() + model_1 = Model(name='M1') + model_2 = Model(name='M2') + p = ModelCollection(model_1, model_2) + d = p.as_dict() + global_object.map._clear() + q = ModelCollection.from_dict(d) + assert len(q) == 2 + + def test_from_dict_with_extra_data_entries(self): + """from_dict should handle data entries correctly.""" + global_object.map._clear() + model_1 = Model(name='M1') + model_2 = Model(name='M2') + p = ModelCollection(model_1, model_2) + d = p.as_dict() + global_object.map._clear() + q = ModelCollection.from_dict(d) + assert len(q) == 2 + assert q[0].name == 'M1' + assert q[1].name == 'M2' diff --git a/tests/model/test_resolution_functions.py b/tests/model/test_resolution_functions.py index 6ff8afd1..480ca2c6 100644 --- a/tests/model/test_resolution_functions.py +++ b/tests/model/test_resolution_functions.py @@ -1,8 +1,12 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + import unittest import numpy as np from easyreflectometry.model.resolution_functions import DEFAULT_RESOLUTION_FWHM_PERCENTAGE +from easyreflectometry.model.resolution_functions import SIGMA_TO_FWHM from easyreflectometry.model.resolution_functions import LinearSpline from easyreflectometry.model.resolution_functions import PercentageFwhm from easyreflectometry.model.resolution_functions import Pointwise @@ -14,21 +18,24 @@ def test_constructor(self): # When resolution_function = PercentageFwhm(1.0) - # Then Expect - assert np.all(resolution_function.smearing([0, 2.5]) == np.array([1.0, 1.0])) - assert resolution_function.smearing([-100]) == np.array([1.0]) - assert resolution_function.smearing([100]) == np.array([1.0]) + # Then Expect: smearing() returns sigma = (constant / 100) * q / SIGMA_TO_FWHM + # Negative q is not asserted: sigma scales with q here, so q < 0 yields a + # negative width, which is meaningless. Leaving it unpinned keeps the door + # open to guarding with abs(q) without failing this test. + expected = (1.0 / 100.0) * np.array([0.0, 2.5]) / SIGMA_TO_FWHM + assert np.allclose(resolution_function.smearing([0, 2.5]), expected) + assert np.allclose(resolution_function.smearing([100]), (1.0 / 100.0) * 100.0 / SIGMA_TO_FWHM) def test_constructor_none(self): # When resolution_function = PercentageFwhm() - # Then Expect - assert np.all( - resolution_function.smearing([0, 2.5]) == [DEFAULT_RESOLUTION_FWHM_PERCENTAGE, DEFAULT_RESOLUTION_FWHM_PERCENTAGE] - ) - assert resolution_function.smearing([-100]) == DEFAULT_RESOLUTION_FWHM_PERCENTAGE - assert resolution_function.smearing([100]) == DEFAULT_RESOLUTION_FWHM_PERCENTAGE + # Then Expect: defaults to DEFAULT_RESOLUTION_FWHM_PERCENTAGE, returned as sigma + # Negative q is not asserted -- see test_constructor. + c = DEFAULT_RESOLUTION_FWHM_PERCENTAGE + expected = (c / 100.0) * np.array([0.0, 2.5]) / SIGMA_TO_FWHM + assert np.allclose(resolution_function.smearing([0, 2.5]), expected) + assert np.allclose(resolution_function.smearing([100]), (c / 100.0) * 100.0 / SIGMA_TO_FWHM) def test_as_dict(self): # When @@ -54,17 +61,23 @@ def test_constructor(self): # When resolution_function = LinearSpline(q_data_points=[0, 10], fwhm_values=[5, 10]) - # Then Expect - assert np.all(resolution_function.smearing([0, 2.5]) == np.array([5, 6.25])) - assert resolution_function.smearing([-100]) == np.array([5.0]) - assert resolution_function.smearing([100]) == np.array([10.0]) + # Then Expect: smearing() returns sigma (FWHM knots converted to sigma) + # Unlike PercentageFwhm, q outside the knot range is meaningful here: + # np.interp clamps to the end knots, so the width stays positive. + assert np.allclose(resolution_function.smearing([0, 2.5]), np.array([5, 6.25]) / SIGMA_TO_FWHM) + assert np.allclose(resolution_function.smearing([-100]), np.array([5.0]) / SIGMA_TO_FWHM) + assert np.allclose(resolution_function.smearing([100]), np.array([10.0]) / SIGMA_TO_FWHM) def test_as_dict(self): # When resolution_function = LinearSpline(q_data_points=[0, 10], fwhm_values=[5, 10]) # Then Expect - resolution_function.as_dict() == {'smearing': 'LinearSpline', 'q_data_points': [0, 10], 'fwhm_values': [5, 10]} + resolution_function.as_dict() == { + 'smearing': 'LinearSpline', + 'q_data_points': [0, 10], + 'fwhm_values': [5, 10], + } def test_dict_round_trip(self): # When @@ -88,11 +101,23 @@ def test_constructor(self): # When resolution_function = Pointwise(q_data_points=self.data_points) - # Then Expect + # Then Expect: smearing returns the resolution width sqrt(sQz) at the data + # points, since sQz holds the variance of Qz (consistent with LinearSpline). + expected_widths = np.sqrt(self.data_points[2]) assert np.allclose( - np.array(resolution_function.smearing()), np.array([2.51664683, 2.84038734, 3.2460762, 3.6796519, 4.07869271]) + np.array(resolution_function.smearing()), + expected_widths, ) + def test_smearing_interpolates_onto_q(self): + # When + resolution_function = Pointwise(q_data_points=self.data_points) + + # Then Expect: requesting points between data points linearly interpolates the width. + widths = np.sqrt(self.data_points[2]) + expected = np.interp([0.15, 0.25], self.data_points[0], widths) + assert np.allclose(resolution_function.smearing([0.15, 0.25]), expected) + def test_as_dict(self): # When resolution_function = Pointwise(q_data_points=self.data_points) diff --git a/tests/package_test.py b/tests/package_test.py index 4d77705c..f18f84f8 100644 --- a/tests/package_test.py +++ b/tests/package_test.py @@ -1,5 +1,6 @@ +# SPDX-FileCopyrightText: 2024 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -# Copyright (c) 2024 Easyscience contributors (https://github.com/EasyScience) + import easyreflectometry as pkg diff --git a/tests/sample/assemblies/test_base_assembly.py b/tests/sample/assemblies/test_base_assembly.py index 178f6382..84228eca 100644 --- a/tests/sample/assemblies/test_base_assembly.py +++ b/tests/sample/assemblies/test_base_assembly.py @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2024 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + """ Tests for BaseAssembly class module """ @@ -179,3 +182,18 @@ def test_set_back_layer_with_front(self, base_assembly: BaseAssembly) -> None: # Expect assert base_assembly.layers == [self.mock_layer_0, self.mock_layer_1] + + def test_layers_setter(self) -> None: + """The layers property setter should replace the layer list.""" + global_object.map._clear() + BaseAssembly.__abstractmethods__ = set() + assembly = BaseAssembly( + name='test', + type='type', + interface=MagicMock(), + layers=[MagicMock(), MagicMock()], + ) + new_layers = [MagicMock(), MagicMock(), MagicMock()] + assembly.layers = new_layers + assert assembly.layers == new_layers + assert len(assembly.layers) == 3 diff --git a/tests/sample/assemblies/test_bilayer.py b/tests/sample/assemblies/test_bilayer.py index b96e6a78..431ad866 100644 --- a/tests/sample/assemblies/test_bilayer.py +++ b/tests/sample/assemblies/test_bilayer.py @@ -1,9 +1,11 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + """ Tests for Bilayer class module """ __author__ = 'github.com/easyscience' -__version__ = '0.0.1' from easyscience import global_object @@ -117,7 +119,7 @@ def test_tail_layers_linked(self): # Initial values should match assert p.front_tail_layer.thickness.value == p.back_tail_layer.thickness.value - assert p.front_tail_layer.area_per_molecule == p.back_tail_layer.area_per_molecule + assert p.front_tail_layer.area_per_molecule.value == p.back_tail_layer.area_per_molecule.value # Change front tail thickness - back tail should follow p.front_tail_layer.thickness.value = 20.0 @@ -126,8 +128,8 @@ def test_tail_layers_linked(self): # Change front tail area per molecule - back tail should follow p.front_tail_layer.area_per_molecule = 55.0 - assert p.front_tail_layer.area_per_molecule == 55.0 - assert p.back_tail_layer.area_per_molecule == 55.0 + assert p.front_tail_layer.area_per_molecule.value == 55.0 + assert p.back_tail_layer.area_per_molecule.value == 55.0 def test_constrain_heads_enabled(self): """Test head thickness/area constraint when enabled.""" @@ -140,8 +142,8 @@ def test_constrain_heads_enabled(self): # Change front head area per molecule - back head should follow p.front_head_layer.area_per_molecule = 60.0 - assert p.front_head_layer.area_per_molecule == 60.0 - assert p.back_head_layer.area_per_molecule == 60.0 + assert p.front_head_layer.area_per_molecule.value == 60.0 + assert p.back_head_layer.area_per_molecule.value == 60.0 def test_constrain_heads_disabled(self): """Test heads are independent when constraint disabled.""" @@ -188,8 +190,8 @@ def test_head_hydration_independent(self): p.back_head_layer.solvent_fraction = 0.5 # They should remain independent - assert p.front_head_layer.solvent_fraction == 0.3 - assert p.back_head_layer.solvent_fraction == 0.5 + assert p.front_head_layer.solvent_fraction.value == 0.3 + assert p.back_head_layer.solvent_fraction.value == 0.5 def test_conformal_roughness_enabled(self): """Test all roughnesses are linked when conformal roughness enabled.""" diff --git a/tests/sample/assemblies/test_gradient_layer.py b/tests/sample/assemblies/test_gradient_layer.py index 3b88df47..7efda4a9 100644 --- a/tests/sample/assemblies/test_gradient_layer.py +++ b/tests/sample/assemblies/test_gradient_layer.py @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + """ Tests for GradientLayer class module """ @@ -168,7 +171,11 @@ def test_prepare_gradient_layers(monkeypatch): mock_LayerCollection = MagicMock() mock_Material = MagicMock(return_value='Material_from_mock') mock_linear_gradient = MagicMock(return_value=[1.0, 2.0, 3.0]) - monkeypatch.setattr(easyreflectometry.sample.assemblies.gradient_layer, '_linear_gradient', mock_linear_gradient) + monkeypatch.setattr( + easyreflectometry.sample.assemblies.gradient_layer, + '_linear_gradient', + mock_linear_gradient, + ) monkeypatch.setattr(easyreflectometry.sample.assemblies.gradient_layer, 'Layer', mock_Layer) monkeypatch.setattr(easyreflectometry.sample.assemblies.gradient_layer, 'Material', mock_Material) monkeypatch.setattr(easyreflectometry.sample.assemblies.gradient_layer, 'LayerCollection', mock_LayerCollection) diff --git a/tests/sample/assemblies/test_multilayer.py b/tests/sample/assemblies/test_multilayer.py index 82f807a4..f2d7bd61 100644 --- a/tests/sample/assemblies/test_multilayer.py +++ b/tests/sample/assemblies/test_multilayer.py @@ -1,10 +1,10 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + """ Tests for MultiLayer class module """ -__author__ = 'github.com/arm61' -__version__ = '0.0.1' - import unittest from easyscience import global_object diff --git a/tests/sample/assemblies/test_repeating_multilayer.py b/tests/sample/assemblies/test_repeating_multilayer.py index 6eb17d0a..3ee2858b 100644 --- a/tests/sample/assemblies/test_repeating_multilayer.py +++ b/tests/sample/assemblies/test_repeating_multilayer.py @@ -1,11 +1,10 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + """ Tests for RepeatingMultiLayer module """ -__author__ = 'github.com/arm61' -__version__ = '0.0.1' - - import unittest from easyscience import global_object diff --git a/tests/sample/assemblies/test_surfactant_layer.py b/tests/sample/assemblies/test_surfactant_layer.py index 3a0549cd..653b19ba 100644 --- a/tests/sample/assemblies/test_surfactant_layer.py +++ b/tests/sample/assemblies/test_surfactant_layer.py @@ -1,11 +1,10 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + """ Tests for SurfactantLayer class module """ -__author__ = 'github.com/arm61' -__version__ = '0.0.1' - - import unittest from easyscience import global_object @@ -42,31 +41,31 @@ def test_from_pars(self): assert p.tail_layer.molecular_formula == 'C8O10H12P' assert p.tail_layer.thickness.value == 12 assert p.tail_layer.solvent.as_dict() == h2o.as_dict() - assert p.tail_layer.solvent_fraction == 0.5 - assert p.tail_layer.area_per_molecule == 50 + assert p.tail_layer.solvent_fraction.value == 0.5 + assert p.tail_layer.area_per_molecule.value == 50 assert p.tail_layer.roughness.value == 2 assert p.layers[1].name == 'A Test Head Layer' assert p.head_layer.name == 'A Test Head Layer' assert p.head_layer.molecular_formula == 'C10H24' assert p.head_layer.thickness.value == 10 assert p.head_layer.solvent.as_dict() == noth2o.as_dict() - assert p.head_layer.solvent_fraction == 0.2 - assert p.head_layer.area_per_molecule == 40 + assert p.head_layer.solvent_fraction.value == 0.2 + assert p.head_layer.area_per_molecule.value == 40 assert p.name == 'A Test' def test_constraint_area_per_molecule(self): p = SurfactantLayer() p.tail_layer._area_per_molecule.value = 30 - assert p.tail_layer.area_per_molecule == 30.0 - assert p.head_layer.area_per_molecule == 48.2 + assert p.tail_layer.area_per_molecule.value == 30.0 + assert p.head_layer.area_per_molecule.value == 48.2 assert p.constrain_area_per_molecule is False p.constrain_area_per_molecule = True - assert p.tail_layer.area_per_molecule == 30 - assert p.head_layer.area_per_molecule == 30 + assert p.tail_layer.area_per_molecule.value == 30 + assert p.head_layer.area_per_molecule.value == 30 assert p.constrain_area_per_molecule is True p.tail_layer._area_per_molecule.value = 40 - assert p.tail_layer.area_per_molecule == 40 - assert p.head_layer.area_per_molecule == 40 + assert p.tail_layer.area_per_molecule.value == 40 + assert p.head_layer.area_per_molecule.value == 40 def test_conformal_roughness(self): p = SurfactantLayer() @@ -111,7 +110,12 @@ def test_dict_repr(self): 'solvent_fraction': '0.200 dimensionless', 'sld': '2.269e-6 1/Å^2', 'isld': '0.000e-6 1/Å^2', - 'material': {'C10H18NO8P': {'sld': '1.246e-6 1/Å^2', 'isld': '0.000e-6 1/Å^2'}}, + 'material': { + 'C10H18NO8P': { + 'sld': '1.246e-6 1/Å^2', + 'isld': '0.000e-6 1/Å^2', + } + }, 'solvent': {'D2O': {'sld': '6.360e-6 1/Å^2', 'isld': '0.000e-6 1/Å^2'}}, } }, diff --git a/tests/sample/collections/test_base_collection.py b/tests/sample/collections/test_base_collection.py index b9ad429e..c3036bc9 100644 --- a/tests/sample/collections/test_base_collection.py +++ b/tests/sample/collections/test_base_collection.py @@ -1,5 +1,10 @@ +# SPDX-FileCopyrightText: 2024 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + from unittest.mock import MagicMock +import pytest + from easyreflectometry.sample.collections.base_collection import BaseCollection from easyreflectometry.sample.elements.layers.layer import Layer @@ -157,10 +162,185 @@ def test_remove(self): p.append(Layer(name='layer_4')) # Then - p.remove(1) + p.remove_at(1) # Then assert len(p) == 3 assert p[0].name == 'layer_1' assert p[1].name == 'layer_3' assert p[2].name == 'layer_4' + + # ---- new BaseCollection (EasyList-based) specific tests ---- + + def test_name_getter_and_setter(self): + """name property should be readable and writable.""" + p = BaseCollection('original', MagicMock()) + assert p.name == 'original' + p.name = 'changed' + assert p.name == 'changed' + + def test_data_property(self): + """data property should return a read-only copy of the internal list.""" + elem = Layer(name='layer') + p = BaseCollection('name', MagicMock(), elem) + data = p.data + assert len(data) == 1 + assert data[0].name == 'layer' + # Mutating the returned copy must not affect the collection + data.append(Layer(name='extra')) + assert len(p) == 1 + + def test_interface_propagates_to_existing_items(self): + """Setting interface after construction should propagate to all items.""" + mock_iface = MagicMock() + elem = Layer(name='layer') + # Pass interface=None explicitly and items as positional args + p = BaseCollection('name', None, elem) + assert p.interface is None + p.interface = mock_iface + # The interface setter propagates to items then calls generate_bindings on the mock + assert elem.interface is mock_iface + mock_iface.generate_bindings.assert_called() + + def test_interface_propagates_to_inserted_items(self): + """Items inserted after interface is set should receive the interface.""" + mock_iface = MagicMock() + p = BaseCollection('name', mock_iface) + elem = Layer(name='new_layer') + p.append(elem) + assert elem.interface is mock_iface + + def test_get_all_variables(self): + """get_all_variables should collect parameters from all items.""" + elem = Layer(name='layer') + p = BaseCollection('name', MagicMock(), elem) + variables = p.get_all_variables() + # A Layer has thickness, roughness, and the material's sld/isld + names = {v.name for v in variables if hasattr(v, 'name')} + assert 'thickness' in names + assert 'roughness' in names + + def test_get_all_parameters(self): + """get_all_parameters should filter to only Parameter instances.""" + elem = Layer(name='layer') + p = BaseCollection('name', MagicMock(), elem) + params = p.get_all_parameters() + for param in params: + assert param.__class__.__name__ == 'Parameter' + + def test_get_free_parameters(self): + """get_free_parameters should return only independent, non-fixed parameters.""" + elem = Layer(name='layer') + # By default thickness/roughness are fixed + p = BaseCollection('name', MagicMock(), elem) + free = p.get_free_parameters() + # By default all params are fixed, so empty + assert len(free) == 0 + # Unfix one + elem.thickness.fixed = False + free = p.get_free_parameters() + assert len(free) == 1 + assert free[0].name == 'thickness' + + def test_get_fit_parameters_alias(self): + """get_fit_parameters should be an alias for get_free_parameters.""" + elem = Layer(name='layer') + p = BaseCollection('name', MagicMock(), elem) + assert p.get_fit_parameters() == p.get_free_parameters() + + def test_get_parameters_shim(self): + """get_parameters should be a compatibility alias for get_all_parameters.""" + elem = Layer(name='layer') + p = BaseCollection('name', MagicMock(), elem) + assert p.get_parameters() == p.get_all_parameters() + + def test_get_linkable_attributes(self): + """_get_linkable_attributes should return get_all_variables.""" + elem = Layer(name='layer') + p = BaseCollection('name', MagicMock(), elem) + assert p._get_linkable_attributes() == p.get_all_variables() + + def test_to_dict_includes_data_and_name(self): + """to_dict should serialize data items and collection metadata.""" + elem = Layer(name='layer') + p = BaseCollection('name', MagicMock(), elem) + d = p.to_dict() + assert d['name'] == 'name' + assert len(d['data']) == 1 + assert d['data'][0]['name'] == 'layer' + + def test_to_dict_skips_interface(self): + """to_dict should exclude the interface field.""" + mock_iface = MagicMock() + p = BaseCollection('name', mock_iface) + d = p.to_dict() + assert 'interface' not in d + + def test_to_dict_skips_unique_name_by_default(self): + """to_dict should drop unique_name (matching legacy behaviour).""" + elem = Layer(name='layer') + p = BaseCollection('name', MagicMock(), elem) + d = p.to_dict() + assert 'unique_name' not in d + + def test_as_dict_is_alias_for_to_dict(self): + """as_dict should delegate to to_dict.""" + elem = Layer(name='layer') + p = BaseCollection('name', MagicMock(), elem) + assert p.as_dict() == p.to_dict() + + def test_deepcopy_round_trips(self): + """__deepcopy__ should produce an equivalent collection via from_dict.""" + import copy + + elem = Layer(name='layer') + # Use a concrete subclass (LayerCollection) that properly supports deepcopy + from easyreflectometry.sample.collections.layer_collection import LayerCollection + + p = LayerCollection(elem, name='test_layers') + p_copy = copy.deepcopy(p) + assert len(p_copy) == len(p) + assert p_copy[0].name == p[0].name + + def test_repr_handles_exception_gracefully(self): + """__repr__ should not crash even with items lacking _dict_repr.""" + mock_item = MagicMock() + # Deliberately make _dict_repr raise + del mock_item._dict_repr + p = BaseCollection('name', interface=None) + # Manually insert the mock item bypassing normal insert + p._data.append(mock_item) + # Should not raise + result = repr(p) + assert isinstance(result, str) + + def test_insert_rejects_non_integer_index(self): + """insert should raise TypeError for non-integer indices.""" + p = BaseCollection('name', interface=None) + with pytest.raises(TypeError, match='Index must be an integer'): + p.insert('not_an_int', Layer(name='x')) + + def test_duplicate_insert_is_warned(self): + """Inserting an already-present item should warn and skip.""" + import warnings + + elem = Layer(name='layer') + p = BaseCollection('name', MagicMock(), elem) + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter('always') + p.append(elem) + assert len(w) == 1 + assert 'already in collection' in str(w[0].message) + # Length unchanged + assert len(p) == 1 + + def test_get_key_uses_name(self): + """_get_key should use the item's name property.""" + elem = Layer(name='mylayer') + p = BaseCollection('name', MagicMock(), elem) + assert p._get_key(elem) == 'mylayer' + + def test_has_interface_setter(self): + """_has_interface_setter should correctly detect interface-writable types.""" + assert BaseCollection._has_interface_setter(Layer) is True + assert BaseCollection._has_interface_setter(int) is False diff --git a/tests/sample/collections/test_layer_collection.py b/tests/sample/collections/test_layer_collection.py index 543e9235..7eb8071b 100644 --- a/tests/sample/collections/test_layer_collection.py +++ b/tests/sample/collections/test_layer_collection.py @@ -1,10 +1,10 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + """ Tests for LayerCollection class. """ -__author__ = 'github.com/arm61' -__version__ = '0.0.1' - import unittest from easyscience import global_object diff --git a/tests/sample/collections/test_material_collection.py b/tests/sample/collections/test_material_collection.py index 25f30b59..3ca9f22b 100644 --- a/tests/sample/collections/test_material_collection.py +++ b/tests/sample/collections/test_material_collection.py @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + """ Tests for LayerCollection class. """ diff --git a/tests/sample/collections/test_sample.py b/tests/sample/collections/test_sample.py index 3cb1d0c1..6f64dd25 100644 --- a/tests/sample/collections/test_sample.py +++ b/tests/sample/collections/test_sample.py @@ -1,11 +1,10 @@ +# SPDX-FileCopyrightText: 2024 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + """ Tests for Sample class. """ -__author__ = 'github.com/arm61' -__version__ = '0.0.1' - - import pytest from easyscience import global_object from numpy.testing import assert_equal diff --git a/tests/sample/elements/layers/test_layer.py b/tests/sample/elements/layers/test_layer.py index 1cbecb17..26a5cf0a 100644 --- a/tests/sample/elements/layers/test_layer.py +++ b/tests/sample/elements/layers/test_layer.py @@ -1,10 +1,10 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + """ Tests for Layer class. """ -__author__ = 'github.com/arm61' -__version__ = '0.0.1' - import unittest import numpy as np diff --git a/tests/sample/elements/layers/test_layer_area_per_molecule.py b/tests/sample/elements/layers/test_layer_area_per_molecule.py index 505eec5d..0d466be7 100644 --- a/tests/sample/elements/layers/test_layer_area_per_molecule.py +++ b/tests/sample/elements/layers/test_layer_area_per_molecule.py @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + """ Tests for LayerAreaPerMolecule class. """ @@ -15,7 +18,7 @@ class TestLayerAreaPerMolecule(unittest.TestCase): def test_default(self): p = LayerAreaPerMolecule() assert p.molecular_formula == 'C10H18NO8P' - assert p.area_per_molecule == 48.2 + assert p.area_per_molecule.value == 48.2 assert str(p._area_per_molecule.unit) == 'Å^2' assert p._area_per_molecule.fixed is True assert p.thickness.value == 10.0 @@ -30,7 +33,7 @@ def test_default(self): assert p.solvent.sld.value == 6.36 assert p.solvent.isld.value == 0 assert p.solvent.name == 'D2O' - assert p.solvent_fraction == 0.2 + assert p.solvent_fraction.value == 0.2 assert str(p.material._fraction.unit) == 'dimensionless' assert p.material._fraction.fixed is True @@ -46,12 +49,12 @@ def test_from_pars(self): name='PG/H2O', ) assert p.molecular_formula == 'C8O10H12P' - assert p.area_per_molecule == 50 + assert p.area_per_molecule.value == 50 assert p.thickness.value == 12 assert p.roughness.value == 2 assert p.solvent.sld.value == -0.561 assert p.solvent.isld.value == 0 - assert p.solvent_fraction == 0.5 + assert p.solvent_fraction.value == 0.5 def test_from_pars_constraint(self): h2o = Material(-0.561, 0, 'H2O') @@ -65,15 +68,15 @@ def test_from_pars_constraint(self): name='PG/H2O', ) assert p.molecular_formula == 'C8O10H12P' - assert p.area_per_molecule == 50 + assert p.area_per_molecule.value == 50 assert_almost_equal(p.material.sld, 0.31494833333333333) assert p.thickness.value == 12 assert p.roughness.value == 2 assert p.solvent.sld.value == -0.561 assert p.solvent.isld.value == 0 - assert p.solvent_fraction == 0.5 + assert p.solvent_fraction.value == 0.5 p.area_per_molecule = 30 - assert p.area_per_molecule == 30 + assert p.area_per_molecule.value == 30 assert_almost_equal(p.material.sld, 0.7119138888888887) p.thickness.value = 10 assert p.thickness.value == 10 @@ -92,24 +95,24 @@ def test_solvent_change(self): name='PG/H2O', ) assert p.molecular_formula == 'C8O10H12P' - assert p.area_per_molecule == 50 + assert p.area_per_molecule.value == 50 print(p.material) assert_almost_equal(p.material.sld, 0.31494833333333333) assert p.thickness.value == 12 assert p.roughness.value == 2 assert p.solvent.sld.value == -0.561 assert p.solvent.isld.value == 0 - assert p.solvent_fraction == 0.5 + assert p.solvent_fraction.value == 0.5 d2o = Material(6.335, 0, 'D2O') p.solvent = d2o assert p.molecular_formula == 'C8O10H12P' - assert p.area_per_molecule == 50 + assert p.area_per_molecule.value == 50 assert_almost_equal(p.material.sld, 3.762948333333333) assert p.thickness.value == 12 assert p.roughness.value == 2 assert p.solvent.sld.value == 6.335 assert p.solvent.isld.value == 0 - assert p.solvent_fraction == 0.5 + assert p.solvent_fraction.value == 0.5 def test_molecular_formula_change(self): h2o = Material(-0.561, 0, 'H2O') @@ -123,24 +126,24 @@ def test_molecular_formula_change(self): name='PG/H2O', ) assert p.molecular_formula == 'C8O10H12P' - assert p.area_per_molecule == 50 + assert p.area_per_molecule.value == 50 assert_almost_equal(p.material.sld, 0.31494833333333333) assert p.thickness.value == 12 assert p.roughness.value == 2 assert p.solvent.sld.value == -0.561 assert p.solvent.isld.value == 0 - assert p.solvent_fraction == 0.5 + assert p.solvent_fraction.value == 0.5 assert p.material.name == 'C8O10H12P in H2O' p.molecular_formula = 'C8O10D12P' assert p.molecular_formula == 'C8O10D12P' - assert p.area_per_molecule == 50 + assert p.area_per_molecule.value == 50 assert_almost_equal(p.material.sld, 1.3558483333333333) assert p.thickness.value == 12 assert p.roughness.value == 2 assert p.solvent.sld.value == -0.561 assert p.solvent.isld.value == 0 - assert p.solvent_fraction == 0.5 + assert p.solvent_fraction.value == 0.5 assert p.material.name == 'C8O10D12P in H2O' def test_dict_repr(self): @@ -182,3 +185,48 @@ def test_dict_round_trip(self): # Expect assert sorted(p.as_dict()) == sorted(q.as_dict()) + + def test_solvent_fraction_metadata_and_mutation_after_round_trip(self): + """Regression covering two bugs at once: + + - ``solvent_fraction`` is a constructor argument but its backing + storage is ``self.material._fraction`` (delegated through + ``MaterialSolvated``). Without an override, ``ModelBase.from_dict`` + would put the saved Parameter on an orphan ``_solvent_fraction`` + attribute and reset the live one to constructor defaults. + - ``__init__`` builds the molecule SLD constraint against the + *temporary* thickness / area_per_molecule Parameters; after + ``from_dict`` reattaches the saved ones, mutating them must still + propagate to ``material.material.sld``. + """ + p = LayerAreaPerMolecule( + molecular_formula='C10H18NO8P', + thickness=12.0, + solvent_fraction=0.3, + area_per_molecule=50.0, + roughness=2.0, + ) + p.solvent_fraction.fixed = False + p.solvent_fraction.min = 0.12 + + original_mol_sld = p.material.material.sld.value + p_dict = p.as_dict() + global_object.map._clear() + + q = LayerAreaPerMolecule.from_dict(p_dict) + + # solvent_fraction metadata preserved, no orphan field. + assert q.solvent_fraction.value == 0.3 + assert q.solvent_fraction.fixed is False + assert q.solvent_fraction.min == 0.12 + assert '_solvent_fraction' not in q.__dict__ + + # Molecule SLD constraint preserved. + assert_almost_equal(q.material.material.sld.value, original_mol_sld) + + # Mutate the independent parameters and verify the constraint chain + # propagates to the derived molecule SLD. + q.area_per_molecule = 25.0 # half APM doubles SLD + assert_almost_equal(q.material.material.sld.value, 2 * original_mol_sld) + q.thickness.value = 6.0 # half thickness doubles SLD again + assert_almost_equal(q.material.material.sld.value, 4 * original_mol_sld) diff --git a/tests/sample/elements/materials/test_material.py b/tests/sample/elements/materials/test_material.py index c07a2217..997102a4 100644 --- a/tests/sample/elements/materials/test_material.py +++ b/tests/sample/elements/materials/test_material.py @@ -1,10 +1,10 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + """ Tests for Material class. """ -__author__ = 'github.com/arm61' -__version__ = '0.0.1' - from easyscience import global_object from easyreflectometry.sample.elements.materials.material import DEFAULTS diff --git a/tests/sample/elements/materials/test_material_density.py b/tests/sample/elements/materials/test_material_density.py index d1945e1e..0b424cad 100644 --- a/tests/sample/elements/materials/test_material_density.py +++ b/tests/sample/elements/materials/test_material_density.py @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + import unittest import numpy as np @@ -61,3 +64,22 @@ def test_dict_round_trip(self): q = MaterialDensity.from_dict(p_dict) assert sorted(p.as_dict()) == sorted(q.as_dict()) + + def test_density_mutation_propagates_after_round_trip(self): + """Regression: after ``from_dict`` reattaches the saved ``_density`` + Parameter, mutating it must propagate to ``sld`` / ``isld`` (which + are constrained off it). The ``__init__``-time constraint references + the temporary constructor Parameter; ``from_dict`` rebuilds the + graph so subsequent mutations propagate correctly. + """ + p = MaterialDensity(chemical_structure='Si', density=2.33) + original_sld = p.sld.value + p_dict = p.as_dict() + global_object.map._clear() + + q = MaterialDensity.from_dict(p_dict) + assert_almost_equal(q.sld.value, original_sld) + + q.density = 4.66 + # SLD scales linearly with density (constraint: d * sl / mw, etc.) + assert_almost_equal(q.sld.value, 2 * original_sld) diff --git a/tests/sample/elements/materials/test_material_mixture.py b/tests/sample/elements/materials/test_material_mixture.py index 423bfb2d..3c2a3f65 100644 --- a/tests/sample/elements/materials/test_material_mixture.py +++ b/tests/sample/elements/materials/test_material_mixture.py @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + from unittest.mock import MagicMock from easyscience import global_object @@ -10,7 +13,7 @@ class TestMaterialMixture: def test_default(self) -> None: material_mixture = MaterialMixture() - assert material_mixture.fraction == 0.5 + assert material_mixture.fraction.value == 0.5 assert str(material_mixture._fraction.unit) == 'dimensionless' assert_almost_equal(material_mixture.sld, 4.186) assert_almost_equal(material_mixture.isld, 0) @@ -19,7 +22,7 @@ def test_default(self) -> None: def test_default_constraint(self) -> None: material_mixture = MaterialMixture() - assert material_mixture.fraction == 0.5 + assert material_mixture.fraction.value == 0.5 assert str(material_mixture._fraction.unit) == 'dimensionless' assert_almost_equal(material_mixture.sld, 4.186) assert_almost_equal(material_mixture.isld, 0) @@ -34,57 +37,57 @@ def test_fraction_constraint(self): p = Material() q = Material(6.908, -0.278, 'Boron') material_mixture = MaterialMixture(p, q, 0.2) - assert material_mixture.fraction == 0.2 + assert material_mixture.fraction.value == 0.2 assert_almost_equal(material_mixture.sld, 4.7304) assert_almost_equal(material_mixture.isld, -0.0556) material_mixture._fraction.value = 0.5 - assert material_mixture.fraction == 0.5 + assert material_mixture.fraction.value == 0.5 assert_almost_equal(material_mixture.sld, 5.54700) assert_almost_equal(material_mixture.isld, -0.1390) def test_material_a_change(self) -> None: material_mixture = MaterialMixture() - assert material_mixture.fraction == 0.5 + assert material_mixture.fraction.value == 0.5 assert str(material_mixture._fraction.unit) == 'dimensionless' assert_almost_equal(material_mixture.sld, 4.186) assert_almost_equal(material_mixture.isld, 0) q = Material(6.908, -0.278, 'Boron') material_mixture.material_a = q - assert material_mixture.fraction == 0.5 + assert material_mixture.fraction.value == 0.5 assert str(material_mixture._fraction.unit) == 'dimensionless' assert_almost_equal(material_mixture.sld, 5.54700) assert_almost_equal(material_mixture.isld, -0.1390) def test_material_b_change(self) -> None: material_mixture = MaterialMixture() - assert material_mixture.fraction == 0.5 + assert material_mixture.fraction.value == 0.5 assert str(material_mixture._fraction.unit) == 'dimensionless' assert_almost_equal(material_mixture.sld, 4.186) assert_almost_equal(material_mixture.isld, 0) q = Material(6.908, -0.278, 'Boron') material_mixture.material_b = q - assert material_mixture.fraction == 0.5 + assert material_mixture.fraction.value == 0.5 assert str(material_mixture._fraction.unit) == 'dimensionless' assert_almost_equal(material_mixture.sld, 5.54700) assert_almost_equal(material_mixture.isld, -0.1390) def test_material_b_change_double(self) -> None: material_mixture = MaterialMixture() - assert material_mixture.fraction == 0.5 + assert material_mixture.fraction.value == 0.5 assert str(material_mixture._fraction.unit) == 'dimensionless' assert_almost_equal(material_mixture.sld, 4.186) assert_almost_equal(material_mixture.isld, 0) q = Material(6.908, -0.278, 'Boron') material_mixture.material_b = q assert material_mixture.name == 'EasyMaterial/Boron' - assert material_mixture.fraction == 0.5 + assert material_mixture.fraction.value == 0.5 assert str(material_mixture._fraction.unit) == 'dimensionless' assert_almost_equal(material_mixture.sld, 5.54700) assert_almost_equal(material_mixture.isld, -0.1390) r = Material(0.00, 0.00, 'ACMW') material_mixture.material_b = r assert material_mixture.name == 'EasyMaterial/ACMW' - assert material_mixture.fraction == 0.5 + assert material_mixture.fraction.value == 0.5 assert str(material_mixture._fraction.unit) == 'dimensionless' assert_almost_equal(material_mixture.sld, 2.0930) assert_almost_equal(material_mixture.isld, 0.0000) @@ -93,7 +96,7 @@ def test_from_pars(self): p = Material() q = Material(6.908, -0.278, 'Boron') material_mixture = MaterialMixture(p, q, 0.2) - assert material_mixture.fraction == 0.2 + assert material_mixture.fraction.value == 0.2 assert str(material_mixture._fraction.unit) == 'dimensionless' assert_almost_equal(material_mixture.sld, 4.7304) assert_almost_equal(material_mixture.isld, -0.0556) @@ -139,3 +142,39 @@ def test_update_name(self) -> None: # Expect assert material_mixture.name == 'name_a/name_b' + + def test_calculator_binding_uses_mixed_sld(self) -> None: + """Regression: the calculator wrapper must bind to the mixture's own + ``_sld``/``_isld`` (the weighted average), not to either child material's + sld/isld parameter. Without an explicit ``_get_linkable_attributes`` + override the inherited dir-walk picks up the first matching child + parameter and the wrapper silently gets the wrong SLD. + """ + from easyreflectometry.calculators import CalculatorFactory + + interface = CalculatorFactory() + material_a = Material(sld=2.0, isld=0.0) + material_b = Material(sld=6.0, isld=0.0) + mixture = MaterialMixture(material_a, material_b, fraction=0.25, interface=interface) + + # 2 * 0.75 + 6 * 0.25 = 1.5 + 1.5 = 3.0 + assert_almost_equal(mixture.sld, 3.0) + wrapper_material = interface()._wrapper.storage['material'][mixture.unique_name] + assert_almost_equal(wrapper_material.real.value, 3.0) + assert_almost_equal(wrapper_material.imag.value, 0.0) + + def test_mutation_propagates_after_round_trip(self) -> None: + """Regression: after ``from_dict`` swaps in the saved ``_fraction`` + Parameter, the dependency graph for ``_sld``/``_isld`` must point at + the live ``_fraction`` (not the temp Parameter created from the + float kwarg in ``__init__``).""" + p = MaterialMixture(Material(sld=2.0), Material(sld=6.0), fraction=0.25) + p_dict = p.as_dict() + global_object.map._clear() + + q = MaterialMixture.from_dict(p_dict) + assert_almost_equal(q.sld, 3.0) + + q.fraction = 0.8 + # 2 * 0.2 + 6 * 0.8 = 0.4 + 4.8 = 5.2 + assert_almost_equal(q.sld, 5.2) diff --git a/tests/sample/elements/materials/test_material_solvated.py b/tests/sample/elements/materials/test_material_solvated.py index a50211d5..3af8e379 100644 --- a/tests/sample/elements/materials/test_material_solvated.py +++ b/tests/sample/elements/materials/test_material_solvated.py @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2024 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + from unittest.mock import MagicMock import pytest @@ -29,7 +32,7 @@ def test_init(self, material_solvated: MaterialSolvated) -> None: # When Then Expect assert material_solvated.material_a == self.material assert material_solvated.material_b == self.solvent - assert material_solvated.fraction == 0.1 + assert material_solvated.fraction.value == 0.1 assert material_solvated.name == 'name' assert material_solvated.interface == self.mock_interface self.mock_interface.generate_bindings.call_count == 2 @@ -66,14 +69,14 @@ def test_set_solvent(self, material_solvated: MaterialSolvated) -> None: def test_solvent_fraction(self, material_solvated: MaterialSolvated) -> None: # When Then Expect - assert material_solvated.solvent_fraction == 0.1 + assert material_solvated.solvent_fraction.value == 0.1 def test_set_solvent_fraction(self, material_solvated: MaterialSolvated) -> None: # When Then material_solvated.solvent_fraction = 1.0 # Expect - assert material_solvated.solvent_fraction == 1.0 + assert material_solvated.solvent_fraction.value == 1.0 def test_set_solvent_fraction_exception(self, material_solvated: MaterialSolvated) -> None: # When Then Expect @@ -130,3 +133,29 @@ def test_update_name(self, material_solvated: MaterialSolvated) -> None: # Expect assert material_solvated.name == 'name_a in name_b' + + def test_solvent_fraction_metadata_survives_round_trip(self) -> None: + """Regression: ``solvent_fraction`` is a constructor argument, but its + backing storage is ``_fraction`` (inherited from MaterialMixture). + ``ModelBase.from_dict`` would write the saved Parameter to + ``_solvent_fraction`` (an orphan), silently resetting the active + parameter to constructor defaults. We re-route to ``_fraction`` in + ``MaterialSolvated.from_dict``. + """ + material = Material(sld=6.36, isld=0, name='D2O') + solvent = Material(sld=-0.561, isld=0, name='H2O') + p = MaterialSolvated(material=material, solvent=solvent, solvent_fraction=0.3) + # Tweak fit metadata that the default would not have. + p.solvent_fraction.fixed = False + p.solvent_fraction.min = 0.12 + + p_dict = p.as_dict() + global_object.map._clear() + + q = MaterialSolvated.from_dict(p_dict) + + assert q.solvent_fraction.value == 0.3 + assert q.solvent_fraction.fixed is False + assert q.solvent_fraction.min == 0.12 + # No orphan field. + assert '_solvent_fraction' not in q.__dict__ diff --git a/tests/sample/test_base_core.py b/tests/sample/test_base_core.py new file mode 100644 index 00000000..e9b32df7 --- /dev/null +++ b/tests/sample/test_base_core.py @@ -0,0 +1,284 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + +"""Tests for BaseCore class — the new ModelBase-based foundation for sample-tree objects.""" + +from __future__ import annotations + +from unittest.mock import MagicMock + +import pytest +from easyscience import global_object +from easyscience.variable import Parameter + +from easyreflectometry.sample.base_core import BaseCore + +# --------------------------------------------------------------------------- +# Minimal concrete subclass for testing the abstract BaseCore +# --------------------------------------------------------------------------- + + +class _ConcreteCore(BaseCore): + """A non-abstract BaseCore that exposes a simple ``_dict_repr``.""" + + def __init__(self, name='TestCore', interface=None, unique_name=None, **kwargs): + super().__init__(name=name, interface=interface, unique_name=unique_name, **kwargs) + + @property + def _dict_repr(self) -> dict[str, str]: + return {self.name: {'type': 'concrete'}} + + +# --------------------------------------------------------------------------- +# Tests +# --------------------------------------------------------------------------- + + +class TestBaseCore: + """Direct unit tests for the BaseCore abstract base class.""" + + # ---- construction ---- + + def test_default_construction(self) -> None: + """A minimal concrete subclass should construct without errors.""" + obj = _ConcreteCore(name='Test') + assert obj.name == 'Test' + assert obj.interface is None + assert obj.user_data == {} + + def test_construction_with_interface(self) -> None: + """Passing an interface should trigger generate_bindings.""" + mock_iface = MagicMock() + obj = _ConcreteCore(name='WithIface', interface=mock_iface) + assert obj.interface is mock_iface + mock_iface.generate_bindings.assert_called_once_with(obj) + + def test_construction_with_unique_name(self) -> None: + """unique_name is passed through to ModelBase.""" + obj = _ConcreteCore(name='Uniq', unique_name='my_unique') + assert obj.unique_name == 'my_unique' + + def test_construction_kwargs_stored_as_attributes(self) -> None: + """Transitional kwargs path: extra kwargs become plain instance attrs.""" + child = Parameter('extra_param', 5.0) + obj = _ConcreteCore(name='Kwargs', extra=child, extra2=42) + assert obj.extra is child + assert obj.extra2 == 42 + + # ---- name property ---- + + def test_name_getter(self) -> None: + obj = _ConcreteCore(name='MyName') + assert obj.name == 'MyName' + + def test_name_setter(self) -> None: + obj = _ConcreteCore(name='Original') + obj.name = 'Changed' + assert obj.name == 'Changed' + + # ---- interface property ---- + + def test_interface_set_to_none(self) -> None: + obj = _ConcreteCore(name='NoIface') + obj.interface = None + assert obj.interface is None + + def test_interface_set_triggers_bindings(self) -> None: + obj = _ConcreteCore(name='Late') + mock_iface = MagicMock() + obj.interface = mock_iface + mock_iface.generate_bindings.assert_called_once_with(obj) + + def test_interface_setter_does_not_call_generate_bindings_for_none(self) -> None: + obj = _ConcreteCore(name='NoneIface') + # Setting to None should be safe (no generate_bindings call) + obj.interface = None + assert obj.interface is None + + # ---- generate_bindings ---- + + def test_generate_bindings_raises_when_interface_is_none(self) -> None: + obj = _ConcreteCore(name='NoIface') + with pytest.raises(AttributeError, match='Interface error'): + obj.generate_bindings() + + def test_generate_bindings_propagates_to_children(self) -> None: + """Children with an interface setter receive the parent's interface.""" + mock_iface = MagicMock() + child = _ConcreteCore(name='Child') + child._interface = None # reset so we can observe propagation + obj = _ConcreteCore(name='Parent', child=child) + obj.interface = mock_iface + # The child should have received the interface too. + assert child.interface is mock_iface + + def test_generate_bindings_propagates_to_parameter_children(self) -> None: + """Parameters stored as plain attrs should not break binding propagation.""" + mock_iface = MagicMock() + param = Parameter('p', 1.0) + obj = _ConcreteCore(name='WithParam', p=param) + obj.interface = mock_iface + mock_iface.generate_bindings.assert_called_once_with(obj) + + # ---- _iter_public_children ---- + + def test_iter_public_children_includes_class_attrs(self) -> None: + child = _ConcreteCore(name='Child') + obj = _ConcreteCore(name='Parent', child=child) + children = list(obj._iter_public_children()) + assert child in children + + def test_iter_public_children_includes_instance_attrs(self) -> None: + param = Parameter('p', 1.0) + obj = _ConcreteCore(name='Parent', p=param) + children = list(obj._iter_public_children()) + assert param in children + + def test_iter_public_children_excludes_private(self) -> None: + obj = _ConcreteCore(name='Parent') + obj._private_thing = 'secret' + children = list(obj._iter_public_children()) + names = [getattr(c, 'name', c) for c in children] + assert 'secret' not in names + + def test_iter_public_children_excludes_interface_and_name(self) -> None: + obj = _ConcreteCore(name='Parent') + children = list(obj._iter_public_children()) + assert obj.interface not in children + + def test_iter_public_children_no_duplicates(self) -> None: + """If a child appears both as a class attr and instance attr, only one copy.""" + child = _ConcreteCore(name='Child') + obj = _ConcreteCore(name='Parent', child=child) + # Also set as attr with same id + obj.duplicate_ref = child + children = list(obj._iter_public_children()) + # child should appear only once + assert children.count(child) == 1 + + # ---- _has_interface_setter ---- + + def test_has_interface_setter_true(self) -> None: + assert BaseCore._has_interface_setter(_ConcreteCore) is True + + def test_has_interface_setter_false_for_bare_object(self) -> None: + assert BaseCore._has_interface_setter(object) is False + + def test_has_interface_setter_false_for_parameter(self) -> None: + """Parameter doesn't have an interface property.""" + assert BaseCore._has_interface_setter(Parameter) is False + + # ---- compatibility shims ---- + + def test_get_linkable_attributes(self) -> None: + param = Parameter('p', 1.0) + obj = _ConcreteCore(name='Core', p=param) + result = obj._get_linkable_attributes() + assert param in result + + def test_get_parameters_shim(self) -> None: + param = Parameter('p', 1.0) + obj = _ConcreteCore(name='Core', p=param) + result = obj.get_parameters() + assert param in result + + def test_add_component(self) -> None: + obj = _ConcreteCore(name='Core') + comp = Parameter('comp', 42.0) + obj._add_component('my_comp', comp) + assert obj.my_comp is comp + + # ---- get_all_variables ---- + + def test_get_all_variables_includes_descriptors(self) -> None: + param = Parameter('p', 1.0) + obj = _ConcreteCore(name='Core', p=param) + result = obj.get_all_variables() + assert param in result + + def test_get_all_variables_recurses_into_children(self) -> None: + inner_param = Parameter('inner', 2.0) + child = _ConcreteCore(name='Child', p=inner_param) + obj = _ConcreteCore(name='Parent', child=child) + result = obj.get_all_variables() + assert inner_param in result + + def test_get_all_variables_no_duplicates_across_children(self) -> None: + param = Parameter('shared', 1.0) + child_a = _ConcreteCore(name='A', p=param) + child_b = _ConcreteCore(name='B', p=param) + obj = _ConcreteCore(name='Parent', a=child_a, b=child_b) + result = obj.get_all_variables() + assert result.count(param) == 1 + + # ---- to_dict / as_dict ---- + + def test_to_dict_skips_interface(self) -> None: + mock_iface = MagicMock() + obj = _ConcreteCore(name='Core', interface=mock_iface) + d = obj.to_dict() + assert 'interface' not in d + + def test_to_dict_skips_unique_name_by_default(self) -> None: + obj = _ConcreteCore(name='Core', unique_name='my_unique') + d = obj.to_dict() + assert 'unique_name' not in d + + def test_to_dict_includes_name(self) -> None: + obj = _ConcreteCore(name='MyName') + d = obj.to_dict() + assert d.get('name') == 'MyName' + + def test_as_dict_is_alias_for_to_dict(self) -> None: + obj = _ConcreteCore(name='Core') + assert obj.as_dict() == obj.to_dict() + + def test_to_dict_respects_custom_skip(self) -> None: + obj = _ConcreteCore(name='Core') + d = obj.to_dict(skip=['name']) + assert 'name' not in d + + def test_to_dict_skip_not_mutated_by_callee(self) -> None: + """Caller's skip list must not be mutated.""" + obj = _ConcreteCore(name='Core') + skip = ['name'] + obj.to_dict(skip=skip) + assert skip == ['name'] # not appended-to + + # ---- repr ---- + + def test_repr_returns_yaml_string(self) -> None: + obj = _ConcreteCore(name='Test') + r = repr(obj) + assert 'Test' in r + assert 'concrete' in r + + # ---- user_data ---- + + def test_user_data_is_dict(self) -> None: + obj = _ConcreteCore(name='Core') + obj.user_data['key'] = 'value' + assert obj.user_data['key'] == 'value' + + # ---- round-trip ---- + + def test_basic_round_trip_via_material(self) -> None: + """Round-trip through a real subclass (Material) to verify BaseCore serialization.""" + from easyreflectometry.sample.elements.materials.material import Material + + global_object.map._clear() + obj = Material(sld=2.0, isld=0.5, name='TestMat') + d = obj.to_dict() + global_object.map._clear() + + restored = Material.from_dict(d) + assert restored.name == 'TestMat' + assert restored.sld.value == 2.0 + assert restored.isld.value == 0.5 + + def test_round_trip_skips_interface(self) -> None: + """Round-trip via to_dict → from_dict should strip the interface.""" + global_object.map._clear() + obj = _ConcreteCore(name='WithIface') + d = obj.to_dict() + assert 'interface' not in d diff --git a/tests/special/test_calculations.py b/tests/special/test_calculations.py index e7f1f89f..10ca1ca1 100644 --- a/tests/special/test_calculations.py +++ b/tests/special/test_calculations.py @@ -1,4 +1,6 @@ -__author__ = 'github.com/arm61' +# SPDX-FileCopyrightText: 2022 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + import unittest diff --git a/tests/summary/test_summary.py b/tests/summary/test_summary.py index 3bbb186e..a636b504 100644 --- a/tests/summary/test_summary.py +++ b/tests/summary/test_summary.py @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2024 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + import os from unittest.mock import MagicMock @@ -133,7 +136,7 @@ def test_experiments_section(self, project: Project) -> None: assert 'No. of data points' in html assert '408' in html assert 'Resolution function' in html - assert 'PercentageFwhm' in html + assert 'Pointwise' in html def test_experiments_section_percentage_fhwm(self, project: Project) -> None: # When @@ -192,17 +195,34 @@ def test_save_fit_experiment_plot(self, project: Project, tmp_path) -> None: # Expect assert os.path.exists(file_path) - def test_figures_section(self, project: Project) -> None: + def test_figures_section_static(self, project: Project) -> None: # When summary = Summary(project) summary.save_sld_plot = MagicMock() summary.save_fit_experiment_plot = MagicMock() # Then - html = summary._figures_section() + html = summary._figures_section(interactive=False) # Expect summary.save_sld_plot.assert_called_once() summary.save_fit_experiment_plot.assert_called_once() assert 'sld_plot' in html assert 'fit_experiment_plot' in html + + def test_figures_section_interactive(self, project: Project) -> None: + # When + summary = Summary(project) + summary.save_sld_plot = MagicMock() + summary.save_fit_experiment_plot = MagicMock() + + # Then + html = summary._figures_section(interactive=True) + + # Expect + # Interactive figures must not fall back to the static image plots. + summary.save_sld_plot.assert_not_called() + summary.save_fit_experiment_plot.assert_not_called() + # Two interactive plotly charts with the library embedded inline once. + assert html.count('class="plotly-graph-div"') == 2 + assert 'Plotly.newPlot' in html diff --git a/tests/test_bayesian.py b/tests/test_bayesian.py new file mode 100644 index 00000000..6d0c3148 --- /dev/null +++ b/tests/test_bayesian.py @@ -0,0 +1,886 @@ +# SPDX-FileCopyrightText: 2026 EasyReflectometry contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Tests for the Bayesian analysis module.""" + +import numpy as np +import pytest + + +@pytest.fixture +def sample_draws(): + """Generate synthetic posterior draws for testing.""" + rng = np.random.default_rng(42) + n_samples = 100 + # Two parameters: 'thickness' and 'sld' + thickness = rng.normal(loc=250, scale=10, size=n_samples) + sld = rng.normal(loc=2.0, scale=0.2, size=n_samples) + draws = np.column_stack([thickness, sld]) + param_names = ['Film_thickness', 'Film_sld'] + return draws, param_names + + +class TestPosteriorSummary: + def test_returns_string(self, sample_draws): + from easyreflectometry.analysis.bayesian import posterior_summary + + draws, param_names = sample_draws + result = posterior_summary(draws, param_names) + assert isinstance(result, str) + assert 'parameter' in result + assert 'mean' in result + assert 'sd' in result + + def test_header_uses_quantile_labels(self, sample_draws): + """The header should label the columns as equal-tailed quantiles, not HDI.""" + from easyreflectometry.analysis.bayesian import posterior_summary + + draws, param_names = sample_draws + result = posterior_summary(draws, param_names) + header = result.splitlines()[0] + assert 'q2.5%' in header + assert 'q97.5%' in header + assert 'hdi' not in header.lower() + + def test_contains_param_names(self, sample_draws): + from easyreflectometry.analysis.bayesian import posterior_summary + + draws, param_names = sample_draws + result = posterior_summary(draws, param_names) + for name in param_names: + assert name in result + + +class TestCredibleIntervals: + def test_returns_dict(self, sample_draws): + from easyreflectometry.analysis.bayesian import credible_intervals + + draws, param_names = sample_draws + result = credible_intervals(draws, param_names) + assert isinstance(result, dict) + for name in param_names: + assert name in result + lo, hi = result[name] + assert lo < hi + + def test_alpha_95_coverage(self, sample_draws): + from easyreflectometry.analysis.bayesian import credible_intervals + + draws, param_names = sample_draws + result = credible_intervals(draws, param_names, alpha=0.95) + for i, name in enumerate(param_names): + lo, hi = result[name] + # 95% interval should contain at least 90% of samples + col = draws[:, i] + inside = np.sum((col >= lo) & (col <= hi)) + assert inside / len(col) >= 0.90 + + def test_alpha_50_narrower(self, sample_draws): + from easyreflectometry.analysis.bayesian import credible_intervals + + draws, param_names = sample_draws + ci_95 = credible_intervals(draws, param_names, alpha=0.95) + ci_50 = credible_intervals(draws, param_names, alpha=0.50) + for name in param_names: + assert (ci_95[name][1] - ci_95[name][0]) > (ci_50[name][1] - ci_50[name][0]) + + +class TestPosteriorResults: + def test_repr(self, sample_draws): + from easyreflectometry.analysis.bayesian import PosteriorResults + + draws, param_names = sample_draws + pr = PosteriorResults(draws, param_names) + rep = repr(pr) + assert 'PosteriorResults' in rep + assert str(draws.shape[0]) in rep + + def test_summary_delegates(self, sample_draws): + from easyreflectometry.analysis.bayesian import PosteriorResults + + draws, param_names = sample_draws + pr = PosteriorResults(draws, param_names) + summary_str = pr.summary() + assert isinstance(summary_str, str) + assert 'parameter' in summary_str + + def test_credible_interval_delegates(self, sample_draws): + from easyreflectometry.analysis.bayesian import PosteriorResults + + draws, param_names = sample_draws + pr = PosteriorResults(draws, param_names) + ci = pr.credible_interval(alpha=0.95) + assert isinstance(ci, dict) + for name in param_names: + assert name in ci + + +class TestPosteriorPredictiveReflectivity: + def test_returns_tuples(self, sample_draws): + """Test with a mock model that returns a constant array.""" + from unittest.mock import MagicMock + + from easyreflectometry.analysis.bayesian import posterior_predictive_reflectivity + + draws, param_names = sample_draws + mock_model = MagicMock() + mock_model.unique_name = 'test_model' + mock_model.interface = MagicMock() + mock_model.interface.fit_func = MagicMock(return_value=np.ones(50)) + mock_model.get_parameters = MagicMock(return_value=[]) + + q_values = np.linspace(0.01, 0.3, 50) + median, lower, upper = posterior_predictive_reflectivity( + draws, + param_names, + mock_model, + q_values, + n_samples=20, + ) + assert median.shape == (50,) + assert lower.shape == (50,) + assert upper.shape == (50,) + + +class TestPosteriorPredictiveSLDProfile: + def test_returns_tuples(self, sample_draws): + """Test with a mock model that returns constant z and sld.""" + from unittest.mock import MagicMock + + from easyreflectometry.analysis.bayesian import posterior_predictive_sld_profile + + draws, param_names = sample_draws + mock_model = MagicMock() + mock_model.unique_name = 'test_model' + mock_model.interface = MagicMock() + mock_model.interface.sld_profile = MagicMock(return_value=(np.linspace(0, 500, 100), np.ones(100) * 2.0)) + mock_model.get_parameters = MagicMock(return_value=[]) + + z, median, lower, upper = posterior_predictive_sld_profile( + draws, + param_names, + mock_model, + n_samples=20, + ) + assert z.shape == (100,) + assert median.shape == (100,) + assert lower.shape == (100,) + assert upper.shape == (100,) + + +class TestCornerPlot: + def test_plot_corner_returns_plotly_figure(self, sample_draws): + """plot_corner returns a Plotly Figure built from posterior draws.""" + try: + from plotly.graph_objects import Figure + + from easyreflectometry.analysis.bayesian import plot_corner + except ImportError: + pytest.skip('plotly not installed') + + draws, param_names = sample_draws + fig = plot_corner(draws, param_names) + assert isinstance(fig, Figure) + assert len(fig.data) > 0 + + +class TestSaveRestoreParameterState: + def test_save_and_restore(self): + """Test that parameter state save/restore works correctly.""" + from easyreflectometry.analysis.bayesian import _restore_parameter_state + from easyreflectometry.analysis.bayesian import _save_parameter_state + + # Use simple objects that support attribute assignment + class MockParam: + def __init__(self, unique_name, raw_value, error): + self.unique_name = unique_name + self.value = raw_value + self.error = error + + param1 = MockParam('param_a', 1.5, 0.1) + param2 = MockParam('param_b', 3.0, 0.2) + + class MockModel: + def get_parameters(self): + return [param1, param2] + + model = MockModel() + + state = _save_parameter_state(model) + assert state['param_a'] == (1.5, 0.1) + assert state['param_b'] == (3.0, 0.2) + + # Modify values + param1.raw_value = 99.0 + param1.value = 99.0 + param2.raw_value = 99.0 + param2.value = 99.0 + + _restore_parameter_state(model, state) + assert param1.value == 1.5 + assert param1.error == 0.1 + assert param2.value == 3.0 + assert param2.error == 0.2 + + +class TestApplyDraw: + def test_apply_draw_updates_parameters(self): + """Test that _apply_draw sets parameter values correctly.""" + from easyreflectometry.analysis.bayesian import _apply_draw + + class MockParam: + def __init__(self, unique_name): + self.unique_name = unique_name + self.value = None + + param_a = MockParam('thickness') + param_b = MockParam('sld') + + class MockModel: + def get_parameters(self): + return [param_a, param_b] + + model = MockModel() + draws = np.array([[250.0, 2.0], [260.0, 2.1]]) + param_names = ['thickness', 'sld'] + + _apply_draw(model, draws, param_names, row=0) + assert param_a.value == 250.0 + assert param_b.value == 2.0 + + _apply_draw(model, draws, param_names, row=1) + assert param_a.value == 260.0 + assert param_b.value == 2.1 + + +class TestGelmanRubinRequiresMultipleChains: + def test_raises_on_2d_draws(self, sample_draws): + """R-hat is undefined for a single chain; ``gelman_rubin`` must reject 2-D input.""" + pytest.importorskip('arviz') + from easyreflectometry.analysis.bayesian import PosteriorResults + + draws, param_names = sample_draws # shape (n_samples, n_params) + pr = PosteriorResults(draws, param_names) + with pytest.raises(ValueError, match='at least 2 chains'): + pr.gelman_rubin() + + def test_raises_on_single_chain_3d(self, sample_draws): + """Even with an explicit chain axis, n_chains == 1 must raise.""" + pytest.importorskip('arviz') + from easyreflectometry.analysis.bayesian import PosteriorResults + + draws, param_names = sample_draws + single_chain = draws[np.newaxis, ...] # (1, n_draws, n_params) + pr = PosteriorResults(single_chain, param_names) + with pytest.raises(ValueError, match='at least 2 chains'): + pr.gelman_rubin() + + def test_accepts_multi_chain(self, sample_draws, monkeypatch): + """With n_chains >= 2 the diagnostic should forward to arviz and return its values.""" + pytest.importorskip('arviz') + from easyreflectometry.analysis import bayesian as bayesian_mod + from easyreflectometry.analysis.bayesian import PosteriorResults + + draws, param_names = sample_draws + rng = np.random.default_rng(7) + second = np.column_stack([ + rng.normal(loc=250, scale=10, size=draws.shape[0]), + rng.normal(loc=2.0, scale=0.2, size=draws.shape[0]), + ]) + multi = np.stack([draws, second], axis=0) # (2, n_draws, n_params) + + # Stub arviz.rhat so the test verifies the wrapper's contract (≥2 chains + # accepted, results unpacked per parameter) without depending on arviz's + # small-sample numerics, which can raise platform-specific TypeErrors. + class _FakeRhatVar: + def __init__(self, value: float) -> None: + self.values = np.array(value) + + fake_rhat = {name: _FakeRhatVar(1.01 + 0.001 * i) for i, name in enumerate(param_names)} + monkeypatch.setattr(bayesian_mod._arviz, 'rhat', lambda _data: fake_rhat) + + pr = PosteriorResults(multi, param_names) + result = pr.gelman_rubin() + assert isinstance(result, dict) + for i, name in enumerate(param_names): + assert name in result + assert result[name] == pytest.approx(1.01 + 0.001 * i) + + +class TestPlotFigureFallbackWarnings: + """When ``return_figure=True`` and plotly is missing, the helpers must warn.""" + + def test_plot_trace_warns_without_plotly(self, sample_draws, monkeypatch): + import builtins + + from easyreflectometry.analysis.bayesian import plot_trace + + real_import = builtins.__import__ + + def _fake_import(name, *args, **kwargs): + if name.startswith('plotly'): + raise ImportError('plotly disabled for test') + return real_import(name, *args, **kwargs) + + monkeypatch.setattr(builtins, '__import__', _fake_import) + + draws, param_names = sample_draws + with pytest.warns(UserWarning, match='plotly'): + result = plot_trace(draws, param_names, return_figure=True) + assert result is None + + def test_plot_distribution_warns_without_plotly(self, sample_draws, monkeypatch): + import builtins + + from easyreflectometry.analysis.bayesian import plot_distribution + + real_import = builtins.__import__ + + def _fake_import(name, *args, **kwargs): + if name.startswith('plotly'): + raise ImportError('plotly disabled for test') + return real_import(name, *args, **kwargs) + + monkeypatch.setattr(builtins, '__import__', _fake_import) + + draws, param_names = sample_draws + with pytest.warns(UserWarning, match='plotly'): + result = plot_distribution(draws, param_names, return_figure=True) + assert result is None + + +# =================================================================== +# Persistence helpers — save_posterior / load_posterior +# =================================================================== + + +class TestSaveLoadPosterior: + """Tests for ``save_posterior`` and ``load_posterior``.""" + + @pytest.fixture + def mock_posterior_results(self, sample_draws): + """Build a PosteriorResults with a real-looking mocked sampler_state.""" + from unittest.mock import MagicMock + + from easyreflectometry.analysis.bayesian import PosteriorResults + + draws, param_names = sample_draws + + # Build a mock that passes isinstance(obj, MCMCDraw) for the + # type guard in save_posterior. We use a non-spec MagicMock and + # reassign its __class__ so isinstance succeeds. + import bumps.dream.state as _bds + + mock_state = MagicMock() + mock_state.__class__ = _bds.MCMCDraw + + mock_state.Nvar = draws.shape[1] + mock_state.Npop = 5 + mock_state.labels = [f'p{name}' for name in param_names] + mock_draw = MagicMock() + mock_draw.points = draws + mock_draw.logp = np.zeros(draws.shape[0]) + mock_state.draw.return_value = mock_draw + + pr = PosteriorResults( + draws=draws, + param_names=param_names, + logp=np.zeros(draws.shape[0]), + sampler_state=mock_state, + ) + return pr + + def test_save_posterior_no_state_raises(self, sample_draws): + """PosteriorResults without sampler_state raises ValueError.""" + from easyreflectometry.analysis.bayesian import PosteriorResults + from easyreflectometry.analysis.bayesian import save_posterior + + draws, param_names = sample_draws + pr = PosteriorResults(draws, param_names) + with pytest.raises(ValueError, match='no sampler_state'): + save_posterior(pr, 'dummy') + + def test_save_posterior_wrong_state_type_raises(self, sample_draws): + """Non-MCMCDraw sampler_state raises TypeError.""" + from unittest.mock import MagicMock + + from easyreflectometry.analysis.bayesian import PosteriorResults + from easyreflectometry.analysis.bayesian import save_posterior + + draws, param_names = sample_draws + pr = PosteriorResults(draws, param_names, sampler_state=MagicMock()) + with pytest.raises(TypeError, match='MCMCDraw'): + save_posterior(pr, 'dummy') + + def test_save_and_load_roundtrip(self, mock_posterior_results, monkeypatch, tmp_path): + """Save then load, verify draws, param_names, logp, and state.""" + from unittest.mock import MagicMock + + # Mock save_state and load_state + import bumps.dream.state as _bds + + from easyreflectometry.analysis.bayesian import load_posterior + from easyreflectometry.analysis.bayesian import save_posterior + + saved_state_ref = mock_posterior_results.sampler_state + monkeypatch.setattr(_bds, 'save_state', MagicMock()) + monkeypatch.setattr(_bds, 'load_state', MagicMock(return_value=saved_state_ref)) + + prefix = str(tmp_path / 'test_run') + save_posterior(mock_posterior_results, prefix) + + # Verify save_state was called + _bds.save_state.assert_called_once_with(saved_state_ref, prefix) + + loaded = load_posterior(prefix) + + assert np.allclose(loaded.draws, mock_posterior_results.draws) + assert loaded.param_names == mock_posterior_results.param_names + assert loaded.sampler_state is saved_state_ref + + def test_save_convenience_method(self, mock_posterior_results, monkeypatch, tmp_path): + """PosteriorResults.save() delegates to save_posterior.""" + from unittest.mock import MagicMock + + import bumps.dream.state as _bds + + monkeypatch.setattr(_bds, 'save_state', MagicMock()) + + prefix = str(tmp_path / 'test_convenience') + mock_posterior_results.save(prefix) + + _bds.save_state.assert_called_once_with(mock_posterior_results.sampler_state, prefix) + + def test_load_posterior_skip(self, mock_posterior_results, monkeypatch, tmp_path): + """load_posterior with skip>0 forwards skip to load_state.""" + from unittest.mock import MagicMock + + import bumps.dream.state as _bds + + from easyreflectometry.analysis.bayesian import load_posterior + + monkeypatch.setattr(_bds, 'load_state', MagicMock(return_value=mock_posterior_results.sampler_state)) + monkeypatch.setattr(_bds, 'save_state', MagicMock()) + + prefix = str(tmp_path / 'test_skip') + load_posterior(prefix, skip=5) + + _bds.load_state.assert_called_once_with(prefix, skip=5) + + +class TestPlotDistributionExported: + def test_in_analysis_namespace(self): + """``plot_distribution`` should be importable from the analysis package.""" + from easyreflectometry import analysis + + assert hasattr(analysis, 'plot_distribution') + assert 'plot_distribution' in analysis.__all__ + + +# =================================================================== +# Label wrapping helper +# =================================================================== + + +class TestWrapPairLabel: + def test_empty_string_unchanged(self): + from easyreflectometry.analysis.bayesian import _wrap_pair_label + + assert _wrap_pair_label('') == '' + + def test_short_name_unchanged(self): + from easyreflectometry.analysis.bayesian import _wrap_pair_label + + assert _wrap_pair_label('thickness') == 'thickness' + + def test_dotted_name_breaks_on_dots(self): + from easyreflectometry.analysis.bayesian import _wrap_pair_label + + assert _wrap_pair_label('layer1.thickness') == 'layer1.
thickness' + + def test_long_multiword_name_wraps(self): + from easyreflectometry.analysis.bayesian import _wrap_pair_label + + wrapped = _wrap_pair_label('a very long parameter name indeed', max_len=16) + assert '
' in wrapped + assert wrapped.replace('
', ' ') == 'a very long parameter name indeed' + + def test_long_single_word_unchanged(self): + from easyreflectometry.analysis.bayesian import _wrap_pair_label + + name = 'averyveryverylongsingleword' + assert _wrap_pair_label(name, max_len=16) == name + + +# =================================================================== +# Optional-dependency guards +# =================================================================== + + +class TestRequireHelpers: + def test_require_arviz_raises_when_unavailable(self, monkeypatch): + from easyreflectometry.analysis import bayesian as bayesian_mod + + monkeypatch.setattr(bayesian_mod, '_HAS_ARVIZ', False) + with pytest.raises(ImportError, match='arviz'): + bayesian_mod._require_arviz() + + def test_require_plotly_raises_when_unavailable(self, monkeypatch): + import builtins + + from easyreflectometry.analysis.bayesian import _require_plotly + + real_import = builtins.__import__ + + def _fake_import(name, *args, **kwargs): + if name.startswith('plotly'): + raise ImportError('plotly disabled for test') + return real_import(name, *args, **kwargs) + + monkeypatch.setattr(builtins, '__import__', _fake_import) + with pytest.raises(ImportError, match='plotly'): + _require_plotly() + + def test_gelman_rubin_warns_and_returns_none_without_arviz(self, sample_draws, monkeypatch): + from easyreflectometry.analysis import bayesian as bayesian_mod + from easyreflectometry.analysis.bayesian import PosteriorResults + + draws, param_names = sample_draws + pr = PosteriorResults(draws, param_names) + monkeypatch.setattr(bayesian_mod, '_HAS_ARVIZ', False) + with pytest.warns(UserWarning, match='arviz'): + result = pr.gelman_rubin() + assert result is None + + +# =================================================================== +# arviz data conversion +# =================================================================== + + +class TestToArvizData: + def test_2d_draws_become_single_chain(self, sample_draws): + pytest.importorskip('arviz') + from easyreflectometry.analysis.bayesian import _to_arviz_data + + draws, param_names = sample_draws + idata = _to_arviz_data(draws, param_names) + posterior = idata.posterior + # Dimension naming varies across arviz versions, so assert on the + # contract itself: every sample survives conversion with its values + # intact, in a single chain. + assert posterior.sizes['chain'] == 1 + for i, name in enumerate(param_names): + values = np.asarray(posterior[name].values).reshape(-1) + assert values.size == draws.shape[0] + assert np.allclose(values, draws[:, i]) + + +# =================================================================== +# Plot construction (plotly available) +# =================================================================== + + +class TestPlotTraceFigure: + def test_returns_figure_for_2d_draws(self, sample_draws): + Figure = pytest.importorskip('plotly.graph_objects').Figure + from easyreflectometry.analysis.bayesian import plot_trace + + draws, param_names = sample_draws + fig = plot_trace(draws, param_names, return_figure=True) + assert isinstance(fig, Figure) + # One line trace and one histogram per parameter for the single chain. + assert len(fig.data) == 2 * len(param_names) + + def test_returns_figure_for_multi_chain_draws(self, sample_draws): + Figure = pytest.importorskip('plotly.graph_objects').Figure + from easyreflectometry.analysis.bayesian import plot_trace + + draws, param_names = sample_draws + multi = np.stack([draws, draws + 1.0], axis=0) # (2, n_draws, n_params) + fig = plot_trace(multi, param_names, return_figure=True) + assert isinstance(fig, Figure) + assert len(fig.data) == 2 * 2 * len(param_names) + + def test_inline_path_delegates_to_arviz(self, sample_draws, monkeypatch): + pytest.importorskip('arviz') + from unittest.mock import MagicMock + + from easyreflectometry.analysis import bayesian as bayesian_mod + + draws, param_names = sample_draws + mock_plot = MagicMock() + monkeypatch.setattr(bayesian_mod._arviz, 'plot_trace', mock_plot) + result = bayesian_mod.plot_trace(draws, param_names) + assert result is None + mock_plot.assert_called_once() + + +class TestPlotDistributionFigure: + def test_returns_none_without_return_figure(self, sample_draws): + from easyreflectometry.analysis.bayesian import plot_distribution + + draws, param_names = sample_draws + assert plot_distribution(draws, param_names) is None + + def test_returns_figure_with_expected_overlays(self, sample_draws): + Figure = pytest.importorskip('plotly.graph_objects').Figure + from easyreflectometry.analysis.bayesian import plot_distribution + + draws, param_names = sample_draws + logp = np.arange(draws.shape[0], dtype=float) + fig = plot_distribution(draws, param_names, logp=logp, return_figure=True) + assert isinstance(fig, Figure) + trace_names = {trace.name for trace in fig.data} + assert 'Posterior histogram' in trace_names + assert '95% credible interval' in trace_names + assert 'Median' in trace_names + # logp was supplied, so the best posterior sample line must be drawn. + assert 'Best posterior sample' in trace_names + + def test_accepts_3d_draws(self, sample_draws): + Figure = pytest.importorskip('plotly.graph_objects').Figure + from easyreflectometry.analysis.bayesian import plot_distribution + + draws, param_names = sample_draws + multi = np.stack([draws, draws], axis=0) # (2, n_draws, n_params) + fig = plot_distribution(multi, param_names, return_figure=True) + assert isinstance(fig, Figure) + + +class TestPosteriorResultsPlotDelegates: + def test_corner_returns_figure(self, sample_draws): + Figure = pytest.importorskip('plotly.graph_objects').Figure + from easyreflectometry.analysis.bayesian import PosteriorResults + + draws, param_names = sample_draws + fig = PosteriorResults(draws, param_names).corner() + assert isinstance(fig, Figure) + + def test_distribution_returns_figure(self, sample_draws): + Figure = pytest.importorskip('plotly.graph_objects').Figure + from easyreflectometry.analysis.bayesian import PosteriorResults + + draws, param_names = sample_draws + fig = PosteriorResults(draws, param_names).distribution() + assert isinstance(fig, Figure) + + def test_trace_delegates_to_plot_trace(self, sample_draws, monkeypatch): + from unittest.mock import MagicMock + + from easyreflectometry.analysis import bayesian as bayesian_mod + from easyreflectometry.analysis.bayesian import PosteriorResults + + draws, param_names = sample_draws + mock_plot = MagicMock() + monkeypatch.setattr(bayesian_mod, 'plot_trace', mock_plot) + PosteriorResults(draws, param_names).trace() + mock_plot.assert_called_once() + + +class TestPlotCornerEdgeCases: + def test_accepts_3d_draws(self, sample_draws): + Figure = pytest.importorskip('plotly.graph_objects').Figure + from easyreflectometry.analysis.bayesian import plot_corner + + draws, param_names = sample_draws + multi = np.stack([draws, draws], axis=0) + fig = plot_corner(multi, param_names) + assert isinstance(fig, Figure) + + def test_thins_scatter_for_large_posteriors(self): + go = pytest.importorskip('plotly.graph_objects') + from easyreflectometry.analysis.bayesian import _POSTERIOR_PAIR_SCATTER_MAX_POINTS + from easyreflectometry.analysis.bayesian import plot_corner + + rng = np.random.default_rng(3) + n_samples = _POSTERIOR_PAIR_SCATTER_MAX_POINTS * 2 + draws = rng.normal(size=(n_samples, 2)) + fig = plot_corner(draws, ['a', 'b']) + scatters = [t for t in fig.data if isinstance(t, go.Scatter) and t.name == 'Posterior samples'] + assert scatters + assert all(len(t.x) <= _POSTERIOR_PAIR_SCATTER_MAX_POINTS for t in scatters) + + def test_single_sample_falls_back_to_histogram(self): + go = pytest.importorskip('plotly.graph_objects') + from easyreflectometry.analysis.bayesian import plot_corner + + # A single draw defeats the KDE, so the diagonal must fall back to a + # histogram and the pair panels must omit contours. + draws = np.array([[250.0, 2.0]]) + fig = plot_corner(draws, ['thickness', 'sld']) + assert any(isinstance(t, go.Histogram) for t in fig.data) + assert not any(isinstance(t, go.Contour) for t in fig.data) + + +# =================================================================== +# Density-estimation helpers +# =================================================================== + + +class TestPosteriorAxisBounds: + def test_empty_returns_none(self): + from easyreflectometry.analysis.bayesian import _posterior_axis_bounds + + assert _posterior_axis_bounds(np.array([])) is None + assert _posterior_axis_bounds(np.array([np.nan, np.inf])) is None + + def test_constant_values_get_padding(self): + from easyreflectometry.analysis.bayesian import _posterior_axis_bounds + + lo, hi = _posterior_axis_bounds(np.array([5.0, 5.0, 5.0])) + assert lo < 5.0 < hi + + def test_constant_zero_gets_padding(self): + from easyreflectometry.analysis.bayesian import _posterior_axis_bounds + + lo, hi = _posterior_axis_bounds(np.zeros(3)) + assert lo < 0.0 < hi + + +class TestPosteriorDensityCurve: + def test_too_few_samples_returns_none(self): + from easyreflectometry.analysis.bayesian import _posterior_density_curve + + assert _posterior_density_curve(np.array([1.0])) is None + + def test_constant_samples_yield_gaussian_bump(self): + pytest.importorskip('scipy') + from easyreflectometry.analysis.bayesian import _posterior_density_curve + + result = _posterior_density_curve(np.full(50, 3.0)) + assert result is not None + grid, density = result + # Density peaks at the constant value and integrates to ~1. + assert grid[np.argmax(density)] == pytest.approx(3.0, abs=(grid[1] - grid[0])) + assert np.trapezoid(density, grid) == pytest.approx(1.0, rel=1e-6) + + def test_returns_none_without_scipy(self, sample_draws, monkeypatch): + import builtins + + from easyreflectometry.analysis.bayesian import _posterior_density_curve + + real_import = builtins.__import__ + + def _fake_import(name, *args, **kwargs): + if name.startswith('scipy'): + raise ImportError('scipy disabled for test') + return real_import(name, *args, **kwargs) + + monkeypatch.setattr(builtins, '__import__', _fake_import) + draws, _ = sample_draws + assert _posterior_density_curve(draws[:, 0]) is None + + +class TestPosteriorDensitySurface: + def test_degenerate_samples_return_none(self): + pytest.importorskip('scipy') + from easyreflectometry.analysis.bayesian import _posterior_density_surface + + constant = np.full(50, 1.0) + # Both axes constant. + assert _posterior_density_surface(constant, constant) is None + # One axis constant: rank-deficient covariance. + rng = np.random.default_rng(11) + assert _posterior_density_surface(constant, rng.normal(size=50)) is None + + def test_too_few_finite_samples_return_none(self): + pytest.importorskip('scipy') + from easyreflectometry.analysis.bayesian import _posterior_density_surface + + x = np.array([1.0, np.nan, np.nan]) + y = np.array([2.0, np.nan, np.nan]) + assert _posterior_density_surface(x, y) is None + + def test_valid_samples_return_grids(self, sample_draws): + pytest.importorskip('scipy') + from easyreflectometry.analysis.bayesian import _posterior_density_surface + + draws, _ = sample_draws + result = _posterior_density_surface(draws[:, 0], draws[:, 1]) + assert result is not None + x_grid, y_grid, density = result + assert density.shape == (len(y_grid), len(x_grid)) + + def test_returns_none_without_scipy(self, sample_draws, monkeypatch): + import builtins + + from easyreflectometry.analysis.bayesian import _posterior_density_surface + + real_import = builtins.__import__ + + def _fake_import(name, *args, **kwargs): + if name.startswith('scipy'): + raise ImportError('scipy disabled for test') + return real_import(name, *args, **kwargs) + + monkeypatch.setattr(builtins, '__import__', _fake_import) + draws, _ = sample_draws + assert _posterior_density_surface(draws[:, 0], draws[:, 1]) is None + + +class TestPosteriorContourColorscales: + def test_negative_correlation_selects_red_palette(self): + from easyreflectometry.analysis.bayesian import _POSTERIOR_NEGATIVE_CONTOUR_FILL_COLORSCALE + from easyreflectometry.analysis.bayesian import _posterior_contour_colorscales + + x = np.linspace(0, 1, 50) + fill, _ = _posterior_contour_colorscales(x, -x) + assert fill is _POSTERIOR_NEGATIVE_CONTOUR_FILL_COLORSCALE + + def test_positive_correlation_selects_blue_palette(self): + from easyreflectometry.analysis.bayesian import _POSTERIOR_CONTOUR_FILL_COLORSCALE + from easyreflectometry.analysis.bayesian import _posterior_contour_colorscales + + x = np.linspace(0, 1, 50) + fill, _ = _posterior_contour_colorscales(x, x) + assert fill is _POSTERIOR_CONTOUR_FILL_COLORSCALE + + +class TestPosteriorMarginalYRange: + def test_covers_histogram_and_kde_peaks(self, sample_draws): + from easyreflectometry.analysis.bayesian import _posterior_density_curve + from easyreflectometry.analysis.bayesian import _posterior_marginal_y_range + + draws, _ = sample_draws + values = draws[:, 0] + curve = _posterior_density_curve(values) + y_range = _posterior_marginal_y_range(values, curve) + assert y_range is not None + lo, hi = y_range + assert lo == 0.0 + hist, _ = np.histogram(values, bins=40, density=True) + assert hi >= np.max(hist) + + def test_no_data_returns_none(self): + from easyreflectometry.analysis.bayesian import _posterior_marginal_y_range + + assert _posterior_marginal_y_range(np.array([]), None) is None + + +# =================================================================== +# Metadata helpers +# =================================================================== + + +class TestMetadataHelpers: + def test_version_returns_string(self): + from easyreflectometry.analysis.bayesian import _easyreflectometry_version + + assert isinstance(_easyreflectometry_version(), str) + + def test_data_fingerprint_is_deterministic(self): + from easyreflectometry.analysis.bayesian import _data_fingerprint + + x = [np.array([1.0, 2.0])] + y = [np.array([3.0, 4.0])] + w = [np.array([0.1, 0.2])] + first = _data_fingerprint(x, y, w) + assert isinstance(first, str) + assert len(first) == 64 + assert _data_fingerprint(x, y, w) == first + assert _data_fingerprint(x, y, [np.array([0.1, 0.3])]) != first + + def test_data_fingerprint_returns_none_on_bad_input(self): + from easyreflectometry.analysis.bayesian import _data_fingerprint + + assert _data_fingerprint([object()], [], []) is None diff --git a/tests/test_data.py b/tests/test_data.py index 0ee95d94..d08402f2 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -1,5 +1,6 @@ -__author__ = 'github.com/arm61' -__version__ = '0.0.1' +# SPDX-FileCopyrightText: 2022 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + import os import unittest @@ -224,7 +225,10 @@ def test_load_txt_three_columns(self): assert coords_name in er_data['coords'] # xe should be zeros for 3-column file - assert_almost_equal(er_data['coords'][coords_name].variances, np.zeros_like(er_data['coords'][coords_name].values)) + assert_almost_equal( + er_data['coords'][coords_name].variances, + np.zeros_like(er_data['coords'][coords_name].values), + ) def test_load_txt_with_zero_errors(self): fpath = os.path.join(PATH_STATIC, 'ref_zero_var.txt') diff --git a/tests/test_fitting.py b/tests/test_fitting.py index 2a03a98c..9fd02a4b 100644 --- a/tests/test_fitting.py +++ b/tests/test_fitting.py @@ -1,7 +1,10 @@ -__author__ = 'github.com/arm61' +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + import os from unittest.mock import MagicMock +from unittest.mock import patch import numpy as np import pytest @@ -175,12 +178,10 @@ def test_fitting_with_manual_zero_variance(): variances[30:32] = 0.0 # 2 more zero variance points # Create scipp DataGroup manually - data = sc.DataGroup( - { - 'coords': {'Qz_0': sc.array(dims=['Qz_0'], values=qz_values)}, - 'data': {'R_0': sc.array(dims=['Qz_0'], values=r_values, variances=variances)}, - } - ) + data = sc.DataGroup({ + 'coords': {'Qz_0': sc.array(dims=['Qz_0'], values=qz_values)}, + 'data': {'R_0': sc.array(dims=['Qz_0'], values=r_values, variances=variances)}, + }) # Create a simple model for fitting si = Material(2.07, 0, 'Si') @@ -428,7 +429,12 @@ def test_prepare_fit_arrays_legacy_mask_drops_zero_variance(): assert np.allclose(x_out, [0.01, 0.03]) assert np.allclose(y_eff, [1.0, 0.6]) assert np.allclose(weights, [1.0 / np.sqrt(0.01), 1.0 / np.sqrt(0.04)]) - assert stats == {'valid': 2, 'mighell_substituted': 0, 'masked': 1, 'transformed_all_points': False} + assert stats == { + 'valid': 2, + 'mighell_substituted': 0, + 'masked': 1, + 'transformed_all_points': False, + } def test_prepare_fit_arrays_hybrid_transforms_zero_variance(): @@ -449,7 +455,12 @@ def test_prepare_fit_arrays_hybrid_transforms_zero_variance(): assert y_eff[1] == pytest.approx(0.8 + 0.8) # sigma = sqrt(y + 1) = sqrt(1.8) assert weights[1] == pytest.approx(1.0 / np.sqrt(1.8)) - assert stats == {'valid': 2, 'mighell_substituted': 1, 'masked': 0, 'transformed_all_points': False} + assert stats == { + 'valid': 2, + 'mighell_substituted': 1, + 'masked': 0, + 'transformed_all_points': False, + } def test_prepare_fit_arrays_mighell_transforms_all(): @@ -466,7 +477,12 @@ def test_prepare_fit_arrays_mighell_transforms_all(): # sigma = sqrt(y + 1) assert weights[0] == pytest.approx(1.0 / np.sqrt(1.5)) assert weights[1] == pytest.approx(1.0 / np.sqrt(1.3)) - assert stats == {'valid': 0, 'mighell_substituted': 2, 'masked': 0, 'transformed_all_points': True} + assert stats == { + 'valid': 0, + 'mighell_substituted': 2, + 'masked': 0, + 'transformed_all_points': True, + } def test_fit_single_data_set_1d_hybrid_keeps_zero_variance_points(): @@ -553,13 +569,17 @@ def test_classical_and_objective_chi_are_split_for_fit_results(): fitter._models = [MagicMock(unique_name='model_0', as_dict=MagicMock(return_value={'name': 'model_0'}))] fitter._fit_func = [lambda x: np.array([0.8, 0.75, 0.7])] - data = sc.DataGroup( - { - 'coords': {'Qz_0': sc.array(dims=['Qz_0'], values=np.array([0.01, 0.02, 0.03]), unit=sc.Unit('1/angstrom'))}, - 'data': {'R_0': sc.array(dims=['Qz_0'], values=np.array([1.0, 0.9, 0.7]), variances=np.array([0.01, 0.0, 0.04]))}, - 'attrs': {}, - } - ) + data = sc.DataGroup({ + 'coords': {'Qz_0': sc.array(dims=['Qz_0'], values=np.array([0.01, 0.02, 0.03]), unit=sc.Unit('1/angstrom'))}, + 'data': { + 'R_0': sc.array( + dims=['Qz_0'], + values=np.array([1.0, 0.9, 0.7]), + variances=np.array([0.01, 0.0, 0.04]), + ) + }, + 'attrs': {}, + }) analysed = fitter.fit(data) @@ -654,12 +674,10 @@ def test_fit_multi_dataset_hybrid_uses_transformed_y_and_weights(): variances = np.ones_like(r_values) * 0.01 variances[3:5] = 0.0 # 2 zero-variance points - data = sc.DataGroup( - { - 'coords': {'Qz_0': sc.array(dims=['Qz_0'], values=qz_values)}, - 'data': {'R_0': sc.array(dims=['Qz_0'], values=r_values, variances=variances)}, - } - ) + data = sc.DataGroup({ + 'coords': {'Qz_0': sc.array(dims=['Qz_0'], values=qz_values)}, + 'data': {'R_0': sc.array(dims=['Qz_0'], values=r_values, variances=variances)}, + }) model = Model() model.interface = CalculatorFactory() @@ -784,3 +802,467 @@ def _fake_fit(*, x, y, weights): fitter.fit_single_data_set_1d(data, objective='legacy_mask') assert len(captured['x'][0]) == 2 # one point dropped + + +# --------------------------------------------------------------------------- +# Tests for MultiFitter.mcmc_sample (Bayesian MCMC) +# --------------------------------------------------------------------------- + + +def _fake_sampling_results(draws=None, param_names=None, state=None, logp=None): + """Build a stand-in for the core ``SamplingResults`` returned by ``Sampler.sample``.""" + res = MagicMock() + res.draws = np.ones((10, 2)) if draws is None else draws + res.param_names = ['a', 'b'] if param_names is None else param_names + res.state = state + res.logp = logp + return res + + +def _patch_sampler(capture, results=None): + """Patch ``easyreflectometry.fitting.Sampler`` and capture its call args. + + Records the constructor's ``(x, y, weights)`` and the ``sample()`` + hyperparameters into the ``capture`` dict, and returns ``results`` (a + fake ``SamplingResults``) from ``sample()``. + """ + results = results if results is not None else _fake_sampling_results() + + def _ctor(fitter, *, x, y, weights, **kwargs): + capture['fitter'] = fitter + capture['x'] = x + capture['y'] = y + capture['weights'] = weights + capture.update(kwargs) # e.g. sampler_kwargs if passed to the ctor + instance = MagicMock() + + def _sample(**sample_kwargs): + capture.update(sample_kwargs) + return results + + instance.sample = MagicMock(side_effect=_sample) + capture['instance'] = instance + return instance + + return patch('easyreflectometry.fitting.Sampler', side_effect=_ctor) + + +class TestMCMCSampleRequiresBumpsEngine: + """mcmc_sample() must raise when the core engine is not a BUMPS instance.""" + + def test_raises_runtime_error_when_not_bumps(self): + model = Model() + model.interface = CalculatorFactory() + fitter = MultiFitter(model) + + data = sc.DataGroup({ + 'coords': {'Qz_0': sc.array(dims=['Qz_0'], values=np.linspace(0.01, 0.3, 10))}, + 'data': {'R_0': sc.array(dims=['Qz_0'], values=np.ones(10), variances=np.ones(10) * 0.01)}, + }) + + with pytest.raises(RuntimeError, match='Bayesian sampling requires a BUMPS minimizer'): + fitter.mcmc_sample(data) + + def test_wrapper_check_runs_before_sampler(self): + """The wrapper-level guard must fire before constructing the core ``Sampler``. + + Patch ``Sampler`` with a sentinel that would record any instantiation; + the guard should raise without ever building it. + """ + model = Model() + model.interface = CalculatorFactory() + fitter = MultiFitter(model) # default minimizer is LMFit, not BUMPS + + capture = {} + + data = sc.DataGroup({ + 'coords': {'Qz_0': sc.array(dims=['Qz_0'], values=np.linspace(0.01, 0.3, 10))}, + 'data': {'R_0': sc.array(dims=['Qz_0'], values=np.ones(10), variances=np.ones(10) * 0.01)}, + }) + + with _patch_sampler(capture) as sampler_cls: + with pytest.raises(RuntimeError, match='Bayesian sampling requires a BUMPS minimizer'): + fitter.mcmc_sample(data) + sampler_cls.assert_not_called() + + +class TestMCMCSampleBasic: + """Basic mcmc_sample() dispatch and return-value forwarding.""" + + def test_returns_result_dict_from_sampler(self): + """mcmc_sample() returns a dict built from the core Sampler's SamplingResults.""" + model = Model() + model.interface = CalculatorFactory() + fitter = MultiFitter(model) + + fitter.easy_science_multi_fitter = MagicMock() + fitter.easy_science_multi_fitter.minimizer.package = 'bumps' + + draws = np.ones((10, 2)) + sentinel_state = object() + logp = np.zeros(10) + results = _fake_sampling_results(draws=draws, param_names=['a', 'b'], state=sentinel_state, logp=logp) + + capture = {} + + data = sc.DataGroup({ + 'coords': {'Qz_0': sc.array(dims=['Qz_0'], values=np.linspace(0.01, 0.3, 10))}, + 'data': {'R_0': sc.array(dims=['Qz_0'], values=np.ones(10), variances=np.ones(10) * 0.01)}, + }) + + with _patch_sampler(capture, results=results): + result = fitter.mcmc_sample(data, samples=100, burn=20, thin=2, population=5) + + # The fitter passed to Sampler is the core MultiFitter + assert capture['fitter'] is fitter.easy_science_multi_fitter + assert result['draws'] is draws + assert result['param_names'] == ['a', 'b'] + assert result['state'] is sentinel_state + assert result['logp'] is logp + + def test_forwards_hyperparams_to_sampler(self): + """Samples, burn, thin, population are forwarded to Sampler.sample().""" + model = Model() + model.interface = CalculatorFactory() + fitter = MultiFitter(model) + + fitter.easy_science_multi_fitter = MagicMock() + fitter.easy_science_multi_fitter.minimizer.package = 'bumps' + + capture = {} + + data = sc.DataGroup({ + 'coords': {'Qz_0': sc.array(dims=['Qz_0'], values=np.linspace(0.01, 0.3, 10))}, + 'data': {'R_0': sc.array(dims=['Qz_0'], values=np.ones(10), variances=np.ones(10) * 0.01)}, + }) + + with _patch_sampler(capture): + fitter.mcmc_sample(data, samples=500, burn=100, thin=5, population=8) + assert capture['samples'] == 500 + assert capture['burn'] == 100 + assert capture['thin'] == 5 + assert capture['population'] == 8 + + def test_forwards_population_to_sampler(self): + """'population' argument is forwarded to Sampler.sample().""" + model = Model() + model.interface = CalculatorFactory() + fitter = MultiFitter(model) + + fitter.easy_science_multi_fitter = MagicMock() + fitter.easy_science_multi_fitter.minimizer.package = 'bumps' + + capture = {} + + data = sc.DataGroup({ + 'coords': {'Qz_0': sc.array(dims=['Qz_0'], values=np.linspace(0.01, 0.3, 10))}, + 'data': {'R_0': sc.array(dims=['Qz_0'], values=np.ones(10), variances=np.ones(10) * 0.01)}, + }) + + with _patch_sampler(capture): + fitter.mcmc_sample(data, samples=100, burn=20, thin=2, population=6) + assert capture['population'] == 6 + + +class TestMCMCSampleInitializer: + """initializer parameter is forwarded via sampler_kwargs.""" + + def test_initializer_passed_as_sampler_kwargs_init(self): + """initializer='lhs' should be passed as sampler_kwargs={'init': 'lhs'} to Sampler.sample().""" + model = Model() + model.interface = CalculatorFactory() + fitter = MultiFitter(model) + + fitter.easy_science_multi_fitter = MagicMock() + fitter.easy_science_multi_fitter.minimizer.package = 'bumps' + + capture = {} + + data = sc.DataGroup({ + 'coords': {'Qz_0': sc.array(dims=['Qz_0'], values=np.linspace(0.01, 0.3, 10))}, + 'data': {'R_0': sc.array(dims=['Qz_0'], values=np.ones(10), variances=np.ones(10) * 0.01)}, + }) + + with _patch_sampler(capture): + fitter.mcmc_sample(data, samples=100, burn=20, thin=2, initializer='lhs') + assert capture['sampler_kwargs'] == {'init': 'lhs'} + + def test_initializer_none_omits_sampler_kwargs(self): + """When initializer is None, sampler_kwargs should be None, not an empty dict.""" + model = Model() + model.interface = CalculatorFactory() + fitter = MultiFitter(model) + + fitter.easy_science_multi_fitter = MagicMock() + fitter.easy_science_multi_fitter.minimizer.package = 'bumps' + + capture = {} + + data = sc.DataGroup({ + 'coords': {'Qz_0': sc.array(dims=['Qz_0'], values=np.linspace(0.01, 0.3, 10))}, + 'data': {'R_0': sc.array(dims=['Qz_0'], values=np.ones(10), variances=np.ones(10) * 0.01)}, + }) + + with _patch_sampler(capture): + fitter.mcmc_sample(data, samples=100, burn=20, thin=2) + assert capture['sampler_kwargs'] is None + + +class TestMCMCSampleZeroVariance: + """Zero-variance handling in the mcmc_sample() data-preparation path.""" + + def test_hybrid_transforms_zero_variance_points(self): + """mcmc_sample() uses the objective from constructor to prepare data arrays.""" + import warnings + + model = Model() + model.interface = CalculatorFactory() + # Use legacy_mask so zero-variance points are dropped + fitter = MultiFitter(model, objective='legacy_mask') + + capture = {} + + fitter.easy_science_multi_fitter = MagicMock() + fitter.easy_science_multi_fitter.minimizer.package = 'bumps' + + qz = np.linspace(0.01, 0.3, 10) + r = np.exp(-qz * 50) + var = np.ones(10) * 0.01 + var[3:5] = 0.0 # 2 zero-variance points + + data = sc.DataGroup({ + 'coords': {'Qz_0': sc.array(dims=['Qz_0'], values=qz)}, + 'data': {'R_0': sc.array(dims=['Qz_0'], values=r, variances=var)}, + }) + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter('always') + with _patch_sampler(capture): + fitter.mcmc_sample(data, samples=100, burn=20, thin=2) + + # legacy_mask should drop the 2 zero-variance points + assert len(capture['x'][0]) == 8 + assert len(capture['y'][0]) == 8 + assert len(capture['weights'][0]) == 8 + + mask_warnings = [str(ww.message) for ww in w if 'Masked' in str(ww.message)] + assert len(mask_warnings) == 1 + assert '2 data point(s)' in mask_warnings[0] + + def test_per_call_objective_override(self): + """mcmc_sample() respects per-call objective override.""" + import warnings + + model = Model() + model.interface = CalculatorFactory() + fitter = MultiFitter(model, objective='legacy_mask') # default + + capture = {} + + fitter.easy_science_multi_fitter = MagicMock() + fitter.easy_science_multi_fitter.minimizer.package = 'bumps' + + qz = np.linspace(0.01, 0.3, 10) + r = np.exp(-qz * 50) + var = np.ones(10) * 0.01 + var[3:5] = 0.0 + + data = sc.DataGroup({ + 'coords': {'Qz_0': sc.array(dims=['Qz_0'], values=qz)}, + 'data': {'R_0': sc.array(dims=['Qz_0'], values=r, variances=var)}, + }) + + # Override to hybrid — should keep all 10 points + with warnings.catch_warnings(record=True): + warnings.simplefilter('always') + with _patch_sampler(capture): + fitter.mcmc_sample(data, samples=100, burn=20, thin=2, objective='hybrid') + + assert len(capture['x'][0]) == 10 # all points kept (Mighell-substituted) + + +class TestMCMCSampleMighellWarningsAndZeroVarianceGuard: + """mcmc_sample() must warn about Mighell-transformed points feeding the + Bayesian likelihood, and refuse data with no uncertainties at all.""" + + @staticmethod + def _make_fitter(objective='hybrid'): + model = Model() + model.interface = CalculatorFactory() + fitter = MultiFitter(model, objective=objective) + fitter.easy_science_multi_fitter = MagicMock() + fitter.easy_science_multi_fitter.minimizer.package = 'bumps' + return fitter + + @staticmethod + def _make_data(variances): + qz = np.linspace(0.01, 0.3, 10) + r = np.exp(-qz * 50) + return sc.DataGroup({ + 'coords': {'Qz_0': sc.array(dims=['Qz_0'], values=qz)}, + 'data': {'R_0': sc.array(dims=['Qz_0'], values=r, variances=variances)}, + }) + + def test_hybrid_partial_zero_variance_warns_mighell_substitution(self): + fitter = self._make_fitter() + variances = np.ones(10) * 0.01 + variances[3:5] = 0.0 + data = self._make_data(variances) + + capture = {} + with pytest.warns( + UserWarning, + match=r'Mighell substitution to 2 zero-variance point\(s\) in reflectivity 0 during sampling', + ): + with _patch_sampler(capture): + fitter.mcmc_sample(data, samples=100, burn=20, thin=2) + + assert len(capture['x'][0]) == 10 + + def test_mighell_objective_warns_transform_all_points(self): + fitter = self._make_fitter(objective='mighell') + data = self._make_data(np.ones(10) * 0.01) + + capture = {} + with pytest.warns( + UserWarning, + match=r'Applied Mighell transform to all 10 point\(s\) in reflectivity 0 during sampling', + ): + with _patch_sampler(capture): + fitter.mcmc_sample(data, samples=100, burn=20, thin=2) + + assert len(capture['x'][0]) == 10 + + def test_all_zero_variance_hybrid_raises(self): + fitter = self._make_fitter() + data = self._make_data(np.zeros(10)) + + capture = {} + with _patch_sampler(capture) as sampler_cls: + with pytest.raises(ValueError, match='all points have zero variance'): + fitter.mcmc_sample(data, samples=100, burn=20, thin=2) + sampler_cls.assert_not_called() + + def test_all_zero_variance_legacy_mask_raises(self): + fitter = self._make_fitter(objective='legacy_mask') + data = self._make_data(np.zeros(10)) + + capture = {} + with _patch_sampler(capture) as sampler_cls: + with pytest.raises(ValueError, match='all points have zero variance'): + fitter.mcmc_sample(data, samples=100, burn=20, thin=2) + sampler_cls.assert_not_called() + + def test_all_zero_variance_allowed_with_explicit_mighell(self): + """Explicitly opting in to objective='mighell' keeps working on + variance-free (e.g. raw count) data, with a warning.""" + fitter = self._make_fitter(objective='mighell') + data = self._make_data(np.zeros(10)) + + capture = {} + with pytest.warns(UserWarning, match='not a true likelihood'): + with _patch_sampler(capture): + fitter.mcmc_sample(data, samples=100, burn=20, thin=2) + + assert len(capture['x'][0]) == 10 + + +# --------------------------------------------------------------------------- +# Analytic weighted-least-squares convention test (issue #370) +# --------------------------------------------------------------------------- + + +def _analytic_wls(design, y, point_weights): + """Solve min_beta sum_i (point_weights_i * (y_i - design_i . beta))^2. + + Returns the solution and the unscaled covariance (X^T W X)^-1 of the + corresponding weighted least-squares problem. + """ + a = design * point_weights[:, None] + b = y * point_weights + beta, *_ = np.linalg.lstsq(a, b, rcond=None) + covariance = np.linalg.inv(a.T @ a) + return beta, covariance + + +@pytest.mark.parametrize('minimizer', [AvailableMinimizers.LMFit, AvailableMinimizers.Bumps]) +def test_fit_weight_convention_matches_analytic_wls(minimizer): + """Pin the weights = 1/sigma convention end-to-end against analytic WLS. + + A reflectometry model is exactly linear in (scale, background): + R(q) = scale * f(q) + background, so weighted least squares has the + closed-form solution beta = (X^T W X)^-1 X^T W y with W = diag(1/sigma^2). + On heteroscedastic data the candidate weight conventions (1/sigma, sigma, + 1/sigma^2) give measurably different solutions, so this test fails if the + convention between ``_prepare_fit_arrays`` and the EasyScience core + minimizers ever drifts. See issue #370. + """ + construction_background = 1e-7 + si = Material(2.07, 0, 'Si') + sio2 = Material(3.47, 0, 'SiO2') + d2o = Material(6.36, 0, 'D2O') + sample = Sample( + Multilayer(Layer(si, 0, 0, 'Si layer')), + Multilayer(Layer(sio2, 30, 3, 'SiO2 layer')), + Multilayer(Layer(d2o, 0, 3, 'D2O Subphase')), + name='WLS Structure', + ) + model = Model(sample, 1.0, construction_background, PercentageFwhm(0.02), 'WLS Model') + model.interface = CalculatorFactory() + fitter = MultiFitter(model) + fitter.easy_science_multi_fitter.switch_minimizer(minimizer) + + # Unit-scale, zero-background reflectivity curve of the fixed structure + q = np.linspace(0.01, 0.25, 30) + f = fitter._fit_func[0](q) - construction_background + assert np.all(f > 0) + + # Deterministic heteroscedastic data that does NOT lie on the model + scale_true, background_true = 1.3, 4.0e-6 + signal = scale_true * f + background_true + fractional_error = 0.03 + 0.02 * np.cos(40.0 * q) ** 2 + sigma = fractional_error * signal + perturbation = 0.8 * np.cos(7.0 * np.arange(q.size) + 0.3) + y = signal + perturbation * sigma + variances = sigma**2 + + design = np.column_stack([f, np.ones_like(f)]) + beta, covariance = _analytic_wls(design, y, 1.0 / sigma) + beta_if_sigma, _ = _analytic_wls(design, y, sigma) + beta_if_inverse_variance, _ = _analytic_wls(design, y, 1.0 / sigma**2) + + # Sanity check: the conventions are distinguishable well beyond the fit tolerance + scale_tolerance = 1e-3 + background_tolerance = 5e-8 + scale_margin = min(abs(beta_if_sigma[0] - beta[0]), abs(beta_if_inverse_variance[0] - beta[0])) + assert scale_margin > 10 * scale_tolerance, 'test data cannot discriminate weight conventions' + + model.scale.fixed = False + model.scale.bounds = (0.5, 3.0) + model.scale.value = 1.0 + model.background.fixed = False + model.background.bounds = (1e-9, 1e-4) + model.background.value = 1e-6 + + data = DataSet1D( + name='wls_convention', + x=q, + y=y, + ye=variances, + model=model, + auto_background=False, + ) + result = fitter.fit_single_data_set_1d(data) + + assert result.success + assert model.scale.value == pytest.approx(beta[0], abs=scale_tolerance) + assert model.background.value == pytest.approx(beta[1], abs=background_tolerance) + + if minimizer == AvailableMinimizers.LMFit: + # lmfit scales the covariance by reduced chi-square (scale_covar=True) + residual = y - design @ beta + chi2 = float(np.sum((residual / sigma) ** 2)) + reduced_chi2 = chi2 / (q.size - 2) + expected_errors = np.sqrt(np.diag(covariance) * reduced_chi2) + assert model.scale.error == pytest.approx(expected_errors[0], rel=0.05) + assert model.background.error == pytest.approx(expected_errors[1], rel=0.05) diff --git a/tests/test_limits.py b/tests/test_limits.py index e8320eb7..2fd1cc74 100644 --- a/tests/test_limits.py +++ b/tests/test_limits.py @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + import numpy as np import pytest from easyscience import global_object diff --git a/tests/test_measurement_comprehensive.py b/tests/test_measurement_comprehensive.py index d6bb0c46..baffb2b8 100644 --- a/tests/test_measurement_comprehensive.py +++ b/tests/test_measurement_comprehensive.py @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2025 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + """ Comprehensive tests for measurement and data store functionality. Tests for all functions in measurement.py and data_store.py modules. @@ -113,7 +116,10 @@ def test_load_txt_handles_three_columns(self): coords_key = list(result['coords'].keys())[0] # xe should be zeros - assert_array_equal(result['coords'][coords_key].variances, np.zeros_like(result['coords'][coords_key].values)) + assert_array_equal( + result['coords'][coords_key].variances, + np.zeros_like(result['coords'][coords_key].values), + ) def test_load_txt_with_insufficient_columns(self): """Test that _load_txt raises error for files with too few columns.""" @@ -159,7 +165,16 @@ def test_constructor_all_parameters(self): xe = [0.1, 0.1, 0.1, 0.1] ye = [1, 2, 3, 4] - dataset = DataSet1D(name='TestData', x=x, y=y, xe=xe, ye=ye, x_label='Q (Å⁻¹)', y_label='Reflectivity', model=None) + dataset = DataSet1D( + name='TestData', + x=x, + y=y, + xe=xe, + ye=ye, + x_label='Q (Å⁻¹)', + y_label='Reflectivity', + model=None, + ) assert dataset.name == 'TestData' assert_array_equal(dataset.x, np.array(x)) @@ -233,7 +248,8 @@ def test_datastore_as_sequence(self): assert store[0].name == 'item2' def test_datastore_experiments_and_simulations_filtering(self): - """Test experiments and simulations properties filter correctly.""" + """Test experiments and simulations properties + filter correctly.""" exp1 = DataSet1D(name='exp1', x=[1], y=[2], model=Mock()) exp2 = DataSet1D(name='exp2', x=[3], y=[4], model=Mock()) sim1 = DataSet1D(name='sim1', x=[5], y=[6]) @@ -264,7 +280,8 @@ class TestProjectDataComprehensive: """Comprehensive tests for ProjectData class.""" def test_project_data_initialization(self): - """Test ProjectData initializes with correct default values.""" + """Test ProjectData initializes with correct + default values.""" project = ProjectData() assert project.name == 'DataStore' @@ -274,7 +291,8 @@ def test_project_data_initialization(self): assert project.sim_data.name == 'Sim Datastore' def test_project_data_with_custom_stores(self): - """Test ProjectData with custom experiment and simulation stores.""" + """Test ProjectData with custom experiment and + simulation stores.""" custom_exp = DataStore(name='CustomExp') custom_sim = DataStore(name='CustomSim') @@ -303,7 +321,8 @@ class TestIntegrationScenarios: """Integration tests for common usage scenarios.""" def test_complete_workflow_orso_file(self): - """Test complete workflow: load ORSO file -> create dataset -> store in project.""" + """Test complete workflow: load ORSO file + -> create dataset -> store in project.""" # Load file fpath = os.path.join(PATH_STATIC, 'test_example1.ort') dataset = load_as_dataset(fpath) @@ -318,7 +337,8 @@ def test_complete_workflow_orso_file(self): assert isinstance(project.exp_data[0], DataSet1D) def test_complete_workflow_txt_file(self): - """Test complete workflow: load txt file -> create dataset -> store in project.""" + """Test complete workflow: load txt file -> + create dataset -> store in project.""" # Load file fpath = os.path.join(PATH_STATIC, 'ref_concat_1.txt') dataset = load_as_dataset(fpath) diff --git a/tests/test_orso_utils.py b/tests/test_orso_utils.py index ebb662a1..4ad2e9ab 100644 --- a/tests/test_orso_utils.py +++ b/tests/test_orso_utils.py @@ -1,5 +1,5 @@ +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -# Copyright (c) 2025 DMSC import os import warnings @@ -91,10 +91,12 @@ def test_load_data_from_orso_file(): def test_orso_sld_unit_conversion(orso_data): - """Test that SLD values from ORSO are correctly converted from A^-2 to 10^-6 A^-2. + """Test that SLD values from ORSO are correctly converted + from A^-2 to 10^-6 A^-2. ORSO stores SLD in absolute units (A^-2), e.g., 3.47e-06. - The internal representation uses 10^-6 A^-2, so the value should be 3.47. + The internal representation uses 10^-6 A^-2, + so the value should be 3.47. """ sample = load_orso_model(orso_data) @@ -114,9 +116,7 @@ def test_orso_sld_unit_conversion(orso_data): subphase = sample[2] si_layer = subphase.layers[0] assert si_layer.material.name == 'Si' - assert abs(si_layer.material.sld.value - 2.07) < 1e-6, ( - f'Expected SLD ~2.07 (10^-6 A^-2), got {si_layer.material.sld.value}' - ) + assert abs(si_layer.material.sld.value - 2.07) < 1e-6, f'Expected SLD ~2.07 (10^-6 A^-2), got {si_layer.material.sld.value}' # Check air superphase layer # ORSO file has: sld: {real: 0.0, imag: 0.0} @@ -152,7 +152,8 @@ def test_LoadOrso_with_nonexistent_file(): def test_get_sld_values_defaults_to_zero_when_sld_and_density_missing(): - """_get_sld_values should return (0.0, 0.0) when both sld and mass_density are None.""" + """_get_sld_values should return (0.0, 0.0) when both + sld and mass_density are None.""" material = SimpleNamespace(sld=None, mass_density=None) m_sld, m_isld = _get_sld_values(material, 'Unknown') assert m_sld == 0.0 @@ -160,7 +161,8 @@ def test_get_sld_values_defaults_to_zero_when_sld_and_density_missing(): def test_load_orso_model_returns_none_and_warns_when_no_sample_model(): - """load_orso_model should return None and emit a warning when the ORSO file has no sample model.""" + """load_orso_model should return None and emit a warning + when the ORSO file has no sample model.""" orso_data = orso.load_orso(os.path.join(PATH_STATIC, 'test_example1.ort')) # Verify the file indeed has no model assert orso_data[0].info.data_source.sample.model is None diff --git a/tests/test_ort_file.py b/tests/test_ort_file.py index 8ef1de16..ed8180df 100644 --- a/tests/test_ort_file.py +++ b/tests/test_ort_file.py @@ -1,15 +1,14 @@ +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -# Copyright (c) 2025 DMSC import logging +import os import numpy as np - -# from dmsc_nightly.data import make_pooch -import pooch import pytest from easyscience.fitting import AvailableMinimizers +import easyreflectometry from easyreflectometry.calculators import CalculatorFactory from easyreflectometry.data import load from easyreflectometry.fitting import MultiFitter @@ -20,30 +19,12 @@ from easyreflectometry.sample import Multilayer from easyreflectometry.sample import Sample - -def make_pooch(base_url: str, registry: dict[str, str | None]) -> pooch.Pooch: - """Make a Pooch object to download test data.""" - return pooch.create( - path=pooch.os_cache('data'), - env='POOCH_DIR', - base_url=base_url, - registry=registry, - ) - - -@pytest.fixture(scope='module') -def data_registry(): - return make_pooch( - base_url='https://pub-6c25ef91903d4301a3338bd53b370098.r2.dev', - registry={ - 'amor_reduced_iofq.ort': None, - }, - ) +PATH_STATIC = os.path.join(os.path.dirname(easyreflectometry.__file__), '..', '..', 'tests', '_static') @pytest.fixture(scope='module') -def load_data(data_registry): - path = data_registry.fetch('amor_reduced_iofq.ort') +def load_data(): + path = os.path.join(PATH_STATIC, 'amor_reduced_iofq.ort') logging.info('Loading data from %s', path) data = load(path) return data @@ -154,7 +135,8 @@ def test_validate_physical_data__r_values_ureal_positive(load_data): for val_a, val_b in zip(a, b): if val_a > val_b: pytest.warns( - UserWarning, reason=f'Reflectivity value {val_a} is unphysically large compared to its uncertainty {val_b}' + UserWarning, + reason=f'Reflectivity value {val_a} is unphysically large compared to its uncertainty {val_b}', ) assert all(load_data['data']['R_0'].values <= 1 + 2 * np.sqrt(load_data['data']['R_0'].variances)) diff --git a/tests/test_parameter_utils.py b/tests/test_parameter_utils.py index d8d2ce97..72e3bbc0 100644 --- a/tests/test_parameter_utils.py +++ b/tests/test_parameter_utils.py @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2024 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + import numpy as np import pytest from numpy.testing import assert_equal diff --git a/tests/test_project.py b/tests/test_project.py index 3c390960..62dd67d2 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2024 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + import datetime import os from pathlib import Path @@ -16,6 +19,7 @@ from easyreflectometry.model import Model from easyreflectometry.model import ModelCollection from easyreflectometry.model import PercentageFwhm +from easyreflectometry.model import Pointwise from easyreflectometry.project import Project from easyreflectometry.sample import Layer from easyreflectometry.sample import Material @@ -133,7 +137,8 @@ def remove_interface(d): models_dict['unique_name'] = 'project_models' remove_interface(project_models_dict) remove_interface(models_dict) - # Since as_dict may not include unique_name, remove it for comparison + # Since as_dict may not include unique_name, + # remove it for comparison for d in [project_models_dict, models_dict]: if 'unique_name' in d: del d['unique_name'] @@ -171,7 +176,12 @@ def test_sld_data_for_model_at_index(self): assert len(sample_data.x) == 500 assert_allclose( np.array([4.6119497e-08, 6.3189932e00, 6.3350000e00, 2.0740000e00]), - np.array([sample_data.y[0], sample_data.y[100], sample_data.y[300], sample_data.y[499]]), + np.array([ + sample_data.y[0], + sample_data.y[100], + sample_data.y[300], + sample_data.y[499], + ]), ) def test_sample_data_for_model_at_index(self): @@ -200,7 +210,12 @@ def test_model_data_for_model_at_index(self): # Expect assert len(model_data.y) == 4 assert_allclose( - np.array([0.9738701849233727, 0.0017678986451491123, 0.00016581714423990004, 3.3290653551465554e-08]), + np.array([ + 0.9738701849233727, + 0.0017678986451491123, + 0.00016581714423990004, + 3.3290653551465554e-08, + ]), model_data.y, ) @@ -363,11 +378,13 @@ def test_as_dict(self): keys.sort() assert keys == [ 'calculator', + 'file_format', 'fitter_minimizer', 'info', 'models', 'with_experiments', ] + assert project_dict['file_format'] == Project.FILE_FORMAT assert project_dict['info'] == { 'name': 'DefaultEasyReflectometryProject', 'short_description': 'Reflectometry, 1D', @@ -403,6 +420,44 @@ def remove_interface(d): remove_interface(project_dict['models']) assert project_dict['models'] == models_dict + def test_from_dict_missing_file_format_raises(self): + """Loading a dict without file_format should raise ValueError.""" + project = Project() + bad_dict = {'info': {}, 'with_experiments': False, 'models': {'data': []}} + with pytest.raises(ValueError, match='predates file_format=2'): + project.from_dict(bad_dict) + + def test_from_dict_wrong_file_format_raises(self): + """Loading a dict with an unsupported file_format should raise ValueError.""" + project = Project() + bad_dict = { + 'file_format': 99, + 'info': {}, + 'with_experiments': False, + 'models': {'data': []}, + } + with pytest.raises(ValueError, match='Unsupported project file_format'): + project.from_dict(bad_dict) + + def test_from_dict_correct_file_format_succeeds(self): + """Loading a dict with the correct file_format should work.""" + global_object.map._clear() + # Build a valid project dict with at least one model + src_project = Project() + src_project._info['name'] = 'Test' + src_project._info['short_description'] = 'Desc' + src_project._info['modified'] = '01.01.2025 00:00' + src_project.default_model() # ensures at least one model exists + src_project._with_experiments = False + good_dict = src_project.as_dict() + global_object.map._clear() + + project = Project() + project.from_dict(good_dict) + assert project._info['name'] == 'Test' + assert project._with_experiments is False + assert len(project._models) >= 1 + def test_as_dict_materials_not_in_model(self): # When project = Project() @@ -558,7 +613,8 @@ def test_load_from_json(self, tmp_path): # Then new_project.load_from_json(tmp_path / 'name' / 'project.json') - # Do it twice to ensure that potential global objects don't collide + # Do it twice to ensure that potential + # global objects don't collide new_project.load_from_json(tmp_path / 'name' / 'project.json') # Expect @@ -608,7 +664,8 @@ def test_load_experiment(self): assert isinstance(project.experiments[5], DataSet1D) assert project.experiments[5].name == 'Example data file from refnx docs' assert project.experiments[5].model == model_5 - assert isinstance(project.models[5].resolution_function, PercentageFwhm) + # example.ort carries an sQz column, so the measured resolution is used + assert isinstance(project.models[5].resolution_function, Pointwise) assert isinstance(project.models[4].resolution_function, PercentageFwhm) def test_load_experiment_sets_resolution_function_pointwise_when_xe_present(self, tmp_path): @@ -624,12 +681,13 @@ def test_load_experiment_sets_resolution_function_pointwise_when_xe_present(self # Then project.load_experiment_for_model_at_index(str(fpath)) - # Resolution is always set to PercentageFwhm - from easyreflectometry.model.resolution_functions import PercentageFwhm - - assert isinstance(project.models[0].resolution_function, PercentageFwhm) + # Expect Pointwise because xe (q-resolution) is present + resolution_function = project.models[0].resolution_function + assert isinstance(resolution_function, Pointwise) + # The 4th column is sQz (sigma); smearing() must return it unchanged + assert_allclose(resolution_function.smearing([0.01, 0.02]), [1e-4, 1e-4]) - def test_load_experiment_sets_linearspline_when_only_ye_present(self, tmp_path): + def test_load_experiment_keeps_percentage_fwhm_when_no_xe(self, tmp_path): # When global_object.map._clear() project = Project() @@ -642,11 +700,45 @@ def test_load_experiment_sets_linearspline_when_only_ye_present(self, tmp_path): # Then project.load_experiment_for_model_at_index(str(fpath)) - # Resolution is always set to PercentageFwhm - from easyreflectometry.model.resolution_functions import PercentageFwhm - + # No q-resolution data available, so the 5% FWHM default is kept assert isinstance(project.models[0].resolution_function, PercentageFwhm) + def test_apply_resolution_function_prefers_pointwise_falls_back_to_percentage(self): + # When + global_object.map._clear() + project = Project() + model = Model() + with_xe = DataSet1D(x=[0.01, 0.02], y=[1.0, 2.0], ye=[0.1, 0.1], xe=[1e-8, 4e-8]) + zero_xe = DataSet1D(x=[0.01, 0.02], y=[1.0, 2.0], ye=[0.1, 0.1], xe=[0.0, 0.0]) + no_xe = DataSet1D(x=[0.01, 0.02], y=[1.0, 2.0], ye=[0.1, 0.1]) + + # Then Expect + project._apply_resolution_function(with_xe, model) + assert isinstance(model.resolution_function, Pointwise) + # xe holds sQz variances; smearing() must return sigma = sqrt(xe) + assert_allclose(model.resolution_function.smearing([0.01, 0.02]), [1e-4, 2e-4]) + + project._apply_resolution_function(zero_xe, model) + assert isinstance(model.resolution_function, PercentageFwhm) + + project._apply_resolution_function(no_xe, model) + assert isinstance(model.resolution_function, PercentageFwhm) + + def test_load_all_experiments_from_file_sets_pointwise_when_sqz_present(self): + # When + global_object.map._clear() + project = Project() + project.models = ModelCollection(Model()) + fpath = os.path.join(PATH_STATIC, 'test_example2.ort') + + # Then + n_loaded = project.load_all_experiments_from_file(fpath) + + # Expect + assert n_loaded == 2 + assert list(project.experiments.keys()) == [0, 1] + assert isinstance(project.models[0].resolution_function, Pointwise) + def test_experimental_data_at_index(self): # When global_object.map._clear() @@ -904,11 +996,13 @@ def test_add_sample_from_orso_with_shared_materials(self): # Expect - shared material should not be duplicated assert len(project._models) == 2 - # The shared material instance is already in the collection, so count should stay the same + # The shared material instance is already in the collection, + # so count should stay the same assert len(project._materials) == initial_material_count def test_replace_models_from_orso(self): - """Test that replace_models_from_orso replaces all existing models with a single new model.""" + """Test that replace_models_from_orso replaces all existing + models with a single new model.""" # When global_object.map._clear() project = Project() @@ -1077,7 +1171,12 @@ def test_remove_model_at_index_removes_experiment_at_same_index(self): project._models.append(model) # Add experiment linked to model 0 experiment = DataSet1D( - name='exp0', x=[0.01, 0.02], y=[1.0, 0.5], ye=[0.1, 0.1], xe=[0.001, 0.001], model=project._models[0] + name='exp0', + x=[0.01, 0.02], + y=[1.0, 0.5], + ye=[0.1, 0.1], + xe=[0.001, 0.001], + model=project._models[0], ) project._experiments[0] = experiment diff --git a/tests/test_topmost_nesting.py b/tests/test_topmost_nesting.py index 5c38ec0b..622991b2 100644 --- a/tests/test_topmost_nesting.py +++ b/tests/test_topmost_nesting.py @@ -1,5 +1,8 @@ +# SPDX-FileCopyrightText: 2024 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + """ -Tests exercising the methods of the topmost classes for nested structure. +Testing the methods of the topmost classes for nested structure. To ensure that the parameters are relayed. """ diff --git a/tests/test_utils.py b/tests/test_utils.py index 64bd7791..4d9c4828 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2024 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + from easyreflectometry import Project from easyreflectometry.utils import count_fixed_parameters from easyreflectometry.utils import count_free_parameters @@ -27,16 +30,3 @@ def test_count_fixed_parameters(): # Expect assert count == 13 - - -def test_count_parameter_user_constraints(): - # When - project = Project() - project.default_model() - # project.parameters[0].user_constraints['name_other_parameter'] = 'constraint' - - # # Then - # count = count_parameter_user_constraints(project) - - # # Expect - # assert count == 1 diff --git a/tests/unit/test_dummy.py b/tests/unit/test_dummy.py new file mode 100644 index 00000000..6927fe89 --- /dev/null +++ b/tests/unit/test_dummy.py @@ -0,0 +1,8 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + + +def test_dummy(): + calculated = 2 + 2 + expected = 4 + assert calculated == expected diff --git a/tests/unit/test_fitting_mcmc.py b/tests/unit/test_fitting_mcmc.py new file mode 100644 index 00000000..ec2e67ec --- /dev/null +++ b/tests/unit/test_fitting_mcmc.py @@ -0,0 +1,220 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Unit tests for the ``Sampler``-based MCMC workflow in ``MultiFitter.mcmc_sample``. + +The actual BUMPS/DREAM sampling is delegated to ``easyscience.fitting.Sampler``; +these tests mock the ``Sampler`` class so they stay fast while still exercising +the wrapper logic: data preparation, zero-variance guards, warning emission, +result-dict construction, and retention of the sampler for chain extension. +""" + +from unittest.mock import MagicMock +from unittest.mock import patch + +import numpy as np +import pytest +import scipp as sc +from easyscience import global_object +from easyscience.fitting.minimizers.factory import AvailableMinimizers + +from easyreflectometry.calculators import CalculatorFactory +from easyreflectometry.fitting import MultiFitter +from easyreflectometry.fitting import _fit_result_reduced_chi +from easyreflectometry.fitting import _flatten_list +from easyreflectometry.model import Model + + +@pytest.fixture(autouse=True) +def clear_global_map(): + global_object.map._clear() + yield + global_object.map._clear() + + +def _make_fitter() -> MultiFitter: + model = Model() + model.interface = CalculatorFactory() + return MultiFitter(model) + + +def _make_bumps_fitter() -> MultiFitter: + """A MultiFitter whose core fitter reports a BUMPS minimizer without running one.""" + fitter = _make_fitter() + fitter.easy_science_multi_fitter = MagicMock() + fitter.easy_science_multi_fitter.minimizer.package = 'bumps' + return fitter + + +def _make_data(variances: np.ndarray, n: int = 10) -> sc.DataGroup: + return sc.DataGroup({ + 'coords': {'Qz_0': sc.array(dims=['Qz_0'], values=np.linspace(0.01, 0.3, n))}, + 'data': {'R_0': sc.array(dims=['Qz_0'], values=np.ones(n), variances=variances)}, + }) + + +def _fake_sampling_results(): + results = MagicMock() + results.draws = np.ones((10, 2)) + results.param_names = ['a', 'b'] + results.state = object() + results.logp = np.zeros(10) + return results + + +def _patch_sampler(capture: dict, results=None): + """Patch ``easyreflectometry.fitting.Sampler`` recording ctor and sample() args.""" + results = results if results is not None else _fake_sampling_results() + + def _ctor(fitter, *, x, y, weights, **kwargs): + capture['fitter'] = fitter + capture['x'] = x + capture['y'] = y + capture['weights'] = weights + instance = MagicMock() + + def _sample(**sample_kwargs): + capture.update(sample_kwargs) + return results + + instance.sample = MagicMock(side_effect=_sample) + capture['instance'] = instance + return instance + + return patch('easyreflectometry.fitting.Sampler', side_effect=_ctor) + + +class TestMCMCSampleGuards: + def test_raises_runtime_error_when_minimizer_is_not_bumps(self): + fitter = _make_fitter() # default minimizer is LMFit, not BUMPS + data = _make_data(np.ones(10) * 0.01) + + with pytest.raises(RuntimeError, match='Bayesian sampling requires a BUMPS minimizer'): + fitter.mcmc_sample(data) + + def test_all_zero_variance_raises_value_error(self): + """Sampling without any uncertainties has no defined likelihood.""" + fitter = _make_bumps_fitter() + data = _make_data(np.zeros(10)) + + capture = {} + with _patch_sampler(capture) as sampler_cls: + with pytest.raises(ValueError, match='all points have zero variance'): + fitter.mcmc_sample(data) + sampler_cls.assert_not_called() + + def test_all_zero_variance_allowed_with_mighell_objective(self): + """objective='mighell' is the explicit opt-in for missing uncertainties.""" + fitter = _make_bumps_fitter() + data = _make_data(np.zeros(10)) + + capture = {} + with _patch_sampler(capture): + with pytest.warns(UserWarning, match='Mighell transform to all'): + result = fitter.mcmc_sample(data, samples=100, burn=10, thin=2, objective='mighell') + assert set(result) == {'draws', 'param_names', 'state', 'logp'} + + +class TestMCMCSampleWarnings: + def test_legacy_mask_warns_about_masked_points(self): + fitter = _make_bumps_fitter() + variances = np.ones(10) * 0.01 + variances[3] = 0.0 + data = _make_data(variances) + + capture = {} + with _patch_sampler(capture): + with pytest.warns(UserWarning, match='Masked 1 data point'): + fitter.mcmc_sample(data, samples=100, burn=10, thin=2, objective='legacy_mask') + # The masked point must not reach the Sampler + assert len(capture['x'][0]) == 9 + + def test_hybrid_warns_about_mighell_substitution(self): + fitter = _make_bumps_fitter() + variances = np.ones(10) * 0.01 + variances[3] = 0.0 + data = _make_data(variances) + + capture = {} + with _patch_sampler(capture): + with pytest.warns(UserWarning, match='Mighell substitution to 1'): + fitter.mcmc_sample(data, samples=100, burn=10, thin=2) + # Hybrid keeps every point + assert len(capture['x'][0]) == 10 + + +class TestMCMCSampleDispatch: + def test_returns_dict_built_from_sampling_results(self): + fitter = _make_bumps_fitter() + data = _make_data(np.ones(10) * 0.01) + results = _fake_sampling_results() + + capture = {} + with _patch_sampler(capture, results=results): + result = fitter.mcmc_sample(data, samples=100, burn=20, thin=2, population=5) + + assert capture['fitter'] is fitter.easy_science_multi_fitter + assert result['draws'] is results.draws + assert result['param_names'] == results.param_names + assert result['state'] is results.state + assert result['logp'] is results.logp + + def test_forwards_hyperparameters_to_sampler_sample(self): + fitter = _make_bumps_fitter() + data = _make_data(np.ones(10) * 0.01) + + capture = {} + with _patch_sampler(capture): + fitter.mcmc_sample(data, samples=500, burn=100, thin=5, population=8) + assert capture['samples'] == 500 + assert capture['burn'] == 100 + assert capture['thin'] == 5 + assert capture['population'] == 8 + assert capture['sampler_kwargs'] is None + + def test_initializer_forwarded_via_sampler_kwargs(self): + fitter = _make_bumps_fitter() + data = _make_data(np.ones(10) * 0.01) + + capture = {} + with _patch_sampler(capture): + fitter.mcmc_sample(data, samples=100, burn=20, thin=2, initializer='lhs') + assert capture['sampler_kwargs'] == {'init': 'lhs'} + + def test_sampler_retained_for_chain_extension(self): + """The Sampler instance must be kept on ``fitter.sampler`` so the chain + can be extended with ``fitter.sampler.extend(...)`` without a new burn-in.""" + fitter = _make_bumps_fitter() + assert fitter.sampler is None + + data = _make_data(np.ones(10) * 0.01) + capture = {} + with _patch_sampler(capture): + fitter.mcmc_sample(data, samples=100, burn=20, thin=2) + + assert fitter.sampler is capture['instance'] + + +class TestMultiFitterHelpers: + def test_switch_minimizer_delegates_to_core_fitter(self): + fitter = _make_fitter() + fitter.easy_science_multi_fitter = MagicMock() + fitter.switch_minimizer(AvailableMinimizers.Bumps) + fitter.easy_science_multi_fitter.switch_minimizer.assert_called_once_with(AvailableMinimizers.Bumps) + + def test_flatten_list_flattens_nested_lists(self): + result = _flatten_list([[1, 2], [3], [4, 5]]) + assert isinstance(result, np.ndarray) + assert list(result) == [1, 2, 3, 4, 5] + + def test_fit_result_reduced_chi_raises_without_any_attribute(self): + result = MagicMock(spec=[]) # no reduced_chi, no reduced_chi2 + with pytest.raises(AttributeError, match='neither reduced_chi nor reduced_chi2'): + _fit_result_reduced_chi(result) + + def test_fit_func_computes_reflectivity_through_calculator(self): + """The factory's fit_func must evaluate the model reflectivity profile.""" + fitter = _make_fitter() + q = np.linspace(0.01, 0.1, 5) + reflectivity = fitter._fit_func[0](q) + assert np.shape(reflectivity) == (5,) + assert np.all(np.isfinite(reflectivity)) diff --git a/tests/unit/test_material_and_calculations.py b/tests/unit/test_material_and_calculations.py new file mode 100644 index 00000000..7c5977b9 --- /dev/null +++ b/tests/unit/test_material_and_calculations.py @@ -0,0 +1,95 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Unit tests for MaterialMixture derived sld/isld, special calculations, +and default parameter limits.""" + +import numpy as np +import pytest +from easyscience import global_object +from easyscience.variable import Parameter + +from easyreflectometry.limits import apply_default_limits +from easyreflectometry.sample import Material +from easyreflectometry.sample.elements.materials.material_mixture import MaterialMixture +from easyreflectometry.special.calculations import molecular_weight +from easyreflectometry.special.calculations import neutron_scattering_length +from easyreflectometry.special.calculations import weighted_average + + +@pytest.fixture(autouse=True) +def clear_global_map(): + global_object.map._clear() + yield + global_object.map._clear() + + +class TestMaterialMixtureSld: + def test_sld_and_isld_are_floats_from_weighted_average(self): + material_a = Material(name='A', sld=2.0, isld=0.5) + material_b = Material(name='B', sld=6.0, isld=1.5) + mixture = MaterialMixture(material_a=material_a, material_b=material_b, fraction=0.25) + + assert isinstance(mixture.sld, float) + assert isinstance(mixture.isld, float) + assert mixture.sld == pytest.approx(weighted_average(2.0, 6.0, 0.25)) + assert mixture.isld == pytest.approx(weighted_average(0.5, 1.5, 0.25)) + + def test_sld_and_isld_follow_fraction_changes(self): + material_a = Material(name='A', sld=2.0, isld=0.0) + material_b = Material(name='B', sld=6.0, isld=1.0) + mixture = MaterialMixture(material_a=material_a, material_b=material_b, fraction=0.25) + + mixture.fraction = 0.75 + + assert mixture.sld == pytest.approx(5.0) + assert mixture.isld == pytest.approx(0.75) + + +class TestNeutronScatteringLength: + def test_element_without_absorption_has_zero_imaginary_part(self): + result = neutron_scattering_length('Si') + assert result.real == pytest.approx(4.1507e-05, rel=1e-3) + assert result.imag == 0.0 + + def test_element_with_absorption_has_negative_imaginary_part(self): + # Boron has a non-zero imaginary bound coherent scattering length (b_c_i) + result = neutron_scattering_length('B') + assert result.real == pytest.approx(5.3e-05, rel=1e-3) + assert result.imag == pytest.approx(-2.1e-06, rel=1e-3) + + def test_formula_scales_with_stoichiometry(self): + single = neutron_scattering_length('B') + double = neutron_scattering_length('B2') + assert double.real == pytest.approx(2 * single.real) + assert double.imag == pytest.approx(2 * single.imag) + + +class TestMolecularWeight: + def test_molecular_weight_of_water(self): + assert molecular_weight('H2O') == pytest.approx(18.015, rel=1e-3) + + +class TestApplyDefaultLimits: + def test_percentage_limits_set_for_infinite_bounds(self): + param = Parameter('thickness', 10.0, min=-np.inf, max=np.inf) + apply_default_limits(param, 'thickness') + assert param.min == pytest.approx(5.0) + assert param.max == pytest.approx(20.0) + + def test_percentage_limits_leave_finite_bounds_untouched(self): + param = Parameter('roughness', 10.0, min=2.0, max=30.0) + apply_default_limits(param, 'roughness') + assert param.min == 2.0 + assert param.max == 30.0 + + def test_percentage_limits_skip_zero_value(self): + param = Parameter('thickness', 0.0, min=-np.inf, max=np.inf) + apply_default_limits(param, 'thickness') + assert np.isinf(param.min) + assert np.isinf(param.max) + + def test_sld_gets_fixed_limits(self): + param = Parameter('sld', 4.0, min=-np.inf, max=np.inf) + apply_default_limits(param, 'sld') + assert param.min == -1.0 + assert param.max == 10.0 diff --git a/tests/unit/test_project_core.py b/tests/unit/test_project_core.py new file mode 100644 index 00000000..5a227255 --- /dev/null +++ b/tests/unit/test_project_core.py @@ -0,0 +1,143 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Unit tests for Project material-index helpers, resolution auto-selection, +ORSO loading, model data generation, and minimizer serialization.""" + +import os + +import numpy as np +import pytest +from easyscience import global_object + +import easyreflectometry +from easyreflectometry.data import DataSet1D +from easyreflectometry.model import PercentageFwhm +from easyreflectometry.model import Pointwise +from easyreflectometry.project import Project + +PATH_STATIC = os.path.join(os.path.dirname(easyreflectometry.__file__), '..', '..', 'tests', '_static') + + +@pytest.fixture(autouse=True) +def clear_global_map(): + global_object.map._clear() + yield + global_object.map._clear() + + +@pytest.fixture +def project() -> Project: + return Project() + + +class TestGetIndexMaterials: + def test_get_index_air_adds_material_when_missing(self, project: Project): + assert len(project._materials) == 0 + index = project.get_index_air() + assert project._materials[index].name == 'Air' + assert project._materials[index].sld.value == 0.0 + + def test_get_index_si_adds_material_when_missing(self, project: Project): + index = project.get_index_si() + assert project._materials[index].name == 'Si' + assert project._materials[index].sld.value == 2.07 + + def test_get_index_sio2_adds_material_when_missing(self, project: Project): + index = project.get_index_sio2() + assert project._materials[index].name == 'SiO2' + assert project._materials[index].sld.value == 3.47 + + def test_get_index_d2o_adds_material_when_missing(self, project: Project): + index = project.get_index_d2o() + assert project._materials[index].name == 'D2O' + assert project._materials[index].sld.value == 6.36 + + def test_get_index_is_idempotent(self, project: Project): + first = project.get_index_d2o() + count_after_first = len(project._materials) + second = project.get_index_d2o() + assert first == second + assert len(project._materials) == count_after_first + + def test_indices_point_at_distinct_materials(self, project: Project): + indices = { + 'Air': project.get_index_air(), + 'D2O': project.get_index_d2o(), + 'Si': project.get_index_si(), + 'SiO2': project.get_index_sio2(), + } + assert len(set(indices.values())) == 4 + for name, index in indices.items(): + assert project._materials[index].name == name + + +class TestApplyResolutionFunction: + def test_pointwise_used_when_experiment_has_q_variances(self, project: Project): + project.default_model() + model = project.models[0] + experiment = DataSet1D(x=[0.01, 0.02], y=[1.0, 2.0], ye=[0.1, 0.1], xe=[1e-8, 4e-8]) + + project._apply_resolution_function(experiment, model) + + assert isinstance(model.resolution_function, Pointwise) + # sigma at the data points is sqrt(sQz) + np.testing.assert_allclose(model.resolution_function.smearing(), np.sqrt([1e-8, 4e-8])) + + def test_percentage_fwhm_fallback_when_q_variances_all_zero(self, project: Project): + project.default_model() + model = project.models[0] + experiment = DataSet1D(x=[0.01, 0.02], y=[1.0, 2.0], ye=[0.1, 0.1], xe=[0.0, 0.0]) + + project._apply_resolution_function(experiment, model) + + assert isinstance(model.resolution_function, PercentageFwhm) + assert model.resolution_function.constant == 5.0 + + def test_percentage_fwhm_fallback_when_q_variances_absent(self, project: Project): + project.default_model() + model = project.models[0] + experiment = DataSet1D(x=[0.01, 0.02], y=[1.0, 2.0], ye=[0.1, 0.1]) + + project._apply_resolution_function(experiment, model) + + assert isinstance(model.resolution_function, PercentageFwhm) + + +class TestLoadOrsoFile: + def test_load_orso_file_creates_model_and_experiment(self, project: Project): + with pytest.warns(UserWarning): + project.load_orso_file(os.path.join(PATH_STATIC, 'example.ort')) + + assert len(project.models) == 1 + assert len(project.experiments) == 1 + assert project.experiments[0].name == 'Experiment from ORSO' + assert project.experiments[0].model is project.models[0] + assert project._with_experiments is True + + +class TestModelData: + def test_model_data_for_model_at_index_returns_reflectivity(self, project: Project): + project.default_model() + q_range = np.linspace(0.01, 0.1, 5) + + dataset = project.model_data_for_model_at_index(0, q_range=q_range) + + np.testing.assert_array_equal(dataset.x, q_range) + assert dataset.y.shape == (5,) + assert np.all(np.isfinite(dataset.y)) + # Reflectivity near total reflection is of order unity and decays with q + assert dataset.y[0] > dataset.y[-1] + + +class TestAsDictMinimizer: + def test_as_dict_uses_selection_when_no_fitter_exists(self, project: Project): + # No models -> the lazy fitter property stays None -> fall back to selection + project_dict = project.as_dict() + assert project._fitter is None + assert project_dict['fitter_minimizer'] == project._minimizer_selection.name + + def test_as_dict_reads_minimizer_from_fitter_when_models_exist(self, project: Project): + project.default_model() + project_dict = project.as_dict() + assert project._fitter is not None + assert project_dict['fitter_minimizer'] == project._fitter.easy_science_multi_fitter.minimizer.name diff --git a/tests/unit/test_summary_goodness_of_fit.py b/tests/unit/test_summary_goodness_of_fit.py new file mode 100644 index 00000000..cb1fb427 --- /dev/null +++ b/tests/unit/test_summary_goodness_of_fit.py @@ -0,0 +1,86 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Unit tests for the summary goodness-of-fit computation and refinement section.""" + +from unittest.mock import MagicMock + +import pytest +from easyscience import global_object + +from easyreflectometry import Project +from easyreflectometry.summary import Summary + + +@pytest.fixture +def project() -> Project: + global_object.map._clear() + project = Project() + project.default_model() + return project + + +def _fit_result(chi2=None, reduced_chi2=None, n_points=0, n_pars=0): + result = MagicMock() + result.chi2 = chi2 + result.reduced_chi2 = reduced_chi2 + result.x = list(range(n_points)) + result.n_pars = n_pars + return result + + +class TestComputeGoodnessOfFit: + def test_returns_na_when_no_fit_has_been_run(self, project: Project): + summary = Summary(project) + assert summary._compute_goodness_of_fit() == 'N/A' + + def test_returns_na_when_last_fit_results_is_empty(self, project: Project): + project._last_fit_results = [] + summary = Summary(project) + assert summary._compute_goodness_of_fit() == 'N/A' + + def test_single_result_uses_its_reduced_chi2(self, project: Project): + project._last_fit_results = [_fit_result(reduced_chi2=1.2345)] + summary = Summary(project) + assert summary._compute_goodness_of_fit() == '1.234' + + def test_multiple_results_aggregate_over_global_dof(self, project: Project): + # total chi2 = 30, total points = 16, n_pars = 6 -> dof = 10 -> gof = 3 + project._last_fit_results = [ + _fit_result(chi2=10.0, n_points=8, n_pars=6), + _fit_result(chi2=20.0, n_points=8, n_pars=6), + ] + summary = Summary(project) + assert summary._compute_goodness_of_fit() == '3' + + def test_multiple_results_with_nonpositive_dof_return_zero(self, project: Project): + project._last_fit_results = [ + _fit_result(chi2=10.0, n_points=2, n_pars=6), + _fit_result(chi2=20.0, n_points=2, n_pars=6), + ] + summary = Summary(project) + assert summary._compute_goodness_of_fit() == '0' + + def test_returns_na_when_result_values_are_invalid(self, project: Project): + project._last_fit_results = [_fit_result(reduced_chi2='not-a-number')] + summary = Summary(project) + assert summary._compute_goodness_of_fit() == 'N/A' + + +class TestRefinementSection: + def test_refinement_section_renders_counts_and_gof(self, project: Project): + project._last_fit_results = [_fit_result(reduced_chi2=2.5)] + summary = Summary(project) + + html = summary._refinement_section() + + assert '2.5' in html + # every placeholder must have been substituted with a number + for placeholder in ( + 'num_total_params', + 'num_free_params', + 'num_fixed_params', + 'num_constriants', + 'num_constraints', + 'goodness_of_fit', + ): + assert placeholder not in html diff --git a/tools/license_headers.py b/tools/license_headers.py new file mode 100644 index 00000000..f276ca1b --- /dev/null +++ b/tools/license_headers.py @@ -0,0 +1,321 @@ +# 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/update_docs_assets.py b/tools/update_docs_assets.py new file mode 100644 index 00000000..d375093c --- /dev/null +++ b/tools/update_docs_assets.py @@ -0,0 +1,91 @@ +""" +Update documentation assets from the assets-branding repository. + +This script fetches branding assets (logos, icons, images) from the +easyscience/assets-branding GitHub repository and copies them to the +appropriate locations in the documentation directory. +""" + +import shutil +from pathlib import Path + +import pooch + +# Configuration: Define what to fetch and where to copy +GITHUB_REPO = 'easyscience/assets-branding' +GITHUB_BRANCH = 'master' +BASE_URL = f'https://raw.githubusercontent.com/{GITHUB_REPO}/refs/heads/{GITHUB_BRANCH}' +PROJECT_NAME = 'easyreflectometry' + +# Mapping of source files to destination paths +# Format: "source_path_in_repo": "destination_path_in_project" +ASSETS_MAP = { + # Logos + f'{PROJECT_NAME}/logos/dark.svg': 'docs/docs/assets/images/logo_dark.svg', + f'{PROJECT_NAME}/logos/light.svg': 'docs/docs/assets/images/logo_light.svg', + # Favicon + f'{PROJECT_NAME}/icons/color.png': 'docs/docs/assets/images/favicon.png', + # Icon overrides + f'{PROJECT_NAME}/icons/bw.svg': f'docs/overrides/.icons/{PROJECT_NAME}.svg', + 'easyscience-org/icons/eso-icon_bw.svg': 'docs/overrides/.icons/easyscience.svg', +} + + +def fetch_and_copy_asset( + source_path: str, + dest_path: str, + cache_dir: Path, +) -> None: + """ + Fetch an asset from GitHub and copy it to the destination. + + Args: + source_path: Path to the file in the GitHub repository + dest_path: Destination path in the project + cache_dir: Directory to cache downloaded files + """ + url = f'{BASE_URL}/{source_path}' + + # Create a unique cache filename based on source path + cache_filename = source_path.replace('/', '_') + + # Download file using pooch + file_path = pooch.retrieve( + url=url, + known_hash=None, # Skip hash verification + path=cache_dir, + fname=cache_filename, + ) + + # Create destination directory if it doesn't exist + dest = Path(dest_path) + dest.parent.mkdir(parents=True, exist_ok=True) + + # Copy the file to destination + shutil.copy2(file_path, dest) + print(f'Copied {file_path} -> {dest_path}') + + +def main(): + """Main function to update all documentation assets.""" + print('📥 Updating documentation assets...') + print(f' Repository: {GITHUB_REPO}') + print(f' Branch: {GITHUB_BRANCH}\n') + + # Use a temporary cache directory + cache_dir = Path.home() / '.cache' / GITHUB_REPO + cache_dir.mkdir(parents=True, exist_ok=True) + + # Fetch and copy each asset + for source_path, dest_path in ASSETS_MAP.items(): + try: + fetch_and_copy_asset(source_path, dest_path, cache_dir) + print() + except Exception as e: + print(f'❌ Failed to fetch {source_path}: {e}') + + print('\n✅ Documentation assets updated successfully!') + + +if __name__ == '__main__': + main() diff --git a/tools/update_github_labels.py b/tools/update_github_labels.py new file mode 100644 index 00000000..84de575e --- /dev/null +++ b/tools/update_github_labels.py @@ -0,0 +1,341 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Set/update GitHub labels for current or specified easyscience +repository. + +Requires: + - gh CLI installed + - gh auth login completed + +Usage: + python update_github_labels.py + python update_github_labels.py --dry-run + python update_github_labels.py --repo easyscience/my-repo + python update_github_labels.py --repo easyscience/my-repo --dry-run +""" + +from __future__ import annotations + +import argparse +import json +import shlex +import subprocess # noqa: S404 +import sys +from dataclasses import dataclass + +EASYSCIENCE_ORG = 'easyscience' + + +# Data structures + + +@dataclass(frozen=True) +class Label: + """A GitHub label with name, color, and description.""" + + name: str + color: str + description: str = '' + + +@dataclass(frozen=True) +class LabelRename: + """Mapping from old label name to new label name.""" + + old: str + new: str + + +class Colors: + """Hex color codes for label groups.""" + + SCOPE = 'd73a4a' + MAINTAINER = '0e8a16' + PRIORITY = 'fbca04' + BOT = '5319e7' + + +LABEL_RENAMES = [ + # Default GitHub labels to rename (if they exist) + LabelRename('bug', '[scope] bug'), + LabelRename('documentation', '[scope] documentation'), + LabelRename('duplicate', '[maintainer] duplicate'), + LabelRename('enhancement', '[scope] enhancement'), + LabelRename('good first issue', '[maintainer] good first issue'), + LabelRename('help wanted', '[maintainer] help wanted'), + LabelRename('invalid', '[maintainer] invalid'), + LabelRename('question', '[maintainer] question'), + LabelRename('wontfix', '[maintainer] wontfix'), + # Custom label renames (if they exist) + LabelRename('[bot] pull request', '[bot] release'), +] + +LABELS = [ + # Scope labels + Label( + '[scope] bug', + Colors.SCOPE, + 'Bug report or fix (major.minor.PATCH)', + ), + Label( + '[scope] documentation', + Colors.SCOPE, + 'Documentation only changes (major.minor.patch.POST)', + ), + Label( + '[scope] enhancement', + Colors.SCOPE, + 'Adds/improves features (major.MINOR.patch)', + ), + Label( + '[scope] maintenance', + Colors.SCOPE, + 'Code/tooling cleanup, no feature or bugfix (major.minor.PATCH)', + ), + Label( + '[scope] significant', + Colors.SCOPE, + 'Breaking or major changes (MAJOR.minor.patch)', + ), + Label( + '[scope] ⚠️ label needed', + Colors.SCOPE, + 'Automatically added to issues and PRs without a [scope] label', + ), + # Maintainer labels + Label( + '[maintainer] duplicate', + Colors.MAINTAINER, + 'Already reported or submitted', + ), + Label( + '[maintainer] good first issue', + Colors.MAINTAINER, + 'Good entry-level issue for newcomers', + ), + Label( + '[maintainer] help wanted', + Colors.MAINTAINER, + 'Needs additional help to resolve or implement', + ), + Label( + '[maintainer] invalid', + Colors.MAINTAINER, + 'Invalid, incorrect or outdated', + ), + Label( + '[maintainer] question', + Colors.MAINTAINER, + 'Needs clarification, discussion, or more information', + ), + Label( + '[maintainer] wontfix', + Colors.MAINTAINER, + 'Will not be fixed or continued', + ), + # Priority labels + Label( + '[priority] lowest', + Colors.PRIORITY, + 'Very low urgency', + ), + Label( + '[priority] low', + Colors.PRIORITY, + 'Low importance', + ), + Label( + '[priority] medium', + Colors.PRIORITY, + 'Normal/default priority', + ), + Label( + '[priority] high', + Colors.PRIORITY, + 'Should be prioritized soon', + ), + Label( + '[priority] highest', + Colors.PRIORITY, + 'Urgent. Needs attention ASAP', + ), + Label( + '[priority] ⚠️ label needed', + Colors.PRIORITY, + 'Automatically added to issues without a [priority] label', + ), + # Bot label + Label( + '[bot] release', + Colors.BOT, + 'Automated release PR. Excluded from changelog/versioning', + ), + Label( + '[bot] backmerge', + Colors.BOT, + 'Automated backmerge master → develop failed due to conflicts', + ), +] + + +# Helpers + + +@dataclass(frozen=True) +class CmdResult: + """Result of a shell command execution.""" + + returncode: int + stdout: str + stderr: str + + +def run_cmd( + args: list[str], + *, + dry_run: bool, + check: bool = True, +) -> CmdResult: + """Run a command (or print it in dry-run mode).""" + cmd_str = ' '.join(shlex.quote(a) for a in args) + + if dry_run: + print(f' [dry-run] {cmd_str}') + return CmdResult(0, '', '') + + proc = subprocess.run( + args=args, + text=True, + capture_output=True, + ) + result = CmdResult( + proc.returncode, + proc.stdout.strip(), + proc.stderr.strip(), + ) + + if check and proc.returncode != 0: + raise RuntimeError(f'Command failed ({proc.returncode}): {cmd_str}\n{result.stderr}') + + return result + + +def get_current_repo() -> str: + """Get the current repository name in 'owner/repo' format.""" + result = subprocess.run( + args=[ + 'gh', + 'repo', + 'view', + '--json', + 'nameWithOwner', + ], + text=True, + capture_output=True, + check=True, + ) + data = json.loads(result.stdout) + name_with_owner = data.get('nameWithOwner', '') + + if '/' not in name_with_owner: + raise RuntimeError('Could not determine current repository name') + + return name_with_owner + + +def rename_label( + repo: str, + rename: LabelRename, + *, + dry_run: bool, +) -> None: + """Rename a label, silently skipping if it doesn't exist.""" + result = run_cmd( + args=[ + 'gh', + 'label', + 'edit', + rename.old, + '--name', + rename.new, + '--repo', + repo, + ], + dry_run=dry_run, + check=False, + ) + + if dry_run or result.returncode == 0: + print(f' Rename: {rename.old!r} → {rename.new!r}') + else: + print(f' Skip (not found): {rename.old!r}') + + +def upsert_label( + repo: str, + label: Label, + *, + dry_run: bool, +) -> None: + """Create or update a label.""" + run_cmd( + [ + 'gh', + 'label', + 'create', + label.name, + '--color', + label.color, + '--description', + label.description, + '--force', + '--repo', + repo, + ], + dry_run=dry_run, + ) + print(f' Upsert: {label.name!r}') + + +# Main + + +def main() -> int: + """Entry point: parse arguments and sync labels.""" + parser = argparse.ArgumentParser(description='Sync GitHub labels for easyscience repos') + parser.add_argument( + '--repo', + help='Target repository (owner/name)', + ) + parser.add_argument( + '--dry-run', + action='store_true', + help='Print actions without applying changes', + ) + args = parser.parse_args() + + repo = args.repo or get_current_repo() + org = repo.split('/')[0] + + if org.lower() != EASYSCIENCE_ORG: + print(f"Error: repository '{repo}' is not under '{EASYSCIENCE_ORG}'", file=sys.stderr) + return 2 + + print(f'Repository: {repo}') + if args.dry_run: + print('Mode: DRY-RUN (no changes will be made)\n') + + print('\nRenaming default labels...') + for rename in LABEL_RENAMES: + rename_label(repo, rename, dry_run=args.dry_run) + + print('\nUpserting labels...') + for label in LABELS: + upsert_label(repo, label, dry_run=args.dry_run) + + print('\nDone.') + return 0 + + +if __name__ == '__main__': + raise SystemExit(main()) diff --git a/vscode-template/settings.json b/vscode-template/settings.json index 35487105..239b6f6a 100644 --- a/vscode-template/settings.json +++ b/vscode-template/settings.json @@ -1,13 +1,11 @@ { - "python.testing.pytestArgs": [ - "tests" - ], - "python.testing.unittestEnabled": false, - "python.testing.pytestEnabled": true, - "[python]": { - "editor.formatOnSave": true, - "editor.codeActionsOnSave": { - "source.organizeImports": "explicit" - }, - }, -} \ No newline at end of file + "python.testing.pytestArgs": ["tests"], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true, + "[python]": { + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.organizeImports": "explicit" + } + } +}