diff --git a/.badgery.yaml b/.badgery.yaml index f22752fa..99bcdc1c 100644 --- a/.badgery.yaml +++ b/.badgery.yaml @@ -5,19 +5,18 @@ cards: - group: Tests type: gh_action title: Code/package tests (GitHub) - file: test.yaml + file: test.yml enabled: true - - group: Tests type: gh_action title: Tutorial tests (GitHub) - file: tutorial-tests.yaml + file: tutorial-tests.yml enabled: true - group: Tests type: gh_action title: Package tests (PyPI) - file: pypi-test.yaml + file: pypi-test.yml enabled: true - group: Code Quality @@ -60,15 +59,14 @@ cards: title: Docstring coverage (interrogate) report: reports/{branch}/coverage-docstring.txt enabled: true - - group: Build & Release type: gh_action title: Publishing (PyPI) - workflow: pypi-publish.yaml + workflow: pypi-publish.yml enabled: true - group: Build & Release type: gh_action title: Docs build/deployment - workflow: docs.yaml + workflow: docs.yml enabled: true diff --git a/.copier-answers.yml b/.copier-answers.yml new file mode 100644 index 00000000..778744fa --- /dev/null +++ b/.copier-answers.yml @@ -0,0 +1,27 @@ +# WARNING: Do not edit this file manually. +# Any changes will be overwritten by Copier. +_commit: v0.10.1-25-ga5301e9 +_src_path: gh:easyscience/templates +app_docs_url: https://easyscience.github.io/diffraction-app +app_doi: 10.5281/zenodo.18163581 +app_package_name: easydiffraction_app +app_python: '3.13' +app_repo_name: diffraction-app +home_page_url: https://easyscience.github.io/diffraction +home_repo_name: diffraction +lib_docs_url: https://easyscience.github.io/diffraction-lib +lib_doi: 10.5281/zenodo.18163581 +lib_package_name: easydiffraction +lib_python_max: '3.13' +lib_python_min: '3.11' +lib_repo_name: diffraction-lib +project_contact_email: support@easydiffraction.org +project_copyright_years: 2021-2026 +project_extended_description: A software for calculating neutron powder diffraction + patterns based on a structural model and refining its parameters against experimental + data +project_name: EasyDiffraction +project_short_description: Diffraction data analysis +project_shortcut: ED +project_type: both +template_type: lib diff --git a/.github/actions/download-artifact/action.yml b/.github/actions/download-artifact/action.yml new file mode 100644 index 00000000..e4fd62f5 --- /dev/null +++ b/.github/actions/download-artifact/action.yml @@ -0,0 +1,50 @@ +name: 'Download artifact' +description: 'Generic 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@v4 + 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..ab32da56 --- /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@v8 + 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..4b28eaf8 --- /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@v2 + with: + app-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..167ee623 --- /dev/null +++ b/.github/actions/setup-pixi/action.yml @@ -0,0 +1,44 @@ +name: 'Setup Pixi Environment' +description: 'Sets up pixi with common configuration' +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..825ac396 --- /dev/null +++ b/.github/actions/upload-artifact/action.yml @@ -0,0 +1,49 @@ +name: 'Upload artifact' +description: 'Generic 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@v4 + 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..37d6298a --- /dev/null +++ b/.github/actions/upload-codecov/action.yml @@ -0,0 +1,42 @@ +name: 'Upload coverage to Codecov' +description: 'Generic wrapper for codecov/codecov-action@v5' + +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@v5 + 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 new file mode 100644 index 00000000..6c342493 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,159 @@ +# Copilot Instructions for EasyDiffraction + +## Project Context + +- Python library for crystallographic diffraction analysis, such as + refinement of the structural model against experimental data. +- Support for + - sample_form: powder and single crystal + - beam_mode: time-of-flight and constant wavelength + - radiation_probe: neutron and x-ray + - scattering_type: bragg and total scattering +- Calculations are done using external calculation libraries: + - `cryspy` for Bragg diffraction + - `crysfml` for Bragg diffraction + - `pdffit2` for Total scattering +- Follow CIF naming conventions where possible. In some places, we + deviate for better API design, but we try to keep the spirit of the + CIF names. +- Reusing the concept of datablocks and categories from CIF. We have + `DatablockItem` (structure or experiment) and `DatablockCollection` + (collection of structures or experiments), as well as `CategoryItem` + (single categories in CIF) and `CategoryCollection` (loop categories + in CIF). +- Metadata via frozen dataclasses: `TypeInfo`, `Compatibility`, + `CalculatorSupport`. +- The API is designed for scientists who use EasyDiffraction as a final + product in a user-friendly, intuitive way. The target users are not + software developers and may have little or no Python experience. The + design is not oriented toward developers building their own tooling on + top of the library, although experienced developers will find their + own way. Prioritize discoverability, clear error messages, and safe + defaults so that non-programmers are not stuck by standard API + conventions. +- This project must be developed to be as error-free as possible, with + the same rigour applied to critical software (e.g. nuclear-plant + control systems). Every code path must be tested, edge cases must be + handled explicitly, and silent failures are not acceptable. + +## Code Style + +- Use snake_case for functions and variables, PascalCase for classes, + and UPPER_SNAKE_CASE for constants. +- Use `from __future__ import annotations` in every module. +- Type-annotate all public function signatures. +- Docstrings on all public classes and methods (numpy style). +- Prefer flat over nested, explicit over clever. +- Write straightforward code; do not add defensive checks for unlikely + edge cases. +- Prefer composition over deep inheritance. +- One class per file when the class is substantial; group small related + classes. +- Avoid `**kwargs`; use explicit keyword arguments for clarity, + autocomplete, and typo detection. +- Do not use string-based dispatch (e.g. `getattr(self, f'_{name}')`) to + route to attributes or methods. Instead, write explicit named methods + (e.g. `_set_sample_form`, `_set_beam_mode`). This keeps the code + greppable, autocomplete-friendly, and type-safe. +- Public parameters and descriptors are either **editable** (property + with both getter and setter) or **read-only** (property with getter + only). If internal code needs to mutate a read-only property, add a + private `_set_` method instead of exposing a public setter. + +## Architecture + +- Eager imports at the top of the module by default. Use lazy imports + (inside a method body) only when necessary to break circular + dependencies or to keep `core/` free of heavy utility imports on + rarely-called paths (e.g. `help()`). +- No `pkgutil` / `importlib` auto-discovery patterns. +- No background/daemon threads. +- No monkey-patching or runtime class mutation. +- Do not use `__all__` in modules; instead, rely on explicit imports in + `__init__.py` to control the public API. +- Do not use redundant `import X as X` aliases in `__init__.py`. Use + plain `from module import X`. +- Concrete classes use `@Factory.register` decorators. To trigger + registration, each package's `__init__.py` must explicitly import + every concrete class (e.g. + `from .chebyshev import ChebyshevPolynomialBackground`). When adding a + new concrete class, always add its import to the corresponding + `__init__.py`. +- Switchable categories (those whose implementation can be swapped at + runtime via a factory) follow a fixed naming convention on the owner + (experiment, structure, or analysis): `` (read-only + property), `_type` (getter + setter), + `show_supported__types()`, `show_current__type()`. + The owner class owns the type setter and the show methods; the show + methods delegate to `Factory.show_supported(...)` passing context. + Every factory-created category must have this full API, even if only + one implementation exists today. +- Categories are flat siblings within their owner (datablock or + analysis). A category must never be a child of another category of a + different type. Categories can reference each other via IDs, but not + via parent-child nesting. +- Every finite, closed set of values (factory tags, experiment axes, + category descriptors with enumerated choices) must use a `(str, Enum)` + class. Internal code compares against enum members, never raw strings. +- Keep `core/` free of domain logic — only base classes and utilities. +- Don't introduce a new abstraction until there is a concrete second use + case. +- Don't add dependencies without asking. + +## Tutorials + +- Jupyter notebooks (`docs/docs/tutorials/*.ipynb`) are **generated + artifacts** — never edit them by hand. Edit only the corresponding + `*.py` script, then run `pixi run notebook-convert` followed by + `pixi run notebook-prepare` to regenerate the notebook. + +## Changes + +- Before implementing any structural or design change (new categories, + new factories, switchable-category wiring, new datablocks, CIF + serialisation changes), read `docs/architecture/architecture.md` to + understand the current design choices and conventions. Follow the + documented patterns (factory registration, switchable-category naming, + metadata classification, etc.) to stay consistent with the rest of the + codebase. For localised bug fixes or test updates, the rules in this + file are sufficient. +- The project is in beta; do not keep legacy code or add deprecation + warnings. Instead, update tests and tutorials to follow the current + API. +- Minimal diffs: don't rewrite working code just to reformat it. +- Never remove or replace existing functionality as part of a new change + without explicit confirmation. If a refactor would drop features, + options, or configurations, highlight every removal and wait for + approval. +- Fix only what's asked; flag adjacent issues as comments, don't fix + them silently. +- Don't add new features or refactor existing code unless explicitly + asked. +- Do not remove TODOs or comments unless the change fully resolves them. +- When renaming, grep the entire project (code, tests, tutorials, docs). +- Every change should be atomic and self-contained, small enough to be + described by a single commit message. Make one change, suggest the + commit message, then stop and wait for confirmation before starting + the next change. +- When in doubt, ask for clarification before making changes. + +## Workflow + +- All open issues, design questions, and planned improvements are + tracked in `docs/architecture/issues_open.md`, ordered by priority. + When an issue is fully implemented, move it from that file to + `docs/architecture/issues_closed.md`. When the resolution affects the + architecture, update the relevant sections of + `docs/architecture/architecture.md`. +- After changes, run linting and formatting fixes with `pixi run fix`. + Do not check what was auto-fixed, just accept the fixes and move on. +- After changes, run unit tests with `pixi run unit-tests`. +- After changes, run integration tests with + `pixi run integration-tests`. +- After changes, run tutorial tests with `pixi run script-tests`. +- Suggest a concise commit message (as a code block) after each change + (less than 72 characters, imperative mood, without prefixing with the + type of change). E.g.: + - Add ChebyshevPolynomialBackground class + - Implement background_type setter on Experiment + - Standardize switchable-category naming convention 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/workflows/backmerge.yaml b/.github/workflows/backmerge.yaml deleted file mode 100644 index f69b5379..00000000 --- a/.github/workflows/backmerge.yaml +++ /dev/null @@ -1,66 +0,0 @@ -# This workflow automatically merges `master` into `develop` whenever a new version tag is pushed (v*). -# -# Key points: -# - Directly merges master into develop without creating a PR. -# - Skips CI on the merge commit using [skip ci] in the commit message. -# - The code being merged has already been tested as part of the release process. -# - This ensures develop stays up-to-date with release changes (version bumps, etc.). -# -# Required repo config: -# https://github.com/organizations/easyscience/settings/secrets/actions -# https://github.com/organizations/easyscience/settings/variables/actions -# - Actions secret: EASYSCIENCE_APP_KEY (GitHub App private key PEM) -# - Actions variable: EASYSCIENCE_APP_ID (GitHub App ID) -# The GitHub App must be added to the develop branch ruleset bypass list. - -name: Backmerge (master -> develop) - -on: - push: - tags: ['v*'] - -permissions: - contents: write - -jobs: - backmerge: - runs-on: ubuntu-latest - - steps: - - name: Create GitHub App installation token - id: app-token - uses: actions/create-github-app-token@v2 - with: - app-id: ${{ vars.EASYSCIENCE_APP_ID }} - private-key: ${{ secrets.EASYSCIENCE_APP_KEY }} - - - name: Checkout repository - uses: actions/checkout@v5 - with: - fetch-depth: 0 - token: ${{ steps.app-token.outputs.token }} - - - name: Configure git for pushing - run: | - git config user.name "easyscience[bot]" - git config user.email "${{ vars.EASYSCIENCE_APP_ID }}+easyscience[bot]@users.noreply.github.com" - - - name: Merge master into develop - run: | - set -euo pipefail - - TAG='${{ github.ref_name }}' - - # Ensure local develop branch exists and is up-to-date with origin - git fetch origin develop:develop - # Switch to develop branch - git checkout develop - - # Merge master into develop (no fast-forward to preserve history) - # Use [skip ci] to avoid triggering CI - the code was already tested on master - git merge origin/master --no-ff -m "Backmerge: ${TAG} from master into develop [skip ci]" - - # Push the merge commit to develop - git push origin develop - - echo "✅ Successfully merged master (${TAG}) into develop" diff --git a/.github/workflows/backmerge.yml b/.github/workflows/backmerge.yml new file mode 100644 index 00000000..47b3384a --- /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@v5 + + - 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@v5 + 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.yaml b/.github/workflows/cleanup.yml similarity index 88% rename from .github/workflows/cleanup.yaml rename to .github/workflows/cleanup.yml index eef184db..7305679b 100644 --- a/.github/workflows/cleanup.yaml +++ b/.github/workflows/cleanup.yml @@ -22,8 +22,8 @@ on: default: 6 delete_workflow_pattern: description: - 'The name or filename of the workflow. if not set then it will target - all workflows.' + 'The name or filename of the workflow. if not set then it will target all + workflows.' required: false delete_workflow_by_state_pattern: description: @@ -40,8 +40,8 @@ on: - disabled_manually delete_run_by_conclusion_pattern: description: - 'Remove workflow by conclusion: action_required, cancelled, failure, - skipped, success' + 'Remove workflow by conclusion: action_required, cancelled, failure, skipped, + success' required: true default: 'All' type: choice @@ -53,8 +53,13 @@ on: - skipped - success dry_run: - description: 'Only log actions, do not perform any delete operations.' + description: 'Only log actions, do not perform any delete operations (dry run).' required: false + default: 'false' + type: choice + options: + - 'false' + - 'true' jobs: del-runs: @@ -71,8 +76,7 @@ jobs: 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_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: diff --git a/.github/workflows/coverage.yaml b/.github/workflows/coverage.yml similarity index 50% rename from .github/workflows/coverage.yaml rename to .github/workflows/coverage.yml index 96ae3386..cd9ff1e0 100644 --- a/.github/workflows/coverage.yaml +++ b/.github/workflows/coverage.yml @@ -3,6 +3,8 @@ name: Coverage checks on: # Trigger the workflow on push push: + # Do not run on version tags (those are handled by other workflows) + tags-ignore: ['v*'] # Trigger the workflow on pull request pull_request: # Allows you to run this workflow manually from the Actions tab @@ -16,8 +18,7 @@ permissions: # 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 }} + 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 @@ -34,18 +35,7 @@ jobs: uses: actions/checkout@v5 - name: Set up pixi - uses: prefix-dev/setup-pixi@v0.9.3 - with: - environments: default - activate-environment: default - run-install: true - frozen: true - cache: false - post-cleanup: false - - - name: Install and setup development dependencies - shell: bash - run: pixi run dev + uses: ./.github/actions/setup-pixi - name: Run docstring coverage run: pixi run docstring-coverage @@ -59,34 +49,21 @@ jobs: uses: actions/checkout@v5 - name: Set up pixi - uses: prefix-dev/setup-pixi@v0.9.3 - with: - environments: default - activate-environment: default - run-install: true - frozen: true - cache: false - post-cleanup: false - - - name: Install and setup development dependencies - shell: bash - run: pixi run dev + 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: codecov/codecov-action@v5 + uses: ./.github/actions/upload-codecov with: name: unit-tests-job flags: unittests files: ./coverage-unit.xml - fail_ci_if_error: true - verbose: true token: ${{ secrets.CODECOV_TOKEN }} - # Job 3: Run integration tests with coverage and upload to Codecov + # Job 2: Run integration tests with coverage and upload to Codecov integration-tests-coverage: runs-on: ubuntu-latest @@ -95,53 +72,23 @@ jobs: uses: actions/checkout@v5 - name: Set up pixi - uses: prefix-dev/setup-pixi@v0.9.3 - with: - environments: default - activate-environment: default - run-install: true - frozen: true - cache: false - post-cleanup: false - - - name: Install and setup development dependencies - shell: bash - run: pixi run dev + uses: ./.github/actions/setup-pixi - name: Run integration tests with coverage run: - pixi run integration-tests-coverage - --cov-report=xml:coverage-integration.xml + pixi run integration-tests-coverage --cov-report=xml:coverage-integration.xml - name: Upload integration tests coverage to Codecov if: ${{ !cancelled() }} - uses: codecov/codecov-action@v5 + uses: ./.github/actions/upload-codecov with: name: integration-tests-job flags: integration files: ./coverage-integration.xml - fail_ci_if_error: true - verbose: true token: ${{ secrets.CODECOV_TOKEN }} - # Job 4: Trigger dashboard build - dashboard-build-trigger: + # 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 - - runs-on: ubuntu-latest - - steps: - - name: Check-out repository - uses: actions/checkout@v5 - - - name: Trigger dashboard build - uses: actions/github-script@v7 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - await github.rest.actions.createWorkflowDispatch({ - owner: context.repo.owner, - repo: context.repo.repo, - workflow_id: "dashboard.yaml", - ref: "${{ env.CI_BRANCH }}" - }); + uses: ./.github/workflows/dashboard.yml + secrets: inherit diff --git a/.github/workflows/dashboard.yaml b/.github/workflows/dashboard.yml similarity index 66% rename from .github/workflows/dashboard.yaml rename to .github/workflows/dashboard.yml index 71d64401..5159f71b 100644 --- a/.github/workflows/dashboard.yaml +++ b/.github/workflows/dashboard.yml @@ -4,12 +4,8 @@ on: workflow_dispatch: workflow_call: -# 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: @@ -24,78 +20,83 @@ jobs: runs-on: ubuntu-latest steps: - # 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: Create GitHub App installation token - id: app-token - uses: actions/create-github-app-token@v2 - with: - app-id: ${{ vars.EASYSCIENCE_APP_ID }} - private-key: ${{ secrets.EASYSCIENCE_APP_KEY }} - repositories: | - ${{ github.event.repository.name }} - dashboard - - name: Checkout repository uses: actions/checkout@v5 with: fetch-depth: 0 - name: Set up pixi - uses: prefix-dev/setup-pixi@v0.9.3 - with: - environments: default - activate-environment: default - run-install: true - frozen: true - cache: false - post-cleanup: false - - - name: Install and setup development dependencies + uses: ./.github/actions/setup-pixi + + - name: Install badgery shell: bash - run: | - pixi run dev - pixi add --pypi --git https://github.com/enhantica/badgery badgery + run: pixi add --pypi --git https://github.com/enhantica/badgery badgery - name: Run docstring coverage and code complexity/maintainability checks run: | - for BRANCH in ${{ env.DEFAULT_BRANCH }} ${{ env.DEVELOP_BRANCH }} ${{ env.CI_BRANCH }}; do - echo "=== Processing branch $BRANCH ===" + 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 + 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 + + # Publish 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: Publish to main branch of ${{ github.repository }} - uses: peaceiris/actions-gh-pages@v3 + uses: Wandalen/wretry.action@v3.8.0 with: - external_repository: ${{ env.REPO_OWNER }}/dashboard - publish_branch: ${{ env.DEFAULT_BRANCH }} - personal_token: ${{ steps.app-token.outputs.token }} - publish_dir: ./_dashboard_publish - keep_files: true + attempt_limit: 3 + attempt_delay: 15000 # 15 seconds between retries + action: peaceiris/actions-gh-pages@v4 + with: | + publish_dir: ./_dashboard_publish + keep_files: true + external_repository: ${{ env.REPO_OWNER }}/dashboard + publish_branch: master + personal_token: ${{ steps.bot.outputs.token }} - name: Add dashboard link to summary run: | diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yml similarity index 66% rename from .github/workflows/docs.yaml rename to .github/workflows/docs.yml index f9bddf67..66b06e1d 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yml @@ -14,16 +14,16 @@ name: Docs build and deployment on: - # Trigger the workflow on pull request - pull_request: - # Selected branches - branches: [master, main, develop] # Trigger the workflow on push push: # Selected branches - branches: [master, main, develop] + 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: @@ -53,11 +53,7 @@ jobs: # Single job that builds and deploys documentation. # Uses macOS runner for consistent Plotly chart rendering. build-deploy-docs: - strategy: - matrix: - os: [macos-14] - - runs-on: ${{ matrix.os }} + runs-on: ubuntu-latest # macos-latest permissions: contents: write # Required for pushing to the gh-pages branch @@ -83,22 +79,11 @@ jobs: fi echo "RELEASE_VERSION=${RELEASE_VERSION}" >> "$GITHUB_ENV" echo "DOCS_VERSION=${DOCS_VERSION}" >> "$GITHUB_ENV" - echo "DEPLOYMENT_URL=https://easyscience.github.io/${{ github.event.repository.name }}/${DOCS_VERSION}" >> "$GITHUB_ENV" - - # Create GitHub App token for pushing to gh-pages as easyscience[bot]. - - name: Create GitHub App installation token - id: app-token - uses: actions/create-github-app-token@v2 - with: - app-id: ${{ vars.EASYSCIENCE_APP_ID }} - private-key: ${{ secrets.EASYSCIENCE_APP_KEY }} # Check out the repository source code. # Note: The gh-pages branch is fetched separately later for mike deployment. - - name: Check-out repository + - name: Checkout repository uses: actions/checkout@v5 - with: - token: ${{ steps.app-token.outputs.token }} # 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 @@ -114,83 +99,53 @@ jobs: # Set up the pixi package manager and install dependencies from pixi.toml. # Uses frozen lockfile to ensure reproducible builds. - name: Set up pixi - uses: prefix-dev/setup-pixi@v0.9.3 - with: - environments: default - activate-environment: default - run-install: true - frozen: true - cache: false - post-cleanup: false - - # Install additional development dependencies (e.g., pre-commit hooks, dev tools). - - name: Install and setup development dependencies - shell: bash - run: pixi run dev - - # Clone shared documentation assets and branding resources from external repositories. - # These contain common MkDocs configuration, templates, stylesheets, and images. - - name: Clone easyscience/assets-docs and easyscience/assets-branding - run: | - cd .. - git clone https://github.com/easyscience/assets-docs.git - git clone https://github.com/easyscience/assets-branding.git - - # Copy assets from the cloned repositories into the docs/ directory. - # This includes stylesheets, images, templates, and other shared resources. - - name: Add files from cloned repositories - run: pixi run docs-assets - - # Convert Python scripts in the tutorials/ directory to Jupyter notebooks. - # This step also strips any existing output from the notebooks and prepares - # them for documentation. - - name: Convert tutorial scripts to notebooks - run: pixi run notebook-prepare - - # Pre-import the main package to trigger Matplotlib font cache building. - # This avoids "Matplotlib is building the font cache" messages during notebook execution. + 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 easydiffraction" + # 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 + # if: false # Temporarily disabled to speed up the docs build run: pixi run notebook-exec - # Move the executed notebooks to docs/tutorials/ directory - # so they can be included in the documentation site. - - name: Move notebooks to docs/tutorials - run: pixi run docs-notebooks - - # Create the mkdocs.yml configuration file - # The file is created by merging two files: - # - assets-docs/mkdocs.yml - the common configuration (theme, plugins, etc.) - # - docs/mkdocs.yml - the project-specific configuration (project name, TOC, etc.) - - name: Create mkdocs.yml file - run: pixi run docs-config - # 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-local + 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: actions/upload-artifact@v4 + uses: ./.github/actions/upload-artifact + with: + name: site-local_easydiffraction-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: - name: site-local_edl-${{ env.RELEASE_VERSION }} - path: site/ - if-no-files-found: 'error' - compression-level: 0 + app-id: ${{ vars.EASYSCIENCE_APP_ID }} + private-key: ${{ secrets.EASYSCIENCE_APP_KEY }} - # Configure git user for mike to commit and push to gh-pages branch. + # 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, @@ -206,10 +161,24 @@ jobs: # 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 "${RELEASE_VERSION#v}" latest + 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 dev + pixi run docs-deploy-pre dev + DEPLOYMENT_URL="${BASE_URL}/dev" + fi - pixi run docs-set-default latest - echo "🔗 deployment url [${{ env.DEPLOYMENT_URL }}](${{ env.DEPLOYMENT_URL }})" >> $GITHUB_STEP_SUMMARY + + # 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/issues-labels.yml b/.github/workflows/issues-labels.yml new file mode 100644 index 00000000..3a60cdd7 --- /dev/null +++ b/.github/workflows/issues-labels.yml @@ -0,0 +1,42 @@ +# Verifies if an issue has at least one of the `[scope]` and one of the +# `[priority]` labels. If not, the bot adds labels with a warning emoji +# to indicate that those labels need to be added. + +name: Issue labels check + +on: + issues: + types: [opened, labeled, unlabeled] + +permissions: + issues: write + +jobs: + check-labels: + runs-on: ubuntu-latest + + steps: + - 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: Check for required [scope] label + uses: trstringer/require-label-prefix@v1 + with: + secret: ${{ steps.bot.outputs.token }} + prefix: '[scope]' + labelSeparator: ' ' + addLabel: true + defaultLabel: '[scope] ⚠️ label needed' + + - name: Check for required [priority] label + uses: trstringer/require-label-prefix@v1 + with: + secret: ${{ steps.bot.outputs.token }} + prefix: '[priority]' + labelSeparator: ' ' + addLabel: true + defaultLabel: '[priority] ⚠️ label needed' diff --git a/.github/workflows/lint-format.yml b/.github/workflows/lint-format.yml new file mode 100644 index 00000000..f1135fa5 --- /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@v5 + + - 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/labels.yaml b/.github/workflows/pr-labels.yml similarity index 70% rename from .github/workflows/labels.yaml rename to .github/workflows/pr-labels.yml index 1e8bd846..642cd318 100644 --- a/.github/workflows/labels.yaml +++ b/.github/workflows/pr-labels.yml @@ -1,7 +1,16 @@ # 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. -name: PR label checks +name: PR labels check on: pull_request_target: @@ -11,15 +20,17 @@ permissions: pull-requests: read jobs: - require-label: + check-labels: runs-on: ubuntu-latest + steps: - - name: Validate required labels + - 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=( - "[maintainer] auto-pull-request" + "[bot] release" "[scope] bug" "[scope] documentation" "[scope] enhancement" diff --git a/.github/workflows/pypi-publish.yaml b/.github/workflows/pypi-publish.yaml deleted file mode 100644 index 7767f8d2..00000000 --- a/.github/workflows/pypi-publish.yaml +++ /dev/null @@ -1,43 +0,0 @@ -# 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 - - steps: - - name: Check-out repository - uses: actions/checkout@v5 - with: - fetch-depth: '0' # full history with tags to get the version number by versioningit - - - name: Set up pixi - uses: prefix-dev/setup-pixi@v0.9.3 - with: - environments: default - activate-environment: default - run-install: true - frozen: true - cache: false - post-cleanup: false - - - name: Install and setup development dependencies - shell: bash - run: pixi run dev - - - name: Create Python package - run: pixi run python -m build - - - name: Publish distribution 📦 to PyPI - uses: pypa/gh-action-pypi-publish@release/v1 - with: - password: ${{ secrets.PYPI_PASSWORD }} diff --git a/.github/workflows/pypi-publish.yml b/.github/workflows/pypi-publish.yml new file mode 100644 index 00000000..e9986cac --- /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@v5 + 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/easydiffraction/settings/publishing/ + # Use the following data: + # Owner: easyscience + # Repository name: diffraction-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.yaml b/.github/workflows/pypi-test.yaml deleted file mode 100644 index e083846e..00000000 --- a/.github/workflows/pypi-test.yaml +++ /dev/null @@ -1,97 +0,0 @@ -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 - -# 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: Set up pixi - uses: prefix-dev/setup-pixi@v0.9.3 - with: - run-install: false - cache: false - post-cleanup: false - - - name: - Download the pixi configuration file from the ${{ env.CI_BRANCH}} - branch - shell: bash - run: | - curl -LO https://raw.githubusercontent.com/easyscience/diffraction-lib/${CI_BRANCH}/pixi.toml - - - name: Download the tests from the ${{ env.DEFAULT_BRANCH }} branch - shell: bash - run: | - curl -LO https://github.com/easyscience/diffraction-lib/archive/refs/heads/${DEFAULT_BRANCH}.zip - unzip ${DEFAULT_BRANCH}.zip -d . - mkdir -p tests - cp -r diffraction-lib-${DEFAULT_BRANCH}/tests/* tests/ - cp diffraction-lib-${DEFAULT_BRANCH}/pytest.ini . - rm -rf ${DEFAULT_BRANCH}.zip diffraction-lib-${DEFAULT_BRANCH} - - - name: Create the environment and install dependencies - run: pixi install - - - name: Run unit tests to verify the installation - run: pixi run unit-tests - - - name: Run integration tests to verify the installation - run: pixi run integration-tests - - # Github token to avoid hitting the unauthenticated API rate limit - - name: List and fetch the EasyDiffraction tutorials - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - pixi run easydiffraction --version - pixi run easydiffraction list-tutorials - pixi run easydiffraction download-all-tutorials - - - name: Test tutorials as notebooks - run: pixi run notebook-tests - - # Job 2: Trigger dashboard build - dashboard-build-trigger: - needs: pypi-package-tests - - runs-on: ubuntu-latest - - steps: - - name: Check-out repository - uses: actions/checkout@v5 - - - name: Trigger dashboard build - uses: actions/github-script@v7 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - await github.rest.actions.createWorkflowDispatch({ - owner: context.repo.owner, - repo: context.repo.repo, - workflow_id: "dashboard.yaml", - ref: "${{ env.CI_BRANCH }}" - }); diff --git a/.github/workflows/pypi-test.yml b/.github/workflows/pypi-test.yml new file mode 100644 index 00000000..6493e64f --- /dev/null +++ b/.github/workflows/pypi-test.yml @@ -0,0 +1,80 @@ +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@v5 + + - 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 easydiffraction + + - name: Add Python 3.13 from Conda + working-directory: easydiffraction + run: pixi add "python=3.13" + + - name: Add other Conda dependencies + working-directory: easydiffraction + run: pixi add gsl + + - name: Add easydiffraction from PyPI + working-directory: easydiffraction + run: pixi add --pypi "easydiffraction" + + - name: Add dev dependencies from PyPI + working-directory: easydiffraction + run: pixi add --pypi pytest pytest-xdist + + - name: Add Pixi task as a shortcut + working-directory: easydiffraction + run: pixi task add easydiffraction "python -m easydiffraction" + + - name: Run unit tests to verify the installation + working-directory: easydiffraction + run: pixi run python -m pytest ../tests/unit/ --color=yes -v + + - name: Run integration tests to verify the installation + working-directory: easydiffraction + run: pixi run python -m pytest ../tests/integration/ --color=yes -n auto + + # 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/quality.yaml b/.github/workflows/quality.yaml deleted file mode 100644 index a3c1b72a..00000000 --- a/.github/workflows/quality.yaml +++ /dev/null @@ -1,123 +0,0 @@ -# The workflow is divided into several steps to ensure code quality: -# - Check the validity of pyproject.toml -# - Check code linting -# - Check code formatting -# - Check formatting of docstrings in the code -# - Check formatting of Markdown, YAML, TOML, etc. files - -name: Code quality checks - -on: - # Trigger the workflow on push - push: - # Every branch - branches: ['**'] - # 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 - -# Set the environment variables to be used in all jobs defined in this workflow -env: - CI_BRANCH: ${{ github.head_ref || github.ref_name }} - -jobs: - code-quality: - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v5 - - - name: Set up pixi - uses: prefix-dev/setup-pixi@v0.9.3 - with: - environments: default - activate-environment: default - run-install: true - frozen: true - cache: false - post-cleanup: false - - - name: Install and setup development dependencies - shell: bash - run: pixi run dev - - # Check the validity of pyproject.toml - - name: Check validity of pyproject.toml - id: check_pyproject - continue-on-error: true - shell: bash - run: pixi run pyproject-check - - # Check code linting with Ruff in the project root - - name: Check code linting - id: check_code_linting - continue-on-error: true - shell: bash - run: pixi run py-lint-check - - # Check code formatting with Ruff in the project root - - name: Check code formatting - id: check_code_formatting - continue-on-error: true - shell: bash - run: pixi run py-format-check - - # Check formatting of docstrings in the code with docformatter - - name: Check formatting of docstrings in the code - id: check_docs_formatting - continue-on-error: true - shell: bash - run: pixi run docs-format-check - - # Check formatting of MD, YAML, TOML, etc. files with Prettier in - # the project root - - name: Check formatting of MD, YAML, TOML, etc. files - id: check_others_formatting - continue-on-error: true - shell: bash - run: pixi run nonpy-format-check - - # Check formatting of Jupyter Notebooks in the tutorials folder - - name: Convert tutorial scripts to notebooks and check formatting - id: check_notebooks_formatting - continue-on-error: true - shell: bash - run: | - pixi run notebook-prepare - pixi run notebook-format-check - - # Add summary - - name: Add quality checks summary - if: always() - shell: bash - run: | - { - echo "## 🧪 Code Quality Checks Summary" - echo "" - echo "| Check | Status |" - echo "|-------|--------|" - echo "| pyproject.toml | ${{ steps.check_pyproject.outcome == 'success' && '✅' || '❌' }} |" - echo "| py lint | ${{ steps.check_code_linting.outcome == 'success' && '✅' || '❌' }} |" - echo "| py format | ${{ steps.check_code_formatting.outcome == 'success' && '✅' || '❌' }} |" - echo "| docstring format | ${{ steps.check_docs_formatting.outcome == 'success' && '✅' || '❌' }} |" - echo "| nonpy format | ${{ steps.check_others_formatting.outcome == 'success' && '✅' || '❌' }} |" - echo "| notebooks format | ${{ steps.check_notebooks_formatting.outcome == 'success' && '✅' || '❌' }} |" - } >> "$GITHUB_STEP_SUMMARY" - - # Fail job requirement - - name: Fail job if any check failed - if: failure() - shell: bash - run: exit 1 diff --git a/.github/workflows/release-notes.yaml b/.github/workflows/release-notes.yml similarity index 81% rename from .github/workflows/release-notes.yaml rename to .github/workflows/release-notes.yml index 73006ff2..cb3e28d6 100644 --- a/.github/workflows/release-notes.yaml +++ b/.github/workflows/release-notes.yml @@ -25,18 +25,14 @@ jobs: fetch-depth: 0 # full history with tags to get the version number - name: Set up pixi - uses: prefix-dev/setup-pixi@v0.9.3 - with: - environments: default - activate-environment: default - run-install: true - frozen: true - cache: false - post-cleanup: false + uses: ./.github/actions/setup-pixi - - name: Install and setup development dependencies - shell: bash - run: pixi run dev + - 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 @@ -61,9 +57,8 @@ jobs: labels: ['[scope] bug'] - title: 'Changed' labels: ['[scope] maintenance', '[scope] documentation'] - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ steps.bot.outputs.token }} - name: Create GitHub draft release uses: softprops/action-gh-release@v2 @@ -73,4 +68,4 @@ jobs: name: ${{ steps.draft.outputs.release_name }} body: ${{ steps.draft.outputs.release_notes }} env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ steps.bot.outputs.token }} diff --git a/.github/workflows/release-pr.yaml b/.github/workflows/release-pr.yaml deleted file mode 100644 index eda67884..00000000 --- a/.github/workflows/release-pr.yaml +++ /dev/null @@ -1,57 +0,0 @@ -# This workflow creates an automated release PR from `develop` into `master`. -# -# Usage: -# - Triggered manually via workflow_dispatch. -# - Creates a PR titled "Release: merge develop into master". -# - Adds the label "[maintainer] auto-pull-request" so it is excluded from changelogs. -# - The PR body makes clear that this is automation only (no review needed). -# -# Required repo config: -# https://github.com/organizations/easyscience/settings/secrets/actions -# https://github.com/organizations/easyscience/settings/variables/actions -# - Actions secret: EASYSCIENCE_APP_KEY (GitHub App private key PEM) -# - Actions variable: EASYSCIENCE_APP_ID (GitHub App ID) - -name: Release PR (develop -> master) - -on: - # Allows you to run this workflow manually from the Actions tab - workflow_dispatch: - -permissions: - contents: read - pull-requests: write - -# Set the environment variables to be used in all jobs defined in this workflow -env: - DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} - -jobs: - create-pull-request: - runs-on: ubuntu-latest - steps: - - name: Create GitHub App installation token - id: app-token - uses: actions/create-github-app-token@v2 - with: - app-id: ${{ vars.EASYSCIENCE_APP_ID }} - private-key: ${{ secrets.EASYSCIENCE_APP_KEY }} - - - name: Checkout develop branch - uses: actions/checkout@v5 - with: - ref: develop - token: ${{ steps.app-token.outputs.token }} - - - name: Create PR from develop to ${{ env.DEFAULT_BRANCH }} - run: | - gh pr create \ - --base ${{ env.DEFAULT_BRANCH }} \ - --head develop \ - --title "Release: merge develop into ${{ env.DEFAULT_BRANCH }}" \ - --label "[maintainer] auto-pull-request" \ - --body "This PR is created automatically to trigger the release pipeline. It merges the accumulated changes from \`develop\` into \`${{ env.DEFAULT_BRANCH }}\`. - - It is labeled \`[maintainer] auto-pull-request\` and is excluded from release notes and version bump logic." - env: - GH_TOKEN: ${{ steps.app-token.outputs.token }} diff --git a/.github/workflows/release-pr.yml b/.github/workflows/release-pr.yml new file mode 100644 index 00000000..7e6fda49 --- /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@v5 + 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.yaml b/.github/workflows/security.yaml deleted file mode 100644 index b837b879..00000000 --- a/.github/workflows/security.yaml +++ /dev/null @@ -1,39 +0,0 @@ -# Integrates a collection of open source static analysis tools with -# GitHub code scanning. -# https://github.com/github/ossar-action - -name: Security scans - -on: - # Trigger the workflow on pull request - pull_request: - # Allows you to run this workflow manually from the Actions tab - workflow_dispatch: - -jobs: - scan-security-ossar: - # OSSAR runs on windows-latest. - # ubuntu-latest and macos-latest support coming soon - runs-on: windows-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v5 - 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' }} - - - name: Run open source static analysis tools - uses: github/ossar-action@main - id: ossar - - - name: Upload results to Security tab - uses: github/codeql-action/upload-sarif@v3 - with: - sarif_file: ${{ steps.ossar.outputs.sarifFile }} diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml new file mode 100644 index 00000000..9b34cccf --- /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@v5 + with: + ref: develop + + - name: Checkout repository + if: ${{ github.event_name != 'schedule' }} + uses: actions/checkout@v5 + + - 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.yaml b/.github/workflows/test-trigger.yml similarity index 61% rename from .github/workflows/test-trigger.yaml rename to .github/workflows/test-trigger.yml index dedfbd91..ecf6b40c 100644 --- a/.github/workflows/test-trigger.yaml +++ b/.github/workflows/test-trigger.yml @@ -7,6 +7,9 @@ on: # Allows you to run this workflow manually from the Actions tab workflow_dispatch: +permissions: + contents: read + jobs: code-tests-trigger: runs-on: ubuntu-latest @@ -17,14 +20,21 @@ jobs: 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: actions/github-script@v7 + uses: ./.github/actions/github-script with: - github-token: ${{ secrets.GITHUB_TOKEN }} + github-token: ${{ steps.bot.outputs.token }} script: | await github.rest.actions.createWorkflowDispatch({ owner: context.repo.owner, repo: context.repo.repo, - workflow_id: "test.yaml", + workflow_id: "test.yml", ref: "develop" }); diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml deleted file mode 100644 index 11b06f14..00000000 --- a/.github/workflows/test.yaml +++ /dev/null @@ -1,259 +0,0 @@ -# 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: - # Every branch - branches: ['**'] - # 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 }} - -jobs: - # Job 1: Prepare environment - 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-14, windows-2022] - - runs-on: ${{ matrix.os }} - - env: - PIXI_ENVS: 'py311-dev py313-dev' - - steps: - - name: Checkout repository - uses: actions/checkout@v5 - with: - fetch-depth: '0' # full history with tags to get the version number by versioningit - - - name: Set up pixi - uses: prefix-dev/setup-pixi@v0.9.3 - with: - environments: ${{ env.PIXI_ENVS }} - run-install: true - frozen: true - cache: false - post-cleanup: false - - - name: Install and setup development dependencies - shell: bash - run: | - for env in ${{ env.PIXI_ENVS }}; do - echo "🔹🔸🔹🔸🔹 Current env: $env 🔹🔸🔹🔸🔹" - pixi run --environment $env dev - echo "PYTHONPATH:" - pixi run printenv PYTHONPATH || true - pixi run --environment $env easydiffraction --version - done - - - name: Run unit tests - shell: bash - run: | - for env in ${{ env.PIXI_ENVS }}; do - echo "🔹🔸🔹🔸🔹 Current env: $env 🔹🔸🔹🔸🔹" - pixi run --environment $env unit-tests - done - - - name: - Run integration tests ${{ needs.env-prepare.outputs.pytest-marks }} - shell: bash - run: | - for env in ${{ env.PIXI_ENVS }}; do - echo "🔹🔸🔹🔸🔹 Current env: $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: Create Python package - shell: bash - run: | - for env in ${{ env.PIXI_ENVS }}; do - echo "🔹🔸🔹🔸🔹 Current env: $env 🔹🔸🔹🔸🔹" - pixi run -e $env dist-build - env_prefix="${env%%-*}" - echo "📦 Moving built wheel to dist/$env_prefix/" - pixi run mkdir -p dist/$env_prefix - pixi run mv dist/*.whl dist/$env_prefix/ - done - - - name: Remove local easydiffraction from pixi.toml - shell: bash - run: pixi remove --pypi easydiffraction - - - 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 Python package for the next job - uses: actions/upload-artifact@v4 - with: - name: edl_${{ matrix.os }}_${{ runner.arch }} - path: | - dist/ - tests/ - pytest.ini - pixi.toml - pixi.lock - if-no-files-found: 'error' - compression-level: 0 - - # Job 3: Test the package - package-test: - needs: [env-prepare, source-test] # depend on previous jobs - - strategy: - fail-fast: false - matrix: - os: [ubuntu-24.04, macos-14, windows-2022] - - runs-on: ${{ matrix.os }} - - env: - PIXI_ENVS: 'py311-dev py313-dev' - - steps: - - name: - Download zipped Python package (incl. extra files) from previous job - uses: actions/download-artifact@v4 - with: # name or path are taken from the upload step of the previous job - name: edl_${{ matrix.os }}_${{ runner.arch }} - path: . # directory to extract downloaded zipped artifacts - - - name: Set up pixi - uses: prefix-dev/setup-pixi@v0.9.3 - with: - environments: ${{ env.PIXI_ENVS }} - run-install: true - frozen: true - cache: false - post-cleanup: false - - - name: Install and setup development dependencies - shell: bash - run: | - for env in ${{ env.PIXI_ENVS }}; do - echo "🔹🔸🔹🔸🔹 Current env: $env 🔹🔸🔹🔸🔹" - pixi run --environment $env wheel - done - - - name: Install easydiffraction package from the built wheel - shell: bash - run: | - for env in ${{ env.PIXI_ENVS }}; do - echo "🔹🔸🔹🔸🔹 Current env: $env 🔹🔸🔹🔸🔹" - env_prefix="${env%%-*}" - echo "📦 Looking for wheel in dist/$env_prefix/" - whl_path="$(find dist/${env_prefix} -name '*.whl' | head -1)" - echo "📦 Installing easydiffraction from: $whl_path" - pixi run --environment $env python -m uv pip install "${whl_path}[all]" --reinstall-package easydiffraction - pixi run --environment $env easydiffraction --version - done - - - name: Run unit tests - shell: bash - run: | - for env in ${{ env.PIXI_ENVS }}; do - echo "🔹🔸🔹🔸🔹 Current env: $env 🔹🔸🔹🔸🔹" - pixi run --environment $env unit-tests - done - - - name: - Run integration tests ${{ needs.env-prepare.outputs.pytest-marks }} - shell: bash - run: | - for env in ${{ env.PIXI_ENVS }}; do - echo "🔹🔸🔹🔸🔹 Current env: $env 🔹🔸🔹🔸🔹" - pixi run --environment $env integration-tests ${{ needs.env-prepare.outputs.pytest-marks }} - done - - # Job 4: Trigger dashboard build - dashboard-build-trigger: - needs: [source-test, package-test] # depend on previous jobs - - runs-on: ubuntu-latest - - steps: - - name: Check-out repository - uses: actions/checkout@v5 - - - name: Trigger dashboard build - uses: actions/github-script@v7 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - await github.rest.actions.createWorkflowDispatch({ - owner: context.repo.owner, - repo: context.repo.repo, - workflow_id: "dashboard.yaml", - ref: "${{ env.CI_BRANCH }}" - }); diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..745c0b03 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,279 @@ +# 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@v5 + + - 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.11 -> py-311-env + + echo "Running tests in environment: $env" + pixi run --environment $env unit-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.11 -> py-311-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: easydiffraction_${{ 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@v5 + + - 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: easydiffraction_${{ 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 easydiffraction 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 easydiffraction_py$py_ver + cd easydiffraction_py$py_ver + + echo "Setting macOS 14.0 as minimum required" + pixi project system-requirements add macos 14.0 + + echo "Adding Python $py_ver" + pixi add "python=$py_ver" + + echo "Adding GNU Scientific Library (required by diffpy.pdffit2)" + pixi add gsl + + # diffpy.pdffit2 wheel links @rpath/libc++.1.dylib, which must be + # present in the conda env lib/ dir on macOS (Python propagates its + # own @loader_path/../lib/ rpath to loaded extensions). Added as + # platform-specific deps so this is a no-op on Linux/Windows. + echo "Adding libc++ for macOS (required by diffpy.pdffit2)" + pixi add --platform osx-arm64 libcxx + pixi add --platform osx-64 libcxx + + 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 easydiffraction from: $whl_url" + pixi add --pypi "easydiffraction[dev] @ ${whl_url}" + + 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 easydiffraction_py$py_ver" + cd easydiffraction_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 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 easydiffraction_py$py_ver" + cd easydiffraction_py$py_ver + + echo "Running tests" + pixi run python -m pytest ../tests/integration/ --color=yes -n auto -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-colab.yaml b/.github/workflows/tutorial-tests-colab.yaml index 08e2e2b6..30966aaa 100644 --- a/.github/workflows/tutorial-tests-colab.yaml +++ b/.github/workflows/tutorial-tests-colab.yaml @@ -7,8 +7,7 @@ on: # 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 }} + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true jobs: @@ -37,10 +36,10 @@ jobs: - name: Install Python dependencies run: - python -m pip install 'easydiffraction[visualization]' nbconvert - nbmake pytest pytest-xdist + python -m pip install 'easydiffraction[visualization]' nbconvert nbmake pytest + pytest-xdist - name: Check if Jupyter Notebooks run without errors run: > - python -m pytest --nbmake docs/tutorials/ --nbmake-timeout=600 - --color=yes -n=auto + python -m pytest --nbmake docs/tutorials/ --nbmake-timeout=1200 --color=yes + -n=auto diff --git a/.github/workflows/tutorial-tests-trigger.yaml b/.github/workflows/tutorial-tests-trigger.yml similarity index 61% rename from .github/workflows/tutorial-tests-trigger.yaml rename to .github/workflows/tutorial-tests-trigger.yml index ea32eef2..1bc27f4f 100644 --- a/.github/workflows/tutorial-tests-trigger.yaml +++ b/.github/workflows/tutorial-tests-trigger.yml @@ -7,6 +7,9 @@ on: # Allows you to run this workflow manually from the Actions tab workflow_dispatch: +permissions: + contents: read + jobs: tutorial-tests-trigger: runs-on: ubuntu-latest @@ -17,14 +20,21 @@ jobs: 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: actions/github-script@v7 + uses: ./.github/actions/github-script with: - github-token: ${{ secrets.GITHUB_TOKEN }} + github-token: ${{ steps.bot.outputs.token }} script: | await github.rest.actions.createWorkflowDispatch({ owner: context.repo.owner, repo: context.repo.repo, - workflow_id: "tutorial-tests.yaml", + workflow_id: "tutorial-tests.yml", ref: "develop" }); diff --git a/.github/workflows/tutorial-tests.yaml b/.github/workflows/tutorial-tests.yml similarity index 55% rename from .github/workflows/tutorial-tests.yaml rename to .github/workflows/tutorial-tests.yml index 301b43d3..4c9244d0 100644 --- a/.github/workflows/tutorial-tests.yaml +++ b/.github/workflows/tutorial-tests.yml @@ -4,7 +4,7 @@ on: # Trigger the workflow on push push: # Selected branches - branches: [master, main, develop] + branches: [develop] # master and main are already verified in PR # Trigger the workflow on pull request pull_request: branches: ['**'] @@ -15,11 +15,13 @@ on: # 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 }} + 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 @@ -41,24 +43,13 @@ jobs: uses: actions/checkout@v5 - name: Set up pixi - uses: prefix-dev/setup-pixi@v0.9.3 - with: - environments: default - activate-environment: default - run-install: true - frozen: true - cache: false - post-cleanup: false - - - name: Install and setup development dependencies - shell: bash - run: pixi run dev + uses: ./.github/actions/setup-pixi - name: Test tutorials as python scripts shell: bash run: pixi run script-tests - - name: Convert tutorial scripts to notebooks + - name: Prepare notebooks shell: bash run: pixi run notebook-prepare @@ -66,24 +57,8 @@ jobs: shell: bash run: pixi run notebook-tests - # Job 2: Trigger dashboard build - dashboard-build-trigger: - needs: tutorial-tests - - runs-on: ubuntu-latest - - steps: - - name: Check-out repository - uses: actions/checkout@v5 - - - name: Trigger dashboard build - uses: actions/github-script@v7 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - await github.rest.actions.createWorkflowDispatch({ - owner: context.repo.owner, - repo: context.repo.repo, - workflow_id: "dashboard.yaml", - ref: "${{ env.CI_BRANCH }}" - }); + # 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 2c43d2b8..6dc595c7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,20 +1,19 @@ # Python -__pycache__ -.venv +__pycache__/ +.venv/ .coverage .pyc -# PyTest -.pytest_cache - -# MyPy -.mypy_cache - # Pixi -.pixi +.pixi/ -# Ruff -.ruff_cache +# PyInstaller +dist/ +build/ +*.spec + +# MkDocs +docs/site/ # Jupyter Notebooks .ipynb_checkpoints @@ -24,16 +23,10 @@ node_modules/ # QtCreator *.autosave - -# QtCreator Qml *.qmlproject.user *.qmlproject.user.* - -# QtCreator Python *.pyproject.user *.pyproject.user.* - -# QtCreator CMake CMakeLists.txt.user* # PyCharm @@ -46,3 +39,8 @@ CMakeLists.txt.user* .DS_Store *.app *.dmg + +# Misc +.cache/ +*.log +*.zip diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c3d471cd..9a3855f4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,57 +1,61 @@ repos: - repo: local hooks: - # ----------------- - # Pre-commit checks - # ----------------- + # ------------- + # Manual checks + # ------------- - id: pixi-pyproject-check name: pixi run pyproject-check entry: pixi run pyproject-check language: system pass_filenames: false - stages: [pre-commit] + stages: [manual] - - id: pixi-py-lint-check-staged - name: pixi run py-lint-check-staged - entry: pixi run py-lint-check-pre + - id: license-headers-check + name: pixi run license-check + entry: pixi run license-check language: system pass_filenames: false - stages: [pre-commit] + stages: [manual] - - id: pixi-py-format-check-staged - name: pixi run py-format-check-staged - entry: pixi run py-format-check-pre + - id: pixi-py-lint-check + name: pixi run py-lint-check + entry: pixi run py-lint-check language: system pass_filenames: false - stages: [pre-commit] + stages: [manual] - - id: pixi-nonpy-format-check-modified - name: pixi run nonpy-format-check-modified - entry: pixi run nonpy-format-check-modified + - id: pixi-py-format-check + name: pixi run py-format-check + entry: pixi run py-format-check language: system pass_filenames: false - stages: [pre-commit] + stages: [manual] - - id: pixi-docs-format-check - name: pixi run docs-format-check - entry: pixi run docs-format-check + - id: pixi-docstring-lint-check + name: pixi run docstring-lint-check + entry: pixi run docstring-lint-check language: system pass_filenames: false - stages: [pre-commit] + stages: [manual] - # ---------------- - # Pre-push checks - # ---------------- - id: pixi-nonpy-format-check name: pixi run nonpy-format-check entry: pixi run nonpy-format-check language: system pass_filenames: false - stages: [pre-push] + 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: [pre-push] + stages: [manual] diff --git a/.prettierignore b/.prettierignore index 4bd61611..a08c3c48 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,6 +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 +.mypy_cache/ + +# Ruff +.ruff_cache/ + +# Node +node_modules + +# Misc +.benchmarks +.cache +deps/ +tmp/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f05c041f..e8dc420f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,94 +1,446 @@ -# Contributing +# Contributing to EasyDiffraction -When contributing, please first discuss the change you wish to make via issue, -email, or any other method with the owners of this repository before making a -change. +Thank you for your interest in contributing to **EasyDiffraction**! -Please note we have a code of conduct, please follow it in all your interactions -with the project. +This guide explains how you can: -## Pull Request Process +- Report issues +- Contribute code +- Improve documentation +- Suggest enhancements +- Interact with the EasyScience community -1. Ensure any install or build dependencies are removed before the end of the - layer when doing a build. -2. Update the README.md with details of changes to the interface, this includes - new environment variables, exposed ports, useful file locations and container - parameters. -3. Increase the version numbers in any example files and the README.md to the - new version that this Pull Request would represent. The versioning scheme we - use is [SemVer](http://semver.org/). -4. You may merge the Pull Request in once you have the sign-off of two other - developers, or if you do not have permission to do that, you may request the - second reviewer to merge it for you. +Whether you are an experienced developer or contributing for the first +time, this document walks you through the entire process step by step. -## Code of Conduct +Please make sure you follow the EasyScience organization-wide +[Code of Conduct](https://github.com/easyscience/.github/blob/master/CODE_OF_CONDUCT.md). -### Our Pledge +--- -In the interest of fostering an open and welcoming environment, we as -contributors and maintainers pledge to make participation in our project and our -community a harassment-free experience for everyone, regardless of age, body -size, disability, ethnicity, gender identity and expression, level of -experience, nationality, personal appearance, race, religion, or sexual identity -and orientation. +## Table of Contents -### Our Standards +- [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) -Examples of behavior that contributes to creating a positive environment -include: +--- -- Being respectful of differing viewpoints and experiences -- Gracefully accepting constructive criticism -- Focusing on what is best for the community +## How to Interact With This Project -Examples of unacceptable behavior by participants include: +If you are not planning to contribute code, you may want to: -- Trolling, insulting/derogatory comments, and personal or political attacks -- Public or private harassment -- Publishing others' private information, such as a physical or electronic - address, without explicit permission -- Other conduct which could reasonably be considered inappropriate in a - professional setting +- 🐞 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/diffraction-lib/discussions) -### Our Responsibilities +If you plan to contribute code or documentation, continue below. -Project maintainers are responsible for clarifying the standards of acceptable -behavior and are expected to take appropriate and fair corrective action in -response to any instances of unacceptable behavior. +--- -Project maintainers have the right and responsibility to remove, edit, or reject -comments, commits, code, wiki edits, issues, and other contributions that are -not aligned to this Code of Conduct, or to ban temporarily or permanently any -contributor for other behaviors that they deem inappropriate, threatening, -offensive, or harmful. +## 1. Understanding the Development Model -### Scope +Before you start coding, it is important to understand how development +works in this project. -This Code of Conduct applies both within project spaces and in public spaces -when an individual is representing the project or its community. Examples of -representing a project or community include using an official project e-mail -address, posting via an official social media account, or acting as an appointed -representative at an online or offline event. Representation of a project may be -further defined and clarified by project maintainers. +### Branching Strategy -### Enforcement +We use the following branches: -Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported by contacting the project team at suport@easydiffraction.org. All -complaints will be reviewed and investigated and will result in a response that -is deemed necessary and appropriate to the circumstances. The project team is -obligated to maintain confidentiality with regard to the reporter of an -incident. Further details of specific enforcement policies may be posted -separately. +- `master` — stable releases only +- `develop` — active development branch +- Short-lived branches — feature or fix branches created for a single + contribution and deleted after merge -Project maintainers who do not follow or enforce the Code of Conduct in good -faith may face temporary or permanent repercussions as determined by other -members of the project's leadership. +> [!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 -### Attribution +See ADR easyscience/.github#12 for more details on the branching +strategy. -This Code of Conduct is adapted from the [Contributor Covenant][homepage], -version 1.4, available at [http://contributor-covenant.org/version/1/4][version] +--- -[homepage]: http://contributor-covenant.org -[version]: http://contributor-covenant.org/version/1/4/ +## 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/diffraction-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//diffraction-lib.git + cd diffraction-lib + ``` + +4. Add the original repository as `upstream`: + + ```bash + git remote add upstream https://github.com/easyscience/diffraction-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 EasyDiffraction and the EasyScience +ecosystem! diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md deleted file mode 100644 index 2acffeb3..00000000 --- a/DEVELOPMENT.md +++ /dev/null @@ -1,150 +0,0 @@ -# Development - -This is an example of a workflow that describes the development process. - -## Installation and setup with Pixi - -- Install Pixi by following the instructions on the - [official Pixi Installation Guide](https://pixi.sh/latest/installation). -- Clone repositories with assets for building documentation - ```bash - git clone https://github.com/easyscience/assets-docs.git - git clone https://github.com/easyscience/assets-branding.git - ``` -- Clone EasyDiffraction library repository - ```bash - git clone https://github.com/easyscience/diffraction-lib - ``` -- Go to the cloned directory - ```bash - cd diffraction-lib - ``` -- Create the environment defined in `pixi.toml` and install all necessary - dependencies: - ```bash - pixi install - ``` -- Install and setup development dependencies - ```bash - pixi run dev - ``` - -## Making changes - -- Checkout/switch to the `develop` branch - ```bash - git checkout develop - ``` -- Create a new branch from the `develop` one - ```bash - git checkout -b new-feature - ``` -- Make changes in the code - ```bash - ... - ``` - -## Checking code quality and testing - -### Pre-commit checks - -- Check code quality (configuration is in `pyproject.toml` and - `prettierrc.toml`) - ```bash - pixi run pre-commit-check - ``` -- Fix some code quality issues automatically - ```bash - pixi run pre-commit-fix - ``` - -### Pre-push checks - -- Run tests and checks before pushing changes - ```bash - pixi run pre-push - ``` - -### Individual tests and checks (if needed) - -- Check coverage by tests and docstrings - ```bash - pixi run cov - ``` -- Run unit tests - ```bash - pixi run unit-tests - ``` -- Run integration tests - ```bash - pixi run integration-tests - ``` -- Test tutorials as python scripts - ```bash - pixi run script-tests - ``` -- Convert tutorial scripts to notebooks - ```bash - pixi run notebook-prepare - ``` -- Test tutorials as notebooks - ```bash - pixi run notebook-tests - ``` - -## Building and checking documentation with MkDocs - -- Move notebooks to docs/tutorials - ```bash - pixi run docs-notebooks - ``` -- Add extra files to build documentation (from `../assets-docs/` and - `../assets-branding/` directories) - ```bash - pixi run docs-assets - ``` -- Create mkdocs.yml file - ```bash - pixi run docs-config - ``` -- Build documentation - ```bash - pixi run docs-build - ``` -- Test the documentation locally (built in the `site/` directory). E.g., on - macOS, open the site in the default browser via the terminal - ```bash - open http://127.0.0.1:8000 - ``` -- Clean up after checking documentation - ```bash - pixi run docs-clean - ``` - -## Committing and pushing changes - -- Commit changes - ```bash - git add . - git commit -m "Add new feature" - ``` -- Push the new branch to a remote repository - ```bash - git push -u origin new-feature - ``` -- Create a pull request on - [EasyScience GitHub repository](https://github.com/easyscience/diffraction-lib/pulls) - and request a review from team members -- Add one of the required labels: - - `[maintainer] auto-pull-request` - - `[scope] bug` - - `[scope] documentation` - - `[scope] enhancement` - - `[scope] maintenance` - - `[scope] significant` -- After approval, merge the pull request into the `develop` branch using "Squash - and merge" option -- Delete the branch remotely - ```bash - git push origin --delete new-feature - ``` diff --git a/LICENSE b/LICENSE index 4debe9f4..c4e3e48e 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ BSD 3-Clause License -Copyright (c) 2021-2025 EasyDiffraction Python Library contributors +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 6278d78f..a9fccc67 100644 --- a/README.md +++ b/README.md @@ -9,45 +9,45 @@

-**EasyDiffraction** is a Python package for calculating neutron powder -diffraction patterns based on a structural model and refining its parameters -against experimental data. +**EasyDiffraction** is a software for calculating neutron powder +diffraction patterns based on a structural model and refining its +parameters against experimental data. -**EasyDiffraction** is built upon the [EasyScience] framework, which provides -essential tools for developing scientific libraries and applications. + -## Useful Links - -- [Main Website] - Learn more about EasyDiffraction. -- [Documentation] - Access the full documentation. -- [Discussions] - Ask questions or share ideas. -- [Issue Tracker] - Report bugs or request new features. -- [Source Code] - Explore the source code repository. - -## Contributing +**EasyDiffraction** is developed both as a Python library and as a +cross-platform desktop application. -We welcome contributions! Our vision is for **EasyDiffraction** to be a -community-driven, open-source project supported by a diverse group of -contributors. +Here, we focus on the Python library. For the graphical user interface +(GUI), please see the corresponding +[GUI resources](https://github.com/easyscience/diffraction-app). -The project is currently maintained by the [European Spallation Source (ESS)]. +License: +[BSD 3-Clause](https://github.com/easyscience/diffraction-lib/blob/master/LICENSE) -If you'd like to contribute, please refer to our [Contributing Guidelines] for -information about our code of conduct and instructions on submitting pull -requests. - -## License - -**EasyDiffraction** is licensed under the [BSD 3-Clause License]. +## Useful Links - -[BSD 3-Clause License]: https://github.com/easyscience/diffraction-lib/blob/master/LICENSE -[Contributing Guidelines]: https://github.com/easyscience/diffraction-lib/blob/master/CONTRIBUTING.md -[EasyScience]: https://easyscience.software -[European Spallation Source (ESS)]: https://ess.eu -[Main Website]: https://easydiffraction.org -[Documentation]: https://docs.easydiffraction.org/lib -[Discussions]: https://github.com/easyscience/diffraction-lib/discussions -[Issue Tracker]: https://github.com/easyscience/diffraction-lib/issues -[Source Code]: https://github.com/easyscience/diffraction-lib - +### For Users + +- 📖 + [Documentation](https://easyscience.github.io/diffraction-lib/latest) +- 🚀 + [Getting Started](https://easyscience.github.io/diffraction-lib/latest/introduction) +- 🧪 + [Tutorials](https://easyscience.github.io/diffraction-lib/latest/tutorials) +- 💬 + [Get in Touch](https://easyscience.github.io/diffraction-lib/latest/introduction/#get-in-touch) +- 🧾 + [Citation](https://easyscience.github.io/diffraction-lib/latest/introduction/#citation) + +### For Contributors + +- 🧑‍💻 [Source Code](https://github.com/easyscience/diffraction-lib) +- 🐞 + [Issue Tracker](https://github.com/easyscience/diffraction-lib/issues) +- 💡 + [Discussions](https://github.com/easyscience/diffraction-lib/discussions) +- 🤝 + [Contributing Guide](https://github.com/easyscience/diffraction-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 index ffe29da9..f62b13ab 100644 --- a/codecov.yml +++ b/codecov.yml @@ -11,8 +11,3 @@ coverage: default: # Require patch coverage but with threshold threshold: 1% - -ignore: - # Vendored third-party code - not our code to test - - 'src/easydiffraction/utils/_vendored/**' - - 'src/easydiffraction/utils/_vendored/jupyter_dark_detect/**' diff --git a/docs/api-reference/experiments.md b/docs/api-reference/experiments.md deleted file mode 100644 index 2eb1bd91..00000000 --- a/docs/api-reference/experiments.md +++ /dev/null @@ -1 +0,0 @@ -::: easydiffraction.experiments diff --git a/docs/api-reference/sample_models.md b/docs/api-reference/sample_models.md deleted file mode 100644 index 43c54901..00000000 --- a/docs/api-reference/sample_models.md +++ /dev/null @@ -1 +0,0 @@ -::: easydiffraction.sample_models diff --git a/docs/architecture/architecture.md b/docs/architecture/architecture.md new file mode 100644 index 00000000..3067e6a6 --- /dev/null +++ b/docs/architecture/architecture.md @@ -0,0 +1,1178 @@ +# EasyDiffraction Architecture + +**Version:** 1.0 +**Date:** 2026-03-24 +**Status:** Living document — updated as the project evolves + +--- + +## 1. Overview + +EasyDiffraction is a Python library for crystallographic diffraction +analysis (Rietveld refinement, pair-distribution-function fitting, +etc.). It models the domain using **CIF-inspired abstractions** — +datablocks, categories, and parameters — while providing a high-level, +user-friendly API through a single `Project` façade. + +### 1.1 Supported Experiment Dimensions + +Every experiment is fully described by four orthogonal axes: + +| Axis | Options | Enum | +| --------------- | ----------------------------------- | -------------------- | +| Sample form | powder, single crystal | `SampleFormEnum` | +| Scattering type | Bragg, total (PDF) | `ScatteringTypeEnum` | +| Beam mode | constant wavelength, time-of-flight | `BeamModeEnum` | +| Radiation probe | neutron, X-ray | `RadiationProbeEnum` | + +> **Planned extensions:** 1D / 2D data dimensionality, polarised / +> unpolarised neutron beam. + +### 1.2 Calculation Engines + +External libraries perform the heavy computation: + +| Engine | Scope | +| --------- | ----------------- | +| `cryspy` | Bragg diffraction | +| `crysfml` | Bragg diffraction | +| `pdffit2` | Total scattering | + +--- + +## 2. Core Abstractions + +All core types live in `core/` which contains **only** base classes and +utilities — no domain logic. + +### 2.1 Object Hierarchy + +```shell +GuardedBase # Controlled attribute access, parent linkage, identity +├── CategoryItem # Single CIF category row (e.g. Cell, Peak, Instrument) +├── CollectionBase # Ordered name→item container +│ ├── CategoryCollection # CIF loop (e.g. AtomSites, Background, Data) +│ └── DatablockCollection # Top-level container (e.g. Structures, Experiments) +└── DatablockItem # CIF data block (e.g. Structure, Experiment) +``` + +`CollectionBase` provides a unified dict-like API over an ordered item +list with name-based indexing. All key operations — `__getitem__`, +`__setitem__`, `__delitem__`, `__contains__`, `remove()` — resolve keys +through a single `_key_for(item)` method that returns +`category_entry_name` for category items or `datablock_entry_name` for +datablock items. Subclasses `CategoryCollection` and +`DatablockCollection` inherit this consistently. + +### 2.2 GuardedBase — Controlled Attribute Access + +`GuardedBase` is the root ABC. It enforces that only **declared +`@property` attributes** are accessible publicly: + +- **`__getattr__`** rejects any attribute not declared as a `@property` + on the class hierarchy. Shows diagnostics with closest-match + suggestions on typos. +- **`__setattr__`** distinguishes: + - **Private** (`_`-prefixed) — always allowed, no diagnostics. + - **Read-only public** (property without setter) — blocked with a + clear error. + - **Writable public** (property with setter) — goes through the + property setter, which is where validation happens. + - **Unknown** — blocked with diagnostics showing allowed writable + attrs. +- **Parent linkage** — when a `GuardedBase` child is assigned to + another, the child's `_parent` is set automatically, forming an + implicit ownership tree. +- **Identity** — every instance gets an `_identity: Identity` object for + lazy CIF-style name resolution (`datablock_entry_name`, + `category_code`, `category_entry_name`) by walking the `_parent` + chain. + +**Key design rule:** if a parameter has a public setter, it is writable +for the user. If only a getter — it is read-only. If internal code needs +to set it, a private method (underscore prefix) is used. See § 2.2.1 +below for the full pattern. + +#### 2.2.1 Public Property Convention — Editable vs Read-Only + +Every public parameter or descriptor exposed on a `GuardedBase` subclass +follows one of two patterns: + +| Kind | Getter | Setter | Internal mutation | +| ------------- | ------ | ------ | ---------------------------------- | +| **Editable** | yes | yes | Via the public setter | +| **Read-only** | yes | no | Via a private `_set_` method | + +**Editable property** — the user can both read and write the value. The +setter runs through `GuardedBase.__setattr__` and into the property +setter, where validation happens: + +```python +@property +def name(self) -> str: + """Human-readable name of the experiment.""" + return self._name + + +@name.setter +def name(self, new: str) -> None: + self._name = new +``` + +**Read-only property** — the user can read but cannot assign. Any +attempt to set the attribute is blocked by `GuardedBase.__setattr__` +with a clear error message. If _internal_ code (factory builders, CIF +loaders, etc.) needs to set the value, it calls a private `_set_` +method instead of exposing a public setter: + +```python +@property +def sample_form(self) -> StringDescriptor: + """Sample form descriptor (read-only for the user).""" + return self._sample_form + + +def _set_sample_form(self, value: str) -> None: + """Internal setter used by factory/CIF code during construction.""" + self._sample_form.value = value +``` + +**Why this matters:** + +- `GuardedBase.__setattr__` uses the presence of a setter to decide + writability. Adding a setter "just for internal use" would open the + attribute to users. +- Private `_set_` methods keep the public API surface minimal and + intention-clear, while remaining greppable and type-safe. +- The pattern avoids string-based dispatch — every mutator has an + explicit named method. + +### 2.3 CategoryItem and CategoryCollection + +| Aspect | `CategoryItem` | `CategoryCollection` | +| --------------- | ---------------------------------- | ----------------------------------------- | +| CIF analogy | Single category row | Loop (table) of rows | +| Examples | Cell, SpaceGroup, Instrument, Peak | AtomSites, Background, Data, LinkedPhases | +| Parameters | All `GenericDescriptorBase` attrs | Aggregated from all child items | +| Serialisation | `as_cif` / `from_cif` | `as_cif` / `from_cif` | +| Update hook | `_update(called_by_minimizer=)` | `_update(called_by_minimizer=)` | +| Update priority | `_update_priority` (default 10) | `_update_priority` (default 10) | +| Display | `show()` on concrete subclasses | `show()` on concrete subclasses | +| Building items | N/A | `add(item)`, `create(**kwargs)` | + +**Update priority:** lower values run first. This ensures correct +execution order within a datablock (e.g. background before data). + +### 2.4 DatablockItem and DatablockCollection + +| Aspect | `DatablockItem` | `DatablockCollection` | +| ------------------ | ------------------------------------------- | ------------------------------ | +| CIF analogy | A single `data_` block | Collection of data blocks | +| Examples | Structure, BraggPdExperiment | Structures, Experiments | +| Category discovery | Scans `vars(self)` for categories | N/A | +| Update cascade | `_update_categories()` — sorted by priority | N/A | +| Parameters | Aggregated from all categories | Aggregated from all datablocks | +| Fittable params | N/A | Non-constrained `Parameter`s | +| Free params | N/A | Fittable + `free == True` | +| Dirty flag | `_need_categories_update` | N/A | + +When any `Parameter.value` is set, it propagates +`_need_categories_update = True` up to the owning `DatablockItem`. +Serialisation (`as_cif`) and plotting trigger `_update_categories()` if +the flag is set. + +### 2.5 Variable System — Parameters and Descriptors + +```shell +GuardedBase +└── GenericDescriptorBase # name, value (validated via AttributeSpec), description + ├── GenericStringDescriptor # _value_type = DataTypes.STRING + └── GenericNumericDescriptor # _value_type = DataTypes.NUMERIC, + units + └── GenericParameter # + free, uncertainty, fit_min, fit_max, constrained, uid +``` + +CIF-bound concrete classes add a `CifHandler` for serialisation: + +| Class | Base | Use case | +| ------------------- | -------------------------- | ---------------------------- | +| `StringDescriptor` | `GenericStringDescriptor` | Read-only or writable text | +| `NumericDescriptor` | `GenericNumericDescriptor` | Read-only or writable number | +| `Parameter` | `GenericParameter` | Fittable numeric value | + +**Initialisation rule:** all Parameters/Descriptors are initialised with +their default values from `value_spec` (an `AttributeSpec`) **without +any validation** — we trust internal definitions. Changes go through +public property setters, which run both type and value validation. + +**Mixin safety:** Parameter/Descriptor classes must not have init +arguments so they can be used as mixins safely (e.g. +`PdTofDataPointMixin`). + +### 2.6 Validation + +`AttributeSpec` bundles `default`, `data_type`, `validator`, +`allow_none`. Validators include: + +| Validator | Purpose | +| --------------------- | -------------------------------------- | +| `TypeValidator` | Checks Python type against `DataTypes` | +| `RangeValidator` | `ge`, `le`, `gt`, `lt` bounds checking | +| `MembershipValidator` | Value must be in an allowed set | +| `RegexValidator` | Value must match a pattern | + +--- + +## 3. Experiment System + +### 3.1 Experiment Type + +An experiment's type is defined by the four enum axes and is **immutable +after creation**. This avoids the complexity of transforming all +internal state when the experiment type changes. The type is stored in +an `ExperimentType` category with four `StringDescriptor`s validated by +`MembershipValidator`s. Public properties are read-only; factory and +CIF-loading code use private setters (`_set_sample_form`, +`_set_beam_mode`, `_set_radiation_probe`, `_set_scattering_type`) during +construction only. + +### 3.2 Experiment Hierarchy + +```shell +DatablockItem +└── ExperimentBase # name, type: ExperimentType, as_cif + ├── PdExperimentBase # + linked_phases, excluded_regions, peak, data + │ ├── BraggPdExperiment # + instrument, background (both via factories) + │ └── TotalPdExperiment # (no extra categories yet) + └── ScExperimentBase # + linked_crystal, extinction, instrument, data + ├── CwlScExperiment + └── TofScExperiment +``` + +Each concrete experiment class carries: + +- `type_info: TypeInfo` — tag and description for factory lookup +- `compatibility: Compatibility` — which enum axis values it supports + +### 3.3 Category Ownership + +Every experiment owns its categories as private attributes with public +read-only or read-write properties: + +```python +# Read-only — user cannot replace the object, only modify its contents +experiment.linked_phases # CategoryCollection +experiment.excluded_regions # CategoryCollection +experiment.instrument # CategoryItem +experiment.peak # CategoryItem +experiment.data # CategoryCollection + +# Type-switchable — recreates the underlying object +experiment.background_type = 'chebyshev' # triggers BackgroundFactory.create(...) +experiment.peak_profile_type = 'thompson-cox-hastings' # triggers PeakFactory.create(...) +experiment.extinction_type = 'shelx' # triggers ExtinctionFactory.create(...) +experiment.linked_crystal_type = 'default' # triggers LinkedCrystalFactory.create(...) +experiment.excluded_regions_type = 'default' # triggers ExcludedRegionsFactory.create(...) +experiment.linked_phases_type = 'default' # triggers LinkedPhasesFactory.create(...) +``` + +**Type switching pattern:** `expt.background_type = 'chebyshev'` rather +than `expt.background.type = 'chebyshev'`. This keeps the API at the +experiment level and makes it clear that the entire category object is +being replaced. + +--- + +## 4. Structure System + +### 4.1 Structure Hierarchy + +```shell +DatablockItem +└── Structure # name, cell, space_group, atom_sites +``` + +A `Structure` contains three categories: + +- `Cell` — unit cell parameters (`CategoryItem`) +- `SpaceGroup` — symmetry information (`CategoryItem`) +- `AtomSites` — atomic positions collection (`CategoryCollection`) + +Symmetry constraints (cell metric, atomic coordinates, ADPs) are applied +via the `crystallography` module during `_update_categories()`. + +--- + +## 5. Factory System + +### 5.1 FactoryBase + +All factories inherit from `FactoryBase`, which provides: + +| Feature | Method / Attribute | Description | +| ------------------ | ---------------------------- | ------------------------------------------------- | +| Registration | `@Factory.register` | Class decorator, appends to `_registry` | +| Supported map | `_supported_map()` | `{tag: class}` from all registered classes | +| Creation | `create(tag)` | Instantiate by tag string | +| Default resolution | `default_tag(**conditions)` | Largest-subset matching on `_default_rules` | +| Context creation | `create_default_for(**cond)` | Resolve tag → create | +| Filtered query | `supported_for(**filters)` | Filter by `Compatibility` and `CalculatorSupport` | +| Display | `show_supported(**filters)` | Pretty-print table of type + description | +| Tag listing | `supported_tags()` | List of all registered tags | + +Each `__init_subclass__` gives every factory its own independent +`_registry` and `_default_rules`. + +### 5.2 Default Rules + +`_default_rules` maps frozensets of `(axis_name, enum_value)` tuples to +tag strings (preferably enum values for type safety): + +```python +class PeakFactory(FactoryBase): + _default_rules = { + frozenset({ + ('scattering_type', ScatteringTypeEnum.BRAGG), + ('beam_mode', BeamModeEnum.CONSTANT_WAVELENGTH), + }): PeakProfileTypeEnum.PSEUDO_VOIGT, + frozenset({ + ('scattering_type', ScatteringTypeEnum.BRAGG), + ('beam_mode', BeamModeEnum.TIME_OF_FLIGHT), + }): PeakProfileTypeEnum.PSEUDO_VOIGT_IKEDA_CARPENTER, + frozenset({ + ('scattering_type', ScatteringTypeEnum.TOTAL), + }): PeakProfileTypeEnum.GAUSSIAN_DAMPED_SINC, + } +``` + +Resolution uses **largest-subset matching**: the rule whose frozenset is +the biggest subset of the given conditions wins. `frozenset()` acts as a +universal fallback. + +### 5.3 Metadata on Registered Classes + +Every `@Factory.register`-ed class carries three frozen dataclass +attributes: + +```python +@PeakFactory.register +class CwlPseudoVoigt(PeakBase, CwlBroadeningMixin): + type_info = TypeInfo( + tag='pseudo-voigt', + description='Pseudo-Voigt profile', + ) + compatibility = Compatibility( + scattering_type=frozenset({ScatteringTypeEnum.BRAGG}), + beam_mode=frozenset({BeamModeEnum.CONSTANT_WAVELENGTH}), + ) + calculator_support = CalculatorSupport( + calculators=frozenset({CalculatorEnum.CRYSPY, CalculatorEnum.CRYSFML}), + ) +``` + +| Metadata | Purpose | +| ------------------- | ------------------------------------------------------- | +| `TypeInfo` | Stable tag for lookup/serialisation + human description | +| `Compatibility` | Which enum axis values this class works with | +| `CalculatorSupport` | Which calculation engines support this class | + +### 5.4 Registration Trigger + +Concrete classes use `@Factory.register` decorators. To trigger +registration, each package's `__init__.py` must **explicitly import** +every concrete class: + +```python +# datablocks/experiment/categories/background/__init__.py +from .chebyshev import ChebyshevPolynomialBackground +from .line_segment import LineSegmentBackground +``` + +### 5.5 All Factories + +| Factory | Domain | Tags resolve to | +| ---------------------------- | ---------------------- | ----------------------------------------------------------- | +| `BackgroundFactory` | Background categories | `LineSegmentBackground`, `ChebyshevPolynomialBackground` | +| `PeakFactory` | Peak profiles | `CwlPseudoVoigt`, `TofPseudoVoigtIkedaCarpenter`, … | +| `InstrumentFactory` | Instruments | `CwlPdInstrument`, `TofPdInstrument`, … | +| `DataFactory` | Data collections | `PdCwlData`, `PdTofData`, `ReflnData`, `TotalData` | +| `ExtinctionFactory` | Extinction models | `ShelxExtinction` | +| `LinkedCrystalFactory` | Linked-crystal refs | `LinkedCrystal` | +| `ExcludedRegionsFactory` | Excluded regions | `ExcludedRegions` | +| `LinkedPhasesFactory` | Linked phases | `LinkedPhases` | +| `ExperimentTypeFactory` | Experiment descriptors | `ExperimentType` | +| `CellFactory` | Unit cells | `Cell` | +| `SpaceGroupFactory` | Space groups | `SpaceGroup` | +| `AtomSitesFactory` | Atom sites | `AtomSites` | +| `AliasesFactory` | Parameter aliases | `Aliases` | +| `ConstraintsFactory` | Parameter constraints | `Constraints` | +| `FitModeFactory` | Fit-mode category | `FitMode` | +| `JointFitExperimentsFactory` | Joint-fit weights | `JointFitExperiments` | +| `CalculatorFactory` | Calculation engines | `CryspyCalculator`, `CrysfmlCalculator`, `PdffitCalculator` | +| `MinimizerFactory` | Minimisers | `LmfitMinimizer`, `DfolsMinimizer`, … | + +> **Note:** `ExperimentFactory` and `StructureFactory` are _builder_ +> factories with `from_cif_path`, `from_cif_str`, `from_data_path`, and +> `from_scratch` classmethods. `ExperimentFactory` inherits +> `FactoryBase` and uses `@register` on all four concrete experiment +> classes; `_resolve_class` looks up the registered class via +> `default_tag()` + `_supported_map()`. `StructureFactory` is a plain +> class without `FactoryBase` inheritance (only one structure type +> exists today). + +### 5.6 Tag Naming Convention + +Tags are the user-facing identifiers for selecting types. They must be: + +- **Consistent** — use the same abbreviations everywhere. +- **Hyphen-separated** — all lowercase, words joined by hyphens. +- **Semantically ordered** — from general to specific. +- **Unique within a factory** — but may overlap across factories. + +#### Standard Abbreviations + +| Concept | Abbreviation | Never use | +| ------------------- | ------------ | --------------------------- | +| Powder | `pd` | `powder` | +| Single crystal | `sc` | `single-crystal` | +| Constant wavelength | `cwl` | `cw`, `constant-wavelength` | +| Time-of-flight | `tof` | `time-of-flight` | +| Bragg (scattering) | `bragg` | | +| Total (scattering) | `total` | | + +#### Complete Tag Registry + +**Background tags** + +| Tag | Class | +| -------------- | ------------------------------- | +| `line-segment` | `LineSegmentBackground` | +| `chebyshev` | `ChebyshevPolynomialBackground` | + +**Peak tags** + +| Tag | Class | +| ---------------------------------- | ------------------------------ | +| `pseudo-voigt` | `CwlPseudoVoigt` | +| `split-pseudo-voigt` | `CwlSplitPseudoVoigt` | +| `thompson-cox-hastings` | `CwlThompsonCoxHastings` | +| `tof-pseudo-voigt` | `TofPseudoVoigt` | +| `tof-pseudo-voigt-ikeda-carpenter` | `TofPseudoVoigtIkedaCarpenter` | +| `tof-pseudo-voigt-back-to-back` | `TofPseudoVoigtBackToBack` | +| `gaussian-damped-sinc` | `TotalGaussianDampedSinc` | + +**Instrument tags** + +| Tag | Class | +| -------- | ----------------- | +| `cwl-pd` | `CwlPdInstrument` | +| `cwl-sc` | `CwlScInstrument` | +| `tof-pd` | `TofPdInstrument` | +| `tof-sc` | `TofScInstrument` | + +**Data tags** + +| Tag | Class | +| -------------- | ----------- | +| `bragg-pd-cwl` | `PdCwlData` | +| `bragg-pd-tof` | `PdTofData` | +| `bragg-sc` | `ReflnData` | +| `total-pd` | `TotalData` | + +**Extinction tags** + +| Tag | Class | +| ------- | ----------------- | +| `shelx` | `ShelxExtinction` | + +**Linked-crystal tags** + +| Tag | Class | +| --------- | --------------- | +| `default` | `LinkedCrystal` | + +**Experiment tags** + +| Tag | Class | +| -------------- | ------------------- | +| `bragg-pd` | `BraggPdExperiment` | +| `total-pd` | `TotalPdExperiment` | +| `bragg-sc-cwl` | `CwlScExperiment` | +| `bragg-sc-tof` | `TofScExperiment` | + +**Calculator tags** + +| Tag | Class | +| --------- | ------------------- | +| `cryspy` | `CryspyCalculator` | +| `crysfml` | `CrysfmlCalculator` | +| `pdffit` | `PdffitCalculator` | + +**Minimizer tags** + +| Tag | Class | +| ----------------------- | ----------------------------------------- | +| `lmfit` | `LmfitMinimizer` | +| `lmfit (leastsq)` | `LmfitMinimizer` (method=`leastsq`) | +| `lmfit (least_squares)` | `LmfitMinimizer` (method=`least_squares`) | +| `dfols` | `DfolsMinimizer` | + +> **Note:** minimizer variant tags (`lmfit (leastsq)`, +> `lmfit (least_squares)`) are planned but not yet re-implemented after +> the `FactoryBase` migration. See `issues_open.md` for details. + +### 5.7 Metadata Classification — Which Classes Get What + +#### The Rule + +> **If a concrete class is created by a factory, it gets `type_info`, +> `compatibility`, and `calculator_support`.** +> +> **If a `CategoryItem` only exists as a child row inside a +> `CategoryCollection`, it does NOT get these attributes — the +> collection does.** + +#### Rationale + +A `LineSegment` item (a single background control point) is never +selected, created, or queried by a factory. It is always instantiated +internally by its parent `LineSegmentBackground` collection. The +meaningful unit of selection is the _collection_, not the item. The user +picks "line-segment background" (the collection type), not individual +line-segment points. + +#### Singleton CategoryItems — factory-created (get all three) + +| Class | Factory | +| ------------------------------ | ----------------------- | +| `CwlPdInstrument` | `InstrumentFactory` | +| `CwlScInstrument` | `InstrumentFactory` | +| `TofPdInstrument` | `InstrumentFactory` | +| `TofScInstrument` | `InstrumentFactory` | +| `CwlPseudoVoigt` | `PeakFactory` | +| `CwlSplitPseudoVoigt` | `PeakFactory` | +| `CwlThompsonCoxHastings` | `PeakFactory` | +| `TofPseudoVoigt` | `PeakFactory` | +| `TofPseudoVoigtIkedaCarpenter` | `PeakFactory` | +| `TofPseudoVoigtBackToBack` | `PeakFactory` | +| `TotalGaussianDampedSinc` | `PeakFactory` | +| `ShelxExtinction` | `ExtinctionFactory` | +| `LinkedCrystal` | `LinkedCrystalFactory` | +| `Cell` | `CellFactory` | +| `SpaceGroup` | `SpaceGroupFactory` | +| `ExperimentType` | `ExperimentTypeFactory` | +| `FitMode` | `FitModeFactory` | + +#### CategoryCollections — factory-created (get all three) + +| Class | Factory | +| ------------------------------- | ---------------------------- | +| `LineSegmentBackground` | `BackgroundFactory` | +| `ChebyshevPolynomialBackground` | `BackgroundFactory` | +| `PdCwlData` | `DataFactory` | +| `PdTofData` | `DataFactory` | +| `TotalData` | `DataFactory` | +| `ReflnData` | `DataFactory` | +| `ExcludedRegions` | `ExcludedRegionsFactory` | +| `LinkedPhases` | `LinkedPhasesFactory` | +| `AtomSites` | `AtomSitesFactory` | +| `Aliases` | `AliasesFactory` | +| `Constraints` | `ConstraintsFactory` | +| `JointFitExperiments` | `JointFitExperimentsFactory` | + +#### CategoryItems that are ONLY children of collections (NO metadata) + +| Class | Parent collection | +| -------------------- | ------------------------------- | +| `LineSegment` | `LineSegmentBackground` | +| `PolynomialTerm` | `ChebyshevPolynomialBackground` | +| `AtomSite` | `AtomSites` | +| `PdCwlDataPoint` | `PdCwlData` | +| `PdTofDataPoint` | `PdTofData` | +| `TotalDataPoint` | `TotalData` | +| `Refln` | `ReflnData` | +| `LinkedPhase` | `LinkedPhases` | +| `ExcludedRegion` | `ExcludedRegions` | +| `Alias` | `Aliases` | +| `Constraint` | `Constraints` | +| `JointFitExperiment` | `JointFitExperiments` | + +#### Non-category classes — factory-created (get `type_info` only) + +| Class | Factory | Notes | +| ------------------- | ------------------- | -------------------------------------------------------- | +| `CryspyCalculator` | `CalculatorFactory` | No `compatibility` — limitations expressed on categories | +| `CrysfmlCalculator` | `CalculatorFactory` | (same) | +| `PdffitCalculator` | `CalculatorFactory` | (same) | +| `LmfitMinimizer` | `MinimizerFactory` | `type_info` only | +| `DfolsMinimizer` | `MinimizerFactory` | (same) | +| `BraggPdExperiment` | `ExperimentFactory` | `type_info` + `compatibility` (no `calculator_support`) | +| `TotalPdExperiment` | `ExperimentFactory` | (same) | +| `CwlScExperiment` | `ExperimentFactory` | (same) | +| `TofScExperiment` | `ExperimentFactory` | (same) | + +--- + +## 6. Analysis + +### 6.1 Calculator + +The calculator performs the actual diffraction computation. It is +attached per-experiment on the `ExperimentBase` object. Each experiment +auto-resolves its calculator on first access based on the data +category's `calculator_support` metadata and +`CalculatorFactory._default_rules`. The `CalculatorFactory` filters its +registry by `engine_imported` (whether the third-party library is +available in the environment). + +The experiment exposes the standard switchable-category API: + +- `calculator` — read-only property (lazy, auto-resolved on first + access) +- `calculator_type` — getter + setter +- `show_supported_calculator_types()` — filtered by data category + support +- `show_current_calculator_type()` + +### 6.2 Minimiser + +The minimiser drives the optimisation loop. `MinimizerFactory` creates +instances by tag (e.g. `'lmfit'`, `'dfols'`). + +### 6.3 Fitter + +`Fitter` wraps a minimiser instance and orchestrates the fitting +workflow: + +1. Collect `free_parameters` from structures + experiments. +2. Record start values. +3. Build an objective function that calls the calculator. +4. Delegate to `minimizer.fit()`. +5. Sync results (values + uncertainties) back to parameters. + +### 6.4 Analysis Object + +`Analysis` is bound to a `Project` and provides the high-level API: + +- Minimiser selection: `current_minimizer`, + `show_available_minimizers()` +- Fit mode: `fit_mode` (`CategoryItem` with a `mode` descriptor + validated by `FitModeEnum`); `'single'` fits each experiment + independently, `'joint'` fits all simultaneously with weights from + `joint_fit_experiments`. +- Joint-fit weights: `joint_fit_experiments` (`CategoryCollection` of + per-experiment weight entries); sibling of `fit_mode`, not a child. +- Parameter tables: `show_all_params()`, `show_fittable_params()`, + `show_free_params()`, `how_to_access_parameters()` +- Fitting: `fit()`, `show_fit_results()` +- Aliases and constraints (switchable categories with `aliases_type`, + `constraints_type`, `fit_mode_type`, `joint_fit_experiments_type`) + +--- + +## 7. Project — The Top-Level Façade + +`Project` is the single entry point for the user: + +```python +import easydiffraction as ed + +project = ed.Project(name='my_project') +``` + +It owns and coordinates all components: + +| Property | Type | Description | +| --------------------- | ------------- | ---------------------------------------- | +| `project.info` | `ProjectInfo` | Metadata: name, title, description, path | +| `project.structures` | `Structures` | Collection of structure datablocks | +| `project.experiments` | `Experiments` | Collection of experiment datablocks | +| `project.analysis` | `Analysis` | Calculator, minimiser, fitting | +| `project.summary` | `Summary` | Report generation | +| `project.plotter` | `Plotter` | Visualisation | +| `project.verbosity` | `str` | Console output level (full/short/silent) | + +### 7.1 Data Flow + +``` +Parameter.value set + → AttributeSpec validation (type + value) + → _need_categories_update = True (on parent DatablockItem) + +Plot / CIF export / fit objective evaluation + → _update_categories() + → categories sorted by _update_priority + → each category._update() + → background: interpolate/evaluate → write to data + → calculator: compute pattern → write to data + → _need_categories_update = False +``` + +### 7.2 Persistence + +Projects are saved as a directory of CIF files: + +```shell +project_dir/ +├── project.cif # ProjectInfo +├── analysis.cif # Analysis settings +├── summary.cif # Summary report +├── structures/ +│ └── lbco.cif # One file per structure +└── experiments/ + └── hrpt.cif # One file per experiment +``` + +### 7.3 Verbosity + +`Project.verbosity` controls how much console output operations produce. +It is backed by `VerbosityEnum` (in `utils/enums.py`) and accepts three +values: + +| Level | Enum member | Behaviour | +| -------- | ---------------------- | -------------------------------------------------- | +| `full` | `VerbosityEnum.FULL` | Multi-line output with headers, tables, and detail | +| `short` | `VerbosityEnum.SHORT` | One-line status message per action | +| `silent` | `VerbosityEnum.SILENT` | No console output | + +The default is `'full'`. + +```python +project.verbosity = 'short' +``` + +**Resolution order:** methods that produce console output (e.g. +`analysis.fit()`, `experiments.add_from_data_path()`) accept an optional +`verbosity` keyword argument. When the argument is `None` (the default), +the method reads `project.verbosity`. When a string is passed, it +overrides the project-level setting for that single call. + +```python +# Use project-level default for all operations +project.verbosity = 'short' +project.analysis.fit() # → short mode + +# Override for a single call +project.analysis.fit(verbosity='silent') # → silent, project stays short +``` + +**Output styles per level:** + +- **Data loading** — `full`: paragraph header + detail line; `short`: + `✅ Data loaded: Experiment 🔬 'name'. N points.`; `silent`: nothing. +- **Fitting** — `full`: per-iteration progress table with improvement + percentages; `short`: one-row-per-experiment summary table; `silent`: + nothing. + +--- + +## 8. User-Facing API Patterns + +All examples below are drawn from the actual tutorials (`tutorials/`). + +> **Notebook workflow:** Jupyter notebooks (`*.ipynb`) in +> `docs/docs/tutorials/` are generated artifacts. Edit only the +> corresponding `*.py` script, then run `pixi run notebook-convert` +> followed by `pixi run notebook-prepare` to regenerate the notebook. +> Never edit `*.ipynb` files by hand. + +### 8.1 Project Setup + +```python +import easydiffraction as ed + +project = ed.Project(name='lbco_hrpt') +project.info.title = 'La0.5Ba0.5CoO3 at HRPT@PSI' +project.save_as(dir_path='lbco_hrpt', temporary=True) +``` + +### 8.2 Define Structures + +```python +# Create a structure datablock +project.structures.create(name='lbco') + +# Set space group and unit cell +project.structures['lbco'].space_group.name_h_m = 'P m -3 m' +project.structures['lbco'].cell.length_a = 3.88 + +# Add atom sites +project.structures['lbco'].atom_sites.create( + label='La', + type_symbol='La', + fract_x=0, + fract_y=0, + fract_z=0, + wyckoff_letter='a', + b_iso=0.5, + occupancy=0.5, +) + +# Show as CIF +project.structures['lbco'].show_as_cif() +``` + +### 8.3 Define Experiments + +```python +# Download data and create experiment from a data file +data_path = ed.download_data(id=3, destination='data') +project.experiments.add_from_data_path( + name='hrpt', + data_path=data_path, + sample_form='powder', + beam_mode='constant wavelength', + radiation_probe='neutron', +) + +# Set instrument parameters +project.experiments['hrpt'].instrument.setup_wavelength = 1.494 +project.experiments['hrpt'].instrument.calib_twotheta_offset = 0.6 + +# Browse and select peak profile type +project.experiments['hrpt'].show_supported_peak_profile_types() +project.experiments['hrpt'].peak_profile_type = 'pseudo-voigt' + +# Set peak profile parameters +project.experiments['hrpt'].peak.broad_gauss_u = 0.1 +project.experiments['hrpt'].peak.broad_gauss_v = -0.1 + +# Browse and select background type +project.experiments['hrpt'].show_supported_background_types() +project.experiments['hrpt'].background_type = 'line-segment' + +# Add background points +project.experiments['hrpt'].background.create(id='10', x=10, y=170) +project.experiments['hrpt'].background.create(id='50', x=50, y=170) + +# Link structure to experiment +project.experiments['hrpt'].linked_phases.create(id='lbco', scale=10.0) +``` + +### 8.4 Analysis and Fitting + +```python +# Calculator is auto-resolved per experiment; override if needed +project.experiments['hrpt'].show_supported_calculator_types() +project.experiments['hrpt'].calculator_type = 'cryspy' +project.analysis.current_minimizer = 'lmfit' + +# Plot before fitting +project.plot_meas_vs_calc(expt_name='hrpt', show_residual=True) + +# Select free parameters +project.structures['lbco'].cell.length_a.free = True +project.experiments['hrpt'].linked_phases['lbco'].scale.free = True +project.experiments['hrpt'].instrument.calib_twotheta_offset.free = True +project.experiments['hrpt'].background['10'].y.free = True + +# Inspect free parameters +project.analysis.show_free_params() + +# Fit and show results +project.analysis.fit() +project.analysis.show_fit_results() + +# Plot after fitting +project.plot_meas_vs_calc(expt_name='hrpt', show_residual=True) + +# Save +project.save() +``` + +### 8.5 TOF Experiment (tutorial ed-7) + +```python +expt = ExperimentFactory.from_data_path( + name='sepd', + data_path=data_path, + beam_mode='time-of-flight', +) +expt.instrument.calib_d_to_tof_offset = 0.0 +expt.instrument.calib_d_to_tof_linear = 7476.91 +expt.peak_profile_type = 'pseudo-voigt * ikeda-carpenter' +expt.peak.broad_gauss_sigma_0 = 3.0 +``` + +### 8.6 Total Scattering / PDF (tutorial ed-12) + +```python +project.experiments.add_from_data_path( + name='xray_pdf', + data_path=data_path, + sample_form='powder', + beam_mode='constant wavelength', + radiation_probe='xray', + scattering_type='total', +) +project.experiments['xray_pdf'].peak_profile_type = 'gaussian-damped-sinc' +# Calculator is auto-resolved to 'pdffit' for total scattering experiments +``` + +--- + +## 9. Design Principles + +### 9.1 Naming and CIF Conventions + +- Follow CIF naming conventions where possible. Deviate for better API + design when necessary, but keep the spirit of CIF names. +- Reuse the concept of datablocks and categories from CIF. +- `DatablockItem` = one CIF `data_` block, `DatablockCollection` = set + of blocks. +- `CategoryItem` = one CIF category, `CategoryCollection` = CIF loop. + +### 9.2 Immutability of Experiment Type + +The experiment type (the four enum axes) can only be set at creation +time. It cannot be changed afterwards. This avoids the complexity of +maintaining different state transformations when switching between +fundamentally different experiment configurations. + +### 9.3 Category Type Switching + +In contrast to experiment type, categories that have multiple +implementations (peak profiles, backgrounds, instruments) can be +switched at runtime by the user. The API pattern uses a type property on +the **experiment**, not on the category itself: + +```python +# ✅ Correct — type property on the experiment +expt.background_type = 'chebyshev' + +# ❌ Not used — type property on the category +expt.background.type = 'chebyshev' +``` + +This makes it clear that the entire category object is being replaced +and simplifies maintenance. + +### 9.4 Switchable-Category Convention + +Categories whose concrete implementation can be swapped at runtime +(background, peak profile, etc.) are called **switchable categories**. +**Every category must be factory-based** — even if only one +implementation exists today. This ensures a uniform API, consistent +discoverability, and makes adding a second implementation trivial. + +| Facet | Naming pattern | Example | +| --------------- | -------------------------------------------- | ------------------------------------------------ | +| Current object | `` property (read-only) | `expt.background`, `expt.peak` | +| Active type tag | `_type` property (getter + setter) | `expt.background_type`, `expt.peak_profile_type` | +| Show supported | `show_supported__types()` | `expt.show_supported_background_types()` | +| Show current | `show_current__type()` | `expt.show_current_peak_profile_type()` | + +The convention applies universally: + +- **Experiment:** `calculator_type`, `background_type`, + `peak_profile_type`, `extinction_type`, `linked_crystal_type`, + `excluded_regions_type`, `linked_phases_type`, `instrument_type`, + `data_type`. +- **Structure:** `cell_type`, `space_group_type`, `atom_sites_type`. +- **Analysis:** `aliases_type`, `constraints_type`, `fit_mode_type`, + `joint_fit_experiments_type`. + +**Design decisions:** + +- The **experiment owns** the `_type` setter because switching replaces + the entire category object + (`self._background = BackgroundFactory.create(...)`). +- The **experiment owns** the `show_*` methods because they are + one-liners that delegate to `Factory.show_supported(...)` and can pass + experiment-specific context (e.g. `scattering_type`, `beam_mode` for + peak filtering). +- Concrete category subclasses provide a public `show()` method for + displaying the current content (not on the base + `CategoryItem`/`CategoryCollection`). + +### 9.5 Discoverable Supported Options + +The user can always discover what is supported for the current +experiment: + +```python +expt.show_supported_peak_profile_types() +expt.show_supported_background_types() +expt.show_supported_calculator_types() +expt.show_supported_extinction_types() +expt.show_supported_linked_crystal_types() +expt.show_supported_excluded_regions_types() +expt.show_supported_linked_phases_types() +expt.show_supported_instrument_types() +expt.show_supported_data_types() +struct.show_supported_cell_types() +struct.show_supported_space_group_types() +struct.show_supported_atom_sites_types() +project.analysis.show_supported_aliases_types() +project.analysis.show_supported_constraints_types() +project.analysis.show_supported_fit_mode_types() +project.analysis.show_supported_joint_fit_experiments_types() +project.analysis.show_available_minimizers() +``` + +Available calculators are filtered by `engine_imported` (whether the +library is installed) and by the experiment's data category +`calculator_support` metadata. + +### 9.6 Enums for Finite Value Sets + +Every attribute, descriptor, or configuration option that accepts a +**finite, closed set of values** must be represented by a `(str, Enum)` +class. This applies to: + +- Factory tags (§5.6) — e.g. `PeakProfileTypeEnum`, `CalculatorEnum`. +- Experiment-axis values — e.g. `SampleFormEnum`, `BeamModeEnum`. +- Category descriptors with enumerated choices — e.g. fit mode + (`FitModeEnum.SINGLE`, `FitModeEnum.JOINT`). + +The enum serves as the **single source of truth** for valid values, +their user-facing string representations, and their descriptions. +Benefits: + +- **Autocomplete and typo safety** — IDEs list valid members; + misspellings are caught at assignment time. +- **Greppable** — searching for `FitModeEnum.JOINT` finds every code + path that handles joint fitting. +- **Type-safe dispatch** — `if mode == FitModeEnum.JOINT:` is checked by + type checkers; `if mode == 'joint':` is not. +- **Consistent validation** — use `MembershipValidator` with the enum + members instead of `RegexValidator` with hand-written patterns. + +**Rule:** internal code must compare against enum members, never raw +strings. User-facing setters accept either the enum member or its string +value (because `str(EnumMember) == EnumMember.value` for `(str, Enum)`), +but internal dispatch always uses the enum: + +```python +# ✅ Correct — compare with enum +if self._fit_mode.mode.value == FitModeEnum.JOINT: + +# ❌ Wrong — compare with raw string +if self._fit_mode.mode.value == 'joint': +``` + +### 9.7 Flat Category Structure — No Nested Categories + +Following CIF conventions, categories are **flat siblings** within their +owner (datablock or analysis object). A category must never be a child +of another category of a different type. Categories can reference each +other via IDs, but the ownership hierarchy is always: + +``` +Owner (DatablockItem / Analysis) +├── CategoryA (CategoryItem or CategoryCollection) +├── CategoryB (CategoryItem or CategoryCollection) +└── CategoryC (CategoryItem or CategoryCollection) +``` + +Never: + +``` +Owner +└── CategoryA + └── CategoryB ← WRONG: CategoryB is a child of CategoryA +``` + +**Example — `fit_mode` and `joint_fit_experiments`:** `fit_mode` is a +`CategoryItem` holding the active strategy (`'single'` or `'joint'`). +`joint_fit_experiments` is a separate `CategoryCollection` holding +per-experiment weights. Both are direct children of `Analysis`, not +nested: + +```python +# ✅ Correct — sibling categories on Analysis +project.analysis.fit_mode.mode = 'joint' +project.analysis.joint_fit_experiments['npd'].weight = 0.7 + +# ❌ Wrong — joint_fit_experiments as a child of fit_mode +project.analysis.fit_mode.joint_fit_experiments['npd'].weight = 0.7 +``` + +In CIF output, sibling categories appear as independent blocks: + +``` +_analysis.fit_mode joint + +loop_ +_joint_fit_experiment.id +_joint_fit_experiment.weight +npd 0.7 +xrd 0.3 +``` + +### 9.8 Property Docstring and Type-Hint Template + +Every public property backed by a private `Parameter`, +`NumericDescriptor`, or `StringDescriptor` attribute must follow the +template below. The `description` field on the descriptor is the +**single source of truth**; docstrings and type hints are mechanically +derived from it. + +**Definitions:** + +| Symbol | Meaning | +| --------- | -------------------------------------------------------------------------------------- | +| `{desc}` | `description` string without trailing period | +| `{units}` | `units` string; omit the `({units})` parenthetical when absent/empty | +| `{Type}` | Descriptor class name: `Parameter`, `NumericDescriptor`, or `StringDescriptor` | +| `{ann}` | Setter value annotation: `float` for numeric descriptors, `str` for string descriptors | + +**Template — writable property:** + +```python +@property +def length_a(self) -> Parameter: + """Length of the a axis of the unit cell (Å). + + Reading this property returns the underlying ``Parameter`` + object. Assigning to it updates the parameter value. + """ + return self._length_a + +@length_a.setter +def length_a(self, value: float) -> None: + self._length_a.value = value +``` + +**Template — read-only property:** + +```python +@property +def length_a(self) -> Parameter: + """Length of the a axis of the unit cell (Å). + + Reading this property returns the underlying ``Parameter`` + object. + """ + return self._length_a +``` + +**Quick-reference table:** + +| Element | Text | +| ---------------------- | -------------------------------------------------------------------------------------------------------------- | +| Getter summary line | `"""{desc} ({units}).` (or `"""{desc}.` when unitless) | +| Getter body (writable) | `Reading this property returns the underlying ``{Type}`` object. Assigning to it updates the parameter value.` | +| Getter body (readonly) | `Reading this property returns the underlying ``{Type}`` object.` | +| Setter docstring | _(none — not rendered by griffe / MkDocs)_ | +| Getter annotation | `-> {Type}` | +| Setter annotation | `value: {ann}` and `-> None` | + +**Notes:** + +- Getter docstrings have **no** `Args:` or `Returns:` sections. +- Setters have **no** docstring. +- Avoid markdown emphasis (`*a*`) in docstrings; use plain text to stay + in sync with the `description` field. +- The CI tool `pixi run param-consistency-check` validates compliance; + `pixi run param-consistency-fix` auto-fixes violations. + +--- + +## 10. Issues + +- **Open:** [`issues_open.md`](issues_open.md) — prioritised backlog. +- **Closed:** [`issues_closed.md`](issues_closed.md) — resolved items + for reference. + +When a resolution affects the architecture described above, the relevant +sections of this document are updated accordingly. diff --git a/docs/architecture/issues_closed.md b/docs/architecture/issues_closed.md new file mode 100644 index 00000000..a67edcbe --- /dev/null +++ b/docs/architecture/issues_closed.md @@ -0,0 +1,67 @@ +# EasyDiffraction — Closed Issues + +Issues that have been fully resolved. Kept for historical reference. + +--- + +## Dirty-Flag Guard Was Disabled + +**Resolution:** added `_set_value_from_minimizer()` on +`GenericDescriptorBase` that writes `_value` directly (no validation) +but sets the dirty flag on the parent `DatablockItem`. Both +`LmfitMinimizer` and `DfolsMinimizer` now use it. The guard in +`DatablockItem._update_categories()` is enabled and skips redundant +updates on the user-facing path (CIF export, plotting). During fitting +the guard is bypassed (`called_by_minimizer=True`) because experiment +calculations depend on structure parameters owned by a different +`DatablockItem`. + +--- + +## Move Calculator from Global to Per-Experiment + +**Resolution:** removed the global calculator from `Analysis`. Each +experiment now owns its calculator, auto-resolved on first access from +`CalculatorFactory._default_rules` (maps `scattering_type` → default +tag) and filtered by the data category's `calculator_support` metadata +(e.g. `PdCwlData` → `{CRYSPY}`, `TotalData` → `{PDFFIT}`). Calculator +classes no longer carry `compatibility` attributes — limitations are +expressed on categories. The experiment exposes the standard +switchable-category API: `calculator` (read-only, lazy), +`calculator_type` (getter + setter), +`show_supported_calculator_types()`, `show_current_calculator_type()`. +Tutorials, tests, and docs updated. + +--- + +## Add Universal Factories for All Categories + +**Resolution:** converted every category to use the `FactoryBase` +pattern. Each former single-file category is now a package with +`factory.py` (trivial `FactoryBase` subclass), `default.py` (concrete +class with `@register` + `type_info`), and `__init__.py` (re-exports +preserving import compatibility). + +Experiment categories: `Extinction` → `ShelxExtinction` / +`ExtinctionFactory` (tag `shelx`), `LinkedCrystal` / +`LinkedCrystalFactory` (tag `default`), `ExcludedRegions` / +`ExcludedRegionsFactory`, `LinkedPhases` / `LinkedPhasesFactory`, +`ExperimentType` / `ExperimentTypeFactory`. + +Structure categories: `Cell` / `CellFactory`, `SpaceGroup` / +`SpaceGroupFactory`, `AtomSites` / `AtomSitesFactory`. + +Analysis categories: `Aliases` / `AliasesFactory`, `Constraints` / +`ConstraintsFactory`, `JointFitExperiments` / +`JointFitExperimentsFactory`. + +`ShelxExtinction` and `LinkedCrystal` get the full switchable-category +API on `ScExperimentBase` (`extinction_type`, `linked_crystal_type` +getter+setter, `show_supported_*_types()`, `show_current_*_type()`). +`ExcludedRegions` and `LinkedPhases` get the same API on +`PdExperimentBase`. `Cell`, `SpaceGroup`, and `AtomSites` get it on +`Structure`. `Aliases` and `Constraints` get it on `Analysis`. +Architecture §3.3, §5.5, §5.7, §9.4, §9.5 updated. Copilot instructions +updated with universal switchable-category scope and architecture-first +workflow rule. Unit tests extended with factory tests for extinction and +linked-crystal. diff --git a/docs/architecture/issues_open.md b/docs/architecture/issues_open.md new file mode 100644 index 00000000..05ed175f --- /dev/null +++ b/docs/architecture/issues_open.md @@ -0,0 +1,359 @@ +# EasyDiffraction — Open Issues + +Prioritised list of issues, improvements, and design questions to +address. Items are ordered by a combination of user impact, blocking +potential, and implementation readiness. When an item is fully +implemented, remove it from this file and update `architecture.md` if +needed. + +**Legend:** 🔴 High · 🟡 Medium · 🟢 Low + +--- + +## 1. 🔴 Implement `Project.load()` + +**Type:** Completeness + +`save()` serialises all components to CIF files but `load()` is a stub +that raises `NotImplementedError`. Users cannot round-trip a project. + +**Why first:** this is the highest-severity gap. Without it the save +functionality is only half useful — CIF files are written but cannot be +read back. Tutorials that demonstrate save/load are blocked. + +**Fix:** implement `load()` that reads CIF files from the project +directory and reconstructs structures, experiments, and analysis +settings. + +**Depends on:** nothing (standalone). + +--- + +## 2. 🟡 Restore Minimiser Variant Support + +**Type:** Feature loss + Design limitation + +After the `FactoryBase` migration only `'lmfit'` and `'dfols'` remain as +registered tags. The ability to select a specific lmfit algorithm (e.g. +`'lmfit (leastsq)'`, `'lmfit (least_squares)'`) raises a `ValueError`. + +The root cause is that `FactoryBase` assumes one class ↔ one tag; +registering the same class twice with different constructor arguments is +not supported. + +**Fix:** decide on an approach (thin subclasses, extended registry, or +two-level selection) and implement. Thin subclasses is the quickest. + +**Planned tags:** + +| Tag | Description | +| ----------------------- | ------------------------------------------------------------------------ | +| `lmfit` | LMFIT library using the default Levenberg-Marquardt least squares method | +| `lmfit (leastsq)` | LMFIT library with Levenberg-Marquardt least squares method | +| `lmfit (least_squares)` | LMFIT library with SciPy's trust region reflective algorithm | +| `dfols` | DFO-LS library for derivative-free least-squares optimization | + +**Trade-offs:** + +| Approach | Pros | Cons | +| -------------------------------------------------------- | ---------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | +| **A. Thin subclasses** (one per variant) | Works today; each variant gets full metadata; no `FactoryBase` changes | Class proliferation; boilerplate | +| **B. Extend registry to store `(class, kwargs)` tuples** | No extra classes; factory handles variants natively | `_supported_map` changes shape; `TypeInfo` moves from class attribute to registration-time data | +| **C. Two-level selection** (`engine` + `algorithm`) | Clean separation; engine maps to class, algorithm is a constructor arg | More complex API (`current_minimizer = ('lmfit', 'least_squares')`); needs new `FactoryBase` protocol | + +**Depends on:** nothing (standalone, but should be decided before more +factories adopt variants). + +--- + +## 3. 🟡 Rebuild Joint-Fit Weights on Every Fit + +**Type:** Fragility + +`joint_fit_experiments` is created once when `fit_mode` becomes +`'joint'`. If experiments are added, removed, or renamed afterwards, the +weight collection is stale. Joint fitting can fail with missing keys or +run with incorrect weights. + +**Fix:** rebuild or validate `joint_fit_experiments` at the start of +every joint fit. At minimum, `fit()` should assert that the weight keys +exactly match `project.experiments.names`. + +**Depends on:** nothing. + +--- + +## 4. 🔴 Refresh Constraint State Before Automatic Updates and Fitting + +**Type:** Correctness + +`ConstraintsHandler` is only synchronised from `analysis.aliases` and +`analysis.constraints` when the user explicitly calls +`project.analysis.apply_constraints()`. The normal fit / serialisation +path calls `constraints_handler.apply()` directly, so newly added or +edited aliases and constraints can be ignored until that manual sync +step happens. + +**Why high:** this produces silently incorrect results. A user can +define constraints, run a fit, and believe they were applied when the +active singleton still contains stale state from a previous run or no +state at all. + +**Fix:** before any automatic constraint application, always refresh the +singleton from the current `Aliases` and `Constraints` collections. The +sync should happen inside `Analysis._update_categories()` or inside the +constraints category itself, not only in a user-facing helper method. + +**Depends on:** nothing. + +--- + +## 5. 🟡 Make `Analysis` a `DatablockItem` + +**Type:** Consistency + +`Analysis` owns categories (`Aliases`, `Constraints`, +`JointFitExperiments`) but does not extend `DatablockItem`. Its ad-hoc +`_update_categories()` iterates over a hard-coded list and does not +participate in standard category discovery, parameter enumeration, or +CIF serialisation. + +**Fix:** make `Analysis` extend `DatablockItem`, or extract a shared +`_update_categories()` protocol. + +**Depends on:** benefits from issue 1 (load/save) being designed first. + +--- + +## 6. 🔴 Restrict `data_type` Switching to Compatible Types and Preserve Data Safety + +**Type:** Correctness + Data safety + +`Experiment.data_type` currently validates against all registered data +tags rather than only those compatible with the experiment's +`sample_form` / `scattering_type` / `beam_mode`. This allows users to +switch an experiment to an incompatible data collection class. The +setter also replaces the existing data object with a fresh empty +instance, discarding loaded data without warning. + +**Why high:** the current API can create internally inconsistent +experiments and silently lose measured data, which is especially +dangerous for notebook and tutorial workflows. + +**Fix:** filter supported data types through +`DataFactory.supported_for(...)` using the current experiment context, +and warn or block when a switch would discard existing data. If runtime +data-type switching is not a real user need, consider making `data` +effectively fixed after experiment creation. + +**Depends on:** nothing. + +--- + +## 7. 🟡 Eliminate Dummy `Experiments` Wrapper in Single-Fit Mode + +**Type:** Fragility + +Single-fit mode creates a throw-away `Experiments` collection per +experiment, manually forces `_parent` via `object.__setattr__`, and +passes it to `Fitter`. This bypasses `GuardedBase` parent tracking and +is fragile. + +**Fix:** make `Fitter.fit()` accept a list of experiment objects (or a +single experiment) instead of requiring an `Experiments` collection. Or +add a `fit_single(experiment)` method. + +**Depends on:** nothing, but simpler after issue 5 (Analysis refactor) +clarifies the fitting orchestration. + +--- + +## 8. 🟡 Add Explicit `create()` Signatures on Collections + +**Type:** API safety + +`CategoryCollection.create(**kwargs)` accepts arbitrary keyword +arguments and applies them via `setattr`. Typos are silently dropped +(GuardedBase logs a warning but does not raise), so items are created +with incorrect defaults. + +**Fix:** concrete collection subclasses (e.g. `AtomSites`, `Background`) +should override `create()` with explicit parameters for IDE autocomplete +and typo detection. The base `create(**kwargs)` remains as an internal +implementation detail. + +**Depends on:** nothing. + +--- + +## 9. 🟢 Add Future Enum Extensions + +**Type:** Design improvement + +The four current experiment axes will be extended with at least two +more: + +| New axis | Options | Enum (proposed) | +| ------------------- | ---------------------- | ------------------------ | +| Data dimensionality | 1D, 2D | `DataDimensionalityEnum` | +| Beam polarisation | unpolarised, polarised | `PolarisationEnum` | + +These should follow the same `str, Enum` pattern and integrate into +`Compatibility` (new `FrozenSet` fields), `_default_rules`, and +`ExperimentType` (new `StringDescriptor`s with `MembershipValidator`s). + +**Migration path:** existing `Compatibility` objects that don't specify +the new fields use `frozenset()` (empty = "any"), so all existing +classes remain compatible without changes. + +**Depends on:** nothing. + +--- + +## 10. 🟢 Unify Project-Level Update Orchestration + +**Type:** Maintainability + +`Project._update_categories(expt_name)` hard-codes the update order +(structures → analysis → one experiment). The `_update_priority` system +exists on categories but is not used across datablocks. The `expt_name` +parameter means only one experiment is updated per call, inconsistent +with joint-fit workflows. + +**Fix:** consider a project-level `_update_priority` on datablocks, or +at minimum document the required update order. For joint fitting, all +experiments should be updateable in a single call. + +**Depends on:** benefits from issue 5 (Analysis as DatablockItem) and +issue 7 (fitter refactor). + +--- + +## 11. 🟢 Document Category `_update` Contract + +**Type:** Maintainability + +`_update()` is an optional override with a no-op default. A clearer +contract would help contributors: + +- **Active categories** (those that compute something, e.g. + `Background`, `Data`) should have an explicit `_update()` + implementation. +- **Passive categories** (those that only store parameters, e.g. `Cell`, + `SpaceGroup`) keep the no-op default. + +The distinction is already implicit in the code; making it explicit in +documentation (and possibly via a naming convention or flag) would +reduce confusion for new contributors. + +**Depends on:** nothing. + +--- + +## 12. 🟢 Add CIF Round-Trip Integration Test + +**Type:** Quality + +Ensuring every parameter survives a `save()` → `load()` cycle is +critical for reproducibility. A systematic integration test that creates +a project, populates all categories, saves, reloads, and compares all +parameter values would strengthen confidence in the serialisation layer. + +**Depends on:** issue 1 (`Project.load()` implementation). + +--- + +## 13. 🟢 Suppress Redundant Dirty-Flag Sets in Symmetry Constraints + +**Type:** Performance + +Symmetry constraint application (cell metric, atomic coordinates, ADPs) +goes through the public `value` setter for each parameter, setting the +dirty flag repeatedly during what is logically a single batch operation. + +No correctness issue — the dirty-flag guard handles this correctly. The +redundant sets are a minor inefficiency that only matters if profiling +shows it is a bottleneck. + +**Fix:** introduce a private `_set_value_no_notify()` method on +`GenericDescriptorBase` for internal batch operations, or a context +manager / flag on the owning datablock to suppress notifications during +a batch. + +**Depends on:** nothing, but low priority. + +--- + +## 14. 🟢 Finer-Grained Parameter Change Tracking + +**Type:** Performance + +The current dirty-flag approach (`_need_categories_update` on +`DatablockItem`) triggers a full update of all categories when any +parameter changes. This is simple and correct. If performance becomes a +concern with many categories, a more granular approach could track which +specific categories are dirty. Only implement when profiling proves it +is needed. + +**Depends on:** nothing, but low priority. + +--- + +## 15. 🟡 Validate Joint-Fit Weights Before Residual Normalisation + +**Type:** Correctness + +Joint-fit weights currently allow invalid numeric values such as +negatives or an all-zero set. The residual code then normalises by the +total weight and applies `sqrt(weight)`, which can produce +division-by-zero or `nan` residuals. + +**Fix:** require weights to be strictly positive, or at minimum validate +that all weights are non-negative and their total is greater than zero +before normalisation. This should fail with a clear user-facing error +instead of letting invalid floating-point values propagate into the +minimiser. + +**Depends on:** related to issue 3, but independent. + +--- + +## 16. 🟡 Persist Per-Experiment `calculator_type` + +**Type:** Completeness + +The current architecture moved calculator selection to the experiment +level via `calculator_type`, but this selection is not written to CIF +during `save()` / `show_as_cif()`. Reloading or exporting a project +therefore loses explicit calculator choices and falls back to +auto-resolution. + +**Fix:** serialise `calculator_type` as part of the experiment or +analysis state, and make sure `load()` restores it. The saved project +should represent the exact active calculator configuration, not just a +re-derivable default. + +**Depends on:** issue 1 (`Project.load()` implementation). + +--- + +## Summary + +| # | Issue | Severity | Type | +| --- | ------------------------------------------ | -------- | ----------------------- | +| 1 | Implement `Project.load()` | 🔴 High | Completeness | +| 2 | Restore minimiser variants | 🟡 Med | Feature loss | +| 3 | Rebuild joint-fit weights | 🟡 Med | Fragility | +| 4 | Refresh constraint state before auto-apply | 🔴 High | Correctness | +| 5 | `Analysis` as `DatablockItem` | 🟡 Med | Consistency | +| 6 | Restrict `data_type` switching | 🔴 High | Correctness/Data safety | +| 7 | Eliminate dummy `Experiments` | 🟡 Med | Fragility | +| 8 | Explicit `create()` signatures | 🟡 Med | API safety | +| 9 | Future enum extensions | 🟢 Low | Design | +| 10 | Unify update orchestration | 🟢 Low | Maintainability | +| 11 | Document `_update` contract | 🟢 Low | Maintainability | +| 12 | CIF round-trip integration test | 🟢 Low | Quality | +| 13 | Suppress redundant dirty-flag sets | 🟢 Low | Performance | +| 14 | Finer-grained change tracking | 🟢 Low | Performance | +| 15 | Validate joint-fit weights | 🟡 Med | Correctness | +| 16 | Persist per-experiment `calculator_type` | 🟡 Med | Completeness | diff --git a/docs/architecture/package-structure-full.md b/docs/architecture/package-structure-full.md index e24eadab..74662449 100644 --- a/docs/architecture/package-structure-full.md +++ b/docs/architecture/package-structure-full.md @@ -67,37 +67,176 @@ │ │ └── 🏷️ class GuardedBase │ ├── 📄 identity.py │ │ └── 🏷️ class Identity -│ ├── 📄 parameters.py -│ │ ├── 🏷️ class GenericDescriptorBase -│ │ ├── 🏷️ class GenericStringDescriptor -│ │ ├── 🏷️ class GenericNumericDescriptor -│ │ ├── 🏷️ class GenericParameter -│ │ ├── 🏷️ class StringDescriptor -│ │ ├── 🏷️ class NumericDescriptor -│ │ └── 🏷️ class Parameter -│ ├── 📄 singletons.py +│ ├── 📄 metadata.py +│ │ ├── 🏷️ class TypeInfo +│ │ ├── 🏷️ class Compatibility +│ │ └── 🏷️ class CalculatorSupport +│ ├── 📄 singleton.py │ │ ├── 🏷️ class SingletonBase │ │ ├── 🏷️ class UidMapHandler │ │ └── 🏷️ class ConstraintsHandler -│ └── 📄 validation.py -│ ├── 🏷️ class DataTypes -│ ├── 🏷️ class ValidationStage -│ ├── 🏷️ class ValidatorBase -│ ├── 🏷️ class TypeValidator -│ ├── 🏷️ class RangeValidator -│ ├── 🏷️ class MembershipValidator -│ ├── 🏷️ class RegexValidator -│ └── 🏷️ class AttributeSpec +│ ├── 📄 validation.py +│ │ ├── 🏷️ class DataTypeHints +│ │ ├── 🏷️ class DataTypes +│ │ ├── 🏷️ class ValidationStage +│ │ ├── 🏷️ class ValidatorBase +│ │ ├── 🏷️ class TypeValidator +│ │ ├── 🏷️ class RangeValidator +│ │ ├── 🏷️ class MembershipValidator +│ │ ├── 🏷️ class RegexValidator +│ │ └── 🏷️ class AttributeSpec +│ └── 📄 variable.py +│ ├── 🏷️ class GenericDescriptorBase +│ ├── 🏷️ class GenericStringDescriptor +│ ├── 🏷️ class GenericNumericDescriptor +│ ├── 🏷️ class GenericParameter +│ ├── 🏷️ class StringDescriptor +│ ├── 🏷️ class NumericDescriptor +│ └── 🏷️ class Parameter ├── 📁 crystallography │ ├── 📄 __init__.py │ ├── 📄 crystallography.py │ └── 📄 space_groups.py +├── 📁 datablocks +│ ├── 📁 experiment +│ │ ├── 📁 categories +│ │ │ ├── 📁 background +│ │ │ │ ├── 📄 __init__.py +│ │ │ │ ├── 📄 base.py +│ │ │ │ │ └── 🏷️ class BackgroundBase +│ │ │ │ ├── 📄 chebyshev.py +│ │ │ │ │ ├── 🏷️ class PolynomialTerm +│ │ │ │ │ └── 🏷️ class ChebyshevPolynomialBackground +│ │ │ │ ├── 📄 enums.py +│ │ │ │ │ └── 🏷️ class BackgroundTypeEnum +│ │ │ │ ├── 📄 factory.py +│ │ │ │ │ └── 🏷️ class BackgroundFactory +│ │ │ │ └── 📄 line_segment.py +│ │ │ │ ├── 🏷️ class LineSegment +│ │ │ │ └── 🏷️ class LineSegmentBackground +│ │ │ ├── 📁 data +│ │ │ │ ├── 📄 __init__.py +│ │ │ │ ├── 📄 bragg_pd.py +│ │ │ │ │ ├── 🏷️ class PdDataPointBaseMixin +│ │ │ │ │ ├── 🏷️ class PdCwlDataPointMixin +│ │ │ │ │ ├── 🏷️ class PdTofDataPointMixin +│ │ │ │ │ ├── 🏷️ class PdCwlDataPoint +│ │ │ │ │ ├── 🏷️ class PdTofDataPoint +│ │ │ │ │ ├── 🏷️ class PdDataBase +│ │ │ │ │ ├── 🏷️ class PdCwlData +│ │ │ │ │ └── 🏷️ class PdTofData +│ │ │ │ ├── 📄 bragg_sc.py +│ │ │ │ │ ├── 🏷️ class Refln +│ │ │ │ │ └── 🏷️ class ReflnData +│ │ │ │ ├── 📄 factory.py +│ │ │ │ │ └── 🏷️ class DataFactory +│ │ │ │ └── 📄 total_pd.py +│ │ │ │ ├── 🏷️ class TotalDataPoint +│ │ │ │ ├── 🏷️ class TotalDataBase +│ │ │ │ └── 🏷️ class TotalData +│ │ │ ├── 📁 instrument +│ │ │ │ ├── 📄 __init__.py +│ │ │ │ ├── 📄 base.py +│ │ │ │ │ └── 🏷️ class InstrumentBase +│ │ │ │ ├── 📄 cwl.py +│ │ │ │ │ ├── 🏷️ class CwlInstrumentBase +│ │ │ │ │ ├── 🏷️ class CwlScInstrument +│ │ │ │ │ └── 🏷️ class CwlPdInstrument +│ │ │ │ ├── 📄 factory.py +│ │ │ │ │ └── 🏷️ class InstrumentFactory +│ │ │ │ └── 📄 tof.py +│ │ │ │ ├── 🏷️ class TofScInstrument +│ │ │ │ └── 🏷️ class TofPdInstrument +│ │ │ ├── 📁 peak +│ │ │ │ ├── 📄 __init__.py +│ │ │ │ ├── 📄 base.py +│ │ │ │ │ └── 🏷️ class PeakBase +│ │ │ │ ├── 📄 cwl.py +│ │ │ │ │ ├── 🏷️ class CwlPseudoVoigt +│ │ │ │ │ ├── 🏷️ class CwlSplitPseudoVoigt +│ │ │ │ │ └── 🏷️ class CwlThompsonCoxHastings +│ │ │ │ ├── 📄 cwl_mixins.py +│ │ │ │ │ ├── 🏷️ class CwlBroadeningMixin +│ │ │ │ │ ├── 🏷️ class EmpiricalAsymmetryMixin +│ │ │ │ │ └── 🏷️ class FcjAsymmetryMixin +│ │ │ │ ├── 📄 factory.py +│ │ │ │ │ └── 🏷️ class PeakFactory +│ │ │ │ ├── 📄 tof.py +│ │ │ │ │ ├── 🏷️ class TofPseudoVoigt +│ │ │ │ │ ├── 🏷️ class TofPseudoVoigtIkedaCarpenter +│ │ │ │ │ └── 🏷️ class TofPseudoVoigtBackToBack +│ │ │ │ ├── 📄 tof_mixins.py +│ │ │ │ │ ├── 🏷️ class TofBroadeningMixin +│ │ │ │ │ └── 🏷️ class IkedaCarpenterAsymmetryMixin +│ │ │ │ ├── 📄 total.py +│ │ │ │ │ └── 🏷️ class TotalGaussianDampedSinc +│ │ │ │ └── 📄 total_mixins.py +│ │ │ │ └── 🏷️ class TotalBroadeningMixin +│ │ │ ├── 📄 __init__.py +│ │ │ ├── 📄 excluded_regions.py +│ │ │ │ ├── 🏷️ class ExcludedRegion +│ │ │ │ └── 🏷️ class ExcludedRegions +│ │ │ ├── 📄 experiment_type.py +│ │ │ │ └── 🏷️ class ExperimentType +│ │ │ ├── 📄 extinction.py +│ │ │ │ └── 🏷️ class Extinction +│ │ │ ├── 📄 linked_crystal.py +│ │ │ │ └── 🏷️ class LinkedCrystal +│ │ │ └── 📄 linked_phases.py +│ │ │ ├── 🏷️ class LinkedPhase +│ │ │ └── 🏷️ class LinkedPhases +│ │ ├── 📁 item +│ │ │ ├── 📄 __init__.py +│ │ │ ├── 📄 base.py +│ │ │ │ ├── 🏷️ class ExperimentBase +│ │ │ │ ├── 🏷️ class ScExperimentBase +│ │ │ │ └── 🏷️ class PdExperimentBase +│ │ │ ├── 📄 bragg_pd.py +│ │ │ │ └── 🏷️ class BraggPdExperiment +│ │ │ ├── 📄 bragg_sc.py +│ │ │ │ ├── 🏷️ class CwlScExperiment +│ │ │ │ └── 🏷️ class TofScExperiment +│ │ │ ├── 📄 enums.py +│ │ │ │ ├── 🏷️ class SampleFormEnum +│ │ │ │ ├── 🏷️ class ScatteringTypeEnum +│ │ │ │ ├── 🏷️ class RadiationProbeEnum +│ │ │ │ ├── 🏷️ class BeamModeEnum +│ │ │ │ ├── 🏷️ class CalculatorEnum +│ │ │ │ └── 🏷️ class PeakProfileTypeEnum +│ │ │ ├── 📄 factory.py +│ │ │ │ └── 🏷️ class ExperimentFactory +│ │ │ └── 📄 total_pd.py +│ │ │ └── 🏷️ class TotalPdExperiment +│ │ ├── 📄 __init__.py +│ │ └── 📄 collection.py +│ │ └── 🏷️ class Experiments +│ ├── 📁 structure +│ │ ├── 📁 categories +│ │ │ ├── 📄 __init__.py +│ │ │ ├── 📄 atom_sites.py +│ │ │ │ ├── 🏷️ class AtomSite +│ │ │ │ └── 🏷️ class AtomSites +│ │ │ ├── 📄 cell.py +│ │ │ │ └── 🏷️ class Cell +│ │ │ └── 📄 space_group.py +│ │ │ └── 🏷️ class SpaceGroup +│ │ ├── 📁 item +│ │ │ ├── 📄 __init__.py +│ │ │ ├── 📄 base.py +│ │ │ │ └── 🏷️ class Structure +│ │ │ └── 📄 factory.py +│ │ │ └── 🏷️ class StructureFactory +│ │ ├── 📄 __init__.py +│ │ └── 📄 collection.py +│ │ └── 🏷️ class Structures +│ └── 📄 __init__.py ├── 📁 display │ ├── 📁 plotters │ │ ├── 📄 __init__.py │ │ ├── 📄 ascii.py │ │ │ └── 🏷️ class AsciiPlotter │ │ ├── 📄 base.py +│ │ │ ├── 🏷️ class XAxisType │ │ │ └── 🏷️ class PlotterBase │ │ └── 📄 plotly.py │ │ └── 🏷️ class PlotlyPlotter @@ -123,108 +262,6 @@ │ │ └── 🏷️ class TableRendererFactory │ └── 📄 utils.py │ └── 🏷️ class JupyterScrollManager -├── 📁 experiments -│ ├── 📁 categories -│ │ ├── 📁 background -│ │ │ ├── 📄 __init__.py -│ │ │ ├── 📄 base.py -│ │ │ │ └── 🏷️ class BackgroundBase -│ │ │ ├── 📄 chebyshev.py -│ │ │ │ ├── 🏷️ class PolynomialTerm -│ │ │ │ └── 🏷️ class ChebyshevPolynomialBackground -│ │ │ ├── 📄 enums.py -│ │ │ │ └── 🏷️ class BackgroundTypeEnum -│ │ │ ├── 📄 factory.py -│ │ │ │ └── 🏷️ class BackgroundFactory -│ │ │ └── 📄 line_segment.py -│ │ │ ├── 🏷️ class LineSegment -│ │ │ └── 🏷️ class LineSegmentBackground -│ │ ├── 📁 data -│ │ │ ├── 📄 bragg_pd.py -│ │ │ │ ├── 🏷️ class PdDataPointBaseMixin -│ │ │ │ ├── 🏷️ class PdCwlDataPointMixin -│ │ │ │ ├── 🏷️ class PdTofDataPointMixin -│ │ │ │ ├── 🏷️ class PdCwlDataPoint -│ │ │ │ ├── 🏷️ class PdTofDataPoint -│ │ │ │ ├── 🏷️ class PdDataBase -│ │ │ │ ├── 🏷️ class PdCwlData -│ │ │ │ └── 🏷️ class PdTofData -│ │ │ ├── 📄 bragg_sc.py -│ │ │ │ └── 🏷️ class Refln -│ │ │ ├── 📄 factory.py -│ │ │ │ └── 🏷️ class DataFactory -│ │ │ └── 📄 total.py -│ │ │ ├── 🏷️ class TotalDataPoint -│ │ │ ├── 🏷️ class TotalDataBase -│ │ │ └── 🏷️ class TotalData -│ │ ├── 📁 instrument -│ │ │ ├── 📄 __init__.py -│ │ │ ├── 📄 base.py -│ │ │ │ └── 🏷️ class InstrumentBase -│ │ │ ├── 📄 cwl.py -│ │ │ │ └── 🏷️ class CwlInstrument -│ │ │ ├── 📄 factory.py -│ │ │ │ └── 🏷️ class InstrumentFactory -│ │ │ └── 📄 tof.py -│ │ │ └── 🏷️ class TofInstrument -│ │ ├── 📁 peak -│ │ │ ├── 📄 __init__.py -│ │ │ ├── 📄 base.py -│ │ │ │ └── 🏷️ class PeakBase -│ │ │ ├── 📄 cwl.py -│ │ │ │ ├── 🏷️ class CwlPseudoVoigt -│ │ │ │ ├── 🏷️ class CwlSplitPseudoVoigt -│ │ │ │ └── 🏷️ class CwlThompsonCoxHastings -│ │ │ ├── 📄 cwl_mixins.py -│ │ │ │ ├── 🏷️ class CwlBroadeningMixin -│ │ │ │ ├── 🏷️ class EmpiricalAsymmetryMixin -│ │ │ │ └── 🏷️ class FcjAsymmetryMixin -│ │ │ ├── 📄 factory.py -│ │ │ │ └── 🏷️ class PeakFactory -│ │ │ ├── 📄 tof.py -│ │ │ │ ├── 🏷️ class TofPseudoVoigt -│ │ │ │ ├── 🏷️ class TofPseudoVoigtIkedaCarpenter -│ │ │ │ └── 🏷️ class TofPseudoVoigtBackToBack -│ │ │ ├── 📄 tof_mixins.py -│ │ │ │ ├── 🏷️ class TofBroadeningMixin -│ │ │ │ └── 🏷️ class IkedaCarpenterAsymmetryMixin -│ │ │ ├── 📄 total.py -│ │ │ │ └── 🏷️ class TotalGaussianDampedSinc -│ │ │ └── 📄 total_mixins.py -│ │ │ └── 🏷️ class TotalBroadeningMixin -│ │ ├── 📄 __init__.py -│ │ ├── 📄 excluded_regions.py -│ │ │ ├── 🏷️ class ExcludedRegion -│ │ │ └── 🏷️ class ExcludedRegions -│ │ ├── 📄 experiment_type.py -│ │ │ └── 🏷️ class ExperimentType -│ │ └── 📄 linked_phases.py -│ │ ├── 🏷️ class LinkedPhase -│ │ └── 🏷️ class LinkedPhases -│ ├── 📁 experiment -│ │ ├── 📄 __init__.py -│ │ ├── 📄 base.py -│ │ │ ├── 🏷️ class ExperimentBase -│ │ │ └── 🏷️ class PdExperimentBase -│ │ ├── 📄 bragg_pd.py -│ │ │ └── 🏷️ class BraggPdExperiment -│ │ ├── 📄 bragg_sc.py -│ │ │ └── 🏷️ class BraggScExperiment -│ │ ├── 📄 enums.py -│ │ │ ├── 🏷️ class SampleFormEnum -│ │ │ ├── 🏷️ class ScatteringTypeEnum -│ │ │ ├── 🏷️ class RadiationProbeEnum -│ │ │ ├── 🏷️ class BeamModeEnum -│ │ │ └── 🏷️ class PeakProfileTypeEnum -│ │ ├── 📄 factory.py -│ │ │ └── 🏷️ class ExperimentFactory -│ │ ├── 📄 instrument_mixin.py -│ │ │ └── 🏷️ class InstrumentMixin -│ │ └── 📄 total_pd.py -│ │ └── 🏷️ class TotalPdExperiment -│ ├── 📄 __init__.py -│ └── 📄 experiments.py -│ └── 🏷️ class Experiments ├── 📁 io │ ├── 📁 cif │ │ ├── 📄 __init__.py @@ -239,30 +276,17 @@ │ │ └── 🏷️ class Project │ └── 📄 project_info.py │ └── 🏷️ class ProjectInfo -├── 📁 sample_models -│ ├── 📁 categories -│ │ ├── 📄 __init__.py -│ │ ├── 📄 atom_sites.py -│ │ │ ├── 🏷️ class AtomSite -│ │ │ └── 🏷️ class AtomSites -│ │ ├── 📄 cell.py -│ │ │ └── 🏷️ class Cell -│ │ └── 📄 space_group.py -│ │ └── 🏷️ class SpaceGroup -│ ├── 📁 sample_model -│ │ ├── 📄 __init__.py -│ │ ├── 📄 base.py -│ │ │ └── 🏷️ class SampleModelBase -│ │ └── 📄 factory.py -│ │ └── 🏷️ class SampleModelFactory -│ ├── 📄 __init__.py -│ └── 📄 sample_models.py -│ └── 🏷️ class SampleModels ├── 📁 summary │ ├── 📄 __init__.py │ └── 📄 summary.py │ └── 🏷️ class Summary ├── 📁 utils +│ ├── 📁 _vendored +│ │ ├── 📁 jupyter_dark_detect +│ │ │ ├── 📄 __init__.py +│ │ │ └── 📄 detector.py +│ │ ├── 📄 __init__.py +│ │ └── 📄 theme_detect.py │ ├── 📄 __init__.py │ ├── 📄 environment.py │ ├── 📄 logging.py diff --git a/docs/architecture/package-structure-short.md b/docs/architecture/package-structure-short.md index 7f9d5af0..efe89066 100644 --- a/docs/architecture/package-structure-short.md +++ b/docs/architecture/package-structure-short.md @@ -38,13 +38,75 @@ │ ├── 📄 factory.py │ ├── 📄 guard.py │ ├── 📄 identity.py -│ ├── 📄 parameters.py -│ ├── 📄 singletons.py -│ └── 📄 validation.py +│ ├── 📄 metadata.py +│ ├── 📄 singleton.py +│ ├── 📄 validation.py +│ └── 📄 variable.py ├── 📁 crystallography │ ├── 📄 __init__.py │ ├── 📄 crystallography.py │ └── 📄 space_groups.py +├── 📁 datablocks +│ ├── 📁 experiment +│ │ ├── 📁 categories +│ │ │ ├── 📁 background +│ │ │ │ ├── 📄 __init__.py +│ │ │ │ ├── 📄 base.py +│ │ │ │ ├── 📄 chebyshev.py +│ │ │ │ ├── 📄 enums.py +│ │ │ │ ├── 📄 factory.py +│ │ │ │ └── 📄 line_segment.py +│ │ │ ├── 📁 data +│ │ │ │ ├── 📄 __init__.py +│ │ │ │ ├── 📄 bragg_pd.py +│ │ │ │ ├── 📄 bragg_sc.py +│ │ │ │ ├── 📄 factory.py +│ │ │ │ └── 📄 total_pd.py +│ │ │ ├── 📁 instrument +│ │ │ │ ├── 📄 __init__.py +│ │ │ │ ├── 📄 base.py +│ │ │ │ ├── 📄 cwl.py +│ │ │ │ ├── 📄 factory.py +│ │ │ │ └── 📄 tof.py +│ │ │ ├── 📁 peak +│ │ │ │ ├── 📄 __init__.py +│ │ │ │ ├── 📄 base.py +│ │ │ │ ├── 📄 cwl.py +│ │ │ │ ├── 📄 cwl_mixins.py +│ │ │ │ ├── 📄 factory.py +│ │ │ │ ├── 📄 tof.py +│ │ │ │ ├── 📄 tof_mixins.py +│ │ │ │ ├── 📄 total.py +│ │ │ │ └── 📄 total_mixins.py +│ │ │ ├── 📄 __init__.py +│ │ │ ├── 📄 excluded_regions.py +│ │ │ ├── 📄 experiment_type.py +│ │ │ ├── 📄 extinction.py +│ │ │ ├── 📄 linked_crystal.py +│ │ │ └── 📄 linked_phases.py +│ │ ├── 📁 item +│ │ │ ├── 📄 __init__.py +│ │ │ ├── 📄 base.py +│ │ │ ├── 📄 bragg_pd.py +│ │ │ ├── 📄 bragg_sc.py +│ │ │ ├── 📄 enums.py +│ │ │ ├── 📄 factory.py +│ │ │ └── 📄 total_pd.py +│ │ ├── 📄 __init__.py +│ │ └── 📄 collection.py +│ ├── 📁 structure +│ │ ├── 📁 categories +│ │ │ ├── 📄 __init__.py +│ │ │ ├── 📄 atom_sites.py +│ │ │ ├── 📄 cell.py +│ │ │ └── 📄 space_group.py +│ │ ├── 📁 item +│ │ │ ├── 📄 __init__.py +│ │ │ ├── 📄 base.py +│ │ │ └── 📄 factory.py +│ │ ├── 📄 __init__.py +│ │ └── 📄 collection.py +│ └── 📄 __init__.py ├── 📁 display │ ├── 📁 plotters │ │ ├── 📄 __init__.py @@ -61,51 +123,6 @@ │ ├── 📄 plotting.py │ ├── 📄 tables.py │ └── 📄 utils.py -├── 📁 experiments -│ ├── 📁 categories -│ │ ├── 📁 background -│ │ │ ├── 📄 __init__.py -│ │ │ ├── 📄 base.py -│ │ │ ├── 📄 chebyshev.py -│ │ │ ├── 📄 enums.py -│ │ │ ├── 📄 factory.py -│ │ │ └── 📄 line_segment.py -│ │ ├── 📁 data -│ │ │ ├── 📄 bragg_pd.py -│ │ │ ├── 📄 bragg_sc.py -│ │ │ ├── 📄 factory.py -│ │ │ └── 📄 total.py -│ │ ├── 📁 instrument -│ │ │ ├── 📄 __init__.py -│ │ │ ├── 📄 base.py -│ │ │ ├── 📄 cwl.py -│ │ │ ├── 📄 factory.py -│ │ │ └── 📄 tof.py -│ │ ├── 📁 peak -│ │ │ ├── 📄 __init__.py -│ │ │ ├── 📄 base.py -│ │ │ ├── 📄 cwl.py -│ │ │ ├── 📄 cwl_mixins.py -│ │ │ ├── 📄 factory.py -│ │ │ ├── 📄 tof.py -│ │ │ ├── 📄 tof_mixins.py -│ │ │ ├── 📄 total.py -│ │ │ └── 📄 total_mixins.py -│ │ ├── 📄 __init__.py -│ │ ├── 📄 excluded_regions.py -│ │ ├── 📄 experiment_type.py -│ │ └── 📄 linked_phases.py -│ ├── 📁 experiment -│ │ ├── 📄 __init__.py -│ │ ├── 📄 base.py -│ │ ├── 📄 bragg_pd.py -│ │ ├── 📄 bragg_sc.py -│ │ ├── 📄 enums.py -│ │ ├── 📄 factory.py -│ │ ├── 📄 instrument_mixin.py -│ │ └── 📄 total_pd.py -│ ├── 📄 __init__.py -│ └── 📄 experiments.py ├── 📁 io │ ├── 📁 cif │ │ ├── 📄 __init__.py @@ -117,22 +134,16 @@ │ ├── 📄 __init__.py │ ├── 📄 project.py │ └── 📄 project_info.py -├── 📁 sample_models -│ ├── 📁 categories -│ │ ├── 📄 __init__.py -│ │ ├── 📄 atom_sites.py -│ │ ├── 📄 cell.py -│ │ └── 📄 space_group.py -│ ├── 📁 sample_model -│ │ ├── 📄 __init__.py -│ │ ├── 📄 base.py -│ │ └── 📄 factory.py -│ ├── 📄 __init__.py -│ └── 📄 sample_models.py ├── 📁 summary │ ├── 📄 __init__.py │ └── 📄 summary.py ├── 📁 utils +│ ├── 📁 _vendored +│ │ ├── 📁 jupyter_dark_detect +│ │ │ ├── 📄 __init__.py +│ │ │ └── 📄 detector.py +│ │ ├── 📄 __init__.py +│ │ └── 📄 theme_detect.py │ ├── 📄 __init__.py │ ├── 📄 environment.py │ ├── 📄 logging.py diff --git a/docs/api-reference/analysis.md b/docs/docs/api-reference/analysis.md similarity index 100% rename from docs/api-reference/analysis.md rename to docs/docs/api-reference/analysis.md diff --git a/docs/api-reference/core.md b/docs/docs/api-reference/core.md similarity index 100% rename from docs/api-reference/core.md rename to docs/docs/api-reference/core.md diff --git a/docs/api-reference/crystallography.md b/docs/docs/api-reference/crystallography.md similarity index 100% rename from docs/api-reference/crystallography.md rename to docs/docs/api-reference/crystallography.md diff --git a/docs/docs/api-reference/datablocks/experiment.md b/docs/docs/api-reference/datablocks/experiment.md new file mode 100644 index 00000000..b0d19a6a --- /dev/null +++ b/docs/docs/api-reference/datablocks/experiment.md @@ -0,0 +1 @@ +::: easydiffraction.datablocks.experiment diff --git a/docs/docs/api-reference/datablocks/structure.md b/docs/docs/api-reference/datablocks/structure.md new file mode 100644 index 00000000..43f752ff --- /dev/null +++ b/docs/docs/api-reference/datablocks/structure.md @@ -0,0 +1 @@ +::: easydiffraction.datablocks.structure diff --git a/docs/api-reference/display.md b/docs/docs/api-reference/display.md similarity index 100% rename from docs/api-reference/display.md rename to docs/docs/api-reference/display.md diff --git a/docs/api-reference/index.md b/docs/docs/api-reference/index.md similarity index 60% rename from docs/api-reference/index.md rename to docs/docs/api-reference/index.md index 25418279..351d7f56 100644 --- a/docs/api-reference/index.md +++ b/docs/docs/api-reference/index.md @@ -7,18 +7,20 @@ icon: material/code-braces-box This section contains the reference detailing the functions and modules available in EasyDiffraction: -- [core](core.md) – Contains core utilities and foundational objects used across - the package. -- [crystallography](crystallography.md) – Handles crystallographic calculations, - space groups, and symmetry operations. +- [core](core.md) – Contains core utilities and foundational objects + used across the package. +- [crystallography](crystallography.md) – Handles crystallographic + calculations, space groups, and symmetry operations. - [utils](utils.md) – Miscellaneous utility functions for formatting, decorators, and general helpers. +- datablocks + - [experiments](datablocks/experiment.md) – Manages experimental + setups and instrument parameters, as well as the associated + diffraction data. + - [structures](datablocks/structure.md) – Defines structures, such as + crystallographic structures, and manages their properties. - [display](display.md) – Tools for plotting data and rendering tables. - [project](project.md) – Defines the project and manages its state. -- [sample_models](sample_models.md) – Defines sample models, such as - crystallographic structures, and manages their properties. -- [experiments](experiments.md) – Manages experimental setups and instrument - parameters, as well as the associated diffraction data. -- [analysis](analysis.md) – Provides tools for analyzing diffraction data, - including fitting and minimization. +- [analysis](analysis.md) – Provides tools for analyzing diffraction + data, including fitting and minimization. - [summary](summary.md) – Provides a summary of the project. diff --git a/docs/api-reference/io.md b/docs/docs/api-reference/io.md similarity index 100% rename from docs/api-reference/io.md rename to docs/docs/api-reference/io.md diff --git a/docs/api-reference/project.md b/docs/docs/api-reference/project.md similarity index 100% rename from docs/api-reference/project.md rename to docs/docs/api-reference/project.md diff --git a/docs/api-reference/summary.md b/docs/docs/api-reference/summary.md similarity index 100% rename from docs/api-reference/summary.md rename to docs/docs/api-reference/summary.md diff --git a/docs/api-reference/utils.md b/docs/docs/api-reference/utils.md similarity index 100% rename from docs/api-reference/utils.md rename to docs/docs/api-reference/utils.md diff --git a/docs/docs/assets/images/favicon.png b/docs/docs/assets/images/favicon.png new file mode 100644 index 00000000..27628988 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..0be819d0 --- /dev/null +++ b/docs/docs/assets/images/logo_dark.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + Logo + + Main circle + + + Inner circles + + + + + + + + + + + Text + + easy + + + diffraction + + + + \ 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..84103655 --- /dev/null +++ b/docs/docs/assets/images/logo_light.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + Logo + + Main circle + + + Inner circles + + + + + + + + + + + Text + + easy + + + diffraction + + + + \ No newline at end of file diff --git a/docs/assets/images/user-guide/data-acquisition_2d-raw-data.jpg b/docs/docs/assets/images/user-guide/data-acquisition_2d-raw-data.jpg similarity index 100% rename from docs/assets/images/user-guide/data-acquisition_2d-raw-data.jpg rename to docs/docs/assets/images/user-guide/data-acquisition_2d-raw-data.jpg diff --git a/docs/assets/images/user-guide/data-acquisition_instrument.png b/docs/docs/assets/images/user-guide/data-acquisition_instrument.png similarity index 100% rename from docs/assets/images/user-guide/data-acquisition_instrument.png rename to docs/docs/assets/images/user-guide/data-acquisition_instrument.png diff --git a/docs/assets/images/user-guide/data-analysis_model.png b/docs/docs/assets/images/user-guide/data-analysis_model.png similarity index 100% rename from docs/assets/images/user-guide/data-analysis_model.png rename to docs/docs/assets/images/user-guide/data-analysis_model.png diff --git a/docs/assets/images/user-guide/data-analysis_refinement.png b/docs/docs/assets/images/user-guide/data-analysis_refinement.png similarity index 100% rename from docs/assets/images/user-guide/data-analysis_refinement.png rename to docs/docs/assets/images/user-guide/data-analysis_refinement.png diff --git a/docs/assets/images/user-guide/data-reduction_1d-pattern.png b/docs/docs/assets/images/user-guide/data-reduction_1d-pattern.png similarity index 100% rename from docs/assets/images/user-guide/data-reduction_1d-pattern.png rename to docs/docs/assets/images/user-guide/data-reduction_1d-pattern.png 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/index.md b/docs/docs/index.md similarity index 53% rename from docs/index.md rename to docs/docs/index.md index a34d3023..73c35423 100644 --- a/docs/index.md +++ b/docs/docs/index.md @@ -4,17 +4,18 @@ Here is a brief overview of the main documentation sections: -- [:material-information-slab-circle: Introduction](introduction/index.md) – - Provides an overview of EasyDiffraction, 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 EasyDiffraction. +- [:material-information-slab-circle: Introduction](introduction/index.md) + – Provides an overview of EasyDiffraction, 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 EasyDiffraction. - [: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 - EasyDiffraction. + 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 EasyDiffraction. diff --git a/docs/installation-and-setup/index.md b/docs/docs/installation-and-setup/index.md similarity index 80% rename from docs/installation-and-setup/index.md rename to docs/docs/installation-and-setup/index.md index 80b64235..0891a55f 100644 --- a/docs/installation-and-setup/index.md +++ b/docs/docs/installation-and-setup/index.md @@ -6,16 +6,16 @@ icon: material/cog-box ## Requirements -EasyDiffraction is a cross-platform Python library compatible with **Python 3.11 -through 3.13**. +EasyDiffraction is a cross-platform Python library compatible with +**Python 3.11 through 3.13**. Make sure Python is installed on your system before proceeding with the installation. ## 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. +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: @@ -76,22 +76,23 @@ and recreate the environment. ### Installing from PyPI recommended { #from-pypi data-toc-label="Installing from PyPI" } -EasyDiffraction is available on **PyPI (Python Package Index)** and can be -installed using `pip`. We strongly recommend installing it within a virtual -environment, as described in the [Environment Setup](#environment-setup) -section. +EasyDiffraction is available on **PyPI (Python Package Index)** and can +be installed using `pip`. We strongly recommend installing it within a +virtual environment, as described in the +[Environment Setup](#environment-setup) section. We recommend installing the latest release of EasyDiffraction with the -`visualization` extras, which include optional dependencies used for simplified -visualization of charts and tables. This can be especially useful for running -the Jupyter Notebook examples. To do so, use the following command: +`visualization` extras, which include optional dependencies used for +simplified visualization of charts and tables. This can be especially +useful for running the Jupyter Notebook examples. To do so, use the +following command: ```bash pip install 'easydiffraction[visualization]' ``` -If only the core functionality is needed, the library can be installed simply -with: +If only the core functionality is needed, the library can be installed +simply with: ```bash pip install easydiffraction @@ -117,8 +118,8 @@ pip show easydiffraction ### Installing from GitHub -Installing unreleased versions is generally not recommended but may be useful -for testing. +Installing unreleased versions is generally not recommended but may be +useful for testing. To install EasyDiffraction from, e.g., the `develop` branch of GitHub: @@ -134,18 +135,20 @@ pip install 'easydiffraction[visualization] @ git+https://github.com/easyscience ## How to Run Tutorials -EasyDiffraction includes a collection of **Jupyter Notebook examples** that -demonstrate key functionality. These tutorials serve as **step-by-step guides** -to help users understand the diffraction data analysis workflow. +EasyDiffraction includes a collection of **Jupyter Notebook examples** +that demonstrate key functionality. These tutorials serve as +**step-by-step guides** to help users understand the diffraction data +analysis workflow. They are available as **static HTML pages** in the -[:material-school: Tutorials](../tutorials/index.md) section. You can also run -them interactively in two ways: +[:material-school: Tutorials](../tutorials/index.md) section. You can +also run them interactively in two ways: - **Run Locally** – Download the notebook via the :material-download: **Download** button and run it on your computer. -- **Run Online** – Use the :google-colab: **Open in Google Colab** button to run - the tutorial directly in your browser (no setup required). +- **Run Online** – Use the :google-colab: **Open in Google Colab** + button to run the tutorial directly in your browser (no setup + required). !!! note @@ -154,8 +157,9 @@ them interactively in two ways: ### Run Tutorials Locally -To run tutorials locally, install **Jupyter Notebook** or **JupyterLab**. Here -are the steps to follow in the case of **Jupyter Notebook**: +To run tutorials locally, install **Jupyter Notebook** or +**JupyterLab**. Here are the steps to follow in the case of **Jupyter +Notebook**: - Install Jupyter Notebook and IPython kernel: ```bash @@ -177,32 +181,34 @@ are the steps to follow in the case of **Jupyter Notebook**: ```bash http://localhost:8888/ ``` -- Open one of the `*.ipynb` files and select the `EasyDiffraction Python kernel` - to get started. +- Open one of the `*.ipynb` files and select the + `EasyDiffraction Python kernel` to get started. ### Run Tutorials via Google Colab -**Google Colab** lets you run Jupyter Notebooks in the cloud without any local -installation. +**Google Colab** lets you run Jupyter Notebooks in the cloud without any +local installation. To use Google Colab: - 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. +- Go to the **[:material-school: Tutorials](../tutorials/index.md)** + section. +- Click the :google-colab: **Open in Google Colab** button on any + tutorial. -This is the fastest way to start experimenting with EasyDiffraction, without -setting up Python on your system. +This is the fastest way to start experimenting with EasyDiffraction, +without setting up Python on your system. ## Installing with Pixi alternative { #installing-with-pixi data-toc-label="Installing with Pixi" } -[Pixi](https://pixi.sh) is a modern package and environment manager for Python -and Conda-compatible packages. It simplifies dependency management, environment -isolation, and reproducibility. +[Pixi](https://pixi.sh) is a modern package and environment manager for +Python and Conda-compatible packages. It simplifies dependency +management, environment isolation, and reproducibility. The following simple steps provide an alternative setup method for -EasyDiffraction using Pixi, replacing the traditional virtual environment -approach. +EasyDiffraction using Pixi, replacing the traditional virtual +environment approach. diff --git a/docs/introduction/index.md b/docs/docs/introduction/index.md similarity index 90% rename from docs/introduction/index.md rename to docs/docs/introduction/index.md index 2996386b..a2b42b59 100644 --- a/docs/introduction/index.md +++ b/docs/docs/introduction/index.md @@ -8,21 +8,22 @@ icon: material/information-slab-circle **EasyDiffraction** is scientific software for calculating diffraction patterns -based on structural models and refining model parameters against experimental -data. +based on structural models and refining model parameters against +experimental data. -It is available as both a cross-platform desktop application and a Python -library. +It is available as both a cross-platform desktop application and a +Python library. -This documentation covers the usage of the EasyDiffraction Python library. +This documentation covers the usage of the EasyDiffraction Python +library. For the graphical user interface (GUI) version, refer to the [GUI documentation](https://docs.easydiffraction.org/app). ## EasyScience EasyDiffraction is developed using the -[EasyScience framework](https://easyscience.software), which provides tools -for +[EasyScience framework](https://easyscience.software), which provides +tools for building modular and flexible scientific libraries and applications. ## License @@ -35,27 +36,28 @@ EasyDiffraction is released under the The latest version of the EasyDiffraction Python library is [{{ vars.release_version }}](https://github.com/easyscience/diffraction-lib/releases/latest). -For a complete list of new features, bug fixes, and improvements, see the +For a complete list of new features, bug fixes, and improvements, see +the [GitHub Releases page](https://github.com/easyscience/diffraction-lib/releases). ## Citation -If you use EasyDiffraction in your work, please cite the specific version you -used. +If you use EasyDiffraction in your work, please cite the specific +version you used. All official releases of the EasyDiffraction 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) +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.16806521). ## Contributing -We welcome contributions from the community! EasyDiffraction is intended to be a -community-driven, open-source project supported by a diverse group of -contributors. +We welcome contributions from the community! EasyDiffraction 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). diff --git a/docs/docs/tutorials/ed-1.ipynb b/docs/docs/tutorials/ed-1.ipynb new file mode 100644 index 00000000..9b46deb0 --- /dev/null +++ b/docs/docs/tutorials/ed-1.ipynb @@ -0,0 +1,203 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0", + "metadata": {}, + "source": [ + "# Structure Refinement: LBCO, HRPT\n", + "\n", + "This minimalistic example is designed to show how Rietveld refinement\n", + "can be performed when both the crystal structure and experiment\n", + "parameters are defined using CIF files.\n", + "\n", + "For this example, constant-wavelength neutron powder diffraction data\n", + "for La0.5Ba0.5CoO3 from HRPT at PSI is used.\n", + "\n", + "It does not contain any advanced features or options, and includes no\n", + "comments or explanations—these can be found in the other tutorials.\n", + "Default values are used for all parameters if not specified. Only\n", + "essential and self-explanatory code is provided.\n", + "\n", + "The example is intended for users who are already familiar with the\n", + "EasyDiffraction library and want to quickly get started with a simple\n", + "refinement. It is also useful for those who want to see what a\n", + "refinement might look like in code. For a more detailed explanation of\n", + "the code, please refer to the other tutorials." + ] + }, + { + "cell_type": "markdown", + "id": "1", + "metadata": {}, + "source": [ + "## Import Library" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2", + "metadata": {}, + "outputs": [], + "source": [ + "import easydiffraction as ed" + ] + }, + { + "cell_type": "markdown", + "id": "3", + "metadata": {}, + "source": [ + "## Step 1: Define Project" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4", + "metadata": {}, + "outputs": [], + "source": [ + "# Create minimal project without name and description\n", + "project = ed.Project()" + ] + }, + { + "cell_type": "markdown", + "id": "5", + "metadata": {}, + "source": [ + "## Step 2: Define Crystal Structure" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6", + "metadata": {}, + "outputs": [], + "source": [ + "# Download CIF file from repository\n", + "structure_path = ed.download_data(id=1, destination='data')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7", + "metadata": {}, + "outputs": [], + "source": [ + "project.structures.add_from_cif_path(structure_path)" + ] + }, + { + "cell_type": "markdown", + "id": "8", + "metadata": {}, + "source": [ + "## Step 3: Define Experiment" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9", + "metadata": {}, + "outputs": [], + "source": [ + "# Download CIF file from repository\n", + "expt_path = ed.download_data(id=2, destination='data')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "10", + "metadata": {}, + "outputs": [], + "source": [ + "project.experiments.add_from_cif_path(expt_path)" + ] + }, + { + "cell_type": "markdown", + "id": "11", + "metadata": {}, + "source": [ + "## Step 4: Perform Analysis" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "12", + "metadata": {}, + "outputs": [], + "source": [ + "# Start refinement. All parameters, which have standard uncertainties\n", + "# in the input CIF files, are refined by default.\n", + "project.analysis.fit()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "13", + "metadata": {}, + "outputs": [], + "source": [ + "# Show fit results summary\n", + "project.analysis.show_fit_results()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "14", + "metadata": {}, + "outputs": [], + "source": [ + "project.experiments.show_names()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "15", + "metadata": {}, + "outputs": [], + "source": [ + "project.plot_meas_vs_calc(expt_name='hrpt', show_residual=True)" + ] + }, + { + "cell_type": "markdown", + "id": "16", + "metadata": {}, + "source": [ + "## Step 5: Show Project Summary" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "17", + "metadata": {}, + "outputs": [], + "source": [ + "project.summary.show_report()" + ] + } + ], + "metadata": { + "jupytext": { + "cell_metadata_filter": "-all", + "main_language": "python", + "notebook_metadata_filter": "-all" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/tutorials/ed-1.py b/docs/docs/tutorials/ed-1.py similarity index 84% rename from tutorials/ed-1.py rename to docs/docs/tutorials/ed-1.py index 38bacfda..51e4e8e6 100644 --- a/tutorials/ed-1.py +++ b/docs/docs/tutorials/ed-1.py @@ -2,8 +2,8 @@ # # Structure Refinement: LBCO, HRPT # # This minimalistic example is designed to show how Rietveld refinement -# of a crystal structure can be performed when both the sample model and -# experiment are defined using CIF files. +# can be performed when both the crystal structure and experiment +# parameters are defined using CIF files. # # For this example, constant-wavelength neutron powder diffraction data # for La0.5Ba0.5CoO3 from HRPT at PSI is used. @@ -33,14 +33,14 @@ project = ed.Project() # %% [markdown] -# ## Step 2: Define Sample Model +# ## Step 2: Define Crystal Structure # %% # Download CIF file from repository -model_path = ed.download_data(id=1, destination='data') +structure_path = ed.download_data(id=1, destination='data') # %% -project.sample_models.add(cif_path=model_path) +project.structures.add_from_cif_path(structure_path) # %% [markdown] # ## Step 3: Define Experiment @@ -50,7 +50,7 @@ expt_path = ed.download_data(id=2, destination='data') # %% -project.experiments.add(cif_path=expt_path) +project.experiments.add_from_cif_path(expt_path) # %% [markdown] # ## Step 4: Perform Analysis diff --git a/docs/docs/tutorials/ed-10.ipynb b/docs/docs/tutorials/ed-10.ipynb new file mode 100644 index 00000000..d6596816 --- /dev/null +++ b/docs/docs/tutorials/ed-10.ipynb @@ -0,0 +1,222 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0", + "metadata": {}, + "source": [ + "# Pair Distribution Function: Ni, NPD\n", + "\n", + "This example demonstrates a pair distribution function (PDF) analysis\n", + "of Ni, based on data collected from a constant wavelength neutron\n", + "powder diffraction experiment.\n", + "\n", + "The dataset is taken from:\n", + "https://github.com/diffpy/cmi_exchange/tree/main/cmi_scripts/fitNiPDF" + ] + }, + { + "cell_type": "markdown", + "id": "1", + "metadata": {}, + "source": [ + "## Import Library" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2", + "metadata": {}, + "outputs": [], + "source": [ + "import easydiffraction as ed" + ] + }, + { + "cell_type": "markdown", + "id": "3", + "metadata": {}, + "source": [ + "## Create Project" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4", + "metadata": {}, + "outputs": [], + "source": [ + "project = ed.Project()" + ] + }, + { + "cell_type": "markdown", + "id": "5", + "metadata": {}, + "source": [ + "## Add Structure" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6", + "metadata": {}, + "outputs": [], + "source": [ + "project.structures.create(name='ni')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7", + "metadata": {}, + "outputs": [], + "source": [ + "project.structures['ni'].space_group.name_h_m = 'F m -3 m'\n", + "project.structures['ni'].space_group.it_coordinate_system_code = '1'\n", + "project.structures['ni'].cell.length_a = 3.52387\n", + "project.structures['ni'].atom_sites.create(\n", + " label='Ni',\n", + " type_symbol='Ni',\n", + " fract_x=0.0,\n", + " fract_y=0.0,\n", + " fract_z=0.0,\n", + " wyckoff_letter='a',\n", + " b_iso=0.5,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "8", + "metadata": {}, + "source": [ + "## Add Experiment" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9", + "metadata": {}, + "outputs": [], + "source": [ + "data_path = ed.download_data(id=6, destination='data')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "10", + "metadata": {}, + "outputs": [], + "source": [ + "project.experiments.add_from_data_path(\n", + " name='pdf',\n", + " data_path=data_path,\n", + " sample_form='powder',\n", + " beam_mode='constant wavelength',\n", + " radiation_probe='neutron',\n", + " scattering_type='total',\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "11", + "metadata": {}, + "outputs": [], + "source": [ + "project.experiments['pdf'].linked_phases.create(id='ni', scale=1.0)\n", + "project.experiments['pdf'].peak.damp_q = 0\n", + "project.experiments['pdf'].peak.broad_q = 0.03\n", + "project.experiments['pdf'].peak.cutoff_q = 27.0\n", + "project.experiments['pdf'].peak.sharp_delta_1 = 0.0\n", + "project.experiments['pdf'].peak.sharp_delta_2 = 2.0\n", + "project.experiments['pdf'].peak.damp_particle_diameter = 0" + ] + }, + { + "cell_type": "markdown", + "id": "12", + "metadata": {}, + "source": [ + "## Select Fitting Parameters" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "13", + "metadata": {}, + "outputs": [], + "source": [ + "project.structures['ni'].cell.length_a.free = True\n", + "project.structures['ni'].atom_sites['Ni'].b_iso.free = True" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "14", + "metadata": {}, + "outputs": [], + "source": [ + "project.experiments['pdf'].linked_phases['ni'].scale.free = True\n", + "project.experiments['pdf'].peak.broad_q.free = True\n", + "project.experiments['pdf'].peak.sharp_delta_2.free = True" + ] + }, + { + "cell_type": "markdown", + "id": "15", + "metadata": {}, + "source": [ + "## Run Fitting" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "16", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.fit()\n", + "project.analysis.show_fit_results()" + ] + }, + { + "cell_type": "markdown", + "id": "17", + "metadata": {}, + "source": [ + "## Plot Measured vs Calculated" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "18", + "metadata": {}, + "outputs": [], + "source": [ + "project.plot_meas_vs_calc(expt_name='pdf', show_residual=True)" + ] + } + ], + "metadata": { + "jupytext": { + "cell_metadata_filter": "-all", + "main_language": "python", + "notebook_metadata_filter": "-all" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/tutorials/ed-10.py b/docs/docs/tutorials/ed-10.py similarity index 75% rename from tutorials/ed-10.py rename to docs/docs/tutorials/ed-10.py index f3253429..1cad89ab 100644 --- a/tutorials/ed-10.py +++ b/docs/docs/tutorials/ed-10.py @@ -21,16 +21,16 @@ project = ed.Project() # %% [markdown] -# ## Add Sample Model +# ## Add Structure # %% -project.sample_models.add(name='ni') +project.structures.create(name='ni') # %% -project.sample_models['ni'].space_group.name_h_m = 'F m -3 m' -project.sample_models['ni'].space_group.it_coordinate_system_code = '1' -project.sample_models['ni'].cell.length_a = 3.52387 -project.sample_models['ni'].atom_sites.add( +project.structures['ni'].space_group.name_h_m = 'F m -3 m' +project.structures['ni'].space_group.it_coordinate_system_code = '1' +project.structures['ni'].cell.length_a = 3.52387 +project.structures['ni'].atom_sites.create( label='Ni', type_symbol='Ni', fract_x=0.0, @@ -47,7 +47,7 @@ data_path = ed.download_data(id=6, destination='data') # %% -project.experiments.add( +project.experiments.add_from_data_path( name='pdf', data_path=data_path, sample_form='powder', @@ -57,7 +57,7 @@ ) # %% -project.experiments['pdf'].linked_phases.add(id='ni', scale=1.0) +project.experiments['pdf'].linked_phases.create(id='ni', scale=1.0) project.experiments['pdf'].peak.damp_q = 0 project.experiments['pdf'].peak.broad_q = 0.03 project.experiments['pdf'].peak.cutoff_q = 27.0 @@ -69,8 +69,8 @@ # ## Select Fitting Parameters # %% -project.sample_models['ni'].cell.length_a.free = True -project.sample_models['ni'].atom_sites['Ni'].b_iso.free = True +project.structures['ni'].cell.length_a.free = True +project.structures['ni'].atom_sites['Ni'].b_iso.free = True # %% project.experiments['pdf'].linked_phases['ni'].scale.free = True @@ -81,7 +81,6 @@ # ## Run Fitting # %% -project.analysis.current_calculator = 'pdffit' project.analysis.fit() project.analysis.show_fit_results() diff --git a/docs/docs/tutorials/ed-11.ipynb b/docs/docs/tutorials/ed-11.ipynb new file mode 100644 index 00000000..ddacaac7 --- /dev/null +++ b/docs/docs/tutorials/ed-11.ipynb @@ -0,0 +1,254 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0", + "metadata": {}, + "source": [ + "# Pair Distribution Function: Si, NPD\n", + "\n", + "This example demonstrates a pair distribution function (PDF) analysis\n", + "of Si, based on data collected from a time-of-flight neutron powder\n", + "diffraction experiment at NOMAD at SNS." + ] + }, + { + "cell_type": "markdown", + "id": "1", + "metadata": {}, + "source": [ + "## Import Library" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2", + "metadata": {}, + "outputs": [], + "source": [ + "import easydiffraction as ed" + ] + }, + { + "cell_type": "markdown", + "id": "3", + "metadata": {}, + "source": [ + "## Create Project" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4", + "metadata": {}, + "outputs": [], + "source": [ + "project = ed.Project()" + ] + }, + { + "cell_type": "markdown", + "id": "5", + "metadata": {}, + "source": [ + "## Set Plotting Engine" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6", + "metadata": {}, + "outputs": [], + "source": [ + "# Keep the auto-selected engine. Alternatively, you can uncomment the\n", + "# line below to explicitly set the engine to the required one.\n", + "# project.plotter.engine = 'plotly'" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7", + "metadata": {}, + "outputs": [], + "source": [ + "# Set global plot range for plots\n", + "project.plotter.x_max = 40" + ] + }, + { + "cell_type": "markdown", + "id": "8", + "metadata": {}, + "source": [ + "## Add Structure" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9", + "metadata": {}, + "outputs": [], + "source": [ + "project.structures.create(name='si')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "10", + "metadata": {}, + "outputs": [], + "source": [ + "structure = project.structures['si']\n", + "structure.space_group.name_h_m.value = 'F d -3 m'\n", + "structure.space_group.it_coordinate_system_code = '1'\n", + "structure.cell.length_a = 5.43146\n", + "structure.atom_sites.create(\n", + " label='Si',\n", + " type_symbol='Si',\n", + " fract_x=0,\n", + " fract_y=0,\n", + " fract_z=0,\n", + " wyckoff_letter='a',\n", + " b_iso=0.5,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "11", + "metadata": {}, + "source": [ + "## Add Experiment" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "12", + "metadata": {}, + "outputs": [], + "source": [ + "data_path = ed.download_data(id=5, destination='data')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "13", + "metadata": {}, + "outputs": [], + "source": [ + "project.experiments.add_from_data_path(\n", + " name='nomad',\n", + " data_path=data_path,\n", + " sample_form='powder',\n", + " beam_mode='time-of-flight',\n", + " radiation_probe='neutron',\n", + " scattering_type='total',\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "14", + "metadata": {}, + "outputs": [], + "source": [ + "experiment = project.experiments['nomad']\n", + "experiment.linked_phases.create(id='si', scale=1.0)\n", + "experiment.peak.damp_q = 0.02\n", + "experiment.peak.broad_q = 0.03\n", + "experiment.peak.cutoff_q = 35.0\n", + "experiment.peak.sharp_delta_1 = 0.0\n", + "experiment.peak.sharp_delta_2 = 4.0\n", + "experiment.peak.damp_particle_diameter = 0" + ] + }, + { + "cell_type": "markdown", + "id": "15", + "metadata": {}, + "source": [ + "## Select Fitting Parameters" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "16", + "metadata": {}, + "outputs": [], + "source": [ + "project.structures['si'].cell.length_a.free = True\n", + "project.structures['si'].atom_sites['Si'].b_iso.free = True\n", + "experiment.linked_phases['si'].scale.free = True" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "17", + "metadata": {}, + "outputs": [], + "source": [ + "experiment.peak.damp_q.free = True\n", + "experiment.peak.broad_q.free = True\n", + "experiment.peak.sharp_delta_1.free = True\n", + "experiment.peak.sharp_delta_2.free = True" + ] + }, + { + "cell_type": "markdown", + "id": "18", + "metadata": {}, + "source": [ + "## Run Fitting" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "19", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.fit()\n", + "project.analysis.show_fit_results()" + ] + }, + { + "cell_type": "markdown", + "id": "20", + "metadata": {}, + "source": [ + "## Plot Measured vs Calculated" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "21", + "metadata": {}, + "outputs": [], + "source": [ + "project.plot_meas_vs_calc(expt_name='nomad', show_residual=False)" + ] + } + ], + "metadata": { + "jupytext": { + "cell_metadata_filter": "-all", + "main_language": "python", + "notebook_metadata_filter": "-all" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/tutorials/ed-11.py b/docs/docs/tutorials/ed-11.py similarity index 77% rename from tutorials/ed-11.py rename to docs/docs/tutorials/ed-11.py index 4a5b3176..a16dbec7 100644 --- a/tutorials/ed-11.py +++ b/docs/docs/tutorials/ed-11.py @@ -30,17 +30,17 @@ project.plotter.x_max = 40 # %% [markdown] -# ## Add Sample Model +# ## Add Structure # %% -project.sample_models.add(name='si') +project.structures.create(name='si') # %% -sample_model = project.sample_models['si'] -sample_model.space_group.name_h_m.value = 'F d -3 m' -sample_model.space_group.it_coordinate_system_code = '1' -sample_model.cell.length_a = 5.43146 -sample_model.atom_sites.add( +structure = project.structures['si'] +structure.space_group.name_h_m.value = 'F d -3 m' +structure.space_group.it_coordinate_system_code = '1' +structure.cell.length_a = 5.43146 +structure.atom_sites.create( label='Si', type_symbol='Si', fract_x=0, @@ -57,7 +57,7 @@ data_path = ed.download_data(id=5, destination='data') # %% -project.experiments.add( +project.experiments.add_from_data_path( name='nomad', data_path=data_path, sample_form='powder', @@ -68,7 +68,7 @@ # %% experiment = project.experiments['nomad'] -experiment.linked_phases.add(id='si', scale=1.0) +experiment.linked_phases.create(id='si', scale=1.0) experiment.peak.damp_q = 0.02 experiment.peak.broad_q = 0.03 experiment.peak.cutoff_q = 35.0 @@ -80,8 +80,8 @@ # ## Select Fitting Parameters # %% -project.sample_models['si'].cell.length_a.free = True -project.sample_models['si'].atom_sites['Si'].b_iso.free = True +project.structures['si'].cell.length_a.free = True +project.structures['si'].atom_sites['Si'].b_iso.free = True experiment.linked_phases['si'].scale.free = True # %% @@ -94,7 +94,6 @@ # ## Run Fitting # %% -project.analysis.current_calculator = 'pdffit' project.analysis.fit() project.analysis.show_fit_results() diff --git a/docs/docs/tutorials/ed-12.ipynb b/docs/docs/tutorials/ed-12.ipynb new file mode 100644 index 00000000..a6394390 --- /dev/null +++ b/docs/docs/tutorials/ed-12.ipynb @@ -0,0 +1,303 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0", + "metadata": {}, + "source": [ + "# Pair Distribution Function: NaCl, XRD\n", + "\n", + "This example demonstrates a pair distribution function (PDF) analysis\n", + "of NaCl, based on data collected from an X-ray powder diffraction\n", + "experiment.\n", + "\n", + "The dataset is taken from:\n", + "https://github.com/diffpy/add2019-diffpy-cmi/tree/master" + ] + }, + { + "cell_type": "markdown", + "id": "1", + "metadata": {}, + "source": [ + "## Import Library" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2", + "metadata": {}, + "outputs": [], + "source": [ + "import easydiffraction as ed" + ] + }, + { + "cell_type": "markdown", + "id": "3", + "metadata": {}, + "source": [ + "## Create Project" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4", + "metadata": {}, + "outputs": [], + "source": [ + "project = ed.Project()" + ] + }, + { + "cell_type": "markdown", + "id": "5", + "metadata": {}, + "source": [ + "## Set Plotting Engine" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6", + "metadata": {}, + "outputs": [], + "source": [ + "# Keep the auto-selected engine. Alternatively, you can uncomment the\n", + "# line below to explicitly set the engine to the required one.\n", + "# project.plotter.engine = 'plotly'" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7", + "metadata": {}, + "outputs": [], + "source": [ + "# Set global plot range for plots\n", + "project.plotter.x_min = 2.0\n", + "project.plotter.x_max = 30.0" + ] + }, + { + "cell_type": "markdown", + "id": "8", + "metadata": {}, + "source": [ + "## Add Structure" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9", + "metadata": {}, + "outputs": [], + "source": [ + "project.structures.create(name='nacl')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "10", + "metadata": {}, + "outputs": [], + "source": [ + "project.structures['nacl'].space_group.name_h_m = 'F m -3 m'\n", + "project.structures['nacl'].space_group.it_coordinate_system_code = '1'\n", + "project.structures['nacl'].cell.length_a = 5.62\n", + "project.structures['nacl'].atom_sites.create(\n", + " label='Na',\n", + " type_symbol='Na',\n", + " fract_x=0,\n", + " fract_y=0,\n", + " fract_z=0,\n", + " wyckoff_letter='a',\n", + " b_iso=1.0,\n", + ")\n", + "project.structures['nacl'].atom_sites.create(\n", + " label='Cl',\n", + " type_symbol='Cl',\n", + " fract_x=0.5,\n", + " fract_y=0.5,\n", + " fract_z=0.5,\n", + " wyckoff_letter='b',\n", + " b_iso=1.0,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "11", + "metadata": {}, + "source": [ + "## Add Experiment" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "12", + "metadata": {}, + "outputs": [], + "source": [ + "data_path = ed.download_data(id=4, destination='data')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "13", + "metadata": {}, + "outputs": [], + "source": [ + "project.experiments.add_from_data_path(\n", + " name='xray_pdf',\n", + " data_path=data_path,\n", + " sample_form='powder',\n", + " beam_mode='constant wavelength',\n", + " radiation_probe='xray',\n", + " scattering_type='total',\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "14", + "metadata": {}, + "outputs": [], + "source": [ + "project.experiments['xray_pdf'].show_supported_peak_profile_types()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "15", + "metadata": {}, + "outputs": [], + "source": [ + "project.experiments['xray_pdf'].show_current_peak_profile_type()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "16", + "metadata": {}, + "outputs": [], + "source": [ + "project.experiments['xray_pdf'].peak_profile_type = 'gaussian-damped-sinc'" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "17", + "metadata": {}, + "outputs": [], + "source": [ + "project.experiments['xray_pdf'].peak.damp_q = 0.03\n", + "project.experiments['xray_pdf'].peak.broad_q = 0\n", + "project.experiments['xray_pdf'].peak.cutoff_q = 21\n", + "project.experiments['xray_pdf'].peak.sharp_delta_1 = 0\n", + "project.experiments['xray_pdf'].peak.sharp_delta_2 = 5\n", + "project.experiments['xray_pdf'].peak.damp_particle_diameter = 0" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "18", + "metadata": {}, + "outputs": [], + "source": [ + "project.experiments['xray_pdf'].linked_phases.create(id='nacl', scale=0.5)" + ] + }, + { + "cell_type": "markdown", + "id": "19", + "metadata": {}, + "source": [ + "## Select Fitting Parameters" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "20", + "metadata": {}, + "outputs": [], + "source": [ + "project.structures['nacl'].cell.length_a.free = True\n", + "project.structures['nacl'].atom_sites['Na'].b_iso.free = True\n", + "project.structures['nacl'].atom_sites['Cl'].b_iso.free = True" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "21", + "metadata": {}, + "outputs": [], + "source": [ + "project.experiments['xray_pdf'].linked_phases['nacl'].scale.free = True\n", + "project.experiments['xray_pdf'].peak.damp_q.free = True\n", + "project.experiments['xray_pdf'].peak.sharp_delta_2.free = True" + ] + }, + { + "cell_type": "markdown", + "id": "22", + "metadata": {}, + "source": [ + "## Run Fitting" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "23", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.fit()\n", + "project.analysis.show_fit_results()" + ] + }, + { + "cell_type": "markdown", + "id": "24", + "metadata": {}, + "source": [ + "## Plot Measured vs Calculated" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "25", + "metadata": {}, + "outputs": [], + "source": [ + "project.plot_meas_vs_calc(expt_name='xray_pdf')" + ] + } + ], + "metadata": { + "jupytext": { + "cell_metadata_filter": "-all", + "main_language": "python", + "notebook_metadata_filter": "-all" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/tutorials/ed-12.py b/docs/docs/tutorials/ed-12.py similarity index 77% rename from tutorials/ed-12.py rename to docs/docs/tutorials/ed-12.py index c40cc5ad..b6701709 100644 --- a/tutorials/ed-12.py +++ b/docs/docs/tutorials/ed-12.py @@ -34,16 +34,16 @@ project.plotter.x_max = 30.0 # %% [markdown] -# ## Add Sample Model +# ## Add Structure # %% -project.sample_models.add(name='nacl') +project.structures.create(name='nacl') # %% -project.sample_models['nacl'].space_group.name_h_m = 'F m -3 m' -project.sample_models['nacl'].space_group.it_coordinate_system_code = '1' -project.sample_models['nacl'].cell.length_a = 5.62 -project.sample_models['nacl'].atom_sites.add( +project.structures['nacl'].space_group.name_h_m = 'F m -3 m' +project.structures['nacl'].space_group.it_coordinate_system_code = '1' +project.structures['nacl'].cell.length_a = 5.62 +project.structures['nacl'].atom_sites.create( label='Na', type_symbol='Na', fract_x=0, @@ -52,7 +52,7 @@ wyckoff_letter='a', b_iso=1.0, ) -project.sample_models['nacl'].atom_sites.add( +project.structures['nacl'].atom_sites.create( label='Cl', type_symbol='Cl', fract_x=0.5, @@ -69,7 +69,7 @@ data_path = ed.download_data(id=4, destination='data') # %% -project.experiments.add( +project.experiments.add_from_data_path( name='xray_pdf', data_path=data_path, sample_form='powder', @@ -96,15 +96,15 @@ project.experiments['xray_pdf'].peak.damp_particle_diameter = 0 # %% -project.experiments['xray_pdf'].linked_phases.add(id='nacl', scale=0.5) +project.experiments['xray_pdf'].linked_phases.create(id='nacl', scale=0.5) # %% [markdown] # ## Select Fitting Parameters # %% -project.sample_models['nacl'].cell.length_a.free = True -project.sample_models['nacl'].atom_sites['Na'].b_iso.free = True -project.sample_models['nacl'].atom_sites['Cl'].b_iso.free = True +project.structures['nacl'].cell.length_a.free = True +project.structures['nacl'].atom_sites['Na'].b_iso.free = True +project.structures['nacl'].atom_sites['Cl'].b_iso.free = True # %% project.experiments['xray_pdf'].linked_phases['nacl'].scale.free = True @@ -115,7 +115,6 @@ # ## Run Fitting # %% -project.analysis.current_calculator = 'pdffit' project.analysis.fit() project.analysis.show_fit_results() diff --git a/docs/docs/tutorials/ed-13.ipynb b/docs/docs/tutorials/ed-13.ipynb new file mode 100644 index 00000000..a31467a1 --- /dev/null +++ b/docs/docs/tutorials/ed-13.ipynb @@ -0,0 +1,2923 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0", + "metadata": {}, + "source": [ + "# Fitting Powder Diffraction data\n", + "\n", + "This notebook guides you through the Rietveld refinement of crystal\n", + "structures using simulated powder diffraction data. It consists of two\n", + "parts:\n", + "- Introduction: A simple reference fit using silicon (Si) crystal\n", + " structure.\n", + "- Exercise: A more complex fit using La₀.₅Ba₀.₅CoO₃ (LBCO) crystal\n", + " structure.\n", + "\n", + "## 🛠️ Import Library\n", + "\n", + "We start by importing the necessary library for the analysis. In this\n", + "notebook, we use the EasyDiffraction library. As mentioned in the\n", + "introduction to EasyScience, EasyDiffraction is built on that\n", + "framework and offers a high-level interface focused specifically for\n", + "diffraction analysis.\n", + "\n", + "This notebook is self-contained and designed for hands-on learning.\n", + "However, if you're interested in exploring more advanced features or\n", + "learning about additional capabilities of the EasyDiffraction library,\n", + "please refer to the official documentation:\n", + "https://docs.easydiffraction.org/lib\n", + "\n", + "Depending on your requirements, you may choose to import only specific\n", + "classes. However, for the sake of simplicity in this notebook, we will\n", + "import the entire library." + ] + }, + { + "cell_type": "markdown", + "id": "1", + "metadata": { + "tags": [ + "doc-link" + ] + }, + "source": [ + "📖 See\n", + "[documentation](https://docs.easydiffraction.org/lib/user-guide/first-steps/#importing-easydiffraction)\n", + "for more details about importing the EasyDiffraction library and its\n", + "components." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2", + "metadata": {}, + "outputs": [], + "source": [ + "import easydiffraction as ed" + ] + }, + { + "cell_type": "markdown", + "id": "3", + "metadata": {}, + "source": [ + "## 📘 Introduction: Simple Reference Fit – Si\n", + "\n", + "Before diving into the more complex fitting exercise with the\n", + "La₀.₅Ba₀.₅CoO₃ (LBCO) crystal structure, let's start with a simpler\n", + "example using the silicon (Si) crystal structure. This will help us\n", + "understand the basic concepts and steps involved in fitting a crystal\n", + "structure using powder diffraction data.\n", + "\n", + "For this part of the notebook, we will use the powder diffraction data\n", + "previously simulated using the Si crystal structure.\n", + "\n", + "### 📦 Create a Project – 'reference'\n", + "\n", + "In EasyDiffraction, a project serves as a container for all\n", + "information related to the analysis of a specific experiment or set of\n", + "experiments. It enables you to organize your data, experiments,\n", + "crystal structures, and fitting parameters in an organized manner. You\n", + "can think of it as a folder containing all the essential details about\n", + "your analysis. The project also allows us to visualize both the\n", + "measured and calculated diffraction patterns, among other things." + ] + }, + { + "cell_type": "markdown", + "id": "4", + "metadata": { + "tags": [ + "doc-link" + ] + }, + "source": [ + "📖 See\n", + "[documentation](https://docs.easydiffraction.org/lib/user-guide/analysis-workflow/project/)\n", + "for more details about creating a project and its purpose in the\n", + "analysis workflow." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5", + "metadata": {}, + "outputs": [], + "source": [ + "project_1 = ed.Project(name='reference')" + ] + }, + { + "cell_type": "markdown", + "id": "6", + "metadata": {}, + "source": [ + "You can set the title and description of the project to provide\n", + "context and information about the analysis being performed. This is\n", + "useful for documentation purposes and helps others (or yourself in the\n", + "future) understand the purpose of the project at a glance." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7", + "metadata": {}, + "outputs": [], + "source": [ + "project_1.info.title = 'Reference Silicon Fit'\n", + "project_1.info.description = 'Fitting simulated powder diffraction pattern of Si.'" + ] + }, + { + "cell_type": "markdown", + "id": "8", + "metadata": {}, + "source": [ + "### 🔬 Create an Experiment\n", + "\n", + "An experiment represents a specific diffraction measurement performed\n", + "on a specific sample using a particular instrument. It contains\n", + "details about the measured data, instrument parameters, and other\n", + "relevant information." + ] + }, + { + "cell_type": "markdown", + "id": "9", + "metadata": { + "tags": [ + "doc-link" + ] + }, + "source": [ + "📖 See\n", + "[documentation](https://docs.easydiffraction.org/lib/user-guide/analysis-workflow/experiment/)\n", + "for more details about experiments and their purpose in the analysis\n", + "workflow." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "10", + "metadata": {}, + "outputs": [], + "source": [ + "data_dir = 'data'\n", + "file_name = 'reduced_Si.xye'\n", + "si_xye_path = f'{data_dir}/{file_name}'" + ] + }, + { + "cell_type": "markdown", + "id": "11", + "metadata": {}, + "source": [ + "Uncomment the following cell if your data reduction failed and the\n", + "reduced data file is missing. In this case, you can download our\n", + "pre-generated reduced data file from the EasyDiffraction repository.\n", + "The `download_data` function will not overwrite an existing file\n", + "unless you set `overwrite=True`, so it's safe to run even if the\n", + "file is already present." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "12", + "metadata": {}, + "outputs": [], + "source": [ + "si_xye_path = ed.download_data(id=17, destination=data_dir)" + ] + }, + { + "cell_type": "markdown", + "id": "13", + "metadata": {}, + "source": [ + "Now we can create the experiment and load the measured data. In this\n", + "case, the experiment is defined as a powder diffraction measurement\n", + "using time-of-flight neutrons. The measured data is loaded from a file\n", + "containing the reduced diffraction pattern of Si from the data\n", + "reduction notebook." + ] + }, + { + "cell_type": "markdown", + "id": "14", + "metadata": { + "tags": [ + "doc-link" + ] + }, + "source": [ + "📖 See\n", + "[documentation](https://docs.easydiffraction.org/lib/user-guide/analysis-workflow/experiment/#defining-an-experiment-manually)\n", + "for more details about different types of experiments." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "15", + "metadata": {}, + "outputs": [], + "source": [ + "project_1.experiments.add_from_data_path(\n", + " name='sim_si',\n", + " data_path=si_xye_path,\n", + " sample_form='powder',\n", + " beam_mode='time-of-flight',\n", + " radiation_probe='neutron',\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "16", + "metadata": {}, + "source": [ + "#### Inspect Measured Data\n", + "\n", + "After creating the experiment, we can examine the measured data. The\n", + "measured data consists of a diffraction pattern having time-of-flight\n", + "(TOF) values and corresponding intensities. The TOF values are given\n", + "in microseconds (μs), and the intensities are in arbitrary units.\n", + "\n", + "The data is stored in XYE format, a simple text format containing\n", + "three columns: TOF, intensity, and intensity error (if available)." + ] + }, + { + "cell_type": "markdown", + "id": "17", + "metadata": { + "tags": [ + "doc-link" + ] + }, + "source": [ + "📖 See\n", + "[documentation](https://docs.easydiffraction.org/lib/user-guide/analysis-workflow/experiment/#measured-data-category)\n", + "for more details about the measured data and its format.\n", + "\n", + "To visualize the measured data, we can use the `plot_meas` method of\n", + "the project. Before plotting, we need to set the plotting engine to\n", + "'plotly', which provides interactive visualizations." + ] + }, + { + "cell_type": "markdown", + "id": "18", + "metadata": { + "tags": [ + "doc-link" + ] + }, + "source": [ + "📖 See\n", + "[documentation](https://easyscience.github.io/diffraction-lib/user-guide/first-steps/#supported-plotters)\n", + "for more details about setting the plotting engine." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "19", + "metadata": {}, + "outputs": [], + "source": [ + "# Keep the auto-selected engine. Alternatively, you can uncomment the\n", + "# line below to explicitly set the engine to the required one.\n", + "# project.plotter.engine = 'plotly'" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "20", + "metadata": {}, + "outputs": [], + "source": [ + "project_1.plot_meas(expt_name='sim_si')" + ] + }, + { + "cell_type": "markdown", + "id": "21", + "metadata": {}, + "source": [ + "If you zoom in on the highest TOF peak (around 120,000 μs), you will\n", + "notice that it has a broad and unusual shape. This distortion, along\n", + "with additional effects on the low TOF peaks, is most likely an\n", + "artifact related to the simplifications made during the simulation\n", + "and/or reduction process and is currently under investigation.\n", + "However, this is outside the scope of this school. Therefore, we will\n", + "simply exclude both the low and high TOF regions from the analysis by\n", + "adding an excluded regions to the experiment.\n", + "\n", + "In real experiments, it is often necessary to exclude certain regions\n", + "from the measured data. For example, the direct beam can significantly\n", + "increase the background at very low angles, making those parts of the\n", + "diffractogram unreliable. Additionally, sample environment components\n", + "may introduce unwanted peaks. In such cases, excluding specific\n", + "regions is often simpler and more effective than modeling them with an\n", + "additional sample phase." + ] + }, + { + "cell_type": "markdown", + "id": "22", + "metadata": { + "tags": [ + "doc-link" + ] + }, + "source": [ + "📖 See\n", + "[documentation](https://docs.easydiffraction.org/lib/user-guide/analysis-workflow/experiment/#excluded-regions-category)\n", + "for more details about excluding regions from the measured data." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "23", + "metadata": {}, + "outputs": [], + "source": [ + "project_1.experiments['sim_si'].excluded_regions.create(id='1', start=0, end=55000)\n", + "project_1.experiments['sim_si'].excluded_regions.create(id='2', start=105500, end=200000)" + ] + }, + { + "cell_type": "markdown", + "id": "24", + "metadata": {}, + "source": [ + "To visualize the effect of excluding the high TOF region, we can plot\n", + "the measured data again. The excluded region will be omitted from the\n", + "plot and is not used in the fitting process." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "25", + "metadata": {}, + "outputs": [], + "source": [ + "project_1.plot_meas(expt_name='sim_si')" + ] + }, + { + "cell_type": "markdown", + "id": "26", + "metadata": {}, + "source": [ + "#### Set Instrument Parameters\n", + "\n", + "After the experiment is created and measured data is loaded, we need\n", + "to set the instrument parameters.\n", + "\n", + "In this type of experiment, the instrument parameters define how the\n", + "measured data is converted between d-spacing and time-of-flight (TOF)\n", + "during the data reduction process as well as the angular position of\n", + "the detector. So, we put values based on those from the reduction.\n", + "These values can be found in the header of the corresponding .XYE\n", + "file. Their names are `two_theta` and `DIFC`, which stand for the\n", + "two-theta angle and the linear conversion factor from d-spacing to\n", + "TOF, respectively.\n", + "\n", + "You can set them manually, but it is more convenient to use the\n", + "`extract_metadata` function from the EasyDiffraction library." + ] + }, + { + "cell_type": "markdown", + "id": "27", + "metadata": { + "tags": [ + "doc-link" + ] + }, + "source": [ + "📖 See\n", + "[documentation](https://docs.easydiffraction.org/lib/user-guide/analysis-workflow/experiment/#instrument-category)\n", + "for more details about the instrument parameters." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "28", + "metadata": {}, + "outputs": [], + "source": [ + "project_1.experiments['sim_si'].instrument.setup_twotheta_bank = ed.extract_metadata(\n", + " si_xye_path, r'two_theta\\s*=\\s*([-+]?\\d*\\.?\\d+(?:[eE][-+]?\\d+)?)'\n", + ")\n", + "project_1.experiments['sim_si'].instrument.calib_d_to_tof_linear = ed.extract_metadata(\n", + " si_xye_path, r'DIFC\\s*=\\s*([-+]?\\d*\\.?\\d+(?:[eE][-+]?\\d+)?)'\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "29", + "metadata": {}, + "source": [ + "Before proceeding, let's take a quick look at the concept of\n", + "parameters in EasyDiffraction, which is similar to the parameter\n", + "concept in EasyScience. The current version of EasyDiffraction is\n", + "transitioning to reuse the parameter system from EasyScience.\n", + "\n", + "That is, every parameter is an object, which has different attributes,\n", + "such as `value`, `units`, etc. To display the parameter of interest,\n", + "you can simply print the parameter object.\n", + "\n", + "For example, to display the linear conversion factor from d-spacing to\n", + "TOF, which is the `calib_d_to_tof_linear` parameter, you can do the\n", + "following:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "30", + "metadata": {}, + "outputs": [], + "source": [ + "print(project_1.experiments['sim_si'].instrument.calib_d_to_tof_linear)" + ] + }, + { + "cell_type": "markdown", + "id": "31", + "metadata": {}, + "source": [ + "The `value` attribute represents the current value of the parameter as\n", + "a float. You can access it directly by using the `value` attribute of\n", + "the parameter. This is useful when you want to use the parameter value\n", + "in calculations or when you want to assign it to another parameter.\n", + "For example, to get only the value of the same parameter as floating\n", + "point number, but not the whole object, you can do the following:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "32", + "metadata": {}, + "outputs": [], + "source": [ + "print(project_1.experiments['sim_si'].instrument.calib_d_to_tof_linear.value)" + ] + }, + { + "cell_type": "markdown", + "id": "33", + "metadata": {}, + "source": [ + "Note that to set the value of the parameter, you can simply assign a\n", + "new value to the parameter object without using the `value` attribute,\n", + "as we did above." + ] + }, + { + "cell_type": "markdown", + "id": "34", + "metadata": { + "tags": [ + "doc-link" + ] + }, + "source": [ + "📖 See\n", + "[documentation](https://docs.easydiffraction.org/lib/user-guide/parameters/)\n", + "for more details about parameters in EasyDiffraction and their\n", + "attributes." + ] + }, + { + "cell_type": "markdown", + "id": "35", + "metadata": {}, + "source": [ + "#### Set Peak Profile Parameters\n", + "\n", + "The next set of parameters is needed to define the peak profile used\n", + "in the fitting process. The peak profile describes the shape of the\n", + "diffraction peaks. They include parameters for the broadening and\n", + "asymmetry of the peaks.\n", + "\n", + "There are several commonly used peak profile functions:\n", + "- **Gaussian**: Describes peaks with a symmetric bell-shaped curve,\n", + " often used when instrumental broadening dominates. [Click for more\n", + " details.](https://mantidproject.github.io/docs-versioned/v6.1.0/fitting/fitfunctions/Gaussian.html)\n", + "- **Lorentzian**: Produces narrower central peaks with longer tails,\n", + " frequently used to model size broadening effects. [Click for more\n", + " details.](https://mantidproject.github.io/docs-versioned/v6.1.0/fitting/fitfunctions/Lorentzian.html)\n", + "- **Pseudo-Voigt**: A linear combination of Gaussian and Lorentzian\n", + " components, providing flexibility to represent real diffraction\n", + " peaks. [Click for more\n", + " details.](https://mantidproject.github.io/docs-versioned/v6.1.0/fitting/fitfunctions/PseudoVoigt.html)\n", + "- **Pseudo-Voigt convoluted with Ikeda-Carpenter**: Incorporates the\n", + " asymmetry introduced by the neutron pulse shape in time-of-flight\n", + " instruments. This is a common choice for TOF neutron powder\n", + " diffraction data. [Click for more\n", + " details.](https://docs.mantidproject.org/v6.1.0/fitting/fitfunctions/IkedaCarpenterPV.html)\n", + "\n", + "Here, we use a pseudo-Voigt peak profile function with Ikeda-Carpenter\n", + "asymmetry.\n", + "\n", + "The parameter values are typically determined experimentally on the\n", + "same instrument and under the same configuration as the data being\n", + "analyzed, using measurements of a standard sample. In our case, the Si\n", + "sample serves as this standard reference. We will refine the peak\n", + "profile parameters here, and these refined values will be used as\n", + "starting points for the more complex fit in the next part of the\n", + "notebook. For this initial fit, we will provide reasonable physical\n", + "guesses as starting values." + ] + }, + { + "cell_type": "markdown", + "id": "36", + "metadata": { + "tags": [ + "doc-link" + ] + }, + "source": [ + "📖 See\n", + "[documentation](https://docs.easydiffraction.org/lib/user-guide/analysis-workflow/experiment/#peak-category)\n", + "for more details about the peak profile types." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "37", + "metadata": {}, + "outputs": [], + "source": [ + "project_1.experiments['sim_si'].peak_profile_type = 'pseudo-voigt * ikeda-carpenter'\n", + "project_1.experiments['sim_si'].peak.broad_gauss_sigma_0 = 69498\n", + "project_1.experiments['sim_si'].peak.broad_gauss_sigma_1 = -55578\n", + "project_1.experiments['sim_si'].peak.broad_gauss_sigma_2 = 14560\n", + "project_1.experiments['sim_si'].peak.broad_mix_beta_0 = 0.0019\n", + "project_1.experiments['sim_si'].peak.broad_mix_beta_1 = 0.0137\n", + "project_1.experiments['sim_si'].peak.asym_alpha_0 = -0.0055\n", + "project_1.experiments['sim_si'].peak.asym_alpha_1 = 0.0147" + ] + }, + { + "cell_type": "markdown", + "id": "38", + "metadata": {}, + "source": [ + "#### Set Background\n", + "\n", + "The background of the diffraction pattern represents the portion of\n", + "the pattern that is not related to the crystal structure of the\n", + "sample. It's rather represents noise and other sources of scattering\n", + "that can affect the measured intensities. This includes contributions\n", + "from the instrument, the sample holder, the sample environment, and\n", + "other sources of incoherent scattering.\n", + "\n", + "The background can be modeled in various ways. In this example, we\n", + "will use a simple line segment background, which is a common approach\n", + "for powder diffraction data. The background intensity at any point is\n", + "defined by linear interpolation between neighboring points. The\n", + "background points are selected to span the range of the diffraction\n", + "pattern while avoiding the peaks.\n", + "\n", + "We will add several background points at specific TOF values (in μs)\n", + "and corresponding intensity values. These points are chosen to\n", + "represent the background level in the diffraction pattern free from\n", + "any peaks.\n", + "\n", + "The background points are added using the `add` method of the\n", + "`background` object. The `x` parameter represents the TOF value, and\n", + "the `y` parameter represents the intensity value at that TOF.\n", + "\n", + "Let's set all the background points at a constant value of 0.01, which\n", + "can be roughly estimated by the eye, and we will refine them later\n", + "during the fitting process." + ] + }, + { + "cell_type": "markdown", + "id": "39", + "metadata": { + "tags": [ + "doc-link" + ] + }, + "source": [ + "📖 See\n", + "[documentation](https://docs.easydiffraction.org/lib/user-guide/analysis-workflow/experiment/#background-category)\n", + "for more details about the background and its types." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "40", + "metadata": {}, + "outputs": [], + "source": [ + "project_1.experiments['sim_si'].background_type = 'line-segment'\n", + "project_1.experiments['sim_si'].background.create(id='1', x=50000, y=0.01)\n", + "project_1.experiments['sim_si'].background.create(id='2', x=60000, y=0.01)\n", + "project_1.experiments['sim_si'].background.create(id='3', x=70000, y=0.01)\n", + "project_1.experiments['sim_si'].background.create(id='4', x=80000, y=0.01)\n", + "project_1.experiments['sim_si'].background.create(id='5', x=90000, y=0.01)\n", + "project_1.experiments['sim_si'].background.create(id='6', x=100000, y=0.01)\n", + "project_1.experiments['sim_si'].background.create(id='7', x=110000, y=0.01)" + ] + }, + { + "cell_type": "markdown", + "id": "41", + "metadata": {}, + "source": [ + "### 🧩 Create a Structure – Si\n", + "\n", + "After setting up the experiment, we need to create a structure that\n", + "describes the crystal structure of the sample being analyzed.\n", + "\n", + "In this case, we will create a structure for silicon (Si) with a\n", + "cubic crystal structure. The structure contains information about\n", + "the space group, lattice parameters, atomic positions of the atoms in\n", + "the unit cell, atom types, occupancies and atomic displacement\n", + "parameters. The structure is essential for the fitting process, as\n", + "it is used to calculate the expected diffraction pattern.\n", + "\n", + "EasyDiffraction refines the crystal structure of the sample, but does\n", + "not solve it. Therefore, we need a good starting point with reasonable\n", + "structural parameters.\n", + "\n", + "Here, we define the Si structure as a cubic structure. As this is a\n", + "cubic structure, we only need to define the single lattice parameter,\n", + "which is the length of the unit cell edge. The Si crystal structure\n", + "has a single atom in the unit cell, which is located at the origin (0,\n", + "0, 0) of the unit cell. The symmetry of this site is defined by the\n", + "Wyckoff letter 'a'. The atomic displacement parameter defines the\n", + "thermal vibrations of the atoms in the unit cell and is presented as\n", + "an isotropic parameter (B_iso).\n", + "\n", + "Sometimes, the initial crystal structure parameters can be obtained\n", + "from one of the crystallographic databases, like for example the\n", + "Crystallography Open Database (COD). In this case, we use the COD\n", + "entry for silicon as a reference for the initial crystal structure\n", + "model: https://www.crystallography.net/cod/4507226.html\n", + "\n", + "Usually, the crystal structure parameters are provided in a CIF file\n", + "format, which is a standard format for crystallographic data. An\n", + "example of a CIF file for silicon is shown below. The CIF file\n", + "contains the space group information, unit cell parameters, and atomic\n", + "positions." + ] + }, + { + "cell_type": "markdown", + "id": "42", + "metadata": { + "tags": [ + "doc-link" + ] + }, + "source": [ + "📖 See\n", + "[documentation](https://docs.easydiffraction.org/lib/user-guide/data-format/)\n", + "for more details about the CIF format and its use in EasyDiffraction." + ] + }, + { + "cell_type": "markdown", + "id": "43", + "metadata": {}, + "source": [ + "```\n", + "data_si\n", + "\n", + "_space_group.name_H-M_alt \"F d -3 m\"\n", + "_space_group.IT_coordinate_system_code 2\n", + "\n", + "_cell.length_a 5.43\n", + "_cell.length_b 5.43\n", + "_cell.length_c 5.43\n", + "_cell.angle_alpha 90.0\n", + "_cell.angle_beta 90.0\n", + "_cell.angle_gamma 90.0\n", + "\n", + "loop_\n", + "_atom_site.label\n", + "_atom_site.type_symbol\n", + "_atom_site.fract_x\n", + "_atom_site.fract_y\n", + "_atom_site.fract_z\n", + "_atom_site.wyckoff_letter\n", + "_atom_site.occupancy\n", + "_atom_site.ADP_type\n", + "_atom_site.B_iso_or_equiv\n", + "Si Si 0 0 0 a 1.0 Biso 0.89\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "44", + "metadata": {}, + "source": [ + "As with adding the experiment in the previous step, we will create a\n", + "default structure and then modify its parameters to match the Si\n", + "structure." + ] + }, + { + "cell_type": "markdown", + "id": "45", + "metadata": { + "tags": [ + "doc-link" + ] + }, + "source": [ + "📖 See\n", + "[documentation](https://docs.easydiffraction.org/lib/user-guide/analysis-workflow/structure/)\n", + "for more details about structures and their purpose in the data\n", + "analysis workflow." + ] + }, + { + "cell_type": "markdown", + "id": "46", + "metadata": {}, + "source": [ + "#### Add Structure" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "47", + "metadata": {}, + "outputs": [], + "source": [ + "project_1.structures.create(name='si')" + ] + }, + { + "cell_type": "markdown", + "id": "48", + "metadata": {}, + "source": [ + "#### Set Space Group" + ] + }, + { + "cell_type": "markdown", + "id": "49", + "metadata": { + "tags": [ + "doc-link" + ] + }, + "source": [ + "📖 See\n", + "[documentation](https://docs.easydiffraction.org/lib/user-guide/analysis-workflow/structure/#space-group-category)\n", + "for more details about the space group." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "50", + "metadata": {}, + "outputs": [], + "source": [ + "project_1.structures['si'].space_group.name_h_m = 'F d -3 m'\n", + "project_1.structures['si'].space_group.it_coordinate_system_code = '2'" + ] + }, + { + "cell_type": "markdown", + "id": "51", + "metadata": {}, + "source": [ + "#### Set Lattice Parameters" + ] + }, + { + "cell_type": "markdown", + "id": "52", + "metadata": { + "tags": [ + "doc-link" + ] + }, + "source": [ + "📖 See\n", + "[documentation](https://docs.easydiffraction.org/lib/user-guide/analysis-workflow/structure/#cell-category)\n", + "for more details about the unit cell parameters." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "53", + "metadata": {}, + "outputs": [], + "source": [ + "project_1.structures['si'].cell.length_a = 5.43" + ] + }, + { + "cell_type": "markdown", + "id": "54", + "metadata": {}, + "source": [ + "#### Set Atom Sites" + ] + }, + { + "cell_type": "markdown", + "id": "55", + "metadata": { + "tags": [ + "doc-link" + ] + }, + "source": [ + "📖 See\n", + "[documentation](https://docs.easydiffraction.org/lib/user-guide/analysis-workflow/structure/#atom-sites-category)\n", + "for more details about the atom sites category." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "56", + "metadata": {}, + "outputs": [], + "source": [ + "project_1.structures['si'].atom_sites.create(\n", + " label='Si',\n", + " type_symbol='Si',\n", + " fract_x=0,\n", + " fract_y=0,\n", + " fract_z=0,\n", + " wyckoff_letter='a',\n", + " b_iso=0.89,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "57", + "metadata": {}, + "source": [ + "### 🔗 Assign Structure to Experiment\n", + "\n", + "Now we need to assign, or link, this structure to the experiment\n", + "created above. This linked crystallographic phase will be used to\n", + "calculate the expected diffraction pattern based on the crystal\n", + "structure defined in the structure." + ] + }, + { + "cell_type": "markdown", + "id": "58", + "metadata": { + "tags": [ + "doc-link" + ] + }, + "source": [ + "📖 See\n", + "[documentation](https://docs.easydiffraction.org/lib/user-guide/analysis-workflow/experiment/#linked-phases-category)\n", + "for more details about linking a structure to an experiment." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "59", + "metadata": {}, + "outputs": [], + "source": [ + "project_1.experiments['sim_si'].linked_phases.create(id='si', scale=1.0)" + ] + }, + { + "cell_type": "markdown", + "id": "60", + "metadata": {}, + "source": [ + "### 🚀 Analyze and Fit the Data\n", + "\n", + "After setting up the experiment and structure, we can now analyze\n", + "the measured diffraction pattern and perform the fit. Building on the\n", + "analogies from the EasyScience library and the previous notebooks, we\n", + "can say that all the parameters we introduced earlier — those defining\n", + "the structure (crystal structure parameters) and the experiment\n", + "(instrument, background, and peak profile parameters) — together form\n", + "the complete set of parameters that can be refined during the fitting\n", + "process.\n", + "\n", + "Unlike in the previous analysis notebooks, we will not create a\n", + "**math_model** object here. The mathematical model used to calculate\n", + "the expected diffraction pattern is already defined in the library and\n", + "will be applied automatically during the fitting process." + ] + }, + { + "cell_type": "markdown", + "id": "61", + "metadata": { + "title": "**Reminder:**" + }, + "source": [ + "\n", + "The fitting process involves comparing the measured diffraction\n", + "pattern with the calculated diffraction pattern based on the crystal\n", + "structure and instrument parameters. The goal is to adjust the\n", + "parameters of the structure and the experiment to minimize the\n", + "difference between the measured and calculated diffraction patterns.\n", + "This is done by refining the parameters of the structure and the\n", + "instrument settings to achieve a better fit." + ] + }, + { + "cell_type": "markdown", + "id": "62", + "metadata": { + "tags": [ + "doc-link" + ] + }, + "source": [ + "📖 See\n", + "[documentation](https://docs.easydiffraction.org/lib/user-guide/analysis-workflow/analysis/#minimization-optimization)\n", + "for more details about the fitting process in EasyDiffraction." + ] + }, + { + "cell_type": "markdown", + "id": "63", + "metadata": {}, + "source": [ + "#### Set Fit Parameters\n", + "\n", + "To perform the fit, we need to specify the refinement parameters.\n", + "These are the parameters that will be adjusted during the fitting\n", + "process to minimize the difference between the measured and calculated\n", + "diffraction patterns. This is done by setting the `free` attribute of\n", + "the corresponding parameters to `True`.\n", + "\n", + "Note: setting `param.free = True` is equivalent to using `param.fixed\n", + "= False` in the EasyScience library.\n", + "\n", + "We will refine the scale factor of the Si phase, the intensities of\n", + "the background points as well as the peak profile parameters. The\n", + "structure parameters of the Si phase will not be refined, as this\n", + "sample is considered a reference sample with known parameters." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "64", + "metadata": {}, + "outputs": [], + "source": [ + "project_1.experiments['sim_si'].linked_phases['si'].scale.free = True\n", + "\n", + "for line_segment in project_1.experiments['sim_si'].background:\n", + " line_segment.y.free = True\n", + "\n", + "project_1.experiments['sim_si'].peak.broad_gauss_sigma_0.free = True\n", + "project_1.experiments['sim_si'].peak.broad_gauss_sigma_1.free = True\n", + "project_1.experiments['sim_si'].peak.broad_gauss_sigma_2.free = True\n", + "project_1.experiments['sim_si'].peak.broad_mix_beta_0.free = True\n", + "project_1.experiments['sim_si'].peak.broad_mix_beta_1.free = True\n", + "project_1.experiments['sim_si'].peak.asym_alpha_0.free = True\n", + "project_1.experiments['sim_si'].peak.asym_alpha_1.free = True" + ] + }, + { + "cell_type": "markdown", + "id": "65", + "metadata": {}, + "source": [ + "#### Show Free Parameters\n", + "\n", + "We can check which parameters are free to be refined by calling the\n", + "`show_free_params` method of the `analysis` object of the project." + ] + }, + { + "cell_type": "markdown", + "id": "66", + "metadata": { + "tags": [ + "doc-link" + ] + }, + "source": [ + "📖 See\n", + "[documentation](https://easyscience.github.io/diffraction-lib/user-guide/first-steps/#available-parameters)\n", + "for more details on how to\n", + "- show all parameters of the project,\n", + "- show all fittable parameters, and\n", + "- show only free parameters of the project." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "67", + "metadata": {}, + "outputs": [], + "source": [ + "project_1.analysis.show_free_params()" + ] + }, + { + "cell_type": "markdown", + "id": "68", + "metadata": {}, + "source": [ + "#### Visualize Diffraction Patterns\n", + "\n", + "Before performing the fit, we can visually compare the measured\n", + "diffraction pattern with the calculated diffraction pattern based on\n", + "the initial parameters of the structure and the instrument. This\n", + "provides an indication of how well the initial parameters match the\n", + "measured data. The `plot_meas_vs_calc` method of the project allows\n", + "this comparison." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "69", + "metadata": {}, + "outputs": [], + "source": [ + "project_1.plot_meas_vs_calc(expt_name='sim_si')" + ] + }, + { + "cell_type": "markdown", + "id": "70", + "metadata": {}, + "source": [ + "#### Run Fitting\n", + "\n", + "We can now perform the fit using the `fit` method of the `analysis`\n", + "object of the project." + ] + }, + { + "cell_type": "markdown", + "id": "71", + "metadata": { + "tags": [ + "doc-link" + ] + }, + "source": [ + "📖 See\n", + "[documentation](https://docs.easydiffraction.org/lib/user-guide/analysis-workflow/analysis/#perform-fit)\n", + "for more details about the fitting process." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "72", + "metadata": {}, + "outputs": [], + "source": [ + "project_1.analysis.fit()\n", + "project_1.analysis.show_fit_results()" + ] + }, + { + "cell_type": "markdown", + "id": "73", + "metadata": {}, + "source": [ + "#### Check Fit Results\n", + "\n", + "You can see that the agreement between the measured and calculated\n", + "diffraction patterns is now much improved and that the intensities of\n", + "the calculated peaks align much better with the measured peaks. To\n", + "check the quality of the fit numerically, we can look at the\n", + "goodness-of-fit χ² value and the reliability R-factors. The χ² value\n", + "is a measure of how well the calculated diffraction pattern matches\n", + "the measured pattern, and it is calculated as the sum of the squared\n", + "differences between the measured and calculated intensities, divided\n", + "by the number of data points. Ideally, the χ² value should be close to\n", + "1, indicating a good fit." + ] + }, + { + "cell_type": "markdown", + "id": "74", + "metadata": {}, + "source": [ + "#### Visualize Fit Results\n", + "\n", + "After the fit is completed, we can plot the comparison between the\n", + "measured and calculated diffraction patterns again to see how well the\n", + "fit improved the agreement between the two. The calculated diffraction\n", + "pattern is now based on the refined parameters." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "75", + "metadata": {}, + "outputs": [], + "source": [ + "project_1.plot_meas_vs_calc(expt_name='sim_si')" + ] + }, + { + "cell_type": "markdown", + "id": "76", + "metadata": {}, + "source": [ + "#### TOF vs d-spacing\n", + "\n", + "The diffraction pattern is typically analyzed and plotted in the\n", + "time-of-flight (TOF) axis, which represents the time it takes for\n", + "neutrons to travel from the sample to the detector. However, it is\n", + "sometimes more convenient to visualize the diffraction pattern in the\n", + "d-spacing axis, which represents the distance between planes in the\n", + "crystal lattice.\n", + "\n", + "The conversion from d-spacing to TOF was already introduced in the\n", + "data reduction notebook. As a reminder, the two are related through\n", + "the instrument calibration parameters according to the equation:\n", + "\n", + "$$ \\text{TOF} = \\text{offset} + \\text{linear} \\cdot d + \\text{quad}\n", + "\\cdot d^{2}, $$\n", + "\n", + "where `offset`, `linear`, and `quad` are calibration parameters.\n", + "\n", + "In our case, only the `linear` term is used (the\n", + "`calib_d_to_tof_linear` parameter we set earlier). The `offset` and\n", + "`quad` terms were not part of the data reduction and are therefore set\n", + "to 0 by default.\n", + "\n", + "The `plot_meas_vs_calc` method of the project allows us to plot the\n", + "measured and calculated diffraction patterns in the d-spacing axis by\n", + "setting the `d_spacing` parameter to `True`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "77", + "metadata": {}, + "outputs": [], + "source": [ + "project_1.plot_meas_vs_calc(expt_name='sim_si', x='d_spacing')" + ] + }, + { + "cell_type": "markdown", + "id": "78", + "metadata": {}, + "source": [ + "As you can see, the calculated diffraction pattern now matches the\n", + "measured pattern much more closely. Typically, additional experimental\n", + "parameters are included in the refinement process to further improve\n", + "the fit. In this example, the structural parameters are not refined\n", + "because the Si crystal structure is a well-known standard reference\n", + "used to calibrate both the instrument and the experimental setup. The\n", + "refined experimental parameters obtained here will then be applied\n", + "when fitting the crystal structures of other materials.\n", + "\n", + "In the next part of the notebook, we will move to a more advanced case\n", + "and fit a more complex crystal structure: La₀.₅Ba₀.₅CoO₃ (LBCO).\n", + "\n", + "#### Save Project\n", + "\n", + "Before moving on, we can save the project to disk for later use. This\n", + "will preserve the entire project structure, including experiments,\n", + "structures, and fitting results. The project is saved into a\n", + "directory specified by the `dir_path` attribute of the project object." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "79", + "metadata": {}, + "outputs": [], + "source": [ + "project_1.save_as(dir_path='powder_diffraction_Si')" + ] + }, + { + "cell_type": "markdown", + "id": "80", + "metadata": {}, + "source": [ + "## 💪 Exercise: Complex Fit – LBCO\n", + "\n", + "Now that you have a basic understanding of the fitting process, we\n", + "will undertake a more complex fit of the La₀.₅Ba₀.₅CoO₃ (LBCO) crystal\n", + "structure using simulated powder diffraction data from the data\n", + "reduction notebook.\n", + "\n", + "You can use the same approach as in the previous part of the notebook,\n", + "but this time we will refine a more complex crystal structure LBCO\n", + "with multiple atoms in the unit cell.\n", + "\n", + "### 📦 Exercise 1: Create a Project\n", + "\n", + "Create a new project for the LBCO fit." + ] + }, + { + "cell_type": "markdown", + "id": "81", + "metadata": {}, + "source": [ + "**Hint:**" + ] + }, + { + "cell_type": "markdown", + "id": "82", + "metadata": { + "tags": [ + "dmsc-school-hint" + ] + }, + "source": [ + "You can use the same approach as in the previous part of the notebook,\n", + "but this time we will create a new project for the LBCO fit." + ] + }, + { + "cell_type": "markdown", + "id": "83", + "metadata": {}, + "source": [ + "**Solution:**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "84", + "metadata": { + "tags": [ + "solution", + "hide-input" + ] + }, + "outputs": [], + "source": [ + "project_2 = ed.Project(name='main')\n", + "project_2.info.title = 'La0.5Ba0.5CoO3 Fit'\n", + "project_2.info.description = 'Fitting simulated powder diffraction pattern of La0.5Ba0.5CoO3.'" + ] + }, + { + "cell_type": "markdown", + "id": "85", + "metadata": {}, + "source": [ + "### 🔬 Exercise 2: Define an Experiment\n", + "\n", + "#### Exercise 2.1: Create an Experiment\n", + "\n", + "Create an experiment within the new project and load the reduced\n", + "diffraction pattern for LBCO." + ] + }, + { + "cell_type": "markdown", + "id": "86", + "metadata": {}, + "source": [ + "**Hint:**" + ] + }, + { + "cell_type": "markdown", + "id": "87", + "metadata": { + "tags": [ + "dmsc-school-hint" + ] + }, + "source": [ + "You can use the same approach as in the previous part of the notebook,\n", + "but this time you need to use the data file for LBCO." + ] + }, + { + "cell_type": "markdown", + "id": "88", + "metadata": {}, + "source": [ + "**Solution:**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "89", + "metadata": { + "tags": [ + "solution", + "hide-input" + ] + }, + "outputs": [], + "source": [ + "data_dir = 'data'\n", + "file_name = 'reduced_LBCO.xye'\n", + "lbco_xye_path = f'{data_dir}/{file_name}'\n", + "\n", + "# Uncomment the following line if your data reduction failed and the\n", + "# reduced data file is missing.\n", + "lbco_xye_path = ed.download_data(id=18, destination=data_dir)\n", + "\n", + "project_2.experiments.add_from_data_path(\n", + " name='sim_lbco',\n", + " data_path=lbco_xye_path,\n", + " sample_form='powder',\n", + " beam_mode='time-of-flight',\n", + " radiation_probe='neutron',\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "90", + "metadata": {}, + "source": [ + "#### Exercise 2.1: Inspect Measured Data\n", + "\n", + "Check the measured data of the LBCO experiment. Are there any peaks\n", + "with the shape similar to those excluded in the Si fit? If so, exclude\n", + "them from this analysis as well." + ] + }, + { + "cell_type": "markdown", + "id": "91", + "metadata": {}, + "source": [ + "**Hint:**" + ] + }, + { + "cell_type": "markdown", + "id": "92", + "metadata": { + "tags": [ + "dmsc-school-hint" + ] + }, + "source": [ + "You can use the `plot_meas` method of the project to visualize the\n", + "measured diffraction pattern. You can also use the `excluded_regions`\n", + "attribute of the experiment to exclude specific regions from the\n", + "analysis as we did in the previous part of the notebook." + ] + }, + { + "cell_type": "markdown", + "id": "93", + "metadata": {}, + "source": [ + "**Solution:**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "94", + "metadata": { + "tags": [ + "solution", + "hide-input" + ] + }, + "outputs": [], + "source": [ + "project_2.plot_meas(expt_name='sim_lbco')\n", + "\n", + "project_2.experiments['sim_lbco'].excluded_regions.create(id='1', start=0, end=55000)\n", + "project_2.experiments['sim_lbco'].excluded_regions.create(id='2', start=105500, end=200000)\n", + "\n", + "project_2.plot_meas(expt_name='sim_lbco')" + ] + }, + { + "cell_type": "markdown", + "id": "95", + "metadata": {}, + "source": [ + "#### Exercise 2.2: Set Instrument Parameters\n", + "\n", + "Set the instrument parameters for the LBCO experiment." + ] + }, + { + "cell_type": "markdown", + "id": "96", + "metadata": {}, + "source": [ + "**Hint:**" + ] + }, + { + "cell_type": "markdown", + "id": "97", + "metadata": { + "tags": [ + "dmsc-school-hint" + ] + }, + "source": [ + "Use the values from the data reduction process for the LBCO and\n", + "follow the same approach as in the previous part of the notebook." + ] + }, + { + "cell_type": "markdown", + "id": "98", + "metadata": {}, + "source": [ + "**Solution:**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "99", + "metadata": { + "tags": [ + "solution", + "hide-input" + ] + }, + "outputs": [], + "source": [ + "project_2.experiments['sim_lbco'].instrument.setup_twotheta_bank = ed.extract_metadata(\n", + " lbco_xye_path, r'two_theta\\s*=\\s*([-+]?\\d*\\.?\\d+(?:[eE][-+]?\\d+)?)'\n", + ")\n", + "project_2.experiments['sim_lbco'].instrument.calib_d_to_tof_linear = ed.extract_metadata(\n", + " lbco_xye_path, r'DIFC\\s*=\\s*([-+]?\\d*\\.?\\d+(?:[eE][-+]?\\d+)?)'\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "100", + "metadata": {}, + "source": [ + "#### Exercise 2.3: Set Peak Profile Parameters\n", + "\n", + "Set the peak profile parameters for the LBCO experiment." + ] + }, + { + "cell_type": "markdown", + "id": "101", + "metadata": {}, + "source": [ + "**Hint:**" + ] + }, + { + "cell_type": "markdown", + "id": "102", + "metadata": { + "tags": [ + "dmsc-school-hint" + ] + }, + "source": [ + "Use the values from the\n", + "previous part of the notebook. You can either manually copy the values\n", + "from the Si fit or use the `value` attribute of the parameters from\n", + "the Si experiment to set the initial values for the LBCO experiment.\n", + "This will help us to have a good starting point for the fit." + ] + }, + { + "cell_type": "markdown", + "id": "103", + "metadata": {}, + "source": [ + "**Solution:**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "104", + "metadata": { + "tags": [ + "solution", + "hide-input" + ] + }, + "outputs": [], + "source": [ + "# # Create a reference to the peak profile parameters from the Si\n", + "sim_si_peak = project_1.experiments['sim_si'].peak\n", + "project_2.experiments['sim_lbco'].peak_profile_type = 'pseudo-voigt * ikeda-carpenter'\n", + "project_2.experiments['sim_lbco'].peak.broad_gauss_sigma_0 = sim_si_peak.broad_gauss_sigma_0.value\n", + "project_2.experiments['sim_lbco'].peak.broad_gauss_sigma_1 = sim_si_peak.broad_gauss_sigma_1.value\n", + "project_2.experiments['sim_lbco'].peak.broad_gauss_sigma_2 = sim_si_peak.broad_gauss_sigma_2.value\n", + "project_2.experiments['sim_lbco'].peak.broad_mix_beta_0 = sim_si_peak.broad_mix_beta_0.value\n", + "project_2.experiments['sim_lbco'].peak.broad_mix_beta_1 = sim_si_peak.broad_mix_beta_1.value\n", + "project_2.experiments['sim_lbco'].peak.asym_alpha_0 = sim_si_peak.asym_alpha_0.value\n", + "project_2.experiments['sim_lbco'].peak.asym_alpha_1 = sim_si_peak.asym_alpha_1.value" + ] + }, + { + "cell_type": "markdown", + "id": "105", + "metadata": {}, + "source": [ + "#### Exercise 2.4: Set Background\n", + "\n", + "Set the background points for the LBCO experiment. What would you\n", + "suggest as the initial intensity value for the background points?" + ] + }, + { + "cell_type": "markdown", + "id": "106", + "metadata": {}, + "source": [ + "**Hint:**" + ] + }, + { + "cell_type": "markdown", + "id": "107", + "metadata": { + "tags": [ + "dmsc-school-hint" + ] + }, + "source": [ + "Use the same approach as in the previous part of the notebook, but\n", + "this time you need to set the background points for the LBCO\n", + "experiment. You can zoom in on the measured diffraction pattern to\n", + "determine the approximate background level." + ] + }, + { + "cell_type": "markdown", + "id": "108", + "metadata": {}, + "source": [ + "**Solution:**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "109", + "metadata": { + "tags": [ + "solution", + "hide-input" + ] + }, + "outputs": [], + "source": [ + "project_2.experiments['sim_lbco'].background_type = 'line-segment'\n", + "project_2.experiments['sim_lbco'].background.create(id='1', x=50000, y=0.2)\n", + "project_2.experiments['sim_lbco'].background.create(id='2', x=60000, y=0.2)\n", + "project_2.experiments['sim_lbco'].background.create(id='3', x=70000, y=0.2)\n", + "project_2.experiments['sim_lbco'].background.create(id='4', x=80000, y=0.2)\n", + "project_2.experiments['sim_lbco'].background.create(id='5', x=90000, y=0.2)\n", + "project_2.experiments['sim_lbco'].background.create(id='6', x=100000, y=0.2)\n", + "project_2.experiments['sim_lbco'].background.create(id='7', x=110000, y=0.2)" + ] + }, + { + "cell_type": "markdown", + "id": "110", + "metadata": {}, + "source": [ + "### 🧩 Exercise 3: Define a Structure – LBCO\n", + "\n", + "The LBSO structure is not as simple as the Si one, as it contains\n", + "multiple atoms in the unit cell. It is not in COD, so we give you the\n", + "structural parameters in CIF format to create the structure.\n", + "\n", + "Note that those parameters are not necessarily the most accurate ones,\n", + "but they are a good starting point for the fit. The aim of the study\n", + "is to refine the LBCO lattice parameters." + ] + }, + { + "cell_type": "markdown", + "id": "111", + "metadata": {}, + "source": [ + "```\n", + "data_lbco\n", + "\n", + "_space_group.name_H-M_alt \"P m -3 m\"\n", + "_space_group.IT_coordinate_system_code 1\n", + "\n", + "_cell.length_a 3.89\n", + "_cell.length_b 3.89\n", + "_cell.length_c 3.89\n", + "_cell.angle_alpha 90.0\n", + "_cell.angle_beta 90.0\n", + "_cell.angle_gamma 90.0\n", + "\n", + "loop_\n", + "_atom_site.label\n", + "_atom_site.type_symbol\n", + "_atom_site.fract_x\n", + "_atom_site.fract_y\n", + "_atom_site.fract_z\n", + "_atom_site.wyckoff_letter\n", + "_atom_site.occupancy\n", + "_atom_site.ADP_type\n", + "_atom_site.B_iso_or_equiv\n", + "La La 0.0 0.0 0.0 a 0.5 Biso 0.95\n", + "Ba Ba 0.0 0.0 0.0 a 0.5 Biso 0.95\n", + "Co Co 0.5 0.5 0.5 b 1.0 Biso 0.80\n", + "O O 0.0 0.5 0.5 c 1.0 Biso 1.66\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "112", + "metadata": {}, + "source": [ + "Note that the `occupancy` of the La and Ba atoms is 0.5\n", + "and those atoms are located in the same position (0, 0, 0) in the unit\n", + "cell. This means that an extra attribute `occupancy` needs to be set\n", + "for those atoms later in the structure.\n", + "\n", + "We model the La/Ba site using the virtual crystal approximation. In\n", + "this approach, the scattering is taken as a weighted average of La and\n", + "Ba. This reproduces the average diffraction pattern well but does not\n", + "capture certain real-world effects.\n", + "\n", + "The edge cases are:\n", + "- **Random distribution**. La and Ba atoms are placed randomly. The\n", + " Bragg peaks still match the average structure, but the pattern also\n", + " shows extra background (diffuse scattering) between the peaks, but\n", + " this is usually neglected in the analysis.\n", + "- **Perfect ordering**. La and Ba arrange themselves in a regular\n", + " pattern, creating a larger repeating unit. This gives rise to extra\n", + " peaks (\"superlattice reflections\") and changes the intensity of\n", + " some existing peaks.\n", + "- **Virtual crystal approximation (our model)**. We replace the site\n", + " with a single \"virtual atom\" that averages La and Ba. This gives\n", + " the correct average Bragg peaks but leaves out the extra background\n", + " of the random case and the extra peaks of the ordered case." + ] + }, + { + "cell_type": "markdown", + "id": "113", + "metadata": {}, + "source": [ + "#### Exercise 3.1: Create Structure\n", + "\n", + "Add a structure for LBCO to the project. The structure\n", + "parameters will be set in the next exercises." + ] + }, + { + "cell_type": "markdown", + "id": "114", + "metadata": {}, + "source": [ + "**Hint:**" + ] + }, + { + "cell_type": "markdown", + "id": "115", + "metadata": { + "tags": [ + "dmsc-school-hint" + ] + }, + "source": [ + "You can use the same approach as in the previous part of the notebook,\n", + "but this time you need to use the name corresponding to the LBCO\n", + "structure, e.g. 'lbco'." + ] + }, + { + "cell_type": "markdown", + "id": "116", + "metadata": {}, + "source": [ + "**Solution:**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "117", + "metadata": { + "tags": [ + "solution", + "hide-input" + ] + }, + "outputs": [], + "source": [ + "project_2.structures.create(name='lbco')" + ] + }, + { + "cell_type": "markdown", + "id": "118", + "metadata": {}, + "source": [ + "#### Exercise 3.2: Set Space Group\n", + "\n", + "Set the space group for the LBCO structure." + ] + }, + { + "cell_type": "markdown", + "id": "119", + "metadata": {}, + "source": [ + "**Hint:**" + ] + }, + { + "cell_type": "markdown", + "id": "120", + "metadata": { + "tags": [ + "dmsc-school-hint" + ] + }, + "source": [ + "Use the space group name and IT coordinate system code from the CIF\n", + "data." + ] + }, + { + "cell_type": "markdown", + "id": "121", + "metadata": {}, + "source": [ + "**Solution:**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "122", + "metadata": { + "tags": [ + "solution", + "hide-input" + ] + }, + "outputs": [], + "source": [ + "project_2.structures['lbco'].space_group.name_h_m = 'P m -3 m'\n", + "project_2.structures['lbco'].space_group.it_coordinate_system_code = '1'" + ] + }, + { + "cell_type": "markdown", + "id": "123", + "metadata": {}, + "source": [ + "#### Exercise 3.3: Set Lattice Parameters\n", + "\n", + "Set the lattice parameters for the LBCO structure." + ] + }, + { + "cell_type": "markdown", + "id": "124", + "metadata": {}, + "source": [ + "**Hint:**" + ] + }, + { + "cell_type": "markdown", + "id": "125", + "metadata": { + "tags": [ + "dmsc-school-hint" + ] + }, + "source": [ + "Use the lattice parameters from the CIF data." + ] + }, + { + "cell_type": "markdown", + "id": "126", + "metadata": {}, + "source": [ + "**Solution:**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "127", + "metadata": { + "tags": [ + "solution", + "hide-input" + ] + }, + "outputs": [], + "source": [ + "project_2.structures['lbco'].cell.length_a = 3.88" + ] + }, + { + "cell_type": "markdown", + "id": "128", + "metadata": {}, + "source": [ + "#### Exercise 3.4: Set Atom Sites\n", + "\n", + "Set the atom sites for the LBCO structure." + ] + }, + { + "cell_type": "markdown", + "id": "129", + "metadata": {}, + "source": [ + "**Hint:**" + ] + }, + { + "cell_type": "markdown", + "id": "130", + "metadata": { + "tags": [ + "dmsc-school-hint" + ] + }, + "source": [ + "Use the atom sites from the CIF data. You can use the `add` method of\n", + "the `atom_sites` attribute of the structure to add the atom sites." + ] + }, + { + "cell_type": "markdown", + "id": "131", + "metadata": {}, + "source": [ + "**Solution:**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "132", + "metadata": { + "tags": [ + "solution", + "hide-input" + ] + }, + "outputs": [], + "source": [ + "project_2.structures['lbco'].atom_sites.create(\n", + " label='La',\n", + " type_symbol='La',\n", + " fract_x=0,\n", + " fract_y=0,\n", + " fract_z=0,\n", + " wyckoff_letter='a',\n", + " b_iso=0.95,\n", + " occupancy=0.5,\n", + ")\n", + "project_2.structures['lbco'].atom_sites.create(\n", + " label='Ba',\n", + " type_symbol='Ba',\n", + " fract_x=0,\n", + " fract_y=0,\n", + " fract_z=0,\n", + " wyckoff_letter='a',\n", + " b_iso=0.95,\n", + " occupancy=0.5,\n", + ")\n", + "project_2.structures['lbco'].atom_sites.create(\n", + " label='Co',\n", + " type_symbol='Co',\n", + " fract_x=0.5,\n", + " fract_y=0.5,\n", + " fract_z=0.5,\n", + " wyckoff_letter='b',\n", + " b_iso=0.80,\n", + ")\n", + "project_2.structures['lbco'].atom_sites.create(\n", + " label='O',\n", + " type_symbol='O',\n", + " fract_x=0,\n", + " fract_y=0.5,\n", + " fract_z=0.5,\n", + " wyckoff_letter='c',\n", + " b_iso=1.66,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "133", + "metadata": {}, + "source": [ + "### 🔗 Exercise 4: Assign Structure to Experiment\n", + "\n", + "Now assign the LBCO structure to the experiment created above." + ] + }, + { + "cell_type": "markdown", + "id": "134", + "metadata": {}, + "source": [ + "**Hint:**" + ] + }, + { + "cell_type": "markdown", + "id": "135", + "metadata": { + "tags": [ + "dmsc-school-hint" + ] + }, + "source": [ + "Use the `linked_phases` attribute of the experiment to link the\n", + "crystal structure." + ] + }, + { + "cell_type": "markdown", + "id": "136", + "metadata": {}, + "source": [ + "**Solution:**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "137", + "metadata": { + "tags": [ + "solution", + "hide-input" + ] + }, + "outputs": [], + "source": [ + "project_2.experiments['sim_lbco'].linked_phases.create(id='lbco', scale=1.0)" + ] + }, + { + "cell_type": "markdown", + "id": "138", + "metadata": {}, + "source": [ + "### 🚀 Exercise 5: Analyze and Fit the Data\n", + "\n", + "#### Exercise 5.1: Set Fit Parameters\n", + "\n", + "Select the initial set of parameters to be refined during the fitting\n", + "process." + ] + }, + { + "cell_type": "markdown", + "id": "139", + "metadata": {}, + "source": [ + "**Hint:**" + ] + }, + { + "cell_type": "markdown", + "id": "140", + "metadata": { + "tags": [ + "dmsc-school-hint" + ] + }, + "source": [ + "You can start with the scale factor and the background points, as in\n", + "the Si fit." + ] + }, + { + "cell_type": "markdown", + "id": "141", + "metadata": {}, + "source": [ + "**Solution:**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "142", + "metadata": { + "tags": [ + "solution", + "hide-input" + ] + }, + "outputs": [], + "source": [ + "project_2.experiments['sim_lbco'].linked_phases['lbco'].scale.free = True\n", + "\n", + "for line_segment in project_2.experiments['sim_lbco'].background:\n", + " line_segment.y.free = True" + ] + }, + { + "cell_type": "markdown", + "id": "143", + "metadata": {}, + "source": [ + "#### Exercise 5.2: Run Fitting\n", + "\n", + "Visualize the measured and calculated diffraction patterns before\n", + "fitting and then run the fitting process." + ] + }, + { + "cell_type": "markdown", + "id": "144", + "metadata": {}, + "source": [ + "**Hint:**" + ] + }, + { + "cell_type": "markdown", + "id": "145", + "metadata": { + "tags": [ + "dmsc-school-hint" + ] + }, + "source": [ + "Use the `plot_meas_vs_calc` method of the project to visualize the\n", + "measured and calculated diffraction patterns before fitting. Then, use\n", + "the `fit` method of the `analysis` object of the project to perform\n", + "the fitting process." + ] + }, + { + "cell_type": "markdown", + "id": "146", + "metadata": {}, + "source": [ + "**Solution:**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "147", + "metadata": { + "tags": [ + "solution", + "hide-input" + ] + }, + "outputs": [], + "source": [ + "project_2.plot_meas_vs_calc(expt_name='sim_lbco')\n", + "\n", + "project_2.analysis.fit()\n", + "project_2.analysis.show_fit_results()" + ] + }, + { + "cell_type": "markdown", + "id": "148", + "metadata": {}, + "source": [ + "#### Exercise 5.3: Find the Misfit in the Fit\n", + "\n", + "Visualize the measured and calculated diffraction patterns after the\n", + "fit. As you can see, the fit shows noticeable discrepancies. If you\n", + "zoom in on different regions of the pattern, you will observe that all\n", + "the calculated peaks are shifted to the left.\n", + "\n", + "What could be the reason for the misfit?" + ] + }, + { + "cell_type": "markdown", + "id": "149", + "metadata": {}, + "source": [ + "**Hint:**" + ] + }, + { + "cell_type": "markdown", + "id": "150", + "metadata": { + "tags": [ + "dmsc-school-hint" + ] + }, + "source": [ + "Consider the following options:\n", + "1. The conversion parameters from TOF to d-spacing are not correct.\n", + "2. The lattice parameters of the LBCO phase are not correct.\n", + "3. The peak profile parameters are not correct.\n", + "4. The background points are not correct." + ] + }, + { + "cell_type": "markdown", + "id": "151", + "metadata": {}, + "source": [ + "**Solution:**" + ] + }, + { + "cell_type": "markdown", + "id": "152", + "metadata": { + "tags": [ + "dmsc-school-hint" + ] + }, + "source": [ + "\n", + "1. ❌ The conversion parameters from TOF to d-spacing were set based\n", + "on the data reduction step. While they are specific to each dataset\n", + "and thus differ from those used for the Si data, the full reduction\n", + "workflow has already been validated with the Si fit. Therefore, they\n", + "are not the cause of the misfit in this case.\n", + "2. ✅ The lattice parameters of the LBCO phase were set based on the\n", + "CIF data, which is a good starting point, but they are not necessarily\n", + "as accurate as needed for the fit. The lattice parameters may need to\n", + "be refined.\n", + "3. ❌ The peak profile parameters do not change the position of the\n", + "peaks, but rather their shape.\n", + "4. ❌ The background points affect the background level, but not the\n", + "peak positions." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "153", + "metadata": { + "tags": [ + "solution", + "hide-input" + ] + }, + "outputs": [], + "source": [ + "project_2.plot_meas_vs_calc(expt_name='sim_lbco')" + ] + }, + { + "cell_type": "markdown", + "id": "154", + "metadata": {}, + "source": [ + "#### Exercise 5.4: Refine the LBCO Lattice Parameter\n", + "\n", + "To improve the fit, refine the lattice parameters of the LBCO phase." + ] + }, + { + "cell_type": "markdown", + "id": "155", + "metadata": {}, + "source": [ + "**Hint:**" + ] + }, + { + "cell_type": "markdown", + "id": "156", + "metadata": { + "tags": [ + "dmsc-school-hint" + ] + }, + "source": [ + "To achieve this, we will set the `free` attribute of the `length_a`\n", + "parameter of the LBCO cell to `True`.\n", + "\n", + "LBCO has a cubic crystal structure (space group `P m -3 m`), which\n", + "means that `length_b` and `length_c` are constrained to be equal to\n", + "`length_a`. Therefore, only `length_a` needs to be refined; the other\n", + "two will be updated automatically. All cell angles are fixed at 90°,\n", + "so they do not require refinement." + ] + }, + { + "cell_type": "markdown", + "id": "157", + "metadata": {}, + "source": [ + "**Solution:**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "158", + "metadata": { + "tags": [ + "solution", + "hide-input" + ] + }, + "outputs": [], + "source": [ + "project_2.structures['lbco'].cell.length_a.free = True\n", + "\n", + "project_2.analysis.fit()\n", + "project_2.analysis.show_fit_results()\n", + "\n", + "project_2.plot_meas_vs_calc(expt_name='sim_lbco')" + ] + }, + { + "cell_type": "markdown", + "id": "159", + "metadata": {}, + "source": [ + "One of the main goals of this study was to refine the lattice\n", + "parameter of the LBCO phase. As shown in the updated fit results, the\n", + "overall fit has improved significantly, even though the change in cell\n", + "length is less than 1% of the initial value. This demonstrates how\n", + "even a small adjustment to the lattice parameter can have a\n", + "substantial impact on the quality of the fit." + ] + }, + { + "cell_type": "markdown", + "id": "160", + "metadata": {}, + "source": [ + "#### Exercise 5.5: Visualize the Fit Results in d-spacing\n", + "\n", + "Plot measured vs calculated diffraction patterns in d-spacing instead\n", + "of TOF." + ] + }, + { + "cell_type": "markdown", + "id": "161", + "metadata": {}, + "source": [ + "**Hint:**" + ] + }, + { + "cell_type": "markdown", + "id": "162", + "metadata": { + "tags": [ + "dmsc-school-hint" + ] + }, + "source": [ + "Use the `plot_meas_vs_calc` method of the project and set the\n", + "`d_spacing` parameter to `True`." + ] + }, + { + "cell_type": "markdown", + "id": "163", + "metadata": {}, + "source": [ + "**Solution:**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "164", + "metadata": { + "tags": [ + "solution", + "hide-input" + ] + }, + "outputs": [], + "source": [ + "project_2.plot_meas_vs_calc(expt_name='sim_lbco', x='d_spacing')" + ] + }, + { + "cell_type": "markdown", + "id": "165", + "metadata": {}, + "source": [ + "#### Exercise 5.6: Refine the Peak Profile Parameters\n", + "\n", + "As you can see, the fit is now relatively good and the peak positions\n", + "are much closer to the measured data.\n", + "\n", + "The peak profile parameters were not refined, and their starting\n", + "values were set based on the previous fit of the Si standard sample.\n", + "Although these starting values are reasonable and provide a good\n", + "starting point for the fit, they are not necessarily optimal for the\n", + "LBCO phase. This can be seen while inspecting the individual peaks in\n", + "the diffraction pattern. For example, the calculated curve does not\n", + "perfectly describe the peak at about 1.38 Å, as can be seen below:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "166", + "metadata": {}, + "outputs": [], + "source": [ + "project_2.plot_meas_vs_calc(expt_name='sim_lbco', x='d_spacing', x_min=1.35, x_max=1.40)" + ] + }, + { + "cell_type": "markdown", + "id": "167", + "metadata": {}, + "source": [ + "The peak profile parameters are determined based on both the\n", + "instrument and the sample characteristics, so they can vary when\n", + "analyzing different samples on the same instrument. Therefore, it is\n", + "better to refine them as well.\n", + "\n", + "Select the peak profile parameters to be refined during the fitting\n", + "process." + ] + }, + { + "cell_type": "markdown", + "id": "168", + "metadata": {}, + "source": [ + "**Hint:**" + ] + }, + { + "cell_type": "markdown", + "id": "169", + "metadata": { + "tags": [ + "dmsc-school-hint" + ] + }, + "source": [ + "You can set the `free` attribute of the peak profile parameters to\n", + "`True` to allow the fitting process to adjust them. You can use the\n", + "same approach as in the previous part of the notebook, but this time\n", + "you will refine the peak profile parameters of the LBCO phase." + ] + }, + { + "cell_type": "markdown", + "id": "170", + "metadata": {}, + "source": [ + "**Solution:**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "171", + "metadata": { + "tags": [ + "solution", + "hide-input" + ] + }, + "outputs": [], + "source": [ + "project_2.experiments['sim_lbco'].peak.broad_gauss_sigma_0.free = True\n", + "project_2.experiments['sim_lbco'].peak.broad_gauss_sigma_1.free = True\n", + "project_2.experiments['sim_lbco'].peak.broad_gauss_sigma_2.free = True\n", + "project_2.experiments['sim_lbco'].peak.broad_mix_beta_0.free = True\n", + "project_2.experiments['sim_lbco'].peak.broad_mix_beta_1.free = True\n", + "project_2.experiments['sim_lbco'].peak.asym_alpha_0.free = True\n", + "project_2.experiments['sim_lbco'].peak.asym_alpha_1.free = True\n", + "\n", + "project_2.analysis.fit()\n", + "project_2.analysis.show_fit_results()\n", + "\n", + "project_2.plot_meas_vs_calc(expt_name='sim_lbco', x='d_spacing', x_min=1.35, x_max=1.40)" + ] + }, + { + "cell_type": "markdown", + "id": "172", + "metadata": {}, + "source": [ + "#### Exercise 5.7: Find Undefined Features\n", + "\n", + "After refining the lattice parameter and the peak profile parameters,\n", + "the fit is significantly improved, but inspect the diffraction pattern\n", + "again. Are you noticing anything undefined?" + ] + }, + { + "cell_type": "markdown", + "id": "173", + "metadata": {}, + "source": [ + "**Hint:**" + ] + }, + { + "cell_type": "markdown", + "id": "174", + "metadata": { + "tags": [ + "dmsc-school-hint" + ] + }, + "source": [ + "While the fit is now significantly better, there are still some\n", + "unexplained peaks in the diffraction pattern. These peaks are not\n", + "accounted for by the LBCO phase. For example, if you zoom in on the\n", + "region around 1.6 Å (or 95,000 μs), you will notice that the rightmost\n", + "peak is not explained by the LBCO phase at all." + ] + }, + { + "cell_type": "markdown", + "id": "175", + "metadata": {}, + "source": [ + "**Solution:**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "176", + "metadata": { + "tags": [ + "solution", + "hide-input" + ] + }, + "outputs": [], + "source": [ + "project_2.plot_meas_vs_calc(expt_name='sim_lbco', x='d_spacing', x_min=1.53, x_max=1.7)" + ] + }, + { + "cell_type": "markdown", + "id": "177", + "metadata": {}, + "source": [ + "#### Exercise 5.8: Identify the Cause of the Unexplained Peaks\n", + "\n", + "Analyze the residual peaks that remain after refining the LBCO phase\n", + "and the peak-profile parameters. Based on their positions and\n", + "characteristics, decide which potential cause best explains the\n", + "misfit." + ] + }, + { + "cell_type": "markdown", + "id": "178", + "metadata": {}, + "source": [ + "**Hint:**" + ] + }, + { + "cell_type": "markdown", + "id": "179", + "metadata": { + "tags": [ + "dmsc-school-hint" + ] + }, + "source": [ + "Consider the following options:\n", + "1. The LBCO phase is not correctly modeled.\n", + "2. The LBCO phase is not the only phase present in the sample.\n", + "3. The data reduction process introduced artifacts.\n", + "4. The studied sample is not LBCO, but rather a different phase." + ] + }, + { + "cell_type": "markdown", + "id": "180", + "metadata": {}, + "source": [ + "**Solution:**" + ] + }, + { + "cell_type": "markdown", + "id": "181", + "metadata": { + "tags": [ + "dmsc-school-hint" + ] + }, + "source": [ + "1. ❌ In principle, this could be the case, as sometimes the presence\n", + "of extra peaks in the diffraction pattern can indicate lower symmetry\n", + "than the one used in the model, or that the model is not complete.\n", + "However, in this case, the LBCO phase is correctly modeled based on\n", + "the CIF data.\n", + "2. ✅ The unexplained peaks are due to the presence of an impurity\n", + "phase in the sample, which is not included in the current model.\n", + "3. ❌ The data reduction process is not likely to introduce such\n", + "specific peaks, as it is tested and verified in the previous part of\n", + "the notebook.\n", + "4. ❌ This could also be the case in real experiments, but in this\n", + "case, we know that the sample is LBCO, as it was simulated based on\n", + "the CIF data." + ] + }, + { + "cell_type": "markdown", + "id": "182", + "metadata": {}, + "source": [ + "#### Exercise 5.9: Identify the impurity phase\n", + "\n", + "Use the positions of the unexplained peaks to identify the most likely\n", + "secondary phase present in the sample." + ] + }, + { + "cell_type": "markdown", + "id": "183", + "metadata": {}, + "source": [ + "**Hint:**" + ] + }, + { + "cell_type": "markdown", + "id": "184", + "metadata": { + "tags": [ + "dmsc-school-hint" + ] + }, + "source": [ + "Check the positions of the unexplained peaks in the diffraction\n", + "pattern. Compare them with the known diffraction patterns in the\n", + "previous part of the notebook." + ] + }, + { + "cell_type": "markdown", + "id": "185", + "metadata": {}, + "source": [ + "**Solution:**" + ] + }, + { + "cell_type": "markdown", + "id": "186", + "metadata": { + "tags": [ + "dmsc-school-hint" + ] + }, + "source": [ + "The unexplained peaks are likely due to the presence of a small amount\n", + "of Si in the LBCO sample. In real experiments, it might happen, e.g.,\n", + "because the sample holder was not cleaned properly after the Si\n", + "experiment.\n", + "\n", + "You can visalize both the patterns of the Si and LBCO phases to\n", + "confirm this hypothesis." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "187", + "metadata": { + "tags": [ + "solution", + "hide-input" + ] + }, + "outputs": [], + "source": [ + "project_1.plot_meas_vs_calc(expt_name='sim_si', x='d_spacing', x_min=1, x_max=1.7)\n", + "project_2.plot_meas_vs_calc(expt_name='sim_lbco', x='d_spacing', x_min=1, x_max=1.7)" + ] + }, + { + "cell_type": "markdown", + "id": "188", + "metadata": {}, + "source": [ + "#### Exercise 5.10: Create a Second Structure – Si as Impurity\n", + "\n", + "Create a second structure for the Si phase, which is the impurity\n", + "phase identified in the previous step. Link this structure to the\n", + "LBCO experiment." + ] + }, + { + "cell_type": "markdown", + "id": "189", + "metadata": {}, + "source": [ + "**Hint:**" + ] + }, + { + "cell_type": "markdown", + "id": "190", + "metadata": { + "tags": [ + "dmsc-school-hint" + ] + }, + "source": [ + "You can use the same approach as in the previous part of the notebook,\n", + "but this time you need to create a structure for Si and link it to\n", + "the LBCO experiment." + ] + }, + { + "cell_type": "markdown", + "id": "191", + "metadata": {}, + "source": [ + "**Solution:**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "192", + "metadata": { + "tags": [ + "solution", + "hide-input" + ] + }, + "outputs": [], + "source": [ + "# Set Space Group\n", + "project_2.structures.create(name='si')\n", + "project_2.structures['si'].space_group.name_h_m = 'F d -3 m'\n", + "project_2.structures['si'].space_group.it_coordinate_system_code = '2'\n", + "\n", + "# Set Lattice Parameters\n", + "project_2.structures['si'].cell.length_a = 5.43\n", + "\n", + "# Set Atom Sites\n", + "project_2.structures['si'].atom_sites.create(\n", + " label='Si',\n", + " type_symbol='Si',\n", + " fract_x=0,\n", + " fract_y=0,\n", + " fract_z=0,\n", + " wyckoff_letter='a',\n", + " b_iso=0.89,\n", + ")\n", + "\n", + "# Assign Structure to Experiment\n", + "project_2.experiments['sim_lbco'].linked_phases.create(id='si', scale=1.0)" + ] + }, + { + "cell_type": "markdown", + "id": "193", + "metadata": {}, + "source": [ + "#### Exercise 5.11: Refine the Scale of the Si Phase\n", + "\n", + "Visualize the measured diffraction pattern and the calculated\n", + "diffraction pattern. Check if the Si phase is contributing to the\n", + "calculated diffraction pattern. Refine the scale factor of the Si\n", + "phase to improve the fit." + ] + }, + { + "cell_type": "markdown", + "id": "194", + "metadata": {}, + "source": [ + "**Hint:**" + ] + }, + { + "cell_type": "markdown", + "id": "195", + "metadata": { + "tags": [ + "dmsc-school-hint" + ] + }, + "source": [ + "You can use the `plot_meas_vs_calc` method of the project to visualize\n", + "the patterns. Then, set the `free` attribute of the `scale` parameter\n", + "of the Si phase to `True` to allow the fitting process to adjust the\n", + "scale factor." + ] + }, + { + "cell_type": "markdown", + "id": "196", + "metadata": {}, + "source": [ + "**Solution:**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "197", + "metadata": { + "tags": [ + "solution", + "hide-input" + ] + }, + "outputs": [], + "source": [ + "# Before optimizing the parameters, we can visualize the measured\n", + "# diffraction pattern and the calculated diffraction pattern based on\n", + "# the two phases: LBCO and Si.\n", + "project_2.plot_meas_vs_calc(expt_name='sim_lbco')\n", + "\n", + "# As you can see, the calculated pattern is now the sum of both phases,\n", + "# and Si peaks are visible in the calculated pattern. However, their\n", + "# intensities are much too high. Therefore, we need to refine the scale\n", + "# factor of the Si phase.\n", + "project_2.experiments['sim_lbco'].linked_phases['si'].scale.free = True\n", + "\n", + "# Now we can perform the fit with both phases included.\n", + "project_2.analysis.fit()\n", + "project_2.analysis.show_fit_results()\n", + "\n", + "# Let's plot the measured diffraction pattern and the calculated\n", + "# diffraction pattern both for the full range and for a zoomed-in region\n", + "# around the previously unexplained peak near 95,000 μs. The calculated\n", + "# pattern will be the sum of the two phases.\n", + "project_2.plot_meas_vs_calc(expt_name='sim_lbco')\n", + "project_2.plot_meas_vs_calc(expt_name='sim_lbco', x_min=88000, x_max=101000)" + ] + }, + { + "cell_type": "markdown", + "id": "198", + "metadata": {}, + "source": [ + "All previously unexplained peaks are now accounted for in the pattern,\n", + "and the fit is improved. Some discrepancies in the peak intensities\n", + "remain, but further improvements would require more advanced data\n", + "reduction and analysis, which are beyond the scope of this school.\n", + "\n", + "To review the analysis results, you can generate and print a summary\n", + "report using the `show_report()` method, as demonstrated in the cell\n", + "below. The report includes parameters related to the structure and\n", + "the experiment, such as the refined unit cell parameter `a` of LBCO.\n", + "\n", + "Information about the crystal or magnetic structure, along with\n", + "experimental details, fitting quality, and other relevant data, is\n", + "often submitted to crystallographic journals as part of a scientific\n", + "publication. It can also be deposited in crystallographic databases\n", + "when relevant." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "199", + "metadata": {}, + "outputs": [], + "source": [ + "project_2.summary.show_report()" + ] + }, + { + "cell_type": "markdown", + "id": "200", + "metadata": {}, + "source": [ + "Finally, we save the project to disk to preserve the current state of\n", + "the analysis." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "201", + "metadata": {}, + "outputs": [], + "source": [ + "project_2.save_as(dir_path='powder_diffraction_LBCO_Si')" + ] + }, + { + "cell_type": "markdown", + "id": "202", + "metadata": {}, + "source": [ + "#### Final Remarks\n", + "\n", + "In this part of the notebook, you learned how to use EasyDiffraction\n", + "to refine lattice parameters of a more complex crystal structure,\n", + "La₀.₅Ba₀.₅CoO₃ (LBCO).\n", + "\n", + "In real experiments, you might also refine\n", + "additional parameters, such as atomic positions, occupancies, and\n", + "atomic displacement factors, to achieve an even better fit. For our\n", + "purposes, we'll stop here, as the goal was to give you a starting\n", + "point for analyzing more complex crystal structures with\n", + "EasyDiffraction." + ] + }, + { + "cell_type": "markdown", + "id": "203", + "metadata": {}, + "source": [ + "## 🎁 Bonus\n", + "\n", + "Congratulations — you've now completed the diffraction data analysis\n", + "part of the DMSC Summer School!\n", + "\n", + "If you'd like to keep exploring, the EasyDiffraction library offers\n", + "many additional tutorials and examples on the official documentation\n", + "site: 👉 https://docs.easydiffraction.org/lib/tutorials/\n", + "\n", + "Besides the Python package, EasyDiffraction also comes with a\n", + "graphical user interface (GUI) that lets you perform similar analyses\n", + "without writing code. To be fair, it's not *quite* feature-complete\n", + "compared to the Python library yet — but we're working on it! 🚧\n", + "\n", + "If you prefer a point-and-click interface over coding, the GUI\n", + "provides a user-friendly way to analyze diffraction data. You can\n", + "download it as a standalone application here: 👉\n", + "https://easydiffraction.org\n", + "\n", + "We'd love to hear your feedback on EasyDiffraction — both the library\n", + "and the GUI! 💬" + ] + } + ], + "metadata": { + "jupytext": { + "cell_metadata_filter": "tags,title,-all", + "main_language": "python", + "notebook_metadata_filter": "-all" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/tutorials/ed-13.py b/docs/docs/tutorials/ed-13.py similarity index 88% rename from tutorials/ed-13.py rename to docs/docs/tutorials/ed-13.py index e2bf1110..42996532 100644 --- a/tutorials/ed-13.py +++ b/docs/docs/tutorials/ed-13.py @@ -52,11 +52,11 @@ # # In EasyDiffraction, a project serves as a container for all # information related to the analysis of a specific experiment or set of -# experiments. It enables you to organize your data, experiments, sample -# models, and fitting parameters in a structured manner. You can think -# of it as a folder containing all the essential details about your -# analysis. The project also allows us to visualize both the measured -# and calculated diffraction patterns, among other things. +# experiments. It enables you to organize your data, experiments, +# crystal structures, and fitting parameters in an organized manner. You +# can think of it as a folder containing all the essential details about +# your analysis. The project also allows us to visualize both the +# measured and calculated diffraction patterns, among other things. # %% [markdown] tags=["doc-link"] # 📖 See @@ -120,7 +120,7 @@ # for more details about different types of experiments. # %% -project_1.experiments.add( +project_1.experiments.add_from_data_path( name='sim_si', data_path=si_xye_path, sample_form='powder', @@ -185,8 +185,8 @@ # for more details about excluding regions from the measured data. # %% -project_1.experiments['sim_si'].excluded_regions.add(id='1', start=0, end=55000) -project_1.experiments['sim_si'].excluded_regions.add(id='2', start=105500, end=200000) +project_1.experiments['sim_si'].excluded_regions.create(id='1', start=0, end=55000) +project_1.experiments['sim_si'].excluded_regions.create(id='2', start=105500, end=200000) # %% [markdown] # To visualize the effect of excluding the high TOF region, we can plot @@ -212,7 +212,7 @@ # TOF, respectively. # # You can set them manually, but it is more convenient to use the -# `get_value_from_xye_header` function from the EasyDiffraction library. +# `extract_metadata` function from the EasyDiffraction library. # %% [markdown] tags=["doc-link"] # 📖 See @@ -220,11 +220,11 @@ # for more details about the instrument parameters. # %% -project_1.experiments['sim_si'].instrument.setup_twotheta_bank = ed.get_value_from_xye_header( - si_xye_path, 'two_theta' +project_1.experiments['sim_si'].instrument.setup_twotheta_bank = ed.extract_metadata( + si_xye_path, r'two_theta\s*=\s*([-+]?\d*\.?\d+(?:[eE][-+]?\d+)?)' ) -project_1.experiments['sim_si'].instrument.calib_d_to_tof_linear = ed.get_value_from_xye_header( - si_xye_path, 'DIFC' +project_1.experiments['sim_si'].instrument.calib_d_to_tof_linear = ed.extract_metadata( + si_xye_path, r'DIFC\s*=\s*([-+]?\d*\.?\d+(?:[eE][-+]?\d+)?)' ) # %% [markdown] @@ -355,25 +355,25 @@ # %% project_1.experiments['sim_si'].background_type = 'line-segment' -project_1.experiments['sim_si'].background.add(id='1', x=50000, y=0.01) -project_1.experiments['sim_si'].background.add(id='2', x=60000, y=0.01) -project_1.experiments['sim_si'].background.add(id='3', x=70000, y=0.01) -project_1.experiments['sim_si'].background.add(id='4', x=80000, y=0.01) -project_1.experiments['sim_si'].background.add(id='5', x=90000, y=0.01) -project_1.experiments['sim_si'].background.add(id='6', x=100000, y=0.01) -project_1.experiments['sim_si'].background.add(id='7', x=110000, y=0.01) +project_1.experiments['sim_si'].background.create(id='1', x=50000, y=0.01) +project_1.experiments['sim_si'].background.create(id='2', x=60000, y=0.01) +project_1.experiments['sim_si'].background.create(id='3', x=70000, y=0.01) +project_1.experiments['sim_si'].background.create(id='4', x=80000, y=0.01) +project_1.experiments['sim_si'].background.create(id='5', x=90000, y=0.01) +project_1.experiments['sim_si'].background.create(id='6', x=100000, y=0.01) +project_1.experiments['sim_si'].background.create(id='7', x=110000, y=0.01) # %% [markdown] -# ### 🧩 Create a Sample Model – Si +# ### 🧩 Create a Structure – Si # -# After setting up the experiment, we need to create a sample model that +# After setting up the experiment, we need to create a structure that # describes the crystal structure of the sample being analyzed. # -# In this case, we will create a sample model for silicon (Si) with a -# cubic crystal structure. The sample model contains information about +# In this case, we will create a structure for silicon (Si) with a +# cubic crystal structure. The structure contains information about # the space group, lattice parameters, atomic positions of the atoms in # the unit cell, atom types, occupancies and atomic displacement -# parameters. The sample model is essential for the fitting process, as +# parameters. The structure is essential for the fitting process, as # it is used to calculate the expected diffraction pattern. # # EasyDiffraction refines the crystal structure of the sample, but does @@ -435,54 +435,54 @@ # %% [markdown] # As with adding the experiment in the previous step, we will create a -# default sample model and then modify its parameters to match the Si +# default structure and then modify its parameters to match the Si # structure. # %% [markdown] tags=["doc-link"] # 📖 See -# [documentation](https://docs.easydiffraction.org/lib/user-guide/analysis-workflow/model/) -# for more details about sample models and their purpose in the data +# [documentation](https://docs.easydiffraction.org/lib/user-guide/analysis-workflow/structure/) +# for more details about structures and their purpose in the data # analysis workflow. # %% [markdown] -# #### Add Sample Model +# #### Add Structure # %% -project_1.sample_models.add(name='si') +project_1.structures.create(name='si') # %% [markdown] # #### Set Space Group # %% [markdown] tags=["doc-link"] # 📖 See -# [documentation](https://docs.easydiffraction.org/lib/user-guide/analysis-workflow/model/#space-group-category) +# [documentation](https://docs.easydiffraction.org/lib/user-guide/analysis-workflow/structure/#space-group-category) # for more details about the space group. # %% -project_1.sample_models['si'].space_group.name_h_m = 'F d -3 m' -project_1.sample_models['si'].space_group.it_coordinate_system_code = '2' +project_1.structures['si'].space_group.name_h_m = 'F d -3 m' +project_1.structures['si'].space_group.it_coordinate_system_code = '2' # %% [markdown] # #### Set Lattice Parameters # %% [markdown] tags=["doc-link"] # 📖 See -# [documentation](https://docs.easydiffraction.org/lib/user-guide/analysis-workflow/model/#cell-category) +# [documentation](https://docs.easydiffraction.org/lib/user-guide/analysis-workflow/structure/#cell-category) # for more details about the unit cell parameters. # %% -project_1.sample_models['si'].cell.length_a = 5.43 +project_1.structures['si'].cell.length_a = 5.43 # %% [markdown] # #### Set Atom Sites # %% [markdown] tags=["doc-link"] # 📖 See -# [documentation](https://docs.easydiffraction.org/lib/user-guide/analysis-workflow/model/#atom-sites-category) +# [documentation](https://docs.easydiffraction.org/lib/user-guide/analysis-workflow/structure/#atom-sites-category) # for more details about the atom sites category. # %% -project_1.sample_models['si'].atom_sites.add( +project_1.structures['si'].atom_sites.create( label='Si', type_symbol='Si', fract_x=0, @@ -493,29 +493,29 @@ ) # %% [markdown] -# ### 🔗 Assign Sample Model to Experiment +# ### 🔗 Assign Structure to Experiment # -# Now we need to assign, or link, this sample model to the experiment +# Now we need to assign, or link, this structure to the experiment # created above. This linked crystallographic phase will be used to # calculate the expected diffraction pattern based on the crystal -# structure defined in the sample model. +# structure defined in the structure. # %% [markdown] tags=["doc-link"] # 📖 See # [documentation](https://docs.easydiffraction.org/lib/user-guide/analysis-workflow/experiment/#linked-phases-category) -# for more details about linking a sample model to an experiment. +# for more details about linking a structure to an experiment. # %% -project_1.experiments['sim_si'].linked_phases.add(id='si', scale=1.0) +project_1.experiments['sim_si'].linked_phases.create(id='si', scale=1.0) # %% [markdown] # ### 🚀 Analyze and Fit the Data # -# After setting up the experiment and sample model, we can now analyze +# After setting up the experiment and structure, we can now analyze # the measured diffraction pattern and perform the fit. Building on the # analogies from the EasyScience library and the previous notebooks, we # can say that all the parameters we introduced earlier — those defining -# the sample model (crystal structure parameters) and the experiment +# the structure (crystal structure parameters) and the experiment # (instrument, background, and peak profile parameters) — together form # the complete set of parameters that can be refined during the fitting # process. @@ -528,12 +528,12 @@ # %% [markdown] **Reminder:** # # The fitting process involves comparing the measured diffraction -# pattern with the calculated diffraction pattern based on the sample -# model and instrument parameters. The goal is to adjust the parameters -# of the sample model and the experiment to minimize the difference -# between the measured and calculated diffraction patterns. This is done -# by refining the parameters of the sample model and the instrument -# settings to achieve a better fit. +# pattern with the calculated diffraction pattern based on the crystal +# structure and instrument parameters. The goal is to adjust the +# parameters of the structure and the experiment to minimize the +# difference between the measured and calculated diffraction patterns. +# This is done by refining the parameters of the structure and the +# instrument settings to achieve a better fit. # %% [markdown] tags=["doc-link"] # 📖 See @@ -593,7 +593,7 @@ # # Before performing the fit, we can visually compare the measured # diffraction pattern with the calculated diffraction pattern based on -# the initial parameters of the sample model and the instrument. This +# the initial parameters of the structure and the instrument. This # provides an indication of how well the initial parameters match the # measured data. The `plot_meas_vs_calc` method of the project allows # this comparison. @@ -670,7 +670,7 @@ # setting the `d_spacing` parameter to `True`. # %% -project_1.plot_meas_vs_calc(expt_name='sim_si', d_spacing=True) +project_1.plot_meas_vs_calc(expt_name='sim_si', x='d_spacing') # %% [markdown] # As you can see, the calculated diffraction pattern now matches the @@ -689,7 +689,7 @@ # # Before moving on, we can save the project to disk for later use. This # will preserve the entire project structure, including experiments, -# sample models, and fitting results. The project is saved into a +# structures, and fitting results. The project is saved into a # directory specified by the `dir_path` attribute of the project object. # %% @@ -753,7 +753,7 @@ # reduced data file is missing. lbco_xye_path = ed.download_data(id=18, destination=data_dir) -project_2.experiments.add( +project_2.experiments.add_from_data_path( name='sim_lbco', data_path=lbco_xye_path, sample_form='powder', @@ -783,8 +783,8 @@ # %% tags=["solution", "hide-input"] project_2.plot_meas(expt_name='sim_lbco') -project_2.experiments['sim_lbco'].excluded_regions.add(id='1', start=0, end=55000) -project_2.experiments['sim_lbco'].excluded_regions.add(id='2', start=105500, end=200000) +project_2.experiments['sim_lbco'].excluded_regions.create(id='1', start=0, end=55000) +project_2.experiments['sim_lbco'].excluded_regions.create(id='2', start=105500, end=200000) project_2.plot_meas(expt_name='sim_lbco') @@ -804,11 +804,11 @@ # **Solution:** # %% tags=["solution", "hide-input"] -project_2.experiments['sim_lbco'].instrument.setup_twotheta_bank = ed.get_value_from_xye_header( - lbco_xye_path, 'two_theta' +project_2.experiments['sim_lbco'].instrument.setup_twotheta_bank = ed.extract_metadata( + lbco_xye_path, r'two_theta\s*=\s*([-+]?\d*\.?\d+(?:[eE][-+]?\d+)?)' ) -project_2.experiments['sim_lbco'].instrument.calib_d_to_tof_linear = ed.get_value_from_xye_header( - lbco_xye_path, 'DIFC' +project_2.experiments['sim_lbco'].instrument.calib_d_to_tof_linear = ed.extract_metadata( + lbco_xye_path, r'DIFC\s*=\s*([-+]?\d*\.?\d+(?:[eE][-+]?\d+)?)' ) # %% [markdown] @@ -861,20 +861,20 @@ # %% tags=["solution", "hide-input"] project_2.experiments['sim_lbco'].background_type = 'line-segment' -project_2.experiments['sim_lbco'].background.add(id='1', x=50000, y=0.2) -project_2.experiments['sim_lbco'].background.add(id='2', x=60000, y=0.2) -project_2.experiments['sim_lbco'].background.add(id='3', x=70000, y=0.2) -project_2.experiments['sim_lbco'].background.add(id='4', x=80000, y=0.2) -project_2.experiments['sim_lbco'].background.add(id='5', x=90000, y=0.2) -project_2.experiments['sim_lbco'].background.add(id='6', x=100000, y=0.2) -project_2.experiments['sim_lbco'].background.add(id='7', x=110000, y=0.2) +project_2.experiments['sim_lbco'].background.create(id='1', x=50000, y=0.2) +project_2.experiments['sim_lbco'].background.create(id='2', x=60000, y=0.2) +project_2.experiments['sim_lbco'].background.create(id='3', x=70000, y=0.2) +project_2.experiments['sim_lbco'].background.create(id='4', x=80000, y=0.2) +project_2.experiments['sim_lbco'].background.create(id='5', x=90000, y=0.2) +project_2.experiments['sim_lbco'].background.create(id='6', x=100000, y=0.2) +project_2.experiments['sim_lbco'].background.create(id='7', x=110000, y=0.2) # %% [markdown] -# ### 🧩 Exercise 3: Define a Sample Model – LBCO +# ### 🧩 Exercise 3: Define a Structure – LBCO # -# The LBSO structure is not as simple as the Si model, as it contains +# The LBSO structure is not as simple as the Si one, as it contains # multiple atoms in the unit cell. It is not in COD, so we give you the -# structural parameters in CIF format to create the sample model. +# structural parameters in CIF format to create the structure. # # Note that those parameters are not necessarily the most accurate ones, # but they are a good starting point for the fit. The aim of the study @@ -914,7 +914,7 @@ # Note that the `occupancy` of the La and Ba atoms is 0.5 # and those atoms are located in the same position (0, 0, 0) in the unit # cell. This means that an extra attribute `occupancy` needs to be set -# for those atoms later in the sample model. +# for those atoms later in the structure. # # We model the La/Ba site using the virtual crystal approximation. In # this approach, the scattering is taken as a weighted average of La and @@ -936,9 +936,9 @@ # of the random case and the extra peaks of the ordered case. # %% [markdown] -# #### Exercise 3.1: Create Sample Model +# #### Exercise 3.1: Create Structure # -# Add a sample model for LBCO to the project. The sample model +# Add a structure for LBCO to the project. The structure # parameters will be set in the next exercises. # %% [markdown] @@ -946,19 +946,19 @@ # %% [markdown] tags=["dmsc-school-hint"] # You can use the same approach as in the previous part of the notebook, -# but this time you need to use the model name corresponding to the LBCO +# but this time you need to use the name corresponding to the LBCO # structure, e.g. 'lbco'. # %% [markdown] # **Solution:** # %% tags=["solution", "hide-input"] -project_2.sample_models.add(name='lbco') +project_2.structures.create(name='lbco') # %% [markdown] # #### Exercise 3.2: Set Space Group # -# Set the space group for the LBCO sample model. +# Set the space group for the LBCO structure. # %% [markdown] # **Hint:** @@ -971,13 +971,13 @@ # **Solution:** # %% tags=["solution", "hide-input"] -project_2.sample_models['lbco'].space_group.name_h_m = 'P m -3 m' -project_2.sample_models['lbco'].space_group.it_coordinate_system_code = '1' +project_2.structures['lbco'].space_group.name_h_m = 'P m -3 m' +project_2.structures['lbco'].space_group.it_coordinate_system_code = '1' # %% [markdown] # #### Exercise 3.3: Set Lattice Parameters # -# Set the lattice parameters for the LBCO sample model. +# Set the lattice parameters for the LBCO structure. # %% [markdown] # **Hint:** @@ -989,25 +989,25 @@ # **Solution:** # %% tags=["solution", "hide-input"] -project_2.sample_models['lbco'].cell.length_a = 3.88 +project_2.structures['lbco'].cell.length_a = 3.88 # %% [markdown] # #### Exercise 3.4: Set Atom Sites # -# Set the atom sites for the LBCO sample model. +# Set the atom sites for the LBCO structure. # %% [markdown] # **Hint:** # %% [markdown] tags=["dmsc-school-hint"] # Use the atom sites from the CIF data. You can use the `add` method of -# the `atom_sites` attribute of the sample model to add the atom sites. +# the `atom_sites` attribute of the structure to add the atom sites. # %% [markdown] # **Solution:** # %% tags=["solution", "hide-input"] -project_2.sample_models['lbco'].atom_sites.add( +project_2.structures['lbco'].atom_sites.create( label='La', type_symbol='La', fract_x=0, @@ -1017,7 +1017,7 @@ b_iso=0.95, occupancy=0.5, ) -project_2.sample_models['lbco'].atom_sites.add( +project_2.structures['lbco'].atom_sites.create( label='Ba', type_symbol='Ba', fract_x=0, @@ -1027,7 +1027,7 @@ b_iso=0.95, occupancy=0.5, ) -project_2.sample_models['lbco'].atom_sites.add( +project_2.structures['lbco'].atom_sites.create( label='Co', type_symbol='Co', fract_x=0.5, @@ -1036,7 +1036,7 @@ wyckoff_letter='b', b_iso=0.80, ) -project_2.sample_models['lbco'].atom_sites.add( +project_2.structures['lbco'].atom_sites.create( label='O', type_symbol='O', fract_x=0, @@ -1047,22 +1047,22 @@ ) # %% [markdown] -# ### 🔗 Exercise 4: Assign Sample Model to Experiment +# ### 🔗 Exercise 4: Assign Structure to Experiment # -# Now assign the LBCO sample model to the experiment created above. +# Now assign the LBCO structure to the experiment created above. # %% [markdown] # **Hint:** # %% [markdown] tags=["dmsc-school-hint"] -# Use the `linked_phases` attribute of the experiment to link the sample -# model. +# Use the `linked_phases` attribute of the experiment to link the +# crystal structure. # %% [markdown] # **Solution:** # %% tags=["solution", "hide-input"] -project_2.experiments['sim_lbco'].linked_phases.add(id='lbco', scale=1.0) +project_2.experiments['sim_lbco'].linked_phases.create(id='lbco', scale=1.0) # %% [markdown] # ### 🚀 Exercise 5: Analyze and Fit the Data @@ -1176,7 +1176,7 @@ # **Solution:** # %% tags=["solution", "hide-input"] -project_2.sample_models['lbco'].cell.length_a.free = True +project_2.structures['lbco'].cell.length_a.free = True project_2.analysis.fit() project_2.analysis.show_fit_results() @@ -1208,7 +1208,7 @@ # **Solution:** # %% tags=["solution", "hide-input"] -project_2.plot_meas_vs_calc(expt_name='sim_lbco', d_spacing=True) +project_2.plot_meas_vs_calc(expt_name='sim_lbco', x='d_spacing') # %% [markdown] # #### Exercise 5.6: Refine the Peak Profile Parameters @@ -1225,7 +1225,7 @@ # perfectly describe the peak at about 1.38 Å, as can be seen below: # %% -project_2.plot_meas_vs_calc(expt_name='sim_lbco', d_spacing=True, x_min=1.35, x_max=1.40) +project_2.plot_meas_vs_calc(expt_name='sim_lbco', x='d_spacing', x_min=1.35, x_max=1.40) # %% [markdown] # The peak profile parameters are determined based on both the @@ -1260,7 +1260,7 @@ project_2.analysis.fit() project_2.analysis.show_fit_results() -project_2.plot_meas_vs_calc(expt_name='sim_lbco', d_spacing=True, x_min=1.35, x_max=1.40) +project_2.plot_meas_vs_calc(expt_name='sim_lbco', x='d_spacing', x_min=1.35, x_max=1.40) # %% [markdown] # #### Exercise 5.7: Find Undefined Features @@ -1283,7 +1283,7 @@ # **Solution:** # %% tags=["solution", "hide-input"] -project_2.plot_meas_vs_calc(expt_name='sim_lbco', x_min=1.53, x_max=1.7, d_spacing=True) +project_2.plot_meas_vs_calc(expt_name='sim_lbco', x='d_spacing', x_min=1.53, x_max=1.7) # %% [markdown] # #### Exercise 5.8: Identify the Cause of the Unexplained Peaks @@ -1348,14 +1348,14 @@ # confirm this hypothesis. # %% tags=["solution", "hide-input"] -project_1.plot_meas_vs_calc(expt_name='sim_si', x_min=1, x_max=1.7, d_spacing=True) -project_2.plot_meas_vs_calc(expt_name='sim_lbco', x_min=1, x_max=1.7, d_spacing=True) +project_1.plot_meas_vs_calc(expt_name='sim_si', x='d_spacing', x_min=1, x_max=1.7) +project_2.plot_meas_vs_calc(expt_name='sim_lbco', x='d_spacing', x_min=1, x_max=1.7) # %% [markdown] -# #### Exercise 5.10: Create a Second Sample Model – Si as Impurity +# #### Exercise 5.10: Create a Second Structure – Si as Impurity # -# Create a second sample model for the Si phase, which is the impurity -# phase identified in the previous step. Link this sample model to the +# Create a second structure for the Si phase, which is the impurity +# phase identified in the previous step. Link this structure to the # LBCO experiment. # %% [markdown] @@ -1363,7 +1363,7 @@ # %% [markdown] tags=["dmsc-school-hint"] # You can use the same approach as in the previous part of the notebook, -# but this time you need to create a sample model for Si and link it to +# but this time you need to create a structure for Si and link it to # the LBCO experiment. # %% [markdown] @@ -1371,15 +1371,15 @@ # %% tags=["solution", "hide-input"] # Set Space Group -project_2.sample_models.add(name='si') -project_2.sample_models['si'].space_group.name_h_m = 'F d -3 m' -project_2.sample_models['si'].space_group.it_coordinate_system_code = '2' +project_2.structures.create(name='si') +project_2.structures['si'].space_group.name_h_m = 'F d -3 m' +project_2.structures['si'].space_group.it_coordinate_system_code = '2' # Set Lattice Parameters -project_2.sample_models['si'].cell.length_a = 5.43 +project_2.structures['si'].cell.length_a = 5.43 # Set Atom Sites -project_2.sample_models['si'].atom_sites.add( +project_2.structures['si'].atom_sites.create( label='Si', type_symbol='Si', fract_x=0, @@ -1389,8 +1389,8 @@ b_iso=0.89, ) -# Assign Sample Model to Experiment -project_2.experiments['sim_lbco'].linked_phases.add(id='si', scale=1.0) +# Assign Structure to Experiment +project_2.experiments['sim_lbco'].linked_phases.create(id='si', scale=1.0) # %% [markdown] # #### Exercise 5.11: Refine the Scale of the Si Phase @@ -1443,7 +1443,7 @@ # # To review the analysis results, you can generate and print a summary # report using the `show_report()` method, as demonstrated in the cell -# below. The report includes parameters related to the sample model and +# below. The report includes parameters related to the structure and # the experiment, such as the refined unit cell parameter `a` of LBCO. # # Information about the crystal or magnetic structure, along with diff --git a/docs/docs/tutorials/ed-14.ipynb b/docs/docs/tutorials/ed-14.ipynb new file mode 100644 index 00000000..25927d6a --- /dev/null +++ b/docs/docs/tutorials/ed-14.ipynb @@ -0,0 +1,319 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0", + "metadata": {}, + "source": [ + "# Structure Refinement: Tb2TiO7, HEiDi\n", + "\n", + "Crystal structure refinement of Tb2TiO7 using single crystal neutron\n", + "diffraction data from HEiDi at FRM II." + ] + }, + { + "cell_type": "markdown", + "id": "1", + "metadata": {}, + "source": [ + "## Import Library" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2", + "metadata": {}, + "outputs": [], + "source": [ + "import easydiffraction as ed" + ] + }, + { + "cell_type": "markdown", + "id": "3", + "metadata": {}, + "source": [ + "## Step 1: Define Project" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4", + "metadata": {}, + "outputs": [], + "source": [ + "# Create minimal project without name and description\n", + "project = ed.Project()" + ] + }, + { + "cell_type": "markdown", + "id": "5", + "metadata": {}, + "source": [ + "## Step 2: Define Structure" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6", + "metadata": {}, + "outputs": [], + "source": [ + "# Download CIF file from repository\n", + "structure_path = ed.download_data(id=20, destination='data')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7", + "metadata": {}, + "outputs": [], + "source": [ + "project.structures.add_from_cif_path(structure_path)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8", + "metadata": {}, + "outputs": [], + "source": [ + "project.structures.show_names()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9", + "metadata": {}, + "outputs": [], + "source": [ + "structure = project.structures['tbti']" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "10", + "metadata": {}, + "outputs": [], + "source": [ + "structure.atom_sites['Tb'].b_iso = 0.0\n", + "structure.atom_sites['Ti'].b_iso = 0.0\n", + "structure.atom_sites['O1'].b_iso = 0.0\n", + "structure.atom_sites['O2'].b_iso = 0.0" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "11", + "metadata": {}, + "outputs": [], + "source": [ + "structure.show_as_cif()" + ] + }, + { + "cell_type": "markdown", + "id": "12", + "metadata": {}, + "source": [ + "## Step 3: Define Experiment" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "13", + "metadata": {}, + "outputs": [], + "source": [ + "data_path = ed.download_data(id=19, destination='data')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "14", + "metadata": {}, + "outputs": [], + "source": [ + "project.experiments.add_from_data_path(\n", + " name='heidi',\n", + " data_path=data_path,\n", + " sample_form='single crystal',\n", + " beam_mode='constant wavelength',\n", + " radiation_probe='neutron',\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "15", + "metadata": {}, + "outputs": [], + "source": [ + "experiment = project.experiments['heidi'] # TODO: " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "16", + "metadata": {}, + "outputs": [], + "source": [ + "experiment.linked_crystal.id = 'tbti'\n", + "experiment.linked_crystal.scale = 1.0" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "17", + "metadata": {}, + "outputs": [], + "source": [ + "experiment.instrument.setup_wavelength = 0.793" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "18", + "metadata": {}, + "outputs": [], + "source": [ + "experiment.extinction.mosaicity = 29820\n", + "experiment.extinction.radius = 30" + ] + }, + { + "cell_type": "markdown", + "id": "19", + "metadata": {}, + "source": [ + "## Step 4: Perform Analysis" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "20", + "metadata": {}, + "outputs": [], + "source": [ + "project.plot_meas_vs_calc(expt_name='heidi')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "21", + "metadata": {}, + "outputs": [], + "source": [ + "experiment.linked_crystal.scale.free = True\n", + "experiment.extinction.radius.free = True" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "22", + "metadata": {}, + "outputs": [], + "source": [ + "experiment.show_as_cif()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "23", + "metadata": {}, + "outputs": [], + "source": [ + "# Start refinement. All parameters, which have standard uncertainties\n", + "# in the input CIF files, are refined by default.\n", + "project.analysis.fit()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "24", + "metadata": {}, + "outputs": [], + "source": [ + "# Show fit results summary\n", + "project.analysis.show_fit_results()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "25", + "metadata": {}, + "outputs": [], + "source": [ + "experiment.show_as_cif()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "26", + "metadata": {}, + "outputs": [], + "source": [ + "project.experiments.show_names()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "27", + "metadata": {}, + "outputs": [], + "source": [ + "project.plot_meas_vs_calc(expt_name='heidi')" + ] + }, + { + "cell_type": "markdown", + "id": "28", + "metadata": {}, + "source": [ + "## Step 5: Show Project Summary" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "29", + "metadata": {}, + "outputs": [], + "source": [ + "project.summary.show_report()" + ] + } + ], + "metadata": { + "jupytext": { + "cell_metadata_filter": "-all", + "main_language": "python", + "notebook_metadata_filter": "-all" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/docs/tutorials/ed-14.py b/docs/docs/tutorials/ed-14.py new file mode 100644 index 00000000..eaa3da2a --- /dev/null +++ b/docs/docs/tutorials/ed-14.py @@ -0,0 +1,109 @@ +# %% [markdown] +# # Structure Refinement: Tb2TiO7, HEiDi +# +# Crystal structure refinement of Tb2TiO7 using single crystal neutron +# diffraction data from HEiDi at FRM II. + +# %% [markdown] +# ## Import Library + +# %% +import easydiffraction as ed + +# %% [markdown] +# ## Step 1: Define Project + +# %% +# Create minimal project without name and description +project = ed.Project() + +# %% [markdown] +# ## Step 2: Define Structure + +# %% +# Download CIF file from repository +structure_path = ed.download_data(id=20, destination='data') + +# %% +project.structures.add_from_cif_path(structure_path) + +# %% +project.structures.show_names() + +# %% +structure = project.structures['tbti'] + +# %% +structure.atom_sites['Tb'].b_iso = 0.0 +structure.atom_sites['Ti'].b_iso = 0.0 +structure.atom_sites['O1'].b_iso = 0.0 +structure.atom_sites['O2'].b_iso = 0.0 + +# %% +structure.show_as_cif() + +# %% [markdown] +# ## Step 3: Define Experiment + +# %% +data_path = ed.download_data(id=19, destination='data') + +# %% +project.experiments.add_from_data_path( + name='heidi', + data_path=data_path, + sample_form='single crystal', + beam_mode='constant wavelength', + radiation_probe='neutron', +) + +# %% +experiment = project.experiments['heidi'] # TODO: + +# %% +experiment.linked_crystal.id = 'tbti' +experiment.linked_crystal.scale = 1.0 + +# %% +experiment.instrument.setup_wavelength = 0.793 + +# %% +experiment.extinction.mosaicity = 29820 +experiment.extinction.radius = 30 + +# %% [markdown] +# ## Step 4: Perform Analysis + +# %% +project.plot_meas_vs_calc(expt_name='heidi') + +# %% +experiment.linked_crystal.scale.free = True +experiment.extinction.radius.free = True + +# %% +experiment.show_as_cif() + +# %% +# Start refinement. All parameters, which have standard uncertainties +# in the input CIF files, are refined by default. +project.analysis.fit() + +# %% +# Show fit results summary +project.analysis.show_fit_results() + +# %% +experiment.show_as_cif() + +# %% +project.experiments.show_names() + +# %% +project.plot_meas_vs_calc(expt_name='heidi') + +# %% [markdown] +# ## Step 5: Show Project Summary + +# %% +project.summary.show_report() diff --git a/docs/docs/tutorials/ed-15.ipynb b/docs/docs/tutorials/ed-15.ipynb new file mode 100644 index 00000000..a426f2ee --- /dev/null +++ b/docs/docs/tutorials/ed-15.ipynb @@ -0,0 +1,296 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0", + "metadata": {}, + "source": [ + "# Structure Refinement: Taurine, SENJU\n", + "\n", + "Crystal structure refinement of Taurine using time-of-flight single\n", + "crystal neutron diffraction data from SENJU at J-PARC." + ] + }, + { + "cell_type": "markdown", + "id": "1", + "metadata": {}, + "source": [ + "## Import Library" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2", + "metadata": {}, + "outputs": [], + "source": [ + "import easydiffraction as ed" + ] + }, + { + "cell_type": "markdown", + "id": "3", + "metadata": {}, + "source": [ + "## Step 1: Define Project" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4", + "metadata": {}, + "outputs": [], + "source": [ + "# Create minimal project without name and description\n", + "project = ed.Project()" + ] + }, + { + "cell_type": "markdown", + "id": "5", + "metadata": {}, + "source": [ + "## Step 2: Define Structure" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6", + "metadata": {}, + "outputs": [], + "source": [ + "# Download CIF file from repository\n", + "structure_path = ed.download_data(id=21, destination='data')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7", + "metadata": {}, + "outputs": [], + "source": [ + "project.structures.add_from_cif_path(structure_path)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8", + "metadata": {}, + "outputs": [], + "source": [ + "project.structures.show_names()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9", + "metadata": {}, + "outputs": [], + "source": [ + "structure = project.structures['taurine']" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "10", + "metadata": {}, + "outputs": [], + "source": [ + "# structure.show_as_cif()" + ] + }, + { + "cell_type": "markdown", + "id": "11", + "metadata": {}, + "source": [ + "## Step 3: Define Experiment" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "12", + "metadata": {}, + "outputs": [], + "source": [ + "data_path = ed.download_data(id=22, destination='data')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "13", + "metadata": {}, + "outputs": [], + "source": [ + "project.experiments.add_from_data_path(\n", + " name='senju',\n", + " data_path=data_path,\n", + " sample_form='single crystal',\n", + " beam_mode='time-of-flight',\n", + " radiation_probe='neutron',\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "14", + "metadata": {}, + "outputs": [], + "source": [ + "experiment = project.experiments['senju']" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "15", + "metadata": {}, + "outputs": [], + "source": [ + "experiment.linked_crystal.id = 'taurine'\n", + "experiment.linked_crystal.scale = 1.0" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "16", + "metadata": {}, + "outputs": [], + "source": [ + "experiment.extinction.mosaicity = 1000.0\n", + "experiment.extinction.radius = 100.0" + ] + }, + { + "cell_type": "markdown", + "id": "17", + "metadata": {}, + "source": [ + "## Step 4: Perform Analysis" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "18", + "metadata": {}, + "outputs": [], + "source": [ + "project.plot_meas_vs_calc(expt_name='senju')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "19", + "metadata": {}, + "outputs": [], + "source": [ + "experiment.linked_crystal.scale.free = True\n", + "experiment.extinction.radius.free = True" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "20", + "metadata": {}, + "outputs": [], + "source": [ + "# experiment.show_as_cif()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "21", + "metadata": {}, + "outputs": [], + "source": [ + "# Start refinement. All parameters, which have standard uncertainties\n", + "# in the input CIF files, are refined by default.\n", + "project.analysis.fit()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "22", + "metadata": {}, + "outputs": [], + "source": [ + "# Show fit results summary\n", + "project.analysis.show_fit_results()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "23", + "metadata": {}, + "outputs": [], + "source": [ + "# experiment.show_as_cif()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "24", + "metadata": {}, + "outputs": [], + "source": [ + "project.experiments.show_names()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "25", + "metadata": {}, + "outputs": [], + "source": [ + "project.plot_meas_vs_calc(expt_name='senju')" + ] + }, + { + "cell_type": "markdown", + "id": "26", + "metadata": {}, + "source": [ + "## Step 5: Show Project Summary" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "27", + "metadata": {}, + "outputs": [], + "source": [ + "project.summary.show_report()" + ] + } + ], + "metadata": { + "jupytext": { + "cell_metadata_filter": "-all", + "main_language": "python", + "notebook_metadata_filter": "-all" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/docs/tutorials/ed-15.py b/docs/docs/tutorials/ed-15.py new file mode 100644 index 00000000..4ae4933a --- /dev/null +++ b/docs/docs/tutorials/ed-15.py @@ -0,0 +1,100 @@ +# %% [markdown] +# # Structure Refinement: Taurine, SENJU +# +# Crystal structure refinement of Taurine using time-of-flight single +# crystal neutron diffraction data from SENJU at J-PARC. + +# %% [markdown] +# ## Import Library + +# %% +import easydiffraction as ed + +# %% [markdown] +# ## Step 1: Define Project + +# %% +# Create minimal project without name and description +project = ed.Project() + +# %% [markdown] +# ## Step 2: Define Structure + +# %% +# Download CIF file from repository +structure_path = ed.download_data(id=21, destination='data') + +# %% +project.structures.add_from_cif_path(structure_path) + +# %% +project.structures.show_names() + +# %% +structure = project.structures['taurine'] + +# %% +# structure.show_as_cif() + +# %% [markdown] +# ## Step 3: Define Experiment + +# %% +data_path = ed.download_data(id=22, destination='data') + +# %% +project.experiments.add_from_data_path( + name='senju', + data_path=data_path, + sample_form='single crystal', + beam_mode='time-of-flight', + radiation_probe='neutron', +) + +# %% +experiment = project.experiments['senju'] + +# %% +experiment.linked_crystal.id = 'taurine' +experiment.linked_crystal.scale = 1.0 + +# %% +experiment.extinction.mosaicity = 1000.0 +experiment.extinction.radius = 100.0 + +# %% [markdown] +# ## Step 4: Perform Analysis + +# %% +project.plot_meas_vs_calc(expt_name='senju') + +# %% +experiment.linked_crystal.scale.free = True +experiment.extinction.radius.free = True + +# %% +# experiment.show_as_cif() + +# %% +# Start refinement. All parameters, which have standard uncertainties +# in the input CIF files, are refined by default. +project.analysis.fit() + +# %% +# Show fit results summary +project.analysis.show_fit_results() + +# %% +# experiment.show_as_cif() + +# %% +project.experiments.show_names() + +# %% +project.plot_meas_vs_calc(expt_name='senju') + +# %% [markdown] +# ## Step 5: Show Project Summary + +# %% +project.summary.show_report() diff --git a/docs/docs/tutorials/ed-16.ipynb b/docs/docs/tutorials/ed-16.ipynb new file mode 100644 index 00000000..688297e6 --- /dev/null +++ b/docs/docs/tutorials/ed-16.ipynb @@ -0,0 +1,623 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0", + "metadata": {}, + "source": [ + "# Joint Refinement: Si, Bragg + PDF\n", + "\n", + "This example demonstrates a joint refinement of the Si crystal\n", + "structure combining Bragg diffraction and pair distribution function\n", + "(PDF) analysis. The Bragg experiment uses time-of-flight neutron\n", + "powder diffraction data from SEPD at Argonne, while the PDF\n", + "experiment uses data from NOMAD at SNS. A single shared Si structure\n", + "is refined simultaneously against both datasets." + ] + }, + { + "cell_type": "markdown", + "id": "1", + "metadata": {}, + "source": [ + "## Import Library" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2", + "metadata": {}, + "outputs": [], + "source": [ + "from easydiffraction import ExperimentFactory\n", + "from easydiffraction import Project\n", + "from easydiffraction import StructureFactory\n", + "from easydiffraction import download_data" + ] + }, + { + "cell_type": "markdown", + "id": "3", + "metadata": {}, + "source": [ + "## Define Structure\n", + "\n", + "A single Si structure is shared between the Bragg and PDF\n", + "experiments. Structural parameters refined against both datasets\n", + "simultaneously.\n", + "\n", + "#### Create Structure" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4", + "metadata": {}, + "outputs": [], + "source": [ + "structure = StructureFactory.from_scratch(name='si')" + ] + }, + { + "cell_type": "markdown", + "id": "5", + "metadata": {}, + "source": [ + "#### Set Space Group" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6", + "metadata": {}, + "outputs": [], + "source": [ + "structure.space_group.name_h_m = 'F d -3 m'\n", + "structure.space_group.it_coordinate_system_code = '1'" + ] + }, + { + "cell_type": "markdown", + "id": "7", + "metadata": {}, + "source": [ + "#### Set Unit Cell" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8", + "metadata": {}, + "outputs": [], + "source": [ + "structure.cell.length_a = 5.42" + ] + }, + { + "cell_type": "markdown", + "id": "9", + "metadata": {}, + "source": [ + "#### Set Atom Sites" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "10", + "metadata": {}, + "outputs": [], + "source": [ + "structure.atom_sites.create(\n", + " label='Si',\n", + " type_symbol='Si',\n", + " fract_x=0,\n", + " fract_y=0,\n", + " fract_z=0,\n", + " wyckoff_letter='a',\n", + " b_iso=0.2,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "11", + "metadata": {}, + "source": [ + "## Define Experiments\n", + "\n", + "Two experiments are defined: one for Bragg diffraction and one for\n", + "PDF analysis. Both are linked to the same Si structure.\n", + "\n", + "### Experiment 1: Bragg (SEPD, TOF)\n", + "\n", + "#### Download Data" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "12", + "metadata": {}, + "outputs": [], + "source": [ + "bragg_data_path = download_data(id=7, destination='data')" + ] + }, + { + "cell_type": "markdown", + "id": "13", + "metadata": {}, + "source": [ + "#### Create Experiment" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "14", + "metadata": {}, + "outputs": [], + "source": [ + "bragg_expt = ExperimentFactory.from_data_path(\n", + " name='sepd', data_path=bragg_data_path, beam_mode='time-of-flight'\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "15", + "metadata": {}, + "source": [ + "#### Set Instrument" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "16", + "metadata": {}, + "outputs": [], + "source": [ + "bragg_expt.instrument.setup_twotheta_bank = 144.845\n", + "bragg_expt.instrument.calib_d_to_tof_offset = -9.2\n", + "bragg_expt.instrument.calib_d_to_tof_linear = 7476.91\n", + "bragg_expt.instrument.calib_d_to_tof_quad = -1.54" + ] + }, + { + "cell_type": "markdown", + "id": "17", + "metadata": {}, + "source": [ + "#### Set Peak Profile" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "18", + "metadata": {}, + "outputs": [], + "source": [ + "bragg_expt.peak_profile_type = 'pseudo-voigt * ikeda-carpenter'\n", + "bragg_expt.peak.broad_gauss_sigma_0 = 5.0\n", + "bragg_expt.peak.broad_gauss_sigma_1 = 45.0\n", + "bragg_expt.peak.broad_gauss_sigma_2 = 1.0\n", + "bragg_expt.peak.broad_mix_beta_0 = 0.04221\n", + "bragg_expt.peak.broad_mix_beta_1 = 0.00946\n", + "bragg_expt.peak.asym_alpha_0 = 0.0\n", + "bragg_expt.peak.asym_alpha_1 = 0.5971" + ] + }, + { + "cell_type": "markdown", + "id": "19", + "metadata": {}, + "source": [ + "#### Set Background" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "20", + "metadata": {}, + "outputs": [], + "source": [ + "bragg_expt.background_type = 'line-segment'\n", + "for x in range(0, 35000, 5000):\n", + " bragg_expt.background.create(id=str(x), x=x, y=200)" + ] + }, + { + "cell_type": "markdown", + "id": "21", + "metadata": {}, + "source": [ + "#### Set Linked Phases" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "22", + "metadata": {}, + "outputs": [], + "source": [ + "bragg_expt.linked_phases.create(id='si', scale=13.0)" + ] + }, + { + "cell_type": "markdown", + "id": "23", + "metadata": {}, + "source": [ + "### Experiment 2: PDF (NOMAD, TOF)\n", + "\n", + "#### Download Data" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "24", + "metadata": {}, + "outputs": [], + "source": [ + "pdf_data_path = download_data(id=5, destination='data')" + ] + }, + { + "cell_type": "markdown", + "id": "25", + "metadata": {}, + "source": [ + "#### Create Experiment" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "26", + "metadata": {}, + "outputs": [], + "source": [ + "pdf_expt = ExperimentFactory.from_data_path(\n", + " name='nomad',\n", + " data_path=pdf_data_path,\n", + " beam_mode='time-of-flight',\n", + " scattering_type='total',\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "27", + "metadata": {}, + "source": [ + "#### Set Peak Profile (PDF Parameters)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "28", + "metadata": {}, + "outputs": [], + "source": [ + "pdf_expt.peak.damp_q = 0.02\n", + "pdf_expt.peak.broad_q = 0.02\n", + "pdf_expt.peak.cutoff_q = 35.0\n", + "pdf_expt.peak.sharp_delta_1 = 0.001\n", + "pdf_expt.peak.sharp_delta_2 = 4.0\n", + "pdf_expt.peak.damp_particle_diameter = 0" + ] + }, + { + "cell_type": "markdown", + "id": "29", + "metadata": {}, + "source": [ + "#### Set Linked Phases" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "30", + "metadata": {}, + "outputs": [], + "source": [ + "pdf_expt.linked_phases.create(id='si', scale=1.0)" + ] + }, + { + "cell_type": "markdown", + "id": "31", + "metadata": {}, + "source": [ + "## Define Project\n", + "\n", + "The project object manages the shared structure, both experiments,\n", + "and the analysis.\n", + "\n", + "#### Create Project" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "32", + "metadata": {}, + "outputs": [], + "source": [ + "project = Project()" + ] + }, + { + "cell_type": "markdown", + "id": "33", + "metadata": {}, + "source": [ + "#### Add Structure" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "34", + "metadata": {}, + "outputs": [], + "source": [ + "project.structures.add(structure)" + ] + }, + { + "cell_type": "markdown", + "id": "35", + "metadata": {}, + "source": [ + "#### Add Experiments" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "36", + "metadata": {}, + "outputs": [], + "source": [ + "project.experiments.add(bragg_expt)\n", + "project.experiments.add(pdf_expt)" + ] + }, + { + "cell_type": "markdown", + "id": "37", + "metadata": {}, + "source": [ + "## Perform Analysis\n", + "\n", + "This section shows the joint analysis process. The calculator is\n", + "auto-resolved per experiment: CrysPy for Bragg, PDFfit for PDF.\n", + "\n", + "#### Set Fit Mode and Weights" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "38", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.fit_mode.mode = 'joint'\n", + "project.analysis.joint_fit_experiments.create(id='sepd', weight=0.7)\n", + "project.analysis.joint_fit_experiments.create(id='nomad', weight=0.3)" + ] + }, + { + "cell_type": "markdown", + "id": "39", + "metadata": {}, + "source": [ + "#### Set Minimizer" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "40", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.current_minimizer = 'lmfit'" + ] + }, + { + "cell_type": "markdown", + "id": "41", + "metadata": {}, + "source": [ + "#### Plot Measured vs Calculated (Before Fit)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "42", + "metadata": {}, + "outputs": [], + "source": [ + "project.plot_meas_vs_calc(expt_name='sepd', show_residual=False)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "43", + "metadata": {}, + "outputs": [], + "source": [ + "project.plot_meas_vs_calc(expt_name='nomad', show_residual=False)" + ] + }, + { + "cell_type": "markdown", + "id": "44", + "metadata": {}, + "source": [ + "#### Set Fitting Parameters\n", + "\n", + "Shared structural parameters are refined against both datasets\n", + "simultaneously." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "45", + "metadata": {}, + "outputs": [], + "source": [ + "structure.cell.length_a.free = True\n", + "structure.atom_sites['Si'].b_iso.free = True" + ] + }, + { + "cell_type": "markdown", + "id": "46", + "metadata": {}, + "source": [ + "Bragg experiment parameters." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "47", + "metadata": {}, + "outputs": [], + "source": [ + "bragg_expt.linked_phases['si'].scale.free = True\n", + "bragg_expt.instrument.calib_d_to_tof_offset.free = True\n", + "bragg_expt.peak.broad_gauss_sigma_0.free = True\n", + "bragg_expt.peak.broad_gauss_sigma_1.free = True\n", + "bragg_expt.peak.broad_gauss_sigma_2.free = True\n", + "for point in bragg_expt.background:\n", + " point.y.free = True" + ] + }, + { + "cell_type": "markdown", + "id": "48", + "metadata": {}, + "source": [ + "PDF experiment parameters." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "49", + "metadata": {}, + "outputs": [], + "source": [ + "pdf_expt.linked_phases['si'].scale.free = True\n", + "pdf_expt.peak.damp_q.free = True\n", + "pdf_expt.peak.broad_q.free = True\n", + "pdf_expt.peak.sharp_delta_1.free = True\n", + "pdf_expt.peak.sharp_delta_2.free = True" + ] + }, + { + "cell_type": "markdown", + "id": "50", + "metadata": {}, + "source": [ + "#### Show Free Parameters" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "51", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.show_free_params()" + ] + }, + { + "cell_type": "markdown", + "id": "52", + "metadata": {}, + "source": [ + "#### Run Fitting" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "53", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.fit()\n", + "project.analysis.show_fit_results()" + ] + }, + { + "cell_type": "markdown", + "id": "54", + "metadata": {}, + "source": [ + "#### Plot Measured vs Calculated (After Fit)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "55", + "metadata": {}, + "outputs": [], + "source": [ + "project.plot_meas_vs_calc(expt_name='sepd', show_residual=False)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "56", + "metadata": { + "lines_to_next_cell": 2 + }, + "outputs": [], + "source": [ + "project.plot_meas_vs_calc(expt_name='nomad', show_residual=False)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "57", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "jupytext": { + "cell_metadata_filter": "-all", + "main_language": "python", + "notebook_metadata_filter": "-all" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/docs/tutorials/ed-16.py b/docs/docs/tutorials/ed-16.py new file mode 100644 index 00000000..e57f8449 --- /dev/null +++ b/docs/docs/tutorials/ed-16.py @@ -0,0 +1,259 @@ +# %% [markdown] +# # Joint Refinement: Si, Bragg + PDF +# +# This example demonstrates a joint refinement of the Si crystal +# structure combining Bragg diffraction and pair distribution function +# (PDF) analysis. The Bragg experiment uses time-of-flight neutron +# powder diffraction data from SEPD at Argonne, while the PDF +# experiment uses data from NOMAD at SNS. A single shared Si structure +# is refined simultaneously against both datasets. + +# %% [markdown] +# ## Import Library + +# %% +from easydiffraction import ExperimentFactory +from easydiffraction import Project +from easydiffraction import StructureFactory +from easydiffraction import download_data + +# %% [markdown] +# ## Define Structure +# +# A single Si structure is shared between the Bragg and PDF +# experiments. Structural parameters refined against both datasets +# simultaneously. +# +# #### Create Structure + +# %% +structure = StructureFactory.from_scratch(name='si') + +# %% [markdown] +# #### Set Space Group + +# %% +structure.space_group.name_h_m = 'F d -3 m' +structure.space_group.it_coordinate_system_code = '1' + +# %% [markdown] +# #### Set Unit Cell + +# %% +structure.cell.length_a = 5.42 + +# %% [markdown] +# #### Set Atom Sites + +# %% +structure.atom_sites.create( + label='Si', + type_symbol='Si', + fract_x=0, + fract_y=0, + fract_z=0, + wyckoff_letter='a', + b_iso=0.2, +) + +# %% [markdown] +# ## Define Experiments +# +# Two experiments are defined: one for Bragg diffraction and one for +# PDF analysis. Both are linked to the same Si structure. +# +# ### Experiment 1: Bragg (SEPD, TOF) +# +# #### Download Data + +# %% +bragg_data_path = download_data(id=7, destination='data') + +# %% [markdown] +# #### Create Experiment + +# %% +bragg_expt = ExperimentFactory.from_data_path( + name='sepd', data_path=bragg_data_path, beam_mode='time-of-flight' +) + +# %% [markdown] +# #### Set Instrument + +# %% +bragg_expt.instrument.setup_twotheta_bank = 144.845 +bragg_expt.instrument.calib_d_to_tof_offset = -9.2 +bragg_expt.instrument.calib_d_to_tof_linear = 7476.91 +bragg_expt.instrument.calib_d_to_tof_quad = -1.54 + +# %% [markdown] +# #### Set Peak Profile + +# %% +bragg_expt.peak_profile_type = 'pseudo-voigt * ikeda-carpenter' +bragg_expt.peak.broad_gauss_sigma_0 = 5.0 +bragg_expt.peak.broad_gauss_sigma_1 = 45.0 +bragg_expt.peak.broad_gauss_sigma_2 = 1.0 +bragg_expt.peak.broad_mix_beta_0 = 0.04221 +bragg_expt.peak.broad_mix_beta_1 = 0.00946 +bragg_expt.peak.asym_alpha_0 = 0.0 +bragg_expt.peak.asym_alpha_1 = 0.5971 + +# %% [markdown] +# #### Set Background + +# %% +bragg_expt.background_type = 'line-segment' +for x in range(0, 35000, 5000): + bragg_expt.background.create(id=str(x), x=x, y=200) + +# %% [markdown] +# #### Set Linked Phases + +# %% +bragg_expt.linked_phases.create(id='si', scale=13.0) + +# %% [markdown] +# ### Experiment 2: PDF (NOMAD, TOF) +# +# #### Download Data + +# %% +pdf_data_path = download_data(id=5, destination='data') + +# %% [markdown] +# #### Create Experiment + +# %% +pdf_expt = ExperimentFactory.from_data_path( + name='nomad', + data_path=pdf_data_path, + beam_mode='time-of-flight', + scattering_type='total', +) + +# %% [markdown] +# #### Set Peak Profile (PDF Parameters) + +# %% +pdf_expt.peak.damp_q = 0.02 +pdf_expt.peak.broad_q = 0.02 +pdf_expt.peak.cutoff_q = 35.0 +pdf_expt.peak.sharp_delta_1 = 0.001 +pdf_expt.peak.sharp_delta_2 = 4.0 +pdf_expt.peak.damp_particle_diameter = 0 + +# %% [markdown] +# #### Set Linked Phases + +# %% +pdf_expt.linked_phases.create(id='si', scale=1.0) + +# %% [markdown] +# ## Define Project +# +# The project object manages the shared structure, both experiments, +# and the analysis. +# +# #### Create Project + +# %% +project = Project() + +# %% [markdown] +# #### Add Structure + +# %% +project.structures.add(structure) + +# %% [markdown] +# #### Add Experiments + +# %% +project.experiments.add(bragg_expt) +project.experiments.add(pdf_expt) + +# %% [markdown] +# ## Perform Analysis +# +# This section shows the joint analysis process. The calculator is +# auto-resolved per experiment: CrysPy for Bragg, PDFfit for PDF. +# +# #### Set Fit Mode and Weights + +# %% +project.analysis.fit_mode.mode = 'joint' +project.analysis.joint_fit_experiments.create(id='sepd', weight=0.7) +project.analysis.joint_fit_experiments.create(id='nomad', weight=0.3) + +# %% [markdown] +# #### Set Minimizer + +# %% +project.analysis.current_minimizer = 'lmfit' + +# %% [markdown] +# #### Plot Measured vs Calculated (Before Fit) + +# %% +project.plot_meas_vs_calc(expt_name='sepd', show_residual=False) + +# %% +project.plot_meas_vs_calc(expt_name='nomad', show_residual=False) + +# %% [markdown] +# #### Set Fitting Parameters +# +# Shared structural parameters are refined against both datasets +# simultaneously. + +# %% +structure.cell.length_a.free = True +structure.atom_sites['Si'].b_iso.free = True + +# %% [markdown] +# Bragg experiment parameters. + +# %% +bragg_expt.linked_phases['si'].scale.free = True +bragg_expt.instrument.calib_d_to_tof_offset.free = True +bragg_expt.peak.broad_gauss_sigma_0.free = True +bragg_expt.peak.broad_gauss_sigma_1.free = True +bragg_expt.peak.broad_gauss_sigma_2.free = True +for point in bragg_expt.background: + point.y.free = True + +# %% [markdown] +# PDF experiment parameters. + +# %% +pdf_expt.linked_phases['si'].scale.free = True +pdf_expt.peak.damp_q.free = True +pdf_expt.peak.broad_q.free = True +pdf_expt.peak.sharp_delta_1.free = True +pdf_expt.peak.sharp_delta_2.free = True + +# %% [markdown] +# #### Show Free Parameters + +# %% +project.analysis.show_free_params() + +# %% [markdown] +# #### Run Fitting + +# %% +project.analysis.fit() +project.analysis.show_fit_results() + +# %% [markdown] +# #### Plot Measured vs Calculated (After Fit) + +# %% +project.plot_meas_vs_calc(expt_name='sepd', show_residual=False) + +# %% +project.plot_meas_vs_calc(expt_name='nomad', show_residual=False) + + +# %% diff --git a/docs/docs/tutorials/ed-17.ipynb b/docs/docs/tutorials/ed-17.ipynb new file mode 100644 index 00000000..a6500f67 --- /dev/null +++ b/docs/docs/tutorials/ed-17.ipynb @@ -0,0 +1,658 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0", + "metadata": {}, + "source": [ + "# Structure Refinement: Co2SiO4, D20 (T-scan)\n", + "\n", + "This example demonstrates a Rietveld refinement of the Co2SiO4 crystal\n", + "structure using constant-wavelength neutron powder diffraction data\n", + "from D20 at ILL. A sequential refinement of the same structure against\n", + "a temperature scan is performed to show how to manage multiple\n", + "experiments in a project." + ] + }, + { + "cell_type": "markdown", + "id": "1", + "metadata": {}, + "source": [ + "## Import Library" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2", + "metadata": {}, + "outputs": [], + "source": [ + "import easydiffraction as ed" + ] + }, + { + "cell_type": "markdown", + "id": "3", + "metadata": {}, + "source": [ + "## Step 1: Define Project\n", + "\n", + "The project object manages structures, experiments, and analysis." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4", + "metadata": {}, + "outputs": [], + "source": [ + "project = ed.Project()" + ] + }, + { + "cell_type": "markdown", + "id": "5", + "metadata": {}, + "source": [ + "Set output verbosity level to \"short\" to show only one-line status\n", + "messages during the analysis process." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6", + "metadata": {}, + "outputs": [], + "source": [ + "project.verbosity = 'short'" + ] + }, + { + "cell_type": "markdown", + "id": "7", + "metadata": {}, + "source": [ + "## Step 2: Define Crystal Structure\n", + "\n", + "This section shows how to add structures and modify their\n", + "parameters.\n", + "\n", + "#### Create Structure" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8", + "metadata": {}, + "outputs": [], + "source": [ + "project.structures.create(name='cosio')\n", + "structure = project.structures['cosio']" + ] + }, + { + "cell_type": "markdown", + "id": "9", + "metadata": {}, + "source": [ + "#### Set Space Group" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "10", + "metadata": {}, + "outputs": [], + "source": [ + "structure.space_group.name_h_m = 'P n m a'\n", + "structure.space_group.it_coordinate_system_code = 'abc'" + ] + }, + { + "cell_type": "markdown", + "id": "11", + "metadata": {}, + "source": [ + "#### Set Unit Cell" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "12", + "metadata": {}, + "outputs": [], + "source": [ + "structure.cell.length_a = 10.31\n", + "structure.cell.length_b = 6.0\n", + "structure.cell.length_c = 4.79" + ] + }, + { + "cell_type": "markdown", + "id": "13", + "metadata": {}, + "source": [ + "#### Set Atom Sites" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "14", + "metadata": {}, + "outputs": [], + "source": [ + "structure.atom_sites.create(\n", + " label='Co1',\n", + " type_symbol='Co',\n", + " fract_x=0,\n", + " fract_y=0,\n", + " fract_z=0,\n", + " wyckoff_letter='a',\n", + " b_iso=0.3,\n", + ")\n", + "structure.atom_sites.create(\n", + " label='Co2',\n", + " type_symbol='Co',\n", + " fract_x=0.279,\n", + " fract_y=0.25,\n", + " fract_z=0.985,\n", + " wyckoff_letter='c',\n", + " b_iso=0.3,\n", + ")\n", + "structure.atom_sites.create(\n", + " label='Si',\n", + " type_symbol='Si',\n", + " fract_x=0.094,\n", + " fract_y=0.25,\n", + " fract_z=0.429,\n", + " wyckoff_letter='c',\n", + " b_iso=0.34,\n", + ")\n", + "structure.atom_sites.create(\n", + " label='O1',\n", + " type_symbol='O',\n", + " fract_x=0.091,\n", + " fract_y=0.25,\n", + " fract_z=0.771,\n", + " wyckoff_letter='c',\n", + " b_iso=0.63,\n", + ")\n", + "structure.atom_sites.create(\n", + " label='O2',\n", + " type_symbol='O',\n", + " fract_x=0.448,\n", + " fract_y=0.25,\n", + " fract_z=0.217,\n", + " wyckoff_letter='c',\n", + " b_iso=0.59,\n", + ")\n", + "structure.atom_sites.create(\n", + " label='O3',\n", + " type_symbol='O',\n", + " fract_x=0.164,\n", + " fract_y=0.032,\n", + " fract_z=0.28,\n", + " wyckoff_letter='d',\n", + " b_iso=0.83,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "15", + "metadata": {}, + "source": [ + "## Step 3: Define Experiments\n", + "\n", + "This section shows how to add experiments, configure their parameters,\n", + "and link the structures defined above.\n", + "\n", + "#### Download Measured Data" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "16", + "metadata": {}, + "outputs": [], + "source": [ + "file_path = ed.download_data(id=27, destination='data')" + ] + }, + { + "cell_type": "markdown", + "id": "17", + "metadata": {}, + "source": [ + "#### Create Experiments and Set Temperature" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "18", + "metadata": {}, + "outputs": [], + "source": [ + "data_paths = ed.extract_data_paths_from_zip(file_path)\n", + "for i, data_path in enumerate(data_paths, start=1):\n", + " name = f'd20_{i}'\n", + " project.experiments.add_from_data_path(\n", + " name=name,\n", + " data_path=data_path,\n", + " )\n", + " expt = project.experiments[name]\n", + " expt.diffrn.ambient_temperature = ed.extract_metadata(\n", + " file_path=data_path,\n", + " pattern=r'^TEMP\\s+([0-9.]+)',\n", + " )" + ] + }, + { + "cell_type": "markdown", + "id": "19", + "metadata": {}, + "source": [ + "#### Set Instrument" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "20", + "metadata": {}, + "outputs": [], + "source": [ + "for expt in project.experiments:\n", + " expt.instrument.setup_wavelength = 1.87\n", + " expt.instrument.calib_twotheta_offset = 0.29" + ] + }, + { + "cell_type": "markdown", + "id": "21", + "metadata": {}, + "source": [ + "#### Set Peak Profile" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "22", + "metadata": {}, + "outputs": [], + "source": [ + "for expt in project.experiments:\n", + " expt.peak.broad_gauss_u = 0.24\n", + " expt.peak.broad_gauss_v = -0.53\n", + " expt.peak.broad_gauss_w = 0.38\n", + " expt.peak.broad_lorentz_y = 0.02" + ] + }, + { + "cell_type": "markdown", + "id": "23", + "metadata": {}, + "source": [ + "#### Set Excluded Regions" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "24", + "metadata": {}, + "outputs": [], + "source": [ + "for expt in project.experiments:\n", + " expt.excluded_regions.create(id='1', start=0, end=8)\n", + " expt.excluded_regions.create(id='2', start=150, end=180)" + ] + }, + { + "cell_type": "markdown", + "id": "25", + "metadata": {}, + "source": [ + "#### Set Background" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "26", + "metadata": {}, + "outputs": [], + "source": [ + "for expt in project.experiments:\n", + " expt.background.create(id='1', x=8, y=609)\n", + " expt.background.create(id='2', x=9, y=581)\n", + " expt.background.create(id='3', x=10, y=563)\n", + " expt.background.create(id='4', x=11, y=540)\n", + " expt.background.create(id='5', x=12, y=520)\n", + " expt.background.create(id='6', x=15, y=507)\n", + " expt.background.create(id='7', x=25, y=463)\n", + " expt.background.create(id='8', x=30, y=434)\n", + " expt.background.create(id='9', x=50, y=451)\n", + " expt.background.create(id='10', x=70, y=431)\n", + " expt.background.create(id='11', x=90, y=414)\n", + " expt.background.create(id='12', x=110, y=361)\n", + " expt.background.create(id='13', x=130, y=292)\n", + " expt.background.create(id='14', x=150, y=241)" + ] + }, + { + "cell_type": "markdown", + "id": "27", + "metadata": {}, + "source": [ + "#### Set Linked Phases" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "28", + "metadata": {}, + "outputs": [], + "source": [ + "for expt in project.experiments:\n", + " expt.linked_phases.create(id='cosio', scale=1.2)" + ] + }, + { + "cell_type": "markdown", + "id": "29", + "metadata": {}, + "source": [ + "## Step 4: Perform Analysis\n", + "\n", + "This section shows how to set free parameters, define constraints,\n", + "and run the refinement." + ] + }, + { + "cell_type": "markdown", + "id": "30", + "metadata": {}, + "source": [ + "#### Set Free Parameters" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "31", + "metadata": {}, + "outputs": [], + "source": [ + "structure.cell.length_a.free = True\n", + "structure.cell.length_b.free = True\n", + "structure.cell.length_c.free = True\n", + "\n", + "structure.atom_sites['Co2'].fract_x.free = True\n", + "structure.atom_sites['Co2'].fract_z.free = True\n", + "structure.atom_sites['Si'].fract_x.free = True\n", + "structure.atom_sites['Si'].fract_z.free = True\n", + "structure.atom_sites['O1'].fract_x.free = True\n", + "structure.atom_sites['O1'].fract_z.free = True\n", + "structure.atom_sites['O2'].fract_x.free = True\n", + "structure.atom_sites['O2'].fract_z.free = True\n", + "structure.atom_sites['O3'].fract_x.free = True\n", + "structure.atom_sites['O3'].fract_y.free = True\n", + "structure.atom_sites['O3'].fract_z.free = True\n", + "\n", + "structure.atom_sites['Co1'].b_iso.free = True\n", + "structure.atom_sites['Co2'].b_iso.free = True\n", + "structure.atom_sites['Si'].b_iso.free = True\n", + "structure.atom_sites['O1'].b_iso.free = True\n", + "structure.atom_sites['O2'].b_iso.free = True\n", + "structure.atom_sites['O3'].b_iso.free = True" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "32", + "metadata": {}, + "outputs": [], + "source": [ + "for expt in project.experiments:\n", + " expt.linked_phases['cosio'].scale.free = True\n", + "\n", + " expt.instrument.calib_twotheta_offset.free = True\n", + "\n", + " expt.peak.broad_gauss_u.free = True\n", + " expt.peak.broad_gauss_v.free = True\n", + " expt.peak.broad_gauss_w.free = True\n", + " expt.peak.broad_lorentz_y.free = True\n", + "\n", + " for point in expt.background:\n", + " point.y.free = True" + ] + }, + { + "cell_type": "markdown", + "id": "33", + "metadata": {}, + "source": [ + "#### Set Constraints\n", + "\n", + "Set aliases for parameters." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "34", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.aliases.create(\n", + " label='biso_Co1',\n", + " param_uid=structure.atom_sites['Co1'].b_iso.uid,\n", + ")\n", + "project.analysis.aliases.create(\n", + " label='biso_Co2',\n", + " param_uid=structure.atom_sites['Co2'].b_iso.uid,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "35", + "metadata": {}, + "source": [ + "Set constraints." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "36", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.constraints.create(\n", + " expression='biso_Co2 = biso_Co1',\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "37", + "metadata": {}, + "source": [ + "Apply constraints." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "38", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.apply_constraints()" + ] + }, + { + "cell_type": "markdown", + "id": "39", + "metadata": {}, + "source": [ + "#### Set Fit Mode" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "40", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.fit_mode.mode = 'single'" + ] + }, + { + "cell_type": "markdown", + "id": "41", + "metadata": {}, + "source": [ + "#### Run Fitting" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "42", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.fit()" + ] + }, + { + "cell_type": "markdown", + "id": "43", + "metadata": {}, + "source": [ + "#### Plot Measured vs Calculated" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "44", + "metadata": {}, + "outputs": [], + "source": [ + "last_expt_name = project.experiments.names[-1]\n", + "project.plot_meas_vs_calc(expt_name=last_expt_name, show_residual=True)" + ] + }, + { + "cell_type": "markdown", + "id": "45", + "metadata": {}, + "source": [ + "#### Plot Parameter Evolution\n", + "\n", + "Define the quantity to use as the x-axis in the following plots." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "46", + "metadata": {}, + "outputs": [], + "source": [ + "temperature = project.experiments[0].diffrn.ambient_temperature" + ] + }, + { + "cell_type": "markdown", + "id": "47", + "metadata": {}, + "source": [ + "Plot unit cell parameters vs. temperature." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "48", + "metadata": {}, + "outputs": [], + "source": [ + "project.plot_param_series(structure.cell.length_a, versus=temperature)\n", + "project.plot_param_series(structure.cell.length_b, versus=temperature)\n", + "project.plot_param_series(structure.cell.length_c, versus=temperature)" + ] + }, + { + "cell_type": "markdown", + "id": "49", + "metadata": {}, + "source": [ + "Plot isotropic displacement parameters vs. temperature." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "50", + "metadata": {}, + "outputs": [], + "source": [ + "project.plot_param_series(structure.atom_sites['Co1'].b_iso, versus=temperature)\n", + "project.plot_param_series(structure.atom_sites['Si'].b_iso, versus=temperature)\n", + "project.plot_param_series(structure.atom_sites['O1'].b_iso, versus=temperature)\n", + "project.plot_param_series(structure.atom_sites['O2'].b_iso, versus=temperature)\n", + "project.plot_param_series(structure.atom_sites['O3'].b_iso, versus=temperature)" + ] + }, + { + "cell_type": "markdown", + "id": "51", + "metadata": {}, + "source": [ + "Plot selected fractional coordinates vs. temperature." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "52", + "metadata": {}, + "outputs": [], + "source": [ + "project.plot_param_series(structure.atom_sites['Co2'].fract_x, versus=temperature)\n", + "project.plot_param_series(structure.atom_sites['Co2'].fract_z, versus=temperature)\n", + "project.plot_param_series(structure.atom_sites['O1'].fract_z, versus=temperature)\n", + "project.plot_param_series(structure.atom_sites['O2'].fract_z, versus=temperature)\n", + "project.plot_param_series(structure.atom_sites['O3'].fract_z, versus=temperature)" + ] + } + ], + "metadata": { + "jupytext": { + "cell_metadata_filter": "-all", + "main_language": "python", + "notebook_metadata_filter": "-all" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/docs/tutorials/ed-17.py b/docs/docs/tutorials/ed-17.py new file mode 100644 index 00000000..af06031f --- /dev/null +++ b/docs/docs/tutorials/ed-17.py @@ -0,0 +1,327 @@ +# %% [markdown] +# # Structure Refinement: Co2SiO4, D20 (T-scan) +# +# This example demonstrates a Rietveld refinement of the Co2SiO4 crystal +# structure using constant-wavelength neutron powder diffraction data +# from D20 at ILL. A sequential refinement of the same structure against +# a temperature scan is performed to show how to manage multiple +# experiments in a project. + +# %% [markdown] +# ## Import Library + +# %% +import easydiffraction as ed + +# %% [markdown] +# ## Step 1: Define Project +# +# The project object manages structures, experiments, and analysis. + +# %% +project = ed.Project() + +# %% [markdown] +# Set output verbosity level to "short" to show only one-line status +# messages during the analysis process. + +# %% +project.verbosity = 'short' + +# %% [markdown] +# ## Step 2: Define Crystal Structure +# +# This section shows how to add structures and modify their +# parameters. +# +# #### Create Structure + +# %% +project.structures.create(name='cosio') +structure = project.structures['cosio'] + +# %% [markdown] +# #### Set Space Group + +# %% +structure.space_group.name_h_m = 'P n m a' +structure.space_group.it_coordinate_system_code = 'abc' + +# %% [markdown] +# #### Set Unit Cell + +# %% +structure.cell.length_a = 10.31 +structure.cell.length_b = 6.0 +structure.cell.length_c = 4.79 + +# %% [markdown] +# #### Set Atom Sites + +# %% +structure.atom_sites.create( + label='Co1', + type_symbol='Co', + fract_x=0, + fract_y=0, + fract_z=0, + wyckoff_letter='a', + b_iso=0.3, +) +structure.atom_sites.create( + label='Co2', + type_symbol='Co', + fract_x=0.279, + fract_y=0.25, + fract_z=0.985, + wyckoff_letter='c', + b_iso=0.3, +) +structure.atom_sites.create( + label='Si', + type_symbol='Si', + fract_x=0.094, + fract_y=0.25, + fract_z=0.429, + wyckoff_letter='c', + b_iso=0.34, +) +structure.atom_sites.create( + label='O1', + type_symbol='O', + fract_x=0.091, + fract_y=0.25, + fract_z=0.771, + wyckoff_letter='c', + b_iso=0.63, +) +structure.atom_sites.create( + label='O2', + type_symbol='O', + fract_x=0.448, + fract_y=0.25, + fract_z=0.217, + wyckoff_letter='c', + b_iso=0.59, +) +structure.atom_sites.create( + label='O3', + type_symbol='O', + fract_x=0.164, + fract_y=0.032, + fract_z=0.28, + wyckoff_letter='d', + b_iso=0.83, +) + +# %% [markdown] +# ## Step 3: Define Experiments +# +# This section shows how to add experiments, configure their parameters, +# and link the structures defined above. +# +# #### Download Measured Data + +# %% +file_path = ed.download_data(id=27, destination='data') + +# %% [markdown] +# #### Create Experiments and Set Temperature + +# %% +data_paths = ed.extract_data_paths_from_zip(file_path) +for i, data_path in enumerate(data_paths, start=1): + name = f'd20_{i}' + project.experiments.add_from_data_path( + name=name, + data_path=data_path, + ) + expt = project.experiments[name] + expt.diffrn.ambient_temperature = ed.extract_metadata( + file_path=data_path, + pattern=r'^TEMP\s+([0-9.]+)', + ) + +# %% [markdown] +# #### Set Instrument + +# %% +for expt in project.experiments: + expt.instrument.setup_wavelength = 1.87 + expt.instrument.calib_twotheta_offset = 0.29 + +# %% [markdown] +# #### Set Peak Profile + +# %% +for expt in project.experiments: + expt.peak.broad_gauss_u = 0.24 + expt.peak.broad_gauss_v = -0.53 + expt.peak.broad_gauss_w = 0.38 + expt.peak.broad_lorentz_y = 0.02 + +# %% [markdown] +# #### Set Excluded Regions + +# %% +for expt in project.experiments: + expt.excluded_regions.create(id='1', start=0, end=8) + expt.excluded_regions.create(id='2', start=150, end=180) + +# %% [markdown] +# #### Set Background + +# %% +for expt in project.experiments: + expt.background.create(id='1', x=8, y=609) + expt.background.create(id='2', x=9, y=581) + expt.background.create(id='3', x=10, y=563) + expt.background.create(id='4', x=11, y=540) + expt.background.create(id='5', x=12, y=520) + expt.background.create(id='6', x=15, y=507) + expt.background.create(id='7', x=25, y=463) + expt.background.create(id='8', x=30, y=434) + expt.background.create(id='9', x=50, y=451) + expt.background.create(id='10', x=70, y=431) + expt.background.create(id='11', x=90, y=414) + expt.background.create(id='12', x=110, y=361) + expt.background.create(id='13', x=130, y=292) + expt.background.create(id='14', x=150, y=241) + +# %% [markdown] +# #### Set Linked Phases + +# %% +for expt in project.experiments: + expt.linked_phases.create(id='cosio', scale=1.2) + +# %% [markdown] +# ## Step 4: Perform Analysis +# +# This section shows how to set free parameters, define constraints, +# and run the refinement. + +# %% [markdown] +# #### Set Free Parameters + +# %% +structure.cell.length_a.free = True +structure.cell.length_b.free = True +structure.cell.length_c.free = True + +structure.atom_sites['Co2'].fract_x.free = True +structure.atom_sites['Co2'].fract_z.free = True +structure.atom_sites['Si'].fract_x.free = True +structure.atom_sites['Si'].fract_z.free = True +structure.atom_sites['O1'].fract_x.free = True +structure.atom_sites['O1'].fract_z.free = True +structure.atom_sites['O2'].fract_x.free = True +structure.atom_sites['O2'].fract_z.free = True +structure.atom_sites['O3'].fract_x.free = True +structure.atom_sites['O3'].fract_y.free = True +structure.atom_sites['O3'].fract_z.free = True + +structure.atom_sites['Co1'].b_iso.free = True +structure.atom_sites['Co2'].b_iso.free = True +structure.atom_sites['Si'].b_iso.free = True +structure.atom_sites['O1'].b_iso.free = True +structure.atom_sites['O2'].b_iso.free = True +structure.atom_sites['O3'].b_iso.free = True + +# %% +for expt in project.experiments: + expt.linked_phases['cosio'].scale.free = True + + expt.instrument.calib_twotheta_offset.free = True + + expt.peak.broad_gauss_u.free = True + expt.peak.broad_gauss_v.free = True + expt.peak.broad_gauss_w.free = True + expt.peak.broad_lorentz_y.free = True + + for point in expt.background: + point.y.free = True + +# %% [markdown] +# #### Set Constraints +# +# Set aliases for parameters. + +# %% +project.analysis.aliases.create( + label='biso_Co1', + param_uid=structure.atom_sites['Co1'].b_iso.uid, +) +project.analysis.aliases.create( + label='biso_Co2', + param_uid=structure.atom_sites['Co2'].b_iso.uid, +) + +# %% [markdown] +# Set constraints. + +# %% +project.analysis.constraints.create( + expression='biso_Co2 = biso_Co1', +) + +# %% [markdown] +# Apply constraints. + +# %% +project.analysis.apply_constraints() + +# %% [markdown] +# #### Set Fit Mode + +# %% +project.analysis.fit_mode.mode = 'single' + +# %% [markdown] +# #### Run Fitting + +# %% +project.analysis.fit() + +# %% [markdown] +# #### Plot Measured vs Calculated + +# %% +last_expt_name = project.experiments.names[-1] +project.plot_meas_vs_calc(expt_name=last_expt_name, show_residual=True) + +# %% [markdown] +# #### Plot Parameter Evolution +# +# Define the quantity to use as the x-axis in the following plots. + +# %% +temperature = project.experiments[0].diffrn.ambient_temperature + +# %% [markdown] +# Plot unit cell parameters vs. temperature. + +# %% +project.plot_param_series(structure.cell.length_a, versus=temperature) +project.plot_param_series(structure.cell.length_b, versus=temperature) +project.plot_param_series(structure.cell.length_c, versus=temperature) + +# %% [markdown] +# Plot isotropic displacement parameters vs. temperature. + +# %% +project.plot_param_series(structure.atom_sites['Co1'].b_iso, versus=temperature) +project.plot_param_series(structure.atom_sites['Si'].b_iso, versus=temperature) +project.plot_param_series(structure.atom_sites['O1'].b_iso, versus=temperature) +project.plot_param_series(structure.atom_sites['O2'].b_iso, versus=temperature) +project.plot_param_series(structure.atom_sites['O3'].b_iso, versus=temperature) + +# %% [markdown] +# Plot selected fractional coordinates vs. temperature. + +# %% +project.plot_param_series(structure.atom_sites['Co2'].fract_x, versus=temperature) +project.plot_param_series(structure.atom_sites['Co2'].fract_z, versus=temperature) +project.plot_param_series(structure.atom_sites['O1'].fract_z, versus=temperature) +project.plot_param_series(structure.atom_sites['O2'].fract_z, versus=temperature) +project.plot_param_series(structure.atom_sites['O3'].fract_z, versus=temperature) diff --git a/docs/docs/tutorials/ed-2.ipynb b/docs/docs/tutorials/ed-2.ipynb new file mode 100644 index 00000000..0a4af0f3 --- /dev/null +++ b/docs/docs/tutorials/ed-2.ipynb @@ -0,0 +1,344 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0", + "metadata": {}, + "source": [ + "# Structure Refinement: LBCO, HRPT\n", + "\n", + "This minimalistic example is designed to show how Rietveld refinement\n", + "can be performed when both the crystal structure and experiment are\n", + "defined directly in code. Only the experimentally measured data is\n", + "loaded from an external file.\n", + "\n", + "For this example, constant-wavelength neutron powder diffraction data\n", + "for La0.5Ba0.5CoO3 from HRPT at PSI is used.\n", + "\n", + "It does not contain any advanced features or options, and includes no\n", + "comments or explanations—these can be found in the other tutorials.\n", + "Default values are used for all parameters if not specified. Only\n", + "essential and self-explanatory code is provided.\n", + "\n", + "The example is intended for users who are already familiar with the\n", + "EasyDiffraction library and want to quickly get started with a simple\n", + "refinement. It is also useful for those who want to see what a\n", + "refinement might look like in code. For a more detailed explanation of\n", + "the code, please refer to the other tutorials." + ] + }, + { + "cell_type": "markdown", + "id": "1", + "metadata": {}, + "source": [ + "## Import Library" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2", + "metadata": {}, + "outputs": [], + "source": [ + "import easydiffraction as ed" + ] + }, + { + "cell_type": "markdown", + "id": "3", + "metadata": {}, + "source": [ + "## Step 1: Define Project" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4", + "metadata": {}, + "outputs": [], + "source": [ + "project = ed.Project()" + ] + }, + { + "cell_type": "markdown", + "id": "5", + "metadata": {}, + "source": [ + "## Step 2: Define Structure" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6", + "metadata": {}, + "outputs": [], + "source": [ + "project.structures.create(name='lbco')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7", + "metadata": {}, + "outputs": [], + "source": [ + "structure = project.structures['lbco']" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8", + "metadata": {}, + "outputs": [], + "source": [ + "structure.space_group.name_h_m = 'P m -3 m'\n", + "structure.space_group.it_coordinate_system_code = '1'" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9", + "metadata": {}, + "outputs": [], + "source": [ + "structure.cell.length_a = 3.88" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "10", + "metadata": {}, + "outputs": [], + "source": [ + "structure.atom_sites.create(\n", + " label='La',\n", + " type_symbol='La',\n", + " fract_x=0,\n", + " fract_y=0,\n", + " fract_z=0,\n", + " wyckoff_letter='a',\n", + " b_iso=0.5,\n", + " occupancy=0.5,\n", + ")\n", + "structure.atom_sites.create(\n", + " label='Ba',\n", + " type_symbol='Ba',\n", + " fract_x=0,\n", + " fract_y=0,\n", + " fract_z=0,\n", + " wyckoff_letter='a',\n", + " b_iso=0.5,\n", + " occupancy=0.5,\n", + ")\n", + "structure.atom_sites.create(\n", + " label='Co',\n", + " type_symbol='Co',\n", + " fract_x=0.5,\n", + " fract_y=0.5,\n", + " fract_z=0.5,\n", + " wyckoff_letter='b',\n", + " b_iso=0.5,\n", + ")\n", + "structure.atom_sites.create(\n", + " label='O',\n", + " type_symbol='O',\n", + " fract_x=0,\n", + " fract_y=0.5,\n", + " fract_z=0.5,\n", + " wyckoff_letter='c',\n", + " b_iso=0.5,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "11", + "metadata": {}, + "source": [ + "## Step 3: Define Experiment" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "12", + "metadata": {}, + "outputs": [], + "source": [ + "data_path = ed.download_data(id=3, destination='data')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "13", + "metadata": {}, + "outputs": [], + "source": [ + "project.experiments.add_from_data_path(\n", + " name='hrpt',\n", + " data_path=data_path,\n", + " sample_form='powder',\n", + " beam_mode='constant wavelength',\n", + " radiation_probe='neutron',\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "14", + "metadata": {}, + "outputs": [], + "source": [ + "experiment = project.experiments['hrpt']" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "15", + "metadata": {}, + "outputs": [], + "source": [ + "experiment.instrument.setup_wavelength = 1.494\n", + "experiment.instrument.calib_twotheta_offset = 0.6" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "16", + "metadata": {}, + "outputs": [], + "source": [ + "experiment.peak.broad_gauss_u = 0.1\n", + "experiment.peak.broad_gauss_v = -0.1\n", + "experiment.peak.broad_gauss_w = 0.1\n", + "experiment.peak.broad_lorentz_y = 0.1" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "17", + "metadata": {}, + "outputs": [], + "source": [ + "experiment.background.create(id='1', x=10, y=170)\n", + "experiment.background.create(id='2', x=30, y=170)\n", + "experiment.background.create(id='3', x=50, y=170)\n", + "experiment.background.create(id='4', x=110, y=170)\n", + "experiment.background.create(id='5', x=165, y=170)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "18", + "metadata": {}, + "outputs": [], + "source": [ + "experiment.excluded_regions.create(id='1', start=0, end=5)\n", + "experiment.excluded_regions.create(id='2', start=165, end=180)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "19", + "metadata": {}, + "outputs": [], + "source": [ + "experiment.linked_phases.create(id='lbco', scale=10.0)" + ] + }, + { + "cell_type": "markdown", + "id": "20", + "metadata": {}, + "source": [ + "## Step 4: Perform Analysis" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "21", + "metadata": {}, + "outputs": [], + "source": [ + "structure.cell.length_a.free = True\n", + "\n", + "structure.atom_sites['La'].b_iso.free = True\n", + "structure.atom_sites['Ba'].b_iso.free = True\n", + "structure.atom_sites['Co'].b_iso.free = True\n", + "structure.atom_sites['O'].b_iso.free = True" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "22", + "metadata": { + "lines_to_next_cell": 2 + }, + "outputs": [], + "source": [ + "experiment.instrument.calib_twotheta_offset.free = True\n", + "\n", + "experiment.peak.broad_gauss_u.free = True\n", + "experiment.peak.broad_gauss_v.free = True\n", + "experiment.peak.broad_gauss_w.free = True\n", + "experiment.peak.broad_lorentz_y.free = True\n", + "\n", + "experiment.background['1'].y.free = True\n", + "experiment.background['2'].y.free = True\n", + "experiment.background['3'].y.free = True\n", + "experiment.background['4'].y.free = True\n", + "experiment.background['5'].y.free = True\n", + "\n", + "experiment.linked_phases['lbco'].scale.free = True" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "23", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.fit()\n", + "project.analysis.show_fit_results()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "24", + "metadata": {}, + "outputs": [], + "source": [ + "project.plot_meas_vs_calc(expt_name='hrpt', show_residual=True)" + ] + } + ], + "metadata": { + "jupytext": { + "cell_metadata_filter": "-all", + "main_language": "python", + "notebook_metadata_filter": "-all" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/tutorials/ed-2.py b/docs/docs/tutorials/ed-2.py similarity index 69% rename from tutorials/ed-2.py rename to docs/docs/tutorials/ed-2.py index 4390b65b..3c8d033e 100644 --- a/tutorials/ed-2.py +++ b/docs/docs/tutorials/ed-2.py @@ -2,9 +2,9 @@ # # Structure Refinement: LBCO, HRPT # # This minimalistic example is designed to show how Rietveld refinement -# of a crystal structure can be performed when both the sample model and -# experiment are defined directly in code. Only the experimentally -# measured data is loaded from an external file. +# can be performed when both the crystal structure and experiment are +# defined directly in code. Only the experimentally measured data is +# loaded from an external file. # # For this example, constant-wavelength neutron powder diffraction data # for La0.5Ba0.5CoO3 from HRPT at PSI is used. @@ -33,23 +33,23 @@ project = ed.Project() # %% [markdown] -# ## Step 2: Define Sample Model +# ## Step 2: Define Structure # %% -project.sample_models.add(name='lbco') +project.structures.create(name='lbco') # %% -sample_model = project.sample_models['lbco'] +structure = project.structures['lbco'] # %% -sample_model.space_group.name_h_m = 'P m -3 m' -sample_model.space_group.it_coordinate_system_code = '1' +structure.space_group.name_h_m = 'P m -3 m' +structure.space_group.it_coordinate_system_code = '1' # %% -sample_model.cell.length_a = 3.88 +structure.cell.length_a = 3.88 # %% -sample_model.atom_sites.add( +structure.atom_sites.create( label='La', type_symbol='La', fract_x=0, @@ -59,7 +59,7 @@ b_iso=0.5, occupancy=0.5, ) -sample_model.atom_sites.add( +structure.atom_sites.create( label='Ba', type_symbol='Ba', fract_x=0, @@ -69,7 +69,7 @@ b_iso=0.5, occupancy=0.5, ) -sample_model.atom_sites.add( +structure.atom_sites.create( label='Co', type_symbol='Co', fract_x=0.5, @@ -78,7 +78,7 @@ wyckoff_letter='b', b_iso=0.5, ) -sample_model.atom_sites.add( +structure.atom_sites.create( label='O', type_symbol='O', fract_x=0, @@ -95,7 +95,7 @@ data_path = ed.download_data(id=3, destination='data') # %% -project.experiments.add( +project.experiments.add_from_data_path( name='hrpt', data_path=data_path, sample_form='powder', @@ -117,29 +117,29 @@ experiment.peak.broad_lorentz_y = 0.1 # %% -experiment.background.add(id='1', x=10, y=170) -experiment.background.add(id='2', x=30, y=170) -experiment.background.add(id='3', x=50, y=170) -experiment.background.add(id='4', x=110, y=170) -experiment.background.add(id='5', x=165, y=170) +experiment.background.create(id='1', x=10, y=170) +experiment.background.create(id='2', x=30, y=170) +experiment.background.create(id='3', x=50, y=170) +experiment.background.create(id='4', x=110, y=170) +experiment.background.create(id='5', x=165, y=170) # %% -experiment.excluded_regions.add(id='1', start=0, end=5) -experiment.excluded_regions.add(id='2', start=165, end=180) +experiment.excluded_regions.create(id='1', start=0, end=5) +experiment.excluded_regions.create(id='2', start=165, end=180) # %% -experiment.linked_phases.add(id='lbco', scale=10.0) +experiment.linked_phases.create(id='lbco', scale=10.0) # %% [markdown] # ## Step 4: Perform Analysis # %% -sample_model.cell.length_a.free = True +structure.cell.length_a.free = True -sample_model.atom_sites['La'].b_iso.free = True -sample_model.atom_sites['Ba'].b_iso.free = True -sample_model.atom_sites['Co'].b_iso.free = True -sample_model.atom_sites['O'].b_iso.free = True +structure.atom_sites['La'].b_iso.free = True +structure.atom_sites['Ba'].b_iso.free = True +structure.atom_sites['Co'].b_iso.free = True +structure.atom_sites['O'].b_iso.free = True # %% experiment.instrument.calib_twotheta_offset.free = True diff --git a/docs/docs/tutorials/ed-3.ipynb b/docs/docs/tutorials/ed-3.ipynb new file mode 100644 index 00000000..5104fcab --- /dev/null +++ b/docs/docs/tutorials/ed-3.ipynb @@ -0,0 +1,1804 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0", + "metadata": {}, + "source": [ + "# Structure Refinement: LBCO, HRPT\n", + "\n", + "This example demonstrates how to use the EasyDiffraction API in a\n", + "simplified, user-friendly manner that closely follows the GUI workflow\n", + "for a Rietveld refinement of La0.5Ba0.5CoO3 crystal structure using\n", + "constant wavelength neutron powder diffraction data from HRPT at PSI.\n", + "\n", + "It is intended for users with minimal programming experience who want\n", + "to learn how to perform standard crystal structure fitting using\n", + "diffraction data. This script covers creating a project, adding\n", + "crystal structures and experiments, performing analysis, and refining\n", + "parameters.\n", + "\n", + "Only a single import of `easydiffraction` is required, and all\n", + "operations are performed through high-level components of the\n", + "`project` object, such as `project.structures`,\n", + "`project.experiments`, and `project.analysis`. The `project` object is\n", + "the main container for all information." + ] + }, + { + "cell_type": "markdown", + "id": "1", + "metadata": {}, + "source": [ + "## Import Library" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2", + "metadata": {}, + "outputs": [], + "source": [ + "import easydiffraction as ed" + ] + }, + { + "cell_type": "markdown", + "id": "3", + "metadata": {}, + "source": [ + "## Step 1: Create a Project\n", + "\n", + "This section explains how to create a project and define its metadata." + ] + }, + { + "cell_type": "markdown", + "id": "4", + "metadata": {}, + "source": [ + "#### Create Project" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5", + "metadata": {}, + "outputs": [], + "source": [ + "project = ed.Project(name='lbco_hrpt')" + ] + }, + { + "cell_type": "markdown", + "id": "6", + "metadata": {}, + "source": [ + "#### Set Project Metadata" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7", + "metadata": {}, + "outputs": [], + "source": [ + "project.info.title = 'La0.5Ba0.5CoO3 at HRPT@PSI'\n", + "project.info.description = \"\"\"This project demonstrates a standard\n", + "refinement of La0.5Ba0.5CoO3, which crystallizes in a perovskite-type\n", + "structure, using neutron powder diffraction data collected in constant\n", + "wavelength mode at the HRPT diffractometer (PSI).\"\"\"" + ] + }, + { + "cell_type": "markdown", + "id": "8", + "metadata": {}, + "source": [ + "#### Show Project Metadata as CIF" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9", + "metadata": {}, + "outputs": [], + "source": [ + "project.info.show_as_cif()" + ] + }, + { + "cell_type": "markdown", + "id": "10", + "metadata": {}, + "source": [ + "#### Save Project\n", + "\n", + "When saving the project for the first time, you need to specify the\n", + "directory path. In the example below, the project is saved to a\n", + "temporary location defined by the system." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "11", + "metadata": {}, + "outputs": [], + "source": [ + "project.save_as(dir_path='lbco_hrpt', temporary=True)" + ] + }, + { + "cell_type": "markdown", + "id": "12", + "metadata": {}, + "source": [ + "#### Set Up Data Plotter" + ] + }, + { + "cell_type": "markdown", + "id": "13", + "metadata": {}, + "source": [ + "Show supported plotting engines." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "14", + "metadata": {}, + "outputs": [], + "source": [ + "project.plotter.show_supported_engines()" + ] + }, + { + "cell_type": "markdown", + "id": "15", + "metadata": {}, + "source": [ + "Show current plotting configuration." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "16", + "metadata": {}, + "outputs": [], + "source": [ + "project.plotter.show_config()" + ] + }, + { + "cell_type": "markdown", + "id": "17", + "metadata": {}, + "source": [ + "Set plotting engine." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "18", + "metadata": {}, + "outputs": [], + "source": [ + "# Keep the auto-selected engine. Alternatively, you can uncomment the\n", + "# line below to explicitly set the engine to the required one.\n", + "# project.plotter.engine = 'plotly'" + ] + }, + { + "cell_type": "markdown", + "id": "19", + "metadata": {}, + "source": [ + "## Step 2: Define Structure\n", + "\n", + "This section shows how to add structures and modify their\n", + "parameters." + ] + }, + { + "cell_type": "markdown", + "id": "20", + "metadata": {}, + "source": [ + "#### Add Structure" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "21", + "metadata": {}, + "outputs": [], + "source": [ + "project.structures.create(name='lbco')" + ] + }, + { + "cell_type": "markdown", + "id": "22", + "metadata": {}, + "source": [ + "#### Show Defined Structures\n", + "\n", + "Show the names of the crystal structures added. These names are used\n", + "to access the structure using the syntax:\n", + "`project.structures[name]`. All structure parameters can be accessed\n", + "via the `project` object." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "23", + "metadata": {}, + "outputs": [], + "source": [ + "project.structures.show_names()" + ] + }, + { + "cell_type": "markdown", + "id": "24", + "metadata": {}, + "source": [ + "#### Set Space Group\n", + "\n", + "Modify the default space group parameters." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "25", + "metadata": {}, + "outputs": [], + "source": [ + "project.structures['lbco'].space_group.name_h_m = 'P m -3 m'\n", + "project.structures['lbco'].space_group.it_coordinate_system_code = '1'" + ] + }, + { + "cell_type": "markdown", + "id": "26", + "metadata": {}, + "source": [ + "#### Set Unit Cell\n", + "\n", + "Modify the default unit cell parameters." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "27", + "metadata": {}, + "outputs": [], + "source": [ + "project.structures['lbco'].cell.length_a = 3.88" + ] + }, + { + "cell_type": "markdown", + "id": "28", + "metadata": {}, + "source": [ + "#### Set Atom Sites\n", + "\n", + "Add atom sites to the structure." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "29", + "metadata": {}, + "outputs": [], + "source": [ + "project.structures['lbco'].atom_sites.create(\n", + " label='La',\n", + " type_symbol='La',\n", + " fract_x=0,\n", + " fract_y=0,\n", + " fract_z=0,\n", + " wyckoff_letter='a',\n", + " b_iso=0.5,\n", + " occupancy=0.5,\n", + ")\n", + "project.structures['lbco'].atom_sites.create(\n", + " label='Ba',\n", + " type_symbol='Ba',\n", + " fract_x=0,\n", + " fract_y=0,\n", + " fract_z=0,\n", + " wyckoff_letter='a',\n", + " b_iso=0.5,\n", + " occupancy=0.5,\n", + ")\n", + "project.structures['lbco'].atom_sites.create(\n", + " label='Co',\n", + " type_symbol='Co',\n", + " fract_x=0.5,\n", + " fract_y=0.5,\n", + " fract_z=0.5,\n", + " wyckoff_letter='b',\n", + " b_iso=0.5,\n", + ")\n", + "project.structures['lbco'].atom_sites.create(\n", + " label='O',\n", + " type_symbol='O',\n", + " fract_x=0,\n", + " fract_y=0.5,\n", + " fract_z=0.5,\n", + " wyckoff_letter='c',\n", + " b_iso=0.5,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "30", + "metadata": {}, + "source": [ + "#### Show Structure as CIF" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "31", + "metadata": {}, + "outputs": [], + "source": [ + "project.structures['lbco'].show_as_cif()" + ] + }, + { + "cell_type": "markdown", + "id": "32", + "metadata": {}, + "source": [ + "#### Show Structure Structure" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "33", + "metadata": {}, + "outputs": [], + "source": [ + "project.structures['lbco'].show()" + ] + }, + { + "cell_type": "markdown", + "id": "34", + "metadata": {}, + "source": [ + "#### Save Project State\n", + "\n", + "Save the project state after adding the structure. This ensures\n", + "that all changes are stored and can be accessed later. The project\n", + "state is saved in the directory specified during project creation." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "35", + "metadata": {}, + "outputs": [], + "source": [ + "project.save()" + ] + }, + { + "cell_type": "markdown", + "id": "36", + "metadata": {}, + "source": [ + "## Step 3: Define Experiment\n", + "\n", + "This section shows how to add experiments, configure their parameters,\n", + "and link the structures defined in the previous step." + ] + }, + { + "cell_type": "markdown", + "id": "37", + "metadata": {}, + "source": [ + "#### Download Measured Data\n", + "\n", + "Download the data file from the EasyDiffraction repository on GitHub." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "38", + "metadata": {}, + "outputs": [], + "source": [ + "data_path = ed.download_data(id=3, destination='data')" + ] + }, + { + "cell_type": "markdown", + "id": "39", + "metadata": {}, + "source": [ + "#### Add Diffraction Experiment" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "40", + "metadata": {}, + "outputs": [], + "source": [ + "project.experiments.add_from_data_path(\n", + " name='hrpt',\n", + " data_path=data_path,\n", + " sample_form='powder',\n", + " beam_mode='constant wavelength',\n", + " radiation_probe='neutron',\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "41", + "metadata": {}, + "source": [ + "#### Show Defined Experiments" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "42", + "metadata": {}, + "outputs": [], + "source": [ + "project.experiments.show_names()" + ] + }, + { + "cell_type": "markdown", + "id": "43", + "metadata": {}, + "source": [ + "#### Show Measured Data" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "44", + "metadata": {}, + "outputs": [], + "source": [ + "project.plot_meas(expt_name='hrpt')" + ] + }, + { + "cell_type": "markdown", + "id": "45", + "metadata": {}, + "source": [ + "#### Set Instrument\n", + "\n", + "Modify the default instrument parameters." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "46", + "metadata": {}, + "outputs": [], + "source": [ + "project.experiments['hrpt'].instrument.setup_wavelength = 1.494\n", + "project.experiments['hrpt'].instrument.calib_twotheta_offset = 0.6" + ] + }, + { + "cell_type": "markdown", + "id": "47", + "metadata": {}, + "source": [ + "#### Set Peak Profile\n", + "\n", + "Show supported peak profile types." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "48", + "metadata": {}, + "outputs": [], + "source": [ + "project.experiments['hrpt'].show_supported_peak_profile_types()" + ] + }, + { + "cell_type": "markdown", + "id": "49", + "metadata": {}, + "source": [ + "Show the current peak profile type." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "50", + "metadata": {}, + "outputs": [], + "source": [ + "project.experiments['hrpt'].show_current_peak_profile_type()" + ] + }, + { + "cell_type": "markdown", + "id": "51", + "metadata": {}, + "source": [ + "Select the desired peak profile type." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "52", + "metadata": {}, + "outputs": [], + "source": [ + "project.experiments['hrpt'].peak_profile_type = 'pseudo-voigt'" + ] + }, + { + "cell_type": "markdown", + "id": "53", + "metadata": {}, + "source": [ + "Modify default peak profile parameters." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "54", + "metadata": {}, + "outputs": [], + "source": [ + "project.experiments['hrpt'].peak.broad_gauss_u = 0.1\n", + "project.experiments['hrpt'].peak.broad_gauss_v = -0.1\n", + "project.experiments['hrpt'].peak.broad_gauss_w = 0.1\n", + "project.experiments['hrpt'].peak.broad_lorentz_x = 0\n", + "project.experiments['hrpt'].peak.broad_lorentz_y = 0.1" + ] + }, + { + "cell_type": "markdown", + "id": "55", + "metadata": {}, + "source": [ + "#### Set Background" + ] + }, + { + "cell_type": "markdown", + "id": "56", + "metadata": {}, + "source": [ + "Show supported background types." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "57", + "metadata": {}, + "outputs": [], + "source": [ + "project.experiments['hrpt'].show_supported_background_types()" + ] + }, + { + "cell_type": "markdown", + "id": "58", + "metadata": {}, + "source": [ + "Show current background type." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "59", + "metadata": {}, + "outputs": [], + "source": [ + "project.experiments['hrpt'].show_current_background_type()" + ] + }, + { + "cell_type": "markdown", + "id": "60", + "metadata": {}, + "source": [ + "Select the desired background type." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "61", + "metadata": {}, + "outputs": [], + "source": [ + "project.experiments['hrpt'].background_type = 'line-segment'" + ] + }, + { + "cell_type": "markdown", + "id": "62", + "metadata": {}, + "source": [ + "Add background points." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "63", + "metadata": {}, + "outputs": [], + "source": [ + "project.experiments['hrpt'].background.create(id='10', x=10, y=170)\n", + "project.experiments['hrpt'].background.create(id='30', x=30, y=170)\n", + "project.experiments['hrpt'].background.create(id='50', x=50, y=170)\n", + "project.experiments['hrpt'].background.create(id='110', x=110, y=170)\n", + "project.experiments['hrpt'].background.create(id='165', x=165, y=170)" + ] + }, + { + "cell_type": "markdown", + "id": "64", + "metadata": {}, + "source": [ + "Show current background points." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "65", + "metadata": {}, + "outputs": [], + "source": [ + "project.experiments['hrpt'].background.show()" + ] + }, + { + "cell_type": "markdown", + "id": "66", + "metadata": {}, + "source": [ + "#### Set Linked Phases\n", + "\n", + "Link the structure defined in the previous step to the experiment." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "67", + "metadata": {}, + "outputs": [], + "source": [ + "project.experiments['hrpt'].linked_phases.create(id='lbco', scale=10.0)" + ] + }, + { + "cell_type": "markdown", + "id": "68", + "metadata": {}, + "source": [ + "#### Show Experiment as CIF" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "69", + "metadata": {}, + "outputs": [], + "source": [ + "project.experiments['hrpt'].show_as_cif()" + ] + }, + { + "cell_type": "markdown", + "id": "70", + "metadata": {}, + "source": [ + "#### Save Project State" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "71", + "metadata": {}, + "outputs": [], + "source": [ + "project.save()" + ] + }, + { + "cell_type": "markdown", + "id": "72", + "metadata": {}, + "source": [ + "## Step 4: Perform Analysis\n", + "\n", + "This section explains the analysis process, including how to set up\n", + "calculation and fitting engines.\n", + "\n", + "#### Set Calculator\n", + "\n", + "Show supported calculation engines for this experiment." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "73", + "metadata": {}, + "outputs": [], + "source": [ + "project.experiments['hrpt'].show_supported_calculator_types()" + ] + }, + { + "cell_type": "markdown", + "id": "74", + "metadata": {}, + "source": [ + "Show current calculation engine for this experiment." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "75", + "metadata": {}, + "outputs": [], + "source": [ + "project.experiments['hrpt'].show_current_calculator_type()" + ] + }, + { + "cell_type": "markdown", + "id": "76", + "metadata": {}, + "source": [ + "Select the desired calculation engine." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "77", + "metadata": {}, + "outputs": [], + "source": [ + "project.experiments['hrpt'].calculator_type = 'cryspy'" + ] + }, + { + "cell_type": "markdown", + "id": "78", + "metadata": {}, + "source": [ + "#### Show Calculated Data" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "79", + "metadata": {}, + "outputs": [], + "source": [ + "project.plot_calc(expt_name='hrpt')" + ] + }, + { + "cell_type": "markdown", + "id": "80", + "metadata": {}, + "source": [ + "#### Plot Measured vs Calculated" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "81", + "metadata": {}, + "outputs": [], + "source": [ + "project.plot_meas_vs_calc(expt_name='hrpt', show_residual=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "82", + "metadata": {}, + "outputs": [], + "source": [ + "project.plot_meas_vs_calc(expt_name='hrpt', x_min=38, x_max=41, show_residual=True)" + ] + }, + { + "cell_type": "markdown", + "id": "83", + "metadata": {}, + "source": [ + "#### Show Parameters\n", + "\n", + "Show all parameters of the project." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "84", + "metadata": {}, + "outputs": [], + "source": [ + "# project.analysis.show_all_params()" + ] + }, + { + "cell_type": "markdown", + "id": "85", + "metadata": {}, + "source": [ + "Show all fittable parameters." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "86", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.show_fittable_params()" + ] + }, + { + "cell_type": "markdown", + "id": "87", + "metadata": {}, + "source": [ + "Show only free parameters." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "88", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.show_free_params()" + ] + }, + { + "cell_type": "markdown", + "id": "89", + "metadata": {}, + "source": [ + "Show how to access parameters in the code." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "90", + "metadata": {}, + "outputs": [], + "source": [ + "# project.analysis.how_to_access_parameters()" + ] + }, + { + "cell_type": "markdown", + "id": "91", + "metadata": {}, + "source": [ + "#### Set Fit Mode\n", + "\n", + "Show supported fit modes." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "92", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.show_supported_fit_mode_types()" + ] + }, + { + "cell_type": "markdown", + "id": "93", + "metadata": {}, + "source": [ + "Show current fit mode." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "94", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.show_current_fit_mode_type()" + ] + }, + { + "cell_type": "markdown", + "id": "95", + "metadata": {}, + "source": [ + "Select desired fit mode." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "96", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.fit_mode.mode = 'single'" + ] + }, + { + "cell_type": "markdown", + "id": "97", + "metadata": {}, + "source": [ + "#### Set Minimizer\n", + "\n", + "Show supported fitting engines." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "98", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.show_available_minimizers()" + ] + }, + { + "cell_type": "markdown", + "id": "99", + "metadata": {}, + "source": [ + "Show current fitting engine." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "100", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.show_current_minimizer()" + ] + }, + { + "cell_type": "markdown", + "id": "101", + "metadata": {}, + "source": [ + "Select desired fitting engine." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "102", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.current_minimizer = 'lmfit'" + ] + }, + { + "cell_type": "markdown", + "id": "103", + "metadata": {}, + "source": [ + "### Perform Fit 1/5\n", + "\n", + "Set structure parameters to be refined." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "104", + "metadata": {}, + "outputs": [], + "source": [ + "project.structures['lbco'].cell.length_a.free = True" + ] + }, + { + "cell_type": "markdown", + "id": "105", + "metadata": {}, + "source": [ + "Set experiment parameters to be refined." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "106", + "metadata": {}, + "outputs": [], + "source": [ + "project.experiments['hrpt'].linked_phases['lbco'].scale.free = True\n", + "project.experiments['hrpt'].instrument.calib_twotheta_offset.free = True\n", + "project.experiments['hrpt'].background['10'].y.free = True\n", + "project.experiments['hrpt'].background['30'].y.free = True\n", + "project.experiments['hrpt'].background['50'].y.free = True\n", + "project.experiments['hrpt'].background['110'].y.free = True\n", + "project.experiments['hrpt'].background['165'].y.free = True" + ] + }, + { + "cell_type": "markdown", + "id": "107", + "metadata": {}, + "source": [ + "Show free parameters after selection." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "108", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.show_free_params()" + ] + }, + { + "cell_type": "markdown", + "id": "109", + "metadata": {}, + "source": [ + "#### Run Fitting" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "110", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.fit()\n", + "project.analysis.show_fit_results()" + ] + }, + { + "cell_type": "markdown", + "id": "111", + "metadata": {}, + "source": [ + "#### Plot Measured vs Calculated" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "112", + "metadata": {}, + "outputs": [], + "source": [ + "project.plot_meas_vs_calc(expt_name='hrpt', show_residual=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "113", + "metadata": {}, + "outputs": [], + "source": [ + "project.plot_meas_vs_calc(expt_name='hrpt', x_min=38, x_max=41, show_residual=True)" + ] + }, + { + "cell_type": "markdown", + "id": "114", + "metadata": {}, + "source": [ + "#### Save Project State" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "115", + "metadata": {}, + "outputs": [], + "source": [ + "project.save_as(dir_path='lbco_hrpt', temporary=True)" + ] + }, + { + "cell_type": "markdown", + "id": "116", + "metadata": {}, + "source": [ + "### Perform Fit 2/5\n", + "\n", + "Set more parameters to be refined." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "117", + "metadata": {}, + "outputs": [], + "source": [ + "project.experiments['hrpt'].peak.broad_gauss_u.free = True\n", + "project.experiments['hrpt'].peak.broad_gauss_v.free = True\n", + "project.experiments['hrpt'].peak.broad_gauss_w.free = True\n", + "project.experiments['hrpt'].peak.broad_lorentz_y.free = True" + ] + }, + { + "cell_type": "markdown", + "id": "118", + "metadata": {}, + "source": [ + "Show free parameters after selection." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "119", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.show_free_params()" + ] + }, + { + "cell_type": "markdown", + "id": "120", + "metadata": {}, + "source": [ + "#### Run Fitting" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "121", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.fit()\n", + "project.analysis.show_fit_results()" + ] + }, + { + "cell_type": "markdown", + "id": "122", + "metadata": {}, + "source": [ + "#### Plot Measured vs Calculated" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "123", + "metadata": {}, + "outputs": [], + "source": [ + "project.plot_meas_vs_calc(expt_name='hrpt', show_residual=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "124", + "metadata": {}, + "outputs": [], + "source": [ + "project.plot_meas_vs_calc(expt_name='hrpt', x_min=38, x_max=41, show_residual=True)" + ] + }, + { + "cell_type": "markdown", + "id": "125", + "metadata": {}, + "source": [ + "#### Save Project State" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "126", + "metadata": {}, + "outputs": [], + "source": [ + "project.save_as(dir_path='lbco_hrpt', temporary=True)" + ] + }, + { + "cell_type": "markdown", + "id": "127", + "metadata": {}, + "source": [ + "### Perform Fit 3/5\n", + "\n", + "Set more parameters to be refined." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "128", + "metadata": {}, + "outputs": [], + "source": [ + "project.structures['lbco'].atom_sites['La'].b_iso.free = True\n", + "project.structures['lbco'].atom_sites['Ba'].b_iso.free = True\n", + "project.structures['lbco'].atom_sites['Co'].b_iso.free = True\n", + "project.structures['lbco'].atom_sites['O'].b_iso.free = True" + ] + }, + { + "cell_type": "markdown", + "id": "129", + "metadata": {}, + "source": [ + "Show free parameters after selection." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "130", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.show_free_params()" + ] + }, + { + "cell_type": "markdown", + "id": "131", + "metadata": {}, + "source": [ + "#### Run Fitting" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "132", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.fit()\n", + "project.analysis.show_fit_results()" + ] + }, + { + "cell_type": "markdown", + "id": "133", + "metadata": {}, + "source": [ + "#### Plot Measured vs Calculated" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "134", + "metadata": {}, + "outputs": [], + "source": [ + "project.plot_meas_vs_calc(expt_name='hrpt', show_residual=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "135", + "metadata": {}, + "outputs": [], + "source": [ + "project.plot_meas_vs_calc(expt_name='hrpt', x_min=38, x_max=41, show_residual=True)" + ] + }, + { + "cell_type": "markdown", + "id": "136", + "metadata": {}, + "source": [ + "#### Save Project State" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "137", + "metadata": {}, + "outputs": [], + "source": [ + "project.save_as(dir_path='lbco_hrpt', temporary=True)" + ] + }, + { + "cell_type": "markdown", + "id": "138", + "metadata": {}, + "source": [ + "### Perform Fit 4/5\n", + "\n", + "#### Set Constraints\n", + "\n", + "Set aliases for parameters." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "139", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.aliases.create(\n", + " label='biso_La',\n", + " param_uid=project.structures['lbco'].atom_sites['La'].b_iso.uid,\n", + ")\n", + "project.analysis.aliases.create(\n", + " label='biso_Ba',\n", + " param_uid=project.structures['lbco'].atom_sites['Ba'].b_iso.uid,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "140", + "metadata": {}, + "source": [ + "Set constraints." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "141", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.constraints.create(expression='biso_Ba = biso_La')" + ] + }, + { + "cell_type": "markdown", + "id": "142", + "metadata": {}, + "source": [ + "Show defined constraints." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "143", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.show_constraints()" + ] + }, + { + "cell_type": "markdown", + "id": "144", + "metadata": {}, + "source": [ + "Show free parameters before applying constraints." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "145", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.show_free_params()" + ] + }, + { + "cell_type": "markdown", + "id": "146", + "metadata": {}, + "source": [ + "Apply constraints." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "147", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.apply_constraints()" + ] + }, + { + "cell_type": "markdown", + "id": "148", + "metadata": {}, + "source": [ + "Show free parameters after applying constraints." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "149", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.show_free_params()" + ] + }, + { + "cell_type": "markdown", + "id": "150", + "metadata": {}, + "source": [ + "#### Run Fitting" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "151", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.fit()\n", + "project.analysis.show_fit_results()" + ] + }, + { + "cell_type": "markdown", + "id": "152", + "metadata": {}, + "source": [ + "#### Plot Measured vs Calculated" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "153", + "metadata": {}, + "outputs": [], + "source": [ + "project.plot_meas_vs_calc(expt_name='hrpt', show_residual=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "154", + "metadata": {}, + "outputs": [], + "source": [ + "project.plot_meas_vs_calc(expt_name='hrpt', x_min=38, x_max=41, show_residual=True)" + ] + }, + { + "cell_type": "markdown", + "id": "155", + "metadata": {}, + "source": [ + "#### Save Project State" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "156", + "metadata": {}, + "outputs": [], + "source": [ + "project.save_as(dir_path='lbco_hrpt', temporary=True)" + ] + }, + { + "cell_type": "markdown", + "id": "157", + "metadata": {}, + "source": [ + "### Perform Fit 5/5\n", + "\n", + "#### Set Constraints\n", + "\n", + "Set more aliases for parameters." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "158", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.aliases.create(\n", + " label='occ_La',\n", + " param_uid=project.structures['lbco'].atom_sites['La'].occupancy.uid,\n", + ")\n", + "project.analysis.aliases.create(\n", + " label='occ_Ba',\n", + " param_uid=project.structures['lbco'].atom_sites['Ba'].occupancy.uid,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "159", + "metadata": {}, + "source": [ + "Set more constraints." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "160", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.constraints.create(\n", + " expression='occ_Ba = 1 - occ_La',\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "161", + "metadata": {}, + "source": [ + "Show defined constraints." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "162", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.show_constraints()" + ] + }, + { + "cell_type": "markdown", + "id": "163", + "metadata": {}, + "source": [ + "Apply constraints." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "164", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.apply_constraints()" + ] + }, + { + "cell_type": "markdown", + "id": "165", + "metadata": {}, + "source": [ + "Set structure parameters to be refined." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "166", + "metadata": {}, + "outputs": [], + "source": [ + "project.structures['lbco'].atom_sites['La'].occupancy.free = True" + ] + }, + { + "cell_type": "markdown", + "id": "167", + "metadata": {}, + "source": [ + "Show free parameters after selection." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "168", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.show_free_params()" + ] + }, + { + "cell_type": "markdown", + "id": "169", + "metadata": {}, + "source": [ + "#### Run Fitting" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "170", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.fit()\n", + "project.analysis.show_fit_results()" + ] + }, + { + "cell_type": "markdown", + "id": "171", + "metadata": {}, + "source": [ + "#### Plot Measured vs Calculated" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "172", + "metadata": {}, + "outputs": [], + "source": [ + "project.plot_meas_vs_calc(expt_name='hrpt', show_residual=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "173", + "metadata": {}, + "outputs": [], + "source": [ + "project.plot_meas_vs_calc(expt_name='hrpt', x_min=38, x_max=41, show_residual=True)" + ] + }, + { + "cell_type": "markdown", + "id": "174", + "metadata": {}, + "source": [ + "#### Save Project State" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "175", + "metadata": {}, + "outputs": [], + "source": [ + "project.save_as(dir_path='lbco_hrpt', temporary=True)" + ] + }, + { + "cell_type": "markdown", + "id": "176", + "metadata": {}, + "source": [ + "## Step 5: Summary\n", + "\n", + "This final section shows how to review the results of the analysis." + ] + }, + { + "cell_type": "markdown", + "id": "177", + "metadata": {}, + "source": [ + "#### Show Project Summary" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "178", + "metadata": {}, + "outputs": [], + "source": [ + "project.summary.show_report()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "179", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "jupytext": { + "cell_metadata_filter": "-all", + "main_language": "python", + "notebook_metadata_filter": "-all" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/tutorials/ed-3.py b/docs/docs/tutorials/ed-3.py similarity index 78% rename from tutorials/ed-3.py rename to docs/docs/tutorials/ed-3.py index c0aefa1b..23b60d88 100644 --- a/tutorials/ed-3.py +++ b/docs/docs/tutorials/ed-3.py @@ -8,12 +8,13 @@ # # It is intended for users with minimal programming experience who want # to learn how to perform standard crystal structure fitting using -# diffraction data. This script covers creating a project, adding sample -# models and experiments, performing analysis, and refining parameters. +# diffraction data. This script covers creating a project, adding +# crystal structures and experiments, performing analysis, and refining +# parameters. # # Only a single import of `easydiffraction` is required, and all # operations are performed through high-level components of the -# `project` object, such as `project.sample_models`, +# `project` object, such as `project.structures`, # `project.experiments`, and `project.analysis`. The `project` object is # the main container for all information. @@ -84,26 +85,27 @@ # project.plotter.engine = 'plotly' # %% [markdown] -# ## Step 2: Define Sample Model +# ## Step 2: Define Structure # -# This section shows how to add sample models and modify their +# This section shows how to add structures and modify their # parameters. # %% [markdown] -# #### Add Sample Model +# #### Add Structure # %% -project.sample_models.add(name='lbco') +project.structures.create(name='lbco') # %% [markdown] -# #### Show Defined Sample Models +# #### Show Defined Structures # -# Show the names of the models added. These names are used to access the -# model using the syntax: `project.sample_models['model_name']`. All -# model parameters can be accessed via the `project` object. +# Show the names of the crystal structures added. These names are used +# to access the structure using the syntax: +# `project.structures[name]`. All structure parameters can be accessed +# via the `project` object. # %% -project.sample_models.show_names() +project.structures.show_names() # %% [markdown] # #### Set Space Group @@ -111,8 +113,8 @@ # Modify the default space group parameters. # %% -project.sample_models['lbco'].space_group.name_h_m = 'P m -3 m' -project.sample_models['lbco'].space_group.it_coordinate_system_code = '1' +project.structures['lbco'].space_group.name_h_m = 'P m -3 m' +project.structures['lbco'].space_group.it_coordinate_system_code = '1' # %% [markdown] # #### Set Unit Cell @@ -120,15 +122,15 @@ # Modify the default unit cell parameters. # %% -project.sample_models['lbco'].cell.length_a = 3.88 +project.structures['lbco'].cell.length_a = 3.88 # %% [markdown] # #### Set Atom Sites # -# Add atom sites to the sample model. +# Add atom sites to the structure. # %% -project.sample_models['lbco'].atom_sites.add( +project.structures['lbco'].atom_sites.create( label='La', type_symbol='La', fract_x=0, @@ -138,7 +140,7 @@ b_iso=0.5, occupancy=0.5, ) -project.sample_models['lbco'].atom_sites.add( +project.structures['lbco'].atom_sites.create( label='Ba', type_symbol='Ba', fract_x=0, @@ -148,7 +150,7 @@ b_iso=0.5, occupancy=0.5, ) -project.sample_models['lbco'].atom_sites.add( +project.structures['lbco'].atom_sites.create( label='Co', type_symbol='Co', fract_x=0.5, @@ -157,7 +159,7 @@ wyckoff_letter='b', b_iso=0.5, ) -project.sample_models['lbco'].atom_sites.add( +project.structures['lbco'].atom_sites.create( label='O', type_symbol='O', fract_x=0, @@ -168,21 +170,21 @@ ) # %% [markdown] -# #### Show Sample Model as CIF +# #### Show Structure as CIF # %% -project.sample_models['lbco'].show_as_cif() +project.structures['lbco'].show_as_cif() # %% [markdown] -# #### Show Sample Model Structure +# #### Show Structure Structure # %% -project.sample_models['lbco'].show_structure() +project.structures['lbco'].show() # %% [markdown] # #### Save Project State # -# Save the project state after adding the sample model. This ensures +# Save the project state after adding the structure. This ensures # that all changes are stored and can be accessed later. The project # state is saved in the directory specified during project creation. @@ -193,7 +195,7 @@ # ## Step 3: Define Experiment # # This section shows how to add experiments, configure their parameters, -# and link the sample models defined in the previous step. +# and link the structures defined in the previous step. # %% [markdown] # #### Download Measured Data @@ -207,7 +209,7 @@ # #### Add Diffraction Experiment # %% -project.experiments.add( +project.experiments.add_from_data_path( name='hrpt', data_path=data_path, sample_form='powder', @@ -291,11 +293,11 @@ # Add background points. # %% -project.experiments['hrpt'].background.add(id='10', x=10, y=170) -project.experiments['hrpt'].background.add(id='30', x=30, y=170) -project.experiments['hrpt'].background.add(id='50', x=50, y=170) -project.experiments['hrpt'].background.add(id='110', x=110, y=170) -project.experiments['hrpt'].background.add(id='165', x=165, y=170) +project.experiments['hrpt'].background.create(id='10', x=10, y=170) +project.experiments['hrpt'].background.create(id='30', x=30, y=170) +project.experiments['hrpt'].background.create(id='50', x=50, y=170) +project.experiments['hrpt'].background.create(id='110', x=110, y=170) +project.experiments['hrpt'].background.create(id='165', x=165, y=170) # %% [markdown] # Show current background points. @@ -306,10 +308,10 @@ # %% [markdown] # #### Set Linked Phases # -# Link the sample model defined in the previous step to the experiment. +# Link the structure defined in the previous step to the experiment. # %% -project.experiments['hrpt'].linked_phases.add(id='lbco', scale=10.0) +project.experiments['hrpt'].linked_phases.create(id='lbco', scale=10.0) # %% [markdown] # #### Show Experiment as CIF @@ -331,22 +333,22 @@ # # #### Set Calculator # -# Show supported calculation engines. +# Show supported calculation engines for this experiment. # %% -project.analysis.show_supported_calculators() +project.experiments['hrpt'].show_supported_calculator_types() # %% [markdown] -# Show current calculation engine. +# Show current calculation engine for this experiment. # %% -project.analysis.show_current_calculator() +project.experiments['hrpt'].show_current_calculator_type() # %% [markdown] # Select the desired calculation engine. # %% -project.analysis.current_calculator = 'cryspy' +project.experiments['hrpt'].calculator_type = 'cryspy' # %% [markdown] # #### Show Calculated Data @@ -395,19 +397,19 @@ # Show supported fit modes. # %% -project.analysis.show_available_fit_modes() +project.analysis.show_supported_fit_mode_types() # %% [markdown] # Show current fit mode. # %% -project.analysis.show_current_fit_mode() +project.analysis.show_current_fit_mode_type() # %% [markdown] # Select desired fit mode. # %% -project.analysis.fit_mode = 'single' +project.analysis.fit_mode.mode = 'single' # %% [markdown] # #### Set Minimizer @@ -427,15 +429,15 @@ # Select desired fitting engine. # %% -project.analysis.current_minimizer = 'lmfit (leastsq)' +project.analysis.current_minimizer = 'lmfit' # %% [markdown] # ### Perform Fit 1/5 # -# Set sample model parameters to be refined. +# Set structure parameters to be refined. # %% -project.sample_models['lbco'].cell.length_a.free = True +project.structures['lbco'].cell.length_a.free = True # %% [markdown] # Set experiment parameters to be refined. @@ -522,10 +524,10 @@ # Set more parameters to be refined. # %% -project.sample_models['lbco'].atom_sites['La'].b_iso.free = True -project.sample_models['lbco'].atom_sites['Ba'].b_iso.free = True -project.sample_models['lbco'].atom_sites['Co'].b_iso.free = True -project.sample_models['lbco'].atom_sites['O'].b_iso.free = True +project.structures['lbco'].atom_sites['La'].b_iso.free = True +project.structures['lbco'].atom_sites['Ba'].b_iso.free = True +project.structures['lbco'].atom_sites['Co'].b_iso.free = True +project.structures['lbco'].atom_sites['O'].b_iso.free = True # %% [markdown] # Show free parameters after selection. @@ -563,20 +565,20 @@ # Set aliases for parameters. # %% -project.analysis.aliases.add( +project.analysis.aliases.create( label='biso_La', - param_uid=project.sample_models['lbco'].atom_sites['La'].b_iso.uid, + param_uid=project.structures['lbco'].atom_sites['La'].b_iso.uid, ) -project.analysis.aliases.add( +project.analysis.aliases.create( label='biso_Ba', - param_uid=project.sample_models['lbco'].atom_sites['Ba'].b_iso.uid, + param_uid=project.structures['lbco'].atom_sites['Ba'].b_iso.uid, ) # %% [markdown] # Set constraints. # %% -project.analysis.constraints.add(lhs_alias='biso_Ba', rhs_expr='biso_La') +project.analysis.constraints.create(expression='biso_Ba = biso_La') # %% [markdown] # Show defined constraints. @@ -632,22 +634,21 @@ # Set more aliases for parameters. # %% -project.analysis.aliases.add( +project.analysis.aliases.create( label='occ_La', - param_uid=project.sample_models['lbco'].atom_sites['La'].occupancy.uid, + param_uid=project.structures['lbco'].atom_sites['La'].occupancy.uid, ) -project.analysis.aliases.add( +project.analysis.aliases.create( label='occ_Ba', - param_uid=project.sample_models['lbco'].atom_sites['Ba'].occupancy.uid, + param_uid=project.structures['lbco'].atom_sites['Ba'].occupancy.uid, ) # %% [markdown] # Set more constraints. # %% -project.analysis.constraints.add( - lhs_alias='occ_Ba', - rhs_expr='1 - occ_La', +project.analysis.constraints.create( + expression='occ_Ba = 1 - occ_La', ) # %% [markdown] @@ -663,10 +664,10 @@ project.analysis.apply_constraints() # %% [markdown] -# Set sample model parameters to be refined. +# Set structure parameters to be refined. # %% -project.sample_models['lbco'].atom_sites['La'].occupancy.free = True +project.structures['lbco'].atom_sites['La'].occupancy.free = True # %% [markdown] # Show free parameters after selection. diff --git a/docs/docs/tutorials/ed-4.ipynb b/docs/docs/tutorials/ed-4.ipynb new file mode 100644 index 00000000..0b784054 --- /dev/null +++ b/docs/docs/tutorials/ed-4.ipynb @@ -0,0 +1,705 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0", + "metadata": {}, + "source": [ + "# Structure Refinement: PbSO4, NPD + XRD\n", + "\n", + "This example demonstrates a more advanced use of the EasyDiffraction\n", + "library by explicitly creating and configuring structures and\n", + "experiments before adding them to a project. It could be more suitable\n", + "for users who are interested in creating custom workflows. This\n", + "tutorial provides minimal explanation and is intended for users\n", + "already familiar with EasyDiffraction.\n", + "\n", + "The tutorial covers a Rietveld refinement of PbSO4 crystal structure\n", + "based on the joint fit of both X-ray and neutron diffraction data." + ] + }, + { + "cell_type": "markdown", + "id": "1", + "metadata": {}, + "source": [ + "## Import Library" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2", + "metadata": {}, + "outputs": [], + "source": [ + "from easydiffraction import ExperimentFactory\n", + "from easydiffraction import Project\n", + "from easydiffraction import StructureFactory\n", + "from easydiffraction import download_data" + ] + }, + { + "cell_type": "markdown", + "id": "3", + "metadata": {}, + "source": [ + "## Define Structure\n", + "\n", + "This section shows how to add structures and modify their\n", + "parameters.\n", + "\n", + "#### Create Structure" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4", + "metadata": {}, + "outputs": [], + "source": [ + "structure = StructureFactory.from_scratch(name='pbso4')" + ] + }, + { + "cell_type": "markdown", + "id": "5", + "metadata": {}, + "source": [ + "#### Set Space Group" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6", + "metadata": {}, + "outputs": [], + "source": [ + "structure.space_group.name_h_m = 'P n m a'" + ] + }, + { + "cell_type": "markdown", + "id": "7", + "metadata": {}, + "source": [ + "#### Set Unit Cell" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8", + "metadata": {}, + "outputs": [], + "source": [ + "structure.cell.length_a = 8.47\n", + "structure.cell.length_b = 5.39\n", + "structure.cell.length_c = 6.95" + ] + }, + { + "cell_type": "markdown", + "id": "9", + "metadata": {}, + "source": [ + "#### Set Atom Sites" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "10", + "metadata": { + "lines_to_next_cell": 2 + }, + "outputs": [], + "source": [ + "structure.atom_sites.create(\n", + " label='Pb',\n", + " type_symbol='Pb',\n", + " fract_x=0.1876,\n", + " fract_y=0.25,\n", + " fract_z=0.167,\n", + " wyckoff_letter='c',\n", + " b_iso=1.37,\n", + ")\n", + "structure.atom_sites.create(\n", + " label='S',\n", + " type_symbol='S',\n", + " fract_x=0.0654,\n", + " fract_y=0.25,\n", + " fract_z=0.684,\n", + " wyckoff_letter='c',\n", + " b_iso=0.3777,\n", + ")\n", + "structure.atom_sites.create(\n", + " label='O1',\n", + " type_symbol='O',\n", + " fract_x=0.9082,\n", + " fract_y=0.25,\n", + " fract_z=0.5954,\n", + " wyckoff_letter='c',\n", + " b_iso=1.9764,\n", + ")\n", + "structure.atom_sites.create(\n", + " label='O2',\n", + " type_symbol='O',\n", + " fract_x=0.1935,\n", + " fract_y=0.25,\n", + " fract_z=0.5432,\n", + " wyckoff_letter='c',\n", + " b_iso=1.4456,\n", + ")\n", + "structure.atom_sites.create(\n", + " label='O3',\n", + " type_symbol='O',\n", + " fract_x=0.0811,\n", + " fract_y=0.0272,\n", + " fract_z=0.8086,\n", + " wyckoff_letter='d',\n", + " b_iso=1.2822,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "11", + "metadata": {}, + "source": [ + "## Define Experiments\n", + "\n", + "This section shows how to add experiments, configure their parameters,\n", + "and link the structures defined in the previous step.\n", + "\n", + "### Experiment 1: npd\n", + "\n", + "#### Download Data" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "12", + "metadata": {}, + "outputs": [], + "source": [ + "data_path1 = download_data(id=13, destination='data')" + ] + }, + { + "cell_type": "markdown", + "id": "13", + "metadata": {}, + "source": [ + "#### Create Experiment" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "14", + "metadata": {}, + "outputs": [], + "source": [ + "expt1 = ExperimentFactory.from_data_path(\n", + " name='npd',\n", + " data_path=data_path1,\n", + " radiation_probe='neutron',\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "15", + "metadata": {}, + "source": [ + "#### Set Instrument" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "16", + "metadata": {}, + "outputs": [], + "source": [ + "expt1.instrument.setup_wavelength = 1.91\n", + "expt1.instrument.calib_twotheta_offset = -0.1406" + ] + }, + { + "cell_type": "markdown", + "id": "17", + "metadata": {}, + "source": [ + "#### Set Peak Profile" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "18", + "metadata": {}, + "outputs": [], + "source": [ + "expt1.peak.broad_gauss_u = 0.139\n", + "expt1.peak.broad_gauss_v = -0.412\n", + "expt1.peak.broad_gauss_w = 0.386\n", + "expt1.peak.broad_lorentz_x = 0\n", + "expt1.peak.broad_lorentz_y = 0.088" + ] + }, + { + "cell_type": "markdown", + "id": "19", + "metadata": {}, + "source": [ + "#### Set Background" + ] + }, + { + "cell_type": "markdown", + "id": "20", + "metadata": {}, + "source": [ + "Select the background type." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "21", + "metadata": {}, + "outputs": [], + "source": [ + "expt1.background_type = 'line-segment'" + ] + }, + { + "cell_type": "markdown", + "id": "22", + "metadata": {}, + "source": [ + "Add background points." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "23", + "metadata": {}, + "outputs": [], + "source": [ + "for id, x, y in [\n", + " ('1', 11.0, 206.1624),\n", + " ('2', 15.0, 194.75),\n", + " ('3', 20.0, 194.505),\n", + " ('4', 30.0, 188.4375),\n", + " ('5', 50.0, 207.7633),\n", + " ('6', 70.0, 201.7002),\n", + " ('7', 120.0, 244.4525),\n", + " ('8', 153.0, 226.0595),\n", + "]:\n", + " expt1.background.create(id=id, x=x, y=y)" + ] + }, + { + "cell_type": "markdown", + "id": "24", + "metadata": {}, + "source": [ + "#### Set Linked Phases" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "25", + "metadata": {}, + "outputs": [], + "source": [ + "expt1.linked_phases.create(id='pbso4', scale=1.5)" + ] + }, + { + "cell_type": "markdown", + "id": "26", + "metadata": {}, + "source": [ + "### Experiment 2: xrd\n", + "\n", + "#### Download Data" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "27", + "metadata": {}, + "outputs": [], + "source": [ + "data_path2 = download_data(id=16, destination='data')" + ] + }, + { + "cell_type": "markdown", + "id": "28", + "metadata": {}, + "source": [ + "#### Create Experiment" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "29", + "metadata": {}, + "outputs": [], + "source": [ + "expt2 = ExperimentFactory.from_data_path(\n", + " name='xrd',\n", + " data_path=data_path2,\n", + " radiation_probe='xray',\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "30", + "metadata": {}, + "source": [ + "#### Set Instrument" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "31", + "metadata": {}, + "outputs": [], + "source": [ + "expt2.instrument.setup_wavelength = 1.540567\n", + "expt2.instrument.calib_twotheta_offset = -0.05181" + ] + }, + { + "cell_type": "markdown", + "id": "32", + "metadata": {}, + "source": [ + "#### Set Peak Profile" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "33", + "metadata": {}, + "outputs": [], + "source": [ + "expt2.peak.broad_gauss_u = 0.304138\n", + "expt2.peak.broad_gauss_v = -0.112622\n", + "expt2.peak.broad_gauss_w = 0.021272\n", + "expt2.peak.broad_lorentz_x = 0\n", + "expt2.peak.broad_lorentz_y = 0.057691" + ] + }, + { + "cell_type": "markdown", + "id": "34", + "metadata": {}, + "source": [ + "#### Set Background" + ] + }, + { + "cell_type": "markdown", + "id": "35", + "metadata": {}, + "source": [ + "Select background type." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "36", + "metadata": {}, + "outputs": [], + "source": [ + "expt2.background_type = 'chebyshev'" + ] + }, + { + "cell_type": "markdown", + "id": "37", + "metadata": {}, + "source": [ + "Add background points." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "38", + "metadata": {}, + "outputs": [], + "source": [ + "for id, x, y in [\n", + " ('1', 0, 119.195),\n", + " ('2', 1, 6.221),\n", + " ('3', 2, -45.725),\n", + " ('4', 3, 8.119),\n", + " ('5', 4, 54.552),\n", + " ('6', 5, -20.661),\n", + "]:\n", + " expt2.background.create(id=id, order=x, coef=y)" + ] + }, + { + "cell_type": "markdown", + "id": "39", + "metadata": {}, + "source": [ + "#### Set Linked Phases" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "40", + "metadata": {}, + "outputs": [], + "source": [ + "expt2.linked_phases.create(id='pbso4', scale=0.001)" + ] + }, + { + "cell_type": "markdown", + "id": "41", + "metadata": {}, + "source": [ + "## Define Project\n", + "\n", + "The project object is used to manage structures, experiments, and\n", + "analysis.\n", + "\n", + "#### Create Project" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "42", + "metadata": {}, + "outputs": [], + "source": [ + "project = Project()" + ] + }, + { + "cell_type": "markdown", + "id": "43", + "metadata": {}, + "source": [ + "#### Add Structure" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "44", + "metadata": {}, + "outputs": [], + "source": [ + "project.structures.add(structure)" + ] + }, + { + "cell_type": "markdown", + "id": "45", + "metadata": {}, + "source": [ + "#### Add Experiments" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "46", + "metadata": {}, + "outputs": [], + "source": [ + "project.experiments.add(expt1)\n", + "project.experiments.add(expt2)" + ] + }, + { + "cell_type": "markdown", + "id": "47", + "metadata": {}, + "source": [ + "## Perform Analysis\n", + "\n", + "This section outlines the analysis process, including how to configure\n", + "calculation and fitting engines.\n", + "\n", + "#### Set Fit Mode" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "48", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.fit_mode.mode = 'joint'" + ] + }, + { + "cell_type": "markdown", + "id": "49", + "metadata": {}, + "source": [ + "#### Set Minimizer" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "50", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.current_minimizer = 'lmfit'" + ] + }, + { + "cell_type": "markdown", + "id": "51", + "metadata": {}, + "source": [ + "#### Set Fitting Parameters\n", + "\n", + "Set structure parameters to be optimized." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "52", + "metadata": {}, + "outputs": [], + "source": [ + "structure.cell.length_a.free = True\n", + "structure.cell.length_b.free = True\n", + "structure.cell.length_c.free = True" + ] + }, + { + "cell_type": "markdown", + "id": "53", + "metadata": {}, + "source": [ + "Set experiment parameters to be optimized." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "54", + "metadata": {}, + "outputs": [], + "source": [ + "expt1.linked_phases['pbso4'].scale.free = True\n", + "\n", + "expt1.instrument.calib_twotheta_offset.free = True\n", + "\n", + "expt1.peak.broad_gauss_u.free = True\n", + "expt1.peak.broad_gauss_v.free = True\n", + "expt1.peak.broad_gauss_w.free = True\n", + "expt1.peak.broad_lorentz_y.free = True" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "55", + "metadata": {}, + "outputs": [], + "source": [ + "expt2.linked_phases['pbso4'].scale.free = True\n", + "\n", + "expt2.instrument.calib_twotheta_offset.free = True\n", + "\n", + "expt2.peak.broad_gauss_u.free = True\n", + "expt2.peak.broad_gauss_v.free = True\n", + "expt2.peak.broad_gauss_w.free = True\n", + "expt2.peak.broad_lorentz_y.free = True\n", + "\n", + "for term in expt2.background:\n", + " term.coef.free = True" + ] + }, + { + "cell_type": "markdown", + "id": "56", + "metadata": {}, + "source": [ + "#### Perform Fit" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "57", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.fit()\n", + "project.analysis.show_fit_results()" + ] + }, + { + "cell_type": "markdown", + "id": "58", + "metadata": {}, + "source": [ + "#### Plot Measured vs Calculated" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "59", + "metadata": {}, + "outputs": [], + "source": [ + "project.plot_meas_vs_calc(expt_name='npd', x_min=35.5, x_max=38.3, show_residual=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "60", + "metadata": {}, + "outputs": [], + "source": [ + "project.plot_meas_vs_calc(expt_name='xrd', x_min=29.0, x_max=30.4, show_residual=True)" + ] + } + ], + "metadata": { + "jupytext": { + "cell_metadata_filter": "-all", + "main_language": "python", + "notebook_metadata_filter": "-all" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/tutorials/ed-4.py b/docs/docs/tutorials/ed-4.py similarity index 78% rename from tutorials/ed-4.py rename to docs/docs/tutorials/ed-4.py index ce857a2a..3275deab 100644 --- a/tutorials/ed-4.py +++ b/docs/docs/tutorials/ed-4.py @@ -2,7 +2,7 @@ # # Structure Refinement: PbSO4, NPD + XRD # # This example demonstrates a more advanced use of the EasyDiffraction -# library by explicitly creating and configuring sample models and +# library by explicitly creating and configuring structures and # experiments before adding them to a project. It could be more suitable # for users who are interested in creating custom workflows. This # tutorial provides minimal explanation and is intended for users @@ -17,39 +17,39 @@ # %% from easydiffraction import ExperimentFactory from easydiffraction import Project -from easydiffraction import SampleModelFactory +from easydiffraction import StructureFactory from easydiffraction import download_data # %% [markdown] -# ## Define Sample Model +# ## Define Structure # -# This section shows how to add sample models and modify their +# This section shows how to add structures and modify their # parameters. # -# #### Create Sample Model +# #### Create Structure # %% -model = SampleModelFactory.create(name='pbso4') +structure = StructureFactory.from_scratch(name='pbso4') # %% [markdown] # #### Set Space Group # %% -model.space_group.name_h_m = 'P n m a' +structure.space_group.name_h_m = 'P n m a' # %% [markdown] # #### Set Unit Cell # %% -model.cell.length_a = 8.47 -model.cell.length_b = 5.39 -model.cell.length_c = 6.95 +structure.cell.length_a = 8.47 +structure.cell.length_b = 5.39 +structure.cell.length_c = 6.95 # %% [markdown] # #### Set Atom Sites # %% -model.atom_sites.add( +structure.atom_sites.create( label='Pb', type_symbol='Pb', fract_x=0.1876, @@ -58,7 +58,7 @@ wyckoff_letter='c', b_iso=1.37, ) -model.atom_sites.add( +structure.atom_sites.create( label='S', type_symbol='S', fract_x=0.0654, @@ -67,7 +67,7 @@ wyckoff_letter='c', b_iso=0.3777, ) -model.atom_sites.add( +structure.atom_sites.create( label='O1', type_symbol='O', fract_x=0.9082, @@ -76,7 +76,7 @@ wyckoff_letter='c', b_iso=1.9764, ) -model.atom_sites.add( +structure.atom_sites.create( label='O2', type_symbol='O', fract_x=0.1935, @@ -85,7 +85,7 @@ wyckoff_letter='c', b_iso=1.4456, ) -model.atom_sites.add( +structure.atom_sites.create( label='O3', type_symbol='O', fract_x=0.0811, @@ -100,7 +100,7 @@ # ## Define Experiments # # This section shows how to add experiments, configure their parameters, -# and link the sample models defined in the previous step. +# and link the structures defined in the previous step. # # ### Experiment 1: npd # @@ -113,7 +113,7 @@ # #### Create Experiment # %% -expt1 = ExperimentFactory.create( +expt1 = ExperimentFactory.from_data_path( name='npd', data_path=data_path1, radiation_probe='neutron', @@ -159,13 +159,13 @@ ('7', 120.0, 244.4525), ('8', 153.0, 226.0595), ]: - expt1.background.add(id=id, x=x, y=y) + expt1.background.create(id=id, x=x, y=y) # %% [markdown] # #### Set Linked Phases # %% -expt1.linked_phases.add(id='pbso4', scale=1.5) +expt1.linked_phases.create(id='pbso4', scale=1.5) # %% [markdown] # ### Experiment 2: xrd @@ -179,7 +179,7 @@ # #### Create Experiment # %% -expt2 = ExperimentFactory.create( +expt2 = ExperimentFactory.from_data_path( name='xrd', data_path=data_path2, radiation_probe='xray', @@ -209,7 +209,7 @@ # Select background type. # %% -expt2.background_type = 'chebyshev polynomial' +expt2.background_type = 'chebyshev' # %% [markdown] # Add background points. @@ -223,18 +223,18 @@ ('5', 4, 54.552), ('6', 5, -20.661), ]: - expt2.background.add(id=id, order=x, coef=y) + expt2.background.create(id=id, order=x, coef=y) # %% [markdown] # #### Set Linked Phases # %% -expt2.linked_phases.add(id='pbso4', scale=0.001) +expt2.linked_phases.create(id='pbso4', scale=0.001) # %% [markdown] # ## Define Project # -# The project object is used to manage sample models, experiments, and +# The project object is used to manage structures, experiments, and # analysis. # # #### Create Project @@ -243,17 +243,17 @@ project = Project() # %% [markdown] -# #### Add Sample Model +# #### Add Structure # %% -project.sample_models.add(sample_model=model) +project.structures.add(structure) # %% [markdown] # #### Add Experiments # %% -project.experiments.add(experiment=expt1) -project.experiments.add(experiment=expt2) +project.experiments.add(expt1) +project.experiments.add(expt2) # %% [markdown] # ## Perform Analysis @@ -261,32 +261,26 @@ # This section outlines the analysis process, including how to configure # calculation and fitting engines. # -# #### Set Calculator - -# %% -project.analysis.current_calculator = 'cryspy' - -# %% [markdown] # #### Set Fit Mode # %% -project.analysis.fit_mode = 'joint' +project.analysis.fit_mode.mode = 'joint' # %% [markdown] # #### Set Minimizer # %% -project.analysis.current_minimizer = 'lmfit (leastsq)' +project.analysis.current_minimizer = 'lmfit' # %% [markdown] # #### Set Fitting Parameters # -# Set sample model parameters to be optimized. +# Set structure parameters to be optimized. # %% -model.cell.length_a.free = True -model.cell.length_b.free = True -model.cell.length_c.free = True +structure.cell.length_a.free = True +structure.cell.length_b.free = True +structure.cell.length_c.free = True # %% [markdown] # Set experiment parameters to be optimized. diff --git a/docs/docs/tutorials/ed-5.ipynb b/docs/docs/tutorials/ed-5.ipynb new file mode 100644 index 00000000..903fe2ae --- /dev/null +++ b/docs/docs/tutorials/ed-5.ipynb @@ -0,0 +1,637 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0", + "metadata": {}, + "source": [ + "# Structure Refinement: Co2SiO4, D20\n", + "\n", + "This example demonstrates a Rietveld refinement of Co2SiO4 crystal\n", + "structure using constant wavelength neutron powder diffraction data\n", + "from D20 at ILL." + ] + }, + { + "cell_type": "markdown", + "id": "1", + "metadata": {}, + "source": [ + "## Import Library" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2", + "metadata": {}, + "outputs": [], + "source": [ + "from easydiffraction import ExperimentFactory\n", + "from easydiffraction import Project\n", + "from easydiffraction import StructureFactory\n", + "from easydiffraction import download_data" + ] + }, + { + "cell_type": "markdown", + "id": "3", + "metadata": {}, + "source": [ + "## Define Structure\n", + "\n", + "This section shows how to add structures and modify their\n", + "parameters.\n", + "\n", + "#### Create Structure" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4", + "metadata": {}, + "outputs": [], + "source": [ + "structure = StructureFactory.from_scratch(name='cosio')" + ] + }, + { + "cell_type": "markdown", + "id": "5", + "metadata": {}, + "source": [ + "#### Set Space Group" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6", + "metadata": {}, + "outputs": [], + "source": [ + "structure.space_group.name_h_m = 'P n m a'\n", + "structure.space_group.it_coordinate_system_code = 'abc'" + ] + }, + { + "cell_type": "markdown", + "id": "7", + "metadata": {}, + "source": [ + "#### Set Unit Cell" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8", + "metadata": {}, + "outputs": [], + "source": [ + "structure.cell.length_a = 10.3\n", + "structure.cell.length_b = 6.0\n", + "structure.cell.length_c = 4.8" + ] + }, + { + "cell_type": "markdown", + "id": "9", + "metadata": {}, + "source": [ + "#### Set Atom Sites" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "10", + "metadata": {}, + "outputs": [], + "source": [ + "structure.atom_sites.create(\n", + " label='Co1',\n", + " type_symbol='Co',\n", + " fract_x=0,\n", + " fract_y=0,\n", + " fract_z=0,\n", + " wyckoff_letter='a',\n", + " b_iso=0.5,\n", + ")\n", + "structure.atom_sites.create(\n", + " label='Co2',\n", + " type_symbol='Co',\n", + " fract_x=0.279,\n", + " fract_y=0.25,\n", + " fract_z=0.985,\n", + " wyckoff_letter='c',\n", + " b_iso=0.5,\n", + ")\n", + "structure.atom_sites.create(\n", + " label='Si',\n", + " type_symbol='Si',\n", + " fract_x=0.094,\n", + " fract_y=0.25,\n", + " fract_z=0.429,\n", + " wyckoff_letter='c',\n", + " b_iso=0.5,\n", + ")\n", + "structure.atom_sites.create(\n", + " label='O1',\n", + " type_symbol='O',\n", + " fract_x=0.091,\n", + " fract_y=0.25,\n", + " fract_z=0.771,\n", + " wyckoff_letter='c',\n", + " b_iso=0.5,\n", + ")\n", + "structure.atom_sites.create(\n", + " label='O2',\n", + " type_symbol='O',\n", + " fract_x=0.448,\n", + " fract_y=0.25,\n", + " fract_z=0.217,\n", + " wyckoff_letter='c',\n", + " b_iso=0.5,\n", + ")\n", + "structure.atom_sites.create(\n", + " label='O3',\n", + " type_symbol='O',\n", + " fract_x=0.164,\n", + " fract_y=0.032,\n", + " fract_z=0.28,\n", + " wyckoff_letter='d',\n", + " b_iso=0.5,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "11", + "metadata": {}, + "source": [ + "## Define Experiment\n", + "\n", + "This section shows how to add experiments, configure their parameters,\n", + "and link the structures defined in the previous step.\n", + "\n", + "#### Download Measured Data" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "12", + "metadata": {}, + "outputs": [], + "source": [ + "data_path = download_data(id=12, destination='data')" + ] + }, + { + "cell_type": "markdown", + "id": "13", + "metadata": {}, + "source": [ + "#### Create Experiment" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "14", + "metadata": {}, + "outputs": [], + "source": [ + "expt = ExperimentFactory.from_data_path(name='d20', data_path=data_path)" + ] + }, + { + "cell_type": "markdown", + "id": "15", + "metadata": {}, + "source": [ + "#### Set Instrument" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "16", + "metadata": {}, + "outputs": [], + "source": [ + "expt.instrument.setup_wavelength = 1.87\n", + "expt.instrument.calib_twotheta_offset = 0.1" + ] + }, + { + "cell_type": "markdown", + "id": "17", + "metadata": {}, + "source": [ + "#### Set Peak Profile" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "18", + "metadata": {}, + "outputs": [], + "source": [ + "expt.peak.broad_gauss_u = 0.3\n", + "expt.peak.broad_gauss_v = -0.5\n", + "expt.peak.broad_gauss_w = 0.4" + ] + }, + { + "cell_type": "markdown", + "id": "19", + "metadata": {}, + "source": [ + "#### Set Background" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "20", + "metadata": {}, + "outputs": [], + "source": [ + "expt.background.create(id='1', x=8, y=500)\n", + "expt.background.create(id='2', x=9, y=500)\n", + "expt.background.create(id='3', x=10, y=500)\n", + "expt.background.create(id='4', x=11, y=500)\n", + "expt.background.create(id='5', x=12, y=500)\n", + "expt.background.create(id='6', x=15, y=500)\n", + "expt.background.create(id='7', x=25, y=500)\n", + "expt.background.create(id='8', x=30, y=500)\n", + "expt.background.create(id='9', x=50, y=500)\n", + "expt.background.create(id='10', x=70, y=500)\n", + "expt.background.create(id='11', x=90, y=500)\n", + "expt.background.create(id='12', x=110, y=500)\n", + "expt.background.create(id='13', x=130, y=500)\n", + "expt.background.create(id='14', x=150, y=500)" + ] + }, + { + "cell_type": "markdown", + "id": "21", + "metadata": {}, + "source": [ + "#### Set Linked Phases" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "22", + "metadata": {}, + "outputs": [], + "source": [ + "expt.linked_phases.create(id='cosio', scale=1.0)" + ] + }, + { + "cell_type": "markdown", + "id": "23", + "metadata": {}, + "source": [ + "## Define Project\n", + "\n", + "The project object is used to manage the structure, experiment, and\n", + "analysis.\n", + "\n", + "#### Create Project" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "24", + "metadata": {}, + "outputs": [], + "source": [ + "project = Project()" + ] + }, + { + "cell_type": "markdown", + "id": "25", + "metadata": {}, + "source": [ + "#### Set Plotting Engine" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "26", + "metadata": {}, + "outputs": [], + "source": [ + "# Keep the auto-selected engine. Alternatively, you can uncomment the\n", + "# line below to explicitly set the engine to the required one.\n", + "# project.plotter.engine = 'plotly'" + ] + }, + { + "cell_type": "markdown", + "id": "27", + "metadata": {}, + "source": [ + "#### Add Structure" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "28", + "metadata": {}, + "outputs": [], + "source": [ + "project.structures.add(structure)" + ] + }, + { + "cell_type": "markdown", + "id": "29", + "metadata": {}, + "source": [ + "#### Add Experiment" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "30", + "metadata": {}, + "outputs": [], + "source": [ + "project.experiments.add(expt)" + ] + }, + { + "cell_type": "markdown", + "id": "31", + "metadata": {}, + "source": [ + "## Perform Analysis\n", + "\n", + "This section shows the analysis process, including how to set up\n", + "calculation and fitting engines.\n", + "\n", + "#### Set Minimizer" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "32", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.current_minimizer = 'lmfit'" + ] + }, + { + "cell_type": "markdown", + "id": "33", + "metadata": {}, + "source": [ + "#### Plot Measured vs Calculated" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "34", + "metadata": {}, + "outputs": [], + "source": [ + "project.plot_meas_vs_calc(expt_name='d20', show_residual=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "35", + "metadata": {}, + "outputs": [], + "source": [ + "project.plot_meas_vs_calc(expt_name='d20', x_min=41, x_max=54, show_residual=True)" + ] + }, + { + "cell_type": "markdown", + "id": "36", + "metadata": {}, + "source": [ + "#### Set Free Parameters" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "37", + "metadata": {}, + "outputs": [], + "source": [ + "structure.cell.length_a.free = True\n", + "structure.cell.length_b.free = True\n", + "structure.cell.length_c.free = True\n", + "\n", + "structure.atom_sites['Co2'].fract_x.free = True\n", + "structure.atom_sites['Co2'].fract_z.free = True\n", + "structure.atom_sites['Si'].fract_x.free = True\n", + "structure.atom_sites['Si'].fract_z.free = True\n", + "structure.atom_sites['O1'].fract_x.free = True\n", + "structure.atom_sites['O1'].fract_z.free = True\n", + "structure.atom_sites['O2'].fract_x.free = True\n", + "structure.atom_sites['O2'].fract_z.free = True\n", + "structure.atom_sites['O3'].fract_x.free = True\n", + "structure.atom_sites['O3'].fract_y.free = True\n", + "structure.atom_sites['O3'].fract_z.free = True\n", + "\n", + "structure.atom_sites['Co1'].b_iso.free = True\n", + "structure.atom_sites['Co2'].b_iso.free = True\n", + "structure.atom_sites['Si'].b_iso.free = True\n", + "structure.atom_sites['O1'].b_iso.free = True\n", + "structure.atom_sites['O2'].b_iso.free = True\n", + "structure.atom_sites['O3'].b_iso.free = True" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "38", + "metadata": {}, + "outputs": [], + "source": [ + "expt.linked_phases['cosio'].scale.free = True\n", + "\n", + "expt.instrument.calib_twotheta_offset.free = True\n", + "\n", + "expt.peak.broad_gauss_u.free = True\n", + "expt.peak.broad_gauss_v.free = True\n", + "expt.peak.broad_gauss_w.free = True\n", + "expt.peak.broad_lorentz_y.free = True\n", + "\n", + "for point in expt.background:\n", + " point.y.free = True" + ] + }, + { + "cell_type": "markdown", + "id": "39", + "metadata": {}, + "source": [ + "#### Set Constraints\n", + "\n", + "Set aliases for parameters." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "40", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.aliases.create(\n", + " label='biso_Co1',\n", + " param_uid=project.structures['cosio'].atom_sites['Co1'].b_iso.uid,\n", + ")\n", + "project.analysis.aliases.create(\n", + " label='biso_Co2',\n", + " param_uid=project.structures['cosio'].atom_sites['Co2'].b_iso.uid,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "41", + "metadata": {}, + "source": [ + "Set constraints." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "42", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.constraints.create(\n", + " expression='biso_Co2 = biso_Co1',\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "43", + "metadata": {}, + "source": [ + "Apply constraints." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "44", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.apply_constraints()" + ] + }, + { + "cell_type": "markdown", + "id": "45", + "metadata": {}, + "source": [ + "#### Run Fitting" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "46", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.fit()\n", + "project.analysis.show_fit_results()" + ] + }, + { + "cell_type": "markdown", + "id": "47", + "metadata": {}, + "source": [ + "#### Plot Measured vs Calculated" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "48", + "metadata": {}, + "outputs": [], + "source": [ + "project.plot_meas_vs_calc(expt_name='d20', show_residual=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "49", + "metadata": {}, + "outputs": [], + "source": [ + "project.plot_meas_vs_calc(expt_name='d20', x_min=41, x_max=54, show_residual=True)" + ] + }, + { + "cell_type": "markdown", + "id": "50", + "metadata": {}, + "source": [ + "## Summary\n", + "\n", + "This final section shows how to review the results of the analysis." + ] + }, + { + "cell_type": "markdown", + "id": "51", + "metadata": {}, + "source": [ + "#### Show Project Summary" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "52", + "metadata": {}, + "outputs": [], + "source": [ + "project.summary.show_report()" + ] + } + ], + "metadata": { + "jupytext": { + "cell_metadata_filter": "-all", + "main_language": "python", + "notebook_metadata_filter": "-all" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/tutorials/ed-5.py b/docs/docs/tutorials/ed-5.py similarity index 58% rename from tutorials/ed-5.py rename to docs/docs/tutorials/ed-5.py index 0e46baa8..74e1d887 100644 --- a/tutorials/ed-5.py +++ b/docs/docs/tutorials/ed-5.py @@ -11,40 +11,40 @@ # %% from easydiffraction import ExperimentFactory from easydiffraction import Project -from easydiffraction import SampleModelFactory +from easydiffraction import StructureFactory from easydiffraction import download_data # %% [markdown] -# ## Define Sample Model +# ## Define Structure # -# This section shows how to add sample models and modify their +# This section shows how to add structures and modify their # parameters. # -# #### Create Sample Model +# #### Create Structure # %% -model = SampleModelFactory.create(name='cosio') +structure = StructureFactory.from_scratch(name='cosio') # %% [markdown] # #### Set Space Group # %% -model.space_group.name_h_m = 'P n m a' -model.space_group.it_coordinate_system_code = 'abc' +structure.space_group.name_h_m = 'P n m a' +structure.space_group.it_coordinate_system_code = 'abc' # %% [markdown] # #### Set Unit Cell # %% -model.cell.length_a = 10.3 -model.cell.length_b = 6.0 -model.cell.length_c = 4.8 +structure.cell.length_a = 10.3 +structure.cell.length_b = 6.0 +structure.cell.length_c = 4.8 # %% [markdown] # #### Set Atom Sites # %% -model.atom_sites.add( +structure.atom_sites.create( label='Co1', type_symbol='Co', fract_x=0, @@ -53,7 +53,7 @@ wyckoff_letter='a', b_iso=0.5, ) -model.atom_sites.add( +structure.atom_sites.create( label='Co2', type_symbol='Co', fract_x=0.279, @@ -62,7 +62,7 @@ wyckoff_letter='c', b_iso=0.5, ) -model.atom_sites.add( +structure.atom_sites.create( label='Si', type_symbol='Si', fract_x=0.094, @@ -71,7 +71,7 @@ wyckoff_letter='c', b_iso=0.5, ) -model.atom_sites.add( +structure.atom_sites.create( label='O1', type_symbol='O', fract_x=0.091, @@ -80,7 +80,7 @@ wyckoff_letter='c', b_iso=0.5, ) -model.atom_sites.add( +structure.atom_sites.create( label='O2', type_symbol='O', fract_x=0.448, @@ -89,7 +89,7 @@ wyckoff_letter='c', b_iso=0.5, ) -model.atom_sites.add( +structure.atom_sites.create( label='O3', type_symbol='O', fract_x=0.164, @@ -103,7 +103,7 @@ # ## Define Experiment # # This section shows how to add experiments, configure their parameters, -# and link the sample models defined in the previous step. +# and link the structures defined in the previous step. # # #### Download Measured Data @@ -114,7 +114,7 @@ # #### Create Experiment # %% -expt = ExperimentFactory.create(name='d20', data_path=data_path) +expt = ExperimentFactory.from_data_path(name='d20', data_path=data_path) # %% [markdown] # #### Set Instrument @@ -135,31 +135,31 @@ # #### Set Background # %% -expt.background.add(id='1', x=8, y=500) -expt.background.add(id='2', x=9, y=500) -expt.background.add(id='3', x=10, y=500) -expt.background.add(id='4', x=11, y=500) -expt.background.add(id='5', x=12, y=500) -expt.background.add(id='6', x=15, y=500) -expt.background.add(id='7', x=25, y=500) -expt.background.add(id='8', x=30, y=500) -expt.background.add(id='9', x=50, y=500) -expt.background.add(id='10', x=70, y=500) -expt.background.add(id='11', x=90, y=500) -expt.background.add(id='12', x=110, y=500) -expt.background.add(id='13', x=130, y=500) -expt.background.add(id='14', x=150, y=500) +expt.background.create(id='1', x=8, y=500) +expt.background.create(id='2', x=9, y=500) +expt.background.create(id='3', x=10, y=500) +expt.background.create(id='4', x=11, y=500) +expt.background.create(id='5', x=12, y=500) +expt.background.create(id='6', x=15, y=500) +expt.background.create(id='7', x=25, y=500) +expt.background.create(id='8', x=30, y=500) +expt.background.create(id='9', x=50, y=500) +expt.background.create(id='10', x=70, y=500) +expt.background.create(id='11', x=90, y=500) +expt.background.create(id='12', x=110, y=500) +expt.background.create(id='13', x=130, y=500) +expt.background.create(id='14', x=150, y=500) # %% [markdown] # #### Set Linked Phases # %% -expt.linked_phases.add(id='cosio', scale=1.0) +expt.linked_phases.create(id='cosio', scale=1.0) # %% [markdown] # ## Define Project # -# The project object is used to manage the sample model, experiment, and +# The project object is used to manage the structure, experiment, and # analysis. # # #### Create Project @@ -176,16 +176,16 @@ # project.plotter.engine = 'plotly' # %% [markdown] -# #### Add Sample Model +# #### Add Structure # %% -project.sample_models.add(sample_model=model) +project.structures.add(structure) # %% [markdown] # #### Add Experiment # %% -project.experiments.add(experiment=expt) +project.experiments.add(expt) # %% [markdown] # ## Perform Analysis @@ -193,16 +193,10 @@ # This section shows the analysis process, including how to set up # calculation and fitting engines. # -# #### Set Calculator - -# %% -project.analysis.current_calculator = 'cryspy' - -# %% [markdown] # #### Set Minimizer # %% -project.analysis.current_minimizer = 'lmfit (leastsq)' +project.analysis.current_minimizer = 'lmfit' # %% [markdown] # #### Plot Measured vs Calculated @@ -217,28 +211,28 @@ # #### Set Free Parameters # %% -model.cell.length_a.free = True -model.cell.length_b.free = True -model.cell.length_c.free = True - -model.atom_sites['Co2'].fract_x.free = True -model.atom_sites['Co2'].fract_z.free = True -model.atom_sites['Si'].fract_x.free = True -model.atom_sites['Si'].fract_z.free = True -model.atom_sites['O1'].fract_x.free = True -model.atom_sites['O1'].fract_z.free = True -model.atom_sites['O2'].fract_x.free = True -model.atom_sites['O2'].fract_z.free = True -model.atom_sites['O3'].fract_x.free = True -model.atom_sites['O3'].fract_y.free = True -model.atom_sites['O3'].fract_z.free = True - -model.atom_sites['Co1'].b_iso.free = True -model.atom_sites['Co2'].b_iso.free = True -model.atom_sites['Si'].b_iso.free = True -model.atom_sites['O1'].b_iso.free = True -model.atom_sites['O2'].b_iso.free = True -model.atom_sites['O3'].b_iso.free = True +structure.cell.length_a.free = True +structure.cell.length_b.free = True +structure.cell.length_c.free = True + +structure.atom_sites['Co2'].fract_x.free = True +structure.atom_sites['Co2'].fract_z.free = True +structure.atom_sites['Si'].fract_x.free = True +structure.atom_sites['Si'].fract_z.free = True +structure.atom_sites['O1'].fract_x.free = True +structure.atom_sites['O1'].fract_z.free = True +structure.atom_sites['O2'].fract_x.free = True +structure.atom_sites['O2'].fract_z.free = True +structure.atom_sites['O3'].fract_x.free = True +structure.atom_sites['O3'].fract_y.free = True +structure.atom_sites['O3'].fract_z.free = True + +structure.atom_sites['Co1'].b_iso.free = True +structure.atom_sites['Co2'].b_iso.free = True +structure.atom_sites['Si'].b_iso.free = True +structure.atom_sites['O1'].b_iso.free = True +structure.atom_sites['O2'].b_iso.free = True +structure.atom_sites['O3'].b_iso.free = True # %% expt.linked_phases['cosio'].scale.free = True @@ -259,22 +253,21 @@ # Set aliases for parameters. # %% -project.analysis.aliases.add( +project.analysis.aliases.create( label='biso_Co1', - param_uid=project.sample_models['cosio'].atom_sites['Co1'].b_iso.uid, + param_uid=project.structures['cosio'].atom_sites['Co1'].b_iso.uid, ) -project.analysis.aliases.add( +project.analysis.aliases.create( label='biso_Co2', - param_uid=project.sample_models['cosio'].atom_sites['Co2'].b_iso.uid, + param_uid=project.structures['cosio'].atom_sites['Co2'].b_iso.uid, ) # %% [markdown] # Set constraints. # %% -project.analysis.constraints.add( - lhs_alias='biso_Co2', - rhs_expr='biso_Co1', +project.analysis.constraints.create( + expression='biso_Co2 = biso_Co1', ) # %% [markdown] diff --git a/docs/docs/tutorials/ed-6.ipynb b/docs/docs/tutorials/ed-6.ipynb new file mode 100644 index 00000000..599c09eb --- /dev/null +++ b/docs/docs/tutorials/ed-6.ipynb @@ -0,0 +1,849 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0", + "metadata": {}, + "source": [ + "# Structure Refinement: HS, HRPT\n", + "\n", + "This example demonstrates a Rietveld refinement of HS crystal\n", + "structure using constant wavelength neutron powder diffraction data\n", + "from HRPT at PSI." + ] + }, + { + "cell_type": "markdown", + "id": "1", + "metadata": {}, + "source": [ + "## Import Library" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2", + "metadata": {}, + "outputs": [], + "source": [ + "from easydiffraction import ExperimentFactory\n", + "from easydiffraction import Project\n", + "from easydiffraction import StructureFactory\n", + "from easydiffraction import download_data" + ] + }, + { + "cell_type": "markdown", + "id": "3", + "metadata": {}, + "source": [ + "## Define Structure\n", + "\n", + "This section shows how to add structures and modify their\n", + "parameters.\n", + "\n", + "#### Create Structure" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4", + "metadata": {}, + "outputs": [], + "source": [ + "structure = StructureFactory.from_scratch(name='hs')" + ] + }, + { + "cell_type": "markdown", + "id": "5", + "metadata": {}, + "source": [ + "#### Set Space Group" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6", + "metadata": {}, + "outputs": [], + "source": [ + "structure.space_group.name_h_m = 'R -3 m'\n", + "structure.space_group.it_coordinate_system_code = 'h'" + ] + }, + { + "cell_type": "markdown", + "id": "7", + "metadata": { + "lines_to_next_cell": 2 + }, + "source": [ + "#### Set Unit Cell" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8", + "metadata": {}, + "outputs": [], + "source": [ + "structure.cell.length_a = 6.9\n", + "structure.cell.length_c = 14.1" + ] + }, + { + "cell_type": "markdown", + "id": "9", + "metadata": {}, + "source": [ + "#### Set Atom Sites" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "10", + "metadata": {}, + "outputs": [], + "source": [ + "structure.atom_sites.create(\n", + " label='Zn',\n", + " type_symbol='Zn',\n", + " fract_x=0,\n", + " fract_y=0,\n", + " fract_z=0.5,\n", + " wyckoff_letter='b',\n", + " b_iso=0.5,\n", + ")\n", + "structure.atom_sites.create(\n", + " label='Cu',\n", + " type_symbol='Cu',\n", + " fract_x=0.5,\n", + " fract_y=0,\n", + " fract_z=0,\n", + " wyckoff_letter='e',\n", + " b_iso=0.5,\n", + ")\n", + "structure.atom_sites.create(\n", + " label='O',\n", + " type_symbol='O',\n", + " fract_x=0.21,\n", + " fract_y=-0.21,\n", + " fract_z=0.06,\n", + " wyckoff_letter='h',\n", + " b_iso=0.5,\n", + ")\n", + "structure.atom_sites.create(\n", + " label='Cl',\n", + " type_symbol='Cl',\n", + " fract_x=0,\n", + " fract_y=0,\n", + " fract_z=0.197,\n", + " wyckoff_letter='c',\n", + " b_iso=0.5,\n", + ")\n", + "structure.atom_sites.create(\n", + " label='H',\n", + " type_symbol='2H',\n", + " fract_x=0.13,\n", + " fract_y=-0.13,\n", + " fract_z=0.08,\n", + " wyckoff_letter='h',\n", + " b_iso=0.5,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "11", + "metadata": {}, + "source": [ + "## Define Experiment\n", + "\n", + "This section shows how to add experiments, configure their parameters,\n", + "and link the structures defined in the previous step.\n", + "\n", + "#### Download Measured Data" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "12", + "metadata": {}, + "outputs": [], + "source": [ + "data_path = download_data(id=11, destination='data')" + ] + }, + { + "cell_type": "markdown", + "id": "13", + "metadata": {}, + "source": [ + "#### Create Experiment" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "14", + "metadata": {}, + "outputs": [], + "source": [ + "expt = ExperimentFactory.from_data_path(name='hrpt', data_path=data_path)" + ] + }, + { + "cell_type": "markdown", + "id": "15", + "metadata": {}, + "source": [ + "#### Set Instrument" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "16", + "metadata": {}, + "outputs": [], + "source": [ + "expt.instrument.setup_wavelength = 1.89\n", + "expt.instrument.calib_twotheta_offset = 0.0" + ] + }, + { + "cell_type": "markdown", + "id": "17", + "metadata": {}, + "source": [ + "#### Set Peak Profile" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "18", + "metadata": {}, + "outputs": [], + "source": [ + "expt.peak.broad_gauss_u = 0.1\n", + "expt.peak.broad_gauss_v = -0.2\n", + "expt.peak.broad_gauss_w = 0.2\n", + "expt.peak.broad_lorentz_x = 0.0\n", + "expt.peak.broad_lorentz_y = 0" + ] + }, + { + "cell_type": "markdown", + "id": "19", + "metadata": {}, + "source": [ + "#### Set Background" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "20", + "metadata": {}, + "outputs": [], + "source": [ + "expt.background.create(id='1', x=4.4196, y=500)\n", + "expt.background.create(id='2', x=6.6207, y=500)\n", + "expt.background.create(id='3', x=10.4918, y=500)\n", + "expt.background.create(id='4', x=15.4634, y=500)\n", + "expt.background.create(id='5', x=45.6041, y=500)\n", + "expt.background.create(id='6', x=74.6844, y=500)\n", + "expt.background.create(id='7', x=103.4187, y=500)\n", + "expt.background.create(id='8', x=121.6311, y=500)\n", + "expt.background.create(id='9', x=159.4116, y=500)" + ] + }, + { + "cell_type": "markdown", + "id": "21", + "metadata": {}, + "source": [ + "#### Set Linked Phases" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "22", + "metadata": {}, + "outputs": [], + "source": [ + "expt.linked_phases.create(id='hs', scale=0.5)" + ] + }, + { + "cell_type": "markdown", + "id": "23", + "metadata": {}, + "source": [ + "## Define Project\n", + "\n", + "The project object is used to manage the structure, experiment, and\n", + "analysis.\n", + "\n", + "#### Create Project" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "24", + "metadata": {}, + "outputs": [], + "source": [ + "project = Project()" + ] + }, + { + "cell_type": "markdown", + "id": "25", + "metadata": {}, + "source": [ + "#### Set Plotting Engine" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "26", + "metadata": {}, + "outputs": [], + "source": [ + "# Keep the auto-selected engine. Alternatively, you can uncomment the\n", + "# line below to explicitly set the engine to the required one.\n", + "# project.plotter.engine = 'plotly'" + ] + }, + { + "cell_type": "markdown", + "id": "27", + "metadata": {}, + "source": [ + "#### Add Structure" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "28", + "metadata": {}, + "outputs": [], + "source": [ + "project.structures.add(structure)" + ] + }, + { + "cell_type": "markdown", + "id": "29", + "metadata": {}, + "source": [ + "#### Add Experiment" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "30", + "metadata": {}, + "outputs": [], + "source": [ + "project.experiments.add(expt)" + ] + }, + { + "cell_type": "markdown", + "id": "31", + "metadata": {}, + "source": [ + "## Perform Analysis\n", + "\n", + "This section shows the analysis process, including how to set up\n", + "calculation and fitting engines.\n", + "\n", + "#### Set Minimizer" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "32", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.current_minimizer = 'lmfit'" + ] + }, + { + "cell_type": "markdown", + "id": "33", + "metadata": {}, + "source": [ + "#### Plot Measured vs Calculated" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "34", + "metadata": {}, + "outputs": [], + "source": [ + "project.plot_meas_vs_calc(expt_name='hrpt', show_residual=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "35", + "metadata": {}, + "outputs": [], + "source": [ + "project.plot_meas_vs_calc(expt_name='hrpt', x_min=48, x_max=51, show_residual=True)" + ] + }, + { + "cell_type": "markdown", + "id": "36", + "metadata": {}, + "source": [ + "### Perform Fit 1/5\n", + "\n", + "Set parameters to be refined." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "37", + "metadata": {}, + "outputs": [], + "source": [ + "structure.cell.length_a.free = True\n", + "structure.cell.length_c.free = True\n", + "\n", + "expt.linked_phases['hs'].scale.free = True\n", + "expt.instrument.calib_twotheta_offset.free = True" + ] + }, + { + "cell_type": "markdown", + "id": "38", + "metadata": {}, + "source": [ + "Show free parameters after selection." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "39", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.show_free_params()" + ] + }, + { + "cell_type": "markdown", + "id": "40", + "metadata": {}, + "source": [ + "#### Run Fitting" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "41", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.fit()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "42", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.show_fit_results()" + ] + }, + { + "cell_type": "markdown", + "id": "43", + "metadata": {}, + "source": [ + "#### Plot Measured vs Calculated" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "44", + "metadata": {}, + "outputs": [], + "source": [ + "project.plot_meas_vs_calc(expt_name='hrpt', show_residual=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "45", + "metadata": {}, + "outputs": [], + "source": [ + "project.plot_meas_vs_calc(expt_name='hrpt', x_min=48, x_max=51, show_residual=True)" + ] + }, + { + "cell_type": "markdown", + "id": "46", + "metadata": {}, + "source": [ + "### Perform Fit 2/5\n", + "\n", + "Set more parameters to be refined." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "47", + "metadata": {}, + "outputs": [], + "source": [ + "expt.peak.broad_gauss_u.free = True\n", + "expt.peak.broad_gauss_v.free = True\n", + "expt.peak.broad_gauss_w.free = True\n", + "expt.peak.broad_lorentz_x.free = True\n", + "\n", + "for point in expt.background:\n", + " point.y.free = True" + ] + }, + { + "cell_type": "markdown", + "id": "48", + "metadata": {}, + "source": [ + "Show free parameters after selection." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "49", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.show_free_params()" + ] + }, + { + "cell_type": "markdown", + "id": "50", + "metadata": {}, + "source": [ + "#### Run Fitting" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "51", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.fit()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "52", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.show_fit_results()" + ] + }, + { + "cell_type": "markdown", + "id": "53", + "metadata": {}, + "source": [ + "#### Plot Measured vs Calculated" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "54", + "metadata": {}, + "outputs": [], + "source": [ + "project.plot_meas_vs_calc(expt_name='hrpt', show_residual=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "55", + "metadata": {}, + "outputs": [], + "source": [ + "project.plot_meas_vs_calc(expt_name='hrpt', x_min=48, x_max=51, show_residual=True)" + ] + }, + { + "cell_type": "markdown", + "id": "56", + "metadata": {}, + "source": [ + "### Perform Fit 3/5\n", + "\n", + "Set more parameters to be refined." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "57", + "metadata": {}, + "outputs": [], + "source": [ + "structure.atom_sites['O'].fract_x.free = True\n", + "structure.atom_sites['O'].fract_z.free = True\n", + "structure.atom_sites['Cl'].fract_z.free = True\n", + "structure.atom_sites['H'].fract_x.free = True\n", + "structure.atom_sites['H'].fract_z.free = True" + ] + }, + { + "cell_type": "markdown", + "id": "58", + "metadata": {}, + "source": [ + "Show free parameters after selection." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "59", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.show_free_params()" + ] + }, + { + "cell_type": "markdown", + "id": "60", + "metadata": {}, + "source": [ + "#### Run Fitting" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "61", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.fit()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "62", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.show_fit_results()" + ] + }, + { + "cell_type": "markdown", + "id": "63", + "metadata": {}, + "source": [ + "#### Plot Measured vs Calculated" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "64", + "metadata": {}, + "outputs": [], + "source": [ + "project.plot_meas_vs_calc(expt_name='hrpt', show_residual=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "65", + "metadata": {}, + "outputs": [], + "source": [ + "project.plot_meas_vs_calc(expt_name='hrpt', x_min=48, x_max=51, show_residual=True)" + ] + }, + { + "cell_type": "markdown", + "id": "66", + "metadata": {}, + "source": [ + "### Perform Fit 4/5\n", + "\n", + "Set more parameters to be refined." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "67", + "metadata": {}, + "outputs": [], + "source": [ + "structure.atom_sites['Zn'].b_iso.free = True\n", + "structure.atom_sites['Cu'].b_iso.free = True\n", + "structure.atom_sites['O'].b_iso.free = True\n", + "structure.atom_sites['Cl'].b_iso.free = True\n", + "structure.atom_sites['H'].b_iso.free = True" + ] + }, + { + "cell_type": "markdown", + "id": "68", + "metadata": {}, + "source": [ + "Show free parameters after selection." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "69", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.show_free_params()" + ] + }, + { + "cell_type": "markdown", + "id": "70", + "metadata": {}, + "source": [ + "#### Run Fitting" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "71", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.fit()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "72", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.show_fit_results()" + ] + }, + { + "cell_type": "markdown", + "id": "73", + "metadata": {}, + "source": [ + "#### Plot Measured vs Calculated" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "74", + "metadata": {}, + "outputs": [], + "source": [ + "project.plot_meas_vs_calc(expt_name='hrpt', show_residual=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "75", + "metadata": {}, + "outputs": [], + "source": [ + "project.plot_meas_vs_calc(expt_name='hrpt', x_min=48, x_max=51, show_residual=True)" + ] + }, + { + "cell_type": "markdown", + "id": "76", + "metadata": {}, + "source": [ + "## Summary\n", + "\n", + "This final section shows how to review the results of the analysis." + ] + }, + { + "cell_type": "markdown", + "id": "77", + "metadata": {}, + "source": [ + "#### Show Project Summary" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "78", + "metadata": {}, + "outputs": [], + "source": [ + "project.summary.show_report()" + ] + } + ], + "metadata": { + "jupytext": { + "cell_metadata_filter": "-all", + "main_language": "python", + "notebook_metadata_filter": "-all" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/tutorials/ed-6.py b/docs/docs/tutorials/ed-6.py similarity index 73% rename from tutorials/ed-6.py rename to docs/docs/tutorials/ed-6.py index 106a088c..e0339c91 100644 --- a/tutorials/ed-6.py +++ b/docs/docs/tutorials/ed-6.py @@ -11,40 +11,40 @@ # %% from easydiffraction import ExperimentFactory from easydiffraction import Project -from easydiffraction import SampleModelFactory +from easydiffraction import StructureFactory from easydiffraction import download_data # %% [markdown] -# ## Define Sample Model +# ## Define Structure # -# This section shows how to add sample models and modify their +# This section shows how to add structures and modify their # parameters. # -# #### Create Sample Model +# #### Create Structure # %% -model = SampleModelFactory.create(name='hs') +structure = StructureFactory.from_scratch(name='hs') # %% [markdown] # #### Set Space Group # %% -model.space_group.name_h_m = 'R -3 m' -model.space_group.it_coordinate_system_code = 'h' +structure.space_group.name_h_m = 'R -3 m' +structure.space_group.it_coordinate_system_code = 'h' # %% [markdown] # #### Set Unit Cell # %% -model.cell.length_a = 6.9 -model.cell.length_c = 14.1 +structure.cell.length_a = 6.9 +structure.cell.length_c = 14.1 # %% [markdown] # #### Set Atom Sites # %% -model.atom_sites.add( +structure.atom_sites.create( label='Zn', type_symbol='Zn', fract_x=0, @@ -53,7 +53,7 @@ wyckoff_letter='b', b_iso=0.5, ) -model.atom_sites.add( +structure.atom_sites.create( label='Cu', type_symbol='Cu', fract_x=0.5, @@ -62,7 +62,7 @@ wyckoff_letter='e', b_iso=0.5, ) -model.atom_sites.add( +structure.atom_sites.create( label='O', type_symbol='O', fract_x=0.21, @@ -71,7 +71,7 @@ wyckoff_letter='h', b_iso=0.5, ) -model.atom_sites.add( +structure.atom_sites.create( label='Cl', type_symbol='Cl', fract_x=0, @@ -80,7 +80,7 @@ wyckoff_letter='c', b_iso=0.5, ) -model.atom_sites.add( +structure.atom_sites.create( label='H', type_symbol='2H', fract_x=0.13, @@ -94,7 +94,7 @@ # ## Define Experiment # # This section shows how to add experiments, configure their parameters, -# and link the sample models defined in the previous step. +# and link the structures defined in the previous step. # # #### Download Measured Data @@ -105,7 +105,7 @@ # #### Create Experiment # %% -expt = ExperimentFactory.create(name='hrpt', data_path=data_path) +expt = ExperimentFactory.from_data_path(name='hrpt', data_path=data_path) # %% [markdown] # #### Set Instrument @@ -128,26 +128,26 @@ # #### Set Background # %% -expt.background.add(id='1', x=4.4196, y=500) -expt.background.add(id='2', x=6.6207, y=500) -expt.background.add(id='3', x=10.4918, y=500) -expt.background.add(id='4', x=15.4634, y=500) -expt.background.add(id='5', x=45.6041, y=500) -expt.background.add(id='6', x=74.6844, y=500) -expt.background.add(id='7', x=103.4187, y=500) -expt.background.add(id='8', x=121.6311, y=500) -expt.background.add(id='9', x=159.4116, y=500) +expt.background.create(id='1', x=4.4196, y=500) +expt.background.create(id='2', x=6.6207, y=500) +expt.background.create(id='3', x=10.4918, y=500) +expt.background.create(id='4', x=15.4634, y=500) +expt.background.create(id='5', x=45.6041, y=500) +expt.background.create(id='6', x=74.6844, y=500) +expt.background.create(id='7', x=103.4187, y=500) +expt.background.create(id='8', x=121.6311, y=500) +expt.background.create(id='9', x=159.4116, y=500) # %% [markdown] # #### Set Linked Phases # %% -expt.linked_phases.add(id='hs', scale=0.5) +expt.linked_phases.create(id='hs', scale=0.5) # %% [markdown] # ## Define Project # -# The project object is used to manage the sample model, experiment, and +# The project object is used to manage the structure, experiment, and # analysis. # # #### Create Project @@ -164,16 +164,16 @@ # project.plotter.engine = 'plotly' # %% [markdown] -# #### Add Sample Model +# #### Add Structure # %% -project.sample_models.add(sample_model=model) +project.structures.add(structure) # %% [markdown] # #### Add Experiment # %% -project.experiments.add(experiment=expt) +project.experiments.add(expt) # %% [markdown] # ## Perform Analysis @@ -181,16 +181,10 @@ # This section shows the analysis process, including how to set up # calculation and fitting engines. # -# #### Set Calculator - -# %% -project.analysis.current_calculator = 'cryspy' - -# %% [markdown] # #### Set Minimizer # %% -project.analysis.current_minimizer = 'lmfit (leastsq)' +project.analysis.current_minimizer = 'lmfit' # %% [markdown] # #### Plot Measured vs Calculated @@ -207,8 +201,8 @@ # Set parameters to be refined. # %% -model.cell.length_a.free = True -model.cell.length_c.free = True +structure.cell.length_a.free = True +structure.cell.length_c.free = True expt.linked_phases['hs'].scale.free = True expt.instrument.calib_twotheta_offset.free = True @@ -281,11 +275,11 @@ # Set more parameters to be refined. # %% -model.atom_sites['O'].fract_x.free = True -model.atom_sites['O'].fract_z.free = True -model.atom_sites['Cl'].fract_z.free = True -model.atom_sites['H'].fract_x.free = True -model.atom_sites['H'].fract_z.free = True +structure.atom_sites['O'].fract_x.free = True +structure.atom_sites['O'].fract_z.free = True +structure.atom_sites['Cl'].fract_z.free = True +structure.atom_sites['H'].fract_x.free = True +structure.atom_sites['H'].fract_z.free = True # %% [markdown] # Show free parameters after selection. @@ -317,11 +311,11 @@ # Set more parameters to be refined. # %% -model.atom_sites['Zn'].b_iso.free = True -model.atom_sites['Cu'].b_iso.free = True -model.atom_sites['O'].b_iso.free = True -model.atom_sites['Cl'].b_iso.free = True -model.atom_sites['H'].b_iso.free = True +structure.atom_sites['Zn'].b_iso.free = True +structure.atom_sites['Cu'].b_iso.free = True +structure.atom_sites['O'].b_iso.free = True +structure.atom_sites['Cl'].b_iso.free = True +structure.atom_sites['H'].b_iso.free = True # %% [markdown] # Show free parameters after selection. diff --git a/docs/docs/tutorials/ed-7.ipynb b/docs/docs/tutorials/ed-7.ipynb new file mode 100644 index 00000000..869dc723 --- /dev/null +++ b/docs/docs/tutorials/ed-7.ipynb @@ -0,0 +1,741 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0", + "metadata": {}, + "source": [ + "# Structure Refinement: Si, SEPD\n", + "\n", + "This example demonstrates a Rietveld refinement of Si crystal\n", + "structure using time-of-flight neutron powder diffraction data from\n", + "SEPD at Argonne." + ] + }, + { + "cell_type": "markdown", + "id": "1", + "metadata": {}, + "source": [ + "## Import Library" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2", + "metadata": {}, + "outputs": [], + "source": [ + "from easydiffraction import ExperimentFactory\n", + "from easydiffraction import Project\n", + "from easydiffraction import StructureFactory\n", + "from easydiffraction import download_data" + ] + }, + { + "cell_type": "markdown", + "id": "3", + "metadata": {}, + "source": [ + "## Define Structure\n", + "\n", + "This section shows how to add structures and modify their\n", + "parameters.\n", + "\n", + "#### Create Structure" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4", + "metadata": {}, + "outputs": [], + "source": [ + "structure = StructureFactory.from_scratch(name='si')" + ] + }, + { + "cell_type": "markdown", + "id": "5", + "metadata": {}, + "source": [ + "#### Set Space Group" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6", + "metadata": {}, + "outputs": [], + "source": [ + "structure.space_group.name_h_m = 'F d -3 m'\n", + "structure.space_group.it_coordinate_system_code = '2'" + ] + }, + { + "cell_type": "markdown", + "id": "7", + "metadata": {}, + "source": [ + "#### Set Unit Cell" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8", + "metadata": {}, + "outputs": [], + "source": [ + "structure.cell.length_a = 5.431" + ] + }, + { + "cell_type": "markdown", + "id": "9", + "metadata": {}, + "source": [ + "#### Set Atom Sites" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "10", + "metadata": {}, + "outputs": [], + "source": [ + "structure.atom_sites.create(\n", + " label='Si',\n", + " type_symbol='Si',\n", + " fract_x=0.125,\n", + " fract_y=0.125,\n", + " fract_z=0.125,\n", + " b_iso=0.5,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "11", + "metadata": {}, + "source": [ + "## Define Experiment\n", + "\n", + "This section shows how to add experiments, configure their\n", + "parameters, and link the structures defined in the previous step.\n", + "\n", + "#### Download Measured Data" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "12", + "metadata": {}, + "outputs": [], + "source": [ + "data_path = download_data(id=7, destination='data')" + ] + }, + { + "cell_type": "markdown", + "id": "13", + "metadata": {}, + "source": [ + "#### Create Experiment" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "14", + "metadata": {}, + "outputs": [], + "source": [ + "expt = ExperimentFactory.from_data_path(\n", + " name='sepd', data_path=data_path, beam_mode='time-of-flight'\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "15", + "metadata": {}, + "source": [ + "#### Set Instrument" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "16", + "metadata": {}, + "outputs": [], + "source": [ + "expt.instrument.setup_twotheta_bank = 144.845\n", + "expt.instrument.calib_d_to_tof_offset = 0.0\n", + "expt.instrument.calib_d_to_tof_linear = 7476.91\n", + "expt.instrument.calib_d_to_tof_quad = -1.54" + ] + }, + { + "cell_type": "markdown", + "id": "17", + "metadata": {}, + "source": [ + "#### Set Peak Profile" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "18", + "metadata": {}, + "outputs": [], + "source": [ + "expt.peak_profile_type = 'pseudo-voigt * ikeda-carpenter'\n", + "expt.peak.broad_gauss_sigma_0 = 3.0\n", + "expt.peak.broad_gauss_sigma_1 = 40.0\n", + "expt.peak.broad_gauss_sigma_2 = 2.0\n", + "expt.peak.broad_mix_beta_0 = 0.04221\n", + "expt.peak.broad_mix_beta_1 = 0.00946" + ] + }, + { + "cell_type": "markdown", + "id": "19", + "metadata": {}, + "source": [ + "#### Set Peak Asymmetry" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "20", + "metadata": {}, + "outputs": [], + "source": [ + "expt.peak.asym_alpha_0 = 0.0\n", + "expt.peak.asym_alpha_1 = 0.5971" + ] + }, + { + "cell_type": "markdown", + "id": "21", + "metadata": {}, + "source": [ + "#### Set Background" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "22", + "metadata": {}, + "outputs": [], + "source": [ + "expt.background_type = 'line-segment'\n", + "for x in range(0, 35000, 5000):\n", + " expt.background.create(id=str(x), x=x, y=200)" + ] + }, + { + "cell_type": "markdown", + "id": "23", + "metadata": {}, + "source": [ + "#### Set Linked Phases" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "24", + "metadata": {}, + "outputs": [], + "source": [ + "expt.linked_phases.create(id='si', scale=10.0)" + ] + }, + { + "cell_type": "markdown", + "id": "25", + "metadata": {}, + "source": [ + "## Define Project\n", + "\n", + "The project object is used to manage the structure, experiment, and\n", + "analysis.\n", + "\n", + "#### Create Project" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "26", + "metadata": {}, + "outputs": [], + "source": [ + "project = Project()" + ] + }, + { + "cell_type": "markdown", + "id": "27", + "metadata": {}, + "source": [ + "#### Add Structure" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "28", + "metadata": {}, + "outputs": [], + "source": [ + "project.structures.add(structure)" + ] + }, + { + "cell_type": "markdown", + "id": "29", + "metadata": {}, + "source": [ + "#### Add Experiment" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "30", + "metadata": {}, + "outputs": [], + "source": [ + "project.experiments.add(expt)" + ] + }, + { + "cell_type": "markdown", + "id": "31", + "metadata": {}, + "source": [ + "## Perform Analysis\n", + "\n", + "This section shows the analysis process, including how to set up\n", + "calculation and fitting engines.\n", + "\n", + "#### Set Minimizer" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "32", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.current_minimizer = 'lmfit'" + ] + }, + { + "cell_type": "markdown", + "id": "33", + "metadata": {}, + "source": [ + "#### Plot Measured vs Calculated" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "34", + "metadata": {}, + "outputs": [], + "source": [ + "project.plot_meas_vs_calc(expt_name='sepd', show_residual=True)\n", + "project.plot_meas_vs_calc(expt_name='sepd', x_min=23200, x_max=23700, show_residual=True)" + ] + }, + { + "cell_type": "markdown", + "id": "35", + "metadata": {}, + "source": [ + "### Perform Fit 1/5\n", + "\n", + "Set parameters to be refined." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "36", + "metadata": {}, + "outputs": [], + "source": [ + "structure.cell.length_a.free = True\n", + "\n", + "expt.linked_phases['si'].scale.free = True\n", + "expt.instrument.calib_d_to_tof_offset.free = True" + ] + }, + { + "cell_type": "markdown", + "id": "37", + "metadata": {}, + "source": [ + "Show free parameters after selection." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "38", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.show_free_params()" + ] + }, + { + "cell_type": "markdown", + "id": "39", + "metadata": {}, + "source": [ + "#### Run Fitting" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "40", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.fit()\n", + "project.analysis.show_fit_results()" + ] + }, + { + "cell_type": "markdown", + "id": "41", + "metadata": {}, + "source": [ + "#### Plot Measured vs Calculated" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "42", + "metadata": {}, + "outputs": [], + "source": [ + "project.plot_meas_vs_calc(expt_name='sepd', show_residual=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "43", + "metadata": {}, + "outputs": [], + "source": [ + "project.plot_meas_vs_calc(expt_name='sepd', x_min=23200, x_max=23700, show_residual=True)" + ] + }, + { + "cell_type": "markdown", + "id": "44", + "metadata": {}, + "source": [ + "### Perform Fit 2/5\n", + "\n", + "Set more parameters to be refined." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "45", + "metadata": {}, + "outputs": [], + "source": [ + "for point in expt.background:\n", + " point.y.free = True" + ] + }, + { + "cell_type": "markdown", + "id": "46", + "metadata": {}, + "source": [ + "Show free parameters after selection." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "47", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.show_free_params()" + ] + }, + { + "cell_type": "markdown", + "id": "48", + "metadata": {}, + "source": [ + "#### Run Fitting" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "49", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.fit()\n", + "project.analysis.show_fit_results()" + ] + }, + { + "cell_type": "markdown", + "id": "50", + "metadata": {}, + "source": [ + "#### Plot Measured vs Calculated" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "51", + "metadata": {}, + "outputs": [], + "source": [ + "project.plot_meas_vs_calc(expt_name='sepd', show_residual=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "52", + "metadata": {}, + "outputs": [], + "source": [ + "project.plot_meas_vs_calc(expt_name='sepd', x_min=23200, x_max=23700, show_residual=True)" + ] + }, + { + "cell_type": "markdown", + "id": "53", + "metadata": {}, + "source": [ + "### Perform Fit 3/5\n", + "\n", + "Fix background points." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "54", + "metadata": {}, + "outputs": [], + "source": [ + "for point in expt.background:\n", + " point.y.free = False" + ] + }, + { + "cell_type": "markdown", + "id": "55", + "metadata": {}, + "source": [ + "Set more parameters to be refined." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "56", + "metadata": {}, + "outputs": [], + "source": [ + "expt.peak.broad_gauss_sigma_0.free = True\n", + "expt.peak.broad_gauss_sigma_1.free = True\n", + "expt.peak.broad_gauss_sigma_2.free = True" + ] + }, + { + "cell_type": "markdown", + "id": "57", + "metadata": {}, + "source": [ + "Show free parameters after selection." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "58", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.show_free_params()" + ] + }, + { + "cell_type": "markdown", + "id": "59", + "metadata": {}, + "source": [ + "#### Run Fitting" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "60", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.fit()\n", + "project.analysis.show_fit_results()" + ] + }, + { + "cell_type": "markdown", + "id": "61", + "metadata": {}, + "source": [ + "#### Plot Measured vs Calculated" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "62", + "metadata": {}, + "outputs": [], + "source": [ + "project.plot_meas_vs_calc(expt_name='sepd', show_residual=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "63", + "metadata": {}, + "outputs": [], + "source": [ + "project.plot_meas_vs_calc(expt_name='sepd', x_min=23200, x_max=23700, show_residual=True)" + ] + }, + { + "cell_type": "markdown", + "id": "64", + "metadata": {}, + "source": [ + "### Perform Fit 4/5\n", + "\n", + "Set more parameters to be refined." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "65", + "metadata": {}, + "outputs": [], + "source": [ + "structure.atom_sites['Si'].b_iso.free = True" + ] + }, + { + "cell_type": "markdown", + "id": "66", + "metadata": {}, + "source": [ + "Show free parameters after selection." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "67", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.show_free_params()" + ] + }, + { + "cell_type": "markdown", + "id": "68", + "metadata": {}, + "source": [ + "#### Run Fitting" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "69", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.fit()\n", + "project.analysis.show_fit_results()" + ] + }, + { + "cell_type": "markdown", + "id": "70", + "metadata": {}, + "source": [ + "#### Plot Measured vs Calculated" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "71", + "metadata": {}, + "outputs": [], + "source": [ + "project.plot_meas_vs_calc(expt_name='sepd', show_residual=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "72", + "metadata": {}, + "outputs": [], + "source": [ + "project.plot_meas_vs_calc(expt_name='sepd', x_min=23200, x_max=23700, show_residual=True)" + ] + } + ], + "metadata": { + "jupytext": { + "cell_metadata_filter": "-all", + "main_language": "python", + "notebook_metadata_filter": "-all" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/tutorials/ed-7.py b/docs/docs/tutorials/ed-7.py similarity index 82% rename from tutorials/ed-7.py rename to docs/docs/tutorials/ed-7.py index 1e12be88..cd719154 100644 --- a/tutorials/ed-7.py +++ b/docs/docs/tutorials/ed-7.py @@ -11,38 +11,38 @@ # %% from easydiffraction import ExperimentFactory from easydiffraction import Project -from easydiffraction import SampleModelFactory +from easydiffraction import StructureFactory from easydiffraction import download_data # %% [markdown] -# ## Define Sample Model +# ## Define Structure # -# This section shows how to add sample models and modify their +# This section shows how to add structures and modify their # parameters. # -# #### Create Sample Model +# #### Create Structure # %% -model = SampleModelFactory.create(name='si') +structure = StructureFactory.from_scratch(name='si') # %% [markdown] # #### Set Space Group # %% -model.space_group.name_h_m = 'F d -3 m' -model.space_group.it_coordinate_system_code = '2' +structure.space_group.name_h_m = 'F d -3 m' +structure.space_group.it_coordinate_system_code = '2' # %% [markdown] # #### Set Unit Cell # %% -model.cell.length_a = 5.431 +structure.cell.length_a = 5.431 # %% [markdown] # #### Set Atom Sites # %% -model.atom_sites.add( +structure.atom_sites.create( label='Si', type_symbol='Si', fract_x=0.125, @@ -55,7 +55,7 @@ # ## Define Experiment # # This section shows how to add experiments, configure their -# parameters, and link the sample models defined in the previous step. +# parameters, and link the structures defined in the previous step. # # #### Download Measured Data @@ -66,7 +66,9 @@ # #### Create Experiment # %% -expt = ExperimentFactory.create(name='sepd', data_path=data_path, beam_mode='time-of-flight') +expt = ExperimentFactory.from_data_path( + name='sepd', data_path=data_path, beam_mode='time-of-flight' +) # %% [markdown] # #### Set Instrument @@ -101,18 +103,18 @@ # %% expt.background_type = 'line-segment' for x in range(0, 35000, 5000): - expt.background.add(id=str(x), x=x, y=200) + expt.background.create(id=str(x), x=x, y=200) # %% [markdown] # #### Set Linked Phases # %% -expt.linked_phases.add(id='si', scale=10.0) +expt.linked_phases.create(id='si', scale=10.0) # %% [markdown] # ## Define Project # -# The project object is used to manage the sample model, experiment, and +# The project object is used to manage the structure, experiment, and # analysis. # # #### Create Project @@ -121,16 +123,16 @@ project = Project() # %% [markdown] -# #### Add Sample Model +# #### Add Structure # %% -project.sample_models.add(sample_model=model) +project.structures.add(structure) # %% [markdown] # #### Add Experiment # %% -project.experiments.add(experiment=expt) +project.experiments.add(expt) # %% [markdown] # ## Perform Analysis @@ -138,16 +140,10 @@ # This section shows the analysis process, including how to set up # calculation and fitting engines. # -# #### Set Calculator - -# %% -project.analysis.current_calculator = 'cryspy' - -# %% [markdown] # #### Set Minimizer # %% -project.analysis.current_minimizer = 'lmfit (leastsq)' +project.analysis.current_minimizer = 'lmfit' # %% [markdown] # #### Plot Measured vs Calculated @@ -162,7 +158,7 @@ # Set parameters to be refined. # %% -model.cell.length_a.free = True +structure.cell.length_a.free = True expt.linked_phases['si'].scale.free = True expt.instrument.calib_d_to_tof_offset.free = True @@ -265,7 +261,7 @@ # Set more parameters to be refined. # %% -model.atom_sites['Si'].b_iso.free = True +structure.atom_sites['Si'].b_iso.free = True # %% [markdown] # Show free parameters after selection. diff --git a/docs/docs/tutorials/ed-8.ipynb b/docs/docs/tutorials/ed-8.ipynb new file mode 100644 index 00000000..4bc42ad1 --- /dev/null +++ b/docs/docs/tutorials/ed-8.ipynb @@ -0,0 +1,747 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0", + "metadata": {}, + "source": [ + "# Structure Refinement: NCAF, WISH\n", + "\n", + "This example demonstrates a Rietveld refinement of Na2Ca3Al2F14\n", + "crystal structure using time-of-flight neutron powder diffraction data\n", + "from WISH at ISIS.\n", + "\n", + "Two datasets from detector banks 5+6 and 4+7 are used for joint\n", + "fitting." + ] + }, + { + "cell_type": "markdown", + "id": "1", + "metadata": {}, + "source": [ + "## Import Library" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2", + "metadata": {}, + "outputs": [], + "source": [ + "from easydiffraction import ExperimentFactory\n", + "from easydiffraction import Project\n", + "from easydiffraction import StructureFactory\n", + "from easydiffraction import download_data" + ] + }, + { + "cell_type": "markdown", + "id": "3", + "metadata": {}, + "source": [ + "## Define Structure\n", + "\n", + "This section covers how to add structures and modify their\n", + "parameters.\n", + "\n", + "#### Create Structure" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4", + "metadata": {}, + "outputs": [], + "source": [ + "structure = StructureFactory.from_scratch(name='ncaf')" + ] + }, + { + "cell_type": "markdown", + "id": "5", + "metadata": {}, + "source": [ + "#### Set Space Group" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6", + "metadata": {}, + "outputs": [], + "source": [ + "structure.space_group.name_h_m = 'I 21 3'\n", + "structure.space_group.it_coordinate_system_code = '1'" + ] + }, + { + "cell_type": "markdown", + "id": "7", + "metadata": {}, + "source": [ + "#### Set Unit Cell" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8", + "metadata": {}, + "outputs": [], + "source": [ + "structure.cell.length_a = 10.250256" + ] + }, + { + "cell_type": "markdown", + "id": "9", + "metadata": {}, + "source": [ + "#### Set Atom Sites" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "10", + "metadata": {}, + "outputs": [], + "source": [ + "structure.atom_sites.create(\n", + " label='Ca',\n", + " type_symbol='Ca',\n", + " fract_x=0.4663,\n", + " fract_y=0.0,\n", + " fract_z=0.25,\n", + " wyckoff_letter='b',\n", + " b_iso=0.92,\n", + ")\n", + "structure.atom_sites.create(\n", + " label='Al',\n", + " type_symbol='Al',\n", + " fract_x=0.2521,\n", + " fract_y=0.2521,\n", + " fract_z=0.2521,\n", + " wyckoff_letter='a',\n", + " b_iso=0.73,\n", + ")\n", + "structure.atom_sites.create(\n", + " label='Na',\n", + " type_symbol='Na',\n", + " fract_x=0.0851,\n", + " fract_y=0.0851,\n", + " fract_z=0.0851,\n", + " wyckoff_letter='a',\n", + " b_iso=2.08,\n", + ")\n", + "structure.atom_sites.create(\n", + " label='F1',\n", + " type_symbol='F',\n", + " fract_x=0.1377,\n", + " fract_y=0.3054,\n", + " fract_z=0.1195,\n", + " wyckoff_letter='c',\n", + " b_iso=0.90,\n", + ")\n", + "structure.atom_sites.create(\n", + " label='F2',\n", + " type_symbol='F',\n", + " fract_x=0.3625,\n", + " fract_y=0.3633,\n", + " fract_z=0.1867,\n", + " wyckoff_letter='c',\n", + " b_iso=1.37,\n", + ")\n", + "structure.atom_sites.create(\n", + " label='F3',\n", + " type_symbol='F',\n", + " fract_x=0.4612,\n", + " fract_y=0.4612,\n", + " fract_z=0.4612,\n", + " wyckoff_letter='a',\n", + " b_iso=0.88,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "11", + "metadata": {}, + "source": [ + "## Define Experiment\n", + "\n", + "This section shows how to add experiments, configure their parameters,\n", + "and link the structures defined in the previous step.\n", + "\n", + "#### Download Measured Data" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "12", + "metadata": {}, + "outputs": [], + "source": [ + "data_path56 = download_data(id=9, destination='data')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "13", + "metadata": {}, + "outputs": [], + "source": [ + "data_path47 = download_data(id=10, destination='data')" + ] + }, + { + "cell_type": "markdown", + "id": "14", + "metadata": {}, + "source": [ + "#### Create Experiment" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "15", + "metadata": {}, + "outputs": [], + "source": [ + "expt56 = ExperimentFactory.from_data_path(\n", + " name='wish_5_6',\n", + " data_path=data_path56,\n", + " beam_mode='time-of-flight',\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "16", + "metadata": {}, + "outputs": [], + "source": [ + "expt47 = ExperimentFactory.from_data_path(\n", + " name='wish_4_7',\n", + " data_path=data_path47,\n", + " beam_mode='time-of-flight',\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "17", + "metadata": {}, + "source": [ + "#### Set Instrument" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "18", + "metadata": {}, + "outputs": [], + "source": [ + "expt56.instrument.setup_twotheta_bank = 152.827\n", + "expt56.instrument.calib_d_to_tof_offset = -13.5\n", + "expt56.instrument.calib_d_to_tof_linear = 20773.0\n", + "expt56.instrument.calib_d_to_tof_quad = -1.08308" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "19", + "metadata": {}, + "outputs": [], + "source": [ + "expt47.instrument.setup_twotheta_bank = 121.660\n", + "expt47.instrument.calib_d_to_tof_offset = -15.0\n", + "expt47.instrument.calib_d_to_tof_linear = 18660.0\n", + "expt47.instrument.calib_d_to_tof_quad = -0.47488" + ] + }, + { + "cell_type": "markdown", + "id": "20", + "metadata": {}, + "source": [ + "#### Set Peak Profile" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "21", + "metadata": {}, + "outputs": [], + "source": [ + "expt56.peak.broad_gauss_sigma_0 = 0.0\n", + "expt56.peak.broad_gauss_sigma_1 = 0.0\n", + "expt56.peak.broad_gauss_sigma_2 = 15.5\n", + "expt56.peak.broad_mix_beta_0 = 0.007\n", + "expt56.peak.broad_mix_beta_1 = 0.01\n", + "expt56.peak.asym_alpha_0 = -0.0094\n", + "expt56.peak.asym_alpha_1 = 0.1" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "22", + "metadata": {}, + "outputs": [], + "source": [ + "expt47.peak.broad_gauss_sigma_0 = 0.0\n", + "expt47.peak.broad_gauss_sigma_1 = 29.8\n", + "expt47.peak.broad_gauss_sigma_2 = 18.0\n", + "expt47.peak.broad_mix_beta_0 = 0.006\n", + "expt47.peak.broad_mix_beta_1 = 0.015\n", + "expt47.peak.asym_alpha_0 = -0.0115\n", + "expt47.peak.asym_alpha_1 = 0.1" + ] + }, + { + "cell_type": "markdown", + "id": "23", + "metadata": {}, + "source": [ + "#### Set Background" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "24", + "metadata": {}, + "outputs": [], + "source": [ + "expt56.background_type = 'line-segment'\n", + "for idx, (x, y) in enumerate(\n", + " [\n", + " (9162, 465),\n", + " (11136, 593),\n", + " (13313, 497),\n", + " (14906, 546),\n", + " (16454, 533),\n", + " (17352, 496),\n", + " (18743, 428),\n", + " (20179, 452),\n", + " (21368, 397),\n", + " (22176, 468),\n", + " (22827, 477),\n", + " (24644, 380),\n", + " (26439, 381),\n", + " (28257, 378),\n", + " (31196, 343),\n", + " (34034, 328),\n", + " (37265, 310),\n", + " (41214, 323),\n", + " (44827, 283),\n", + " (49830, 273),\n", + " (52905, 257),\n", + " (58204, 260),\n", + " (62916, 261),\n", + " (70186, 262),\n", + " (74204, 262),\n", + " (82103, 268),\n", + " (91958, 268),\n", + " (102712, 262),\n", + " ],\n", + " start=1,\n", + "):\n", + " expt56.background.create(id=str(idx), x=x, y=y)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "25", + "metadata": {}, + "outputs": [], + "source": [ + "expt47.background_type = 'line-segment'\n", + "for idx, (x, y) in enumerate(\n", + " [\n", + " (9090, 488),\n", + " (10672, 566),\n", + " (12287, 494),\n", + " (14037, 559),\n", + " (15451, 529),\n", + " (16764, 445),\n", + " (18076, 460),\n", + " (19456, 413),\n", + " (20466, 511),\n", + " (21880, 396),\n", + " (23798, 391),\n", + " (25447, 385),\n", + " (28073, 349),\n", + " (30058, 332),\n", + " (32583, 309),\n", + " (34804, 355),\n", + " (37160, 318),\n", + " (40324, 290),\n", + " (46895, 260),\n", + " (50631, 256),\n", + " (54602, 246),\n", + " (58439, 264),\n", + " (66520, 250),\n", + " (75002, 258),\n", + " (83649, 257),\n", + " (92770, 255),\n", + " (101524, 260),\n", + " ],\n", + " start=1,\n", + "):\n", + " expt47.background.create(id=str(idx), x=x, y=y)" + ] + }, + { + "cell_type": "markdown", + "id": "26", + "metadata": {}, + "source": [ + "#### Set Linked Phases" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "27", + "metadata": {}, + "outputs": [], + "source": [ + "expt56.linked_phases.create(id='ncaf', scale=1.0)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "28", + "metadata": {}, + "outputs": [], + "source": [ + "expt47.linked_phases.create(id='ncaf', scale=2.0)" + ] + }, + { + "cell_type": "markdown", + "id": "29", + "metadata": {}, + "source": [ + "#### Set Excluded Regions" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "30", + "metadata": {}, + "outputs": [], + "source": [ + "expt56.excluded_regions.create(id='1', start=0, end=10010)\n", + "expt56.excluded_regions.create(id='2', start=100010, end=200000)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "31", + "metadata": {}, + "outputs": [], + "source": [ + "expt47.excluded_regions.create(id='1', start=0, end=10006)\n", + "expt47.excluded_regions.create(id='2', start=100004, end=200000)" + ] + }, + { + "cell_type": "markdown", + "id": "32", + "metadata": {}, + "source": [ + "## Define Project\n", + "\n", + "The project object is used to manage the structure, experiments,\n", + "and analysis\n", + "\n", + "#### Create Project" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "33", + "metadata": {}, + "outputs": [], + "source": [ + "project = Project()" + ] + }, + { + "cell_type": "markdown", + "id": "34", + "metadata": {}, + "source": [ + "#### Set Plotting Engine" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "35", + "metadata": {}, + "outputs": [], + "source": [ + "# Keep the auto-selected engine. Alternatively, you can uncomment the\n", + "# line below to explicitly set the engine to the required one.\n", + "# project.plotter.engine = 'plotly'" + ] + }, + { + "cell_type": "markdown", + "id": "36", + "metadata": {}, + "source": [ + "#### Add Structure" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "37", + "metadata": {}, + "outputs": [], + "source": [ + "project.structures.add(structure)" + ] + }, + { + "cell_type": "markdown", + "id": "38", + "metadata": {}, + "source": [ + "#### Add Experiment" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "39", + "metadata": {}, + "outputs": [], + "source": [ + "project.experiments.add(expt56)\n", + "project.experiments.add(expt47)" + ] + }, + { + "cell_type": "markdown", + "id": "40", + "metadata": {}, + "source": [ + "## Perform Analysis\n", + "\n", + "This section shows the analysis process, including how to set up\n", + "calculation and fitting engines.\n", + "\n", + "#### Set Minimizer" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "41", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.current_minimizer = 'lmfit'" + ] + }, + { + "cell_type": "markdown", + "id": "42", + "metadata": {}, + "source": [ + "#### Set Fit Mode" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "43", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.fit_mode.mode = 'joint'" + ] + }, + { + "cell_type": "markdown", + "id": "44", + "metadata": {}, + "source": [ + "#### Set Free Parameters" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "45", + "metadata": {}, + "outputs": [], + "source": [ + "structure.atom_sites['Ca'].b_iso.free = True\n", + "structure.atom_sites['Al'].b_iso.free = True\n", + "structure.atom_sites['Na'].b_iso.free = True\n", + "structure.atom_sites['F1'].b_iso.free = True\n", + "structure.atom_sites['F2'].b_iso.free = True\n", + "structure.atom_sites['F3'].b_iso.free = True" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "46", + "metadata": {}, + "outputs": [], + "source": [ + "expt56.linked_phases['ncaf'].scale.free = True\n", + "expt56.instrument.calib_d_to_tof_offset.free = True\n", + "expt56.instrument.calib_d_to_tof_linear.free = True\n", + "expt56.peak.broad_gauss_sigma_2.free = True\n", + "expt56.peak.broad_mix_beta_0.free = True\n", + "expt56.peak.broad_mix_beta_1.free = True\n", + "expt56.peak.asym_alpha_1.free = True\n", + "\n", + "expt47.linked_phases['ncaf'].scale.free = True\n", + "expt47.instrument.calib_d_to_tof_linear.free = True\n", + "expt47.instrument.calib_d_to_tof_offset.free = True\n", + "expt47.peak.broad_gauss_sigma_2.free = True\n", + "expt47.peak.broad_mix_beta_0.free = True\n", + "expt47.peak.broad_mix_beta_1.free = True\n", + "expt47.peak.asym_alpha_1.free = True" + ] + }, + { + "cell_type": "markdown", + "id": "47", + "metadata": {}, + "source": [ + "#### Plot Measured vs Calculated" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "48", + "metadata": {}, + "outputs": [], + "source": [ + "project.plot_meas_vs_calc(expt_name='wish_5_6', show_residual=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "49", + "metadata": {}, + "outputs": [], + "source": [ + "project.plot_meas_vs_calc(expt_name='wish_4_7', show_residual=True)" + ] + }, + { + "cell_type": "markdown", + "id": "50", + "metadata": {}, + "source": [ + "#### Run Fitting" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "51", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.fit()\n", + "project.analysis.show_fit_results()" + ] + }, + { + "cell_type": "markdown", + "id": "52", + "metadata": {}, + "source": [ + "#### Plot Measured vs Calculated" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "53", + "metadata": {}, + "outputs": [], + "source": [ + "project.plot_meas_vs_calc(expt_name='wish_5_6', show_residual=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "54", + "metadata": {}, + "outputs": [], + "source": [ + "project.plot_meas_vs_calc(expt_name='wish_4_7', show_residual=True)" + ] + }, + { + "cell_type": "markdown", + "id": "55", + "metadata": {}, + "source": [ + "## Summary\n", + "\n", + "This final section shows how to review the results of the analysis." + ] + }, + { + "cell_type": "markdown", + "id": "56", + "metadata": {}, + "source": [ + "#### Show Project Summary" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "57", + "metadata": {}, + "outputs": [], + "source": [ + "project.summary.show_report()" + ] + } + ], + "metadata": { + "jupytext": { + "cell_metadata_filter": "-all", + "main_language": "python", + "notebook_metadata_filter": "-all" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/tutorials/ed-8.py b/docs/docs/tutorials/ed-8.py similarity index 79% rename from tutorials/ed-8.py rename to docs/docs/tutorials/ed-8.py index 2aa85227..b8cdf0bd 100644 --- a/tutorials/ed-8.py +++ b/docs/docs/tutorials/ed-8.py @@ -14,38 +14,38 @@ # %% from easydiffraction import ExperimentFactory from easydiffraction import Project -from easydiffraction import SampleModelFactory +from easydiffraction import StructureFactory from easydiffraction import download_data # %% [markdown] -# ## Define Sample Model +# ## Define Structure # -# This section covers how to add sample models and modify their +# This section covers how to add structures and modify their # parameters. # -# #### Create Sample Model +# #### Create Structure # %% -model = SampleModelFactory.create(name='ncaf') +structure = StructureFactory.from_scratch(name='ncaf') # %% [markdown] # #### Set Space Group # %% -model.space_group.name_h_m = 'I 21 3' -model.space_group.it_coordinate_system_code = '1' +structure.space_group.name_h_m = 'I 21 3' +structure.space_group.it_coordinate_system_code = '1' # %% [markdown] # #### Set Unit Cell # %% -model.cell.length_a = 10.250256 +structure.cell.length_a = 10.250256 # %% [markdown] # #### Set Atom Sites # %% -model.atom_sites.add( +structure.atom_sites.create( label='Ca', type_symbol='Ca', fract_x=0.4663, @@ -54,7 +54,7 @@ wyckoff_letter='b', b_iso=0.92, ) -model.atom_sites.add( +structure.atom_sites.create( label='Al', type_symbol='Al', fract_x=0.2521, @@ -63,7 +63,7 @@ wyckoff_letter='a', b_iso=0.73, ) -model.atom_sites.add( +structure.atom_sites.create( label='Na', type_symbol='Na', fract_x=0.0851, @@ -72,7 +72,7 @@ wyckoff_letter='a', b_iso=2.08, ) -model.atom_sites.add( +structure.atom_sites.create( label='F1', type_symbol='F', fract_x=0.1377, @@ -81,7 +81,7 @@ wyckoff_letter='c', b_iso=0.90, ) -model.atom_sites.add( +structure.atom_sites.create( label='F2', type_symbol='F', fract_x=0.3625, @@ -90,7 +90,7 @@ wyckoff_letter='c', b_iso=1.37, ) -model.atom_sites.add( +structure.atom_sites.create( label='F3', type_symbol='F', fract_x=0.4612, @@ -104,7 +104,7 @@ # ## Define Experiment # # This section shows how to add experiments, configure their parameters, -# and link the sample models defined in the previous step. +# and link the structures defined in the previous step. # # #### Download Measured Data @@ -118,14 +118,14 @@ # #### Create Experiment # %% -expt56 = ExperimentFactory.create( +expt56 = ExperimentFactory.from_data_path( name='wish_5_6', data_path=data_path56, beam_mode='time-of-flight', ) # %% -expt47 = ExperimentFactory.create( +expt47 = ExperimentFactory.from_data_path( name='wish_4_7', data_path=data_path47, beam_mode='time-of-flight', @@ -205,7 +205,7 @@ ], start=1, ): - expt56.background.add(id=str(idx), x=x, y=y) + expt56.background.create(id=str(idx), x=x, y=y) # %% expt47.background_type = 'line-segment' @@ -241,32 +241,32 @@ ], start=1, ): - expt47.background.add(id=str(idx), x=x, y=y) + expt47.background.create(id=str(idx), x=x, y=y) # %% [markdown] # #### Set Linked Phases # %% -expt56.linked_phases.add(id='ncaf', scale=1.0) +expt56.linked_phases.create(id='ncaf', scale=1.0) # %% -expt47.linked_phases.add(id='ncaf', scale=2.0) +expt47.linked_phases.create(id='ncaf', scale=2.0) # %% [markdown] # #### Set Excluded Regions # %% -expt56.excluded_regions.add(id='1', start=0, end=10010) -expt56.excluded_regions.add(id='2', start=100010, end=200000) +expt56.excluded_regions.create(id='1', start=0, end=10010) +expt56.excluded_regions.create(id='2', start=100010, end=200000) # %% -expt47.excluded_regions.add(id='1', start=0, end=10006) -expt47.excluded_regions.add(id='2', start=100004, end=200000) +expt47.excluded_regions.create(id='1', start=0, end=10006) +expt47.excluded_regions.create(id='2', start=100004, end=200000) # %% [markdown] # ## Define Project # -# The project object is used to manage the sample model, experiments, +# The project object is used to manage the structure, experiments, # and analysis # # #### Create Project @@ -283,17 +283,17 @@ # project.plotter.engine = 'plotly' # %% [markdown] -# #### Add Sample Model +# #### Add Structure # %% -project.sample_models.add(sample_model=model) +project.structures.add(structure) # %% [markdown] # #### Add Experiment # %% -project.experiments.add(experiment=expt56) -project.experiments.add(experiment=expt47) +project.experiments.add(expt56) +project.experiments.add(expt47) # %% [markdown] # ## Perform Analysis @@ -301,33 +301,27 @@ # This section shows the analysis process, including how to set up # calculation and fitting engines. # -# #### Set Calculator - -# %% -project.analysis.current_calculator = 'cryspy' - -# %% [markdown] # #### Set Minimizer # %% -project.analysis.current_minimizer = 'lmfit (leastsq)' +project.analysis.current_minimizer = 'lmfit' # %% [markdown] # #### Set Fit Mode # %% -project.analysis.fit_mode = 'joint' +project.analysis.fit_mode.mode = 'joint' # %% [markdown] # #### Set Free Parameters # %% -model.atom_sites['Ca'].b_iso.free = True -model.atom_sites['Al'].b_iso.free = True -model.atom_sites['Na'].b_iso.free = True -model.atom_sites['F1'].b_iso.free = True -model.atom_sites['F2'].b_iso.free = True -model.atom_sites['F3'].b_iso.free = True +structure.atom_sites['Ca'].b_iso.free = True +structure.atom_sites['Al'].b_iso.free = True +structure.atom_sites['Na'].b_iso.free = True +structure.atom_sites['F1'].b_iso.free = True +structure.atom_sites['F2'].b_iso.free = True +structure.atom_sites['F3'].b_iso.free = True # %% expt56.linked_phases['ncaf'].scale.free = True diff --git a/docs/docs/tutorials/ed-9.ipynb b/docs/docs/tutorials/ed-9.ipynb new file mode 100644 index 00000000..735964ae --- /dev/null +++ b/docs/docs/tutorials/ed-9.ipynb @@ -0,0 +1,704 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0", + "metadata": {}, + "source": [ + "# Structure Refinement: LBCO+Si, McStas\n", + "\n", + "This example demonstrates a Rietveld refinement of La0.5Ba0.5CoO3\n", + "crystal structure with a small amount of Si phase using time-of-flight\n", + "neutron powder diffraction data simulated with McStas." + ] + }, + { + "cell_type": "markdown", + "id": "1", + "metadata": {}, + "source": [ + "## Import Library" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2", + "metadata": {}, + "outputs": [], + "source": [ + "from easydiffraction import ExperimentFactory\n", + "from easydiffraction import Project\n", + "from easydiffraction import StructureFactory\n", + "from easydiffraction import download_data" + ] + }, + { + "cell_type": "markdown", + "id": "3", + "metadata": {}, + "source": [ + "## Define Structures\n", + "\n", + "This section shows how to add structures and modify their\n", + "parameters.\n", + "\n", + "### Create Structure 1: LBCO" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4", + "metadata": {}, + "outputs": [], + "source": [ + "structure_1 = StructureFactory.from_scratch(name='lbco')" + ] + }, + { + "cell_type": "markdown", + "id": "5", + "metadata": {}, + "source": [ + "#### Set Space Group" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6", + "metadata": {}, + "outputs": [], + "source": [ + "structure_1.space_group.name_h_m = 'P m -3 m'\n", + "structure_1.space_group.it_coordinate_system_code = '1'" + ] + }, + { + "cell_type": "markdown", + "id": "7", + "metadata": {}, + "source": [ + "#### Set Unit Cell" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8", + "metadata": {}, + "outputs": [], + "source": [ + "structure_1.cell.length_a = 3.8909" + ] + }, + { + "cell_type": "markdown", + "id": "9", + "metadata": {}, + "source": [ + "#### Set Atom Sites" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "10", + "metadata": {}, + "outputs": [], + "source": [ + "structure_1.atom_sites.create(\n", + " label='La',\n", + " type_symbol='La',\n", + " fract_x=0,\n", + " fract_y=0,\n", + " fract_z=0,\n", + " wyckoff_letter='a',\n", + " b_iso=0.2,\n", + " occupancy=0.5,\n", + ")\n", + "structure_1.atom_sites.create(\n", + " label='Ba',\n", + " type_symbol='Ba',\n", + " fract_x=0,\n", + " fract_y=0,\n", + " fract_z=0,\n", + " wyckoff_letter='a',\n", + " b_iso=0.2,\n", + " occupancy=0.5,\n", + ")\n", + "structure_1.atom_sites.create(\n", + " label='Co',\n", + " type_symbol='Co',\n", + " fract_x=0.5,\n", + " fract_y=0.5,\n", + " fract_z=0.5,\n", + " wyckoff_letter='b',\n", + " b_iso=0.2567,\n", + ")\n", + "structure_1.atom_sites.create(\n", + " label='O',\n", + " type_symbol='O',\n", + " fract_x=0,\n", + " fract_y=0.5,\n", + " fract_z=0.5,\n", + " wyckoff_letter='c',\n", + " b_iso=1.4041,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "11", + "metadata": {}, + "source": [ + "### Create Structure 2: Si" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "12", + "metadata": {}, + "outputs": [], + "source": [ + "structure_2 = StructureFactory.from_scratch(name='si')" + ] + }, + { + "cell_type": "markdown", + "id": "13", + "metadata": {}, + "source": [ + "#### Set Space Group" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "14", + "metadata": {}, + "outputs": [], + "source": [ + "structure_2.space_group.name_h_m = 'F d -3 m'\n", + "structure_2.space_group.it_coordinate_system_code = '2'" + ] + }, + { + "cell_type": "markdown", + "id": "15", + "metadata": {}, + "source": [ + "#### Set Unit Cell" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "16", + "metadata": {}, + "outputs": [], + "source": [ + "structure_2.cell.length_a = 5.43146" + ] + }, + { + "cell_type": "markdown", + "id": "17", + "metadata": {}, + "source": [ + "#### Set Atom Sites" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "18", + "metadata": {}, + "outputs": [], + "source": [ + "structure_2.atom_sites.create(\n", + " label='Si',\n", + " type_symbol='Si',\n", + " fract_x=0.0,\n", + " fract_y=0.0,\n", + " fract_z=0.0,\n", + " wyckoff_letter='a',\n", + " b_iso=0.0,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "19", + "metadata": {}, + "source": [ + "## Define Experiment\n", + "\n", + "This section shows how to add experiments, configure their parameters,\n", + "and link the structures defined in the previous step.\n", + "\n", + "#### Download Data" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "20", + "metadata": {}, + "outputs": [], + "source": [ + "data_path = download_data(id=8, destination='data')" + ] + }, + { + "cell_type": "markdown", + "id": "21", + "metadata": {}, + "source": [ + "#### Create Experiment" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "22", + "metadata": {}, + "outputs": [], + "source": [ + "experiment = ExperimentFactory.from_data_path(\n", + " name='mcstas',\n", + " data_path=data_path,\n", + " sample_form='powder',\n", + " beam_mode='time-of-flight',\n", + " radiation_probe='neutron',\n", + " scattering_type='bragg',\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "23", + "metadata": {}, + "source": [ + "#### Set Instrument" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "24", + "metadata": {}, + "outputs": [], + "source": [ + "experiment.instrument.setup_twotheta_bank = 94.90931761529106\n", + "experiment.instrument.calib_d_to_tof_offset = 0.0\n", + "experiment.instrument.calib_d_to_tof_linear = 58724.76869981215\n", + "experiment.instrument.calib_d_to_tof_quad = -0.00001" + ] + }, + { + "cell_type": "markdown", + "id": "25", + "metadata": {}, + "source": [ + "#### Set Peak Profile" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "26", + "metadata": {}, + "outputs": [], + "source": [ + "# experiment.peak_profile_type = 'pseudo-voigt * ikeda-carpenter'\n", + "experiment.peak.broad_gauss_sigma_0 = 45137\n", + "experiment.peak.broad_gauss_sigma_1 = -52394\n", + "experiment.peak.broad_gauss_sigma_2 = 22998\n", + "experiment.peak.broad_mix_beta_0 = 0.0055\n", + "experiment.peak.broad_mix_beta_1 = 0.0041\n", + "experiment.peak.asym_alpha_0 = 0\n", + "experiment.peak.asym_alpha_1 = 0.0097" + ] + }, + { + "cell_type": "markdown", + "id": "27", + "metadata": {}, + "source": [ + "#### Set Background" + ] + }, + { + "cell_type": "markdown", + "id": "28", + "metadata": {}, + "source": [ + "Select the background type." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "29", + "metadata": {}, + "outputs": [], + "source": [ + "experiment.background_type = 'line-segment'" + ] + }, + { + "cell_type": "markdown", + "id": "30", + "metadata": {}, + "source": [ + "Add background points." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "31", + "metadata": {}, + "outputs": [], + "source": [ + "experiment.background.create(id='1', x=45000, y=0.2)\n", + "experiment.background.create(id='2', x=50000, y=0.2)\n", + "experiment.background.create(id='3', x=55000, y=0.2)\n", + "experiment.background.create(id='4', x=65000, y=0.2)\n", + "experiment.background.create(id='5', x=70000, y=0.2)\n", + "experiment.background.create(id='6', x=75000, y=0.2)\n", + "experiment.background.create(id='7', x=80000, y=0.2)\n", + "experiment.background.create(id='8', x=85000, y=0.2)\n", + "experiment.background.create(id='9', x=90000, y=0.2)\n", + "experiment.background.create(id='10', x=95000, y=0.2)\n", + "experiment.background.create(id='11', x=100000, y=0.2)\n", + "experiment.background.create(id='12', x=105000, y=0.2)\n", + "experiment.background.create(id='13', x=110000, y=0.2)" + ] + }, + { + "cell_type": "markdown", + "id": "32", + "metadata": {}, + "source": [ + "#### Set Linked Phases" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "33", + "metadata": {}, + "outputs": [], + "source": [ + "experiment.linked_phases.create(id='lbco', scale=4.0)\n", + "experiment.linked_phases.create(id='si', scale=0.2)" + ] + }, + { + "cell_type": "markdown", + "id": "34", + "metadata": {}, + "source": [ + "## Define Project\n", + "\n", + "The project object is used to manage structures, experiments, and\n", + "analysis.\n", + "\n", + "#### Create Project" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "35", + "metadata": {}, + "outputs": [], + "source": [ + "project = Project()" + ] + }, + { + "cell_type": "markdown", + "id": "36", + "metadata": {}, + "source": [ + "#### Add Structures" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "37", + "metadata": {}, + "outputs": [], + "source": [ + "project.structures.add(structure_1)\n", + "project.structures.add(structure_2)" + ] + }, + { + "cell_type": "markdown", + "id": "38", + "metadata": {}, + "source": [ + "#### Show Structures" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "39", + "metadata": {}, + "outputs": [], + "source": [ + "project.structures.show_names()" + ] + }, + { + "cell_type": "markdown", + "id": "40", + "metadata": {}, + "source": [ + "#### Add Experiments" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "41", + "metadata": {}, + "outputs": [], + "source": [ + "project.experiments.add(experiment)" + ] + }, + { + "cell_type": "markdown", + "id": "42", + "metadata": {}, + "source": [ + "#### Set Excluded Regions\n", + "\n", + "Show measured data as loaded from the file." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "43", + "metadata": {}, + "outputs": [], + "source": [ + "project.plot_meas(expt_name='mcstas')" + ] + }, + { + "cell_type": "markdown", + "id": "44", + "metadata": {}, + "source": [ + "Add excluded regions." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "45", + "metadata": {}, + "outputs": [], + "source": [ + "experiment.excluded_regions.create(id='1', start=0, end=40000)\n", + "experiment.excluded_regions.create(id='2', start=108000, end=200000)" + ] + }, + { + "cell_type": "markdown", + "id": "46", + "metadata": {}, + "source": [ + "Show excluded regions." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "47", + "metadata": {}, + "outputs": [], + "source": [ + "experiment.excluded_regions.show()" + ] + }, + { + "cell_type": "markdown", + "id": "48", + "metadata": {}, + "source": [ + "Show measured data after adding excluded regions." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "49", + "metadata": {}, + "outputs": [], + "source": [ + "project.plot_meas(expt_name='mcstas')" + ] + }, + { + "cell_type": "markdown", + "id": "50", + "metadata": {}, + "source": [ + "Show experiment as CIF." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "51", + "metadata": {}, + "outputs": [], + "source": [ + "project.experiments['mcstas'].show_as_cif()" + ] + }, + { + "cell_type": "markdown", + "id": "52", + "metadata": {}, + "source": [ + "## Perform Analysis\n", + "\n", + "This section outlines the analysis process, including how to configure\n", + "calculation and fitting engines.\n", + "\n", + "#### Set Minimizer" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "53", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.current_minimizer = 'lmfit'" + ] + }, + { + "cell_type": "markdown", + "id": "54", + "metadata": {}, + "source": [ + "#### Set Fitting Parameters\n", + "\n", + "Set structure parameters to be optimized." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "55", + "metadata": {}, + "outputs": [], + "source": [ + "structure_1.cell.length_a.free = True\n", + "structure_1.atom_sites['Co'].b_iso.free = True\n", + "structure_1.atom_sites['O'].b_iso.free = True\n", + "\n", + "structure_2.cell.length_a.free = True" + ] + }, + { + "cell_type": "markdown", + "id": "56", + "metadata": {}, + "source": [ + "Set experiment parameters to be optimized." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "57", + "metadata": {}, + "outputs": [], + "source": [ + "experiment.linked_phases['lbco'].scale.free = True\n", + "experiment.linked_phases['si'].scale.free = True\n", + "\n", + "experiment.peak.broad_gauss_sigma_0.free = True\n", + "experiment.peak.broad_gauss_sigma_1.free = True\n", + "experiment.peak.broad_gauss_sigma_2.free = True\n", + "\n", + "experiment.peak.asym_alpha_1.free = True\n", + "experiment.peak.broad_mix_beta_0.free = True\n", + "experiment.peak.broad_mix_beta_1.free = True\n", + "\n", + "for point in experiment.background:\n", + " point.y.free = True" + ] + }, + { + "cell_type": "markdown", + "id": "58", + "metadata": {}, + "source": [ + "#### Perform Fit" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "59", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.fit()\n", + "project.analysis.show_fit_results()" + ] + }, + { + "cell_type": "markdown", + "id": "60", + "metadata": {}, + "source": [ + "#### Plot Measured vs Calculated" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "61", + "metadata": {}, + "outputs": [], + "source": [ + "project.plot_meas_vs_calc(expt_name='mcstas')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "62", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "jupytext": { + "cell_metadata_filter": "-all", + "main_language": "python", + "notebook_metadata_filter": "-all" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/tutorials/ed-9.py b/docs/docs/tutorials/ed-9.py similarity index 65% rename from tutorials/ed-9.py rename to docs/docs/tutorials/ed-9.py index a5fe1647..34da9359 100644 --- a/tutorials/ed-9.py +++ b/docs/docs/tutorials/ed-9.py @@ -11,38 +11,38 @@ # %% from easydiffraction import ExperimentFactory from easydiffraction import Project -from easydiffraction import SampleModelFactory +from easydiffraction import StructureFactory from easydiffraction import download_data # %% [markdown] -# ## Define Sample Models +# ## Define Structures # -# This section shows how to add sample models and modify their +# This section shows how to add structures and modify their # parameters. # -# ### Create Sample Model 1: LBCO +# ### Create Structure 1: LBCO # %% -model_1 = SampleModelFactory.create(name='lbco') +structure_1 = StructureFactory.from_scratch(name='lbco') # %% [markdown] # #### Set Space Group # %% -model_1.space_group.name_h_m = 'P m -3 m' -model_1.space_group.it_coordinate_system_code = '1' +structure_1.space_group.name_h_m = 'P m -3 m' +structure_1.space_group.it_coordinate_system_code = '1' # %% [markdown] # #### Set Unit Cell # %% -model_1.cell.length_a = 3.8909 +structure_1.cell.length_a = 3.8909 # %% [markdown] # #### Set Atom Sites # %% -model_1.atom_sites.add( +structure_1.atom_sites.create( label='La', type_symbol='La', fract_x=0, @@ -52,7 +52,7 @@ b_iso=0.2, occupancy=0.5, ) -model_1.atom_sites.add( +structure_1.atom_sites.create( label='Ba', type_symbol='Ba', fract_x=0, @@ -62,7 +62,7 @@ b_iso=0.2, occupancy=0.5, ) -model_1.atom_sites.add( +structure_1.atom_sites.create( label='Co', type_symbol='Co', fract_x=0.5, @@ -71,7 +71,7 @@ wyckoff_letter='b', b_iso=0.2567, ) -model_1.atom_sites.add( +structure_1.atom_sites.create( label='O', type_symbol='O', fract_x=0, @@ -82,29 +82,29 @@ ) # %% [markdown] -# ### Create Sample Model 2: Si +# ### Create Structure 2: Si # %% -model_2 = SampleModelFactory.create(name='si') +structure_2 = StructureFactory.from_scratch(name='si') # %% [markdown] # #### Set Space Group # %% -model_2.space_group.name_h_m = 'F d -3 m' -model_2.space_group.it_coordinate_system_code = '2' +structure_2.space_group.name_h_m = 'F d -3 m' +structure_2.space_group.it_coordinate_system_code = '2' # %% [markdown] # #### Set Unit Cell # %% -model_2.cell.length_a = 5.43146 +structure_2.cell.length_a = 5.43146 # %% [markdown] # #### Set Atom Sites # %% -model_2.atom_sites.add( +structure_2.atom_sites.create( label='Si', type_symbol='Si', fract_x=0.0, @@ -118,7 +118,7 @@ # ## Define Experiment # # This section shows how to add experiments, configure their parameters, -# and link the sample models defined in the previous step. +# and link the structures defined in the previous step. # # #### Download Data @@ -129,7 +129,7 @@ # #### Create Experiment # %% -experiment = ExperimentFactory.create( +experiment = ExperimentFactory.from_data_path( name='mcstas', data_path=data_path, sample_form='powder', @@ -173,31 +173,31 @@ # Add background points. # %% -experiment.background.add(id='1', x=45000, y=0.2) -experiment.background.add(id='2', x=50000, y=0.2) -experiment.background.add(id='3', x=55000, y=0.2) -experiment.background.add(id='4', x=65000, y=0.2) -experiment.background.add(id='5', x=70000, y=0.2) -experiment.background.add(id='6', x=75000, y=0.2) -experiment.background.add(id='7', x=80000, y=0.2) -experiment.background.add(id='8', x=85000, y=0.2) -experiment.background.add(id='9', x=90000, y=0.2) -experiment.background.add(id='10', x=95000, y=0.2) -experiment.background.add(id='11', x=100000, y=0.2) -experiment.background.add(id='12', x=105000, y=0.2) -experiment.background.add(id='13', x=110000, y=0.2) +experiment.background.create(id='1', x=45000, y=0.2) +experiment.background.create(id='2', x=50000, y=0.2) +experiment.background.create(id='3', x=55000, y=0.2) +experiment.background.create(id='4', x=65000, y=0.2) +experiment.background.create(id='5', x=70000, y=0.2) +experiment.background.create(id='6', x=75000, y=0.2) +experiment.background.create(id='7', x=80000, y=0.2) +experiment.background.create(id='8', x=85000, y=0.2) +experiment.background.create(id='9', x=90000, y=0.2) +experiment.background.create(id='10', x=95000, y=0.2) +experiment.background.create(id='11', x=100000, y=0.2) +experiment.background.create(id='12', x=105000, y=0.2) +experiment.background.create(id='13', x=110000, y=0.2) # %% [markdown] # #### Set Linked Phases # %% -experiment.linked_phases.add(id='lbco', scale=4.0) -experiment.linked_phases.add(id='si', scale=0.2) +experiment.linked_phases.create(id='lbco', scale=4.0) +experiment.linked_phases.create(id='si', scale=0.2) # %% [markdown] # ## Define Project # -# The project object is used to manage sample models, experiments, and +# The project object is used to manage structures, experiments, and # analysis. # # #### Create Project @@ -206,23 +206,23 @@ project = Project() # %% [markdown] -# #### Add Sample Models +# #### Add Structures # %% -project.sample_models.add(sample_model=model_1) -project.sample_models.add(sample_model=model_2) +project.structures.add(structure_1) +project.structures.add(structure_2) # %% [markdown] -# #### Show Sample Models +# #### Show Structures # %% -project.sample_models.show_names() +project.structures.show_names() # %% [markdown] # #### Add Experiments # %% -project.experiments.add(experiment=experiment) +project.experiments.add(experiment) # %% [markdown] # #### Set Excluded Regions @@ -236,8 +236,8 @@ # Add excluded regions. # %% -experiment.excluded_regions.add(id='1', start=0, end=40000) -experiment.excluded_regions.add(id='2', start=108000, end=200000) +experiment.excluded_regions.create(id='1', start=0, end=40000) +experiment.excluded_regions.create(id='2', start=108000, end=200000) # %% [markdown] # Show excluded regions. @@ -263,28 +263,22 @@ # This section outlines the analysis process, including how to configure # calculation and fitting engines. # -# #### Set Calculator - -# %% -project.analysis.current_calculator = 'cryspy' - -# %% [markdown] # #### Set Minimizer # %% -project.analysis.current_minimizer = 'lmfit (leastsq)' +project.analysis.current_minimizer = 'lmfit' # %% [markdown] # #### Set Fitting Parameters # -# Set sample model parameters to be optimized. +# Set structure parameters to be optimized. # %% -model_1.cell.length_a.free = True -model_1.atom_sites['Co'].b_iso.free = True -model_1.atom_sites['O'].b_iso.free = True +structure_1.cell.length_a.free = True +structure_1.atom_sites['Co'].b_iso.free = True +structure_1.atom_sites['O'].b_iso.free = True -model_2.cell.length_a.free = True +structure_2.cell.length_a.free = True # %% [markdown] # Set experiment parameters to be optimized. diff --git a/tutorials/index.json b/docs/docs/tutorials/index.json similarity index 73% rename from tutorials/index.json rename to docs/docs/tutorials/index.json index 983499fd..3f2f223c 100644 --- a/tutorials/index.json +++ b/docs/docs/tutorials/index.json @@ -3,14 +3,14 @@ "url": "https://easyscience.github.io/diffraction-lib/{version}/tutorials/ed-1/ed-1.ipynb", "original_name": "quick_from-cif_pd-neut-cwl_LBCO-HRPT", "title": "Quick Start: LBCO from CIF", - "description": "Minimalistic Rietveld refinement of La0.5Ba0.5CoO3 using sample model and experiment defined via CIF files", + "description": "Minimalistic Rietveld refinement of La0.5Ba0.5CoO3 using structure and experiment defined via CIF files", "level": "quick" }, "2": { "url": "https://easyscience.github.io/diffraction-lib/{version}/tutorials/ed-2/ed-2.ipynb", "original_name": "quick_from-code_pd-neut-cwl_LBCO-HRPT", "title": "Quick Start: LBCO from Code", - "description": "Minimalistic Rietveld refinement of La0.5Ba0.5CoO3 with sample model and experiment defined directly in code", + "description": "Minimalistic Rietveld refinement of La0.5Ba0.5CoO3 with structure and experiment defined directly in code", "level": "quick" }, "3": { @@ -86,8 +86,36 @@ "13": { "url": "https://easyscience.github.io/diffraction-lib/{version}/tutorials/ed-13/ed-13.ipynb", "original_name": "dmsc-summer-school-2025_analysis-powder-diffraction", - "title": "DMSC Summer School 2025: Powder Diffraction Analysis", + "title": "DMSC Summer School: Powder Diffraction Analysis", "description": "Comprehensive workshop tutorial covering Rietveld refinement of Si and La0.5Ba0.5CoO3 using simulated powder diffraction data", "level": "workshop" + }, + "14": { + "url": "https://easyscience.github.io/diffraction-lib/{version}/tutorials/ed-14/ed-14.ipynb", + "original_name": "", + "title": "Crystal Structure: Tb2TiO7, HEiDi", + "description": "Crystal structure refinement of Tb2TiO7 using single crystal neutron diffraction data from HEiDi at FRM II", + "level": "intermediate" + }, + "15": { + "url": "https://easyscience.github.io/diffraction-lib/{version}/tutorials/ed-15/ed-15.ipynb", + "original_name": "", + "title": "Crystal Structure: Taurine, SENJU (TOF)", + "description": "Crystal structure refinement of Taurine using time-of-flight neutron single crystal diffraction data from SENJU at J-PARC", + "level": "intermediate" + }, + "16": { + "url": "https://easyscience.github.io/diffraction-lib/{version}/tutorials/ed-16/ed-16.ipynb", + "original_name": "advanced_joint-fit_bragg-pdf_pd-neut-tof_Si", + "title": "Advanced: Si Joint Bragg+PDF Fit", + "description": "Joint refinement of Si crystal structure combining Bragg diffraction (SEPD) and pair distribution function (NOMAD) analysis", + "level": "advanced" + }, + "17": { + "url": "https://easyscience.github.io/diffraction-lib/{version}/tutorials/ed-17/ed-17.ipynb", + "original_name": "", + "title": "Structure Refinement: Co2SiO4, D20 (Temperature scan)", + "description": "Sequential Rietveld refinement of Co2SiO4 using constant wavelength neutron powder diffraction data from D20 at ILL across a temperature scan", + "level": "advanced" } } diff --git a/docs/docs/tutorials/index.md b/docs/docs/tutorials/index.md new file mode 100644 index 00000000..ef22e949 --- /dev/null +++ b/docs/docs/tutorials/index.md @@ -0,0 +1,98 @@ +--- +icon: material/school +--- + +# :material-school: Tutorials + +This section presents a collection of **Jupyter Notebook** tutorials +that demonstrate how to use EasyDiffraction for various tasks. These +tutorials serve as self-contained, step-by-step **guides** to help users +grasp the workflow of diffraction data analysis using EasyDiffraction. + +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. + +The tutorials are organized into the following categories. + +## Getting Started + +- [LBCO `quick` CIF](ed-1.ipynb) – A minimal example intended as a quick + reference for users already familiar with the EasyDiffraction API or + who want to see how Rietveld refinement of the La0.5Ba0.5CoO3 crystal + structure can be performed when both the structure and experiment are + loaded from CIF files. Data collected from constant wavelength neutron + powder diffraction at HRPT at PSI. +- [LBCO `quick` `code`](ed-2.ipynb) – A minimal example intended as a + quick reference for users already familiar with the EasyDiffraction + API or who want to see an example refinement when both the structure + and experiment are defined directly in code. This tutorial covers a + Rietveld refinement of the La0.5Ba0.5CoO3 crystal structure using + constant wavelength neutron powder diffraction data from HRPT at PSI. +- [LBCO `complete`](ed-3.ipynb) – Demonstrates the use of the + EasyDiffraction API in a simplified, user-friendly manner that closely + follows the GUI workflow for a Rietveld refinement of the + La0.5Ba0.5CoO3 crystal structure using constant wavelength neutron + powder diffraction data from HRPT at PSI. This tutorial provides a + full explanation of the workflow with detailed comments and + descriptions of every step, making it suitable for users who are new + to EasyDiffraction or those who prefer a more guided approach. + +## Powder Diffraction + +- [Co2SiO4 `pd-neut-cwl`](ed-5.ipynb) – Demonstrates a Rietveld + refinement of the Co2SiO4 crystal structure using constant wavelength + neutron powder diffraction data from D20 at ILL. +- [HS `pd-neut-cwl`](ed-6.ipynb) – Demonstrates a Rietveld refinement of + the HS crystal structure using constant wavelength neutron powder + diffraction data from HRPT at PSI. +- [Si `pd-neut-tof`](ed-7.ipynb) – Demonstrates a Rietveld refinement of + the Si crystal structure using time-of-flight neutron powder + diffraction data from SEPD at Argonne. +- [NCAF `pd-neut-tof`](ed-8.ipynb) – Demonstrates a Rietveld refinement + of the Na2Ca3Al2F14 crystal structure using two time-of-flight neutron + powder diffraction datasets (from two detector banks) of the WISH + instrument at ISIS. + +## Single Crystal Diffraction + +- [Tb2TiO7 `sg-neut-cwl`](ed-14.ipynb) – Demonstrates structure + refinement of Tb2TiO7 using constant wavelength neutron single crystal + diffraction data from HEiDi at FRM II. +- [Taurine `sg-neut-tof`](ed-15.ipynb) – Demonstrates structure + refinement of Taurine using time-of-flight neutron single crystal + diffraction data from SENJU at J-PARC. + +## Pair Distribution Function (PDF) + +- [Ni `pd-neut-cwl`](ed-10.ipynb) – Demonstrates a PDF analysis of Ni + using data collected from a constant wavelength neutron powder + diffraction experiment. +- [Si `pd-neut-tof`](ed-11.ipynb) – Demonstrates a PDF analysis of Si + using data collected from a time-of-flight neutron powder diffraction + experiment at NOMAD at SNS. +- [NaCl `pd-xray`](ed-12.ipynb) – Demonstrates a PDF analysis of NaCl + using data collected from an X-ray powder diffraction experiment. + +## Multi-Structure & Multi-Experiment Refinement + +- [PbSO4 NPD+XRD](ed-4.ipynb) – Joint fit of PbSO4 using X-ray and + neutron constant wavelength powder diffraction data. +- [LBCO+Si McStas](ed-9.ipynb) – Multi-phase Rietveld refinement of + La0.5Ba0.5CoO3 with Si impurity using time-of-flight neutron data + simulated with McStas. +- [Si Bragg+PDF](ed-16.ipynb) – Joint refinement of Si combining Bragg + diffraction (SEPD) and pair distribution function (NOMAD) analysis. A + single shared structure is refined simultaneously against both + datasets. +- [Co2SiO4 Temperature scan](ed-17.ipynb) – Sequential Rietveld + refinement of Co2SiO4 using constant wavelength neutron powder + diffraction data from D20 at ILL across a temperature scan. + +## Workshops & Schools + +- [DMSC Summer School](ed-13.ipynb) – A workshop tutorial that + demonstrates a Rietveld refinement of the La0.5Ba0.5CoO3 crystal + structure using time-of-flight neutron powder diffraction data + simulated with McStas. This tutorial is designed for the ESS DMSC + Summer School. diff --git a/docs/user-guide/analysis-workflow/analysis.md b/docs/docs/user-guide/analysis-workflow/analysis.md similarity index 50% rename from docs/user-guide/analysis-workflow/analysis.md rename to docs/docs/user-guide/analysis-workflow/analysis.md index cdb948d2..76aaa699 100644 --- a/docs/user-guide/analysis-workflow/analysis.md +++ b/docs/docs/user-guide/analysis-workflow/analysis.md @@ -5,80 +5,86 @@ icon: material/calculator # :material-calculator: Analysis This section provides an overview of **diffraction data analysis** in -EasyDiffraction, focusing on model-dependent analysis, calculation engines, and -minimization techniques. +EasyDiffraction, focusing on model-dependent analysis, calculation +engines, and minimization techniques. -In EasyDiffraction, we focus on **model-dependent analysis**, where a model is -constructed based on prior knowledge of the studied system, and its parameters -are optimized to achieve the best agreement between experimental and calculated -diffraction data. Model-dependent analysis is widely used in neutron and X-ray -scattering data. +In EasyDiffraction, we focus on **model-dependent analysis**, where a +model is constructed based on prior knowledge of the studied system, and +its parameters are optimized to achieve the best agreement between +experimental and calculated diffraction data. Model-dependent analysis +is widely used in neutron and X-ray scattering data. ## Calculation -EasyDiffraction relies on third-party crystallographic libraries, referred to as -**calculation engines** or just **calculators**, to perform the calculations. +EasyDiffraction relies on third-party crystallographic libraries, +referred to as **calculation engines** or just **calculators**, to +perform the calculations. -The calculation engines are used to calculate the diffraction pattern for the -defined model of the studied sample using the instrumental and other required -experiment-related parameters, such as the wavelength, resolution, etc. +The calculation engines are used to calculate the diffraction pattern +for the defined model of the studied structure using the instrumental +and other required experiment-related parameters, such as the +wavelength, resolution, etc. -You do not necessarily need the measured data to perform the calculations, but -you need a structural model and some details about the type of experiment you -want to simulate. +You do not necessarily need the measured data to perform the +calculations, but you need a structural model and some details about the +type of experiment you want to simulate. -EasyDiffraction is designed as a flexible and extensible tool that supports -different **calculation engines** for diffraction pattern calculations. -Currently, we integrate CrysPy, CrysFML, and PDFfit2 libraries as calculation -engines. +EasyDiffraction is designed as a flexible and extensible tool that +supports different **calculation engines** for diffraction pattern +calculations. Currently, we integrate CrysPy, CrysFML, and PDFfit2 +libraries as calculation engines. ### CrysPy Calculator -[CrysPy](https://www.cryspy.fr) is a Python library originally developed for -analysing polarised neutron diffraction data. It is now evolving into a more -general purpose library and covers powders and single crystals, nuclear and -(commensurate) magnetic structures, unpolarised neutron and X-ray diffraction. +[CrysPy](https://www.cryspy.fr) is a Python library originally developed +for analysing polarised neutron diffraction data. It is now evolving +into a more general purpose library and covers powders and single +crystals, nuclear and (commensurate) magnetic structures, unpolarised +neutron and X-ray diffraction. ### CrysFML Calculator -[CrysFML](https://code.ill.fr/scientific-software/CrysFML2008) library is a -collection of Fortran modules for crystallographic computations. It is used in -the software package [FullProf](https://www.ill.eu/sites/fullprof/), and we are -currently working on its integration into EasyDiffraction. +[CrysFML](https://code.ill.fr/scientific-software/CrysFML2008) library +is a collection of Fortran modules for crystallographic computations. It +is used in the software package +[FullProf](https://www.ill.eu/sites/fullprof/), and we are currently +working on its integration into EasyDiffraction. ### PDFfit2 Calculator -[PDFfit2](https://github.com/diffpy/diffpy.pdffit2/) is a Python library for -calculating the pair distribution function (PDF) from crystallographic models. +[PDFfit2](https://github.com/diffpy/diffpy.pdffit2/) is a Python library +for calculating the pair distribution function (PDF) from +crystallographic models. ### Set Calculator -To show the supported calculation engines: +The calculator is automatically selected based on the experiment type +(e.g., `cryspy` for Bragg diffraction, `pdffit` for total scattering). +To show the supported calculation engines for a specific experiment: ```python -project.analysis.show_supported_calculators() +project.experiments['hrpt'].show_supported_calculator_types() ``` The example of the output is: -Supported calculators +Supported calculator types -| Calculator | Description | -| ---------- | ----------------------------------------------------------- | -| cryspy | CrysPy library for crystallographic calculations | -| pdffit | PDFfit2 library for pair distribution function calculations | +| Calculator | Description | +| ---------- | ------------------------------------------------ | +| cryspy | CrysPy library for crystallographic calculations | -To select the desired calculation engine, e.g., 'cryspy': +To explicitly select a calculation engine for an experiment: ```python -project.analysis.current_calculator = 'cryspy' +project.experiments['hrpt'].calculator_type = 'cryspy' ``` ## Minimization / Optimization -The process of refining model parameters involves iterating through multiple -steps until the calculated data sufficiently matches the experimental data. This -process is illustrated in the following diagram: +The process of refining model parameters involves iterating through +multiple steps until the calculated data sufficiently matches the +experimental data. This process is illustrated in the following diagram: ```mermaid flowchart LR @@ -94,31 +100,34 @@ flowchart LR d-- Threshold
reached -->e ``` -Like the calculation engines, EasyDiffraction is designed to utilize various -third-party libraries for model refinement and parameter optimization. These -libraries provide robust curve fitting and uncertainty estimation tools. +Like the calculation engines, EasyDiffraction is designed to utilize +various third-party libraries for model refinement and parameter +optimization. These libraries provide robust curve fitting and +uncertainty estimation tools. ### Lmfit Minimizer Most of the examples in this section will use the -[lmfit](https://lmfit.github.io/lmfit-py/) package, which provides a high-level -interface to non-linear optimisation and curve fitting problems for Python. It -is one of the tools that can be used to fit models to the experimental data. +[lmfit](https://lmfit.github.io/lmfit-py/) package, which provides a +high-level interface to non-linear optimisation and curve fitting +problems for Python. It is one of the tools that can be used to fit +models to the experimental data. ### Bumps Minimizer Another package that can be used for the same purpose is -[bumps](https://bumps.readthedocs.io/en/latest/). In addition to traditional -optimizers which search for the best minimum they can find in the search space, -bumps provides Bayesian uncertainty analysis which explores all viable minima -and finds confidence intervals on the parameters based on uncertainty in the -measured values. +[bumps](https://bumps.readthedocs.io/en/latest/). In addition to +traditional optimizers which search for the best minimum they can find +in the search space, bumps provides Bayesian uncertainty analysis which +explores all viable minima and finds confidence intervals on the +parameters based on uncertainty in the measured values. ### DFO-LS Minimizer -[DFO-LS](https://github.com/numericalalgorithmsgroup/dfols) (Derivative-Free -Optimizer for Least-Squares) is a Python library for solving nonlinear -least-squares minimization, without requiring derivatives of the objective. +[DFO-LS](https://github.com/numericalalgorithmsgroup/dfols) +(Derivative-Free Optimizer for Least-Squares) is a Python library for +solving nonlinear least-squares minimization, without requiring +derivatives of the objective. ### Set Minimizer @@ -136,65 +145,57 @@ Supported minimizers | --------------------- | ------------------------------------------------------------------------ | | lmfit | LMFIT library using the default Levenberg-Marquardt least squares method | | lmfit (leastsq) | LMFIT library with Levenberg-Marquardt least squares method | -| lmfit (least_squares) | LMFIT library with SciPy’s trust region reflective algorithm | +| lmfit (least_squares) | LMFIT library with SciPy's trust region reflective algorithm | | dfols | DFO-LS library for derivative-free least-squares optimization | -To select the desired calculation engine, e.g., 'lmfit (least_squares)': +To select the desired minimizer, e.g., 'lmfit': ```python -project.analysis.current_minimizer = 'lmfit (leastsq)' +project.analysis.current_minimizer = 'lmfit' ``` ### Fit Mode -In EasyDiffraction, you can set the **fit mode** to control how the refinement -process is performed. The fit mode determines whether the refinement is -performed independently for each experiment or jointly across all experiments. +In EasyDiffraction, you can set the **fit mode** to control how the +refinement process is performed. The fit mode determines whether the +refinement is performed independently for each experiment or jointly +across all experiments. -To show the supported fit modes: +The supported fit modes are: -```python -project.analysis.show_supported_fit_modes() -``` - -An example of supported fit modes is: - -Supported fit modes +| Mode | Description | +| ------ | ------------------------------------------------------------------- | +| single | Independent fitting of each experiment; no shared parameters | +| joint | Simultaneous fitting of all experiments; some parameters are shared | -| Strategy | Description | -| -------- | ------------------------------------------------------------------- | -| single | Independent fitting of each experiment; no shared parameters | -| joint | Simultaneous fitting of all experiments; some parameters are shared | - -You can set the fit mode using the `set_fit_mode` method of the `analysis` -object: +You can set the fit mode on the `analysis` object: ```python -project.analysis.fit_mode = 'joint' +project.analysis.fit_mode.mode = 'joint' ``` -To check the current fit mode, you can use the `show_current_fit_mode` method: +To check the current fit mode: ```python -project.analysis.show_current_fit_mode() +print(project.analysis.fit_mode.mode.value) ``` ### Perform Fit -Refining the sample model and experiment parameters against measured data is -usually divided into several steps, where each step involves adding or removing -parameters to be refined, calculating the model data, and comparing it to the -experimental data as shown in the diagram above. +Refining the structure and experiment parameters against measured data +is usually divided into several steps, where each step involves adding +or removing parameters to be refined, calculating the model data, and +comparing it to the experimental data as shown in the diagram above. -To select the parameters to be refined, you can set the attribute `free` of the -parameters to `True`. This indicates that the parameter is free to be optimized -during the refinement process. +To select the parameters to be refined, you can set the attribute `free` +of the parameters to `True`. This indicates that the parameter is free +to be optimized during the refinement process. Here is an example of how to set parameters to be refined: ```python -# Set sample model parameters to be refined. -project.sample_models['lbco'].cell.length_a.free = True +# Set structure parameters to be refined. +project.structures['lbco'].cell.length_a.free = True # Set experiment parameters to be refined. project.experiments['hrpt'].linked_phases['lbco'].scale.free = True @@ -203,15 +204,16 @@ project.experiments['hrpt'].background['10'].y.free = True project.experiments['hrpt'].background['165'].y.free = True ``` -After setting the parameters to be refined, you can perform the fit using the -`fit` method of the `analysis` object: +After setting the parameters to be refined, you can perform the fit +using the `fit` method of the `analysis` object: ```python project.analysis.fit() ``` -This method will iterate through the defined steps, adjusting the parameters -until the calculated data sufficiently matches the experimental data. +This method will iterate through the defined steps, adjusting the +parameters until the calculated data sufficiently matches the +experimental data. An example of the output after performing the fit is: @@ -241,9 +243,9 @@ Fit results 📈 Fitted parameters: ``` -Now, you can inspect the fitted parameters to see how they have changed during -the refinement process, select more parameters to be refined, and perform -additional fits as needed. +Now, you can inspect the fitted parameters to see how they have changed +during the refinement process, select more parameters to be refined, and +perform additional fits as needed. To plot the measured vs calculated data after the fit, you can use the `plot_meas_vs_calc` method of the `analysis` object: @@ -254,72 +256,67 @@ project.plot_meas_vs_calc(expt_name='hrpt', show_residual=True) ## Constraints -In EasyDiffraction, you can define **constraints** on the model parameters to -ensure that they remain within a specific range or follow a certain relationship -during the refinement process. +In EasyDiffraction, you can define **constraints** on the model +parameters to ensure that they remain within a specific range or follow +a certain relationship during the refinement process. ### Setting Aliases -Before setting constraints, you need to set aliases for the parameters you want -to constrain. This can be done using the `add` method of the `aliases` object. -Aliases are used to reference parameters in a more readable way, making it -easier to manage constraints. +Before setting constraints, you need to set aliases for the parameters +you want to constrain. This can be done using the `add` method of the +`aliases` object. Aliases are used to reference parameters in a more +readable way, making it easier to manage constraints. -An example of setting aliases for parameters in a sample model: +An example of setting aliases for parameters in a structure: ```python # Set aliases for the atomic displacement parameters -project.analysis.aliases.add( +project.analysis.aliases.create( label='biso_La', - param_uid=project.sample_models['lbco'].atom_sites['La'].b_iso.uid + param_uid=project.structures['lbco'].atom_sites['La'].b_iso.uid, ) -project.analysis.aliases.add( +project.analysis.aliases.create( label='biso_Ba', - param_uid=project.sample_models['lbco'].atom_sites['Ba'].b_iso.uid + param_uid=project.structures['lbco'].atom_sites['Ba'].b_iso.uid, ) # Set aliases for the occupancies of the atom sites -project.analysis.aliases.add( +project.analysis.aliases.create( label='occ_La', - param_uid=project.sample_models['lbco'].atom_sites['La'].occupancy.uid + param_uid=project.structures['lbco'].atom_sites['La'].occupancy.uid, ) -project.analysis.aliases.add( +project.analysis.aliases.create( label='occ_Ba', - param_uid=project.sample_models['lbco'].atom_sites['Ba'].occupancy.uid + param_uid=project.structures['lbco'].atom_sites['Ba'].occupancy.uid, ) ``` ### Setting Constraints -Now that you have set the aliases, you can define constraints using the `add` -method of the `constraints` object. Constraints are defined by specifying the -**left-hand side (lhs) alias** and the **right-hand side (rhs) expression**. The -rhs expression can be a simple alias or a more complex expression involving -other aliases. +Now that you have set the aliases, you can define constraints using the +`create` method of the `constraints` object. Each constraint is a single +expression string of the form `lhs = rhs`, where the left-hand side is +an alias and the right-hand side is an expression involving other +aliases. An example of setting constraints for the aliases defined above: ```python -project.analysis.constraints.add( - lhs_alias='biso_Ba', - rhs_expr='biso_La' -) +project.analysis.constraints.create(expression='biso_Ba = biso_La') -project.analysis.constraints.add( - lhs_alias='occ_Ba', - rhs_expr='1 - occ_La' -) +project.analysis.constraints.create(expression='occ_Ba = 1 - occ_La') ``` -These constraints ensure that the `biso_Ba` parameter is equal to `biso_La`, and -the `occ_Ba` parameter is equal to `1 - occ_La`. This means that the occupancy -of the Ba atom will always be adjusted based on the occupancy of the La atom, -and the isotropic displacement parameter for Ba will be equal to that of La -during the refinement process. +These constraints ensure that the `biso_Ba` parameter is equal to +`biso_La`, and the `occ_Ba` parameter is equal to `1 - occ_La`. This +means that the occupancy of the Ba atom will always be adjusted based on +the occupancy of the La atom, and the isotropic displacement parameter +for Ba will be equal to that of La during the refinement process. ### Viewing Constraints -To view the defined constraints, you can use the `show_constraints` method: +To view the defined constraints, you can use the `show_constraints` +method: ```python project.analysis.show_constraints() @@ -329,18 +326,18 @@ The example of the output is: User defined constraints -| lhs_alias | rhs_expr | full expression | -| --------- | ---------- | ------------------- | -| biso_Ba | biso_La | biso_Ba = biso_La | -| occ_Ba | 1 - occ_La | occ_Ba = 1 - occ_La | +| expression | +| ------------------- | +| biso_Ba = biso_La | +| occ_Ba = 1 - occ_La | ## Analysis as CIF To inspect an analysis configuration in CIF format, use: ```python -# Show sample model as CIF -project.sample_models['lbco'].show_as_cif() +# Show structure as CIF +project.structures['lbco'].show_as_cif() ``` Example output: @@ -360,17 +357,17 @@ Example output: │ occ_Ba lbco.atom_site.Ba.occupancy │ │ │ │ loop_ │ -│ _constraint.lhs_alias │ -│ _constraint.rhs_expr │ -│ biso_Ba biso_La │ -│ occ_Ba "1 - occ_La" │ +│ _constraint.expression │ +│ "biso_Ba = biso_La" │ +│ "occ_Ba = 1 - occ_La" │ ╘════════════════════════════════════════════════╛ ``` ## Saving an Analysis -Saving the project, as described in the [Project](project.md) section, will also -save the analysis settings to the `analysis.cif` inside the project directory. +Saving the project, as described in the [Project](project.md) section, +will also save the analysis settings to the `analysis.cif` inside the +project directory.
diff --git a/docs/user-guide/analysis-workflow/experiment.md b/docs/docs/user-guide/analysis-workflow/experiment.md similarity index 76% rename from docs/user-guide/analysis-workflow/experiment.md rename to docs/docs/user-guide/analysis-workflow/experiment.md index 3bbc9fdc..c121d6a9 100644 --- a/docs/user-guide/analysis-workflow/experiment.md +++ b/docs/docs/user-guide/analysis-workflow/experiment.md @@ -4,10 +4,10 @@ icon: material/microscope # :material-microscope: Experiment -An **Experiment** in EasyDiffraction includes the measured diffraction data -along with all relevant parameters that describe the experimental setup and -associated conditions. This can include information about the instrumental -resolution, peak shape, background, etc. +An **Experiment** in EasyDiffraction includes the measured diffraction +data along with all relevant parameters that describe the experimental +setup and associated conditions. This can include information about the +instrumental resolution, peak shape, background, etc. ## Defining an Experiment @@ -15,23 +15,26 @@ EasyDiffraction allows you to: - **Load an existing experiment** from a file (**CIF** format). Both the metadata and measured data are expected to be in CIF format. -- **Manually define** a new experiment by specifying its type, other necessary - experimental parameters, as well as load measured data. This is useful when - you want to create an experiment from scratch or when you have a measured data - file in a non-CIF format (e.g., `.xye`, `.xy`). - -Below, you will find instructions on how to define and manage experiments in -EasyDiffraction. It is assumed that you have already created a `project` object, -as described in the [Project](project.md) section as well as defined its -`sample_models`, as described in the [Sample Model](model.md) section. +- **Manually define** a new experiment by specifying its type, other + necessary experimental parameters, as well as load measured data. This + is useful when you want to create an experiment from scratch or when + you have a measured data file in a non-CIF format (e.g., `.xye`, + `.xy`). + +Below, you will find instructions on how to define and manage +experiments in EasyDiffraction. It is assumed that you have already +created a `project` object, as described in the [Project](project.md) +section as well as defined its `structures`, as described in the +[Structure](model.md) section. ### Adding from CIF -This is the most straightforward way to define an experiment in EasyDiffraction. -If you have a crystallographic information file (CIF) for your experiment, that -contains both the necessary information (metadata) about the experiment as well -as the measured data, you can add it to your `project.experiments` collection -using the `add_from_cif_path` method. In this case, the name of the experiment +This is the most straightforward way to define an experiment in +EasyDiffraction. If you have a crystallographic information file (CIF) +for your experiment, that contains both the necessary information +(metadata) about the experiment as well as the measured data, you can +add it to your `project.experiments` collection using the +`add_from_cif_path` method. In this case, the name of the experiment will be taken from CIF. ```python @@ -51,9 +54,9 @@ project.experiments.add_from_cif_str(cif_string) ``` Accessing the experiment after adding it will also be done through the -`experiments` object of the `project` instance. The name of the experiment will -be the same as the data block id in the CIF file. For example, if the CIF file -contains a data block with the id `hrpt`, +`experiments` object of the `project` instance. The name of the +experiment will be the same as the data block id in the CIF file. For +example, if the CIF file contains a data block with the id `hrpt`, @@ -77,20 +80,22 @@ project.experiments['hrpt'] ### Defining Manually -If you do not have a CIF file or prefer to define the experiment manually, you -can use the `add_from_data_path` method of the `experiments` object of the -`project` instance. In this case, you will need to specify the **name** of the -experiment, which will be used to reference it later, as well as **data_path** -to the measured data file (e.g., `.xye`, `.xy`). Supported formats are described -in the [Measured Data Category](#5-measured-data-category) section. +If you do not have a CIF file or prefer to define the experiment +manually, you can use the `add_from_data_path` method of the +`experiments` object of the `project` instance. In this case, you will +need to specify the **name** of the experiment, which will be used to +reference it later, as well as **data_path** to the measured data file +(e.g., `.xye`, `.xy`). Supported formats are described in the +[Measured Data Category](#measured-data-category) section. -Optionally, you can also specify the additional parameters that define the -**type of experiment** you want to create. If you do not specify any of these -parameters, the default values will be used, which are the first in the list of -supported options for each parameter: +Optionally, you can also specify the additional parameters that define +the **type of experiment** you want to create. If you do not specify any +of these parameters, the default values will be used, which are the +first in the list of supported options for each parameter: - **sample_form**: The form of the sample (powder, single crystal). -- **beam_mode**: The mode of the beam (constant wavelength, time-of-flight). +- **beam_mode**: The mode of the beam (constant wavelength, + time-of-flight). - **radiation_probe**: The type of radiation used (neutron, X-ray). - **scattering_type**: The type of scattering (bragg, total). @@ -100,37 +105,43 @@ supported options for each parameter: these parameters. If you need to change them, you must create a new experiment or redefine the existing one. -Here is an example of how to add an experiment with all relevant components -explicitly defined: +Here is an example of how to add an experiment with all relevant +components explicitly defined: ```python # Add an experiment with default parameters, based on the specified type. -project.experiments.add_from_data_path(name='hrpt', - data_path='data/hrpt_lbco.xye', - sample_form='powder', - beam_mode='constant wavelength', - radiation_probe='neutron', - scattering_type='bragg') +project.experiments.add_from_data_path( + name='hrpt', + data_path='data/hrpt_lbco.xye', + sample_form='powder', + beam_mode='constant wavelength', + radiation_probe='neutron', + scattering_type='bragg', +) ``` To add an experiment of default type, you can simply do: ```python # Add an experiment of default type -project.experiments.add_from_data_path(name='hrpt', - data_path='data/hrpt_lbco.xye') +project.experiments.add_from_data_path( + name='hrpt', + data_path='data/hrpt_lbco.xye', +) ``` -If you do not have measured data for fitting and only want to view the simulated -pattern, you can define an experiment without measured data using the -`add_without_data` method: +If you do not have measured data for fitting and only want to view the +simulated pattern, you can define an experiment without measured data +using the `create` method: ```python # Add an experiment without measured data -project.experiments.add_without_data(name='hrpt', - sample_form='powder', - beam_mode='constant wavelength', - radiation_probe='x-ray') +project.experiments.create( + name='hrpt', + sample_form='powder', + beam_mode='constant wavelength', + radiation_probe='x-ray', +) ``` Finally, you can also add an experiment by passing the experiment object @@ -138,34 +149,39 @@ directly using the `add` method: ```python # Add an experiment by passing the experiment object directly -from easydiffraction import Experiment -experiment = Experiment(name='hrpt', - sample_form='powder', - beam_mode='constant wavelength', - radiation_probe='neutron', - scattering_type='bragg') +from easydiffraction import ExperimentFactory + +experiment = ExperimentFactory.create( + name='hrpt', + data_path='data/hrpt_lbco.xye', + sample_form='powder', + beam_mode='constant wavelength', + radiation_probe='neutron', + scattering_type='bragg', +) project.experiments.add(experiment) ``` ## Modifying Parameters -When an experiment is added, it is created with a set of default parameters that -you can modify to match your specific experimental setup. All parameters are -grouped into categories based on their function, making it easier to manage and -understand the different aspects of the experiment: - -1. **Instrument Category**: Defines the instrument configuration, including - wavelength, two-theta offset, and resolution parameters. -2. **Peak Category**: Specifies the peak profile type and its parameters, such - as broadening and asymmetry. -3. **Background Category**: Defines the background type and allows you to add - background points. -4. **Linked Phases Category**: Links the sample model defined in the previous - step to the experiment, allowing you to specify the scale factor for the - linked phase. -5. **Measured Data Category**: Contains the measured data. The expected format - depends on the experiment type, but generally includes columns for 2θ angle - or TOF and intensity. +When an experiment is added, it is created with a set of default +parameters that you can modify to match your specific experimental +setup. All parameters are grouped into categories based on their +function, making it easier to manage and understand the different +aspects of the experiment: + +1. **Instrument Category**: Defines the instrument configuration, + including wavelength, two-theta offset, and resolution parameters. +2. **Peak Category**: Specifies the peak profile type and its + parameters, such as broadening and asymmetry. +3. **Background Category**: Defines the background type and allows you + to add background points. +4. **Linked Phases Category**: Links the structure defined in the + previous step to the experiment, allowing you to specify the scale + factor for the linked phase. +5. **Measured Data Category**: Contains the measured data. The expected + format depends on the experiment type, but generally includes columns + for 2θ angle or TOF and intensity. ### 1. Instrument Category { #instrument-category } @@ -179,8 +195,8 @@ project.experiments['hrpt'].instrument.calib_twotheta_offset = 0.6 ```python # Add excluded regions to the experiment -project.experiments['hrpt'].excluded_regions.add(start=0, end=10) -project.experiments['hrpt'].excluded_regions.add(start=160, end=180) +project.experiments['hrpt'].excluded_regions.create(start=0, end=10) +project.experiments['hrpt'].excluded_regions.create(start=160, end=180) ``` ### 3. Peak Category { #peak-category } @@ -204,26 +220,26 @@ project.experiments['hrpt'].peak.broad_lorentz_y = 0.1 project.experiments['hrpt'].background_type = 'line-segment' # Add background points -project.experiments['hrpt'].background.add(x=10, y=170) -project.experiments['hrpt'].background.add(x=30, y=170) -project.experiments['hrpt'].background.add(x=50, y=170) -project.experiments['hrpt'].background.add(x=110, y=170) -project.experiments['hrpt'].background.add(x=165, y=170) +project.experiments['hrpt'].background.create(x=10, y=170) +project.experiments['hrpt'].background.create(x=30, y=170) +project.experiments['hrpt'].background.create(x=50, y=170) +project.experiments['hrpt'].background.create(x=110, y=170) +project.experiments['hrpt'].background.create(x=165, y=170) ``` ### 5. Linked Phases Category { #linked-phases-category } ```python -# Link the sample model defined in the previous step to the experiment -project.experiments['hrpt'].linked_phases.add(id='lbco', scale=10.0) +# Link the structure defined in the previous step to the experiment +project.experiments['hrpt'].linked_phases.create(id='lbco', scale=10.0) ``` ### 6. Measured Data Category { #measured-data-category } -If you do not have a CIF file for your experiment, you can load measured data -from a file in a supported format. The measured data will be automatically -converted into CIF format and added to the experiment. The expected format -depends on the experiment type. +If you do not have a CIF file for your experiment, you can load measured +data from a file in a supported format. The measured data will be +automatically converted into CIF format and added to the experiment. The +expected format depends on the experiment type. #### Supported data file formats: @@ -235,8 +251,8 @@ depends on the experiment type. - [\_pd_meas.2theta_scan](../parameters/pd_meas.md) - [\_pd_meas.intensity_total](../parameters/pd_meas.md) -If no **standard deviations** are provided, they are automatically calculated as -the **square root** of measured intensities. +If no **standard deviations** are provided, they are automatically +calculated as the **square root** of measured intensities. Optional comments with `#` are possible in data file headers. @@ -592,5 +608,5 @@ loop_ --- -Now that the experiment has been defined, you can proceed to the next step: -[Analysis](analysis.md). +Now that the experiment has been defined, you can proceed to the next +step: [Analysis](analysis.md). diff --git a/docs/docs/user-guide/analysis-workflow/index.md b/docs/docs/user-guide/analysis-workflow/index.md new file mode 100644 index 00000000..84598210 --- /dev/null +++ b/docs/docs/user-guide/analysis-workflow/index.md @@ -0,0 +1,37 @@ +# Analysis Workflow + +To streamline the **data analysis process**, EasyDiffraction follows a +structured workflow divided into **five key steps**: + +```mermaid +flowchart LR + a(Project) + b(Model) + c(Experiment) + d(Analysis) + e(Summary) + a --> b + b --> c + c --> d + d --> e +``` + +- [:material-archive: Project](project.md) – Establish a **project** as + a container for structure and experiment parameters, measured and + calculated data, analysis settings and results. +- [:material-puzzle: Structure](model.md) – Load an existing + **crystallographic model** in CIF format or define a new one from + scratch. +- [:material-microscope: Experiment](experiment.md) – Import + **experimental diffraction data** and configure **instrumental** and + other relevant parameters. +- [:material-calculator: Analysis](analysis.md) – **Calculate the + diffraction pattern** and **optimize the structural model** by + refining its parameters to match experimental measurements. +- [:material-clipboard-text: Summary](summary.md) – Generate a + **report** summarizing the results of the analysis, including refined + parameters. + +Each step is described in detail in its respective section, guiding +users through the **entire diffraction data analysis workflow** in +EasyDiffraction. diff --git a/docs/user-guide/analysis-workflow/model.md b/docs/docs/user-guide/analysis-workflow/model.md similarity index 64% rename from docs/user-guide/analysis-workflow/model.md rename to docs/docs/user-guide/analysis-workflow/model.md index 69dbe765..95e309cd 100644 --- a/docs/user-guide/analysis-workflow/model.md +++ b/docs/docs/user-guide/analysis-workflow/model.md @@ -2,39 +2,41 @@ icon: material/puzzle --- -# :material-puzzle: Sample Model +# :material-puzzle: Structure -The **Sample Model** in EasyDiffraction represents the **crystallographic -structure** used to calculate the diffraction pattern, which is then fitted to -the **experimentally measured data** to refine the structural parameters. +The **Structure** in EasyDiffraction represents the **crystallographic +structure** used to calculate the diffraction pattern, which is then +fitted to the **experimentally measured data** to refine the structural +parameters. EasyDiffraction allows you to: - **Load an existing model** from a file (**CIF** format). -- **Manually define** a new sample model by specifying crystallographic +- **Manually define** a new structure by specifying crystallographic parameters. -Below, you will find instructions on how to define and manage crystallographic -models in EasyDiffraction. It is assumed that you have already created a -`project` object, as described in the [Project](project.md) section. +Below, you will find instructions on how to define and manage +crystallographic models in EasyDiffraction. It is assumed that you have +already created a `project` object, as described in the +[Project](project.md) section. ## Adding a Model from CIF -This is the most straightforward way to define a sample model in -EasyDiffraction. If you have a crystallographic information file (CIF) for your -sample model, you can add it to your project using the `add_phase_from_file` -method of the `project` instance. In this case, the name of the model will be -taken from CIF. +This is the most straightforward way to define a structure in +EasyDiffraction. If you have a crystallographic information file (CIF) +for your structure, you can add it to your project using the +`add_from_cif_path` method of the `project.structures` collection. In +this case, the name of the model will be taken from CIF. ```python # Load a phase from a CIF file -project.add_phase_from_file('data/lbco.cif') +project.structures.add_from_cif_path('data/lbco.cif') ``` -Accessing the model after loading it will be done through the `sample_models` -object of the `project` instance. The name of the model will be the same as the -data block id in the CIF file. For example, if the CIF file contains a data -block with the id `lbco`, +Accessing the model after loading it will be done through the +`structures` collection of the `project` instance. The name of the model +will be the same as the data block id in the CIF file. For example, if +the CIF file contains a data block with the id `lbco`, @@ -52,100 +54,102 @@ data_lbco you can access it in the code as follows: ```python -# Access the sample model by its name -project.sample_models['lbco'] +# Access the structure by its name +project.structures['lbco'] ``` ## Defining a Model Manually -If you do not have a CIF file or prefer to define the model manually, you can -use the `add` method of the `sample_models` object of the `project` instance. In -this case, you will need to specify the name of the model, which will be used to -reference it later. +If you do not have a CIF file or prefer to define the model manually, +you can use the `create` method of the `structures` object of the +`project` instance. In this case, you will need to specify the name of +the model, which will be used to reference it later. ```python -# Add a sample model with default parameters -# The sample model name is used to reference it later. -project.sample_models.add(name='nacl') +# Add a structure with default parameters +# The structure name is used to reference it later. +project.structures.create(name='nacl') ``` -The `add` method creates a new sample model with default parameters. You can -then modify its parameters to match your specific crystallographic structure. -All parameters are grouped into the following categories, which makes it easier -to manage the model: +The `add` method creates a new structure with default parameters. You +can then modify its parameters to match your specific crystallographic +structure. All parameters are grouped into the following categories, +which makes it easier to manage the model: -1. **Space Group Category**: Defines the symmetry of the crystal structure. -2. **Cell Category**: Specifies the dimensions and angles of the unit cell. -3. **Atom Sites Category**: Describes the positions and properties of atoms - within the unit cell. +1. **Space Group Category**: Defines the symmetry of the crystal + structure. +2. **Cell Category**: Specifies the dimensions and angles of the unit + cell. +3. **Atom Sites Category**: Describes the positions and properties of + atoms within the unit cell. ### 1. Space Group Category { #space-group-category } ```python # Set space group -project.sample_models['nacl'].space_group.name_h_m = 'F m -3 m' +project.structures['nacl'].space_group.name_h_m = 'F m -3 m' ``` ### 2. Cell Category { #cell-category } ```python # Define unit cell parameters -project.sample_models['nacl'].cell.length_a = 5.691694 +project.structures['nacl'].cell.length_a = 5.691694 ``` ### 3. Atom Sites Category { #atom-sites-category } ```python # Add atomic sites -project.sample_models['nacl'].atom_sites.append( +project.structures['nacl'].atom_sites.create( label='Na', type_symbol='Na', fract_x=0, fract_y=0, fract_z=0, occupancy=1, - b_iso_or_equiv=0.5 + b_iso_or_equiv=0.5, ) -project.sample_models['nacl'].atom_sites.append( +project.structures['nacl'].atom_sites.create( label='Cl', type_symbol='Cl', fract_x=0, fract_y=0, fract_z=0.5, occupancy=1, - b_iso_or_equiv=0.5 + b_iso_or_equiv=0.5, ) ``` ## Listing Defined Models -To check which sample models have been added to the `project`, use: +To check which structures have been added to the `project`, use: ```python -# Show defined sample models -project.sample_models.show_names() +# Show defined structures +project.structures.show_names() ``` Expected output: ``` -Defined sample models 🧩 +Defined structures 🧩 ['lbco', 'nacl'] ``` ## Viewing a Model as CIF -To inspect a sample model in CIF format, use: +To inspect a structure in CIF format, use: ```python -# Show sample model as CIF -project.sample_models['lbco'].show_as_cif() +# Show structure as CIF +project.structures['lbco'].show_as_cif() ``` Example output: ``` -Sample model 🧩 'lbco' as cif +Structure 🧩 'lbco' as cif ╒═══════════════════════════════════════════╕ │ data_lbco │ │ │ @@ -178,10 +182,10 @@ Sample model 🧩 'lbco' as cif ## Saving a Model -Saving the project, as described in the [Project](project.md) section, will also -save the model. Each model is saved as a separate CIF file in the -`sample_models` subdirectory of the project directory. The project file contains -references to these files. +Saving the project, as described in the [Project](project.md) section, +will also save the model. Each model is saved as a separate CIF file in +the `structures` subdirectory of the project directory. The project file +contains references to these files. Below is an example of the saved CIF file for the `lbco` model: @@ -224,5 +228,5 @@ O O 0 0.5 0.5 c 1 Biso 1.4041 --- -Now that the crystallographic model has been defined and added to the project, -you can proceed to the next step: [Experiment](experiment.md). +Now that the crystallographic model has been defined and added to the +project, you can proceed to the next step: [Experiment](experiment.md). diff --git a/docs/user-guide/analysis-workflow/project.md b/docs/docs/user-guide/analysis-workflow/project.md similarity index 78% rename from docs/user-guide/analysis-workflow/project.md rename to docs/docs/user-guide/analysis-workflow/project.md index 6987f10d..60c944b9 100644 --- a/docs/user-guide/analysis-workflow/project.md +++ b/docs/docs/user-guide/analysis-workflow/project.md @@ -4,25 +4,26 @@ icon: material/archive # :material-archive: Project -The **Project** serves as a container for all data and metadata associated with -a particular data analysis task. It acts as the top-level entity in -EasyDiffraction, ensuring structured organization and easy access to relevant -information. Each project can contain multiple **experimental datasets**, with -each dataset containing contribution from multiple **sample models**. +The **Project** serves as a container for all data and metadata +associated with a particular data analysis task. It acts as the +top-level entity in EasyDiffraction, ensuring structured organization +and easy access to relevant information. Each project can contain +multiple **experimental datasets**, with each dataset containing +contribution from multiple **structures**. EasyDiffraction allows you to: - **Manually create** a new project by specifying its metadata. - **Load an existing project** from a file (**CIF** format). -Below are instructions on how to set up a project in EasyDiffraction. It is -assumed that you have already imported the `easydiffraction` package, as -described in the [First Steps](../first-steps.md) section. +Below are instructions on how to set up a project in EasyDiffraction. It +is assumed that you have already imported the `easydiffraction` package, +as described in the [First Steps](../first-steps.md) section. ## Creating a Project Manually -You can manually create a new project and specify its short **name**, **title** -and **description**. All these parameters are optional. +You can manually create a new project and specify its short **name**, +**title** and **description**. All these parameters are optional. ```py # Create a new project @@ -30,10 +31,10 @@ project = ed.Project(name='lbco_hrpt') # Define project info project.info.title = 'La0.5Ba0.5CoO3 from neutron diffraction at HRPT@PSI' -project.info.description = '''This project demonstrates a standard refinement +project.info.description = """This project demonstrates a standard refinement of La0.5Ba0.5CoO3, which crystallizes in a perovskite-type structure, using neutron powder diffraction data collected in constant wavelength mode at the -HRPT diffractometer (PSI).''' +HRPT diffractometer (PSI).""" ``` ## Saving a Project @@ -44,10 +45,11 @@ Saving the initial project requires specifying the directory path: project.save_as(dir_path='lbco_hrpt') ``` -If working in the interactive mode in a Jupyter notebook or similar environment, -you can also save the project after every significant change. This is useful for -keeping track of changes and ensuring that your work is not lost. If you already -saved the project with `save_as`, you can just call the `save`: +If working in the interactive mode in a Jupyter notebook or similar +environment, you can also save the project after every significant +change. This is useful for keeping track of changes and ensuring that +your work is not lost. If you already saved the project with `save_as`, +you can just call the `save`: ```python project.save() @@ -55,8 +57,9 @@ project.save() ## Loading a Project from CIF -If you have an existing project, you can load it directly from a CIF file. This -is useful for reusing previously defined projects or sharing them with others. +If you have an existing project, you can load it directly from a CIF +file. This is useful for reusing previously defined projects or sharing +them with others. ```python project.load('data/lbco_hrpt.cif') @@ -73,7 +76,7 @@ The example below illustrates a typical **project structure** for a
 📁 La0.5Ba0.5CoO3     - Project directory.
 ├── 📄 project.cif    - Main project description file.
-├── 📁 sample_models  - Folder with sample models (crystallographic structures).
+├── 📁 structures  - Folder with structures (crystallographic structures).
 │   ├── 📄 lbco.cif   - File with La0.5Ba0.5CoO3 structure parameters.
 │   └── ...
 ├── 📁 experiments    - Folder with instrumental parameters and measured data.
@@ -89,20 +92,20 @@ The example below illustrates a typical **project structure** for a
 
 ## Project Files
 
-Below is a complete project example stored in the `La0.5Ba0.5CoO3` directory,
-showing the contents of all files in the project.
+Below is a complete project example stored in the `La0.5Ba0.5CoO3`
+directory, showing the contents of all files in the project.
 
 !!! warning "Important"
 
     If you save the project right after creating it, the project directory will
     only contain the `project.cif` file. The other folders and files will be
-    created as you add sample models, experiments, and set up the analysis. The
+    created as you add structures, experiments, and set up the analysis. The
     summary folder will be created after the analysis is completed.
 
 ### 1. project.cif
 
-This file provides an overview of the project, including file names of the
-**sample models** and **experiments** associated with the project.
+This file provides an overview of the project, including file names of
+the **structures** and **experiments** associated with the project.
 
 
 
@@ -114,7 +117,7 @@ data_La0.5Ba0.5CoO3
 _project.description "neutrons, powder, constant wavelength, HRPT@PSI"
 
 loop_
-_sample_model.cif_file_name
+_structure.cif_file_name
 lbco.cif
 
 loop_
@@ -125,11 +128,11 @@ hrpt.cif
 
 
 
-### 2. sample_models / lbco.cif
+### 2. structures / lbco.cif
 
-This file contains crystallographic information associated with the sample
-model, including **space group**, **unit cell parameters**, and **atomic
-positions**.
+This file contains crystallographic information associated with the
+structure model, including **space group**, **unit cell parameters**,
+and **atomic positions**.
 
 
 
@@ -168,9 +171,9 @@ O  O    0   0.5 0.5   c   1    Biso 1.4041
 
 ### 3. experiments / hrpt.cif
 
-This file contains the **experiment type**, **instrumental parameters**, **peak
-parameters**, **associated phases**, **background parameters** and **measured
-diffraction data**.
+This file contains the **experiment type**, **instrumental parameters**,
+**peak parameters**, **associated phases**, **background parameters**
+and **measured diffraction data**.
 
 
 
@@ -236,8 +239,8 @@ loop_
 
 ### 4. analysis.cif
 
-This file contains settings used for data analysis, including the choice of
-**calculation** and **fitting** engines, as well as user defined
+This file contains settings used for data analysis, including the choice
+of **calculation** and **fitting** engines, as well as user defined
 **constraints**.
 
 
@@ -257,10 +260,9 @@ occ_La   lbco.atom_site.La.occupancy
 occ_Ba   lbco.atom_site.Ba.occupancy
 
 loop_
-_constraint.lhs_alias
-_constraint.rhs_expr
-biso_Ba  biso_La
-occ_Ba   "1 - occ_La"
+_constraint.expression
+"biso_Ba = biso_La"
+"occ_Ba = 1 - occ_La"
 
@@ -271,4 +273,4 @@ occ_Ba "1 - occ_La" --- Now that the Project has been defined, you can proceed to the next step: -[Sample Model](model.md). +[Structure](model.md). diff --git a/docs/user-guide/analysis-workflow/summary.md b/docs/docs/user-guide/analysis-workflow/summary.md similarity index 72% rename from docs/user-guide/analysis-workflow/summary.md rename to docs/docs/user-guide/analysis-workflow/summary.md index 4790f857..34a89b61 100644 --- a/docs/user-guide/analysis-workflow/summary.md +++ b/docs/docs/user-guide/analysis-workflow/summary.md @@ -5,20 +5,20 @@ icon: material/clipboard-text # :material-clipboard-text: Summary The **Summary** section represents the final step in the data processing -workflow. It involves generating a **summary report** that consolidates the -results of the diffraction data analysis, providing a comprehensive overview of -the model refinement process and its outcomes. +workflow. It involves generating a **summary report** that consolidates +the results of the diffraction data analysis, providing a comprehensive +overview of the model refinement process and its outcomes. ## Contents of the Summary Report The summary report includes key details such as: -- Final refined model parameters – Optimized crystallographic and instrumental - parameters. -- Goodness-of-fit indicators – Metrics such as R-factors, chi-square (χ²), and - residuals. -- Graphical representation – Visualization of experimental vs. calculated - diffraction patterns. +- Final refined model parameters – Optimized crystallographic and + instrumental parameters. +- Goodness-of-fit indicators – Metrics such as R-factors, chi-square + (χ²), and residuals. +- Graphical representation – Visualization of experimental vs. + calculated diffraction patterns. ## Viewing the Summary Report @@ -36,8 +36,9 @@ including model parameters, fit statistics, and data visualizations. ## Saving a Summary -Saving the project, as described in the [Project](project.md) section, will also -save the summary report to the `summary.cif` inside the project directory. +Saving the project, as described in the [Project](project.md) section, +will also save the summary report to the `summary.cif` inside the +project directory. ![](../assets/images/user-guide/data-acquisition_instrument.png){ width="450", loading=lazy } @@ -42,10 +43,10 @@ Credits: DOI 10.1126/science.1238932 ## Data Reduction -Data reduction involves processing the raw data to remove background noise, -correct for instrumental effects, and convert the data into a more usable -format. The goal is to produce a clean and reliable dataset suitable for -analysis. +Data reduction involves processing the raw data to remove background +noise, correct for instrumental effects, and convert the data into a +more usable format. The goal is to produce a clean and reliable dataset +suitable for analysis. ![](../assets/images/user-guide/data-reduction_1d-pattern.png){ width="450", loading=lazy } @@ -57,28 +58,32 @@ Credits: DOI 10.1126/science.1238932 ## Data Analysis -Data analysis uses the reduced data to extract meaningful information about the -sample. This may include determining the crystal or magnetic structure, -identifying phases, performing quantitative analysis, etc. - -Analysis often involves comparing experimental data with data calculated from a -crystallographic model to validate and interpret the results. For powder -diffraction, techniques such as Rietveld or Le Bail refinement may be used. - -In EasyDiffraction, we focus on this **model-dependent analysis**. A model is -built using prior knowledge of the system, and its parameters are optimized to -achieve the best agreement between experimental and calculated diffraction data. - -By "model", we usually refer to a **crystallographic model** of the sample. This -includes unit cell parameters, space group, atomic positions, thermal -parameters, and more. However, the term "model" also encompasses experimental -aspects such as instrumental resolution, background, peak shape, etc. Therefore, -EasyDiffraction separates the model into two parts: the **sample model** and the -**experiment**. - -The aim of data analysis is to refine the structural parameters of the sample by -minimizing the difference (or **residual**) between the experimental and -calculated data — and this is exactly where EasyDiffraction comes into play. +Data analysis uses the reduced data to extract meaningful information +about the crystallographic structure. This may include determining the +crystal or magnetic structure, identifying phases, performing +quantitative analysis, etc. + +Analysis often involves comparing experimental data with data calculated +from a crystallographic model to validate and interpret the results. For +powder diffraction, techniques such as Rietveld or Le Bail refinement +may be used. + +In EasyDiffraction, we focus on this **model-dependent analysis**. A +model is built using prior knowledge of the system, and its parameters +are optimized to achieve the best agreement between experimental and +calculated diffraction data. + +By "model", we usually refer to a **crystallographic model** of the +sample. This includes unit cell parameters, space group, atomic +positions, thermal parameters, and more. However, the term "model" also +encompasses experimental aspects such as instrumental resolution, +background, peak shape, etc. Therefore, EasyDiffraction separates the +model into two parts: the **structure** and the **experiment**. + +The aim of data analysis is to refine the structural parameters of the +sample by minimizing the difference (or **residual**) between the +experimental and calculated data — and this is exactly where +EasyDiffraction comes into play. ![](../assets/images/user-guide/data-analysis_refinement.png){ width="450", loading=lazy } diff --git a/docs/user-guide/data-format.md b/docs/docs/user-guide/data-format.md similarity index 75% rename from docs/user-guide/data-format.md rename to docs/docs/user-guide/data-format.md index acbe6bfe..da7c92b1 100644 --- a/docs/user-guide/data-format.md +++ b/docs/docs/user-guide/data-format.md @@ -1,46 +1,48 @@ # Data Format -Before starting the data analysis workflow, it is important to define the **data -formats** used in EasyDiffraction. +Before starting the data analysis workflow, it is important to define +the **data formats** used in EasyDiffraction. ## Crystallographic Information File -Each software package typically uses its own **data format** and **parameter -names** for storing and sharing data. In EasyDiffraction, we use the -**Crystallographic Information File (CIF)** format, which is widely used in -crystallography and materials science. It provides both a human-readable syntax -and a set of dictionaries that define the meaning of each parameter. +Each software package typically uses its own **data format** and +**parameter names** for storing and sharing data. In EasyDiffraction, we +use the **Crystallographic Information File (CIF)** format, which is +widely used in crystallography and materials science. It provides both a +human-readable syntax and a set of dictionaries that define the meaning +of each parameter. These dictionaries are maintained by the [International Union of Crystallography (IUCr)](https://www.iucr.org). The base dictionary, **coreCIF**, contains the most common parameters in -crystallography. The **pdCIF** dictionary covers parameters specific to powder -diffraction, **magCIF** is used for magnetic structure analysis. +crystallography. The **pdCIF** dictionary covers parameters specific to +powder diffraction, **magCIF** is used for magnetic structure analysis. -As most parameters needed for diffraction data analysis are already covered by -IUCr dictionaries, EasyDiffraction uses the strict **CIF format** and follows -these dictionaries as closely as possible — for both input and output — -throughout the workflow described in the +As most parameters needed for diffraction data analysis are already +covered by IUCr dictionaries, EasyDiffraction uses the strict **CIF +format** and follows these dictionaries as closely as possible — for +both input and output — throughout the workflow described in the [Analysis Workflow](analysis-workflow/index.md) section. The key advantage of CIF is the standardized naming of parameters and -categories, which promotes interoperability and familiarity among researchers. +categories, which promotes interoperability and familiarity among +researchers. If a required parameter is not defined in the standard dictionaries, EasyDiffraction introduces **custom CIF keywords**, documented in the -[Parameters](parameters.md) section under the **CIF name for serialization** -columns. +[Parameters](parameters.md) section under the **CIF name for +serialization** columns. ## Format Comparison -Below, we compare **CIF** with another common data format in programming: -**JSON**. +Below, we compare **CIF** with another common data format in +programming: **JSON**. ### Scientific Journals Let's assume the following structural data for La₀.₅Ba₀.₅CoO₃ (LBCO), as -reported in a scientific publication. These parameters are to be refined during -diffraction data analysis: +reported in a scientific publication. These parameters are to be refined +during diffraction data analysis: Table 1. Crystallographic data. Space group: _Pm3̅m_. @@ -53,8 +55,8 @@ Table 1. Crystallographic data. Space group: _Pm3̅m_. | beta | 90.0 | | gamma | 90.0 | -Table 2. Atomic coordinates (_x_, _y_, _z_), occupancies (occ) and isotropic -displacement parameters (_Biso_) +Table 2. Atomic coordinates (_x_, _y_, _z_), occupancies (occ) and +isotropic displacement parameters (_Biso_) | Label | Type | x | y | z | occ | Biso | | ----- | ---- | --- | --- | --- | --- | ------ | @@ -102,17 +104,17 @@ O O 0 0.5 0.5 c 1 Biso 1.4041 -Here, unit cell parameters are grouped under the `_cell` category, and atomic -positions under the `_atom_site` category. The `loop_` keyword indicates that -multiple rows follow for the listed parameters. Each atom is identified using -`_atom_site.label`. +Here, unit cell parameters are grouped under the `_cell` category, and +atomic positions under the `_atom_site` category. The `loop_` keyword +indicates that multiple rows follow for the listed parameters. Each atom +is identified using `_atom_site.label`. ### JSON -Representing the same data in **JSON** results in a format that is more verbose -and less human-readable, especially for large datasets. JSON is ideal for -structured data in programming environments, whereas CIF is better suited for -human-readable crystallographic data. +Representing the same data in **JSON** results in a format that is more +verbose and less human-readable, especially for large datasets. JSON is +ideal for structured data in programming environments, whereas CIF is +better suited for human-readable crystallographic data. ```json { @@ -173,11 +175,11 @@ human-readable crystallographic data. ## Experiment Definition -The previous example described the **sample model** (crystallographic model), -but how is the **experiment** itself represented? +The previous example described the **structure** (crystallographic +model), but how is the **experiment** itself represented? -The experiment is also saved as a CIF file. For example, background intensity in -a powder diffraction experiment might be represented as: +The experiment is also saved as a CIF file. For example, background +intensity in a powder diffraction experiment might be represented as: @@ -197,16 +199,16 @@ loop_ -More details on how to define the experiment in CIF format are provided in the -[Experiment](analysis-workflow/experiment.md) section. +More details on how to define the experiment in CIF format are provided +in the [Experiment](analysis-workflow/experiment.md) section. ## Other Input/Output Blocks -EasyDiffraction uses CIF consistently throughout its workflow, including in the -following blocks: +EasyDiffraction uses CIF consistently throughout its workflow, including +in the following blocks: - **project**: contains the project information -- **sample model**: defines the sample model +- **structure**: defines the structure - **experiment**: contains the experiment setup and measured data - **analysis**: stores fitting and analysis parameters - **summary**: captures analysis results @@ -217,11 +219,13 @@ Example CIF files for each block are provided in the ## Other Data Formats -While CIF is the primary format in EasyDiffraction, we also support other -formats for importing measured data. These include plain text files with -multiple columns. The meaning of the columns depends on the experiment type. +While CIF is the primary format in EasyDiffraction, we also support +other formats for importing measured data. These include plain text +files with multiple columns. The meaning of the columns depends on the +experiment type. -For example, in a standard constant-wavelength powder diffraction experiment: +For example, in a standard constant-wavelength powder diffraction +experiment: - Column 1: 2θ angle - Column 2: intensity diff --git a/docs/docs/user-guide/first-steps.md b/docs/docs/user-guide/first-steps.md new file mode 100644 index 00000000..b4366684 --- /dev/null +++ b/docs/docs/user-guide/first-steps.md @@ -0,0 +1,190 @@ +# First Steps + +This section introduces the basic usage of the EasyDiffraction Python +API. You'll learn how to import the package, use core classes and +utility functions, and access built-in helper methods to streamline +diffraction data analysis workflows. + +## Importing EasyDiffraction + +### Importing the entire package + +To start using EasyDiffraction, first import the package in your Python +script or Jupyter Notebook. This can be done with the following command: + +```python +import easydiffraction +``` + +Alternatively, you can import it with an alias to avoid naming conflicts +and for convenience: + +```python +import easydiffraction as ed +``` + +The latter syntax allows you to access all the modules and classes +within the package using the `ed` prefix. For example, you can create a +project instance like this: + +```python +project = ed.Project() +``` + +A complete tutorial using the `import` syntax can be found +[here](../tutorials/ed-3.ipynb). + +### Importing specific parts + +Alternatively, you can import specific classes or methods from the +package. For example, you can import the `Project`, `Structure`, +`Experiment` classes and `download_from_repository` method like this: + +```python +from easydiffraction import Project +from easydiffraction import Structure +from easydiffraction import Experiment +from easydiffraction import download_from_repository +``` + +This enables you to use these classes and methods directly without the +package prefix. This is especially useful when you're using only a few +components and want to keep your code clean and concise. In this case, +you can create a project instance like this: + +```python +project = Project() +``` + +A complete tutorial using the `from` syntax can be found +[here](../tutorials/ed-4.ipynb). + +## Utility functions + +EasyDiffraction also provides several utility functions that can +simplify your workflow. One of them is the `download_from_repository` +function, which allows you to download data files from our remote +repository, making it easy to access and use them while experimenting +with EasyDiffraction. + +For example, you can download a data file like this: + +```python +import easydiffraction as ed + +ed.download_from_repository( + 'hrpt_lbco.xye', + branch='docs', + destination='data', +) +``` + +This command will download the `hrpt_lbco.xye` file from the `docs` +branch of the EasyDiffraction repository and save it in the `data` +directory of your current working directory. This is particularly useful +for quickly accessing example datasets without having to manually +download them. + +## Help methods + +EasyDiffraction provides several helper methods to display supported +engines for calculation, minimization, and plotting. These methods can +be called on the `Project` instance to display the available options for +different categories. + +### Supported calculators + +The calculator is automatically selected based on the experiment type. +You can use the `show_supported_calculator_types()` method on an +experiment to see which calculation engines are compatible: + +```python +project.experiments['hrpt'].show_supported_calculator_types() +``` + +This will display a list of supported calculators along with their +descriptions, allowing you to choose the one that best fits your needs. + +An example of the output for a Bragg diffraction experiment: + +| Calculator | Description | +| ---------- | ------------------------------------------------ | +| cryspy | CrysPy library for crystallographic calculations | + +### Supported minimizers + +You can also check the available minimizers using the +`show_available_minimizers()` method: + +```python +project.show_available_minimizers() +``` + +### Available parameters + +EasyDiffraction provides several methods for showing the available +parameters grouped in different categories. For example, you can use: + +- `project.analysis.show_all_params()` – to display all available + parameters for the analysis step. +- `project.analysis.show_fittable_params()` – to display only the + parameters that can be fitted during the analysis. +- `project.analysis.show_free_params()` – to display the parameters that + are currently free to be adjusted during the fitting process. + +Finally, you can use the `project.analysis.how_to_access_parameters()` +method to get a brief overview of how to access and modify parameters in +the analysis step, along with their unique identifiers in the CIF +format. This can be particularly useful for users who are new to the +EasyDiffraction API or those who want to quickly understand how to work +with parameters in their projects. + +An example of the output for the +`project.analysis.how_to_access_parameters()` method is: + +| | Code variable | Unique ID for CIF | +| --- | --------------------------------------------------- | -------------------------------- | +| 1 | project.structures['lbco'].atom_site['La'].adp_type | lbco.atom_site.La.ADP_type | +| 2 | project.structures['lbco'].atom_site['La'].b_iso | lbco.atom_site.La.B_iso_or_equiv | +| 3 | project.structures['lbco'].atom_site['La'].fract_x | lbco.atom_site.La.fract_x | +| 4 | project.structures['lbco'].atom_site['La'].fract_y | lbco.atom_site.La.fract_y | +| ... | ... | ... | +| 59 | project.experiments['hrpt'].peak.broad_gauss_u | hrpt.peak.broad_gauss_u | +| 60 | project.experiments['hrpt'].peak.broad_gauss_v | hrpt.peak.broad_gauss_v | +| 61 | project.experiments['hrpt'].peak.broad_gauss_w | hrpt.peak.broad_gauss_w | + +### Supported plotters + +To see the available plotters, you can use the +`show_available_plotters()` method on the `plotter` attribute of the +`Project` instance: + +```python +project.plotter.show_supported_engines() +``` + +An example of the output is: + +| Engine | Description | +| ------------ | ------------------------------------------ | +| asciichartpy | Console ASCII line charts | +| plotly | Interactive browser-based graphing library | + +## Data analysis workflow + +Once the EasyDiffraction package is imported, you can proceed with the +**data analysis**. This step can be split into several sub-steps, such +as creating a project, defining structures, adding experimental data, +etc. + +EasyDiffraction provides a **Python API** that allows you to perform +these steps programmatically in a certain linear order. This is +especially useful for users who prefer to work in a script or Jupyter +Notebook environment. The API is designed to be intuitive and easy to +use, allowing you to focus on the analysis rather than low-level +implementation details. + +Because this workflow is an important part of the EasyDiffraction +package, it is described in detail in the separate +[Analysis Workflow](analysis-workflow/index.md) section of the +documentation. diff --git a/docs/user-guide/glossary.md b/docs/docs/user-guide/glossary.md similarity index 80% rename from docs/user-guide/glossary.md rename to docs/docs/user-guide/glossary.md index 827cfdf5..75f0300c 100644 --- a/docs/user-guide/glossary.md +++ b/docs/docs/user-guide/glossary.md @@ -1,25 +1,29 @@ # Glossary -Before guiding you through the use of EasyDiffraction, we define some common -terms and abbreviations used throughout the documentation and tutorials. +Before guiding you through the use of EasyDiffraction, we define some +common terms and abbreviations used throughout the documentation and +tutorials. ## Dictionary Type Labels -The following labels are used to identify different types of CIF dictionaries: +The following labels are used to identify different types of CIF +dictionaries: - [coreCIF][1]{:.label-cif} – Core CIF dictionary by the [IUCr](https://www.iucr.org). - [pdCIF][2]{:.label-cif} – Powder CIF dictionary by the [IUCr](https://www.iucr.org). -- [easydiffractionCIF][0]{:.label-cif} – Custom CIF dictionary developed for - EasyDiffraction. +- [easydiffractionCIF][0]{:.label-cif} – Custom CIF dictionary developed + for EasyDiffraction. -For more information about CIF, see the [Data Format](data-format.md) section. +For more information about CIF, see the [Data Format](data-format.md) +section. ## Experiment Type Labels -EasyDiffraction supports a variety of experiment types, each with its own set of -parameters. The following labels identify the supported experiment types: +EasyDiffraction supports a variety of experiment types, each with its +own set of parameters. The following labels identify the supported +experiment types: ### Neutron Diffraction @@ -27,8 +31,8 @@ parameters. The following labels identify the supported experiment types: constant wavelength. - [pd-neut-tof][0]{:.label-experiment} – Powder neutron diffraction with time-of-flight. -- [sc-neut-cwl][0]{:.label-experiment} – Single-crystal neutron diffraction with - constant wavelength. +- [sc-neut-cwl][0]{:.label-experiment} – Single-crystal neutron + diffraction with constant wavelength. ### X-ray Diffraction diff --git a/docs/user-guide/index.md b/docs/docs/user-guide/index.md similarity index 57% rename from docs/user-guide/index.md rename to docs/docs/user-guide/index.md index a6671f3d..2ddc28b9 100644 --- a/docs/user-guide/index.md +++ b/docs/docs/user-guide/index.md @@ -4,20 +4,21 @@ 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 EasyDiffraction effectively. +This section provides an overview of the **core concepts**, **key +parameters** and **workflow steps** required for using EasyDiffraction +effectively. Here is a brief overview of the User Guide sections: -- [Glossary](glossary.md) – Defines common terms and labels used throughout the - documentation. -- [Concept](concept.md) – Introduces the overall idea behind diffraction data - processing and where EasyDiffraction fits. -- [Data Format](data-format.md) – Explains the Crystallographic Information File - (CIF) and how it's used in EasyDiffraction. -- [Parameters](parameters.md) – Describes how parameters are structured, named, - and accessed within the EasyDiffraction library. -- [First Steps](first-steps.md) – Shows how to begin using EasyDiffraction in - Python or Jupyter notebooks. +- [Glossary](glossary.md) – Defines common terms and labels used + throughout the documentation. +- [Concept](concept.md) – Introduces the overall idea behind diffraction + data processing and where EasyDiffraction fits. +- [Data Format](data-format.md) – Explains the Crystallographic + Information File (CIF) and how it's used in EasyDiffraction. +- [Parameters](parameters.md) – Describes how parameters are structured, + named, and accessed within the EasyDiffraction library. +- [First Steps](first-steps.md) – Shows how to begin using + EasyDiffraction in Python or Jupyter notebooks. - [Analysis Workflow](analysis-workflow/index.md) – Breaks down the data analysis pipeline into practical, sequential steps. diff --git a/docs/user-guide/parameters.md b/docs/docs/user-guide/parameters.md similarity index 89% rename from docs/user-guide/parameters.md rename to docs/docs/user-guide/parameters.md index 0fc1d609..622bf6f1 100644 --- a/docs/user-guide/parameters.md +++ b/docs/docs/user-guide/parameters.md @@ -1,82 +1,91 @@ # Parameters -The data analysis process, introduced in the [Concept](concept.md) section, -assumes that you mainly work with different parameters. The parameters are used -to describe the sample model and the experiment and are required to set up the -analysis. +The data analysis process, introduced in the [Concept](concept.md) +section, assumes that you mainly work with different parameters. The +parameters are used to describe the structure and the experiment and are +required to set up the analysis. -Each parameter in EasyDiffraction has a specific name used for code reference, -and it belongs to a specific category. +Each parameter in EasyDiffraction has a specific name used for code +reference, and it belongs to a specific category. - In many cases, the EasyDiffraction name is the same as the CIF name. -- In some cases, the EasyDiffraction name is a slightly modified version of the - CIF name to comply with Python naming conventions. For example, `name_H-M_alt` - becomes `name_h_m`, replacing hyphens with underscores and using lowercase - letters. -- In rare cases, the EasyDiffraction name is a bit shorter, like `b_iso` instead - of CIF `B_iso_or_equiv`, to make the code a bit more user-friendly. -- When there is no defined CIF name for a parameter, EasyDiffraction introduces - its own name, which is used in the code as well as an equivalent CIF name to - be placed in the custom CIF dictionary `easydiffractionCIF`. - -EasyDiffraction names are used in code, while CIF names are used to store and -retrieve the full state of a data analysis project in CIF format. You can find -more about the project in the [Project](analysis-workflow/project.md) section. +- In some cases, the EasyDiffraction name is a slightly modified version + of the CIF name to comply with Python naming conventions. For example, + `name_H-M_alt` becomes `name_h_m`, replacing hyphens with underscores + and using lowercase letters. +- In rare cases, the EasyDiffraction name is a bit shorter, like `b_iso` + instead of CIF `B_iso_or_equiv`, to make the code a bit more + user-friendly. +- When there is no defined CIF name for a parameter, EasyDiffraction + introduces its own name, which is used in the code as well as an + equivalent CIF name to be placed in the custom CIF dictionary + `easydiffractionCIF`. + +EasyDiffraction names are used in code, while CIF names are used to +store and retrieve the full state of a data analysis project in CIF +format. You can find more about the project in the +[Project](analysis-workflow/project.md) section. ## Parameter Attributes -Parameters in EasyDiffraction are more than just variables. They are objects -that, in addition to the name and value, also include attributes such as the -description, unit, uncertainty, minimum and maximum values, etc. All these -attributes are described in the [API Reference](../api-reference/index.md) -section. Examples of how to use these parameters in code are provided in the +Parameters in EasyDiffraction are more than just variables. They are +objects that, in addition to the name and value, also include attributes +such as the description, unit, uncertainty, minimum and maximum values, +etc. All these attributes are described in the +[API Reference](../api-reference/index.md) section. Examples of how to +use these parameters in code are provided in the [Analysis Workflow](analysis-workflow/index.md) and [Tutorials](../tutorials/index.md) sections. -The most important attribute, besides `name` and `value`, is `free`, which is -used to define whether the parameter is free or fixed for optimization during -the fitting process. The `free` attribute is set to `False` by default, which -means the parameter is fixed. To optimize a parameter, set `free` to `True`. +The most important attribute, besides `name` and `value`, is `free`, +which is used to define whether the parameter is free or fixed for +optimization during the fitting process. The `free` attribute is set to +`False` by default, which means the parameter is fixed. To optimize a +parameter, set `free` to `True`. -Although parameters are central, EasyDiffraction hides their creation and -attribute handling from the user. The user only accesses the required parameters -through the top-level objects, such as `project`, `sample_models`, -`experiments`, etc. The parameters are created and initialized automatically -when a new project is created or an existing one is loaded. +Although parameters are central, EasyDiffraction hides their creation +and attribute handling from the user. The user only accesses the +required parameters through the top-level objects, such as `project`, +`structures`, `experiments`, etc. The parameters are created and +initialized automatically when a new project is created or an existing +one is loaded. In the following sections, you can see a list of the parameters used in -EasyDiffraction. Use the tabs to switch between how to access a parameter in -code and its CIF name for serialization. +EasyDiffraction. Use the tabs to switch between how to access a +parameter in code and its CIF name for serialization. !!! warning "Important" Remember that parameters are accessed in code through their parent objects, - such as `project`, `sample_models`, or `experiments`. For example, if you - have a sample model with the ID `nacl`, you can access the space group name + such as `project`, `structures`, or `experiments`. For example, if you + have a structure with the ID `nacl`, you can access the space group name using the following syntax: ```python - project.sample_models['nacl'].space_group.name_h_m + project.structures['nacl'].space_group.name_h_m ``` -In the example above, `space_group` is a sample model category, and `name_h_m` -is the parameter. For simplicity, only the last part (`category.parameter`) of -the full access name will be shown in the tables below. +In the example above, `space_group` is a structure category, and +`name_h_m` is the parameter. For simplicity, only the last part +(`category.parameter`) of the full access name will be shown in the +tables below. -In addition, the CIF names are also provided for each parameter, which are used -to serialize the parameters in the CIF format. +In addition, the CIF names are also provided for each parameter, which +are used to serialize the parameters in the CIF format. -Tags defining the corresponding experiment type are also given before the table. +Tags defining the corresponding experiment type are also given before +the table. -## Sample model parameters +## Structure parameters -Below is a list of parameters used to describe the sample model in +Below is a list of parameters used to describe the structure in EasyDiffraction. ### Crystall structure parameters -[pd-neut-cwl][3]{:.label-experiment} [pd-neut-tof][3]{:.label-experiment} -[pd-xray][3]{:.label-experiment} [sc-neut-cwl][3]{:.label-experiment} +[pd-neut-cwl][3]{:.label-experiment} +[pd-neut-tof][3]{:.label-experiment} [pd-xray][3]{:.label-experiment} +[sc-neut-cwl][3]{:.label-experiment} === "How to access in the code" @@ -131,8 +140,9 @@ EasyDiffraction. ### Common parameters -[pd-neut-cwl][3]{:.label-experiment} [pd-neut-tof][3]{:.label-experiment} -[pd-xray][3]{:.label-experiment} [sc-neut-cwl][3]{:.label-experiment} +[pd-neut-cwl][3]{:.label-experiment} +[pd-neut-tof][3]{:.label-experiment} [pd-xray][3]{:.label-experiment} +[sc-neut-cwl][3]{:.label-experiment} === "How to access in the code" @@ -154,8 +164,8 @@ EasyDiffraction. ### Standard powder diffraction -[pd-neut-cwl][3]{:.label-experiment} [pd-neut-tof][3]{:.label-experiment} -[pd-xray][3]{:.label-experiment} +[pd-neut-cwl][3]{:.label-experiment} +[pd-neut-tof][3]{:.label-experiment} [pd-xray][3]{:.label-experiment} === "How to access in the code" @@ -241,7 +251,8 @@ EasyDiffraction. ### Total scattering -[pd-neut-total][3]{:.label-experiment} [pd-xray-total][3]{:.label-experiment} +[pd-neut-total][3]{:.label-experiment} +[pd-xray-total][3]{:.label-experiment} === "How to access in the code" diff --git a/docs/user-guide/parameters/_diffrn_radiation.md b/docs/docs/user-guide/parameters/_diffrn_radiation.md similarity index 87% rename from docs/user-guide/parameters/_diffrn_radiation.md rename to docs/docs/user-guide/parameters/_diffrn_radiation.md index e3c0a7e9..6e2f0a06 100644 --- a/docs/user-guide/parameters/_diffrn_radiation.md +++ b/docs/docs/user-guide/parameters/_diffrn_radiation.md @@ -4,13 +4,13 @@ Data items in this category describe the radiation used in measuring the diffraction intensities. Please see the -[IUCr page](https://www.iucr.org/resources/cif/dictionaries/browse/cif_pd) for -further details. +[IUCr page](https://www.iucr.org/resources/cif/dictionaries/browse/cif_pd) +for further details. ## [\_diffrn_radiation.probe](https://www.iucr.org/resources/cif/dictionaries/browse/cif_pd) -The nature of the radiation used (i.e. the name of the subatomic particle or the -region of the electromagnetic spectrum). +The nature of the radiation used (i.e. the name of the subatomic +particle or the region of the electromagnetic spectrum). Supported values: `neutron` and `x-ray` diff --git a/docs/user-guide/parameters/_diffrn_radiation_wavelength.md b/docs/docs/user-guide/parameters/_diffrn_radiation_wavelength.md similarity index 95% rename from docs/user-guide/parameters/_diffrn_radiation_wavelength.md rename to docs/docs/user-guide/parameters/_diffrn_radiation_wavelength.md index 4d205788..3b750b22 100644 --- a/docs/user-guide/parameters/_diffrn_radiation_wavelength.md +++ b/docs/docs/user-guide/parameters/_diffrn_radiation_wavelength.md @@ -4,8 +4,8 @@ Data items in this category describe the wavelength of radiation used in diffraction measurements. Please see the -[IUCr page](https://www.iucr.org/resources/cif/dictionaries/browse/cif_pd) for -further details. +[IUCr page](https://www.iucr.org/resources/cif/dictionaries/browse/cif_pd) +for further details. ## [\_diffrn_radiation_wavelength.wavelength](https://www.iucr.org/resources/cif/dictionaries/browse/cif_pd) diff --git a/docs/user-guide/parameters/_exptl_crystal.md b/docs/docs/user-guide/parameters/_exptl_crystal.md similarity index 100% rename from docs/user-guide/parameters/_exptl_crystal.md rename to docs/docs/user-guide/parameters/_exptl_crystal.md diff --git a/docs/user-guide/parameters/_extinction.md b/docs/docs/user-guide/parameters/_extinction.md similarity index 100% rename from docs/user-guide/parameters/_extinction.md rename to docs/docs/user-guide/parameters/_extinction.md diff --git a/docs/user-guide/parameters/_pd_calib.md b/docs/docs/user-guide/parameters/_pd_calib.md similarity index 94% rename from docs/user-guide/parameters/_pd_calib.md rename to docs/docs/user-guide/parameters/_pd_calib.md index 2b96a4c8..407c5f73 100644 --- a/docs/user-guide/parameters/_pd_calib.md +++ b/docs/docs/user-guide/parameters/_pd_calib.md @@ -2,8 +2,8 @@ # \_pd_calib -This section defines the parameters used for the calibration of the instrument, -similar to this +This section defines the parameters used for the calibration of the +instrument, similar to this [IUCr section](https://www.iucr.org/resources/cif/dictionaries/browse/cif_pd). ## [\_pd_calib.2theta_offset](https://www.iucr.org/resources/cif/dictionaries/browse/cif_pd) diff --git a/docs/user-guide/parameters/atom_site.md b/docs/docs/user-guide/parameters/atom_site.md similarity index 82% rename from docs/user-guide/parameters/atom_site.md rename to docs/docs/user-guide/parameters/atom_site.md index 44325b8a..7acdb650 100644 --- a/docs/user-guide/parameters/atom_site.md +++ b/docs/docs/user-guide/parameters/atom_site.md @@ -2,16 +2,16 @@ # \_atom_site -Data items in this category record details about the atom sites in a crystal -structure, such as the positional coordinates and atomic displacement -parameters. Please see the +Data items in this category record details about the atom sites in a +crystal structure, such as the positional coordinates and atomic +displacement parameters. Please see the [IUCr page](https://www.iucr.org/__data/iucr/cifdic_html/3/CORE_DIC/CATOM_SITE.html) for further details. ## [\_atom_site.label](https://www.iucr.org/__data/iucr/cifdic_html/3/CORE_DIC/Iatom_site.label.html) -This is a unique identifier for a particular site in the asymmetric unit of the -crystal unit cell. +This is a unique identifier for a particular site in the asymmetric unit +of the crystal unit cell. ## [\_atom_site.type_symbol](https://www.iucr.org/__data/iucr/cifdic_html/3/CORE_DIC/Iatom_site.type_symbol.html) @@ -19,7 +19,8 @@ A code to identify the atom specie(s) occupying this site. ## \_atom_site.fract -Atom-site coordinates as fractions of the [\_cell_length](cell.md) values. +Atom-site coordinates as fractions of the [\_cell_length](cell.md) +values. - [\_atom_site.fract_x](https://www.iucr.org/__data/iucr/cifdic_html/3/CORE_DIC/Iatom_site.fract_x.html) - [\_atom_site.fract_y](https://www.iucr.org/__data/iucr/cifdic_html/3/CORE_DIC/Iatom_site.fract_y.html) @@ -31,8 +32,8 @@ The fraction of the atom type present at this site. ## [\_atom_site.ADP_type](https://www.iucr.org/__data/iucr/cifdic_html/3/CORE_DIC/Iatom_site.adp_type.html) -Code for type of atomic displacement parameters used for the site. Currently -only `Biso` (isotropic B) is supported. +Code for type of atomic displacement parameters used for the site. +Currently only `Biso` (isotropic B) is supported. ## [\_atom_site.B_iso_or_equiv](https://www.iucr.org/__data/iucr/cifdic_html/3/CORE_DIC/Iatom_site.B_iso_or_equiv.html) @@ -43,17 +44,17 @@ displacement parameter, in angstroms squared. `optional parameter` -The number of different sites that are generated by the application of the -space-group symmetry to the coordinates given for this site. It is equal to the -multiplicity given for this Wyckoff site in International Tables for -Crystallography Vol. A (2002). +The number of different sites that are generated by the application of +the space-group symmetry to the coordinates given for this site. It is +equal to the multiplicity given for this Wyckoff site in International +Tables for Crystallography Vol. A (2002). ## [\_atom_site.Wyckoff_symbol](https://www.iucr.org/__data/iucr/cifdic_html/3/CORE_DIC/Iatom_site.Wyckoff_symbol.html) `optional parameter` -The Wyckoff symbol (letter) as listed in the space-group tables of International -Tables for Crystallography Vol. A. +The Wyckoff symbol (letter) as listed in the space-group tables of +International Tables for Crystallography Vol. A. [0]: # diff --git a/docs/user-guide/parameters/background.md b/docs/docs/user-guide/parameters/background.md similarity index 75% rename from docs/user-guide/parameters/background.md rename to docs/docs/user-guide/parameters/background.md index 89985470..e307c981 100644 --- a/docs/user-guide/parameters/background.md +++ b/docs/docs/user-guide/parameters/background.md @@ -2,26 +2,27 @@ # \_pd_background -This category defines various background functions that could be used when -calculating diffractograms. Please see the -[IUCr page](https://www.iucr.org/resources/cif/dictionaries/browse/cif_pd) for -further details. +This category defines various background functions that could be used +when calculating diffractograms. Please see the +[IUCr page](https://www.iucr.org/resources/cif/dictionaries/browse/cif_pd) +for further details. ## [\_pd_background.line_segment_X](https://www.iucr.org/resources/cif/dictionaries/browse/cif_pd) -List of X-coordinates used to create many straight-line segments representing -the background in a calculated diffractogram. +List of X-coordinates used to create many straight-line segments +representing the background in a calculated diffractogram. Supported values: `2theta` and `time-of-flight` ## [\_pd_background.line_segment_intensity](https://www.iucr.org/resources/cif/dictionaries/browse/cif_pd) -List of intensities used to create many straight-line segments representing the -background in a calculated diffractogram. +List of intensities used to create many straight-line segments +representing the background in a calculated diffractogram. ## [\_pd_background.X_coordinate](https://www.iucr.org/resources/cif/dictionaries/browse/cif_pd) -The type of X-coordinate against which the pd_background values were calculated. +The type of X-coordinate against which the pd_background values were +calculated. [0]: # diff --git a/docs/user-guide/parameters/cell.md b/docs/docs/user-guide/parameters/cell.md similarity index 95% rename from docs/user-guide/parameters/cell.md rename to docs/docs/user-guide/parameters/cell.md index 5aeb12ae..806df516 100644 --- a/docs/user-guide/parameters/cell.md +++ b/docs/docs/user-guide/parameters/cell.md @@ -2,8 +2,8 @@ # \_cell -Data items in this category record details about the crystallographic cell -parameters and their measurement. Please see the +Data items in this category record details about the crystallographic +cell parameters and their measurement. Please see the [IUCr page](https://www.iucr.org/__data/iucr/cifdic_html/3/CORE_DIC/CCELL.html) for further details. diff --git a/docs/user-guide/parameters/expt_type.md b/docs/docs/user-guide/parameters/expt_type.md similarity index 95% rename from docs/user-guide/parameters/expt_type.md rename to docs/docs/user-guide/parameters/expt_type.md index 5aeb12ae..806df516 100644 --- a/docs/user-guide/parameters/expt_type.md +++ b/docs/docs/user-guide/parameters/expt_type.md @@ -2,8 +2,8 @@ # \_cell -Data items in this category record details about the crystallographic cell -parameters and their measurement. Please see the +Data items in this category record details about the crystallographic +cell parameters and their measurement. Please see the [IUCr page](https://www.iucr.org/__data/iucr/cifdic_html/3/CORE_DIC/CCELL.html) for further details. diff --git a/docs/user-guide/parameters/instrument.md b/docs/docs/user-guide/parameters/instrument.md similarity index 73% rename from docs/user-guide/parameters/instrument.md rename to docs/docs/user-guide/parameters/instrument.md index 3975161c..fbeca6c9 100644 --- a/docs/user-guide/parameters/instrument.md +++ b/docs/docs/user-guide/parameters/instrument.md @@ -2,21 +2,22 @@ # \_pd_instr -This section contains information relevant to the instrument used for the -diffraction measurement, similar to this +This section contains information relevant to the instrument used for +the diffraction measurement, similar to this [IUCr section](https://www.iucr.org/resources/cif/dictionaries/browse/cif_pd). ## [\_pd_instr.resolution](#) -In general, the profile of a Bragg reflection centred at the peak position can -be approximated by mathematical convolution of contributions from the -instrument, called the instrumental resolution function, and from the -microstructure of the sample. Because many contributions to powder diffraction -peaks have a nearly Gaussian or Lorentzian shape, the pseudo-Voigt function, is -widely used to describe peak profiles in powder diffraction. +In general, the profile of a Bragg reflection centred at the peak +position can be approximated by mathematical convolution of +contributions from the instrument, called the instrumental resolution +function, and from the microstructure of the sample. Because many +contributions to powder diffraction peaks have a nearly Gaussian or +Lorentzian shape, the pseudo-Voigt function, is widely used to describe +peak profiles in powder diffraction. -Half-width parameters (normally characterising the instrumental resolution -function) as implemented in [CrysPy](https://cryspy.fr): +Half-width parameters (normally characterising the instrumental +resolution function) as implemented in [CrysPy](https://cryspy.fr): - \_pd_instr.resolution_u - \_pd_instr.resolution_v @@ -34,7 +35,8 @@ Lorentzian isotropic particle size parameteras implemented in ## [\_pd_instr.reflex_asymmetry](#) -Peak profile asymmetry parameters as implemented in [CrysPy](https://cryspy.fr). +Peak profile asymmetry parameters as implemented in +[CrysPy](https://cryspy.fr). - \_pd_instr.reflex_asymmetry_p1 - \_pd_instr.reflex_asymmetry_p2 diff --git a/docs/user-guide/parameters/linked_phases.md b/docs/docs/user-guide/parameters/linked_phases.md similarity index 86% rename from docs/user-guide/parameters/linked_phases.md rename to docs/docs/user-guide/parameters/linked_phases.md index df3f6d29..1d00f1d2 100644 --- a/docs/user-guide/parameters/linked_phases.md +++ b/docs/docs/user-guide/parameters/linked_phases.md @@ -2,10 +2,10 @@ # \_pd_phase_block -A table of phases relevant to the current data block. Each phase is identified -by its data block identifier. Please see the -[IUCr page](https://www.iucr.org/resources/cif/dictionaries/browse/cif_pd) for -further details. +A table of phases relevant to the current data block. Each phase is +identified by its data block identifier. Please see the +[IUCr page](https://www.iucr.org/resources/cif/dictionaries/browse/cif_pd) +for further details. ## [\_pd_phase_block.id](https://www.iucr.org/resources/cif/dictionaries/browse/cif_pd) diff --git a/docs/user-guide/parameters/pd_meas.md b/docs/docs/user-guide/parameters/pd_meas.md similarity index 97% rename from docs/user-guide/parameters/pd_meas.md rename to docs/docs/user-guide/parameters/pd_meas.md index b294a96a..fcb4db17 100644 --- a/docs/user-guide/parameters/pd_meas.md +++ b/docs/docs/user-guide/parameters/pd_meas.md @@ -7,8 +7,8 @@ This section contains the measured diffractogram, similar to this ## [\_pd_meas.2theta_scan](https://raw.githubusercontent.com/COMCIFS/Powder_Dictionary/master/cif_pow.dic) -2θ diffraction angle (in degrees) for intensity points measured in a scanning -method. +2θ diffraction angle (in degrees) for intensity points measured in a +scanning method. ## [\_pd_meas.time-of-flight](https://raw.githubusercontent.com/COMCIFS/Powder_Dictionary/master/cif_pow.dic) diff --git a/docs/user-guide/parameters/peak.md b/docs/docs/user-guide/parameters/peak.md similarity index 95% rename from docs/user-guide/parameters/peak.md rename to docs/docs/user-guide/parameters/peak.md index 5aeb12ae..806df516 100644 --- a/docs/user-guide/parameters/peak.md +++ b/docs/docs/user-guide/parameters/peak.md @@ -2,8 +2,8 @@ # \_cell -Data items in this category record details about the crystallographic cell -parameters and their measurement. Please see the +Data items in this category record details about the crystallographic +cell parameters and their measurement. Please see the [IUCr page](https://www.iucr.org/__data/iucr/cifdic_html/3/CORE_DIC/CCELL.html) for further details. diff --git a/docs/user-guide/parameters/space_group.md b/docs/docs/user-guide/parameters/space_group.md similarity index 87% rename from docs/user-guide/parameters/space_group.md rename to docs/docs/user-guide/parameters/space_group.md index 883be103..ae5bce6b 100644 --- a/docs/user-guide/parameters/space_group.md +++ b/docs/docs/user-guide/parameters/space_group.md @@ -2,16 +2,16 @@ # \_space_group -Contains all the data items that refer to the space group as a whole. Please see -the +Contains all the data items that refer to the space group as a whole. +Please see the [IUCr page](https://www.iucr.org/__data/iucr/cifdic_html/3/CORE_DIC/CSPACE_GROUP.html) for further details. ## [\_space_group.name_H-M_alt](https://www.iucr.org/__data/iucr/cifdic_html/3/CORE_DIC/Ispace_group.name_H-M_alt.html) -The international Hermann-Mauguin space-group symbol as defined in International -Tables for Crystallography Volume A. It allows any Hermann-Mauguin symbol to be -given. +The international Hermann-Mauguin space-group symbol as defined in +International Tables for Crystallography Volume A. It allows any +Hermann-Mauguin symbol to be given. ## [\_space_group.IT_coordinate_system_code](https://www.iucr.org/__data/iucr/cifdic_html/3/CORE_DIC/Ispace_group.IT_coordinate_system_code.html) 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 index 6641a35b..c272bdec 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -1,45 +1,167 @@ -########################### -# Override default settings -########################### - # Project information -site_name: '' #EasyDiffraction Library -site_url: https://easyscience.github.io/diffraction-lib/ #https://docs.easydiffraction.org/lib/ +site_name: EasyDiffraction Library +site_url: https://easyscience.github.io/diffraction-lib # Repository -repo_url: https://github.com/easyscience/diffraction-lib/ +repo_url: https://github.com/easyscience/diffraction-lib edit_uri: edit/develop/docs/ # Copyright -copyright: © 2025 EasyDiffraction +copyright: © 2021-2026 EasyDiffraction + +# 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 -# Extra icons in the bottom right corner +# A set of key-value pairs, where the values can be any valid YAML +# construct, that will be passed to the template extra: - social: + 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: easydiffraction # File: overrides/.icons/easydiffraction.svg - link: https://easydiffraction.org + link: https://easyscience.github.io/diffraction name: EasyDiffraction Main Webpage - icon: app # File: overrides/.icons/app.svg - link: https://docs.easydiffraction.org/app/ + link: https://easyscience.github.io/diffraction-app name: EasyDiffraction Application Docs - icon: fontawesome/brands/github # Name as in Font Awesome link: https://github.com/easyscience/diffraction-lib name: EasyDiffraction 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 -# Jupyter notebooks +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: - execute_ignore: - - '*.ipynb' # Do not execute any notebooks -# - 'quick*.ipynb' -# - 'basic*.ipynb' -# - 'advanced*.ipynb' -# - 'cryst*.ipynb' -# - 'pdf*.ipynb' + 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: + docstring_style: numpy + group_by_category: false + heading_level: 1 + show_root_heading: true + show_root_full_path: false + show_submodules: true + show_source: true + - search -################## -# Add new settings -################## +# Determines additional directories to watch when running mkdocs serve +watch: + - includes + - overrides + - ../src # Exclude files and folders from the global navigation not_in_nav: | @@ -62,7 +184,7 @@ nav: - Analysis Workflow: - Analysis Workflow: user-guide/analysis-workflow/index.md - Project: user-guide/analysis-workflow/project.md - - Sample Model: user-guide/analysis-workflow/model.md + - Structure: user-guide/analysis-workflow/model.md - Experiment: user-guide/analysis-workflow/experiment.md - Analysis: user-guide/analysis-workflow/analysis.md - Summary: user-guide/analysis-workflow/summary.md @@ -71,29 +193,37 @@ nav: - Getting Started: - LBCO quick CIF: tutorials/ed-1.ipynb - LBCO quick code: tutorials/ed-2.ipynb - - LBCO basic: tutorials/ed-3.ipynb - - PbSO4 advanced: tutorials/ed-4.ipynb - - Standard Diffraction: + - LBCO complete: tutorials/ed-3.ipynb + - Powder Diffraction: - Co2SiO4 pd-neut-cwl: tutorials/ed-5.ipynb - HS pd-neut-cwl: tutorials/ed-6.ipynb - Si pd-neut-tof: tutorials/ed-7.ipynb - NCAF pd-neut-tof: tutorials/ed-8.ipynb - LBCO+Si McStas: tutorials/ed-9.ipynb + - Single Crystal Diffraction: + - Tb2TiO7 sg-neut-cwl: tutorials/ed-14.ipynb + - Taurine sg-neut-tof: tutorials/ed-15.ipynb - Pair Distribution Function: - Ni pd-neut-cwl: tutorials/ed-10.ipynb - Si pd-neut-tof: tutorials/ed-11.ipynb - NaCl pd-xray: tutorials/ed-12.ipynb + - Multiple Data Blocks: + - PbSO4 NPD+XRD: tutorials/ed-4.ipynb + - LBCO+Si McStas: tutorials/ed-9.ipynb + - Si Bragg+PDF: tutorials/ed-16.ipynb + - Co2SiO4 T-scan: tutorials/ed-17.ipynb - Workshops & Schools: - - 2025 DMSC: tutorials/ed-13.ipynb + - DMSC Summer School: tutorials/ed-13.ipynb - API Reference: - API Reference: api-reference/index.md - analysis: api-reference/analysis.md - core: api-reference/core.md - crystallography: api-reference/crystallography.md + - datablocks: + - experiment: api-reference/datablocks/experiment.md + - structure: api-reference/datablocks/structure.md - display: api-reference/display.md - - experiments: api-reference/experiments.md - io: api-reference/io.md - project: api-reference/project.md - - sample_models: api-reference/sample_models.md - summary: api-reference/summary.md - utils: api-reference/utils.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/easydiffraction.svg b/docs/overrides/.icons/easydiffraction.svg new file mode 100644 index 00000000..5813e570 --- /dev/null +++ b/docs/overrides/.icons/easydiffraction.svg @@ -0,0 +1,3 @@ + + + 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/tutorials/index.md b/docs/tutorials/index.md deleted file mode 100644 index 55f406a4..00000000 --- a/docs/tutorials/index.md +++ /dev/null @@ -1,79 +0,0 @@ ---- -icon: material/school ---- - -# :material-school: Tutorials - -This section presents a collection of **Jupyter Notebook** tutorials that -demonstrate how to use EasyDiffraction for various tasks. These tutorials serve -as self-contained, step-by-step **guides** to help users grasp the workflow of -diffraction data analysis using EasyDiffraction. - -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. - -The tutorials are organized into the following categories. - -## Getting Started - -- [LBCO `quick` CIF](ed-1.ipynb) – A minimal example intended as a quick - reference for users already familiar with the EasyDiffraction API or who want - to see how Rietveld refinement of the La0.5Ba0.5CoO3 crystal structure can be - performed when both the sample model and experiment are loaded from CIF files. - Data collected from constant wavelength neutron powder diffraction at HRPT at - PSI. -- [LBCO `quick` `code`](ed-2.ipynb) – A minimal example intended as a quick - reference for users already familiar with the EasyDiffraction API or who want - to see an example refinement when both the sample model and experiment are - defined directly in code. This tutorial covers a Rietveld refinement of the - La0.5Ba0.5CoO3 crystal structure using constant wavelength neutron powder - diffraction data from HRPT at PSI. -- [LBCO `basic`](ed-3.ipynb) – Demonstrates the use of the EasyDiffraction API - in a simplified, user-friendly manner that closely follows the GUI workflow - for a Rietveld refinement of the La0.5Ba0.5CoO3 crystal structure using - constant wavelength neutron powder diffraction data from HRPT at PSI. This - tutorial provides a full explanation of the workflow with detailed comments - and descriptions of every step, making it suitable for users who are new to - EasyDiffraction or those who prefer a more guided approach. -- [PbSO4 `advanced`](ed-4.ipynb) – Demonstrates a more flexible and advanced - approach to using the EasyDiffraction library, intended for users who are more - comfortable with Python programming. This tutorial covers a Rietveld - refinement of the PbSO4 crystal structure based on the joint fit of both X-ray - and neutron diffraction data. - -## Standard Diffraction - -- [Co2SiO4 `pd-neut-cwl`](ed-5.ipynb) – Demonstrates a Rietveld refinement of - the Co2SiO4 crystal structure using constant wavelength neutron powder - diffraction data from D20 at ILL. -- [HS `pd-neut-cwl`](ed-6.ipynb) – Demonstrates a Rietveld refinement of the HS - crystal structure using constant wavelength neutron powder diffraction data - from HRPT at PSI. -- [Si `pd-neut-tof`](ed-7.ipynb) – Demonstrates a Rietveld refinement of the Si - crystal structure using time-of-flight neutron powder diffraction data from - SEPD at Argonne. -- [NCAF `pd-neut-tof`](ed-8.ipynb) – Demonstrates a Rietveld refinement of the - Na2Ca3Al2F14 crystal structure using two time-of-flight neutron powder - diffraction datasets (from two detector banks) of the WISH instrument at ISIS. -- [LBCO+Si McStas](ed-9.ipynb) – Demonstrates a Rietveld refinement of the - La0.5Ba0.5CoO3 crystal structure with a small amount of Si impurity as a - secondary phase using time-of-flight neutron powder diffraction data simulated - with McStas. - -## Pair Distribution Function (PDF) - -- [Ni `pd-neut-cwl`](ed-10.ipynb) – Demonstrates a PDF analysis of Ni using data - collected from a constant wavelength neutron powder diffraction experiment. -- [Si `pd-neut-tof`](ed-11.ipynb) – Demonstrates a PDF analysis of Si using data - collected from a time-of-flight neutron powder diffraction experiment at NOMAD - at SNS. -- [NaCl `pd-xray`](ed-12.ipynb) – Demonstrates a PDF analysis of NaCl using data - collected from an X-ray powder diffraction experiment. - -## Workshops & Schools - -- [2025 DMSC](ed-13.ipynb) – A workshop tutorial that demonstrates a Rietveld - refinement of the La0.5Ba0.5CoO3 crystal structure using time-of-flight - neutron powder diffraction data simulated with McStas. This tutorial is - designed for the ESS DMSC Summer School 2025. diff --git a/docs/user-guide/analysis-workflow/index.md b/docs/user-guide/analysis-workflow/index.md deleted file mode 100644 index 1ce3ec66..00000000 --- a/docs/user-guide/analysis-workflow/index.md +++ /dev/null @@ -1,34 +0,0 @@ -# Analysis Workflow - -To streamline the **data analysis process**, EasyDiffraction follows a -structured workflow divided into **five key steps**: - -```mermaid -flowchart LR - a(Project) - b(Model) - c(Experiment) - d(Analysis) - e(Summary) - a --> b - b --> c - c --> d - d --> e -``` - -- [:material-archive: Project](project.md) – Establish a **project** as a - container for sample model and experiment parameters, measured and calculated - data, analysis settings and results. -- [:material-puzzle: Sample Model](model.md) – Load an existing - **crystallographic model** in CIF format or define a new one from scratch. -- [:material-microscope: Experiment](experiment.md) – Import **experimental - diffraction data** and configure **instrumental** and other relevant - parameters. -- [:material-calculator: Analysis](analysis.md) – **Calculate the diffraction - pattern** and **optimize the structural model** by refining its parameters to - match experimental measurements. -- [:material-clipboard-text: Summary](summary.md) – Generate a **report** - summarizing the results of the analysis, including refined parameters. - -Each step is described in detail in its respective section, guiding users -through the **entire diffraction data analysis workflow** in EasyDiffraction. diff --git a/docs/user-guide/first-steps.md b/docs/user-guide/first-steps.md deleted file mode 100644 index 0500b189..00000000 --- a/docs/user-guide/first-steps.md +++ /dev/null @@ -1,182 +0,0 @@ -# First Steps - -This section introduces the basic usage of the EasyDiffraction Python API. -You'll learn how to import the package, use core classes and utility functions, -and access built-in helper methods to streamline diffraction data analysis -workflows. - -## Importing EasyDiffraction - -### Importing the entire package - -To start using EasyDiffraction, first import the package in your Python script -or Jupyter Notebook. This can be done with the following command: - -```python -import easydiffraction -``` - -Alternatively, you can import it with an alias to avoid naming conflicts and for -convenience: - -```python -import easydiffraction as ed -``` - -The latter syntax allows you to access all the modules and classes within the -package using the `ed` prefix. For example, you can create a project instance -like this: - -```python -project = ed.Project() -``` - -A complete tutorial using the `import` syntax can be found -[here](../../tutorials/ed-3/). - -### Importing specific parts - -Alternatively, you can import specific classes or methods from the package. For -example, you can import the `Project`, `SampleModel`, `Experiment` classes and -`download_from_repository` method like this: - -```python -from easydiffraction import ( - Project, - SampleModel, - Experiment, - download_from_repository -) -``` - -This enables you to use these classes and methods directly without the package -prefix. This is especially useful when you're using only a few components and -want to keep your code clean and concise. In this case, you can create a project -instance like this: - -```python -project = Project() -``` - -A complete tutorial using the `from` syntax can be found -[here](../../tutorials/ed-4/). - -## Utility functions - -EasyDiffraction also provides several utility functions that can simplify your -workflow. One of them is the `download_from_repository` function, which allows -you to download data files from our remote repository, making it easy to access -and use them while experimenting with EasyDiffraction. - -For example, you can download a sample data file like this: - -```python -import easydiffraction as ed - -ed.download_from_repository('hrpt_lbco.xye', - branch='docs', - destination='data') -``` - -This command will download the `hrpt_lbco.xye` file from the `docs` branch of -the EasyDiffraction repository and save it in the `data` directory of your -current working directory. This is particularly useful for quickly accessing -example datasets without having to manually download them. - -## Help methods - -EasyDiffraction provides several helper methods to display supported engines for -calculation, minimization, and plotting. These methods can be called on the -`Project` instance to display the available options for different categories. - -### Supported calculators - -For example, you can use the `show_supported_calculators()` method to see which -calculation engines are available for use in your project: - -```python -project.show_supported_calculators() -``` - -This will display a list of supported calculators along with their descriptions, -allowing you to choose the one that best fits your needs. - -An example of the output for the `show_supported_calculators()` method is: - -| Calculator | Description | -| ---------- | ----------------------------------------------------------- | -| cryspy | CrysPy library for crystallographic calculations | -| pdffit | PDFfit2 library for pair distribution function calculations | - -### Supported minimizers - -You can also check the available minimizers using the -`show_available_minimizers()` method: - -```python -project.show_available_minimizers() -``` - -### Available parameters - -EasyDiffraction provides several methods for showing the available parameters -grouped in different categories. For example, you can use: - -- `project.analysis.show_all_params()` – to display all available parameters for - the analysis step. -- `project.analysis.show_fittable_params()` – to display only the parameters - that can be fitted during the analysis. -- `project.analysis.show_free_params()` – to display the parameters that are - currently free to be adjusted during the fitting process. - -Finally, you can use the `project.analysis.how_to_access_parameters()` method to -get a brief overview of how to access and modify parameters in the analysis -step, along with their unique identifiers in the CIF format. This can be -particularly useful for users who are new to the EasyDiffraction API or those -who want to quickly understand how to work with parameters in their projects. - -An example of the output for the `project.analysis.how_to_access_parameters()` -method is: - -| | Code variable | Unique ID for CIF | -| --- | ------------------------------------------------------ | -------------------------------- | -| 1 | project.sample_models['lbco'].atom_site['La'].adp_type | lbco.atom_site.La.ADP_type | -| 2 | project.sample_models['lbco'].atom_site['La'].b_iso | lbco.atom_site.La.B_iso_or_equiv | -| 3 | project.sample_models['lbco'].atom_site['La'].fract_x | lbco.atom_site.La.fract_x | -| 4 | project.sample_models['lbco'].atom_site['La'].fract_y | lbco.atom_site.La.fract_y | -| ... | ... | ... | -| 59 | project.experiments['hrpt'].peak.broad_gauss_u | hrpt.peak.broad_gauss_u | -| 60 | project.experiments['hrpt'].peak.broad_gauss_v | hrpt.peak.broad_gauss_v | -| 61 | project.experiments['hrpt'].peak.broad_gauss_w | hrpt.peak.broad_gauss_w | - -### Supported plotters - -To see the available plotters, you can use the `show_available_plotters()` -method on the `plotter` attribute of the `Project` instance: - -```python -project.plotter.show_supported_engines() -``` - -An example of the output is: - -| Engine | Description | -| ------------ | ------------------------------------------ | -| asciichartpy | Console ASCII line charts | -| plotly | Interactive browser-based graphing library | - -## Data analysis workflow - -Once the EasyDiffraction package is imported, you can proceed with the **data -analysis**. This step can be split into several sub-steps, such as creating a -project, defining sample models, adding experimental data, etc. - -EasyDiffraction provides a **Python API** that allows you to perform these steps -programmatically in a certain linear order. This is especially useful for users -who prefer to work in a script or Jupyter Notebook environment. The API is -designed to be intuitive and easy to use, allowing you to focus on the analysis -rather than low-level implementation details. - -Because this workflow is an important part of the EasyDiffraction package, it is -described in detail in the separate -[Analysis Workflow](analysis-workflow/index.md) section of the documentation. diff --git a/pixi.lock b/pixi.lock index c8913145..5195fec7 100644 --- a/pixi.lock +++ b/pixi.lock @@ -5,142 +5,163 @@ environments: - url: https://conda.anaconda.org/conda-forge/ indexes: - https://pypi.org/simple + options: + pypi-prerelease-mode: if-necessary-or-explicit packages: linux-64: - - conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_8.conda + - 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/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/noarch/ca-certificates-2026.1.4-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.2.25-hbd8a1cb_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/gsl-2.8-hbf7d49c_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/icu-75.1-he02047a_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45-default_hbd61a6d_105.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libabseil-20250512.1-cxx17_hba17884_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libblas-3.11.0-5_h4a7cf45_openblas.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/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/libblas-3.11.0-6_h4a7cf45_openblas.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/libcblas-3.11.0-5_h0358290_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.11.0-6_h0358290_openblas.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.7.3-hecca717_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h9ec8514_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-he0feb66_16.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.2.0-h69a702a_16.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.2.0-h69a702a_16.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.2.0-h68bc16d_16.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-he0feb66_16.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb9d3cd8_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libnghttp2-1.67.0-had1ee68_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.30-pthreads_h94d23a6_4.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.51.1-h0c1763c_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h934c35e_16.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-15.2.0-hdf11a46_16.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.4-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/libgfortran-15.2.0-h69a702a_18.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.2.0-h68bc16d_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.2-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/libopenblas-0.3.32-pthreads_h94d23a6_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.52.0-hf4e2dac_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.41.3-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.1-hb9d3cd8_2.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/ncurses-6.5-h2d0b736_3.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/nodejs-25.2.1-he2c55a7_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.0-h26f9b46_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.13.11-hc97d973_100_cp313.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.1-h35e630c_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.13.12-hc97d973_100_cp313.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.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/tk-8.6.13-noxft_ha0e22de_103.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/noarch/tzdata-2025c-hc9c84f9_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda - pypi: https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7c/24/75d274228acf35ceeb2850b8ce04de9dd7355ff7a0b49d607ee60c29c518/aiohttp-3.13.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_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/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.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/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/09/52/94108adfdd6e2ddf58be64f959a0b9c7d4ef2fa71086c38356d22dc501ea/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/ed/c9/d7977eaacb9df673210491da99e6a247e93df98c715fc43fd136ce1d3d33/arrow-1.4.0-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/6c/25/4f103d1bedb3593718713b3f743df7b3ff3fc68d36d6666c30265ef59c8a/ase-3.28.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/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/03/49/d10027df9fce941cb8184e78a02857af36360d33e1721df81c5ed2179a1a/async_lru-2.0.5-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e5/e2/c2e3abf398f80732e58b03be77bde9022550d221dd8781bf586bd4d97cc1/async_lru-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.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/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/eb/95/7118e935b0b0bd3f94dfec2d852fd4e4f4f9757bdb49850519acd245cd3a/backrefs-6.1-py313-none-any.whl + - pypi: https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c5/71/c754b1737ad99102e03fa3235acb6cb6d3ac9d6f596cbc3e5f236705abd8/backrefs-6.2-py313-none-any.whl - pypi: https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-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/cd/3a/577b549de0cc09d95f11087ee63c739bba856cd3952697eec4c4bb91350a/bleach-6.3.0-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/cb/8c/2b30c12155ad8de0cf641d76a8b396a16d2c36bc6d50b621a62b7c4567c1/build-1.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4a/57/3b7d4dd193ade4641c865bc2b93aeeb71162e81fc348b8dad020215601ed/build-1.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/43/8e/278ea79cf8ee0c395a5ad74625b3465c2fc234bb277f171dd59dd203820d/bumps-1.0.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_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/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/d1/3b/6103194ea934f1c3a4ea080905c8849f71e83de455c16cb625d25f49b779/chardet-7.4.0.post1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/2b/58/a199d245894b12db0b957d627516c78e055adc3a0d978bc7f65ddaf7c399/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.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/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-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/12/da/91a52516e9d5aea87d32d1523f9cdcf7a35a3b298e6be05d6509ba3cfab2/coverage-7.13.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/df/91/4a151c94320458895049a3e23b7b2cfc08953c60b14892de837e8eb51d0a/copier-9.14.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ac/68/1666e3a4462f8202d836920114fa7a5ee9275d1fa45366d336c551a162dd/coverage-7.13.5-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl - pypi: https://files.pythonhosted.org/packages/52/e8/c14cc8af8cd38e86887053843382629bd8ebd117f83f15eb1194d65a2c9d/cryspy-0.10.0-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/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-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/25/3e/e27078370414ef35fafad2c06d182110073daaeb5d3bf734b0b1eeefe452/debugpy-1.8.19-py2.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/e0/c3/7f67dea8ccf8fdcb9c99033bbe3e90b9e7395415843accb81428c441be2d/debugpy-1.8.20-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/68/49/869b49db0bad6fba7c7471d612c356e745929d14e0de63acbc53e88f2a50/dfo_ls-1.6-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/ee/af/3b86dbd18d8dab5646f5b7c7db5bd9c43108e093864032aabd35d41b127d/diffpy_pdffit2-1.5.2.tar.gz - pypi: https://files.pythonhosted.org/packages/8e/52/39914bf42bb01901c91781def35c1aeffa431a59299e9748c0cfae3e5493/diffpy_structure-3.3.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/79/c8/f0f4ab7fd08950d5358579364a0a9b9198bf03e179a4fcceae4b353be32e/diffpy_utils-3.6.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/50/3d/9373ad9c56321fdab5b41197068e1d8c25883b3fea29dd361f9b55116869/dill-0.4.0-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/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/dc/b4/a7ec1eaee86761a9dbfd339732b4706db3c6b65e970c12f0f56cfcce3dcf/docformatter-1.7.7-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/82/ee/c9ca3f81d69ec6c99ca41e10432a58c60fef3a6e63cce6df7e796f90fe22/easydiffraction-0.10.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/50/98b146aea0f1cd7531d25f12bea69fa9ce8d1662124f93fb30dc4511b65e/docstring_parser_fork-0.0.14-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/87/10/2c7edbf230e5c507d38367af498fa94258ed97205d9b4b6f63a921fe9c49/dunamai-1.26.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/55/cd/d729a1bb63fa95387228cc508552dea4685ea0116e484e73238db10f9521/essdiffraction-26.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f1/bb/66c80d7f801b191f7b3ee6149a39be9d1a8a81c233e20adaf796d171f93a/essreduce-26.4.0-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/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cb/a8/20d0723294217e47de6d9e2e40fd4a9d2f7c4b6ef974babd482a59743694/fastjsonschema-2.21.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/9a/30/ab407e2ec752aa541704ed8f93c11e2a5d92c168b8a755d818b74a3c5c2d/filelock-3.20.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a3/4b/d67eedaed19def5967fade3297fed8161b25ba94699efc124b14fb68cdbc/fonttools-4.61.1-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/a4/a5/842ae8f0c08b61d6484b52f99a03510a3a72d23141942d216ebe81fefbce/filelock-3.25.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/33/b2/986d1220f6ee931e338d272bc1f3ec02cfe5f9b5fad84e95afdad57f1ebc/format_docstring-0.2.7-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cf/58/8acf1b3e91c58313ce5cb67df61001fc9dcd21be4fadb76c1a2d540e09ed/fqdn-1.5.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/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/ed/34/a6536afaeee07fa351e2087bf7b5b1522aa703bc1f6e29d53c27a722ac33/gemmi-0.7.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/e6/ab/fb21f4c939bb440104cc2b396d3be1d9b7a9fd3c6c2a53d98c45b3d7c954/fsspec-2026.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d5/08/c2409cb01d5368dcfedcbaffa7d044cc8957d57a9d0855244a5eb4709d30/funcy-2.0-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/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/fd/8e/424b8c6e78bd9837d14ff7df01a9829fc883ba2ab4ea787d4f848435f23f/greenlet-3.3.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/9c/83/3b1d03d36f224edded98e9affd0467630fc09d766c0e56fb1498cbb04a9b/griffe-1.15.0-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/6a/09/e21df6aef1e1ffc0c816f0522ddc3f6dcded766c3261813131c78a704470/gitpython-3.1.46-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/7a/34/259b28ea7a2a0c904b11cd36c79b8cef8019b26ee5dbe24e73b469dea347/greenlet-3.3.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/4b/4c/cc8c68196db727cfc1432f2ad5de50aa6707e630d44b2e6361dc06d8f134/griffelib-2.0.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d9/69/4402ea66272dacc10b298cca18ed73e1c0791ff2ae9ed218d3859f9698ac/h5py-3.15.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.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/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/0f/1c/e5fd8f973d4f375adb21565739498e2e9a1e54c858a97b9a8ccfdc81da9b/identify-2.6.15-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/46/33/92ef41c6fad0233e41d3d84ba8e8ad18d1780f1e5d99b3c683e6d7f98b63/identify-2.6.18-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a4/ed/1f1afb2e9e7f38a545d628f864d562a5ae64fe6f7a10e28ffb9b185b4e89/importlib_resources-6.5.2-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/12/c9/6869a1dcf4aaf309b9543ec070be3ec3adebee7c9bec9af8c230494134b9/interrogate-1.7.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f6/d8/502954a4ec0efcf264f99b65b41c3c54e65a647d9f0d6f62cd02227d242c/ipykernel-6.31.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/86/92/162cfaee4ccf370465c5af1ce36a9eacec1becb552f2033bb3584e6f640a/ipython-9.9.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/82/b9/e73d5d9f405cba7706c539aa8b311b49d4c2f3d698d9c12f815231169c71/ipykernel-7.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b2/90/45c72becc57158facc6a6404f663b77bbcea2519ca57f760e2879ae1315d/ipython-9.11.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-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/7b/55/e5326141505c5d5e34c5e0935d2908a74e4561eca44108fbfb9c13d2911a/isoduration-20.11.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/72/b9/313e8f2f2e9517ae050a692ae7b3e4b3f17cc5e6dfea0db51fe14e586580/jinja2_ansible_filters-1.3.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d7/9e/038522f50ceb7e74f1f991bf1b699f24b0c2bbe7c390dd36ad69f4582258/json5-0.13.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/71/92/5e77f98553e9e75130c78900d000368476aed74276eb8ae8796f65f00918/jsonpointer-3.0.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9e/6a/a83720e953b1682d2d109d3c2dbb0bc9bf28cc1cbc205be4ef4be5da709d/jsonpointer-3.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/bb/f5/fddaec430367be9d62a7ed125530e133bfd4a1c0350fe221149ee0f2b526/jupyter_client-8.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2d/0b/ceb7694d864abc0a047649aec263878acb9f792e1fec3e676f22dc9015e3/jupyter_client-8.8.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/76/c0/8cc378aad5ccfcc37adeda27d3864f20ea808ad5b63572f5b986a42a649c/jupyter_dark_detect-0.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e2/48/577993f1f99c552f18a0428731a755e06171f9902fa118c379eb7c04ea22/jupyter_events-0.12.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1a/60/1f6cee0c46263de1173894f0fafcb3475ded276c472c14d25e0280c18d6d/jupyter_lsp-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f4/a4/61adb19f3c74b0dc0e411de4f06ebef564b1f179928f9dffcbd4b378f2ef/jupyter_notebook_parser-0.1.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/92/80/a24767e6ca280f5a49525d987bf3e4d7552bf67c8be07e8ccf20271f8568/jupyter_server-2.17.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/07/2d/2b32cdbe8d2a602f697a649798554e4f072115438e92249624e532e8aca6/jupyter_server_terminals-0.5.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/af/c3/acced767eecc11a70c65c45295db5396c4f0c1937874937d5a76d7b177b6/jupyterlab-4.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d1/2d/6674563f71c6320841fc300911a55143925112a72a883e2ca71fba4c618d/jupyter_server_terminals-0.5.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e1/1b/dad6fdcc658ed7af26fdf3841e7394072c9549a8b896c381ab49dd11e2d9/jupyterlab-4.5.6-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b1/dd/ead9d8ea85bf202d90cc513b533f9c363121c7792674f78e0d8a854b63b4/jupyterlab_pygments-0.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e0/07/a000fe835f76b7e1143242ab1122e6362ef1c03f23f83a045c38859c2ae0/jupyterlab_server-2.28.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f7/5a/488f492b49b712ca38326e12c78d0d48ab6be682a2989ce533f0f2c4dfd2/jupyterquiz-2.9.6.2-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/bd/0d/2d240e7098e0cafba4d25e9530e7596b1bb1bd4476e41b10346bcaaa36d6/jupytext-1.18.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e9/e9/f218a2cb3a9ffbe324ca29a9e399fa2d2866d7f348ec3a88df87fc248fc5/kiwisolver-1.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/ab/b5/36c712098e6191d1b4e349304ef73a8d06aed77e56ceaac8c0a306c7bda1/jupyterlab_widgets-3.0.16-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/52/bc858b1665d0dec3a2511f4e6f5c18ea85c0977563d624d597c95d6d0fd7/jupyterquiz-2.9.6.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/16/5a/736dd2f4535dbf3bf26523f9158c011389ef88dd06ec2eef67fd744f1c7b/jupytext-1.19.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2b/0a/7b98e1e119878a27ba8618ca1e18b14f992ff1eda40f47bccccf4de44121/kiwisolver-1.5.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - pypi: https://files.pythonhosted.org/packages/82/3d/14ce75ef66813643812f3093ab17e46d3a206942ce7376d31ec2d36229e7/lark-1.3.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/38/7e/7b91c89a4cf0f543a83be978657afb20c86af6d725253e319589dcc4ce52/lmfit-1.3.4-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/d2/f0/834e479e47e499b6478e807fb57b31cc2db696c4db30557bb6f5aea4a90b/mando-0.7.1-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/70/81/54e3ce63502cd085a0c556652a4e1b919c45a446bd1e5300e10c44c8c521/markdown-3.10-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/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/75/97/a471f1c3eb1fd6f6c24a31a5858f443891d5127e63a7788678d14e249aea/matplotlib-3.10.8-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl @@ -148,260 +169,303 @@ environments: - 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/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/fd/1a/31b7cd6e4e7a02df4e076162e9783620777592bea9e4bb036389389af99d/mike-2.1.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/48/f7/10f5e101db25741b91e4f4792c5d97b4fa834ead5cf509ae91097d939424/mike-2.1.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9b/f7/4a5e785ec9fbd65146a27b6b70b6cdc161a66f2024e4b04ac06a67f5578b/mistune-3.2.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/71/26/4d39d52ea2219604053a4d05b98e90d6a335511cc01806436ec4886b1028/mkdocs_autorefs-1.2.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/9f/d4/029f984e8d3f3b6b726bd33cafc473b75e9e44c0f7e80a5b29abc466bdea/mkdocs_get_deps-0.2.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/08/37/5f1fd5c3f6954b3256f8126275e62af493b96fb6aef6c0dbc4ee326032ad/mkdocs_jupyter-0.25.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/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/93/89/eb601278b12c471235860992f5973cf3c55ca3f77d1d6127389eb045a021/mkdocs_jupyter-0.26.1-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/3e/32/ed071cb721aca8c227718cffcf7bd539620e9799bbf2619e90c757bfd030/mkdocs_material-9.7.1-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/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.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/cd/10/4c27c3063c2b3681a4b7942f8dbdeb4fa34fecb2c19b594e7345ebf4f86f/mkdocstrings-0.27.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/51/23/d02d86553327296c3bf369d444194ea83410cce8f0e690565264f37f3261/mkdocstrings_python-1.13.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/04/41/1cf02e3df279d2dd846a1bf235a928254eba9006dd22b4a14caa71aed0f7/mkdocstrings-1.0.3-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/5b/69/93b34728cc386efdde0c342f8c680b9187dea7beb7adaf6b58a0713be101/mpld3-0.5.12-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/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-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/28/83/36557b04cfdc317ed8a525c4993b23e43a8fbcddaddd78619112ca07138c/msgspec-0.20.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/c6/2d/f0b184fa88d6630aa267680bdb8623fb69cb0d024b8c6f0d23f9a0f406d3/multidict-6.7.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/3d/2e/cf2ffeb386ac3763526151163ad7da9f1b586aac96d2b4f7de1eaebf0c61/narwhals-2.15.0-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/3f/c3/06490e98393dcb4d6ce2bf331a39335375c300afaef526897881fbeae6ab/narwhals-2.18.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/83/a0/5b0c2f11142ed1dddec842457d3f65eaf71a0080894eb6f018755b319c3a/nbclient-0.10.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/cc/9a/cd673b2f773a12c992f41309ef81b99da1690426bd2f96957a7ade0d3ed7/nbconvert-7.16.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0d/4b/8d5f796a792f8a25f6925a96032f098789f448571eb92011df1ae59e8ea8/nbconvert-7.17.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a9/82/0340caa499416c78e5d8f5f05947ae4bc3cba53c9f038ab6e9ed964e22f1/nbformat-5.10.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/eb/be/b257e12f9710819fde40adc972578bee6b72c5992da1bc8369bef2597756/nbmake-1.5.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/28/88/4789719fbbe166d12d345b3ac66b96105f10001b16e00a9765ba29261a21/nbqa-1.9.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/12/e5/838eb1004fb9b6f0383c47c0d902eb698fda052e8e64ca48c70a2144a48c/nbstripout-0.8.2-py2.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/fc/39/dfdef4cfbfcf7c80cd144b1b2e262c2e785f18b152eeebe14ecd70ee7455/ncrystal-4.2.12-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e3/8b/1f02771d91ceafec996cef7f92f6a24010fedc47fd9404f8e11772d8501c/ncrystal_core-4.2.12-py3-none-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/78/98/20fdd6825d1876fc54bb098509e736073d20f569b033f2f23bf74aa1df9c/ncrystal_python-4.2.12-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.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/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f9/33/bd5b9137445ea4b680023eb0469b2bb969d61303dedb2aac6560ff3d14a1/notebook_shim-0.2.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f5/10/ca162f45a102738958dcec8023062dad0cbc17d1ab99d68c4e4a6c45fb2b/numpy-2.3.5-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7e/eb/7daecbea84ec935b7fc732e18f532073064a3816f0932a40a17f3349185f/numpy-2.4.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/15/07/284f757f63f8a8d69ed4472bfd85122bd086e637bf4ed09de572d575a693/pandas-2.3.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/f2/85/ab6d04733a7d6ff32bfc8382bf1b07078228f5d6ebec5266b91bfc5c4ff7/pandas-3.0.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/ef/af/4fbc8cab944db5d21b7e2a5b8e9211a03a79852b1157e2c102fcc61ac440/pandocfilters-1.5.1-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/16/32/f8e3c85d1d5250232a5d3477a2a28cc291968ff175caeadaf3cc19ce0e4a/parso-0.8.5-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/05/bb/39e6768529454cc2b57e1e2fa0a0a18ff64397a16303270e215a3e03285f/pathspec-1.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b6/61/fae042894f4296ec49e3f193aff5d7c18440da9e48102c3315e1bc4519a7/parso-0.8.6-py2.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/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/01/9a/632e58ec89a32738cabfd9ec418f0e9898a2b4719afc581f07c04a05e3c9/pillow-12.1.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/44/3c/d717024885424591d5376220b5e836c2d5293ce2011523c9de23ff7bf068/pip-25.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/71/24/538bff45bde96535d7d998c6fed1a751c75ac7c53c37c90dc2601b243893/pillow-12.1.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/c4/36/ce5f75aa7c736a663a901766edc3580098c7ea3959a0e878363a54a3714e/pixi_kernel-0.7.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e7/c3/3031c931098de393393e1f93a38dc9ed6805d86bb801acc3cf2d5bd1e6b7/plotly-6.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/63/d7/97f7e3a6abb67d8080dd406fd4df842c2be0efaf712d1c899c32a075027c/platformdirs-4.9.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/31/8b/9e8baf7dacac8d0c174925c38ff43c6d94bc9abb35503f67762caccb6869/plopp-26.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/52/d2/c6e44dba74f17c6216ce1b56044a9b93a929f1c2d5bdaff892512b260f5e/plotly-6.6.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/79/ad/45312df6b63ba64ea35b8d8f5f0c577aac16e6b416eafe8e1cb34e03f9a7/plumbum-1.10.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/a8/87/77cc11c7a9ea9fd05503def69e3d18605852cd0d4b0d3b8f15bbeb3ef1d1/pooch-1.8.2-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/5d/19/fd3ef348460c80af7bb4669ea7926651d1f95c23ff2df18b9d24bab4f3fa/pre_commit-4.5.1-py2.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/b8/db/14bafcb4af2139e046d03fd00dea7873e48eafe18b7d2797e73d6681f210/prometheus_client-0.23.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/74/c3/24a2f845e3917201628ecaba4f18bab4d18a337834c1df2a159ee9d22a42/prometheus_client-0.24.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-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/29/f8/40e01c350ad9a2b3cb4e6adbcc8a83b17ee50dd5792102b6142385937db5/psutil-7.2.1-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-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/0b/a6/e2e8535d8146bce05de6e0ecf1099a7e2887d840ae2a7b3a09385543fd02/py3dmol-2.5.3-py2.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/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/78/3c/2a612b95ddbb9a6bdcb47b7a93c4884f74c6ff22356b2f7b213b16e65c35/pycifstar-0.3.0.tar.gz - pypi: https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/87/6f/cc2b231dc78d8c3aaa674a676db190b8f8071c50134af8f8cf39b9b8e8df/pydoclint-0.8.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ea/10/47caf89cbb52e5bb764696fd52a8c591a2f0e851a93270c05a17f36000b5/pymdown_extensions-10.20-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/8b/40/2614036cdd416452f5bf98ec037f38a1afb17f327cb8e6b652d4729e0af8/pyparsing-3.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6f/2c/5b079febdc65e1c3fb2729bf958d18b45be7113828528e8a0b5850dd819a/pymdown_extensions-10.21-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.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/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/50/74/c655a6eda0fd188d490c14142a0f0380655ac7099604e1fbf8fa1a97f0a1/python_engineio-4.13.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/67/0f/019d3949a40280f6193b62bc010177d4ce702d0fce424322286488569cd3/python_discovery-1.2.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/aa/54/0cce26da03a981f949bb8449c9778537f75f5917c172e1d2992ff25cb57d/python_engineio-4.13.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/51/e5/fecf13f06e5e5f67e8837d777d1bc43fac0ed2b77a676804df5c34744727/python_json_logger-4.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/28/d2/2ccc2b69a187b80fda3152745670cfba936704f296a9fa54c6c8ac694d12/python_socketio-5.16.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.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/d8/8b/e2bbeb42068f0c48899e8eddd34902afc0f7429d4d2a152d2dc2670dc661/pythreejs-2.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.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/f8/9b/c108cdb55560eaf253f0cbdb61b29971e9fb34d9c3499b0e96e4e60ed8a5/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/3c/26/1062c7ec1b053db9e499b4d2d5bc231743201b74051c973dadeac80a8f43/questionary-2.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/93/f7/d00d9b4a0313a6be3a3e0818e6375e15da6d7076f4ae47d1324e7ca986a1/radon-6.0.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/56/5d/c814546c2333ceea4ba42262d8c4d55763003e767fa169adc693bd524478/requests-2.33.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/57/4d/a7545bf6c62b0dbe5795f22ea9e88cc070fdced5c34663ebc5bed2f610c0/returns-0.26.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9e/51/17023c0f8f1869d8806b979a2bffa3f861f26a3f1a66b094288323fba52f/rfc3986_validator-0.1.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7e/71/44ce230e1b7fadd372515a97e32a83011f906ddded8d03e3c6aafbdedbb7/rfc3987_syntax-1.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/b3/19/9e050c0dca8aba824d67cc0db69fb459c28d8cd3f6855b1405b3f29cc91d/ruff-0.14.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/21/f6/4bfb5695d8941e5c570a04d9fcd0d36bce7511b7d78e6e75c8f9791f82d0/scipy-1.16.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/1b/5a/f2f2e5eda25579f754acd83399c522ee03d6acbe001dfe53c8a1ec928b44/send2trash-2.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f8/22/d7f2fabdba4fae9f3b570e5605d5eb4500dcb7b770d3217dca4428484b17/ruff-0.15.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/af/46/661159ad844034ba8b3f4e0516215c41e4ee17db4213d13a82227670764f/sciline-25.11.1-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/fb/46/e50b38629e9e3f4a1dd55fb36d8b8abd1d59768c31151c1c8ed696f7b865/scippneutron-26.3.0-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/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/1c/78/504fdd027da3b84ff1aecd9f6957e65f35134534ccc6da8628eb71e76d3f/send2trash-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9d/76/f789f7a86709c6b087c5a2f52f911838cad707cc613162401badc665acfe/setuptools-82.0.1-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/52/59/0782e51887ac6b07ffd1570e0364cf901ebc36345fea669969d2084baebb/simple_websocket-1.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/48/f3/b67d6ea49ca9154453b6d70b34ea22f3996b9fa55da105a79d8732227adc/soupsieve-2.8.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/0e/50/80a8d080ac7d3d321e5e5d420c9a522b0aa770ec7013ea91f9a8b7d36e4a/sqlalchemy-2.0.45-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/c1/d4/59e74daffcb57a07668852eeeb6035af9f32cbfd7a1d2511f17d2fe6a738/smmap-5.0.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-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/53/01/1c0485ae02e645bc517bf5d5a6ca674f62c97e247890b954cbfe85c64dae/spglib-2.6.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/fe/88/cb59509e4668d8001818d7355d9995be90c321313078c912420603a7cb95/sqlalchemy-2.0.48-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-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/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-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/6a/9e/2064975477fdc887e47ad42157e214526dcad8f317a948dee17e1659a62f/terminado-0.18.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e6/34/ebdc18bae6aa14fbee1a08b63c015c72b64868ff7dae68808ab500c492e2/tinycss2-1.4.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/33/f0/3fe8c6e69135a845f4106f2ff8b6805638d4e85c264e70114e8126689587/tokenize_rt-6.2.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d5/f4/0fbd014909748706c01d16824eadb0307115f9562a15cbb012cd9b3512c5/tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/50/d4/e51d52047e7eb9a582da59f32125d17c0482d065afd5d3bc435ff2120dc5/tornado-6.5.4-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b2/04/7b5705d5b3c0fab088f434f9c83edac1573830ca49ccf29fb83bf7178eec/tornado-6.5.5-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl - pypi: https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/4f/7e/bc19996fa86cad8801e8ffe6f1bba5836ca0160df76d0410d27432193712/trove_classifiers-2025.12.1.14-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/1b/a9/e3aee762739c1d7528da1c3e06d518503f8b6c439c35549b53735ba52ead/typeguard-4.4.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a0/1d/d9257dd49ff2ca23ea5f132edf1281a0c4f9de8a762b9ae399b670a59235/typer-0.21.1-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/bb/4a/2e5583e544bc437d5e8e54b47db87430df9031b29b48d17f26d129fa60c0/trove_classifiers-2026.1.14.14-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/4a/91/48db081e7a63bb37284f9fbcefda7c44c277b18b0e13fbc36ea2335b71e6/typer-0.24.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8f/5e/f1e1dd319e35e962a4e00b33150a8868b6329cc1d19fd533436ba5488f09/uncertainties-3.2.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f7/46/e7cea8159199096e1df52da20a57a6665da80c37fb8aeb848a3e47442c32/untokenize-0.1.1.tar.gz - pypi: https://files.pythonhosted.org/packages/e7/00/3fca040d7cf8a32776d3d81a00c8ee7457e00f80c649f1e4a863c8321ae9/uri_template-1.3.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/6d/b9/4095b668ea3678bf6a0af005527f39de12fb026516fb3df17495a733b7f8/urllib3-2.6.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/40/15/63fb7a6908db2f03716c4a50aea7e27a7440fe6a09854282c401139afaf7/uv-0.9.22-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/a4/39/6983dd79f01aaa4c75d9ffa550fa393f0c4c28f7ccd6956e4188c62cefbc/validate_pyproject-0.24.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/ee/e9c95cda829131f71a8dff5ce0406059fd16e591c074414e31ada19ba7c3/validate_pyproject-0.25-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/bc/4a/c6fd02a642bbe4e9f25cdd3714a328e3fc3eb6bd7b5e96f1a2285bd928b9/varname-0.15.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1c/59/964ecb8008722d27d8a835baea81f56a91cea8e097b3be992bc6ccde6367/versioningit-3.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a4/ce/3b6fee91c85626eaf769d617f1be9d2e15c1cca027bbdeb2e0d751469355/verspec-0.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/79/0c/c05523fa3181fdf0c9c52a6ba91a23fbf3246cc095f26f6516f9c60e6771/virtualenv-20.35.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c6/59/7d02447a55b2e55755011a647479041bc92a82e143f96a8195cb33bd0a1c/virtualenv-21.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/af/b5/123f13c975e9f27ab9c0770f514345bd406d0e8d3b7a0723af9d43f710af/wcwidth-0.2.14-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/68/5a/199c59e0a824a3db2b89c5d2dade7ab5f9624dbf6448dc291b46d5ec94d3/wcwidth-0.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e2/cc/e097523dd85c9cf5d354f78310927f1656c422bd7b2613b2db3e3f9a0f2c/webcolors-25.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/34/db/b10e48aa8fff7407e67470363eac595018441cf32d5e1001567a7aeba5d2/websocket_client-1.9.0-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/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-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/11/c9/cd8538dc2e7727095e0c1d867bad1e40c98f37763e6d995c1939f5fdc7b1/yarl-1.22.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.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: ./ osx-64: - conda: https://conda.anaconda.org/conda-forge/osx-64/_openmp_mutex-4.5-7_kmp_llvm.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/bzip2-1.0.8-h500dc9f_8.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/bzip2-1.0.8-h500dc9f_9.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/c-ares-1.34.6-hb5e19a0_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.1.4-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.2.25-hbd8a1cb_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/gsl-2.8-hc707ee6_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/icu-75.1-h120a0e1_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libabseil-20250512.1-cxx17_hfc00f1c_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libblas-3.11.0-5_he492b99_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/icu-78.3-h25d91c4_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libabseil-20260107.1-cxx17_h7ed6875_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libblas-3.11.0-6_he492b99_openblas.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/libbrotlicommon-1.2.0-h8616949_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/libbrotlidec-1.2.0-h8616949_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/libbrotlienc-1.2.0-h8616949_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libcblas-3.11.0-5_h9b27e0a_openblas.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libcxx-21.1.8-h3d58e20_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libcblas-3.11.0-6_h9b27e0a_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libcxx-22.1.2-h19cb2f5_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/libev-4.33-h10d778d_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libexpat-2.7.3-heffb93a_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libffi-3.5.2-h750e83c_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libgcc-15.2.0-h08519bb_15.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libgfortran-15.2.0-h7e5c614_15.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libgfortran5-15.2.0-hd16e46c_15.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/liblzma-5.8.1-hd471939_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libmpdec-4.0.0-h6e16a3a_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libnghttp2-1.67.0-h3338091_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libopenblas-0.3.30-openmp_h6006d49_4.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libsqlite-3.51.1-hb99441e_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libexpat-2.7.4-h991f03e_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libffi-3.5.2-hd1f9c09_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libgcc-15.2.0-h08519bb_18.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libgfortran-15.2.0-h7e5c614_18.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libgfortran5-15.2.0-hd16e46c_18.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/liblzma-5.8.2-h11316ed_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libmpdec-4.0.0-hf3981d6_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libnghttp2-1.68.1-h70048d4_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libopenblas-0.3.32-openmp_h9e49c7b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libsqlite-3.52.0-h77d7759_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/libuv-1.51.0-h58003a5_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libzlib-1.3.1-hd23fc13_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/llvm-openmp-21.1.8-h472b3d1_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libzlib-1.3.2-hbb4bfdb_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/llvm-openmp-22.1.1-h0d3cbff_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/ncurses-6.5-h0622a9a_3.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/nodejs-25.2.1-h5523da6_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/openssl-3.6.0-h230baf5_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/python-3.13.11-h17c18a5_100_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/nodejs-25.8.2-hf3170e9_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/openssl-3.6.1-hb6871ef_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/python-3.13.12-h894a449_100_cp313.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/readline-8.3-h68b038d_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-hf689a15_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-h7142dee_3.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/zstd-1.5.7-h3eecb57_6.conda - pypi: https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/32/08/de43984c74ed1fca5c014808963cc83cb00d7bb06af228f132d33862ca76/aiohttp-3.13.3-cp313-cp313-macosx_10_13_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/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.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/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/81/29/5ecc3a15d5a33e31b26c11426c45c501e439cb865d0bff96315d86443b78/appnope-0.1.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/0a/08/a9bebdb2e0e602dde230bdde8021b29f71f7841bd54801bcfd514acb5dcf/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_10_9_x86_64.whl - pypi: https://files.pythonhosted.org/packages/ed/c9/d7977eaacb9df673210491da99e6a247e93df98c715fc43fd136ce1d3d33/arrow-1.4.0-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/6c/25/4f103d1bedb3593718713b3f743df7b3ff3fc68d36d6666c30265ef59c8a/ase-3.28.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/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/03/49/d10027df9fce941cb8184e78a02857af36360d33e1721df81c5ed2179a1a/async_lru-2.0.5-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e5/e2/c2e3abf398f80732e58b03be77bde9022550d221dd8781bf586bd4d97cc1/async_lru-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.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/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/eb/95/7118e935b0b0bd3f94dfec2d852fd4e4f4f9757bdb49850519acd245cd3a/backrefs-6.1-py313-none-any.whl + - pypi: https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c5/71/c754b1737ad99102e03fa3235acb6cb6d3ac9d6f596cbc3e5f236705abd8/backrefs-6.2-py313-none-any.whl - pypi: https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-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/cd/3a/577b549de0cc09d95f11087ee63c739bba856cd3952697eec4c4bb91350a/bleach-6.3.0-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/cb/8c/2b30c12155ad8de0cf641d76a8b396a16d2c36bc6d50b621a62b7c4567c1/build-1.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4a/57/3b7d4dd193ade4641c865bc2b93aeeb71162e81fc348b8dad020215601ed/build-1.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/43/8e/278ea79cf8ee0c395a5ad74625b3465c2fc234bb277f171dd59dd203820d/bumps-1.0.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_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/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl + - pypi: https://files.pythonhosted.org/packages/e9/32/83a15c6077e7f240834ffd9ed78ef12f20f6e1924d7d7986d33f3d2af905/chardet-7.4.0.post1-cp313-cp313-macosx_10_13_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/1e/1d/4fdabeef4e231153b6ed7567602f3b68265ec4e5b76d6024cf647d43d981/charset_normalizer-3.4.6-cp313-cp313-macosx_10_13_universal2.whl - pypi: https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.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/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/68/35/0167aad910bbdb9599272bd96d01a9ec6852f36b9455cf2ca67bd4cc2d23/contourpy-1.3.3-cp313-cp313-macosx_10_13_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/a3/a4/e98e689347a1ff1a7f67932ab535cef82eb5e78f32a9e4132e114bbb3a0a/coverage-7.13.1-cp313-cp313-macosx_10_13_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/df/91/4a151c94320458895049a3e23b7b2cfc08953c60b14892de837e8eb51d0a/copier-9.14.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/74/8c/74fedc9663dcf168b0a059d4ea756ecae4da77a489048f94b5f512a8d0b3/coverage-7.13.5-cp313-cp313-macosx_10_13_x86_64.whl - pypi: https://files.pythonhosted.org/packages/52/e8/c14cc8af8cd38e86887053843382629bd8ebd117f83f15eb1194d65a2c9d/cryspy-0.10.0-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/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-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/25/3e/e27078370414ef35fafad2c06d182110073daaeb5d3bf734b0b1eeefe452/debugpy-1.8.19-py2.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/e0/c3/7f67dea8ccf8fdcb9c99033bbe3e90b9e7395415843accb81428c441be2d/debugpy-1.8.20-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/68/49/869b49db0bad6fba7c7471d612c356e745929d14e0de63acbc53e88f2a50/dfo_ls-1.6-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/19/eb/bb3ff420acdaf9bcaf94c510f42df11974bc3fc475ef50d619366f33fda3/diffpy_pdffit2-1.5.2-cp313-cp313-macosx_10_13_x86_64.whl - pypi: https://files.pythonhosted.org/packages/8e/52/39914bf42bb01901c91781def35c1aeffa431a59299e9748c0cfae3e5493/diffpy_structure-3.3.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/79/c8/f0f4ab7fd08950d5358579364a0a9b9198bf03e179a4fcceae4b353be32e/diffpy_utils-3.6.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/50/3d/9373ad9c56321fdab5b41197068e1d8c25883b3fea29dd361f9b55116869/dill-0.4.0-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/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/dc/b4/a7ec1eaee86761a9dbfd339732b4706db3c6b65e970c12f0f56cfcce3dcf/docformatter-1.7.7-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/82/ee/c9ca3f81d69ec6c99ca41e10432a58c60fef3a6e63cce6df7e796f90fe22/easydiffraction-0.10.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/50/98b146aea0f1cd7531d25f12bea69fa9ce8d1662124f93fb30dc4511b65e/docstring_parser_fork-0.0.14-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/87/10/2c7edbf230e5c507d38367af498fa94258ed97205d9b4b6f63a921fe9c49/dunamai-1.26.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/55/cd/d729a1bb63fa95387228cc508552dea4685ea0116e484e73238db10f9521/essdiffraction-26.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f1/bb/66c80d7f801b191f7b3ee6149a39be9d1a8a81c233e20adaf796d171f93a/essreduce-26.4.0-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/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cb/a8/20d0723294217e47de6d9e2e40fd4a9d2f7c4b6ef974babd482a59743694/fastjsonschema-2.21.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/9a/30/ab407e2ec752aa541704ed8f93c11e2a5d92c168b8a755d818b74a3c5c2d/filelock-3.20.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/5a/ca/468c9a8446a2103ae645d14fee3f610567b7042aba85031c1c65e3ef7471/fonttools-4.61.1-cp313-cp313-macosx_10_13_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/a4/a5/842ae8f0c08b61d6484b52f99a03510a3a72d23141942d216ebe81fefbce/filelock-3.25.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/03/c5/0e3966edd5ec668d41dfe418787726752bc07e2f5fd8c8f208615e61fa89/fonttools-4.62.1-cp313-cp313-macosx_10_13_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/33/b2/986d1220f6ee931e338d272bc1f3ec02cfe5f9b5fad84e95afdad57f1ebc/format_docstring-0.2.7-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cf/58/8acf1b3e91c58313ce5cb67df61001fc9dcd21be4fadb76c1a2d540e09ed/fqdn-1.5.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/3e/e7/b88b72919c910d3233065680cbc74a2a9d00ed65a06b100751d5b78e08e1/gemmi-0.7.4-cp313-cp313-macosx_10_14_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/e6/ab/fb21f4c939bb440104cc2b396d3be1d9b7a9fd3c6c2a53d98c45b3d7c954/fsspec-2026.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d5/08/c2409cb01d5368dcfedcbaffa7d044cc8957d57a9d0855244a5eb4709d30/funcy-2.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c4/80/fd758344a72ca7b5e1c5bbdc1d263f3b215d3897941b5f450380445ca0a9/gemmi-0.7.5-cp313-cp313-macosx_10_14_x86_64.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/02/2f/28592176381b9ab2cafa12829ba7b472d177f3acc35d8fbcf3673d966fff/greenlet-3.3.0-cp313-cp313-macosx_11_0_universal2.whl - - pypi: https://files.pythonhosted.org/packages/9c/83/3b1d03d36f224edded98e9affd0467630fc09d766c0e56fb1498cbb04a9b/griffe-1.15.0-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/6a/09/e21df6aef1e1ffc0c816f0522ddc3f6dcded766c3261813131c78a704470/gitpython-3.1.46-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/ac/48/f8b875fa7dea7dd9b33245e37f065af59df6a25af2f9561efa8d822fde51/greenlet-3.3.2-cp313-cp313-macosx_11_0_universal2.whl + - pypi: https://files.pythonhosted.org/packages/4b/4c/cc8c68196db727cfc1432f2ad5de50aa6707e630d44b2e6361dc06d8f134/griffelib-2.0.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/88/b3/40207e0192415cbff7ea1d37b9f24b33f6d38a5a2f5d18a678de78f967ae/h5py-3.15.1-cp313-cp313-macosx_10_13_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/0f/9e/6142ebfda0cb6e9349c091eae73c2e01a770b7659255248d637bec54a88b/h5py-3.16.0-cp313-cp313-macosx_10_13_x86_64.whl - pypi: https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/0f/1c/e5fd8f973d4f375adb21565739498e2e9a1e54c858a97b9a8ccfdc81da9b/identify-2.6.15-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/46/33/92ef41c6fad0233e41d3d84ba8e8ad18d1780f1e5d99b3c683e6d7f98b63/identify-2.6.18-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a4/ed/1f1afb2e9e7f38a545d628f864d562a5ae64fe6f7a10e28ffb9b185b4e89/importlib_resources-6.5.2-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/12/c9/6869a1dcf4aaf309b9543ec070be3ec3adebee7c9bec9af8c230494134b9/interrogate-1.7.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f6/d8/502954a4ec0efcf264f99b65b41c3c54e65a647d9f0d6f62cd02227d242c/ipykernel-6.31.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/86/92/162cfaee4ccf370465c5af1ce36a9eacec1becb552f2033bb3584e6f640a/ipython-9.9.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/82/b9/e73d5d9f405cba7706c539aa8b311b49d4c2f3d698d9c12f815231169c71/ipykernel-7.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b2/90/45c72becc57158facc6a6404f663b77bbcea2519ca57f760e2879ae1315d/ipython-9.11.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-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/7b/55/e5326141505c5d5e34c5e0935d2908a74e4561eca44108fbfb9c13d2911a/isoduration-20.11.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/72/b9/313e8f2f2e9517ae050a692ae7b3e4b3f17cc5e6dfea0db51fe14e586580/jinja2_ansible_filters-1.3.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d7/9e/038522f50ceb7e74f1f991bf1b699f24b0c2bbe7c390dd36ad69f4582258/json5-0.13.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/71/92/5e77f98553e9e75130c78900d000368476aed74276eb8ae8796f65f00918/jsonpointer-3.0.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9e/6a/a83720e953b1682d2d109d3c2dbb0bc9bf28cc1cbc205be4ef4be5da709d/jsonpointer-3.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/bb/f5/fddaec430367be9d62a7ed125530e133bfd4a1c0350fe221149ee0f2b526/jupyter_client-8.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2d/0b/ceb7694d864abc0a047649aec263878acb9f792e1fec3e676f22dc9015e3/jupyter_client-8.8.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/76/c0/8cc378aad5ccfcc37adeda27d3864f20ea808ad5b63572f5b986a42a649c/jupyter_dark_detect-0.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e2/48/577993f1f99c552f18a0428731a755e06171f9902fa118c379eb7c04ea22/jupyter_events-0.12.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1a/60/1f6cee0c46263de1173894f0fafcb3475ded276c472c14d25e0280c18d6d/jupyter_lsp-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f4/a4/61adb19f3c74b0dc0e411de4f06ebef564b1f179928f9dffcbd4b378f2ef/jupyter_notebook_parser-0.1.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/92/80/a24767e6ca280f5a49525d987bf3e4d7552bf67c8be07e8ccf20271f8568/jupyter_server-2.17.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/07/2d/2b32cdbe8d2a602f697a649798554e4f072115438e92249624e532e8aca6/jupyter_server_terminals-0.5.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/af/c3/acced767eecc11a70c65c45295db5396c4f0c1937874937d5a76d7b177b6/jupyterlab-4.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d1/2d/6674563f71c6320841fc300911a55143925112a72a883e2ca71fba4c618d/jupyter_server_terminals-0.5.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e1/1b/dad6fdcc658ed7af26fdf3841e7394072c9549a8b896c381ab49dd11e2d9/jupyterlab-4.5.6-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b1/dd/ead9d8ea85bf202d90cc513b533f9c363121c7792674f78e0d8a854b63b4/jupyterlab_pygments-0.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e0/07/a000fe835f76b7e1143242ab1122e6362ef1c03f23f83a045c38859c2ae0/jupyterlab_server-2.28.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f7/5a/488f492b49b712ca38326e12c78d0d48ab6be682a2989ce533f0f2c4dfd2/jupyterquiz-2.9.6.2-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/bd/0d/2d240e7098e0cafba4d25e9530e7596b1bb1bd4476e41b10346bcaaa36d6/jupytext-1.18.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ca/f0/f44f50c9f5b1a1860261092e3bc91ecdc9acda848a8b8c6abfda4a24dd5c/kiwisolver-1.4.9-cp313-cp313-macosx_10_13_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/ab/b5/36c712098e6191d1b4e349304ef73a8d06aed77e56ceaac8c0a306c7bda1/jupyterlab_widgets-3.0.16-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/52/bc858b1665d0dec3a2511f4e6f5c18ea85c0977563d624d597c95d6d0fd7/jupyterquiz-2.9.6.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/16/5a/736dd2f4535dbf3bf26523f9158c011389ef88dd06ec2eef67fd744f1c7b/jupytext-1.19.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ce/48/adbb40df306f587054a348831220812b9b1d787aff714cfbc8556e38fccd/kiwisolver-1.5.0-cp313-cp313-macosx_10_13_x86_64.whl - pypi: https://files.pythonhosted.org/packages/82/3d/14ce75ef66813643812f3093ab17e46d3a206942ce7376d31ec2d36229e7/lark-1.3.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/38/7e/7b91c89a4cf0f543a83be978657afb20c86af6d725253e319589dcc4ce52/lmfit-1.3.4-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/d2/f0/834e479e47e499b6478e807fb57b31cc2db696c4db30557bb6f5aea4a90b/mando-0.7.1-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/70/81/54e3ce63502cd085a0c556652a4e1b919c45a446bd1e5300e10c44c8c521/markdown-3.10-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/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl - pypi: https://files.pythonhosted.org/packages/3d/b9/15fd5541ef4f5b9a17eefd379356cf12175fe577424e7b1d80676516031a/matplotlib-3.10.8-cp313-cp313-macosx_10_13_x86_64.whl @@ -409,259 +473,302 @@ environments: - 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/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/fd/1a/31b7cd6e4e7a02df4e076162e9783620777592bea9e4bb036389389af99d/mike-2.1.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/48/f7/10f5e101db25741b91e4f4792c5d97b4fa834ead5cf509ae91097d939424/mike-2.1.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9b/f7/4a5e785ec9fbd65146a27b6b70b6cdc161a66f2024e4b04ac06a67f5578b/mistune-3.2.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/71/26/4d39d52ea2219604053a4d05b98e90d6a335511cc01806436ec4886b1028/mkdocs_autorefs-1.2.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/9f/d4/029f984e8d3f3b6b726bd33cafc473b75e9e44c0f7e80a5b29abc466bdea/mkdocs_get_deps-0.2.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/08/37/5f1fd5c3f6954b3256f8126275e62af493b96fb6aef6c0dbc4ee326032ad/mkdocs_jupyter-0.25.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/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/93/89/eb601278b12c471235860992f5973cf3c55ca3f77d1d6127389eb045a021/mkdocs_jupyter-0.26.1-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/3e/32/ed071cb721aca8c227718cffcf7bd539620e9799bbf2619e90c757bfd030/mkdocs_material-9.7.1-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/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.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/cd/10/4c27c3063c2b3681a4b7942f8dbdeb4fa34fecb2c19b594e7345ebf4f86f/mkdocstrings-0.27.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/51/23/d02d86553327296c3bf369d444194ea83410cce8f0e690565264f37f3261/mkdocstrings_python-1.13.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/04/41/1cf02e3df279d2dd846a1bf235a928254eba9006dd22b4a14caa71aed0f7/mkdocstrings-1.0.3-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/5b/69/93b34728cc386efdde0c342f8c680b9187dea7beb7adaf6b58a0713be101/mpld3-0.5.12-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/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/6b/31/b46518ecc604d7edf3a4f94cb3bf021fc62aa301f0cb849936968164ef23/msgpack-1.1.2-cp313-cp313-macosx_10_13_x86_64.whl - pypi: https://files.pythonhosted.org/packages/8a/d1/b902d38b6e5ba3bdddbec469bba388d647f960aeed7b5b3623a8debe8a76/msgspec-0.20.0-cp313-cp313-macosx_10_13_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/91/1c/eb97db117a1ebe46d457a3d235a7b9d2e6dcab174f42d1b67663dd9e5371/multidict-6.7.0-cp313-cp313-macosx_10_13_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/3d/2e/cf2ffeb386ac3763526151163ad7da9f1b586aac96d2b4f7de1eaebf0c61/narwhals-2.15.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c7/75/bc704ae15fee974f8fccd871305e254754167dce5f9e42d88a2def741a1d/multidict-6.7.1-cp313-cp313-macosx_10_13_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/3f/c3/06490e98393dcb4d6ce2bf331a39335375c300afaef526897881fbeae6ab/narwhals-2.18.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/83/a0/5b0c2f11142ed1dddec842457d3f65eaf71a0080894eb6f018755b319c3a/nbclient-0.10.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/cc/9a/cd673b2f773a12c992f41309ef81b99da1690426bd2f96957a7ade0d3ed7/nbconvert-7.16.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0d/4b/8d5f796a792f8a25f6925a96032f098789f448571eb92011df1ae59e8ea8/nbconvert-7.17.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a9/82/0340caa499416c78e5d8f5f05947ae4bc3cba53c9f038ab6e9ed964e22f1/nbformat-5.10.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/eb/be/b257e12f9710819fde40adc972578bee6b72c5992da1bc8369bef2597756/nbmake-1.5.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/28/88/4789719fbbe166d12d345b3ac66b96105f10001b16e00a9765ba29261a21/nbqa-1.9.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/12/e5/838eb1004fb9b6f0383c47c0d902eb698fda052e8e64ca48c70a2144a48c/nbstripout-0.8.2-py2.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/fc/39/dfdef4cfbfcf7c80cd144b1b2e262c2e785f18b152eeebe14ecd70ee7455/ncrystal-4.2.12-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ab/ee/0d9d9218d2081e56828194f83d0eac6292b7182708fd07a62756c66f7194/ncrystal_core-4.2.12-py3-none-macosx_10_9_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/78/98/20fdd6825d1876fc54bb098509e736073d20f569b033f2f23bf74aa1df9c/ncrystal_python-4.2.12-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.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/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f9/33/bd5b9137445ea4b680023eb0469b2bb969d61303dedb2aac6560ff3d14a1/notebook_shim-0.2.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/db/69/9cde09f36da4b5a505341180a3f2e6fadc352fd4d2b7096ce9778db83f1a/numpy-2.3.5-cp313-cp313-macosx_10_13_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a2/2f/687722910b5a5601de2135c891108f51dfc873d8e43c8ed9f4ebb440b4a2/numpy-2.4.3-cp313-cp313-macosx_14_0_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/cd/4b/18b035ee18f97c1040d94debd8f2e737000ad70ccc8f5513f4eefad75f4b/pandas-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/0b/48/aad6ec4f8d007534c091e9a7172b3ec1b1ee6d99a9cbb936b5eab6c6cf58/pandas-3.0.1-cp313-cp313-macosx_10_13_x86_64.whl - pypi: https://files.pythonhosted.org/packages/ef/af/4fbc8cab944db5d21b7e2a5b8e9211a03a79852b1157e2c102fcc61ac440/pandocfilters-1.5.1-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/16/32/f8e3c85d1d5250232a5d3477a2a28cc291968ff175caeadaf3cc19ce0e4a/parso-0.8.5-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/05/bb/39e6768529454cc2b57e1e2fa0a0a18ff64397a16303270e215a3e03285f/pathspec-1.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b6/61/fae042894f4296ec49e3f193aff5d7c18440da9e48102c3315e1bc4519a7/parso-0.8.6-py2.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/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/4f/bf/28ab865de622e14b747f0cd7877510848252d950e43002e224fb1c9ababf/pillow-12.1.0-cp313-cp313-macosx_10_13_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/44/3c/d717024885424591d5376220b5e836c2d5293ce2011523c9de23ff7bf068/pip-25.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/14/a1/16c4b823838ba4c9c52c0e6bbda903a3fe5a1bdbf1b8eb4fff7156f3e318/pillow-12.1.1-cp313-cp313-macosx_10_13_x86_64.whl - pypi: https://files.pythonhosted.org/packages/c4/36/ce5f75aa7c736a663a901766edc3580098c7ea3959a0e878363a54a3714e/pixi_kernel-0.7.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e7/c3/3031c931098de393393e1f93a38dc9ed6805d86bb801acc3cf2d5bd1e6b7/plotly-6.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/63/d7/97f7e3a6abb67d8080dd406fd4df842c2be0efaf712d1c899c32a075027c/platformdirs-4.9.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/31/8b/9e8baf7dacac8d0c174925c38ff43c6d94bc9abb35503f67762caccb6869/plopp-26.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/52/d2/c6e44dba74f17c6216ce1b56044a9b93a929f1c2d5bdaff892512b260f5e/plotly-6.6.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/79/ad/45312df6b63ba64ea35b8d8f5f0c577aac16e6b416eafe8e1cb34e03f9a7/plumbum-1.10.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/a8/87/77cc11c7a9ea9fd05503def69e3d18605852cd0d4b0d3b8f15bbeb3ef1d1/pooch-1.8.2-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/5d/19/fd3ef348460c80af7bb4669ea7926651d1f95c23ff2df18b9d24bab4f3fa/pre_commit-4.5.1-py2.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/b8/db/14bafcb4af2139e046d03fd00dea7873e48eafe18b7d2797e73d6681f210/prometheus_client-0.23.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/74/c3/24a2f845e3917201628ecaba4f18bab4d18a337834c1df2a159ee9d22a42/prometheus_client-0.24.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8b/e8/677a0025e8a2acf07d3418a2e7ba529c9c33caf09d3c1f25513023c1db56/propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/c5/cf/5180eb8c8bdf6a503c6919f1da28328bd1e6b3b1b5b9d5b01ae64f019616/psutil-7.2.1-cp36-abi3-macosx_10_9_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl - pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-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/0b/a6/e2e8535d8146bce05de6e0ecf1099a7e2887d840ae2a7b3a09385543fd02/py3dmol-2.5.3-py2.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/87/0d/6af0bb9a45c771ffccd5c4c035c57ac9005e711b1191ddad1dd954187cfe/pycifrw-5.0.1-cp313-cp313-macosx_10_13_x86_64.whl - pypi: https://files.pythonhosted.org/packages/78/3c/2a612b95ddbb9a6bdcb47b7a93c4884f74c6ff22356b2f7b213b16e65c35/pycifstar-0.3.0.tar.gz - pypi: https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/87/6f/cc2b231dc78d8c3aaa674a676db190b8f8071c50134af8f8cf39b9b8e8df/pydoclint-0.8.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ea/10/47caf89cbb52e5bb764696fd52a8c591a2f0e851a93270c05a17f36000b5/pymdown_extensions-10.20-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/8b/40/2614036cdd416452f5bf98ec037f38a1afb17f327cb8e6b652d4729e0af8/pyparsing-3.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6f/2c/5b079febdc65e1c3fb2729bf958d18b45be7113828528e8a0b5850dd819a/pymdown_extensions-10.21-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.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/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/50/74/c655a6eda0fd188d490c14142a0f0380655ac7099604e1fbf8fa1a97f0a1/python_engineio-4.13.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/67/0f/019d3949a40280f6193b62bc010177d4ce702d0fce424322286488569cd3/python_discovery-1.2.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/aa/54/0cce26da03a981f949bb8449c9778537f75f5917c172e1d2992ff25cb57d/python_engineio-4.13.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/51/e5/fecf13f06e5e5f67e8837d777d1bc43fac0ed2b77a676804df5c34744727/python_json_logger-4.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/28/d2/2ccc2b69a187b80fda3152745670cfba936704f296a9fa54c6c8ac694d12/python_socketio-5.16.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.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/d8/8b/e2bbeb42068f0c48899e8eddd34902afc0f7429d4d2a152d2dc2670dc661/pythreejs-2.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.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/92/e7/038aab64a946d535901103da16b953c8c9cc9c961dadcbf3609ed6428d23/pyzmq-27.1.0-cp312-abi3-macosx_10_15_universal2.whl + - pypi: https://files.pythonhosted.org/packages/3c/26/1062c7ec1b053db9e499b4d2d5bc231743201b74051c973dadeac80a8f43/questionary-2.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/93/f7/d00d9b4a0313a6be3a3e0818e6375e15da6d7076f4ae47d1324e7ca986a1/radon-6.0.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/56/5d/c814546c2333ceea4ba42262d8c4d55763003e767fa169adc693bd524478/requests-2.33.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/57/4d/a7545bf6c62b0dbe5795f22ea9e88cc070fdced5c34663ebc5bed2f610c0/returns-0.26.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9e/51/17023c0f8f1869d8806b979a2bffa3f861f26a3f1a66b094288323fba52f/rfc3986_validator-0.1.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7e/71/44ce230e1b7fadd372515a97e32a83011f906ddded8d03e3c6aafbdedbb7/rfc3987_syntax-1.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/df/58/a0349197a7dfa603ffb7f5b0470391efa79ddc327c1e29c4851e85b09cc5/ruff-0.14.10-py3-none-macosx_10_12_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/72/f1/57e8327ab1508272029e27eeef34f2302ffc156b69e7e233e906c2a5c379/scipy-1.16.3-cp313-cp313-macosx_10_14_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/1b/5a/f2f2e5eda25579f754acd83399c522ee03d6acbe001dfe53c8a1ec928b44/send2trash-2.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/eb/92/f1c662784d149ad1414cae450b082cf736430c12ca78367f20f5ed569d65/ruff-0.15.8-py3-none-macosx_10_12_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/af/46/661159ad844034ba8b3f4e0516215c41e4ee17db4213d13a82227670764f/sciline-25.11.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e2/69/1dcb8e967f62759578938db5b29792b82ea8939a2d712e79491fa3e1cf0a/scipp-26.3.1-cp313-cp313-macosx_14_0_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/fb/46/e50b38629e9e3f4a1dd55fb36d8b8abd1d59768c31151c1c8ed696f7b865/scippneutron-26.3.0-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/b2/83/15087d945e0e4d48ce2377498abf5ad171ae013232ae31d06f336e64c999/scipy-1.17.1-cp313-cp313-macosx_14_0_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/1c/78/504fdd027da3b84ff1aecd9f6957e65f35134534ccc6da8628eb71e76d3f/send2trash-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9d/76/f789f7a86709c6b087c5a2f52f911838cad707cc613162401badc665acfe/setuptools-82.0.1-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/52/59/0782e51887ac6b07ffd1570e0364cf901ebc36345fea669969d2084baebb/simple_websocket-1.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/48/f3/b67d6ea49ca9154453b6d70b34ea22f3996b9fa55da105a79d8732227adc/soupsieve-2.8.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/bf/e1/3ccb13c643399d22289c6a9786c1a91e3dcbb68bce4beb44926ac2c557bf/sqlalchemy-2.0.45-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/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-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/1b/a5/174d33068d4383df4be9ee1ea9251f17820a622f3be744ca2ab7334818ee/spglib-2.6.0-cp313-cp313-macosx_10_13_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/46/2c/9664130905f03db57961b8980b05cab624afd114bf2be2576628a9f22da4/sqlalchemy-2.0.48-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-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/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-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/6a/9e/2064975477fdc887e47ad42157e214526dcad8f317a948dee17e1659a62f/terminado-0.18.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e6/34/ebdc18bae6aa14fbee1a08b63c015c72b64868ff7dae68808ab500c492e2/tinycss2-1.4.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/33/f0/3fe8c6e69135a845f4106f2ff8b6805638d4e85c264e70114e8126689587/tokenize_rt-6.2.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/89/48/06ee6eabe4fdd9ecd48bf488f4ac783844fd777f547b8d1b61c11939974e/tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/db/7e/f7b8d8c4453f305a51f80dbb49014257bb7d28ccb4bbb8dd328ea995ecad/tornado-6.5.4-cp39-abi3-macosx_10_9_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ab/5e/7625b76cd10f98f1516c36ce0346de62061156352353ef2da44e5c21523c/tornado-6.5.5-cp39-abi3-macosx_10_9_x86_64.whl - pypi: https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/4f/7e/bc19996fa86cad8801e8ffe6f1bba5836ca0160df76d0410d27432193712/trove_classifiers-2025.12.1.14-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/1b/a9/e3aee762739c1d7528da1c3e06d518503f8b6c439c35549b53735ba52ead/typeguard-4.4.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a0/1d/d9257dd49ff2ca23ea5f132edf1281a0c4f9de8a762b9ae399b670a59235/typer-0.21.1-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/bb/4a/2e5583e544bc437d5e8e54b47db87430df9031b29b48d17f26d129fa60c0/trove_classifiers-2026.1.14.14-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/4a/91/48db081e7a63bb37284f9fbcefda7c44c277b18b0e13fbc36ea2335b71e6/typer-0.24.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8f/5e/f1e1dd319e35e962a4e00b33150a8868b6329cc1d19fd533436ba5488f09/uncertainties-3.2.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f7/46/e7cea8159199096e1df52da20a57a6665da80c37fb8aeb848a3e47442c32/untokenize-0.1.1.tar.gz - pypi: https://files.pythonhosted.org/packages/e7/00/3fca040d7cf8a32776d3d81a00c8ee7457e00f80c649f1e4a863c8321ae9/uri_template-1.3.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/6d/b9/4095b668ea3678bf6a0af005527f39de12fb026516fb3df17495a733b7f8/urllib3-2.6.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/5e/68/bb76c97c284ce7fb8efa868994c2510588faa7075e60d8865d1373e54b7b/uv-0.9.22-py3-none-macosx_10_12_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/a4/39/6983dd79f01aaa4c75d9ffa550fa393f0c4c28f7ccd6956e4188c62cefbc/validate_pyproject-0.24.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/ee/e9c95cda829131f71a8dff5ce0406059fd16e591c074414e31ada19ba7c3/validate_pyproject-0.25-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/bc/4a/c6fd02a642bbe4e9f25cdd3714a328e3fc3eb6bd7b5e96f1a2285bd928b9/varname-0.15.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1c/59/964ecb8008722d27d8a835baea81f56a91cea8e097b3be992bc6ccde6367/versioningit-3.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a4/ce/3b6fee91c85626eaf769d617f1be9d2e15c1cca027bbdeb2e0d751469355/verspec-0.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/79/0c/c05523fa3181fdf0c9c52a6ba91a23fbf3246cc095f26f6516f9c60e6771/virtualenv-20.35.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c6/59/7d02447a55b2e55755011a647479041bc92a82e143f96a8195cb33bd0a1c/virtualenv-21.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/af/b5/123f13c975e9f27ab9c0770f514345bd406d0e8d3b7a0723af9d43f710af/wcwidth-0.2.14-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/68/5a/199c59e0a824a3db2b89c5d2dade7ab5f9624dbf6448dc291b46d5ec94d3/wcwidth-0.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e2/cc/e097523dd85c9cf5d354f78310927f1656c422bd7b2613b2db3e3f9a0f2c/webcolors-25.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/34/db/b10e48aa8fff7407e67470363eac595018441cf32d5e1001567a7aeba5d2/websocket_client-1.9.0-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/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-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/01/88/04d98af0b47e0ef42597b9b28863b9060bb515524da0a65d5f4db160b2d5/yarl-1.22.0-cp313-cp313-macosx_10_13_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/67/b6/8925d68af039b835ae876db5838e82e76ec87b9782ecc97e192b809c4831/yarl-1.23.0-cp313-cp313-macosx_10_13_x86_64.whl + - pypi: ./ osx-arm64: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/_openmp_mutex-4.5-7_kmp_llvm.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-hd037594_8.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/noarch/ca-certificates-2026.1.4-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.2.25-hbd8a1cb_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/gsl-2.8-h8d0574d_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/icu-75.1-hfee45f7_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libabseil-20250512.1-cxx17_hd41c47c_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libblas-3.11.0-5_h51639a9_openblas.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/libabseil-20260107.1-cxx17_h2062a1b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libblas-3.11.0-6_h51639a9_openblas.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/libcblas-3.11.0-5_hb0561ab_openblas.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-21.1.8-hf598326_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcblas-3.11.0-6_hb0561ab_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-22.1.2-h55c6f16_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.7.3-haf25636_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.5.2-he5f378a_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgcc-15.2.0-hcbb3090_16.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran-15.2.0-h07b0088_16.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran5-15.2.0-hdae7583_16.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.1-h39f12f2_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libmpdec-4.0.0-h5505292_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libnghttp2-1.67.0-hc438710_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopenblas-0.3.30-openmp_ha158390_3.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.51.1-h1b79a29_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.7.4-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/libgcc-15.2.0-hcbb3090_18.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran-15.2.0-h07b0088_18.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran5-15.2.0-hdae7583_18.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.2-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/libopenblas-0.3.32-openmp_he657e61_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.52.0-h1ae2325_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.1-h8359307_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/llvm-openmp-21.1.8-h4a912ad_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/llvm-openmp-22.1.1-hc7d1edf_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.5-h5e97a16_3.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/nodejs-25.2.1-h5230ea7_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.6.0-h5503f6c_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.13.11-hfc2f54d_100_cp313.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.1-hd24854e_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.13.12-h20e6be0_100_cp313.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.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/tk-8.6.13-h892fb3f_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h010d191_3.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hbf9d68e_6.conda - pypi: https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/17/f8/8dd2cf6112a5a76f81f81a5130c57ca829d101ad583ce57f889179accdda/aiohttp-3.13.3-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.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/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/81/29/5ecc3a15d5a33e31b26c11426c45c501e439cb865d0bff96315d86443b78/appnope-0.1.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b6/02/d297943bcacf05e4f2a94ab6f462831dc20158614e5d067c35d4e63b9acb/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/ed/c9/d7977eaacb9df673210491da99e6a247e93df98c715fc43fd136ce1d3d33/arrow-1.4.0-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/6c/25/4f103d1bedb3593718713b3f743df7b3ff3fc68d36d6666c30265ef59c8a/ase-3.28.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/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/03/49/d10027df9fce941cb8184e78a02857af36360d33e1721df81c5ed2179a1a/async_lru-2.0.5-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e5/e2/c2e3abf398f80732e58b03be77bde9022550d221dd8781bf586bd4d97cc1/async_lru-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.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/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/eb/95/7118e935b0b0bd3f94dfec2d852fd4e4f4f9757bdb49850519acd245cd3a/backrefs-6.1-py313-none-any.whl + - pypi: https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c5/71/c754b1737ad99102e03fa3235acb6cb6d3ac9d6f596cbc3e5f236705abd8/backrefs-6.2-py313-none-any.whl - pypi: https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-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/cd/3a/577b549de0cc09d95f11087ee63c739bba856cd3952697eec4c4bb91350a/bleach-6.3.0-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/cb/8c/2b30c12155ad8de0cf641d76a8b396a16d2c36bc6d50b621a62b7c4567c1/build-1.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4a/57/3b7d4dd193ade4641c865bc2b93aeeb71162e81fc348b8dad020215601ed/build-1.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/43/8e/278ea79cf8ee0c395a5ad74625b3465c2fc234bb277f171dd59dd203820d/bumps-1.0.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.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/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl + - pypi: https://files.pythonhosted.org/packages/83/d3/80554c1cc15631446c9b90aec6fe63b7310aa0b82d3004f7ba38bd8a8270/chardet-7.4.0.post1-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/1e/1d/4fdabeef4e231153b6ed7567602f3b68265ec4e5b76d6024cf647d43d981/charset_normalizer-3.4.6-cp313-cp313-macosx_10_13_universal2.whl - pypi: https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.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/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-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/32/33/7cbfe2bdc6e2f03d6b240d23dc45fdaf3fd270aaf2d640be77b7f16989ab/coverage-7.13.1-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/df/91/4a151c94320458895049a3e23b7b2cfc08953c60b14892de837e8eb51d0a/copier-9.14.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0c/c9/44fb661c55062f0818a6ffd2685c67aa30816200d5f2817543717d4b92eb/coverage-7.13.5-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/52/e8/c14cc8af8cd38e86887053843382629bd8ebd117f83f15eb1194d65a2c9d/cryspy-0.10.0-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/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-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/25/3e/e27078370414ef35fafad2c06d182110073daaeb5d3bf734b0b1eeefe452/debugpy-1.8.19-py2.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/e0/c3/7f67dea8ccf8fdcb9c99033bbe3e90b9e7395415843accb81428c441be2d/debugpy-1.8.20-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/68/49/869b49db0bad6fba7c7471d612c356e745929d14e0de63acbc53e88f2a50/dfo_ls-1.6-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/a3/28/d050c2716c74c6fce9ace360e727e6f86b68212fb6b0ea57c005ebe574ea/diffpy_pdffit2-1.5.2-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/8e/52/39914bf42bb01901c91781def35c1aeffa431a59299e9748c0cfae3e5493/diffpy_structure-3.3.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/79/c8/f0f4ab7fd08950d5358579364a0a9b9198bf03e179a4fcceae4b353be32e/diffpy_utils-3.6.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/50/3d/9373ad9c56321fdab5b41197068e1d8c25883b3fea29dd361f9b55116869/dill-0.4.0-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/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/dc/b4/a7ec1eaee86761a9dbfd339732b4706db3c6b65e970c12f0f56cfcce3dcf/docformatter-1.7.7-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/82/ee/c9ca3f81d69ec6c99ca41e10432a58c60fef3a6e63cce6df7e796f90fe22/easydiffraction-0.10.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/50/98b146aea0f1cd7531d25f12bea69fa9ce8d1662124f93fb30dc4511b65e/docstring_parser_fork-0.0.14-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/87/10/2c7edbf230e5c507d38367af498fa94258ed97205d9b4b6f63a921fe9c49/dunamai-1.26.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/55/cd/d729a1bb63fa95387228cc508552dea4685ea0116e484e73238db10f9521/essdiffraction-26.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f1/bb/66c80d7f801b191f7b3ee6149a39be9d1a8a81c233e20adaf796d171f93a/essreduce-26.4.0-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/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cb/a8/20d0723294217e47de6d9e2e40fd4a9d2f7c4b6ef974babd482a59743694/fastjsonschema-2.21.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/9a/30/ab407e2ec752aa541704ed8f93c11e2a5d92c168b8a755d818b74a3c5c2d/filelock-3.20.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/4b/cf/00ba28b0990982530addb8dc3e9e6f2fa9cb5c20df2abdda7baa755e8fe1/fonttools-4.61.1-cp313-cp313-macosx_10_13_universal2.whl + - pypi: https://files.pythonhosted.org/packages/a4/a5/842ae8f0c08b61d6484b52f99a03510a3a72d23141942d216ebe81fefbce/filelock-3.25.2-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/33/b2/986d1220f6ee931e338d272bc1f3ec02cfe5f9b5fad84e95afdad57f1ebc/format_docstring-0.2.7-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cf/58/8acf1b3e91c58313ce5cb67df61001fc9dcd21be4fadb76c1a2d540e09ed/fqdn-1.5.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/19/5b/0976c1af0dd59a6850e9ea3b6c6d28f3dff0651c694a6a6192a2933e8feb/gemmi-0.7.4-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/e6/ab/fb21f4c939bb440104cc2b396d3be1d9b7a9fd3c6c2a53d98c45b3d7c954/fsspec-2026.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d5/08/c2409cb01d5368dcfedcbaffa7d044cc8957d57a9d0855244a5eb4709d30/funcy-2.0-py2.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/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/9c/83/3b1d03d36f224edded98e9affd0467630fc09d766c0e56fb1498cbb04a9b/griffe-1.15.0-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/6a/09/e21df6aef1e1ffc0c816f0522ddc3f6dcded766c3261813131c78a704470/gitpython-3.1.46-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/4b/4c/cc8c68196db727cfc1432f2ad5de50aa6707e630d44b2e6361dc06d8f134/griffelib-2.0.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/31/96/ba99a003c763998035b0de4c299598125df5fc6c9ccf834f152ddd60e0fb/h5py-3.15.1-cp313-cp313-macosx_11_0_arm64.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/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/0f/1c/e5fd8f973d4f375adb21565739498e2e9a1e54c858a97b9a8ccfdc81da9b/identify-2.6.15-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/46/33/92ef41c6fad0233e41d3d84ba8e8ad18d1780f1e5d99b3c683e6d7f98b63/identify-2.6.18-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a4/ed/1f1afb2e9e7f38a545d628f864d562a5ae64fe6f7a10e28ffb9b185b4e89/importlib_resources-6.5.2-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/12/c9/6869a1dcf4aaf309b9543ec070be3ec3adebee7c9bec9af8c230494134b9/interrogate-1.7.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f6/d8/502954a4ec0efcf264f99b65b41c3c54e65a647d9f0d6f62cd02227d242c/ipykernel-6.31.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/86/92/162cfaee4ccf370465c5af1ce36a9eacec1becb552f2033bb3584e6f640a/ipython-9.9.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/82/b9/e73d5d9f405cba7706c539aa8b311b49d4c2f3d698d9c12f815231169c71/ipykernel-7.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b2/90/45c72becc57158facc6a6404f663b77bbcea2519ca57f760e2879ae1315d/ipython-9.11.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-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/7b/55/e5326141505c5d5e34c5e0935d2908a74e4561eca44108fbfb9c13d2911a/isoduration-20.11.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/72/b9/313e8f2f2e9517ae050a692ae7b3e4b3f17cc5e6dfea0db51fe14e586580/jinja2_ansible_filters-1.3.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d7/9e/038522f50ceb7e74f1f991bf1b699f24b0c2bbe7c390dd36ad69f4582258/json5-0.13.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/71/92/5e77f98553e9e75130c78900d000368476aed74276eb8ae8796f65f00918/jsonpointer-3.0.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9e/6a/a83720e953b1682d2d109d3c2dbb0bc9bf28cc1cbc205be4ef4be5da709d/jsonpointer-3.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/bb/f5/fddaec430367be9d62a7ed125530e133bfd4a1c0350fe221149ee0f2b526/jupyter_client-8.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2d/0b/ceb7694d864abc0a047649aec263878acb9f792e1fec3e676f22dc9015e3/jupyter_client-8.8.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/76/c0/8cc378aad5ccfcc37adeda27d3864f20ea808ad5b63572f5b986a42a649c/jupyter_dark_detect-0.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e2/48/577993f1f99c552f18a0428731a755e06171f9902fa118c379eb7c04ea22/jupyter_events-0.12.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1a/60/1f6cee0c46263de1173894f0fafcb3475ded276c472c14d25e0280c18d6d/jupyter_lsp-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f4/a4/61adb19f3c74b0dc0e411de4f06ebef564b1f179928f9dffcbd4b378f2ef/jupyter_notebook_parser-0.1.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/92/80/a24767e6ca280f5a49525d987bf3e4d7552bf67c8be07e8ccf20271f8568/jupyter_server-2.17.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/07/2d/2b32cdbe8d2a602f697a649798554e4f072115438e92249624e532e8aca6/jupyter_server_terminals-0.5.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/af/c3/acced767eecc11a70c65c45295db5396c4f0c1937874937d5a76d7b177b6/jupyterlab-4.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d1/2d/6674563f71c6320841fc300911a55143925112a72a883e2ca71fba4c618d/jupyter_server_terminals-0.5.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e1/1b/dad6fdcc658ed7af26fdf3841e7394072c9549a8b896c381ab49dd11e2d9/jupyterlab-4.5.6-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b1/dd/ead9d8ea85bf202d90cc513b533f9c363121c7792674f78e0d8a854b63b4/jupyterlab_pygments-0.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e0/07/a000fe835f76b7e1143242ab1122e6362ef1c03f23f83a045c38859c2ae0/jupyterlab_server-2.28.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f7/5a/488f492b49b712ca38326e12c78d0d48ab6be682a2989ce533f0f2c4dfd2/jupyterquiz-2.9.6.2-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/bd/0d/2d240e7098e0cafba4d25e9530e7596b1bb1bd4476e41b10346bcaaa36d6/jupytext-1.18.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/2d/7a/9d90a151f558e29c3936b8a47ac770235f436f2120aca41a6d5f3d62ae8d/kiwisolver-1.4.9-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/ab/b5/36c712098e6191d1b4e349304ef73a8d06aed77e56ceaac8c0a306c7bda1/jupyterlab_widgets-3.0.16-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/52/bc858b1665d0dec3a2511f4e6f5c18ea85c0977563d624d597c95d6d0fd7/jupyterquiz-2.9.6.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/16/5a/736dd2f4535dbf3bf26523f9158c011389ef88dd06ec2eef67fd744f1c7b/jupytext-1.19.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a8/3a/d0a972b34e1c63e2409413104216cd1caa02c5a37cb668d1687d466c1c45/kiwisolver-1.5.0-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/82/3d/14ce75ef66813643812f3093ab17e46d3a206942ce7376d31ec2d36229e7/lark-1.3.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/38/7e/7b91c89a4cf0f543a83be978657afb20c86af6d725253e319589dcc4ce52/lmfit-1.3.4-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/d2/f0/834e479e47e499b6478e807fb57b31cc2db696c4db30557bb6f5aea4a90b/mando-0.7.1-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/70/81/54e3ce63502cd085a0c556652a4e1b919c45a446bd1e5300e10c44c8c521/markdown-3.10-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/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/8d/a0/2ba3473c1b66b9c74dc7107c67e9008cb1782edbe896d4c899d39ae9cf78/matplotlib-3.10.8-cp313-cp313-macosx_11_0_arm64.whl @@ -669,155 +776,176 @@ environments: - 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/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/fd/1a/31b7cd6e4e7a02df4e076162e9783620777592bea9e4bb036389389af99d/mike-2.1.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/48/f7/10f5e101db25741b91e4f4792c5d97b4fa834ead5cf509ae91097d939424/mike-2.1.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9b/f7/4a5e785ec9fbd65146a27b6b70b6cdc161a66f2024e4b04ac06a67f5578b/mistune-3.2.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/71/26/4d39d52ea2219604053a4d05b98e90d6a335511cc01806436ec4886b1028/mkdocs_autorefs-1.2.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/9f/d4/029f984e8d3f3b6b726bd33cafc473b75e9e44c0f7e80a5b29abc466bdea/mkdocs_get_deps-0.2.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/08/37/5f1fd5c3f6954b3256f8126275e62af493b96fb6aef6c0dbc4ee326032ad/mkdocs_jupyter-0.25.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/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/93/89/eb601278b12c471235860992f5973cf3c55ca3f77d1d6127389eb045a021/mkdocs_jupyter-0.26.1-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/3e/32/ed071cb721aca8c227718cffcf7bd539620e9799bbf2619e90c757bfd030/mkdocs_material-9.7.1-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/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.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/cd/10/4c27c3063c2b3681a4b7942f8dbdeb4fa34fecb2c19b594e7345ebf4f86f/mkdocstrings-0.27.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/51/23/d02d86553327296c3bf369d444194ea83410cce8f0e690565264f37f3261/mkdocstrings_python-1.13.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/04/41/1cf02e3df279d2dd846a1bf235a928254eba9006dd22b4a14caa71aed0f7/mkdocstrings-1.0.3-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/5b/69/93b34728cc386efdde0c342f8c680b9187dea7beb7adaf6b58a0713be101/mpld3-0.5.12-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/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-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/57/b6/eff0305961a1d9447ec2b02f8c73c8946f22564d302a504185b730c9a761/msgspec-0.20.0-cp313-cp313-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/f1/d8/6c3442322e41fb1dd4de8bd67bfd11cd72352ac131f6368315617de752f1/multidict-6.7.0-cp313-cp313-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/3d/2e/cf2ffeb386ac3763526151163ad7da9f1b586aac96d2b4f7de1eaebf0c61/narwhals-2.15.0-py3-none-any.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/3f/c3/06490e98393dcb4d6ce2bf331a39335375c300afaef526897881fbeae6ab/narwhals-2.18.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/83/a0/5b0c2f11142ed1dddec842457d3f65eaf71a0080894eb6f018755b319c3a/nbclient-0.10.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/cc/9a/cd673b2f773a12c992f41309ef81b99da1690426bd2f96957a7ade0d3ed7/nbconvert-7.16.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0d/4b/8d5f796a792f8a25f6925a96032f098789f448571eb92011df1ae59e8ea8/nbconvert-7.17.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a9/82/0340caa499416c78e5d8f5f05947ae4bc3cba53c9f038ab6e9ed964e22f1/nbformat-5.10.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/eb/be/b257e12f9710819fde40adc972578bee6b72c5992da1bc8369bef2597756/nbmake-1.5.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/28/88/4789719fbbe166d12d345b3ac66b96105f10001b16e00a9765ba29261a21/nbqa-1.9.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/12/e5/838eb1004fb9b6f0383c47c0d902eb698fda052e8e64ca48c70a2144a48c/nbstripout-0.8.2-py2.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/fc/39/dfdef4cfbfcf7c80cd144b1b2e262c2e785f18b152eeebe14ecd70ee7455/ncrystal-4.2.12-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/55/8d/2b26572e909238bb114d50fb0d1b6b54eb6dafa2d83a7264f18146796b0d/ncrystal_core-4.2.12-py3-none-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/78/98/20fdd6825d1876fc54bb098509e736073d20f569b033f2f23bf74aa1df9c/ncrystal_python-4.2.12-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.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/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f9/33/bd5b9137445ea4b680023eb0469b2bb969d61303dedb2aac6560ff3d14a1/notebook_shim-0.2.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/79/fb/f505c95ceddd7027347b067689db71ca80bd5ecc926f913f1a23e65cf09b/numpy-2.3.5-cp313-cp313-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3a/66/bd096b13a87549683812b53ab211e6d413497f84e794fb3c39191948da97/numpy-2.4.3-cp313-cp313-macosx_14_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/31/94/72fac03573102779920099bcac1c3b05975c2cb5f01eac609faf34bed1ca/pandas-2.3.3-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/a8/14/5990826f779f79148ae9d3a2c39593dc04d61d5d90541e71b5749f35af95/pandas-3.0.1-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/ef/af/4fbc8cab944db5d21b7e2a5b8e9211a03a79852b1157e2c102fcc61ac440/pandocfilters-1.5.1-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/16/32/f8e3c85d1d5250232a5d3477a2a28cc291968ff175caeadaf3cc19ce0e4a/parso-0.8.5-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/05/bb/39e6768529454cc2b57e1e2fa0a0a18ff64397a16303270e215a3e03285f/pathspec-1.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b6/61/fae042894f4296ec49e3f193aff5d7c18440da9e48102c3315e1bc4519a7/parso-0.8.6-py2.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/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/1c/34/583420a1b55e715937a85bd48c5c0991598247a1fd2eb5423188e765ea02/pillow-12.1.0-cp313-cp313-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/44/3c/d717024885424591d5376220b5e836c2d5293ce2011523c9de23ff7bf068/pip-25.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bb/ad/ad9dc98ff24f485008aa5cdedaf1a219876f6f6c42a4626c08bc4e80b120/pillow-12.1.1-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/c4/36/ce5f75aa7c736a663a901766edc3580098c7ea3959a0e878363a54a3714e/pixi_kernel-0.7.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e7/c3/3031c931098de393393e1f93a38dc9ed6805d86bb801acc3cf2d5bd1e6b7/plotly-6.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/63/d7/97f7e3a6abb67d8080dd406fd4df842c2be0efaf712d1c899c32a075027c/platformdirs-4.9.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/31/8b/9e8baf7dacac8d0c174925c38ff43c6d94bc9abb35503f67762caccb6869/plopp-26.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/52/d2/c6e44dba74f17c6216ce1b56044a9b93a929f1c2d5bdaff892512b260f5e/plotly-6.6.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/79/ad/45312df6b63ba64ea35b8d8f5f0c577aac16e6b416eafe8e1cb34e03f9a7/plumbum-1.10.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/a8/87/77cc11c7a9ea9fd05503def69e3d18605852cd0d4b0d3b8f15bbeb3ef1d1/pooch-1.8.2-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/5d/19/fd3ef348460c80af7bb4669ea7926651d1f95c23ff2df18b9d24bab4f3fa/pre_commit-4.5.1-py2.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/b8/db/14bafcb4af2139e046d03fd00dea7873e48eafe18b7d2797e73d6681f210/prometheus_client-0.23.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/74/c3/24a2f845e3917201628ecaba4f18bab4d18a337834c1df2a159ee9d22a42/prometheus_client-0.24.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-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/c5/2c/78e4a789306a92ade5000da4f5de3255202c534acdadc3aac7b5458fadef/psutil-7.2.1-cp36-abi3-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-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/0b/a6/e2e8535d8146bce05de6e0ecf1099a7e2887d840ae2a7b3a09385543fd02/py3dmol-2.5.3-py2.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/83/81/bdd4bfabe70b7c9a8c0716a722ced4ebd27311afd1f4800cd405d3229c1b/pycifrw-5.0.1-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/78/3c/2a612b95ddbb9a6bdcb47b7a93c4884f74c6ff22356b2f7b213b16e65c35/pycifstar-0.3.0.tar.gz - pypi: https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/87/6f/cc2b231dc78d8c3aaa674a676db190b8f8071c50134af8f8cf39b9b8e8df/pydoclint-0.8.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ea/10/47caf89cbb52e5bb764696fd52a8c591a2f0e851a93270c05a17f36000b5/pymdown_extensions-10.20-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/8b/40/2614036cdd416452f5bf98ec037f38a1afb17f327cb8e6b652d4729e0af8/pyparsing-3.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6f/2c/5b079febdc65e1c3fb2729bf958d18b45be7113828528e8a0b5850dd819a/pymdown_extensions-10.21-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.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/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/50/74/c655a6eda0fd188d490c14142a0f0380655ac7099604e1fbf8fa1a97f0a1/python_engineio-4.13.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/67/0f/019d3949a40280f6193b62bc010177d4ce702d0fce424322286488569cd3/python_discovery-1.2.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/aa/54/0cce26da03a981f949bb8449c9778537f75f5917c172e1d2992ff25cb57d/python_engineio-4.13.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/51/e5/fecf13f06e5e5f67e8837d777d1bc43fac0ed2b77a676804df5c34744727/python_json_logger-4.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/28/d2/2ccc2b69a187b80fda3152745670cfba936704f296a9fa54c6c8ac694d12/python_socketio-5.16.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.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/d8/8b/e2bbeb42068f0c48899e8eddd34902afc0f7429d4d2a152d2dc2670dc661/pythreejs-2.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.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/92/e7/038aab64a946d535901103da16b953c8c9cc9c961dadcbf3609ed6428d23/pyzmq-27.1.0-cp312-abi3-macosx_10_15_universal2.whl + - pypi: https://files.pythonhosted.org/packages/3c/26/1062c7ec1b053db9e499b4d2d5bc231743201b74051c973dadeac80a8f43/questionary-2.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/93/f7/d00d9b4a0313a6be3a3e0818e6375e15da6d7076f4ae47d1324e7ca986a1/radon-6.0.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/56/5d/c814546c2333ceea4ba42262d8c4d55763003e767fa169adc693bd524478/requests-2.33.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/57/4d/a7545bf6c62b0dbe5795f22ea9e88cc070fdced5c34663ebc5bed2f610c0/returns-0.26.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9e/51/17023c0f8f1869d8806b979a2bffa3f861f26a3f1a66b094288323fba52f/rfc3986_validator-0.1.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7e/71/44ce230e1b7fadd372515a97e32a83011f906ddded8d03e3c6aafbdedbb7/rfc3987_syntax-1.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/7b/82/36be59f00a6082e38c23536df4e71cdbc6af8d7c707eade97fcad5c98235/ruff-0.14.10-py3-none-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/44/13/7e63cfba8a7452eb756306aa2fd9b37a29a323b672b964b4fdeded9a3f21/scipy-1.16.3-cp313-cp313-macosx_12_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/1b/5a/f2f2e5eda25579f754acd83399c522ee03d6acbe001dfe53c8a1ec928b44/send2trash-2.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ca/f2/7a631a8af6d88bcef997eb1bf87cc3da158294c57044aafd3e17030613de/ruff-0.15.8-py3-none-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/79/fe/b14d806894cf05178f1e77d0d619f071db50cf698bc654c54f9241223bcf/scipp-26.3.1-cp313-cp313-macosx_14_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/fb/46/e50b38629e9e3f4a1dd55fb36d8b8abd1d59768c31151c1c8ed696f7b865/scippneutron-26.3.0-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/5b/58/3ce96251560107b381cbd6e8413c483bbb1228a6b919fa8652b0d4090e7f/scipy-1.17.1-cp313-cp313-macosx_14_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/1c/78/504fdd027da3b84ff1aecd9f6957e65f35134534ccc6da8628eb71e76d3f/send2trash-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9d/76/f789f7a86709c6b087c5a2f52f911838cad707cc613162401badc665acfe/setuptools-82.0.1-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/52/59/0782e51887ac6b07ffd1570e0364cf901ebc36345fea669969d2084baebb/simple_websocket-1.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/48/f3/b67d6ea49ca9154453b6d70b34ea22f3996b9fa55da105a79d8732227adc/soupsieve-2.8.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/bf/e1/3ccb13c643399d22289c6a9786c1a91e3dcbb68bce4beb44926ac2c557bf/sqlalchemy-2.0.45-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/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-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/2d/47/634fe8323c6c2bfa86e10eb41ebfe410db5e6231aa1727a31ce4f002480f/spglib-2.6.0-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/d1/c6/569dc8bf3cd375abc5907e82235923e986799f301cd79a903f784b996fca/sqlalchemy-2.0.48-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-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/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-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/6a/9e/2064975477fdc887e47ad42157e214526dcad8f317a948dee17e1659a62f/terminado-0.18.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e6/34/ebdc18bae6aa14fbee1a08b63c015c72b64868ff7dae68808ab500c492e2/tinycss2-1.4.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/33/f0/3fe8c6e69135a845f4106f2ff8b6805638d4e85c264e70114e8126689587/tokenize_rt-6.2.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f1/01/88793757d54d8937015c75dcdfb673c65471945f6be98e6a0410fba167ed/tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/ab/a9/e94a9d5224107d7ce3cc1fab8d5dc97f5ea351ccc6322ee4fb661da94e35/tornado-6.5.4-cp39-abi3-macosx_10_9_universal2.whl + - pypi: https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/59/8c/77f5097695f4dd8255ecbd08b2a1ed8ba8b953d337804dd7080f199e12bf/tornado-6.5.5-cp39-abi3-macosx_10_9_universal2.whl - pypi: https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/4f/7e/bc19996fa86cad8801e8ffe6f1bba5836ca0160df76d0410d27432193712/trove_classifiers-2025.12.1.14-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/1b/a9/e3aee762739c1d7528da1c3e06d518503f8b6c439c35549b53735ba52ead/typeguard-4.4.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a0/1d/d9257dd49ff2ca23ea5f132edf1281a0c4f9de8a762b9ae399b670a59235/typer-0.21.1-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/bb/4a/2e5583e544bc437d5e8e54b47db87430df9031b29b48d17f26d129fa60c0/trove_classifiers-2026.1.14.14-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/4a/91/48db081e7a63bb37284f9fbcefda7c44c277b18b0e13fbc36ea2335b71e6/typer-0.24.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8f/5e/f1e1dd319e35e962a4e00b33150a8868b6329cc1d19fd533436ba5488f09/uncertainties-3.2.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f7/46/e7cea8159199096e1df52da20a57a6665da80c37fb8aeb848a3e47442c32/untokenize-0.1.1.tar.gz - pypi: https://files.pythonhosted.org/packages/e7/00/3fca040d7cf8a32776d3d81a00c8ee7457e00f80c649f1e4a863c8321ae9/uri_template-1.3.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/6d/b9/4095b668ea3678bf6a0af005527f39de12fb026516fb3df17495a733b7f8/urllib3-2.6.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/af/49/7230b1d56aeaee0eefd346a70f582463f11fb7036d2d020bcf68053bd994/uv-0.9.22-py3-none-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/a4/39/6983dd79f01aaa4c75d9ffa550fa393f0c4c28f7ccd6956e4188c62cefbc/validate_pyproject-0.24.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/ee/e9c95cda829131f71a8dff5ce0406059fd16e591c074414e31ada19ba7c3/validate_pyproject-0.25-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/bc/4a/c6fd02a642bbe4e9f25cdd3714a328e3fc3eb6bd7b5e96f1a2285bd928b9/varname-0.15.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1c/59/964ecb8008722d27d8a835baea81f56a91cea8e097b3be992bc6ccde6367/versioningit-3.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a4/ce/3b6fee91c85626eaf769d617f1be9d2e15c1cca027bbdeb2e0d751469355/verspec-0.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/79/0c/c05523fa3181fdf0c9c52a6ba91a23fbf3246cc095f26f6516f9c60e6771/virtualenv-20.35.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c6/59/7d02447a55b2e55755011a647479041bc92a82e143f96a8195cb33bd0a1c/virtualenv-21.2.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 - - pypi: https://files.pythonhosted.org/packages/af/b5/123f13c975e9f27ab9c0770f514345bd406d0e8d3b7a0723af9d43f710af/wcwidth-0.2.14-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/68/5a/199c59e0a824a3db2b89c5d2dade7ab5f9624dbf6448dc291b46d5ec94d3/wcwidth-0.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e2/cc/e097523dd85c9cf5d354f78310927f1656c422bd7b2613b2db3e3f9a0f2c/webcolors-25.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/34/db/b10e48aa8fff7407e67470363eac595018441cf32d5e1001567a7aeba5d2/websocket_client-1.9.0-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/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-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/18/91/3274b215fd8442a03975ce6bee5fe6aa57a8326b29b9d3d56234a1dca244/yarl-1.22.0-cp313-cp313-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-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: ./ win-64: - - conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h0ad9c76_8.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.1.4-h4c7d964_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h0ad9c76_9.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.2.25-h4c7d964_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/gsl-2.8-h5b8d9c4_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/icu-78.1-h637d24d_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libblas-3.11.0-5_hf2e6a31_mkl.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libcblas-3.11.0-5_h2a3cdd5_mkl.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.7.3-hac47afa_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libffi-3.5.2-h52bdfb6_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libhwloc-2.12.1-default_h4379cf1_1003.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libblas-3.11.0-6_hf2e6a31_mkl.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libcblas-3.11.0-6_h2a3cdd5_mkl.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.7.4-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/libhwloc-2.12.2-default_h4379cf1_1000.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libiconv-1.18-hc1393d2_2.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-5.8.1-h2466b09_2.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libmpdec-4.0.0-h2466b09_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.51.1-hf5d6505_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-5.8.2-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/libsqlite-3.52.0-hf5d6505_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libwinpthread-12.0.0.r4.gg4f2fc60ca-h57928b3_10.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libxml2-16-2.15.1-h3cfd58e_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libxml2-2.15.1-h779ef1b_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libzlib-1.3.1-h2466b09_2.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/llvm-openmp-21.1.8-h4fa8253_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/mkl-2025.3.0-hac47afa_455.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/nodejs-25.2.1-he453025_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.6.0-h725018a_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/python-3.13.11-h09917c8_100_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libxml2-16-2.15.2-h692994f_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libxml2-2.15.2-h5d26750_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/llvm-openmp-22.1.1-h4fa8253_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/mkl-2025.3.1-hac47afa_11.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.1-hf411b9b_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/python-3.13.12-h09917c8_100_cp313.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/tbb-2022.3.0-hd094cb3_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h2c6b04d_3.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/tbb-2022.3.0-h3155e25_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h6ed50ae_3.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.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 @@ -826,96 +954,117 @@ environments: - pypi: https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4e/f1/ab0395f8a79933577cdd996dd2f9aa6014af9535f65dddcf88204682fe62/aiohttp-3.13.3-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.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/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e2/c6/a759ece8f1829d1f162261226fbfd2c6832b3ff7657384045286d2afa384/argon2_cffi_bindings-25.1.0-cp39-abi3-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/ed/c9/d7977eaacb9df673210491da99e6a247e93df98c715fc43fd136ce1d3d33/arrow-1.4.0-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/6c/25/4f103d1bedb3593718713b3f743df7b3ff3fc68d36d6666c30265ef59c8a/ase-3.28.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/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/03/49/d10027df9fce941cb8184e78a02857af36360d33e1721df81c5ed2179a1a/async_lru-2.0.5-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e5/e2/c2e3abf398f80732e58b03be77bde9022550d221dd8781bf586bd4d97cc1/async_lru-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.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/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/eb/95/7118e935b0b0bd3f94dfec2d852fd4e4f4f9757bdb49850519acd245cd3a/backrefs-6.1-py313-none-any.whl + - pypi: https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c5/71/c754b1737ad99102e03fa3235acb6cb6d3ac9d6f596cbc3e5f236705abd8/backrefs-6.2-py313-none-any.whl - pypi: https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-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/cd/3a/577b549de0cc09d95f11087ee63c739bba856cd3952697eec4c4bb91350a/bleach-6.3.0-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/cb/8c/2b30c12155ad8de0cf641d76a8b396a16d2c36bc6d50b621a62b7c4567c1/build-1.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4a/57/3b7d4dd193ade4641c865bc2b93aeeb71162e81fc348b8dad020215601ed/build-1.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/43/8e/278ea79cf8ee0c395a5ad74625b3465c2fc234bb277f171dd59dd203820d/bumps-1.0.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.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/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/91/d7/47988d40231b41376f5a66346ef3b322c81091dfd4c0f84df5a1e3bb06b5/chardet-7.4.0.post1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a1/5c/724b6b363603e419829f561c854b87ed7c7e31231a7908708ac086cdf3e2/charset_normalizer-3.4.6-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.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/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-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/45/0b/0768b4231d5a044da8f75e097a8714ae1041246bb765d6b5563bab456735/coverage-7.13.1-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/df/91/4a151c94320458895049a3e23b7b2cfc08953c60b14892de837e8eb51d0a/copier-9.14.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/52/e8/c14cc8af8cd38e86887053843382629bd8ebd117f83f15eb1194d65a2c9d/cryspy-0.10.0-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/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-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/fb/4e/931480b9552c7d0feebe40c73725dd7703dcc578ba9efc14fe0e6d31cfd1/debugpy-1.8.19-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/4a/f3/00bb1e867fba351e2d784170955713bee200c43ea306c59f30bd7e748192/dask-2026.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/13/f7/a0b368ce54ffff9e9028c098bd2d28cfc5b54f9f6c186929083d4c60ba58/debugpy-1.8.20-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/68/49/869b49db0bad6fba7c7471d612c356e745929d14e0de63acbc53e88f2a50/dfo_ls-1.6-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/1f/0c/6826cb2151628c59cca66ca6089ff910ab3ccd62b0524c2b398dc145ee52/diffpy_pdffit2-1.5.2-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/8e/52/39914bf42bb01901c91781def35c1aeffa431a59299e9748c0cfae3e5493/diffpy_structure-3.3.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/79/c8/f0f4ab7fd08950d5358579364a0a9b9198bf03e179a4fcceae4b353be32e/diffpy_utils-3.6.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/50/3d/9373ad9c56321fdab5b41197068e1d8c25883b3fea29dd361f9b55116869/dill-0.4.0-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/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/dc/b4/a7ec1eaee86761a9dbfd339732b4706db3c6b65e970c12f0f56cfcce3dcf/docformatter-1.7.7-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/82/ee/c9ca3f81d69ec6c99ca41e10432a58c60fef3a6e63cce6df7e796f90fe22/easydiffraction-0.10.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/50/98b146aea0f1cd7531d25f12bea69fa9ce8d1662124f93fb30dc4511b65e/docstring_parser_fork-0.0.14-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/87/10/2c7edbf230e5c507d38367af498fa94258ed97205d9b4b6f63a921fe9c49/dunamai-1.26.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/55/cd/d729a1bb63fa95387228cc508552dea4685ea0116e484e73238db10f9521/essdiffraction-26.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f1/bb/66c80d7f801b191f7b3ee6149a39be9d1a8a81c233e20adaf796d171f93a/essreduce-26.4.0-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/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cb/a8/20d0723294217e47de6d9e2e40fd4a9d2f7c4b6ef974babd482a59743694/fastjsonschema-2.21.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/9a/30/ab407e2ec752aa541704ed8f93c11e2a5d92c168b8a755d818b74a3c5c2d/filelock-3.20.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/1a/59/453c06d1d83dc0951b69ef692d6b9f1846680342927df54e9a1ca91c6f90/fonttools-4.61.1-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/a4/a5/842ae8f0c08b61d6484b52f99a03510a3a72d23141942d216ebe81fefbce/filelock-3.25.2-py3-none-any.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/33/b2/986d1220f6ee931e338d272bc1f3ec02cfe5f9b5fad84e95afdad57f1ebc/format_docstring-0.2.7-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cf/58/8acf1b3e91c58313ce5cb67df61001fc9dcd21be4fadb76c1a2d540e09ed/fqdn-1.5.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/19/f7/1a03aa6b06d26dbd1231b3ac907f8fdfcf02c0b27fc5f4c31493ad6ecdad/gemmi-0.7.4-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/e6/ab/fb21f4c939bb440104cc2b396d3be1d9b7a9fd3c6c2a53d98c45b3d7c954/fsspec-2026.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d5/08/c2409cb01d5368dcfedcbaffa7d044cc8957d57a9d0855244a5eb4709d30/funcy-2.0-py2.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/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/7e/71/ba21c3fb8c5dce83b8c01f458a42e99ffdb1963aeec08fff5a18588d8fd7/greenlet-3.3.0-cp313-cp313-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/9c/83/3b1d03d36f224edded98e9affd0467630fc09d766c0e56fb1498cbb04a9b/griffe-1.15.0-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/6a/09/e21df6aef1e1ffc0c816f0522ddc3f6dcded766c3261813131c78a704470/gitpython-3.1.46-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/39/5ef5aa23bc545aa0d31e1b9b55822b32c8da93ba657295840b6b34124009/greenlet-3.3.2-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/4b/4c/cc8c68196db727cfc1432f2ad5de50aa6707e630d44b2e6361dc06d8f134/griffelib-2.0.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e5/ea/fbb258a98863f99befb10ed727152b4ae659f322e1d9c0576f8a62754e81/h5py-3.15.1-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/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/0f/1c/e5fd8f973d4f375adb21565739498e2e9a1e54c858a97b9a8ccfdc81da9b/identify-2.6.15-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/46/33/92ef41c6fad0233e41d3d84ba8e8ad18d1780f1e5d99b3c683e6d7f98b63/identify-2.6.18-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a4/ed/1f1afb2e9e7f38a545d628f864d562a5ae64fe6f7a10e28ffb9b185b4e89/importlib_resources-6.5.2-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/12/c9/6869a1dcf4aaf309b9543ec070be3ec3adebee7c9bec9af8c230494134b9/interrogate-1.7.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f6/d8/502954a4ec0efcf264f99b65b41c3c54e65a647d9f0d6f62cd02227d242c/ipykernel-6.31.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/86/92/162cfaee4ccf370465c5af1ce36a9eacec1becb552f2033bb3584e6f640a/ipython-9.9.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/82/b9/e73d5d9f405cba7706c539aa8b311b49d4c2f3d698d9c12f815231169c71/ipykernel-7.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b2/90/45c72becc57158facc6a6404f663b77bbcea2519ca57f760e2879ae1315d/ipython-9.11.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-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/7b/55/e5326141505c5d5e34c5e0935d2908a74e4561eca44108fbfb9c13d2911a/isoduration-20.11.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/72/b9/313e8f2f2e9517ae050a692ae7b3e4b3f17cc5e6dfea0db51fe14e586580/jinja2_ansible_filters-1.3.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d7/9e/038522f50ceb7e74f1f991bf1b699f24b0c2bbe7c390dd36ad69f4582258/json5-0.13.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/71/92/5e77f98553e9e75130c78900d000368476aed74276eb8ae8796f65f00918/jsonpointer-3.0.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9e/6a/a83720e953b1682d2d109d3c2dbb0bc9bf28cc1cbc205be4ef4be5da709d/jsonpointer-3.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/bb/f5/fddaec430367be9d62a7ed125530e133bfd4a1c0350fe221149ee0f2b526/jupyter_client-8.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2d/0b/ceb7694d864abc0a047649aec263878acb9f792e1fec3e676f22dc9015e3/jupyter_client-8.8.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/76/c0/8cc378aad5ccfcc37adeda27d3864f20ea808ad5b63572f5b986a42a649c/jupyter_dark_detect-0.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e2/48/577993f1f99c552f18a0428731a755e06171f9902fa118c379eb7c04ea22/jupyter_events-0.12.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1a/60/1f6cee0c46263de1173894f0fafcb3475ded276c472c14d25e0280c18d6d/jupyter_lsp-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f4/a4/61adb19f3c74b0dc0e411de4f06ebef564b1f179928f9dffcbd4b378f2ef/jupyter_notebook_parser-0.1.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/92/80/a24767e6ca280f5a49525d987bf3e4d7552bf67c8be07e8ccf20271f8568/jupyter_server-2.17.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/07/2d/2b32cdbe8d2a602f697a649798554e4f072115438e92249624e532e8aca6/jupyter_server_terminals-0.5.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/af/c3/acced767eecc11a70c65c45295db5396c4f0c1937874937d5a76d7b177b6/jupyterlab-4.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d1/2d/6674563f71c6320841fc300911a55143925112a72a883e2ca71fba4c618d/jupyter_server_terminals-0.5.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e1/1b/dad6fdcc658ed7af26fdf3841e7394072c9549a8b896c381ab49dd11e2d9/jupyterlab-4.5.6-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b1/dd/ead9d8ea85bf202d90cc513b533f9c363121c7792674f78e0d8a854b63b4/jupyterlab_pygments-0.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e0/07/a000fe835f76b7e1143242ab1122e6362ef1c03f23f83a045c38859c2ae0/jupyterlab_server-2.28.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f7/5a/488f492b49b712ca38326e12c78d0d48ab6be682a2989ce533f0f2c4dfd2/jupyterquiz-2.9.6.2-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/bd/0d/2d240e7098e0cafba4d25e9530e7596b1bb1bd4476e41b10346bcaaa36d6/jupytext-1.18.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/75/bd/f1a5d894000941739f2ae1b65a32892349423ad49c2e6d0771d0bad3fae4/kiwisolver-1.4.9-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/ab/b5/36c712098e6191d1b4e349304ef73a8d06aed77e56ceaac8c0a306c7bda1/jupyterlab_widgets-3.0.16-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/52/bc858b1665d0dec3a2511f4e6f5c18ea85c0977563d624d597c95d6d0fd7/jupyterquiz-2.9.6.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/16/5a/736dd2f4535dbf3bf26523f9158c011389ef88dd06ec2eef67fd744f1c7b/jupytext-1.19.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/be/8a/be60e3bbcf513cc5a50f4a3e88e1dcecebb79c1ad607a7222877becaa101/kiwisolver-1.5.0-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/82/3d/14ce75ef66813643812f3093ab17e46d3a206942ce7376d31ec2d36229e7/lark-1.3.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/38/7e/7b91c89a4cf0f543a83be978657afb20c86af6d725253e319589dcc4ce52/lmfit-1.3.4-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/d2/f0/834e479e47e499b6478e807fb57b31cc2db696c4db30557bb6f5aea4a90b/mando-0.7.1-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/70/81/54e3ce63502cd085a0c556652a4e1b919c45a446bd1e5300e10c44c8c521/markdown-3.10-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/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/64/40/37612487cc8a437d4dd261b32ca21fe2d79510fe74af74e1f42becb1bdb8/matplotlib-3.10.8-cp313-cp313-win_amd64.whl @@ -923,269 +1072,314 @@ environments: - 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/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/fd/1a/31b7cd6e4e7a02df4e076162e9783620777592bea9e4bb036389389af99d/mike-2.1.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/48/f7/10f5e101db25741b91e4f4792c5d97b4fa834ead5cf509ae91097d939424/mike-2.1.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9b/f7/4a5e785ec9fbd65146a27b6b70b6cdc161a66f2024e4b04ac06a67f5578b/mistune-3.2.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/71/26/4d39d52ea2219604053a4d05b98e90d6a335511cc01806436ec4886b1028/mkdocs_autorefs-1.2.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/9f/d4/029f984e8d3f3b6b726bd33cafc473b75e9e44c0f7e80a5b29abc466bdea/mkdocs_get_deps-0.2.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/08/37/5f1fd5c3f6954b3256f8126275e62af493b96fb6aef6c0dbc4ee326032ad/mkdocs_jupyter-0.25.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/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/93/89/eb601278b12c471235860992f5973cf3c55ca3f77d1d6127389eb045a021/mkdocs_jupyter-0.26.1-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/3e/32/ed071cb721aca8c227718cffcf7bd539620e9799bbf2619e90c757bfd030/mkdocs_material-9.7.1-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/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.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/cd/10/4c27c3063c2b3681a4b7942f8dbdeb4fa34fecb2c19b594e7345ebf4f86f/mkdocstrings-0.27.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/51/23/d02d86553327296c3bf369d444194ea83410cce8f0e690565264f37f3261/mkdocstrings_python-1.13.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/04/41/1cf02e3df279d2dd846a1bf235a928254eba9006dd22b4a14caa71aed0f7/mkdocstrings-1.0.3-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/5b/69/93b34728cc386efdde0c342f8c680b9187dea7beb7adaf6b58a0713be101/mpld3-0.5.12-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/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-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/f1/25/5e8080fe0117f799b1b68008dc29a65862077296b92550632de015128579/msgspec-0.20.0-cp313-cp313-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/32/31/75c59e7d3b4205075b4c183fa4ca398a2daf2303ddf616b04ae6ef55cffe/multidict-6.7.0-cp313-cp313-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/3d/2e/cf2ffeb386ac3763526151163ad7da9f1b586aac96d2b4f7de1eaebf0c61/narwhals-2.15.0-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/3f/c3/06490e98393dcb4d6ce2bf331a39335375c300afaef526897881fbeae6ab/narwhals-2.18.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/83/a0/5b0c2f11142ed1dddec842457d3f65eaf71a0080894eb6f018755b319c3a/nbclient-0.10.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/cc/9a/cd673b2f773a12c992f41309ef81b99da1690426bd2f96957a7ade0d3ed7/nbconvert-7.16.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0d/4b/8d5f796a792f8a25f6925a96032f098789f448571eb92011df1ae59e8ea8/nbconvert-7.17.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a9/82/0340caa499416c78e5d8f5f05947ae4bc3cba53c9f038ab6e9ed964e22f1/nbformat-5.10.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/eb/be/b257e12f9710819fde40adc972578bee6b72c5992da1bc8369bef2597756/nbmake-1.5.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/28/88/4789719fbbe166d12d345b3ac66b96105f10001b16e00a9765ba29261a21/nbqa-1.9.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/12/e5/838eb1004fb9b6f0383c47c0d902eb698fda052e8e64ca48c70a2144a48c/nbstripout-0.8.2-py2.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/fc/39/dfdef4cfbfcf7c80cd144b1b2e262c2e785f18b152eeebe14ecd70ee7455/ncrystal-4.2.12-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7b/51/e13a37a8d924feefb444d7eb42094750ba1bba756cbb8c1f9a523414c4fb/ncrystal_core-4.2.12-py3-none-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/78/98/20fdd6825d1876fc54bb098509e736073d20f569b033f2f23bf74aa1df9c/ncrystal_python-4.2.12-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.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/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f9/33/bd5b9137445ea4b680023eb0469b2bb969d61303dedb2aac6560ff3d14a1/notebook_shim-0.2.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/0c/88/e2eaa6cffb115b85ed7c7c87775cb8bcf0816816bc98ca8dbfa2ee33fe6e/numpy-2.3.5-cp313-cp313-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b1/3c/88af0040119209b9b5cb59485fa48b76f372c73068dbf9254784b975ac53/numpy-2.4.3-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/4f/c7/e54682c96a895d0c808453269e0b5928a07a127a15704fedb643e9b0a4c8/pandas-2.3.3-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/d6/7d/216a1588b65a7aa5f4535570418a599d943c85afb1d95b0876fc00aa1468/pandas-3.0.1-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/ef/af/4fbc8cab944db5d21b7e2a5b8e9211a03a79852b1157e2c102fcc61ac440/pandocfilters-1.5.1-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/16/32/f8e3c85d1d5250232a5d3477a2a28cc291968ff175caeadaf3cc19ce0e4a/parso-0.8.5-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/05/bb/39e6768529454cc2b57e1e2fa0a0a18ff64397a16303270e215a3e03285f/pathspec-1.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e5/07/74a9d941fa45c90a0d9465098fe1ec85de3e2afbdc15cc4766622d516056/pillow-12.1.0-cp313-cp313-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/44/3c/d717024885424591d5376220b5e836c2d5293ce2011523c9de23ff7bf068/pip-25.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b6/61/fae042894f4296ec49e3f193aff5d7c18440da9e48102c3315e1bc4519a7/parso-0.8.6-py2.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/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3f/eb/b0834ad8b583d7d9d42b80becff092082a1c3c156bb582590fcc973f1c7c/pillow-12.1.1-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/c4/36/ce5f75aa7c736a663a901766edc3580098c7ea3959a0e878363a54a3714e/pixi_kernel-0.7.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e7/c3/3031c931098de393393e1f93a38dc9ed6805d86bb801acc3cf2d5bd1e6b7/plotly-6.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/63/d7/97f7e3a6abb67d8080dd406fd4df842c2be0efaf712d1c899c32a075027c/platformdirs-4.9.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/31/8b/9e8baf7dacac8d0c174925c38ff43c6d94bc9abb35503f67762caccb6869/plopp-26.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/52/d2/c6e44dba74f17c6216ce1b56044a9b93a929f1c2d5bdaff892512b260f5e/plotly-6.6.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/79/ad/45312df6b63ba64ea35b8d8f5f0c577aac16e6b416eafe8e1cb34e03f9a7/plumbum-1.10.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/a8/87/77cc11c7a9ea9fd05503def69e3d18605852cd0d4b0d3b8f15bbeb3ef1d1/pooch-1.8.2-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/5d/19/fd3ef348460c80af7bb4669ea7926651d1f95c23ff2df18b9d24bab4f3fa/pre_commit-4.5.1-py2.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/b8/db/14bafcb4af2139e046d03fd00dea7873e48eafe18b7d2797e73d6681f210/prometheus_client-0.23.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/74/c3/24a2f845e3917201628ecaba4f18bab4d18a337834c1df2a159ee9d22a42/prometheus_client-0.24.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-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/34/68/d9317542e3f2b180c4306e3f45d3c922d7e86d8ce39f941bb9e2e9d8599e/psutil-7.2.1-cp37-abi3-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-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/0b/a6/e2e8535d8146bce05de6e0ecf1099a7e2887d840ae2a7b3a09385543fd02/py3dmol-2.5.3-py2.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/9f/9b/50835e8fd86073fa7aa921df61b4cebc1f0ff400e4338541675cb72b5507/pycifrw-5.0.1-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/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/87/6f/cc2b231dc78d8c3aaa674a676db190b8f8071c50134af8f8cf39b9b8e8df/pydoclint-0.8.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ea/10/47caf89cbb52e5bb764696fd52a8c591a2f0e851a93270c05a17f36000b5/pymdown_extensions-10.20-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/8b/40/2614036cdd416452f5bf98ec037f38a1afb17f327cb8e6b652d4729e0af8/pyparsing-3.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6f/2c/5b079febdc65e1c3fb2729bf958d18b45be7113828528e8a0b5850dd819a/pymdown_extensions-10.21-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.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/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/50/74/c655a6eda0fd188d490c14142a0f0380655ac7099604e1fbf8fa1a97f0a1/python_engineio-4.13.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/67/0f/019d3949a40280f6193b62bc010177d4ce702d0fce424322286488569cd3/python_discovery-1.2.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/aa/54/0cce26da03a981f949bb8449c9778537f75f5917c172e1d2992ff25cb57d/python_engineio-4.13.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/51/e5/fecf13f06e5e5f67e8837d777d1bc43fac0ed2b77a676804df5c34744727/python_json_logger-4.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/28/d2/2ccc2b69a187b80fda3152745670cfba936704f296a9fa54c6c8ac694d12/python_socketio-5.16.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/fc/19/b757fe28008236a4a713e813283721b8a40aa60cd7d3f83549f2e25a3155/pywinpty-3.0.2-cp313-cp313-win_amd64.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/d8/8b/e2bbeb42068f0c48899e8eddd34902afc0f7429d4d2a152d2dc2670dc661/pythreejs-2.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/e5/cb/58d6ed3fd429c96a90ef01ac9a617af10a6d41469219c25e7dc162abbb71/pywinpty-3.0.3-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-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/fc/7f/a21b20d577e4100c6a41795842028235998a643b1ad406a6d4163ea8f53e/pyzmq-27.1.0-cp312-abi3-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/3c/26/1062c7ec1b053db9e499b4d2d5bc231743201b74051c973dadeac80a8f43/questionary-2.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/93/f7/d00d9b4a0313a6be3a3e0818e6375e15da6d7076f4ae47d1324e7ca986a1/radon-6.0.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/56/5d/c814546c2333ceea4ba42262d8c4d55763003e767fa169adc693bd524478/requests-2.33.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/57/4d/a7545bf6c62b0dbe5795f22ea9e88cc070fdced5c34663ebc5bed2f610c0/returns-0.26.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9e/51/17023c0f8f1869d8806b979a2bffa3f861f26a3f1a66b094288323fba52f/rfc3986_validator-0.1.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7e/71/44ce230e1b7fadd372515a97e32a83011f906ddded8d03e3c6aafbdedbb7/rfc3987_syntax-1.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/fb/9c/896c862e13886fae2af961bef3e6312db9ebc6adc2b156fe95e615dee8c1/ruff-0.14.10-py3-none-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/cd/01/1204382461fcbfeb05b6161b594f4007e78b6eba9b375382f79153172b4d/scipy-1.16.3-cp313-cp313-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/1b/5a/f2f2e5eda25579f754acd83399c522ee03d6acbe001dfe53c8a1ec928b44/send2trash-2.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1f/a2/ef467cb77099062317154c63f234b8a7baf7cb690b99af760c5b68b9ee7f/ruff-0.15.8-py3-none-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/af/46/661159ad844034ba8b3f4e0516215c41e4ee17db4213d13a82227670764f/sciline-25.11.1-py3-none-any.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/fb/46/e50b38629e9e3f4a1dd55fb36d8b8abd1d59768c31151c1c8ed696f7b865/scippneutron-26.3.0-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/35/e5/d6d0e51fc888f692a35134336866341c08655d92614f492c6860dc45bb2c/scipy-1.17.1-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/1c/78/504fdd027da3b84ff1aecd9f6957e65f35134534ccc6da8628eb71e76d3f/send2trash-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9d/76/f789f7a86709c6b087c5a2f52f911838cad707cc613162401badc665acfe/setuptools-82.0.1-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/52/59/0782e51887ac6b07ffd1570e0364cf901ebc36345fea669969d2084baebb/simple_websocket-1.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/48/f3/b67d6ea49ca9154453b6d70b34ea22f3996b9fa55da105a79d8732227adc/soupsieve-2.8.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/c0/c5/d17113020b2d43073412aeca09b60d2009442420372123b8d49cc253f8b8/sqlalchemy-2.0.45-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/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-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/16/56/a31e8d3c9e8d21100b83bbe1c1f3f7c94db317393a229e193461e5e6d2a4/spglib-2.6.0-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/b7/2b/b9040bec58c58225f073f5b0c1870defe1940835549dafec680cbd58c3c3/sqlalchemy-2.0.48-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-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/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-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/6a/9e/2064975477fdc887e47ad42157e214526dcad8f317a948dee17e1659a62f/terminado-0.18.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e6/34/ebdc18bae6aa14fbee1a08b63c015c72b64868ff7dae68808ab500c492e2/tinycss2-1.4.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/33/f0/3fe8c6e69135a845f4106f2ff8b6805638d4e85c264e70114e8126689587/tokenize_rt-6.2.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b2/b7/718cd1da0884f281f95ccfa3a6cc572d30053cba64603f79d431d3c9b61b/tomli-2.3.0-cp313-cp313-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/d6/6d/c69be695a0a64fd37a97db12355a035a6d90f79067a3cf936ec2b1dc38cd/tornado-6.5.4-cp39-abi3-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2c/23/f6c6112a04d28eed765e374435fb1a9198f73e1ec4b4024184f21faeb1ad/tornado-6.5.5-cp39-abi3-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/4f/7e/bc19996fa86cad8801e8ffe6f1bba5836ca0160df76d0410d27432193712/trove_classifiers-2025.12.1.14-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/1b/a9/e3aee762739c1d7528da1c3e06d518503f8b6c439c35549b53735ba52ead/typeguard-4.4.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a0/1d/d9257dd49ff2ca23ea5f132edf1281a0c4f9de8a762b9ae399b670a59235/typer-0.21.1-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/bb/4a/2e5583e544bc437d5e8e54b47db87430df9031b29b48d17f26d129fa60c0/trove_classifiers-2026.1.14.14-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/4a/91/48db081e7a63bb37284f9fbcefda7c44c277b18b0e13fbc36ea2335b71e6/typer-0.24.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8f/5e/f1e1dd319e35e962a4e00b33150a8868b6329cc1d19fd533436ba5488f09/uncertainties-3.2.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f7/46/e7cea8159199096e1df52da20a57a6665da80c37fb8aeb848a3e47442c32/untokenize-0.1.1.tar.gz - pypi: https://files.pythonhosted.org/packages/e7/00/3fca040d7cf8a32776d3d81a00c8ee7457e00f80c649f1e4a863c8321ae9/uri_template-1.3.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/6d/b9/4095b668ea3678bf6a0af005527f39de12fb026516fb3df17495a733b7f8/urllib3-2.6.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/32/49/9e3e19ba756c4a5e6acb4ea74336d3035f7959254fbb05f5eb77bff067ed/uv-0.9.22-py3-none-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/a4/39/6983dd79f01aaa4c75d9ffa550fa393f0c4c28f7ccd6956e4188c62cefbc/validate_pyproject-0.24.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/ee/e9c95cda829131f71a8dff5ce0406059fd16e591c074414e31ada19ba7c3/validate_pyproject-0.25-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/bc/4a/c6fd02a642bbe4e9f25cdd3714a328e3fc3eb6bd7b5e96f1a2285bd928b9/varname-0.15.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1c/59/964ecb8008722d27d8a835baea81f56a91cea8e097b3be992bc6ccde6367/versioningit-3.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a4/ce/3b6fee91c85626eaf769d617f1be9d2e15c1cca027bbdeb2e0d751469355/verspec-0.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/79/0c/c05523fa3181fdf0c9c52a6ba91a23fbf3246cc095f26f6516f9c60e6771/virtualenv-20.35.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c6/59/7d02447a55b2e55755011a647479041bc92a82e143f96a8195cb33bd0a1c/virtualenv-21.2.0-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/af/b5/123f13c975e9f27ab9c0770f514345bd406d0e8d3b7a0723af9d43f710af/wcwidth-0.2.14-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/68/5a/199c59e0a824a3db2b89c5d2dade7ab5f9624dbf6448dc291b46d5ec94d3/wcwidth-0.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e2/cc/e097523dd85c9cf5d354f78310927f1656c422bd7b2613b2db3e3f9a0f2c/webcolors-25.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/34/db/b10e48aa8fff7407e67470363eac595018441cf32d5e1001567a7aeba5d2/websocket_client-1.9.0-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/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-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/9a/ee/450914ae11b419eadd067c6183ae08381cfdfcb9798b90b2b713bbebddda/yarl-1.22.0-cp313-cp313-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl - py311-dev: + - pypi: https://files.pythonhosted.org/packages/7a/84/266e8da36879c6edcd37b02b547e2d9ecdfea776be49598e75696e3316e1/yarl-1.23.0-cp313-cp313-win_amd64.whl + - pypi: ./ + py-311-env: channels: - url: https://conda.anaconda.org/conda-forge/ indexes: - https://pypi.org/simple + options: + pypi-prerelease-mode: if-necessary-or-explicit packages: linux-64: - - conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_8.conda + - 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/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/noarch/ca-certificates-2026.1.4-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.2.25-hbd8a1cb_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/gsl-2.8-hbf7d49c_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/icu-75.1-he02047a_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45-default_hbd61a6d_105.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libabseil-20250512.1-cxx17_hba17884_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libblas-3.11.0-5_h4a7cf45_openblas.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/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/libblas-3.11.0-6_h4a7cf45_openblas.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/libcblas-3.11.0-5_h0358290_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.11.0-6_h0358290_openblas.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.7.3-hecca717_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h9ec8514_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-he0feb66_16.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.2.0-h69a702a_16.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.2.0-h69a702a_16.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.2.0-h68bc16d_16.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-he0feb66_16.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libnghttp2-1.67.0-had1ee68_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.4-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/libgfortran-15.2.0-h69a702a_18.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.2.0-h68bc16d_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.2-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/libopenblas-0.3.30-pthreads_h94d23a6_4.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.51.1-h0c1763c_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h934c35e_16.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-15.2.0-hdf11a46_16.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.32-pthreads_h94d23a6_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.52.0-hf4e2dac_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.41.3-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.1-hb9d3cd8_2.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/ncurses-6.5-h2d0b736_3.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/nodejs-25.2.1-he2c55a7_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.0-h26f9b46_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.11.14-hd63d673_2_cpython.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.1-h35e630c_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/readline-8.3-h853b02a_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_ha0e22de_103.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/noarch/tzdata-2025c-hc9c84f9_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda - pypi: https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a1/7e/6815aab7d3a56610891c76ef79095677b8b5be6646aaf00f69b221765021/aiohttp-3.13.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_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/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.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/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/09/52/94108adfdd6e2ddf58be64f959a0b9c7d4ef2fa71086c38356d22dc501ea/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/ed/c9/d7977eaacb9df673210491da99e6a247e93df98c715fc43fd136ce1d3d33/arrow-1.4.0-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/6c/25/4f103d1bedb3593718713b3f743df7b3ff3fc68d36d6666c30265ef59c8a/ase-3.28.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/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/03/49/d10027df9fce941cb8184e78a02857af36360d33e1721df81c5ed2179a1a/async_lru-2.0.5-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e5/e2/c2e3abf398f80732e58b03be77bde9022550d221dd8781bf586bd4d97cc1/async_lru-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.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/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e6/9a/8da246d988ded941da96c7ed945d63e94a445637eaad985a0ed88787cb89/backrefs-6.1-py311-none-any.whl + - pypi: https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0f/f0/35240571e1b67ffb19dafb29ab34150b6f59f93f717b041082cdb1bfceb1/backrefs-6.2-py311-none-any.whl - pypi: https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-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/cd/3a/577b549de0cc09d95f11087ee63c739bba856cd3952697eec4c4bb91350a/bleach-6.3.0-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/cb/8c/2b30c12155ad8de0cf641d76a8b396a16d2c36bc6d50b621a62b7c4567c1/build-1.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4a/57/3b7d4dd193ade4641c865bc2b93aeeb71162e81fc348b8dad020215601ed/build-1.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/43/8e/278ea79cf8ee0c395a5ad74625b3465c2fc234bb277f171dd59dd203820d/bumps-1.0.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_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/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/e3/a2/dab58511fbeef06dd88866568ea1a11b2f15654223cafc2681e2da84b1f2/chardet-7.4.0.post1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/60/ac/3233d262a310c1b12633536a07cde5ddd16985e6e7e238e9f3f9423d8eb9/charset_normalizer-3.4.6-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.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/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-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/f7/7c/347280982982383621d29b8c544cf497ae07ac41e44b1ca4903024131f55/coverage-7.13.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/df/91/4a151c94320458895049a3e23b7b2cfc08953c60b14892de837e8eb51d0a/copier-9.14.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/52/e8/c14cc8af8cd38e86887053843382629bd8ebd117f83f15eb1194d65a2c9d/cryspy-0.10.0-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/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-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/25/3e/e27078370414ef35fafad2c06d182110073daaeb5d3bf734b0b1eeefe452/debugpy-1.8.19-py2.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/e0/c3/7f67dea8ccf8fdcb9c99033bbe3e90b9e7395415843accb81428c441be2d/debugpy-1.8.20-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/68/49/869b49db0bad6fba7c7471d612c356e745929d14e0de63acbc53e88f2a50/dfo_ls-1.6-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/ee/af/3b86dbd18d8dab5646f5b7c7db5bd9c43108e093864032aabd35d41b127d/diffpy_pdffit2-1.5.2.tar.gz - pypi: https://files.pythonhosted.org/packages/8e/52/39914bf42bb01901c91781def35c1aeffa431a59299e9748c0cfae3e5493/diffpy_structure-3.3.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/79/c8/f0f4ab7fd08950d5358579364a0a9b9198bf03e179a4fcceae4b353be32e/diffpy_utils-3.6.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/50/3d/9373ad9c56321fdab5b41197068e1d8c25883b3fea29dd361f9b55116869/dill-0.4.0-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/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/dc/b4/a7ec1eaee86761a9dbfd339732b4706db3c6b65e970c12f0f56cfcce3dcf/docformatter-1.7.7-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/82/ee/c9ca3f81d69ec6c99ca41e10432a58c60fef3a6e63cce6df7e796f90fe22/easydiffraction-0.10.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/50/98b146aea0f1cd7531d25f12bea69fa9ce8d1662124f93fb30dc4511b65e/docstring_parser_fork-0.0.14-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/87/10/2c7edbf230e5c507d38367af498fa94258ed97205d9b4b6f63a921fe9c49/dunamai-1.26.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/55/cd/d729a1bb63fa95387228cc508552dea4685ea0116e484e73238db10f9521/essdiffraction-26.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f1/bb/66c80d7f801b191f7b3ee6149a39be9d1a8a81c233e20adaf796d171f93a/essreduce-26.4.0-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/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cb/a8/20d0723294217e47de6d9e2e40fd4a9d2f7c4b6ef974babd482a59743694/fastjsonschema-2.21.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/9a/30/ab407e2ec752aa541704ed8f93c11e2a5d92c168b8a755d818b74a3c5c2d/filelock-3.20.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/79/61/1ca198af22f7dd22c17ab86e9024ed3c06299cfdb08170640e9996d501a0/fonttools-4.61.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/a4/a5/842ae8f0c08b61d6484b52f99a03510a3a72d23141942d216ebe81fefbce/filelock-3.25.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/cc/a1/40a5c4d8e28b0851d53a8eeeb46fbd73c325a2a9a165f290a5ed90e6c597/fonttools-4.62.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/33/b2/986d1220f6ee931e338d272bc1f3ec02cfe5f9b5fad84e95afdad57f1ebc/format_docstring-0.2.7-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cf/58/8acf1b3e91c58313ce5cb67df61001fc9dcd21be4fadb76c1a2d540e09ed/fqdn-1.5.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/11/b1/71a477adc7c36e5fb628245dfbdea2166feae310757dea848d02bd0689fd/frozenlist-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/96/ae/41aff180c36dd3c8f0a84faf38ac8683f8dca99250abcfbc3ed19897290b/gemmi-0.7.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/e6/ab/fb21f4c939bb440104cc2b396d3be1d9b7a9fd3c6c2a53d98c45b3d7c954/fsspec-2026.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d5/08/c2409cb01d5368dcfedcbaffa7d044cc8957d57a9d0855244a5eb4709d30/funcy-2.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/48/eb/46e443fc70b4aabe6e775521ff476aefb051db9acabb16a5cb51f04e3e2b/gemmi-0.7.5-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.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/dc/a6/e959a127b630a58e23529972dbc868c107f9d583b5a9f878fb858c46bc1a/greenlet-3.3.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/9c/83/3b1d03d36f224edded98e9affd0467630fc09d766c0e56fb1498cbb04a9b/griffe-1.15.0-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/6a/09/e21df6aef1e1ffc0c816f0522ddc3f6dcded766c3261813131c78a704470/gitpython-3.1.46-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/72/83/3e06a52aca8128bdd4dcd67e932b809e76a96ab8c232a8b025b2850264c5/greenlet-3.3.2-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/4b/4c/cc8c68196db727cfc1432f2ad5de50aa6707e630d44b2e6361dc06d8f134/griffelib-2.0.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/8b/23/4ab1108e87851ccc69694b03b817d92e142966a6c4abd99e17db77f2c066/h5py-3.15.1-cp311-cp311-manylinux_2_27_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/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/0f/1c/e5fd8f973d4f375adb21565739498e2e9a1e54c858a97b9a8ccfdc81da9b/identify-2.6.15-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/46/33/92ef41c6fad0233e41d3d84ba8e8ad18d1780f1e5d99b3c683e6d7f98b63/identify-2.6.18-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a4/ed/1f1afb2e9e7f38a545d628f864d562a5ae64fe6f7a10e28ffb9b185b4e89/importlib_resources-6.5.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/38/3d/2d244233ac4f76e38533cfcb2991c9eb4c7bf688ae0a036d30725b8faafe/importlib_metadata-9.0.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/12/c9/6869a1dcf4aaf309b9543ec070be3ec3adebee7c9bec9af8c230494134b9/interrogate-1.7.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f6/d8/502954a4ec0efcf264f99b65b41c3c54e65a647d9f0d6f62cd02227d242c/ipykernel-6.31.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/86/92/162cfaee4ccf370465c5af1ce36a9eacec1becb552f2033bb3584e6f640a/ipython-9.9.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/82/b9/e73d5d9f405cba7706c539aa8b311b49d4c2f3d698d9c12f815231169c71/ipykernel-7.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3d/aa/898dec789a05731cd5a9f50605b7b44a72bd198fd0d4528e11fc610177cc/ipython-9.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-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/7b/55/e5326141505c5d5e34c5e0935d2908a74e4561eca44108fbfb9c13d2911a/isoduration-20.11.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/72/b9/313e8f2f2e9517ae050a692ae7b3e4b3f17cc5e6dfea0db51fe14e586580/jinja2_ansible_filters-1.3.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d7/9e/038522f50ceb7e74f1f991bf1b699f24b0c2bbe7c390dd36ad69f4582258/json5-0.13.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/71/92/5e77f98553e9e75130c78900d000368476aed74276eb8ae8796f65f00918/jsonpointer-3.0.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9e/6a/a83720e953b1682d2d109d3c2dbb0bc9bf28cc1cbc205be4ef4be5da709d/jsonpointer-3.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/bb/f5/fddaec430367be9d62a7ed125530e133bfd4a1c0350fe221149ee0f2b526/jupyter_client-8.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2d/0b/ceb7694d864abc0a047649aec263878acb9f792e1fec3e676f22dc9015e3/jupyter_client-8.8.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/76/c0/8cc378aad5ccfcc37adeda27d3864f20ea808ad5b63572f5b986a42a649c/jupyter_dark_detect-0.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e2/48/577993f1f99c552f18a0428731a755e06171f9902fa118c379eb7c04ea22/jupyter_events-0.12.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1a/60/1f6cee0c46263de1173894f0fafcb3475ded276c472c14d25e0280c18d6d/jupyter_lsp-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f4/a4/61adb19f3c74b0dc0e411de4f06ebef564b1f179928f9dffcbd4b378f2ef/jupyter_notebook_parser-0.1.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/92/80/a24767e6ca280f5a49525d987bf3e4d7552bf67c8be07e8ccf20271f8568/jupyter_server-2.17.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/07/2d/2b32cdbe8d2a602f697a649798554e4f072115438e92249624e532e8aca6/jupyter_server_terminals-0.5.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/af/c3/acced767eecc11a70c65c45295db5396c4f0c1937874937d5a76d7b177b6/jupyterlab-4.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d1/2d/6674563f71c6320841fc300911a55143925112a72a883e2ca71fba4c618d/jupyter_server_terminals-0.5.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e1/1b/dad6fdcc658ed7af26fdf3841e7394072c9549a8b896c381ab49dd11e2d9/jupyterlab-4.5.6-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b1/dd/ead9d8ea85bf202d90cc513b533f9c363121c7792674f78e0d8a854b63b4/jupyterlab_pygments-0.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e0/07/a000fe835f76b7e1143242ab1122e6362ef1c03f23f83a045c38859c2ae0/jupyterlab_server-2.28.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f7/5a/488f492b49b712ca38326e12c78d0d48ab6be682a2989ce533f0f2c4dfd2/jupyterquiz-2.9.6.2-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/bd/0d/2d240e7098e0cafba4d25e9530e7596b1bb1bd4476e41b10346bcaaa36d6/jupytext-1.18.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/66/e1/e533435c0be77c3f64040d68d7a657771194a63c279f55573188161e81ca/kiwisolver-1.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/ab/b5/36c712098e6191d1b4e349304ef73a8d06aed77e56ceaac8c0a306c7bda1/jupyterlab_widgets-3.0.16-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/52/bc858b1665d0dec3a2511f4e6f5c18ea85c0977563d624d597c95d6d0fd7/jupyterquiz-2.9.6.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/16/5a/736dd2f4535dbf3bf26523f9158c011389ef88dd06ec2eef67fd744f1c7b/jupytext-1.19.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/80/46/bddc13df6c2a40741e0cc7865bb1c9ed4796b6760bd04ce5fae3928ef917/kiwisolver-1.5.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - pypi: https://files.pythonhosted.org/packages/82/3d/14ce75ef66813643812f3093ab17e46d3a206942ce7376d31ec2d36229e7/lark-1.3.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/38/7e/7b91c89a4cf0f543a83be978657afb20c86af6d725253e319589dcc4ce52/lmfit-1.3.4-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/d2/f0/834e479e47e499b6478e807fb57b31cc2db696c4db30557bb6f5aea4a90b/mando-0.7.1-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/70/81/54e3ce63502cd085a0c556652a4e1b919c45a446bd1e5300e10c44c8c521/markdown-3.10-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/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/8f/a0/7024215e95d456de5883e6732e708d8187d9753a21d32f8ddb3befc0c445/matplotlib-3.10.8-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl @@ -1193,259 +1387,304 @@ environments: - 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/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/fd/1a/31b7cd6e4e7a02df4e076162e9783620777592bea9e4bb036389389af99d/mike-2.1.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/48/f7/10f5e101db25741b91e4f4792c5d97b4fa834ead5cf509ae91097d939424/mike-2.1.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9b/f7/4a5e785ec9fbd65146a27b6b70b6cdc161a66f2024e4b04ac06a67f5578b/mistune-3.2.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/71/26/4d39d52ea2219604053a4d05b98e90d6a335511cc01806436ec4886b1028/mkdocs_autorefs-1.2.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/9f/d4/029f984e8d3f3b6b726bd33cafc473b75e9e44c0f7e80a5b29abc466bdea/mkdocs_get_deps-0.2.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/08/37/5f1fd5c3f6954b3256f8126275e62af493b96fb6aef6c0dbc4ee326032ad/mkdocs_jupyter-0.25.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/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/93/89/eb601278b12c471235860992f5973cf3c55ca3f77d1d6127389eb045a021/mkdocs_jupyter-0.26.1-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/3e/32/ed071cb721aca8c227718cffcf7bd539620e9799bbf2619e90c757bfd030/mkdocs_material-9.7.1-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/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.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/cd/10/4c27c3063c2b3681a4b7942f8dbdeb4fa34fecb2c19b594e7345ebf4f86f/mkdocstrings-0.27.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/51/23/d02d86553327296c3bf369d444194ea83410cce8f0e690565264f37f3261/mkdocstrings_python-1.13.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/04/41/1cf02e3df279d2dd846a1bf235a928254eba9006dd22b4a14caa71aed0f7/mkdocstrings-1.0.3-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/5b/69/93b34728cc386efdde0c342f8c680b9187dea7beb7adaf6b58a0713be101/mpld3-0.5.12-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/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-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/6b/96/5c095b940de3aa6b43a71ec76275ac3537b21bd45c7499b5a17a429110fa/msgspec-0.20.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/a8/9f/78f8761c2705d4c6d7516faed63c0ebdac569f6db1bef95e0d5218fdc146/multidict-6.7.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/3d/2e/cf2ffeb386ac3763526151163ad7da9f1b586aac96d2b4f7de1eaebf0c61/narwhals-2.15.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5a/56/21b27c560c13822ed93133f08aa6372c53a8e067f11fbed37b4adcdac922/multidict-6.7.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/3f/c3/06490e98393dcb4d6ce2bf331a39335375c300afaef526897881fbeae6ab/narwhals-2.18.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/83/a0/5b0c2f11142ed1dddec842457d3f65eaf71a0080894eb6f018755b319c3a/nbclient-0.10.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/cc/9a/cd673b2f773a12c992f41309ef81b99da1690426bd2f96957a7ade0d3ed7/nbconvert-7.16.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0d/4b/8d5f796a792f8a25f6925a96032f098789f448571eb92011df1ae59e8ea8/nbconvert-7.17.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a9/82/0340caa499416c78e5d8f5f05947ae4bc3cba53c9f038ab6e9ed964e22f1/nbformat-5.10.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/eb/be/b257e12f9710819fde40adc972578bee6b72c5992da1bc8369bef2597756/nbmake-1.5.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/28/88/4789719fbbe166d12d345b3ac66b96105f10001b16e00a9765ba29261a21/nbqa-1.9.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/12/e5/838eb1004fb9b6f0383c47c0d902eb698fda052e8e64ca48c70a2144a48c/nbstripout-0.8.2-py2.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/fc/39/dfdef4cfbfcf7c80cd144b1b2e262c2e785f18b152eeebe14ecd70ee7455/ncrystal-4.2.12-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e3/8b/1f02771d91ceafec996cef7f92f6a24010fedc47fd9404f8e11772d8501c/ncrystal_core-4.2.12-py3-none-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/78/98/20fdd6825d1876fc54bb098509e736073d20f569b033f2f23bf74aa1df9c/ncrystal_python-4.2.12-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.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/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f9/33/bd5b9137445ea4b680023eb0469b2bb969d61303dedb2aac6560ff3d14a1/notebook_shim-0.2.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/65/fb/2b23769462b34398d9326081fad5655198fcf18966fcb1f1e49db44fbf31/numpy-2.3.5-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/78/51/9f5d7a41f0b51649ddf2f2320595e15e122a40610b233d51928dd6c92353/numpy-2.4.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/2c/ab/fc8290c6a4c722e5514d80f62b2dc4c4df1a68a41d1364e625c35990fcf3/overrides-7.7.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/bf/c9/63f8d545568d9ab91476b1818b4741f521646cbdd151c6efebf40d6de6f7/pandas-2.3.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/2e/7c/870c7e7daec2a6c7ff2ac9e33b23317230d4e4e954b35112759ea4a924a7/pandas-3.0.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/ef/af/4fbc8cab944db5d21b7e2a5b8e9211a03a79852b1157e2c102fcc61ac440/pandocfilters-1.5.1-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/16/32/f8e3c85d1d5250232a5d3477a2a28cc291968ff175caeadaf3cc19ce0e4a/parso-0.8.5-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/05/bb/39e6768529454cc2b57e1e2fa0a0a18ff64397a16303270e215a3e03285f/pathspec-1.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b6/61/fae042894f4296ec49e3f193aff5d7c18440da9e48102c3315e1bc4519a7/parso-0.8.6-py2.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/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/5c/1f/8e66ab9be3aaf1435bc03edd1ebdf58ffcd17f7349c1d970cafe87af27d9/pillow-12.1.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/44/3c/d717024885424591d5376220b5e836c2d5293ce2011523c9de23ff7bf068/pip-25.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a2/c8/46dfeac5825e600579157eea177be43e2f7ff4a99da9d0d0a49533509ac5/pillow-12.1.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/c4/36/ce5f75aa7c736a663a901766edc3580098c7ea3959a0e878363a54a3714e/pixi_kernel-0.7.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e7/c3/3031c931098de393393e1f93a38dc9ed6805d86bb801acc3cf2d5bd1e6b7/plotly-6.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/63/d7/97f7e3a6abb67d8080dd406fd4df842c2be0efaf712d1c899c32a075027c/platformdirs-4.9.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/31/8b/9e8baf7dacac8d0c174925c38ff43c6d94bc9abb35503f67762caccb6869/plopp-26.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/52/d2/c6e44dba74f17c6216ce1b56044a9b93a929f1c2d5bdaff892512b260f5e/plotly-6.6.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/79/ad/45312df6b63ba64ea35b8d8f5f0c577aac16e6b416eafe8e1cb34e03f9a7/plumbum-1.10.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/a8/87/77cc11c7a9ea9fd05503def69e3d18605852cd0d4b0d3b8f15bbeb3ef1d1/pooch-1.8.2-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/5d/19/fd3ef348460c80af7bb4669ea7926651d1f95c23ff2df18b9d24bab4f3fa/pre_commit-4.5.1-py2.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/b8/db/14bafcb4af2139e046d03fd00dea7873e48eafe18b7d2797e73d6681f210/prometheus_client-0.23.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/74/c3/24a2f845e3917201628ecaba4f18bab4d18a337834c1df2a159ee9d22a42/prometheus_client-0.24.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-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/29/f8/40e01c350ad9a2b3cb4e6adbcc8a83b17ee50dd5792102b6142385937db5/psutil-7.2.1-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-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/0b/a6/e2e8535d8146bce05de6e0ecf1099a7e2887d840ae2a7b3a09385543fd02/py3dmol-2.5.3-py2.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/75/35/a44ce3d7c3f52a2a443cae261a05c2affc52fde7f1643974adbef105785f/pycifrw-5.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/78/3c/2a612b95ddbb9a6bdcb47b7a93c4884f74c6ff22356b2f7b213b16e65c35/pycifstar-0.3.0.tar.gz - pypi: https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/87/6f/cc2b231dc78d8c3aaa674a676db190b8f8071c50134af8f8cf39b9b8e8df/pydoclint-0.8.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ea/10/47caf89cbb52e5bb764696fd52a8c591a2f0e851a93270c05a17f36000b5/pymdown_extensions-10.20-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/8b/40/2614036cdd416452f5bf98ec037f38a1afb17f327cb8e6b652d4729e0af8/pyparsing-3.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6f/2c/5b079febdc65e1c3fb2729bf958d18b45be7113828528e8a0b5850dd819a/pymdown_extensions-10.21-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.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/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/50/74/c655a6eda0fd188d490c14142a0f0380655ac7099604e1fbf8fa1a97f0a1/python_engineio-4.13.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/67/0f/019d3949a40280f6193b62bc010177d4ce702d0fce424322286488569cd3/python_discovery-1.2.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/aa/54/0cce26da03a981f949bb8449c9778537f75f5917c172e1d2992ff25cb57d/python_engineio-4.13.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/51/e5/fecf13f06e5e5f67e8837d777d1bc43fac0ed2b77a676804df5c34744727/python_json_logger-4.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/28/d2/2ccc2b69a187b80fda3152745670cfba936704f296a9fa54c6c8ac694d12/python_socketio-5.16.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.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/d8/8b/e2bbeb42068f0c48899e8eddd34902afc0f7429d4d2a152d2dc2670dc661/pythreejs-2.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.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/b1/c4/2a6fe5111a01005fc7af3878259ce17684fabb8852815eda6225620f3c59/pyzmq-27.1.0-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/3c/26/1062c7ec1b053db9e499b4d2d5bc231743201b74051c973dadeac80a8f43/questionary-2.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/93/f7/d00d9b4a0313a6be3a3e0818e6375e15da6d7076f4ae47d1324e7ca986a1/radon-6.0.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/56/5d/c814546c2333ceea4ba42262d8c4d55763003e767fa169adc693bd524478/requests-2.33.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/57/4d/a7545bf6c62b0dbe5795f22ea9e88cc070fdced5c34663ebc5bed2f610c0/returns-0.26.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9e/51/17023c0f8f1869d8806b979a2bffa3f861f26a3f1a66b094288323fba52f/rfc3986_validator-0.1.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7e/71/44ce230e1b7fadd372515a97e32a83011f906ddded8d03e3c6aafbdedbb7/rfc3987_syntax-1.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f8/1e/372195d326549bb51f0ba0f2ecb9874579906b97e08880e7a65c3bef1a99/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/b3/19/9e050c0dca8aba824d67cc0db69fb459c28d8cd3f6855b1405b3f29cc91d/ruff-0.14.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/ca/6e/8942461cf2636cdae083e3eb72622a7fbbfa5cf559c7d13ab250a5dbdc01/scipy-1.16.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/1b/5a/f2f2e5eda25579f754acd83399c522ee03d6acbe001dfe53c8a1ec928b44/send2trash-2.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f8/22/d7f2fabdba4fae9f3b570e5605d5eb4500dcb7b770d3217dca4428484b17/ruff-0.15.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/af/46/661159ad844034ba8b3f4e0516215c41e4ee17db4213d13a82227670764f/sciline-25.11.1-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/fb/46/e50b38629e9e3f4a1dd55fb36d8b8abd1d59768c31151c1c8ed696f7b865/scippneutron-26.3.0-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/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/1c/78/504fdd027da3b84ff1aecd9f6957e65f35134534ccc6da8628eb71e76d3f/send2trash-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9d/76/f789f7a86709c6b087c5a2f52f911838cad707cc613162401badc665acfe/setuptools-82.0.1-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/52/59/0782e51887ac6b07ffd1570e0364cf901ebc36345fea669969d2084baebb/simple_websocket-1.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/48/f3/b67d6ea49ca9154453b6d70b34ea22f3996b9fa55da105a79d8732227adc/soupsieve-2.8.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f6/2b/60ce3ee7a5ae172bfcd419ce23259bb874d2cddd44f67c5df3760a1e22f9/sqlalchemy-2.0.45-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/c1/d4/59e74daffcb57a07668852eeeb6035af9f32cbfd7a1d2511f17d2fe6a738/smmap-5.0.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-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/55/41/591cd1e94254c20f00bb1f32c0b1a6de68c03d54e6daf78dd7b146d0b3fc/spglib-2.6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/21/dd/3b7c53f1dbbf736fd27041aee68f8ac52226b610f914085b1652c2323442/sqlalchemy-2.0.48-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-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/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-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/6a/9e/2064975477fdc887e47ad42157e214526dcad8f317a948dee17e1659a62f/terminado-0.18.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e6/34/ebdc18bae6aa14fbee1a08b63c015c72b64868ff7dae68808ab500c492e2/tinycss2-1.4.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/33/f0/3fe8c6e69135a845f4106f2ff8b6805638d4e85c264e70114e8126689587/tokenize_rt-6.2.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/89/da/75dfd804fc11e6612846758a23f13271b76d577e299592b4371a4ca4cd09/tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/50/d4/e51d52047e7eb9a582da59f32125d17c0482d065afd5d3bc435ff2120dc5/tornado-6.5.4-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b2/04/7b5705d5b3c0fab088f434f9c83edac1573830ca49ccf29fb83bf7178eec/tornado-6.5.5-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl - pypi: https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/4f/7e/bc19996fa86cad8801e8ffe6f1bba5836ca0160df76d0410d27432193712/trove_classifiers-2025.12.1.14-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/1b/a9/e3aee762739c1d7528da1c3e06d518503f8b6c439c35549b53735ba52ead/typeguard-4.4.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a0/1d/d9257dd49ff2ca23ea5f132edf1281a0c4f9de8a762b9ae399b670a59235/typer-0.21.1-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/bb/4a/2e5583e544bc437d5e8e54b47db87430df9031b29b48d17f26d129fa60c0/trove_classifiers-2026.1.14.14-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/4a/91/48db081e7a63bb37284f9fbcefda7c44c277b18b0e13fbc36ea2335b71e6/typer-0.24.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8f/5e/f1e1dd319e35e962a4e00b33150a8868b6329cc1d19fd533436ba5488f09/uncertainties-3.2.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f7/46/e7cea8159199096e1df52da20a57a6665da80c37fb8aeb848a3e47442c32/untokenize-0.1.1.tar.gz - pypi: https://files.pythonhosted.org/packages/e7/00/3fca040d7cf8a32776d3d81a00c8ee7457e00f80c649f1e4a863c8321ae9/uri_template-1.3.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/6d/b9/4095b668ea3678bf6a0af005527f39de12fb026516fb3df17495a733b7f8/urllib3-2.6.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/40/15/63fb7a6908db2f03716c4a50aea7e27a7440fe6a09854282c401139afaf7/uv-0.9.22-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/a4/39/6983dd79f01aaa4c75d9ffa550fa393f0c4c28f7ccd6956e4188c62cefbc/validate_pyproject-0.24.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/ee/e9c95cda829131f71a8dff5ce0406059fd16e591c074414e31ada19ba7c3/validate_pyproject-0.25-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/bc/4a/c6fd02a642bbe4e9f25cdd3714a328e3fc3eb6bd7b5e96f1a2285bd928b9/varname-0.15.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1c/59/964ecb8008722d27d8a835baea81f56a91cea8e097b3be992bc6ccde6367/versioningit-3.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a4/ce/3b6fee91c85626eaf769d617f1be9d2e15c1cca027bbdeb2e0d751469355/verspec-0.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/79/0c/c05523fa3181fdf0c9c52a6ba91a23fbf3246cc095f26f6516f9c60e6771/virtualenv-20.35.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c6/59/7d02447a55b2e55755011a647479041bc92a82e143f96a8195cb33bd0a1c/virtualenv-21.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/af/b5/123f13c975e9f27ab9c0770f514345bd406d0e8d3b7a0723af9d43f710af/wcwidth-0.2.14-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/68/5a/199c59e0a824a3db2b89c5d2dade7ab5f9624dbf6448dc291b46d5ec94d3/wcwidth-0.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e2/cc/e097523dd85c9cf5d354f78310927f1656c422bd7b2613b2db3e3f9a0f2c/webcolors-25.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/34/db/b10e48aa8fff7407e67470363eac595018441cf32d5e1001567a7aeba5d2/websocket_client-1.9.0-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/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-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/46/00/71b90ed48e895667ecfb1eaab27c1523ee2fa217433ed77a73b13205ca4b/yarl-1.22.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.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/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl + - pypi: ./ osx-64: - conda: https://conda.anaconda.org/conda-forge/osx-64/_openmp_mutex-4.5-7_kmp_llvm.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/bzip2-1.0.8-h500dc9f_8.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/bzip2-1.0.8-h500dc9f_9.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/c-ares-1.34.6-hb5e19a0_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.1.4-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.2.25-hbd8a1cb_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/gsl-2.8-hc707ee6_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/icu-75.1-h120a0e1_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libabseil-20250512.1-cxx17_hfc00f1c_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libblas-3.11.0-5_he492b99_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/icu-78.3-h25d91c4_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libabseil-20260107.1-cxx17_h7ed6875_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libblas-3.11.0-6_he492b99_openblas.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/libbrotlicommon-1.2.0-h8616949_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/libbrotlidec-1.2.0-h8616949_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/libbrotlienc-1.2.0-h8616949_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libcblas-3.11.0-5_h9b27e0a_openblas.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libcxx-21.1.8-h3d58e20_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libcblas-3.11.0-6_h9b27e0a_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libcxx-22.1.2-h19cb2f5_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/libev-4.33-h10d778d_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libexpat-2.7.3-heffb93a_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libffi-3.5.2-h750e83c_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libgcc-15.2.0-h08519bb_15.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libgfortran-15.2.0-h7e5c614_15.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libgfortran5-15.2.0-hd16e46c_15.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/liblzma-5.8.1-hd471939_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libnghttp2-1.67.0-h3338091_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libopenblas-0.3.30-openmp_h6006d49_4.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libsqlite-3.51.1-hb99441e_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libexpat-2.7.4-h991f03e_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libffi-3.5.2-hd1f9c09_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libgcc-15.2.0-h08519bb_18.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libgfortran-15.2.0-h7e5c614_18.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libgfortran5-15.2.0-hd16e46c_18.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/liblzma-5.8.2-h11316ed_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libnghttp2-1.68.1-h70048d4_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libopenblas-0.3.32-openmp_h9e49c7b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libsqlite-3.52.0-h77d7759_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/libuv-1.51.0-h58003a5_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libzlib-1.3.1-hd23fc13_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/llvm-openmp-21.1.8-h472b3d1_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libzlib-1.3.2-hbb4bfdb_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/llvm-openmp-22.1.1-h0d3cbff_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/ncurses-6.5-h0622a9a_3.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/nodejs-25.2.1-h5523da6_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/openssl-3.6.0-h230baf5_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/python-3.11.14-h74c2667_2_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/nodejs-25.8.2-hf3170e9_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/openssl-3.6.1-hb6871ef_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/python-3.11.15-ha9537fe_0_cpython.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/readline-8.3-h68b038d_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-hf689a15_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-h7142dee_3.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/zstd-1.5.7-h3eecb57_6.conda - pypi: https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/82/71/d5c31390d18d4f58115037c432b7e0348c60f6f53b727cad33172144a112/aiohttp-3.13.3-cp311-cp311-macosx_10_9_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/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.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/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/81/29/5ecc3a15d5a33e31b26c11426c45c501e439cb865d0bff96315d86443b78/appnope-0.1.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/0a/08/a9bebdb2e0e602dde230bdde8021b29f71f7841bd54801bcfd514acb5dcf/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_10_9_x86_64.whl - pypi: https://files.pythonhosted.org/packages/ed/c9/d7977eaacb9df673210491da99e6a247e93df98c715fc43fd136ce1d3d33/arrow-1.4.0-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/6c/25/4f103d1bedb3593718713b3f743df7b3ff3fc68d36d6666c30265ef59c8a/ase-3.28.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/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/03/49/d10027df9fce941cb8184e78a02857af36360d33e1721df81c5ed2179a1a/async_lru-2.0.5-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e5/e2/c2e3abf398f80732e58b03be77bde9022550d221dd8781bf586bd4d97cc1/async_lru-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.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/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e6/9a/8da246d988ded941da96c7ed945d63e94a445637eaad985a0ed88787cb89/backrefs-6.1-py311-none-any.whl + - pypi: https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0f/f0/35240571e1b67ffb19dafb29ab34150b6f59f93f717b041082cdb1bfceb1/backrefs-6.2-py311-none-any.whl - pypi: https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-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/cd/3a/577b549de0cc09d95f11087ee63c739bba856cd3952697eec4c4bb91350a/bleach-6.3.0-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/cb/8c/2b30c12155ad8de0cf641d76a8b396a16d2c36bc6d50b621a62b7c4567c1/build-1.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4a/57/3b7d4dd193ade4641c865bc2b93aeeb71162e81fc348b8dad020215601ed/build-1.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/43/8e/278ea79cf8ee0c395a5ad74625b3465c2fc234bb277f171dd59dd203820d/bumps-1.0.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_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/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl + - pypi: https://files.pythonhosted.org/packages/3e/38/fe380893cbba72febb24d5dc0c2f9ac99f437153c36a409a8e254ed77bb6/chardet-7.4.0.post1-cp311-cp311-macosx_10_9_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/62/28/ff6f234e628a2de61c458be2779cb182bc03f6eec12200d4a525bbfc9741/charset_normalizer-3.4.6-cp311-cp311-macosx_10_9_universal2.whl - pypi: https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.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/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/91/2e/c4390a31919d8a78b90e8ecf87cd4b4c4f05a5b48d05ec17db8e5404c6f4/contourpy-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/b4/9b/77baf488516e9ced25fc215a6f75d803493fc3f6a1a1227ac35697910c2a/coverage-7.13.1-cp311-cp311-macosx_10_9_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/df/91/4a151c94320458895049a3e23b7b2cfc08953c60b14892de837e8eb51d0a/copier-9.14.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4b/37/d24c8f8220ff07b839b2c043ea4903a33b0f455abe673ae3c03bbdb7f212/coverage-7.13.5-cp311-cp311-macosx_10_9_x86_64.whl - pypi: https://files.pythonhosted.org/packages/52/e8/c14cc8af8cd38e86887053843382629bd8ebd117f83f15eb1194d65a2c9d/cryspy-0.10.0-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/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-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/25/3e/e27078370414ef35fafad2c06d182110073daaeb5d3bf734b0b1eeefe452/debugpy-1.8.19-py2.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/e0/c3/7f67dea8ccf8fdcb9c99033bbe3e90b9e7395415843accb81428c441be2d/debugpy-1.8.20-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/68/49/869b49db0bad6fba7c7471d612c356e745929d14e0de63acbc53e88f2a50/dfo_ls-1.6-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/94/9e/0e27056c6165ab3e2536d5efbc358cf495e6cd57fbaf51e68b1113fa7771/diffpy_pdffit2-1.5.2-cp311-cp311-macosx_10_13_x86_64.whl - pypi: https://files.pythonhosted.org/packages/8e/52/39914bf42bb01901c91781def35c1aeffa431a59299e9748c0cfae3e5493/diffpy_structure-3.3.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/79/c8/f0f4ab7fd08950d5358579364a0a9b9198bf03e179a4fcceae4b353be32e/diffpy_utils-3.6.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/50/3d/9373ad9c56321fdab5b41197068e1d8c25883b3fea29dd361f9b55116869/dill-0.4.0-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/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/dc/b4/a7ec1eaee86761a9dbfd339732b4706db3c6b65e970c12f0f56cfcce3dcf/docformatter-1.7.7-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/82/ee/c9ca3f81d69ec6c99ca41e10432a58c60fef3a6e63cce6df7e796f90fe22/easydiffraction-0.10.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/50/98b146aea0f1cd7531d25f12bea69fa9ce8d1662124f93fb30dc4511b65e/docstring_parser_fork-0.0.14-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/87/10/2c7edbf230e5c507d38367af498fa94258ed97205d9b4b6f63a921fe9c49/dunamai-1.26.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/55/cd/d729a1bb63fa95387228cc508552dea4685ea0116e484e73238db10f9521/essdiffraction-26.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f1/bb/66c80d7f801b191f7b3ee6149a39be9d1a8a81c233e20adaf796d171f93a/essreduce-26.4.0-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/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cb/a8/20d0723294217e47de6d9e2e40fd4a9d2f7c4b6ef974babd482a59743694/fastjsonschema-2.21.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/9a/30/ab407e2ec752aa541704ed8f93c11e2a5d92c168b8a755d818b74a3c5c2d/filelock-3.20.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ac/49/4138d1acb6261499bedde1c07f8c2605d1d8f9d77a151e5507fd3ef084b6/fonttools-4.61.1-cp311-cp311-macosx_10_9_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/a4/a5/842ae8f0c08b61d6484b52f99a03510a3a72d23141942d216ebe81fefbce/filelock-3.25.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/24/7f/66d3f8a9338a9b67fe6e1739f47e1cd5cee78bd3bc1206ef9b0b982289a5/fonttools-4.62.1-cp311-cp311-macosx_10_9_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/33/b2/986d1220f6ee931e338d272bc1f3ec02cfe5f9b5fad84e95afdad57f1ebc/format_docstring-0.2.7-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cf/58/8acf1b3e91c58313ce5cb67df61001fc9dcd21be4fadb76c1a2d540e09ed/fqdn-1.5.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/df/b5/7610b6bd13e4ae77b96ba85abea1c8cb249683217ef09ac9e0ae93f25a91/frozenlist-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/28/bc/e943898c25121f36625ab4913b8e24d0bdd054a17e380d19924066102574/gemmi-0.7.4-cp311-cp311-macosx_10_14_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/e6/ab/fb21f4c939bb440104cc2b396d3be1d9b7a9fd3c6c2a53d98c45b3d7c954/fsspec-2026.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d5/08/c2409cb01d5368dcfedcbaffa7d044cc8957d57a9d0855244a5eb4709d30/funcy-2.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7f/79/b13830a65bf9fc85474a984604f094cc18817dc93a784f4c567a2dc05169/gemmi-0.7.5-cp311-cp311-macosx_10_14_x86_64.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/1f/cb/48e964c452ca2b92175a9b2dca037a553036cb053ba69e284650ce755f13/greenlet-3.3.0-cp311-cp311-macosx_11_0_universal2.whl - - pypi: https://files.pythonhosted.org/packages/9c/83/3b1d03d36f224edded98e9affd0467630fc09d766c0e56fb1498cbb04a9b/griffe-1.15.0-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/6a/09/e21df6aef1e1ffc0c816f0522ddc3f6dcded766c3261813131c78a704470/gitpython-3.1.46-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/f3/47/16400cb42d18d7a6bb46f0626852c1718612e35dcb0dffa16bbaffdf5dd2/greenlet-3.3.2-cp311-cp311-macosx_11_0_universal2.whl + - pypi: https://files.pythonhosted.org/packages/4b/4c/cc8c68196db727cfc1432f2ad5de50aa6707e630d44b2e6361dc06d8f134/griffelib-2.0.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/41/fd/8349b48b15b47768042cff06ad6e1c229f0a4bd89225bf6b6894fea27e6d/h5py-3.15.1-cp311-cp311-macosx_10_9_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/ba/95/a825894f3e45cbac7554c4e97314ce886b233a20033787eda755ca8fecc7/h5py-3.16.0-cp311-cp311-macosx_10_9_x86_64.whl - pypi: https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/0f/1c/e5fd8f973d4f375adb21565739498e2e9a1e54c858a97b9a8ccfdc81da9b/identify-2.6.15-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/46/33/92ef41c6fad0233e41d3d84ba8e8ad18d1780f1e5d99b3c683e6d7f98b63/identify-2.6.18-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a4/ed/1f1afb2e9e7f38a545d628f864d562a5ae64fe6f7a10e28ffb9b185b4e89/importlib_resources-6.5.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/38/3d/2d244233ac4f76e38533cfcb2991c9eb4c7bf688ae0a036d30725b8faafe/importlib_metadata-9.0.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/12/c9/6869a1dcf4aaf309b9543ec070be3ec3adebee7c9bec9af8c230494134b9/interrogate-1.7.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f6/d8/502954a4ec0efcf264f99b65b41c3c54e65a647d9f0d6f62cd02227d242c/ipykernel-6.31.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/86/92/162cfaee4ccf370465c5af1ce36a9eacec1becb552f2033bb3584e6f640a/ipython-9.9.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/82/b9/e73d5d9f405cba7706c539aa8b311b49d4c2f3d698d9c12f815231169c71/ipykernel-7.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3d/aa/898dec789a05731cd5a9f50605b7b44a72bd198fd0d4528e11fc610177cc/ipython-9.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-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/7b/55/e5326141505c5d5e34c5e0935d2908a74e4561eca44108fbfb9c13d2911a/isoduration-20.11.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/72/b9/313e8f2f2e9517ae050a692ae7b3e4b3f17cc5e6dfea0db51fe14e586580/jinja2_ansible_filters-1.3.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d7/9e/038522f50ceb7e74f1f991bf1b699f24b0c2bbe7c390dd36ad69f4582258/json5-0.13.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/71/92/5e77f98553e9e75130c78900d000368476aed74276eb8ae8796f65f00918/jsonpointer-3.0.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9e/6a/a83720e953b1682d2d109d3c2dbb0bc9bf28cc1cbc205be4ef4be5da709d/jsonpointer-3.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/bb/f5/fddaec430367be9d62a7ed125530e133bfd4a1c0350fe221149ee0f2b526/jupyter_client-8.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2d/0b/ceb7694d864abc0a047649aec263878acb9f792e1fec3e676f22dc9015e3/jupyter_client-8.8.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/76/c0/8cc378aad5ccfcc37adeda27d3864f20ea808ad5b63572f5b986a42a649c/jupyter_dark_detect-0.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e2/48/577993f1f99c552f18a0428731a755e06171f9902fa118c379eb7c04ea22/jupyter_events-0.12.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1a/60/1f6cee0c46263de1173894f0fafcb3475ded276c472c14d25e0280c18d6d/jupyter_lsp-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f4/a4/61adb19f3c74b0dc0e411de4f06ebef564b1f179928f9dffcbd4b378f2ef/jupyter_notebook_parser-0.1.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/92/80/a24767e6ca280f5a49525d987bf3e4d7552bf67c8be07e8ccf20271f8568/jupyter_server-2.17.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/07/2d/2b32cdbe8d2a602f697a649798554e4f072115438e92249624e532e8aca6/jupyter_server_terminals-0.5.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/af/c3/acced767eecc11a70c65c45295db5396c4f0c1937874937d5a76d7b177b6/jupyterlab-4.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d1/2d/6674563f71c6320841fc300911a55143925112a72a883e2ca71fba4c618d/jupyter_server_terminals-0.5.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e1/1b/dad6fdcc658ed7af26fdf3841e7394072c9549a8b896c381ab49dd11e2d9/jupyterlab-4.5.6-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b1/dd/ead9d8ea85bf202d90cc513b533f9c363121c7792674f78e0d8a854b63b4/jupyterlab_pygments-0.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e0/07/a000fe835f76b7e1143242ab1122e6362ef1c03f23f83a045c38859c2ae0/jupyterlab_server-2.28.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f7/5a/488f492b49b712ca38326e12c78d0d48ab6be682a2989ce533f0f2c4dfd2/jupyterquiz-2.9.6.2-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/bd/0d/2d240e7098e0cafba4d25e9530e7596b1bb1bd4476e41b10346bcaaa36d6/jupytext-1.18.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a0/c0/27fe1a68a39cf62472a300e2879ffc13c0538546c359b86f149cc19f6ac3/kiwisolver-1.4.9-cp311-cp311-macosx_10_9_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/ab/b5/36c712098e6191d1b4e349304ef73a8d06aed77e56ceaac8c0a306c7bda1/jupyterlab_widgets-3.0.16-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/52/bc858b1665d0dec3a2511f4e6f5c18ea85c0977563d624d597c95d6d0fd7/jupyterquiz-2.9.6.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/16/5a/736dd2f4535dbf3bf26523f9158c011389ef88dd06ec2eef67fd744f1c7b/jupytext-1.19.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/11/60/37b4047a2af0cf5ef6d8b4b26e91829ae6fc6a2d1f74524bcb0e7cd28a32/kiwisolver-1.5.0-cp311-cp311-macosx_10_9_x86_64.whl - pypi: https://files.pythonhosted.org/packages/82/3d/14ce75ef66813643812f3093ab17e46d3a206942ce7376d31ec2d36229e7/lark-1.3.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/38/7e/7b91c89a4cf0f543a83be978657afb20c86af6d725253e319589dcc4ce52/lmfit-1.3.4-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/d2/f0/834e479e47e499b6478e807fb57b31cc2db696c4db30557bb6f5aea4a90b/mando-0.7.1-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/70/81/54e3ce63502cd085a0c556652a4e1b919c45a446bd1e5300e10c44c8c521/markdown-3.10-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/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl - pypi: https://files.pythonhosted.org/packages/f8/86/de7e3a1cdcfc941483af70609edc06b83e7c8a0e0dc9ac325200a3f4d220/matplotlib-3.10.8-cp311-cp311-macosx_10_12_x86_64.whl @@ -1453,258 +1692,303 @@ environments: - 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/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/fd/1a/31b7cd6e4e7a02df4e076162e9783620777592bea9e4bb036389389af99d/mike-2.1.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/48/f7/10f5e101db25741b91e4f4792c5d97b4fa834ead5cf509ae91097d939424/mike-2.1.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9b/f7/4a5e785ec9fbd65146a27b6b70b6cdc161a66f2024e4b04ac06a67f5578b/mistune-3.2.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/71/26/4d39d52ea2219604053a4d05b98e90d6a335511cc01806436ec4886b1028/mkdocs_autorefs-1.2.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/9f/d4/029f984e8d3f3b6b726bd33cafc473b75e9e44c0f7e80a5b29abc466bdea/mkdocs_get_deps-0.2.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/08/37/5f1fd5c3f6954b3256f8126275e62af493b96fb6aef6c0dbc4ee326032ad/mkdocs_jupyter-0.25.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/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/93/89/eb601278b12c471235860992f5973cf3c55ca3f77d1d6127389eb045a021/mkdocs_jupyter-0.26.1-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/3e/32/ed071cb721aca8c227718cffcf7bd539620e9799bbf2619e90c757bfd030/mkdocs_material-9.7.1-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/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.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/cd/10/4c27c3063c2b3681a4b7942f8dbdeb4fa34fecb2c19b594e7345ebf4f86f/mkdocstrings-0.27.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/51/23/d02d86553327296c3bf369d444194ea83410cce8f0e690565264f37f3261/mkdocstrings_python-1.13.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/04/41/1cf02e3df279d2dd846a1bf235a928254eba9006dd22b4a14caa71aed0f7/mkdocstrings-1.0.3-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/5b/69/93b34728cc386efdde0c342f8c680b9187dea7beb7adaf6b58a0713be101/mpld3-0.5.12-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/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/97/560d11202bcd537abca693fd85d81cebe2107ba17301de42b01ac1677b69/msgpack-1.1.2-cp311-cp311-macosx_10_9_x86_64.whl - pypi: https://files.pythonhosted.org/packages/03/59/fdcb3af72f750a8de2bcf39d62ada70b5eb17b06d7f63860e0a679cb656b/msgspec-0.20.0-cp311-cp311-macosx_10_9_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/17/e4/67b5c27bd17c085a5ea8f1ec05b8a3e5cba0ca734bfcad5560fb129e70ca/multidict-6.7.0-cp311-cp311-macosx_10_9_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/3d/2e/cf2ffeb386ac3763526151163ad7da9f1b586aac96d2b4f7de1eaebf0c61/narwhals-2.15.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a6/9b/267e64eaf6fc637a15b35f5de31a566634a2740f97d8d094a69d34f524a4/multidict-6.7.1-cp311-cp311-macosx_10_9_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/3f/c3/06490e98393dcb4d6ce2bf331a39335375c300afaef526897881fbeae6ab/narwhals-2.18.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/83/a0/5b0c2f11142ed1dddec842457d3f65eaf71a0080894eb6f018755b319c3a/nbclient-0.10.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/cc/9a/cd673b2f773a12c992f41309ef81b99da1690426bd2f96957a7ade0d3ed7/nbconvert-7.16.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0d/4b/8d5f796a792f8a25f6925a96032f098789f448571eb92011df1ae59e8ea8/nbconvert-7.17.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a9/82/0340caa499416c78e5d8f5f05947ae4bc3cba53c9f038ab6e9ed964e22f1/nbformat-5.10.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/eb/be/b257e12f9710819fde40adc972578bee6b72c5992da1bc8369bef2597756/nbmake-1.5.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/28/88/4789719fbbe166d12d345b3ac66b96105f10001b16e00a9765ba29261a21/nbqa-1.9.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/12/e5/838eb1004fb9b6f0383c47c0d902eb698fda052e8e64ca48c70a2144a48c/nbstripout-0.8.2-py2.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/fc/39/dfdef4cfbfcf7c80cd144b1b2e262c2e785f18b152eeebe14ecd70ee7455/ncrystal-4.2.12-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ab/ee/0d9d9218d2081e56828194f83d0eac6292b7182708fd07a62756c66f7194/ncrystal_core-4.2.12-py3-none-macosx_10_9_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/78/98/20fdd6825d1876fc54bb098509e736073d20f569b033f2f23bf74aa1df9c/ncrystal_python-4.2.12-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.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/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f9/33/bd5b9137445ea4b680023eb0469b2bb969d61303dedb2aac6560ff3d14a1/notebook_shim-0.2.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/43/77/84dd1d2e34d7e2792a236ba180b5e8fcc1e3e414e761ce0253f63d7f572e/numpy-2.3.5-cp311-cp311-macosx_10_9_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/2b/09/3c4abbc1dcd8010bf1a611d174c7aa689fc505585ec806111b4406f6f1b1/numpy-2.4.3-cp311-cp311-macosx_14_0_x86_64.whl - pypi: https://files.pythonhosted.org/packages/2c/ab/fc8290c6a4c722e5514d80f62b2dc4c4df1a68a41d1364e625c35990fcf3/overrides-7.7.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/c1/fa/7ac648108144a095b4fb6aa3de1954689f7af60a14cf25583f4960ecb878/pandas-2.3.3-cp311-cp311-macosx_10_9_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/ff/07/c7087e003ceee9b9a82539b40414ec557aa795b584a1a346e89180853d79/pandas-3.0.1-cp311-cp311-macosx_10_9_x86_64.whl - pypi: https://files.pythonhosted.org/packages/ef/af/4fbc8cab944db5d21b7e2a5b8e9211a03a79852b1157e2c102fcc61ac440/pandocfilters-1.5.1-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/16/32/f8e3c85d1d5250232a5d3477a2a28cc291968ff175caeadaf3cc19ce0e4a/parso-0.8.5-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/05/bb/39e6768529454cc2b57e1e2fa0a0a18ff64397a16303270e215a3e03285f/pathspec-1.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b6/61/fae042894f4296ec49e3f193aff5d7c18440da9e48102c3315e1bc4519a7/parso-0.8.6-py2.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/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/43/c4/bf8328039de6cc22182c3ef007a2abfbbdab153661c0a9aa78af8d706391/pillow-12.1.0-cp311-cp311-macosx_10_10_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/44/3c/d717024885424591d5376220b5e836c2d5293ce2011523c9de23ff7bf068/pip-25.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2b/46/5da1ec4a5171ee7bf1a0efa064aba70ba3d6e0788ce3f5acd1375d23c8c0/pillow-12.1.1-cp311-cp311-macosx_10_10_x86_64.whl - pypi: https://files.pythonhosted.org/packages/c4/36/ce5f75aa7c736a663a901766edc3580098c7ea3959a0e878363a54a3714e/pixi_kernel-0.7.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e7/c3/3031c931098de393393e1f93a38dc9ed6805d86bb801acc3cf2d5bd1e6b7/plotly-6.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/63/d7/97f7e3a6abb67d8080dd406fd4df842c2be0efaf712d1c899c32a075027c/platformdirs-4.9.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/31/8b/9e8baf7dacac8d0c174925c38ff43c6d94bc9abb35503f67762caccb6869/plopp-26.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/52/d2/c6e44dba74f17c6216ce1b56044a9b93a929f1c2d5bdaff892512b260f5e/plotly-6.6.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/79/ad/45312df6b63ba64ea35b8d8f5f0c577aac16e6b416eafe8e1cb34e03f9a7/plumbum-1.10.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/a8/87/77cc11c7a9ea9fd05503def69e3d18605852cd0d4b0d3b8f15bbeb3ef1d1/pooch-1.8.2-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/5d/19/fd3ef348460c80af7bb4669ea7926651d1f95c23ff2df18b9d24bab4f3fa/pre_commit-4.5.1-py2.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/b8/db/14bafcb4af2139e046d03fd00dea7873e48eafe18b7d2797e73d6681f210/prometheus_client-0.23.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/74/c3/24a2f845e3917201628ecaba4f18bab4d18a337834c1df2a159ee9d22a42/prometheus_client-0.24.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c2/21/d7b68e911f9c8e18e4ae43bdbc1e1e9bbd971f8866eb81608947b6f585ff/propcache-0.4.1-cp311-cp311-macosx_10_9_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/c5/cf/5180eb8c8bdf6a503c6919f1da28328bd1e6b3b1b5b9d5b01ae64f019616/psutil-7.2.1-cp36-abi3-macosx_10_9_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl - pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-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/0b/a6/e2e8535d8146bce05de6e0ecf1099a7e2887d840ae2a7b3a09385543fd02/py3dmol-2.5.3-py2.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/62/b6/84364503e0726da4a263e1736d0e1754526d1b1729d0087c680d96345570/pycifrw-5.0.1-cp311-cp311-macosx_10_9_x86_64.whl - pypi: https://files.pythonhosted.org/packages/78/3c/2a612b95ddbb9a6bdcb47b7a93c4884f74c6ff22356b2f7b213b16e65c35/pycifstar-0.3.0.tar.gz - pypi: https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/87/6f/cc2b231dc78d8c3aaa674a676db190b8f8071c50134af8f8cf39b9b8e8df/pydoclint-0.8.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ea/10/47caf89cbb52e5bb764696fd52a8c591a2f0e851a93270c05a17f36000b5/pymdown_extensions-10.20-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/8b/40/2614036cdd416452f5bf98ec037f38a1afb17f327cb8e6b652d4729e0af8/pyparsing-3.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6f/2c/5b079febdc65e1c3fb2729bf958d18b45be7113828528e8a0b5850dd819a/pymdown_extensions-10.21-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.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/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/50/74/c655a6eda0fd188d490c14142a0f0380655ac7099604e1fbf8fa1a97f0a1/python_engineio-4.13.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/67/0f/019d3949a40280f6193b62bc010177d4ce702d0fce424322286488569cd3/python_discovery-1.2.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/aa/54/0cce26da03a981f949bb8449c9778537f75f5917c172e1d2992ff25cb57d/python_engineio-4.13.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/51/e5/fecf13f06e5e5f67e8837d777d1bc43fac0ed2b77a676804df5c34744727/python_json_logger-4.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/28/d2/2ccc2b69a187b80fda3152745670cfba936704f296a9fa54c6c8ac694d12/python_socketio-5.16.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.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/d8/8b/e2bbeb42068f0c48899e8eddd34902afc0f7429d4d2a152d2dc2670dc661/pythreejs-2.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.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/5d/305323ba86b284e6fcb0d842d6adaa2999035f70f8c38a9b6d21ad28c3d4/pyzmq-27.1.0-cp311-cp311-macosx_10_15_universal2.whl + - pypi: https://files.pythonhosted.org/packages/3c/26/1062c7ec1b053db9e499b4d2d5bc231743201b74051c973dadeac80a8f43/questionary-2.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/93/f7/d00d9b4a0313a6be3a3e0818e6375e15da6d7076f4ae47d1324e7ca986a1/radon-6.0.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/56/5d/c814546c2333ceea4ba42262d8c4d55763003e767fa169adc693bd524478/requests-2.33.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/57/4d/a7545bf6c62b0dbe5795f22ea9e88cc070fdced5c34663ebc5bed2f610c0/returns-0.26.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9e/51/17023c0f8f1869d8806b979a2bffa3f861f26a3f1a66b094288323fba52f/rfc3986_validator-0.1.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7e/71/44ce230e1b7fadd372515a97e32a83011f906ddded8d03e3c6aafbdedbb7/rfc3987_syntax-1.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4d/6e/f964e88b3d2abee2a82c1ac8366da848fce1c6d834dc2132c3fda3970290/rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/df/58/a0349197a7dfa603ffb7f5b0470391efa79ddc327c1e29c4851e85b09cc5/ruff-0.14.10-py3-none-macosx_10_12_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/9b/5f/6f37d7439de1455ce9c5a556b8d1db0979f03a796c030bafdf08d35b7bf9/scipy-1.16.3-cp311-cp311-macosx_10_14_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/1b/5a/f2f2e5eda25579f754acd83399c522ee03d6acbe001dfe53c8a1ec928b44/send2trash-2.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/eb/92/f1c662784d149ad1414cae450b082cf736430c12ca78367f20f5ed569d65/ruff-0.15.8-py3-none-macosx_10_12_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/af/46/661159ad844034ba8b3f4e0516215c41e4ee17db4213d13a82227670764f/sciline-25.11.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/81/21/4962b1daddf0422e56c5ed4c41bea1ccb6d2a9ab72b795196835a20969c7/scipp-26.3.1-cp311-cp311-macosx_14_0_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/fb/46/e50b38629e9e3f4a1dd55fb36d8b8abd1d59768c31151c1c8ed696f7b865/scippneutron-26.3.0-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/ec/e6/cef1cf3557f0c54954198554a10016b6a03b2ec9e22a4e1df734936bd99c/scipy-1.17.1-cp311-cp311-macosx_14_0_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/1c/78/504fdd027da3b84ff1aecd9f6957e65f35134534ccc6da8628eb71e76d3f/send2trash-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9d/76/f789f7a86709c6b087c5a2f52f911838cad707cc613162401badc665acfe/setuptools-82.0.1-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/52/59/0782e51887ac6b07ffd1570e0364cf901ebc36345fea669969d2084baebb/simple_websocket-1.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/48/f3/b67d6ea49ca9154453b6d70b34ea22f3996b9fa55da105a79d8732227adc/soupsieve-2.8.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/bf/e1/3ccb13c643399d22289c6a9786c1a91e3dcbb68bce4beb44926ac2c557bf/sqlalchemy-2.0.45-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/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-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/6c/44/30888e2a5b2fa2e6df18606b442cb8b126b0bea5a2f1ec4a2a82538ffecf/spglib-2.6.0-cp311-cp311-macosx_10_9_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/46/2c/9664130905f03db57961b8980b05cab624afd114bf2be2576628a9f22da4/sqlalchemy-2.0.48-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-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/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-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/6a/9e/2064975477fdc887e47ad42157e214526dcad8f317a948dee17e1659a62f/terminado-0.18.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e6/34/ebdc18bae6aa14fbee1a08b63c015c72b64868ff7dae68808ab500c492e2/tinycss2-1.4.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/33/f0/3fe8c6e69135a845f4106f2ff8b6805638d4e85c264e70114e8126689587/tokenize_rt-6.2.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b3/2e/299f62b401438d5fe1624119c723f5d877acc86a4c2492da405626665f12/tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/db/7e/f7b8d8c4453f305a51f80dbb49014257bb7d28ccb4bbb8dd328ea995ecad/tornado-6.5.4-cp39-abi3-macosx_10_9_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ab/5e/7625b76cd10f98f1516c36ce0346de62061156352353ef2da44e5c21523c/tornado-6.5.5-cp39-abi3-macosx_10_9_x86_64.whl - pypi: https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/4f/7e/bc19996fa86cad8801e8ffe6f1bba5836ca0160df76d0410d27432193712/trove_classifiers-2025.12.1.14-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/1b/a9/e3aee762739c1d7528da1c3e06d518503f8b6c439c35549b53735ba52ead/typeguard-4.4.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a0/1d/d9257dd49ff2ca23ea5f132edf1281a0c4f9de8a762b9ae399b670a59235/typer-0.21.1-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/bb/4a/2e5583e544bc437d5e8e54b47db87430df9031b29b48d17f26d129fa60c0/trove_classifiers-2026.1.14.14-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/4a/91/48db081e7a63bb37284f9fbcefda7c44c277b18b0e13fbc36ea2335b71e6/typer-0.24.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8f/5e/f1e1dd319e35e962a4e00b33150a8868b6329cc1d19fd533436ba5488f09/uncertainties-3.2.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f7/46/e7cea8159199096e1df52da20a57a6665da80c37fb8aeb848a3e47442c32/untokenize-0.1.1.tar.gz - pypi: https://files.pythonhosted.org/packages/e7/00/3fca040d7cf8a32776d3d81a00c8ee7457e00f80c649f1e4a863c8321ae9/uri_template-1.3.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/6d/b9/4095b668ea3678bf6a0af005527f39de12fb026516fb3df17495a733b7f8/urllib3-2.6.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/5e/68/bb76c97c284ce7fb8efa868994c2510588faa7075e60d8865d1373e54b7b/uv-0.9.22-py3-none-macosx_10_12_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/a4/39/6983dd79f01aaa4c75d9ffa550fa393f0c4c28f7ccd6956e4188c62cefbc/validate_pyproject-0.24.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/ee/e9c95cda829131f71a8dff5ce0406059fd16e591c074414e31ada19ba7c3/validate_pyproject-0.25-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/bc/4a/c6fd02a642bbe4e9f25cdd3714a328e3fc3eb6bd7b5e96f1a2285bd928b9/varname-0.15.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1c/59/964ecb8008722d27d8a835baea81f56a91cea8e097b3be992bc6ccde6367/versioningit-3.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a4/ce/3b6fee91c85626eaf769d617f1be9d2e15c1cca027bbdeb2e0d751469355/verspec-0.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/79/0c/c05523fa3181fdf0c9c52a6ba91a23fbf3246cc095f26f6516f9c60e6771/virtualenv-20.35.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c6/59/7d02447a55b2e55755011a647479041bc92a82e143f96a8195cb33bd0a1c/virtualenv-21.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/af/b5/123f13c975e9f27ab9c0770f514345bd406d0e8d3b7a0723af9d43f710af/wcwidth-0.2.14-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/68/5a/199c59e0a824a3db2b89c5d2dade7ab5f9624dbf6448dc291b46d5ec94d3/wcwidth-0.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e2/cc/e097523dd85c9cf5d354f78310927f1656c422bd7b2613b2db3e3f9a0f2c/webcolors-25.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/34/db/b10e48aa8fff7407e67470363eac595018441cf32d5e1001567a7aeba5d2/websocket_client-1.9.0-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/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-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/6a/a1/d065d51d02dc02ce81501d476b9ed2229d9a990818332242a882d5d60340/yarl-1.22.0-cp311-cp311-macosx_10_9_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/24/84/e237607faf4e099dbb8a4f511cfd5efcb5f75918baad200ff7380635631b/yarl-1.23.0-cp311-cp311-macosx_10_9_x86_64.whl - pypi: https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl + - pypi: ./ osx-arm64: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/_openmp_mutex-4.5-7_kmp_llvm.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-hd037594_8.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/noarch/ca-certificates-2026.1.4-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.2.25-hbd8a1cb_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/gsl-2.8-h8d0574d_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/icu-75.1-hfee45f7_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libabseil-20250512.1-cxx17_hd41c47c_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libblas-3.11.0-5_h51639a9_openblas.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/libabseil-20260107.1-cxx17_h2062a1b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libblas-3.11.0-6_h51639a9_openblas.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/libcblas-3.11.0-5_hb0561ab_openblas.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-21.1.8-hf598326_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcblas-3.11.0-6_hb0561ab_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-22.1.2-h55c6f16_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.7.3-haf25636_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.5.2-he5f378a_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgcc-15.2.0-hcbb3090_16.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran-15.2.0-h07b0088_16.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran5-15.2.0-hdae7583_16.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.1-h39f12f2_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libnghttp2-1.67.0-hc438710_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopenblas-0.3.30-openmp_ha158390_3.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.51.1-h1b79a29_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.7.4-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/libgcc-15.2.0-hcbb3090_18.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran-15.2.0-h07b0088_18.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran5-15.2.0-hdae7583_18.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.2-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/libopenblas-0.3.32-openmp_he657e61_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.52.0-h1ae2325_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.1-h8359307_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/llvm-openmp-21.1.8-h4a912ad_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/llvm-openmp-22.1.1-hc7d1edf_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.5-h5e97a16_3.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/nodejs-25.2.1-h5230ea7_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.6.0-h5503f6c_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.11.14-h18782d2_2_cpython.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.1-hd24854e_1.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/readline-8.3-h46df422_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h892fb3f_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h010d191_3.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hbf9d68e_6.conda - pypi: https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/0e/c9/741f8ac91e14b1d2e7100690425a5b2b919a87a5075406582991fb7de920/aiohttp-3.13.3-cp311-cp311-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.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/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/81/29/5ecc3a15d5a33e31b26c11426c45c501e439cb865d0bff96315d86443b78/appnope-0.1.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b6/02/d297943bcacf05e4f2a94ab6f462831dc20158614e5d067c35d4e63b9acb/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/ed/c9/d7977eaacb9df673210491da99e6a247e93df98c715fc43fd136ce1d3d33/arrow-1.4.0-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/6c/25/4f103d1bedb3593718713b3f743df7b3ff3fc68d36d6666c30265ef59c8a/ase-3.28.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/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/03/49/d10027df9fce941cb8184e78a02857af36360d33e1721df81c5ed2179a1a/async_lru-2.0.5-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e5/e2/c2e3abf398f80732e58b03be77bde9022550d221dd8781bf586bd4d97cc1/async_lru-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.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/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e6/9a/8da246d988ded941da96c7ed945d63e94a445637eaad985a0ed88787cb89/backrefs-6.1-py311-none-any.whl + - pypi: https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0f/f0/35240571e1b67ffb19dafb29ab34150b6f59f93f717b041082cdb1bfceb1/backrefs-6.2-py311-none-any.whl - pypi: https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-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/cd/3a/577b549de0cc09d95f11087ee63c739bba856cd3952697eec4c4bb91350a/bleach-6.3.0-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/cb/8c/2b30c12155ad8de0cf641d76a8b396a16d2c36bc6d50b621a62b7c4567c1/build-1.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4a/57/3b7d4dd193ade4641c865bc2b93aeeb71162e81fc348b8dad020215601ed/build-1.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/43/8e/278ea79cf8ee0c395a5ad74625b3465c2fc234bb277f171dd59dd203820d/bumps-1.0.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.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/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl + - pypi: https://files.pythonhosted.org/packages/5e/24/3c1522d777b66e2e3615ee33d1d4291c47b0ec258a9471b559339b01fac5/chardet-7.4.0.post1-cp311-cp311-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/62/28/ff6f234e628a2de61c458be2779cb182bc03f6eec12200d4a525bbfc9741/charset_normalizer-3.4.6-cp311-cp311-macosx_10_9_universal2.whl - pypi: https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.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/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-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/d7/cd/7ab01154e6eb79ee2fab76bf4d89e94c6648116557307ee4ebbb85e5c1bf/coverage-7.13.1-cp311-cp311-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/df/91/4a151c94320458895049a3e23b7b2cfc08953c60b14892de837e8eb51d0a/copier-9.14.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/35/8b/cd129b0ca4afe886a6ce9d183c44d8301acbd4ef248622e7c49a23145605/coverage-7.13.5-cp311-cp311-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/52/e8/c14cc8af8cd38e86887053843382629bd8ebd117f83f15eb1194d65a2c9d/cryspy-0.10.0-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/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-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/25/3e/e27078370414ef35fafad2c06d182110073daaeb5d3bf734b0b1eeefe452/debugpy-1.8.19-py2.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/e0/c3/7f67dea8ccf8fdcb9c99033bbe3e90b9e7395415843accb81428c441be2d/debugpy-1.8.20-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/68/49/869b49db0bad6fba7c7471d612c356e745929d14e0de63acbc53e88f2a50/dfo_ls-1.6-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/ce/c9/7b61255980383781774d9857aa9e97fe7e9b8b08f97c0974afeef3083dd9/diffpy_pdffit2-1.5.2-cp311-cp311-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/8e/52/39914bf42bb01901c91781def35c1aeffa431a59299e9748c0cfae3e5493/diffpy_structure-3.3.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/79/c8/f0f4ab7fd08950d5358579364a0a9b9198bf03e179a4fcceae4b353be32e/diffpy_utils-3.6.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/50/3d/9373ad9c56321fdab5b41197068e1d8c25883b3fea29dd361f9b55116869/dill-0.4.0-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/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/dc/b4/a7ec1eaee86761a9dbfd339732b4706db3c6b65e970c12f0f56cfcce3dcf/docformatter-1.7.7-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/82/ee/c9ca3f81d69ec6c99ca41e10432a58c60fef3a6e63cce6df7e796f90fe22/easydiffraction-0.10.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/50/98b146aea0f1cd7531d25f12bea69fa9ce8d1662124f93fb30dc4511b65e/docstring_parser_fork-0.0.14-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/87/10/2c7edbf230e5c507d38367af498fa94258ed97205d9b4b6f63a921fe9c49/dunamai-1.26.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/55/cd/d729a1bb63fa95387228cc508552dea4685ea0116e484e73238db10f9521/essdiffraction-26.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f1/bb/66c80d7f801b191f7b3ee6149a39be9d1a8a81c233e20adaf796d171f93a/essreduce-26.4.0-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/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cb/a8/20d0723294217e47de6d9e2e40fd4a9d2f7c4b6ef974babd482a59743694/fastjsonschema-2.21.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/9a/30/ab407e2ec752aa541704ed8f93c11e2a5d92c168b8a755d818b74a3c5c2d/filelock-3.20.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/69/12/bf9f4eaa2fad039356cc627587e30ed008c03f1cebd3034376b5ee8d1d44/fonttools-4.61.1-cp311-cp311-macosx_10_9_universal2.whl + - pypi: https://files.pythonhosted.org/packages/a4/a5/842ae8f0c08b61d6484b52f99a03510a3a72d23141942d216ebe81fefbce/filelock-3.25.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/39/23ff32561ec8d45a4d48578b4d241369d9270dc50926c017570e60893701/fonttools-4.62.1-cp311-cp311-macosx_10_9_universal2.whl + - pypi: https://files.pythonhosted.org/packages/33/b2/986d1220f6ee931e338d272bc1f3ec02cfe5f9b5fad84e95afdad57f1ebc/format_docstring-0.2.7-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cf/58/8acf1b3e91c58313ce5cb67df61001fc9dcd21be4fadb76c1a2d540e09ed/fqdn-1.5.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/6e/ef/0e8f1fe32f8a53dd26bdd1f9347efe0778b0fddf62789ea683f4cc7d787d/frozenlist-1.8.0-cp311-cp311-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/46/3e/51e7914c8a640548d1b980140b1bd1419c169bee300a556cfd7f4175444d/gemmi-0.7.4-cp311-cp311-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/e6/ab/fb21f4c939bb440104cc2b396d3be1d9b7a9fd3c6c2a53d98c45b3d7c954/fsspec-2026.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d5/08/c2409cb01d5368dcfedcbaffa7d044cc8957d57a9d0855244a5eb4709d30/funcy-2.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/42/15/26cac702cdf6281ddeb185d5912ce14e555e277c6e4caeb1d36966e43822/gemmi-0.7.5-cp311-cp311-macosx_11_0_arm64.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/9c/83/3b1d03d36f224edded98e9affd0467630fc09d766c0e56fb1498cbb04a9b/griffe-1.15.0-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/6a/09/e21df6aef1e1ffc0c816f0522ddc3f6dcded766c3261813131c78a704470/gitpython-3.1.46-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/4b/4c/cc8c68196db727cfc1432f2ad5de50aa6707e630d44b2e6361dc06d8f134/griffelib-2.0.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/c1/b0/1c628e26a0b95858f54aba17e1599e7f6cd241727596cc2580b72cb0a9bf/h5py-3.15.1-cp311-cp311-macosx_11_0_arm64.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/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/0f/1c/e5fd8f973d4f375adb21565739498e2e9a1e54c858a97b9a8ccfdc81da9b/identify-2.6.15-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/46/33/92ef41c6fad0233e41d3d84ba8e8ad18d1780f1e5d99b3c683e6d7f98b63/identify-2.6.18-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a4/ed/1f1afb2e9e7f38a545d628f864d562a5ae64fe6f7a10e28ffb9b185b4e89/importlib_resources-6.5.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/38/3d/2d244233ac4f76e38533cfcb2991c9eb4c7bf688ae0a036d30725b8faafe/importlib_metadata-9.0.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/12/c9/6869a1dcf4aaf309b9543ec070be3ec3adebee7c9bec9af8c230494134b9/interrogate-1.7.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f6/d8/502954a4ec0efcf264f99b65b41c3c54e65a647d9f0d6f62cd02227d242c/ipykernel-6.31.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/86/92/162cfaee4ccf370465c5af1ce36a9eacec1becb552f2033bb3584e6f640a/ipython-9.9.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/82/b9/e73d5d9f405cba7706c539aa8b311b49d4c2f3d698d9c12f815231169c71/ipykernel-7.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3d/aa/898dec789a05731cd5a9f50605b7b44a72bd198fd0d4528e11fc610177cc/ipython-9.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-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/7b/55/e5326141505c5d5e34c5e0935d2908a74e4561eca44108fbfb9c13d2911a/isoduration-20.11.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/72/b9/313e8f2f2e9517ae050a692ae7b3e4b3f17cc5e6dfea0db51fe14e586580/jinja2_ansible_filters-1.3.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d7/9e/038522f50ceb7e74f1f991bf1b699f24b0c2bbe7c390dd36ad69f4582258/json5-0.13.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/71/92/5e77f98553e9e75130c78900d000368476aed74276eb8ae8796f65f00918/jsonpointer-3.0.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9e/6a/a83720e953b1682d2d109d3c2dbb0bc9bf28cc1cbc205be4ef4be5da709d/jsonpointer-3.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/bb/f5/fddaec430367be9d62a7ed125530e133bfd4a1c0350fe221149ee0f2b526/jupyter_client-8.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2d/0b/ceb7694d864abc0a047649aec263878acb9f792e1fec3e676f22dc9015e3/jupyter_client-8.8.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/76/c0/8cc378aad5ccfcc37adeda27d3864f20ea808ad5b63572f5b986a42a649c/jupyter_dark_detect-0.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e2/48/577993f1f99c552f18a0428731a755e06171f9902fa118c379eb7c04ea22/jupyter_events-0.12.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1a/60/1f6cee0c46263de1173894f0fafcb3475ded276c472c14d25e0280c18d6d/jupyter_lsp-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f4/a4/61adb19f3c74b0dc0e411de4f06ebef564b1f179928f9dffcbd4b378f2ef/jupyter_notebook_parser-0.1.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/92/80/a24767e6ca280f5a49525d987bf3e4d7552bf67c8be07e8ccf20271f8568/jupyter_server-2.17.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/07/2d/2b32cdbe8d2a602f697a649798554e4f072115438e92249624e532e8aca6/jupyter_server_terminals-0.5.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/af/c3/acced767eecc11a70c65c45295db5396c4f0c1937874937d5a76d7b177b6/jupyterlab-4.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d1/2d/6674563f71c6320841fc300911a55143925112a72a883e2ca71fba4c618d/jupyter_server_terminals-0.5.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e1/1b/dad6fdcc658ed7af26fdf3841e7394072c9549a8b896c381ab49dd11e2d9/jupyterlab-4.5.6-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b1/dd/ead9d8ea85bf202d90cc513b533f9c363121c7792674f78e0d8a854b63b4/jupyterlab_pygments-0.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e0/07/a000fe835f76b7e1143242ab1122e6362ef1c03f23f83a045c38859c2ae0/jupyterlab_server-2.28.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f7/5a/488f492b49b712ca38326e12c78d0d48ab6be682a2989ce533f0f2c4dfd2/jupyterquiz-2.9.6.2-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/bd/0d/2d240e7098e0cafba4d25e9530e7596b1bb1bd4476e41b10346bcaaa36d6/jupytext-1.18.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/31/a2/a12a503ac1fd4943c50f9822678e8015a790a13b5490354c68afb8489814/kiwisolver-1.4.9-cp311-cp311-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/ab/b5/36c712098e6191d1b4e349304ef73a8d06aed77e56ceaac8c0a306c7bda1/jupyterlab_widgets-3.0.16-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/52/bc858b1665d0dec3a2511f4e6f5c18ea85c0977563d624d597c95d6d0fd7/jupyterquiz-2.9.6.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/16/5a/736dd2f4535dbf3bf26523f9158c011389ef88dd06ec2eef67fd744f1c7b/jupytext-1.19.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0a/aa/510dc933d87767584abfe03efa445889996c70c2990f6f87c3ebaa0a18c5/kiwisolver-1.5.0-cp311-cp311-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/82/3d/14ce75ef66813643812f3093ab17e46d3a206942ce7376d31ec2d36229e7/lark-1.3.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/38/7e/7b91c89a4cf0f543a83be978657afb20c86af6d725253e319589dcc4ce52/lmfit-1.3.4-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/d2/f0/834e479e47e499b6478e807fb57b31cc2db696c4db30557bb6f5aea4a90b/mando-0.7.1-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/70/81/54e3ce63502cd085a0c556652a4e1b919c45a446bd1e5300e10c44c8c521/markdown-3.10-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/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/fd/14/baad3222f424b19ce6ad243c71de1ad9ec6b2e4eb1e458a48fdc6d120401/matplotlib-3.10.8-cp311-cp311-macosx_11_0_arm64.whl @@ -1712,154 +1996,176 @@ environments: - 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/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/fd/1a/31b7cd6e4e7a02df4e076162e9783620777592bea9e4bb036389389af99d/mike-2.1.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/48/f7/10f5e101db25741b91e4f4792c5d97b4fa834ead5cf509ae91097d939424/mike-2.1.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9b/f7/4a5e785ec9fbd65146a27b6b70b6cdc161a66f2024e4b04ac06a67f5578b/mistune-3.2.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/71/26/4d39d52ea2219604053a4d05b98e90d6a335511cc01806436ec4886b1028/mkdocs_autorefs-1.2.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/9f/d4/029f984e8d3f3b6b726bd33cafc473b75e9e44c0f7e80a5b29abc466bdea/mkdocs_get_deps-0.2.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/08/37/5f1fd5c3f6954b3256f8126275e62af493b96fb6aef6c0dbc4ee326032ad/mkdocs_jupyter-0.25.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/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/93/89/eb601278b12c471235860992f5973cf3c55ca3f77d1d6127389eb045a021/mkdocs_jupyter-0.26.1-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/3e/32/ed071cb721aca8c227718cffcf7bd539620e9799bbf2619e90c757bfd030/mkdocs_material-9.7.1-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/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.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/cd/10/4c27c3063c2b3681a4b7942f8dbdeb4fa34fecb2c19b594e7345ebf4f86f/mkdocstrings-0.27.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/51/23/d02d86553327296c3bf369d444194ea83410cce8f0e690565264f37f3261/mkdocstrings_python-1.13.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/04/41/1cf02e3df279d2dd846a1bf235a928254eba9006dd22b4a14caa71aed0f7/mkdocstrings-1.0.3-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/5b/69/93b34728cc386efdde0c342f8c680b9187dea7beb7adaf6b58a0713be101/mpld3-0.5.12-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/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.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/5a/15/3c225610da9f02505d37d69a77f4a2e7daae2a125f99d638df211ba84e59/msgspec-0.20.0-cp311-cp311-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/4d/e1/866a5d77be6ea435711bef2a4291eed11032679b6b28b56b4776ab06ba3e/multidict-6.7.0-cp311-cp311-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/3d/2e/cf2ffeb386ac3763526151163ad7da9f1b586aac96d2b4f7de1eaebf0c61/narwhals-2.15.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/3f/c3/06490e98393dcb4d6ce2bf331a39335375c300afaef526897881fbeae6ab/narwhals-2.18.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/83/a0/5b0c2f11142ed1dddec842457d3f65eaf71a0080894eb6f018755b319c3a/nbclient-0.10.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/cc/9a/cd673b2f773a12c992f41309ef81b99da1690426bd2f96957a7ade0d3ed7/nbconvert-7.16.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0d/4b/8d5f796a792f8a25f6925a96032f098789f448571eb92011df1ae59e8ea8/nbconvert-7.17.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a9/82/0340caa499416c78e5d8f5f05947ae4bc3cba53c9f038ab6e9ed964e22f1/nbformat-5.10.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/eb/be/b257e12f9710819fde40adc972578bee6b72c5992da1bc8369bef2597756/nbmake-1.5.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/28/88/4789719fbbe166d12d345b3ac66b96105f10001b16e00a9765ba29261a21/nbqa-1.9.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/12/e5/838eb1004fb9b6f0383c47c0d902eb698fda052e8e64ca48c70a2144a48c/nbstripout-0.8.2-py2.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/fc/39/dfdef4cfbfcf7c80cd144b1b2e262c2e785f18b152eeebe14ecd70ee7455/ncrystal-4.2.12-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/55/8d/2b26572e909238bb114d50fb0d1b6b54eb6dafa2d83a7264f18146796b0d/ncrystal_core-4.2.12-py3-none-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/78/98/20fdd6825d1876fc54bb098509e736073d20f569b033f2f23bf74aa1df9c/ncrystal_python-4.2.12-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.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/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f9/33/bd5b9137445ea4b680023eb0469b2bb969d61303dedb2aac6560ff3d14a1/notebook_shim-0.2.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/2a/ea/25e26fa5837106cde46ae7d0b667e20f69cbbc0efd64cba8221411ab26ae/numpy-2.3.5-cp311-cp311-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/ef/27/d26c85cbcd86b26e4f125b0668e7a7c0542d19dd7d23ee12e87b550e95b5/numpy-2.4.3-cp311-cp311-macosx_14_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/2c/ab/fc8290c6a4c722e5514d80f62b2dc4c4df1a68a41d1364e625c35990fcf3/overrides-7.7.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/9b/35/74442388c6cf008882d4d4bdfc4109be87e9b8b7ccd097ad1e7f006e2e95/pandas-2.3.3-cp311-cp311-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/c1/27/90683c7122febeefe84a56f2cde86a9f05f68d53885cebcc473298dfc33e/pandas-3.0.1-cp311-cp311-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/ef/af/4fbc8cab944db5d21b7e2a5b8e9211a03a79852b1157e2c102fcc61ac440/pandocfilters-1.5.1-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/16/32/f8e3c85d1d5250232a5d3477a2a28cc291968ff175caeadaf3cc19ce0e4a/parso-0.8.5-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/05/bb/39e6768529454cc2b57e1e2fa0a0a18ff64397a16303270e215a3e03285f/pathspec-1.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b6/61/fae042894f4296ec49e3f193aff5d7c18440da9e48102c3315e1bc4519a7/parso-0.8.6-py2.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/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/43/06/7264c0597e676104cc22ca73ee48f752767cd4b1fe084662620b17e10120/pillow-12.1.0-cp311-cp311-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/44/3c/d717024885424591d5376220b5e836c2d5293ce2011523c9de23ff7bf068/pip-25.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/78/93/a29e9bc02d1cf557a834da780ceccd54e02421627200696fcf805ebdc3fb/pillow-12.1.1-cp311-cp311-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/c4/36/ce5f75aa7c736a663a901766edc3580098c7ea3959a0e878363a54a3714e/pixi_kernel-0.7.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e7/c3/3031c931098de393393e1f93a38dc9ed6805d86bb801acc3cf2d5bd1e6b7/plotly-6.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/63/d7/97f7e3a6abb67d8080dd406fd4df842c2be0efaf712d1c899c32a075027c/platformdirs-4.9.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/31/8b/9e8baf7dacac8d0c174925c38ff43c6d94bc9abb35503f67762caccb6869/plopp-26.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/52/d2/c6e44dba74f17c6216ce1b56044a9b93a929f1c2d5bdaff892512b260f5e/plotly-6.6.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/79/ad/45312df6b63ba64ea35b8d8f5f0c577aac16e6b416eafe8e1cb34e03f9a7/plumbum-1.10.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/a8/87/77cc11c7a9ea9fd05503def69e3d18605852cd0d4b0d3b8f15bbeb3ef1d1/pooch-1.8.2-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/5d/19/fd3ef348460c80af7bb4669ea7926651d1f95c23ff2df18b9d24bab4f3fa/pre_commit-4.5.1-py2.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/b8/db/14bafcb4af2139e046d03fd00dea7873e48eafe18b7d2797e73d6681f210/prometheus_client-0.23.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/74/c3/24a2f845e3917201628ecaba4f18bab4d18a337834c1df2a159ee9d22a42/prometheus_client-0.24.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-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/c5/2c/78e4a789306a92ade5000da4f5de3255202c534acdadc3aac7b5458fadef/psutil-7.2.1-cp36-abi3-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-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/0b/a6/e2e8535d8146bce05de6e0ecf1099a7e2887d840ae2a7b3a09385543fd02/py3dmol-2.5.3-py2.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/f5/5c/b999ea3e64981018d52846b9b69193fa581a70cd255912cb6962a33a666a/pycifrw-5.0.1-cp311-cp311-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/78/3c/2a612b95ddbb9a6bdcb47b7a93c4884f74c6ff22356b2f7b213b16e65c35/pycifstar-0.3.0.tar.gz - pypi: https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/87/6f/cc2b231dc78d8c3aaa674a676db190b8f8071c50134af8f8cf39b9b8e8df/pydoclint-0.8.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ea/10/47caf89cbb52e5bb764696fd52a8c591a2f0e851a93270c05a17f36000b5/pymdown_extensions-10.20-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/8b/40/2614036cdd416452f5bf98ec037f38a1afb17f327cb8e6b652d4729e0af8/pyparsing-3.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6f/2c/5b079febdc65e1c3fb2729bf958d18b45be7113828528e8a0b5850dd819a/pymdown_extensions-10.21-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.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/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/50/74/c655a6eda0fd188d490c14142a0f0380655ac7099604e1fbf8fa1a97f0a1/python_engineio-4.13.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/67/0f/019d3949a40280f6193b62bc010177d4ce702d0fce424322286488569cd3/python_discovery-1.2.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/aa/54/0cce26da03a981f949bb8449c9778537f75f5917c172e1d2992ff25cb57d/python_engineio-4.13.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/51/e5/fecf13f06e5e5f67e8837d777d1bc43fac0ed2b77a676804df5c34744727/python_json_logger-4.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/28/d2/2ccc2b69a187b80fda3152745670cfba936704f296a9fa54c6c8ac694d12/python_socketio-5.16.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.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/d8/8b/e2bbeb42068f0c48899e8eddd34902afc0f7429d4d2a152d2dc2670dc661/pythreejs-2.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.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/5d/305323ba86b284e6fcb0d842d6adaa2999035f70f8c38a9b6d21ad28c3d4/pyzmq-27.1.0-cp311-cp311-macosx_10_15_universal2.whl + - pypi: https://files.pythonhosted.org/packages/3c/26/1062c7ec1b053db9e499b4d2d5bc231743201b74051c973dadeac80a8f43/questionary-2.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/93/f7/d00d9b4a0313a6be3a3e0818e6375e15da6d7076f4ae47d1324e7ca986a1/radon-6.0.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/56/5d/c814546c2333ceea4ba42262d8c4d55763003e767fa169adc693bd524478/requests-2.33.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/57/4d/a7545bf6c62b0dbe5795f22ea9e88cc070fdced5c34663ebc5bed2f610c0/returns-0.26.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9e/51/17023c0f8f1869d8806b979a2bffa3f861f26a3f1a66b094288323fba52f/rfc3986_validator-0.1.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7e/71/44ce230e1b7fadd372515a97e32a83011f906ddded8d03e3c6aafbdedbb7/rfc3987_syntax-1.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/94/ba/24e5ebb7c1c82e74c4e4f33b2112a5573ddc703915b13a073737b59b86e0/rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/7b/82/36be59f00a6082e38c23536df4e71cdbc6af8d7c707eade97fcad5c98235/ruff-0.14.10-py3-none-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/7c/89/d70e9f628749b7e4db2aa4cd89735502ff3f08f7b9b27d2e799485987cd9/scipy-1.16.3-cp311-cp311-macosx_12_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/1b/5a/f2f2e5eda25579f754acd83399c522ee03d6acbe001dfe53c8a1ec928b44/send2trash-2.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ca/f2/7a631a8af6d88bcef997eb1bf87cc3da158294c57044aafd3e17030613de/ruff-0.15.8-py3-none-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/60/54/5011adb56853caabfd90686c2e543d1e3c76a8ef2755809b7e12e3f3583b/scipp-26.3.1-cp311-cp311-macosx_14_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/fb/46/e50b38629e9e3f4a1dd55fb36d8b8abd1d59768c31151c1c8ed696f7b865/scippneutron-26.3.0-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/6d/ee/18146b7757ed4976276b9c9819108adbc73c5aad636e5353e20746b73069/scipy-1.17.1-cp311-cp311-macosx_14_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/1c/78/504fdd027da3b84ff1aecd9f6957e65f35134534ccc6da8628eb71e76d3f/send2trash-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9d/76/f789f7a86709c6b087c5a2f52f911838cad707cc613162401badc665acfe/setuptools-82.0.1-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/52/59/0782e51887ac6b07ffd1570e0364cf901ebc36345fea669969d2084baebb/simple_websocket-1.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/48/f3/b67d6ea49ca9154453b6d70b34ea22f3996b9fa55da105a79d8732227adc/soupsieve-2.8.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a2/1c/769552a9d840065137272ebe86ffbb0bc92b0f1e0a68ee5266a225f8cd7b/sqlalchemy-2.0.45-cp311-cp311-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/c1/d4/59e74daffcb57a07668852eeeb6035af9f32cbfd7a1d2511f17d2fe6a738/smmap-5.0.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-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/f6/ca/270d463f6c34f539bb55acdab14099c092d3be28c8af64d61399aa07610c/spglib-2.6.0-cp311-cp311-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/d7/6d/b8b78b5b80f3c3ab3f7fa90faa195ec3401f6d884b60221260fd4d51864c/sqlalchemy-2.0.48-cp311-cp311-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-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/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-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/6a/9e/2064975477fdc887e47ad42157e214526dcad8f317a948dee17e1659a62f/terminado-0.18.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e6/34/ebdc18bae6aa14fbee1a08b63c015c72b64868ff7dae68808ab500c492e2/tinycss2-1.4.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/33/f0/3fe8c6e69135a845f4106f2ff8b6805638d4e85c264e70114e8126689587/tokenize_rt-6.2.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/86/7f/d8fffe6a7aefdb61bced88fcb5e280cfd71e08939da5894161bd71bea022/tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/ab/a9/e94a9d5224107d7ce3cc1fab8d5dc97f5ea351ccc6322ee4fb661da94e35/tornado-6.5.4-cp39-abi3-macosx_10_9_universal2.whl + - pypi: https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/59/8c/77f5097695f4dd8255ecbd08b2a1ed8ba8b953d337804dd7080f199e12bf/tornado-6.5.5-cp39-abi3-macosx_10_9_universal2.whl - pypi: https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/4f/7e/bc19996fa86cad8801e8ffe6f1bba5836ca0160df76d0410d27432193712/trove_classifiers-2025.12.1.14-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/1b/a9/e3aee762739c1d7528da1c3e06d518503f8b6c439c35549b53735ba52ead/typeguard-4.4.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a0/1d/d9257dd49ff2ca23ea5f132edf1281a0c4f9de8a762b9ae399b670a59235/typer-0.21.1-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/bb/4a/2e5583e544bc437d5e8e54b47db87430df9031b29b48d17f26d129fa60c0/trove_classifiers-2026.1.14.14-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/4a/91/48db081e7a63bb37284f9fbcefda7c44c277b18b0e13fbc36ea2335b71e6/typer-0.24.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8f/5e/f1e1dd319e35e962a4e00b33150a8868b6329cc1d19fd533436ba5488f09/uncertainties-3.2.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f7/46/e7cea8159199096e1df52da20a57a6665da80c37fb8aeb848a3e47442c32/untokenize-0.1.1.tar.gz - pypi: https://files.pythonhosted.org/packages/e7/00/3fca040d7cf8a32776d3d81a00c8ee7457e00f80c649f1e4a863c8321ae9/uri_template-1.3.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/6d/b9/4095b668ea3678bf6a0af005527f39de12fb026516fb3df17495a733b7f8/urllib3-2.6.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/af/49/7230b1d56aeaee0eefd346a70f582463f11fb7036d2d020bcf68053bd994/uv-0.9.22-py3-none-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/a4/39/6983dd79f01aaa4c75d9ffa550fa393f0c4c28f7ccd6956e4188c62cefbc/validate_pyproject-0.24.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/ee/e9c95cda829131f71a8dff5ce0406059fd16e591c074414e31ada19ba7c3/validate_pyproject-0.25-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/bc/4a/c6fd02a642bbe4e9f25cdd3714a328e3fc3eb6bd7b5e96f1a2285bd928b9/varname-0.15.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1c/59/964ecb8008722d27d8a835baea81f56a91cea8e097b3be992bc6ccde6367/versioningit-3.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a4/ce/3b6fee91c85626eaf769d617f1be9d2e15c1cca027bbdeb2e0d751469355/verspec-0.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/79/0c/c05523fa3181fdf0c9c52a6ba91a23fbf3246cc095f26f6516f9c60e6771/virtualenv-20.35.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c6/59/7d02447a55b2e55755011a647479041bc92a82e143f96a8195cb33bd0a1c/virtualenv-21.2.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/af/b5/123f13c975e9f27ab9c0770f514345bd406d0e8d3b7a0723af9d43f710af/wcwidth-0.2.14-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/68/5a/199c59e0a824a3db2b89c5d2dade7ab5f9624dbf6448dc291b46d5ec94d3/wcwidth-0.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e2/cc/e097523dd85c9cf5d354f78310927f1656c422bd7b2613b2db3e3f9a0f2c/webcolors-25.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/34/db/b10e48aa8fff7407e67470363eac595018441cf32d5e1001567a7aeba5d2/websocket_client-1.9.0-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/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-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/c1/da/8da9f6a53f67b5106ffe902c6fa0164e10398d4e150d85838b82f424072a/yarl-1.22.0-cp311-cp311-macosx_11_0_arm64.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/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl + - pypi: ./ win-64: - - conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h0ad9c76_8.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.1.4-h4c7d964_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h0ad9c76_9.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.2.25-h4c7d964_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/gsl-2.8-h5b8d9c4_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/icu-78.1-h637d24d_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libblas-3.11.0-5_hf2e6a31_mkl.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libcblas-3.11.0-5_h2a3cdd5_mkl.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.7.3-hac47afa_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libffi-3.5.2-h52bdfb6_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libhwloc-2.12.1-default_h4379cf1_1003.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libblas-3.11.0-6_hf2e6a31_mkl.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libcblas-3.11.0-6_h2a3cdd5_mkl.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.7.4-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/libhwloc-2.12.2-default_h4379cf1_1000.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libiconv-1.18-hc1393d2_2.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-5.8.1-h2466b09_2.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.51.1-hf5d6505_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-5.8.2-hfd05255_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.52.0-hf5d6505_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libwinpthread-12.0.0.r4.gg4f2fc60ca-h57928b3_10.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libxml2-16-2.15.1-h3cfd58e_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libxml2-2.15.1-h779ef1b_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libzlib-1.3.1-h2466b09_2.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/llvm-openmp-21.1.8-h4fa8253_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/mkl-2025.3.0-hac47afa_455.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/nodejs-25.2.1-he453025_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.6.0-h725018a_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/python-3.11.14-h0159041_2_cpython.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/tbb-2022.3.0-hd094cb3_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h2c6b04d_3.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libxml2-16-2.15.2-h692994f_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libxml2-2.15.2-h5d26750_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/llvm-openmp-22.1.1-h4fa8253_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/mkl-2025.3.1-hac47afa_11.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.1-hf411b9b_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/tbb-2022.3.0-h3155e25_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h6ed50ae_3.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.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 @@ -1868,96 +2174,118 @@ environments: - pypi: https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/dc/f2/27cdf04c9851712d6c1b99df6821a6623c3c9e55956d4b1e318c337b5a48/aiohttp-3.13.3-cp311-cp311-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.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/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e2/c6/a759ece8f1829d1f162261226fbfd2c6832b3ff7657384045286d2afa384/argon2_cffi_bindings-25.1.0-cp39-abi3-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/ed/c9/d7977eaacb9df673210491da99e6a247e93df98c715fc43fd136ce1d3d33/arrow-1.4.0-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/6c/25/4f103d1bedb3593718713b3f743df7b3ff3fc68d36d6666c30265ef59c8a/ase-3.28.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/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/03/49/d10027df9fce941cb8184e78a02857af36360d33e1721df81c5ed2179a1a/async_lru-2.0.5-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e5/e2/c2e3abf398f80732e58b03be77bde9022550d221dd8781bf586bd4d97cc1/async_lru-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.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/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e6/9a/8da246d988ded941da96c7ed945d63e94a445637eaad985a0ed88787cb89/backrefs-6.1-py311-none-any.whl + - pypi: https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0f/f0/35240571e1b67ffb19dafb29ab34150b6f59f93f717b041082cdb1bfceb1/backrefs-6.2-py311-none-any.whl - pypi: https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-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/cd/3a/577b549de0cc09d95f11087ee63c739bba856cd3952697eec4c4bb91350a/bleach-6.3.0-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/cb/8c/2b30c12155ad8de0cf641d76a8b396a16d2c36bc6d50b621a62b7c4567c1/build-1.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4a/57/3b7d4dd193ade4641c865bc2b93aeeb71162e81fc348b8dad020215601ed/build-1.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/43/8e/278ea79cf8ee0c395a5ad74625b3465c2fc234bb277f171dd59dd203820d/bumps-1.0.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.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/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/91/d7/47988d40231b41376f5a66346ef3b322c81091dfd4c0f84df5a1e3bb06b5/chardet-7.4.0.post1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c6/e3/76f2facfe8eddee0bbd38d2594e709033338eae44ebf1738bcefe0a06185/charset_normalizer-3.4.6-cp311-cp311-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.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/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/98/4b/9bd370b004b5c9d8045c6c33cf65bae018b27aca550a3f657cdc99acdbd8/contourpy-1.3.3-cp311-cp311-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/27/56/c216625f453df6e0559ed666d246fcbaaa93f3aa99eaa5080cea1229aa3d/coverage-7.13.1-cp311-cp311-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/df/91/4a151c94320458895049a3e23b7b2cfc08953c60b14892de837e8eb51d0a/copier-9.14.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/af/7f/4cd8a92531253f9d7c1bbecd9fa1b472907fb54446ca768c59b531248dc5/coverage-7.13.5-cp311-cp311-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/52/e8/c14cc8af8cd38e86887053843382629bd8ebd117f83f15eb1194d65a2c9d/cryspy-0.10.0-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/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-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/f2/a8/aaac7ff12ddf5d68a39e13a423a8490426f5f661384f5ad8d9062761bd8e/debugpy-1.8.19-cp311-cp311-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/4a/f3/00bb1e867fba351e2d784170955713bee200c43ea306c59f30bd7e748192/dask-2026.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d5/92/1cb532e88560cbee973396254b21bece8c5d7c2ece958a67afa08c9f10dc/debugpy-1.8.20-cp311-cp311-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/68/49/869b49db0bad6fba7c7471d612c356e745929d14e0de63acbc53e88f2a50/dfo_ls-1.6-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/52/33/fae9a52a6cb97efd21176303dfef44e487b56e3473c1329e019d5682d158/diffpy_pdffit2-1.5.2-cp311-cp311-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/8e/52/39914bf42bb01901c91781def35c1aeffa431a59299e9748c0cfae3e5493/diffpy_structure-3.3.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/79/c8/f0f4ab7fd08950d5358579364a0a9b9198bf03e179a4fcceae4b353be32e/diffpy_utils-3.6.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/50/3d/9373ad9c56321fdab5b41197068e1d8c25883b3fea29dd361f9b55116869/dill-0.4.0-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/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/dc/b4/a7ec1eaee86761a9dbfd339732b4706db3c6b65e970c12f0f56cfcce3dcf/docformatter-1.7.7-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/82/ee/c9ca3f81d69ec6c99ca41e10432a58c60fef3a6e63cce6df7e796f90fe22/easydiffraction-0.10.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/50/98b146aea0f1cd7531d25f12bea69fa9ce8d1662124f93fb30dc4511b65e/docstring_parser_fork-0.0.14-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/87/10/2c7edbf230e5c507d38367af498fa94258ed97205d9b4b6f63a921fe9c49/dunamai-1.26.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/55/cd/d729a1bb63fa95387228cc508552dea4685ea0116e484e73238db10f9521/essdiffraction-26.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f1/bb/66c80d7f801b191f7b3ee6149a39be9d1a8a81c233e20adaf796d171f93a/essreduce-26.4.0-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/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cb/a8/20d0723294217e47de6d9e2e40fd4a9d2f7c4b6ef974babd482a59743694/fastjsonschema-2.21.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/9a/30/ab407e2ec752aa541704ed8f93c11e2a5d92c168b8a755d818b74a3c5c2d/filelock-3.20.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/07/ad/37dd1ae5fa6e01612a1fbb954f0927681f282925a86e86198ccd7b15d515/fonttools-4.61.1-cp311-cp311-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/a4/a5/842ae8f0c08b61d6484b52f99a03510a3a72d23141942d216ebe81fefbce/filelock-3.25.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d3/97/bf54c5b3f2be34e1f143e6db838dfdc54f2ffa3e68c738934c82f3b2a08d/fonttools-4.62.1-cp311-cp311-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/33/b2/986d1220f6ee931e338d272bc1f3ec02cfe5f9b5fad84e95afdad57f1ebc/format_docstring-0.2.7-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cf/58/8acf1b3e91c58313ce5cb67df61001fc9dcd21be4fadb76c1a2d540e09ed/fqdn-1.5.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/0a/f5/603d0d6a02cfd4c8f2a095a54672b3cf967ad688a60fb9faf04fc4887f65/frozenlist-1.8.0-cp311-cp311-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/dd/9d/412d75eb7b9c0aa1e939b419a66c7d61471aa387919d4be32893921564af/gemmi-0.7.4-cp311-cp311-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/e6/ab/fb21f4c939bb440104cc2b396d3be1d9b7a9fd3c6c2a53d98c45b3d7c954/fsspec-2026.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d5/08/c2409cb01d5368dcfedcbaffa7d044cc8957d57a9d0855244a5eb4709d30/funcy-2.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b9/5e/62402bf021183bc6122cb01b8f1be17cac67545713fb30f888f59357a782/gemmi-0.7.5-cp311-cp311-win_amd64.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/1d/d5/c339b3b4bc8198b7caa4f2bd9fd685ac9f29795816d8db112da3d04175bb/greenlet-3.3.0-cp311-cp311-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/9c/83/3b1d03d36f224edded98e9affd0467630fc09d766c0e56fb1498cbb04a9b/griffe-1.15.0-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/6a/09/e21df6aef1e1ffc0c816f0522ddc3f6dcded766c3261813131c78a704470/gitpython-3.1.46-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/f1/3a/efb2cf697fbccdf75b24e2c18025e7dfa54c4f31fab75c51d0fe79942cef/greenlet-3.3.2-cp311-cp311-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/4b/4c/cc8c68196db727cfc1432f2ad5de50aa6707e630d44b2e6361dc06d8f134/griffelib-2.0.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/23/95/499b4e56452ef8b6c95a271af0dde08dac4ddb70515a75f346d4f400579b/h5py-3.15.1-cp311-cp311-win_amd64.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/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/0f/1c/e5fd8f973d4f375adb21565739498e2e9a1e54c858a97b9a8ccfdc81da9b/identify-2.6.15-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/46/33/92ef41c6fad0233e41d3d84ba8e8ad18d1780f1e5d99b3c683e6d7f98b63/identify-2.6.18-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a4/ed/1f1afb2e9e7f38a545d628f864d562a5ae64fe6f7a10e28ffb9b185b4e89/importlib_resources-6.5.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/38/3d/2d244233ac4f76e38533cfcb2991c9eb4c7bf688ae0a036d30725b8faafe/importlib_metadata-9.0.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/12/c9/6869a1dcf4aaf309b9543ec070be3ec3adebee7c9bec9af8c230494134b9/interrogate-1.7.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f6/d8/502954a4ec0efcf264f99b65b41c3c54e65a647d9f0d6f62cd02227d242c/ipykernel-6.31.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/86/92/162cfaee4ccf370465c5af1ce36a9eacec1becb552f2033bb3584e6f640a/ipython-9.9.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/82/b9/e73d5d9f405cba7706c539aa8b311b49d4c2f3d698d9c12f815231169c71/ipykernel-7.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3d/aa/898dec789a05731cd5a9f50605b7b44a72bd198fd0d4528e11fc610177cc/ipython-9.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-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/7b/55/e5326141505c5d5e34c5e0935d2908a74e4561eca44108fbfb9c13d2911a/isoduration-20.11.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/72/b9/313e8f2f2e9517ae050a692ae7b3e4b3f17cc5e6dfea0db51fe14e586580/jinja2_ansible_filters-1.3.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d7/9e/038522f50ceb7e74f1f991bf1b699f24b0c2bbe7c390dd36ad69f4582258/json5-0.13.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/71/92/5e77f98553e9e75130c78900d000368476aed74276eb8ae8796f65f00918/jsonpointer-3.0.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9e/6a/a83720e953b1682d2d109d3c2dbb0bc9bf28cc1cbc205be4ef4be5da709d/jsonpointer-3.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/bb/f5/fddaec430367be9d62a7ed125530e133bfd4a1c0350fe221149ee0f2b526/jupyter_client-8.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2d/0b/ceb7694d864abc0a047649aec263878acb9f792e1fec3e676f22dc9015e3/jupyter_client-8.8.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/76/c0/8cc378aad5ccfcc37adeda27d3864f20ea808ad5b63572f5b986a42a649c/jupyter_dark_detect-0.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e2/48/577993f1f99c552f18a0428731a755e06171f9902fa118c379eb7c04ea22/jupyter_events-0.12.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1a/60/1f6cee0c46263de1173894f0fafcb3475ded276c472c14d25e0280c18d6d/jupyter_lsp-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f4/a4/61adb19f3c74b0dc0e411de4f06ebef564b1f179928f9dffcbd4b378f2ef/jupyter_notebook_parser-0.1.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/92/80/a24767e6ca280f5a49525d987bf3e4d7552bf67c8be07e8ccf20271f8568/jupyter_server-2.17.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/07/2d/2b32cdbe8d2a602f697a649798554e4f072115438e92249624e532e8aca6/jupyter_server_terminals-0.5.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/af/c3/acced767eecc11a70c65c45295db5396c4f0c1937874937d5a76d7b177b6/jupyterlab-4.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d1/2d/6674563f71c6320841fc300911a55143925112a72a883e2ca71fba4c618d/jupyter_server_terminals-0.5.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e1/1b/dad6fdcc658ed7af26fdf3841e7394072c9549a8b896c381ab49dd11e2d9/jupyterlab-4.5.6-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b1/dd/ead9d8ea85bf202d90cc513b533f9c363121c7792674f78e0d8a854b63b4/jupyterlab_pygments-0.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e0/07/a000fe835f76b7e1143242ab1122e6362ef1c03f23f83a045c38859c2ae0/jupyterlab_server-2.28.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f7/5a/488f492b49b712ca38326e12c78d0d48ab6be682a2989ce533f0f2c4dfd2/jupyterquiz-2.9.6.2-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/bd/0d/2d240e7098e0cafba4d25e9530e7596b1bb1bd4476e41b10346bcaaa36d6/jupytext-1.18.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/3b/c6/f8df8509fd1eee6c622febe54384a96cfaf4d43bf2ccec7a0cc17e4715c9/kiwisolver-1.4.9-cp311-cp311-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/ab/b5/36c712098e6191d1b4e349304ef73a8d06aed77e56ceaac8c0a306c7bda1/jupyterlab_widgets-3.0.16-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/52/bc858b1665d0dec3a2511f4e6f5c18ea85c0977563d624d597c95d6d0fd7/jupyterquiz-2.9.6.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/16/5a/736dd2f4535dbf3bf26523f9158c011389ef88dd06ec2eef67fd744f1c7b/jupytext-1.19.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/be/6c/28f17390b62b8f2f520e2915095b3c94d88681ecf0041e75389d9667f202/kiwisolver-1.5.0-cp311-cp311-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/82/3d/14ce75ef66813643812f3093ab17e46d3a206942ce7376d31ec2d36229e7/lark-1.3.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/38/7e/7b91c89a4cf0f543a83be978657afb20c86af6d725253e319589dcc4ce52/lmfit-1.3.4-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/d2/f0/834e479e47e499b6478e807fb57b31cc2db696c4db30557bb6f5aea4a90b/mando-0.7.1-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/70/81/54e3ce63502cd085a0c556652a4e1b919c45a446bd1e5300e10c44c8c521/markdown-3.10-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/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/6f/d3/a4bbc01c237ab710a1f22b4da72f4ff6d77eb4c7735ea9811a94ae239067/matplotlib-3.10.8-cp311-cp311-win_amd64.whl @@ -1965,270 +2293,315 @@ environments: - 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/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/fd/1a/31b7cd6e4e7a02df4e076162e9783620777592bea9e4bb036389389af99d/mike-2.1.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/48/f7/10f5e101db25741b91e4f4792c5d97b4fa834ead5cf509ae91097d939424/mike-2.1.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9b/f7/4a5e785ec9fbd65146a27b6b70b6cdc161a66f2024e4b04ac06a67f5578b/mistune-3.2.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/71/26/4d39d52ea2219604053a4d05b98e90d6a335511cc01806436ec4886b1028/mkdocs_autorefs-1.2.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/9f/d4/029f984e8d3f3b6b726bd33cafc473b75e9e44c0f7e80a5b29abc466bdea/mkdocs_get_deps-0.2.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/08/37/5f1fd5c3f6954b3256f8126275e62af493b96fb6aef6c0dbc4ee326032ad/mkdocs_jupyter-0.25.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/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/93/89/eb601278b12c471235860992f5973cf3c55ca3f77d1d6127389eb045a021/mkdocs_jupyter-0.26.1-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/3e/32/ed071cb721aca8c227718cffcf7bd539620e9799bbf2619e90c757bfd030/mkdocs_material-9.7.1-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/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.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/cd/10/4c27c3063c2b3681a4b7942f8dbdeb4fa34fecb2c19b594e7345ebf4f86f/mkdocstrings-0.27.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/51/23/d02d86553327296c3bf369d444194ea83410cce8f0e690565264f37f3261/mkdocstrings_python-1.13.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/04/41/1cf02e3df279d2dd846a1bf235a928254eba9006dd22b4a14caa71aed0f7/mkdocstrings-1.0.3-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/5b/69/93b34728cc386efdde0c342f8c680b9187dea7beb7adaf6b58a0713be101/mpld3-0.5.12-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/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.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/89/5e/406b7d578926b68790e390d83a1165a9bfc2d95612a1a9c1c4d5c72ea815/msgspec-0.20.0-cp311-cp311-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/32/0f/13228f26f8b882c34da36efa776c3b7348455ec383bab4a66390e42963ae/multidict-6.7.0-cp311-cp311-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/3d/2e/cf2ffeb386ac3763526151163ad7da9f1b586aac96d2b4f7de1eaebf0c61/narwhals-2.15.0-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/3f/c3/06490e98393dcb4d6ce2bf331a39335375c300afaef526897881fbeae6ab/narwhals-2.18.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/83/a0/5b0c2f11142ed1dddec842457d3f65eaf71a0080894eb6f018755b319c3a/nbclient-0.10.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/cc/9a/cd673b2f773a12c992f41309ef81b99da1690426bd2f96957a7ade0d3ed7/nbconvert-7.16.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0d/4b/8d5f796a792f8a25f6925a96032f098789f448571eb92011df1ae59e8ea8/nbconvert-7.17.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a9/82/0340caa499416c78e5d8f5f05947ae4bc3cba53c9f038ab6e9ed964e22f1/nbformat-5.10.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/eb/be/b257e12f9710819fde40adc972578bee6b72c5992da1bc8369bef2597756/nbmake-1.5.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/28/88/4789719fbbe166d12d345b3ac66b96105f10001b16e00a9765ba29261a21/nbqa-1.9.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/12/e5/838eb1004fb9b6f0383c47c0d902eb698fda052e8e64ca48c70a2144a48c/nbstripout-0.8.2-py2.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/fc/39/dfdef4cfbfcf7c80cd144b1b2e262c2e785f18b152eeebe14ecd70ee7455/ncrystal-4.2.12-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7b/51/e13a37a8d924feefb444d7eb42094750ba1bba756cbb8c1f9a523414c4fb/ncrystal_core-4.2.12-py3-none-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/78/98/20fdd6825d1876fc54bb098509e736073d20f569b033f2f23bf74aa1df9c/ncrystal_python-4.2.12-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.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/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f9/33/bd5b9137445ea4b680023eb0469b2bb969d61303dedb2aac6560ff3d14a1/notebook_shim-0.2.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/aa/44/9fe81ae1dcc29c531843852e2874080dc441338574ccc4306b39e2ff6e59/numpy-2.3.5-cp311-cp311-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/76/1d/edccf27adedb754db7c4511d5eac8b83f004ae948fe2d3509e8b78097d4c/numpy-2.4.3-cp311-cp311-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/2c/ab/fc8290c6a4c722e5514d80f62b2dc4c4df1a68a41d1364e625c35990fcf3/overrides-7.7.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/8e/59/712db1d7040520de7a4965df15b774348980e6df45c129b8c64d0dbe74ef/pandas-2.3.3-cp311-cp311-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/1f/67/af63f83cd6ca603a00fe8530c10a60f0879265b8be00b5930e8e78c5b30b/pandas-3.0.1-cp311-cp311-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/ef/af/4fbc8cab944db5d21b7e2a5b8e9211a03a79852b1157e2c102fcc61ac440/pandocfilters-1.5.1-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/16/32/f8e3c85d1d5250232a5d3477a2a28cc291968ff175caeadaf3cc19ce0e4a/parso-0.8.5-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/05/bb/39e6768529454cc2b57e1e2fa0a0a18ff64397a16303270e215a3e03285f/pathspec-1.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/6c/af/b1d7e301c4cd26cd45d4af884d9ee9b6fab893b0ad2450d4746d74a6968c/pillow-12.1.0-cp311-cp311-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/44/3c/d717024885424591d5376220b5e836c2d5293ce2011523c9de23ff7bf068/pip-25.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b6/61/fae042894f4296ec49e3f193aff5d7c18440da9e48102c3315e1bc4519a7/parso-0.8.6-py2.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/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/31/03/bef822e4f2d8f9d7448c133d0a18185d3cce3e70472774fffefe8b0ed562/pillow-12.1.1-cp311-cp311-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/c4/36/ce5f75aa7c736a663a901766edc3580098c7ea3959a0e878363a54a3714e/pixi_kernel-0.7.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e7/c3/3031c931098de393393e1f93a38dc9ed6805d86bb801acc3cf2d5bd1e6b7/plotly-6.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/63/d7/97f7e3a6abb67d8080dd406fd4df842c2be0efaf712d1c899c32a075027c/platformdirs-4.9.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/31/8b/9e8baf7dacac8d0c174925c38ff43c6d94bc9abb35503f67762caccb6869/plopp-26.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/52/d2/c6e44dba74f17c6216ce1b56044a9b93a929f1c2d5bdaff892512b260f5e/plotly-6.6.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/79/ad/45312df6b63ba64ea35b8d8f5f0c577aac16e6b416eafe8e1cb34e03f9a7/plumbum-1.10.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/a8/87/77cc11c7a9ea9fd05503def69e3d18605852cd0d4b0d3b8f15bbeb3ef1d1/pooch-1.8.2-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/5d/19/fd3ef348460c80af7bb4669ea7926651d1f95c23ff2df18b9d24bab4f3fa/pre_commit-4.5.1-py2.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/b8/db/14bafcb4af2139e046d03fd00dea7873e48eafe18b7d2797e73d6681f210/prometheus_client-0.23.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/74/c3/24a2f845e3917201628ecaba4f18bab4d18a337834c1df2a159ee9d22a42/prometheus_client-0.24.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-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/34/68/d9317542e3f2b180c4306e3f45d3c922d7e86d8ce39f941bb9e2e9d8599e/psutil-7.2.1-cp37-abi3-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-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/0b/a6/e2e8535d8146bce05de6e0ecf1099a7e2887d840ae2a7b3a09385543fd02/py3dmol-2.5.3-py2.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/7c/58/e60915c59f4adcbd97af30047694978127d63139ae05a0cf987c6f2e90f9/pycifrw-5.0.1-cp311-cp311-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/78/3c/2a612b95ddbb9a6bdcb47b7a93c4884f74c6ff22356b2f7b213b16e65c35/pycifstar-0.3.0.tar.gz - pypi: https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/87/6f/cc2b231dc78d8c3aaa674a676db190b8f8071c50134af8f8cf39b9b8e8df/pydoclint-0.8.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ea/10/47caf89cbb52e5bb764696fd52a8c591a2f0e851a93270c05a17f36000b5/pymdown_extensions-10.20-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/8b/40/2614036cdd416452f5bf98ec037f38a1afb17f327cb8e6b652d4729e0af8/pyparsing-3.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6f/2c/5b079febdc65e1c3fb2729bf958d18b45be7113828528e8a0b5850dd819a/pymdown_extensions-10.21-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.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/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/50/74/c655a6eda0fd188d490c14142a0f0380655ac7099604e1fbf8fa1a97f0a1/python_engineio-4.13.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/67/0f/019d3949a40280f6193b62bc010177d4ce702d0fce424322286488569cd3/python_discovery-1.2.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/aa/54/0cce26da03a981f949bb8449c9778537f75f5917c172e1d2992ff25cb57d/python_engineio-4.13.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/51/e5/fecf13f06e5e5f67e8837d777d1bc43fac0ed2b77a676804df5c34744727/python_json_logger-4.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/28/d2/2ccc2b69a187b80fda3152745670cfba936704f296a9fa54c6c8ac694d12/python_socketio-5.16.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a6/a1/409c1651c9f874d598c10f51ff586c416625601df4bca315d08baec4c3e3/pywinpty-3.0.2-cp311-cp311-win_amd64.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/d8/8b/e2bbeb42068f0c48899e8eddd34902afc0f7429d4d2a152d2dc2670dc661/pythreejs-2.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/51/8f/9bb81dd5bb77d22243d33c8397f09377056d5c687aa6d4042bea7fbf8364/pywin32-311-cp311-cp311-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/79/c3/3e75075c7f71735f22b66fab0481f2c98e3a4d58cba55cb50ba29114bcf6/pywinpty-3.0.3-cp311-cp311-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-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/23/6d/d8d92a0eb270a925c9b4dd039c0b4dc10abc2fcbc48331788824ef113935/pyzmq-27.1.0-cp311-cp311-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/3c/26/1062c7ec1b053db9e499b4d2d5bc231743201b74051c973dadeac80a8f43/questionary-2.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/93/f7/d00d9b4a0313a6be3a3e0818e6375e15da6d7076f4ae47d1324e7ca986a1/radon-6.0.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/56/5d/c814546c2333ceea4ba42262d8c4d55763003e767fa169adc693bd524478/requests-2.33.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/57/4d/a7545bf6c62b0dbe5795f22ea9e88cc070fdced5c34663ebc5bed2f610c0/returns-0.26.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9e/51/17023c0f8f1869d8806b979a2bffa3f861f26a3f1a66b094288323fba52f/rfc3986_validator-0.1.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7e/71/44ce230e1b7fadd372515a97e32a83011f906ddded8d03e3c6aafbdedbb7/rfc3987_syntax-1.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/fa/5b/e7b7aa136f28462b344e652ee010d4de26ee9fd16f1bfd5811f5153ccf89/rpds_py-0.30.0-cp311-cp311-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/fb/9c/896c862e13886fae2af961bef3e6312db9ebc6adc2b156fe95e615dee8c1/ruff-0.14.10-py3-none-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/f1/d0/22ec7036ba0b0a35bccb7f25ab407382ed34af0b111475eb301c16f8a2e5/scipy-1.16.3-cp311-cp311-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/1b/5a/f2f2e5eda25579f754acd83399c522ee03d6acbe001dfe53c8a1ec928b44/send2trash-2.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1f/a2/ef467cb77099062317154c63f234b8a7baf7cb690b99af760c5b68b9ee7f/ruff-0.15.8-py3-none-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/af/46/661159ad844034ba8b3f4e0516215c41e4ee17db4213d13a82227670764f/sciline-25.11.1-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/fb/46/e50b38629e9e3f4a1dd55fb36d8b8abd1d59768c31151c1c8ed696f7b865/scippneutron-26.3.0-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/95/da/0d1df507cf574b3f224ccc3d45244c9a1d732c81dcb26b1e8a766ae271a8/scipy-1.17.1-cp311-cp311-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/1c/78/504fdd027da3b84ff1aecd9f6957e65f35134534ccc6da8628eb71e76d3f/send2trash-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9d/76/f789f7a86709c6b087c5a2f52f911838cad707cc613162401badc665acfe/setuptools-82.0.1-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/52/59/0782e51887ac6b07ffd1570e0364cf901ebc36345fea669969d2084baebb/simple_websocket-1.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/48/f3/b67d6ea49ca9154453b6d70b34ea22f3996b9fa55da105a79d8732227adc/soupsieve-2.8.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/bc/fa/09d0a11fe9f15c7fa5c7f0dd26be3d235b0c0cbf2f9544f43bc42efc8a24/sqlalchemy-2.0.45-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/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-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/25/a8/d89e1bde525baba10eb8d0be79a5bbaf56c59a47b32bb954866d96a228e3/spglib-2.6.0-cp311-cp311-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/58/d5/dd767277f6feef12d05651538f280277e661698f617fa4d086cce6055416/sqlalchemy-2.0.48-cp311-cp311-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-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/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-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/6a/9e/2064975477fdc887e47ad42157e214526dcad8f317a948dee17e1659a62f/terminado-0.18.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e6/34/ebdc18bae6aa14fbee1a08b63c015c72b64868ff7dae68808ab500c492e2/tinycss2-1.4.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/33/f0/3fe8c6e69135a845f4106f2ff8b6805638d4e85c264e70114e8126689587/tokenize_rt-6.2.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/0a/fe/3d3420c4cb1ad9cb462fb52967080575f15898da97e21cb6f1361d505383/tomli-2.3.0-cp311-cp311-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/d6/6d/c69be695a0a64fd37a97db12355a035a6d90f79067a3cf936ec2b1dc38cd/tornado-6.5.4-cp39-abi3-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2c/23/f6c6112a04d28eed765e374435fb1a9198f73e1ec4b4024184f21faeb1ad/tornado-6.5.5-cp39-abi3-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/4f/7e/bc19996fa86cad8801e8ffe6f1bba5836ca0160df76d0410d27432193712/trove_classifiers-2025.12.1.14-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/1b/a9/e3aee762739c1d7528da1c3e06d518503f8b6c439c35549b53735ba52ead/typeguard-4.4.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a0/1d/d9257dd49ff2ca23ea5f132edf1281a0c4f9de8a762b9ae399b670a59235/typer-0.21.1-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/bb/4a/2e5583e544bc437d5e8e54b47db87430df9031b29b48d17f26d129fa60c0/trove_classifiers-2026.1.14.14-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/4a/91/48db081e7a63bb37284f9fbcefda7c44c277b18b0e13fbc36ea2335b71e6/typer-0.24.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8f/5e/f1e1dd319e35e962a4e00b33150a8868b6329cc1d19fd533436ba5488f09/uncertainties-3.2.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f7/46/e7cea8159199096e1df52da20a57a6665da80c37fb8aeb848a3e47442c32/untokenize-0.1.1.tar.gz - pypi: https://files.pythonhosted.org/packages/e7/00/3fca040d7cf8a32776d3d81a00c8ee7457e00f80c649f1e4a863c8321ae9/uri_template-1.3.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/6d/b9/4095b668ea3678bf6a0af005527f39de12fb026516fb3df17495a733b7f8/urllib3-2.6.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/32/49/9e3e19ba756c4a5e6acb4ea74336d3035f7959254fbb05f5eb77bff067ed/uv-0.9.22-py3-none-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/a4/39/6983dd79f01aaa4c75d9ffa550fa393f0c4c28f7ccd6956e4188c62cefbc/validate_pyproject-0.24.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/ee/e9c95cda829131f71a8dff5ce0406059fd16e591c074414e31ada19ba7c3/validate_pyproject-0.25-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/bc/4a/c6fd02a642bbe4e9f25cdd3714a328e3fc3eb6bd7b5e96f1a2285bd928b9/varname-0.15.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1c/59/964ecb8008722d27d8a835baea81f56a91cea8e097b3be992bc6ccde6367/versioningit-3.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a4/ce/3b6fee91c85626eaf769d617f1be9d2e15c1cca027bbdeb2e0d751469355/verspec-0.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/79/0c/c05523fa3181fdf0c9c52a6ba91a23fbf3246cc095f26f6516f9c60e6771/virtualenv-20.35.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c6/59/7d02447a55b2e55755011a647479041bc92a82e143f96a8195cb33bd0a1c/virtualenv-21.2.0-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/af/b5/123f13c975e9f27ab9c0770f514345bd406d0e8d3b7a0723af9d43f710af/wcwidth-0.2.14-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/68/5a/199c59e0a824a3db2b89c5d2dade7ab5f9624dbf6448dc291b46d5ec94d3/wcwidth-0.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e2/cc/e097523dd85c9cf5d354f78310927f1656c422bd7b2613b2db3e3f9a0f2c/webcolors-25.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/34/db/b10e48aa8fff7407e67470363eac595018441cf32d5e1001567a7aeba5d2/websocket_client-1.9.0-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/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-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/b5/f7/149bb6f45f267cb5c074ac40c01c6b3ea6d8a620d34b337f6321928a1b4d/yarl-1.22.0-cp311-cp311-win_amd64.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/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl - py313-dev: + - pypi: ./ + py-313-env: channels: - url: https://conda.anaconda.org/conda-forge/ indexes: - https://pypi.org/simple + options: + pypi-prerelease-mode: if-necessary-or-explicit packages: linux-64: - - conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_8.conda + - 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/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/noarch/ca-certificates-2026.1.4-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.2.25-hbd8a1cb_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/gsl-2.8-hbf7d49c_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/icu-75.1-he02047a_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45-default_hbd61a6d_105.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libabseil-20250512.1-cxx17_hba17884_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libblas-3.11.0-5_h4a7cf45_openblas.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/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/libblas-3.11.0-6_h4a7cf45_openblas.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/libcblas-3.11.0-5_h0358290_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.11.0-6_h0358290_openblas.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.7.3-hecca717_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h9ec8514_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-he0feb66_16.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.2.0-h69a702a_16.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.2.0-h69a702a_16.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.2.0-h68bc16d_16.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-he0feb66_16.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb9d3cd8_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libnghttp2-1.67.0-had1ee68_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.30-pthreads_h94d23a6_4.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.51.1-h0c1763c_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h934c35e_16.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-15.2.0-hdf11a46_16.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.4-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/libgfortran-15.2.0-h69a702a_18.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.2.0-h68bc16d_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.2-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/libopenblas-0.3.32-pthreads_h94d23a6_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.52.0-hf4e2dac_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.41.3-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.1-hb9d3cd8_2.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/ncurses-6.5-h2d0b736_3.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/nodejs-25.2.1-he2c55a7_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.0-h26f9b46_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.13.11-hc97d973_100_cp313.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.1-h35e630c_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.13.12-hc97d973_100_cp313.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.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/tk-8.6.13-noxft_ha0e22de_103.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/noarch/tzdata-2025c-hc9c84f9_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda - pypi: https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7c/24/75d274228acf35ceeb2850b8ce04de9dd7355ff7a0b49d607ee60c29c518/aiohttp-3.13.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_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/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.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/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/09/52/94108adfdd6e2ddf58be64f959a0b9c7d4ef2fa71086c38356d22dc501ea/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/ed/c9/d7977eaacb9df673210491da99e6a247e93df98c715fc43fd136ce1d3d33/arrow-1.4.0-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/6c/25/4f103d1bedb3593718713b3f743df7b3ff3fc68d36d6666c30265ef59c8a/ase-3.28.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/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/03/49/d10027df9fce941cb8184e78a02857af36360d33e1721df81c5ed2179a1a/async_lru-2.0.5-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e5/e2/c2e3abf398f80732e58b03be77bde9022550d221dd8781bf586bd4d97cc1/async_lru-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.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/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/eb/95/7118e935b0b0bd3f94dfec2d852fd4e4f4f9757bdb49850519acd245cd3a/backrefs-6.1-py313-none-any.whl + - pypi: https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c5/71/c754b1737ad99102e03fa3235acb6cb6d3ac9d6f596cbc3e5f236705abd8/backrefs-6.2-py313-none-any.whl - pypi: https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-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/cd/3a/577b549de0cc09d95f11087ee63c739bba856cd3952697eec4c4bb91350a/bleach-6.3.0-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/cb/8c/2b30c12155ad8de0cf641d76a8b396a16d2c36bc6d50b621a62b7c4567c1/build-1.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4a/57/3b7d4dd193ade4641c865bc2b93aeeb71162e81fc348b8dad020215601ed/build-1.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/43/8e/278ea79cf8ee0c395a5ad74625b3465c2fc234bb277f171dd59dd203820d/bumps-1.0.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_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/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/d1/3b/6103194ea934f1c3a4ea080905c8849f71e83de455c16cb625d25f49b779/chardet-7.4.0.post1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/2b/58/a199d245894b12db0b957d627516c78e055adc3a0d978bc7f65ddaf7c399/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.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/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-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/12/da/91a52516e9d5aea87d32d1523f9cdcf7a35a3b298e6be05d6509ba3cfab2/coverage-7.13.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/df/91/4a151c94320458895049a3e23b7b2cfc08953c60b14892de837e8eb51d0a/copier-9.14.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ac/68/1666e3a4462f8202d836920114fa7a5ee9275d1fa45366d336c551a162dd/coverage-7.13.5-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl - pypi: https://files.pythonhosted.org/packages/52/e8/c14cc8af8cd38e86887053843382629bd8ebd117f83f15eb1194d65a2c9d/cryspy-0.10.0-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/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-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/25/3e/e27078370414ef35fafad2c06d182110073daaeb5d3bf734b0b1eeefe452/debugpy-1.8.19-py2.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/e0/c3/7f67dea8ccf8fdcb9c99033bbe3e90b9e7395415843accb81428c441be2d/debugpy-1.8.20-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/68/49/869b49db0bad6fba7c7471d612c356e745929d14e0de63acbc53e88f2a50/dfo_ls-1.6-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/ee/af/3b86dbd18d8dab5646f5b7c7db5bd9c43108e093864032aabd35d41b127d/diffpy_pdffit2-1.5.2.tar.gz - pypi: https://files.pythonhosted.org/packages/8e/52/39914bf42bb01901c91781def35c1aeffa431a59299e9748c0cfae3e5493/diffpy_structure-3.3.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/79/c8/f0f4ab7fd08950d5358579364a0a9b9198bf03e179a4fcceae4b353be32e/diffpy_utils-3.6.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/50/3d/9373ad9c56321fdab5b41197068e1d8c25883b3fea29dd361f9b55116869/dill-0.4.0-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/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/dc/b4/a7ec1eaee86761a9dbfd339732b4706db3c6b65e970c12f0f56cfcce3dcf/docformatter-1.7.7-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/82/ee/c9ca3f81d69ec6c99ca41e10432a58c60fef3a6e63cce6df7e796f90fe22/easydiffraction-0.10.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/50/98b146aea0f1cd7531d25f12bea69fa9ce8d1662124f93fb30dc4511b65e/docstring_parser_fork-0.0.14-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/87/10/2c7edbf230e5c507d38367af498fa94258ed97205d9b4b6f63a921fe9c49/dunamai-1.26.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/55/cd/d729a1bb63fa95387228cc508552dea4685ea0116e484e73238db10f9521/essdiffraction-26.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f1/bb/66c80d7f801b191f7b3ee6149a39be9d1a8a81c233e20adaf796d171f93a/essreduce-26.4.0-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/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cb/a8/20d0723294217e47de6d9e2e40fd4a9d2f7c4b6ef974babd482a59743694/fastjsonschema-2.21.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/9a/30/ab407e2ec752aa541704ed8f93c11e2a5d92c168b8a755d818b74a3c5c2d/filelock-3.20.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a3/4b/d67eedaed19def5967fade3297fed8161b25ba94699efc124b14fb68cdbc/fonttools-4.61.1-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/a4/a5/842ae8f0c08b61d6484b52f99a03510a3a72d23141942d216ebe81fefbce/filelock-3.25.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/33/b2/986d1220f6ee931e338d272bc1f3ec02cfe5f9b5fad84e95afdad57f1ebc/format_docstring-0.2.7-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cf/58/8acf1b3e91c58313ce5cb67df61001fc9dcd21be4fadb76c1a2d540e09ed/fqdn-1.5.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/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/ed/34/a6536afaeee07fa351e2087bf7b5b1522aa703bc1f6e29d53c27a722ac33/gemmi-0.7.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/e6/ab/fb21f4c939bb440104cc2b396d3be1d9b7a9fd3c6c2a53d98c45b3d7c954/fsspec-2026.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d5/08/c2409cb01d5368dcfedcbaffa7d044cc8957d57a9d0855244a5eb4709d30/funcy-2.0-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/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/fd/8e/424b8c6e78bd9837d14ff7df01a9829fc883ba2ab4ea787d4f848435f23f/greenlet-3.3.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/9c/83/3b1d03d36f224edded98e9affd0467630fc09d766c0e56fb1498cbb04a9b/griffe-1.15.0-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/6a/09/e21df6aef1e1ffc0c816f0522ddc3f6dcded766c3261813131c78a704470/gitpython-3.1.46-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/7a/34/259b28ea7a2a0c904b11cd36c79b8cef8019b26ee5dbe24e73b469dea347/greenlet-3.3.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/4b/4c/cc8c68196db727cfc1432f2ad5de50aa6707e630d44b2e6361dc06d8f134/griffelib-2.0.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d9/69/4402ea66272dacc10b298cca18ed73e1c0791ff2ae9ed218d3859f9698ac/h5py-3.15.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.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/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/0f/1c/e5fd8f973d4f375adb21565739498e2e9a1e54c858a97b9a8ccfdc81da9b/identify-2.6.15-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/46/33/92ef41c6fad0233e41d3d84ba8e8ad18d1780f1e5d99b3c683e6d7f98b63/identify-2.6.18-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a4/ed/1f1afb2e9e7f38a545d628f864d562a5ae64fe6f7a10e28ffb9b185b4e89/importlib_resources-6.5.2-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/12/c9/6869a1dcf4aaf309b9543ec070be3ec3adebee7c9bec9af8c230494134b9/interrogate-1.7.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f6/d8/502954a4ec0efcf264f99b65b41c3c54e65a647d9f0d6f62cd02227d242c/ipykernel-6.31.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/86/92/162cfaee4ccf370465c5af1ce36a9eacec1becb552f2033bb3584e6f640a/ipython-9.9.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/82/b9/e73d5d9f405cba7706c539aa8b311b49d4c2f3d698d9c12f815231169c71/ipykernel-7.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b2/90/45c72becc57158facc6a6404f663b77bbcea2519ca57f760e2879ae1315d/ipython-9.11.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-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/7b/55/e5326141505c5d5e34c5e0935d2908a74e4561eca44108fbfb9c13d2911a/isoduration-20.11.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/72/b9/313e8f2f2e9517ae050a692ae7b3e4b3f17cc5e6dfea0db51fe14e586580/jinja2_ansible_filters-1.3.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d7/9e/038522f50ceb7e74f1f991bf1b699f24b0c2bbe7c390dd36ad69f4582258/json5-0.13.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/71/92/5e77f98553e9e75130c78900d000368476aed74276eb8ae8796f65f00918/jsonpointer-3.0.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9e/6a/a83720e953b1682d2d109d3c2dbb0bc9bf28cc1cbc205be4ef4be5da709d/jsonpointer-3.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/bb/f5/fddaec430367be9d62a7ed125530e133bfd4a1c0350fe221149ee0f2b526/jupyter_client-8.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2d/0b/ceb7694d864abc0a047649aec263878acb9f792e1fec3e676f22dc9015e3/jupyter_client-8.8.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/76/c0/8cc378aad5ccfcc37adeda27d3864f20ea808ad5b63572f5b986a42a649c/jupyter_dark_detect-0.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e2/48/577993f1f99c552f18a0428731a755e06171f9902fa118c379eb7c04ea22/jupyter_events-0.12.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1a/60/1f6cee0c46263de1173894f0fafcb3475ded276c472c14d25e0280c18d6d/jupyter_lsp-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f4/a4/61adb19f3c74b0dc0e411de4f06ebef564b1f179928f9dffcbd4b378f2ef/jupyter_notebook_parser-0.1.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/92/80/a24767e6ca280f5a49525d987bf3e4d7552bf67c8be07e8ccf20271f8568/jupyter_server-2.17.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/07/2d/2b32cdbe8d2a602f697a649798554e4f072115438e92249624e532e8aca6/jupyter_server_terminals-0.5.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/af/c3/acced767eecc11a70c65c45295db5396c4f0c1937874937d5a76d7b177b6/jupyterlab-4.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d1/2d/6674563f71c6320841fc300911a55143925112a72a883e2ca71fba4c618d/jupyter_server_terminals-0.5.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e1/1b/dad6fdcc658ed7af26fdf3841e7394072c9549a8b896c381ab49dd11e2d9/jupyterlab-4.5.6-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b1/dd/ead9d8ea85bf202d90cc513b533f9c363121c7792674f78e0d8a854b63b4/jupyterlab_pygments-0.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e0/07/a000fe835f76b7e1143242ab1122e6362ef1c03f23f83a045c38859c2ae0/jupyterlab_server-2.28.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f7/5a/488f492b49b712ca38326e12c78d0d48ab6be682a2989ce533f0f2c4dfd2/jupyterquiz-2.9.6.2-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/bd/0d/2d240e7098e0cafba4d25e9530e7596b1bb1bd4476e41b10346bcaaa36d6/jupytext-1.18.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e9/e9/f218a2cb3a9ffbe324ca29a9e399fa2d2866d7f348ec3a88df87fc248fc5/kiwisolver-1.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/ab/b5/36c712098e6191d1b4e349304ef73a8d06aed77e56ceaac8c0a306c7bda1/jupyterlab_widgets-3.0.16-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/52/bc858b1665d0dec3a2511f4e6f5c18ea85c0977563d624d597c95d6d0fd7/jupyterquiz-2.9.6.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/16/5a/736dd2f4535dbf3bf26523f9158c011389ef88dd06ec2eef67fd744f1c7b/jupytext-1.19.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2b/0a/7b98e1e119878a27ba8618ca1e18b14f992ff1eda40f47bccccf4de44121/kiwisolver-1.5.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - pypi: https://files.pythonhosted.org/packages/82/3d/14ce75ef66813643812f3093ab17e46d3a206942ce7376d31ec2d36229e7/lark-1.3.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/38/7e/7b91c89a4cf0f543a83be978657afb20c86af6d725253e319589dcc4ce52/lmfit-1.3.4-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/d2/f0/834e479e47e499b6478e807fb57b31cc2db696c4db30557bb6f5aea4a90b/mando-0.7.1-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/70/81/54e3ce63502cd085a0c556652a4e1b919c45a446bd1e5300e10c44c8c521/markdown-3.10-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/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/75/97/a471f1c3eb1fd6f6c24a31a5858f443891d5127e63a7788678d14e249aea/matplotlib-3.10.8-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl @@ -2236,260 +2609,303 @@ environments: - 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/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/fd/1a/31b7cd6e4e7a02df4e076162e9783620777592bea9e4bb036389389af99d/mike-2.1.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/48/f7/10f5e101db25741b91e4f4792c5d97b4fa834ead5cf509ae91097d939424/mike-2.1.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9b/f7/4a5e785ec9fbd65146a27b6b70b6cdc161a66f2024e4b04ac06a67f5578b/mistune-3.2.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/71/26/4d39d52ea2219604053a4d05b98e90d6a335511cc01806436ec4886b1028/mkdocs_autorefs-1.2.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/9f/d4/029f984e8d3f3b6b726bd33cafc473b75e9e44c0f7e80a5b29abc466bdea/mkdocs_get_deps-0.2.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/08/37/5f1fd5c3f6954b3256f8126275e62af493b96fb6aef6c0dbc4ee326032ad/mkdocs_jupyter-0.25.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/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/93/89/eb601278b12c471235860992f5973cf3c55ca3f77d1d6127389eb045a021/mkdocs_jupyter-0.26.1-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/3e/32/ed071cb721aca8c227718cffcf7bd539620e9799bbf2619e90c757bfd030/mkdocs_material-9.7.1-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/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.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/cd/10/4c27c3063c2b3681a4b7942f8dbdeb4fa34fecb2c19b594e7345ebf4f86f/mkdocstrings-0.27.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/51/23/d02d86553327296c3bf369d444194ea83410cce8f0e690565264f37f3261/mkdocstrings_python-1.13.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/04/41/1cf02e3df279d2dd846a1bf235a928254eba9006dd22b4a14caa71aed0f7/mkdocstrings-1.0.3-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/5b/69/93b34728cc386efdde0c342f8c680b9187dea7beb7adaf6b58a0713be101/mpld3-0.5.12-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/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-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/28/83/36557b04cfdc317ed8a525c4993b23e43a8fbcddaddd78619112ca07138c/msgspec-0.20.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/c6/2d/f0b184fa88d6630aa267680bdb8623fb69cb0d024b8c6f0d23f9a0f406d3/multidict-6.7.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/3d/2e/cf2ffeb386ac3763526151163ad7da9f1b586aac96d2b4f7de1eaebf0c61/narwhals-2.15.0-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/3f/c3/06490e98393dcb4d6ce2bf331a39335375c300afaef526897881fbeae6ab/narwhals-2.18.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/83/a0/5b0c2f11142ed1dddec842457d3f65eaf71a0080894eb6f018755b319c3a/nbclient-0.10.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/cc/9a/cd673b2f773a12c992f41309ef81b99da1690426bd2f96957a7ade0d3ed7/nbconvert-7.16.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0d/4b/8d5f796a792f8a25f6925a96032f098789f448571eb92011df1ae59e8ea8/nbconvert-7.17.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a9/82/0340caa499416c78e5d8f5f05947ae4bc3cba53c9f038ab6e9ed964e22f1/nbformat-5.10.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/eb/be/b257e12f9710819fde40adc972578bee6b72c5992da1bc8369bef2597756/nbmake-1.5.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/28/88/4789719fbbe166d12d345b3ac66b96105f10001b16e00a9765ba29261a21/nbqa-1.9.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/12/e5/838eb1004fb9b6f0383c47c0d902eb698fda052e8e64ca48c70a2144a48c/nbstripout-0.8.2-py2.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/fc/39/dfdef4cfbfcf7c80cd144b1b2e262c2e785f18b152eeebe14ecd70ee7455/ncrystal-4.2.12-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e3/8b/1f02771d91ceafec996cef7f92f6a24010fedc47fd9404f8e11772d8501c/ncrystal_core-4.2.12-py3-none-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/78/98/20fdd6825d1876fc54bb098509e736073d20f569b033f2f23bf74aa1df9c/ncrystal_python-4.2.12-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.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/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f9/33/bd5b9137445ea4b680023eb0469b2bb969d61303dedb2aac6560ff3d14a1/notebook_shim-0.2.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f5/10/ca162f45a102738958dcec8023062dad0cbc17d1ab99d68c4e4a6c45fb2b/numpy-2.3.5-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7e/eb/7daecbea84ec935b7fc732e18f532073064a3816f0932a40a17f3349185f/numpy-2.4.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/15/07/284f757f63f8a8d69ed4472bfd85122bd086e637bf4ed09de572d575a693/pandas-2.3.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/f2/85/ab6d04733a7d6ff32bfc8382bf1b07078228f5d6ebec5266b91bfc5c4ff7/pandas-3.0.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/ef/af/4fbc8cab944db5d21b7e2a5b8e9211a03a79852b1157e2c102fcc61ac440/pandocfilters-1.5.1-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/16/32/f8e3c85d1d5250232a5d3477a2a28cc291968ff175caeadaf3cc19ce0e4a/parso-0.8.5-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/05/bb/39e6768529454cc2b57e1e2fa0a0a18ff64397a16303270e215a3e03285f/pathspec-1.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b6/61/fae042894f4296ec49e3f193aff5d7c18440da9e48102c3315e1bc4519a7/parso-0.8.6-py2.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/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/01/9a/632e58ec89a32738cabfd9ec418f0e9898a2b4719afc581f07c04a05e3c9/pillow-12.1.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/44/3c/d717024885424591d5376220b5e836c2d5293ce2011523c9de23ff7bf068/pip-25.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/71/24/538bff45bde96535d7d998c6fed1a751c75ac7c53c37c90dc2601b243893/pillow-12.1.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/c4/36/ce5f75aa7c736a663a901766edc3580098c7ea3959a0e878363a54a3714e/pixi_kernel-0.7.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e7/c3/3031c931098de393393e1f93a38dc9ed6805d86bb801acc3cf2d5bd1e6b7/plotly-6.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/63/d7/97f7e3a6abb67d8080dd406fd4df842c2be0efaf712d1c899c32a075027c/platformdirs-4.9.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/31/8b/9e8baf7dacac8d0c174925c38ff43c6d94bc9abb35503f67762caccb6869/plopp-26.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/52/d2/c6e44dba74f17c6216ce1b56044a9b93a929f1c2d5bdaff892512b260f5e/plotly-6.6.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/79/ad/45312df6b63ba64ea35b8d8f5f0c577aac16e6b416eafe8e1cb34e03f9a7/plumbum-1.10.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/a8/87/77cc11c7a9ea9fd05503def69e3d18605852cd0d4b0d3b8f15bbeb3ef1d1/pooch-1.8.2-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/5d/19/fd3ef348460c80af7bb4669ea7926651d1f95c23ff2df18b9d24bab4f3fa/pre_commit-4.5.1-py2.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/b8/db/14bafcb4af2139e046d03fd00dea7873e48eafe18b7d2797e73d6681f210/prometheus_client-0.23.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/74/c3/24a2f845e3917201628ecaba4f18bab4d18a337834c1df2a159ee9d22a42/prometheus_client-0.24.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-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/29/f8/40e01c350ad9a2b3cb4e6adbcc8a83b17ee50dd5792102b6142385937db5/psutil-7.2.1-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-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/0b/a6/e2e8535d8146bce05de6e0ecf1099a7e2887d840ae2a7b3a09385543fd02/py3dmol-2.5.3-py2.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/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/78/3c/2a612b95ddbb9a6bdcb47b7a93c4884f74c6ff22356b2f7b213b16e65c35/pycifstar-0.3.0.tar.gz - pypi: https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/87/6f/cc2b231dc78d8c3aaa674a676db190b8f8071c50134af8f8cf39b9b8e8df/pydoclint-0.8.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ea/10/47caf89cbb52e5bb764696fd52a8c591a2f0e851a93270c05a17f36000b5/pymdown_extensions-10.20-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/8b/40/2614036cdd416452f5bf98ec037f38a1afb17f327cb8e6b652d4729e0af8/pyparsing-3.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6f/2c/5b079febdc65e1c3fb2729bf958d18b45be7113828528e8a0b5850dd819a/pymdown_extensions-10.21-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.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/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/50/74/c655a6eda0fd188d490c14142a0f0380655ac7099604e1fbf8fa1a97f0a1/python_engineio-4.13.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/67/0f/019d3949a40280f6193b62bc010177d4ce702d0fce424322286488569cd3/python_discovery-1.2.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/aa/54/0cce26da03a981f949bb8449c9778537f75f5917c172e1d2992ff25cb57d/python_engineio-4.13.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/51/e5/fecf13f06e5e5f67e8837d777d1bc43fac0ed2b77a676804df5c34744727/python_json_logger-4.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/28/d2/2ccc2b69a187b80fda3152745670cfba936704f296a9fa54c6c8ac694d12/python_socketio-5.16.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.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/d8/8b/e2bbeb42068f0c48899e8eddd34902afc0f7429d4d2a152d2dc2670dc661/pythreejs-2.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.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/f8/9b/c108cdb55560eaf253f0cbdb61b29971e9fb34d9c3499b0e96e4e60ed8a5/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/3c/26/1062c7ec1b053db9e499b4d2d5bc231743201b74051c973dadeac80a8f43/questionary-2.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/93/f7/d00d9b4a0313a6be3a3e0818e6375e15da6d7076f4ae47d1324e7ca986a1/radon-6.0.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/56/5d/c814546c2333ceea4ba42262d8c4d55763003e767fa169adc693bd524478/requests-2.33.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/57/4d/a7545bf6c62b0dbe5795f22ea9e88cc070fdced5c34663ebc5bed2f610c0/returns-0.26.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9e/51/17023c0f8f1869d8806b979a2bffa3f861f26a3f1a66b094288323fba52f/rfc3986_validator-0.1.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7e/71/44ce230e1b7fadd372515a97e32a83011f906ddded8d03e3c6aafbdedbb7/rfc3987_syntax-1.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/b3/19/9e050c0dca8aba824d67cc0db69fb459c28d8cd3f6855b1405b3f29cc91d/ruff-0.14.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/21/f6/4bfb5695d8941e5c570a04d9fcd0d36bce7511b7d78e6e75c8f9791f82d0/scipy-1.16.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/1b/5a/f2f2e5eda25579f754acd83399c522ee03d6acbe001dfe53c8a1ec928b44/send2trash-2.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f8/22/d7f2fabdba4fae9f3b570e5605d5eb4500dcb7b770d3217dca4428484b17/ruff-0.15.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/af/46/661159ad844034ba8b3f4e0516215c41e4ee17db4213d13a82227670764f/sciline-25.11.1-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/fb/46/e50b38629e9e3f4a1dd55fb36d8b8abd1d59768c31151c1c8ed696f7b865/scippneutron-26.3.0-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/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/1c/78/504fdd027da3b84ff1aecd9f6957e65f35134534ccc6da8628eb71e76d3f/send2trash-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9d/76/f789f7a86709c6b087c5a2f52f911838cad707cc613162401badc665acfe/setuptools-82.0.1-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/52/59/0782e51887ac6b07ffd1570e0364cf901ebc36345fea669969d2084baebb/simple_websocket-1.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/48/f3/b67d6ea49ca9154453b6d70b34ea22f3996b9fa55da105a79d8732227adc/soupsieve-2.8.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/0e/50/80a8d080ac7d3d321e5e5d420c9a522b0aa770ec7013ea91f9a8b7d36e4a/sqlalchemy-2.0.45-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/c1/d4/59e74daffcb57a07668852eeeb6035af9f32cbfd7a1d2511f17d2fe6a738/smmap-5.0.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-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/53/01/1c0485ae02e645bc517bf5d5a6ca674f62c97e247890b954cbfe85c64dae/spglib-2.6.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/fe/88/cb59509e4668d8001818d7355d9995be90c321313078c912420603a7cb95/sqlalchemy-2.0.48-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-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/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-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/6a/9e/2064975477fdc887e47ad42157e214526dcad8f317a948dee17e1659a62f/terminado-0.18.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e6/34/ebdc18bae6aa14fbee1a08b63c015c72b64868ff7dae68808ab500c492e2/tinycss2-1.4.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/33/f0/3fe8c6e69135a845f4106f2ff8b6805638d4e85c264e70114e8126689587/tokenize_rt-6.2.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d5/f4/0fbd014909748706c01d16824eadb0307115f9562a15cbb012cd9b3512c5/tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/50/d4/e51d52047e7eb9a582da59f32125d17c0482d065afd5d3bc435ff2120dc5/tornado-6.5.4-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b2/04/7b5705d5b3c0fab088f434f9c83edac1573830ca49ccf29fb83bf7178eec/tornado-6.5.5-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl - pypi: https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/4f/7e/bc19996fa86cad8801e8ffe6f1bba5836ca0160df76d0410d27432193712/trove_classifiers-2025.12.1.14-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/1b/a9/e3aee762739c1d7528da1c3e06d518503f8b6c439c35549b53735ba52ead/typeguard-4.4.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a0/1d/d9257dd49ff2ca23ea5f132edf1281a0c4f9de8a762b9ae399b670a59235/typer-0.21.1-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/bb/4a/2e5583e544bc437d5e8e54b47db87430df9031b29b48d17f26d129fa60c0/trove_classifiers-2026.1.14.14-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/4a/91/48db081e7a63bb37284f9fbcefda7c44c277b18b0e13fbc36ea2335b71e6/typer-0.24.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8f/5e/f1e1dd319e35e962a4e00b33150a8868b6329cc1d19fd533436ba5488f09/uncertainties-3.2.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f7/46/e7cea8159199096e1df52da20a57a6665da80c37fb8aeb848a3e47442c32/untokenize-0.1.1.tar.gz - pypi: https://files.pythonhosted.org/packages/e7/00/3fca040d7cf8a32776d3d81a00c8ee7457e00f80c649f1e4a863c8321ae9/uri_template-1.3.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/6d/b9/4095b668ea3678bf6a0af005527f39de12fb026516fb3df17495a733b7f8/urllib3-2.6.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/40/15/63fb7a6908db2f03716c4a50aea7e27a7440fe6a09854282c401139afaf7/uv-0.9.22-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/a4/39/6983dd79f01aaa4c75d9ffa550fa393f0c4c28f7ccd6956e4188c62cefbc/validate_pyproject-0.24.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/ee/e9c95cda829131f71a8dff5ce0406059fd16e591c074414e31ada19ba7c3/validate_pyproject-0.25-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/bc/4a/c6fd02a642bbe4e9f25cdd3714a328e3fc3eb6bd7b5e96f1a2285bd928b9/varname-0.15.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1c/59/964ecb8008722d27d8a835baea81f56a91cea8e097b3be992bc6ccde6367/versioningit-3.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a4/ce/3b6fee91c85626eaf769d617f1be9d2e15c1cca027bbdeb2e0d751469355/verspec-0.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/79/0c/c05523fa3181fdf0c9c52a6ba91a23fbf3246cc095f26f6516f9c60e6771/virtualenv-20.35.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c6/59/7d02447a55b2e55755011a647479041bc92a82e143f96a8195cb33bd0a1c/virtualenv-21.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/af/b5/123f13c975e9f27ab9c0770f514345bd406d0e8d3b7a0723af9d43f710af/wcwidth-0.2.14-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/68/5a/199c59e0a824a3db2b89c5d2dade7ab5f9624dbf6448dc291b46d5ec94d3/wcwidth-0.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e2/cc/e097523dd85c9cf5d354f78310927f1656c422bd7b2613b2db3e3f9a0f2c/webcolors-25.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/34/db/b10e48aa8fff7407e67470363eac595018441cf32d5e1001567a7aeba5d2/websocket_client-1.9.0-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/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-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/11/c9/cd8538dc2e7727095e0c1d867bad1e40c98f37763e6d995c1939f5fdc7b1/yarl-1.22.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.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: ./ osx-64: - conda: https://conda.anaconda.org/conda-forge/osx-64/_openmp_mutex-4.5-7_kmp_llvm.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/bzip2-1.0.8-h500dc9f_8.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/bzip2-1.0.8-h500dc9f_9.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/c-ares-1.34.6-hb5e19a0_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.1.4-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.2.25-hbd8a1cb_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/gsl-2.8-hc707ee6_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/icu-75.1-h120a0e1_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libabseil-20250512.1-cxx17_hfc00f1c_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libblas-3.11.0-5_he492b99_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/icu-78.3-h25d91c4_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libabseil-20260107.1-cxx17_h7ed6875_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libblas-3.11.0-6_he492b99_openblas.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/libbrotlicommon-1.2.0-h8616949_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/libbrotlidec-1.2.0-h8616949_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/libbrotlienc-1.2.0-h8616949_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libcblas-3.11.0-5_h9b27e0a_openblas.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libcxx-21.1.8-h3d58e20_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libcblas-3.11.0-6_h9b27e0a_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libcxx-22.1.2-h19cb2f5_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/libev-4.33-h10d778d_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libexpat-2.7.3-heffb93a_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libffi-3.5.2-h750e83c_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libgcc-15.2.0-h08519bb_15.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libgfortran-15.2.0-h7e5c614_15.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libgfortran5-15.2.0-hd16e46c_15.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/liblzma-5.8.1-hd471939_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libmpdec-4.0.0-h6e16a3a_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libnghttp2-1.67.0-h3338091_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libopenblas-0.3.30-openmp_h6006d49_4.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libsqlite-3.51.1-hb99441e_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libexpat-2.7.4-h991f03e_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libffi-3.5.2-hd1f9c09_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libgcc-15.2.0-h08519bb_18.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libgfortran-15.2.0-h7e5c614_18.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libgfortran5-15.2.0-hd16e46c_18.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/liblzma-5.8.2-h11316ed_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libmpdec-4.0.0-hf3981d6_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libnghttp2-1.68.1-h70048d4_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libopenblas-0.3.32-openmp_h9e49c7b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libsqlite-3.52.0-h77d7759_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/libuv-1.51.0-h58003a5_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libzlib-1.3.1-hd23fc13_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/llvm-openmp-21.1.8-h472b3d1_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libzlib-1.3.2-hbb4bfdb_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/llvm-openmp-22.1.1-h0d3cbff_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/ncurses-6.5-h0622a9a_3.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/nodejs-25.2.1-h5523da6_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/openssl-3.6.0-h230baf5_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/python-3.13.11-h17c18a5_100_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/nodejs-25.8.2-hf3170e9_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/openssl-3.6.1-hb6871ef_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/python-3.13.12-h894a449_100_cp313.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/readline-8.3-h68b038d_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-hf689a15_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-h7142dee_3.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/zstd-1.5.7-h3eecb57_6.conda - pypi: https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/32/08/de43984c74ed1fca5c014808963cc83cb00d7bb06af228f132d33862ca76/aiohttp-3.13.3-cp313-cp313-macosx_10_13_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/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.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/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/81/29/5ecc3a15d5a33e31b26c11426c45c501e439cb865d0bff96315d86443b78/appnope-0.1.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/0a/08/a9bebdb2e0e602dde230bdde8021b29f71f7841bd54801bcfd514acb5dcf/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_10_9_x86_64.whl - pypi: https://files.pythonhosted.org/packages/ed/c9/d7977eaacb9df673210491da99e6a247e93df98c715fc43fd136ce1d3d33/arrow-1.4.0-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/6c/25/4f103d1bedb3593718713b3f743df7b3ff3fc68d36d6666c30265ef59c8a/ase-3.28.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/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/03/49/d10027df9fce941cb8184e78a02857af36360d33e1721df81c5ed2179a1a/async_lru-2.0.5-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e5/e2/c2e3abf398f80732e58b03be77bde9022550d221dd8781bf586bd4d97cc1/async_lru-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.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/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/eb/95/7118e935b0b0bd3f94dfec2d852fd4e4f4f9757bdb49850519acd245cd3a/backrefs-6.1-py313-none-any.whl + - pypi: https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c5/71/c754b1737ad99102e03fa3235acb6cb6d3ac9d6f596cbc3e5f236705abd8/backrefs-6.2-py313-none-any.whl - pypi: https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-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/cd/3a/577b549de0cc09d95f11087ee63c739bba856cd3952697eec4c4bb91350a/bleach-6.3.0-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/cb/8c/2b30c12155ad8de0cf641d76a8b396a16d2c36bc6d50b621a62b7c4567c1/build-1.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4a/57/3b7d4dd193ade4641c865bc2b93aeeb71162e81fc348b8dad020215601ed/build-1.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/43/8e/278ea79cf8ee0c395a5ad74625b3465c2fc234bb277f171dd59dd203820d/bumps-1.0.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_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/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl + - pypi: https://files.pythonhosted.org/packages/e9/32/83a15c6077e7f240834ffd9ed78ef12f20f6e1924d7d7986d33f3d2af905/chardet-7.4.0.post1-cp313-cp313-macosx_10_13_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/1e/1d/4fdabeef4e231153b6ed7567602f3b68265ec4e5b76d6024cf647d43d981/charset_normalizer-3.4.6-cp313-cp313-macosx_10_13_universal2.whl - pypi: https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.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/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/68/35/0167aad910bbdb9599272bd96d01a9ec6852f36b9455cf2ca67bd4cc2d23/contourpy-1.3.3-cp313-cp313-macosx_10_13_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/a3/a4/e98e689347a1ff1a7f67932ab535cef82eb5e78f32a9e4132e114bbb3a0a/coverage-7.13.1-cp313-cp313-macosx_10_13_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/df/91/4a151c94320458895049a3e23b7b2cfc08953c60b14892de837e8eb51d0a/copier-9.14.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/74/8c/74fedc9663dcf168b0a059d4ea756ecae4da77a489048f94b5f512a8d0b3/coverage-7.13.5-cp313-cp313-macosx_10_13_x86_64.whl - pypi: https://files.pythonhosted.org/packages/52/e8/c14cc8af8cd38e86887053843382629bd8ebd117f83f15eb1194d65a2c9d/cryspy-0.10.0-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/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-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/25/3e/e27078370414ef35fafad2c06d182110073daaeb5d3bf734b0b1eeefe452/debugpy-1.8.19-py2.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/e0/c3/7f67dea8ccf8fdcb9c99033bbe3e90b9e7395415843accb81428c441be2d/debugpy-1.8.20-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/68/49/869b49db0bad6fba7c7471d612c356e745929d14e0de63acbc53e88f2a50/dfo_ls-1.6-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/19/eb/bb3ff420acdaf9bcaf94c510f42df11974bc3fc475ef50d619366f33fda3/diffpy_pdffit2-1.5.2-cp313-cp313-macosx_10_13_x86_64.whl - pypi: https://files.pythonhosted.org/packages/8e/52/39914bf42bb01901c91781def35c1aeffa431a59299e9748c0cfae3e5493/diffpy_structure-3.3.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/79/c8/f0f4ab7fd08950d5358579364a0a9b9198bf03e179a4fcceae4b353be32e/diffpy_utils-3.6.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/50/3d/9373ad9c56321fdab5b41197068e1d8c25883b3fea29dd361f9b55116869/dill-0.4.0-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/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/dc/b4/a7ec1eaee86761a9dbfd339732b4706db3c6b65e970c12f0f56cfcce3dcf/docformatter-1.7.7-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/82/ee/c9ca3f81d69ec6c99ca41e10432a58c60fef3a6e63cce6df7e796f90fe22/easydiffraction-0.10.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/50/98b146aea0f1cd7531d25f12bea69fa9ce8d1662124f93fb30dc4511b65e/docstring_parser_fork-0.0.14-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/87/10/2c7edbf230e5c507d38367af498fa94258ed97205d9b4b6f63a921fe9c49/dunamai-1.26.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/55/cd/d729a1bb63fa95387228cc508552dea4685ea0116e484e73238db10f9521/essdiffraction-26.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f1/bb/66c80d7f801b191f7b3ee6149a39be9d1a8a81c233e20adaf796d171f93a/essreduce-26.4.0-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/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cb/a8/20d0723294217e47de6d9e2e40fd4a9d2f7c4b6ef974babd482a59743694/fastjsonschema-2.21.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/9a/30/ab407e2ec752aa541704ed8f93c11e2a5d92c168b8a755d818b74a3c5c2d/filelock-3.20.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/5a/ca/468c9a8446a2103ae645d14fee3f610567b7042aba85031c1c65e3ef7471/fonttools-4.61.1-cp313-cp313-macosx_10_13_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/a4/a5/842ae8f0c08b61d6484b52f99a03510a3a72d23141942d216ebe81fefbce/filelock-3.25.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/03/c5/0e3966edd5ec668d41dfe418787726752bc07e2f5fd8c8f208615e61fa89/fonttools-4.62.1-cp313-cp313-macosx_10_13_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/33/b2/986d1220f6ee931e338d272bc1f3ec02cfe5f9b5fad84e95afdad57f1ebc/format_docstring-0.2.7-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cf/58/8acf1b3e91c58313ce5cb67df61001fc9dcd21be4fadb76c1a2d540e09ed/fqdn-1.5.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/3e/e7/b88b72919c910d3233065680cbc74a2a9d00ed65a06b100751d5b78e08e1/gemmi-0.7.4-cp313-cp313-macosx_10_14_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/e6/ab/fb21f4c939bb440104cc2b396d3be1d9b7a9fd3c6c2a53d98c45b3d7c954/fsspec-2026.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d5/08/c2409cb01d5368dcfedcbaffa7d044cc8957d57a9d0855244a5eb4709d30/funcy-2.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c4/80/fd758344a72ca7b5e1c5bbdc1d263f3b215d3897941b5f450380445ca0a9/gemmi-0.7.5-cp313-cp313-macosx_10_14_x86_64.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/02/2f/28592176381b9ab2cafa12829ba7b472d177f3acc35d8fbcf3673d966fff/greenlet-3.3.0-cp313-cp313-macosx_11_0_universal2.whl - - pypi: https://files.pythonhosted.org/packages/9c/83/3b1d03d36f224edded98e9affd0467630fc09d766c0e56fb1498cbb04a9b/griffe-1.15.0-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/6a/09/e21df6aef1e1ffc0c816f0522ddc3f6dcded766c3261813131c78a704470/gitpython-3.1.46-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/ac/48/f8b875fa7dea7dd9b33245e37f065af59df6a25af2f9561efa8d822fde51/greenlet-3.3.2-cp313-cp313-macosx_11_0_universal2.whl + - pypi: https://files.pythonhosted.org/packages/4b/4c/cc8c68196db727cfc1432f2ad5de50aa6707e630d44b2e6361dc06d8f134/griffelib-2.0.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/88/b3/40207e0192415cbff7ea1d37b9f24b33f6d38a5a2f5d18a678de78f967ae/h5py-3.15.1-cp313-cp313-macosx_10_13_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/0f/9e/6142ebfda0cb6e9349c091eae73c2e01a770b7659255248d637bec54a88b/h5py-3.16.0-cp313-cp313-macosx_10_13_x86_64.whl - pypi: https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/0f/1c/e5fd8f973d4f375adb21565739498e2e9a1e54c858a97b9a8ccfdc81da9b/identify-2.6.15-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/46/33/92ef41c6fad0233e41d3d84ba8e8ad18d1780f1e5d99b3c683e6d7f98b63/identify-2.6.18-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a4/ed/1f1afb2e9e7f38a545d628f864d562a5ae64fe6f7a10e28ffb9b185b4e89/importlib_resources-6.5.2-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/12/c9/6869a1dcf4aaf309b9543ec070be3ec3adebee7c9bec9af8c230494134b9/interrogate-1.7.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f6/d8/502954a4ec0efcf264f99b65b41c3c54e65a647d9f0d6f62cd02227d242c/ipykernel-6.31.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/86/92/162cfaee4ccf370465c5af1ce36a9eacec1becb552f2033bb3584e6f640a/ipython-9.9.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/82/b9/e73d5d9f405cba7706c539aa8b311b49d4c2f3d698d9c12f815231169c71/ipykernel-7.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b2/90/45c72becc57158facc6a6404f663b77bbcea2519ca57f760e2879ae1315d/ipython-9.11.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-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/7b/55/e5326141505c5d5e34c5e0935d2908a74e4561eca44108fbfb9c13d2911a/isoduration-20.11.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/72/b9/313e8f2f2e9517ae050a692ae7b3e4b3f17cc5e6dfea0db51fe14e586580/jinja2_ansible_filters-1.3.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d7/9e/038522f50ceb7e74f1f991bf1b699f24b0c2bbe7c390dd36ad69f4582258/json5-0.13.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/71/92/5e77f98553e9e75130c78900d000368476aed74276eb8ae8796f65f00918/jsonpointer-3.0.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9e/6a/a83720e953b1682d2d109d3c2dbb0bc9bf28cc1cbc205be4ef4be5da709d/jsonpointer-3.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/bb/f5/fddaec430367be9d62a7ed125530e133bfd4a1c0350fe221149ee0f2b526/jupyter_client-8.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2d/0b/ceb7694d864abc0a047649aec263878acb9f792e1fec3e676f22dc9015e3/jupyter_client-8.8.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/76/c0/8cc378aad5ccfcc37adeda27d3864f20ea808ad5b63572f5b986a42a649c/jupyter_dark_detect-0.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e2/48/577993f1f99c552f18a0428731a755e06171f9902fa118c379eb7c04ea22/jupyter_events-0.12.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1a/60/1f6cee0c46263de1173894f0fafcb3475ded276c472c14d25e0280c18d6d/jupyter_lsp-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f4/a4/61adb19f3c74b0dc0e411de4f06ebef564b1f179928f9dffcbd4b378f2ef/jupyter_notebook_parser-0.1.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/92/80/a24767e6ca280f5a49525d987bf3e4d7552bf67c8be07e8ccf20271f8568/jupyter_server-2.17.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/07/2d/2b32cdbe8d2a602f697a649798554e4f072115438e92249624e532e8aca6/jupyter_server_terminals-0.5.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/af/c3/acced767eecc11a70c65c45295db5396c4f0c1937874937d5a76d7b177b6/jupyterlab-4.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d1/2d/6674563f71c6320841fc300911a55143925112a72a883e2ca71fba4c618d/jupyter_server_terminals-0.5.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e1/1b/dad6fdcc658ed7af26fdf3841e7394072c9549a8b896c381ab49dd11e2d9/jupyterlab-4.5.6-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b1/dd/ead9d8ea85bf202d90cc513b533f9c363121c7792674f78e0d8a854b63b4/jupyterlab_pygments-0.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e0/07/a000fe835f76b7e1143242ab1122e6362ef1c03f23f83a045c38859c2ae0/jupyterlab_server-2.28.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f7/5a/488f492b49b712ca38326e12c78d0d48ab6be682a2989ce533f0f2c4dfd2/jupyterquiz-2.9.6.2-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/bd/0d/2d240e7098e0cafba4d25e9530e7596b1bb1bd4476e41b10346bcaaa36d6/jupytext-1.18.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ca/f0/f44f50c9f5b1a1860261092e3bc91ecdc9acda848a8b8c6abfda4a24dd5c/kiwisolver-1.4.9-cp313-cp313-macosx_10_13_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/ab/b5/36c712098e6191d1b4e349304ef73a8d06aed77e56ceaac8c0a306c7bda1/jupyterlab_widgets-3.0.16-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/52/bc858b1665d0dec3a2511f4e6f5c18ea85c0977563d624d597c95d6d0fd7/jupyterquiz-2.9.6.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/16/5a/736dd2f4535dbf3bf26523f9158c011389ef88dd06ec2eef67fd744f1c7b/jupytext-1.19.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ce/48/adbb40df306f587054a348831220812b9b1d787aff714cfbc8556e38fccd/kiwisolver-1.5.0-cp313-cp313-macosx_10_13_x86_64.whl - pypi: https://files.pythonhosted.org/packages/82/3d/14ce75ef66813643812f3093ab17e46d3a206942ce7376d31ec2d36229e7/lark-1.3.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/38/7e/7b91c89a4cf0f543a83be978657afb20c86af6d725253e319589dcc4ce52/lmfit-1.3.4-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/d2/f0/834e479e47e499b6478e807fb57b31cc2db696c4db30557bb6f5aea4a90b/mando-0.7.1-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/70/81/54e3ce63502cd085a0c556652a4e1b919c45a446bd1e5300e10c44c8c521/markdown-3.10-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/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl - pypi: https://files.pythonhosted.org/packages/3d/b9/15fd5541ef4f5b9a17eefd379356cf12175fe577424e7b1d80676516031a/matplotlib-3.10.8-cp313-cp313-macosx_10_13_x86_64.whl @@ -2497,259 +2913,302 @@ environments: - 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/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/fd/1a/31b7cd6e4e7a02df4e076162e9783620777592bea9e4bb036389389af99d/mike-2.1.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/48/f7/10f5e101db25741b91e4f4792c5d97b4fa834ead5cf509ae91097d939424/mike-2.1.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9b/f7/4a5e785ec9fbd65146a27b6b70b6cdc161a66f2024e4b04ac06a67f5578b/mistune-3.2.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/71/26/4d39d52ea2219604053a4d05b98e90d6a335511cc01806436ec4886b1028/mkdocs_autorefs-1.2.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/9f/d4/029f984e8d3f3b6b726bd33cafc473b75e9e44c0f7e80a5b29abc466bdea/mkdocs_get_deps-0.2.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/08/37/5f1fd5c3f6954b3256f8126275e62af493b96fb6aef6c0dbc4ee326032ad/mkdocs_jupyter-0.25.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/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/93/89/eb601278b12c471235860992f5973cf3c55ca3f77d1d6127389eb045a021/mkdocs_jupyter-0.26.1-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/3e/32/ed071cb721aca8c227718cffcf7bd539620e9799bbf2619e90c757bfd030/mkdocs_material-9.7.1-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/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.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/cd/10/4c27c3063c2b3681a4b7942f8dbdeb4fa34fecb2c19b594e7345ebf4f86f/mkdocstrings-0.27.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/51/23/d02d86553327296c3bf369d444194ea83410cce8f0e690565264f37f3261/mkdocstrings_python-1.13.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/04/41/1cf02e3df279d2dd846a1bf235a928254eba9006dd22b4a14caa71aed0f7/mkdocstrings-1.0.3-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/5b/69/93b34728cc386efdde0c342f8c680b9187dea7beb7adaf6b58a0713be101/mpld3-0.5.12-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/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/6b/31/b46518ecc604d7edf3a4f94cb3bf021fc62aa301f0cb849936968164ef23/msgpack-1.1.2-cp313-cp313-macosx_10_13_x86_64.whl - pypi: https://files.pythonhosted.org/packages/8a/d1/b902d38b6e5ba3bdddbec469bba388d647f960aeed7b5b3623a8debe8a76/msgspec-0.20.0-cp313-cp313-macosx_10_13_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/91/1c/eb97db117a1ebe46d457a3d235a7b9d2e6dcab174f42d1b67663dd9e5371/multidict-6.7.0-cp313-cp313-macosx_10_13_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/3d/2e/cf2ffeb386ac3763526151163ad7da9f1b586aac96d2b4f7de1eaebf0c61/narwhals-2.15.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c7/75/bc704ae15fee974f8fccd871305e254754167dce5f9e42d88a2def741a1d/multidict-6.7.1-cp313-cp313-macosx_10_13_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/3f/c3/06490e98393dcb4d6ce2bf331a39335375c300afaef526897881fbeae6ab/narwhals-2.18.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/83/a0/5b0c2f11142ed1dddec842457d3f65eaf71a0080894eb6f018755b319c3a/nbclient-0.10.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/cc/9a/cd673b2f773a12c992f41309ef81b99da1690426bd2f96957a7ade0d3ed7/nbconvert-7.16.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0d/4b/8d5f796a792f8a25f6925a96032f098789f448571eb92011df1ae59e8ea8/nbconvert-7.17.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a9/82/0340caa499416c78e5d8f5f05947ae4bc3cba53c9f038ab6e9ed964e22f1/nbformat-5.10.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/eb/be/b257e12f9710819fde40adc972578bee6b72c5992da1bc8369bef2597756/nbmake-1.5.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/28/88/4789719fbbe166d12d345b3ac66b96105f10001b16e00a9765ba29261a21/nbqa-1.9.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/12/e5/838eb1004fb9b6f0383c47c0d902eb698fda052e8e64ca48c70a2144a48c/nbstripout-0.8.2-py2.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/fc/39/dfdef4cfbfcf7c80cd144b1b2e262c2e785f18b152eeebe14ecd70ee7455/ncrystal-4.2.12-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ab/ee/0d9d9218d2081e56828194f83d0eac6292b7182708fd07a62756c66f7194/ncrystal_core-4.2.12-py3-none-macosx_10_9_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/78/98/20fdd6825d1876fc54bb098509e736073d20f569b033f2f23bf74aa1df9c/ncrystal_python-4.2.12-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.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/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f9/33/bd5b9137445ea4b680023eb0469b2bb969d61303dedb2aac6560ff3d14a1/notebook_shim-0.2.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/db/69/9cde09f36da4b5a505341180a3f2e6fadc352fd4d2b7096ce9778db83f1a/numpy-2.3.5-cp313-cp313-macosx_10_13_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a2/2f/687722910b5a5601de2135c891108f51dfc873d8e43c8ed9f4ebb440b4a2/numpy-2.4.3-cp313-cp313-macosx_14_0_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/cd/4b/18b035ee18f97c1040d94debd8f2e737000ad70ccc8f5513f4eefad75f4b/pandas-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/0b/48/aad6ec4f8d007534c091e9a7172b3ec1b1ee6d99a9cbb936b5eab6c6cf58/pandas-3.0.1-cp313-cp313-macosx_10_13_x86_64.whl - pypi: https://files.pythonhosted.org/packages/ef/af/4fbc8cab944db5d21b7e2a5b8e9211a03a79852b1157e2c102fcc61ac440/pandocfilters-1.5.1-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/16/32/f8e3c85d1d5250232a5d3477a2a28cc291968ff175caeadaf3cc19ce0e4a/parso-0.8.5-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/05/bb/39e6768529454cc2b57e1e2fa0a0a18ff64397a16303270e215a3e03285f/pathspec-1.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b6/61/fae042894f4296ec49e3f193aff5d7c18440da9e48102c3315e1bc4519a7/parso-0.8.6-py2.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/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/4f/bf/28ab865de622e14b747f0cd7877510848252d950e43002e224fb1c9ababf/pillow-12.1.0-cp313-cp313-macosx_10_13_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/44/3c/d717024885424591d5376220b5e836c2d5293ce2011523c9de23ff7bf068/pip-25.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/14/a1/16c4b823838ba4c9c52c0e6bbda903a3fe5a1bdbf1b8eb4fff7156f3e318/pillow-12.1.1-cp313-cp313-macosx_10_13_x86_64.whl - pypi: https://files.pythonhosted.org/packages/c4/36/ce5f75aa7c736a663a901766edc3580098c7ea3959a0e878363a54a3714e/pixi_kernel-0.7.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e7/c3/3031c931098de393393e1f93a38dc9ed6805d86bb801acc3cf2d5bd1e6b7/plotly-6.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/63/d7/97f7e3a6abb67d8080dd406fd4df842c2be0efaf712d1c899c32a075027c/platformdirs-4.9.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/31/8b/9e8baf7dacac8d0c174925c38ff43c6d94bc9abb35503f67762caccb6869/plopp-26.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/52/d2/c6e44dba74f17c6216ce1b56044a9b93a929f1c2d5bdaff892512b260f5e/plotly-6.6.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/79/ad/45312df6b63ba64ea35b8d8f5f0c577aac16e6b416eafe8e1cb34e03f9a7/plumbum-1.10.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/a8/87/77cc11c7a9ea9fd05503def69e3d18605852cd0d4b0d3b8f15bbeb3ef1d1/pooch-1.8.2-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/5d/19/fd3ef348460c80af7bb4669ea7926651d1f95c23ff2df18b9d24bab4f3fa/pre_commit-4.5.1-py2.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/b8/db/14bafcb4af2139e046d03fd00dea7873e48eafe18b7d2797e73d6681f210/prometheus_client-0.23.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/74/c3/24a2f845e3917201628ecaba4f18bab4d18a337834c1df2a159ee9d22a42/prometheus_client-0.24.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8b/e8/677a0025e8a2acf07d3418a2e7ba529c9c33caf09d3c1f25513023c1db56/propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/c5/cf/5180eb8c8bdf6a503c6919f1da28328bd1e6b3b1b5b9d5b01ae64f019616/psutil-7.2.1-cp36-abi3-macosx_10_9_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl - pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-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/0b/a6/e2e8535d8146bce05de6e0ecf1099a7e2887d840ae2a7b3a09385543fd02/py3dmol-2.5.3-py2.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/87/0d/6af0bb9a45c771ffccd5c4c035c57ac9005e711b1191ddad1dd954187cfe/pycifrw-5.0.1-cp313-cp313-macosx_10_13_x86_64.whl - pypi: https://files.pythonhosted.org/packages/78/3c/2a612b95ddbb9a6bdcb47b7a93c4884f74c6ff22356b2f7b213b16e65c35/pycifstar-0.3.0.tar.gz - pypi: https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/87/6f/cc2b231dc78d8c3aaa674a676db190b8f8071c50134af8f8cf39b9b8e8df/pydoclint-0.8.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ea/10/47caf89cbb52e5bb764696fd52a8c591a2f0e851a93270c05a17f36000b5/pymdown_extensions-10.20-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/8b/40/2614036cdd416452f5bf98ec037f38a1afb17f327cb8e6b652d4729e0af8/pyparsing-3.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6f/2c/5b079febdc65e1c3fb2729bf958d18b45be7113828528e8a0b5850dd819a/pymdown_extensions-10.21-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.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/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/50/74/c655a6eda0fd188d490c14142a0f0380655ac7099604e1fbf8fa1a97f0a1/python_engineio-4.13.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/67/0f/019d3949a40280f6193b62bc010177d4ce702d0fce424322286488569cd3/python_discovery-1.2.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/aa/54/0cce26da03a981f949bb8449c9778537f75f5917c172e1d2992ff25cb57d/python_engineio-4.13.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/51/e5/fecf13f06e5e5f67e8837d777d1bc43fac0ed2b77a676804df5c34744727/python_json_logger-4.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/28/d2/2ccc2b69a187b80fda3152745670cfba936704f296a9fa54c6c8ac694d12/python_socketio-5.16.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.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/d8/8b/e2bbeb42068f0c48899e8eddd34902afc0f7429d4d2a152d2dc2670dc661/pythreejs-2.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.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/92/e7/038aab64a946d535901103da16b953c8c9cc9c961dadcbf3609ed6428d23/pyzmq-27.1.0-cp312-abi3-macosx_10_15_universal2.whl + - pypi: https://files.pythonhosted.org/packages/3c/26/1062c7ec1b053db9e499b4d2d5bc231743201b74051c973dadeac80a8f43/questionary-2.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/93/f7/d00d9b4a0313a6be3a3e0818e6375e15da6d7076f4ae47d1324e7ca986a1/radon-6.0.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/56/5d/c814546c2333ceea4ba42262d8c4d55763003e767fa169adc693bd524478/requests-2.33.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/57/4d/a7545bf6c62b0dbe5795f22ea9e88cc070fdced5c34663ebc5bed2f610c0/returns-0.26.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9e/51/17023c0f8f1869d8806b979a2bffa3f861f26a3f1a66b094288323fba52f/rfc3986_validator-0.1.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7e/71/44ce230e1b7fadd372515a97e32a83011f906ddded8d03e3c6aafbdedbb7/rfc3987_syntax-1.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/df/58/a0349197a7dfa603ffb7f5b0470391efa79ddc327c1e29c4851e85b09cc5/ruff-0.14.10-py3-none-macosx_10_12_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/72/f1/57e8327ab1508272029e27eeef34f2302ffc156b69e7e233e906c2a5c379/scipy-1.16.3-cp313-cp313-macosx_10_14_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/1b/5a/f2f2e5eda25579f754acd83399c522ee03d6acbe001dfe53c8a1ec928b44/send2trash-2.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/eb/92/f1c662784d149ad1414cae450b082cf736430c12ca78367f20f5ed569d65/ruff-0.15.8-py3-none-macosx_10_12_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/af/46/661159ad844034ba8b3f4e0516215c41e4ee17db4213d13a82227670764f/sciline-25.11.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e2/69/1dcb8e967f62759578938db5b29792b82ea8939a2d712e79491fa3e1cf0a/scipp-26.3.1-cp313-cp313-macosx_14_0_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/fb/46/e50b38629e9e3f4a1dd55fb36d8b8abd1d59768c31151c1c8ed696f7b865/scippneutron-26.3.0-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/b2/83/15087d945e0e4d48ce2377498abf5ad171ae013232ae31d06f336e64c999/scipy-1.17.1-cp313-cp313-macosx_14_0_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/1c/78/504fdd027da3b84ff1aecd9f6957e65f35134534ccc6da8628eb71e76d3f/send2trash-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9d/76/f789f7a86709c6b087c5a2f52f911838cad707cc613162401badc665acfe/setuptools-82.0.1-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/52/59/0782e51887ac6b07ffd1570e0364cf901ebc36345fea669969d2084baebb/simple_websocket-1.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/48/f3/b67d6ea49ca9154453b6d70b34ea22f3996b9fa55da105a79d8732227adc/soupsieve-2.8.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/bf/e1/3ccb13c643399d22289c6a9786c1a91e3dcbb68bce4beb44926ac2c557bf/sqlalchemy-2.0.45-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/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-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/1b/a5/174d33068d4383df4be9ee1ea9251f17820a622f3be744ca2ab7334818ee/spglib-2.6.0-cp313-cp313-macosx_10_13_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/46/2c/9664130905f03db57961b8980b05cab624afd114bf2be2576628a9f22da4/sqlalchemy-2.0.48-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-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/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-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/6a/9e/2064975477fdc887e47ad42157e214526dcad8f317a948dee17e1659a62f/terminado-0.18.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e6/34/ebdc18bae6aa14fbee1a08b63c015c72b64868ff7dae68808ab500c492e2/tinycss2-1.4.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/33/f0/3fe8c6e69135a845f4106f2ff8b6805638d4e85c264e70114e8126689587/tokenize_rt-6.2.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/89/48/06ee6eabe4fdd9ecd48bf488f4ac783844fd777f547b8d1b61c11939974e/tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/db/7e/f7b8d8c4453f305a51f80dbb49014257bb7d28ccb4bbb8dd328ea995ecad/tornado-6.5.4-cp39-abi3-macosx_10_9_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ab/5e/7625b76cd10f98f1516c36ce0346de62061156352353ef2da44e5c21523c/tornado-6.5.5-cp39-abi3-macosx_10_9_x86_64.whl - pypi: https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/4f/7e/bc19996fa86cad8801e8ffe6f1bba5836ca0160df76d0410d27432193712/trove_classifiers-2025.12.1.14-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/1b/a9/e3aee762739c1d7528da1c3e06d518503f8b6c439c35549b53735ba52ead/typeguard-4.4.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a0/1d/d9257dd49ff2ca23ea5f132edf1281a0c4f9de8a762b9ae399b670a59235/typer-0.21.1-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/bb/4a/2e5583e544bc437d5e8e54b47db87430df9031b29b48d17f26d129fa60c0/trove_classifiers-2026.1.14.14-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/4a/91/48db081e7a63bb37284f9fbcefda7c44c277b18b0e13fbc36ea2335b71e6/typer-0.24.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8f/5e/f1e1dd319e35e962a4e00b33150a8868b6329cc1d19fd533436ba5488f09/uncertainties-3.2.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f7/46/e7cea8159199096e1df52da20a57a6665da80c37fb8aeb848a3e47442c32/untokenize-0.1.1.tar.gz - pypi: https://files.pythonhosted.org/packages/e7/00/3fca040d7cf8a32776d3d81a00c8ee7457e00f80c649f1e4a863c8321ae9/uri_template-1.3.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/6d/b9/4095b668ea3678bf6a0af005527f39de12fb026516fb3df17495a733b7f8/urllib3-2.6.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/5e/68/bb76c97c284ce7fb8efa868994c2510588faa7075e60d8865d1373e54b7b/uv-0.9.22-py3-none-macosx_10_12_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/a4/39/6983dd79f01aaa4c75d9ffa550fa393f0c4c28f7ccd6956e4188c62cefbc/validate_pyproject-0.24.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/ee/e9c95cda829131f71a8dff5ce0406059fd16e591c074414e31ada19ba7c3/validate_pyproject-0.25-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/bc/4a/c6fd02a642bbe4e9f25cdd3714a328e3fc3eb6bd7b5e96f1a2285bd928b9/varname-0.15.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1c/59/964ecb8008722d27d8a835baea81f56a91cea8e097b3be992bc6ccde6367/versioningit-3.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a4/ce/3b6fee91c85626eaf769d617f1be9d2e15c1cca027bbdeb2e0d751469355/verspec-0.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/79/0c/c05523fa3181fdf0c9c52a6ba91a23fbf3246cc095f26f6516f9c60e6771/virtualenv-20.35.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c6/59/7d02447a55b2e55755011a647479041bc92a82e143f96a8195cb33bd0a1c/virtualenv-21.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/af/b5/123f13c975e9f27ab9c0770f514345bd406d0e8d3b7a0723af9d43f710af/wcwidth-0.2.14-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/68/5a/199c59e0a824a3db2b89c5d2dade7ab5f9624dbf6448dc291b46d5ec94d3/wcwidth-0.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e2/cc/e097523dd85c9cf5d354f78310927f1656c422bd7b2613b2db3e3f9a0f2c/webcolors-25.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/34/db/b10e48aa8fff7407e67470363eac595018441cf32d5e1001567a7aeba5d2/websocket_client-1.9.0-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/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-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/01/88/04d98af0b47e0ef42597b9b28863b9060bb515524da0a65d5f4db160b2d5/yarl-1.22.0-cp313-cp313-macosx_10_13_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/67/b6/8925d68af039b835ae876db5838e82e76ec87b9782ecc97e192b809c4831/yarl-1.23.0-cp313-cp313-macosx_10_13_x86_64.whl + - pypi: ./ osx-arm64: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/_openmp_mutex-4.5-7_kmp_llvm.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-hd037594_8.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/noarch/ca-certificates-2026.1.4-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.2.25-hbd8a1cb_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/gsl-2.8-h8d0574d_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/icu-75.1-hfee45f7_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libabseil-20250512.1-cxx17_hd41c47c_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libblas-3.11.0-5_h51639a9_openblas.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/libabseil-20260107.1-cxx17_h2062a1b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libblas-3.11.0-6_h51639a9_openblas.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/libcblas-3.11.0-5_hb0561ab_openblas.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-21.1.8-hf598326_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcblas-3.11.0-6_hb0561ab_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-22.1.2-h55c6f16_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.7.3-haf25636_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.5.2-he5f378a_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgcc-15.2.0-hcbb3090_16.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran-15.2.0-h07b0088_16.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran5-15.2.0-hdae7583_16.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.1-h39f12f2_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libmpdec-4.0.0-h5505292_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libnghttp2-1.67.0-hc438710_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopenblas-0.3.30-openmp_ha158390_3.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.51.1-h1b79a29_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.7.4-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/libgcc-15.2.0-hcbb3090_18.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran-15.2.0-h07b0088_18.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran5-15.2.0-hdae7583_18.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.2-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/libopenblas-0.3.32-openmp_he657e61_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.52.0-h1ae2325_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.1-h8359307_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/llvm-openmp-21.1.8-h4a912ad_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/llvm-openmp-22.1.1-hc7d1edf_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.5-h5e97a16_3.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/nodejs-25.2.1-h5230ea7_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.6.0-h5503f6c_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.13.11-hfc2f54d_100_cp313.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.1-hd24854e_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.13.12-h20e6be0_100_cp313.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.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/tk-8.6.13-h892fb3f_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h010d191_3.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hbf9d68e_6.conda - pypi: https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/17/f8/8dd2cf6112a5a76f81f81a5130c57ca829d101ad583ce57f889179accdda/aiohttp-3.13.3-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.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/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/81/29/5ecc3a15d5a33e31b26c11426c45c501e439cb865d0bff96315d86443b78/appnope-0.1.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b6/02/d297943bcacf05e4f2a94ab6f462831dc20158614e5d067c35d4e63b9acb/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/ed/c9/d7977eaacb9df673210491da99e6a247e93df98c715fc43fd136ce1d3d33/arrow-1.4.0-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/6c/25/4f103d1bedb3593718713b3f743df7b3ff3fc68d36d6666c30265ef59c8a/ase-3.28.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/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/03/49/d10027df9fce941cb8184e78a02857af36360d33e1721df81c5ed2179a1a/async_lru-2.0.5-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e5/e2/c2e3abf398f80732e58b03be77bde9022550d221dd8781bf586bd4d97cc1/async_lru-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.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/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/eb/95/7118e935b0b0bd3f94dfec2d852fd4e4f4f9757bdb49850519acd245cd3a/backrefs-6.1-py313-none-any.whl + - pypi: https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c5/71/c754b1737ad99102e03fa3235acb6cb6d3ac9d6f596cbc3e5f236705abd8/backrefs-6.2-py313-none-any.whl - pypi: https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-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/cd/3a/577b549de0cc09d95f11087ee63c739bba856cd3952697eec4c4bb91350a/bleach-6.3.0-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/cb/8c/2b30c12155ad8de0cf641d76a8b396a16d2c36bc6d50b621a62b7c4567c1/build-1.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4a/57/3b7d4dd193ade4641c865bc2b93aeeb71162e81fc348b8dad020215601ed/build-1.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/43/8e/278ea79cf8ee0c395a5ad74625b3465c2fc234bb277f171dd59dd203820d/bumps-1.0.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.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/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl + - pypi: https://files.pythonhosted.org/packages/83/d3/80554c1cc15631446c9b90aec6fe63b7310aa0b82d3004f7ba38bd8a8270/chardet-7.4.0.post1-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/1e/1d/4fdabeef4e231153b6ed7567602f3b68265ec4e5b76d6024cf647d43d981/charset_normalizer-3.4.6-cp313-cp313-macosx_10_13_universal2.whl - pypi: https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.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/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-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/32/33/7cbfe2bdc6e2f03d6b240d23dc45fdaf3fd270aaf2d640be77b7f16989ab/coverage-7.13.1-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/df/91/4a151c94320458895049a3e23b7b2cfc08953c60b14892de837e8eb51d0a/copier-9.14.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0c/c9/44fb661c55062f0818a6ffd2685c67aa30816200d5f2817543717d4b92eb/coverage-7.13.5-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/52/e8/c14cc8af8cd38e86887053843382629bd8ebd117f83f15eb1194d65a2c9d/cryspy-0.10.0-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/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-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/25/3e/e27078370414ef35fafad2c06d182110073daaeb5d3bf734b0b1eeefe452/debugpy-1.8.19-py2.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/e0/c3/7f67dea8ccf8fdcb9c99033bbe3e90b9e7395415843accb81428c441be2d/debugpy-1.8.20-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/68/49/869b49db0bad6fba7c7471d612c356e745929d14e0de63acbc53e88f2a50/dfo_ls-1.6-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/a3/28/d050c2716c74c6fce9ace360e727e6f86b68212fb6b0ea57c005ebe574ea/diffpy_pdffit2-1.5.2-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/8e/52/39914bf42bb01901c91781def35c1aeffa431a59299e9748c0cfae3e5493/diffpy_structure-3.3.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/79/c8/f0f4ab7fd08950d5358579364a0a9b9198bf03e179a4fcceae4b353be32e/diffpy_utils-3.6.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/50/3d/9373ad9c56321fdab5b41197068e1d8c25883b3fea29dd361f9b55116869/dill-0.4.0-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/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/dc/b4/a7ec1eaee86761a9dbfd339732b4706db3c6b65e970c12f0f56cfcce3dcf/docformatter-1.7.7-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/82/ee/c9ca3f81d69ec6c99ca41e10432a58c60fef3a6e63cce6df7e796f90fe22/easydiffraction-0.10.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/50/98b146aea0f1cd7531d25f12bea69fa9ce8d1662124f93fb30dc4511b65e/docstring_parser_fork-0.0.14-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/87/10/2c7edbf230e5c507d38367af498fa94258ed97205d9b4b6f63a921fe9c49/dunamai-1.26.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/55/cd/d729a1bb63fa95387228cc508552dea4685ea0116e484e73238db10f9521/essdiffraction-26.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f1/bb/66c80d7f801b191f7b3ee6149a39be9d1a8a81c233e20adaf796d171f93a/essreduce-26.4.0-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/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cb/a8/20d0723294217e47de6d9e2e40fd4a9d2f7c4b6ef974babd482a59743694/fastjsonschema-2.21.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/9a/30/ab407e2ec752aa541704ed8f93c11e2a5d92c168b8a755d818b74a3c5c2d/filelock-3.20.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/4b/cf/00ba28b0990982530addb8dc3e9e6f2fa9cb5c20df2abdda7baa755e8fe1/fonttools-4.61.1-cp313-cp313-macosx_10_13_universal2.whl + - pypi: https://files.pythonhosted.org/packages/a4/a5/842ae8f0c08b61d6484b52f99a03510a3a72d23141942d216ebe81fefbce/filelock-3.25.2-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/33/b2/986d1220f6ee931e338d272bc1f3ec02cfe5f9b5fad84e95afdad57f1ebc/format_docstring-0.2.7-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cf/58/8acf1b3e91c58313ce5cb67df61001fc9dcd21be4fadb76c1a2d540e09ed/fqdn-1.5.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/19/5b/0976c1af0dd59a6850e9ea3b6c6d28f3dff0651c694a6a6192a2933e8feb/gemmi-0.7.4-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/e6/ab/fb21f4c939bb440104cc2b396d3be1d9b7a9fd3c6c2a53d98c45b3d7c954/fsspec-2026.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d5/08/c2409cb01d5368dcfedcbaffa7d044cc8957d57a9d0855244a5eb4709d30/funcy-2.0-py2.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/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/9c/83/3b1d03d36f224edded98e9affd0467630fc09d766c0e56fb1498cbb04a9b/griffe-1.15.0-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/6a/09/e21df6aef1e1ffc0c816f0522ddc3f6dcded766c3261813131c78a704470/gitpython-3.1.46-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/4b/4c/cc8c68196db727cfc1432f2ad5de50aa6707e630d44b2e6361dc06d8f134/griffelib-2.0.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/31/96/ba99a003c763998035b0de4c299598125df5fc6c9ccf834f152ddd60e0fb/h5py-3.15.1-cp313-cp313-macosx_11_0_arm64.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/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/0f/1c/e5fd8f973d4f375adb21565739498e2e9a1e54c858a97b9a8ccfdc81da9b/identify-2.6.15-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/46/33/92ef41c6fad0233e41d3d84ba8e8ad18d1780f1e5d99b3c683e6d7f98b63/identify-2.6.18-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a4/ed/1f1afb2e9e7f38a545d628f864d562a5ae64fe6f7a10e28ffb9b185b4e89/importlib_resources-6.5.2-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/12/c9/6869a1dcf4aaf309b9543ec070be3ec3adebee7c9bec9af8c230494134b9/interrogate-1.7.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f6/d8/502954a4ec0efcf264f99b65b41c3c54e65a647d9f0d6f62cd02227d242c/ipykernel-6.31.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/86/92/162cfaee4ccf370465c5af1ce36a9eacec1becb552f2033bb3584e6f640a/ipython-9.9.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/82/b9/e73d5d9f405cba7706c539aa8b311b49d4c2f3d698d9c12f815231169c71/ipykernel-7.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b2/90/45c72becc57158facc6a6404f663b77bbcea2519ca57f760e2879ae1315d/ipython-9.11.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-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/7b/55/e5326141505c5d5e34c5e0935d2908a74e4561eca44108fbfb9c13d2911a/isoduration-20.11.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/72/b9/313e8f2f2e9517ae050a692ae7b3e4b3f17cc5e6dfea0db51fe14e586580/jinja2_ansible_filters-1.3.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d7/9e/038522f50ceb7e74f1f991bf1b699f24b0c2bbe7c390dd36ad69f4582258/json5-0.13.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/71/92/5e77f98553e9e75130c78900d000368476aed74276eb8ae8796f65f00918/jsonpointer-3.0.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9e/6a/a83720e953b1682d2d109d3c2dbb0bc9bf28cc1cbc205be4ef4be5da709d/jsonpointer-3.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/bb/f5/fddaec430367be9d62a7ed125530e133bfd4a1c0350fe221149ee0f2b526/jupyter_client-8.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2d/0b/ceb7694d864abc0a047649aec263878acb9f792e1fec3e676f22dc9015e3/jupyter_client-8.8.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/76/c0/8cc378aad5ccfcc37adeda27d3864f20ea808ad5b63572f5b986a42a649c/jupyter_dark_detect-0.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e2/48/577993f1f99c552f18a0428731a755e06171f9902fa118c379eb7c04ea22/jupyter_events-0.12.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1a/60/1f6cee0c46263de1173894f0fafcb3475ded276c472c14d25e0280c18d6d/jupyter_lsp-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f4/a4/61adb19f3c74b0dc0e411de4f06ebef564b1f179928f9dffcbd4b378f2ef/jupyter_notebook_parser-0.1.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/92/80/a24767e6ca280f5a49525d987bf3e4d7552bf67c8be07e8ccf20271f8568/jupyter_server-2.17.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/07/2d/2b32cdbe8d2a602f697a649798554e4f072115438e92249624e532e8aca6/jupyter_server_terminals-0.5.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/af/c3/acced767eecc11a70c65c45295db5396c4f0c1937874937d5a76d7b177b6/jupyterlab-4.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d1/2d/6674563f71c6320841fc300911a55143925112a72a883e2ca71fba4c618d/jupyter_server_terminals-0.5.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e1/1b/dad6fdcc658ed7af26fdf3841e7394072c9549a8b896c381ab49dd11e2d9/jupyterlab-4.5.6-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b1/dd/ead9d8ea85bf202d90cc513b533f9c363121c7792674f78e0d8a854b63b4/jupyterlab_pygments-0.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e0/07/a000fe835f76b7e1143242ab1122e6362ef1c03f23f83a045c38859c2ae0/jupyterlab_server-2.28.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f7/5a/488f492b49b712ca38326e12c78d0d48ab6be682a2989ce533f0f2c4dfd2/jupyterquiz-2.9.6.2-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/bd/0d/2d240e7098e0cafba4d25e9530e7596b1bb1bd4476e41b10346bcaaa36d6/jupytext-1.18.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/2d/7a/9d90a151f558e29c3936b8a47ac770235f436f2120aca41a6d5f3d62ae8d/kiwisolver-1.4.9-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/ab/b5/36c712098e6191d1b4e349304ef73a8d06aed77e56ceaac8c0a306c7bda1/jupyterlab_widgets-3.0.16-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/52/bc858b1665d0dec3a2511f4e6f5c18ea85c0977563d624d597c95d6d0fd7/jupyterquiz-2.9.6.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/16/5a/736dd2f4535dbf3bf26523f9158c011389ef88dd06ec2eef67fd744f1c7b/jupytext-1.19.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a8/3a/d0a972b34e1c63e2409413104216cd1caa02c5a37cb668d1687d466c1c45/kiwisolver-1.5.0-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/82/3d/14ce75ef66813643812f3093ab17e46d3a206942ce7376d31ec2d36229e7/lark-1.3.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/38/7e/7b91c89a4cf0f543a83be978657afb20c86af6d725253e319589dcc4ce52/lmfit-1.3.4-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/d2/f0/834e479e47e499b6478e807fb57b31cc2db696c4db30557bb6f5aea4a90b/mando-0.7.1-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/70/81/54e3ce63502cd085a0c556652a4e1b919c45a446bd1e5300e10c44c8c521/markdown-3.10-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/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/8d/a0/2ba3473c1b66b9c74dc7107c67e9008cb1782edbe896d4c899d39ae9cf78/matplotlib-3.10.8-cp313-cp313-macosx_11_0_arm64.whl @@ -2757,155 +3216,176 @@ environments: - 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/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/fd/1a/31b7cd6e4e7a02df4e076162e9783620777592bea9e4bb036389389af99d/mike-2.1.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/48/f7/10f5e101db25741b91e4f4792c5d97b4fa834ead5cf509ae91097d939424/mike-2.1.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9b/f7/4a5e785ec9fbd65146a27b6b70b6cdc161a66f2024e4b04ac06a67f5578b/mistune-3.2.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/71/26/4d39d52ea2219604053a4d05b98e90d6a335511cc01806436ec4886b1028/mkdocs_autorefs-1.2.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/9f/d4/029f984e8d3f3b6b726bd33cafc473b75e9e44c0f7e80a5b29abc466bdea/mkdocs_get_deps-0.2.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/08/37/5f1fd5c3f6954b3256f8126275e62af493b96fb6aef6c0dbc4ee326032ad/mkdocs_jupyter-0.25.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/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/93/89/eb601278b12c471235860992f5973cf3c55ca3f77d1d6127389eb045a021/mkdocs_jupyter-0.26.1-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/3e/32/ed071cb721aca8c227718cffcf7bd539620e9799bbf2619e90c757bfd030/mkdocs_material-9.7.1-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/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.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/cd/10/4c27c3063c2b3681a4b7942f8dbdeb4fa34fecb2c19b594e7345ebf4f86f/mkdocstrings-0.27.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/51/23/d02d86553327296c3bf369d444194ea83410cce8f0e690565264f37f3261/mkdocstrings_python-1.13.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/04/41/1cf02e3df279d2dd846a1bf235a928254eba9006dd22b4a14caa71aed0f7/mkdocstrings-1.0.3-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/5b/69/93b34728cc386efdde0c342f8c680b9187dea7beb7adaf6b58a0713be101/mpld3-0.5.12-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/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-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/57/b6/eff0305961a1d9447ec2b02f8c73c8946f22564d302a504185b730c9a761/msgspec-0.20.0-cp313-cp313-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/f1/d8/6c3442322e41fb1dd4de8bd67bfd11cd72352ac131f6368315617de752f1/multidict-6.7.0-cp313-cp313-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/3d/2e/cf2ffeb386ac3763526151163ad7da9f1b586aac96d2b4f7de1eaebf0c61/narwhals-2.15.0-py3-none-any.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/3f/c3/06490e98393dcb4d6ce2bf331a39335375c300afaef526897881fbeae6ab/narwhals-2.18.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/83/a0/5b0c2f11142ed1dddec842457d3f65eaf71a0080894eb6f018755b319c3a/nbclient-0.10.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/cc/9a/cd673b2f773a12c992f41309ef81b99da1690426bd2f96957a7ade0d3ed7/nbconvert-7.16.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0d/4b/8d5f796a792f8a25f6925a96032f098789f448571eb92011df1ae59e8ea8/nbconvert-7.17.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a9/82/0340caa499416c78e5d8f5f05947ae4bc3cba53c9f038ab6e9ed964e22f1/nbformat-5.10.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/eb/be/b257e12f9710819fde40adc972578bee6b72c5992da1bc8369bef2597756/nbmake-1.5.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/28/88/4789719fbbe166d12d345b3ac66b96105f10001b16e00a9765ba29261a21/nbqa-1.9.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/12/e5/838eb1004fb9b6f0383c47c0d902eb698fda052e8e64ca48c70a2144a48c/nbstripout-0.8.2-py2.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/fc/39/dfdef4cfbfcf7c80cd144b1b2e262c2e785f18b152eeebe14ecd70ee7455/ncrystal-4.2.12-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/55/8d/2b26572e909238bb114d50fb0d1b6b54eb6dafa2d83a7264f18146796b0d/ncrystal_core-4.2.12-py3-none-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/78/98/20fdd6825d1876fc54bb098509e736073d20f569b033f2f23bf74aa1df9c/ncrystal_python-4.2.12-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.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/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f9/33/bd5b9137445ea4b680023eb0469b2bb969d61303dedb2aac6560ff3d14a1/notebook_shim-0.2.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/79/fb/f505c95ceddd7027347b067689db71ca80bd5ecc926f913f1a23e65cf09b/numpy-2.3.5-cp313-cp313-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3a/66/bd096b13a87549683812b53ab211e6d413497f84e794fb3c39191948da97/numpy-2.4.3-cp313-cp313-macosx_14_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/31/94/72fac03573102779920099bcac1c3b05975c2cb5f01eac609faf34bed1ca/pandas-2.3.3-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/a8/14/5990826f779f79148ae9d3a2c39593dc04d61d5d90541e71b5749f35af95/pandas-3.0.1-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/ef/af/4fbc8cab944db5d21b7e2a5b8e9211a03a79852b1157e2c102fcc61ac440/pandocfilters-1.5.1-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/16/32/f8e3c85d1d5250232a5d3477a2a28cc291968ff175caeadaf3cc19ce0e4a/parso-0.8.5-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/05/bb/39e6768529454cc2b57e1e2fa0a0a18ff64397a16303270e215a3e03285f/pathspec-1.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b6/61/fae042894f4296ec49e3f193aff5d7c18440da9e48102c3315e1bc4519a7/parso-0.8.6-py2.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/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/1c/34/583420a1b55e715937a85bd48c5c0991598247a1fd2eb5423188e765ea02/pillow-12.1.0-cp313-cp313-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/44/3c/d717024885424591d5376220b5e836c2d5293ce2011523c9de23ff7bf068/pip-25.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bb/ad/ad9dc98ff24f485008aa5cdedaf1a219876f6f6c42a4626c08bc4e80b120/pillow-12.1.1-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/c4/36/ce5f75aa7c736a663a901766edc3580098c7ea3959a0e878363a54a3714e/pixi_kernel-0.7.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e7/c3/3031c931098de393393e1f93a38dc9ed6805d86bb801acc3cf2d5bd1e6b7/plotly-6.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/63/d7/97f7e3a6abb67d8080dd406fd4df842c2be0efaf712d1c899c32a075027c/platformdirs-4.9.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/31/8b/9e8baf7dacac8d0c174925c38ff43c6d94bc9abb35503f67762caccb6869/plopp-26.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/52/d2/c6e44dba74f17c6216ce1b56044a9b93a929f1c2d5bdaff892512b260f5e/plotly-6.6.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/79/ad/45312df6b63ba64ea35b8d8f5f0c577aac16e6b416eafe8e1cb34e03f9a7/plumbum-1.10.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/a8/87/77cc11c7a9ea9fd05503def69e3d18605852cd0d4b0d3b8f15bbeb3ef1d1/pooch-1.8.2-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/5d/19/fd3ef348460c80af7bb4669ea7926651d1f95c23ff2df18b9d24bab4f3fa/pre_commit-4.5.1-py2.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/b8/db/14bafcb4af2139e046d03fd00dea7873e48eafe18b7d2797e73d6681f210/prometheus_client-0.23.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/74/c3/24a2f845e3917201628ecaba4f18bab4d18a337834c1df2a159ee9d22a42/prometheus_client-0.24.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-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/c5/2c/78e4a789306a92ade5000da4f5de3255202c534acdadc3aac7b5458fadef/psutil-7.2.1-cp36-abi3-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-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/0b/a6/e2e8535d8146bce05de6e0ecf1099a7e2887d840ae2a7b3a09385543fd02/py3dmol-2.5.3-py2.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/83/81/bdd4bfabe70b7c9a8c0716a722ced4ebd27311afd1f4800cd405d3229c1b/pycifrw-5.0.1-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/78/3c/2a612b95ddbb9a6bdcb47b7a93c4884f74c6ff22356b2f7b213b16e65c35/pycifstar-0.3.0.tar.gz - pypi: https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/87/6f/cc2b231dc78d8c3aaa674a676db190b8f8071c50134af8f8cf39b9b8e8df/pydoclint-0.8.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ea/10/47caf89cbb52e5bb764696fd52a8c591a2f0e851a93270c05a17f36000b5/pymdown_extensions-10.20-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/8b/40/2614036cdd416452f5bf98ec037f38a1afb17f327cb8e6b652d4729e0af8/pyparsing-3.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6f/2c/5b079febdc65e1c3fb2729bf958d18b45be7113828528e8a0b5850dd819a/pymdown_extensions-10.21-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.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/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/50/74/c655a6eda0fd188d490c14142a0f0380655ac7099604e1fbf8fa1a97f0a1/python_engineio-4.13.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/67/0f/019d3949a40280f6193b62bc010177d4ce702d0fce424322286488569cd3/python_discovery-1.2.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/aa/54/0cce26da03a981f949bb8449c9778537f75f5917c172e1d2992ff25cb57d/python_engineio-4.13.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/51/e5/fecf13f06e5e5f67e8837d777d1bc43fac0ed2b77a676804df5c34744727/python_json_logger-4.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/28/d2/2ccc2b69a187b80fda3152745670cfba936704f296a9fa54c6c8ac694d12/python_socketio-5.16.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.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/d8/8b/e2bbeb42068f0c48899e8eddd34902afc0f7429d4d2a152d2dc2670dc661/pythreejs-2.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.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/92/e7/038aab64a946d535901103da16b953c8c9cc9c961dadcbf3609ed6428d23/pyzmq-27.1.0-cp312-abi3-macosx_10_15_universal2.whl + - pypi: https://files.pythonhosted.org/packages/3c/26/1062c7ec1b053db9e499b4d2d5bc231743201b74051c973dadeac80a8f43/questionary-2.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/93/f7/d00d9b4a0313a6be3a3e0818e6375e15da6d7076f4ae47d1324e7ca986a1/radon-6.0.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/56/5d/c814546c2333ceea4ba42262d8c4d55763003e767fa169adc693bd524478/requests-2.33.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/57/4d/a7545bf6c62b0dbe5795f22ea9e88cc070fdced5c34663ebc5bed2f610c0/returns-0.26.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9e/51/17023c0f8f1869d8806b979a2bffa3f861f26a3f1a66b094288323fba52f/rfc3986_validator-0.1.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7e/71/44ce230e1b7fadd372515a97e32a83011f906ddded8d03e3c6aafbdedbb7/rfc3987_syntax-1.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/7b/82/36be59f00a6082e38c23536df4e71cdbc6af8d7c707eade97fcad5c98235/ruff-0.14.10-py3-none-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/44/13/7e63cfba8a7452eb756306aa2fd9b37a29a323b672b964b4fdeded9a3f21/scipy-1.16.3-cp313-cp313-macosx_12_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/1b/5a/f2f2e5eda25579f754acd83399c522ee03d6acbe001dfe53c8a1ec928b44/send2trash-2.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ca/f2/7a631a8af6d88bcef997eb1bf87cc3da158294c57044aafd3e17030613de/ruff-0.15.8-py3-none-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/79/fe/b14d806894cf05178f1e77d0d619f071db50cf698bc654c54f9241223bcf/scipp-26.3.1-cp313-cp313-macosx_14_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/fb/46/e50b38629e9e3f4a1dd55fb36d8b8abd1d59768c31151c1c8ed696f7b865/scippneutron-26.3.0-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/5b/58/3ce96251560107b381cbd6e8413c483bbb1228a6b919fa8652b0d4090e7f/scipy-1.17.1-cp313-cp313-macosx_14_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/1c/78/504fdd027da3b84ff1aecd9f6957e65f35134534ccc6da8628eb71e76d3f/send2trash-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9d/76/f789f7a86709c6b087c5a2f52f911838cad707cc613162401badc665acfe/setuptools-82.0.1-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/52/59/0782e51887ac6b07ffd1570e0364cf901ebc36345fea669969d2084baebb/simple_websocket-1.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/48/f3/b67d6ea49ca9154453b6d70b34ea22f3996b9fa55da105a79d8732227adc/soupsieve-2.8.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/bf/e1/3ccb13c643399d22289c6a9786c1a91e3dcbb68bce4beb44926ac2c557bf/sqlalchemy-2.0.45-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/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-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/2d/47/634fe8323c6c2bfa86e10eb41ebfe410db5e6231aa1727a31ce4f002480f/spglib-2.6.0-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/d1/c6/569dc8bf3cd375abc5907e82235923e986799f301cd79a903f784b996fca/sqlalchemy-2.0.48-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-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/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-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/6a/9e/2064975477fdc887e47ad42157e214526dcad8f317a948dee17e1659a62f/terminado-0.18.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e6/34/ebdc18bae6aa14fbee1a08b63c015c72b64868ff7dae68808ab500c492e2/tinycss2-1.4.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/33/f0/3fe8c6e69135a845f4106f2ff8b6805638d4e85c264e70114e8126689587/tokenize_rt-6.2.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f1/01/88793757d54d8937015c75dcdfb673c65471945f6be98e6a0410fba167ed/tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/ab/a9/e94a9d5224107d7ce3cc1fab8d5dc97f5ea351ccc6322ee4fb661da94e35/tornado-6.5.4-cp39-abi3-macosx_10_9_universal2.whl + - pypi: https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/59/8c/77f5097695f4dd8255ecbd08b2a1ed8ba8b953d337804dd7080f199e12bf/tornado-6.5.5-cp39-abi3-macosx_10_9_universal2.whl - pypi: https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/4f/7e/bc19996fa86cad8801e8ffe6f1bba5836ca0160df76d0410d27432193712/trove_classifiers-2025.12.1.14-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/1b/a9/e3aee762739c1d7528da1c3e06d518503f8b6c439c35549b53735ba52ead/typeguard-4.4.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a0/1d/d9257dd49ff2ca23ea5f132edf1281a0c4f9de8a762b9ae399b670a59235/typer-0.21.1-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/bb/4a/2e5583e544bc437d5e8e54b47db87430df9031b29b48d17f26d129fa60c0/trove_classifiers-2026.1.14.14-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/4a/91/48db081e7a63bb37284f9fbcefda7c44c277b18b0e13fbc36ea2335b71e6/typer-0.24.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8f/5e/f1e1dd319e35e962a4e00b33150a8868b6329cc1d19fd533436ba5488f09/uncertainties-3.2.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f7/46/e7cea8159199096e1df52da20a57a6665da80c37fb8aeb848a3e47442c32/untokenize-0.1.1.tar.gz - pypi: https://files.pythonhosted.org/packages/e7/00/3fca040d7cf8a32776d3d81a00c8ee7457e00f80c649f1e4a863c8321ae9/uri_template-1.3.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/6d/b9/4095b668ea3678bf6a0af005527f39de12fb026516fb3df17495a733b7f8/urllib3-2.6.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/af/49/7230b1d56aeaee0eefd346a70f582463f11fb7036d2d020bcf68053bd994/uv-0.9.22-py3-none-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/a4/39/6983dd79f01aaa4c75d9ffa550fa393f0c4c28f7ccd6956e4188c62cefbc/validate_pyproject-0.24.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/ee/e9c95cda829131f71a8dff5ce0406059fd16e591c074414e31ada19ba7c3/validate_pyproject-0.25-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/bc/4a/c6fd02a642bbe4e9f25cdd3714a328e3fc3eb6bd7b5e96f1a2285bd928b9/varname-0.15.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1c/59/964ecb8008722d27d8a835baea81f56a91cea8e097b3be992bc6ccde6367/versioningit-3.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a4/ce/3b6fee91c85626eaf769d617f1be9d2e15c1cca027bbdeb2e0d751469355/verspec-0.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/79/0c/c05523fa3181fdf0c9c52a6ba91a23fbf3246cc095f26f6516f9c60e6771/virtualenv-20.35.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c6/59/7d02447a55b2e55755011a647479041bc92a82e143f96a8195cb33bd0a1c/virtualenv-21.2.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 - - pypi: https://files.pythonhosted.org/packages/af/b5/123f13c975e9f27ab9c0770f514345bd406d0e8d3b7a0723af9d43f710af/wcwidth-0.2.14-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/68/5a/199c59e0a824a3db2b89c5d2dade7ab5f9624dbf6448dc291b46d5ec94d3/wcwidth-0.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e2/cc/e097523dd85c9cf5d354f78310927f1656c422bd7b2613b2db3e3f9a0f2c/webcolors-25.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/34/db/b10e48aa8fff7407e67470363eac595018441cf32d5e1001567a7aeba5d2/websocket_client-1.9.0-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/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-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/18/91/3274b215fd8442a03975ce6bee5fe6aa57a8326b29b9d3d56234a1dca244/yarl-1.22.0-cp313-cp313-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-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: ./ win-64: - - conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h0ad9c76_8.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.1.4-h4c7d964_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h0ad9c76_9.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.2.25-h4c7d964_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/gsl-2.8-h5b8d9c4_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/icu-78.1-h637d24d_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libblas-3.11.0-5_hf2e6a31_mkl.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libcblas-3.11.0-5_h2a3cdd5_mkl.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.7.3-hac47afa_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libffi-3.5.2-h52bdfb6_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libhwloc-2.12.1-default_h4379cf1_1003.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libblas-3.11.0-6_hf2e6a31_mkl.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libcblas-3.11.0-6_h2a3cdd5_mkl.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.7.4-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/libhwloc-2.12.2-default_h4379cf1_1000.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libiconv-1.18-hc1393d2_2.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-5.8.1-h2466b09_2.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libmpdec-4.0.0-h2466b09_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.51.1-hf5d6505_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-5.8.2-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/libsqlite-3.52.0-hf5d6505_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libwinpthread-12.0.0.r4.gg4f2fc60ca-h57928b3_10.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libxml2-16-2.15.1-h3cfd58e_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libxml2-2.15.1-h779ef1b_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libzlib-1.3.1-h2466b09_2.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/llvm-openmp-21.1.8-h4fa8253_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/mkl-2025.3.0-hac47afa_455.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/nodejs-25.2.1-he453025_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.6.0-h725018a_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/python-3.13.11-h09917c8_100_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libxml2-16-2.15.2-h692994f_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libxml2-2.15.2-h5d26750_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/llvm-openmp-22.1.1-h4fa8253_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/mkl-2025.3.1-hac47afa_11.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.1-hf411b9b_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/python-3.13.12-h09917c8_100_cp313.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/tbb-2022.3.0-hd094cb3_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h2c6b04d_3.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/tbb-2022.3.0-h3155e25_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h6ed50ae_3.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.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 @@ -2914,96 +3394,117 @@ environments: - pypi: https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4e/f1/ab0395f8a79933577cdd996dd2f9aa6014af9535f65dddcf88204682fe62/aiohttp-3.13.3-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.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/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e2/c6/a759ece8f1829d1f162261226fbfd2c6832b3ff7657384045286d2afa384/argon2_cffi_bindings-25.1.0-cp39-abi3-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/ed/c9/d7977eaacb9df673210491da99e6a247e93df98c715fc43fd136ce1d3d33/arrow-1.4.0-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/6c/25/4f103d1bedb3593718713b3f743df7b3ff3fc68d36d6666c30265ef59c8a/ase-3.28.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/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/03/49/d10027df9fce941cb8184e78a02857af36360d33e1721df81c5ed2179a1a/async_lru-2.0.5-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e5/e2/c2e3abf398f80732e58b03be77bde9022550d221dd8781bf586bd4d97cc1/async_lru-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.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/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/eb/95/7118e935b0b0bd3f94dfec2d852fd4e4f4f9757bdb49850519acd245cd3a/backrefs-6.1-py313-none-any.whl + - pypi: https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c5/71/c754b1737ad99102e03fa3235acb6cb6d3ac9d6f596cbc3e5f236705abd8/backrefs-6.2-py313-none-any.whl - pypi: https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-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/cd/3a/577b549de0cc09d95f11087ee63c739bba856cd3952697eec4c4bb91350a/bleach-6.3.0-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/cb/8c/2b30c12155ad8de0cf641d76a8b396a16d2c36bc6d50b621a62b7c4567c1/build-1.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4a/57/3b7d4dd193ade4641c865bc2b93aeeb71162e81fc348b8dad020215601ed/build-1.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/43/8e/278ea79cf8ee0c395a5ad74625b3465c2fc234bb277f171dd59dd203820d/bumps-1.0.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.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/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/91/d7/47988d40231b41376f5a66346ef3b322c81091dfd4c0f84df5a1e3bb06b5/chardet-7.4.0.post1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a1/5c/724b6b363603e419829f561c854b87ed7c7e31231a7908708ac086cdf3e2/charset_normalizer-3.4.6-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.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/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-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/45/0b/0768b4231d5a044da8f75e097a8714ae1041246bb765d6b5563bab456735/coverage-7.13.1-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/df/91/4a151c94320458895049a3e23b7b2cfc08953c60b14892de837e8eb51d0a/copier-9.14.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/52/e8/c14cc8af8cd38e86887053843382629bd8ebd117f83f15eb1194d65a2c9d/cryspy-0.10.0-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/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-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/fb/4e/931480b9552c7d0feebe40c73725dd7703dcc578ba9efc14fe0e6d31cfd1/debugpy-1.8.19-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/4a/f3/00bb1e867fba351e2d784170955713bee200c43ea306c59f30bd7e748192/dask-2026.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/13/f7/a0b368ce54ffff9e9028c098bd2d28cfc5b54f9f6c186929083d4c60ba58/debugpy-1.8.20-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/68/49/869b49db0bad6fba7c7471d612c356e745929d14e0de63acbc53e88f2a50/dfo_ls-1.6-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/1f/0c/6826cb2151628c59cca66ca6089ff910ab3ccd62b0524c2b398dc145ee52/diffpy_pdffit2-1.5.2-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/8e/52/39914bf42bb01901c91781def35c1aeffa431a59299e9748c0cfae3e5493/diffpy_structure-3.3.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/79/c8/f0f4ab7fd08950d5358579364a0a9b9198bf03e179a4fcceae4b353be32e/diffpy_utils-3.6.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/50/3d/9373ad9c56321fdab5b41197068e1d8c25883b3fea29dd361f9b55116869/dill-0.4.0-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/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/dc/b4/a7ec1eaee86761a9dbfd339732b4706db3c6b65e970c12f0f56cfcce3dcf/docformatter-1.7.7-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/82/ee/c9ca3f81d69ec6c99ca41e10432a58c60fef3a6e63cce6df7e796f90fe22/easydiffraction-0.10.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/50/98b146aea0f1cd7531d25f12bea69fa9ce8d1662124f93fb30dc4511b65e/docstring_parser_fork-0.0.14-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/87/10/2c7edbf230e5c507d38367af498fa94258ed97205d9b4b6f63a921fe9c49/dunamai-1.26.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/55/cd/d729a1bb63fa95387228cc508552dea4685ea0116e484e73238db10f9521/essdiffraction-26.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f1/bb/66c80d7f801b191f7b3ee6149a39be9d1a8a81c233e20adaf796d171f93a/essreduce-26.4.0-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/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cb/a8/20d0723294217e47de6d9e2e40fd4a9d2f7c4b6ef974babd482a59743694/fastjsonschema-2.21.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/9a/30/ab407e2ec752aa541704ed8f93c11e2a5d92c168b8a755d818b74a3c5c2d/filelock-3.20.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/1a/59/453c06d1d83dc0951b69ef692d6b9f1846680342927df54e9a1ca91c6f90/fonttools-4.61.1-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/a4/a5/842ae8f0c08b61d6484b52f99a03510a3a72d23141942d216ebe81fefbce/filelock-3.25.2-py3-none-any.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/33/b2/986d1220f6ee931e338d272bc1f3ec02cfe5f9b5fad84e95afdad57f1ebc/format_docstring-0.2.7-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cf/58/8acf1b3e91c58313ce5cb67df61001fc9dcd21be4fadb76c1a2d540e09ed/fqdn-1.5.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/19/f7/1a03aa6b06d26dbd1231b3ac907f8fdfcf02c0b27fc5f4c31493ad6ecdad/gemmi-0.7.4-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/e6/ab/fb21f4c939bb440104cc2b396d3be1d9b7a9fd3c6c2a53d98c45b3d7c954/fsspec-2026.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d5/08/c2409cb01d5368dcfedcbaffa7d044cc8957d57a9d0855244a5eb4709d30/funcy-2.0-py2.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/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/7e/71/ba21c3fb8c5dce83b8c01f458a42e99ffdb1963aeec08fff5a18588d8fd7/greenlet-3.3.0-cp313-cp313-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/9c/83/3b1d03d36f224edded98e9affd0467630fc09d766c0e56fb1498cbb04a9b/griffe-1.15.0-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/6a/09/e21df6aef1e1ffc0c816f0522ddc3f6dcded766c3261813131c78a704470/gitpython-3.1.46-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/39/5ef5aa23bc545aa0d31e1b9b55822b32c8da93ba657295840b6b34124009/greenlet-3.3.2-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/4b/4c/cc8c68196db727cfc1432f2ad5de50aa6707e630d44b2e6361dc06d8f134/griffelib-2.0.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e5/ea/fbb258a98863f99befb10ed727152b4ae659f322e1d9c0576f8a62754e81/h5py-3.15.1-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/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/0f/1c/e5fd8f973d4f375adb21565739498e2e9a1e54c858a97b9a8ccfdc81da9b/identify-2.6.15-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/46/33/92ef41c6fad0233e41d3d84ba8e8ad18d1780f1e5d99b3c683e6d7f98b63/identify-2.6.18-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a4/ed/1f1afb2e9e7f38a545d628f864d562a5ae64fe6f7a10e28ffb9b185b4e89/importlib_resources-6.5.2-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/12/c9/6869a1dcf4aaf309b9543ec070be3ec3adebee7c9bec9af8c230494134b9/interrogate-1.7.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f6/d8/502954a4ec0efcf264f99b65b41c3c54e65a647d9f0d6f62cd02227d242c/ipykernel-6.31.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/86/92/162cfaee4ccf370465c5af1ce36a9eacec1becb552f2033bb3584e6f640a/ipython-9.9.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/82/b9/e73d5d9f405cba7706c539aa8b311b49d4c2f3d698d9c12f815231169c71/ipykernel-7.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b2/90/45c72becc57158facc6a6404f663b77bbcea2519ca57f760e2879ae1315d/ipython-9.11.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-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/7b/55/e5326141505c5d5e34c5e0935d2908a74e4561eca44108fbfb9c13d2911a/isoduration-20.11.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/72/b9/313e8f2f2e9517ae050a692ae7b3e4b3f17cc5e6dfea0db51fe14e586580/jinja2_ansible_filters-1.3.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d7/9e/038522f50ceb7e74f1f991bf1b699f24b0c2bbe7c390dd36ad69f4582258/json5-0.13.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/71/92/5e77f98553e9e75130c78900d000368476aed74276eb8ae8796f65f00918/jsonpointer-3.0.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9e/6a/a83720e953b1682d2d109d3c2dbb0bc9bf28cc1cbc205be4ef4be5da709d/jsonpointer-3.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/bb/f5/fddaec430367be9d62a7ed125530e133bfd4a1c0350fe221149ee0f2b526/jupyter_client-8.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2d/0b/ceb7694d864abc0a047649aec263878acb9f792e1fec3e676f22dc9015e3/jupyter_client-8.8.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/76/c0/8cc378aad5ccfcc37adeda27d3864f20ea808ad5b63572f5b986a42a649c/jupyter_dark_detect-0.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e2/48/577993f1f99c552f18a0428731a755e06171f9902fa118c379eb7c04ea22/jupyter_events-0.12.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1a/60/1f6cee0c46263de1173894f0fafcb3475ded276c472c14d25e0280c18d6d/jupyter_lsp-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f4/a4/61adb19f3c74b0dc0e411de4f06ebef564b1f179928f9dffcbd4b378f2ef/jupyter_notebook_parser-0.1.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/92/80/a24767e6ca280f5a49525d987bf3e4d7552bf67c8be07e8ccf20271f8568/jupyter_server-2.17.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/07/2d/2b32cdbe8d2a602f697a649798554e4f072115438e92249624e532e8aca6/jupyter_server_terminals-0.5.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/af/c3/acced767eecc11a70c65c45295db5396c4f0c1937874937d5a76d7b177b6/jupyterlab-4.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d1/2d/6674563f71c6320841fc300911a55143925112a72a883e2ca71fba4c618d/jupyter_server_terminals-0.5.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e1/1b/dad6fdcc658ed7af26fdf3841e7394072c9549a8b896c381ab49dd11e2d9/jupyterlab-4.5.6-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b1/dd/ead9d8ea85bf202d90cc513b533f9c363121c7792674f78e0d8a854b63b4/jupyterlab_pygments-0.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e0/07/a000fe835f76b7e1143242ab1122e6362ef1c03f23f83a045c38859c2ae0/jupyterlab_server-2.28.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f7/5a/488f492b49b712ca38326e12c78d0d48ab6be682a2989ce533f0f2c4dfd2/jupyterquiz-2.9.6.2-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/bd/0d/2d240e7098e0cafba4d25e9530e7596b1bb1bd4476e41b10346bcaaa36d6/jupytext-1.18.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/75/bd/f1a5d894000941739f2ae1b65a32892349423ad49c2e6d0771d0bad3fae4/kiwisolver-1.4.9-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/ab/b5/36c712098e6191d1b4e349304ef73a8d06aed77e56ceaac8c0a306c7bda1/jupyterlab_widgets-3.0.16-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/52/bc858b1665d0dec3a2511f4e6f5c18ea85c0977563d624d597c95d6d0fd7/jupyterquiz-2.9.6.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/16/5a/736dd2f4535dbf3bf26523f9158c011389ef88dd06ec2eef67fd744f1c7b/jupytext-1.19.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/be/8a/be60e3bbcf513cc5a50f4a3e88e1dcecebb79c1ad607a7222877becaa101/kiwisolver-1.5.0-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/82/3d/14ce75ef66813643812f3093ab17e46d3a206942ce7376d31ec2d36229e7/lark-1.3.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/38/7e/7b91c89a4cf0f543a83be978657afb20c86af6d725253e319589dcc4ce52/lmfit-1.3.4-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/d2/f0/834e479e47e499b6478e807fb57b31cc2db696c4db30557bb6f5aea4a90b/mando-0.7.1-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/70/81/54e3ce63502cd085a0c556652a4e1b919c45a446bd1e5300e10c44c8c521/markdown-3.10-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/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/64/40/37612487cc8a437d4dd261b32ca21fe2d79510fe74af74e1f42becb1bdb8/matplotlib-3.10.8-cp313-cp313-win_amd64.whl @@ -3011,150 +3512,166 @@ environments: - 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/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/fd/1a/31b7cd6e4e7a02df4e076162e9783620777592bea9e4bb036389389af99d/mike-2.1.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/48/f7/10f5e101db25741b91e4f4792c5d97b4fa834ead5cf509ae91097d939424/mike-2.1.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9b/f7/4a5e785ec9fbd65146a27b6b70b6cdc161a66f2024e4b04ac06a67f5578b/mistune-3.2.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/71/26/4d39d52ea2219604053a4d05b98e90d6a335511cc01806436ec4886b1028/mkdocs_autorefs-1.2.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/9f/d4/029f984e8d3f3b6b726bd33cafc473b75e9e44c0f7e80a5b29abc466bdea/mkdocs_get_deps-0.2.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/08/37/5f1fd5c3f6954b3256f8126275e62af493b96fb6aef6c0dbc4ee326032ad/mkdocs_jupyter-0.25.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/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/93/89/eb601278b12c471235860992f5973cf3c55ca3f77d1d6127389eb045a021/mkdocs_jupyter-0.26.1-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/3e/32/ed071cb721aca8c227718cffcf7bd539620e9799bbf2619e90c757bfd030/mkdocs_material-9.7.1-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/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.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/cd/10/4c27c3063c2b3681a4b7942f8dbdeb4fa34fecb2c19b594e7345ebf4f86f/mkdocstrings-0.27.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/51/23/d02d86553327296c3bf369d444194ea83410cce8f0e690565264f37f3261/mkdocstrings_python-1.13.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/04/41/1cf02e3df279d2dd846a1bf235a928254eba9006dd22b4a14caa71aed0f7/mkdocstrings-1.0.3-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/5b/69/93b34728cc386efdde0c342f8c680b9187dea7beb7adaf6b58a0713be101/mpld3-0.5.12-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/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-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/f1/25/5e8080fe0117f799b1b68008dc29a65862077296b92550632de015128579/msgspec-0.20.0-cp313-cp313-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/32/31/75c59e7d3b4205075b4c183fa4ca398a2daf2303ddf616b04ae6ef55cffe/multidict-6.7.0-cp313-cp313-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/3d/2e/cf2ffeb386ac3763526151163ad7da9f1b586aac96d2b4f7de1eaebf0c61/narwhals-2.15.0-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/3f/c3/06490e98393dcb4d6ce2bf331a39335375c300afaef526897881fbeae6ab/narwhals-2.18.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/83/a0/5b0c2f11142ed1dddec842457d3f65eaf71a0080894eb6f018755b319c3a/nbclient-0.10.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/cc/9a/cd673b2f773a12c992f41309ef81b99da1690426bd2f96957a7ade0d3ed7/nbconvert-7.16.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0d/4b/8d5f796a792f8a25f6925a96032f098789f448571eb92011df1ae59e8ea8/nbconvert-7.17.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a9/82/0340caa499416c78e5d8f5f05947ae4bc3cba53c9f038ab6e9ed964e22f1/nbformat-5.10.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/eb/be/b257e12f9710819fde40adc972578bee6b72c5992da1bc8369bef2597756/nbmake-1.5.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/28/88/4789719fbbe166d12d345b3ac66b96105f10001b16e00a9765ba29261a21/nbqa-1.9.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/12/e5/838eb1004fb9b6f0383c47c0d902eb698fda052e8e64ca48c70a2144a48c/nbstripout-0.8.2-py2.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/fc/39/dfdef4cfbfcf7c80cd144b1b2e262c2e785f18b152eeebe14ecd70ee7455/ncrystal-4.2.12-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7b/51/e13a37a8d924feefb444d7eb42094750ba1bba756cbb8c1f9a523414c4fb/ncrystal_core-4.2.12-py3-none-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/78/98/20fdd6825d1876fc54bb098509e736073d20f569b033f2f23bf74aa1df9c/ncrystal_python-4.2.12-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.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/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f9/33/bd5b9137445ea4b680023eb0469b2bb969d61303dedb2aac6560ff3d14a1/notebook_shim-0.2.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/0c/88/e2eaa6cffb115b85ed7c7c87775cb8bcf0816816bc98ca8dbfa2ee33fe6e/numpy-2.3.5-cp313-cp313-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b1/3c/88af0040119209b9b5cb59485fa48b76f372c73068dbf9254784b975ac53/numpy-2.4.3-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/4f/c7/e54682c96a895d0c808453269e0b5928a07a127a15704fedb643e9b0a4c8/pandas-2.3.3-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/d6/7d/216a1588b65a7aa5f4535570418a599d943c85afb1d95b0876fc00aa1468/pandas-3.0.1-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/ef/af/4fbc8cab944db5d21b7e2a5b8e9211a03a79852b1157e2c102fcc61ac440/pandocfilters-1.5.1-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/16/32/f8e3c85d1d5250232a5d3477a2a28cc291968ff175caeadaf3cc19ce0e4a/parso-0.8.5-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/05/bb/39e6768529454cc2b57e1e2fa0a0a18ff64397a16303270e215a3e03285f/pathspec-1.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e5/07/74a9d941fa45c90a0d9465098fe1ec85de3e2afbdc15cc4766622d516056/pillow-12.1.0-cp313-cp313-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/44/3c/d717024885424591d5376220b5e836c2d5293ce2011523c9de23ff7bf068/pip-25.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b6/61/fae042894f4296ec49e3f193aff5d7c18440da9e48102c3315e1bc4519a7/parso-0.8.6-py2.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/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3f/eb/b0834ad8b583d7d9d42b80becff092082a1c3c156bb582590fcc973f1c7c/pillow-12.1.1-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/c4/36/ce5f75aa7c736a663a901766edc3580098c7ea3959a0e878363a54a3714e/pixi_kernel-0.7.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e7/c3/3031c931098de393393e1f93a38dc9ed6805d86bb801acc3cf2d5bd1e6b7/plotly-6.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/63/d7/97f7e3a6abb67d8080dd406fd4df842c2be0efaf712d1c899c32a075027c/platformdirs-4.9.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/31/8b/9e8baf7dacac8d0c174925c38ff43c6d94bc9abb35503f67762caccb6869/plopp-26.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/52/d2/c6e44dba74f17c6216ce1b56044a9b93a929f1c2d5bdaff892512b260f5e/plotly-6.6.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/79/ad/45312df6b63ba64ea35b8d8f5f0c577aac16e6b416eafe8e1cb34e03f9a7/plumbum-1.10.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/a8/87/77cc11c7a9ea9fd05503def69e3d18605852cd0d4b0d3b8f15bbeb3ef1d1/pooch-1.8.2-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/5d/19/fd3ef348460c80af7bb4669ea7926651d1f95c23ff2df18b9d24bab4f3fa/pre_commit-4.5.1-py2.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/b8/db/14bafcb4af2139e046d03fd00dea7873e48eafe18b7d2797e73d6681f210/prometheus_client-0.23.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/74/c3/24a2f845e3917201628ecaba4f18bab4d18a337834c1df2a159ee9d22a42/prometheus_client-0.24.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-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/34/68/d9317542e3f2b180c4306e3f45d3c922d7e86d8ce39f941bb9e2e9d8599e/psutil-7.2.1-cp37-abi3-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-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/0b/a6/e2e8535d8146bce05de6e0ecf1099a7e2887d840ae2a7b3a09385543fd02/py3dmol-2.5.3-py2.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/9f/9b/50835e8fd86073fa7aa921df61b4cebc1f0ff400e4338541675cb72b5507/pycifrw-5.0.1-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/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/87/6f/cc2b231dc78d8c3aaa674a676db190b8f8071c50134af8f8cf39b9b8e8df/pydoclint-0.8.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ea/10/47caf89cbb52e5bb764696fd52a8c591a2f0e851a93270c05a17f36000b5/pymdown_extensions-10.20-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/8b/40/2614036cdd416452f5bf98ec037f38a1afb17f327cb8e6b652d4729e0af8/pyparsing-3.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6f/2c/5b079febdc65e1c3fb2729bf958d18b45be7113828528e8a0b5850dd819a/pymdown_extensions-10.21-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.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/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/50/74/c655a6eda0fd188d490c14142a0f0380655ac7099604e1fbf8fa1a97f0a1/python_engineio-4.13.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/67/0f/019d3949a40280f6193b62bc010177d4ce702d0fce424322286488569cd3/python_discovery-1.2.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/aa/54/0cce26da03a981f949bb8449c9778537f75f5917c172e1d2992ff25cb57d/python_engineio-4.13.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/51/e5/fecf13f06e5e5f67e8837d777d1bc43fac0ed2b77a676804df5c34744727/python_json_logger-4.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/28/d2/2ccc2b69a187b80fda3152745670cfba936704f296a9fa54c6c8ac694d12/python_socketio-5.16.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/fc/19/b757fe28008236a4a713e813283721b8a40aa60cd7d3f83549f2e25a3155/pywinpty-3.0.2-cp313-cp313-win_amd64.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/d8/8b/e2bbeb42068f0c48899e8eddd34902afc0f7429d4d2a152d2dc2670dc661/pythreejs-2.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/e5/cb/58d6ed3fd429c96a90ef01ac9a617af10a6d41469219c25e7dc162abbb71/pywinpty-3.0.3-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-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/fc/7f/a21b20d577e4100c6a41795842028235998a643b1ad406a6d4163ea8f53e/pyzmq-27.1.0-cp312-abi3-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/3c/26/1062c7ec1b053db9e499b4d2d5bc231743201b74051c973dadeac80a8f43/questionary-2.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/93/f7/d00d9b4a0313a6be3a3e0818e6375e15da6d7076f4ae47d1324e7ca986a1/radon-6.0.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/56/5d/c814546c2333ceea4ba42262d8c4d55763003e767fa169adc693bd524478/requests-2.33.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/57/4d/a7545bf6c62b0dbe5795f22ea9e88cc070fdced5c34663ebc5bed2f610c0/returns-0.26.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9e/51/17023c0f8f1869d8806b979a2bffa3f861f26a3f1a66b094288323fba52f/rfc3986_validator-0.1.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7e/71/44ce230e1b7fadd372515a97e32a83011f906ddded8d03e3c6aafbdedbb7/rfc3987_syntax-1.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/fb/9c/896c862e13886fae2af961bef3e6312db9ebc6adc2b156fe95e615dee8c1/ruff-0.14.10-py3-none-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/cd/01/1204382461fcbfeb05b6161b594f4007e78b6eba9b375382f79153172b4d/scipy-1.16.3-cp313-cp313-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/1b/5a/f2f2e5eda25579f754acd83399c522ee03d6acbe001dfe53c8a1ec928b44/send2trash-2.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1f/a2/ef467cb77099062317154c63f234b8a7baf7cb690b99af760c5b68b9ee7f/ruff-0.15.8-py3-none-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/af/46/661159ad844034ba8b3f4e0516215c41e4ee17db4213d13a82227670764f/sciline-25.11.1-py3-none-any.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/fb/46/e50b38629e9e3f4a1dd55fb36d8b8abd1d59768c31151c1c8ed696f7b865/scippneutron-26.3.0-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/35/e5/d6d0e51fc888f692a35134336866341c08655d92614f492c6860dc45bb2c/scipy-1.17.1-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/1c/78/504fdd027da3b84ff1aecd9f6957e65f35134534ccc6da8628eb71e76d3f/send2trash-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9d/76/f789f7a86709c6b087c5a2f52f911838cad707cc613162401badc665acfe/setuptools-82.0.1-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/52/59/0782e51887ac6b07ffd1570e0364cf901ebc36345fea669969d2084baebb/simple_websocket-1.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/48/f3/b67d6ea49ca9154453b6d70b34ea22f3996b9fa55da105a79d8732227adc/soupsieve-2.8.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/c0/c5/d17113020b2d43073412aeca09b60d2009442420372123b8d49cc253f8b8/sqlalchemy-2.0.45-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/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-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/16/56/a31e8d3c9e8d21100b83bbe1c1f3f7c94db317393a229e193461e5e6d2a4/spglib-2.6.0-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/b7/2b/b9040bec58c58225f073f5b0c1870defe1940835549dafec680cbd58c3c3/sqlalchemy-2.0.48-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-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/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-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/6a/9e/2064975477fdc887e47ad42157e214526dcad8f317a948dee17e1659a62f/terminado-0.18.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e6/34/ebdc18bae6aa14fbee1a08b63c015c72b64868ff7dae68808ab500c492e2/tinycss2-1.4.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/33/f0/3fe8c6e69135a845f4106f2ff8b6805638d4e85c264e70114e8126689587/tokenize_rt-6.2.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b2/b7/718cd1da0884f281f95ccfa3a6cc572d30053cba64603f79d431d3c9b61b/tomli-2.3.0-cp313-cp313-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/d6/6d/c69be695a0a64fd37a97db12355a035a6d90f79067a3cf936ec2b1dc38cd/tornado-6.5.4-cp39-abi3-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2c/23/f6c6112a04d28eed765e374435fb1a9198f73e1ec4b4024184f21faeb1ad/tornado-6.5.5-cp39-abi3-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/4f/7e/bc19996fa86cad8801e8ffe6f1bba5836ca0160df76d0410d27432193712/trove_classifiers-2025.12.1.14-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/1b/a9/e3aee762739c1d7528da1c3e06d518503f8b6c439c35549b53735ba52ead/typeguard-4.4.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a0/1d/d9257dd49ff2ca23ea5f132edf1281a0c4f9de8a762b9ae399b670a59235/typer-0.21.1-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/bb/4a/2e5583e544bc437d5e8e54b47db87430df9031b29b48d17f26d129fa60c0/trove_classifiers-2026.1.14.14-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/4a/91/48db081e7a63bb37284f9fbcefda7c44c277b18b0e13fbc36ea2335b71e6/typer-0.24.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8f/5e/f1e1dd319e35e962a4e00b33150a8868b6329cc1d19fd533436ba5488f09/uncertainties-3.2.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f7/46/e7cea8159199096e1df52da20a57a6665da80c37fb8aeb848a3e47442c32/untokenize-0.1.1.tar.gz - pypi: https://files.pythonhosted.org/packages/e7/00/3fca040d7cf8a32776d3d81a00c8ee7457e00f80c649f1e4a863c8321ae9/uri_template-1.3.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/6d/b9/4095b668ea3678bf6a0af005527f39de12fb026516fb3df17495a733b7f8/urllib3-2.6.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/32/49/9e3e19ba756c4a5e6acb4ea74336d3035f7959254fbb05f5eb77bff067ed/uv-0.9.22-py3-none-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/a4/39/6983dd79f01aaa4c75d9ffa550fa393f0c4c28f7ccd6956e4188c62cefbc/validate_pyproject-0.24.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/ee/e9c95cda829131f71a8dff5ce0406059fd16e591c074414e31ada19ba7c3/validate_pyproject-0.25-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/bc/4a/c6fd02a642bbe4e9f25cdd3714a328e3fc3eb6bd7b5e96f1a2285bd928b9/varname-0.15.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1c/59/964ecb8008722d27d8a835baea81f56a91cea8e097b3be992bc6ccde6367/versioningit-3.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a4/ce/3b6fee91c85626eaf769d617f1be9d2e15c1cca027bbdeb2e0d751469355/verspec-0.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/79/0c/c05523fa3181fdf0c9c52a6ba91a23fbf3246cc095f26f6516f9c60e6771/virtualenv-20.35.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c6/59/7d02447a55b2e55755011a647479041bc92a82e143f96a8195cb33bd0a1c/virtualenv-21.2.0-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/af/b5/123f13c975e9f27ab9c0770f514345bd406d0e8d3b7a0723af9d43f710af/wcwidth-0.2.14-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/68/5a/199c59e0a824a3db2b89c5d2dade7ab5f9624dbf6448dc291b46d5ec94d3/wcwidth-0.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e2/cc/e097523dd85c9cf5d354f78310927f1656c422bd7b2613b2db3e3f9a0f2c/webcolors-25.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/34/db/b10e48aa8fff7407e67470363eac595018441cf32d5e1001567a7aeba5d2/websocket_client-1.9.0-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/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-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/9a/ee/450914ae11b419eadd067c6183ae08381cfdfcb9798b90b2b713bbebddda/yarl-1.22.0-cp313-cp313-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7a/84/266e8da36879c6edcd37b02b547e2d9ecdfea776be49598e75696e3316e1/yarl-1.23.0-cp313-cp313-win_amd64.whl + - pypi: ./ packages: -- conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 - sha256: fe51de6107f9edc7aa4f786a70f4a883943bc9d39b3bb7307c04c41410990726 - md5: d7c89558ba9fa0495403155b64376d81 - license: None - purls: [] - size: 2562 - timestamp: 1578324546067 -- conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2 - build_number: 16 - sha256: fbe2c5e56a653bebb982eda4876a9178aedfc2b545f25d0ce9c4c0b508253d22 - md5: 73aaf86a425cc6e73fcf236a5a46396d - depends: - - _libgcc_mutex 0.1 conda_forge +- 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 9999 + - openmp_impl <0.0a0 license: BSD-3-Clause license_family: BSD purls: [] - size: 23621 - timestamp: 1650670423406 + size: 28948 + timestamp: 1770939786096 - conda: https://conda.anaconda.org/conda-forge/osx-64/_openmp_mutex-4.5-7_kmp_llvm.conda build_number: 7 sha256: 30006902a9274de8abdad5a9f02ef7c8bb3d69a503486af0c1faee30b023e5b7 @@ -3334,17 +3851,28 @@ packages: - frozenlist>=1.1.0 - typing-extensions>=4.2 ; python_full_version < '3.13' requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl +- 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/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl + name: annotated-types + version: 0.7.0 + sha256: 1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53 + requires_dist: + - typing-extensions>=4.0.0 ; python_full_version < '3.9' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl name: anyio - version: 4.12.1 - sha256: d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c + version: 4.13.0 + sha256: 08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708 requires_dist: - exceptiongroup>=1.0.2 ; python_full_version < '3.11' - idna>=2.8 - typing-extensions>=4.5 ; python_full_version < '3.13' - - trio>=0.32.0 ; python_full_version >= '3.10' and extra == 'trio' - - trio>=0.31.0 ; python_full_version < '3.10' and extra == 'trio' - requires_python: '>=3.9' + - trio>=0.32.0 ; extra == 'trio' + requires_python: '>=3.10' - pypi: https://files.pythonhosted.org/packages/81/29/5ecc3a15d5a33e31b26c11426c45c501e439cb865d0bff96315d86443b78/appnope-0.1.4-py2.py3-none-any.whl name: appnope version: 0.1.4 @@ -3417,6 +3945,27 @@ packages: requires_dist: - setuptools - flake8 ; extra == 'qa' +- 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/99/31/6cf181011dc738c33bf6ba7aea2e8e1d3c1f71b7dab1942f3054f66f6202/asteval-1.0.8-py3-none-any.whl name: asteval version: 1.0.8 @@ -3441,17 +3990,17 @@ packages: - pytest-cov ; extra == 'test' - pytest-xdist ; extra == 'test' requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/03/49/d10027df9fce941cb8184e78a02857af36360d33e1721df81c5ed2179a1a/async_lru-2.0.5-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/e5/e2/c2e3abf398f80732e58b03be77bde9022550d221dd8781bf586bd4d97cc1/async_lru-2.3.0-py3-none-any.whl name: async-lru - version: 2.0.5 - sha256: ab95404d8d2605310d345932697371a5f40def0487c03d6d0ad9138de52c9943 + version: 2.3.0 + sha256: eea27b01841909316f2cc739807acea1c623df2be8c5cfad7583286397bb8315 requires_dist: - typing-extensions>=4.0.0 ; python_full_version < '3.11' - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl name: attrs - version: 25.4.0 - sha256: adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373 + version: 26.1.0 + sha256: c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309 requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/9e/43/53afb8ba17218f19b77c7834128566c5bbb100a0ad9ba2e8e89d089d7079/autopep8-2.3.2-py2.py3-none-any.whl name: autopep8 @@ -3461,10 +4010,10 @@ packages: - pycodestyle>=2.12.0 - tomli ; python_full_version < '3.11' requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl name: babel - version: 2.17.0 - sha256: 4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2 + version: 2.18.0 + sha256: e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35 requires_dist: - pytz>=2015.7 ; python_full_version < '3.9' - tzdata ; sys_platform == 'win32' and extra == 'dev' @@ -3476,17 +4025,17 @@ packages: - pytz ; extra == 'dev' - setuptools ; extra == 'dev' requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/e6/9a/8da246d988ded941da96c7ed945d63e94a445637eaad985a0ed88787cb89/backrefs-6.1-py311-none-any.whl +- pypi: https://files.pythonhosted.org/packages/0f/f0/35240571e1b67ffb19dafb29ab34150b6f59f93f717b041082cdb1bfceb1/backrefs-6.2-py311-none-any.whl name: backrefs - version: '6.1' - sha256: e82bba3875ee4430f4de4b6db19429a27275d95a5f3773c57e9e18abc23fd2b7 + version: '6.2' + sha256: 08aa7fae530c6b2361d7bdcbda1a7c454e330cc9dbcd03f5c23205e430e5c3be requires_dist: - regex ; extra == 'extras' requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/eb/95/7118e935b0b0bd3f94dfec2d852fd4e4f4f9757bdb49850519acd245cd3a/backrefs-6.1-py313-none-any.whl +- pypi: https://files.pythonhosted.org/packages/c5/71/c754b1737ad99102e03fa3235acb6cb6d3ac9d6f596cbc3e5f236705abd8/backrefs-6.2-py313-none-any.whl name: backrefs - version: '6.1' - sha256: 4c9d3dc1e2e558965202c012304f33d4e0e477e1c103663fd2c3cc9bb18b0d05 + version: '6.2' + sha256: 12df81596ab511f783b7d87c043ce26bc5b0288cf3bb03610fe76b8189282b2b requires_dist: - regex ; extra == 'extras' requires_python: '>=3.9' @@ -3521,12 +4070,12 @@ packages: version: 1.9.0 sha256: ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/cb/8c/2b30c12155ad8de0cf641d76a8b396a16d2c36bc6d50b621a62b7c4567c1/build-1.3.0-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/4a/57/3b7d4dd193ade4641c865bc2b93aeeb71162e81fc348b8dad020215601ed/build-1.4.2-py3-none-any.whl name: build - version: 1.3.0 - sha256: 7145f0b5061ba90a1500d60bd1b13ca0a8a4cebdd0cc16ed8adf1c0e739f43b4 + version: 1.4.2 + sha256: 7a4d8651ea877cb2a89458b1b198f2e69f536c95e89129dbf5d448045d60db88 requires_dist: - - packaging>=19.1 + - packaging>=24.0 - pyproject-hooks - colorama ; os_name == 'nt' - importlib-metadata>=4.6 ; python_full_version < '3.10.2' @@ -3564,40 +4113,40 @@ packages: - sphinx ; extra == 'dev' - versioningit ; extra == 'dev' requires_python: '>=3.9' -- conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_8.conda - sha256: c30daba32ddebbb7ded490f0e371eae90f51e72db620554089103b4a6934b0d5 - md5: 51a19bba1b8ebfb60df25cde030b7ebc +- 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: 260341 - timestamp: 1757437258798 -- conda: https://conda.anaconda.org/conda-forge/osx-64/bzip2-1.0.8-h500dc9f_8.conda - sha256: 8f50b58efb29c710f3cecf2027a8d7325ba769ab10c746eff75cea3ac050b10c - md5: 97c4b3bd8a90722104798175a1bdddbf + size: 260182 + timestamp: 1771350215188 +- conda: https://conda.anaconda.org/conda-forge/osx-64/bzip2-1.0.8-h500dc9f_9.conda + sha256: 9f242f13537ef1ce195f93f0cc162965d6cc79da578568d6d8e50f70dd025c42 + md5: 4173ac3b19ec0a4f400b4f782910368b depends: - __osx >=10.13 license: bzip2-1.0.6 license_family: BSD purls: [] - size: 132607 - timestamp: 1757437730085 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-hd037594_8.conda - sha256: b456200636bd5fecb2bec63f7e0985ad2097cf1b83d60ce0b6968dffa6d02aa1 - md5: 58fd217444c2a5701a44244faf518206 + size: 133427 + timestamp: 1771350680709 +- 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: 125061 - timestamp: 1757437486465 -- conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h0ad9c76_8.conda - sha256: d882712855624641f48aa9dc3f5feea2ed6b4e6004585d3616386a18186fe692 - md5: 1077e9333c41ff0be8edd1a5ec0ddace + size: 124834 + timestamp: 1771350416561 +- 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 @@ -3605,8 +4154,8 @@ packages: license: bzip2-1.0.6 license_family: BSD purls: [] - size: 55977 - timestamp: 1757437738856 + size: 56115 + timestamp: 1771350256444 - conda: https://conda.anaconda.org/conda-forge/linux-64/c-ares-1.34.6-hb03c661_0.conda sha256: cc9accf72fa028d31c2a038460787751127317dcfa991f8d1f1babf216bb454e md5: 920bb03579f15389b9e512095ad995b7 @@ -3638,28 +4187,28 @@ packages: purls: [] size: 180327 timestamp: 1765215064054 -- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.1.4-h4c7d964_0.conda - sha256: 4ddcb01be03f85d3db9d881407fb13a673372f1b9fac9c836ea441893390e049 - md5: 84d389c9eee640dda3d26fc5335c67d8 +- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.2.25-h4c7d964_0.conda + sha256: 37950019c59b99585cee5d30dbc2cc9696ed4e11f5742606a4db1621ed8f94d6 + md5: f001e6e220355b7f87403a4d0e5bf1ca depends: - __win license: ISC purls: [] - size: 147139 - timestamp: 1767500904211 -- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.1.4-hbd8a1cb_0.conda - sha256: b5974ec9b50e3c514a382335efa81ed02b05906849827a34061c496f4defa0b2 - md5: bddacf101bb4dd0e51811cb69c7790e2 + size: 147734 + timestamp: 1772006322223 +- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.2.25-hbd8a1cb_0.conda + sha256: 67cc7101b36421c5913a1687ef1b99f85b5d6868da3abbf6ec1a4181e79782fc + md5: 4492fd26db29495f0ba23f146cd5638d depends: - __unix license: ISC purls: [] - size: 146519 - timestamp: 1767500828366 -- pypi: https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl + size: 147413 + timestamp: 1772006283803 +- pypi: https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl name: certifi - version: 2026.1.4 - sha256: 9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c + version: 2026.2.25 + sha256: 027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa requires_python: '>=3.7' - pypi: https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl name: cffi @@ -3722,35 +4271,70 @@ packages: version: 3.5.0 sha256: a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0 requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl +- pypi: https://files.pythonhosted.org/packages/3e/38/fe380893cbba72febb24d5dc0c2f9ac99f437153c36a409a8e254ed77bb6/chardet-7.4.0.post1-cp311-cp311-macosx_10_9_x86_64.whl + name: chardet + version: 7.4.0.post1 + sha256: 2769be12361a6c7873392e435c708eca88c9f0fb6a647af75fa1386db64032d6 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/5e/24/3c1522d777b66e2e3615ee33d1d4291c47b0ec258a9471b559339b01fac5/chardet-7.4.0.post1-cp311-cp311-macosx_11_0_arm64.whl + name: chardet + version: 7.4.0.post1 + sha256: 8e1eaa942ae81d43d535092ff3ba660c967344178cc3876b54834a56c1207f3a + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/83/d3/80554c1cc15631446c9b90aec6fe63b7310aa0b82d3004f7ba38bd8a8270/chardet-7.4.0.post1-cp313-cp313-macosx_11_0_arm64.whl + name: chardet + version: 7.4.0.post1 + sha256: e6285d35f79d0cdc8838d3cb01876f979c8419a74662e8de39444e40639e0b2b + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/91/d7/47988d40231b41376f5a66346ef3b322c81091dfd4c0f84df5a1e3bb06b5/chardet-7.4.0.post1-py3-none-any.whl + name: chardet + version: 7.4.0.post1 + sha256: 57a62ef50f69bc2fb3a3ea1ffffec6d10f3d2112d3b05d6e3cb15c2c9b55f6cc + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/d1/3b/6103194ea934f1c3a4ea080905c8849f71e83de455c16cb625d25f49b779/chardet-7.4.0.post1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + name: chardet + version: 7.4.0.post1 + sha256: 329aa8766c4917d3acc1b1d0462f0b2e820e24e9f341d0f858aee85396ae3002 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/e3/a2/dab58511fbeef06dd88866568ea1a11b2f15654223cafc2681e2da84b1f2/chardet-7.4.0.post1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + name: chardet + version: 7.4.0.post1 + sha256: ad98a6c2e61624b1120919353d222121b8f5848b9d33c885d949fe0235575682 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/e9/32/83a15c6077e7f240834ffd9ed78ef12f20f6e1924d7d7986d33f3d2af905/chardet-7.4.0.post1-cp313-cp313-macosx_10_13_x86_64.whl + name: chardet + version: 7.4.0.post1 + sha256: efdb3785c8700b3d0b354553827a166480a439f9754f7366f795bbe8b42d6daf + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/1e/1d/4fdabeef4e231153b6ed7567602f3b68265ec4e5b76d6024cf647d43d981/charset_normalizer-3.4.6-cp313-cp313-macosx_10_13_universal2.whl name: charset-normalizer - version: 3.4.4 - sha256: 5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016 + version: 3.4.6 + sha256: 11afb56037cbc4b1555a34dd69151e8e069bee82e613a73bef6e714ce733585f requires_python: '>=3.7' -- pypi: https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl +- pypi: https://files.pythonhosted.org/packages/2b/58/a199d245894b12db0b957d627516c78e055adc3a0d978bc7f65ddaf7c399/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl name: charset-normalizer - version: 3.4.4 - sha256: 840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381 + version: 3.4.6 + sha256: 530e8cebeea0d76bdcf93357aa5e41336f48c3dc709ac52da2bb167c5b8271d9 requires_python: '>=3.7' -- pypi: https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl +- pypi: https://files.pythonhosted.org/packages/60/ac/3233d262a310c1b12633536a07cde5ddd16985e6e7e238e9f3f9423d8eb9/charset_normalizer-3.4.6-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl name: charset-normalizer - version: 3.4.4 - sha256: e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794 + version: 3.4.6 + sha256: 9cc4fc6c196d6a8b76629a70ddfcd4635a6898756e2d9cac5565cf0654605d73 requires_python: '>=3.7' -- pypi: https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl +- pypi: https://files.pythonhosted.org/packages/62/28/ff6f234e628a2de61c458be2779cb182bc03f6eec12200d4a525bbfc9741/charset_normalizer-3.4.6-cp311-cp311-macosx_10_9_universal2.whl name: charset-normalizer - version: 3.4.4 - sha256: b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14 + version: 3.4.6 + sha256: 82060f995ab5003a2d6e0f4ad29065b7672b6593c8c63559beefe5b443242c3e requires_python: '>=3.7' -- pypi: https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl +- pypi: https://files.pythonhosted.org/packages/a1/5c/724b6b363603e419829f561c854b87ed7c7e31231a7908708ac086cdf3e2/charset_normalizer-3.4.6-cp313-cp313-win_amd64.whl name: charset-normalizer - version: 3.4.4 - sha256: 6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8 + version: 3.4.6 + sha256: 572d7c822caf521f0525ba1bce1a622a0b85cf47ffbdae6c9c19e3b5ac3c4389 requires_python: '>=3.7' -- pypi: https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl +- pypi: https://files.pythonhosted.org/packages/c6/e3/76f2facfe8eddee0bbd38d2594e709033338eae44ebf1738bcefe0a06185/charset_normalizer-3.4.6-cp311-cp311-win_amd64.whl name: charset-normalizer - version: 3.4.4 - sha256: a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894 + version: 3.4.6 + sha256: a9e68c9d88823b274cf1e72f28cb5dc89c990edf430b0bfd3e2fb0785bfeabf4 requires_python: '>=3.7' - pypi: https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl name: click @@ -3976,59 +4560,79 @@ packages: - pytest-xdist ; extra == 'test-no-images' - wurlitzer ; extra == 'test-no-images' requires_python: '>=3.11' -- pypi: https://files.pythonhosted.org/packages/12/da/91a52516e9d5aea87d32d1523f9cdcf7a35a3b298e6be05d6509ba3cfab2/coverage-7.13.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl +- pypi: https://files.pythonhosted.org/packages/df/91/4a151c94320458895049a3e23b7b2cfc08953c60b14892de837e8eb51d0a/copier-9.14.0-py3-none-any.whl + name: copier + version: 9.14.0 + sha256: e12a18cfef22e67254e5229f0b4bdab85e1e3e82926e448226be0b70d0f4de53 + requires_dist: + - colorama>=0.4.6 + - dunamai>=1.7.0 + - funcy>=1.17 + - jinja2-ansible-filters>=1.3.1 + - jinja2>=3.1.5 + - packaging>=23.0 + - pathspec>=0.9.0 + - platformdirs>=4.3.6 + - plumbum>=1.6.9 + - pydantic>=2.4.2 + - pygments>=2.7.1 + - pyyaml>=5.3.1 + - questionary>=1.8.1 + - typing-extensions>=4.0.0,<5.0.0 ; python_full_version < '3.11' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/0c/c9/44fb661c55062f0818a6ffd2685c67aa30816200d5f2817543717d4b92eb/coverage-7.13.5-cp313-cp313-macosx_11_0_arm64.whl name: coverage - version: 7.13.1 - sha256: fa3edde1aa8807de1d05934982416cb3ec46d1d4d91e280bcce7cca01c507992 + 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/27/56/c216625f453df6e0559ed666d246fcbaaa93f3aa99eaa5080cea1229aa3d/coverage-7.13.1-cp311-cp311-win_amd64.whl +- 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.1 - sha256: a998cc0aeeea4c6d5622a3754da5a493055d2d95186bad877b0a34ea6e6dbe0a + 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/32/33/7cbfe2bdc6e2f03d6b240d23dc45fdaf3fd270aaf2d640be77b7f16989ab/coverage-7.13.1-cp313-cp313-macosx_11_0_arm64.whl +- pypi: https://files.pythonhosted.org/packages/4b/37/d24c8f8220ff07b839b2c043ea4903a33b0f455abe673ae3c03bbdb7f212/coverage-7.13.5-cp311-cp311-macosx_10_9_x86_64.whl name: coverage - version: 7.13.1 - sha256: 1dcb645d7e34dcbcc96cd7c132b1fc55c39263ca62eb961c064eb3928997363b + version: 7.13.5 + sha256: 66a80c616f80181f4d643b0f9e709d97bcea413ecd9631e1dedc7401c8e6695d requires_dist: - tomli ; python_full_version <= '3.11' and extra == 'toml' requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/45/0b/0768b4231d5a044da8f75e097a8714ae1041246bb765d6b5563bab456735/coverage-7.13.1-cp313-cp313-win_amd64.whl +- pypi: https://files.pythonhosted.org/packages/66/40/7732d648ab9d069a46e686043241f01206348e2bbf128daea85be4d6414b/coverage-7.13.5-cp313-cp313-win_amd64.whl name: coverage - version: 7.13.1 - sha256: 5899d28b5276f536fcf840b18b61a9fce23cc3aec1d114c44c07fe94ebeaa500 + 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/a3/a4/e98e689347a1ff1a7f67932ab535cef82eb5e78f32a9e4132e114bbb3a0a/coverage-7.13.1-cp313-cp313-macosx_10_13_x86_64.whl +- pypi: https://files.pythonhosted.org/packages/74/8c/74fedc9663dcf168b0a059d4ea756ecae4da77a489048f94b5f512a8d0b3/coverage-7.13.5-cp313-cp313-macosx_10_13_x86_64.whl name: coverage - version: 7.13.1 - sha256: cb237bfd0ef4d5eb6a19e29f9e528ac67ac3be932ea6b44fb6cc09b9f3ecff78 + version: 7.13.5 + sha256: 5ec4af212df513e399cf11610cc27063f1586419e814755ab362e50a85ea69c1 requires_dist: - tomli ; python_full_version <= '3.11' and extra == 'toml' requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/b4/9b/77baf488516e9ced25fc215a6f75d803493fc3f6a1a1227ac35697910c2a/coverage-7.13.1-cp311-cp311-macosx_10_9_x86_64.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 name: coverage - version: 7.13.1 - sha256: 1a55d509a1dc5a5b708b5dad3b5334e07a16ad4c2185e27b40e4dba796ab7f88 + 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/d7/cd/7ab01154e6eb79ee2fab76bf4d89e94c6648116557307ee4ebbb85e5c1bf/coverage-7.13.1-cp311-cp311-macosx_11_0_arm64.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 name: coverage - version: 7.13.1 - sha256: 4d010d080c4888371033baab27e47c9df7d6fb28d0b7b7adf85a4a49be9298b3 + 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/f7/7c/347280982982383621d29b8c544cf497ae07ac41e44b1ca4903024131f55/coverage-7.13.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl +- pypi: https://files.pythonhosted.org/packages/af/7f/4cd8a92531253f9d7c1bbecd9fa1b472907fb54446ca768c59b531248dc5/coverage-7.13.5-cp311-cp311-win_amd64.whl name: coverage - version: 7.13.1 - sha256: bf100a3288f9bb7f919b87eb84f87101e197535b9bd0e2c2b5b3179633324fee + version: 7.13.5 + sha256: 258354455f4e86e3e9d0d17571d522e13b4e1e19bf0f8596bcf9476d61e7d8a9 requires_dist: - tomli ; python_full_version <= '3.11' and extra == 'toml' requires_python: '>=3.10' @@ -4041,6 +4645,13 @@ packages: - scipy - pycifstar - matplotlib +- 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/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl name: cycler version: 0.12.1 @@ -4061,20 +4672,52 @@ packages: requires_dist: - pyobjc-framework-cocoa ; sys_platform == 'darwin' and extra == 'macos-listener' requires_python: '>=3.6' -- pypi: https://files.pythonhosted.org/packages/25/3e/e27078370414ef35fafad2c06d182110073daaeb5d3bf734b0b1eeefe452/debugpy-1.8.19-py2.py3-none-any.whl +- 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/13/f7/a0b368ce54ffff9e9028c098bd2d28cfc5b54f9f6c186929083d4c60ba58/debugpy-1.8.20-cp313-cp313-win_amd64.whl name: debugpy - version: 1.8.19 - sha256: 360ffd231a780abbc414ba0f005dad409e71c78637efe8f2bd75837132a41d38 + version: 1.8.20 + sha256: eb506e45943cab2efb7c6eafdd65b842f3ae779f020c82221f55aca9de135ed7 requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/f2/a8/aaac7ff12ddf5d68a39e13a423a8490426f5f661384f5ad8d9062761bd8e/debugpy-1.8.19-cp311-cp311-win_amd64.whl +- pypi: https://files.pythonhosted.org/packages/d5/92/1cb532e88560cbee973396254b21bece8c5d7c2ece958a67afa08c9f10dc/debugpy-1.8.20-cp311-cp311-win_amd64.whl name: debugpy - version: 1.8.19 - sha256: 14035cbdbb1fe4b642babcdcb5935c2da3b1067ac211c5c5a8fdc0bb31adbcaa + version: 1.8.20 + sha256: 1f7650546e0eded1902d0f6af28f787fa1f1dbdbc97ddabaf1cd963a405930cb requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/fb/4e/931480b9552c7d0feebe40c73725dd7703dcc578ba9efc14fe0e6d31cfd1/debugpy-1.8.19-cp313-cp313-win_amd64.whl +- pypi: https://files.pythonhosted.org/packages/e0/c3/7f67dea8ccf8fdcb9c99033bbe3e90b9e7395415843accb81428c441be2d/debugpy-1.8.20-py2.py3-none-any.whl name: debugpy - version: 1.8.19 - sha256: c30639998a9f9cd9699b4b621942c0179a6527f083c72351f95c6ab1728d5b73 + version: 1.8.20 + sha256: 5be9bed9ae3be00665a06acaa48f8329d2b9632f15fd09f6a9a8c8d9907e54d7 requires_python: '>=3.8' - pypi: https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl name: decorator @@ -4086,10 +4729,10 @@ packages: version: 0.7.1 sha256: a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61 requires_python: '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*' -- pypi: https://files.pythonhosted.org/packages/68/49/869b49db0bad6fba7c7471d612c356e745929d14e0de63acbc53e88f2a50/dfo_ls-1.6-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/c7/a0/5ff05d1919ca249508012cad89f08fdc6cfbdaa15b41651c5fe6dffaf1d3/dfo_ls-1.6.5-py3-none-any.whl name: dfo-ls - version: '1.6' - sha256: 416edce5537237fa417bd27aef5ba91201e856f3daae52ffd44cfacb10f5b771 + version: 1.6.5 + sha256: d147d42e471e240f9abf8bc38351a88f555ea6a8fcfd83119bbbf93c36f75ab2 requires_dist: - setuptools - numpy @@ -4157,53 +4800,97 @@ packages: - numpy - pycifrw requires_python: '>=3.11,<3.14' -- pypi: https://files.pythonhosted.org/packages/79/c8/f0f4ab7fd08950d5358579364a0a9b9198bf03e179a4fcceae4b353be32e/diffpy_utils-3.6.1-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/61/2b/e260d50e64690d2a9e405d52ccd18a63c286c5088937dd0107cb23eb3195/diffpy_utils-3.7.2-py3-none-any.whl name: diffpy-utils - version: 3.6.1 - sha256: b7917926f35a84db81d2950ce44f243d7d4c7494a96a18dc673ce555cde0d2d2 + version: 3.7.2 + sha256: 6100600736791a8e4638e3dd476704f4dabe3cab75bcb5c60c83c16a2032519a requires_dist: - numpy - xraydb - scipy - requires_python: '>=3.11,<3.14' -- pypi: https://files.pythonhosted.org/packages/50/3d/9373ad9c56321fdab5b41197068e1d8c25883b3fea29dd361f9b55116869/dill-0.4.0-py3-none-any.whl + requires_python: '>=3.10,<3.15' +- pypi: https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl name: dill - version: 0.4.0 - sha256: 44f54bf6412c2c8464c14e8243eb163690a9800dbe2c367330883b19c7561049 + version: 0.4.1 + sha256: 1e1ce33e978ae97fcfcff5638477032b801c46c7c65cf717f95fbc2248f79a9d requires_dist: - objgraph>=1.7.2 ; extra == 'graph' - gprof2dot>=2022.7.29 ; extra == 'profile' - requires_python: '>=3.8' + requires_python: '>=3.9' - 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/dc/b4/a7ec1eaee86761a9dbfd339732b4706db3c6b65e970c12f0f56cfcce3dcf/docformatter-1.7.7-py3-none-any.whl - name: docformatter - version: 1.7.7 - sha256: 7af49f8a46346a77858f6651f431b882c503c2f4442c8b4524b920c863277834 - requires_dist: - - charset-normalizer>=3.0.0,<4.0.0 - - tomli>=2.0.0,<3.0.0 ; python_full_version < '3.11' and extra == 'tomli' - - untokenize>=0.1.1,<0.2.0 - requires_python: '>=3.9,<4.0' -- pypi: https://files.pythonhosted.org/packages/82/ee/c9ca3f81d69ec6c99ca41e10432a58c60fef3a6e63cce6df7e796f90fe22/easydiffraction-0.10.0-py3-none-any.whl +- 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/bf/50/98b146aea0f1cd7531d25f12bea69fa9ce8d1662124f93fb30dc4511b65e/docstring_parser_fork-0.0.14-py3-none-any.whl + name: docstring-parser-fork + version: 0.0.14 + sha256: 4c544f234ef2cc2749a3df32b70c437d77888b1099143a1ad5454452c574b9af + requires_dist: + - docstring-parser[docs] ; extra == 'dev' + - docstring-parser[test] ; extra == 'dev' + - pre-commit>=2.16.0 ; python_full_version >= '3.9' and extra == 'dev' + - pydoctor>=25.4.0 ; extra == 'docs' + - pytest ; extra == 'test' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/87/10/2c7edbf230e5c507d38367af498fa94258ed97205d9b4b6f63a921fe9c49/dunamai-1.26.0-py3-none-any.whl + name: dunamai + version: 1.26.0 + sha256: f584edf0fda0d308cce0961f807bc90a8fe3d9ff4d62f94e72eca7b43f0ed5f6 + requires_dist: + - importlib-metadata>=1.6.0 ; python_full_version < '3.8' + - packaging>=20.9 + requires_python: '>=3.5' +- pypi: ./ name: easydiffraction - version: 0.10.0 - sha256: eef4593f36b9cacf6ded23aa2c22b6b3c317d97b5791c4625c4a66ca457a8e3d + version: 0.10.2+dev7 + sha256: 322f1ccfe97af5f9fa88e7c14eb8b8e12221deb7058cef6320de02e52560d1ad requires_dist: - asciichartpy - asteval - bumps - colorama - cryspy + - darkdetect - dfo-ls - diffpy-pdffit2 - diffpy-utils + - essdiffraction - gemmi + - jupyterlab - lmfit - - numpy<2.4 + - numpy + - pandas + - pixi-kernel + - plotly - pooch + - py3dmol - rich - scipy - sympy @@ -4212,69 +4899,101 @@ packages: - typer - uncertainties - varname - - build ; extra == 'all' - - darkdetect ; extra == 'all' - - docformatter ; extra == 'all' - - interrogate ; extra == 'all' - - jinja2 ; extra == 'all' - - jupyter-dark-detect ; extra == 'all' - - jupyterquiz ; extra == 'all' - - jupytext ; extra == 'all' - - mike ; extra == 'all' - - mkdocs ; extra == 'all' - - mkdocs-autorefs<1.3.0 ; extra == 'all' - - mkdocs-jupyter ; extra == 'all' - - mkdocs-markdownextradata-plugin ; extra == 'all' - - mkdocs-material ; extra == 'all' - - mkdocs-plugin-inline-svg ; extra == 'all' - - mkdocstrings-python ; extra == 'all' - - nbmake ; extra == 'all' - - nbqa ; extra == 'all' - - nbstripout ; extra == 'all' - - pandas ; extra == 'all' - - plotly ; extra == 'all' - - pre-commit ; extra == 'all' - - py3dmol ; extra == 'all' - - pytest ; extra == 'all' - - pytest-cov ; extra == 'all' - - pytest-xdist ; extra == 'all' - - pyyaml ; extra == 'all' - - radon ; extra == 'all' - - ruff ; extra == 'all' - - validate-pyproject[all] ; extra == 'all' - - versioningit ; extra == 'all' - build ; extra == 'dev' - - docformatter ; extra == 'dev' + - copier ; 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-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' - - mike ; extra == 'docs' - - mkdocs ; extra == 'docs' - - mkdocs-autorefs<1.3.0 ; extra == 'docs' - - mkdocs-jupyter ; extra == 'docs' - - mkdocs-markdownextradata-plugin ; extra == 'docs' - - mkdocs-material ; extra == 'docs' - - mkdocs-plugin-inline-svg ; extra == 'docs' - - mkdocstrings-python ; extra == 'docs' - - pyyaml ; extra == 'docs' - - darkdetect ; extra == 'visualization' - - jupyter-dark-detect ; extra == 'visualization' - - pandas ; extra == 'visualization' - - plotly ; extra == 'visualization' - - py3dmol ; extra == 'visualization' - requires_python: '>=3.11,<3.14' + requires_python: '>=3.11' +- 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/55/cd/d729a1bb63fa95387228cc508552dea4685ea0116e484e73238db10f9521/essdiffraction-26.4.0-py3-none-any.whl + name: essdiffraction + version: 26.4.0 + sha256: a58c1ac09c2196ae3769ab8c2d1f8c0152e4e1a69a02bc286caf419cdaa9ec1d + 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' + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/f1/bb/66c80d7f801b191f7b3ee6149a39be9d1a8a81c233e20adaf796d171f93a/essreduce-26.4.0-py3-none-any.whl + name: essreduce + version: 26.4.0 + sha256: 06a9ebf58cba3cc29ac70f7b89a3e596be92d6a61130361b8c19fa8afec2b1b5 + requires_dist: + - sciline>=25.11.0 + - scipp>=26.3.0 + - 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/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl name: execnet version: 2.1.2 @@ -4311,15 +5030,15 @@ packages: - pytest-benchmark ; extra == 'devel' - pytest-cache ; extra == 'devel' - validictory ; extra == 'devel' -- pypi: https://files.pythonhosted.org/packages/9a/30/ab407e2ec752aa541704ed8f93c11e2a5d92c168b8a755d818b74a3c5c2d/filelock-3.20.2-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/a4/a5/842ae8f0c08b61d6484b52f99a03510a3a72d23141942d216ebe81fefbce/filelock-3.25.2-py3-none-any.whl name: filelock - version: 3.20.2 - sha256: fbba7237d6ea277175a32c54bb71ef814a8546d8601269e1bfc388de333974e8 + version: 3.25.2 + sha256: ca8afb0da15f229774c9ad1b455ed96e85a81373065fb10446672f64444ddf70 requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/07/ad/37dd1ae5fa6e01612a1fbb954f0927681f282925a86e86198ccd7b15d515/fonttools-4.61.1-cp311-cp311-win_amd64.whl +- pypi: https://files.pythonhosted.org/packages/03/c5/0e3966edd5ec668d41dfe418787726752bc07e2f5fd8c8f208615e61fa89/fonttools-4.62.1-cp313-cp313-macosx_10_13_x86_64.whl name: fonttools - version: 4.61.1 - sha256: fe2efccb324948a11dd09d22136fe2ac8a97d6c1347cf0b58a911dcd529f66b7 + version: 4.62.1 + sha256: 68959f5fc58ed4599b44aad161c2837477d7f35f5f79402d97439974faebfebe requires_dist: - lxml>=4.0 ; extra == 'lxml' - brotli>=1.0.1 ; platform_python_implementation == 'CPython' and extra == 'woff' @@ -4350,10 +5069,10 @@ packages: - skia-pathops>=0.5.0 ; extra == 'all' - uharfbuzz>=0.45.0 ; extra == 'all' requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/1a/59/453c06d1d83dc0951b69ef692d6b9f1846680342927df54e9a1ca91c6f90/fonttools-4.61.1-cp313-cp313-win_amd64.whl +- pypi: https://files.pythonhosted.org/packages/24/7f/66d3f8a9338a9b67fe6e1739f47e1cd5cee78bd3bc1206ef9b0b982289a5/fonttools-4.62.1-cp311-cp311-macosx_10_9_x86_64.whl name: fonttools - version: 4.61.1 - sha256: 21e7c8d76f62ab13c9472ccf74515ca5b9a761d1bde3265152a6dc58700d895b + version: 4.62.1 + sha256: 9dde91633f77fa576879a0c76b1d89de373cae751a98ddf0109d54e173b40f14 requires_dist: - lxml>=4.0 ; extra == 'lxml' - brotli>=1.0.1 ; platform_python_implementation == 'CPython' and extra == 'woff' @@ -4384,10 +5103,10 @@ packages: - skia-pathops>=0.5.0 ; extra == 'all' - uharfbuzz>=0.45.0 ; extra == 'all' requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/4b/cf/00ba28b0990982530addb8dc3e9e6f2fa9cb5c20df2abdda7baa755e8fe1/fonttools-4.61.1-cp313-cp313-macosx_10_13_universal2.whl +- pypi: https://files.pythonhosted.org/packages/38/60/35186529de1db3c01f5ad625bde07c1f576305eab6d86bbda4c58445f721/fonttools-4.62.1-cp313-cp313-win_amd64.whl name: fonttools - version: 4.61.1 - sha256: 8c56c488ab471628ff3bfa80964372fc13504ece601e0d97a78ee74126b2045c + version: 4.62.1 + sha256: 7aa21ff53e28a9c2157acbc44e5b401149d3c9178107130e82d74ceb500e5056 requires_dist: - lxml>=4.0 ; extra == 'lxml' - brotli>=1.0.1 ; platform_python_implementation == 'CPython' and extra == 'woff' @@ -4418,10 +5137,10 @@ packages: - skia-pathops>=0.5.0 ; extra == 'all' - uharfbuzz>=0.45.0 ; extra == 'all' requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/5a/ca/468c9a8446a2103ae645d14fee3f610567b7042aba85031c1c65e3ef7471/fonttools-4.61.1-cp313-cp313-macosx_10_13_x86_64.whl +- pypi: https://files.pythonhosted.org/packages/3b/56/6f389de21c49555553d6a5aeed5ac9767631497ac836c4f076273d15bd72/fonttools-4.62.1-cp313-cp313-macosx_10_13_universal2.whl name: fonttools - version: 4.61.1 - sha256: dc492779501fa723b04d0ab1f5be046797fee17d27700476edc7ee9ae535a61e + version: 4.62.1 + sha256: c22b1014017111c401469e3acc5433e6acf6ebcc6aa9efb538a533c800971c79 requires_dist: - lxml>=4.0 ; extra == 'lxml' - brotli>=1.0.1 ; platform_python_implementation == 'CPython' and extra == 'woff' @@ -4452,10 +5171,10 @@ packages: - skia-pathops>=0.5.0 ; extra == 'all' - uharfbuzz>=0.45.0 ; extra == 'all' requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/69/12/bf9f4eaa2fad039356cc627587e30ed008c03f1cebd3034376b5ee8d1d44/fonttools-4.61.1-cp311-cp311-macosx_10_9_universal2.whl +- pypi: https://files.pythonhosted.org/packages/88/39/23ff32561ec8d45a4d48578b4d241369d9270dc50926c017570e60893701/fonttools-4.62.1-cp311-cp311-macosx_10_9_universal2.whl name: fonttools - version: 4.61.1 - sha256: c6604b735bb12fef8e0efd5578c9fb5d3d8532d5001ea13a19cddf295673ee09 + version: 4.62.1 + sha256: 40975849bac44fb0b9253d77420c6d8b523ac4dcdcefeff6e4d706838a5b80f7 requires_dist: - lxml>=4.0 ; extra == 'lxml' - brotli>=1.0.1 ; platform_python_implementation == 'CPython' and extra == 'woff' @@ -4486,10 +5205,10 @@ packages: - skia-pathops>=0.5.0 ; extra == 'all' - uharfbuzz>=0.45.0 ; extra == 'all' requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/79/61/1ca198af22f7dd22c17ab86e9024ed3c06299cfdb08170640e9996d501a0/fonttools-4.61.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.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 name: fonttools - version: 4.61.1 - sha256: 75c1a6dfac6abd407634420c93864a1e274ebc1c7531346d9254c0d8f6ca00f9 + version: 4.62.1 + sha256: 1c5c25671ce8805e0d080e2ffdeca7f1e86778c5cbfbeae86d7f866d8830517b requires_dist: - lxml>=4.0 ; extra == 'lxml' - brotli>=1.0.1 ; platform_python_implementation == 'CPython' and extra == 'woff' @@ -4520,10 +5239,10 @@ packages: - skia-pathops>=0.5.0 ; extra == 'all' - uharfbuzz>=0.45.0 ; extra == 'all' requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/a3/4b/d67eedaed19def5967fade3297fed8161b25ba94699efc124b14fb68cdbc/fonttools-4.61.1-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl +- pypi: https://files.pythonhosted.org/packages/d3/97/bf54c5b3f2be34e1f143e6db838dfdc54f2ffa3e68c738934c82f3b2a08d/fonttools-4.62.1-cp311-cp311-win_amd64.whl name: fonttools - version: 4.61.1 - sha256: 64102ca87e84261419c3747a0d20f396eb024bdbeb04c2bfb37e2891f5fadcb5 + version: 4.62.1 + sha256: e8514f4924375f77084e81467e63238b095abda5107620f49421c368a6017ed2 requires_dist: - lxml>=4.0 ; extra == 'lxml' - brotli>=1.0.1 ; platform_python_implementation == 'CPython' and extra == 'woff' @@ -4554,10 +5273,10 @@ packages: - skia-pathops>=0.5.0 ; extra == 'all' - uharfbuzz>=0.45.0 ; extra == 'all' requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/ac/49/4138d1acb6261499bedde1c07f8c2605d1d8f9d77a151e5507fd3ef084b6/fonttools-4.61.1-cp311-cp311-macosx_10_9_x86_64.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 name: fonttools - version: 4.61.1 - sha256: 5ce02f38a754f207f2f06557523cd39a06438ba3aafc0639c477ac409fc64e37 + version: 4.62.1 + sha256: 6706d1cb1d5e6251a97ad3c1b9347505c5615c112e66047abbef0f8545fa30d1 requires_dist: - lxml>=4.0 ; extra == 'lxml' - brotli>=1.0.1 ; platform_python_implementation == 'CPython' and extra == 'woff' @@ -4588,6 +5307,15 @@ packages: - skia-pathops>=0.5.0 ; extra == 'all' - uharfbuzz>=0.45.0 ; extra == 'all' requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/33/b2/986d1220f6ee931e338d272bc1f3ec02cfe5f9b5fad84e95afdad57f1ebc/format_docstring-0.2.7-py3-none-any.whl + name: format-docstring + version: 0.2.7 + sha256: c9d50eafebe0f260e3270ca662ff3a0ed4050f64d95e352f8c5f88d9aede42d6 + requires_dist: + - click>=8.0 + - jupyter-notebook-parser>=0.1.4 + - tomli>=1.1.0 ; python_full_version < '3.11' + requires_python: '>=3.10' - pypi: https://files.pythonhosted.org/packages/cf/58/8acf1b3e91c58313ce5cb67df61001fc9dcd21be4fadb76c1a2d540e09ed/fqdn-1.5.1-py3-none-any.whl name: fqdn version: 1.5.1 @@ -4635,46 +5363,158 @@ packages: version: 1.8.0 sha256: 17c883ab0ab67200b5f964d2b9ed6b00971917d5d8a92df149dc2c9779208ee9 requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/19/5b/0976c1af0dd59a6850e9ea3b6c6d28f3dff0651c694a6a6192a2933e8feb/gemmi-0.7.4-cp313-cp313-macosx_11_0_arm64.whl +- pypi: https://files.pythonhosted.org/packages/e6/ab/fb21f4c939bb440104cc2b396d3be1d9b7a9fd3c6c2a53d98c45b3d7c954/fsspec-2026.2.0-py3-none-any.whl + name: fsspec + version: 2026.2.0 + sha256: 98de475b5cb3bd66bedd5c4679e87b4fdfe1a3bf4d707b151b3c07e58c9a2437 + 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/08/c2409cb01d5368dcfedcbaffa7d044cc8957d57a9d0855244a5eb4709d30/funcy-2.0-py2.py3-none-any.whl + name: funcy + version: '2.0' + sha256: 53df23c8bb1651b12f095df764bfb057935d49537a56de211b098f4c79614bb0 +- pypi: https://files.pythonhosted.org/packages/42/15/26cac702cdf6281ddeb185d5912ce14e555e277c6e4caeb1d36966e43822/gemmi-0.7.5-cp311-cp311-macosx_11_0_arm64.whl name: gemmi - version: 0.7.4 - sha256: d1757e5210fb4244190af782a175787cdd3baa3128f1157cd43d3ccbd3133a60 - requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/19/f7/1a03aa6b06d26dbd1231b3ac907f8fdfcf02c0b27fc5f4c31493ad6ecdad/gemmi-0.7.4-cp313-cp313-win_amd64.whl + version: 0.7.5 + sha256: 4db34eaa3d3fc102afea7a156330862cbeb82f557444c079403d4412e326c527 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/48/eb/46e443fc70b4aabe6e775521ff476aefb051db9acabb16a5cb51f04e3e2b/gemmi-0.7.5-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl name: gemmi - version: 0.7.4 - sha256: ce51f071a69d72c5c242be17e3cd31fbfe7e4a31c86b532c44ea083761389274 - requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/28/bc/e943898c25121f36625ab4913b8e24d0bdd054a17e380d19924066102574/gemmi-0.7.4-cp311-cp311-macosx_10_14_x86_64.whl + version: 0.7.5 + sha256: 895c63c7bcf30cffba97cf12c89dc3905f4645f838c17009b4534459a6c53a1e + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/7f/79/b13830a65bf9fc85474a984604f094cc18817dc93a784f4c567a2dc05169/gemmi-0.7.5-cp311-cp311-macosx_10_14_x86_64.whl name: gemmi - version: 0.7.4 - sha256: 006e3251a1cc70e050460e5b4bd8ab30225feb32dace39a6354806ce77e61e5a - requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/3e/e7/b88b72919c910d3233065680cbc74a2a9d00ed65a06b100751d5b78e08e1/gemmi-0.7.4-cp313-cp313-macosx_10_14_x86_64.whl + version: 0.7.5 + sha256: e134fd33f34bf9f2ffacd9e0207aeac6329dde818f62340e7390217a25ee8e2d + requires_python: '>=3.9' +- 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.4 - sha256: 4088c03764d8b5c0202b43e7def0fa53909b93e13b2fea33473953cd64969e2a - requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/46/3e/51e7914c8a640548d1b980140b1bd1419c169bee300a556cfd7f4175444d/gemmi-0.7.4-cp311-cp311-macosx_11_0_arm64.whl + version: 0.7.5 + sha256: 750b4d9751aaf1460ac4f0f45308ddced25f47bcf7a30355eb3b1f779f03952a + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/b9/5e/62402bf021183bc6122cb01b8f1be17cac67545713fb30f888f59357a782/gemmi-0.7.5-cp311-cp311-win_amd64.whl name: gemmi - version: 0.7.4 - sha256: ee61dd8579fbe19d658806ca42013e780846a407597777db83abbeb68da7100e - requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/96/ae/41aff180c36dd3c8f0a84faf38ac8683f8dca99250abcfbc3ed19897290b/gemmi-0.7.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + version: 0.7.5 + sha256: 06cb44f4e3657b7e3a2b23cd40b67a8e7b5d00bfb92ea94cb4060bd47ba50df6 + 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.4 - sha256: 7ff3f09305f9f7e8c6a4332c8882f3b909b97efc912cd7b376651f4b2e095d42 - requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/dd/9d/412d75eb7b9c0aa1e939b419a66c7d61471aa387919d4be32893921564af/gemmi-0.7.4-cp311-cp311-win_amd64.whl + version: 0.7.5 + sha256: c7d8b08c33fe6ba375223306149092440c69cbfbd55c3d3e3436e5fb315a225d + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/c4/80/fd758344a72ca7b5e1c5bbdc1d263f3b215d3897941b5f450380445ca0a9/gemmi-0.7.5-cp313-cp313-macosx_10_14_x86_64.whl name: gemmi - version: 0.7.4 - sha256: c802b5dc495e53e055c056d5f647dd28f6c0c815d14a778d0cf607db8691c9f3 - requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/ed/34/a6536afaeee07fa351e2087bf7b5b1522aa703bc1f6e29d53c27a722ac33/gemmi-0.7.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + version: 0.7.5 + sha256: ef9b6ada1c00c6ba7c7a5b9e938cc3b45d83e775c23d12bf63b6882d5f3cdd6b + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/ee/ab/7d7463cda94f8b68b969ea97aaad679655a0e436efd6a643e528a8de114e/gemmi-0.7.5-cp313-cp313-win_amd64.whl name: gemmi - version: 0.7.4 - sha256: aaa1b1a613649db9f9d6c67df6662c8357c3459112842dd7a77203552b795a02 - requires_python: '>=3.8' + version: 0.7.5 + sha256: ad1f72ffa24adbfaf259e11471f6f071a668667f6ca846051f3bfea024fd337d + 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 @@ -4685,10 +5525,59 @@ packages: - markdown ; extra == 'dev' - flake8 ; extra == 'dev' - wheel ; extra == 'dev' -- pypi: https://files.pythonhosted.org/packages/02/2f/28592176381b9ab2cafa12829ba7b472d177f3acc35d8fbcf3673d966fff/greenlet-3.3.0-cp313-cp313-macosx_11_0_universal2.whl +- 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/6a/09/e21df6aef1e1ffc0c816f0522ddc3f6dcded766c3261813131c78a704470/gitpython-3.1.46-py3-none-any.whl + name: gitpython + version: 3.1.46 + sha256: 79812ed143d9d25b6d176a10bb511de0f9c67b1fa641d82097b0ab90398a2058 + 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.1.2,<7.2 ; extra == 'doc' + - sphinx-rtd-theme ; extra == 'doc' + - sphinx-autodoc-typehints ; extra == 'doc' + requires_python: '>=3.7' +- 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/72/83/3e06a52aca8128bdd4dcd67e932b809e76a96ab8c232a8b025b2850264c5/greenlet-3.3.2-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl name: greenlet - version: 3.3.0 - sha256: a1e41a81c7e2825822f4e068c48cb2196002362619e2d70b148f20a831c00739 + version: 3.3.2 + sha256: 8e2cd90d413acbf5e77ae41e5d3c9b3ac1d011a756d7284d7f3f2b806bbd6358 requires_dist: - sphinx ; extra == 'docs' - furo ; extra == 'docs' @@ -4696,10 +5585,10 @@ packages: - psutil ; extra == 'test' - setuptools ; extra == 'test' requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/1d/d5/c339b3b4bc8198b7caa4f2bd9fd685ac9f29795816d8db112da3d04175bb/greenlet-3.3.0-cp311-cp311-win_amd64.whl +- pypi: https://files.pythonhosted.org/packages/7a/34/259b28ea7a2a0c904b11cd36c79b8cef8019b26ee5dbe24e73b469dea347/greenlet-3.3.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl name: greenlet - version: 3.3.0 - sha256: 7652ee180d16d447a683c04e4c5f6441bae7ba7b17ffd9f6b3aff4605e9e6f71 + version: 3.3.2 + sha256: b6997d360a4e6a4e936c0f9625b1c20416b8a0ea18a8e19cabbefc712e7397ab requires_dist: - sphinx ; extra == 'docs' - furo ; extra == 'docs' @@ -4707,10 +5596,10 @@ packages: - psutil ; extra == 'test' - setuptools ; extra == 'test' requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/1f/cb/48e964c452ca2b92175a9b2dca037a553036cb053ba69e284650ce755f13/greenlet-3.3.0-cp311-cp311-macosx_11_0_universal2.whl +- pypi: https://files.pythonhosted.org/packages/91/39/5ef5aa23bc545aa0d31e1b9b55822b32c8da93ba657295840b6b34124009/greenlet-3.3.2-cp313-cp313-win_amd64.whl name: greenlet - version: 3.3.0 - sha256: e29f3018580e8412d6aaf5641bb7745d38c85228dacf51a73bd4e26ddf2a6a8e + version: 3.3.2 + sha256: a7945dd0eab63ded0a48e4dcade82939783c172290a7903ebde9e184333ca124 requires_dist: - sphinx ; extra == 'docs' - furo ; extra == 'docs' @@ -4718,10 +5607,10 @@ packages: - psutil ; extra == 'test' - setuptools ; extra == 'test' requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/7e/71/ba21c3fb8c5dce83b8c01f458a42e99ffdb1963aeec08fff5a18588d8fd7/greenlet-3.3.0-cp313-cp313-win_amd64.whl +- pypi: https://files.pythonhosted.org/packages/ac/48/f8b875fa7dea7dd9b33245e37f065af59df6a25af2f9561efa8d822fde51/greenlet-3.3.2-cp313-cp313-macosx_11_0_universal2.whl name: greenlet - version: 3.3.0 - sha256: 9ee1942ea19550094033c35d25d20726e4f1c40d59545815e1128ac58d416d38 + version: 3.3.2 + sha256: aa6ac98bdfd716a749b84d4034486863fd81c3abde9aa3cf8eff9127981a4ae4 requires_dist: - sphinx ; extra == 'docs' - furo ; extra == 'docs' @@ -4729,10 +5618,10 @@ packages: - psutil ; extra == 'test' - setuptools ; extra == 'test' requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/dc/a6/e959a127b630a58e23529972dbc868c107f9d583b5a9f878fb858c46bc1a/greenlet-3.3.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl +- pypi: https://files.pythonhosted.org/packages/f1/3a/efb2cf697fbccdf75b24e2c18025e7dfa54c4f31fab75c51d0fe79942cef/greenlet-3.3.2-cp311-cp311-win_amd64.whl name: greenlet - version: 3.3.0 - sha256: 6cb3a8ec3db4a3b0eb8a3c25436c2d49e3505821802074969db017b87bc6a948 + version: 3.3.2 + sha256: 1e692b2dae4cc7077cbb11b47d258533b48c8fde69a33d0d8a82e2fe8d8531d5 requires_dist: - sphinx ; extra == 'docs' - furo ; extra == 'docs' @@ -4740,10 +5629,10 @@ packages: - psutil ; extra == 'test' - setuptools ; extra == 'test' requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/fd/8e/424b8c6e78bd9837d14ff7df01a9829fc883ba2ab4ea787d4f848435f23f/greenlet-3.3.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl +- pypi: https://files.pythonhosted.org/packages/f3/47/16400cb42d18d7a6bb46f0626852c1718612e35dcb0dffa16bbaffdf5dd2/greenlet-3.3.2-cp311-cp311-macosx_11_0_universal2.whl name: greenlet - version: 3.3.0 - sha256: 087ea5e004437321508a8d6f20efc4cfec5e3c30118e1417ea96ed1d93950527 + version: 3.3.2 + sha256: c56692189a7d1c7606cb794be0a8381470d95c57ce5be03fb3d0ef57c7853b86 requires_dist: - sphinx ; extra == 'docs' - furo ; extra == 'docs' @@ -4751,12 +5640,11 @@ packages: - psutil ; extra == 'test' - setuptools ; extra == 'test' requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/9c/83/3b1d03d36f224edded98e9affd0467630fc09d766c0e56fb1498cbb04a9b/griffe-1.15.0-py3-none-any.whl - name: griffe - version: 1.15.0 - sha256: 6f6762661949411031f5fcda9593f586e6ce8340f0ba88921a0f2ef7a81eb9a3 +- pypi: https://files.pythonhosted.org/packages/4b/4c/cc8c68196db727cfc1432f2ad5de50aa6707e630d44b2e6361dc06d8f134/griffelib-2.0.1-py3-none-any.whl + name: griffelib + version: 2.0.1 + sha256: b769eed581c0e857d362fc8fcd8e57ecd2330c124b6104ac8b4c1c86d76970aa requires_dist: - - colorama>=0.4 - pip>=24.0 ; extra == 'pypi' - platformdirs>=4.2 ; extra == 'pypi' - wheel>=0.42 ; extra == 'pypi' @@ -4817,59 +5705,59 @@ packages: version: 0.16.0 sha256: 63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86 requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/23/95/499b4e56452ef8b6c95a271af0dde08dac4ddb70515a75f346d4f400579b/h5py-3.15.1-cp311-cp311-win_amd64.whl +- pypi: https://files.pythonhosted.org/packages/0f/9e/6142ebfda0cb6e9349c091eae73c2e01a770b7659255248d637bec54a88b/h5py-3.16.0-cp313-cp313-macosx_10_13_x86_64.whl name: h5py - version: 3.15.1 - sha256: 550e51131376889656feec4aff2170efc054a7fe79eb1da3bb92e1625d1ac878 + version: 3.16.0 + sha256: 370a845f432c2c9619db8eed334d1e610c6015796122b0e57aa46312c22617d9 requires_dist: - numpy>=1.21.2 requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/31/96/ba99a003c763998035b0de4c299598125df5fc6c9ccf834f152ddd60e0fb/h5py-3.15.1-cp313-cp313-macosx_11_0_arm64.whl +- 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.15.1 - sha256: ab2219dbc6fcdb6932f76b548e2b16f34a1f52b7666e998157a4dfc02e2c4123 + version: 3.16.0 + sha256: fb1720028d99040792bb2fb31facb8da44a6f29df7697e0b84f0d79aff2e9bd3 requires_dist: - numpy>=1.21.2 requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/41/fd/8349b48b15b47768042cff06ad6e1c229f0a4bd89225bf6b6894fea27e6d/h5py-3.15.1-cp311-cp311-macosx_10_9_x86_64.whl +- pypi: https://files.pythonhosted.org/packages/b0/65/5e088a45d0f43cd814bc5bec521c051d42005a472e804b1a36c48dada09b/h5py-3.16.0-cp313-cp313-macosx_11_0_arm64.whl name: h5py - version: 3.15.1 - sha256: 5aaa330bcbf2830150c50897ea5dcbed30b5b6d56897289846ac5b9e529ec243 + version: 3.16.0 + sha256: 42108e93326c50c2810025aade9eac9d6827524cdccc7d4b75a546e5ab308edb requires_dist: - numpy>=1.21.2 requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/88/b3/40207e0192415cbff7ea1d37b9f24b33f6d38a5a2f5d18a678de78f967ae/h5py-3.15.1-cp313-cp313-macosx_10_13_x86_64.whl +- pypi: https://files.pythonhosted.org/packages/ba/95/a825894f3e45cbac7554c4e97314ce886b233a20033787eda755ca8fecc7/h5py-3.16.0-cp311-cp311-macosx_10_9_x86_64.whl name: h5py - version: 3.15.1 - sha256: c8440fd8bee9500c235ecb7aa1917a0389a2adb80c209fa1cc485bd70e0d94a5 + version: 3.16.0 + sha256: 719439d14b83f74eeb080e9650a6c7aa6d0d9ea0ca7f804347b05fac6fbf18af requires_dist: - numpy>=1.21.2 requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/8b/23/4ab1108e87851ccc69694b03b817d92e142966a6c4abd99e17db77f2c066/h5py-3.15.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl +- 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.15.1 - sha256: 5b849ba619a066196169763c33f9f0f02e381156d61c03e000bb0100f9950faf + version: 3.16.0 + sha256: 9300ad32dea9dfc5171f94d5f6948e159ed93e4701280b0f508773b3f582f402 requires_dist: - numpy>=1.21.2 requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/c1/b0/1c628e26a0b95858f54aba17e1599e7f6cd241727596cc2580b72cb0a9bf/h5py-3.15.1-cp311-cp311-macosx_11_0_arm64.whl +- pypi: https://files.pythonhosted.org/packages/bf/3b/38ff88b347c3e346cda1d3fc1b65a7aa75d40632228d8b8a5d7b58508c24/h5py-3.16.0-cp311-cp311-macosx_11_0_arm64.whl name: h5py - version: 3.15.1 - sha256: c970fb80001fffabb0109eaf95116c8e7c0d3ca2de854e0901e8a04c1f098509 + version: 3.16.0 + sha256: c3f0a0e136f2e95dd0b67146abb6668af4f1a69c81ef8651a2d316e8e01de447 requires_dist: - numpy>=1.21.2 requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/d9/69/4402ea66272dacc10b298cca18ed73e1c0791ff2ae9ed218d3859f9698ac/h5py-3.15.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl +- pypi: https://files.pythonhosted.org/packages/c3/d9/a27997f84341fc0dfcdd1fe4179b6ba6c32a7aa880fdb8c514d4dad6fba3/h5py-3.16.0-cp313-cp313-win_amd64.whl name: h5py - version: 3.15.1 - sha256: 121b2b7a4c1915d63737483b7bff14ef253020f617c2fb2811f67a4bed9ac5e8 + version: 3.16.0 + sha256: 18f2bbcd545e6991412253b98727374c356d67caa920e68dc79eab36bf5fedad requires_dist: - numpy>=1.21.2 requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/e5/ea/fbb258a98863f99befb10ed727152b4ae659f322e1d9c0576f8a62754e81/h5py-3.15.1-cp313-cp313-win_amd64.whl +- pypi: https://files.pythonhosted.org/packages/f7/20/e6c0ff62ca2ad1a396a34f4380bafccaaf8791ff8fccf3d995a1fc12d417/h5py-3.16.0-cp311-cp311-win_amd64.whl name: h5py - version: 3.15.1 - sha256: dea78b092fd80a083563ed79a3171258d4a4d307492e7cf8b2313d464c82ba52 + version: 3.16.0 + sha256: 17d1f1630f92ad74494a9a7392ab25982ce2b469fc62da6074c0ce48366a2999 requires_dist: - numpy>=1.21.2 requires_python: '>=3.10' @@ -4903,57 +5791,45 @@ packages: - socksio==1.* ; extra == 'socks' - zstandard>=0.18.0 ; extra == 'zstd' requires_python: '>=3.8' -- conda: https://conda.anaconda.org/conda-forge/linux-64/icu-75.1-he02047a_0.conda - sha256: 71e750d509f5fa3421087ba88ef9a7b9be11c53174af3aa4d06aff4c18b38e8e - md5: 8b189310083baabfb622af68fd9d3ae3 +- 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-ng >=12 - - libstdcxx-ng >=12 - license: MIT - license_family: MIT - purls: [] - size: 12129203 - timestamp: 1720853576813 -- conda: https://conda.anaconda.org/conda-forge/osx-64/icu-75.1-h120a0e1_0.conda - sha256: 2e64307532f482a0929412976c8450c719d558ba20c0962832132fd0d07ba7a7 - md5: d68d48a3060eb5abdc1cdc8e2a3a5966 - depends: - - __osx >=10.13 + - libgcc >=14 + - libstdcxx >=14 license: MIT license_family: MIT purls: [] - size: 11761697 - timestamp: 1720853679409 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/icu-75.1-hfee45f7_0.conda - sha256: 9ba12c93406f3df5ab0a43db8a4b4ef67a5871dfd401010fbe29b218b2cbe620 - md5: 5eb22c1d7b3fc4abb50d92d621583137 + size: 12723451 + timestamp: 1773822285671 +- conda: https://conda.anaconda.org/conda-forge/osx-64/icu-78.3-h25d91c4_0.conda + sha256: 1294117122d55246bb83ad5b589e2a031aacdf2d0b1f99fd338aa4394f881735 + md5: 627eca44e62e2b665eeec57a984a7f00 depends: - __osx >=11.0 license: MIT license_family: MIT purls: [] - size: 11857802 - timestamp: 1720853997952 -- conda: https://conda.anaconda.org/conda-forge/win-64/icu-78.1-h637d24d_0.conda - sha256: bee083d5a0f05c380fcec1f30a71ef5518b23563aeb0a21f6b60b792645f9689 - md5: cb8048bed35ef01431184d6a88e46b3e + size: 12273764 + timestamp: 1773822733780 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/icu-78.3-hef89b57_0.conda + sha256: 3a7907a17e9937d3a46dfd41cffaf815abad59a569440d1e25177c15fd0684e5 + md5: f1182c91c0de31a7abd40cedf6a5ebef depends: - - ucrt >=10.0.20348.0 - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 + - __osx >=11.0 license: MIT license_family: MIT purls: [] - size: 13849749 - timestamp: 1766299627069 -- pypi: https://files.pythonhosted.org/packages/0f/1c/e5fd8f973d4f375adb21565739498e2e9a1e54c858a97b9a8ccfdc81da9b/identify-2.6.15-py2.py3-none-any.whl + size: 12361647 + timestamp: 1773822915649 +- pypi: https://files.pythonhosted.org/packages/46/33/92ef41c6fad0233e41d3d84ba8e8ad18d1780f1e5d99b3c683e6d7f98b63/identify-2.6.18-py2.py3-none-any.whl name: identify - version: 2.6.15 - sha256: 1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757 + version: 2.6.18 + sha256: 8db9d3c8ea9079db92cafb0ebf97abdc09d52e97f4dcf773a2e694048b7cd737 requires_dist: - ukkonen ; extra == 'license' - requires_python: '>=3.9' + requires_python: '>=3.10' - pypi: https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl name: idna version: '3.11' @@ -4964,18 +5840,16 @@ packages: - pytest>=8.3.2 ; extra == 'all' - flake8>=7.1.1 ; extra == 'all' requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/38/3d/2d244233ac4f76e38533cfcb2991c9eb4c7bf688ae0a036d30725b8faafe/importlib_metadata-9.0.0-py3-none-any.whl name: importlib-metadata - version: 8.7.1 - sha256: 5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151 + version: 9.0.0 + sha256: 2d21d1cc5a017bd0559e36150c21c830ab1dc304dedd1b7ea85d20f45ef3edd7 requires_dist: - zipp>=3.20 - pytest>=6,!=8.1.* ; extra == 'test' - packaging ; extra == 'test' - pyfakefs ; extra == 'test' - - flufl-flake8 ; extra == 'test' - pytest-perf>=0.9.2 ; extra == 'test' - - jaraco-test>=5.4 ; extra == 'test' - sphinx>=3.5 ; extra == 'doc' - jaraco-packaging>=9.3 ; extra == 'doc' - rst-linker>=1.9 ; extra == 'doc' @@ -4983,34 +5857,12 @@ packages: - sphinx-lint ; extra == 'doc' - jaraco-tidelift>=1.4 ; extra == 'doc' - ipython ; extra == 'perf' - - pytest-checkdocs>=2.4 ; extra == 'check' + - pytest-checkdocs>=2.14 ; extra == 'check' - pytest-ruff>=0.2.1 ; sys_platform != 'cygwin' and extra == 'check' - pytest-cov ; extra == 'cover' - pytest-enabler>=3.4 ; extra == 'enabler' - - pytest-mypy>=1.0.1 ; extra == 'type' - - mypy<1.19 ; platform_python_implementation == 'PyPy' and extra == 'type' - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/a4/ed/1f1afb2e9e7f38a545d628f864d562a5ae64fe6f7a10e28ffb9b185b4e89/importlib_resources-6.5.2-py3-none-any.whl - name: importlib-resources - version: 6.5.2 - sha256: 789cfdc3ed28c78b67a06acb8126751ced69a3d5f79c095a98298cd8a760ccec - requires_dist: - - zipp>=3.1.0 ; python_full_version < '3.10' - - pytest>=6,!=8.1.* ; extra == 'test' - - zipp>=3.17 ; extra == 'test' - - jaraco-test>=5.4 ; extra == 'test' - - sphinx>=3.5 ; extra == 'doc' - - jaraco-packaging>=9.3 ; extra == 'doc' - - rst-linker>=1.9 ; extra == 'doc' - - furo ; extra == 'doc' - - sphinx-lint ; extra == 'doc' - - jaraco-tidelift>=1.4 ; extra == 'doc' - - pytest-checkdocs>=2.4 ; extra == 'check' - - pytest-ruff>=0.2.1 ; sys_platform != 'cygwin' and extra == 'check' - - pytest-cov ; extra == 'cover' - - pytest-enabler>=2.2 ; extra == 'enabler' - - pytest-mypy ; extra == 'type' - requires_python: '>=3.9' + - pytest-mypy>=1.0.1 ; platform_python_implementation != 'PyPy' and extra == 'type' + requires_python: '>=3.10' - pypi: https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl name: iniconfig version: 2.3.0 @@ -5044,23 +5896,38 @@ packages: - pytest-mock ; extra == 'tests' - coverage[toml] ; extra == 'tests' requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/f6/d8/502954a4ec0efcf264f99b65b41c3c54e65a647d9f0d6f62cd02227d242c/ipykernel-6.31.0-py3-none-any.whl +- 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/82/b9/e73d5d9f405cba7706c539aa8b311b49d4c2f3d698d9c12f815231169c71/ipykernel-7.2.0-py3-none-any.whl name: ipykernel - version: 6.31.0 - sha256: abe5386f6ced727a70e0eb0cf1da801fa7c5fa6ff82147747d5a0406cd8c94af + version: 7.2.0 + sha256: 3bbd4420d2b3cc105cbdf3756bfc04500b1e52f090a90716851f3916c62e1661 requires_dist: - appnope>=0.1.2 ; sys_platform == 'darwin' - comm>=0.1.1 - debugpy>=1.6.5 - ipython>=7.23.1 - - jupyter-client>=8.0.0 - - jupyter-core>=4.12,!=5.0.* + - jupyter-client>=8.8.0 + - jupyter-core>=5.1,!=6.0.* - matplotlib-inline>=0.1 - nest-asyncio>=1.4 - packaging>=22 - psutil>=5.7 - pyzmq>=25 - - tornado>=6.2 + - tornado>=6.4.1 - traitlets>=5.4.0 - coverage[toml] ; extra == 'cov' - matplotlib ; extra == 'cov' @@ -5069,8 +5936,8 @@ packages: - intersphinx-registry ; extra == 'docs' - myst-parser ; extra == 'docs' - pydata-sphinx-theme ; extra == 'docs' - - sphinx ; extra == 'docs' - sphinx-autodoc-typehints ; extra == 'docs' + - sphinx<8.2.0 ; extra == 'docs' - sphinxcontrib-github-alt ; extra == 'docs' - sphinxcontrib-spelling ; extra == 'docs' - trio ; extra == 'docs' @@ -5082,12 +5949,12 @@ packages: - pytest-asyncio>=0.23.5 ; extra == 'test' - pytest-cov ; extra == 'test' - pytest-timeout ; extra == 'test' - - pytest>=7.0,<9 ; extra == 'test' - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/86/92/162cfaee4ccf370465c5af1ce36a9eacec1becb552f2033bb3584e6f640a/ipython-9.9.0-py3-none-any.whl + - pytest>=7.0,<10 ; extra == 'test' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/3d/aa/898dec789a05731cd5a9f50605b7b44a72bd198fd0d4528e11fc610177cc/ipython-9.10.0-py3-none-any.whl name: ipython - version: 9.9.0 - sha256: b457fe9165df2b84e8ec909a97abcf2ed88f565970efba16b1f7229c283d252b + version: 9.10.0 + sha256: c6ab68cc23bba8c7e18e9b932797014cc61ea7fd6f19de180ab9ba73e65ee58d requires_dist: - colorama>=0.4.4 ; sys_platform == 'win32' - decorator>=4.3.2 @@ -5130,6 +5997,52 @@ packages: - ipython[doc,matplotlib,terminal,test,test-extra] ; extra == 'all' - argcomplete>=3.0 ; extra == 'all' requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/b2/90/45c72becc57158facc6a6404f663b77bbcea2519ca57f760e2879ae1315d/ipython-9.11.0-py3-none-any.whl + name: ipython + version: 9.11.0 + sha256: 6922d5bcf944c6e525a76a0a304451b60a2b6f875e86656d8bc2dfda5d710e19 + requires_dist: + - colorama>=0.4.4 ; sys_platform == 'win32' + - decorator>=5.1.0 + - ipython-pygments-lexers>=1.0.0 + - jedi>=0.18.2 + - matplotlib-inline>=0.1.6 + - pexpect>4.6 ; sys_platform != 'emscripten' and sys_platform != 'win32' + - prompt-toolkit>=3.0.41,<3.1.0 + - pygments>=2.14.0 + - stack-data>=0.6.0 + - traitlets>=5.13.0 + - black ; extra == 'black' + - docrepr ; extra == 'doc' + - exceptiongroup ; extra == 'doc' + - intersphinx-registry ; extra == 'doc' + - ipykernel ; extra == 'doc' + - ipython[matplotlib,test] ; extra == 'doc' + - setuptools>=80.0 ; extra == 'doc' + - sphinx-toml==0.0.4 ; extra == 'doc' + - sphinx-rtd-theme>=0.1.8 ; extra == 'doc' + - sphinx>=8.0 ; extra == 'doc' + - typing-extensions ; extra == 'doc' + - pytest>=7.0.0 ; extra == 'test' + - pytest-asyncio>=1.0.0 ; extra == 'test' + - testpath>=0.2 ; extra == 'test' + - packaging>=23.0.0 ; extra == 'test' + - setuptools>=80.0 ; extra == 'test' + - ipython[test] ; extra == 'test-extra' + - curio ; extra == 'test-extra' + - jupyter-ai ; extra == 'test-extra' + - ipython[matplotlib] ; extra == 'test-extra' + - nbformat ; extra == 'test-extra' + - nbclient ; extra == 'test-extra' + - ipykernel>6.30 ; extra == 'test-extra' + - numpy>=2.0 ; extra == 'test-extra' + - pandas>2.1 ; extra == 'test-extra' + - trio>=0.22.0 ; extra == 'test-extra' + - matplotlib>3.9 ; extra == 'matplotlib' + - ipython[doc,matplotlib,terminal,test,test-extra] ; extra == 'all' + - argcomplete>=3.0 ; extra == 'all' + - types-decorator ; extra == 'all' + requires_python: '>=3.12' - pypi: https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl name: ipython-pygments-lexers version: 1.1.1 @@ -5137,6 +6050,22 @@ packages: requires_dist: - pygments requires_python: '>=3.8' +- 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/7b/55/e5326141505c5d5e34c5e0935d2908a74e4561eca44108fbfb9c13d2911a/isoduration-20.11.0-py3-none-any.whl name: isoduration version: 20.11.0 @@ -5192,25 +6121,34 @@ packages: - markupsafe>=2.0 - babel>=2.7 ; extra == 'i18n' requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/72/b9/313e8f2f2e9517ae050a692ae7b3e4b3f17cc5e6dfea0db51fe14e586580/jinja2_ansible_filters-1.3.2-py3-none-any.whl + name: jinja2-ansible-filters + version: 1.3.2 + sha256: e1082f5564917649c76fed239117820610516ec10f87735d0338688800a55b34 + requires_dist: + - jinja2 + - pyyaml + - pytest ; extra == 'test' + - pytest-cov ; extra == 'test' - pypi: https://files.pythonhosted.org/packages/d7/9e/038522f50ceb7e74f1f991bf1b699f24b0c2bbe7c390dd36ad69f4582258/json5-0.13.0-py3-none-any.whl name: json5 version: 0.13.0 sha256: 9a08e1dd65f6a4d4c6fa82d216cf2477349ec2346a38fd70cc11d2557499fbcc requires_python: '>=3.8.0' -- pypi: https://files.pythonhosted.org/packages/71/92/5e77f98553e9e75130c78900d000368476aed74276eb8ae8796f65f00918/jsonpointer-3.0.0-py2.py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/9e/6a/a83720e953b1682d2d109d3c2dbb0bc9bf28cc1cbc205be4ef4be5da709d/jsonpointer-3.1.1-py3-none-any.whl name: jsonpointer - version: 3.0.0 - sha256: 13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942 - requires_python: '>=3.7' -- pypi: https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl + version: 3.1.1 + sha256: 8ff8b95779d071ba472cf5bc913028df06031797532f08a7d5b602d8b2a488ca + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl name: jsonschema - version: 4.25.1 - sha256: 3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63 + version: 4.26.0 + sha256: d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce requires_dist: - attrs>=22.2.0 - jsonschema-specifications>=2023.3.6 - referencing>=0.28.4 - - rpds-py>=0.7.1 + - rpds-py>=0.25.0 - fqdn ; extra == 'format' - idna ; extra == 'format' - isoduration ; extra == 'format' @@ -5228,7 +6166,7 @@ packages: - rfc3987-syntax>=1.1.0 ; extra == 'format-nongpl' - uri-template ; extra == 'format-nongpl' - webcolors>=24.6.0 ; extra == 'format-nongpl' - requires_python: '>=3.9' + requires_python: '>=3.10' - pypi: https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl name: jsonschema-specifications version: 2025.9.1 @@ -5236,10 +6174,10 @@ packages: requires_dist: - referencing>=0.31.0 requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/bb/f5/fddaec430367be9d62a7ed125530e133bfd4a1c0350fe221149ee0f2b526/jupyter_client-8.7.0-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/2d/0b/ceb7694d864abc0a047649aec263878acb9f792e1fec3e676f22dc9015e3/jupyter_client-8.8.0-py3-none-any.whl name: jupyter-client - version: 8.7.0 - sha256: 3671a94fd25e62f5f2f554f5e95389c2294d89822378a5f2dd24353e1494a9e0 + version: 8.8.0 + sha256: f93a5b99c5e23a507b773d3a1136bd6e16c67883ccdbd9a829b0bbdb98cd7d7a requires_dist: - jupyter-core>=5.1 - python-dateutil>=2.8.2 @@ -5253,10 +6191,12 @@ packages: - sphinx>=4 ; extra == 'docs' - sphinxcontrib-github-alt ; extra == 'docs' - sphinxcontrib-spelling ; extra == 'docs' + - orjson ; extra == 'orjson' - anyio ; extra == 'test' - coverage ; extra == 'test' - ipykernel>=6.14 ; extra == 'test' - - mypy ; extra == 'test' + - msgpack ; extra == 'test' + - mypy ; platform_python_implementation != 'PyPy' and extra == 'test' - paramiko ; sys_platform == 'win32' and extra == 'test' - pre-commit ; extra == 'test' - pytest ; extra == 'test' @@ -5283,13 +6223,6 @@ packages: - pytest-timeout ; extra == 'test' - pytest<9 ; extra == 'test' requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/76/c0/8cc378aad5ccfcc37adeda27d3864f20ea808ad5b63572f5b986a42a649c/jupyter_dark_detect-0.1.0-py3-none-any.whl - name: jupyter-dark-detect - version: 0.1.0 - sha256: 198a7fa1270c6ce1ac2fd056cea9300e018bb8d04211de4c946cbe9e03e7a89d - requires_dist: - - ipython>=7.0.0 - requires_python: '>=3.7' - pypi: https://files.pythonhosted.org/packages/e2/48/577993f1f99c552f18a0428731a755e06171f9902fa118c379eb7c04ea22/jupyter_events-0.12.0-py3-none-any.whl name: jupyter-events version: 0.12.0 @@ -5325,6 +6258,11 @@ packages: - jupyter-server>=1.1.2 - importlib-metadata>=4.8.3 ; python_full_version < '3.10' requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/f4/a4/61adb19f3c74b0dc0e411de4f06ebef564b1f179928f9dffcbd4b378f2ef/jupyter_notebook_parser-0.1.4-py2.py3-none-any.whl + name: jupyter-notebook-parser + version: 0.1.4 + sha256: 27b3b67cf898684e646d569f017cb27046774ad23866cb0bdf51d5f76a46476b + requires_python: '>=3.7' - pypi: https://files.pythonhosted.org/packages/92/80/a24767e6ca280f5a49525d987bf3e4d7552bf67c8be07e8ccf20271f8568/jupyter_server-2.17.0-py3-none-any.whl name: jupyter-server version: 2.17.0 @@ -5373,10 +6311,10 @@ packages: - pytest>=7.0,<9 ; extra == 'test' - requests ; extra == 'test' requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/07/2d/2b32cdbe8d2a602f697a649798554e4f072115438e92249624e532e8aca6/jupyter_server_terminals-0.5.3-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/d1/2d/6674563f71c6320841fc300911a55143925112a72a883e2ca71fba4c618d/jupyter_server_terminals-0.5.4-py3-none-any.whl name: jupyter-server-terminals - version: 0.5.3 - sha256: 41ee0d7dc0ebf2809c668e0fc726dfaf258fcd3e769568996ca731b6194ae9aa + version: 0.5.4 + sha256: 55be353fc74a80bc7f3b20e6be50a55a61cd525626f578dcb66a5708e2007d14 requires_dist: - pywinpty>=2.0.3 ; os_name == 'nt' - terminado>=0.8.3 @@ -5397,10 +6335,10 @@ packages: - pytest-timeout ; extra == 'test' - pytest>=7.0 ; extra == 'test' requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/af/c3/acced767eecc11a70c65c45295db5396c4f0c1937874937d5a76d7b177b6/jupyterlab-4.5.1-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/e1/1b/dad6fdcc658ed7af26fdf3841e7394072c9549a8b896c381ab49dd11e2d9/jupyterlab-4.5.6-py3-none-any.whl name: jupyterlab - version: 4.5.1 - sha256: 31b059de96de0754ff1f2ce6279774b6aab8c34d7082e9752db58207c99bd514 + version: 4.5.6 + sha256: d6b3dac883aa4d9993348e0f8e95b24624f75099aed64eab6a4351a9cdd1e580 requires_dist: - async-lru>=1.0.0 - httpx>=0.25.0,<1 @@ -5501,14 +6439,19 @@ packages: - strict-rfc3339 ; extra == 'test' - werkzeug ; extra == 'test' requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/f7/5a/488f492b49b712ca38326e12c78d0d48ab6be682a2989ce533f0f2c4dfd2/jupyterquiz-2.9.6.2-py2.py3-none-any.whl +- 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/b3/52/bc858b1665d0dec3a2511f4e6f5c18ea85c0977563d624d597c95d6d0fd7/jupyterquiz-2.9.6.4-py2.py3-none-any.whl name: jupyterquiz - version: 2.9.6.2 - sha256: f60f358c809d06a38c423c804740c1113c8840e130227aef50694a9201474bef -- pypi: https://files.pythonhosted.org/packages/bd/0d/2d240e7098e0cafba4d25e9530e7596b1bb1bd4476e41b10346bcaaa36d6/jupytext-1.18.1-py3-none-any.whl + version: 2.9.6.4 + sha256: f8c4418f6c827454523fc882a30d744b585cb58ac1ae277769c3059d04fc272b +- pypi: https://files.pythonhosted.org/packages/16/5a/736dd2f4535dbf3bf26523f9158c011389ef88dd06ec2eef67fd744f1c7b/jupytext-1.19.1-py3-none-any.whl name: jupytext - version: 1.18.1 - sha256: 24f999400726a1c658beae55e15fdd2a6255ab1a418697864cd779874e6011ab + version: 1.19.1 + sha256: d8975035155d034bdfde5c0c37891425314b7ea8d3a6c4b5d18c294348714cd9 requires_dist: - markdown-it-py>=1.0 - mdit-py-plugins @@ -5524,6 +6467,7 @@ packages: - 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' @@ -5558,6 +6502,7 @@ packages: - 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' @@ -5581,45 +6526,45 @@ packages: - pytest-xdist ; extra == 'test-integration' - bash-kernel ; extra == 'test-ui' requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/2d/7a/9d90a151f558e29c3936b8a47ac770235f436f2120aca41a6d5f3d62ae8d/kiwisolver-1.4.9-cp313-cp313-macosx_11_0_arm64.whl +- pypi: https://files.pythonhosted.org/packages/0a/aa/510dc933d87767584abfe03efa445889996c70c2990f6f87c3ebaa0a18c5/kiwisolver-1.5.0-cp311-cp311-macosx_11_0_arm64.whl name: kiwisolver - version: 1.4.9 - sha256: 1a12cf6398e8a0a001a059747a1cbf24705e18fe413bc22de7b3d15c67cffe3f + version: 1.5.0 + sha256: 0df54df7e686afa55e6f21fb86195224a6d9beb71d637e8d7920c95cf0f89aac requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/31/a2/a12a503ac1fd4943c50f9822678e8015a790a13b5490354c68afb8489814/kiwisolver-1.4.9-cp311-cp311-macosx_11_0_arm64.whl +- pypi: https://files.pythonhosted.org/packages/11/60/37b4047a2af0cf5ef6d8b4b26e91829ae6fc6a2d1f74524bcb0e7cd28a32/kiwisolver-1.5.0-cp311-cp311-macosx_10_9_x86_64.whl name: kiwisolver - version: 1.4.9 - sha256: 2405a7d98604b87f3fc28b1716783534b1b4b8510d8142adca34ee0bc3c87543 + version: 1.5.0 + sha256: 3c4923e404d6bcd91b6779c009542e5647fef32e4a5d75e115e3bbac6f2335eb requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/3b/c6/f8df8509fd1eee6c622febe54384a96cfaf4d43bf2ccec7a0cc17e4715c9/kiwisolver-1.4.9-cp311-cp311-win_amd64.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 name: kiwisolver - version: 1.4.9 - sha256: be6a04e6c79819c9a8c2373317d19a96048e5a3f90bec587787e86a1153883c2 + version: 1.5.0 + sha256: 332b4f0145c30b5f5ad9374881133e5aa64320428a57c2c2b61e9d891a51c2f3 requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/66/e1/e533435c0be77c3f64040d68d7a657771194a63c279f55573188161e81ca/kiwisolver-1.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_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 name: kiwisolver - version: 1.4.9 - sha256: dc1ae486f9abcef254b5618dfb4113dd49f94c68e3e027d03cf0143f3f772b61 + version: 1.5.0 + sha256: 2517e24d7315eb51c10664cdb865195df38ab74456c677df67bb47f12d088a27 requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/75/bd/f1a5d894000941739f2ae1b65a32892349423ad49c2e6d0771d0bad3fae4/kiwisolver-1.4.9-cp313-cp313-win_amd64.whl +- pypi: https://files.pythonhosted.org/packages/a8/3a/d0a972b34e1c63e2409413104216cd1caa02c5a37cb668d1687d466c1c45/kiwisolver-1.5.0-cp313-cp313-macosx_11_0_arm64.whl name: kiwisolver - version: 1.4.9 - sha256: dd0a578400839256df88c16abddf9ba14813ec5f21362e1fe65022e00c883d4d + version: 1.5.0 + sha256: dda366d548e89a90d88a86c692377d18d8bd64b39c1fb2b92cb31370e2896bbd requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/a0/c0/27fe1a68a39cf62472a300e2879ffc13c0538546c359b86f149cc19f6ac3/kiwisolver-1.4.9-cp311-cp311-macosx_10_9_x86_64.whl +- pypi: https://files.pythonhosted.org/packages/be/6c/28f17390b62b8f2f520e2915095b3c94d88681ecf0041e75389d9667f202/kiwisolver-1.5.0-cp311-cp311-win_amd64.whl name: kiwisolver - version: 1.4.9 - sha256: 39a219e1c81ae3b103643d2aedb90f1ef22650deb266ff12a19e7773f3e5f089 + version: 1.5.0 + sha256: beb7f344487cdcb9e1efe4b7a29681b74d34c08f0043a327a74da852a6749e7b requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/ca/f0/f44f50c9f5b1a1860261092e3bc91ecdc9acda848a8b8c6abfda4a24dd5c/kiwisolver-1.4.9-cp313-cp313-macosx_10_13_x86_64.whl +- pypi: https://files.pythonhosted.org/packages/be/8a/be60e3bbcf513cc5a50f4a3e88e1dcecebb79c1ad607a7222877becaa101/kiwisolver-1.5.0-cp313-cp313-win_amd64.whl name: kiwisolver - version: 1.4.9 - sha256: efb3a45b35622bb6c16dbfab491a8f5a391fe0e9d45ef32f4df85658232ca0e2 + version: 1.5.0 + sha256: 0bf3acf1419fa93064a4c2189ac0b58e3be7872bf6ee6177b0d4c63dc4cea276 requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/e9/e9/f218a2cb3a9ffbe324ca29a9e399fa2d2866d7f348ec3a88df87fc248fc5/kiwisolver-1.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl +- pypi: https://files.pythonhosted.org/packages/ce/48/adbb40df306f587054a348831220812b9b1d787aff714cfbc8556e38fccd/kiwisolver-1.5.0-cp313-cp313-macosx_10_13_x86_64.whl name: kiwisolver - version: 1.4.9 - sha256: b67e6efbf68e077dd71d1a6b37e43e1a99d0bff1a3d51867d45ee8908b931098 + version: 1.5.0 + sha256: c0e1403fd7c26d77c1f03e096dc58a5c726503fa0db0456678b8668f76f521e3 requires_python: '>=3.10' - pypi: https://files.pythonhosted.org/packages/82/3d/14ce75ef66813643812f3093ab17e46d3a206942ce7376d31ec2d36229e7/lark-1.3.1-py3-none-any.whl name: lark @@ -5631,132 +6576,141 @@ packages: - atomicwrites ; extra == 'atomic-cache' - interegular>=0.3.1,<0.4.0 ; extra == 'interegular' requires_python: '>=3.8' -- conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45-default_hbd61a6d_105.conda - sha256: 1027bd8aa0d5144e954e426ab6218fd5c14e54a98f571985675468b339c808ca - md5: 3ec0aa5037d39b06554109a01e6fb0c6 +- 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' +- 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 + - binutils_impl_linux-64 2.45.1 license: GPL-3.0-only license_family: GPL purls: [] - size: 730831 - timestamp: 1766513089214 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libabseil-20250512.1-cxx17_hba17884_0.conda - sha256: dcd1429a1782864c452057a6c5bc1860f2b637dc20a2b7e6eacd57395bbceff8 - md5: 83b160d4da3e1e847bf044997621ed63 + 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 >=13 - - libstdcxx >=13 + - libgcc >=14 + - libstdcxx >=14 constrains: - - libabseil-static =20250512.1=cxx17* - - abseil-cpp =20250512.1 + - libabseil-static =20260107.1=cxx17* + - abseil-cpp =20260107.1 license: Apache-2.0 license_family: Apache purls: [] - size: 1310612 - timestamp: 1750194198254 -- conda: https://conda.anaconda.org/conda-forge/osx-64/libabseil-20250512.1-cxx17_hfc00f1c_0.conda - sha256: a878efebf62f039a1f1733c1e150a75a99c7029ece24e34efdf23d56256585b1 - md5: ddf1acaed2276c7eb9d3c76b49699a11 + size: 1384817 + timestamp: 1770863194876 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libabseil-20260107.1-cxx17_h7ed6875_0.conda + sha256: 2b4ff36082ddfbacc47ac6e11d4dd9f3403cd109ce8d7f0fbee0cdd47cdef013 + md5: 317f40d7bd7bf6d54b56d4a5b5f5085d depends: - __osx >=10.13 - - libcxx >=18 + - libcxx >=19 constrains: - - abseil-cpp =20250512.1 - - libabseil-static =20250512.1=cxx17* + - libabseil-static =20260107.1=cxx17* + - abseil-cpp =20260107.1 license: Apache-2.0 license_family: Apache purls: [] - size: 1162435 - timestamp: 1750194293086 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libabseil-20250512.1-cxx17_hd41c47c_0.conda - sha256: 7f0ee9ae7fa2cf7ac92b0acf8047c8bac965389e48be61bf1d463e057af2ea6a - md5: 360dbb413ee2c170a0a684a33c4fc6b8 + size: 1217836 + timestamp: 1770863510112 +- 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 >=18 + - libcxx >=19 constrains: - - libabseil-static =20250512.1=cxx17* - - abseil-cpp =20250512.1 + - abseil-cpp =20260107.1 + - libabseil-static =20260107.1=cxx17* license: Apache-2.0 license_family: Apache purls: [] - size: 1174081 - timestamp: 1750194620012 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libblas-3.11.0-5_h4a7cf45_openblas.conda - build_number: 5 - sha256: 18c72545080b86739352482ba14ba2c4815e19e26a7417ca21a95b76ec8da24c - md5: c160954f7418d7b6e87eaf05a8913fa9 + size: 1229639 + timestamp: 1770863511331 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libblas-3.11.0-6_h4a7cf45_openblas.conda + build_number: 6 + sha256: 7bfe936dbb5db04820cf300a9cc1f5ee8d5302fc896c2d66e30f1ee2f20fbfd6 + md5: 6d6d225559bfa6e2f3c90ee9c03d4e2e depends: - - libopenblas >=0.3.30,<0.3.31.0a0 - - libopenblas >=0.3.30,<1.0a0 + - libopenblas >=0.3.32,<0.3.33.0a0 + - libopenblas >=0.3.32,<1.0a0 constrains: + - blas 2.306 openblas + - liblapack 3.11.0 6*_openblas + - liblapacke 3.11.0 6*_openblas + - libcblas 3.11.0 6*_openblas - mkl <2026 - - liblapack 3.11.0 5*_openblas - - libcblas 3.11.0 5*_openblas - - blas 2.305 openblas - - liblapacke 3.11.0 5*_openblas license: BSD-3-Clause - license_family: BSD purls: [] - size: 18213 - timestamp: 1765818813880 -- conda: https://conda.anaconda.org/conda-forge/osx-64/libblas-3.11.0-5_he492b99_openblas.conda - build_number: 5 - sha256: 4754de83feafa6c0b41385f8dab1b13f13476232e16f524564a340871a9fc3bc - md5: 36d2e68a156692cbae776b75d6ca6eae + size: 18621 + timestamp: 1774503034895 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libblas-3.11.0-6_he492b99_openblas.conda + build_number: 6 + sha256: 6865098475f3804208038d0c424edf926f4dc9eacaa568d14e29f59df53731fd + md5: 93e7fc07b395c9e1341d3944dcf2aced depends: - - libopenblas >=0.3.30,<0.3.31.0a0 - - libopenblas >=0.3.30,<1.0a0 + - libopenblas >=0.3.32,<0.3.33.0a0 + - libopenblas >=0.3.32,<1.0a0 constrains: - - liblapack 3.11.0 5*_openblas - - blas 2.305 openblas - - libcblas 3.11.0 5*_openblas + - libcblas 3.11.0 6*_openblas + - blas 2.306 openblas - mkl <2026 - - liblapacke 3.11.0 5*_openblas + - liblapacke 3.11.0 6*_openblas + - liblapack 3.11.0 6*_openblas license: BSD-3-Clause - license_family: BSD purls: [] - size: 18476 - timestamp: 1765819054657 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libblas-3.11.0-5_h51639a9_openblas.conda - build_number: 5 - sha256: 620a6278f194dcabc7962277da6835b1e968e46ad0c8e757736255f5ddbfca8d - md5: bcc025e2bbaf8a92982d20863fe1fb69 + size: 18724 + timestamp: 1774503646078 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libblas-3.11.0-6_h51639a9_openblas.conda + build_number: 6 + sha256: 979227fc03628925037ab2dfda008eb7b5592644d9c2c21dd285cefe8c42553d + md5: e551103471911260488a02155cef9c94 depends: - - libopenblas >=0.3.30,<0.3.31.0a0 - - libopenblas >=0.3.30,<1.0a0 + - libopenblas >=0.3.32,<0.3.33.0a0 + - libopenblas >=0.3.32,<1.0a0 constrains: - - libcblas 3.11.0 5*_openblas - - liblapack 3.11.0 5*_openblas - - liblapacke 3.11.0 5*_openblas - - blas 2.305 openblas + - liblapacke 3.11.0 6*_openblas + - liblapack 3.11.0 6*_openblas + - blas 2.306 openblas + - libcblas 3.11.0 6*_openblas - mkl <2026 license: BSD-3-Clause - license_family: BSD purls: [] - size: 18546 - timestamp: 1765819094137 -- conda: https://conda.anaconda.org/conda-forge/win-64/libblas-3.11.0-5_hf2e6a31_mkl.conda - build_number: 5 - sha256: f0cb7b2697461a306341f7ff32d5b361bb84f3e94478464c1e27ee01fc8f276b - md5: f9decf88743af85c9c9e05556a4c47c0 + size: 18859 + timestamp: 1774504387211 +- conda: https://conda.anaconda.org/conda-forge/win-64/libblas-3.11.0-6_hf2e6a31_mkl.conda + build_number: 6 + sha256: 10c8054f007adca8c780cd8bb9335fa5d990f0494b825158d3157983a25b1ea2 + md5: 95543eec964b4a4a7ca3c4c9be481aa1 depends: - - mkl >=2025.3.0,<2026.0a0 + - mkl >=2025.3.1,<2026.0a0 constrains: - - liblapack 3.11.0 5*_mkl - - libcblas 3.11.0 5*_mkl - - blas 2.305 mkl - - liblapacke 3.11.0 5*_mkl + - blas 2.306 mkl + - liblapacke 3.11.0 6*_mkl + - liblapack 3.11.0 6*_mkl + - libcblas 3.11.0 6*_mkl license: BSD-3-Clause - license_family: BSD purls: [] - size: 67438 - timestamp: 1765819100043 + size: 68082 + timestamp: 1774503684284 - conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlicommon-1.2.0-hb03c661_1.conda sha256: 318f36bd49ca8ad85e6478bd8506c88d82454cc008c1ac1c6bf00a3c42fa610e md5: 72c8fd1af66bd67bf580645b426513ed @@ -5856,86 +6810,82 @@ packages: purls: [] size: 290754 timestamp: 1764018009077 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.11.0-5_h0358290_openblas.conda - build_number: 5 - sha256: 0cbdcc67901e02dc17f1d19e1f9170610bd828100dc207de4d5b6b8ad1ae7ad8 - md5: 6636a2b6f1a87572df2970d3ebc87cc0 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.11.0-6_h0358290_openblas.conda + build_number: 6 + sha256: 57edafa7796f6fa3ebbd5367692dd4c7f552be42109c2dd1a7c89b55089bf374 + md5: 36ae340a916635b97ac8a0655ace2a35 depends: - - libblas 3.11.0 5_h4a7cf45_openblas + - libblas 3.11.0 6_h4a7cf45_openblas constrains: - - liblapacke 3.11.0 5*_openblas - - blas 2.305 openblas - - liblapack 3.11.0 5*_openblas + - blas 2.306 openblas + - liblapack 3.11.0 6*_openblas + - liblapacke 3.11.0 6*_openblas license: BSD-3-Clause - license_family: BSD purls: [] - size: 18194 - timestamp: 1765818837135 -- conda: https://conda.anaconda.org/conda-forge/osx-64/libcblas-3.11.0-5_h9b27e0a_openblas.conda - build_number: 5 - sha256: 8077c29ea720bd152be6e6859a3765228cde51301fe62a3b3f505b377c2cb48c - md5: b31d771cbccff686e01a687708a7ca41 + size: 18622 + timestamp: 1774503050205 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libcblas-3.11.0-6_h9b27e0a_openblas.conda + build_number: 6 + sha256: 8422e1ce083e015bdb44addd25c9a8fe99aa9b0edbd9b7f1239b7ac1e3d04f77 + md5: 2a174868cb9e136c4e92b3ffc2815f04 depends: - - libblas 3.11.0 5_he492b99_openblas + - libblas 3.11.0 6_he492b99_openblas constrains: - - liblapack 3.11.0 5*_openblas - - blas 2.305 openblas - - liblapacke 3.11.0 5*_openblas + - liblapacke 3.11.0 6*_openblas + - blas 2.306 openblas + - liblapack 3.11.0 6*_openblas license: BSD-3-Clause - license_family: BSD purls: [] - size: 18484 - timestamp: 1765819073006 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcblas-3.11.0-5_hb0561ab_openblas.conda - build_number: 5 - sha256: 38809c361bbd165ecf83f7f05fae9b791e1baa11e4447367f38ae1327f402fc0 - md5: efd8bd15ca56e9d01748a3beab8404eb + size: 18713 + timestamp: 1774503667477 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcblas-3.11.0-6_hb0561ab_openblas.conda + build_number: 6 + sha256: 2e6b3e9b1ab672133b70fc6730e42290e952793f132cb5e72eee22835463eba0 + md5: 805c6d31c5621fd75e53dfcf21fb243a depends: - - libblas 3.11.0 5_h51639a9_openblas + - libblas 3.11.0 6_h51639a9_openblas constrains: - - liblapacke 3.11.0 5*_openblas - - liblapack 3.11.0 5*_openblas - - blas 2.305 openblas + - liblapacke 3.11.0 6*_openblas + - blas 2.306 openblas + - liblapack 3.11.0 6*_openblas license: BSD-3-Clause - license_family: BSD purls: [] - size: 18548 - timestamp: 1765819108956 -- conda: https://conda.anaconda.org/conda-forge/win-64/libcblas-3.11.0-5_h2a3cdd5_mkl.conda - build_number: 5 - sha256: 49dc59d8e58360920314b8d276dd80da7866a1484a9abae4ee2760bc68f3e68d - md5: b3fa8e8b55310ba8ef0060103afb02b5 + size: 18863 + timestamp: 1774504433388 +- conda: https://conda.anaconda.org/conda-forge/win-64/libcblas-3.11.0-6_h2a3cdd5_mkl.conda + build_number: 6 + sha256: 02b2a2225f4899c6aaa1dc723e06b3f7a4903d2129988f91fc1527409b07b0a5 + md5: 9e4bf521c07f4d423cba9296b7927e3c depends: - - libblas 3.11.0 5_hf2e6a31_mkl + - libblas 3.11.0 6_hf2e6a31_mkl constrains: - - liblapack 3.11.0 5*_mkl - - liblapacke 3.11.0 5*_mkl - - blas 2.305 mkl + - blas 2.306 mkl + - liblapacke 3.11.0 6*_mkl + - liblapack 3.11.0 6*_mkl license: BSD-3-Clause - license_family: BSD purls: [] - size: 68079 - timestamp: 1765819124349 -- conda: https://conda.anaconda.org/conda-forge/osx-64/libcxx-21.1.8-h3d58e20_0.conda - sha256: cbd8e821e97436d8fc126c24b50df838b05ba4c80494fbb93ccaf2e3b2d109fb - md5: 9f8a60a77ecafb7966ca961c94f33bd1 + size: 68221 + timestamp: 1774503722413 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libcxx-22.1.2-h19cb2f5_0.conda + sha256: 46561199545890e050a8a90edcfce984e5f881da86b09388926e3a6c6b759dec + md5: ed6f7b7a35f942a0301e581d72616f7d depends: - - __osx >=10.13 + - __osx >=11.0 license: Apache-2.0 WITH LLVM-exception license_family: Apache purls: [] - size: 569777 - timestamp: 1765919624323 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-21.1.8-hf598326_0.conda - sha256: 82e228975fd491bcf1071ecd0a6ec2a0fcc5f57eb0bd1d52cb13a18d57c67786 - md5: 780f0251b757564e062187044232c2b7 + size: 564908 + timestamp: 1774439353713 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-22.1.2-h55c6f16_0.conda + sha256: d1402087c8792461bfc081629e8aa97e6e577a31ae0b84e6b9cc144a18f48067 + md5: 4280e0a7fd613b271e022e60dea0138c depends: - __osx >=11.0 license: Apache-2.0 WITH LLVM-exception license_family: Apache purls: [] - size: 569118 - timestamp: 1765919724254 + size: 568094 + timestamp: 1774439202359 - conda: https://conda.anaconda.org/conda-forge/linux-64/libev-4.33-hd590300_2.conda sha256: 1cd6048169fa0395af74ed5d8f1716e22c19a81a8a36f934c110ca3ad4dd27b4 md5: 172bf1cd1ff8629f2b1179945ed45055 @@ -5962,91 +6912,91 @@ packages: purls: [] size: 107458 timestamp: 1702146414478 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.3-hecca717_0.conda - sha256: 1e1b08f6211629cbc2efe7a5bca5953f8f6b3cae0eeb04ca4dacee1bd4e2db2f - md5: 8b09ae86839581147ef2e5c5e229d164 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.4-hecca717_0.conda + sha256: d78f1d3bea8c031d2f032b760f36676d87929b18146351c4464c66b0869df3f5 + md5: e7f7ce06ec24cfcfb9e36d28cf82ba57 depends: - __glibc >=2.17,<3.0.a0 - libgcc >=14 constrains: - - expat 2.7.3.* + - expat 2.7.4.* license: MIT license_family: MIT purls: [] - size: 76643 - timestamp: 1763549731408 -- conda: https://conda.anaconda.org/conda-forge/osx-64/libexpat-2.7.3-heffb93a_0.conda - sha256: d11b3a6ce5b2e832f430fd112084533a01220597221bee16d6c7dc3947dffba6 - md5: 222e0732a1d0780a622926265bee14ef + size: 76798 + timestamp: 1771259418166 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libexpat-2.7.4-h991f03e_0.conda + sha256: 8d9d79b2de7d6f335692391f5281607221bf5d040e6724dad4c4d77cd603ce43 + md5: a684eb8a19b2aa68fde0267df172a1e3 depends: - __osx >=10.13 constrains: - - expat 2.7.3.* + - expat 2.7.4.* license: MIT license_family: MIT purls: [] - size: 74058 - timestamp: 1763549886493 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.7.3-haf25636_0.conda - sha256: fce22610ecc95e6d149e42a42fbc3cc9d9179bd4eb6232639a60f06e080eec98 - md5: b79875dbb5b1db9a4a22a4520f918e1a + size: 74578 + timestamp: 1771260142624 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.7.4-hf6b4638_0.conda + sha256: 03887d8080d6a8fe02d75b80929271b39697ecca7628f0657d7afaea87761edf + md5: a92e310ae8dfc206ff449f362fc4217f depends: - __osx >=11.0 constrains: - - expat 2.7.3.* + - expat 2.7.4.* license: MIT license_family: MIT purls: [] - size: 67800 - timestamp: 1763549994166 -- conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.7.3-hac47afa_0.conda - sha256: 844ab708594bdfbd7b35e1a67c379861bcd180d6efe57b654f482ae2f7f5c21e - md5: 8c9e4f1a0e688eef2e95711178061a0f + size: 68199 + timestamp: 1771260020767 +- conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.7.4-hac47afa_0.conda + sha256: b31f6fb629c4e17885aaf2082fb30384156d16b48b264e454de4a06a313b533d + md5: 1c1ced969021592407f16ada4573586d depends: - ucrt >=10.0.20348.0 - vc >=14.3,<15 - vc14_runtime >=14.44.35208 constrains: - - expat 2.7.3.* + - expat 2.7.4.* license: MIT license_family: MIT purls: [] - size: 70137 - timestamp: 1763550049107 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h9ec8514_0.conda - sha256: 25cbdfa65580cfab1b8d15ee90b4c9f1e0d72128f1661449c9a999d341377d54 - md5: 35f29eec58405aaf55e01cb470d8c26a + size: 70323 + timestamp: 1771259521393 +- 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: 57821 - timestamp: 1760295480630 -- conda: https://conda.anaconda.org/conda-forge/osx-64/libffi-3.5.2-h750e83c_0.conda - sha256: 277dc89950f5d97f1683f26e362d6dca3c2efa16cb2f6fdb73d109effa1cd3d0 - md5: d214916b24c625bcc459b245d509f22e + size: 58592 + timestamp: 1769456073053 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libffi-3.5.2-hd1f9c09_0.conda + sha256: 951958d1792238006fdc6fce7f71f1b559534743b26cc1333497d46e5903a2d6 + md5: 66a0dc7464927d0853b590b6f53ba3ea depends: - __osx >=10.13 license: MIT license_family: MIT purls: [] - size: 52573 - timestamp: 1760295626449 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.5.2-he5f378a_0.conda - sha256: 9b8acdf42df61b7bfe8bdc545c016c29e61985e79748c64ad66df47dbc2e295f - md5: 411ff7cd5d1472bba0f55c0faf04453b + size: 53583 + timestamp: 1769456300951 +- 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: 40251 - timestamp: 1760295839166 -- conda: https://conda.anaconda.org/conda-forge/win-64/libffi-3.5.2-h52bdfb6_0.conda - sha256: ddff25aaa4f0aa535413f5d831b04073789522890a4d8626366e43ecde1534a3 - md5: ba4ad812d2afc22b9a34ce8327a0930f + size: 40979 + timestamp: 1769456747661 +- 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 @@ -6054,97 +7004,97 @@ packages: license: MIT license_family: MIT purls: [] - size: 44866 - timestamp: 1760295760649 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-he0feb66_16.conda - sha256: 6eed58051c2e12b804d53ceff5994a350c61baf117ec83f5f10c953a3f311451 - md5: 6d0363467e6ed84f11435eb309f2ff06 + size: 45831 + timestamp: 1769456418774 +- 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=*_16 - - libgomp 15.2.0 he0feb66_16 + - 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: 1042798 - timestamp: 1765256792743 -- conda: https://conda.anaconda.org/conda-forge/osx-64/libgcc-15.2.0-h08519bb_15.conda - sha256: e04b115ae32f8cbf95905971856ff557b296511735f4e1587b88abf519ff6fb8 - md5: c816665789d1e47cdfd6da8a81e1af64 + size: 1041788 + timestamp: 1771378212382 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libgcc-15.2.0-h08519bb_18.conda + sha256: 83366f11615ab234aa1e0797393f9e07b78124b5a24c4a9f8af0113d02df818e + md5: 9a5cb96e43f5c2296690186e15b3296f depends: - _openmp_mutex constrains: - - libgomp 15.2.0 15 - - libgcc-ng ==15.2.0=*_15 + - libgcc-ng ==15.2.0=*_18 + - libgomp 15.2.0 18 license: GPL-3.0-only WITH GCC-exception-3.1 license_family: GPL purls: [] - size: 422960 - timestamp: 1764839601296 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgcc-15.2.0-hcbb3090_16.conda - sha256: 646c91dbc422fe92a5f8a3a5409c9aac66549f4ce8f8d1cab7c2aa5db789bb69 - md5: 8b216bac0de7a9d60f3ddeba2515545c + size: 423025 + timestamp: 1771378225170 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgcc-15.2.0-hcbb3090_18.conda + sha256: 1d9c4f35586adb71bcd23e31b68b7f3e4c4ab89914c26bed5f2859290be5560e + md5: 92df6107310b1fff92c4cc84f0de247b depends: - _openmp_mutex constrains: - - libgcc-ng ==15.2.0=*_16 - - libgomp 15.2.0 16 + - libgcc-ng ==15.2.0=*_18 + - libgomp 15.2.0 18 license: GPL-3.0-only WITH GCC-exception-3.1 license_family: GPL purls: [] - size: 402197 - timestamp: 1765258985740 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.2.0-h69a702a_16.conda - sha256: 5f07f9317f596a201cc6e095e5fc92621afca64829785e483738d935f8cab361 - md5: 5a68259fac2da8f2ee6f7bfe49c9eb8b + size: 401974 + timestamp: 1771378877463 +- 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_16 + - libgcc 15.2.0 he0feb66_18 license: GPL-3.0-only WITH GCC-exception-3.1 license_family: GPL purls: [] - size: 27256 - timestamp: 1765256804124 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.2.0-h69a702a_16.conda - sha256: 8a7b01e1ee1c462ad243524d76099e7174ebdd94ff045fe3e9b1e58db196463b - md5: 40d9b534410403c821ff64f00d0adc22 + size: 27526 + timestamp: 1771378224552 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.2.0-h69a702a_18.conda + sha256: d2c9fad338fd85e4487424865da8e74006ab2e2475bd788f624d7a39b2a72aee + md5: 9063115da5bc35fdc3e1002e69b9ef6e depends: - - libgfortran5 15.2.0 h68bc16d_16 + - libgfortran5 15.2.0 h68bc16d_18 constrains: - - libgfortran-ng ==15.2.0=*_16 + - libgfortran-ng ==15.2.0=*_18 license: GPL-3.0-only WITH GCC-exception-3.1 license_family: GPL purls: [] - size: 27215 - timestamp: 1765256845586 -- conda: https://conda.anaconda.org/conda-forge/osx-64/libgfortran-15.2.0-h7e5c614_15.conda - sha256: 7bb4d51348e8f7c1a565df95f4fc2a2021229d42300aab8366eda0ea1af90587 - md5: a089323fefeeaba2ae60e1ccebf86ddc + size: 27523 + timestamp: 1771378269450 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libgfortran-15.2.0-h7e5c614_18.conda + sha256: fb06c2a2ef06716a0f2a6550f5d13cdd1d89365993068512b7ae3c34e6e665d9 + md5: 34a9f67498721abcfef00178bcf4b190 depends: - - libgfortran5 15.2.0 hd16e46c_15 + - libgfortran5 15.2.0 hd16e46c_18 constrains: - - libgfortran-ng ==15.2.0=*_15 + - libgfortran-ng ==15.2.0=*_18 license: GPL-3.0-only WITH GCC-exception-3.1 license_family: GPL purls: [] - size: 139002 - timestamp: 1764839892631 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran-15.2.0-h07b0088_16.conda - sha256: 68a6c1384d209f8654112c4c57c68c540540dd8e09e17dd1facf6cf3467798b5 - md5: 11e09edf0dde4c288508501fe621bab4 + size: 139761 + timestamp: 1771378423828 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran-15.2.0-h07b0088_18.conda + sha256: 63f89087c3f0c8621c5c89ecceec1e56e5e1c84f65fc9c5feca33a07c570a836 + md5: 26981599908ed2205366e8fc91b37fc6 depends: - - libgfortran5 15.2.0 hdae7583_16 + - libgfortran5 15.2.0 hdae7583_18 constrains: - - libgfortran-ng ==15.2.0=*_16 + - libgfortran-ng ==15.2.0=*_18 license: GPL-3.0-only WITH GCC-exception-3.1 license_family: GPL purls: [] - size: 138630 - timestamp: 1765259217400 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.2.0-h68bc16d_16.conda - sha256: d0e974ebc937c67ae37f07a28edace978e01dc0f44ee02f29ab8a16004b8148b - md5: 39183d4e0c05609fd65f130633194e37 + size: 138973 + timestamp: 1771379054939 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.2.0-h68bc16d_18.conda + sha256: 539b57cf50ec85509a94ba9949b7e30717839e4d694bc94f30d41c9d34de2d12 + md5: 646855f357199a12f02a87382d429b75 depends: - __glibc >=2.17,<3.0.a0 - libgcc >=15.2.0 @@ -6153,11 +7103,11 @@ packages: license: GPL-3.0-only WITH GCC-exception-3.1 license_family: GPL purls: [] - size: 2480559 - timestamp: 1765256819588 -- conda: https://conda.anaconda.org/conda-forge/osx-64/libgfortran5-15.2.0-hd16e46c_15.conda - sha256: 456385a7d3357d5fdfc8e11bf18dcdf71753c4016c440f92a2486057524dd59a - md5: c2a6149bf7f82774a0118b9efef966dd + size: 2482475 + timestamp: 1771378241063 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libgfortran5-15.2.0-hd16e46c_18.conda + sha256: ddaf9dcf008c031b10987991aa78643e03c24a534ad420925cbd5851b31faa11 + md5: ca52daf58cea766656266c8771d8be81 depends: - libgcc >=15.2.0 constrains: @@ -6165,11 +7115,11 @@ packages: license: GPL-3.0-only WITH GCC-exception-3.1 license_family: GPL purls: [] - size: 1061950 - timestamp: 1764839609607 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran5-15.2.0-hdae7583_16.conda - sha256: 9fb7f4ff219e3fb5decbd0ee90a950f4078c90a86f5d8d61ca608c913062f9b0 - md5: 265a9d03461da24884ecc8eb58396d57 + size: 1062274 + timestamp: 1771378232014 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran5-15.2.0-hdae7583_18.conda + sha256: 91033978ba25e6a60fb86843cf7e1f7dc8ad513f9689f991c9ddabfaf0361e7e + md5: c4a6f7989cffb0544bfd9207b6789971 depends: - libgcc >=15.2.0 constrains: @@ -6177,21 +7127,21 @@ packages: license: GPL-3.0-only WITH GCC-exception-3.1 license_family: GPL purls: [] - size: 598291 - timestamp: 1765258993165 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-he0feb66_16.conda - sha256: 5b3e5e4e9270ecfcd48f47e3a68f037f5ab0f529ccb223e8e5d5ac75a58fc687 - md5: 26c46f90d0e727e95c6c9498a33a09f3 + size: 598634 + timestamp: 1771378886363 +- 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: 603284 - timestamp: 1765256703881 -- conda: https://conda.anaconda.org/conda-forge/win-64/libhwloc-2.12.1-default_h4379cf1_1003.conda - sha256: 2d534c09f92966b885acb3f4a838f7055cea043165a03079a539b06c54e20a49 - md5: d1699ce4fe195a9f61264a1c29b87035 + size: 603262 + timestamp: 1771378117851 +- conda: https://conda.anaconda.org/conda-forge/win-64/libhwloc-2.12.2-default_h4379cf1_1000.conda + sha256: 8cdf11333a81085468d9aa536ebb155abd74adc293576f6013fc0c85a7a90da3 + md5: 3b576f6860f838f950c570f4433b086e depends: - libwinpthread >=12.0.0.r4.gg4f2fc60ca - libxml2 @@ -6202,8 +7152,8 @@ packages: license: BSD-3-Clause license_family: BSD purls: [] - size: 2412642 - timestamp: 1765090345611 + size: 2411241 + timestamp: 1765104337762 - conda: https://conda.anaconda.org/conda-forge/win-64/libiconv-1.18-hc1393d2_2.conda sha256: 0dcdb1a5f01863ac4e8ba006a8b0dc1a02d2221ec3319b5915a1863254d7efa7 md5: 64571d1dd6cdcfa25d0664a5950fdaa2 @@ -6215,145 +7165,145 @@ packages: purls: [] size: 696926 timestamp: 1754909290005 -- conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda - sha256: f2591c0069447bbe28d4d696b7fcb0c5bd0b4ac582769b89addbcf26fb3430d8 - md5: 1a580f7796c7bf6393fddb8bbbde58dc +- conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.2-hb03c661_0.conda + sha256: 755c55ebab181d678c12e49cced893598f2bab22d582fbbf4d8b83c18be207eb + md5: c7c83eecbb72d88b940c249af56c8b17 depends: - __glibc >=2.17,<3.0.a0 - - libgcc >=13 + - libgcc >=14 constrains: - - xz 5.8.1.* + - xz 5.8.2.* license: 0BSD purls: [] - size: 112894 - timestamp: 1749230047870 -- conda: https://conda.anaconda.org/conda-forge/osx-64/liblzma-5.8.1-hd471939_2.conda - sha256: 7e22fd1bdb8bf4c2be93de2d4e718db5c548aa082af47a7430eb23192de6bb36 - md5: 8468beea04b9065b9807fc8b9cdc5894 + size: 113207 + timestamp: 1768752626120 +- conda: https://conda.anaconda.org/conda-forge/osx-64/liblzma-5.8.2-h11316ed_0.conda + sha256: 7ab3c98abd3b5d5ec72faa8d9f5d4b50dcee4970ed05339bc381861199dabb41 + md5: 688a0c3d57fa118b9c97bf7e471ab46c depends: - __osx >=10.13 constrains: - - xz 5.8.1.* + - xz 5.8.2.* license: 0BSD purls: [] - size: 104826 - timestamp: 1749230155443 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.1-h39f12f2_2.conda - sha256: 0cb92a9e026e7bd4842f410a5c5c665c89b2eb97794ffddba519a626b8ce7285 - md5: d6df911d4564d77c4374b02552cb17d1 + size: 105482 + timestamp: 1768753411348 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.2-h8088a28_0.conda + sha256: 7bfc7ffb2d6a9629357a70d4eadeadb6f88fa26ebc28f606b1c1e5e5ed99dc7e + md5: 009f0d956d7bfb00de86901d16e486c7 depends: - __osx >=11.0 constrains: - - xz 5.8.1.* + - xz 5.8.2.* license: 0BSD purls: [] - size: 92286 - timestamp: 1749230283517 -- conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-5.8.1-h2466b09_2.conda - sha256: 55764956eb9179b98de7cc0e55696f2eff8f7b83fc3ebff5e696ca358bca28cc - md5: c15148b2e18da456f5108ccb5e411446 + size: 92242 + timestamp: 1768752982486 +- conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-5.8.2-hfd05255_0.conda + sha256: f25bf293f550c8ed2e0c7145eb404324611cfccff37660869d97abf526eb957c + md5: ba0bfd4c3cf73f299ffe46ff0eaeb8e3 depends: - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 constrains: - - xz 5.8.1.* + - xz 5.8.2.* license: 0BSD purls: [] - size: 104935 - timestamp: 1749230611612 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb9d3cd8_0.conda - sha256: 3aa92d4074d4063f2a162cd8ecb45dccac93e543e565c01a787e16a43501f7ee - md5: c7e925f37e3b40d893459e625f6a53f1 + size: 106169 + timestamp: 1768752763559 +- 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 >=13 + - libgcc >=14 license: BSD-2-Clause license_family: BSD purls: [] - size: 91183 - timestamp: 1748393666725 -- conda: https://conda.anaconda.org/conda-forge/osx-64/libmpdec-4.0.0-h6e16a3a_0.conda - sha256: 98299c73c7a93cd4f5ff8bb7f43cd80389f08b5a27a296d806bdef7841cc9b9e - md5: 18b81186a6adb43f000ad19ed7b70381 + size: 92400 + timestamp: 1769482286018 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libmpdec-4.0.0-hf3981d6_1.conda + sha256: 1096c740109386607938ab9f09a7e9bca06d86770a284777586d6c378b8fb3fd + md5: ec88ba8a245855935b871a7324373105 depends: - __osx >=10.13 license: BSD-2-Clause license_family: BSD purls: [] - size: 77667 - timestamp: 1748393757154 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libmpdec-4.0.0-h5505292_0.conda - sha256: 0a1875fc1642324ebd6c4ac864604f3f18f57fbcf558a8264f6ced028a3c75b2 - md5: 85ccccb47823dd9f7a99d2c7f530342f + size: 79899 + timestamp: 1769482558610 +- 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: 71829 - timestamp: 1748393749336 -- conda: https://conda.anaconda.org/conda-forge/win-64/libmpdec-4.0.0-h2466b09_0.conda - sha256: fc529fc82c7caf51202cc5cec5bb1c2e8d90edbac6d0a4602c966366efe3c7bf - md5: 74860100b2029e2523cf480804c76b9b + size: 73690 + timestamp: 1769482560514 +- 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.2,<15 - - vc14_runtime >=14.29.30139 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 license: BSD-2-Clause license_family: BSD purls: [] - size: 88657 - timestamp: 1723861474602 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libnghttp2-1.67.0-had1ee68_0.conda - sha256: a4a7dab8db4dc81c736e9a9b42bdfd97b087816e029e221380511960ac46c690 - md5: b499ce4b026493a13774bcf0f4c33849 + size: 89411 + timestamp: 1769482314283 +- 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.5,<2.0a0 + - 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.2,<4.0a0 + - openssl >=3.5.5,<4.0a0 license: MIT license_family: MIT purls: [] - size: 666600 - timestamp: 1756834976695 -- conda: https://conda.anaconda.org/conda-forge/osx-64/libnghttp2-1.67.0-h3338091_0.conda - sha256: c48d7e1cc927aef83ff9c48ae34dd1d7495c6ccc1edc4a3a6ba6aff1624be9ac - md5: e7630cef881b1174d40f3e69a883e55f + size: 663344 + timestamp: 1773854035739 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libnghttp2-1.68.1-h70048d4_0.conda + sha256: 899551e16aac9dfb85bfc2fd98b655f4d1b7fea45720ec04ccb93d95b4d24798 + md5: dba4c95e2fe24adcae4b77ebf33559ae depends: - - __osx >=10.13 - - c-ares >=1.34.5,<2.0a0 + - __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.2,<4.0a0 + - openssl >=3.5.5,<4.0a0 license: MIT license_family: MIT purls: [] - size: 605680 - timestamp: 1756835898134 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libnghttp2-1.67.0-hc438710_0.conda - sha256: a07cb53b5ffa2d5a18afc6fd5a526a5a53dd9523fbc022148bd2f9395697c46d - md5: a4b4dd73c67df470d091312ab87bf6ae + size: 606749 + timestamp: 1773854765508 +- 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.5,<2.0a0 + - 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.2,<4.0a0 + - openssl >=3.5.5,<4.0a0 license: MIT license_family: MIT purls: [] - size: 575454 - timestamp: 1756835746393 + size: 576526 + timestamp: 1773854624224 - conda: https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hb9d3cd8_1.conda sha256: 927fe72b054277cde6cb82597d0fcf6baf127dcbce2e0a9d8925a68f1265eef5 md5: d864d34357c3b65a4b731f78c0801dc4 @@ -6365,116 +7315,105 @@ packages: purls: [] size: 33731 timestamp: 1750274110928 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.30-pthreads_h94d23a6_4.conda - sha256: 199d79c237afb0d4780ccd2fbf829cea80743df60df4705202558675e07dd2c5 - md5: be43915efc66345cccb3c310b6ed0374 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.32-pthreads_h94d23a6_0.conda + sha256: 6dc30b28f32737a1c52dada10c8f3a41bc9e021854215efca04a7f00487d09d9 + md5: 89d61bc91d3f39fda0ca10fcd3c68594 depends: - __glibc >=2.17,<3.0.a0 - libgcc >=14 - libgfortran - libgfortran5 >=14.3.0 constrains: - - openblas >=0.3.30,<0.3.31.0a0 + - openblas >=0.3.32,<0.3.33.0a0 license: BSD-3-Clause - license_family: BSD purls: [] - size: 5927939 - timestamp: 1763114673331 -- conda: https://conda.anaconda.org/conda-forge/osx-64/libopenblas-0.3.30-openmp_h6006d49_4.conda - sha256: ba642353f7f41ab2d2eb6410fbe522238f0f4483bcd07df30b3222b4454ee7cd - md5: 9241a65e6e9605e4581a2a8005d7f789 + size: 5928890 + timestamp: 1774471724897 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libopenblas-0.3.32-openmp_h9e49c7b_0.conda + sha256: 6764229359cd927c9efc036930ba28f83436b8d6759c5ac4ea9242fc29a7184e + md5: 4058c5f8dbef6d28cb069f96b95ae6df depends: - - __osx >=10.13 + - __osx >=11.0 - libgfortran - libgfortran5 >=14.3.0 - llvm-openmp >=19.1.7 constrains: - - openblas >=0.3.30,<0.3.31.0a0 + - openblas >=0.3.32,<0.3.33.0a0 license: BSD-3-Clause - license_family: BSD purls: [] - size: 6268795 - timestamp: 1763117623665 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopenblas-0.3.30-openmp_ha158390_3.conda - sha256: dcc626c7103503d1dfc0371687ad553cb948b8ed0249c2a721147bdeb8db4a73 - md5: a18a7f471c517062ee71b843ef95eb8a + size: 6289730 + timestamp: 1774474444702 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopenblas-0.3.32-openmp_he657e61_0.conda + sha256: 713e453bde3531c22a660577e59bf91ef578dcdfd5edb1253a399fa23514949a + md5: 3a1111a4b6626abebe8b978bb5a323bf depends: - __osx >=11.0 - libgfortran - libgfortran5 >=14.3.0 - llvm-openmp >=19.1.7 constrains: - - openblas >=0.3.30,<0.3.31.0a0 + - openblas >=0.3.32,<0.3.33.0a0 license: BSD-3-Clause - license_family: BSD purls: [] - size: 4285762 - timestamp: 1761749506256 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.51.1-h0c1763c_1.conda - sha256: 5ef162b2a1390d1495a759734afe2312a358a58441cf8f378be651903646f3b7 - md5: ad1fd565aff83b543d726382c0ab0af2 + size: 4308797 + timestamp: 1774472508546 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.52.0-hf4e2dac_0.conda + sha256: d716847b7deca293d2e49ed1c8ab9e4b9e04b9d780aea49a97c26925b28a7993 + md5: fd893f6a3002a635b5e50ceb9dd2c0f4 depends: - __glibc >=2.17,<3.0.a0 + - icu >=78.2,<79.0a0 - libgcc >=14 - libzlib >=1.3.1,<2.0a0 license: blessing purls: [] - size: 940686 - timestamp: 1766319628770 -- conda: https://conda.anaconda.org/conda-forge/osx-64/libsqlite-3.51.1-hb99441e_1.conda - sha256: d62dcce2ecf555864db393fa0fdc0492f9f644c0435f516d0ba4c5f9f934234b - md5: ec7a2bad1b422f3966e4776442adb05c + size: 951405 + timestamp: 1772818874251 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libsqlite-3.52.0-h77d7759_0.conda + sha256: f500d1cd50cfcd288d02b8fc3c3b7ecf8de6fec7b86e57ea058def02908e4231 + md5: d553eb96758e038b04027b30fe314b2d depends: - - __osx >=10.13 + - __osx >=11.0 - libzlib >=1.3.1,<2.0a0 license: blessing purls: [] - size: 987257 - timestamp: 1766319833399 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.51.1-h1b79a29_1.conda - sha256: f2c3cbf2ca7d697098964a748fbf19d6e4adcefa23844ec49f0166f1d36af83c - md5: 8c3951797658e10b610929c3e57e9ad9 + size: 996526 + timestamp: 1772819669038 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.52.0-h1ae2325_0.conda + sha256: beb0fd5594d6d7c7cd42c992b6bb4d66cbb39d6c94a8234f15956da99a04306c + md5: f6233a3fddc35a2ec9f617f79d6f3d71 depends: - __osx >=11.0 + - icu >=78.2,<79.0a0 - libzlib >=1.3.1,<2.0a0 license: blessing purls: [] - size: 905861 - timestamp: 1766319901587 -- conda: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.51.1-hf5d6505_1.conda - sha256: d6d86715a1afe11f626b7509935e9d2e14a4946632c0ac474526e20fc6c55f99 - md5: be65be5f758709fc01b01626152e96b0 + size: 918420 + timestamp: 1772819478684 +- conda: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.52.0-hf5d6505_0.conda + sha256: 5fccf1e4e4062f8b9a554abf4f9735a98e70f82e2865d0bfdb47b9de94887583 + md5: 8830689d537fda55f990620680934bb1 depends: - ucrt >=10.0.20348.0 - vc >=14.3,<15 - vc14_runtime >=14.44.35208 license: blessing purls: [] - size: 1292859 - timestamp: 1766319616777 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h934c35e_16.conda - sha256: 813427918316a00c904723f1dfc3da1bbc1974c5cfe1ed1e704c6f4e0798cbc6 - md5: 68f68355000ec3f1d6f26ea13e8f525f + size: 1297302 + timestamp: 1772818899033 +- 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_16 + - libgcc 15.2.0 he0feb66_18 constrains: - - libstdcxx-ng ==15.2.0=*_16 - license: GPL-3.0-only WITH GCC-exception-3.1 - license_family: GPL - purls: [] - size: 5856456 - timestamp: 1765256838573 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-15.2.0-hdf11a46_16.conda - sha256: 81f2f246c7533b41c5e0c274172d607829019621c4a0823b5c0b4a8c7028ee84 - md5: 1b3152694d236cf233b76b8c56bf0eae - depends: - - libstdcxx 15.2.0 h934c35e_16 + - libstdcxx-ng ==15.2.0=*_18 license: GPL-3.0-only WITH GCC-exception-3.1 license_family: GPL purls: [] - size: 27300 - timestamp: 1765256885128 + size: 5852330 + timestamp: 1771378262446 - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.41.3-h5347b49_0.conda sha256: 1a7539cfa7df00714e8943e18de0b06cceef6778e420a5ee3a2a145773758aee md5: db409b7c1720428638e7c0d509d3e1b5 @@ -6538,133 +7477,133 @@ packages: purls: [] size: 100393 timestamp: 1702724383534 -- conda: https://conda.anaconda.org/conda-forge/win-64/libxml2-2.15.1-h779ef1b_1.conda - sha256: 8b47d5fb00a6ccc0f495d16787ab5f37a434d51965584d6000966252efecf56d - md5: 68dc154b8d415176c07b6995bd3a65d9 +- conda: https://conda.anaconda.org/conda-forge/win-64/libxml2-2.15.2-h5d26750_0.conda + sha256: f905eb7046987c336122121759e7f09144729f6898f48cd06df2a945b86998d8 + md5: 1007e1bfe181a2aee214779ee7f13d30 depends: - - icu >=78.1,<79.0a0 - libiconv >=1.18,<2.0a0 - - liblzma >=5.8.1,<6.0a0 - - libxml2-16 2.15.1 h3cfd58e_1 + - liblzma >=5.8.2,<6.0a0 + - libxml2-16 2.15.2 h692994f_0 - libzlib >=1.3.1,<2.0a0 - ucrt >=10.0.20348.0 - vc >=14.3,<15 - vc14_runtime >=14.44.35208 + constrains: + - icu <0.0a0 license: MIT license_family: MIT purls: [] - size: 43387 - timestamp: 1766327259710 -- conda: https://conda.anaconda.org/conda-forge/win-64/libxml2-16-2.15.1-h3cfd58e_1.conda - sha256: a857e941156b7f462063e34e086d212c6ccbc1521ebdf75b9ed66bd90add57dc - md5: 07d73826fde28e7dbaec52a3297d7d26 + size: 43681 + timestamp: 1772704748950 +- conda: https://conda.anaconda.org/conda-forge/win-64/libxml2-16-2.15.2-h692994f_0.conda + sha256: b8c71b3b609c7cfe17f3f2a47c75394d7b30acfb8b34ad7a049ea8757b4d33df + md5: e365238134188e42ed36ee996159d482 depends: - - icu >=78.1,<79.0a0 - libiconv >=1.18,<2.0a0 - - liblzma >=5.8.1,<6.0a0 + - liblzma >=5.8.2,<6.0a0 - libzlib >=1.3.1,<2.0a0 - ucrt >=10.0.20348.0 - vc >=14.3,<15 - vc14_runtime >=14.44.35208 constrains: - - libxml2 2.15.1 + - libxml2 2.15.2 + - icu <0.0a0 license: MIT license_family: MIT purls: [] - size: 518964 - timestamp: 1766327232819 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda - sha256: d4bfe88d7cb447768e31650f06257995601f89076080e76df55e3112d4e47dc4 - md5: edb0dca6bc32e4f4789199455a1dbeb8 + size: 520078 + timestamp: 1772704728534 +- 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 - - libgcc >=13 constrains: - - zlib 1.3.1 *_2 + - zlib 1.3.2 *_2 license: Zlib license_family: Other purls: [] - size: 60963 - timestamp: 1727963148474 -- conda: https://conda.anaconda.org/conda-forge/osx-64/libzlib-1.3.1-hd23fc13_2.conda - sha256: 8412f96504fc5993a63edf1e211d042a1fd5b1d51dedec755d2058948fcced09 - md5: 003a54a4e32b02f7355b50a837e699da + size: 63629 + timestamp: 1774072609062 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libzlib-1.3.2-hbb4bfdb_2.conda + sha256: 4c6da089952b2d70150c74234679d6f7ac04f4a98f9432dec724968f912691e7 + md5: 30439ff30578e504ee5e0b390afc8c65 depends: - - __osx >=10.13 + - __osx >=11.0 constrains: - - zlib 1.3.1 *_2 + - zlib 1.3.2 *_2 license: Zlib license_family: Other purls: [] - size: 57133 - timestamp: 1727963183990 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.1-h8359307_2.conda - sha256: ce34669eadaba351cd54910743e6a2261b67009624dbc7daeeafdef93616711b - md5: 369964e85dc26bfe78f41399b366c435 + size: 59000 + timestamp: 1774073052242 +- 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.1 *_2 + - zlib 1.3.2 *_2 license: Zlib license_family: Other purls: [] - size: 46438 - timestamp: 1727963202283 -- conda: https://conda.anaconda.org/conda-forge/win-64/libzlib-1.3.1-h2466b09_2.conda - sha256: ba945c6493449bed0e6e29883c4943817f7c79cbff52b83360f7b341277c6402 - md5: 41fbfac52c601159df6c01f875de31b9 + size: 47759 + timestamp: 1774072956767 +- 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.2,<15 - - vc14_runtime >=14.29.30139 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 constrains: - - zlib 1.3.1 *_2 + - zlib 1.3.2 *_2 license: Zlib license_family: Other purls: [] - size: 55476 - timestamp: 1727963768015 -- conda: https://conda.anaconda.org/conda-forge/osx-64/llvm-openmp-21.1.8-h472b3d1_0.conda - sha256: 2a41885f44cbc1546ff26369924b981efa37a29d20dc5445b64539ba240739e6 - md5: e2d811e9f464dd67398b4ce1f9c7c872 + size: 58347 + timestamp: 1774072851498 +- conda: https://conda.anaconda.org/conda-forge/osx-64/llvm-openmp-22.1.1-h0d3cbff_0.conda + sha256: 65c30298a921b3f6d49e2f4ec220bc2d2237cbdabd1c19031f4fafbfdad8aa5e + md5: d5e67fb9aeb3f32fc474ca7859a5583b depends: - - __osx >=10.13 + - __osx >=11.0 constrains: - - openmp 21.1.8|21.1.8.* - intel-openmp <0.0a0 + - openmp 22.1.1|22.1.1.* license: Apache-2.0 WITH LLVM-exception license_family: APACHE purls: [] - size: 311405 - timestamp: 1765965194247 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/llvm-openmp-21.1.8-h4a912ad_0.conda - sha256: 56bcd20a0a44ddd143b6ce605700fdf876bcf5c509adc50bf27e76673407a070 - md5: 206ad2df1b5550526e386087bef543c7 + size: 311088 + timestamp: 1774349643537 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/llvm-openmp-22.1.1-hc7d1edf_0.conda + sha256: c6f67e928f47603aca7e4b83632d8f3e82bd698051c7c0b34fcce3796eb9b63c + md5: 5a44f53783d87427790fc8692542f1bb depends: - __osx >=11.0 constrains: - - openmp 21.1.8|21.1.8.* - intel-openmp <0.0a0 + - openmp 22.1.1|22.1.1.* license: Apache-2.0 WITH LLVM-exception license_family: APACHE purls: [] - size: 285974 - timestamp: 1765964756583 -- conda: https://conda.anaconda.org/conda-forge/win-64/llvm-openmp-21.1.8-h4fa8253_0.conda - sha256: 145c4370abe870f10987efa9fc15a8383f1dab09abbc9ad4ff15a55d45658f7b - md5: 0d8b425ac862bcf17e4b28802c9351cb + size: 285912 + timestamp: 1774349644882 +- conda: https://conda.anaconda.org/conda-forge/win-64/llvm-openmp-22.1.1-h4fa8253_0.conda + sha256: 64c7fe6490583f3c49c36c2f413e681072102db8abea13a3e1832f44eaf55518 + md5: d9f479404fe316e575f4a4575f3df406 depends: - ucrt >=10.0.20348.0 - vc >=14.3,<15 - vc14_runtime >=14.44.35208 constrains: + - openmp 22.1.1|22.1.1.* - intel-openmp <0.0a0 - - openmp 21.1.8|21.1.8.* license: Apache-2.0 WITH LLVM-exception license_family: APACHE purls: [] - size: 347566 - timestamp: 1765964942856 + size: 347138 + timestamp: 1774349485844 - pypi: https://files.pythonhosted.org/packages/38/7e/7b91c89a4cf0f543a83be978657afb20c86af6d725253e319589dcc4ce52/lmfit-1.3.4-py3-none-any.whl name: lmfit version: 1.3.4 @@ -6700,6 +7639,11 @@ packages: - pytest-cov ; extra == 'test' - lmfit[dev,doc,test] ; extra == 'all' requires_python: '>=3.9' +- 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/d2/f0/834e479e47e499b6478e807fb57b31cc2db696c4db30557bb6f5aea4a90b/mando-0.7.1-py2.py3-none-any.whl name: mando version: 0.7.1 @@ -6709,17 +7653,17 @@ packages: - argparse ; python_full_version < '2.7' - funcsigs ; python_full_version < '3.3' - rst2ansi ; extra == 'restructuredtext' -- pypi: https://files.pythonhosted.org/packages/70/81/54e3ce63502cd085a0c556652a4e1b919c45a446bd1e5300e10c44c8c521/markdown-3.10-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl name: markdown - version: '3.10' - sha256: b5b99d6951e2e4948d939255596523444c0e677c669700b1d17aa4a8a464cb7c + 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] ; extra == 'docs' + - mkdocstrings[python]>=0.28.3 ; extra == 'docs' - mkdocs-gen-files ; extra == 'docs' - mkdocs-section-index ; extra == 'docs' - mkdocs-literate-nav ; extra == 'docs' @@ -6985,19 +7929,19 @@ packages: version: 1.3.4 sha256: 70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307 requires_python: '>=3.6' -- pypi: https://files.pythonhosted.org/packages/fd/1a/31b7cd6e4e7a02df4e076162e9783620777592bea9e4bb036389389af99d/mike-2.1.3-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/48/f7/10f5e101db25741b91e4f4792c5d97b4fa834ead5cf509ae91097d939424/mike-2.1.4-py3-none-any.whl name: mike - version: 2.1.3 - sha256: d90c64077e84f06272437b464735130d380703a76a5738b152932884c60c062a + version: 2.1.4 + sha256: 39933e992e155dd70f2297e749a0ed78d8fd7942bc33a3666195d177758a280e requires_dist: - - importlib-metadata - - importlib-resources - 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' @@ -7049,31 +7993,31 @@ packages: - pyyaml==5.1 ; extra == 'min-versions' - watchdog==2.0 ; extra == 'min-versions' requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/71/26/4d39d52ea2219604053a4d05b98e90d6a335511cc01806436ec4886b1028/mkdocs_autorefs-1.2.0-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/28/de/a3e710469772c6a89595fc52816da05c1e164b4c866a89e3cb82fb1b67c5/mkdocs_autorefs-1.4.4-py3-none-any.whl name: mkdocs-autorefs - version: 1.2.0 - sha256: d588754ae89bd0ced0c70c06f58566a4ee43471eeeee5202427da7de9ef85a2f + version: 1.4.4 + sha256: 834ef5408d827071ad1bc69e0f39704fa34c7fc05bc8e1c72b227dfdc5c76089 requires_dist: - markdown>=3.3 - markupsafe>=2.0.1 - mkdocs>=1.1 - requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/9f/d4/029f984e8d3f3b6b726bd33cafc473b75e9e44c0f7e80a5b29abc466bdea/mkdocs_get_deps-0.2.0-py3-none-any.whl + 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.0 - sha256: 2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134 + 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.8' -- pypi: https://files.pythonhosted.org/packages/08/37/5f1fd5c3f6954b3256f8126275e62af493b96fb6aef6c0dbc4ee326032ad/mkdocs_jupyter-0.25.1-py3-none-any.whl + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/93/89/eb601278b12c471235860992f5973cf3c55ca3f77d1d6127389eb045a021/mkdocs_jupyter-0.26.1-py3-none-any.whl name: mkdocs-jupyter - version: 0.25.1 - sha256: 3f679a857609885d322880e72533ef5255561bbfdb13cfee2a1e92ef4d4ad8d8 + version: 0.26.1 + sha256: 527242c2c8f1d30970764bbab752de41243e5703f458d8bc05336ec53828192e requires_dist: - - ipykernel>6.0.0,<7.0.0 + - ipykernel>6.0.0,<8 - jupytext>1.13.8,<2 - mkdocs-material>9.0.0 - mkdocs>=1.4.0,<2 @@ -7088,10 +8032,10 @@ packages: - mkdocs - pyyaml requires_python: '>=3.6' -- pypi: https://files.pythonhosted.org/packages/3e/32/ed071cb721aca8c227718cffcf7bd539620e9799bbf2619e90c757bfd030/mkdocs_material-9.7.1-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/2c/01/bc663630c510822c95c47a66af9fa7a443c295b47d5f041e5e6ae62ef659/mkdocs_material-9.7.6-py3-none-any.whl name: mkdocs-material - version: 9.7.1 - sha256: 3f6100937d7d731f87f1e3e3b021c97f7239666b9ba1151ab476cabb96c60d5c + version: 9.7.6 + sha256: 71b84353921b8ea1ba84fe11c50912cc512da8fe0881038fcc9a0761c0e635ba requires_dist: - babel>=2.10 - backrefs>=5.7.post1 @@ -7099,18 +8043,18 @@ packages: - jinja2>=3.1 - markdown>=3.2 - mkdocs-material-extensions>=1.3 - - mkdocs>=1.6 + - mkdocs>=1.6,<2 - paginate>=0.5 - pygments>=2.16 - pymdown-extensions>=10.2 - requests>=2.30 - - mkdocs-git-committers-plugin-2>=1.1,<3 ; extra == 'git' - - mkdocs-git-revision-date-localized-plugin~=1.2,>=1.2.4 ; extra == 'git' - - cairosvg~=2.6 ; extra == 'imaging' - - pillow>=10.2,<12.0 ; extra == 'imaging' - - mkdocs-minify-plugin~=0.7 ; extra == 'recommended' - - mkdocs-redirects~=1.2 ; extra == 'recommended' - - mkdocs-rss-plugin~=1.6 ; extra == 'recommended' + - 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/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl name: mkdocs-material-extensions @@ -7124,39 +8068,36 @@ packages: requires_dist: - mkdocs requires_python: '>=3.5' -- pypi: https://files.pythonhosted.org/packages/cd/10/4c27c3063c2b3681a4b7942f8dbdeb4fa34fecb2c19b594e7345ebf4f86f/mkdocstrings-0.27.0-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/04/41/1cf02e3df279d2dd846a1bf235a928254eba9006dd22b4a14caa71aed0f7/mkdocstrings-1.0.3-py3-none-any.whl name: mkdocstrings - version: 0.27.0 - sha256: 6ceaa7ea830770959b55a16203ac63da24badd71325b96af950e59fd37366332 + version: 1.0.3 + sha256: 0d66d18430c2201dc7fe85134277382baaa15e6b30979f3f3bdbabd6dbdb6046 requires_dist: - - click>=7.0 - - jinja2>=2.11.1 + - jinja2>=3.1 - markdown>=3.6 - markupsafe>=1.1 - - mkdocs>=1.4 - - mkdocs-autorefs>=1.2 - - platformdirs>=2.2 + - mkdocs>=1.6 + - mkdocs-autorefs>=1.4 - pymdown-extensions>=6.3 - - importlib-metadata>=4.6 ; python_full_version < '3.10' - - typing-extensions>=4.1 ; python_full_version < '3.10' - mkdocstrings-crystal>=0.3.4 ; extra == 'crystal' - mkdocstrings-python-legacy>=0.2.1 ; extra == 'python-legacy' - - mkdocstrings-python>=0.5.2 ; extra == 'python' - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/51/23/d02d86553327296c3bf369d444194ea83410cce8f0e690565264f37f3261/mkdocstrings_python-1.13.0-py3-none-any.whl + - mkdocstrings-python>=1.16.2 ; extra == 'python' + 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: 1.13.0 - sha256: b88bbb207bab4086434743849f8e796788b373bd32e7bfefbf8560ac45d88f97 + version: 2.0.3 + sha256: 0b83513478bdfd803ff05aa43e9b1fca9dd22bcd9471f09ca6257f009bc5ee12 requires_dist: - - mkdocstrings>=0.26 - - mkdocs-autorefs>=1.2 - - griffe>=0.49 - requires_python: '>=3.9' -- conda: https://conda.anaconda.org/conda-forge/win-64/mkl-2025.3.0-hac47afa_455.conda - sha256: b2b4c84b95210760e4d12319416c60ab66e03674ccdcbd14aeb59f82ebb1318d - md5: fd05d1e894497b012d05a804232254ed + - mkdocstrings>=0.30 + - mkdocs-autorefs>=1.4 + - griffelib>=2.0 + - typing-extensions>=4.0 ; python_full_version < '3.11' + requires_python: '>=3.10' +- conda: https://conda.anaconda.org/conda-forge/win-64/mkl-2025.3.1-hac47afa_11.conda + sha256: f2c2b2a3c2e7d08d78c10bef7c135a4262c80d1d48c85fb5902ca30d61d645f4 + md5: 3fd3009cef89c36e9898a6feeb0f5530 depends: - - llvm-openmp >=21.1.8 + - llvm-openmp >=22.1.1 - tbb >=2022.3.0 - ucrt >=10.0.20348.0 - vc >=14.3,<15 @@ -7164,8 +8105,8 @@ packages: license: LicenseRef-IntelSimplifiedSoftwareOct2022 license_family: Proprietary purls: [] - size: 100224829 - timestamp: 1767634557029 + size: 99997309 + timestamp: 1774449747739 - pypi: https://files.pythonhosted.org/packages/5b/69/93b34728cc386efdde0c342f8c680b9187dea7beb7adaf6b58a0713be101/mpld3-0.5.12-py3-none-any.whl name: mpld3 version: 0.5.12 @@ -7173,6 +8114,15 @@ packages: requires_dist: - jinja2 - matplotlib +- 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/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl name: mpmath version: 1.3.0 @@ -7298,68 +8248,68 @@ packages: - tomli-w ; extra == 'toml' - pyyaml ; extra == 'yaml' requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/17/e4/67b5c27bd17c085a5ea8f1ec05b8a3e5cba0ca734bfcad5560fb129e70ca/multidict-6.7.0-cp311-cp311-macosx_10_9_x86_64.whl +- pypi: https://files.pythonhosted.org/packages/5a/56/21b27c560c13822ed93133f08aa6372c53a8e067f11fbed37b4adcdac922/multidict-6.7.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl name: multidict - version: 6.7.0 - sha256: 14c9e076eede3b54c636f8ce1c9c252b5f057c62131211f0ceeec273810c9721 + 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/32/0f/13228f26f8b882c34da36efa776c3b7348455ec383bab4a66390e42963ae/multidict-6.7.0-cp311-cp311-win_amd64.whl +- 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.0 - sha256: 95b5ffa4349df2887518bb839409bcf22caa72d82beec453216802f475b23c81 + 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/32/31/75c59e7d3b4205075b4c183fa4ca398a2daf2303ddf616b04ae6ef55cffe/multidict-6.7.0-cp313-cp313-win_amd64.whl +- pypi: https://files.pythonhosted.org/packages/a6/9b/267e64eaf6fc637a15b35f5de31a566634a2740f97d8d094a69d34f524a4/multidict-6.7.1-cp311-cp311-macosx_10_9_x86_64.whl name: multidict - version: 6.7.0 - sha256: 30d193c6cc6d559db42b6bcec8a5d395d34d60c9877a0b71ecd7c204fcf15390 + version: 6.7.1 + sha256: 844c5bca0b5444adb44a623fb0a1310c2f4cd41f402126bb269cd44c9b3f3e1e requires_dist: - typing-extensions>=4.1.0 ; python_full_version < '3.11' requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/4d/e1/866a5d77be6ea435711bef2a4291eed11032679b6b28b56b4776ab06ba3e/multidict-6.7.0-cp311-cp311-macosx_11_0_arm64.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 name: multidict - version: 6.7.0 - sha256: 4c09703000a9d0fa3c3404b27041e574cc7f4df4c6563873246d0e11812a94b6 + 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/91/1c/eb97db117a1ebe46d457a3d235a7b9d2e6dcab174f42d1b67663dd9e5371/multidict-6.7.0-cp313-cp313-macosx_10_13_x86_64.whl +- pypi: https://files.pythonhosted.org/packages/b2/35/e994121b0e90e46134673422dd564623f93304614f5d11886b1b3e06f503/multidict-6.7.1-cp313-cp313-win_amd64.whl name: multidict - version: 6.7.0 - sha256: 7ef6b61cad77091056ce0e7ce69814ef72afacb150b7ac6a3e9470def2198159 + 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/a8/9f/78f8761c2705d4c6d7516faed63c0ebdac569f6db1bef95e0d5218fdc146/multidict-6.7.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl +- pypi: https://files.pythonhosted.org/packages/c7/75/bc704ae15fee974f8fccd871305e254754167dce5f9e42d88a2def741a1d/multidict-6.7.1-cp313-cp313-macosx_10_13_x86_64.whl name: multidict - version: 6.7.0 - sha256: 3e56d780c238f9e1ae66a22d2adf8d16f485381878250db8d496623cd38b22bd + version: 6.7.1 + sha256: 84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2 requires_dist: - typing-extensions>=4.1.0 ; python_full_version < '3.11' requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/c6/2d/f0b184fa88d6630aa267680bdb8623fb69cb0d024b8c6f0d23f9a0f406d3/multidict-6.7.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl +- pypi: https://files.pythonhosted.org/packages/c9/68/f16a3a8ba6f7b6dc92a1f19669c0810bd2c43fc5a02da13b1cbf8e253845/multidict-6.7.1-cp311-cp311-win_amd64.whl name: multidict - version: 6.7.0 - sha256: 9ff96e8815eecacc6645da76c413eb3b3d34cfca256c70b16b286a687d013c32 + 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/f1/d8/6c3442322e41fb1dd4de8bd67bfd11cd72352ac131f6368315617de752f1/multidict-6.7.0-cp313-cp313-macosx_11_0_arm64.whl +- pypi: https://files.pythonhosted.org/packages/dd/a4/d45caf2b97b035c57267791ecfaafbd59c68212004b3842830954bb4b02e/multidict-6.7.1-cp311-cp311-macosx_11_0_arm64.whl name: multidict - version: 6.7.0 - sha256: 9c0359b1ec12b1d6849c59f9d319610b7f20ef990a6d454ab151aa0e3b9f78ca + 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/3d/2e/cf2ffeb386ac3763526151163ad7da9f1b586aac96d2b4f7de1eaebf0c61/narwhals-2.15.0-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/3f/c3/06490e98393dcb4d6ce2bf331a39335375c300afaef526897881fbeae6ab/narwhals-2.18.1-py3-none-any.whl name: narwhals - version: 2.15.0 - sha256: cbfe21ca19d260d9fd67f995ec75c44592d1f106933b03ddd375df7ac841f9d6 + version: 2.18.1 + sha256: a0a8bb80205323851338888ba3a12b4f65d352362c8a94be591244faf36504ad requires_dist: - - cudf>=24.10.0 ; extra == 'cudf' + - 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' @@ -7372,6 +8322,8 @@ packages: - 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/83/a0/5b0c2f11142ed1dddec842457d3f65eaf71a0080894eb6f018755b319c3a/nbclient-0.10.4-py3-none-any.whl @@ -7412,10 +8364,10 @@ packages: - testpath ; extra == 'test' - xmltodict ; extra == 'test' requires_python: '>=3.10.0' -- pypi: https://files.pythonhosted.org/packages/cc/9a/cd673b2f773a12c992f41309ef81b99da1690426bd2f96957a7ade0d3ed7/nbconvert-7.16.6-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/0d/4b/8d5f796a792f8a25f6925a96032f098789f448571eb92011df1ae59e8ea8/nbconvert-7.17.0-py3-none-any.whl name: nbconvert - version: 7.16.6 - sha256: 1375a7b67e0c2883678c48e506dc320febb57685e5ee67faa51b18a90f3a712b + version: 7.17.0 + sha256: 4f99a63b337b9a23504347afdab24a11faa7d86b405e5c8f9881cd313336d518 requires_dist: - beautifulsoup4 - bleach[css]!=5.0.0 @@ -7433,6 +8385,7 @@ packages: - pygments>=2.4.1 - traitlets>=5.1 - flaky ; extra == 'all' + - intersphinx-registry ; extra == 'all' - ipykernel ; extra == 'all' - ipython ; extra == 'all' - ipywidgets>=7.5 ; extra == 'all' @@ -7442,15 +8395,16 @@ packages: - pydata-sphinx-theme ; extra == 'all' - pyqtwebengine>=5.15 ; extra == 'all' - pytest>=7 ; extra == 'all' - - sphinx==5.0.2 ; extra == 'all' + - sphinx>=5.0.2 ; extra == 'all' - sphinxcontrib-spelling ; extra == 'all' - tornado>=6.1 ; extra == 'all' + - intersphinx-registry ; extra == 'docs' - ipykernel ; extra == 'docs' - ipython ; extra == 'docs' - myst-parser ; extra == 'docs' - nbsphinx>=0.2.12 ; extra == 'docs' - pydata-sphinx-theme ; extra == 'docs' - - sphinx==5.0.2 ; extra == 'docs' + - sphinx>=5.0.2 ; extra == 'docs' - sphinxcontrib-spelling ; extra == 'docs' - pyqtwebengine>=5.15 ; extra == 'qtpdf' - pyqtwebengine>=5.15 ; extra == 'qtpng' @@ -7460,7 +8414,7 @@ packages: - ipywidgets>=7.5 ; extra == 'test' - pytest>=7 ; extra == 'test' - playwright ; extra == 'webpdf' - requires_python: '>=3.8' + requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/a9/82/0340caa499416c78e5d8f5f05947ae4bc3cba53c9f038ab6e9ed964e22f1/nbformat-5.10.4-py3-none-any.whl name: nbformat version: 5.10.4 @@ -7510,12 +8464,71 @@ packages: - pyupgrade ; extra == 'toolchain' - ruff ; extra == 'toolchain' requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/12/e5/838eb1004fb9b6f0383c47c0d902eb698fda052e8e64ca48c70a2144a48c/nbstripout-0.8.2-py2.py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/20/16/e777eadfa0c0305878c36fae1d5e6db474fbb15dae202b9ec378809dfb4d/nbstripout-0.9.1-py3-none-any.whl name: nbstripout - version: 0.8.2 - sha256: 5f06f9138cb64daed3e91c5359ff0fff85bab4d0db7d72723be1da6f51b890ae + version: 0.9.1 + sha256: ca027ee45742ee77e4f8e9080254f9a707f1161ba11367b82fdf4a29892c759e requires_dist: - nbformat + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/fc/39/dfdef4cfbfcf7c80cd144b1b2e262c2e785f18b152eeebe14ecd70ee7455/ncrystal-4.2.12-py3-none-any.whl + name: ncrystal + version: 4.2.12 + sha256: 45c414c786cab7e64cf6c69c89e8d49da08f83833b7e27f382366bd5fe7b8582 + requires_dist: + - ncrystal-core==4.2.12 + - ncrystal-python==4.2.12 + - 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/55/8d/2b26572e909238bb114d50fb0d1b6b54eb6dafa2d83a7264f18146796b0d/ncrystal_core-4.2.12-py3-none-macosx_11_0_arm64.whl + name: ncrystal-core + version: 4.2.12 + sha256: d47a1dd3c3348bdc0b7cb8df19ad4c51eeb4befce1279f56a67ca1d773e5d84c + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/7b/51/e13a37a8d924feefb444d7eb42094750ba1bba756cbb8c1f9a523414c4fb/ncrystal_core-4.2.12-py3-none-win_amd64.whl + name: ncrystal-core + version: 4.2.12 + sha256: a04c835c20f5ef5e1869282e921e0eae7665d99c48d27dbe4cb78c0cda0a52cc + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/ab/ee/0d9d9218d2081e56828194f83d0eac6292b7182708fd07a62756c66f7194/ncrystal_core-4.2.12-py3-none-macosx_10_9_x86_64.whl + name: ncrystal-core + version: 4.2.12 + sha256: 8ccf39338f5745334e88036f3df885008294ad1228e5b3ffc6ff4bfd1b01dd99 + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/e3/8b/1f02771d91ceafec996cef7f92f6a24010fedc47fd9404f8e11772d8501c/ncrystal_core-4.2.12-py3-none-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + name: ncrystal-core + version: 4.2.12 + sha256: dd622af09c422973c7effc5bbb6f64d7ad5dc020010564362f6c9eebf99f00da + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/78/98/20fdd6825d1876fc54bb098509e736073d20f569b033f2f23bf74aa1df9c/ncrystal_python-4.2.12-py3-none-any.whl + name: ncrystal-python + version: 4.2.12 + sha256: 60bb715d9d74bae031de25858d33665a4c407a5ecaa4c5626cb42bcf182400bb + requires_dist: + - numpy>=1.22 requires_python: '>=3.8' - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda sha256: 3fde293232fa3fca98635e1167de6b7c7fda83caf24b9d6c91ec9eefb4f4d586 @@ -7550,92 +8563,131 @@ packages: version: 1.6.0 sha256: 87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c requires_python: '>=3.5' +- 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/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.*' -- conda: https://conda.anaconda.org/conda-forge/linux-64/nodejs-25.2.1-he2c55a7_1.conda - sha256: 6516f99fe400181ebe27cba29180ca0c7425c15d7392f74220a028ad0e0064a2 - md5: d8005b3a90515c952b51026f6b7d005d +- conda: https://conda.anaconda.org/conda-forge/linux-64/nodejs-25.8.2-he4ff34a_0.conda + sha256: d1a673d1418d9e956b6e4e46c23e72a511c5c1d45dc5519c947457427036d5e2 + md5: baffb1570b3918c784d4490babc52fbf depends: - - __glibc >=2.28,<3.0.a0 - - libstdcxx >=14 - libgcc >=14 - - zstd >=1.5.7,<1.6.0a0 - - c-ares >=1.34.6,<2.0a0 + - libstdcxx >=14 + - __glibc >=2.28,<3.0.a0 + - libnghttp2 >=1.68.1,<2.0a0 - libuv >=1.51.0,<2.0a0 - - libsqlite >=3.51.1,<4.0a0 - - libnghttp2 >=1.67.0,<2.0a0 - - openssl >=3.5.4,<4.0a0 - - libabseil >=20250512.1,<20250513.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* - - libzlib >=1.3.1,<2.0a0 + - 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 - - icu >=75.1,<76.0a0 license: MIT - license_family: MIT purls: [] - size: 17246248 - timestamp: 1765444698486 -- conda: https://conda.anaconda.org/conda-forge/osx-64/nodejs-25.2.1-h5523da6_1.conda - sha256: 25ade898cb9e6f26622cc563dab89810f59e898e37ec4ffabd079f9f9a068998 - md5: 18ce8107e5d71b65aaa585c238a9e90d + size: 18829340 + timestamp: 1774514313036 +- conda: https://conda.anaconda.org/conda-forge/osx-64/nodejs-25.8.2-hf3170e9_0.conda + sha256: 6e82ed9c2de2e5a472a9c25f7a4a3a296d33aa38b94151acbbb5a28754962d8d + md5: de36be6257a15d17e85c96b47d290f82 depends: - - __osx >=11.0 - libcxx >=19 - - libsqlite >=3.51.1,<4.0a0 - - libabseil >=20250512.1,<20250513.0a0 + - __osx >=11.0 + - libzlib >=1.3.2,<2.0a0 + - libnghttp2 >=1.68.1,<2.0a0 + - libabseil >=20260107.1,<20260108.0a0 - libabseil * cxx17* - - zstd >=1.5.7,<1.6.0a0 + - 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.67.0,<2.0a0 - - openssl >=3.5.4,<4.0a0 + - libsqlite >=3.52.0,<4.0a0 - libuv >=1.51.0,<2.0a0 - c-ares >=1.34.6,<2.0a0 - - icu >=75.1,<76.0a0 - - libzlib >=1.3.1,<2.0a0 + - openssl >=3.5.5,<4.0a0 + - zstd >=1.5.7,<1.6.0a0 license: MIT - license_family: MIT purls: [] - size: 16923801 - timestamp: 1765444650323 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/nodejs-25.2.1-h5230ea7_1.conda - sha256: acb4a33a096fa89d0ec0eea5d5f19988594d4e5c8d482ac60d2b0365d16dd984 - md5: 0b6dfe96bcfb469afe82885b3fecbd56 + size: 18382168 + timestamp: 1774517889949 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/nodejs-25.8.2-h7039424_0.conda + sha256: 4782b172b3b8a557b60bf5f591821cf100e2092ba7a5494ce047dfa41626de26 + md5: ca8277c52fdface8bb8ebff7cd9a6f56 depends: - - __osx >=11.0 - libcxx >=19 - - libsqlite >=3.51.1,<4.0a0 + - __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 - - openssl >=3.5.4,<4.0a0 - - c-ares >=1.34.6,<2.0a0 - - icu >=75.1,<76.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 - - libabseil >=20250512.1,<20250513.0a0 + - c-ares >=1.34.6,<2.0a0 + - libabseil >=20260107.1,<20260108.0a0 - libabseil * cxx17* - - libnghttp2 >=1.67.0,<2.0a0 - - libuv >=1.51.0,<2.0a0 - - libzlib >=1.3.1,<2.0a0 license: MIT - license_family: MIT purls: [] - size: 16202237 - timestamp: 1765482731453 -- conda: https://conda.anaconda.org/conda-forge/win-64/nodejs-25.2.1-he453025_1.conda - sha256: 9742d28cf4a171dc9898bfb3c8512858f1ed46aa3cbc26d8839003d879564beb - md5: 461d47b472740c68ec0771c8b759868b + size: 17101803 + timestamp: 1774517834028 +- 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: 30449097 - timestamp: 1765444649904 + size: 31271315 + timestamp: 1774517904472 - pypi: https://files.pythonhosted.org/packages/f9/33/bd5b9137445ea4b680023eb0469b2bb969d61303dedb2aac6560ff3d14a1/notebook_shim-0.2.4-py3-none-any.whl name: notebook-shim version: 0.2.4 @@ -7647,49 +8699,49 @@ packages: - pytest-jupyter ; extra == 'test' - pytest-tornasync ; extra == 'test' requires_python: '>=3.7' -- pypi: https://files.pythonhosted.org/packages/0c/88/e2eaa6cffb115b85ed7c7c87775cb8bcf0816816bc98ca8dbfa2ee33fe6e/numpy-2.3.5-cp313-cp313-win_amd64.whl +- pypi: https://files.pythonhosted.org/packages/2b/09/3c4abbc1dcd8010bf1a611d174c7aa689fc505585ec806111b4406f6f1b1/numpy-2.4.3-cp311-cp311-macosx_14_0_x86_64.whl name: numpy - version: 2.3.5 - sha256: 00dc4e846108a382c5869e77c6ed514394bdeb3403461d25a829711041217d5b + version: 2.4.3 + sha256: 23b46bb6d8ecb68b58c09944483c135ae5f0e9b8d8858ece5e4ead783771d2a9 requires_python: '>=3.11' -- pypi: https://files.pythonhosted.org/packages/2a/ea/25e26fa5837106cde46ae7d0b667e20f69cbbc0efd64cba8221411ab26ae/numpy-2.3.5-cp311-cp311-macosx_11_0_arm64.whl +- pypi: https://files.pythonhosted.org/packages/3a/66/bd096b13a87549683812b53ab211e6d413497f84e794fb3c39191948da97/numpy-2.4.3-cp313-cp313-macosx_14_0_arm64.whl name: numpy - version: 2.3.5 - sha256: acfd89508504a19ed06ef963ad544ec6664518c863436306153e13e94605c218 + version: 2.4.3 + sha256: bb2e3cf95854233799013779216c57e153c1ee67a0bf92138acca0e429aefaee requires_python: '>=3.11' -- pypi: https://files.pythonhosted.org/packages/43/77/84dd1d2e34d7e2792a236ba180b5e8fcc1e3e414e761ce0253f63d7f572e/numpy-2.3.5-cp311-cp311-macosx_10_9_x86_64.whl +- pypi: https://files.pythonhosted.org/packages/76/1d/edccf27adedb754db7c4511d5eac8b83f004ae948fe2d3509e8b78097d4c/numpy-2.4.3-cp311-cp311-win_amd64.whl name: numpy - version: 2.3.5 - sha256: de5672f4a7b200c15a4127042170a694d4df43c992948f5e1af57f0174beed10 + version: 2.4.3 + sha256: 77e76d932c49a75617c6d13464e41203cd410956614d0a0e999b25e9e8d27eec requires_python: '>=3.11' -- pypi: https://files.pythonhosted.org/packages/65/fb/2b23769462b34398d9326081fad5655198fcf18966fcb1f1e49db44fbf31/numpy-2.3.5-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl +- pypi: https://files.pythonhosted.org/packages/78/51/9f5d7a41f0b51649ddf2f2320595e15e122a40610b233d51928dd6c92353/numpy-2.4.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl name: numpy - version: 2.3.5 - sha256: 8cba086a43d54ca804ce711b2a940b16e452807acebe7852ff327f1ecd49b0d4 + version: 2.4.3 + sha256: 715de7f82e192e8cae5a507a347d97ad17598f8e026152ca97233e3666daaa71 requires_python: '>=3.11' -- pypi: https://files.pythonhosted.org/packages/79/fb/f505c95ceddd7027347b067689db71ca80bd5ecc926f913f1a23e65cf09b/numpy-2.3.5-cp313-cp313-macosx_11_0_arm64.whl +- pypi: https://files.pythonhosted.org/packages/7e/eb/7daecbea84ec935b7fc732e18f532073064a3816f0932a40a17f3349185f/numpy-2.4.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl name: numpy - version: 2.3.5 - sha256: aa5bc7c5d59d831d9773d1170acac7893ce3a5e130540605770ade83280e7188 + version: 2.4.3 + sha256: d5f51900414fc9204a0e0da158ba2ac52b75656e7dce7e77fb9f84bfa343b4cc requires_python: '>=3.11' -- pypi: https://files.pythonhosted.org/packages/aa/44/9fe81ae1dcc29c531843852e2874080dc441338574ccc4306b39e2ff6e59/numpy-2.3.5-cp311-cp311-win_amd64.whl +- pypi: https://files.pythonhosted.org/packages/a2/2f/687722910b5a5601de2135c891108f51dfc873d8e43c8ed9f4ebb440b4a2/numpy-2.4.3-cp313-cp313-macosx_14_0_x86_64.whl name: numpy - version: 2.3.5 - sha256: a414504bef8945eae5f2d7cb7be2d4af77c5d1cb5e20b296c2c25b61dff2900c + version: 2.4.3 + sha256: 7f3408ff897f8ab07a07fbe2823d7aee6ff644c097cc1f90382511fe982f647f requires_python: '>=3.11' -- pypi: https://files.pythonhosted.org/packages/db/69/9cde09f36da4b5a505341180a3f2e6fadc352fd4d2b7096ce9778db83f1a/numpy-2.3.5-cp313-cp313-macosx_10_13_x86_64.whl +- pypi: https://files.pythonhosted.org/packages/b1/3c/88af0040119209b9b5cb59485fa48b76f372c73068dbf9254784b975ac53/numpy-2.4.3-cp313-cp313-win_amd64.whl name: numpy - version: 2.3.5 - sha256: d0f23b44f57077c1ede8c5f26b30f706498b4862d3ff0a7298b8411dd2f043ff + version: 2.4.3 + sha256: 0a60e17a14d640f49146cb38e3f105f571318db7826d9b6fef7e4dce758faecd requires_python: '>=3.11' -- pypi: https://files.pythonhosted.org/packages/f5/10/ca162f45a102738958dcec8023062dad0cbc17d1ab99d68c4e4a6c45fb2b/numpy-2.3.5-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl +- pypi: https://files.pythonhosted.org/packages/ef/27/d26c85cbcd86b26e4f125b0668e7a7c0542d19dd7d23ee12e87b550e95b5/numpy-2.4.3-cp311-cp311-macosx_14_0_arm64.whl name: numpy - version: 2.3.5 - sha256: 11e06aa0af8c0f05104d56450d6093ee639e15f24ecf62d417329d06e522e017 + version: 2.4.3 + sha256: a1988292870c7cb9d0ebb4cc96b4d447513a9644801de54606dc7aabf2b7d920 requires_python: '>=3.11' -- conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.0-h26f9b46_0.conda - sha256: a47271202f4518a484956968335b2521409c8173e123ab381e775c358c67fe6d - md5: 9ee58d5c534af06558933af3c845a780 +- conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.1-h35e630c_1.conda + sha256: 44c877f8af015332a5d12f5ff0fb20ca32f896526a7d0cdb30c769df1144fb5c + md5: f61eb8cd60ff9057122a3d338b99c00f depends: - __glibc >=2.17,<3.0.a0 - ca-certificates @@ -7697,33 +8749,33 @@ packages: license: Apache-2.0 license_family: Apache purls: [] - size: 3165399 - timestamp: 1762839186699 -- conda: https://conda.anaconda.org/conda-forge/osx-64/openssl-3.6.0-h230baf5_0.conda - sha256: 36fe9fb316be22fcfb46d5fa3e2e85eec5ef84f908b7745f68f768917235b2d5 - md5: 3f50cdf9a97d0280655758b735781096 + size: 3164551 + timestamp: 1769555830639 +- conda: https://conda.anaconda.org/conda-forge/osx-64/openssl-3.6.1-hb6871ef_1.conda + sha256: e02e5639b0e4d6d4fcf0f3b082642844fb5a37316f5b0a1126c6271347462e90 + md5: 30bb8d08b99b9a7600d39efb3559fff0 depends: - __osx >=10.13 - ca-certificates license: Apache-2.0 license_family: Apache purls: [] - size: 2778996 - timestamp: 1762840724922 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.6.0-h5503f6c_0.conda - sha256: ebe93dafcc09e099782fe3907485d4e1671296bc14f8c383cb6f3dfebb773988 - md5: b34dc4172653c13dcf453862f251af2b + size: 2777136 + timestamp: 1769557662405 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.6.1-hd24854e_1.conda + sha256: 361f5c5e60052abc12bdd1b50d7a1a43e6a6653aab99a2263bf2288d709dcf67 + md5: f4f6ad63f98f64191c3e77c5f5f29d76 depends: - __osx >=11.0 - ca-certificates license: Apache-2.0 license_family: Apache purls: [] - size: 3108371 - timestamp: 1762839712322 -- conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.6.0-h725018a_0.conda - sha256: 6d72d6f766293d4f2aa60c28c244c8efed6946c430814175f959ffe8cab899b3 - md5: 84f8fb4afd1157f59098f618cd2437e4 + size: 3104268 + timestamp: 1769556384749 +- conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.6.1-hf411b9b_1.conda + sha256: 53a5ad2e5553b8157a91bb8aa375f78c5958f77cb80e9d2ce59471ea8e5c0bd6 + md5: eb585509b815415bc964b2c7e11c7eb3 depends: - ca-certificates - ucrt >=10.0.20348.0 @@ -7732,8 +8784,8 @@ packages: license: Apache-2.0 license_family: Apache purls: [] - size: 9440812 - timestamp: 1762841722179 + size: 9343023 + timestamp: 1769557547888 - pypi: https://files.pythonhosted.org/packages/2c/ab/fc8290c6a4c722e5514d80f62b2dc4c4df1a68a41d1364e625c35990fcf3/overrides-7.7.0-py3-none-any.whl name: overrides version: 7.7.0 @@ -7741,10 +8793,10 @@ packages: requires_dist: - typing ; python_full_version < '3.5' requires_python: '>=3.6' -- pypi: https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl name: packaging - version: '25.0' - sha256: 29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484 + version: '26.0' + sha256: b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529 requires_python: '>=3.8' - pypi: https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl name: paginate @@ -7754,754 +8806,758 @@ packages: - pytest ; extra == 'dev' - tox ; extra == 'dev' - black ; extra == 'lint' -- pypi: https://files.pythonhosted.org/packages/15/07/284f757f63f8a8d69ed4472bfd85122bd086e637bf4ed09de572d575a693/pandas-2.3.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl +- pypi: https://files.pythonhosted.org/packages/0b/48/aad6ec4f8d007534c091e9a7172b3ec1b1ee6d99a9cbb936b5eab6c6cf58/pandas-3.0.1-cp313-cp313-macosx_10_13_x86_64.whl name: pandas - version: 2.3.3 - sha256: 318d77e0e42a628c04dc56bcef4b40de67918f7041c2b061af1da41dcff670ac + version: 3.0.1 + sha256: 5272627187b5d9c20e55d27caf5f2cd23e286aba25cadf73c8590e432e2b7262 requires_dist: - - numpy>=1.22.4 ; python_full_version < '3.11' - - numpy>=1.23.2 ; python_full_version == '3.11.*' - - numpy>=1.26.0 ; python_full_version >= '3.12' + - numpy>=1.26.0 ; python_full_version < '3.14' + - numpy>=2.3.3 ; python_full_version >= '3.14' - python-dateutil>=2.8.2 - - pytz>=2020.1 - - tzdata>=2022.7 - - hypothesis>=6.46.1 ; extra == 'test' - - pytest>=7.3.2 ; extra == 'test' - - pytest-xdist>=2.2.0 ; extra == 'test' - - pyarrow>=10.0.1 ; extra == 'pyarrow' - - bottleneck>=1.3.6 ; extra == 'performance' - - numba>=0.56.4 ; extra == 'performance' - - numexpr>=2.8.4 ; extra == 'performance' - - scipy>=1.10.0 ; extra == 'computation' - - xarray>=2022.12.0 ; extra == 'computation' - - fsspec>=2022.11.0 ; extra == 'fss' - - s3fs>=2022.11.0 ; extra == 'aws' - - gcsfs>=2022.11.0 ; extra == 'gcp' - - pandas-gbq>=0.19.0 ; extra == 'gcp' + - 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.0 ; extra == 'excel' - - python-calamine>=0.1.7 ; 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.0.5 ; extra == 'excel' - - pyarrow>=10.0.1 ; extra == 'parquet' - - pyarrow>=10.0.1 ; extra == 'feather' - - tables>=3.8.0 ; extra == 'hdf5' - - pyreadstat>=1.2.0 ; extra == 'spss' - - sqlalchemy>=2.0.0 ; extra == 'postgresql' - - psycopg2>=2.9.6 ; extra == 'postgresql' - - adbc-driver-postgresql>=0.8.0 ; extra == 'postgresql' - - sqlalchemy>=2.0.0 ; extra == 'mysql' - - pymysql>=1.0.2 ; extra == 'mysql' - - sqlalchemy>=2.0.0 ; extra == 'sql-other' - - adbc-driver-postgresql>=0.8.0 ; extra == 'sql-other' - - adbc-driver-sqlite>=0.8.0 ; extra == 'sql-other' - - beautifulsoup4>=4.11.2 ; extra == 'html' + - 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>=4.9.2 ; extra == 'html' - - lxml>=4.9.2 ; extra == 'xml' - - matplotlib>=3.6.3 ; extra == 'plot' - - jinja2>=3.1.2 ; extra == 'output-formatting' + - 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.3.0 ; extra == 'clipboard' - - zstandard>=0.19.0 ; extra == 'compression' - - dataframe-api-compat>=0.1.7 ; extra == 'consortium-standard' - - adbc-driver-postgresql>=0.8.0 ; extra == 'all' - - adbc-driver-sqlite>=0.8.0 ; extra == 'all' - - beautifulsoup4>=4.11.2 ; extra == 'all' - - bottleneck>=1.3.6 ; extra == 'all' - - dataframe-api-compat>=0.1.7 ; extra == 'all' - - fastparquet>=2022.12.0 ; extra == 'all' - - fsspec>=2022.11.0 ; extra == 'all' - - gcsfs>=2022.11.0 ; extra == 'all' + - 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.46.1 ; extra == 'all' - - jinja2>=3.1.2 ; extra == 'all' - - lxml>=4.9.2 ; extra == 'all' - - matplotlib>=3.6.3 ; extra == 'all' - - numba>=0.56.4 ; extra == 'all' - - numexpr>=2.8.4 ; 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.0 ; extra == 'all' - - pandas-gbq>=0.19.0 ; extra == 'all' - - psycopg2>=2.9.6 ; extra == 'all' - - pyarrow>=10.0.1 ; extra == 'all' - - pymysql>=1.0.2 ; 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.0 ; extra == 'all' - - pytest>=7.3.2 ; extra == 'all' - - pytest-xdist>=2.2.0 ; extra == 'all' - - python-calamine>=0.1.7 ; 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.3.0 ; extra == 'all' - - scipy>=1.10.0 ; extra == 'all' - - s3fs>=2022.11.0 ; extra == 'all' - - sqlalchemy>=2.0.0 ; extra == 'all' - - tables>=3.8.0 ; 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>=2022.12.0 ; extra == 'all' + - xarray>=2024.10.0 ; extra == 'all' - xlrd>=2.0.1 ; extra == 'all' - - xlsxwriter>=3.0.5 ; extra == 'all' - - zstandard>=0.19.0 ; extra == 'all' - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/31/94/72fac03573102779920099bcac1c3b05975c2cb5f01eac609faf34bed1ca/pandas-2.3.3-cp313-cp313-macosx_11_0_arm64.whl + - xlsxwriter>=3.2.0 ; extra == 'all' + - zstandard>=0.23.0 ; extra == 'all' + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/1f/67/af63f83cd6ca603a00fe8530c10a60f0879265b8be00b5930e8e78c5b30b/pandas-3.0.1-cp311-cp311-win_amd64.whl name: pandas - version: 2.3.3 - sha256: bdcd9d1167f4885211e401b3036c0c8d9e274eee67ea8d0758a256d60704cfe8 + version: 3.0.1 + sha256: 84f0904a69e7365f79a0c77d3cdfccbfb05bf87847e3a51a41e1426b0edb9c79 requires_dist: - - numpy>=1.22.4 ; python_full_version < '3.11' - - numpy>=1.23.2 ; python_full_version == '3.11.*' - - numpy>=1.26.0 ; python_full_version >= '3.12' + - numpy>=1.26.0 ; python_full_version < '3.14' + - numpy>=2.3.3 ; python_full_version >= '3.14' - python-dateutil>=2.8.2 - - pytz>=2020.1 - - tzdata>=2022.7 - - hypothesis>=6.46.1 ; extra == 'test' - - pytest>=7.3.2 ; extra == 'test' - - pytest-xdist>=2.2.0 ; extra == 'test' - - pyarrow>=10.0.1 ; extra == 'pyarrow' - - bottleneck>=1.3.6 ; extra == 'performance' - - numba>=0.56.4 ; extra == 'performance' - - numexpr>=2.8.4 ; extra == 'performance' - - scipy>=1.10.0 ; extra == 'computation' - - xarray>=2022.12.0 ; extra == 'computation' - - fsspec>=2022.11.0 ; extra == 'fss' - - s3fs>=2022.11.0 ; extra == 'aws' - - gcsfs>=2022.11.0 ; extra == 'gcp' - - pandas-gbq>=0.19.0 ; extra == 'gcp' + - 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.0 ; extra == 'excel' - - python-calamine>=0.1.7 ; 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.0.5 ; extra == 'excel' - - pyarrow>=10.0.1 ; extra == 'parquet' - - pyarrow>=10.0.1 ; extra == 'feather' - - tables>=3.8.0 ; extra == 'hdf5' - - pyreadstat>=1.2.0 ; extra == 'spss' - - sqlalchemy>=2.0.0 ; extra == 'postgresql' - - psycopg2>=2.9.6 ; extra == 'postgresql' - - adbc-driver-postgresql>=0.8.0 ; extra == 'postgresql' - - sqlalchemy>=2.0.0 ; extra == 'mysql' - - pymysql>=1.0.2 ; extra == 'mysql' - - sqlalchemy>=2.0.0 ; extra == 'sql-other' - - adbc-driver-postgresql>=0.8.0 ; extra == 'sql-other' - - adbc-driver-sqlite>=0.8.0 ; extra == 'sql-other' - - beautifulsoup4>=4.11.2 ; extra == 'html' + - 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>=4.9.2 ; extra == 'html' - - lxml>=4.9.2 ; extra == 'xml' - - matplotlib>=3.6.3 ; extra == 'plot' - - jinja2>=3.1.2 ; extra == 'output-formatting' + - 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.3.0 ; extra == 'clipboard' - - zstandard>=0.19.0 ; extra == 'compression' - - dataframe-api-compat>=0.1.7 ; extra == 'consortium-standard' - - adbc-driver-postgresql>=0.8.0 ; extra == 'all' - - adbc-driver-sqlite>=0.8.0 ; extra == 'all' - - beautifulsoup4>=4.11.2 ; extra == 'all' - - bottleneck>=1.3.6 ; extra == 'all' - - dataframe-api-compat>=0.1.7 ; extra == 'all' - - fastparquet>=2022.12.0 ; extra == 'all' - - fsspec>=2022.11.0 ; extra == 'all' - - gcsfs>=2022.11.0 ; extra == 'all' + - 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.46.1 ; extra == 'all' - - jinja2>=3.1.2 ; extra == 'all' - - lxml>=4.9.2 ; extra == 'all' - - matplotlib>=3.6.3 ; extra == 'all' - - numba>=0.56.4 ; extra == 'all' - - numexpr>=2.8.4 ; 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.0 ; extra == 'all' - - pandas-gbq>=0.19.0 ; extra == 'all' - - psycopg2>=2.9.6 ; extra == 'all' - - pyarrow>=10.0.1 ; extra == 'all' - - pymysql>=1.0.2 ; 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.0 ; extra == 'all' - - pytest>=7.3.2 ; extra == 'all' - - pytest-xdist>=2.2.0 ; extra == 'all' - - python-calamine>=0.1.7 ; 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.3.0 ; extra == 'all' - - scipy>=1.10.0 ; extra == 'all' - - s3fs>=2022.11.0 ; extra == 'all' - - sqlalchemy>=2.0.0 ; extra == 'all' - - tables>=3.8.0 ; 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>=2022.12.0 ; extra == 'all' + - xarray>=2024.10.0 ; extra == 'all' - xlrd>=2.0.1 ; extra == 'all' - - xlsxwriter>=3.0.5 ; extra == 'all' - - zstandard>=0.19.0 ; extra == 'all' - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/4f/c7/e54682c96a895d0c808453269e0b5928a07a127a15704fedb643e9b0a4c8/pandas-2.3.3-cp313-cp313-win_amd64.whl + - xlsxwriter>=3.2.0 ; extra == 'all' + - zstandard>=0.23.0 ; extra == 'all' + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/2e/7c/870c7e7daec2a6c7ff2ac9e33b23317230d4e4e954b35112759ea4a924a7/pandas-3.0.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl name: pandas - version: 2.3.3 - sha256: f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee + version: 3.0.1 + sha256: 830994d7e1f31dd7e790045235605ab61cff6c94defc774547e8b7fdfbff3dc7 requires_dist: - - numpy>=1.22.4 ; python_full_version < '3.11' - - numpy>=1.23.2 ; python_full_version == '3.11.*' - - numpy>=1.26.0 ; python_full_version >= '3.12' + - numpy>=1.26.0 ; python_full_version < '3.14' + - numpy>=2.3.3 ; python_full_version >= '3.14' - python-dateutil>=2.8.2 - - pytz>=2020.1 - - tzdata>=2022.7 - - hypothesis>=6.46.1 ; extra == 'test' - - pytest>=7.3.2 ; extra == 'test' - - pytest-xdist>=2.2.0 ; extra == 'test' - - pyarrow>=10.0.1 ; extra == 'pyarrow' - - bottleneck>=1.3.6 ; extra == 'performance' - - numba>=0.56.4 ; extra == 'performance' - - numexpr>=2.8.4 ; extra == 'performance' - - scipy>=1.10.0 ; extra == 'computation' - - xarray>=2022.12.0 ; extra == 'computation' - - fsspec>=2022.11.0 ; extra == 'fss' - - s3fs>=2022.11.0 ; extra == 'aws' - - gcsfs>=2022.11.0 ; extra == 'gcp' - - pandas-gbq>=0.19.0 ; extra == 'gcp' + - 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.0 ; extra == 'excel' - - python-calamine>=0.1.7 ; 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.0.5 ; extra == 'excel' - - pyarrow>=10.0.1 ; extra == 'parquet' - - pyarrow>=10.0.1 ; extra == 'feather' - - tables>=3.8.0 ; extra == 'hdf5' - - pyreadstat>=1.2.0 ; extra == 'spss' - - sqlalchemy>=2.0.0 ; extra == 'postgresql' - - psycopg2>=2.9.6 ; extra == 'postgresql' - - adbc-driver-postgresql>=0.8.0 ; extra == 'postgresql' - - sqlalchemy>=2.0.0 ; extra == 'mysql' - - pymysql>=1.0.2 ; extra == 'mysql' - - sqlalchemy>=2.0.0 ; extra == 'sql-other' - - adbc-driver-postgresql>=0.8.0 ; extra == 'sql-other' - - adbc-driver-sqlite>=0.8.0 ; extra == 'sql-other' - - beautifulsoup4>=4.11.2 ; extra == 'html' + - 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>=4.9.2 ; extra == 'html' - - lxml>=4.9.2 ; extra == 'xml' - - matplotlib>=3.6.3 ; extra == 'plot' - - jinja2>=3.1.2 ; extra == 'output-formatting' + - 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.3.0 ; extra == 'clipboard' - - zstandard>=0.19.0 ; extra == 'compression' - - dataframe-api-compat>=0.1.7 ; extra == 'consortium-standard' - - adbc-driver-postgresql>=0.8.0 ; extra == 'all' - - adbc-driver-sqlite>=0.8.0 ; extra == 'all' - - beautifulsoup4>=4.11.2 ; extra == 'all' - - bottleneck>=1.3.6 ; extra == 'all' - - dataframe-api-compat>=0.1.7 ; extra == 'all' - - fastparquet>=2022.12.0 ; extra == 'all' - - fsspec>=2022.11.0 ; extra == 'all' - - gcsfs>=2022.11.0 ; extra == 'all' + - 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.46.1 ; extra == 'all' - - jinja2>=3.1.2 ; extra == 'all' - - lxml>=4.9.2 ; extra == 'all' - - matplotlib>=3.6.3 ; extra == 'all' - - numba>=0.56.4 ; extra == 'all' - - numexpr>=2.8.4 ; 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.0 ; extra == 'all' - - pandas-gbq>=0.19.0 ; extra == 'all' - - psycopg2>=2.9.6 ; extra == 'all' - - pyarrow>=10.0.1 ; extra == 'all' - - pymysql>=1.0.2 ; 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.0 ; extra == 'all' - - pytest>=7.3.2 ; extra == 'all' - - pytest-xdist>=2.2.0 ; extra == 'all' - - python-calamine>=0.1.7 ; 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.3.0 ; extra == 'all' - - scipy>=1.10.0 ; extra == 'all' - - s3fs>=2022.11.0 ; extra == 'all' - - sqlalchemy>=2.0.0 ; extra == 'all' - - tables>=3.8.0 ; 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>=2022.12.0 ; extra == 'all' + - xarray>=2024.10.0 ; extra == 'all' - xlrd>=2.0.1 ; extra == 'all' - - xlsxwriter>=3.0.5 ; extra == 'all' - - zstandard>=0.19.0 ; extra == 'all' - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/8e/59/712db1d7040520de7a4965df15b774348980e6df45c129b8c64d0dbe74ef/pandas-2.3.3-cp311-cp311-win_amd64.whl + - xlsxwriter>=3.2.0 ; extra == 'all' + - zstandard>=0.23.0 ; extra == 'all' + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/a8/14/5990826f779f79148ae9d3a2c39593dc04d61d5d90541e71b5749f35af95/pandas-3.0.1-cp313-cp313-macosx_11_0_arm64.whl name: pandas - version: 2.3.3 - sha256: f086f6fe114e19d92014a1966f43a3e62285109afe874f067f5abbdcbb10e59c + version: 3.0.1 + sha256: 661e0f665932af88c7877f31da0dc743fe9c8f2524bdffe23d24fdcb67ef9d56 requires_dist: - - numpy>=1.22.4 ; python_full_version < '3.11' - - numpy>=1.23.2 ; python_full_version == '3.11.*' - - numpy>=1.26.0 ; python_full_version >= '3.12' + - numpy>=1.26.0 ; python_full_version < '3.14' + - numpy>=2.3.3 ; python_full_version >= '3.14' - python-dateutil>=2.8.2 - - pytz>=2020.1 - - tzdata>=2022.7 - - hypothesis>=6.46.1 ; extra == 'test' - - pytest>=7.3.2 ; extra == 'test' - - pytest-xdist>=2.2.0 ; extra == 'test' - - pyarrow>=10.0.1 ; extra == 'pyarrow' - - bottleneck>=1.3.6 ; extra == 'performance' - - numba>=0.56.4 ; extra == 'performance' - - numexpr>=2.8.4 ; extra == 'performance' - - scipy>=1.10.0 ; extra == 'computation' - - xarray>=2022.12.0 ; extra == 'computation' - - fsspec>=2022.11.0 ; extra == 'fss' - - s3fs>=2022.11.0 ; extra == 'aws' - - gcsfs>=2022.11.0 ; extra == 'gcp' - - pandas-gbq>=0.19.0 ; extra == 'gcp' + - 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.0 ; extra == 'excel' - - python-calamine>=0.1.7 ; 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.0.5 ; extra == 'excel' - - pyarrow>=10.0.1 ; extra == 'parquet' - - pyarrow>=10.0.1 ; extra == 'feather' - - tables>=3.8.0 ; extra == 'hdf5' - - pyreadstat>=1.2.0 ; extra == 'spss' - - sqlalchemy>=2.0.0 ; extra == 'postgresql' - - psycopg2>=2.9.6 ; extra == 'postgresql' - - adbc-driver-postgresql>=0.8.0 ; extra == 'postgresql' - - sqlalchemy>=2.0.0 ; extra == 'mysql' - - pymysql>=1.0.2 ; extra == 'mysql' - - sqlalchemy>=2.0.0 ; extra == 'sql-other' - - adbc-driver-postgresql>=0.8.0 ; extra == 'sql-other' - - adbc-driver-sqlite>=0.8.0 ; extra == 'sql-other' - - beautifulsoup4>=4.11.2 ; extra == 'html' + - 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>=4.9.2 ; extra == 'html' - - lxml>=4.9.2 ; extra == 'xml' - - matplotlib>=3.6.3 ; extra == 'plot' - - jinja2>=3.1.2 ; extra == 'output-formatting' + - 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.3.0 ; extra == 'clipboard' - - zstandard>=0.19.0 ; extra == 'compression' - - dataframe-api-compat>=0.1.7 ; extra == 'consortium-standard' - - adbc-driver-postgresql>=0.8.0 ; extra == 'all' - - adbc-driver-sqlite>=0.8.0 ; extra == 'all' - - beautifulsoup4>=4.11.2 ; extra == 'all' - - bottleneck>=1.3.6 ; extra == 'all' - - dataframe-api-compat>=0.1.7 ; extra == 'all' - - fastparquet>=2022.12.0 ; extra == 'all' - - fsspec>=2022.11.0 ; extra == 'all' - - gcsfs>=2022.11.0 ; extra == 'all' + - 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.46.1 ; extra == 'all' - - jinja2>=3.1.2 ; extra == 'all' - - lxml>=4.9.2 ; extra == 'all' - - matplotlib>=3.6.3 ; extra == 'all' - - numba>=0.56.4 ; extra == 'all' - - numexpr>=2.8.4 ; 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.0 ; extra == 'all' - - pandas-gbq>=0.19.0 ; extra == 'all' - - psycopg2>=2.9.6 ; extra == 'all' - - pyarrow>=10.0.1 ; extra == 'all' - - pymysql>=1.0.2 ; 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.0 ; extra == 'all' - - pytest>=7.3.2 ; extra == 'all' - - pytest-xdist>=2.2.0 ; extra == 'all' - - python-calamine>=0.1.7 ; 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.3.0 ; extra == 'all' - - scipy>=1.10.0 ; extra == 'all' - - s3fs>=2022.11.0 ; extra == 'all' - - sqlalchemy>=2.0.0 ; extra == 'all' - - tables>=3.8.0 ; 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>=2022.12.0 ; extra == 'all' + - xarray>=2024.10.0 ; extra == 'all' - xlrd>=2.0.1 ; extra == 'all' - - xlsxwriter>=3.0.5 ; extra == 'all' - - zstandard>=0.19.0 ; extra == 'all' - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/9b/35/74442388c6cf008882d4d4bdfc4109be87e9b8b7ccd097ad1e7f006e2e95/pandas-2.3.3-cp311-cp311-macosx_11_0_arm64.whl + - xlsxwriter>=3.2.0 ; extra == 'all' + - zstandard>=0.23.0 ; extra == 'all' + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/c1/27/90683c7122febeefe84a56f2cde86a9f05f68d53885cebcc473298dfc33e/pandas-3.0.1-cp311-cp311-macosx_11_0_arm64.whl name: pandas - version: 2.3.3 - sha256: 8fe25fc7b623b0ef6b5009149627e34d2a4657e880948ec3c840e9402e5c1b45 + version: 3.0.1 + sha256: 24ba315ba3d6e5806063ac6eb717504e499ce30bd8c236d8693a5fd3f084c796 requires_dist: - - numpy>=1.22.4 ; python_full_version < '3.11' - - numpy>=1.23.2 ; python_full_version == '3.11.*' - - numpy>=1.26.0 ; python_full_version >= '3.12' + - numpy>=1.26.0 ; python_full_version < '3.14' + - numpy>=2.3.3 ; python_full_version >= '3.14' - python-dateutil>=2.8.2 - - pytz>=2020.1 - - tzdata>=2022.7 - - hypothesis>=6.46.1 ; extra == 'test' - - pytest>=7.3.2 ; extra == 'test' - - pytest-xdist>=2.2.0 ; extra == 'test' - - pyarrow>=10.0.1 ; extra == 'pyarrow' - - bottleneck>=1.3.6 ; extra == 'performance' - - numba>=0.56.4 ; extra == 'performance' - - numexpr>=2.8.4 ; extra == 'performance' - - scipy>=1.10.0 ; extra == 'computation' - - xarray>=2022.12.0 ; extra == 'computation' - - fsspec>=2022.11.0 ; extra == 'fss' - - s3fs>=2022.11.0 ; extra == 'aws' - - gcsfs>=2022.11.0 ; extra == 'gcp' - - pandas-gbq>=0.19.0 ; extra == 'gcp' + - 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.0 ; extra == 'excel' - - python-calamine>=0.1.7 ; 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.0.5 ; extra == 'excel' - - pyarrow>=10.0.1 ; extra == 'parquet' - - pyarrow>=10.0.1 ; extra == 'feather' - - tables>=3.8.0 ; extra == 'hdf5' - - pyreadstat>=1.2.0 ; extra == 'spss' - - sqlalchemy>=2.0.0 ; extra == 'postgresql' - - psycopg2>=2.9.6 ; extra == 'postgresql' - - adbc-driver-postgresql>=0.8.0 ; extra == 'postgresql' - - sqlalchemy>=2.0.0 ; extra == 'mysql' - - pymysql>=1.0.2 ; extra == 'mysql' - - sqlalchemy>=2.0.0 ; extra == 'sql-other' - - adbc-driver-postgresql>=0.8.0 ; extra == 'sql-other' - - adbc-driver-sqlite>=0.8.0 ; extra == 'sql-other' - - beautifulsoup4>=4.11.2 ; extra == 'html' + - 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>=4.9.2 ; extra == 'html' - - lxml>=4.9.2 ; extra == 'xml' - - matplotlib>=3.6.3 ; extra == 'plot' - - jinja2>=3.1.2 ; extra == 'output-formatting' + - 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.3.0 ; extra == 'clipboard' - - zstandard>=0.19.0 ; extra == 'compression' - - dataframe-api-compat>=0.1.7 ; extra == 'consortium-standard' - - adbc-driver-postgresql>=0.8.0 ; extra == 'all' - - adbc-driver-sqlite>=0.8.0 ; extra == 'all' - - beautifulsoup4>=4.11.2 ; extra == 'all' - - bottleneck>=1.3.6 ; extra == 'all' - - dataframe-api-compat>=0.1.7 ; extra == 'all' - - fastparquet>=2022.12.0 ; extra == 'all' - - fsspec>=2022.11.0 ; extra == 'all' - - gcsfs>=2022.11.0 ; extra == 'all' + - 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.46.1 ; extra == 'all' - - jinja2>=3.1.2 ; extra == 'all' - - lxml>=4.9.2 ; extra == 'all' - - matplotlib>=3.6.3 ; extra == 'all' - - numba>=0.56.4 ; extra == 'all' - - numexpr>=2.8.4 ; 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.0 ; extra == 'all' - - pandas-gbq>=0.19.0 ; extra == 'all' - - psycopg2>=2.9.6 ; extra == 'all' - - pyarrow>=10.0.1 ; extra == 'all' - - pymysql>=1.0.2 ; 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.0 ; extra == 'all' - - pytest>=7.3.2 ; extra == 'all' - - pytest-xdist>=2.2.0 ; extra == 'all' - - python-calamine>=0.1.7 ; 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.3.0 ; extra == 'all' - - scipy>=1.10.0 ; extra == 'all' - - s3fs>=2022.11.0 ; extra == 'all' - - sqlalchemy>=2.0.0 ; extra == 'all' - - tables>=3.8.0 ; 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>=2022.12.0 ; extra == 'all' + - xarray>=2024.10.0 ; extra == 'all' - xlrd>=2.0.1 ; extra == 'all' - - xlsxwriter>=3.0.5 ; extra == 'all' - - zstandard>=0.19.0 ; extra == 'all' - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/bf/c9/63f8d545568d9ab91476b1818b4741f521646cbdd151c6efebf40d6de6f7/pandas-2.3.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl + - xlsxwriter>=3.2.0 ; extra == 'all' + - zstandard>=0.23.0 ; extra == 'all' + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/d6/7d/216a1588b65a7aa5f4535570418a599d943c85afb1d95b0876fc00aa1468/pandas-3.0.1-cp313-cp313-win_amd64.whl name: pandas - version: 2.3.3 - sha256: b98560e98cb334799c0b07ca7967ac361a47326e9b4e5a7dfb5ab2b1c9d35a1b + version: 3.0.1 + sha256: 9fea306c783e28884c29057a1d9baa11a349bbf99538ec1da44c8476563d1b25 requires_dist: - - numpy>=1.22.4 ; python_full_version < '3.11' - - numpy>=1.23.2 ; python_full_version == '3.11.*' - - numpy>=1.26.0 ; python_full_version >= '3.12' + - numpy>=1.26.0 ; python_full_version < '3.14' + - numpy>=2.3.3 ; python_full_version >= '3.14' - python-dateutil>=2.8.2 - - pytz>=2020.1 - - tzdata>=2022.7 - - hypothesis>=6.46.1 ; extra == 'test' - - pytest>=7.3.2 ; extra == 'test' - - pytest-xdist>=2.2.0 ; extra == 'test' - - pyarrow>=10.0.1 ; extra == 'pyarrow' - - bottleneck>=1.3.6 ; extra == 'performance' - - numba>=0.56.4 ; extra == 'performance' - - numexpr>=2.8.4 ; extra == 'performance' - - scipy>=1.10.0 ; extra == 'computation' - - xarray>=2022.12.0 ; extra == 'computation' - - fsspec>=2022.11.0 ; extra == 'fss' - - s3fs>=2022.11.0 ; extra == 'aws' - - gcsfs>=2022.11.0 ; extra == 'gcp' - - pandas-gbq>=0.19.0 ; extra == 'gcp' + - 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.0 ; extra == 'excel' - - python-calamine>=0.1.7 ; 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.0.5 ; extra == 'excel' - - pyarrow>=10.0.1 ; extra == 'parquet' - - pyarrow>=10.0.1 ; extra == 'feather' - - tables>=3.8.0 ; extra == 'hdf5' - - pyreadstat>=1.2.0 ; extra == 'spss' - - sqlalchemy>=2.0.0 ; extra == 'postgresql' - - psycopg2>=2.9.6 ; extra == 'postgresql' - - adbc-driver-postgresql>=0.8.0 ; extra == 'postgresql' - - sqlalchemy>=2.0.0 ; extra == 'mysql' - - pymysql>=1.0.2 ; extra == 'mysql' - - sqlalchemy>=2.0.0 ; extra == 'sql-other' - - adbc-driver-postgresql>=0.8.0 ; extra == 'sql-other' - - adbc-driver-sqlite>=0.8.0 ; extra == 'sql-other' - - beautifulsoup4>=4.11.2 ; extra == 'html' + - 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>=4.9.2 ; extra == 'html' - - lxml>=4.9.2 ; extra == 'xml' - - matplotlib>=3.6.3 ; extra == 'plot' - - jinja2>=3.1.2 ; extra == 'output-formatting' + - 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.3.0 ; extra == 'clipboard' - - zstandard>=0.19.0 ; extra == 'compression' - - dataframe-api-compat>=0.1.7 ; extra == 'consortium-standard' - - adbc-driver-postgresql>=0.8.0 ; extra == 'all' - - adbc-driver-sqlite>=0.8.0 ; extra == 'all' - - beautifulsoup4>=4.11.2 ; extra == 'all' - - bottleneck>=1.3.6 ; extra == 'all' - - dataframe-api-compat>=0.1.7 ; extra == 'all' - - fastparquet>=2022.12.0 ; extra == 'all' - - fsspec>=2022.11.0 ; extra == 'all' - - gcsfs>=2022.11.0 ; extra == 'all' + - 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.46.1 ; extra == 'all' - - jinja2>=3.1.2 ; extra == 'all' - - lxml>=4.9.2 ; extra == 'all' - - matplotlib>=3.6.3 ; extra == 'all' - - numba>=0.56.4 ; extra == 'all' - - numexpr>=2.8.4 ; 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.0 ; extra == 'all' - - pandas-gbq>=0.19.0 ; extra == 'all' - - psycopg2>=2.9.6 ; extra == 'all' - - pyarrow>=10.0.1 ; extra == 'all' - - pymysql>=1.0.2 ; 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.0 ; extra == 'all' - - pytest>=7.3.2 ; extra == 'all' - - pytest-xdist>=2.2.0 ; extra == 'all' - - python-calamine>=0.1.7 ; 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.3.0 ; extra == 'all' - - scipy>=1.10.0 ; extra == 'all' - - s3fs>=2022.11.0 ; extra == 'all' - - sqlalchemy>=2.0.0 ; extra == 'all' - - tables>=3.8.0 ; 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>=2022.12.0 ; extra == 'all' + - xarray>=2024.10.0 ; extra == 'all' - xlrd>=2.0.1 ; extra == 'all' - - xlsxwriter>=3.0.5 ; extra == 'all' - - zstandard>=0.19.0 ; extra == 'all' - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/c1/fa/7ac648108144a095b4fb6aa3de1954689f7af60a14cf25583f4960ecb878/pandas-2.3.3-cp311-cp311-macosx_10_9_x86_64.whl + - xlsxwriter>=3.2.0 ; extra == 'all' + - zstandard>=0.23.0 ; extra == 'all' + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/f2/85/ab6d04733a7d6ff32bfc8382bf1b07078228f5d6ebec5266b91bfc5c4ff7/pandas-3.0.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl name: pandas - version: 2.3.3 - sha256: 602b8615ebcc4a0c1751e71840428ddebeb142ec02c786e8ad6b1ce3c8dec523 + version: 3.0.1 + sha256: 1ff8cf1d2896e34343197685f432450ec99a85ba8d90cce2030c5eee2ef98791 requires_dist: - - numpy>=1.22.4 ; python_full_version < '3.11' - - numpy>=1.23.2 ; python_full_version == '3.11.*' - - numpy>=1.26.0 ; python_full_version >= '3.12' + - numpy>=1.26.0 ; python_full_version < '3.14' + - numpy>=2.3.3 ; python_full_version >= '3.14' - python-dateutil>=2.8.2 - - pytz>=2020.1 - - tzdata>=2022.7 - - hypothesis>=6.46.1 ; extra == 'test' - - pytest>=7.3.2 ; extra == 'test' - - pytest-xdist>=2.2.0 ; extra == 'test' - - pyarrow>=10.0.1 ; extra == 'pyarrow' - - bottleneck>=1.3.6 ; extra == 'performance' - - numba>=0.56.4 ; extra == 'performance' - - numexpr>=2.8.4 ; extra == 'performance' - - scipy>=1.10.0 ; extra == 'computation' - - xarray>=2022.12.0 ; extra == 'computation' - - fsspec>=2022.11.0 ; extra == 'fss' - - s3fs>=2022.11.0 ; extra == 'aws' - - gcsfs>=2022.11.0 ; extra == 'gcp' - - pandas-gbq>=0.19.0 ; extra == 'gcp' + - 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.0 ; extra == 'excel' - - python-calamine>=0.1.7 ; 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.0.5 ; extra == 'excel' - - pyarrow>=10.0.1 ; extra == 'parquet' - - pyarrow>=10.0.1 ; extra == 'feather' - - tables>=3.8.0 ; extra == 'hdf5' - - pyreadstat>=1.2.0 ; extra == 'spss' - - sqlalchemy>=2.0.0 ; extra == 'postgresql' - - psycopg2>=2.9.6 ; extra == 'postgresql' - - adbc-driver-postgresql>=0.8.0 ; extra == 'postgresql' - - sqlalchemy>=2.0.0 ; extra == 'mysql' - - pymysql>=1.0.2 ; extra == 'mysql' - - sqlalchemy>=2.0.0 ; extra == 'sql-other' - - adbc-driver-postgresql>=0.8.0 ; extra == 'sql-other' - - adbc-driver-sqlite>=0.8.0 ; extra == 'sql-other' - - beautifulsoup4>=4.11.2 ; extra == 'html' + - 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>=4.9.2 ; extra == 'html' - - lxml>=4.9.2 ; extra == 'xml' - - matplotlib>=3.6.3 ; extra == 'plot' - - jinja2>=3.1.2 ; extra == 'output-formatting' + - 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.3.0 ; extra == 'clipboard' - - zstandard>=0.19.0 ; extra == 'compression' - - dataframe-api-compat>=0.1.7 ; extra == 'consortium-standard' - - adbc-driver-postgresql>=0.8.0 ; extra == 'all' - - adbc-driver-sqlite>=0.8.0 ; extra == 'all' - - beautifulsoup4>=4.11.2 ; extra == 'all' - - bottleneck>=1.3.6 ; extra == 'all' - - dataframe-api-compat>=0.1.7 ; extra == 'all' - - fastparquet>=2022.12.0 ; extra == 'all' - - fsspec>=2022.11.0 ; extra == 'all' - - gcsfs>=2022.11.0 ; extra == 'all' + - 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.46.1 ; extra == 'all' - - jinja2>=3.1.2 ; extra == 'all' - - lxml>=4.9.2 ; extra == 'all' - - matplotlib>=3.6.3 ; extra == 'all' - - numba>=0.56.4 ; extra == 'all' - - numexpr>=2.8.4 ; 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.0 ; extra == 'all' - - pandas-gbq>=0.19.0 ; extra == 'all' - - psycopg2>=2.9.6 ; extra == 'all' - - pyarrow>=10.0.1 ; extra == 'all' - - pymysql>=1.0.2 ; 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.0 ; extra == 'all' - - pytest>=7.3.2 ; extra == 'all' - - pytest-xdist>=2.2.0 ; extra == 'all' - - python-calamine>=0.1.7 ; 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.3.0 ; extra == 'all' - - scipy>=1.10.0 ; extra == 'all' - - s3fs>=2022.11.0 ; extra == 'all' - - sqlalchemy>=2.0.0 ; extra == 'all' - - tables>=3.8.0 ; 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>=2022.12.0 ; extra == 'all' + - xarray>=2024.10.0 ; extra == 'all' - xlrd>=2.0.1 ; extra == 'all' - - xlsxwriter>=3.0.5 ; extra == 'all' - - zstandard>=0.19.0 ; extra == 'all' - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/cd/4b/18b035ee18f97c1040d94debd8f2e737000ad70ccc8f5513f4eefad75f4b/pandas-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl + - xlsxwriter>=3.2.0 ; extra == 'all' + - zstandard>=0.23.0 ; extra == 'all' + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/ff/07/c7087e003ceee9b9a82539b40414ec557aa795b584a1a346e89180853d79/pandas-3.0.1-cp311-cp311-macosx_10_9_x86_64.whl name: pandas - version: 2.3.3 - sha256: 56851a737e3470de7fa88e6131f41281ed440d29a9268dcbf0002da5ac366713 + version: 3.0.1 + sha256: de09668c1bf3b925c07e5762291602f0d789eca1b3a781f99c1c78f6cac0e7ea requires_dist: - - numpy>=1.22.4 ; python_full_version < '3.11' - - numpy>=1.23.2 ; python_full_version == '3.11.*' - - numpy>=1.26.0 ; python_full_version >= '3.12' + - numpy>=1.26.0 ; python_full_version < '3.14' + - numpy>=2.3.3 ; python_full_version >= '3.14' - python-dateutil>=2.8.2 - - pytz>=2020.1 - - tzdata>=2022.7 - - hypothesis>=6.46.1 ; extra == 'test' - - pytest>=7.3.2 ; extra == 'test' - - pytest-xdist>=2.2.0 ; extra == 'test' - - pyarrow>=10.0.1 ; extra == 'pyarrow' - - bottleneck>=1.3.6 ; extra == 'performance' - - numba>=0.56.4 ; extra == 'performance' - - numexpr>=2.8.4 ; extra == 'performance' - - scipy>=1.10.0 ; extra == 'computation' - - xarray>=2022.12.0 ; extra == 'computation' - - fsspec>=2022.11.0 ; extra == 'fss' - - s3fs>=2022.11.0 ; extra == 'aws' - - gcsfs>=2022.11.0 ; extra == 'gcp' - - pandas-gbq>=0.19.0 ; extra == 'gcp' + - 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.0 ; extra == 'excel' - - python-calamine>=0.1.7 ; 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.0.5 ; extra == 'excel' - - pyarrow>=10.0.1 ; extra == 'parquet' - - pyarrow>=10.0.1 ; extra == 'feather' - - tables>=3.8.0 ; extra == 'hdf5' - - pyreadstat>=1.2.0 ; extra == 'spss' - - sqlalchemy>=2.0.0 ; extra == 'postgresql' - - psycopg2>=2.9.6 ; extra == 'postgresql' - - adbc-driver-postgresql>=0.8.0 ; extra == 'postgresql' - - sqlalchemy>=2.0.0 ; extra == 'mysql' - - pymysql>=1.0.2 ; extra == 'mysql' - - sqlalchemy>=2.0.0 ; extra == 'sql-other' - - adbc-driver-postgresql>=0.8.0 ; extra == 'sql-other' - - adbc-driver-sqlite>=0.8.0 ; extra == 'sql-other' - - beautifulsoup4>=4.11.2 ; extra == 'html' + - 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>=4.9.2 ; extra == 'html' - - lxml>=4.9.2 ; extra == 'xml' - - matplotlib>=3.6.3 ; extra == 'plot' - - jinja2>=3.1.2 ; extra == 'output-formatting' + - 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.3.0 ; extra == 'clipboard' - - zstandard>=0.19.0 ; extra == 'compression' - - dataframe-api-compat>=0.1.7 ; extra == 'consortium-standard' - - adbc-driver-postgresql>=0.8.0 ; extra == 'all' - - adbc-driver-sqlite>=0.8.0 ; extra == 'all' - - beautifulsoup4>=4.11.2 ; extra == 'all' - - bottleneck>=1.3.6 ; extra == 'all' - - dataframe-api-compat>=0.1.7 ; extra == 'all' - - fastparquet>=2022.12.0 ; extra == 'all' - - fsspec>=2022.11.0 ; extra == 'all' - - gcsfs>=2022.11.0 ; extra == 'all' + - 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.46.1 ; extra == 'all' - - jinja2>=3.1.2 ; extra == 'all' - - lxml>=4.9.2 ; extra == 'all' - - matplotlib>=3.6.3 ; extra == 'all' - - numba>=0.56.4 ; extra == 'all' - - numexpr>=2.8.4 ; 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.0 ; extra == 'all' - - pandas-gbq>=0.19.0 ; extra == 'all' - - psycopg2>=2.9.6 ; extra == 'all' - - pyarrow>=10.0.1 ; extra == 'all' - - pymysql>=1.0.2 ; 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.0 ; extra == 'all' - - pytest>=7.3.2 ; extra == 'all' - - pytest-xdist>=2.2.0 ; extra == 'all' - - python-calamine>=0.1.7 ; 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.3.0 ; extra == 'all' - - scipy>=1.10.0 ; extra == 'all' - - s3fs>=2022.11.0 ; extra == 'all' - - sqlalchemy>=2.0.0 ; extra == 'all' - - tables>=3.8.0 ; 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>=2022.12.0 ; extra == 'all' + - xarray>=2024.10.0 ; extra == 'all' - xlrd>=2.0.1 ; extra == 'all' - - xlsxwriter>=3.0.5 ; extra == 'all' - - zstandard>=0.19.0 ; extra == 'all' - requires_python: '>=3.9' + - xlsxwriter>=3.2.0 ; extra == 'all' + - zstandard>=0.23.0 ; extra == 'all' + requires_python: '>=3.11' - pypi: https://files.pythonhosted.org/packages/ef/af/4fbc8cab944db5d21b7e2a5b8e9211a03a79852b1157e2c102fcc61ac440/pandocfilters-1.5.1-py2.py3-none-any.whl name: pandocfilters version: 1.5.1 sha256: 93be382804a9cdb0a7267585f157e5d1731bbe5545a85b268d6f5fe6232de2bc requires_python: '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*' -- pypi: https://files.pythonhosted.org/packages/16/32/f8e3c85d1d5250232a5d3477a2a28cc291968ff175caeadaf3cc19ce0e4a/parso-0.8.5-py2.py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/b6/61/fae042894f4296ec49e3f193aff5d7c18440da9e48102c3315e1bc4519a7/parso-0.8.6-py2.py3-none-any.whl name: parso - version: 0.8.5 - sha256: 646204b5ee239c396d040b90f9e272e9a8017c630092bf59980beb62fd033887 + version: 0.8.6 + sha256: 2c549f800b70a5c4952197248825584cb00f033b29c692671d3bf08bf380baff requires_dist: - pytest ; extra == 'testing' - docopt ; extra == 'testing' - flake8==5.0.4 ; extra == 'qa' - - mypy==0.971 ; extra == 'qa' + - zuban==0.5.1 ; extra == 'qa' - types-setuptools==67.2.0.1 ; extra == 'qa' requires_python: '>=3.6' -- pypi: https://files.pythonhosted.org/packages/05/bb/39e6768529454cc2b57e1e2fa0a0a18ff64397a16303270e215a3e03285f/pathspec-1.0.0-py3-none-any.whl +- 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/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl name: pathspec - version: 1.0.0 - sha256: 1373719036e64a2b9de3b8ddd9e30afb082a915619f07265ed76d9ae507800ae + version: 1.0.4 + sha256: fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723 requires_dist: - hyperscan>=0.7 ; extra == 'hyperscan' - typing-extensions>=4 ; extra == 'optional' @@ -8515,10 +9571,10 @@ packages: sha256: 7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523 requires_dist: - ptyprocess>=0.5 -- pypi: https://files.pythonhosted.org/packages/01/9a/632e58ec89a32738cabfd9ec418f0e9898a2b4719afc581f07c04a05e3c9/pillow-12.1.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl +- pypi: https://files.pythonhosted.org/packages/14/a1/16c4b823838ba4c9c52c0e6bbda903a3fe5a1bdbf1b8eb4fff7156f3e318/pillow-12.1.1-cp313-cp313-macosx_10_13_x86_64.whl name: pillow - version: 12.1.0 - sha256: 6741e6f3074a35e47c77b23a4e4f2d90db3ed905cb1c5e6e0d49bff2045632bc + version: 12.1.1 + sha256: 6c6db3b84c87d48d0088943bf33440e0c42370b99b1c2a7989216f7b42eede60 requires_dist: - furo ; extra == 'docs' - olefile ; extra == 'docs' @@ -8547,10 +9603,10 @@ packages: - trove-classifiers>=2024.10.12 ; extra == 'tests' - defusedxml ; extra == 'xmp' requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/1c/34/583420a1b55e715937a85bd48c5c0991598247a1fd2eb5423188e765ea02/pillow-12.1.0-cp313-cp313-macosx_11_0_arm64.whl +- pypi: https://files.pythonhosted.org/packages/2b/46/5da1ec4a5171ee7bf1a0efa064aba70ba3d6e0788ce3f5acd1375d23c8c0/pillow-12.1.1-cp311-cp311-macosx_10_10_x86_64.whl name: pillow - version: 12.1.0 - sha256: db44d5c160a90df2d24a24760bbd37607d53da0b34fb546c4c232af7192298ac + version: 12.1.1 + sha256: e879bb6cd5c73848ef3b2b48b8af9ff08c5b71ecda8048b7dd22d8a33f60be32 requires_dist: - furo ; extra == 'docs' - olefile ; extra == 'docs' @@ -8579,10 +9635,10 @@ packages: - trove-classifiers>=2024.10.12 ; extra == 'tests' - defusedxml ; extra == 'xmp' requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/43/06/7264c0597e676104cc22ca73ee48f752767cd4b1fe084662620b17e10120/pillow-12.1.0-cp311-cp311-macosx_11_0_arm64.whl +- pypi: https://files.pythonhosted.org/packages/31/03/bef822e4f2d8f9d7448c133d0a18185d3cce3e70472774fffefe8b0ed562/pillow-12.1.1-cp311-cp311-win_amd64.whl name: pillow - version: 12.1.0 - sha256: b6e53e82ec2db0717eabb276aa56cf4e500c9a7cec2c2e189b55c24f65a3e8c0 + version: 12.1.1 + sha256: fbfa2a7c10cc2623f412753cddf391c7f971c52ca40a3f65dc5039b2939e8563 requires_dist: - furo ; extra == 'docs' - olefile ; extra == 'docs' @@ -8611,10 +9667,10 @@ packages: - trove-classifiers>=2024.10.12 ; extra == 'tests' - defusedxml ; extra == 'xmp' requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/43/c4/bf8328039de6cc22182c3ef007a2abfbbdab153661c0a9aa78af8d706391/pillow-12.1.0-cp311-cp311-macosx_10_10_x86_64.whl +- pypi: https://files.pythonhosted.org/packages/3f/eb/b0834ad8b583d7d9d42b80becff092082a1c3c156bb582590fcc973f1c7c/pillow-12.1.1-cp313-cp313-win_amd64.whl name: pillow - version: 12.1.0 - sha256: a83e0850cb8f5ac975291ebfc4170ba481f41a28065277f7f735c202cd8e0af3 + version: 12.1.1 + sha256: 344cf1e3dab3be4b1fa08e449323d98a2a3f819ad20f4b22e77a0ede31f0faa1 requires_dist: - furo ; extra == 'docs' - olefile ; extra == 'docs' @@ -8643,10 +9699,10 @@ packages: - trove-classifiers>=2024.10.12 ; extra == 'tests' - defusedxml ; extra == 'xmp' requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/4f/bf/28ab865de622e14b747f0cd7877510848252d950e43002e224fb1c9ababf/pillow-12.1.0-cp313-cp313-macosx_10_13_x86_64.whl +- pypi: https://files.pythonhosted.org/packages/71/24/538bff45bde96535d7d998c6fed1a751c75ac7c53c37c90dc2601b243893/pillow-12.1.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl name: pillow - version: 12.1.0 - sha256: 983976c2ab753166dc66d36af6e8ec15bb511e4a25856e2227e5f7e00a160587 + version: 12.1.1 + sha256: 47b94983da0c642de92ced1702c5b6c292a84bd3a8e1d1702ff923f183594717 requires_dist: - furo ; extra == 'docs' - olefile ; extra == 'docs' @@ -8675,10 +9731,10 @@ packages: - trove-classifiers>=2024.10.12 ; extra == 'tests' - defusedxml ; extra == 'xmp' requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/5c/1f/8e66ab9be3aaf1435bc03edd1ebdf58ffcd17f7349c1d970cafe87af27d9/pillow-12.1.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl +- pypi: https://files.pythonhosted.org/packages/78/93/a29e9bc02d1cf557a834da780ceccd54e02421627200696fcf805ebdc3fb/pillow-12.1.1-cp311-cp311-macosx_11_0_arm64.whl name: pillow - version: 12.1.0 - sha256: 1f345e7bc9d7f368887c712aa5054558bad44d2a301ddf9248599f4161abc7c0 + version: 12.1.1 + sha256: 365b10bb9417dd4498c0e3b128018c4a624dc11c7b97d8cc54effe3b096f4c38 requires_dist: - furo ; extra == 'docs' - olefile ; extra == 'docs' @@ -8707,10 +9763,10 @@ packages: - trove-classifiers>=2024.10.12 ; extra == 'tests' - defusedxml ; extra == 'xmp' requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/6c/af/b1d7e301c4cd26cd45d4af884d9ee9b6fab893b0ad2450d4746d74a6968c/pillow-12.1.0-cp311-cp311-win_amd64.whl +- pypi: https://files.pythonhosted.org/packages/a2/c8/46dfeac5825e600579157eea177be43e2f7ff4a99da9d0d0a49533509ac5/pillow-12.1.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl name: pillow - version: 12.1.0 - sha256: 806f3987ffe10e867bab0ddad45df1148a2b98221798457fa097ad85d6e8bc75 + version: 12.1.1 + sha256: 597bd9c8419bc7c6af5604e55847789b69123bbe25d65cc6ad3012b4f3c98d8b requires_dist: - furo ; extra == 'docs' - olefile ; extra == 'docs' @@ -8739,10 +9795,10 @@ packages: - trove-classifiers>=2024.10.12 ; extra == 'tests' - defusedxml ; extra == 'xmp' requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/e5/07/74a9d941fa45c90a0d9465098fe1ec85de3e2afbdc15cc4766622d516056/pillow-12.1.0-cp313-cp313-win_amd64.whl +- pypi: https://files.pythonhosted.org/packages/bb/ad/ad9dc98ff24f485008aa5cdedaf1a219876f6f6c42a4626c08bc4e80b120/pillow-12.1.1-cp313-cp313-macosx_11_0_arm64.whl name: pillow - version: 12.1.0 - sha256: 461f9dfdafa394c59cd6d818bdfdbab4028b83b02caadaff0ffd433faf4c9a7a + version: 12.1.1 + sha256: 8b7e5304e34942bf62e15184219a7b5ad4ff7f3bb5cca4d984f37df1a0e1aee2 requires_dist: - furo ; extra == 'docs' - olefile ; extra == 'docs' @@ -8771,11 +9827,6 @@ packages: - trove-classifiers>=2024.10.12 ; extra == 'tests' - defusedxml ; extra == 'xmp' requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/44/3c/d717024885424591d5376220b5e836c2d5293ce2011523c9de23ff7bf068/pip-25.3-py3-none-any.whl - name: pip - version: '25.3' - sha256: 9655943313a94722b7774661c21049070f6bbb0a1516bf02f7c8d5d9201514cd - requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/c4/36/ce5f75aa7c736a663a901766edc3580098c7ea3959a0e878363a54a3714e/pixi_kernel-0.7.1-py3-none-any.whl name: pixi-kernel version: 0.7.1 @@ -8788,26 +9839,46 @@ packages: - returns>=0.23 - tomli>=2 ; python_full_version < '3.11' requires_python: '>=3.10,<4.0' -- pypi: https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/63/d7/97f7e3a6abb67d8080dd406fd4df842c2be0efaf712d1c899c32a075027c/platformdirs-4.9.4-py3-none-any.whl name: platformdirs - version: 4.5.1 - sha256: d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31 - requires_dist: - - furo>=2025.9.25 ; extra == 'docs' - - proselint>=0.14 ; extra == 'docs' - - sphinx-autodoc-typehints>=3.2 ; extra == 'docs' - - sphinx>=8.2.3 ; extra == 'docs' - - appdirs==1.4.4 ; extra == 'test' - - covdefaults>=2.3 ; extra == 'test' - - pytest-cov>=7 ; extra == 'test' - - pytest-mock>=3.15.1 ; extra == 'test' - - pytest>=8.4.2 ; extra == 'test' - - mypy>=1.18.2 ; extra == 'type' + version: 4.9.4 + sha256: 68a9a4619a666ea6439f2ff250c12a853cd1cbd5158d258bd824a7df6be2f868 requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/e7/c3/3031c931098de393393e1f93a38dc9ed6805d86bb801acc3cf2d5bd1e6b7/plotly-6.5.0-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/31/8b/9e8baf7dacac8d0c174925c38ff43c6d94bc9abb35503f67762caccb6869/plopp-26.3.1-py3-none-any.whl + name: plopp + version: 26.3.1 + sha256: 56531f2f71fa4f7f33c172312d2423d969deb9b9dd29b2524ad3ed7e33d220eb + requires_dist: + - lazy-loader>=0.4 + - matplotlib>=3.8 + - scipp>=25.5.0 ; extra == 'scipp' + - scipp>=25.5.0 ; 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' + - 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' + - scipp>=25.5.0 ; 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/52/d2/c6e44dba74f17c6216ce1b56044a9b93a929f1c2d5bdaff892512b260f5e/plotly-6.6.0-py3-none-any.whl name: plotly - version: 6.5.0 - sha256: 5ac851e100367735250206788a2b1325412aa4a4917a4fe3e6f0bc5aa6f3d90a + version: 6.6.0 + sha256: 8d6daf0f87412e0c0bfe72e809d615217ab57cc715899a1e5145135a7800d1d0 requires_dist: - narwhals>=1.15.1 - packaging @@ -8855,14 +9926,22 @@ packages: - pytest-benchmark ; extra == 'testing' - coverage ; extra == 'testing' requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/79/ad/45312df6b63ba64ea35b8d8f5f0c577aac16e6b416eafe8e1cb34e03f9a7/plumbum-1.10.0-py3-none-any.whl + name: plumbum + version: 1.10.0 + sha256: 9583d737ac901c474d99d030e4d5eec4c4e6d2d7417b1cf49728cf3be34f6dc8 + requires_dist: + - pywin32 ; platform_python_implementation != 'PyPy' and sys_platform == 'win32' + - paramiko ; extra == 'ssh' + requires_python: '>=3.9' - 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/a8/87/77cc11c7a9ea9fd05503def69e3d18605852cd0d4b0d3b8f15bbeb3ef1d1/pooch-1.8.2-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/2a/2d/d4bf65e47cea8ff2c794a600c4fd1273a7902f268757c531e0ee9f18aa58/pooch-1.9.0-py3-none-any.whl name: pooch - version: 1.8.2 - sha256: 3529a57096f7198778a5ceefd5ac3ef0e4d06a6ddaf9fc2d609b806f25302c47 + version: 1.9.0 + sha256: f265597baa9f760d25ceb29d0beb8186c243d6607b0f60b83ecf14078dbc703b requires_dist: - platformdirs>=2.5.0 - packaging>=20.0 @@ -8870,7 +9949,9 @@ packages: - tqdm>=4.41.0,<5.0.0 ; extra == 'progress' - paramiko>=2.7.0 ; extra == 'sftp' - xxhash>=1.4.3 ; extra == 'xxhash' - requires_python: '>=3.7' + - pytest-httpserver ; extra == 'test' + - pytest-localftpserver ; extra == 'test' + requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/5d/19/fd3ef348460c80af7bb4669ea7926651d1f95c23ff2df18b9d24bab4f3fa/pre_commit-4.5.1-py2.py3-none-any.whl name: pre-commit version: 4.5.1 @@ -8892,12 +9973,14 @@ packages: - pytest-cov ; extra == 'tests' - pytest-lazy-fixtures ; extra == 'tests' requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/b8/db/14bafcb4af2139e046d03fd00dea7873e48eafe18b7d2797e73d6681f210/prometheus_client-0.23.1-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/74/c3/24a2f845e3917201628ecaba4f18bab4d18a337834c1df2a159ee9d22a42/prometheus_client-0.24.1-py3-none-any.whl name: prometheus-client - version: 0.23.1 - sha256: dd1913e6e76b59cfe44e7a4b83e01afc9873c1bdfd2ed8739f1e76aeca115f99 + version: 0.24.1 + sha256: 150db128af71a5c2482b36e588fc8a6b95e498750da4b17065947c16070f4055 requires_dist: - twisted ; extra == 'twisted' + - aiohttp ; extra == 'aiohttp' + - django ; extra == 'django' requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl name: prompt-toolkit @@ -8946,10 +10029,10 @@ packages: version: 0.4.1 sha256: 381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/29/f8/40e01c350ad9a2b3cb4e6adbcc8a83b17ee50dd5792102b6142385937db5/psutil-7.2.1-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl +- pypi: https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl name: psutil - version: 7.2.1 - sha256: 5e38404ca2bb30ed7267a46c02f06ff842e92da3bb8c5bfdadbd35a5722314d8 + version: 7.2.2 + sha256: 1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979 requires_dist: - psleak ; extra == 'dev' - pytest ; extra == 'dev' @@ -8976,25 +10059,30 @@ packages: - virtualenv ; extra == 'dev' - vulture ; extra == 'dev' - wheel ; extra == 'dev' + - colorama ; os_name == 'nt' and extra == 'dev' + - pyreadline3 ; os_name == 'nt' and extra == 'dev' + - pywin32 ; implementation_name != 'pypy' and os_name == 'nt' and extra == 'dev' + - wheel ; implementation_name != 'pypy' and os_name == 'nt' and extra == 'dev' + - wmi ; implementation_name != 'pypy' and os_name == 'nt' and extra == 'dev' - psleak ; extra == 'test' - pytest ; extra == 'test' - pytest-instafail ; extra == 'test' - pytest-xdist ; extra == 'test' - setuptools ; extra == 'test' + - pywin32 ; implementation_name != 'pypy' and os_name == 'nt' and extra == 'test' + - wheel ; implementation_name != 'pypy' and os_name == 'nt' and extra == 'test' + - wmi ; implementation_name != 'pypy' and os_name == 'nt' and extra == 'test' requires_python: '>=3.6' -- pypi: https://files.pythonhosted.org/packages/34/68/d9317542e3f2b180c4306e3f45d3c922d7e86d8ce39f941bb9e2e9d8599e/psutil-7.2.1-cp37-abi3-win_amd64.whl +- pypi: https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl name: psutil - version: 7.2.1 - sha256: b1b0671619343aa71c20ff9767eced0483e4fc9e1f489d50923738caf6a03c17 + version: 7.2.2 + sha256: eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988 requires_dist: - psleak ; extra == 'dev' - pytest ; extra == 'dev' - pytest-instafail ; extra == 'dev' - pytest-xdist ; extra == 'dev' - setuptools ; extra == 'dev' - - pywin32 ; extra == 'dev' - - wheel ; extra == 'dev' - - wmi ; extra == 'dev' - abi3audit ; extra == 'dev' - black ; extra == 'dev' - check-manifest ; extra == 'dev' @@ -9014,21 +10102,25 @@ packages: - validate-pyproject[all] ; extra == 'dev' - virtualenv ; extra == 'dev' - vulture ; extra == 'dev' - - colorama ; extra == 'dev' - - pyreadline3 ; extra == 'dev' + - wheel ; extra == 'dev' + - colorama ; os_name == 'nt' and extra == 'dev' + - pyreadline3 ; os_name == 'nt' and extra == 'dev' + - pywin32 ; implementation_name != 'pypy' and os_name == 'nt' and extra == 'dev' + - wheel ; implementation_name != 'pypy' and os_name == 'nt' and extra == 'dev' + - wmi ; implementation_name != 'pypy' and os_name == 'nt' and extra == 'dev' - psleak ; extra == 'test' - pytest ; extra == 'test' - pytest-instafail ; extra == 'test' - pytest-xdist ; extra == 'test' - setuptools ; extra == 'test' - - pywin32 ; extra == 'test' - - wheel ; extra == 'test' - - wmi ; extra == 'test' + - pywin32 ; implementation_name != 'pypy' and os_name == 'nt' and extra == 'test' + - wheel ; implementation_name != 'pypy' and os_name == 'nt' and extra == 'test' + - wmi ; implementation_name != 'pypy' and os_name == 'nt' and extra == 'test' requires_python: '>=3.6' -- pypi: https://files.pythonhosted.org/packages/c5/2c/78e4a789306a92ade5000da4f5de3255202c534acdadc3aac7b5458fadef/psutil-7.2.1-cp36-abi3-macosx_11_0_arm64.whl +- pypi: https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl name: psutil - version: 7.2.1 - sha256: 05cc68dbb8c174828624062e73078e7e35406f4ca2d0866c272c2410d8ef06d1 + version: 7.2.2 + sha256: 076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9 requires_dist: - psleak ; extra == 'dev' - pytest ; extra == 'dev' @@ -9055,16 +10147,24 @@ packages: - virtualenv ; extra == 'dev' - vulture ; extra == 'dev' - wheel ; extra == 'dev' + - colorama ; os_name == 'nt' and extra == 'dev' + - pyreadline3 ; os_name == 'nt' and extra == 'dev' + - pywin32 ; implementation_name != 'pypy' and os_name == 'nt' and extra == 'dev' + - wheel ; implementation_name != 'pypy' and os_name == 'nt' and extra == 'dev' + - wmi ; implementation_name != 'pypy' and os_name == 'nt' and extra == 'dev' - psleak ; extra == 'test' - pytest ; extra == 'test' - pytest-instafail ; extra == 'test' - pytest-xdist ; extra == 'test' - setuptools ; extra == 'test' + - pywin32 ; implementation_name != 'pypy' and os_name == 'nt' and extra == 'test' + - wheel ; implementation_name != 'pypy' and os_name == 'nt' and extra == 'test' + - wmi ; implementation_name != 'pypy' and os_name == 'nt' and extra == 'test' requires_python: '>=3.6' -- pypi: https://files.pythonhosted.org/packages/c5/cf/5180eb8c8bdf6a503c6919f1da28328bd1e6b3b1b5b9d5b01ae64f019616/psutil-7.2.1-cp36-abi3-macosx_10_9_x86_64.whl +- pypi: https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl name: psutil - version: 7.2.1 - sha256: b2e953fcfaedcfbc952b44744f22d16575d3aa78eb4f51ae74165b4e96e55f42 + version: 7.2.2 + sha256: ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486 requires_dist: - psleak ; extra == 'dev' - pytest ; extra == 'dev' @@ -9091,11 +10191,19 @@ packages: - virtualenv ; extra == 'dev' - vulture ; extra == 'dev' - wheel ; extra == 'dev' + - colorama ; os_name == 'nt' and extra == 'dev' + - pyreadline3 ; os_name == 'nt' and extra == 'dev' + - pywin32 ; implementation_name != 'pypy' and os_name == 'nt' and extra == 'dev' + - wheel ; implementation_name != 'pypy' and os_name == 'nt' and extra == 'dev' + - wmi ; implementation_name != 'pypy' and os_name == 'nt' and extra == 'dev' - psleak ; extra == 'test' - pytest ; extra == 'test' - pytest-instafail ; extra == 'test' - pytest-xdist ; extra == 'test' - setuptools ; extra == 'test' + - pywin32 ; implementation_name != 'pypy' and os_name == 'nt' and extra == 'test' + - wheel ; implementation_name != 'pypy' and os_name == 'nt' and extra == 'test' + - wmi ; implementation_name != 'pypy' and os_name == 'nt' and extra == 'test' requires_python: '>=3.6' - pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl name: ptyprocess @@ -9112,10 +10220,10 @@ packages: 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/0b/a6/e2e8535d8146bce05de6e0ecf1099a7e2887d840ae2a7b3a09385543fd02/py3dmol-2.5.3-py2.py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/25/7d/cea3531f77df694ac7f169378250d85f19f69b09a5f4fa45f650837ae7cc/py3dmol-2.5.4-py2.py3-none-any.whl name: py3dmol - version: 2.5.3 - sha256: 5c1c9ee40bda82b91978e75f3c144be5b90cdf472e765bcef4890db76cc8f843 + version: 2.5.4 + sha256: 32806726b5310524a2b5bfee320737f7feef635cafc945c991062806daa9e43a requires_dist: - ipython ; extra == 'ipython' - 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 @@ -9191,11 +10299,89 @@ packages: version: 2.14.0 sha256: dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl name: pycparser - version: '2.23' - sha256: e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934 - requires_python: '>=3.8' + version: '3.0' + sha256: b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl + name: pydantic + version: 2.12.5 + sha256: e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d + requires_dist: + - annotated-types>=0.6.0 + - pydantic-core==2.41.5 + - typing-extensions>=4.14.1 + - typing-inspection>=0.4.2 + - email-validator>=2.0.0 ; extra == 'email' + - tzdata ; python_full_version >= '3.9' and sys_platform == 'win32' and extra == 'timezone' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl + name: pydantic-core + version: 2.41.5 + sha256: 76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe + requires_dist: + - typing-extensions>=4.14.1 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl + name: pydantic-core + version: 2.41.5 + sha256: 7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b + requires_dist: + - typing-extensions>=4.14.1 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl + name: pydantic-core + version: 2.41.5 + sha256: 941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9 + requires_dist: + - typing-extensions>=4.14.1 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl + name: pydantic-core + version: 2.41.5 + sha256: 112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34 + requires_dist: + - typing-extensions>=4.14.1 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl + name: pydantic-core + version: 2.41.5 + sha256: 79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11 + requires_dist: + - typing-extensions>=4.14.1 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + name: pydantic-core + version: 2.41.5 + sha256: f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b + requires_dist: + - typing-extensions>=4.14.1 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + name: pydantic-core + version: 2.41.5 + sha256: 406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586 + requires_dist: + - typing-extensions>=4.14.1 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl + name: pydantic-core + version: 2.41.5 + sha256: a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6 + requires_dist: + - typing-extensions>=4.14.1 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/87/6f/cc2b231dc78d8c3aaa674a676db190b8f8071c50134af8f8cf39b9b8e8df/pydoclint-0.8.3-py3-none-any.whl + name: pydoclint + version: 0.8.3 + sha256: 5fc9b82d0d515afce0908cb70e8ff695a68b19042785c248c4f227ad66b4a164 + requires_dist: + - click>=8.1.0 + - docstring-parser-fork>=0.0.12 + - tomli>=2.0.1 ; python_full_version < '3.11' + - flake8>=4 ; extra == 'flake8' + requires_python: '>=3.10' - pypi: https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl name: pygments version: 2.19.2 @@ -9203,19 +10389,19 @@ packages: requires_dist: - colorama>=0.4.6 ; extra == 'windows-terminal' requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/ea/10/47caf89cbb52e5bb764696fd52a8c591a2f0e851a93270c05a17f36000b5/pymdown_extensions-10.20-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/6f/2c/5b079febdc65e1c3fb2729bf958d18b45be7113828528e8a0b5850dd819a/pymdown_extensions-10.21-py3-none-any.whl name: pymdown-extensions - version: '10.20' - sha256: ea9e62add865da80a271d00bfa1c0fa085b20d133fb3fc97afdc88e682f60b2f + version: '10.21' + sha256: 91b879f9f864d49794c2d9534372b10150e6141096c3908a455e45ca72ad9d3f requires_dist: - markdown>=3.6 - pyyaml - pygments>=2.19.1 ; extra == 'extra' requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/8b/40/2614036cdd416452f5bf98ec037f38a1afb17f327cb8e6b652d4729e0af8/pyparsing-3.3.1-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl name: pyparsing - version: 3.3.1 - sha256: 023b5e7e5520ad96642e2c6db4cb683d3970bd640cdf7115049a6e9c3682df82 + version: 3.3.2 + sha256: 850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d requires_dist: - railroad-diagrams ; extra == 'diagrams' - jinja2 ; extra == 'diagrams' @@ -9245,10 +10431,10 @@ packages: - setuptools ; extra == 'dev' - xmlschema ; extra == 'dev' requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl name: pytest-cov - version: 7.0.0 - sha256: 3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861 + version: 7.1.0 + sha256: a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678 requires_dist: - coverage[toml]>=7.10.6 - pluggy>=1.2 @@ -9268,38 +10454,37 @@ packages: - psutil>=3.0 ; extra == 'psutil' - setproctitle ; extra == 'setproctitle' requires_python: '>=3.9' -- conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.11.14-hd63d673_2_cpython.conda - build_number: 2 - sha256: 5b872f7747891e50e990a96d2b235236a5c66cc9f8c9dcb7149aee674ea8145a - md5: c4202a55b4486314fbb8c11bc43a29a0 +- 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.1,<3.0a0 + - libexpat >=2.7.4,<3.0a0 - libffi >=3.5.2,<3.6.0a0 - libgcc >=14 - - liblzma >=5.8.1,<6.0a0 + - liblzma >=5.8.2,<6.0a0 - libnsl >=2.0.1,<2.1.0a0 - - libsqlite >=3.50.4,<4.0a0 - - libuuid >=2.41.2,<3.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.4,<4.0a0 - - readline >=8.2,<9.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: 30874708 - timestamp: 1761174520369 -- conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.13.11-hc97d973_100_cp313.conda + size: 30949404 + timestamp: 1772730362552 +- conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.13.12-hc97d973_100_cp313.conda build_number: 100 - sha256: 9cf014cf28e93ee242bacfbf664e8b45ae06e50b04291e640abeaeb0cba0364c - md5: 0cbb0010f1d8ecb64a428a8d4214609e + sha256: 8a08fe5b7cb5a28aa44e2994d18dbf77f443956990753a4ca8173153ffb6eb56 + md5: 4c875ed0e78c2d407ec55eadffb8cf3d depends: - __glibc >=2.17,<3.0.a0 - bzip2 >=1.0.8,<2.0a0 @@ -9307,128 +10492,125 @@ packages: - libexpat >=2.7.3,<3.0a0 - libffi >=3.5.2,<3.6.0a0 - libgcc >=14 - - liblzma >=5.8.1,<6.0a0 + - liblzma >=5.8.2,<6.0a0 - libmpdec >=4.0.0,<5.0a0 - - libsqlite >=3.51.1,<4.0a0 - - libuuid >=2.41.2,<3.0a0 + - libsqlite >=3.51.2,<4.0a0 + - libuuid >=2.41.3,<3.0a0 - libzlib >=1.3.1,<2.0a0 - ncurses >=6.5,<7.0a0 - - openssl >=3.5.4,<4.0a0 + - openssl >=3.5.5,<4.0a0 - python_abi 3.13.* *_cp313 - - readline >=8.2,<9.0a0 + - readline >=8.3,<9.0a0 - tk >=8.6.13,<8.7.0a0 - tzdata license: Python-2.0 purls: [] - size: 37226336 - timestamp: 1765021889577 + size: 37364553 + timestamp: 1770272309861 python_site_packages_path: lib/python3.13/site-packages -- conda: https://conda.anaconda.org/conda-forge/osx-64/python-3.11.14-h74c2667_2_cpython.conda - build_number: 2 - sha256: 0a17479efb8df514c3777c015ffe430d38a3a59c01dc46358e87d7ff459c9aeb - md5: 37ac5f13a245f08746e0d658b245d670 +- conda: https://conda.anaconda.org/conda-forge/osx-64/python-3.11.15-ha9537fe_0_cpython.conda + sha256: e02e12cd8d391f18bb3bf91d07e16b993592ec0d76ee37cf639390b766e0e687 + md5: 93b802a91de90b2c17b808608726bf45 depends: - - __osx >=10.13 + - __osx >=11.0 - bzip2 >=1.0.8,<2.0a0 - - libexpat >=2.7.1,<3.0a0 + - libexpat >=2.7.4,<3.0a0 - libffi >=3.5.2,<3.6.0a0 - - liblzma >=5.8.1,<6.0a0 - - libsqlite >=3.50.4,<4.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.4,<4.0a0 - - readline >=8.2,<9.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: 15697126 - timestamp: 1761174493171 -- conda: https://conda.anaconda.org/conda-forge/osx-64/python-3.13.11-h17c18a5_100_cp313.conda + size: 15664115 + timestamp: 1772730794934 +- conda: https://conda.anaconda.org/conda-forge/osx-64/python-3.13.12-h894a449_100_cp313.conda build_number: 100 - sha256: 58e23beaf3174a809c785900477c37df9f88993b5a3ccd0d76d57d6688a1be37 - md5: 6ffffd784fe1126b73329e29c80ddf53 + sha256: 9548dcf58cf6045aa4aa1f2f3fa6110115ca616a8d5fa142a24081d2b9d91291 + md5: 99b1fa1fe8a8ab58224969f4568aadca depends: - __osx >=10.13 - bzip2 >=1.0.8,<2.0a0 - libexpat >=2.7.3,<3.0a0 - libffi >=3.5.2,<3.6.0a0 - - liblzma >=5.8.1,<6.0a0 + - liblzma >=5.8.2,<6.0a0 - libmpdec >=4.0.0,<5.0a0 - - libsqlite >=3.51.1,<4.0a0 + - libsqlite >=3.51.2,<4.0a0 - libzlib >=1.3.1,<2.0a0 - ncurses >=6.5,<7.0a0 - - openssl >=3.5.4,<4.0a0 + - openssl >=3.5.5,<4.0a0 - python_abi 3.13.* *_cp313 - - readline >=8.2,<9.0a0 + - readline >=8.3,<9.0a0 - tk >=8.6.13,<8.7.0a0 - tzdata license: Python-2.0 purls: [] - size: 17360881 - timestamp: 1765022591905 + size: 17570178 + timestamp: 1770272361922 python_site_packages_path: lib/python3.13/site-packages -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.11.14-h18782d2_2_cpython.conda - build_number: 2 - sha256: 64a2bc6be8582fae75f1f2da7bdc49afd81c2793f65bb843fc37f53c99734063 - md5: da948e6cd735249ab4cfbb3fdede785e +- 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.1,<3.0a0 + - libexpat >=2.7.4,<3.0a0 - libffi >=3.5.2,<3.6.0a0 - - liblzma >=5.8.1,<6.0a0 - - libsqlite >=3.50.4,<4.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.4,<4.0a0 - - readline >=8.2,<9.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: 14788204 - timestamp: 1761174033541 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.13.11-hfc2f54d_100_cp313.conda + size: 14753109 + timestamp: 1772730203101 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.13.12-h20e6be0_100_cp313.conda build_number: 100 - sha256: c476f4e9b6d97c46b496b442878924868a54e5727251549ebfc82027aa52af68 - md5: 18a8c69608151098a8fb75eea64cc266 + sha256: 9a4f16a64def0853f0a7b6a7beb40d498fd6b09bee10b90c3d6069b664156817 + md5: 179c0f5ae4f22bc3be567298ed0b17b9 depends: - __osx >=11.0 - bzip2 >=1.0.8,<2.0a0 - libexpat >=2.7.3,<3.0a0 - libffi >=3.5.2,<3.6.0a0 - - liblzma >=5.8.1,<6.0a0 + - liblzma >=5.8.2,<6.0a0 - libmpdec >=4.0.0,<5.0a0 - - libsqlite >=3.51.1,<4.0a0 + - libsqlite >=3.51.2,<4.0a0 - libzlib >=1.3.1,<2.0a0 - ncurses >=6.5,<7.0a0 - - openssl >=3.5.4,<4.0a0 + - openssl >=3.5.5,<4.0a0 - python_abi 3.13.* *_cp313 - - readline >=8.2,<9.0a0 + - readline >=8.3,<9.0a0 - tk >=8.6.13,<8.7.0a0 - tzdata license: Python-2.0 purls: [] - size: 12920650 - timestamp: 1765020887340 + size: 12770674 + timestamp: 1770272314517 python_site_packages_path: lib/python3.13/site-packages -- conda: https://conda.anaconda.org/conda-forge/win-64/python-3.11.14-h0159041_2_cpython.conda - build_number: 2 - sha256: d5f455472597aefcdde1bc39bca313fcb40bf084f3ad987da0441f2a2ec242e4 - md5: 02a9ba5950d8b78e6c9862d6ba7a5045 +- 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.1,<3.0a0 + - libexpat >=2.7.4,<3.0a0 - libffi >=3.5.2,<3.6.0a0 - - liblzma >=5.8.1,<6.0a0 - - libsqlite >=3.50.4,<4.0a0 + - liblzma >=5.8.2,<6.0a0 + - libsqlite >=3.51.2,<4.0a0 - libzlib >=1.3.1,<2.0a0 - - openssl >=3.5.4,<4.0a0 + - openssl >=3.5.5,<4.0a0 - tk >=8.6.13,<8.7.0a0 - tzdata - ucrt >=10.0.20348.0 @@ -9438,21 +10620,21 @@ packages: - python_abi 3.11.* *_cp311 license: Python-2.0 purls: [] - size: 18514691 - timestamp: 1761172844103 -- conda: https://conda.anaconda.org/conda-forge/win-64/python-3.13.11-h09917c8_100_cp313.conda + size: 18416208 + timestamp: 1772728847666 +- conda: https://conda.anaconda.org/conda-forge/win-64/python-3.13.12-h09917c8_100_cp313.conda build_number: 100 - sha256: 0ee0402368783e1fad10025719530499c517a3dbbdfbe18351841d9b7aef1d6a - md5: 9e4c9a7ee9c4ab5b3778ab73e583283e + sha256: da70aec20ff5a5ae18bbba9fdd1e18190b419605cafaafb3bdad8becf11ce94d + md5: 4440c24966d0aa0c8f1e1d5006dac2d6 depends: - bzip2 >=1.0.8,<2.0a0 - libexpat >=2.7.3,<3.0a0 - libffi >=3.5.2,<3.6.0a0 - - liblzma >=5.8.1,<6.0a0 + - liblzma >=5.8.2,<6.0a0 - libmpdec >=4.0.0,<5.0a0 - - libsqlite >=3.51.1,<4.0a0 + - libsqlite >=3.51.2,<4.0a0 - libzlib >=1.3.1,<2.0a0 - - openssl >=3.5.4,<4.0a0 + - openssl >=3.5.5,<4.0a0 - python_abi 3.13.* *_cp313 - tk >=8.6.13,<8.7.0a0 - tzdata @@ -9461,8 +10643,8 @@ packages: - vc14_runtime >=14.44.35208 license: Python-2.0 purls: [] - size: 16617922 - timestamp: 1765019627175 + size: 16535316 + timestamp: 1770270322707 python_site_packages_path: Lib/site-packages - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl name: python-dateutil @@ -9471,17 +10653,35 @@ packages: requires_dist: - six>=1.5 requires_python: '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*' -- pypi: https://files.pythonhosted.org/packages/50/74/c655a6eda0fd188d490c14142a0f0380655ac7099604e1fbf8fa1a97f0a1/python_engineio-4.13.0-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/67/0f/019d3949a40280f6193b62bc010177d4ce702d0fce424322286488569cd3/python_discovery-1.2.1-py3-none-any.whl + name: python-discovery + version: 1.2.1 + sha256: b6a957b24c1cd79252484d3566d1b49527581d46e789aaf43181005e56201502 + 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/aa/54/0cce26da03a981f949bb8449c9778537f75f5917c172e1d2992ff25cb57d/python_engineio-4.13.1-py3-none-any.whl name: python-engineio - version: 4.13.0 - sha256: 57b94eac094fa07b050c6da59f48b12250ab1cd920765f4849963e3d89ad9de3 + 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.4 ; extra == 'asyncio-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/51/e5/fecf13f06e5e5f67e8837d777d1bc43fac0ed2b77a676804df5c34744727/python_json_logger-4.0.0-py3-none-any.whl name: python-json-logger @@ -9509,10 +10709,10 @@ packages: - mkdocs-literate-nav ; extra == 'dev' - mike ; extra == 'dev' requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/28/d2/2ccc2b69a187b80fda3152745670cfba936704f296a9fa54c6c8ac694d12/python_socketio-5.16.0-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/07/c7/deb8c5e604404dbf10a3808a858946ca3547692ff6316b698945bb72177e/python_socketio-5.16.1-py3-none-any.whl name: python-socketio - version: 5.16.0 - sha256: d95802961e15c7bd54ecf884c6e7644f81be8460f0a02ee66b473df58088ee8a + version: 5.16.1 + sha256: a3eb1702e92aa2f2b5d3ba00261b61f062cce51f1cfb6900bf3ab4d1934d2d35 requires_dist: - bidict>=0.21.0 - python-engineio>=4.11.0 @@ -9521,6 +10721,7 @@ packages: - aiohttp>=3.4 ; extra == 'asyncio-client' - tox ; extra == 'dev' - sphinx ; extra == 'docs' + - furo ; extra == 'docs' requires_python: '>=3.8' - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda build_number: 8 @@ -9533,19 +10734,44 @@ packages: purls: [] size: 7002 timestamp: 1752805902938 -- pypi: https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl - name: pytz - version: '2025.2' - sha256: 5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00 -- pypi: https://files.pythonhosted.org/packages/a6/a1/409c1651c9f874d598c10f51ff586c416625601df4bca315d08baec4c3e3/pywinpty-3.0.2-cp311-cp311-win_amd64.whl +- 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/51/8f/9bb81dd5bb77d22243d33c8397f09377056d5c687aa6d4042bea7fbf8364/pywin32-311-cp311-cp311-win_amd64.whl + name: pywin32 + version: '311' + sha256: 3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503 +- pypi: https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl + name: pywin32 + version: '311' + sha256: 718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d +- pypi: https://files.pythonhosted.org/packages/79/c3/3e75075c7f71735f22b66fab0481f2c98e3a4d58cba55cb50ba29114bcf6/pywinpty-3.0.3-cp311-cp311-win_amd64.whl name: pywinpty - version: 3.0.2 - sha256: 327790d70e4c841ebd9d0f295a780177149aeb405bca44c7115a3de5c2054b23 + version: 3.0.3 + sha256: dff25a9a6435f527d7c65608a7e62783fc12076e7d44487a4911ee91be5a8ac8 requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/fc/19/b757fe28008236a4a713e813283721b8a40aa60cd7d3f83549f2e25a3155/pywinpty-3.0.2-cp313-cp313-win_amd64.whl +- pypi: https://files.pythonhosted.org/packages/e5/cb/58d6ed3fd429c96a90ef01ac9a617af10a6d41469219c25e7dc162abbb71/pywinpty-3.0.3-cp313-cp313-win_amd64.whl name: pywinpty - version: 3.0.2 - sha256: 18f78b81e4cfee6aabe7ea8688441d30247b73e52cd9657138015c5f4ee13a51 + version: 3.0.3 + sha256: 9c91dbb026050c77bdcef964e63a4f10f01a639113c4d3658332614544c467ab requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl name: pyyaml @@ -9636,6 +10862,13 @@ packages: requires_dist: - cffi ; implementation_name == 'pypy' requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/3c/26/1062c7ec1b053db9e499b4d2d5bc231743201b74051c973dadeac80a8f43/questionary-2.1.1-py3-none-any.whl + name: questionary + version: 2.1.1 + sha256: a51af13f345f1cdea62347589fbb6df3b290306ab8930713bfae4d475a7d4a59 + requires_dist: + - prompt-toolkit>=2.0,<4.0 + requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/93/f7/d00d9b4a0313a6be3a3e0818e6375e15da6d7076f4ae47d1324e7ca986a1/radon-6.0.1-py2.py3-none-any.whl name: radon version: 6.0.1 @@ -9688,18 +10921,24 @@ packages: - rpds-py>=0.7.0 - typing-extensions>=4.4.0 ; python_full_version < '3.13' requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/56/5d/c814546c2333ceea4ba42262d8c4d55763003e767fa169adc693bd524478/requests-2.33.0-py3-none-any.whl name: requests - version: 2.32.5 - sha256: 2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6 + version: 2.33.0 + sha256: 3324635456fa185245e24865e810cecec7b4caf933d7eb133dcde67d48cee69b requires_dist: - charset-normalizer>=2,<4 - idna>=2.5,<4 - - urllib3>=1.21.1,<3 - - certifi>=2017.4.17 + - urllib3>=1.26,<3 + - certifi>=2023.5.7 - pysocks>=1.5.6,!=1.5.7 ; extra == 'socks' - - chardet>=3.0.2,<6 ; extra == 'use-chardet-on-py3' - requires_python: '>=3.9' + - chardet>=3.0.2,<8 ; extra == 'use-chardet-on-py3' + - pytest-httpbin==2.1.0 ; extra == 'test' + - pytest-cov ; extra == 'test' + - pytest-mock ; extra == 'test' + - pytest-xdist ; extra == 'test' + - pysocks>=1.5.6,!=1.5.7 ; extra == 'test' + - pytest>=3 ; extra == 'test' + requires_python: '>=3.10' - pypi: https://files.pythonhosted.org/packages/57/4d/a7545bf6c62b0dbe5795f22ea9e88cc070fdced5c34663ebc5bed2f610c0/returns-0.26.0-py3-none-any.whl name: returns version: 0.26.0 @@ -9730,10 +10969,10 @@ packages: - lark>=1.2.2 - pytest>=8.3.5 ; extra == 'testing' requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl name: rich - version: 14.2.0 - sha256: 76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd + version: 14.3.3 + sha256: 793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d requires_dist: - ipywidgets>=7.5.1,<9 ; extra == 'jupyter' - markdown-it-py>=2.2.0 @@ -9779,32 +11018,296 @@ packages: version: 0.30.0 sha256: e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8 requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/7b/82/36be59f00a6082e38c23536df4e71cdbc6af8d7c707eade97fcad5c98235/ruff-0.14.10-py3-none-macosx_11_0_arm64.whl +- pypi: https://files.pythonhosted.org/packages/1f/a2/ef467cb77099062317154c63f234b8a7baf7cb690b99af760c5b68b9ee7f/ruff-0.15.8-py3-none-win_amd64.whl name: ruff - version: 0.14.10 - sha256: d85713d522348837ef9df8efca33ccb8bd6fcfc86a2cde3ccb4bc9d28a18003d + version: 0.15.8 + sha256: 04f79eff02a72db209d47d665ba7ebcad609d8918a134f86cb13dd132159fc89 requires_python: '>=3.7' -- pypi: https://files.pythonhosted.org/packages/b3/19/9e050c0dca8aba824d67cc0db69fb459c28d8cd3f6855b1405b3f29cc91d/ruff-0.14.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl +- pypi: https://files.pythonhosted.org/packages/ca/f2/7a631a8af6d88bcef997eb1bf87cc3da158294c57044aafd3e17030613de/ruff-0.15.8-py3-none-macosx_11_0_arm64.whl name: ruff - version: 0.14.10 - sha256: 59aabd2e2c4fd614d2862e7939c34a532c04f1084476d6833dddef4afab87e9f + version: 0.15.8 + sha256: 6ee3ae5c65a42f273f126686353f2e08ff29927b7b7e203b711514370d500de3 requires_python: '>=3.7' -- pypi: https://files.pythonhosted.org/packages/df/58/a0349197a7dfa603ffb7f5b0470391efa79ddc327c1e29c4851e85b09cc5/ruff-0.14.10-py3-none-macosx_10_12_x86_64.whl +- pypi: https://files.pythonhosted.org/packages/eb/92/f1c662784d149ad1414cae450b082cf736430c12ca78367f20f5ed569d65/ruff-0.15.8-py3-none-macosx_10_12_x86_64.whl name: ruff - version: 0.14.10 - sha256: 674f9be9372907f7257c51f1d4fc902cb7cf014b9980152b802794317941f08f + version: 0.15.8 + sha256: d3e3d0b6ba8dca1b7ef9ab80a28e840a20070c4b62e56d675c24f366ef330570 requires_python: '>=3.7' -- pypi: https://files.pythonhosted.org/packages/fb/9c/896c862e13886fae2af961bef3e6312db9ebc6adc2b156fe95e615dee8c1/ruff-0.14.10-py3-none-win_amd64.whl +- pypi: https://files.pythonhosted.org/packages/f8/22/d7f2fabdba4fae9f3b570e5605d5eb4500dcb7b770d3217dca4428484b17/ruff-0.15.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl name: ruff - version: 0.14.10 - sha256: 466297bd73638c6bdf06485683e812db1c00c7ac96d4ddd0294a338c62fdc154 + version: 0.15.8 + sha256: 0f29b989a55572fb885b77464cf24af05500806ab4edf9a0fd8977f9759d85b1 requires_python: '>=3.7' -- pypi: https://files.pythonhosted.org/packages/21/f6/4bfb5695d8941e5c570a04d9fcd0d36bce7511b7d78e6e75c8f9791f82d0/scipy-1.16.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl +- 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/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/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/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/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/81/21/4962b1daddf0422e56c5ed4c41bea1ccb6d2a9ab72b795196835a20969c7/scipp-26.3.1-cp311-cp311-macosx_14_0_x86_64.whl + name: scipp + version: 26.3.1 + sha256: 7c90e78fcba1d272df059fc01350c9e18f017aec26369b03def723a3702d763d + 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/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/e2/69/1dcb8e967f62759578938db5b29792b82ea8939a2d712e79491fa3e1cf0a/scipp-26.3.1-cp313-cp313-macosx_14_0_x86_64.whl + name: scipp + version: 26.3.1 + sha256: 4c9c8632ba24ce74bd98430a1376310fa5b3fcd2c3467a7e6a484ebb091e915f + 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/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/fb/46/e50b38629e9e3f4a1dd55fb36d8b8abd1d59768c31151c1c8ed696f7b865/scippneutron-26.3.0-py3-none-any.whl + name: scippneutron + version: 26.3.0 + sha256: 6bc9e36f68059bb792460cc897e6247236289f170134a953ed9fee8578872dd7 + requires_dist: + - python-dateutil>=2.8 + - email-validator>=2 + - h5py>=3.12 + - lazy-loader>=0.4 + - mpltoolbox>=24.6.0 + - numpy>=1.20 + - plopp>=24.9.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/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/09/7d/af933f0f6e0767995b4e2d705a0665e454d1c19402aa7e895de3951ebb04/scipy-1.17.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl name: scipy - version: 1.16.3 - sha256: 7dc1360c06535ea6116a2220f760ae572db9f661aba2d88074fe30ec2aa1ff88 + version: 1.17.1 + sha256: 43af8d1f3bea642559019edfe64e9b11192a8978efbd1539d7bc2aaa23d92de4 requires_dist: - - numpy>=1.25.2,<2.6 + - numpy>=1.26.4,<2.7 - pytest>=8.0.0 ; extra == 'test' - pytest-cov ; extra == 'test' - pytest-timeout ; extra == 'test' @@ -9833,22 +11336,22 @@ packages: - 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.0.292 ; extra == 'dev' + - ruff>=0.12.0 ; extra == 'dev' - cython-lint>=0.12.2 ; extra == 'dev' - - rich-click ; extra == 'dev' - - doit>=0.36.0 ; extra == 'dev' - - pydevtool ; extra == 'dev' requires_python: '>=3.11' -- pypi: https://files.pythonhosted.org/packages/44/13/7e63cfba8a7452eb756306aa2fd9b37a29a323b672b964b4fdeded9a3f21/scipy-1.16.3-cp313-cp313-macosx_12_0_arm64.whl +- pypi: https://files.pythonhosted.org/packages/35/e5/d6d0e51fc888f692a35134336866341c08655d92614f492c6860dc45bb2c/scipy-1.17.1-cp313-cp313-win_amd64.whl name: scipy - version: 1.16.3 - sha256: 16b8bc35a4cc24db80a0ec836a9286d0e31b2503cb2fd7ff7fb0e0374a97081d + version: 1.17.1 + sha256: 37425bc9175607b0268f493d79a292c39f9d001a357bebb6b88fdfaff13f6448 requires_dist: - - numpy>=1.25.2,<2.6 + - numpy>=1.26.4,<2.7 - pytest>=8.0.0 ; extra == 'test' - pytest-cov ; extra == 'test' - pytest-timeout ; extra == 'test' @@ -9877,22 +11380,22 @@ packages: - 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.0.292 ; extra == 'dev' + - ruff>=0.12.0 ; extra == 'dev' - cython-lint>=0.12.2 ; extra == 'dev' - - rich-click ; extra == 'dev' - - doit>=0.36.0 ; extra == 'dev' - - pydevtool ; extra == 'dev' requires_python: '>=3.11' -- pypi: https://files.pythonhosted.org/packages/72/f1/57e8327ab1508272029e27eeef34f2302ffc156b69e7e233e906c2a5c379/scipy-1.16.3-cp313-cp313-macosx_10_14_x86_64.whl +- pypi: https://files.pythonhosted.org/packages/5b/58/3ce96251560107b381cbd6e8413c483bbb1228a6b919fa8652b0d4090e7f/scipy-1.17.1-cp313-cp313-macosx_14_0_arm64.whl name: scipy - version: 1.16.3 - sha256: d2ec56337675e61b312179a1ad124f5f570c00f920cc75e1000025451b88241c + version: 1.17.1 + sha256: 7ff200bf9d24f2e4d5dc6ee8c3ac64d739d3a89e2326ba68aaf6c4a2b838fd7d requires_dist: - - numpy>=1.25.2,<2.6 + - numpy>=1.26.4,<2.7 - pytest>=8.0.0 ; extra == 'test' - pytest-cov ; extra == 'test' - pytest-timeout ; extra == 'test' @@ -9921,22 +11424,22 @@ packages: - 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.0.292 ; extra == 'dev' + - ruff>=0.12.0 ; extra == 'dev' - cython-lint>=0.12.2 ; extra == 'dev' - - rich-click ; extra == 'dev' - - doit>=0.36.0 ; extra == 'dev' - - pydevtool ; extra == 'dev' requires_python: '>=3.11' -- pypi: https://files.pythonhosted.org/packages/7c/89/d70e9f628749b7e4db2aa4cd89735502ff3f08f7b9b27d2e799485987cd9/scipy-1.16.3-cp311-cp311-macosx_12_0_arm64.whl +- pypi: https://files.pythonhosted.org/packages/6d/ee/18146b7757ed4976276b9c9819108adbc73c5aad636e5353e20746b73069/scipy-1.17.1-cp311-cp311-macosx_14_0_arm64.whl name: scipy - version: 1.16.3 - sha256: 8be1ca9170fcb6223cc7c27f4305d680ded114a1567c0bd2bfcbf947d1b17511 + version: 1.17.1 + sha256: a3472cfbca0a54177d0faa68f697d8ba4c80bbdc19908c3465556d9f7efce9ee requires_dist: - - numpy>=1.25.2,<2.6 + - numpy>=1.26.4,<2.7 - pytest>=8.0.0 ; extra == 'test' - pytest-cov ; extra == 'test' - pytest-timeout ; extra == 'test' @@ -9965,22 +11468,22 @@ packages: - 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.0.292 ; extra == 'dev' + - ruff>=0.12.0 ; extra == 'dev' - cython-lint>=0.12.2 ; extra == 'dev' - - rich-click ; extra == 'dev' - - doit>=0.36.0 ; extra == 'dev' - - pydevtool ; extra == 'dev' requires_python: '>=3.11' -- pypi: https://files.pythonhosted.org/packages/9b/5f/6f37d7439de1455ce9c5a556b8d1db0979f03a796c030bafdf08d35b7bf9/scipy-1.16.3-cp311-cp311-macosx_10_14_x86_64.whl +- pypi: https://files.pythonhosted.org/packages/95/da/0d1df507cf574b3f224ccc3d45244c9a1d732c81dcb26b1e8a766ae271a8/scipy-1.17.1-cp311-cp311-win_amd64.whl name: scipy - version: 1.16.3 - sha256: 40be6cf99e68b6c4321e9f8782e7d5ff8265af28ef2cd56e9c9b2638fa08ad97 + version: 1.17.1 + sha256: d30e57c72013c2a4fe441c2fcb8e77b14e152ad48b5464858e07e2ad9fbfceff requires_dist: - - numpy>=1.25.2,<2.6 + - numpy>=1.26.4,<2.7 - pytest>=8.0.0 ; extra == 'test' - pytest-cov ; extra == 'test' - pytest-timeout ; extra == 'test' @@ -10009,22 +11512,22 @@ packages: - 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.0.292 ; extra == 'dev' + - ruff>=0.12.0 ; extra == 'dev' - cython-lint>=0.12.2 ; extra == 'dev' - - rich-click ; extra == 'dev' - - doit>=0.36.0 ; extra == 'dev' - - pydevtool ; extra == 'dev' requires_python: '>=3.11' -- pypi: https://files.pythonhosted.org/packages/ca/6e/8942461cf2636cdae083e3eb72622a7fbbfa5cf559c7d13ab250a5dbdc01/scipy-1.16.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl +- pypi: https://files.pythonhosted.org/packages/b2/83/15087d945e0e4d48ce2377498abf5ad171ae013232ae31d06f336e64c999/scipy-1.17.1-cp313-cp313-macosx_14_0_x86_64.whl name: scipy - version: 1.16.3 - sha256: 0151a0749efeaaab78711c78422d413c583b8cdd2011a3c1d6c794938ee9fdb2 + version: 1.17.1 + sha256: 4b400bdc6f79fa02a4d86640310dde87a21fba0c979efff5248908c6f15fad1b requires_dist: - - numpy>=1.25.2,<2.6 + - numpy>=1.26.4,<2.7 - pytest>=8.0.0 ; extra == 'test' - pytest-cov ; extra == 'test' - pytest-timeout ; extra == 'test' @@ -10053,22 +11556,22 @@ packages: - 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.0.292 ; extra == 'dev' + - ruff>=0.12.0 ; extra == 'dev' - cython-lint>=0.12.2 ; extra == 'dev' - - rich-click ; extra == 'dev' - - doit>=0.36.0 ; extra == 'dev' - - pydevtool ; extra == 'dev' requires_python: '>=3.11' -- pypi: https://files.pythonhosted.org/packages/cd/01/1204382461fcbfeb05b6161b594f4007e78b6eba9b375382f79153172b4d/scipy-1.16.3-cp313-cp313-win_amd64.whl +- pypi: https://files.pythonhosted.org/packages/ec/e6/cef1cf3557f0c54954198554a10016b6a03b2ec9e22a4e1df734936bd99c/scipy-1.17.1-cp311-cp311-macosx_14_0_x86_64.whl name: scipy - version: 1.16.3 - sha256: 062246acacbe9f8210de8e751b16fc37458213f124bef161a5a02c7a39284304 + version: 1.17.1 + sha256: 766e0dc5a616d026a3a1cffa379af959671729083882f50307e18175797b3dfd requires_dist: - - numpy>=1.25.2,<2.6 + - numpy>=1.26.4,<2.7 - pytest>=8.0.0 ; extra == 'test' - pytest-cov ; extra == 'test' - pytest-timeout ; extra == 'test' @@ -10097,22 +11600,22 @@ packages: - 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.0.292 ; extra == 'dev' + - ruff>=0.12.0 ; extra == 'dev' - cython-lint>=0.12.2 ; extra == 'dev' - - rich-click ; extra == 'dev' - - doit>=0.36.0 ; extra == 'dev' - - pydevtool ; extra == 'dev' requires_python: '>=3.11' -- pypi: https://files.pythonhosted.org/packages/f1/d0/22ec7036ba0b0a35bccb7f25ab407382ed34af0b111475eb301c16f8a2e5/scipy-1.16.3-cp311-cp311-win_amd64.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 name: scipy - version: 1.16.3 - sha256: 53c3844d527213631e886621df5695d35e4f6a75f620dca412bcd292f6b87d78 + version: 1.17.1 + sha256: 581b2264fc0aa555f3f435a5944da7504ea3a065d7029ad60e7c3d1ae09c5464 requires_dist: - - numpy>=1.25.2,<2.6 + - numpy>=1.26.4,<2.7 - pytest>=8.0.0 ; extra == 'test' - pytest-cov ; extra == 'test' - pytest-timeout ; extra == 'test' @@ -10141,30 +11644,29 @@ packages: - 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.0.292 ; extra == 'dev' + - ruff>=0.12.0 ; extra == 'dev' - cython-lint>=0.12.2 ; extra == 'dev' - - rich-click ; extra == 'dev' - - doit>=0.36.0 ; extra == 'dev' - - pydevtool ; extra == 'dev' requires_python: '>=3.11' -- pypi: https://files.pythonhosted.org/packages/1b/5a/f2f2e5eda25579f754acd83399c522ee03d6acbe001dfe53c8a1ec928b44/send2trash-2.0.0-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/1c/78/504fdd027da3b84ff1aecd9f6957e65f35134534ccc6da8628eb71e76d3f/send2trash-2.1.0-py3-none-any.whl name: send2trash - version: 2.0.0 - sha256: e70d5ce41dbb890882cc78bc25d137478330b39a391e756fadf82e34da4d85b8 - requires_dist: - - pywin32 ; sys_platform == 'win32' and extra == 'win32' - - pyobjc-framework-cocoa ; sys_platform == 'darwin' and extra == 'objc' - - pywin32 ; sys_platform == 'win32' and extra == 'nativelib' - - pyobjc-framework-cocoa ; sys_platform == 'darwin' and extra == 'nativelib' - requires_python: '!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,!=3.7.*' -- pypi: https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl + version: 2.1.0 + sha256: 0da2f112e6d6bb22de6aa6daa7e144831a4febf2a87261451c4ad849fe9a873c + requires_dist: + - pytest>=8 ; extra == 'test' + - pywin32>=305 ; sys_platform == 'win32' and extra == 'nativelib' + - pyobjc>=9.0 ; sys_platform == 'darwin' and extra == 'nativelib' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/9d/76/f789f7a86709c6b087c5a2f52f911838cad707cc613162401badc665acfe/setuptools-82.0.1-py3-none-any.whl name: setuptools - version: 80.9.0 - sha256: 062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922 + version: 82.0.1 + sha256: a59e362652f08dcd477c78bb6e7bd9d80a7995bc73ce773050228a348ce2e5bb requires_dist: - pytest>=6,!=8.1.* ; extra == 'test' - virtualenv>=13.0.0 ; extra == 'test' @@ -10205,16 +11707,15 @@ packages: - importlib-metadata>=6 ; python_full_version < '3.10' and extra == 'core' - tomli>=2.0.1 ; python_full_version < '3.11' and extra == 'core' - wheel>=0.43.0 ; extra == 'core' - - platformdirs>=4.2.2 ; extra == 'core' - jaraco-functools>=4 ; extra == 'core' - more-itertools ; extra == 'core' - pytest-checkdocs>=2.4 ; extra == 'check' - pytest-ruff>=0.2.1 ; sys_platform != 'cygwin' and extra == 'check' - - ruff>=0.8.0 ; sys_platform != 'cygwin' and extra == 'check' + - ruff>=0.13.0 ; sys_platform != 'cygwin' and extra == 'check' - pytest-cov ; extra == 'cover' - pytest-enabler>=2.2 ; extra == 'enabler' - pytest-mypy ; extra == 'type' - - mypy==1.14.* ; extra == 'type' + - mypy==1.18.* ; extra == 'type' - importlib-metadata>=7.0.2 ; python_full_version < '3.10' and extra == 'type' - jaraco-develop>=7.21 ; sys_platform != 'cygwin' and extra == 'type' requires_python: '>=3.9' @@ -10240,15 +11741,288 @@ packages: version: 1.17.0 sha256: 4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 requires_python: '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*' -- pypi: https://files.pythonhosted.org/packages/48/f3/b67d6ea49ca9154453b6d70b34ea22f3996b9fa55da105a79d8732227adc/soupsieve-2.8.1-py3-none-any.whl +- 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/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-py3-none-any.whl name: soupsieve - version: 2.8.1 - sha256: a11fe2a6f3d76ab3cf2de04eb339c1be5b506a8a47f2ceb6d139803177f85434 + version: 2.8.3 + sha256: ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95 + 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/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/1b/a5/174d33068d4383df4be9ee1ea9251f17820a622f3be744ca2ab7334818ee/spglib-2.6.0-cp313-cp313-macosx_10_13_x86_64.whl + name: spglib + version: 2.6.0 + sha256: d7872819b6448024ecf7dfccac45147d8cac839bdfff6763b92784aae63bd139 + 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/25/a8/d89e1bde525baba10eb8d0be79a5bbaf56c59a47b32bb954866d96a228e3/spglib-2.6.0-cp311-cp311-win_amd64.whl + name: spglib + version: 2.6.0 + sha256: 9a72daeefcabf62ca88eff77adacf5b2b9d91a17c4f93d61d86f635476de8400 + 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/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/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/55/41/591cd1e94254c20f00bb1f32c0b1a6de68c03d54e6daf78dd7b146d0b3fc/spglib-2.6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + name: spglib + version: 2.6.0 + sha256: ff6b2f21226f7ece7758eb1d320907168018aa0a30a57c2b0a24cbf8f6860211 + 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/6c/44/30888e2a5b2fa2e6df18606b442cb8b126b0bea5a2f1ec4a2a82538ffecf/spglib-2.6.0-cp311-cp311-macosx_10_9_x86_64.whl + name: spglib + version: 2.6.0 + sha256: c22d87849e1086cbe88399c08c93b4e7babec92c1db49f15ef8b081339b67e25 + 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/0e/50/80a8d080ac7d3d321e5e5d420c9a522b0aa770ec7013ea91f9a8b7d36e4a/sqlalchemy-2.0.45-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl +- pypi: https://files.pythonhosted.org/packages/f6/ca/270d463f6c34f539bb55acdab14099c092d3be28c8af64d61399aa07610c/spglib-2.6.0-cp311-cp311-macosx_11_0_arm64.whl + name: spglib + version: 2.6.0 + sha256: 02d2e730a3b2cb43e318944493d0c288592b0e2ddbab0d222a548312659ee22a + 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/21/dd/3b7c53f1dbbf736fd27041aee68f8ac52226b610f914085b1652c2323442/sqlalchemy-2.0.48-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + name: sqlalchemy + version: 2.0.48 + sha256: 6f7b7243850edd0b8b97043f04748f31de50cf426e939def5c16bedb540698f7 + 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/46/2c/9664130905f03db57961b8980b05cab624afd114bf2be2576628a9f22da4/sqlalchemy-2.0.48-py3-none-any.whl name: sqlalchemy - version: 2.0.45 - sha256: 672c45cae53ba88e0dad74b9027dddd09ef6f441e927786b05bec75d949fbb2e + version: 2.0.48 + sha256: a66fe406437dd65cacd96a72689a3aaaecaebbcd62d81c5ac1c0fdbeac835096 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' @@ -10283,10 +12057,10 @@ packages: - typing-extensions!=3.10.0.1 ; extra == 'aiosqlite' - sqlcipher3-binary ; extra == 'sqlcipher' requires_python: '>=3.7' -- pypi: https://files.pythonhosted.org/packages/a2/1c/769552a9d840065137272ebe86ffbb0bc92b0f1e0a68ee5266a225f8cd7b/sqlalchemy-2.0.45-cp311-cp311-macosx_11_0_arm64.whl +- pypi: https://files.pythonhosted.org/packages/58/d5/dd767277f6feef12d05651538f280277e661698f617fa4d086cce6055416/sqlalchemy-2.0.48-cp311-cp311-win_amd64.whl name: sqlalchemy - version: 2.0.45 - sha256: 2e90a344c644a4fa871eb01809c32096487928bd2038bf10f3e4515cb688cc56 + version: 2.0.48 + sha256: 583849c743e0e3c9bb7446f5b5addeacedc168d657a69b418063dfdb2d90081c 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' @@ -10321,10 +12095,10 @@ packages: - typing-extensions!=3.10.0.1 ; extra == 'aiosqlite' - sqlcipher3-binary ; extra == 'sqlcipher' requires_python: '>=3.7' -- pypi: https://files.pythonhosted.org/packages/bc/fa/09d0a11fe9f15c7fa5c7f0dd26be3d235b0c0cbf2f9544f43bc42efc8a24/sqlalchemy-2.0.45-cp311-cp311-win_amd64.whl +- pypi: https://files.pythonhosted.org/packages/b7/2b/b9040bec58c58225f073f5b0c1870defe1940835549dafec680cbd58c3c3/sqlalchemy-2.0.48-cp313-cp313-win_amd64.whl name: sqlalchemy - version: 2.0.45 - sha256: a15b98adb7f277316f2c276c090259129ee4afca783495e212048daf846654b2 + version: 2.0.48 + sha256: d612c976cbc2d17edfcc4c006874b764e85e990c29ce9bd411f926bbfb02b9a2 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' @@ -10359,10 +12133,10 @@ packages: - typing-extensions!=3.10.0.1 ; extra == 'aiosqlite' - sqlcipher3-binary ; extra == 'sqlcipher' requires_python: '>=3.7' -- pypi: https://files.pythonhosted.org/packages/bf/e1/3ccb13c643399d22289c6a9786c1a91e3dcbb68bce4beb44926ac2c557bf/sqlalchemy-2.0.45-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/d1/c6/569dc8bf3cd375abc5907e82235923e986799f301cd79a903f784b996fca/sqlalchemy-2.0.48-cp313-cp313-macosx_11_0_arm64.whl name: sqlalchemy - version: 2.0.45 - sha256: 5225a288e4c8cc2308dbdd874edad6e7d0fd38eac1e9e5f23503425c8eee20d0 + version: 2.0.48 + sha256: e3070c03701037aa418b55d36532ecb8f8446ed0135acb71c678dbdf12f5b6e4 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' @@ -10397,10 +12171,10 @@ packages: - typing-extensions!=3.10.0.1 ; extra == 'aiosqlite' - sqlcipher3-binary ; extra == 'sqlcipher' requires_python: '>=3.7' -- pypi: https://files.pythonhosted.org/packages/c0/c5/d17113020b2d43073412aeca09b60d2009442420372123b8d49cc253f8b8/sqlalchemy-2.0.45-cp313-cp313-win_amd64.whl +- pypi: https://files.pythonhosted.org/packages/d7/6d/b8b78b5b80f3c3ab3f7fa90faa195ec3401f6d884b60221260fd4d51864c/sqlalchemy-2.0.48-cp311-cp311-macosx_11_0_arm64.whl name: sqlalchemy - version: 2.0.45 - sha256: afbf47dc4de31fa38fd491f3705cac5307d21d4bb828a4f020ee59af412744ee + version: 2.0.48 + sha256: 1b4c575df7368b3b13e0cebf01d4679f9a28ed2ae6c1cd0b1d5beffb6b2007dc 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' @@ -10435,10 +12209,10 @@ packages: - typing-extensions!=3.10.0.1 ; extra == 'aiosqlite' - sqlcipher3-binary ; extra == 'sqlcipher' requires_python: '>=3.7' -- pypi: https://files.pythonhosted.org/packages/f6/2b/60ce3ee7a5ae172bfcd419ce23259bb874d2cddd44f67c5df3760a1e22f9/sqlalchemy-2.0.45-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl +- pypi: https://files.pythonhosted.org/packages/fe/88/cb59509e4668d8001818d7355d9995be90c321313078c912420603a7cb95/sqlalchemy-2.0.48-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl name: sqlalchemy - version: 2.0.45 - sha256: 12c694ed6468333a090d2f60950e4250b928f457e4962389553d6ba5fe9951ac + version: 2.0.48 + sha256: b19151e76620a412c2ac1c6f977ab1b9fa7ad43140178345136456d5265b32ed 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' @@ -10495,26 +12269,26 @@ packages: - pytest>=7.1.0 ; extra == 'dev' - hypothesis>=6.70.0 ; extra == 'dev' requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl name: tabulate - version: 0.9.0 - sha256: 024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f + version: 0.10.0 + sha256: f0b0622e567335c8fabaaa659f1b33bcb6ddfe2e496071b743aa113f8774f2d3 requires_dist: - wcwidth ; extra == 'widechars' - requires_python: '>=3.7' -- conda: https://conda.anaconda.org/conda-forge/win-64/tbb-2022.3.0-hd094cb3_1.conda - sha256: c31cac57913a699745d124cdc016a63e31c5749f16f60b3202414d071fc50573 - md5: 17c38aaf14c640b85c4617ccb59c1146 + requires_python: '>=3.10' +- conda: https://conda.anaconda.org/conda-forge/win-64/tbb-2022.3.0-h3155e25_2.conda + sha256: abd9a489f059fba85c8ffa1abdaa4d515d6de6a3325238b8e81203b913cf65a9 + md5: 0f9817ffbe25f9e69ceba5ea70c52606 depends: - - libhwloc >=2.12.1,<2.12.2.0a0 + - libhwloc >=2.12.2,<2.12.3.0a0 - ucrt >=10.0.20348.0 - vc >=14.3,<15 - vc14_runtime >=14.44.35208 license: Apache-2.0 license_family: APACHE purls: [] - size: 155714 - timestamp: 1762510341121 + size: 155869 + timestamp: 1767886839029 - pypi: https://files.pythonhosted.org/packages/6a/9e/2064975477fdc887e47ad42157e214526dcad8f317a948dee17e1659a62f/terminado-0.18.1-py3-none-any.whl name: terminado version: 0.18.1 @@ -10543,118 +12317,135 @@ packages: - pytest ; extra == 'test' - ruff ; extra == 'test' requires_python: '>=3.8' -- conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_ha0e22de_103.conda - sha256: 1544760538a40bcd8ace2b1d8ebe3eb5807ac268641f8acdc18c69c5ebfeaf64 - md5: 86bc20552bf46075e3d92b67f089172d +- 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 >=13 + - libgcc >=14 - libzlib >=1.3.1,<2.0a0 constrains: - xorg-libx11 >=1.8.12,<2.0a0 license: TCL license_family: BSD purls: [] - size: 3284905 - timestamp: 1763054914403 -- conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-hf689a15_3.conda - sha256: 0d0b6cef83fec41bc0eb4f3b761c4621b7adfb14378051a8177bd9bb73d26779 - md5: bd9f1de651dbd80b51281c694827f78f + size: 3301196 + timestamp: 1769460227866 +- conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-h7142dee_3.conda + sha256: 7f0d9c320288532873e2d8486c331ec6d87919c9028208d3f6ac91dc8f99a67b + md5: 6e6efb7463f8cef69dbcb4c2205bf60e depends: - __osx >=10.13 - libzlib >=1.3.1,<2.0a0 license: TCL license_family: BSD purls: [] - size: 3262702 - timestamp: 1763055085507 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h892fb3f_3.conda - sha256: ad0c67cb03c163a109820dc9ecf77faf6ec7150e942d1e8bb13e5d39dc058ab7 - md5: a73d54a5abba6543cb2f0af1bfbd6851 + size: 3282953 + timestamp: 1769460532442 +- 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: 3125484 - timestamp: 1763055028377 -- conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h2c6b04d_3.conda - sha256: 4581f4ffb432fefa1ac4f85c5682cc27014bcd66e7beaa0ee330e927a7858790 - md5: 7cb36e506a7dba4817970f8adb6396f9 + size: 3127137 + timestamp: 1769460817696 +- 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.2,<15 - - vc14_runtime >=14.29.30139 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 license: TCL license_family: BSD purls: [] - size: 3472313 - timestamp: 1763055164278 + size: 3526350 + timestamp: 1769460339384 +- 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/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/0a/fe/3d3420c4cb1ad9cb462fb52967080575f15898da97e21cb6f1361d505383/tomli-2.3.0-cp311-cp311-win_amd64.whl +- pypi: https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl name: tomli - version: 2.3.0 - sha256: 4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be + version: 2.4.1 + sha256: 36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54 requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/86/7f/d8fffe6a7aefdb61bced88fcb5e280cfd71e08939da5894161bd71bea022/tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl +- pypi: https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl name: tomli - version: 2.3.0 - sha256: 883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba + version: 2.4.1 + sha256: f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/89/48/06ee6eabe4fdd9ecd48bf488f4ac783844fd777f547b8d1b61c11939974e/tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl +- pypi: https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl name: tomli - version: 2.3.0 - sha256: 5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b + version: 2.4.1 + sha256: eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/89/da/75dfd804fc11e6612846758a23f13271b76d577e299592b4371a4ca4cd09/tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl +- pypi: https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl name: tomli - version: 2.3.0 - sha256: a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441 + version: 2.4.1 + sha256: 5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/b2/b7/718cd1da0884f281f95ccfa3a6cc572d30053cba64603f79d431d3c9b61b/tomli-2.3.0-cp313-cp313-win_amd64.whl +- pypi: https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl name: tomli - version: 2.3.0 - sha256: 0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999 + version: 2.4.1 + sha256: 5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9 requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/b3/2e/299f62b401438d5fe1624119c723f5d877acc86a4c2492da405626665f12/tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl +- pypi: https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl name: tomli - version: 2.3.0 - sha256: 88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45 + version: 2.4.1 + sha256: 8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36 requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/d5/f4/0fbd014909748706c01d16824eadb0307115f9562a15cbb012cd9b3512c5/tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl +- pypi: https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl name: tomli - version: 2.3.0 - sha256: 4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf + version: 2.4.1 + sha256: 4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/f1/01/88793757d54d8937015c75dcdfb673c65471945f6be98e6a0410fba167ed/tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl +- pypi: https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl name: tomli - version: 2.3.0 - sha256: be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae + version: 2.4.1 + sha256: f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30 requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/50/d4/e51d52047e7eb9a582da59f32125d17c0482d065afd5d3bc435ff2120dc5/tornado-6.5.4-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl +- 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/2c/23/f6c6112a04d28eed765e374435fb1a9198f73e1ec4b4024184f21faeb1ad/tornado-6.5.5-cp39-abi3-win_amd64.whl name: tornado - version: 6.5.4 - sha256: e5fb5e04efa54cf0baabdd10061eb4148e0be137166146fff835745f59ab9f7f + version: 6.5.5 + sha256: 6443a794ba961a9f619b1ae926a2e900ac20c34483eea67be4ed8f1e58d3ef7b requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/ab/a9/e94a9d5224107d7ce3cc1fab8d5dc97f5ea351ccc6322ee4fb661da94e35/tornado-6.5.4-cp39-abi3-macosx_10_9_universal2.whl +- pypi: https://files.pythonhosted.org/packages/59/8c/77f5097695f4dd8255ecbd08b2a1ed8ba8b953d337804dd7080f199e12bf/tornado-6.5.5-cp39-abi3-macosx_10_9_universal2.whl name: tornado - version: 6.5.4 - sha256: d6241c1a16b1c9e4cc28148b1cda97dd1c6cb4fb7068ac1bedc610768dff0ba9 + version: 6.5.5 + sha256: 487dc9cc380e29f58c7ab88f9e27cdeef04b2140862e5076a66fb6bb68bb1bfa requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/d6/6d/c69be695a0a64fd37a97db12355a035a6d90f79067a3cf936ec2b1dc38cd/tornado-6.5.4-cp39-abi3-win_amd64.whl +- pypi: https://files.pythonhosted.org/packages/ab/5e/7625b76cd10f98f1516c36ce0346de62061156352353ef2da44e5c21523c/tornado-6.5.5-cp39-abi3-macosx_10_9_x86_64.whl name: tornado - version: 6.5.4 - sha256: fa07d31e0cd85c60713f2b995da613588aa03e1303d75705dca6af8babc18ddc + version: 6.5.5 + sha256: 65a7f1d46d4bb41df1ac99f5fcb685fb25c7e61613742d5108b010975a9a6521 requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/db/7e/f7b8d8c4453f305a51f80dbb49014257bb7d28ccb4bbb8dd328ea995ecad/tornado-6.5.4-cp39-abi3-macosx_10_9_x86_64.whl +- pypi: https://files.pythonhosted.org/packages/b2/04/7b5705d5b3c0fab088f434f9c83edac1573830ca49ccf29fb83bf7178eec/tornado-6.5.5-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl name: tornado - version: 6.5.4 - sha256: 2d50f63dda1d2cac3ae1fa23d254e16b5e38153758470e9956cbc3d813d40843 + version: 6.5.5 + sha256: e74c92e8e65086b338fd56333fb9a68b9f6f2fe7ad532645a290a464bcf46be5 requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl name: traitlets @@ -10671,33 +12462,50 @@ packages: - pytest-mypy-testing ; extra == 'test' - pytest>=7.0,<8.2 ; extra == 'test' requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/4f/7e/bc19996fa86cad8801e8ffe6f1bba5836ca0160df76d0410d27432193712/trove_classifiers-2025.12.1.14-py3-none-any.whl +- 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/bb/4a/2e5583e544bc437d5e8e54b47db87430df9031b29b48d17f26d129fa60c0/trove_classifiers-2026.1.14.14-py3-none-any.whl name: trove-classifiers - version: 2025.12.1.14 - sha256: a8206978ede95937b9959c3aff3eb258bbf7b07dff391ddd4ea7e61f316635ab -- pypi: https://files.pythonhosted.org/packages/1b/a9/e3aee762739c1d7528da1c3e06d518503f8b6c439c35549b53735ba52ead/typeguard-4.4.4-py3-none-any.whl + version: 2026.1.14.14 + sha256: 1f9553927f18d0513d8e5ff80ab8980b8202ce37ecae0e3274ed2ef11880e74d +- pypi: https://files.pythonhosted.org/packages/91/88/b55b3117287a8540b76dbdd87733808d4d01c8067a3b339408c250bb3600/typeguard-4.5.1-py3-none-any.whl name: typeguard - version: 4.4.4 - sha256: b5f562281b6bfa1f5492470464730ef001646128b180769880468bd84b68b09e + 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/a0/1d/d9257dd49ff2ca23ea5f132edf1281a0c4f9de8a762b9ae399b670a59235/typer-0.21.1-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/4a/91/48db081e7a63bb37284f9fbcefda7c44c277b18b0e13fbc36ea2335b71e6/typer-0.24.1-py3-none-any.whl name: typer - version: 0.21.1 - sha256: 7985e89081c636b88d172c2ee0cfe33c253160994d47bdfdc302defd7d1f1d01 + version: 0.24.1 + sha256: 112c1f0ce578bfb4cab9ffdabc68f031416ebcc216536611ba21f04e9aa84c9e requires_dist: - - click>=8.0.0 - - typing-extensions>=3.7.4.3 + - click>=8.2.1 - shellingham>=1.3.0 - - rich>=10.11.0 - requires_python: '>=3.9' + - rich>=12.3.0 + - annotated-doc>=0.0.2 + requires_python: '>=3.10' - pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl name: typing-extensions version: 4.15.0 sha256: f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548 requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl + name: typing-inspection + version: 0.4.2 + sha256: 4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7 + requires_dist: + - typing-extensions>=4.12.0 + requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl name: tzdata version: '2025.3' @@ -10735,10 +12543,6 @@ packages: - python-docs-theme ; extra == 'doc' - uncertainties[arrays,doc,test] ; extra == 'all' requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/f7/46/e7cea8159199096e1df52da20a57a6665da80c37fb8aeb848a3e47442c32/untokenize-0.1.1.tar.gz - name: untokenize - version: 0.1.1 - sha256: 3865dbbbb8efb4bb5eaa72f1be7f3e0be00ea8b7f125c69cbd1f5fda926f37a2 - pypi: https://files.pythonhosted.org/packages/e7/00/3fca040d7cf8a32776d3d81a00c8ee7457e00f80c649f1e4a863c8321ae9/uri_template-1.3.0-py3-none-any.whl name: uri-template version: 1.3.0 @@ -10765,10 +12569,10 @@ packages: - flake8-use-fstring ; extra == 'dev' - pep8-naming ; extra == 'dev' requires_python: '>=3.7' -- pypi: https://files.pythonhosted.org/packages/6d/b9/4095b668ea3678bf6a0af005527f39de12fb026516fb3df17495a733b7f8/urllib3-2.6.2-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl name: urllib3 - version: 2.6.2 - sha256: ec21cddfe7724fc7cb4ba4bea7aa8e2ef36f607a4bab81aa6ce42a13dc3f03dd + version: 2.6.3 + sha256: bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4 requires_dist: - brotli>=1.2.0 ; platform_python_implementation == 'CPython' and extra == 'brotli' - brotlicffi>=1.2.0.0 ; platform_python_implementation != 'CPython' and extra == 'brotli' @@ -10776,30 +12580,10 @@ packages: - pysocks>=1.5.6,!=1.5.7,<2.0 ; extra == 'socks' - backports-zstd>=1.0.0 ; python_full_version < '3.14' and extra == 'zstd' requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/32/49/9e3e19ba756c4a5e6acb4ea74336d3035f7959254fbb05f5eb77bff067ed/uv-0.9.22-py3-none-win_amd64.whl - name: uv - version: 0.9.22 - sha256: 9c238525272506845fe07c0b9088c5e33fcd738e1f49ef49dc3c8112096d2e3a - requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/40/15/63fb7a6908db2f03716c4a50aea7e27a7440fe6a09854282c401139afaf7/uv-0.9.22-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - name: uv - version: 0.9.22 - sha256: 1f45e1e0f26dd47fa01eb421c54cfd39de10fd52ac0a9d7ae45b92fce7d92b0b - requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/5e/68/bb76c97c284ce7fb8efa868994c2510588faa7075e60d8865d1373e54b7b/uv-0.9.22-py3-none-macosx_10_12_x86_64.whl - name: uv - version: 0.9.22 - sha256: b78f2605d65c4925631d891dec99b677b05f50c774dedc6ef8968039a5bcfdb0 - requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/af/49/7230b1d56aeaee0eefd346a70f582463f11fb7036d2d020bcf68053bd994/uv-0.9.22-py3-none-macosx_11_0_arm64.whl - name: uv - version: 0.9.22 - sha256: 2a4155cf7d0231d0adae94257ee10d70c57c2f592207536ddd55d924590a8c15 - requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/a4/39/6983dd79f01aaa4c75d9ffa550fa393f0c4c28f7ccd6956e4188c62cefbc/validate_pyproject-0.24.1-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/b7/ee/e9c95cda829131f71a8dff5ce0406059fd16e591c074414e31ada19ba7c3/validate_pyproject-0.25-py3-none-any.whl name: validate-pyproject - version: 0.24.1 - sha256: b7b05fa9117204c9c4606ab317acd095b18d1bfc78fb7dc8cc06f77d0582ca2d + version: '0.25' + sha256: f9d05e2686beff82f9ea954f582306b036ced3d3feb258c1110f2c2a495b1981 requires_dist: - fastjsonschema>=2.16.2,<=3 - packaging>=24.2 ; extra == 'all' @@ -10873,35 +12657,18 @@ packages: - mypy ; extra == 'test' - pretend ; extra == 'test' - pytest ; extra == 'test' -- pypi: https://files.pythonhosted.org/packages/79/0c/c05523fa3181fdf0c9c52a6ba91a23fbf3246cc095f26f6516f9c60e6771/virtualenv-20.35.4-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/c6/59/7d02447a55b2e55755011a647479041bc92a82e143f96a8195cb33bd0a1c/virtualenv-21.2.0-py3-none-any.whl name: virtualenv - version: 20.35.4 - sha256: c21c9cede36c9753eeade68ba7d523529f228a403463376cf821eaae2b650f1b + version: 21.2.0 + sha256: 1bd755b504931164a5a496d217c014d098426cddc79363ad66ac78125f9d908f requires_dist: - distlib>=0.3.7,<1 - - filelock>=3.12.2,<4 + - 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 - typing-extensions>=4.13.2 ; python_full_version < '3.11' - - furo>=2023.7.26 ; extra == 'docs' - - proselint>=0.13 ; extra == 'docs' - - sphinx>=7.1.2,!=7.3 ; extra == 'docs' - - sphinx-argparse>=0.4 ; extra == 'docs' - - sphinxcontrib-towncrier>=0.2.1a0 ; extra == 'docs' - - towncrier>=23.6 ; extra == 'docs' - - covdefaults>=2.3 ; extra == 'test' - - coverage-enable-subprocess>=1 ; extra == 'test' - - coverage>=7.2.7 ; extra == 'test' - - flaky>=3.7 ; extra == 'test' - - packaging>=23.1 ; extra == 'test' - - pytest-env>=0.8.2 ; extra == 'test' - - pytest-freezer>=0.4.8 ; (python_full_version >= '3.13' and platform_python_implementation == 'CPython' and sys_platform == 'win32' and extra == 'test') or (platform_python_implementation == 'GraalVM' and extra == 'test') or (platform_python_implementation == 'PyPy' and extra == 'test') - - pytest-mock>=3.11.1 ; extra == 'test' - - pytest-randomly>=3.12 ; extra == 'test' - - pytest-timeout>=2.1 ; extra == 'test' - - pytest>=7.4 ; extra == 'test' - - setuptools>=68 ; extra == 'test' - - time-machine>=2.10 ; platform_python_implementation == 'CPython' and extra == 'test' requires_python: '>=3.8' - pypi: https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl name: watchdog @@ -10945,11 +12712,11 @@ packages: requires_dist: - pyyaml>=3.10 ; extra == 'watchmedo' requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/af/b5/123f13c975e9f27ab9c0770f514345bd406d0e8d3b7a0723af9d43f710af/wcwidth-0.2.14-py2.py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/68/5a/199c59e0a824a3db2b89c5d2dade7ab5f9624dbf6448dc291b46d5ec94d3/wcwidth-0.6.0-py3-none-any.whl name: wcwidth - version: 0.2.14 - sha256: a7bb560c8aee30f9957e5f9895805edd20602f2d7f720186dfd906e82b4982e1 - requires_python: '>=3.6' + version: 0.6.0 + sha256: 1a3a1e510b553315f8e146c54764f4fb6264ffad731b3d78088cdb1478ffbdad + requires_python: '>=3.8' - pypi: https://files.pythonhosted.org/packages/e2/cc/e097523dd85c9cf5d354f78310927f1656c422bd7b2613b2db3e3f9a0f2c/webcolors-25.10.0-py3-none-any.whl name: webcolors version: 25.10.0 @@ -10972,6 +12739,11 @@ packages: - sphinx-rtd-theme>=1.1.0 ; extra == 'docs' - myst-parser>=2.0.0 ; extra == 'docs' 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/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-py3-none-any.whl name: wsproto version: 1.3.2 @@ -10996,78 +12768,78 @@ packages: - coverage ; extra == 'test' - xraydb[dev,doc,test] ; extra == 'all' requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/01/88/04d98af0b47e0ef42597b9b28863b9060bb515524da0a65d5f4db160b2d5/yarl-1.22.0-cp313-cp313-macosx_10_13_x86_64.whl +- pypi: https://files.pythonhosted.org/packages/24/84/e237607faf4e099dbb8a4f511cfd5efcb5f75918baad200ff7380635631b/yarl-1.23.0-cp311-cp311-macosx_10_9_x86_64.whl name: yarl - version: 1.22.0 - sha256: 01e73b85a5434f89fc4fe27dcda2aff08ddf35e4d47bbbea3bdcd25321af538a + version: 1.23.0 + sha256: cbb0fef01f0c6b38cb0f39b1f78fc90b807e0e3c86a7ff3ce74ad77ce5c7880c requires_dist: - idna>=2.0 - multidict>=4.0 - propcache>=0.2.1 - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/11/c9/cd8538dc2e7727095e0c1d867bad1e40c98f37763e6d995c1939f5fdc7b1/yarl-1.22.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + 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.22.0 - sha256: bec03d0d388060058f5d291a813f21c011041938a441c593374da6077fe21b1b + version: 1.23.0 + sha256: 34b6cf500e61c90f305094911f9acc9c86da1a05a7a3f5be9f68817043f486e4 requires_dist: - idna>=2.0 - multidict>=4.0 - propcache>=0.2.1 - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/18/91/3274b215fd8442a03975ce6bee5fe6aa57a8326b29b9d3d56234a1dca244/yarl-1.22.0-cp313-cp313-macosx_11_0_arm64.whl + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/67/b6/8925d68af039b835ae876db5838e82e76ec87b9782ecc97e192b809c4831/yarl-1.23.0-cp313-cp313-macosx_10_13_x86_64.whl name: yarl - version: 1.22.0 - sha256: 22965c2af250d20c873cdbee8ff958fb809940aeb2e74ba5f20aaf6b7ac8c70c + version: 1.23.0 + sha256: 4a42e651629dafb64fd5b0286a3580613702b5809ad3f24934ea87595804f2c5 requires_dist: - idna>=2.0 - multidict>=4.0 - propcache>=0.2.1 - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/46/00/71b90ed48e895667ecfb1eaab27c1523ee2fa217433ed77a73b13205ca4b/yarl-1.22.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/7a/84/266e8da36879c6edcd37b02b547e2d9ecdfea776be49598e75696e3316e1/yarl-1.23.0-cp313-cp313-win_amd64.whl name: yarl - version: 1.22.0 - sha256: 4c52a6e78aef5cf47a98ef8e934755abf53953379b7d53e68b15ff4420e6683d + version: 1.23.0 + sha256: baaf55442359053c7d62f6f8413a62adba3205119bcb6f49594894d8be47e5e3 requires_dist: - idna>=2.0 - multidict>=4.0 - propcache>=0.2.1 - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/6a/a1/d065d51d02dc02ce81501d476b9ed2229d9a990818332242a882d5d60340/yarl-1.22.0-cp311-cp311-macosx_10_9_x86_64.whl + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/93/22/b85eca6fa2ad9491af48c973e4c8cf6b103a73dbb271fe3346949449fca0/yarl-1.23.0-cp311-cp311-win_amd64.whl name: yarl - version: 1.22.0 - sha256: 669930400e375570189492dc8d8341301578e8493aec04aebc20d4717f899dd6 + version: 1.23.0 + sha256: bf49a3ae946a87083ef3a34c8f677ae4243f5b824bfc4c69672e72b3d6719d46 requires_dist: - idna>=2.0 - multidict>=4.0 - propcache>=0.2.1 - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/9a/ee/450914ae11b419eadd067c6183ae08381cfdfcb9798b90b2b713bbebddda/yarl-1.22.0-cp313-cp313-win_amd64.whl + 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.22.0 - sha256: 47743b82b76d89a1d20b83e60d5c20314cbd5ba2befc9cda8f28300c4a08ed4d + version: 1.23.0 + sha256: 99c8a9ed30f4164bc4c14b37a90208836cbf50d4ce2a57c71d0f52c7fb4f7598 requires_dist: - idna>=2.0 - multidict>=4.0 - propcache>=0.2.1 - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/b5/f7/149bb6f45f267cb5c074ac40c01c6b3ea6d8a620d34b337f6321928a1b4d/yarl-1.22.0-cp311-cp311-win_amd64.whl + 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.22.0 - sha256: 078278b9b0b11568937d9509b589ee83ef98ed6d561dfe2020e24a9fd08eaa2b + version: 1.23.0 + sha256: 7c6b9461a2a8b47c65eef63bb1c76a4f1c119618ffa99ea79bc5bb1e46c5821b requires_dist: - idna>=2.0 - multidict>=4.0 - propcache>=0.2.1 - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/c1/da/8da9f6a53f67b5106ffe902c6fa0164e10398d4e150d85838b82f424072a/yarl-1.22.0-cp311-cp311-macosx_11_0_arm64.whl + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/b2/0d/71ceabc14c146ba8ee3804ca7b3d42b1664c8440439de5214d366fec7d3a/yarl-1.23.0-cp311-cp311-macosx_11_0_arm64.whl name: yarl - version: 1.22.0 - sha256: 792a2af6d58177ef7c19cbf0097aba92ca1b9cb3ffdd9c7470e156c8f9b5e028 + version: 1.23.0 + sha256: dc52310451fc7c629e13c4e061cbe2dd01684d91f2f8ee2821b083c58bd72432 requires_dist: - idna>=2.0 - multidict>=4.0 - propcache>=0.2.1 - requires_python: '>=3.9' + requires_python: '>=3.10' - pypi: https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl name: zipp version: 3.23.0 diff --git a/pixi.toml b/pixi.toml index 04f71c03..37952beb 100644 --- a/pixi.toml +++ b/pixi.toml @@ -5,7 +5,7 @@ # Platform-independent [activation.env] -PYTHONIOENCODING = "utf-8" +PYTHONIOENCODING = 'utf-8' # Platform-specific @@ -27,12 +27,25 @@ PYTHONPATH = "${PIXI_PROJECT_ROOT}/src;%PYTHONPATH%" ########### [workspace] + # Supported platforms for the lock file (pixi.lock) platforms = ['win-64', 'linux-64', 'osx-64', 'osx-arm64'] # Channels for fetching packages channels = ['conda-forge'] +##################### +# SYSTEM REQUIREMENTS +##################### + +[system-requirements] + +# Set minimum supported version for macOS to be 14.0 to ensure packages +# like `skipp` that only have wheels for macOS 14.0+ (macosx_14_0_arm64) +# are used instead of building from source. This is a workaround for +# Pixi, see https://github.com/prefix-dev/pixi/issues/5667 +macos = '14.0' + ########## # FEATURES ########## @@ -40,33 +53,20 @@ channels = ['conda-forge'] # Default feature configuration [dependencies] -gsl = '*' # GNU Scientific Library; required for pdffit2. - -#[target.win-64.dependencies] -#libcblas = '*' # CBLAS library for linear algebra; required for pdffit2. +nodejs = '*' # Required for Prettier (non-Python formatting) +gsl = '*' # GNU Scientific Library; required for diffpy.pdffit2 [pypi-dependencies] # == [feature.default.pypi-dependencies] -pip = '*' # Native package installer -uv = '*' # Package manager -jupyterlab = '*' # Jupyter notebooks -pixi-kernel = '*' # Pixi Jupyter kernel -easydiffraction = { version = '*', extras = ['all'] } # Main package +#pip = '*' # Native package installer +easydiffraction = { path = '.', editable = true, extras = ['dev'] } -# Specific features +# Specific features: Set specific Python versions -# Each feature sets a specific Python version for the environment. - -[feature.py311.dependencies] +[feature.py-min.dependencies] python = '3.11.*' - -[feature.py313.dependencies] +[feature.py-max.dependencies] python = '3.13.*' -# This feature installs Node.js for formatting non-Python files with Prettier. - -[feature.nodejs.dependencies] -nodejs = '*' - ############## # ENVIRONMENTS ############## @@ -75,13 +75,12 @@ nodejs = '*' # The `default` feature is always included in all environments. # Additional features can be specified per environment. +py-311-env = { features = ['default', 'py-min'] } +py-313-env = { features = ['default', 'py-max'] } # The `default` environment is always created and includes the `default` feature. # It does not need to be specified explicitly unless non-default features are included. - -default = { features = ['py313', 'nodejs'] } -py311-dev = { features = ['py311', 'nodejs'] } -py313-dev = { features = ['py313', 'nodejs'] } +default = { features = ['default', 'py-max'] } ####### # TASKS @@ -89,59 +88,59 @@ py313-dev = { features = ['py313', 'nodejs'] } [tasks] -## 🧪 Testing Tasks +################## +# 🧪 Testing Tasks +################## + unit-tests = 'python -m pytest tests/unit/ --color=yes -v' integration-tests = 'python -m pytest tests/integration/ --color=yes -n auto -v' -notebook-tests = 'python -m pytest --nbmake tutorials/ --nbmake-timeout=600 --color=yes -n auto -v' script-tests = 'python -m pytest tools/test_scripts.py --color=yes -n auto -v' -extra = 'python -m pytest tests/unit/extra.py -q --tb=no --disable-warnings --color=yes' +notebook-tests = 'python -m pytest --nbmake docs/docs/tutorials/ --nbmake-timeout=1200 --color=yes -n auto -v' test = { depends-on = ['unit-tests'] } -# 🧹 Code Quality +########### +# ✔️ Checks +########### -### ✔️ Checks pyproject-check = 'python -m validate_pyproject pyproject.toml' -py-lint-check-pre = "python -m ruff check" -py-lint-check = 'pixi run py-lint-check-pre .' -py-format-check-pre = "python -m ruff format --check" -py-format-check = "pixi run py-format-check-pre ." -nonpy-format-check-pre = "npx prettier --list-different --config=prettierrc.toml" -nonpy-format-check-modified = "pixi run nonpy-format-check-pre $(git diff --diff-filter=d --name-only HEAD | grep -E '\\.(json|ya?ml|toml|md|css|html)$' || echo .)" -nonpy-format-check = "pixi run nonpy-format-check-pre ." -notebook-format-check = 'nbqa ruff tutorials/' -docs-format-check = 'docformatter src/ tutorials/ --check' -# Run like a real commit: staged files only (almost) -pre-commit-check = 'pre-commit run --hook-stage pre-commit' -# CI check: lint/format everything -pre-commit-check-all = 'pre-commit run --all-files --hook-stage pre-commit' -# Pre-push check: lint/format everything -pre-push-check = 'pre-commit run --all-files --hook-stage pre-push' - -check = { depends-on = [ - 'docs-format-check', - 'py-format-check', - 'py-lint-check', - 'nonpy-format-check-modified', -] } +param-docstring-check = 'python tools/param_consistency.py src/ --check' +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 +########## -### 🛠️ Fixes -py-lint-fix = 'pixi run py-lint-check --fix' -#py-format-fix = "python -m ruff format $(git diff --cached --name-only -- '*.py')" -py-format-fix = "python -m ruff format" -nonpy-format-fix = 'pixi run nonpy-format-check --write' -nonpy-format-fix-modified = "pixi run nonpy-format-check-modified --write" -notebook-format-fix = 'pixi run notebook-format-check --fix' -docs-format-fix = 'docformatter src/ tutorials/ --in-place' +param-docstring-fix = 'python tools/param_consistency.py src/ --fix' +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-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 = [ + #'param-docstring-fix', # ED only + 'docstring-format-fix', 'py-format-fix', - 'docs-format-fix', 'py-lint-fix', 'nonpy-format-fix', + 'notebook-lint-fix', + 'success-message', ] } -## 🧮 Code Complexity +#################### +# 🧮 Code Complexity +#################### + complexity-check = 'radon cc -s src/' complexity-check-json = 'radon cc -s -j src/' maintainability-check = 'radon mi src/' @@ -149,62 +148,127 @@ maintainability-check-json = 'radon mi -j src/' raw-metrics = 'radon raw -s src/' raw-metrics-json = 'radon raw -s -j src/' -## 📊 Coverage +############# +# 📊 Coverage +############# + unit-tests-coverage = 'pixi run unit-tests --cov=src/easydiffraction --cov-report=term-missing' integration-tests-coverage = 'pixi run integration-tests --cov=src/easydiffraction --cov-report=term-missing' -docstring-coverage = 'interrogate -c pyproject.toml src/' +docstring-coverage = 'interrogate -c pyproject.toml src/easydiffraction' + +cov = { depends-on = [ + 'docstring-coverage', + 'unit-tests-coverage', + 'integration-tests-coverage', +] } -cov = { depends-on = ['docstring-coverage', 'integration-tests-coverage'] } +######################## +# 📓 Notebook Management +######################## -## 📓 Notebook Management -notebook-convert = 'jupytext tutorials/*.py --from py:percent --to ipynb' -notebook-strip = 'nbstripout tutorials/*.ipynb' +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 tutorials/' -notebook-clean = 'rm -f tutorials/*.ipynb' -notebook-exec = 'python -m pytest --nbmake tutorials/ --nbmake-timeout=600 --overwrite --color=yes -n auto -v' +notebook-exec = 'python -m pytest --nbmake docs/docs/tutorials/ --nbmake-timeout=1200 --overwrite --color=yes -n auto -v' notebook-prepare = { depends-on = [ - 'notebook-convert', + #'notebook-convert', 'notebook-strip', - 'notebook-tweak', + #'notebook-tweak', +] } + +######################## +# 📚 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 ../diffraction/.copier-answers.yml --data template_type=lib' +copier-recopy = 'copier recopy --data-file ../diffraction/.copier-answers.yml --data template_type=lib' +copier-update = 'copier update --data-file ../diffraction/.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/diffraction-lib -f has_wiki=false' +repo-discussions = 'gh api -X PATCH repos/easyscience/diffraction-lib -f has_discussions=true' +repo-description = "gh api -X PATCH repos/easyscience/diffraction-lib -f description='Diffraction data analysis'" +repo-homepage = "gh api -X PATCH repos/easyscience/diffraction-lib -f homepage='https://easyscience.github.io/diffraction-lib'" +repo-config = { depends-on = [ + 'repo-wiki', + 'repo-discussions', + 'repo-description', + 'repo-homepage', ] } -## 📚 Documentation Tasks -docs-assets = 'tools/add_assets_to_docs.sh' -docs-notebooks = 'mv tutorials/*.* docs/tutorials/' # *.py, *.ipynb, index.json -docs-config = 'python tools/create_mkdocs_yml.py' -docs-deploy = 'mike deploy --push --branch gh-pages --update-aliases --alias-type redirect' -docs-set-default = 'mike set-default --push --branch gh-pages' -docs-serve = "JUPYTER_PLATFORM_DIRS=1 PYTHONWARNINGS='ignore::RuntimeWarning' python -m mkdocs serve --dirty" -docs-build = "JUPYTER_PLATFORM_DIRS=1 PYTHONWARNINGS='ignore::RuntimeWarning' python -m mkdocs build" -docs-local = "pixi run docs-build --no-directory-urls" -docs-clean = 'tools/cleanup_docs.sh' -docs-setup = { depends-on = [ - 'docs-config', - 'docs-assets', - 'notebook-prepare', - 'docs-notebooks', +master-protection = 'gh api -X POST repos/easyscience/diffraction-lib/rulesets --input .github/configs/rulesets-master.json' +develop-protection = 'gh api -X POST repos/easyscience/diffraction-lib/rulesets --input .github/configs/rulesets-develop.json' +gh-pages-protection = 'gh api -X POST repos/easyscience/diffraction-lib/rulesets --input .github/configs/rulesets-gh-pages.json' +branch-protection = { depends-on = [ + 'master-protection', + 'develop-protection', + 'gh-pages-protection', ] } -## 🚀 Development & Build Tasks +pages-deployment = 'gh api -X POST repos/easyscience/diffraction-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' -spdx-update = 'python tools/update_spdx.py' -#dev-install = 'uv pip install --requirements pyproject.toml --extra all' -dev-install = "python -m uv pip install --force-reinstall --editable '.[all]'" + npm-config = 'npm config set registry https://registry.npmjs.org/' prettier-install = 'npm install --no-save --no-audit --no-fund prettier prettier-plugin-toml' -pre-commit-setup = 'pre-commit clean && pre-commit uninstall && pre-commit install --hook-type pre-commit --hook-type pre-push --overwrite' -pre-commit-update = 'pre-commit autoupdate' + clean-pycache = "find . -type d -name '__pycache__' -prune -exec rm -rf '{}' +" -dev = { depends-on = [ - 'dev-install', +post-install = { depends-on = [ 'npm-config', 'prettier-install', - 'pre-commit-setup', + #'pre-commit-setup', ] } -wheel = { depends-on = ['npm-config', 'prettier-install'] } - -## 🔗 Main Package Shortcut +########################## +# 🔗 Main Package Shortcut +########################## easydiffraction = 'python -m easydiffraction' diff --git a/prettierrc.toml b/prettierrc.toml index 6874d28d..b98c86eb 100644 --- a/prettierrc.toml +++ b/prettierrc.toml @@ -1,11 +1,22 @@ +plugins = [ + "prettier-plugin-toml", # use the TOML plugin +] + endOfLine = 'lf' # change line endings to LF -printWidth = 80 # wrap Markdown files at 80 characters 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 -plugins = [ - 'prettier-plugin-toml', # use the TOML plugin -] +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/pycrysfml.md b/pycrysfml.md index af681478..de9bc4d0 100644 --- a/pycrysfml.md +++ b/pycrysfml.md @@ -12,7 +12,8 @@ ```bash otool -L .venv/lib/python3.12/site-packages/pycrysfml/crysfml08lib.so ``` -- If the library is linked to the wrong Python version, you can fix it with: +- If the library is linked to the wrong Python version, you can fix it + with: ```bash install_name_tool -change `python3-config --prefix`/Python `python3-config --prefix`/lib/libpython3.12.dylib .venv/lib/python3.12/site-packages/pycrysfml/crysfml08lib.so ``` diff --git a/pyproject.toml b/pyproject.toml index fb0118e3..88036948 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,9 +6,10 @@ name = 'easydiffraction' dynamic = ['version'] # Use versioningit to manage the version description = 'Diffraction data analysis' -authors = [{ name = 'EasyDiffraction contributors' }] +authors = [{ name = 'EasyScience contributors' }] readme = 'README.md' -license = { file = 'LICENSE' } +license = 'BSD-3-Clause' +license-files = ['LICENSE'] classifiers = [ 'Intended Audience :: Science/Research', 'Topic :: Scientific/Engineering', @@ -20,9 +21,9 @@ classifiers = [ 'Programming Language :: Python :: 3.12', 'Programming Language :: Python :: 3.13', ] -requires-python = '>=3.11,<3.14' +requires-python = '>=3.11' dependencies = [ - 'essdiffraction', # ESS Diffraction library + 'essdiffraction', # ESS-specific diffraction library 'numpy', # Numerical computing library 'colorama', # Color terminal output 'tabulate', # Pretty-print tabular data for terminal output @@ -43,29 +44,36 @@ dependencies = [ 'diffpy.utils', # Utilities for PDF calculations 'uncertainties', # Propagation of uncertainties 'typeguard', # Runtime type checking + 'darkdetect', # Detecting dark mode (system-level) + 'pandas', # Displaying tables in Jupyter notebooks + 'plotly', # Interactive plots + 'py3Dmol', # Visualisation of crystal structures + 'jupyterlab', # Jupyter notebooks + 'pixi-kernel', # Pixi Jupyter kernel ] [project.optional-dependencies] dev = [ - '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 - 'docformatter', # Code formatter for docstrings - 'interrogate', # Check for missing docstrings -] -docs = [ + 'GitPython', # Interact with Git repositories + 'build', # Building the package + 'pre-commit', # Pre-commit hooks + 'jinja2', # Templating + 'nbmake', # Building notebooks + 'nbstripout', # Strip output from notebooks + 'nbqa', # Linting and formatting notebooks + 'pytest', # Testing + 'pytest-cov', # Test coverage + 'pytest-xdist', # Enable parallel testing + 'ruff', # Linting and formatting code + 'radon', # Code complexity and maintainability + 'validate-pyproject[all]', # Validate pyproject.toml + 'versioningit', # Automatic versioning from git tags + 'jupytext', # Jupyter notebook text format support + 'jupyterquiz', # Quizzes in Jupyter notebooks + 'pydoclint', # Docstring linter + 'format-docstring', # Docstring formatter + 'interrogate', # Docstring coverage checker + 'copier', # Template management 'mike', # MkDocs: Versioned documentation support 'mkdocs', # Static site generator 'mkdocs-material', # Documentation framework on top of MkDocs @@ -75,35 +83,20 @@ docs = [ 'mkdocs-markdownextradata-plugin', # MkDocs: Markdown extra data support, such as global variables 'mkdocstrings-python', # MkDocs: Python docstring support 'pyyaml', # YAML parser -] -visualization = [ - 'darkdetect', # Detecting dark mode (system-level) - 'pandas', # Displaying tables in Jupyter notebooks - 'plotly', # Interactive plots - 'py3Dmol', # Visualisation of crystal structures -] -all = [ - 'easydiffraction[dev]', - 'easydiffraction[docs]', - 'easydiffraction[visualization]', + 'spdx-headers', # SPDX license header validation ] [project.urls] -homepage = 'https://easydiffraction.org' -documentation = 'https://docs.easydiffraction.org/lib' -source = 'https://github.com/easyscience/diffraction-lib' -tracker = 'https://github.com/easyscience/diffraction-lib/issues' +Homepage = 'https://easydiffraction.org' +Documentation = 'https://easyscience.github.io/diffraction-lib' +'Release Notes' = 'https://github.com/easyscience/diffraction-lib/releases' +'Source Code' = 'https://github.com/easyscience/diffraction-lib' +'Issue Tracker' = 'https://github.com/easyscience/diffraction-lib/issues' ############################ # Build system configuration ############################ -# Build system 'hatch' -- Python project manager -# https://hatch.pypa.io/ - -# Versioning system 'versioningit' -- Versioning from git tags -# https://versioningit.readthedocs.io/ - [build-system] build-backend = 'hatchling.build' requires = ['hatchling', 'versioningit'] @@ -112,6 +105,9 @@ requires = ['hatchling', 'versioningit'] # Configuration for hatchling ############################# +# 'hatch' -- Build system for Python +# https://hatch.pypa.io/ + [tool.hatch.build.targets.wheel] packages = ['src/easydiffraction'] @@ -125,6 +121,9 @@ source = 'versioningit' # Use versioningit to manage the version # Configuration for versioningit ################################ +# 'versioningit' -- Versioning from git tags +# https://versioningit.readthedocs.io/ + # Versioningit generates versions from git tags, so we don't need to # either specify them statically in pyproject.toml or save them in the # source code. Do not use {distance} in the version format, as it @@ -144,43 +143,45 @@ method = 'git' match = ['v*'] default-tag = 'v999.0.0' -################################ -# Configuration for docformatter -################################ - -# 'docformatter' -- Code formatter for docstrings -# https://docformatter.readthedocs.io/en/latest/ - -[tool.docformatter] -recursive = true -wrap-summaries = 72 -wrap-descriptions = 72 -close-quotes-on-newline = true - ################################ # Configuration for interrogate ################################ -# 'interrogate' -- Check for missing docstrings +# 'interrogate' -- Docstring coverage checker # https://interrogate.readthedocs.io/en/latest/ [tool.interrogate] -fail-under = 35 # Temporarily reduce to allow gradual improvement +fail-under = 35 # Minimum docstring coverage percentage to pass verbose = 1 -exclude = ["src/**/__init__.py"] +#exclude = ['src/**/__init__.py'] ####################################### # Configuration for coverage/pytest-cov ####################################### +# 'coverage' -- Code coverage measurement tool +# https://coverage.readthedocs.io/en/latest/ + [tool.coverage.run] -branch = true # Measure branch coverage as well -source = ['src/easydiffraction'] # Limit coverage to the source code directory +branch = true # Measure branch coverage as well +source = ['src'] # Limit coverage to the source code directory [tool.coverage.report] -show_missing = true # Show missing lines -skip_covered = true # Skip files with 100% coverage in the report -fail_under = 65 # Temporarily reduce to allow gradual improvement +show_missing = true # Show missing lines +skip_covered = false # Skip files with 100% coverage in the report +fail_under = 60 # Minimum coverage percentage to pass + +########################## +# Configuration for pytest +########################## + +# 'pytest' -- Testing framework +# https://docs.pytest.org/en/stable/ + +[tool.pytest.ini_options] +addopts = '--import-mode=importlib' +markers = ['fast: mark test as fast (should be run on every push)'] +testpaths = ['tests'] ######################## # Configuration for ruff @@ -190,112 +191,201 @@ fail_under = 65 # Temporarily reduce to allow gradual improvement # https://docs.astral.sh/ruff/rules/ [tool.ruff] -# Temporarily exclude some directories until we have improved the code quality there -#exclude = ['tests', 'tmp'] exclude = [ 'tmp', - 'tests/unit', - 'tests/integration/fitting', - 'tests/integration/scipp-analysis/tmp', + # Vendored jupyter_dark_detect: keep as-is from upstream for easy updates + # https://github.com/OpenMined/jupyter-dark-detect/tree/main/jupyter_dark_detect + 'src/easydiffraction/utils/_vendored/jupyter_dark_detect', ] indent-width = 4 -line-length = 99 -# Enable new rules that are not yet stable, like DOC -preview = true +line-length = 99 # See also `max-line-length` in [tool.ruff.lint.pycodestyle] +preview = true # Enable new rules that are not yet stable, like DOC + +# Formatting options for Ruff + +[tool.ruff.format] +docstring-code-format = true # Whether to format code snippets in docstrings +docstring-code-line-length = 72 # Line length for code snippets in docstrings +indent-style = 'space' # PEP 8 recommends using spaces over tabs +line-ending = 'lf' # Line endings will be converted to \n +quote-style = 'single' # But double quotes in docstrings (PEP 8, PEP 257) + +# Linting rules to use with Ruff -# https://docs.astral.sh/ruff/rules/ [tool.ruff.lint] select = [ - 'ARG', # Argument-related issues (e.g., unused arguments) - 'B', # Bugbear-specific checks (e.g., likely bugs, bad patterns) - 'C', # Complexity-related issues (e.g., high McCabe complexity) - 'D', # Docstring formatting issues (old rules) - 'DOC', # Docstring formatting issues (new rules) - 'DTZ', # Datetime timezone issues (e.g., inconsistent timezone formats) - 'E', # General PEP 8 style errors - 'F', # Pyflakes-specific checks (e.g., unused variables, imports) - 'FLY', # Flynt-specific checks (e.g., enforcing f-strings) - 'G', # Type annotation issues (e.g., missing or incorrect type hints) - 'I', # Import sorting issues (e.g., unsorted imports) - 'ICN', # Import conventions (e.g., enforce aliasing like import numpy as np) - 'N', # Naming convention issues (e.g., variable names, function names) - 'NPY', # NumPy-specific checks (e.g., array operations, broadcasting) - 'PGH', # Misc text patterns checks - 'PTH', # Encourages using pathlib over os.path - 'S', # Security-related issues (e.g., use of insecure functions or libraries) - 'SIM', # Simplification issues (e.g., redundant code, unnecessary constructs) - 'TCH', # Type checking issues (e.g., incompatible types, missing type annotations) - 'TID252', # Enforces absolute imports over relative imports - 'W', # General PEP 8 warnings (e.g., lines too long, trailing whitespace) - #'ANN', # Missing or incorrect type annotations - #'COM', # Comment formatting issues - #'PERF', # Performance-related issues (e.g., inefficient code patterns) - #'PIE', # Potentially problematic idioms and errors - #'PL', # PyLint-specific checks (e.g., code smells, potential errors) - #'PT', # Pytest-related issues - #'RET', # Return statement issues (e.g., inconsistent returns) - #'RUF', # Ruff-specific checks (e.g., enforcing best practices) - #'SLF', # Self argument-related issues (e.g., missing or misused self) - #'T20', # Flake8-print-specific checks (e.g., print statements left in code) - #'TD', # Type definition issues (e.g., incorrect or missing type definitions) - #'TRY', # Tryceratops Try/Except-related issues (e.g., broad exceptions, empty except blocks) - #'UP', # Pyupgrade-specific checks + # 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 ] -# Temporarily disable some docstring checks until we have improved the docstring coverage + +# Exceptions to the linting rules + +# Ignore specific rules globally ignore = [ - 'C408', # Ignore: Unnecessary `dict()` call - 'C416', # Ignore: Unnecessary list comprehension - 'D100', # Ignore: Missing docstring in public module - 'D101', # Ignore: Missing docstring in class - 'D102', # Ignore: Missing docstring in public method - 'D103', # Ignore: Missing docstring in public function - 'D104', # Ignore: Missing docstring in public package - 'D105', # Ignore: Missing docstring in magic method - 'D107', # Ignore: Missing docstring in __init__ - 'D205', # Ignore: 1 blank line required between summary and description - 'DOC201', # Ignore: `return` is not documented in docstring - 'DOC501', # Ignore: Raised exception `ValueError` missing from docstring - 'DOC502', # Ignore: Raised exception is not explicitly raised: `TypeError` - 'DTZ005', # Ignore: `datetime.datetime.now()` called without a `tz` argument + '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/ + # Temporary: + 'D100', # https://docs.astral.sh/ruff/rules/undocumented-public-module/#undocumented-publi-module-d100 + 'D104', # https://docs.astral.sh/ruff/rules/undocumented-public-package/#undocumented-public-package-d104 + 'DTZ005', # https://docs.astral.sh/ruff/rules/call-datetime-now-without-tzinfo/#call-datetime-now-without-tzinfo-dtz005 ] -# Temporarily increase McCabe complexity limit to 19 to allow -# refactoring in smaller steps. -[tool.ruff.lint.mccabe] -max-complexity = 19 # default is 10 +# 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/ + # Temporary: + 'ARG001', + 'ARG002', + 'ARG004', + 'ARG005', + 'B011', + 'B017', + 'B018', + 'E501', + 'E741', + 'F841', + 'I001', + 'N801', + 'N805', + 'N812', + 'PLC', + 'PLE', + 'PLR', + 'PLW', + 'SIM117', + 'W505', +] +'docs/**' = [ + 'INP001', # https://docs.astral.sh/ruff/rules/implicit-namespace-package/ + 'T201', # https://docs.astral.sh/ruff/rules/print/ + # Temporary: + 'ANN', + 'D', + 'W', +] + +# Specific options for certain rules [tool.ruff.lint.flake8-tidy-imports] +# Disallow all relative imports ban-relative-imports = 'all' [tool.ruff.lint.isort] +# Forces all from imports to appear on their own line force-single-line = true -[tool.ruff.lint.per-file-ignores] -'*test_*.py' = ['S101'] # allow asserts in test files -'conftest.py' = ['S101'] # allow asserts in test files -# Vendored jupyter_dark_detect: keep as-is from upstream for easy updates -# https://github.com/OpenMined/jupyter-dark-detect/tree/main/jupyter_dark_detect -'src/easydiffraction/utils/_vendored/jupyter_dark_detect/*' = [ - 'S110', # try-except-pass - 'S112', # try-except-continue - 'S404', # subprocess module - 'S607', # partial executable path - 'TID252', # relative imports - 'W291', # trailing whitespace (in JS strings) - 'W505', # doc line too long - 'E501', # line too long (in JS strings) -] +[tool.ruff.lint.mccabe] +# Cyclomatic complexity threshold (default is 10) +max-complexity = 10 [tool.ruff.lint.pycodestyle] -max-line-length = 100 #99# https://peps.python.org/pep-0008/#maximum-line-length -max-doc-length = 72 # https://peps.python.org/pep-0008/#maximum-line-length +# 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 = 99 # See also `line-length` in [tool.ruff] +max-doc-length = 72 [tool.ruff.lint.pydocstyle] -convention = 'google' +convention = 'numpy' -[tool.ruff.format] -docstring-code-format = true # Whether to format code snippets in docstrings -docstring-code-line-length = 72 # Line length for code snippets in docstrings -indent-style = 'space' # PEP 8 recommends using spaces over tabs -line-ending = 'lf' # Line endings will be converted to \n -quote-style = 'single' # But double quotes in docstrings (PEP 8, PEP 257) +############################# +# 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 +exclude = 'src/easydiffraction/utils/_vendored/jupyter_dark_detect' # ED only +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 +exclude = 'src/easydiffraction/utils/_vendored/jupyter_dark_detect' # ED only +docstring_style = 'numpy' +line_length = 72 +fix_rst_backticks = true +verbose = 'default' diff --git a/pytest.ini b/pytest.ini deleted file mode 100644 index 907883e8..00000000 --- a/pytest.ini +++ /dev/null @@ -1,13 +0,0 @@ -[pytest] -addopts = --import-mode=importlib -markers = - fast: mark test as fast (should be run on every push) - integration: mark test as integration (slow; opt-in) -testpaths = - tests -filterwarnings = - ignore::DeprecationWarning:cryspy\. - ignore:.*scipy\.misc is deprecated.*:DeprecationWarning - # Suppress expected UserWarnings emitted during tutorial list fetching in tests - ignore:Falling back to latest release info\...:UserWarning:easydiffraction\.utils\.logging - ignore:'tutorials\.zip' not found in the release\.:UserWarning:easydiffraction\.utils\.logging diff --git a/src/easydiffraction/__init__.py b/src/easydiffraction/__init__.py index 259c931d..10308402 100644 --- a/src/easydiffraction/__init__.py +++ b/src/easydiffraction/__init__.py @@ -1,30 +1,17 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -from easydiffraction.experiments.experiment.factory import ExperimentFactory +from easydiffraction.datablocks.experiment.item.factory import ExperimentFactory +from easydiffraction.datablocks.structure.item.factory import StructureFactory +from easydiffraction.io.ascii import extract_data_paths_from_dir +from easydiffraction.io.ascii import extract_data_paths_from_zip +from easydiffraction.io.ascii import extract_metadata from easydiffraction.project.project import Project -from easydiffraction.sample_models.sample_model.factory import SampleModelFactory from easydiffraction.utils.logging import Logger from easydiffraction.utils.logging import console from easydiffraction.utils.logging import log from easydiffraction.utils.utils import download_all_tutorials from easydiffraction.utils.utils import download_data from easydiffraction.utils.utils import download_tutorial -from easydiffraction.utils.utils import get_value_from_xye_header from easydiffraction.utils.utils import list_tutorials from easydiffraction.utils.utils import show_version - -__all__ = [ - 'Project', - 'ExperimentFactory', - 'SampleModelFactory', - 'download_data', - 'download_tutorial', - 'download_all_tutorials', - 'list_tutorials', - 'get_value_from_xye_header', - 'show_version', - 'Logger', - 'log', - 'console', -] diff --git a/src/easydiffraction/__main__.py b/src/easydiffraction/__main__.py index c850b1fa..1a058dd6 100644 --- a/src/easydiffraction/__main__.py +++ b/src/easydiffraction/__main__.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause import sys @@ -25,7 +25,7 @@ def main( help='Show easydiffraction version and exit.', is_eager=True, ), -): +) -> None: """EasyDiffraction command-line interface.""" if version: ed.show_version() @@ -38,7 +38,7 @@ def main( @app.command('list-tutorials') -def list_tutorials(): +def list_tutorials() -> None: """List available tutorial notebooks.""" ed.list_tutorials() @@ -58,7 +58,7 @@ def download_tutorial( '-o', help='Overwrite existing file if present.', ), -): +) -> None: """Download a specific tutorial notebook by ID.""" ed.download_tutorial(id=id, destination=destination, overwrite=overwrite) @@ -77,7 +77,7 @@ def download_all_tutorials( '-o', help='Overwrite existing files if present.', ), -): +) -> None: """Download all available tutorial notebooks.""" ed.download_all_tutorials(destination=destination, overwrite=overwrite) diff --git a/src/easydiffraction/analysis/__init__.py b/src/easydiffraction/analysis/__init__.py index 429f2648..78150ea5 100644 --- a/src/easydiffraction/analysis/__init__.py +++ b/src/easydiffraction/analysis/__init__.py @@ -1,2 +1,2 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause diff --git a/src/easydiffraction/analysis/analysis.py b/src/easydiffraction/analysis/analysis.py index 3532290e..c87fc545 100644 --- a/src/easydiffraction/analysis/analysis.py +++ b/src/easydiffraction/analysis/analysis.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause from typing import List @@ -7,18 +7,20 @@ import pandas as pd -from easydiffraction.analysis.calculators.factory import CalculatorFactory -from easydiffraction.analysis.categories.aliases import Aliases -from easydiffraction.analysis.categories.constraints import Constraints +from easydiffraction.analysis.categories.aliases.factory import AliasesFactory +from easydiffraction.analysis.categories.constraints.factory import ConstraintsFactory +from easydiffraction.analysis.categories.fit_mode import FitModeEnum +from easydiffraction.analysis.categories.fit_mode import FitModeFactory from easydiffraction.analysis.categories.joint_fit_experiments import JointFitExperiments from easydiffraction.analysis.fitting import Fitter from easydiffraction.analysis.minimizers.factory import MinimizerFactory -from easydiffraction.core.parameters import NumericDescriptor -from easydiffraction.core.parameters import Parameter -from easydiffraction.core.parameters import StringDescriptor -from easydiffraction.core.singletons import ConstraintsHandler +from easydiffraction.core.singleton import ConstraintsHandler +from easydiffraction.core.variable import NumericDescriptor +from easydiffraction.core.variable import Parameter +from easydiffraction.core.variable import StringDescriptor +from easydiffraction.datablocks.experiment.collection import Experiments from easydiffraction.display.tables import TableRenderer -from easydiffraction.experiments.experiments import Experiments +from easydiffraction.utils.enums import VerbosityEnum from easydiffraction.utils.logging import console from easydiffraction.utils.logging import log from easydiffraction.utils.utils import render_cif @@ -26,53 +28,194 @@ class Analysis: - """High-level orchestration of analysis tasks for a Project. + """ + High-level orchestration of analysis tasks for a Project. This class wires calculators and minimizers, exposes a compact interface for parameters, constraints and results, and coordinates - computations across the project's sample models and experiments. + computations across the project's structures and experiments. + """ - Typical usage: + def __init__(self, project: object) -> None: + """ + Create a new Analysis instance bound to a project. - - Display or filter parameters to fit. - - Select a calculator/minimizer implementation. - - Calculate patterns and run single or joint fits. + Parameters + ---------- + project : object + The project that owns models and experiments. + """ + self.project = project + self._aliases_type: str = AliasesFactory.default_tag() + self.aliases = AliasesFactory.create(self._aliases_type) + self._constraints_type: str = ConstraintsFactory.default_tag() + self.constraints = ConstraintsFactory.create(self._constraints_type) + self.constraints_handler = ConstraintsHandler.get() + self._fit_mode_type: str = FitModeFactory.default_tag() + self._fit_mode = FitModeFactory.create(self._fit_mode_type) + self._joint_fit_experiments = JointFitExperiments() + self.fitter = Fitter('lmfit') + self.fit_results = None + self._parameter_snapshots: dict[str, dict[str, dict]] = {} + + def help(self) -> None: + """Print a summary of analysis properties and methods.""" + from easydiffraction.core.guard import GuardedBase + + console.paragraph("Help for 'Analysis'") + + cls = type(self) + + # Auto-discover properties from MRO + seen_props: dict = {} + for base in cls.mro(): + for key, attr in base.__dict__.items(): + if key.startswith('_') or not isinstance(attr, property): + continue + if key not in seen_props: + seen_props[key] = attr + + prop_rows = [] + for i, key in enumerate(sorted(seen_props), 1): + prop = seen_props[key] + writable = '✓' if prop.fset else '✗' + doc = GuardedBase._first_sentence(prop.fget.__doc__ if prop.fget else None) + prop_rows.append([str(i), key, writable, doc]) + + if prop_rows: + console.paragraph('Properties') + render_table( + columns_headers=['#', 'Name', 'Writable', 'Description'], + columns_alignment=['right', 'left', 'center', 'left'], + columns_data=prop_rows, + ) - Attributes: - project: The parent Project object. - aliases: A registry of human-friendly aliases for parameters. - constraints: Symbolic constraints between parameters. - calculator: Active calculator used for computations. - fitter: Active fitter/minimizer driver. - """ + # Auto-discover methods from MRO + seen_methods: set = set() + methods_list: list = [] + for base in cls.mro(): + for key, attr in base.__dict__.items(): + if key.startswith('_') or key in seen_methods: + continue + if isinstance(attr, property): + continue + raw = attr + if isinstance(raw, (staticmethod, classmethod)): + raw = raw.__func__ + if callable(raw): + seen_methods.add(key) + methods_list.append((key, raw)) + + method_rows = [] + for i, (key, method) in enumerate(sorted(methods_list), 1): + doc = GuardedBase._first_sentence(getattr(method, '__doc__', None)) + method_rows.append([str(i), f'{key}()', doc]) + + if method_rows: + console.paragraph('Methods') + render_table( + columns_headers=['#', 'Name', 'Description'], + columns_alignment=['right', 'left', 'left'], + columns_data=method_rows, + ) - _calculator = CalculatorFactory.create_calculator('cryspy') + # ------------------------------------------------------------------ + # Aliases (switchable-category pattern) + # ------------------------------------------------------------------ - def __init__(self, project) -> None: - """Create a new Analysis instance bound to a project. + @property + def aliases_type(self) -> str: + """Tag of the active aliases collection type.""" + return self._aliases_type - Args: - project: The project that owns models and experiments. + @aliases_type.setter + def aliases_type(self, new_type: str) -> None: """ - self.project = project - self.aliases = Aliases() - self.constraints = Constraints() - self.constraints_handler = ConstraintsHandler.get() - self.calculator = Analysis._calculator # Default calculator shared by project - self._calculator_key: str = 'cryspy' # Added to track the current calculator - self._fit_mode: str = 'single' - self.fitter = Fitter('lmfit (leastsq)') + Switch to a different aliases collection type. + + Parameters + ---------- + new_type : str + Aliases tag (e.g. ``'default'``). + """ + supported_tags = AliasesFactory.supported_tags() + if new_type not in supported_tags: + log.warning( + f"Unsupported aliases type '{new_type}'. " + f'Supported: {supported_tags}. ' + f"For more information, use 'show_supported_aliases_types()'", + ) + return + self.aliases = AliasesFactory.create(new_type) + self._aliases_type = new_type + console.paragraph('Aliases type changed to') + console.print(new_type) + + def show_supported_aliases_types(self) -> None: + """Print a table of supported aliases collection types.""" + AliasesFactory.show_supported() + + def show_current_aliases_type(self) -> None: + """Print the currently used aliases collection type.""" + console.paragraph('Current aliases type') + console.print(self._aliases_type) + + # ------------------------------------------------------------------ + # Constraints (switchable-category pattern) + # ------------------------------------------------------------------ + + @property + def constraints_type(self) -> str: + """Tag of the active constraints collection type.""" + return self._constraints_type + + @constraints_type.setter + def constraints_type(self, new_type: str) -> None: + """ + Switch to a different constraints collection type. + + Parameters + ---------- + new_type : str + Constraints tag (e.g. ``'default'``). + """ + supported_tags = ConstraintsFactory.supported_tags() + if new_type not in supported_tags: + log.warning( + f"Unsupported constraints type '{new_type}'. " + f'Supported: {supported_tags}. ' + f"For more information, use 'show_supported_constraints_types()'", + ) + return + self.constraints = ConstraintsFactory.create(new_type) + self._constraints_type = new_type + console.paragraph('Constraints type changed to') + console.print(new_type) + + def show_supported_constraints_types(self) -> None: + """Print a table of supported constraints collection types.""" + ConstraintsFactory.show_supported() + + def show_current_constraints_type(self) -> None: + """Print the currently used constraints collection type.""" + console.paragraph('Current constraints type') + console.print(self._constraints_type) def _get_params_as_dataframe( self, params: List[Union[NumericDescriptor, Parameter]], ) -> pd.DataFrame: - """Convert a list of parameters to a DataFrame. + """ + Convert a list of parameters to a DataFrame. - Args: - params: List of DescriptorFloat or Parameter objects. + Parameters + ---------- + params : List[Union[NumericDescriptor, Parameter]] + List of DescriptorFloat or Parameter objects. - Returns: + Returns + ------- + pd.DataFrame A pandas DataFrame containing parameter information. """ records = [] @@ -108,13 +251,11 @@ def _get_params_as_dataframe( return df def show_all_params(self) -> None: - """Print a table with all parameters for sample models and - experiments. - """ - sample_models_params = self.project.sample_models.parameters + """Print all parameters for structures and experiments.""" + structures_params = self.project.structures.parameters experiments_params = self.project.experiments.parameters - if not sample_models_params and not experiments_params: + if not structures_params and not experiments_params: log.warning('No parameters found.') return @@ -129,8 +270,8 @@ def show_all_params(self) -> None: 'fittable', ] - console.paragraph('All parameters for all sample models (🧩 data blocks)') - df = self._get_params_as_dataframe(sample_models_params) + console.paragraph('All parameters for all structures (🧩 data blocks)') + df = self._get_params_as_dataframe(structures_params) filtered_df = df[filtered_headers] tabler.render(filtered_df) @@ -140,13 +281,11 @@ def show_all_params(self) -> None: tabler.render(filtered_df) def show_fittable_params(self) -> None: - """Print a table with parameters that can be included in - fitting. - """ - sample_models_params = self.project.sample_models.fittable_parameters + """Print all fittable parameters.""" + structures_params = self.project.structures.fittable_parameters experiments_params = self.project.experiments.fittable_parameters - if not sample_models_params and not experiments_params: + if not structures_params and not experiments_params: log.warning('No fittable parameters found.') return @@ -163,8 +302,8 @@ def show_fittable_params(self) -> None: 'free', ] - console.paragraph('Fittable parameters for all sample models (🧩 data blocks)') - df = self._get_params_as_dataframe(sample_models_params) + console.paragraph('Fittable parameters for all structures (🧩 data blocks)') + df = self._get_params_as_dataframe(structures_params) filtered_df = df[filtered_headers] tabler.render(filtered_df) @@ -174,12 +313,10 @@ def show_fittable_params(self) -> None: tabler.render(filtered_df) def show_free_params(self) -> None: - """Print a table with only currently-free (varying) - parameters. - """ - sample_models_params = self.project.sample_models.free_parameters + """Print only currently free (varying) parameters.""" + structures_params = self.project.structures.free_parameters experiments_params = self.project.experiments.free_parameters - free_params = sample_models_params + experiments_params + free_params = structures_params + experiments_params if not free_params: log.warning('No free parameters found.') @@ -200,23 +337,23 @@ def show_free_params(self) -> None: ] console.paragraph( - 'Free parameters for both sample models (🧩 data blocks) ' - 'and experiments (🔬 data blocks)' + 'Free parameters for both structures (🧩 data blocks) and experiments (🔬 data blocks)' ) df = self._get_params_as_dataframe(free_params) filtered_df = df[filtered_headers] tabler.render(filtered_df) def how_to_access_parameters(self) -> None: - """Show Python access paths for all parameters. + """ + Show Python access paths for all parameters. The output explains how to reference specific parameters in code. """ - sample_models_params = self.project.sample_models.parameters + structures_params = self.project.structures.parameters experiments_params = self.project.experiments.parameters all_params = { - 'sample_models': sample_models_params, + 'structures': structures_params, 'experiments': experiments_params, } @@ -272,15 +409,16 @@ def how_to_access_parameters(self) -> None: ) def show_parameter_cif_uids(self) -> None: - """Show CIF unique IDs for all parameters. + """ + Show CIF unique IDs for all parameters. The output explains which unique identifiers are used when creating CIF-based constraints. """ - sample_models_params = self.project.sample_models.parameters + structures_params = self.project.structures.parameters experiments_params = self.project.experiments.parameters all_params = { - 'sample_models': sample_models_params, + 'structures': structures_params, 'experiments': experiments_params, } @@ -328,40 +466,6 @@ def show_parameter_cif_uids(self) -> None: columns_data=columns_data, ) - def show_current_calculator(self) -> None: - """Print the name of the currently selected calculator - engine. - """ - console.paragraph('Current calculator') - console.print(self.current_calculator) - - @staticmethod - def show_supported_calculators() -> None: - """Print a table of available calculator backends on this - system. - """ - CalculatorFactory.show_supported_calculators() - - @property - def current_calculator(self) -> str: - """The key/name of the active calculator backend.""" - return self._calculator_key - - @current_calculator.setter - def current_calculator(self, calculator_name: str) -> None: - """Switch to a different calculator backend. - - Args: - calculator_name: Calculator key to use (e.g. 'cryspy'). - """ - calculator = CalculatorFactory.create_calculator(calculator_name) - if calculator is None: - return - self.calculator = calculator - self._calculator_key = calculator_name - console.paragraph('Current calculator changed to') - console.print(self.current_calculator) - def show_current_minimizer(self) -> None: """Print the name of the currently selected minimizer.""" console.paragraph('Current minimizer') @@ -369,10 +473,8 @@ def show_current_minimizer(self) -> None: @staticmethod def show_available_minimizers() -> None: - """Print a table of available minimizer drivers on this - system. - """ - MinimizerFactory.show_available_minimizers() + """Print available minimizer drivers on this system.""" + MinimizerFactory.show_supported() @property def current_minimizer(self) -> Optional[str]: @@ -381,114 +483,92 @@ def current_minimizer(self) -> Optional[str]: @current_minimizer.setter def current_minimizer(self, selection: str) -> None: - """Switch to a different minimizer implementation. + """ + Switch to a different minimizer implementation. - Args: - selection: Minimizer selection string, e.g. - 'lmfit (leastsq)'. + Parameters + ---------- + selection : str + Minimizer selection string, e.g. 'lmfit'. """ self.fitter = Fitter(selection) console.paragraph('Current minimizer changed to') console.print(self.current_minimizer) + # ------------------------------------------------------------------ + # Fit mode (switchable-category pattern) + # ------------------------------------------------------------------ + @property - def fit_mode(self) -> str: - """Current fitting strategy: either 'single' or 'joint'.""" + def fit_mode(self) -> object: + """Fit-mode category item holding the active strategy.""" return self._fit_mode - @fit_mode.setter - def fit_mode(self, strategy: str) -> None: - """Set the fitting strategy. - - When set to 'joint', all experiments get default weights and - are used together in a single optimization. - - Args: - strategy: Either 'single' or 'joint'. + @property + def fit_mode_type(self) -> str: + """Tag of the active fit-mode category type.""" + return self._fit_mode_type - Raises: - ValueError: If an unsupported strategy value is - provided. + @fit_mode_type.setter + def fit_mode_type(self, new_type: str) -> None: """ - if strategy not in ['single', 'joint']: - raise ValueError("Fit mode must be either 'single' or 'joint'") - self._fit_mode = strategy - if strategy == 'joint' and not hasattr(self, 'joint_fit_experiments'): - # Pre-populate all experiments with weight 0.5 - self.joint_fit_experiments = JointFitExperiments() - for id in self.project.experiments.names: - self.joint_fit_experiments.add(id=id, weight=0.5) - console.paragraph('Current fit mode changed to') - console.print(self._fit_mode) - - def show_available_fit_modes(self) -> None: - """Print all supported fitting strategies and their - descriptions. + Switch to a different fit-mode category type. + + Parameters + ---------- + new_type : str + Fit-mode tag (e.g. ``'default'``). """ - strategies = [ - { - 'Strategy': 'single', - 'Description': 'Independent fitting of each experiment; no shared parameters', - }, - { - 'Strategy': 'joint', - 'Description': 'Simultaneous fitting of all experiments; ' - 'some parameters are shared', - }, - ] + supported_tags = FitModeFactory.supported_tags() + if new_type not in supported_tags: + log.warning( + f"Unsupported fit-mode type '{new_type}'. " + f'Supported: {supported_tags}. ' + f"For more information, use 'show_supported_fit_mode_types()'", + ) + return + self._fit_mode = FitModeFactory.create(new_type) + self._fit_mode_type = new_type + console.paragraph('Fit-mode type changed to') + console.print(new_type) - columns_headers = ['Strategy', 'Description'] - columns_alignment = ['left', 'left'] - columns_data = [] - for item in strategies: - strategy = item['Strategy'] - description = item['Description'] - columns_data.append([strategy, description]) + def show_supported_fit_mode_types(self) -> None: + """Print a table of supported fit-mode category types.""" + FitModeFactory.show_supported() - console.paragraph('Available fit modes') - render_table( - columns_headers=columns_headers, - columns_alignment=columns_alignment, - columns_data=columns_data, - ) + def show_current_fit_mode_type(self) -> None: + """Print the currently used fit-mode category type.""" + console.paragraph('Current fit-mode type') + console.print(self._fit_mode_type) + + # ------------------------------------------------------------------ + # Joint-fit experiments (category) + # ------------------------------------------------------------------ - def show_current_fit_mode(self) -> None: - """Print the currently active fitting strategy.""" - console.paragraph('Current fit mode') - console.print(self.fit_mode) + @property + def joint_fit_experiments(self) -> object: + """Per-experiment weight collection for joint fitting.""" + return self._joint_fit_experiments def show_constraints(self) -> None: """Print a table of all user-defined symbolic constraints.""" - constraints_dict = dict(self.constraints) - if not self.constraints._items: log.warning('No constraints defined.') return rows = [] - for constraint in constraints_dict.values(): - row = { - 'lhs_alias': constraint.lhs_alias.value, - 'rhs_expr': constraint.rhs_expr.value, - 'full expression': f'{constraint.lhs_alias.value} = {constraint.rhs_expr.value}', - } - rows.append(row) - - headers = ['lhs_alias', 'rhs_expr', 'full expression'] - alignments = ['left', 'left', 'left'] - rows = [[row[header] for header in headers] for row in rows] + for constraint in self.constraints: + rows.append([constraint.expression.value]) console.paragraph('User defined constraints') render_table( - columns_headers=headers, - columns_alignment=alignments, + columns_headers=['expression'], + columns_alignment=['left'], columns_data=rows, ) - def apply_constraints(self): - """Apply the currently defined constraints to the active - project. - """ + def apply_constraints(self) -> None: + """Apply currently defined constraints to the project.""" if not self.constraints._items: log.warning('No constraints defined.') return @@ -497,31 +577,41 @@ def apply_constraints(self): self.constraints_handler.set_constraints(self.constraints) self.constraints_handler.apply() - def fit(self): - """Execute fitting using the selected mode, calculator and - minimizer. + def fit(self, verbosity: str | None = None) -> None: + """ + Execute fitting for all experiments. This method performs the optimization but does not display results automatically. Call :meth:`show_fit_results` after fitting to see a summary of the fit quality and parameter values. - In 'single' mode, fits each experiment independently. In - 'joint' mode, performs a simultaneous fit across experiments - with weights. + In 'single' mode, fits each experiment independently. In 'joint' + mode, performs a simultaneous fit across experiments with + weights. Sets :attr:`fit_results` on success, which can be accessed - programmatically - (e.g., ``analysis.fit_results.reduced_chi_square``). - - Example:: - - project.analysis.fit() - project.analysis.show_fit_results() # Display results + programmatically (e.g., + ``analysis.fit_results.reduced_chi_square``). + + Parameters + ---------- + verbosity : str | None, default=None + Console output verbosity: ``'full'`` for detailed per- + experiment progress, ``'short'`` for a + one-row-per-experiment summary table, or ``'silent'`` for no + output. When ``None``, uses ``project.verbosity``. + + Raises + ------ + NotImplementedError + If the fit mode is not ``'single'`` or ``'joint'``. """ - sample_models = self.project.sample_models - if not sample_models: - log.warning('No sample models found in the project. Cannot run fit.') + verb = VerbosityEnum(verbosity if verbosity is not None else self.project.verbosity) + + structures = self.project.structures + if not structures: + log.warning('No structures found in the project. Cannot run fit.') return experiments = self.project.experiments @@ -530,23 +620,58 @@ def fit(self): return # Run the fitting process - if self.fit_mode == 'joint': - console.paragraph( - f"Using all experiments 🔬 {experiments.names} for '{self.fit_mode}' fitting" - ) + mode = FitModeEnum(self._fit_mode.mode.value) + if mode is FitModeEnum.JOINT: + # Auto-populate joint_fit_experiments if empty + if not len(self._joint_fit_experiments): + for id in experiments.names: + self._joint_fit_experiments.create(id=id, weight=0.5) + if verb is not VerbosityEnum.SILENT: + console.paragraph( + f"Using all experiments 🔬 {experiments.names} for '{mode.value}' fitting" + ) self.fitter.fit( - sample_models, + structures, experiments, - weights=self.joint_fit_experiments, + weights=self._joint_fit_experiments, analysis=self, + verbosity=verb, ) - elif self.fit_mode == 'single': - # TODO: Find a better way without creating dummy - # experiments? - for expt_name in experiments.names: + + # After fitting, get the results + self.fit_results = self.fitter.results + + elif mode is FitModeEnum.SINGLE: + expt_names = experiments.names + num_expts = len(expt_names) + + # Short mode: print header and create display handle once + short_headers = ['experiment', 'χ²', 'iterations', 'status'] + short_alignments = ['left', 'right', 'right', 'center'] + short_rows: list[list[str]] = [] + short_display_handle: object | None = None + if verb is VerbosityEnum.SHORT: + from easydiffraction.analysis.fit_helpers.tracking import _make_display_handle + + first = expt_names[0] + last = expt_names[-1] + minimizer_name = self.fitter.selection console.paragraph( - f"Using experiment 🔬 '{expt_name}' for '{self.fit_mode}' fitting" + f"Using {num_expts} experiments 🔬 from '{first}' to " + f"'{last}' for '{mode.value}' fitting" ) + console.print(f"🚀 Starting fit process with '{minimizer_name}'...") + console.print('📈 Goodness-of-fit (reduced χ²) per experiment:') + short_display_handle = _make_display_handle() + + # TODO: Find a better way without creating dummy + # experiments? + for _idx, expt_name in enumerate(expt_names, start=1): + if verb is VerbosityEnum.FULL: + console.paragraph( + f"Using experiment 🔬 '{expt_name}' for '{mode.value}' fitting" + ) + experiment = experiments[expt_name] dummy_experiments = Experiments() # TODO: Find a better name @@ -555,20 +680,64 @@ def fit(self): # parameters can be resolved correctly during fitting. object.__setattr__(dummy_experiments, '_parent', self.project) - dummy_experiments._add(experiment) + dummy_experiments.add(experiment) self.fitter.fit( - sample_models, + structures, dummy_experiments, analysis=self, + verbosity=verb, ) + + # After fitting, snapshot parameter values before + # they get overwritten by the next experiment's fit + results = self.fitter.results + snapshot: dict[str, dict] = {} + for param in results.parameters: + snapshot[param.unique_name] = { + 'value': param.value, + 'uncertainty': param.uncertainty, + 'units': param.units, + } + self._parameter_snapshots[expt_name] = snapshot + self.fit_results = results + + # Short mode: append one summary row and update in-place + if verb is VerbosityEnum.SHORT: + chi2_str = ( + f'{results.reduced_chi_square:.2f}' + if results.reduced_chi_square is not None + else '—' + ) + iters = str(self.fitter.minimizer.tracker.best_iteration or 0) + status = '✅' if results.success else '❌' + short_rows.append([expt_name, chi2_str, iters, status]) + render_table( + columns_headers=short_headers, + columns_alignment=short_alignments, + columns_data=short_rows, + display_handle=short_display_handle, + ) + + # Short mode: close the display handle + if short_display_handle is not None and hasattr(short_display_handle, 'close'): + from contextlib import suppress + + with suppress(Exception): + short_display_handle.close() + else: - raise NotImplementedError(f'Fit mode {self.fit_mode} not implemented yet.') + raise NotImplementedError(f'Fit mode {mode.value} not implemented yet.') - # After fitting, get the results - self.fit_results = self.fitter.results + # After fitting, save the project + # TODO: Consider saving individual data during sequential + # (single) fitting, instead of waiting until the end and save + # only the last one + if self.project.info.path is not None: + self.project.save() def show_fit_results(self) -> None: - """Display a summary of the fit results. + """ + Display a summary of the fit results. Renders the fit quality metrics (reduced χ², R-factors) and a table of fitted parameters with their starting values, final @@ -579,26 +748,28 @@ def show_fit_results(self) -> None: Example:: - project.analysis.fit() - project.analysis.show_fit_results() + project.analysis.fit() project.analysis.show_fit_results() """ - if not hasattr(self, 'fit_results') or self.fit_results is None: + if self.fit_results is None: log.warning('No fit results available. Run fit() first.') return - sample_models = self.project.sample_models + structures = self.project.structures experiments = self.project.experiments - self.fitter._process_fit_results(sample_models, experiments) + self.fitter._process_fit_results(structures, experiments) - def _update_categories(self, called_by_minimizer=False) -> None: - """Update all categories owned by Analysis. + def _update_categories(self, called_by_minimizer: bool = False) -> None: + """ + Update all categories owned by Analysis. This ensures aliases and constraints are up-to-date before serialization or after parameter changes. - Args: - called_by_minimizer: Whether this is called during fitting. + Parameters + ---------- + called_by_minimizer : bool, default=False + Whether this is called during fitting. """ # Apply constraints to sync dependent parameters if self.constraints._items: @@ -610,10 +781,13 @@ def _update_categories(self, called_by_minimizer=False) -> None: if hasattr(category, '_update'): category._update(called_by_minimizer=called_by_minimizer) - def as_cif(self): - """Serialize the analysis section to a CIF string. + def as_cif(self) -> str: + """ + Serialize the analysis section to a CIF string. - Returns: + Returns + ------- + str The analysis section represented as a CIF document string. """ from easydiffraction.io.cif.serialize import analysis_to_cif @@ -622,9 +796,7 @@ def as_cif(self): return analysis_to_cif(self) def show_as_cif(self) -> None: - """Render the analysis section as CIF in a formatted console - view. - """ + """Render the analysis section as CIF in console.""" cif_text: str = self.as_cif() paragraph_title: str = 'Analysis 🧮 info as cif' console.paragraph(paragraph_title) diff --git a/src/easydiffraction/analysis/calculators/__init__.py b/src/easydiffraction/analysis/calculators/__init__.py index 429f2648..38bd1aa0 100644 --- a/src/easydiffraction/analysis/calculators/__init__.py +++ b/src/easydiffraction/analysis/calculators/__init__.py @@ -1,2 +1,6 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause + +from easydiffraction.analysis.calculators.crysfml import CrysfmlCalculator +from easydiffraction.analysis.calculators.cryspy import CryspyCalculator +from easydiffraction.analysis.calculators.pdffit import PdffitCalculator diff --git a/src/easydiffraction/analysis/calculators/base.py b/src/easydiffraction/analysis/calculators/base.py index 80c86a36..bd667ac8 100644 --- a/src/easydiffraction/analysis/calculators/base.py +++ b/src/easydiffraction/analysis/calculators/base.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause from abc import ABC @@ -6,9 +6,9 @@ import numpy as np -from easydiffraction.experiments.experiment.base import ExperimentBase -from easydiffraction.sample_models.sample_model.base import SampleModelBase -from easydiffraction.sample_models.sample_models import SampleModels +from easydiffraction.datablocks.experiment.item.base import ExperimentBase +from easydiffraction.datablocks.structure.collection import Structures +from easydiffraction.datablocks.structure.item.base import Structure class CalculatorBase(ABC): @@ -17,41 +17,48 @@ class CalculatorBase(ABC): @property @abstractmethod def name(self) -> str: + """Short identifier of the calculation engine.""" pass @property @abstractmethod def engine_imported(self) -> bool: + """True if the underlying calculation library is available.""" pass @abstractmethod def calculate_structure_factors( self, - sample_model: SampleModelBase, + structure: Structure, experiment: ExperimentBase, + called_by_minimizer: bool, ) -> None: - """Calculate structure factors for a single sample model and - experiment. - """ + """Calculate structure factors for one experiment.""" pass @abstractmethod def calculate_pattern( self, - sample_model: SampleModels, # TODO: SampleModelBase? + structure: Structures, # TODO: Structure? experiment: ExperimentBase, called_by_minimizer: bool, ) -> np.ndarray: - """Calculate the diffraction pattern for a single sample model - and experiment. + """ + Calculate diffraction pattern for one structure-experiment pair. - Args: - sample_model: The sample model object. - experiment: The experiment object. - called_by_minimizer: Whether the calculation is called by a - minimizer. + Parameters + ---------- + structure : Structures + The structure object. + experiment : ExperimentBase + The experiment object. + called_by_minimizer : bool + Whether the calculation is called by a minimizer. Default is + False. - Returns: + Returns + ------- + np.ndarray The calculated diffraction pattern as a NumPy array. """ pass diff --git a/src/easydiffraction/analysis/calculators/crysfml.py b/src/easydiffraction/analysis/calculators/crysfml.py index babd3e08..34624e2b 100644 --- a/src/easydiffraction/analysis/calculators/crysfml.py +++ b/src/easydiffraction/analysis/calculators/crysfml.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause from typing import Any @@ -9,10 +9,12 @@ import numpy as np from easydiffraction.analysis.calculators.base import CalculatorBase -from easydiffraction.experiments.experiment.base import ExperimentBase -from easydiffraction.experiments.experiments import Experiments -from easydiffraction.sample_models.sample_model.base import SampleModelBase -from easydiffraction.sample_models.sample_models import SampleModels +from easydiffraction.analysis.calculators.factory import CalculatorFactory +from easydiffraction.core.metadata import TypeInfo +from easydiffraction.datablocks.experiment.collection import Experiments +from easydiffraction.datablocks.experiment.item.base import ExperimentBase +from easydiffraction.datablocks.structure.collection import Structures +from easydiffraction.datablocks.structure.item.base import Structure try: from pycrysfml import cfml_py_utilities @@ -27,53 +29,71 @@ cfml_py_utilities = None +@CalculatorFactory.register class CrysfmlCalculator(CalculatorBase): """Wrapper for Crysfml library.""" + type_info = TypeInfo( + tag='crysfml', + description='CrysFML library for crystallographic calculations', + ) engine_imported: bool = cfml_py_utilities is not None @property def name(self) -> str: + """Short identifier of this calculator engine.""" return 'crysfml' def calculate_structure_factors( self, - sample_models: SampleModels, + structures: Structures, experiments: Experiments, ) -> None: - """Call Crysfml to calculate structure factors. - - Args: - sample_models: The sample models to calculate structure - factors for. - experiments: The experiments associated with the sample - models. + """ + Call Crysfml to calculate structure factors. + + Parameters + ---------- + structures : Structures + The structures to calculate structure factors for. + experiments : Experiments + The experiments associated with the sample models. + + Raises + ------ + NotImplementedError + HKL calculation is not implemented for CrysfmlCalculator. """ raise NotImplementedError('HKL calculation is not implemented for CrysfmlCalculator.') def calculate_pattern( self, - sample_model: SampleModels, + structure: Structures, experiment: ExperimentBase, called_by_minimizer: bool = False, ) -> Union[np.ndarray, List[float]]: - """Calculates the diffraction pattern using Crysfml for the - given sample model and experiment. - - Args: - sample_model: The sample model to calculate the pattern for. - experiment: The experiment associated with the sample model. - called_by_minimizer: Whether the calculation is called by a - minimizer. - - Returns: + """ + Calculate the diffraction pattern using Crysfml. + + Parameters + ---------- + structure : Structures + The structure to calculate the pattern for. + experiment : ExperimentBase + The experiment associated with the structure. + called_by_minimizer : bool, default=False + Whether the calculation is called by a minimizer. + + Returns + ------- + Union[np.ndarray, List[float]] The calculated diffraction pattern as a NumPy array or a - list of floats. + list of floats. """ # Intentionally unused, required by public API/signature del called_by_minimizer - crysfml_dict = self._crysfml_dict(sample_model, experiment) + crysfml_dict = self._crysfml_dict(structure, experiment) try: _, y = cfml_py_utilities.cw_powder_pattern_from_dict(crysfml_dict) y = self._adjust_pattern_length(y, len(experiment.data.x)) @@ -87,13 +107,19 @@ def _adjust_pattern_length( pattern: List[float], target_length: int, ) -> List[float]: - """Adjusts the length of the pattern to match the target length. - - Args: - pattern: The pattern to adjust. - target_length: The desired length of the pattern. - - Returns: + """ + Adjust the pattern length to match the target length. + + Parameters + ---------- + pattern : List[float] + The pattern to adjust. + target_length : int + The desired length of the pattern. + + Returns + ------- + List[float] The adjusted pattern. """ # TODO: Check the origin of this discrepancy coming from @@ -104,53 +130,62 @@ def _adjust_pattern_length( def _crysfml_dict( self, - sample_model: SampleModels, + structure: Structures, experiment: ExperimentBase, - ) -> Dict[str, Union[ExperimentBase, SampleModelBase]]: - """Converts the sample model and experiment into a dictionary - format for Crysfml. - - Args: - sample_model: The sample model to convert. - experiment: The experiment to convert. - - Returns: - A dictionary representation of the sample model and - experiment. + ) -> Dict[str, Union[ExperimentBase, Structure]]: """ - sample_model_dict = self._convert_sample_model_to_dict(sample_model) + Convert structure and experiment into a Crysfml dictionary. + + Parameters + ---------- + structure : Structures + The structure to convert. + experiment : ExperimentBase + The experiment to convert. + + Returns + ------- + Dict[str, Union[ExperimentBase, Structure]] + A dictionary representation of the structure and experiment. + """ + structure_dict = self._convert_structure_to_dict(structure) experiment_dict = self._convert_experiment_to_dict(experiment) return { - 'phases': [sample_model_dict], + 'phases': [structure_dict], 'experiments': [experiment_dict], } - def _convert_sample_model_to_dict( + def _convert_structure_to_dict( self, - sample_model: SampleModelBase, + structure: Structure, ) -> Dict[str, Any]: - """Converts a sample model into a dictionary format. + """ + Convert a structure into a dictionary format. - Args: - sample_model: The sample model to convert. + Parameters + ---------- + structure : Structure + The structure to convert. - Returns: - A dictionary representation of the sample model. + Returns + ------- + Dict[str, Any] + A dictionary representation of the structure. """ - sample_model_dict = { - sample_model.name: { - '_space_group_name_H-M_alt': sample_model.space_group.name_h_m.value, - '_cell_length_a': sample_model.cell.length_a.value, - '_cell_length_b': sample_model.cell.length_b.value, - '_cell_length_c': sample_model.cell.length_c.value, - '_cell_angle_alpha': sample_model.cell.angle_alpha.value, - '_cell_angle_beta': sample_model.cell.angle_beta.value, - '_cell_angle_gamma': sample_model.cell.angle_gamma.value, + structure_dict = { + structure.name: { + '_space_group_name_H-M_alt': structure.space_group.name_h_m.value, + '_cell_length_a': structure.cell.length_a.value, + '_cell_length_b': structure.cell.length_b.value, + '_cell_length_c': structure.cell.length_c.value, + '_cell_angle_alpha': structure.cell.angle_alpha.value, + '_cell_angle_beta': structure.cell.angle_beta.value, + '_cell_angle_gamma': structure.cell.angle_gamma.value, '_atom_site': [], } } - for atom in sample_model.atom_sites: + for atom in structure.atom_sites: atom_site = { '_label': atom.label.value, '_type_symbol': atom.type_symbol.value, @@ -161,20 +196,25 @@ def _convert_sample_model_to_dict( '_adp_type': 'Biso', # Assuming Biso for simplicity '_B_iso_or_equiv': atom.b_iso.value, } - sample_model_dict[sample_model.name]['_atom_site'].append(atom_site) + structure_dict[structure.name]['_atom_site'].append(atom_site) - return sample_model_dict + return structure_dict def _convert_experiment_to_dict( self, experiment: ExperimentBase, ) -> Dict[str, Any]: - """Converts an experiment into a dictionary format. + """ + Convert an experiment into a dictionary format. - Args: - experiment: The experiment to convert. + Parameters + ---------- + experiment : ExperimentBase + The experiment to convert. - Returns: + Returns + ------- + Dict[str, Any] A dictionary representation of the experiment. """ expt_type = getattr(experiment, 'type', None) diff --git a/src/easydiffraction/analysis/calculators/cryspy.py b/src/easydiffraction/analysis/calculators/cryspy.py index 18e7858d..b6a0e4d6 100644 --- a/src/easydiffraction/analysis/calculators/cryspy.py +++ b/src/easydiffraction/analysis/calculators/cryspy.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause import contextlib @@ -12,9 +12,12 @@ import numpy as np from easydiffraction.analysis.calculators.base import CalculatorBase -from easydiffraction.experiments.experiment.base import ExperimentBase -from easydiffraction.experiments.experiment.enums import BeamModeEnum -from easydiffraction.sample_models.sample_model.base import SampleModelBase +from easydiffraction.analysis.calculators.factory import CalculatorFactory +from easydiffraction.core.metadata import TypeInfo +from easydiffraction.datablocks.experiment.item.base import ExperimentBase +from easydiffraction.datablocks.experiment.item.enums import BeamModeEnum +from easydiffraction.datablocks.experiment.item.enums import SampleFormEnum +from easydiffraction.datablocks.structure.item.base import Structure try: import cryspy @@ -30,17 +33,24 @@ cryspy = None +@CalculatorFactory.register class CryspyCalculator(CalculatorBase): - """Cryspy-based diffraction calculator. + """ + Cryspy-based diffraction calculator. Converts EasyDiffraction models into Cryspy objects and computes patterns. """ + type_info = TypeInfo( + tag='cryspy', + description='CrysPy library for crystallographic calculations', + ) engine_imported: bool = cryspy is not None @property def name(self) -> str: + """Short identifier of this calculator engine.""" return 'cryspy' def __init__(self) -> None: @@ -49,55 +59,102 @@ def __init__(self) -> None: def calculate_structure_factors( self, - sample_model: SampleModelBase, + structure: Structure, experiment: ExperimentBase, + called_by_minimizer: bool = False, ) -> None: - """Raises a NotImplementedError as HKL calculation is not - implemented. - - Args: - sample_model: The sample model to calculate structure - factors for. - experiment: The experiment associated with the sample - models. """ - raise NotImplementedError('HKL calculation is not implemented for CryspyCalculator.') + Raise NotImplementedError as HKL calculation is not implemented. + + Parameters + ---------- + structure : Structure + The structure to calculate structure factors for. + experiment : ExperimentBase + The experiment associated with the sample models. + called_by_minimizer : bool, default=False + Whether the calculation is called by a minimizer. + """ + combined_name = f'{structure.name}_{experiment.name}' + + if called_by_minimizer: + if self._cryspy_dicts and combined_name in self._cryspy_dicts: + cryspy_dict = self._recreate_cryspy_dict(structure, experiment) + else: + cryspy_obj = self._recreate_cryspy_obj(structure, experiment) + cryspy_dict = cryspy_obj.get_dictionary() + else: + cryspy_obj = self._recreate_cryspy_obj(structure, experiment) + cryspy_dict = cryspy_obj.get_dictionary() + + self._cryspy_dicts[combined_name] = copy.deepcopy(cryspy_dict) + + cryspy_in_out_dict: Dict[str, Any] = {} + + # Calculate the pattern using Cryspy + # TODO: Redirect stderr to suppress Cryspy warnings. + # This is a temporary solution to avoid cluttering the output. + # E.g. cryspy/A_functions_base/powder_diffraction_tof.py:106: + # RuntimeWarning: overflow encountered in exp + # Remove this when Cryspy is updated to handle warnings better. + with contextlib.redirect_stderr(io.StringIO()): + rhochi_calc_chi_sq_by_dictionary( + cryspy_dict, + dict_in_out=cryspy_in_out_dict, + flag_use_precalculated_data=False, + flag_calc_analytical_derivatives=False, + ) + + cryspy_block_name = f'diffrn_{experiment.name}' + + try: + y_calc = cryspy_in_out_dict[cryspy_block_name]['intensity_calc'] + stol = cryspy_in_out_dict[cryspy_block_name]['sthovl'] + except KeyError: + print(f'[CryspyCalculator] Error: No calculated data for {cryspy_block_name}') + return [], [] + + return stol, y_calc def calculate_pattern( self, - sample_model: SampleModelBase, + structure: Structure, experiment: ExperimentBase, called_by_minimizer: bool = False, ) -> Union[np.ndarray, List[float]]: - """Calculates the diffraction pattern using Cryspy for the given - sample model and experiment. - - We only recreate the cryspy_obj if this method is - - NOT called by the minimizer, or - - the cryspy_dict is NOT yet created. - In other cases, we are modifying the existing cryspy_dict - This allows significantly speeding up the calculation - - Args: - sample_model: The sample model to calculate the pattern for. - experiment: The experiment associated with the sample model. - called_by_minimizer: Whether the calculation is called by a - minimizer. - - Returns: + """ + Calculate the diffraction pattern using Cryspy. + + We only recreate the cryspy_obj if this method is - NOT called + by the minimizer, or - the cryspy_dict is NOT yet created. In + other cases, we are modifying the existing cryspy_dict This + allows significantly speeding up the calculation + + Parameters + ---------- + structure : Structure + The structure to calculate the pattern for. + experiment : ExperimentBase + The experiment associated with the structure. + called_by_minimizer : bool, default=False + Whether the calculation is called by a minimizer. + + Returns + ------- + Union[np.ndarray, List[float]] The calculated diffraction pattern as a NumPy array or a - list of floats. + list of floats. """ - combined_name = f'{sample_model.name}_{experiment.name}' + combined_name = f'{structure.name}_{experiment.name}' if called_by_minimizer: if self._cryspy_dicts and combined_name in self._cryspy_dicts: - cryspy_dict = self._recreate_cryspy_dict(sample_model, experiment) + cryspy_dict = self._recreate_cryspy_dict(structure, experiment) else: - cryspy_obj = self._recreate_cryspy_obj(sample_model, experiment) + cryspy_obj = self._recreate_cryspy_obj(structure, experiment) cryspy_dict = cryspy_obj.get_dictionary() else: - cryspy_obj = self._recreate_cryspy_obj(sample_model, experiment) + cryspy_obj = self._recreate_cryspy_obj(structure, experiment) cryspy_dict = cryspy_obj.get_dictionary() self._cryspy_dicts[combined_name] = copy.deepcopy(cryspy_dict) @@ -141,126 +198,153 @@ def calculate_pattern( def _recreate_cryspy_dict( self, - sample_model: SampleModelBase, + structure: Structure, experiment: ExperimentBase, ) -> Dict[str, Any]: - """Recreates the Cryspy dictionary for the given sample model - and experiment. - - Args: - sample_model: The sample model to update. - experiment: The experiment to update. - - Returns: + """ + Recreate the Cryspy dictionary for structure and experiment. + + Parameters + ---------- + structure : Structure + The structure to update. + experiment : ExperimentBase + The experiment to update. + + Returns + ------- + Dict[str, Any] The updated Cryspy dictionary. """ - combined_name = f'{sample_model.name}_{experiment.name}' + combined_name = f'{structure.name}_{experiment.name}' cryspy_dict = copy.deepcopy(self._cryspy_dicts[combined_name]) - cryspy_model_id = f'crystal_{sample_model.name}' + cryspy_model_id = f'crystal_{structure.name}' cryspy_model_dict = cryspy_dict[cryspy_model_id] - # Update sample model parameters + ################################ + # Update structure parameters + ################################ # Cell cryspy_cell = cryspy_model_dict['unit_cell_parameters'] - cryspy_cell[0] = sample_model.cell.length_a.value - cryspy_cell[1] = sample_model.cell.length_b.value - cryspy_cell[2] = sample_model.cell.length_c.value - cryspy_cell[3] = np.deg2rad(sample_model.cell.angle_alpha.value) - cryspy_cell[4] = np.deg2rad(sample_model.cell.angle_beta.value) - cryspy_cell[5] = np.deg2rad(sample_model.cell.angle_gamma.value) + cryspy_cell[0] = structure.cell.length_a.value + cryspy_cell[1] = structure.cell.length_b.value + cryspy_cell[2] = structure.cell.length_c.value + cryspy_cell[3] = np.deg2rad(structure.cell.angle_alpha.value) + cryspy_cell[4] = np.deg2rad(structure.cell.angle_beta.value) + cryspy_cell[5] = np.deg2rad(structure.cell.angle_gamma.value) # Atomic coordinates cryspy_xyz = cryspy_model_dict['atom_fract_xyz'] - for idx, atom_site in enumerate(sample_model.atom_sites): + for idx, atom_site in enumerate(structure.atom_sites): cryspy_xyz[0][idx] = atom_site.fract_x.value cryspy_xyz[1][idx] = atom_site.fract_y.value cryspy_xyz[2][idx] = atom_site.fract_z.value # Atomic occupancies cryspy_occ = cryspy_model_dict['atom_occupancy'] - for idx, atom_site in enumerate(sample_model.atom_sites): + for idx, atom_site in enumerate(structure.atom_sites): cryspy_occ[idx] = atom_site.occupancy.value # Atomic ADPs - Biso only for now cryspy_biso = cryspy_model_dict['atom_b_iso'] - for idx, atom_site in enumerate(sample_model.atom_sites): + for idx, atom_site in enumerate(structure.atom_sites): cryspy_biso[idx] = atom_site.b_iso.value + ############################## # Update experiment parameters - - if experiment.type.beam_mode.value == BeamModeEnum.CONSTANT_WAVELENGTH: - cryspy_expt_name = f'pd_{experiment.name}' + ############################## + + if experiment.type.sample_form.value == SampleFormEnum.POWDER: + if experiment.type.beam_mode.value == BeamModeEnum.CONSTANT_WAVELENGTH: + cryspy_expt_name = f'pd_{experiment.name}' + cryspy_expt_dict = cryspy_dict[cryspy_expt_name] + + # Instrument + cryspy_expt_dict['offset_ttheta'][0] = np.deg2rad( + experiment.instrument.calib_twotheta_offset.value + ) + cryspy_expt_dict['wavelength'][0] = experiment.instrument.setup_wavelength.value + + # Peak + cryspy_resolution = cryspy_expt_dict['resolution_parameters'] + cryspy_resolution[0] = experiment.peak.broad_gauss_u.value + cryspy_resolution[1] = experiment.peak.broad_gauss_v.value + cryspy_resolution[2] = experiment.peak.broad_gauss_w.value + cryspy_resolution[3] = experiment.peak.broad_lorentz_x.value + cryspy_resolution[4] = experiment.peak.broad_lorentz_y.value + + elif experiment.type.beam_mode.value == BeamModeEnum.TIME_OF_FLIGHT: + cryspy_expt_name = f'tof_{experiment.name}' + cryspy_expt_dict = cryspy_dict[cryspy_expt_name] + + # Instrument + cryspy_expt_dict['zero'][0] = experiment.instrument.calib_d_to_tof_offset.value + cryspy_expt_dict['dtt1'][0] = experiment.instrument.calib_d_to_tof_linear.value + cryspy_expt_dict['dtt2'][0] = experiment.instrument.calib_d_to_tof_quad.value + cryspy_expt_dict['ttheta_bank'] = np.deg2rad( + experiment.instrument.setup_twotheta_bank.value + ) + + # Peak + cryspy_sigma = cryspy_expt_dict['profile_sigmas'] + cryspy_sigma[0] = experiment.peak.broad_gauss_sigma_0.value + cryspy_sigma[1] = experiment.peak.broad_gauss_sigma_1.value + cryspy_sigma[2] = experiment.peak.broad_gauss_sigma_2.value + + cryspy_beta = cryspy_expt_dict['profile_betas'] + cryspy_beta[0] = experiment.peak.broad_mix_beta_0.value + cryspy_beta[1] = experiment.peak.broad_mix_beta_1.value + + cryspy_alpha = cryspy_expt_dict['profile_alphas'] + cryspy_alpha[0] = experiment.peak.asym_alpha_0.value + cryspy_alpha[1] = experiment.peak.asym_alpha_1.value + + if experiment.type.sample_form.value == SampleFormEnum.SINGLE_CRYSTAL: + cryspy_expt_name = f'diffrn_{experiment.name}' cryspy_expt_dict = cryspy_dict[cryspy_expt_name] # Instrument - cryspy_expt_dict['offset_ttheta'][0] = np.deg2rad( - experiment.instrument.calib_twotheta_offset.value - ) - cryspy_expt_dict['wavelength'][0] = experiment.instrument.setup_wavelength.value - - # Peak - cryspy_resolution = cryspy_expt_dict['resolution_parameters'] - cryspy_resolution[0] = experiment.peak.broad_gauss_u.value - cryspy_resolution[1] = experiment.peak.broad_gauss_v.value - cryspy_resolution[2] = experiment.peak.broad_gauss_w.value - cryspy_resolution[3] = experiment.peak.broad_lorentz_x.value - cryspy_resolution[4] = experiment.peak.broad_lorentz_y.value - - elif experiment.type.beam_mode.value == BeamModeEnum.TIME_OF_FLIGHT: - cryspy_expt_name = f'tof_{experiment.name}' - cryspy_expt_dict = cryspy_dict[cryspy_expt_name] - - # Instrument - cryspy_expt_dict['zero'][0] = experiment.instrument.calib_d_to_tof_offset.value - cryspy_expt_dict['dtt1'][0] = experiment.instrument.calib_d_to_tof_linear.value - cryspy_expt_dict['dtt2'][0] = experiment.instrument.calib_d_to_tof_quad.value - cryspy_expt_dict['ttheta_bank'] = np.deg2rad( - experiment.instrument.setup_twotheta_bank.value - ) - - # Peak - cryspy_sigma = cryspy_expt_dict['profile_sigmas'] - cryspy_sigma[0] = experiment.peak.broad_gauss_sigma_0.value - cryspy_sigma[1] = experiment.peak.broad_gauss_sigma_1.value - cryspy_sigma[2] = experiment.peak.broad_gauss_sigma_2.value + if experiment.type.beam_mode.value == BeamModeEnum.CONSTANT_WAVELENGTH: + cryspy_expt_dict['wavelength'][0] = experiment.instrument.setup_wavelength.value - cryspy_beta = cryspy_expt_dict['profile_betas'] - cryspy_beta[0] = experiment.peak.broad_mix_beta_0.value - cryspy_beta[1] = experiment.peak.broad_mix_beta_1.value - - cryspy_alpha = cryspy_expt_dict['profile_alphas'] - cryspy_alpha[0] = experiment.peak.asym_alpha_0.value - cryspy_alpha[1] = experiment.peak.asym_alpha_1.value + # Extinction + cryspy_expt_dict['extinction_radius'][0] = experiment.extinction.radius.value + cryspy_expt_dict['extinction_mosaicity'][0] = experiment.extinction.mosaicity.value return cryspy_dict def _recreate_cryspy_obj( self, - sample_model: SampleModelBase, + structure: Structure, experiment: ExperimentBase, - ) -> Any: - """Recreates the Cryspy object for the given sample model and - experiment. - - Args: - sample_model: The sample model to recreate. - experiment: The experiment to recreate. - - Returns: + ) -> object: + """ + Recreate the Cryspy object for structure and experiment. + + Parameters + ---------- + structure : Structure + The structure to recreate. + experiment : ExperimentBase + The experiment to recreate. + + Returns + ------- + object The recreated Cryspy object. """ cryspy_obj = str_to_globaln('') - cryspy_sample_model_cif = self._convert_sample_model_to_cryspy_cif(sample_model) - cryspy_sample_model_obj = str_to_globaln(cryspy_sample_model_cif) - cryspy_obj.add_items(cryspy_sample_model_obj.items) + cryspy_structure_cif = self._convert_structure_to_cryspy_cif(structure) + cryspy_structure_obj = str_to_globaln(cryspy_structure_cif) + cryspy_obj.add_items(cryspy_structure_obj.items) # Add single experiment to cryspy_obj cryspy_experiment_cif = self._convert_experiment_to_cryspy_cif( experiment, - linked_phase=sample_model, + linked_structure=structure, ) cryspy_experiment_obj = str_to_globaln(cryspy_experiment_cif) @@ -268,41 +352,55 @@ def _recreate_cryspy_obj( return cryspy_obj - def _convert_sample_model_to_cryspy_cif( + def _convert_structure_to_cryspy_cif( self, - sample_model: SampleModelBase, + structure: Structure, ) -> str: - """Converts a sample model to a Cryspy CIF string. + """ + Convert a structure to a Cryspy CIF string. - Args: - sample_model: The sample model to convert. + Parameters + ---------- + structure : Structure + The structure to convert. - Returns: - The Cryspy CIF string representation of the sample model. + Returns + ------- + str + The Cryspy CIF string representation of the structure. """ - return sample_model.as_cif + return structure.as_cif def _convert_experiment_to_cryspy_cif( self, experiment: ExperimentBase, - linked_phase: Any, + linked_structure: object, ) -> str: - """Converts an experiment to a Cryspy CIF string. - - Args: - experiment: The experiment to convert. - linked_phase: The linked phase associated with the - experiment. - - Returns: + """ + Convert an experiment to a Cryspy CIF string. + + Parameters + ---------- + experiment : ExperimentBase + The experiment to convert. + linked_structure : object + The structure linked to the experiment. + + Returns + ------- + str The Cryspy CIF string representation of the experiment. """ + # Try to get experiment attributes expt_type = getattr(experiment, 'type', None) instrument = getattr(experiment, 'instrument', None) peak = getattr(experiment, 'peak', None) + extinction = getattr(experiment, 'extinction', None) + # Add experiment datablock name cif_lines = [f'data_{experiment.name}'] + # Add experiment type attribute dat if expt_type is not None: cif_lines.append('') radiation_probe = expt_type.radiation_probe.value @@ -310,22 +408,39 @@ def _convert_experiment_to_cryspy_cif( radiation_probe = radiation_probe.replace('xray', 'X-rays') cif_lines.append(f'_setup_radiation {radiation_probe}') + # Add instrument attribute data if instrument: # Restrict to only attributes relevant for the beam mode to # avoid probing non-existent guarded attributes (which # triggers diagnostics). if expt_type.beam_mode.value == BeamModeEnum.CONSTANT_WAVELENGTH: - instrument_mapping = { - 'setup_wavelength': '_setup_wavelength', - 'calib_twotheta_offset': '_setup_offset_2theta', - } + if expt_type.sample_form.value == SampleFormEnum.POWDER: + instrument_mapping = { + 'setup_wavelength': '_setup_wavelength', + 'calib_twotheta_offset': '_setup_offset_2theta', + } + elif expt_type.sample_form.value == SampleFormEnum.SINGLE_CRYSTAL: + instrument_mapping = { + 'setup_wavelength': '_setup_wavelength', + } + # Add dummy 0.0 value for _setup_field required by + # Cryspy + cif_lines.append('') + cif_lines.append('_setup_field 0.0') elif expt_type.beam_mode.value == BeamModeEnum.TIME_OF_FLIGHT: - instrument_mapping = { - 'setup_twotheta_bank': '_tof_parameters_2theta_bank', - 'calib_d_to_tof_offset': '_tof_parameters_Zero', - 'calib_d_to_tof_linear': '_tof_parameters_Dtt1', - 'calib_d_to_tof_quad': '_tof_parameters_dtt2', - } + if expt_type.sample_form.value == SampleFormEnum.POWDER: + instrument_mapping = { + 'setup_twotheta_bank': '_tof_parameters_2theta_bank', + 'calib_d_to_tof_offset': '_tof_parameters_Zero', + 'calib_d_to_tof_linear': '_tof_parameters_Dtt1', + 'calib_d_to_tof_quad': '_tof_parameters_dtt2', + } + elif expt_type.sample_form.value == SampleFormEnum.SINGLE_CRYSTAL: + instrument_mapping = {} # TODO: Check this mapping! + # Add dummy 0.0 value for _setup_field required by + # Cryspy + cif_lines.append('') + cif_lines.append('_setup_field 0.0') cif_lines.append('') for local_attr_name, engine_key_name in instrument_mapping.items(): # attr_obj = instrument.__dict__.get(local_attr_name) @@ -333,6 +448,7 @@ def _convert_experiment_to_cryspy_cif( if attr_obj is not None: cif_lines.append(f'{engine_key_name} {attr_obj.value}') + # Add peak attribute data if peak: if expt_type.beam_mode.value == BeamModeEnum.CONSTANT_WAVELENGTH: peak_mapping = { @@ -360,57 +476,140 @@ def _convert_experiment_to_cryspy_cif( if attr_obj is not None: cif_lines.append(f'{engine_key_name} {attr_obj.value}') - x_data = experiment.data.x - twotheta_min = f'{np.round(x_data.min(), 5):.5f}' # float(x_data.min()) - twotheta_max = f'{np.round(x_data.max(), 5):.5f}' # float(x_data.max()) - cif_lines.append('') - if expt_type.beam_mode.value == BeamModeEnum.CONSTANT_WAVELENGTH: - cif_lines.append(f'_range_2theta_min {twotheta_min}') - cif_lines.append(f'_range_2theta_max {twotheta_max}') - elif expt_type.beam_mode.value == BeamModeEnum.TIME_OF_FLIGHT: - cif_lines.append(f'_range_time_min {twotheta_min}') - cif_lines.append(f'_range_time_max {twotheta_max}') - - cif_lines.append('') - cif_lines.append('loop_') - cif_lines.append('_phase_label') - cif_lines.append('_phase_scale') - cif_lines.append(f'{linked_phase.name} 1.0') - - if expt_type.beam_mode.value == BeamModeEnum.CONSTANT_WAVELENGTH: + # Add extinction attribute data + if extinction and expt_type.sample_form.value == SampleFormEnum.SINGLE_CRYSTAL: + extinction_mapping = { + 'mosaicity': '_extinction_mosaicity', + 'radius': '_extinction_radius', + } cif_lines.append('') - cif_lines.append('loop_') - cif_lines.append('_pd_background_2theta') - cif_lines.append('_pd_background_intensity') - cif_lines.append(f'{twotheta_min} 0.0') - cif_lines.append(f'{twotheta_max} 0.0') - elif expt_type.beam_mode.value == BeamModeEnum.TIME_OF_FLIGHT: + cif_lines.append('_extinction_model gauss') + for local_attr_name, engine_key_name in extinction_mapping.items(): + attr_obj = getattr(extinction, local_attr_name) + if attr_obj is not None: + cif_lines.append(f'{engine_key_name} {attr_obj.value}') + + # Add range data + if expt_type.sample_form.value == SampleFormEnum.POWDER: + x_data = experiment.data.x + twotheta_min = f'{np.round(x_data.min(), 5):.5f}' # float(x_data.min()) + twotheta_max = f'{np.round(x_data.max(), 5):.5f}' # float(x_data.max()) cif_lines.append('') - cif_lines.append('loop_') - cif_lines.append('_tof_backgroundpoint_time') - cif_lines.append('_tof_backgroundpoint_intensity') - cif_lines.append(f'{twotheta_min} 0.0') - cif_lines.append(f'{twotheta_max} 0.0') + if expt_type.beam_mode.value == BeamModeEnum.CONSTANT_WAVELENGTH: + cif_lines.append(f'_range_2theta_min {twotheta_min}') + cif_lines.append(f'_range_2theta_max {twotheta_max}') + elif expt_type.beam_mode.value == BeamModeEnum.TIME_OF_FLIGHT: + cif_lines.append(f'_range_time_min {twotheta_min}') + cif_lines.append(f'_range_time_max {twotheta_max}') - if expt_type.beam_mode.value == BeamModeEnum.CONSTANT_WAVELENGTH: + # Add orientation matrix data + # Hardcoded example values for now, as we don't use them yet, + # but Cryspy requires them for single crystal data. + if expt_type.sample_form.value == SampleFormEnum.SINGLE_CRYSTAL: cif_lines.append('') - cif_lines.append('loop_') - cif_lines.append('_pd_meas_2theta') - cif_lines.append('_pd_meas_intensity') - cif_lines.append('_pd_meas_intensity_sigma') - elif expt_type.beam_mode.value == BeamModeEnum.TIME_OF_FLIGHT: + cif_lines.append('_diffrn_orient_matrix_type CCSL') + cif_lines.append('_diffrn_orient_matrix_ub_11 -0.088033') + cif_lines.append('_diffrn_orient_matrix_ub_12 -0.088004') + cif_lines.append('_diffrn_orient_matrix_ub_13 0.069970') + cif_lines.append('_diffrn_orient_matrix_ub_21 0.034058') + cif_lines.append('_diffrn_orient_matrix_ub_22 -0.188170') + cif_lines.append('_diffrn_orient_matrix_ub_23 -0.013039') + cif_lines.append('_diffrn_orient_matrix_ub_31 0.223600') + cif_lines.append('_diffrn_orient_matrix_ub_32 0.125751') + cif_lines.append('_diffrn_orient_matrix_ub_33 0.029490') + + # Add phase data + if expt_type.sample_form.value == SampleFormEnum.SINGLE_CRYSTAL: + cif_lines.append('') + cif_lines.append(f'_phase_label {linked_structure.name}') + cif_lines.append('_phase_scale 1.0') + elif expt_type.sample_form.value == SampleFormEnum.POWDER: cif_lines.append('') cif_lines.append('loop_') - cif_lines.append('_tof_meas_time') - cif_lines.append('_tof_meas_intensity') - cif_lines.append('_tof_meas_intensity_sigma') - - y_data: np.ndarray = experiment.data.meas - sy_data: np.ndarray = experiment.data.meas_su - - for x_val, y_val, sy_val in zip(x_data, y_data, sy_data, strict=True): - cif_lines.append(f' {x_val:.5f} {y_val:.5f} {sy_val:.5f}') + cif_lines.append('_phase_label') + cif_lines.append('_phase_scale') + cif_lines.append(f'{linked_structure.name} 1.0') + # Add background data + if expt_type.sample_form.value == SampleFormEnum.POWDER: + if expt_type.beam_mode.value == BeamModeEnum.CONSTANT_WAVELENGTH: + cif_lines.append('') + cif_lines.append('loop_') + cif_lines.append('_pd_background_2theta') + cif_lines.append('_pd_background_intensity') + cif_lines.append(f'{twotheta_min} 0.0') + cif_lines.append(f'{twotheta_max} 0.0') + elif expt_type.beam_mode.value == BeamModeEnum.TIME_OF_FLIGHT: + cif_lines.append('') + cif_lines.append('loop_') + cif_lines.append('_tof_backgroundpoint_time') # TODO: !!!!???? + cif_lines.append('_tof_backgroundpoint_intensity') # TODO: !!!!???? + cif_lines.append(f'{twotheta_min} 0.0') # TODO: !!!!???? + cif_lines.append(f'{twotheta_max} 0.0') # TODO: !!!!???? + + # Add measured data: Single crystal + if expt_type.sample_form.value == SampleFormEnum.SINGLE_CRYSTAL: + if expt_type.beam_mode.value == BeamModeEnum.CONSTANT_WAVELENGTH: + cif_lines.append('') + cif_lines.append('loop_') + cif_lines.append('_diffrn_refln_index_h') + cif_lines.append('_diffrn_refln_index_k') + cif_lines.append('_diffrn_refln_index_l') + cif_lines.append('_diffrn_refln_intensity') + cif_lines.append('_diffrn_refln_intensity_sigma') + indices_h: np.ndarray = experiment.data.index_h + indices_k: np.ndarray = experiment.data.index_k + indices_l: np.ndarray = experiment.data.index_l + y_data: np.ndarray = experiment.data.intensity_meas + sy_data: np.ndarray = experiment.data.intensity_meas_su + for index_h, index_k, index_l, y_val, sy_val in zip( + indices_h, indices_k, indices_l, y_data, sy_data, strict=True + ): + cif_lines.append( + f'{index_h:4.0f}{index_k:4.0f}{index_l:4.0f} {y_val:.5f} {sy_val:.5f}' + ) + elif expt_type.beam_mode.value == BeamModeEnum.TIME_OF_FLIGHT: + cif_lines.append('') + cif_lines.append('loop_') + cif_lines.append('_diffrn_refln_index_h') + cif_lines.append('_diffrn_refln_index_k') + cif_lines.append('_diffrn_refln_index_l') + cif_lines.append('_diffrn_refln_intensity') + cif_lines.append('_diffrn_refln_intensity_sigma') + cif_lines.append('_diffrn_refln_wavelength') + indices_h: np.ndarray = experiment.data.index_h + indices_k: np.ndarray = experiment.data.index_k + indices_l: np.ndarray = experiment.data.index_l + y_data: np.ndarray = experiment.data.intensity_meas + sy_data: np.ndarray = experiment.data.intensity_meas_su + wl_data: np.ndarray = experiment.data.wavelength + for index_h, index_k, index_l, y_val, sy_val, wl_val in zip( + indices_h, indices_k, indices_l, y_data, sy_data, wl_data, strict=True + ): + cif_lines.append( + f'{index_h:4.0f}{index_k:4.0f}{index_l:4.0f} {y_val:.5f} ' + f'{sy_val:.5f} {wl_val:.5f}' + ) + # Add measured data: Powder + elif expt_type.sample_form.value == SampleFormEnum.POWDER: + if expt_type.beam_mode.value == BeamModeEnum.CONSTANT_WAVELENGTH: + cif_lines.append('') + cif_lines.append('loop_') + cif_lines.append('_pd_meas_2theta') + cif_lines.append('_pd_meas_intensity') + cif_lines.append('_pd_meas_intensity_sigma') + elif expt_type.beam_mode.value == BeamModeEnum.TIME_OF_FLIGHT: + cif_lines.append('') + cif_lines.append('loop_') + cif_lines.append('_tof_meas_time') + cif_lines.append('_tof_meas_intensity') + cif_lines.append('_tof_meas_intensity_sigma') + y_data: np.ndarray = experiment.data.intensity_meas + sy_data: np.ndarray = experiment.data.intensity_meas_su + for x_val, y_val, sy_val in zip(x_data, y_data, sy_data, strict=True): + cif_lines.append(f' {x_val:.5f} {y_val:.5f} {sy_val:.5f}') + + # Combine all lines into a single CIF string cryspy_experiment_cif = '\n'.join(cif_lines) return cryspy_experiment_cif diff --git a/src/easydiffraction/analysis/calculators/factory.py b/src/easydiffraction/analysis/calculators/factory.py index 79b4eba9..f9860f81 100644 --- a/src/easydiffraction/analysis/calculators/factory.py +++ b/src/easydiffraction/analysis/calculators/factory.py @@ -1,105 +1,40 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +""" +Calculator factory — delegates to ``FactoryBase``. + +Overrides ``_supported_map`` to filter out calculators whose engines are +not importable in the current environment. +""" + +from __future__ import annotations from typing import Dict -from typing import List -from typing import Optional from typing import Type -from typing import Union -from easydiffraction.analysis.calculators.base import CalculatorBase -from easydiffraction.analysis.calculators.crysfml import CrysfmlCalculator -from easydiffraction.analysis.calculators.cryspy import CryspyCalculator -from easydiffraction.analysis.calculators.pdffit import PdffitCalculator -from easydiffraction.utils.logging import console -from easydiffraction.utils.logging import log -from easydiffraction.utils.utils import render_table +from easydiffraction.core.factory import FactoryBase +from easydiffraction.datablocks.experiment.item.enums import CalculatorEnum +from easydiffraction.datablocks.experiment.item.enums import ScatteringTypeEnum -class CalculatorFactory: - """Factory for creating calculation engine instances. +class CalculatorFactory(FactoryBase): + """ + Factory for creating calculation engine instances. - The factory exposes discovery helpers to list and show available - calculators in the current environment and a creator that returns an - instantiated calculator or ``None`` if the requested one is not - available. + Only calculators whose ``engine_imported`` flag is ``True`` are + available for creation. """ - _potential_calculators: Dict[str, Dict[str, Union[str, Type[CalculatorBase]]]] = { - 'crysfml': { - 'description': 'CrysFML library for crystallographic calculations', - 'class': CrysfmlCalculator, - }, - 'cryspy': { - 'description': 'CrysPy library for crystallographic calculations', - 'class': CryspyCalculator, - }, - 'pdffit': { - 'description': 'PDFfit2 library for pair distribution function calculations', - 'class': PdffitCalculator, - }, + _default_rules = { + frozenset({ + ('scattering_type', ScatteringTypeEnum.BRAGG), + }): CalculatorEnum.CRYSPY, + frozenset({ + ('scattering_type', ScatteringTypeEnum.TOTAL), + }): CalculatorEnum.PDFFIT, } @classmethod - def _supported_calculators( - cls, - ) -> Dict[str, Dict[str, Union[str, Type[CalculatorBase]]]]: - """Return calculators whose engines are importable. - - This filters the list of potential calculators by instantiating - their classes and checking the ``engine_imported`` property. - - Returns: - Mapping from calculator name to its config dict. - """ - return { - name: cfg - for name, cfg in cls._potential_calculators.items() - if cfg['class']().engine_imported # instantiate and check the @property - } - - @classmethod - def list_supported_calculators(cls) -> List[str]: - """List names of calculators available in the environment. - - Returns: - List of calculator identifiers, e.g. ``["crysfml", ...]``. - """ - return list(cls._supported_calculators().keys()) - - @classmethod - def show_supported_calculators(cls) -> None: - """Pretty-print supported calculators and their descriptions.""" - columns_headers: List[str] = ['Calculator', 'Description'] - columns_alignment = ['left', 'left'] - columns_data: List[List[str]] = [] - for name, config in cls._supported_calculators().items(): - description: str = config.get('description', 'No description provided.') - columns_data.append([name, description]) - - console.paragraph('Supported calculators') - render_table( - columns_headers=columns_headers, - columns_alignment=columns_alignment, - columns_data=columns_data, - ) - - @classmethod - def create_calculator(cls, calculator_name: str) -> Optional[CalculatorBase]: - """Create a calculator instance by name. - - Args: - calculator_name: Identifier of the calculator to create. - - Returns: - A calculator instance or ``None`` if unknown or unsupported. - """ - config = cls._supported_calculators().get(calculator_name) - if not config: - log.warning( - f"Unknown calculator '{calculator_name}', " - f'Supported calculators: {cls.list_supported_calculators()}' - ) - return None - - return config['class']() + def _supported_map(cls) -> Dict[str, Type]: + """Only include calculators whose engines are importable.""" + return {klass.type_info.tag: klass for klass in cls._registry if klass.engine_imported} diff --git a/src/easydiffraction/analysis/calculators/pdffit.py b/src/easydiffraction/analysis/calculators/pdffit.py index 9f099ff6..4ce20276 100644 --- a/src/easydiffraction/analysis/calculators/pdffit.py +++ b/src/easydiffraction/analysis/calculators/pdffit.py @@ -1,6 +1,7 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -"""PDF calculation backend using diffpy.pdffit2 if available. +""" +PDF calculation backend using diffpy.pdffit2 if available. The class adapts the engine to EasyDiffraction calculator interface and silences stdio on import to avoid noisy output in notebooks and logs. @@ -14,8 +15,10 @@ import numpy as np from easydiffraction.analysis.calculators.base import CalculatorBase -from easydiffraction.experiments.experiment.base import ExperimentBase -from easydiffraction.sample_models.sample_model.base import SampleModelBase +from easydiffraction.analysis.calculators.factory import CalculatorFactory +from easydiffraction.core.metadata import TypeInfo +from easydiffraction.datablocks.experiment.item.base import ExperimentBase +from easydiffraction.datablocks.structure.item.base import Structure try: from diffpy.pdffit2 import PdfFit @@ -36,30 +39,72 @@ # print("⚠️ 'pdffit' module not found. This calculation engine will # not be available.") PdfFit = None + redirect_stdout = None + pdffit_cif_parser = None + _pdffit_devnull = None +@CalculatorFactory.register class PdffitCalculator(CalculatorBase): """Wrapper for Pdffit library.""" + type_info = TypeInfo( + tag='pdffit', + description='PDFfit2 for pair distribution function calculations', + ) engine_imported: bool = PdfFit is not None @property - def name(self): + def name(self) -> str: + """Short identifier of this calculator engine.""" return 'pdffit' - def calculate_structure_factors(self, sample_models, experiments): + def calculate_structure_factors( + self, + structures: object, + experiments: object, + ) -> list: + """ + Return an empty list; PDF does not compute structure factors. + + Parameters + ---------- + structures : object + Unused; kept for interface consistency. + experiments : object + Unused; kept for interface consistency. + + Returns + ------- + list + An empty list. + """ # PDF doesn't compute HKL but we keep interface consistent # Intentionally unused, required by public API/signature - del sample_models, experiments + del structures, experiments print('[pdffit] Calculating HKLs (not applicable)...') return [] def calculate_pattern( self, - sample_model: SampleModelBase, + structure: Structure, experiment: ExperimentBase, called_by_minimizer: bool = False, - ): + ) -> None: + """ + Calculate the PDF pattern using PDFfit2. + + Parameters + ---------- + structure : Structure + The structure object supplying atom sites and cell + parameters. + experiment : ExperimentBase + The experiment object supplying instrument and peak + parameters. + called_by_minimizer : bool, default=False + Unused; kept for interface consistency. + """ # Intentionally unused, required by public API/signature del called_by_minimizer @@ -67,12 +112,12 @@ def calculate_pattern( calculator = PdfFit() # --------------------------- - # Set sample model parameters + # Set structure parameters # --------------------------- # TODO: move CIF v2 -> CIF v1 conversion to a separate module - # Convert the sample model to CIF supported by PDFfit - cif_string_v2 = sample_model.as_cif + # Convert the structure to CIF supported by PDFfit + cif_string_v2 = structure.as_cif # convert to version 1 of CIF format # this means: replace all dots with underscores for # cases where the dot is surrounded by letters on both sides. @@ -80,18 +125,18 @@ def calculate_pattern( cif_string_v1 = re.sub(pattern, '_', cif_string_v2) # Create the PDFit structure - structure = pdffit_cif_parser().parse(cif_string_v1) + pdffit_structure = pdffit_cif_parser().parse(cif_string_v1) # Set all model parameters: # space group, cell parameters, and atom sites (including ADPs) - calculator.add_structure(structure) + calculator.add_structure(pdffit_structure) # ------------------------- # Set experiment parameters # ------------------------- # Set some peak-related parameters - calculator.setvar('pscale', experiment.linked_phases[sample_model.name].scale.value) + calculator.setvar('pscale', experiment.linked_phases[structure.name].scale.value) calculator.setvar('delta1', experiment.peak.sharp_delta_1.value) calculator.setvar('delta2', experiment.peak.sharp_delta_2.value) calculator.setvar('spdiameter', experiment.peak.damp_particle_diameter.value) diff --git a/src/easydiffraction/analysis/categories/__init__.py b/src/easydiffraction/analysis/categories/__init__.py index 429f2648..4e798e20 100644 --- a/src/easydiffraction/analysis/categories/__init__.py +++ b/src/easydiffraction/analysis/categories/__init__.py @@ -1,2 +1,2 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause diff --git a/src/easydiffraction/analysis/categories/aliases.py b/src/easydiffraction/analysis/categories/aliases.py deleted file mode 100644 index b55db11d..00000000 --- a/src/easydiffraction/analysis/categories/aliases.py +++ /dev/null @@ -1,106 +0,0 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors -# SPDX-License-Identifier: BSD-3-Clause -"""Alias category for mapping friendly names to parameter UIDs. - -Defines a small record type used by analysis configuration to refer to -parameters via readable labels instead of raw unique identifiers. -""" - -from easydiffraction.core.category import CategoryCollection -from easydiffraction.core.category import CategoryItem -from easydiffraction.core.parameters import StringDescriptor -from easydiffraction.core.validation import AttributeSpec -from easydiffraction.core.validation import DataTypes -from easydiffraction.core.validation import RegexValidator -from easydiffraction.io.cif.handler import CifHandler - - -class Alias(CategoryItem): - """Single alias entry. - - Maps a human-readable ``label`` to a concrete ``param_uid`` used by - the engine. - - Args: - label: Alias label. Must match ``^[A-Za-z_][A-Za-z0-9_]*$``. - param_uid: Target parameter uid. Same identifier pattern as - ``label``. - """ - - def __init__( - self, - *, - label: str, - param_uid: str, - ) -> None: - super().__init__() - - self._label: StringDescriptor = StringDescriptor( - name='label', - description='...', - value_spec=AttributeSpec( - value=label, - type_=DataTypes.STRING, - default='...', - content_validator=RegexValidator(pattern=r'^[A-Za-z_][A-Za-z0-9_]*$'), - ), - cif_handler=CifHandler( - names=[ - '_alias.label', - ] - ), - ) - self._param_uid: StringDescriptor = StringDescriptor( - name='param_uid', - description='...', - value_spec=AttributeSpec( - value=param_uid, - type_=DataTypes.STRING, - default='...', - content_validator=RegexValidator(pattern=r'^[A-Za-z_][A-Za-z0-9_]*$'), - ), - cif_handler=CifHandler( - names=[ - '_alias.param_uid', - ] - ), - ) - - self._identity.category_code = 'alias' - self._identity.category_entry_name = lambda: str(self.label.value) - - @property - def label(self): - """Alias label descriptor.""" - return self._label - - @label.setter - def label(self, value): - """Set alias label. - - Args: - value: New label. - """ - self._label.value = value - - @property - def param_uid(self): - """Parameter uid descriptor the alias points to.""" - return self._param_uid - - @param_uid.setter - def param_uid(self, value): - """Set the parameter uid. - - Args: - value: New uid. - """ - self._param_uid.value = value - - -class Aliases(CategoryCollection): - """Collection of :class:`Alias` items.""" - - def __init__(self): - """Create an empty collection of aliases.""" - super().__init__(item_type=Alias) diff --git a/src/easydiffraction/analysis/categories/aliases/__init__.py b/src/easydiffraction/analysis/categories/aliases/__init__.py new file mode 100644 index 00000000..6ca3a859 --- /dev/null +++ b/src/easydiffraction/analysis/categories/aliases/__init__.py @@ -0,0 +1,5 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + +from easydiffraction.analysis.categories.aliases.default import Alias +from easydiffraction.analysis.categories.aliases.default import Aliases diff --git a/src/easydiffraction/analysis/categories/aliases/default.py b/src/easydiffraction/analysis/categories/aliases/default.py new file mode 100644 index 00000000..7b1e0df0 --- /dev/null +++ b/src/easydiffraction/analysis/categories/aliases/default.py @@ -0,0 +1,101 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +""" +Alias category for mapping friendly names to parameter UIDs. + +Defines a small record type used by analysis configuration to refer to +parameters via readable labels instead of raw unique identifiers. +""" + +from __future__ import annotations + +from easydiffraction.analysis.categories.aliases.factory import AliasesFactory +from easydiffraction.core.category import CategoryCollection +from easydiffraction.core.category import CategoryItem +from easydiffraction.core.metadata import TypeInfo +from easydiffraction.core.validation import AttributeSpec +from easydiffraction.core.validation import RegexValidator +from easydiffraction.core.variable import StringDescriptor +from easydiffraction.io.cif.handler import CifHandler + + +class Alias(CategoryItem): + """ + Single alias entry. + + Maps a human-readable ``label`` to a concrete ``param_uid`` used by + the engine. + """ + + def __init__(self) -> None: + super().__init__() + + self._label = StringDescriptor( + name='label', + description='...', # TODO + value_spec=AttributeSpec( + default='_', # TODO, Maybe None? + validator=RegexValidator(pattern=r'^[A-Za-z_][A-Za-z0-9_]*$'), + ), + cif_handler=CifHandler(names=['_alias.label']), + ) + self._param_uid = StringDescriptor( + name='param_uid', + description='...', # TODO + value_spec=AttributeSpec( + default='_', + validator=RegexValidator(pattern=r'^[A-Za-z_][A-Za-z0-9_]*$'), + ), + cif_handler=CifHandler(names=['_alias.param_uid']), + ) + + self._identity.category_code = 'alias' + self._identity.category_entry_name = lambda: str(self.label.value) + + # ------------------------------------------------------------------ + # Public properties + # ------------------------------------------------------------------ + + @property + def label(self) -> StringDescriptor: + """ + ... + + Reading this property returns the underlying + ``StringDescriptor`` object. Assigning to it updates the + parameter value. + """ + return self._label + + @label.setter + def label(self, value: str) -> None: + self._label.value = value + + @property + def param_uid(self) -> StringDescriptor: + """ + ... + + Reading this property returns the underlying + ``StringDescriptor`` object. Assigning to it updates the + parameter value. + """ + return self._param_uid + + @param_uid.setter + def param_uid(self, value: str) -> None: + self._param_uid.value = value + + +@AliasesFactory.register +class Aliases(CategoryCollection): + """Collection of :class:`Alias` items.""" + + type_info = TypeInfo( + tag='default', + description='Parameter alias mappings', + ) + + def __init__(self) -> None: + """Create an empty collection of aliases.""" + super().__init__(item_type=Alias) diff --git a/src/easydiffraction/analysis/categories/aliases/factory.py b/src/easydiffraction/analysis/categories/aliases/factory.py new file mode 100644 index 00000000..f2bebe43 --- /dev/null +++ b/src/easydiffraction/analysis/categories/aliases/factory.py @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Aliases factory — delegates entirely to ``FactoryBase``.""" + +from __future__ import annotations + +from easydiffraction.core.factory import FactoryBase + + +class AliasesFactory(FactoryBase): + """Create alias collections by tag.""" + + _default_rules = { + frozenset(): 'default', + } diff --git a/src/easydiffraction/analysis/categories/constraints.py b/src/easydiffraction/analysis/categories/constraints.py deleted file mode 100644 index 205a28b1..00000000 --- a/src/easydiffraction/analysis/categories/constraints.py +++ /dev/null @@ -1,111 +0,0 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors -# SPDX-License-Identifier: BSD-3-Clause -"""Simple symbolic constraint between parameters. - -Represents an equation of the form ``lhs_alias = rhs_expr`` where -``rhs_expr`` is evaluated elsewhere by the analysis engine. -""" - -from easydiffraction.core.category import CategoryCollection -from easydiffraction.core.category import CategoryItem -from easydiffraction.core.parameters import StringDescriptor -from easydiffraction.core.singletons import ConstraintsHandler -from easydiffraction.core.validation import AttributeSpec -from easydiffraction.core.validation import DataTypes -from easydiffraction.core.validation import RegexValidator -from easydiffraction.io.cif.handler import CifHandler - - -class Constraint(CategoryItem): - """Single constraint item. - - Args: - lhs_alias: Left-hand side alias name being constrained. - rhs_expr: Right-hand side expression as a string. - """ - - def __init__( - self, - *, - lhs_alias: str, - rhs_expr: str, - ) -> None: - super().__init__() - - self._lhs_alias: StringDescriptor = StringDescriptor( - name='lhs_alias', - description='...', - value_spec=AttributeSpec( - value=lhs_alias, - type_=DataTypes.STRING, - default='...', - content_validator=RegexValidator(pattern=r'.*'), - ), - cif_handler=CifHandler( - names=[ - '_constraint.lhs_alias', - ] - ), - ) - self._rhs_expr: StringDescriptor = StringDescriptor( - name='rhs_expr', - description='...', - value_spec=AttributeSpec( - value=rhs_expr, - type_=DataTypes.STRING, - default='...', - content_validator=RegexValidator(pattern=r'.*'), - ), - cif_handler=CifHandler( - names=[ - '_constraint.rhs_expr', - ] - ), - ) - - self._identity.category_code = 'constraint' - self._identity.category_entry_name = lambda: str(self.lhs_alias.value) - - @property - def lhs_alias(self): - """Alias name on the left-hand side of the equation.""" - return self._lhs_alias - - @lhs_alias.setter - def lhs_alias(self, value): - """Set the left-hand side alias. - - Args: - value: New alias string. - """ - self._lhs_alias.value = value - - @property - def rhs_expr(self): - """Right-hand side expression string.""" - return self._rhs_expr - - @rhs_expr.setter - def rhs_expr(self, value): - """Set the right-hand side expression. - - Args: - value: New expression string. - """ - self._rhs_expr.value = value - - -class Constraints(CategoryCollection): - """Collection of :class:`Constraint` items.""" - - _update_priority = 90 # After most others, but before data categories - - def __init__(self): - """Create an empty constraints collection.""" - super().__init__(item_type=Constraint) - - def _update(self, called_by_minimizer=False): - del called_by_minimizer - - constraints = ConstraintsHandler.get() - constraints.apply() diff --git a/src/easydiffraction/analysis/categories/constraints/__init__.py b/src/easydiffraction/analysis/categories/constraints/__init__.py new file mode 100644 index 00000000..97d8c03c --- /dev/null +++ b/src/easydiffraction/analysis/categories/constraints/__init__.py @@ -0,0 +1,5 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + +from easydiffraction.analysis.categories.constraints.default import Constraint +from easydiffraction.analysis.categories.constraints.default import Constraints diff --git a/src/easydiffraction/analysis/categories/constraints/default.py b/src/easydiffraction/analysis/categories/constraints/default.py new file mode 100644 index 00000000..3bb1b77e --- /dev/null +++ b/src/easydiffraction/analysis/categories/constraints/default.py @@ -0,0 +1,124 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +""" +Simple symbolic constraint between parameters. + +Represents an equation of the form ``lhs_alias = rhs_expr`` stored as a +single expression string. The left- and right-hand sides are derived by +splitting the expression at the ``=`` sign. +""" + +from __future__ import annotations + +from easydiffraction.analysis.categories.constraints.factory import ConstraintsFactory +from easydiffraction.core.category import CategoryCollection +from easydiffraction.core.category import CategoryItem +from easydiffraction.core.metadata import TypeInfo +from easydiffraction.core.singleton import ConstraintsHandler +from easydiffraction.core.validation import AttributeSpec +from easydiffraction.core.validation import RegexValidator +from easydiffraction.core.variable import StringDescriptor +from easydiffraction.io.cif.handler import CifHandler + + +class Constraint(CategoryItem): + """Single constraint item stored as ``lhs = rhs`` expression.""" + + def __init__(self) -> None: + super().__init__() + + self._expression = StringDescriptor( + name='expression', + description='Constraint equation, e.g. "occ_Ba = 1 - occ_La".', + value_spec=AttributeSpec( + default='_', # TODO, Maybe None? + validator=RegexValidator(pattern=r'.*'), + ), + cif_handler=CifHandler(names=['_constraint.expression']), + ) + + self._identity.category_code = 'constraint' + self._identity.category_entry_name = lambda: self.lhs_alias + + # ------------------------------------------------------------------ + # Public properties + # ------------------------------------------------------------------ + + @property + def expression(self) -> StringDescriptor: + """ + Full constraint equation (e.g. ``'occ_Ba = 1 - occ_La'``). + + Reading this property returns the underlying + ``StringDescriptor`` object. Assigning to it updates the value. + """ + return self._expression + + @expression.setter + def expression(self, value: str) -> None: + self._expression.value = value + + @property + def lhs_alias(self) -> str: + """Left-hand side alias derived from the expression.""" + return self._split_expression()[0] + + @property + def rhs_expr(self) -> str: + """Right-hand side expression derived from the expression.""" + return self._split_expression()[1] + + # ------------------------------------------------------------------ + # Internal helpers + # ------------------------------------------------------------------ + + def _split_expression(self) -> tuple[str, str]: + """ + Split the expression at the first ``=`` sign. + + Returns + ------- + tuple[str, str] + ``(lhs_alias, rhs_expr)`` with whitespace stripped. + """ + raw = self._expression.value or '' + if '=' not in raw: + return (raw.strip(), '') + lhs, rhs = raw.split('=', 1) + return (lhs.strip(), rhs.strip()) + + +@ConstraintsFactory.register +class Constraints(CategoryCollection): + """Collection of :class:`Constraint` items.""" + + type_info = TypeInfo( + tag='default', + description='Symbolic parameter constraints', + ) + + _update_priority = 90 # After most others, but before data categories + + def __init__(self) -> None: + """Create an empty constraints collection.""" + super().__init__(item_type=Constraint) + + def create(self, *, expression: str) -> None: + """ + Create a constraint from an expression string. + + Parameters + ---------- + expression : str + Constraint equation, e.g. ``'biso_Co2 = biso_Co1'`` or + ``'occ_Ba = 1 - occ_La'``. + """ + item = Constraint() + item.expression = expression + self.add(item) + + def _update(self, called_by_minimizer: bool = False) -> None: + del called_by_minimizer + + constraints = ConstraintsHandler.get() + constraints.apply() diff --git a/src/easydiffraction/analysis/categories/constraints/factory.py b/src/easydiffraction/analysis/categories/constraints/factory.py new file mode 100644 index 00000000..682c9684 --- /dev/null +++ b/src/easydiffraction/analysis/categories/constraints/factory.py @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Constraints factory — delegates entirely to ``FactoryBase``.""" + +from __future__ import annotations + +from easydiffraction.core.factory import FactoryBase + + +class ConstraintsFactory(FactoryBase): + """Create constraint collections by tag.""" + + _default_rules = { + frozenset(): 'default', + } diff --git a/src/easydiffraction/analysis/categories/fit_mode/__init__.py b/src/easydiffraction/analysis/categories/fit_mode/__init__.py new file mode 100644 index 00000000..058d054c --- /dev/null +++ b/src/easydiffraction/analysis/categories/fit_mode/__init__.py @@ -0,0 +1,6 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + +from easydiffraction.analysis.categories.fit_mode.enums import FitModeEnum +from easydiffraction.analysis.categories.fit_mode.factory import FitModeFactory +from easydiffraction.analysis.categories.fit_mode.fit_mode import FitMode diff --git a/src/easydiffraction/analysis/categories/fit_mode/enums.py b/src/easydiffraction/analysis/categories/fit_mode/enums.py new file mode 100644 index 00000000..5e904eae --- /dev/null +++ b/src/easydiffraction/analysis/categories/fit_mode/enums.py @@ -0,0 +1,26 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Enumeration for fit-mode values.""" + +from __future__ import annotations + +from enum import Enum + + +class FitModeEnum(str, Enum): + """Fitting strategy for the analysis.""" + + SINGLE = 'single' + JOINT = 'joint' + + @classmethod + def default(cls) -> FitModeEnum: + """Return the default fit mode (SINGLE).""" + return cls.SINGLE + + def description(self) -> str: + """Return a human-readable description of this fit mode.""" + if self is FitModeEnum.SINGLE: + return 'Independent fitting of each experiment; no shared parameters' + elif self is FitModeEnum.JOINT: + return 'Simultaneous fitting of all experiments; some parameters are shared' diff --git a/src/easydiffraction/analysis/categories/fit_mode/factory.py b/src/easydiffraction/analysis/categories/fit_mode/factory.py new file mode 100644 index 00000000..f10485f8 --- /dev/null +++ b/src/easydiffraction/analysis/categories/fit_mode/factory.py @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Fit-mode factory — delegates entirely to ``FactoryBase``.""" + +from __future__ import annotations + +from easydiffraction.core.factory import FactoryBase + + +class FitModeFactory(FactoryBase): + """Create fit-mode category items by tag.""" + + _default_rules = { + frozenset(): 'default', + } diff --git a/src/easydiffraction/analysis/categories/fit_mode/fit_mode.py b/src/easydiffraction/analysis/categories/fit_mode/fit_mode.py new file mode 100644 index 00000000..b0589a2e --- /dev/null +++ b/src/easydiffraction/analysis/categories/fit_mode/fit_mode.py @@ -0,0 +1,64 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +""" +Fit-mode category item. + +Stores the active fitting strategy as a CIF-serializable descriptor +validated by ``FitModeEnum``. +""" + +from __future__ import annotations + +from easydiffraction.analysis.categories.fit_mode.enums import FitModeEnum +from easydiffraction.analysis.categories.fit_mode.factory import FitModeFactory +from easydiffraction.core.category import CategoryItem +from easydiffraction.core.metadata import TypeInfo +from easydiffraction.core.validation import AttributeSpec +from easydiffraction.core.validation import MembershipValidator +from easydiffraction.core.variable import StringDescriptor +from easydiffraction.io.cif.handler import CifHandler + + +@FitModeFactory.register +class FitMode(CategoryItem): + """ + Fitting strategy selector. + + Holds a single ``mode`` descriptor whose value is one of + ``FitModeEnum`` members (``'single'`` or ``'joint'``). + """ + + type_info = TypeInfo( + tag='default', + description='Fit-mode category', + ) + + def __init__(self) -> None: + super().__init__() + + self._mode: StringDescriptor = StringDescriptor( + name='mode', + description='Fitting strategy', + value_spec=AttributeSpec( + default=FitModeEnum.default().value, + validator=MembershipValidator(allowed=[member.value for member in FitModeEnum]), + ), + cif_handler=CifHandler(names=['_analysis.fit_mode']), + ) + + self._identity.category_code = 'fit_mode' + + @property + def mode(self) -> StringDescriptor: + """ + Fitting strategy. + + Reading this property returns the underlying + ``StringDescriptor`` object. Assigning to it updates the + parameter value. + """ + return self._mode + + @mode.setter + def mode(self, value: str) -> None: + self._mode.value = value diff --git a/src/easydiffraction/analysis/categories/joint_fit_experiments.py b/src/easydiffraction/analysis/categories/joint_fit_experiments.py deleted file mode 100644 index 818dd6f6..00000000 --- a/src/easydiffraction/analysis/categories/joint_fit_experiments.py +++ /dev/null @@ -1,104 +0,0 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors -# SPDX-License-Identifier: BSD-3-Clause -"""Joint-fit experiment weighting configuration. - -Stores per-experiment weights to be used when multiple experiments are -fitted simultaneously. -""" - -from easydiffraction.core.category import CategoryCollection -from easydiffraction.core.category import CategoryItem -from easydiffraction.core.parameters import NumericDescriptor -from easydiffraction.core.parameters import StringDescriptor -from easydiffraction.core.validation import AttributeSpec -from easydiffraction.core.validation import DataTypes -from easydiffraction.core.validation import RangeValidator -from easydiffraction.core.validation import RegexValidator -from easydiffraction.io.cif.handler import CifHandler - - -class JointFitExperiment(CategoryItem): - """A single joint-fit entry. - - Args: - id: Experiment identifier used in the fit session. - weight: Relative weight factor in the combined objective. - """ - - def __init__( - self, - *, - id: str, - weight: float, - ) -> None: - super().__init__() - - self._id: StringDescriptor = StringDescriptor( - name='id', # TODO: need new name instead of id - description='...', - value_spec=AttributeSpec( - value=id, - type_=DataTypes.STRING, - default='...', - content_validator=RegexValidator(pattern=r'^[A-Za-z_][A-Za-z0-9_]*$'), - ), - cif_handler=CifHandler( - names=[ - '_joint_fit_experiment.id', - ] - ), - ) - self._weight: NumericDescriptor = NumericDescriptor( - name='weight', - description='...', - value_spec=AttributeSpec( - value=weight, - type_=DataTypes.NUMERIC, - default=0.0, - content_validator=RangeValidator(), - ), - cif_handler=CifHandler( - names=[ - '_joint_fit_experiment.weight', - ] - ), - ) - - self._identity.category_code = 'joint_fit_experiment' - self._identity.category_entry_name = lambda: str(self.id.value) - - @property - def id(self): - """Experiment identifier descriptor.""" - return self._id - - @id.setter - def id(self, value): - """Set the experiment identifier. - - Args: - value: New id string. - """ - self._id.value = value - - @property - def weight(self): - """Weight factor descriptor.""" - return self._weight - - @weight.setter - def weight(self, value): - """Set the weight factor. - - Args: - value: New weight value. - """ - self._weight.value = value - - -class JointFitExperiments(CategoryCollection): - """Collection of :class:`JointFitExperiment` items.""" - - def __init__(self): - """Create an empty joint-fit experiments collection.""" - super().__init__(item_type=JointFitExperiment) diff --git a/src/easydiffraction/analysis/categories/joint_fit_experiments/__init__.py b/src/easydiffraction/analysis/categories/joint_fit_experiments/__init__.py new file mode 100644 index 00000000..1aa8f0ae --- /dev/null +++ b/src/easydiffraction/analysis/categories/joint_fit_experiments/__init__.py @@ -0,0 +1,5 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + +from easydiffraction.analysis.categories.joint_fit_experiments.default import JointFitExperiment +from easydiffraction.analysis.categories.joint_fit_experiments.default import JointFitExperiments diff --git a/src/easydiffraction/analysis/categories/joint_fit_experiments/default.py b/src/easydiffraction/analysis/categories/joint_fit_experiments/default.py new file mode 100644 index 00000000..b94bd7de --- /dev/null +++ b/src/easydiffraction/analysis/categories/joint_fit_experiments/default.py @@ -0,0 +1,100 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +""" +Joint-fit experiment weighting configuration. + +Stores per-experiment weights to be used when multiple experiments are +fitted simultaneously. +""" + +from __future__ import annotations + +from easydiffraction.analysis.categories.joint_fit_experiments.factory import ( + JointFitExperimentsFactory, +) +from easydiffraction.core.category import CategoryCollection +from easydiffraction.core.category import CategoryItem +from easydiffraction.core.metadata import TypeInfo +from easydiffraction.core.validation import AttributeSpec +from easydiffraction.core.validation import RangeValidator +from easydiffraction.core.validation import RegexValidator +from easydiffraction.core.variable import NumericDescriptor +from easydiffraction.core.variable import StringDescriptor +from easydiffraction.io.cif.handler import CifHandler + + +class JointFitExperiment(CategoryItem): + """A single joint-fit entry.""" + + def __init__(self) -> None: + super().__init__() + + self._id: StringDescriptor = StringDescriptor( + name='id', # TODO: need new name instead of id + description='Experiment identifier', # TODO + value_spec=AttributeSpec( + default='_', + validator=RegexValidator(pattern=r'^[A-Za-z_][A-Za-z0-9_]*$'), + ), + cif_handler=CifHandler(names=['_joint_fit_experiment.id']), + ) + self._weight: NumericDescriptor = NumericDescriptor( + name='weight', + description='Weight factor', # TODO + value_spec=AttributeSpec( + default=0.0, + validator=RangeValidator(), + ), + cif_handler=CifHandler(names=['_joint_fit_experiment.weight']), + ) + + self._identity.category_code = 'joint_fit_experiment' + self._identity.category_entry_name = lambda: str(self.id.value) + + # ------------------------------------------------------------------ + # Public properties + # ------------------------------------------------------------------ + + @property + def id(self) -> StringDescriptor: + """ + Experiment identifier. + + Reading this property returns the underlying + ``StringDescriptor`` object. Assigning to it updates the + parameter value. + """ + return self._id + + @id.setter + def id(self, value: str) -> None: + self._id.value = value + + @property + def weight(self) -> NumericDescriptor: + """ + Weight factor. + + Reading this property returns the underlying + ``NumericDescriptor`` object. Assigning to it updates the + parameter value. + """ + return self._weight + + @weight.setter + def weight(self, value: float) -> None: + self._weight.value = value + + +@JointFitExperimentsFactory.register +class JointFitExperiments(CategoryCollection): + """Collection of :class:`JointFitExperiment` items.""" + + type_info = TypeInfo( + tag='default', + description='Joint-fit experiment weights', + ) + + def __init__(self) -> None: + """Create an empty joint-fit experiments collection.""" + super().__init__(item_type=JointFitExperiment) diff --git a/src/easydiffraction/analysis/categories/joint_fit_experiments/factory.py b/src/easydiffraction/analysis/categories/joint_fit_experiments/factory.py new file mode 100644 index 00000000..57666098 --- /dev/null +++ b/src/easydiffraction/analysis/categories/joint_fit_experiments/factory.py @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Joint-fit-experiments factory — delegates to ``FactoryBase``.""" + +from __future__ import annotations + +from easydiffraction.core.factory import FactoryBase + + +class JointFitExperimentsFactory(FactoryBase): + """Create joint-fit experiment collections by tag.""" + + _default_rules = { + frozenset(): 'default', + } diff --git a/src/easydiffraction/analysis/fit_helpers/__init__.py b/src/easydiffraction/analysis/fit_helpers/__init__.py index 429f2648..4e798e20 100644 --- a/src/easydiffraction/analysis/fit_helpers/__init__.py +++ b/src/easydiffraction/analysis/fit_helpers/__init__.py @@ -1,2 +1,2 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause diff --git a/src/easydiffraction/analysis/fit_helpers/metrics.py b/src/easydiffraction/analysis/fit_helpers/metrics.py index b6ef9928..dee08f86 100644 --- a/src/easydiffraction/analysis/fit_helpers/metrics.py +++ b/src/easydiffraction/analysis/fit_helpers/metrics.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause from typing import Optional @@ -6,22 +6,27 @@ import numpy as np -from easydiffraction.experiments.experiments import Experiments -from easydiffraction.sample_models.sample_models import SampleModels +from easydiffraction.datablocks.experiment.collection import Experiments +from easydiffraction.datablocks.structure.collection import Structures def calculate_r_factor( y_obs: np.ndarray, y_calc: np.ndarray, ) -> float: - """Calculate the R-factor (reliability factor) between observed and - calculated data. - - Args: - y_obs: Observed data points. - y_calc: Calculated data points. - - Returns: + """ + Calculate the R-factor between observed and calculated data. + + Parameters + ---------- + y_obs : np.ndarray + Observed data points. + y_calc : np.ndarray + Calculated data points. + + Returns + ------- + float R-factor value. """ y_obs = np.asarray(y_obs) @@ -36,15 +41,21 @@ def calculate_weighted_r_factor( y_calc: np.ndarray, weights: np.ndarray, ) -> float: - """Calculate the weighted R-factor between observed and calculated - data. - - Args: - y_obs: Observed data points. - y_calc: Calculated data points. - weights: Weights for each data point. - - Returns: + """ + Calculate weighted R-factor between observed and calculated data. + + Parameters + ---------- + y_obs : np.ndarray + Observed data points. + y_calc : np.ndarray + Calculated data points. + weights : np.ndarray + Weights for each data point. + + Returns + ------- + float Weighted R-factor value. """ y_obs = np.asarray(y_obs) @@ -59,14 +70,19 @@ def calculate_rb_factor( y_obs: np.ndarray, y_calc: np.ndarray, ) -> float: - """Calculate the Bragg R-factor between observed and calculated - data. - - Args: - y_obs: Observed data points. - y_calc: Calculated data points. - - Returns: + """ + Calculate the Bragg R-factor between observed and calculated data. + + Parameters + ---------- + y_obs : np.ndarray + Observed data points. + y_calc : np.ndarray + Calculated data points. + + Returns + ------- + float Bragg R-factor value. """ y_obs = np.asarray(y_obs) @@ -80,14 +96,19 @@ def calculate_r_factor_squared( y_obs: np.ndarray, y_calc: np.ndarray, ) -> float: - """Calculate the R-factor squared between observed and calculated - data. - - Args: - y_obs: Observed data points. - y_calc: Calculated data points. - - Returns: + """ + Calculate the R-factor squared between observed and calculated data. + + Parameters + ---------- + y_obs : np.ndarray + Observed data points. + y_calc : np.ndarray + Calculated data points. + + Returns + ------- + float R-factor squared value. """ y_obs = np.asarray(y_obs) @@ -101,13 +122,19 @@ def calculate_reduced_chi_square( residuals: np.ndarray, num_parameters: int, ) -> float: - """Calculate the reduced chi-square statistic. - - Args: - residuals: Residuals between observed and calculated data. - num_parameters: Number of free parameters used in the model. - - Returns: + """ + Calculate the reduced chi-square statistic. + + Parameters + ---------- + residuals : np.ndarray + Residuals between observed and calculated data. + num_parameters : int + Number of free parameters used in the model. + + Returns + ------- + float Reduced chi-square value. """ residuals = np.asarray(residuals) @@ -121,31 +148,39 @@ def calculate_reduced_chi_square( def get_reliability_inputs( - sample_models: SampleModels, + structures: Structures, experiments: Experiments, ) -> Tuple[np.ndarray, np.ndarray, Optional[np.ndarray]]: - """Collect observed and calculated data points for reliability - calculations. - - Args: - sample_models: Collection of sample models. - experiments: Collection of experiments. - - Returns: - Tuple containing arrays of (observed values, calculated values, - error values) + """ + Collect observed and calculated data for reliability calculations. + + Parameters + ---------- + structures : Structures + Collection of structures. + experiments : Experiments + Collection of experiments. + + Returns + ------- + np.ndarray + Observed values. + np.ndarray + Calculated values. + Optional[np.ndarray] + Error values, or None if not available. """ y_obs_all = [] y_calc_all = [] y_err_all = [] for experiment in experiments.values(): - for sample_model in sample_models: - sample_model._update_categories() + for structure in structures: + structure._update_categories() experiment._update_categories() - y_calc = experiment.data.calc - y_meas = experiment.data.meas - y_meas_su = experiment.data.meas_su + y_calc = experiment.data.intensity_calc + y_meas = experiment.data.intensity_meas + y_meas_su = experiment.data.intensity_meas_su if y_meas is not None and y_calc is not None: # If standard uncertainty is not provided, use ones diff --git a/src/easydiffraction/analysis/fit_helpers/reporting.py b/src/easydiffraction/analysis/fit_helpers/reporting.py index d9dbc40e..a1468aa5 100644 --- a/src/easydiffraction/analysis/fit_helpers/reporting.py +++ b/src/easydiffraction/analysis/fit_helpers/reporting.py @@ -1,7 +1,6 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -from typing import Any from typing import List from typing import Optional @@ -14,7 +13,8 @@ class FitResults: - """Container for results of a single optimization run. + """ + Container for results of a single optimization run. Holds success flag, chi-square metrics, iteration counts, timing, and parameter objects. Provides a printer to summarize key @@ -24,41 +24,53 @@ class FitResults: def __init__( self, success: bool = False, - parameters: Optional[List[Any]] = None, + parameters: Optional[List[object]] = None, chi_square: Optional[float] = None, reduced_chi_square: Optional[float] = None, message: str = '', iterations: int = 0, - engine_result: Optional[Any] = None, - starting_parameters: Optional[List[Any]] = None, + engine_result: Optional[object] = None, + starting_parameters: Optional[List[object]] = None, fitting_time: Optional[float] = None, - **kwargs: Any, + **kwargs: object, ) -> None: - """Initialize FitResults with the given parameters. - - Args: - success: Indicates if the fit was successful. - parameters: List of parameters used in the fit. - chi_square: Chi-square value of the fit. - reduced_chi_square: Reduced chi-square value of the fit. - message: Message related to the fit. - iterations: Number of iterations performed. - engine_result: Result from the fitting engine. - starting_parameters: Initial parameters for the fit. - fitting_time: Time taken for the fitting process. - **kwargs: Additional engine-specific fields. If ``redchi`` - is provided and ``reduced_chi_square`` is not set, it is - used as the reduced chi-square value. + """ + Initialize FitResults with the given parameters. + + Parameters + ---------- + success : bool, default=False + Indicates if the fit was successful. + parameters : Optional[List[object]], default=None + List of parameters used in the fit. + chi_square : Optional[float], default=None + Chi-square value of the fit. + reduced_chi_square : Optional[float], default=None + Reduced chi-square value of the fit. + message : str, default='' + Message related to the fit. + iterations : int, default=0 + Number of iterations performed. + engine_result : Optional[object], default=None + Result from the fitting engine. + starting_parameters : Optional[List[object]], default=None + Initial parameters for the fit. + fitting_time : Optional[float], default=None + Time taken for the fitting process. + **kwargs : object + Additional engine-specific fields. If ``redchi`` is provided + and ``reduced_chi_square`` is not set, it is used as the + reduced chi-square value. """ self.success: bool = success - self.parameters: List[Any] = parameters if parameters is not None else [] + self.parameters: List[object] = parameters if parameters is not None else [] self.chi_square: Optional[float] = chi_square self.reduced_chi_square: Optional[float] = reduced_chi_square self.message: str = message self.iterations: int = iterations - self.engine_result: Optional[Any] = engine_result - self.result: Optional[Any] = None - self.starting_parameters: List[Any] = ( + self.engine_result: Optional[object] = engine_result + self.result: Optional[object] = None + self.starting_parameters: List[object] = ( starting_parameters if starting_parameters is not None else [] ) self.fitting_time: Optional[float] = fitting_time @@ -77,14 +89,21 @@ def display_results( f_obs: Optional[List[float]] = None, f_calc: Optional[List[float]] = None, ) -> None: - """Render a human-readable summary of the fit. - - Args: - y_obs: Observed intensities for pattern R-factor metrics. - y_calc: Calculated intensities for pattern R-factor metrics. - y_err: Standard deviations of observed intensities for wR. - f_obs: Observed structure-factor magnitudes for Bragg R. - f_calc: Calculated structure-factor magnitudes for Bragg R. + """ + Render a human-readable summary of the fit. + + Parameters + ---------- + y_obs : Optional[List[float]], default=None + Observed intensities for pattern R-factor metrics. + y_calc : Optional[List[float]], default=None + Calculated intensities for pattern R-factor metrics. + y_err : Optional[List[float]], default=None + Standard deviations of observed intensities for wR. + f_obs : Optional[List[float]], default=None + Observed structure-factor magnitudes for Bragg R. + f_calc : Optional[List[float]], default=None + Calculated structure-factor magnitudes for Bragg R. """ status_icon = '✅' if self.success else '❌' rf = rf2 = wr = br = None diff --git a/src/easydiffraction/analysis/fit_helpers/tracking.py b/src/easydiffraction/analysis/fit_helpers/tracking.py index 438fd46d..804dc536 100644 --- a/src/easydiffraction/analysis/fit_helpers/tracking.py +++ b/src/easydiffraction/analysis/fit_helpers/tracking.py @@ -1,9 +1,8 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause import time from contextlib import suppress -from typing import Any from typing import List from typing import Optional @@ -20,6 +19,7 @@ clear_output = None from easydiffraction.analysis.fit_helpers.metrics import calculate_reduced_chi_square +from easydiffraction.utils.enums import VerbosityEnum from easydiffraction.utils.environment import in_jupyter from easydiffraction.utils.utils import render_table @@ -36,31 +36,40 @@ class _TerminalLiveHandle: - """Adapter that exposes update()/close() for terminal live updates. + """ + Adapter that exposes update()/close() for terminal live updates. Wraps a rich.live.Live instance but keeps the tracker decoupled from the underlying UI mechanism. """ - def __init__(self, live) -> None: + def __init__(self, live: object) -> None: self._live = live - def update(self, renderable) -> None: + def update(self, renderable: object) -> None: + """ + Refresh the live display with a new renderable. + + Parameters + ---------- + renderable : object + A Rich-compatible renderable to display. + """ self._live.update(renderable, refresh=True) def close(self) -> None: + """Stop the live display, suppressing any errors.""" with suppress(Exception): self._live.stop() -def _make_display_handle() -> Any | None: - """Create and initialize a display/update handle for the - environment. +def _make_display_handle() -> object | None: + """ + Create and initialize a display/update handle for the environment. - In Jupyter, returns an IPython DisplayHandle and creates a - placeholder. - - In terminal, returns a _TerminalLiveHandle backed by rich Live. - - If neither applies, returns None. + placeholder. - In terminal, returns a _TerminalLiveHandle backed by + rich Live. - If neither applies, returns None. """ if in_jupyter() and display is not None and HTML is not None: h = DisplayHandle() @@ -77,7 +86,8 @@ def _make_display_handle() -> Any | None: class FitProgressTracker: - """Track and report reduced chi-square during optimization. + """ + Track and report reduced chi-square during optimization. The tracker keeps iteration counters, remembers the best observed reduced chi-square and when it occurred, and can display progress as @@ -92,10 +102,11 @@ def __init__(self) -> None: self._best_chi2: Optional[float] = None self._best_iteration: Optional[int] = None self._fitting_time: Optional[float] = None + self._verbosity: VerbosityEnum = VerbosityEnum.FULL self._df_rows: List[List[str]] = [] - self._display_handle: Optional[Any] = None - self._live: Optional[Any] = None + self._display_handle: Optional[object] = None + self._live: Optional[object] = None def reset(self) -> None: """Reset internal state before a new optimization run.""" @@ -112,13 +123,19 @@ def track( residuals: np.ndarray, parameters: List[float], ) -> np.ndarray: - """Update progress with current residuals and parameters. - - Args: - residuals: Residuals between measured and calculated data. - parameters: Current free parameters being fitted. - - Returns: + """ + Update progress with current residuals and parameters. + + Parameters + ---------- + residuals : np.ndarray + Residuals between measured and calculated data. + parameters : List[float] + Current free parameters being fitted. + + Returns + ------- + np.ndarray Residuals unchanged, for optimizer consumption. """ self._iteration += 1 @@ -200,11 +217,19 @@ def stop_timer(self) -> None: self._fitting_time = self._end_time - self._start_time def start_tracking(self, minimizer_name: str) -> None: - """Initialize display and headers and announce the minimizer. + """ + Initialize display and headers and announce the minimizer. - Args: - minimizer_name: Name of the minimizer used for the run. + Parameters + ---------- + minimizer_name : str + Name of the minimizer used for the run. """ + if self._verbosity is VerbosityEnum.SILENT: + return + if self._verbosity is VerbosityEnum.SHORT: + return + console.print(f"🚀 Starting fit process with '{minimizer_name}'...") console.print('📈 Goodness-of-fit (reduced χ²) change:') @@ -221,14 +246,19 @@ def start_tracking(self, minimizer_name: str) -> None: ) def add_tracking_info(self, row: List[str]) -> None: - """Append a formatted row to the progress display. + """ + Append a formatted row to the progress display. - Args: - row: Columns corresponding to DEFAULT_HEADERS. + Parameters + ---------- + row : List[str] + Columns corresponding to DEFAULT_HEADERS. """ + self._df_rows.append(row) + if self._verbosity is not VerbosityEnum.FULL: + return # Append and update via the active handle (Jupyter or # terminal live) - self._df_rows.append(row) render_table( columns_headers=DEFAULT_HEADERS, columns_alignment=DEFAULT_ALIGNMENTS, @@ -246,6 +276,9 @@ def finish_tracking(self) -> None: ] self.add_tracking_info(row) + if self._verbosity is not VerbosityEnum.FULL: + return + # Close terminal live if used if self._display_handle is not None and hasattr(self._display_handle, 'close'): with suppress(Exception): diff --git a/src/easydiffraction/analysis/fitting.py b/src/easydiffraction/analysis/fitting.py index ee6e3c84..de6dcdaa 100644 --- a/src/easydiffraction/analysis/fitting.py +++ b/src/easydiffraction/analysis/fitting.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause from typing import TYPE_CHECKING @@ -11,9 +11,10 @@ from easydiffraction.analysis.fit_helpers.metrics import get_reliability_inputs from easydiffraction.analysis.minimizers.factory import MinimizerFactory -from easydiffraction.core.parameters import Parameter -from easydiffraction.experiments.experiments import Experiments -from easydiffraction.sample_models.sample_models import SampleModels +from easydiffraction.core.variable import Parameter +from easydiffraction.datablocks.experiment.collection import Experiments +from easydiffraction.datablocks.structure.collection import Structures +from easydiffraction.utils.enums import VerbosityEnum if TYPE_CHECKING: from easydiffraction.analysis.fit_helpers.reporting import FitResults @@ -22,33 +23,42 @@ class Fitter: """Handles the fitting workflow using a pluggable minimizer.""" - def __init__(self, selection: str = 'lmfit (leastsq)') -> None: + def __init__(self, selection: str = 'lmfit') -> None: self.selection: str = selection - self.engine: str = selection.split(' ')[0] # Extracts 'lmfit' or 'dfols' - self.minimizer = MinimizerFactory.create_minimizer(selection) + self.engine: str = selection + self.minimizer = MinimizerFactory.create(selection) self.results: Optional[FitResults] = None def fit( self, - sample_models: SampleModels, + structures: Structures, experiments: Experiments, weights: Optional[np.array] = None, - analysis=None, + analysis: object = None, + verbosity: VerbosityEnum = VerbosityEnum.FULL, ) -> None: - """Run the fitting process. + """ + Run the fitting process. This method performs the optimization but does not display - results. Use :meth:`show_fit_results` on the Analysis object - to display the fit results after fitting is complete. - - Args: - sample_models: Collection of sample models. - experiments: Collection of experiments. - weights: Optional weights for joint fitting. - analysis: Optional Analysis object to update its categories - during fitting. + results. Use :meth:`show_fit_results` on the Analysis object to + display the fit results after fitting is complete. + + Parameters + ---------- + structures : Structures + Collection of structures. + experiments : Experiments + Collection of experiments. + weights : Optional[np.array], default=None + Optional weights for joint fitting. + analysis : object, default=None + Optional Analysis object to update its categories during + fitting. + verbosity : VerbosityEnum, default=VerbosityEnum.FULL + Console output verbosity. """ - params = sample_models.free_parameters + experiments.free_parameters + params = structures.free_parameters + experiments.free_parameters if not params: print('⚠️ No parameters selected for fitting.') @@ -58,36 +68,53 @@ def fit( param._fit_start_value = param.value def objective_function(engine_params: Dict[str, Any]) -> np.ndarray: + """ + Evaluate the residual for the current minimizer parameters. + + Parameters + ---------- + engine_params : Dict[str, Any] + Parameter values provided by the minimizer engine. + + Returns + ------- + np.ndarray + Residual array passed back to the minimizer. + """ return self._residual_function( engine_params=engine_params, parameters=params, - sample_models=sample_models, + structures=structures, experiments=experiments, weights=weights, analysis=analysis, ) # Perform fitting - self.results = self.minimizer.fit(params, objective_function) + self.results = self.minimizer.fit(params, objective_function, verbosity=verbosity) def _process_fit_results( self, - sample_models: SampleModels, + structures: Structures, experiments: Experiments, ) -> None: - """Collect reliability inputs and display fit results. + """ + Collect reliability inputs and display fit results. This method is typically called by :meth:`Analysis.show_fit_results` rather than directly. It - calculates R-factors and other metrics, then renders them to - the console. - - Args: - sample_models: Collection of sample models. - experiments: Collection of experiments. + calculates R-factors and other metrics, then renders them to the + console. + + Parameters + ---------- + structures : Structures + Collection of structures. + experiments : Experiments + Collection of experiments. """ y_obs, y_calc, y_err = get_reliability_inputs( - sample_models, + structures, experiments, ) @@ -105,54 +132,71 @@ def _process_fit_results( def _collect_free_parameters( self, - sample_models: SampleModels, + structures: Structures, experiments: Experiments, ) -> List[Parameter]: - """Collect free parameters from sample models and experiments. - - Args: - sample_models: Collection of sample models. - experiments: Collection of experiments. - - Returns: + """ + Collect free parameters from structures and experiments. + + Parameters + ---------- + structures : Structures + Collection of structures. + experiments : Experiments + Collection of experiments. + + Returns + ------- + List[Parameter] List of free parameters. """ - free_params: List[Parameter] = sample_models.free_parameters + experiments.free_parameters + free_params: List[Parameter] = structures.free_parameters + experiments.free_parameters return free_params def _residual_function( self, engine_params: Dict[str, Any], parameters: List[Parameter], - sample_models: SampleModels, + structures: Structures, experiments: Experiments, weights: Optional[np.array] = None, - analysis=None, + analysis: object = None, ) -> np.ndarray: - """Residual function computes the difference between measured - and calculated patterns. It updates the parameter values - according to the optimizer-provided engine_params. - - Args: - engine_params: Engine-specific parameter dict. - parameters: List of parameters being optimized. - sample_models: Collection of sample models. - experiments: Collection of experiments. - weights: Optional weights for joint fitting. - analysis: Optional Analysis object to update its categories - during fitting. - - Returns: + """ + Compute residuals between measured and calculated patterns. + + It updates the parameter values according to the + optimizer-provided engine_params. + + Parameters + ---------- + engine_params : Dict[str, Any] + Engine-specific parameter dict. + parameters : List[Parameter] + List of parameters being optimized. + structures : Structures + Collection of structures. + experiments : Experiments + Collection of experiments. + weights : Optional[np.array], default=None + Optional weights for joint fitting. + analysis : object, default=None + Optional Analysis object to update its categories during + fitting. + + Returns + ------- + np.ndarray Array of weighted residuals. """ # Sync parameters back to objects self.minimizer._sync_result_to_parameters(parameters, engine_params) # Update categories to reflect new parameter values - # Order matters: sample models first (symmetry, structure), + # Order matters: structures first (symmetry, structure), # then analysis (constraints), then experiments (calculations) - for sample_model in sample_models: - sample_model._update_categories() + for structure in structures: + structure._update_categories() if analysis is not None: analysis._update_categories(called_by_minimizer=True) @@ -182,9 +226,9 @@ def _residual_function( # Calculate the difference between measured and calculated # patterns - y_calc: np.ndarray = experiment.data.calc - y_meas: np.ndarray = experiment.data.meas - y_meas_su: np.ndarray = experiment.data.meas_su + y_calc: np.ndarray = experiment.data.intensity_calc + y_meas: np.ndarray = experiment.data.intensity_meas + y_meas_su: np.ndarray = experiment.data.intensity_meas_su diff = (y_meas - y_calc) / y_meas_su # Residuals are squared before going into reduced diff --git a/src/easydiffraction/analysis/minimizers/__init__.py b/src/easydiffraction/analysis/minimizers/__init__.py index 429f2648..01fbc136 100644 --- a/src/easydiffraction/analysis/minimizers/__init__.py +++ b/src/easydiffraction/analysis/minimizers/__init__.py @@ -1,2 +1,5 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause + +from easydiffraction.analysis.minimizers.dfols import DfolsMinimizer +from easydiffraction.analysis.minimizers.lmfit import LmfitMinimizer diff --git a/src/easydiffraction/analysis/minimizers/base.py b/src/easydiffraction/analysis/minimizers/base.py index 275ad973..63b6bc27 100644 --- a/src/easydiffraction/analysis/minimizers/base.py +++ b/src/easydiffraction/analysis/minimizers/base.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause from abc import ABC @@ -13,17 +13,17 @@ from easydiffraction.analysis.fit_helpers.reporting import FitResults from easydiffraction.analysis.fit_helpers.tracking import FitProgressTracker +from easydiffraction.utils.enums import VerbosityEnum class MinimizerBase(ABC): - """Abstract base for concrete minimizers. - - Contract: - - Subclasses must implement ``_prepare_solver_args``, - ``_run_solver``, ``_sync_result_to_parameters`` and - ``_check_success``. - - The ``fit`` method orchestrates the full workflow and returns - :class:`FitResults`. + """ + Abstract base for concrete minimizers. + + Contract: - Subclasses must implement ``_prepare_solver_args``, + ``_run_solver``, ``_sync_result_to_parameters`` and + ``_check_success``. - The ``fit`` method orchestrates the full + workflow and returns :class:`FitResults`. """ def __init__( @@ -43,13 +43,23 @@ def __init__( self._fitting_time: Optional[float] = None self.tracker: FitProgressTracker = FitProgressTracker() - def _start_tracking(self, minimizer_name: str) -> None: - """Initialize progress tracking and timer. - - Args: - minimizer_name: Human-readable name shown in progress. + def _start_tracking( + self, + minimizer_name: str, + verbosity: VerbosityEnum = VerbosityEnum.FULL, + ) -> None: + """ + Initialize progress tracking and timer. + + Parameters + ---------- + minimizer_name : str + Human-readable name shown in progress. + verbosity : VerbosityEnum, default=VerbosityEnum.FULL + Console output verbosity. """ self.tracker.reset() + self.tracker._verbosity = verbosity self.tracker.start_tracking(minimizer_name) self.tracker.start_timer() @@ -60,12 +70,17 @@ def _stop_tracking(self) -> None: @abstractmethod def _prepare_solver_args(self, parameters: List[Any]) -> Dict[str, Any]: - """Prepare keyword-arguments for the underlying solver. + """ + Prepare keyword-arguments for the underlying solver. - Args: - parameters: List of free parameters to be fitted. + Parameters + ---------- + parameters : List[Any] + List of free parameters to be fitted. - Returns: + Returns + ------- + Dict[str, Any] Mapping of keyword arguments to pass into ``_run_solver``. """ pass @@ -73,36 +88,40 @@ def _prepare_solver_args(self, parameters: List[Any]) -> Dict[str, Any]: @abstractmethod def _run_solver( self, - objective_function: Callable[..., Any], - engine_parameters: Dict[str, Any], - ) -> Any: + objective_function: Callable[..., object], + engine_parameters: Dict[str, object], + ) -> object: """Execute the concrete solver and return its raw result.""" pass @abstractmethod def _sync_result_to_parameters( self, - raw_result: Any, - parameters: List[Any], + raw_result: object, + parameters: List[object], ) -> None: - """Copy values from ``raw_result`` back to ``parameters`` in- - place. - """ + """Copy raw_result values back to parameters in-place.""" pass def _finalize_fit( self, - parameters: List[Any], - raw_result: Any, + parameters: List[object], + raw_result: object, ) -> FitResults: - """Build :class:`FitResults` and store it on ``self.result``. - - Args: - parameters: Parameters after the solver finished. - raw_result: Backend-specific solver output object. - - Returns: - FitResults: Aggregated outcome of the fit. + """ + Build :class:`FitResults` and store it on ``self.result``. + + Parameters + ---------- + parameters : List[object] + Parameters after the solver finished. + raw_result : object + Backend-specific solver output object. + + Returns + ------- + FitResults + Aggregated outcome of the fit. """ self._sync_result_to_parameters(parameters, raw_result) success = self._check_success(raw_result) @@ -117,30 +136,39 @@ def _finalize_fit( return self.result @abstractmethod - def _check_success(self, raw_result: Any) -> bool: + def _check_success(self, raw_result: object) -> bool: """Determine whether the fit was successful.""" pass def fit( self, - parameters: List[Any], - objective_function: Callable[..., Any], + parameters: List[object], + objective_function: Callable[..., object], + verbosity: VerbosityEnum = VerbosityEnum.FULL, ) -> FitResults: - """Run the full minimization workflow. - - Args: - parameters: Free parameters to optimize. - objective_function: Callable returning residuals for a given - set of engine arguments. - - Returns: + """ + Run the full minimization workflow. + + Parameters + ---------- + parameters : List[object] + Free parameters to optimize. + objective_function : Callable[..., object] + Callable returning residuals for a given set of engine + arguments. + verbosity : VerbosityEnum, default=VerbosityEnum.FULL + Console output verbosity. + + Returns + ------- + FitResults FitResults with success flag, best chi2 and timing. """ minimizer_name = self.name or 'Unnamed Minimizer' if self.method is not None: minimizer_name += f' ({self.method})' - self._start_tracking(minimizer_name) + self._start_tracking(minimizer_name, verbosity=verbosity) solver_args = self._prepare_solver_args(parameters) raw_result = self._run_solver(objective_function, **solver_args) @@ -153,33 +181,33 @@ def fit( def _objective_function( self, - engine_params: Dict[str, Any], - parameters: List[Any], - sample_models: Any, - experiments: Any, - calculator: Any, + engine_params: Dict[str, object], + parameters: List[object], + structures: object, + experiments: object, + calculator: object, ) -> np.ndarray: """Default objective helper computing residuals array.""" return self._compute_residuals( engine_params, parameters, - sample_models, + structures, experiments, calculator, ) def _create_objective_function( self, - parameters: List[Any], - sample_models: Any, - experiments: Any, - calculator: Any, - ) -> Callable[[Dict[str, Any]], np.ndarray]: + parameters: List[object], + structures: object, + experiments: object, + calculator: object, + ) -> Callable[[Dict[str, object]], np.ndarray]: """Return a closure capturing problem context for the solver.""" return lambda engine_params: self._objective_function( engine_params, parameters, - sample_models, + structures, experiments, calculator, ) diff --git a/src/easydiffraction/analysis/minimizers/dfols.py b/src/easydiffraction/analysis/minimizers/dfols.py index 544d4b7a..fad9ad06 100644 --- a/src/easydiffraction/analysis/minimizers/dfols.py +++ b/src/easydiffraction/analysis/minimizers/dfols.py @@ -1,7 +1,6 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -from typing import Any from typing import Dict from typing import List @@ -9,26 +8,32 @@ from dfols import solve from easydiffraction.analysis.minimizers.base import MinimizerBase +from easydiffraction.analysis.minimizers.factory import MinimizerFactory +from easydiffraction.core.metadata import TypeInfo DEFAULT_MAX_ITERATIONS = 1000 +@MinimizerFactory.register class DfolsMinimizer(MinimizerBase): - """Minimizer using the DFO-LS package (Derivative-Free Optimization - for Least-Squares). - """ + """Minimizer using DFO-LS (derivative-free least-squares).""" + + type_info = TypeInfo( + tag='dfols', + description='DFO-LS derivative-free least-squares optimization', + ) def __init__( self, name: str = 'dfols', max_iterations: int = DEFAULT_MAX_ITERATIONS, - **kwargs: Any, + **kwargs: object, ) -> None: super().__init__(name=name, method=None, max_iterations=max_iterations) # Intentionally unused, accepted for API compatibility del kwargs - def _prepare_solver_args(self, parameters: List[Any]) -> Dict[str, Any]: + def _prepare_solver_args(self, parameters: List[object]) -> Dict[str, object]: x0 = [] bounds_lower = [] bounds_upper = [] @@ -39,38 +44,49 @@ def _prepare_solver_args(self, parameters: List[Any]) -> Dict[str, Any]: bounds = (np.array(bounds_lower), np.array(bounds_upper)) return {'x0': np.array(x0), 'bounds': bounds} - def _run_solver(self, objective_function: Any, **kwargs: Any) -> Any: + def _run_solver(self, objective_function: object, **kwargs: object) -> object: x0 = kwargs.get('x0') bounds = kwargs.get('bounds') return solve(objective_function, x0=x0, bounds=bounds, maxfun=self.max_iterations) def _sync_result_to_parameters( self, - parameters: List[Any], - raw_result: Any, + parameters: List[object], + raw_result: object, ) -> None: - """Synchronizes the result from the solver to the parameters. + """ + Synchronize the solver result back to the parameters. - Args: - parameters: List of parameters being optimized. - raw_result: The result object returned by the solver. + Parameters + ---------- + parameters : List[object] + List of parameters being optimized. + raw_result : object + The result object returned by the solver. """ # Ensure compatibility with raw_result coming from dfols.solve() result_values = raw_result.x if hasattr(raw_result, 'x') else raw_result for i, param in enumerate(parameters): - param.value = result_values[i] + # Bypass validation but set the dirty flag so + # _update_categories() knows work is needed. + param._set_value_from_minimizer(result_values[i]) # DFO-LS doesn't provide uncertainties; set to None or # calculate later if needed param.uncertainty = None - def _check_success(self, raw_result: Any) -> bool: - """Determines success from DFO-LS result dictionary. + def _check_success(self, raw_result: object) -> bool: + """ + Determine success from DFO-LS result dictionary. - Args: - raw_result: The result object returned by the solver. + Parameters + ---------- + raw_result : object + The result object returned by the solver. - Returns: + Returns + ------- + bool True if the optimization was successful, False otherwise. """ return raw_result.flag == raw_result.EXIT_SUCCESS diff --git a/src/easydiffraction/analysis/minimizers/factory.py b/src/easydiffraction/analysis/minimizers/factory.py index 0b1afaa2..18f67cc6 100644 --- a/src/easydiffraction/analysis/minimizers/factory.py +++ b/src/easydiffraction/analysis/minimizers/factory.py @@ -1,126 +1,15 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +"""Minimizer factory — delegates to ``FactoryBase``.""" -from typing import Any -from typing import Dict -from typing import List -from typing import Optional -from typing import Type +from __future__ import annotations -from easydiffraction.analysis.minimizers.base import MinimizerBase -from easydiffraction.analysis.minimizers.dfols import DfolsMinimizer -from easydiffraction.analysis.minimizers.lmfit import LmfitMinimizer -from easydiffraction.utils.logging import console -from easydiffraction.utils.utils import render_table +from easydiffraction.core.factory import FactoryBase -class MinimizerFactory: - _available_minimizers: Dict[str, Dict[str, Any]] = { - 'lmfit': { - 'engine': 'lmfit', - 'method': 'leastsq', - 'description': 'LMFIT library using the default Levenberg-Marquardt ' - 'least squares method', - 'class': LmfitMinimizer, - }, - 'lmfit (leastsq)': { - 'engine': 'lmfit', - 'method': 'leastsq', - 'description': 'LMFIT library with Levenberg-Marquardt least squares method', - 'class': LmfitMinimizer, - }, - 'lmfit (least_squares)': { - 'engine': 'lmfit', - 'method': 'least_squares', - 'description': 'LMFIT library with SciPy’s trust region reflective algorithm', - 'class': LmfitMinimizer, - }, - 'dfols': { - 'engine': 'dfols', - 'method': None, - 'description': 'DFO-LS library for derivative-free least-squares optimization', - 'class': DfolsMinimizer, - }, - } - - @classmethod - def list_available_minimizers(cls) -> List[str]: - """List all available minimizers. - - Returns: - A list of minimizer names. - """ - return list(cls._available_minimizers.keys()) - - @classmethod - def show_available_minimizers(cls) -> None: - # TODO: Rename this method to `show_supported_minimizers` for - # consistency with other methods in the library. E.g. - # `show_supported_calculators`, etc. - """Display a table of available minimizers and their - descriptions. - """ - columns_headers: List[str] = ['Minimizer', 'Description'] - columns_alignment = ['left', 'left'] - columns_data: List[List[str]] = [] - for name, config in cls._available_minimizers.items(): - description: str = config.get('description', 'No description provided.') - columns_data.append([name, description]) - - console.paragraph('Supported minimizers') - render_table( - columns_headers=columns_headers, - columns_alignment=columns_alignment, - columns_data=columns_data, - ) - - @classmethod - def create_minimizer(cls, selection: str) -> MinimizerBase: - """Create a minimizer instance based on the selection. - - Args: - selection: The name of the minimizer to create. +class MinimizerFactory(FactoryBase): + """Factory for creating minimizer instances.""" - Returns: - An instance of the selected minimizer. - - Raises: - ValueError: If the selection is not a valid minimizer. - """ - config = cls._available_minimizers.get(selection) - if not config: - raise ValueError( - f"Unknown minimizer '{selection}'. Use one of {cls.list_available_minimizers()}" - ) - - minimizer_class: Type[MinimizerBase] = config.get('class') - method: Optional[str] = config.get('method') - - kwargs: Dict[str, Any] = {} - if method is not None: - kwargs['method'] = method - - return minimizer_class(**kwargs) - - @classmethod - def register_minimizer( - cls, - name: str, - minimizer_cls: Type[MinimizerBase], - method: Optional[str] = None, - description: str = 'No description provided.', - ) -> None: - """Register a new minimizer. - - Args: - name: The name of the minimizer. - minimizer_cls: The class of the minimizer. - method: The method used by the minimizer (optional). - description: A description of the minimizer. - """ - cls._available_minimizers[name] = { - 'engine': name, - 'method': method, - 'description': description, - 'class': minimizer_cls, - } + _default_rules = { + frozenset(): 'lmfit', + } diff --git a/src/easydiffraction/analysis/minimizers/lmfit.py b/src/easydiffraction/analysis/minimizers/lmfit.py index d7a651cb..771bacad 100644 --- a/src/easydiffraction/analysis/minimizers/lmfit.py +++ b/src/easydiffraction/analysis/minimizers/lmfit.py @@ -1,21 +1,28 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -from typing import Any from typing import Dict from typing import List import lmfit from easydiffraction.analysis.minimizers.base import MinimizerBase +from easydiffraction.analysis.minimizers.factory import MinimizerFactory +from easydiffraction.core.metadata import TypeInfo DEFAULT_METHOD = 'leastsq' DEFAULT_MAX_ITERATIONS = 1000 +@MinimizerFactory.register class LmfitMinimizer(MinimizerBase): """Minimizer using the lmfit package.""" + type_info = TypeInfo( + tag='lmfit', + description='LMFIT with Levenberg-Marquardt least squares', + ) + def __init__( self, name: str = 'lmfit', @@ -30,16 +37,21 @@ def __init__( def _prepare_solver_args( self, - parameters: List[Any], - ) -> Dict[str, Any]: - """Prepares the solver arguments for the lmfit minimizer. + parameters: List[object], + ) -> Dict[str, object]: + """ + Prepare the solver arguments for the lmfit minimizer. - Args: - parameters: List of parameters to be optimized. + Parameters + ---------- + parameters : List[object] + List of parameters to be optimized. - Returns: + Returns + ------- + Dict[str, object] A dictionary containing the prepared lmfit. Parameters - object. + object. """ engine_parameters = lmfit.Parameters() for param in parameters: @@ -52,14 +64,20 @@ def _prepare_solver_args( ) return {'engine_parameters': engine_parameters} - def _run_solver(self, objective_function: Any, **kwargs: Any) -> Any: - """Runs the lmfit solver. - - Args: - objective_function: The objective function to minimize. - **kwargs: Additional arguments for the solver. - - Returns: + def _run_solver(self, objective_function: object, **kwargs: object) -> object: + """ + Run the lmfit solver. + + Parameters + ---------- + objective_function : object + The objective function to minimize. + **kwargs : object + Additional arguments for the solver. + + Returns + ------- + object The result of the lmfit minimization. """ engine_parameters = kwargs.get('engine_parameters') @@ -74,30 +92,41 @@ def _run_solver(self, objective_function: Any, **kwargs: Any) -> Any: def _sync_result_to_parameters( self, - parameters: List[Any], - raw_result: Any, + parameters: List[object], + raw_result: object, ) -> None: - """Synchronizes the result from the solver to the parameters. - - Args: - parameters: List of parameters being optimized. - raw_result: The result object returned by the solver. + """ + Synchronize the result from the solver to the parameters. + + Parameters + ---------- + parameters : List[object] + List of parameters being optimized. + raw_result : object + The result object returned by the solver. """ param_values = raw_result.params if hasattr(raw_result, 'params') else raw_result for param in parameters: param_result = param_values.get(param._minimizer_uid) if param_result is not None: - param._value = param_result.value # Bypass ranges check + # Bypass validation but set the dirty flag so + # _update_categories() knows work is needed. + param._set_value_from_minimizer(param_result.value) param.uncertainty = getattr(param_result, 'stderr', None) - def _check_success(self, raw_result: Any) -> bool: - """Determines success from lmfit MinimizerResult. + def _check_success(self, raw_result: object) -> bool: + """ + Determine success from lmfit MinimizerResult. - Args: - raw_result: The result object returned by the solver. + Parameters + ---------- + raw_result : object + The result object returned by the solver. - Returns: + Returns + ------- + bool True if the optimization was successful, False otherwise. """ return getattr(raw_result, 'success', False) @@ -106,18 +135,25 @@ def _iteration_callback( self, params: lmfit.Parameters, iter: int, - resid: Any, - *args: Any, - **kwargs: Any, + resid: object, + *args: object, + **kwargs: object, ) -> None: - """Callback function for each iteration of the minimizer. - - Args: - params: The current parameters. - iter: The current iteration number. - resid: The residuals. - *args: Additional positional arguments. - **kwargs: Additional keyword arguments. + """ + Handle each iteration callback of the minimizer. + + Parameters + ---------- + params : lmfit.Parameters + The current parameters. + iter : int + The current iteration number. + resid : object + The residuals. + *args : object + Additional positional arguments. + **kwargs : object + Additional keyword arguments. """ # Intentionally unused, required by callback signature del params, resid, args, kwargs diff --git a/src/easydiffraction/core/__init__.py b/src/easydiffraction/core/__init__.py index 429f2648..4e798e20 100644 --- a/src/easydiffraction/core/__init__.py +++ b/src/easydiffraction/core/__init__.py @@ -1,2 +1,2 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause diff --git a/src/easydiffraction/core/category.py b/src/easydiffraction/core/category.py index 6be057da..25e81894 100644 --- a/src/easydiffraction/core/category.py +++ b/src/easydiffraction/core/category.py @@ -1,17 +1,19 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause from __future__ import annotations from easydiffraction.core.collection import CollectionBase from easydiffraction.core.guard import GuardedBase -from easydiffraction.core.parameters import GenericDescriptorBase -from easydiffraction.core.validation import checktype +from easydiffraction.core.variable import GenericDescriptorBase +from easydiffraction.core.variable import GenericStringDescriptor from easydiffraction.io.cif.serialize import category_collection_from_cif from easydiffraction.io.cif.serialize import category_collection_to_cif from easydiffraction.io.cif.serialize import category_item_from_cif from easydiffraction.io.cif.serialize import category_item_to_cif +# ====================================================================== + class CategoryItem(GuardedBase): """Base class for items in a category collection.""" @@ -28,12 +30,13 @@ def __str__(self) -> str: return f'<{name} ({params})>' # TODO: Common for all categories - def _update(self, called_by_minimizer=False): + def _update(self, called_by_minimizer: bool = False) -> None: del called_by_minimizer pass @property - def unique_name(self): + def unique_name(self) -> str: + """Fully qualified name: datablock, category, entry.""" parts = [ self._identity.datablock_entry_name, self._identity.category_code, @@ -44,7 +47,8 @@ def unique_name(self): return '.'.join(str_parts) @property - def parameters(self): + def parameters(self) -> list: + """All GenericDescriptorBase instances on this item.""" return [v for v in vars(self).values() if isinstance(v, GenericDescriptorBase)] @property @@ -52,13 +56,114 @@ def as_cif(self) -> str: """Return CIF representation of this object.""" return category_item_to_cif(self) - def from_cif(self, block, idx=0): + def from_cif(self, block: object, idx: int = 0) -> None: """Populate this item from a CIF block.""" category_item_from_cif(self, block, idx) + def help(self) -> None: + """Print parameters, other properties, and methods.""" + from easydiffraction.utils.logging import console + from easydiffraction.utils.utils import render_table + + cls = type(self) + console.paragraph(f"Help for '{cls.__name__}'") + + # Deduplicate properties + seen: dict = {} + for key, prop in cls._iter_properties(): + if key not in seen: + seen[key] = prop + + # Split into descriptor-backed and other + param_rows = [] + other_rows = [] + p_idx = 0 + o_idx = 0 + for key in sorted(seen): + prop = seen[key] + try: + val = getattr(self, key) + except Exception: + val = None + if isinstance(val, GenericDescriptorBase): + p_idx += 1 + type_str = 'string' if isinstance(val, GenericStringDescriptor) else 'numeric' + writable = '✓' if prop.fset else '✗' + param_rows.append([ + str(p_idx), + key, + type_str, + str(val.value), + writable, + val.description or '', + ]) + else: + o_idx += 1 + writable = '✓' if prop.fset else '✗' + doc = self._first_sentence(prop.fget.__doc__ if prop.fget else None) + other_rows.append([str(o_idx), key, writable, doc]) + + if param_rows: + console.paragraph('Parameters') + render_table( + columns_headers=[ + '#', + 'Name', + 'Type', + 'Value', + 'Writable', + 'Description', + ], + columns_alignment=[ + 'right', + 'left', + 'left', + 'right', + 'center', + 'left', + ], + columns_data=param_rows, + ) + + if other_rows: + console.paragraph('Other properties') + render_table( + columns_headers=[ + '#', + 'Name', + 'Writable', + 'Description', + ], + columns_alignment=[ + 'right', + 'left', + 'center', + 'left', + ], + columns_data=other_rows, + ) + + methods = dict(cls._iter_methods()) + method_rows = [] + for i, key in enumerate(sorted(methods), 1): + doc = self._first_sentence(getattr(methods[key], '__doc__', None)) + method_rows.append([str(i), f'{key}()', doc]) + + if method_rows: + console.paragraph('Methods') + render_table( + columns_headers=['#', 'Name', 'Description'], + columns_alignment=['right', 'left', 'left'], + columns_data=method_rows, + ) + + +# ====================================================================== + class CategoryCollection(CollectionBase): - """Handles loop-style category containers (e.g. AtomSites). + """ + Handles loop-style category containers (e.g. AtomSites). Each item is a CategoryItem (component). """ @@ -66,6 +171,22 @@ class CategoryCollection(CollectionBase): # TODO: Common for all categories _update_priority = 10 # Default. Lower values run first. + def _key_for(self, item: object) -> str | None: + """Return the category-level identity key for *item*.""" + return item._identity.category_entry_name + + def _mark_parent_dirty(self) -> None: + """ + Set ``_need_categories_update`` on the parent datablock. + + Called whenever the collection content changes (items added or + removed) so that subsequent ``_update_categories()`` calls re- + run all category updates. + """ + parent = getattr(self, '_parent', None) + if parent is not None and hasattr(parent, '_need_categories_update'): + parent._need_categories_update = True + def __str__(self) -> str: """Human-readable representation of this component.""" name = self._log_name @@ -73,16 +194,17 @@ def __str__(self) -> str: return f'<{name} collection ({size} items)>' # TODO: Common for all categories - def _update(self, called_by_minimizer=False): + def _update(self, called_by_minimizer: bool = False) -> None: del called_by_minimizer pass @property - def unique_name(self): + def unique_name(self) -> str | None: + """Return None; collections have no unique name.""" return None @property - def parameters(self): + def parameters(self) -> list: """All parameters from all items in this collection.""" params = [] for item in self._items: @@ -94,22 +216,37 @@ def as_cif(self) -> str: """Return CIF representation of this object.""" return category_collection_to_cif(self) - def from_cif(self, block): + def from_cif(self, block: object) -> None: """Populate this collection from a CIF block.""" category_collection_from_cif(self, block) - @checktype - def _add(self, item) -> None: - """Add an item to the collection.""" + def add(self, item: object) -> None: + """ + Insert or replace a pre-built item into the collection. + + Parameters + ---------- + item : object + A ``CategoryItem`` instance to add. + """ self[item._identity.category_entry_name] = item + self._mark_parent_dirty() + + def create(self, **kwargs: object) -> None: + """ + Create a new item with the given attributes and add it. + + A default instance of the collection's item type is created, + then each keyword argument is applied via ``setattr``. - # TODO: Disallow args and only allow kwargs? - # TODO: Check kwargs as for, e.g., - # ExperimentFactory.create(**kwargs)? - @checktype - def add(self, *args, **kwargs) -> None: - """Create and add a new child instance from the provided - arguments. + Parameters + ---------- + **kwargs : object + Attribute names and values for the new item. """ - child_obj = self._item_type(*args, **kwargs) - self._add(child_obj) + child_obj = self._item_type() + + for attr, val in kwargs.items(): + setattr(child_obj, attr, val) + + self.add(child_obj) diff --git a/src/easydiffraction/core/collection.py b/src/easydiffraction/core/collection.py index a84bdb51..8f590b07 100644 --- a/src/easydiffraction/core/collection.py +++ b/src/easydiffraction/core/collection.py @@ -1,48 +1,72 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -"""Lightweight container for guarded items with name-based indexing. +""" +Lightweight container for guarded items with name-based indexing. -`CollectionBase` maintains an ordered list of items and a lazily rebuilt -index by the item's identity key. It supports dict-like access for get, -set and delete, along with iteration over the items. +``CollectionBase`` maintains an ordered list of items and a lazily +rebuilt index by the item's identity key. It supports dict-like access +for get, set and delete, along with iteration over the items. """ from __future__ import annotations +from typing import Generator +from typing import Iterator + from easydiffraction.core.guard import GuardedBase class CollectionBase(GuardedBase): - """A minimal collection with stable iteration and name indexing. + """ + A minimal collection with stable iteration and name indexing. - Args: - item_type: Type of items accepted by the collection. Used for - validation and tooling; not enforced at runtime here. + Parameters + ---------- + item_type : type + Type of items accepted by the collection. Used for validation + and tooling; not enforced at runtime here. """ - def __init__(self, item_type) -> None: + def __init__(self, item_type: type) -> None: super().__init__() self._items: list = [] self._index: dict = {} self._item_type = item_type - def __getitem__(self, name: str): - """Return an item by its identity key. - - Rebuilds the internal index on a cache miss to stay consistent - with recent mutations. + def __getitem__(self, key: str | int) -> GuardedBase: """ - try: - return self._index[name] - except KeyError: - self._rebuild_index() - return self._index[name] + Return an item by name or positional index. + + Parameters + ---------- + key : str | int + Identity key (str) or zero-based positional index (int). + + Returns + ------- + GuardedBase + The item matching the given key or index. + + Raises + ------ + TypeError + If *key* is neither ``str`` nor ``int``. + """ + if isinstance(key, int): + return self._items[key] + if isinstance(key, str): + try: + return self._index[key] + except KeyError: + self._rebuild_index() + return self._index[key] + raise TypeError(f'Collection indices must be str or int, not {type(key).__name__}') - def __setitem__(self, name: str, item) -> None: + def __setitem__(self, name: str, item: GuardedBase) -> None: """Insert or replace an item under the given identity key.""" - # Check if item with same identity exists; if so, replace it + # Check if item with same key exists; if so, replace it for i, existing_item in enumerate(self._items): - if existing_item._identity.category_entry_name == name: + if self._key_for(existing_item) == name: self._items[i] = item self._rebuild_index() return @@ -53,16 +77,20 @@ def __setitem__(self, name: str, item) -> None: def __delitem__(self, name: str) -> None: """Delete an item by key or raise ``KeyError`` if missing.""" - # Remove from _items by identity entry name for i, item in enumerate(self._items): - if item._identity.category_entry_name == name: + if self._key_for(item) == name: object.__setattr__(item, '_parent', None) # Unlink the parent before removal del self._items[i] self._rebuild_index() return raise KeyError(name) - def __iter__(self): + def __contains__(self, name: str) -> bool: + """Check whether an item with the given key exists.""" + self._rebuild_index() + return name in self._index + + def __iter__(self) -> Iterator[GuardedBase]: """Iterate over items in insertion order.""" return iter(self._items) @@ -70,9 +98,31 @@ def __len__(self) -> int: """Return the number of items in the collection.""" return len(self._items) - def _key_for(self, item): - """Return the identity key for ``item`` (category or - datablock). + def remove(self, name: str) -> None: + """ + Remove an item by its key. + + Parameters + ---------- + name : str + Identity key of the item to remove. + + Raises + ------ + KeyError + If no item with the given key exists. + """ + try: + del self[name] + except KeyError: + raise + + def _key_for(self, item: GuardedBase) -> str | None: + """ + Return the identity key for *item*. + + Subclasses must override to return the appropriate key + (``category_entry_name`` or ``datablock_entry_name``). """ return item._identity.category_entry_name or item._identity.datablock_entry_name @@ -84,19 +134,41 @@ def _rebuild_index(self) -> None: if key: self._index[key] = item - def keys(self): + def keys(self) -> Generator[str | None, None, None]: """Yield keys for all items in insertion order.""" return (self._key_for(item) for item in self._items) - def values(self): + def values(self) -> Generator[GuardedBase, None, None]: """Yield items in insertion order.""" return (item for item in self._items) - def items(self): + def items(self) -> Generator[tuple[str | None, GuardedBase], None, None]: """Yield ``(key, item)`` pairs in insertion order.""" return ((self._key_for(item), item) for item in self._items) @property - def names(self): + def names(self) -> list[str | None]: """List of all item keys in the collection.""" return list(self.keys()) + + def help(self) -> None: + """Print a summary of public attributes and contained items.""" + super().help() + + from easydiffraction.utils.logging import console + from easydiffraction.utils.utils import render_table + + if self._items: + console.paragraph(f'Items ({len(self._items)})') + rows = [] + for i, item in enumerate(self._items, 1): + key = self._key_for(item) + rows.append([str(i), str(key), f"['{key}']"]) + render_table( + columns_headers=['#', 'Name', 'Access'], + columns_alignment=['right', 'left', 'left'], + columns_data=rows, + ) + else: + console.paragraph('Items') + console.print('(empty)') diff --git a/src/easydiffraction/core/datablock.py b/src/easydiffraction/core/datablock.py index a8cc0ed8..6a1ef289 100644 --- a/src/easydiffraction/core/datablock.py +++ b/src/easydiffraction/core/datablock.py @@ -1,37 +1,43 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause from __future__ import annotations -from typeguard import typechecked - from easydiffraction.core.category import CategoryCollection from easydiffraction.core.category import CategoryItem from easydiffraction.core.collection import CollectionBase from easydiffraction.core.guard import GuardedBase -from easydiffraction.core.parameters import Parameter +from easydiffraction.core.variable import Parameter class DatablockItem(GuardedBase): """Base class for items in a datablock collection.""" - def __init__(self): + def __init__(self) -> None: super().__init__() - self._need_categories_update = False + self._need_categories_update = True def __str__(self) -> str: """Human-readable representation of this component.""" - name = self._log_name - items = getattr(self, '_items', None) - return f'<{name} ({items})>' + name = self.unique_name + cls = type(self).__name__ + categories = '\n'.join(f' - {c}' for c in self.categories) + return f"{cls} datablock '{name}':\n{categories}" + + def __repr__(self) -> str: + """Developer-oriented representation of this component.""" + name = self.unique_name + cls = type(self).__name__ + num_categories = len(self.categories) + return f'<{cls} datablock "{name}" ({num_categories} categories)>' def _update_categories( self, - called_by_minimizer=False, + called_by_minimizer: bool = False, ) -> None: # TODO: Make abstract method and implement in subclasses. # This should call apply_symmetry and apply_constraints in the - # case of sample models. In the case of experiments, it should + # case of structures. In the case of experiments, it should # run calculations to update the "data" categories. # Any parameter change should set _need_categories_update to # True. @@ -40,9 +46,15 @@ def _update_categories( # Should this be also called when parameters are accessed? E.g. # if one change background coefficients, then access the # background points in the data category? - # return - # if not self._need_categories_update: - # return + # + # Dirty-flag guard: skip if no parameter has changed since the + # last update. Minimisers use _set_value_from_minimizer() + # which bypasses validation but still sets this flag. + # During fitting the guard is bypassed because experiment + # calculations depend on structure parameters owned by a + # different DatablockItem whose flag changes are invisible here. + if not called_by_minimizer and not self._need_categories_update: + return for category in self.categories: category._update(called_by_minimizer=called_by_minimizer) @@ -50,11 +62,13 @@ def _update_categories( self._need_categories_update = False @property - def unique_name(self): + def unique_name(self) -> str | None: + """Unique name of this datablock item (from identity).""" return self._identity.datablock_entry_name @property - def categories(self): + def categories(self) -> list: + """All category objects in this datablock by priority.""" cats = [ v for v in vars(self).values() if isinstance(v, (CategoryItem, CategoryCollection)) ] @@ -62,10 +76,8 @@ def categories(self): return sorted(cats, key=lambda c: type(c)._update_priority) @property - def parameters(self): - """All parameters from all categories contained in this - datablock. - """ + def parameters(self) -> list: + """All parameters from all categories in this datablock.""" params = [] for v in self.categories: params.extend(v.parameters) @@ -79,14 +91,59 @@ def as_cif(self) -> str: self._update_categories() return datablock_item_to_cif(self) + def help(self) -> None: + """Print a summary of public attributes and categories.""" + super().help() + + from easydiffraction.utils.logging import console + from easydiffraction.utils.utils import render_table + + cats = self.categories + if cats: + console.paragraph('Categories') + rows = [] + for c in cats: + code = c._identity.category_code or type(c).__name__ + type_name = type(c).__name__ + num_params = len(c.parameters) + rows.append([code, type_name, str(num_params)]) + render_table( + columns_headers=['Category', 'Type', '# Parameters'], + columns_alignment=['left', 'left', 'right'], + columns_data=rows, + ) + + +# ====================================================================== + class DatablockCollection(CollectionBase): - """Handles top-level category collections (e.g. SampleModels, - Experiments). + """ + Collection of top-level datablocks (e.g. Structures, Experiments). Each item is a DatablockItem. + + Subclasses provide explicit ``add_from_*`` convenience methods that + delegate to the corresponding factory classmethods, then call + :meth:`add` with the resulting item. """ + def _key_for(self, item: object) -> str | None: + """Return the datablock-level identity key for *item*.""" + return item._identity.datablock_entry_name + + def add(self, item: object) -> None: + """ + Add a pre-built item to the collection. + + Parameters + ---------- + item : object + A ``DatablockItem`` instance (e.g. a ``Structure`` or + ``ExperimentBase`` subclass). + """ + self[item._identity.datablock_entry_name] = item + def __str__(self) -> str: """Human-readable representation of this component.""" name = self._log_name @@ -94,25 +151,26 @@ def __str__(self) -> str: return f'<{name} collection ({size} items)>' @property - def unique_name(self): + def unique_name(self) -> str | None: + """Return None; collections have no unique name.""" return None @property - def parameters(self): + def parameters(self) -> list: """All parameters from all datablocks in this collection.""" params = [] for db in self._items: params.extend(db.parameters) return params - # was in class AbstractDatablock(ABC): @property def fittable_parameters(self) -> list: + """All non-constrained Parameters in this collection.""" return [p for p in self.parameters if isinstance(p, Parameter) and not p.constrained] - # was in class AbstractDatablock(ABC): @property def free_parameters(self) -> list: + """All fittable parameters that are currently marked as free.""" return [p for p in self.fittable_parameters if p.free] @property @@ -121,8 +179,3 @@ def as_cif(self) -> str: from easydiffraction.io.cif.serialize import datablock_collection_to_cif return datablock_collection_to_cif(self) - - @typechecked - def _add(self, item) -> None: - """Add an item to the collection.""" - self[item._identity.datablock_entry_name] = item diff --git a/src/easydiffraction/core/diagnostic.py b/src/easydiffraction/core/diagnostic.py index 74c2ab3b..dc5ea4c4 100644 --- a/src/easydiffraction/core/diagnostic.py +++ b/src/easydiffraction/core/diagnostic.py @@ -1,6 +1,7 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -"""Diagnostics helpers for logging validation messages. +""" +Diagnostics helpers for logging validation messages. This module centralizes human-friendly error and debug logs for attribute validation and configuration checks. @@ -19,8 +20,9 @@ class Diagnostics: # ============================================================== @staticmethod - def type_override_error(cls_name: str, expected, got): - """Report an invalid DataTypes override. + def type_override_error(cls_name: str, expected: object, got: object) -> None: + """ + Report an invalid DataTypes override. Used when descriptor and AttributeSpec types conflict. """ @@ -41,7 +43,7 @@ def type_override_error(cls_name: str, expected, got): def readonly_error( name: str, key: str | None = None, - ): + ) -> None: """Log an attempt to change a read-only attribute.""" Diagnostics._log_error( f"Cannot modify read-only attribute '{key}' of <{name}>.", @@ -53,11 +55,9 @@ def attr_error( name: str, key: str, allowed: set[str], - label='Allowed', - ): - """Log access to an unknown attribute and suggest closest - key. - """ + label: str = 'Allowed', + ) -> None: + """Log unknown attribute access and suggest closest key.""" suggestion = Diagnostics._build_suggestion(key, allowed) # Use consistent (label) logic for allowed hint = suggestion or Diagnostics._build_allowed(allowed, label=label) @@ -73,11 +73,11 @@ def attr_error( @staticmethod def type_mismatch( name: str, - value, - expected_type, - current=None, - default=None, - ): + value: object, + expected_type: object, + current: object = None, + default: object = None, + ) -> None: """Log a type mismatch and keep current or default value.""" got_type = type(value).__name__ msg = ( @@ -91,12 +91,12 @@ def type_mismatch( @staticmethod def range_mismatch( name: str, - value, - ge, - le, - current=None, - default=None, - ): + value: object, + ge: float, + le: float, + current: object = None, + default: object = None, + ) -> None: """Log range violation for a numeric value.""" msg = f'Value mismatch for <{name}>. Provided {value!r} outside [{ge}, {le}].' Diagnostics._log_error_with_fallback( @@ -106,11 +106,11 @@ def range_mismatch( @staticmethod def choice_mismatch( name: str, - value, - allowed, - current=None, - default=None, - ): + value: object, + allowed: object, + current: object = None, + default: object = None, + ) -> None: """Log an invalid choice against allowed values.""" msg = f'Value mismatch for <{name}>. Provided {value!r} is unknown.' if allowed is not None: @@ -122,11 +122,11 @@ def choice_mismatch( @staticmethod def regex_mismatch( name: str, - value, - pattern, - current=None, - default=None, - ): + value: object, + pattern: str, + current: object = None, + default: object = None, + ) -> None: """Log a regex mismatch with the expected pattern.""" msg = ( f"Value mismatch for <{name}>. Provided {value!r} does not match pattern '{pattern}'." @@ -136,24 +136,24 @@ def regex_mismatch( ) @staticmethod - def no_value(name, default): + def no_value(name: str, default: object) -> None: """Log that default will be used due to missing value.""" Diagnostics._log_debug(f'No value provided for <{name}>. Using default {default!r}.') @staticmethod - def none_value(name): + def none_value(name: str) -> None: """Log explicit None provided by a user.""" Diagnostics._log_debug(f'Using `None` explicitly provided for <{name}>.') @staticmethod - def none_value_skip_range(name): + def none_value_skip_range(name: str) -> None: """Log that range validation is skipped due to None.""" Diagnostics._log_debug( f'Skipping range validation as `None` is explicitly provided for <{name}>.' ) @staticmethod - def validated(name, value, stage: str | None = None): + def validated(name: str, value: object, stage: str | None = None) -> None: """Log that a value passed a validation stage.""" stage_info = f' {stage}' if stage else '' Diagnostics._log_debug(f'Value {value!r} for <{name}> passed{stage_info} validation.') @@ -163,17 +163,17 @@ def validated(name, value, stage: str | None = None): # ============================================================== @staticmethod - def _log_error(msg, exc_type=Exception): + def _log_error(msg: str, exc_type: type[Exception] = Exception) -> None: """Emit an error-level message via shared logger.""" log.error(msg, exc_type=exc_type) @staticmethod def _log_error_with_fallback( - msg, - current=None, - default=None, - exc_type=Exception, - ): + msg: str, + current: object = None, + default: object = None, + exc_type: type[Exception] = Exception, + ) -> None: """Emit an error message and mention kept or default value.""" if current is not None: msg += f' Keeping current {current!r}.' @@ -182,7 +182,7 @@ def _log_error_with_fallback( log.error(msg, exc_type=exc_type) @staticmethod - def _log_debug(msg): + def _log_debug(msg: str) -> None: """Emit a debug-level message via shared logger.""" log.debug(msg) @@ -191,7 +191,7 @@ def _log_debug(msg): # ============================================================== @staticmethod - def _suggest(key: str, allowed: set[str]): + def _suggest(key: str, allowed: set[str]) -> str | None: """Suggest closest allowed key using string similarity.""" if not allowed: return None @@ -200,12 +200,12 @@ def _suggest(key: str, allowed: set[str]): return matches[0] if matches else None @staticmethod - def _build_suggestion(key: str, allowed: set[str]): + def _build_suggestion(key: str, allowed: set[str]) -> str: s = Diagnostics._suggest(key, allowed) return f" Did you mean '{s}'?" if s else '' @staticmethod - def _build_allowed(allowed, label='Allowed attributes'): + def _build_allowed(allowed: object, label: str = 'Allowed attributes') -> str: # allowed may be a set, list, or other iterable if allowed: allowed_list = list(allowed) diff --git a/src/easydiffraction/core/factory.py b/src/easydiffraction/core/factory.py index 3500768f..af99217a 100644 --- a/src/easydiffraction/core/factory.py +++ b/src/easydiffraction/core/factory.py @@ -1,36 +1,271 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +""" +Base factory with registration, lookup, and context-dependent defaults. -from typing import Iterable -from typing import Mapping +Concrete factories inherit from ``FactoryBase`` and only need to define +``_default_rules``. +""" + +from __future__ import annotations + +from typing import Any +from typing import Dict +from typing import FrozenSet +from typing import List +from typing import Tuple +from typing import Type + +from easydiffraction.utils.logging import console +from easydiffraction.utils.utils import render_table class FactoryBase: - """Reusable argument validation mixin.""" + """ + Shared base for all factories. + + Subclasses must set: + + * ``_default_rules`` -- mapping of ``frozenset`` conditions to tag + strings. Use ``frozenset(): 'tag'`` for a universal default. + + The ``__init_subclass__`` hook ensures every subclass gets its own + independent ``_registry`` list. + """ + + _registry: List[Type] = [] + _default_rules: Dict[FrozenSet[Tuple[str, Any]], str] = {} + + def __init_subclass__(cls, **kwargs: object) -> None: + """Give each subclass its own independent registry and rules.""" + super().__init_subclass__(**kwargs) + cls._registry = [] + if '_default_rules' not in cls.__dict__: + cls._default_rules = {} + + # ------------------------------------------------------------------ + # Registration + # ------------------------------------------------------------------ + + @classmethod + def register(cls, klass: type) -> type: + """ + Class decorator to register a concrete class. + + Usage:: + + @SomeFactory.register class MyClass(SomeBase): type_info = + TypeInfo(...) + + Returns the class unmodified. + """ + cls._registry.append(klass) + return klass + + # ------------------------------------------------------------------ + # Supported-map helpers + # ------------------------------------------------------------------ + + @classmethod + def _supported_map(cls) -> Dict[str, Type]: + """Build ``{tag: class}`` from all registered classes.""" + return {klass.type_info.tag: klass for klass in cls._registry} + + @classmethod + def supported_tags(cls) -> List[str]: + """Return list of all supported tags.""" + return list(cls._supported_map().keys()) + + # ------------------------------------------------------------------ + # Default resolution + # ------------------------------------------------------------------ + + @classmethod + def default_tag(cls, **conditions: object) -> str: + """ + Resolve the default tag for a given experimental context. + + Uses *largest-subset matching*: the rule whose key is the + biggest subset of the given conditions wins. A rule with an + empty key (``frozenset()``) acts as a universal fallback. + + Parameters + ---------- + **conditions : object + Experimental-axis values, e.g. + ``scattering_type=ScatteringTypeEnum.BRAGG``. - @staticmethod - def _validate_args( - present: set[str], - allowed_specs: Iterable[Mapping[str, Iterable[str]]], - factory_name: str, + Returns + ------- + str + The resolved default tag string. + + Raises + ------ + ValueError + If no rule matches the given conditions. + """ + condition_set = frozenset(conditions.items()) + best_match_tag: str | None = None + best_match_size = -1 + + for rule_key, rule_tag in cls._default_rules.items(): + if rule_key <= condition_set and len(rule_key) > best_match_size: + best_match_tag = rule_tag + best_match_size = len(rule_key) + + if best_match_tag is None: + raise ValueError( + f'No default rule matches conditions {dict(conditions)}. ' + f'Available rules: {cls._default_rules}' + ) + return best_match_tag + + # ------------------------------------------------------------------ + # Creation + # ------------------------------------------------------------------ + + @classmethod + def create(cls, tag: str, **kwargs: object) -> object: + """ + Instantiate a registered class by *tag*. + + Parameters + ---------- + tag : str + ``type_info.tag`` value. + **kwargs : object + Forwarded to the class constructor. + + Returns + ------- + object + A new instance of the registered class. + + Raises + ------ + ValueError + If *tag* is not in the registry. + """ + supported = cls._supported_map() + if tag not in supported: + raise ValueError(f"Unsupported type: '{tag}'. Supported: {list(supported.keys())}") + return supported[tag](**kwargs) + + @classmethod + def create_default_for(cls, **conditions: object) -> object: + """ + Instantiate the default class for a given context. + + Combines ``default_tag(**conditions)`` with ``create(tag)``. + + Parameters + ---------- + **conditions : object + Experimental-axis values. + + Returns + ------- + object + A new instance of the default class. + """ + tag = cls.default_tag(**conditions) + return cls.create(tag) + + # ------------------------------------------------------------------ + # Querying + # ------------------------------------------------------------------ + + @classmethod + def supported_for( + cls, + *, + calculator: object = None, + sample_form: object = None, + scattering_type: object = None, + beam_mode: object = None, + radiation_probe: object = None, + ) -> List[Type]: + """ + Return classes matching conditions and/or calculator. + + Parameters + ---------- + calculator : object, default=None + Optional ``CalculatorEnum`` value. + sample_form : object, default=None + Optional ``SampleFormEnum`` value. + scattering_type : object, default=None + Optional ``ScatteringTypeEnum`` value. + beam_mode : object, default=None + Optional ``BeamModeEnum`` value. + radiation_probe : object, default=None + Optional ``RadiationProbeEnum`` value. + + Returns + ------- + List[Type] + Classes matching the given conditions. + """ + result = [] + for klass in cls._supported_map().values(): + compat = getattr(klass, 'compatibility', None) + if compat and not compat.supports( + sample_form=sample_form, + scattering_type=scattering_type, + beam_mode=beam_mode, + radiation_probe=radiation_probe, + ): + continue + calc_support = getattr(klass, 'calculator_support', None) + if calculator and calc_support and not calc_support.supports(calculator): + continue + result.append(klass) + return result + + # ------------------------------------------------------------------ + # Display + # ------------------------------------------------------------------ + + @classmethod + def show_supported( + cls, + *, + calculator: object = None, + sample_form: object = None, + scattering_type: object = None, + beam_mode: object = None, + radiation_probe: object = None, ) -> None: - """Validate provided arguments against allowed combinations.""" - for spec in allowed_specs: - required = set(spec.get('required', [])) - optional = set(spec.get('optional', [])) - if required.issubset(present) and present <= (required | optional): - return # valid combo - # build readable error message - combos = [] - for spec in allowed_specs: - req = ', '.join(spec.get('required', [])) - opt = ', '.join(spec.get('optional', [])) - if opt: - combos.append(f'({req}[, {opt}])') - else: - combos.append(f'({req})') - raise ValueError( - f'Invalid argument combination for {factory_name} creation.\n' - f'Provided: {sorted(present)}\n' - f'Allowed combinations:\n ' + '\n '.join(combos) + """ + Pretty-print a table of supported types. + + Parameters + ---------- + calculator : object, default=None + Optional ``CalculatorEnum`` filter. + sample_form : object, default=None + Optional ``SampleFormEnum`` filter. + scattering_type : object, default=None + Optional ``ScatteringTypeEnum`` filter. + beam_mode : object, default=None + Optional ``BeamModeEnum`` filter. + radiation_probe : object, default=None + Optional ``RadiationProbeEnum`` filter. + """ + matching = cls.supported_for( + calculator=calculator, + sample_form=sample_form, + scattering_type=scattering_type, + beam_mode=beam_mode, + radiation_probe=radiation_probe, + ) + columns_headers = ['Type', 'Description'] + columns_alignment = ['left', 'left'] + columns_data = [[klass.type_info.tag, klass.type_info.description] for klass in matching] + console.paragraph('Supported types') + render_table( + columns_headers=columns_headers, + columns_alignment=columns_alignment, + columns_data=columns_data, ) diff --git a/src/easydiffraction/core/guard.py b/src/easydiffraction/core/guard.py index 0979f4cd..17b60fb5 100644 --- a/src/easydiffraction/core/guard.py +++ b/src/easydiffraction/core/guard.py @@ -1,30 +1,35 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +from __future__ import annotations + from abc import ABC from abc import abstractmethod +from typing import Generator from easydiffraction.core.diagnostic import Diagnostics from easydiffraction.core.identity import Identity class GuardedBase(ABC): - """Base class enforcing controlled attribute access and parent - linkage. - """ + """Base class enforcing controlled attribute access and linkage.""" _diagnoser = Diagnostics() - def __init__(self): + def __init__(self) -> None: + super().__init__() self._identity = Identity(owner=self) def __str__(self) -> str: + """Return the string representation of this object.""" return f'<{self.unique_name}>' def __repr__(self) -> str: + """Return the developer representation of this object.""" return self.__str__() - def __getattr__(self, key: str): + def __getattr__(self, key: str) -> None: + """Raise a descriptive error for unknown attribute access.""" cls = type(self) allowed = cls._public_attrs() if key not in allowed: @@ -35,7 +40,8 @@ def __getattr__(self, key: str): label='Allowed readable/writable', ) - def __setattr__(self, key: str, value): + def __setattr__(self, key: str, value: object) -> None: + """Set an attribute with access-control diagnostics.""" # Always allow private or special attributes without diagnostics if key.startswith('_'): object.__setattr__(self, key, value) @@ -67,20 +73,21 @@ def __setattr__(self, key: str, value): self._assign_attr(key, value) - def _assign_attr(self, key, value): + def _assign_attr(self, key: str, value: object) -> None: """Low-level assignment with parent linkage.""" object.__setattr__(self, key, value) if key != '_parent' and isinstance(value, GuardedBase): object.__setattr__(value, '_parent', self) @classmethod - def _iter_properties(cls): - """Iterate over all public properties defined in the class - hierarchy. + def _iter_properties(cls) -> Generator[tuple[str, property], None, None]: + """ + Iterate over all public properties in the class hierarchy. - Yields: - tuple[str, property]: Each (key, property) pair for public - attributes. + Yields + ------ + tuple[str, property] + Each (key, property) pair for public attributes. """ for base in cls.mro(): for key, attr in base.__dict__.items(): @@ -89,12 +96,12 @@ def _iter_properties(cls): yield key, attr @classmethod - def _public_attrs(cls): + def _public_attrs(cls) -> set[str]: """All public properties (read-only + writable).""" return {key for key, _ in cls._iter_properties()} @classmethod - def _public_readonly_attrs(cls): + def _public_readonly_attrs(cls) -> set[str]: """Public properties without a setter.""" return {key for key, prop in cls._iter_properties() if prop.fset is None} @@ -103,18 +110,19 @@ def _public_writable_attrs(cls) -> set[str]: """Public properties with a setter.""" return {key for key, prop in cls._iter_properties() if prop.fset is not None} - def _allowed_attrs(self, writable_only=False): + def _allowed_attrs(self, writable_only: bool = False) -> set[str]: cls = type(self) if writable_only: return cls._public_writable_attrs() return cls._public_attrs() @property - def _log_name(self): + def _log_name(self) -> str: return self.unique_name or type(self).__name__ @property - def unique_name(self): + def unique_name(self) -> str: + """Fallback unique name: the class name.""" return type(self).__name__ # @property @@ -128,16 +136,92 @@ def unique_name(self): @property @abstractmethod - def parameters(self): - """Return a list of parameter objects (to be implemented by - subclasses). - """ + def parameters(self) -> list: + """Return a list of parameters (implemented by subclasses).""" raise NotImplementedError @property @abstractmethod def as_cif(self) -> str: - """Return CIF representation of this object (to be implemented - by subclasses). - """ + """Return CIF representation (implemented by subclasses).""" raise NotImplementedError + + @staticmethod + def _first_sentence(docstring: str | None) -> str: + """ + Extract the first paragraph from a docstring. + + Returns text before the first blank line, with continuation + lines joined into a single string. + """ + if not docstring: + return '' + first_para = docstring.strip().split('\n\n')[0] + return ' '.join(line.strip() for line in first_para.splitlines()) + + @classmethod + def _iter_methods(cls) -> Generator[tuple[str, object], None, None]: + """ + Iterate over public methods in the class hierarchy. + + Yields + ------ + tuple[str, object] + Each (name, function) pair. + """ + seen: set = set() + for base in cls.mro(): + for key, attr in base.__dict__.items(): + if key.startswith('_') or key in seen: + continue + if isinstance(attr, property): + continue + raw = attr + if isinstance(raw, (staticmethod, classmethod)): + raw = raw.__func__ + if callable(raw): + seen.add(key) + yield key, raw + + def help(self) -> None: + """Print a summary of public properties and methods.""" + from easydiffraction.utils.logging import console + from easydiffraction.utils.utils import render_table + + cls = type(self) + console.paragraph(f"Help for '{cls.__name__}'") + + # Deduplicate (MRO may yield the same name) + seen: dict = {} + for key, prop in cls._iter_properties(): + if key not in seen: + seen[key] = prop + + prop_rows = [] + for i, key in enumerate(sorted(seen), 1): + prop = seen[key] + writable = '✓' if prop.fset else '✗' + doc = self._first_sentence(prop.fget.__doc__ if prop.fget else None) + prop_rows.append([str(i), key, writable, doc]) + + if prop_rows: + console.paragraph('Properties') + render_table( + columns_headers=['#', 'Name', 'Writable', 'Description'], + columns_alignment=['right', 'left', 'center', 'left'], + columns_data=prop_rows, + ) + + methods = dict(cls._iter_methods()) + method_rows = [] + for i, key in enumerate(sorted(methods), 1): + doc = self._first_sentence(getattr(methods[key], '__doc__', None)) + method_rows.append([str(i), f'{key}()', doc]) + + if method_rows: + console.paragraph('Methods') + render_table( + columns_headers=['#', 'Name', 'Description'], + columns_alignment=['right', 'left', 'left'], + columns_data=method_rows, + ) diff --git a/src/easydiffraction/core/identity.py b/src/easydiffraction/core/identity.py index 5848bf18..d64fce81 100644 --- a/src/easydiffraction/core/identity.py +++ b/src/easydiffraction/core/identity.py @@ -1,6 +1,7 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -"""Identity helpers to build CIF-like hierarchical names. +""" +Identity helpers to build CIF-like hierarchical names. Used by containers and items to expose datablock/category/entry names without tight coupling. @@ -19,13 +20,13 @@ def __init__( datablock_entry: Callable | None = None, category_code: str | None = None, category_entry: Callable | None = None, - ): + ) -> None: self._owner = owner self._datablock_entry = datablock_entry self._category_code = category_code self._category_entry = category_entry - def _resolve_up(self, attr: str, visited=None): + def _resolve_up(self, attr: str, visited: set[int] | None = None) -> str | None: """Resolve attribute by walking up parent chain safely.""" if visited is None: visited = set() @@ -47,31 +48,31 @@ def _resolve_up(self, attr: str, visited=None): return None @property - def datablock_entry_name(self): + def datablock_entry_name(self) -> str | None: """Datablock entry name or None if not set.""" return self._resolve_up('datablock_entry') @datablock_entry_name.setter - def datablock_entry_name(self, func: callable): + def datablock_entry_name(self, func: callable) -> None: """Set callable returning datablock entry name.""" self._datablock_entry = func @property - def category_code(self): + def category_code(self) -> str | None: """Category code like 'atom_site' or 'background'.""" return self._resolve_up('category_code') @category_code.setter - def category_code(self, value: str): + def category_code(self, value: str) -> None: """Set category code value.""" self._category_code = value @property - def category_entry_name(self): + def category_entry_name(self) -> str | None: """Category entry name or None if not set.""" return self._resolve_up('category_entry') @category_entry_name.setter - def category_entry_name(self, func: callable): + def category_entry_name(self, func: callable) -> None: """Set callable returning category entry name.""" self._category_entry = func diff --git a/src/easydiffraction/core/metadata.py b/src/easydiffraction/core/metadata.py new file mode 100644 index 00000000..318d64bb --- /dev/null +++ b/src/easydiffraction/core/metadata.py @@ -0,0 +1,119 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +""" +Metadata dataclasses for factory-created classes. + +Three frozen dataclasses describe a concrete class: + +- ``TypeInfo`` — stable tag and human-readable description. - +``Compatibility`` — experimental conditions (multiple fields). - +``CalculatorSupport`` — which calculation engines can handle it. +""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import FrozenSet + + +@dataclass(frozen=True) +class TypeInfo: + """ + Stable identity and description for a factory-created class. + + Attributes + ---------- + tag : str + Short, stable string identifier used for serialization, + user-facing selection, and factory lookup. Must be unique within + a factory's registry. Examples: ``'line-segment'``, + ``'pseudo-voigt'``, ``'cryspy'``. + description : str, default='' + One-line human-readable explanation. Used in + ``show_supported()`` tables and documentation. + """ + + tag: str + description: str = '' + + +@dataclass(frozen=True) +class Compatibility: + """ + Experimental conditions under which a class can be used. + + Each field is a frozenset of enum values representing the set of + supported values for that axis. An empty frozenset means + "compatible with any value of this axis" (i.e. no restriction). + """ + + sample_form: FrozenSet = frozenset() + scattering_type: FrozenSet = frozenset() + beam_mode: FrozenSet = frozenset() + radiation_probe: FrozenSet = frozenset() + + def supports( + self, + sample_form: object = None, + scattering_type: object = None, + beam_mode: object = None, + radiation_probe: object = None, + ) -> bool: + """ + Check if this compatibility matches the given conditions. + + Each argument is an optional enum member. Returns ``True`` if + every provided value is in the corresponding frozenset (or the + frozenset is empty, meaning *any*). + + Example:: + + compat.supports( scattering_type=ScatteringTypeEnum.BRAGG, + beam_mode=BeamModeEnum.CONSTANT_WAVELENGTH, ) + """ + for axis, value in ( + ('sample_form', sample_form), + ('scattering_type', scattering_type), + ('beam_mode', beam_mode), + ('radiation_probe', radiation_probe), + ): + if value is None: + continue + allowed = getattr(self, axis) + if allowed and value not in allowed: + return False + return True + + +@dataclass(frozen=True) +class CalculatorSupport: + """ + Which calculation engines can handle this class. + + Attributes + ---------- + calculators : FrozenSet, default=frozenset() + Frozenset of ``CalculatorEnum`` values. Empty means "any + calculator" (no restriction). + """ + + calculators: FrozenSet = frozenset() + + def supports(self, calculator: object) -> bool: + """ + Check if a specific calculator can handle this class. + + Parameters + ---------- + calculator : object + A ``CalculatorEnum`` value. + + Returns + ------- + bool + ``True`` if the calculator is in the set, or if the set is + empty (meaning any calculator is accepted). + """ + if not self.calculators: + return True + return calculator in self.calculators diff --git a/src/easydiffraction/core/singletons.py b/src/easydiffraction/core/singleton.py similarity index 71% rename from src/easydiffraction/core/singletons.py rename to src/easydiffraction/core/singleton.py index 2cd553c1..d40e62bc 100644 --- a/src/easydiffraction/core/singletons.py +++ b/src/easydiffraction/core/singleton.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause from typing import Any @@ -12,9 +12,12 @@ T = TypeVar('T', bound='SingletonBase') +# ====================================================================== + class SingletonBase: - """Base class to implement Singleton pattern. + """ + Base class to implement Singleton pattern. Ensures only one shared instance of a class is ever created. Useful for managing shared state across the library. @@ -24,12 +27,15 @@ class SingletonBase: @classmethod def get(cls: Type[T]) -> T: - """Returns the shared instance, creating it if needed.""" + """Return the shared instance, creating it if needed.""" if cls._instance is None: cls._instance = cls() return cls._instance +# ====================================================================== + + class UidMapHandler(SingletonBase): """Global handler to manage UID-to-Parameter object mapping.""" @@ -38,16 +44,17 @@ def __init__(self) -> None: self._uid_map: Dict[str, Any] = {} def get_uid_map(self) -> Dict[str, Any]: - """Returns the current UID-to-Parameter map.""" + """Return the current UID-to-Parameter map.""" return self._uid_map - def add_to_uid_map(self, parameter): - """Adds a single Parameter or Descriptor object to the UID map. + def add_to_uid_map(self, parameter: object) -> None: + """ + Add a single Parameter or Descriptor object to the UID map. Only Descriptor or Parameter instances are allowed (not Components or others). """ - from easydiffraction.core.parameters import GenericDescriptorBase + from easydiffraction.core.variable import GenericDescriptorBase if not isinstance(parameter, GenericDescriptorBase): raise TypeError( @@ -56,8 +63,9 @@ def add_to_uid_map(self, parameter): ) self._uid_map[parameter.uid] = parameter - def replace_uid(self, old_uid, new_uid): - """Replaces an existing UID key in the UID map with a new UID. + def replace_uid(self, old_uid: str, new_uid: str) -> None: + """ + Replace an existing UID key in the UID map with a new UID. Moves the associated parameter from old_uid to new_uid. Raises a KeyError if the old_uid doesn't exist. @@ -71,11 +79,14 @@ def replace_uid(self, old_uid, new_uid): # TODO: Implement removing from the UID map +# ====================================================================== + + # TODO: Implement changing atrr '.constrained' back to False # when removing constraints class ConstraintsHandler(SingletonBase): - """Manages user-defined parameter constraints using aliases and - expressions. + """ + Manage parameter constraints using aliases and expressions. Uses the asteval interpreter for safe evaluation of mathematical expressions. Constraints are defined as: lhs_alias = @@ -94,46 +105,43 @@ def __init__(self) -> None: # Internally parsed constraints as (lhs_alias, rhs_expr) tuples self._parsed_constraints: List[Tuple[str, str]] = [] - def set_aliases(self, aliases): - """Sets the alias map (name → parameter wrapper). + def set_aliases(self, aliases: object) -> None: + """ + Set the alias map (name → parameter wrapper). Called when user registers parameter aliases like: - alias='biso_La', param=model.atom_sites['La'].b_iso + alias='biso_La', param=model.atom_sites['La'].b_iso """ self._alias_to_param = dict(aliases.items()) - def set_constraints(self, constraints): - """Sets the constraints and triggers parsing into internal - format. + def set_constraints(self, constraints: object) -> None: + """ + Set the constraints and triggers parsing into internal format. - Called when user registers expressions like: - lhs_alias='occ_Ba', rhs_expr='1 - occ_La' + Called when user registers expressions like: lhs_alias='occ_Ba', + rhs_expr='1 - occ_La' """ self._constraints = constraints._items self._parse_constraints() def _parse_constraints(self) -> None: - """Converts raw expression input into a normalized internal list - of (lhs_alias, rhs_expr) pairs, stripping whitespace and - skipping invalid entries. - """ + """Parse raw expressions into (lhs_alias, rhs_expr) pairs.""" self._parsed_constraints = [] for expr_obj in self._constraints: - lhs_alias = expr_obj.lhs_alias.value - rhs_expr = expr_obj.rhs_expr.value + lhs_alias = expr_obj.lhs_alias + rhs_expr = expr_obj.rhs_expr if lhs_alias and rhs_expr: constraint = (lhs_alias.strip(), rhs_expr.strip()) self._parsed_constraints.append(constraint) def apply(self) -> None: - """Evaluates constraints and applies them to dependent - parameters. + """ + Evaluate constraints and applies them to dependent parameters. - For each constraint: - - Evaluate RHS using current values of aliases - - Locate the dependent parameter by alias → uid → param + For each constraint: - Evaluate RHS using current values of + aliases - Locate the dependent parameter by alias → uid → param - Update its value and mark it as constrained """ if not self._parsed_constraints: @@ -164,8 +172,7 @@ def apply(self) -> None: param = uid_map[dependent_uid] # Update its value and mark it as constrained - param._value = rhs_value # To bypass ranges check - param._constrained = True # To bypass read-only check + param._set_value_constrained(rhs_value) except Exception as error: print(f"Failed to apply constraint '{lhs_alias} = {rhs_expr}': {error}") diff --git a/src/easydiffraction/core/validation.py b/src/easydiffraction/core/validation.py index 5e31c485..d3c411af 100644 --- a/src/easydiffraction/core/validation.py +++ b/src/easydiffraction/core/validation.py @@ -1,12 +1,12 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -"""Lightweight runtime validation utilities. +""" +Lightweight runtime validation utilities. Provides DataTypes, type/content validators, and AttributeSpec used by descriptors and parameters. Only documentation was added here. """ -import functools import re from abc import ABC from abc import abstractmethod @@ -14,73 +14,47 @@ from enum import auto import numpy as np -from typeguard import TypeCheckError -from typeguard import typechecked from easydiffraction.core.diagnostic import Diagnostics -from easydiffraction.utils.logging import log -# ============================================================== +# ====================================================================== # Shared constants -# ============================================================== +# ====================================================================== + + +# TODO: MkDocs doesn't unpack types +class DataTypeHints: + """Type hint aliases for numeric, string, and boolean types.""" + + Numeric = int | float | np.integer | np.floating + String = str + Bool = bool + + +# ====================================================================== class DataTypes(Enum): - NUMERIC = (int, float, np.integer, np.floating, np.number) + """Enumeration of supported data types for descriptors.""" + + NUMERIC = (int, float, np.integer, np.floating) STRING = (str,) BOOL = (bool,) ANY = (object,) # fallback for unconstrained - def __str__(self): + def __str__(self) -> str: + """Return the lowercase name of the data type.""" return self.name.lower() @property - def expected_type(self): + def expected_type(self) -> tuple: """Convenience alias for tuple of allowed Python types.""" return self.value -# ============================================================== -# Runtime type checking decorator -# ============================================================== - -# Runtime type checking decorator for validating those methods -# annotated with type hints, which are writable for the user, and -# which are not covered by custom validators for Parameter attribute -# types and content, implemented below. - - -def checktype(func=None, *, context=None): - """Runtime type check decorator using typeguard. - - When a TypeCheckError occurs, the error is logged and None is - returned. If context is provided, it is added to the message. - """ - - def decorator(f): - checked_func = typechecked(f) - - @functools.wraps(f) - def wrapper(*args, **kwargs): - try: - return checked_func(*args, **kwargs) - except TypeCheckError as err: - msg = str(err) - if context: - msg = f'{context}: {msg}' - log.error(message=msg, exc_type=TypeError) - return None - - return wrapper - - if func is None: - return decorator - return decorator(func) - - -# ============================================================== +# ====================================================================== # Validation stages (enum/constant) -# ============================================================== +# ====================================================================== class ValidationStage(Enum): @@ -91,21 +65,29 @@ class ValidationStage(Enum): MEMBERSHIP = auto() REGEX = auto() - def __str__(self): + def __str__(self) -> str: + """Return the lowercase name of the validation stage.""" return self.name.lower() -# ============================================================== +# ====================================================================== # Advanced runtime custom validators for Parameter types/content -# ============================================================== +# ====================================================================== class ValidatorBase(ABC): """Abstract base class for all validators.""" @abstractmethod - def validated(self, value, name, default=None, current=None): - """Return a validated value or fallback. + def validated( + self, + value: object, + name: str, + default: object = None, + current: object = None, + ) -> object: + """ + Return a validated value or fallback. Subclasses must implement this method. """ @@ -113,17 +95,20 @@ def validated(self, value, name, default=None, current=None): def _fallback( self, - current=None, - default=None, - ): + current: object = None, + default: object = None, + ) -> object: """Return current if set, else default.""" return current if current is not None else default +# ====================================================================== + + class TypeValidator(ValidatorBase): - """Ensure a value is of the expected Python type.""" + """Ensure a value is of the expected data type.""" - def __init__(self, expected_type: DataTypes): + def __init__(self, expected_type: DataTypes) -> None: if isinstance(expected_type, DataTypes): self.expected_type = expected_type self.expected_label = str(expected_type) @@ -132,13 +117,14 @@ def __init__(self, expected_type: DataTypes): def validated( self, - value, - name, - default=None, - current=None, - allow_none=False, - ): - """Validate type and return value or fallback. + value: object, + name: str, + default: object = None, + current: object = None, + allow_none: bool = False, + ) -> object: + """ + Validate type and return value or fallback. If allow_none is True, None bypasses content checks. """ @@ -171,24 +157,27 @@ def validated( return value +# ====================================================================== + + class RangeValidator(ValidatorBase): """Ensure a numeric value lies within [ge, le].""" def __init__( self, *, - ge=-np.inf, - le=np.inf, - ): + ge: float = -np.inf, + le: float = np.inf, + ) -> None: self.ge, self.le = ge, le def validated( self, - value, - name, - default=None, - current=None, - ): + value: object, + name: str, + default: object = None, + current: object = None, + ) -> object: """Validate range and return value or fallback.""" if not (self.ge <= value <= self.le): Diagnostics.range_mismatch( @@ -209,23 +198,27 @@ def validated( return value +# ====================================================================== + + class MembershipValidator(ValidatorBase): - """Ensure that a value is among allowed choices. + """ + Ensure that a value is among allowed choices. - `allowed` may be an iterable or a callable returning a collection. + ``allowed`` may be an iterable or a callable returning a collection. """ - def __init__(self, allowed): + def __init__(self, allowed: object) -> None: # Do not convert immediately to list — may be callable self.allowed = allowed def validated( self, - value, - name, - default=None, - current=None, - ): + value: object, + name: str, + default: object = None, + current: object = None, + ) -> object: """Validate membership and return value or fallback.""" # Dynamically evaluate allowed if callable (e.g. lambda) allowed_values = self.allowed() if callable(self.allowed) else self.allowed @@ -248,19 +241,22 @@ def validated( return value +# ====================================================================== + + class RegexValidator(ValidatorBase): """Ensure that a string matches a given regular expression.""" - def __init__(self, pattern): + def __init__(self, pattern: str) -> None: self.pattern = re.compile(pattern) def validated( self, - value, - name, - default=None, - current=None, - ): + value: object, + name: str, + default: object = None, + current: object = None, + ) -> object: """Validate regex and return value or fallback.""" if not self.pattern.fullmatch(value): Diagnostics.regex_mismatch( @@ -280,9 +276,9 @@ def validated( return value -# ============================================================== +# ====================================================================== # Attribute specification holding metadata and validators -# ============================================================== +# ====================================================================== class AttributeSpec: @@ -291,25 +287,24 @@ class AttributeSpec: def __init__( self, *, - value=None, - type_=None, - default=None, - content_validator=None, + default: object = None, + data_type: DataTypes | None = None, + validator: ValidatorBase | None = None, allow_none: bool = False, - ): - self.value = value + ) -> None: self.default = default self.allow_none = allow_none - self._type_validator = TypeValidator(type_) if type_ else None - self._content_validator = content_validator + self._data_type_validator = TypeValidator(data_type) if data_type else None + self._validator = validator def validated( self, - value, - name, - current=None, - ): - """Validate through type and content validators. + value: object, + name: str, + current: object = None, + ) -> object: + """ + Validate through type and content validators. Returns validated value, possibly default or current if errors occur. None may short-circuit further checks when allowed. @@ -319,8 +314,8 @@ def validated( default = self.default() if callable(self.default) else self.default # Type validation - if self._type_validator: - val = self._type_validator.validated( + if self._data_type_validator: + val = self._data_type_validator.validated( val, name, default=default, @@ -334,8 +329,8 @@ def validated( return None # Content validation - if self._content_validator and val is not None: - val = self._content_validator.validated( + if self._validator and val is not None: + val = self._validator.validated( val, name, default=default, diff --git a/src/easydiffraction/core/parameters.py b/src/easydiffraction/core/variable.py similarity index 59% rename from src/easydiffraction/core/parameters.py rename to src/easydiffraction/core/variable.py index 37b982e0..8e06b2a9 100644 --- a/src/easydiffraction/core/parameters.py +++ b/src/easydiffraction/core/variable.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause from __future__ import annotations @@ -6,13 +6,12 @@ import secrets import string from typing import TYPE_CHECKING -from typing import Any import numpy as np from easydiffraction.core.diagnostic import Diagnostics from easydiffraction.core.guard import GuardedBase -from easydiffraction.core.singletons import UidMapHandler +from easydiffraction.core.singleton import UidMapHandler from easydiffraction.core.validation import AttributeSpec from easydiffraction.core.validation import DataTypes from easydiffraction.core.validation import RangeValidator @@ -23,24 +22,22 @@ if TYPE_CHECKING: from easydiffraction.io.cif.handler import CifHandler +# ====================================================================== + class GenericDescriptorBase(GuardedBase): - """Base class for all parameter-like descriptors. + """ + Base class for all parameter-like descriptors. A descriptor encapsulates a typed value with validation, human-readable name/description and a globally unique identifier that is stable across the session. Concrete subclasses specialize - the expected data type and can extend the public API with - additional behavior (e.g. units). - - Attributes: - name: Local parameter name (e.g. 'a', 'b_iso'). - description: Optional human-readable description. - uid: Stable random identifier for external references. + the expected data type and can extend the public API with additional + behavior (e.g. units). """ _BOOL_SPEC_TEMPLATE = AttributeSpec( - type_=DataTypes.BOOL, + data_type=DataTypes.BOOL, default=False, ) @@ -50,13 +47,18 @@ def __init__( value_spec: AttributeSpec, name: str, description: str = None, - ): - """Initialize the descriptor with validation and identity. - - Args: - value_spec: Validation specification for the value. - name: Local name of the descriptor within its category. - description: Optional human-readable description. + ) -> None: + """ + Initialize the descriptor with validation and identity. + + Parameters + ---------- + value_spec : AttributeSpec + Validation specification for the value. + name : str + Local name of the descriptor within its category. + description : str, default=None + Optional human-readable description. """ super().__init__() @@ -64,8 +66,8 @@ def __init__( if expected_type: user_type = ( - value_spec._type_validator.expected_type - if value_spec._type_validator is not None + value_spec._data_type_validator.expected_type + if value_spec._data_type_validator is not None else None ) if user_type and user_type is not expected_type: @@ -76,19 +78,27 @@ def __init__( ) else: # Enforce descriptor's own type if not already defined - value_spec._type_validator = TypeValidator(expected_type) + value_spec._data_type_validator = TypeValidator(expected_type) self._value_spec = value_spec self._name = name self._description = description # Initial validated states - self._value = self._value_spec.validated( - value_spec.value, - name=self.unique_name, - ) + # self._value = self._value_spec.validated( + # value_spec.value, + # name=self.unique_name, + # ) + + # Assign default directly. + # Skip validation — defaults are trusted. + # Callable is needed for dynamic defaults like SpaceGroup + # it_coordinate_system_code, and similar cases. + default = value_spec.default + self._value = default() if callable(default) else default def __str__(self) -> str: + """Return the string representation of this descriptor.""" return f'<{self.unique_name} = {self.value!r}>' @property @@ -97,11 +107,8 @@ def name(self) -> str: return self._name @property - def unique_name(self): - """Fully qualified name including datablock, category and entry - name. - """ - # 7c: Use filter(None, [...]) + def unique_name(self) -> str: + """Fully qualified name: datablock, category and entry.""" parts = [ self._identity.datablock_entry_name, self._identity.category_code, @@ -110,10 +117,8 @@ def unique_name(self): ] return '.'.join(filter(None, parts)) - def _parent_of_type(self, cls): - """Walk up the parent chain and return the first parent of type - `cls`. - """ + def _parent_of_type(self, cls: type) -> object | None: + """Traverse parents and return the first of type cls.""" obj = getattr(self, '_parent', None) visited = set() while obj is not None and id(obj) not in visited: @@ -123,19 +128,19 @@ def _parent_of_type(self, cls): obj = getattr(obj, '_parent', None) return None - def _datablock_item(self): + def _datablock_item(self) -> object | None: """Return the DatablockItem ancestor, if any.""" from easydiffraction.core.datablock import DatablockItem return self._parent_of_type(DatablockItem) @property - def value(self): + def value(self) -> object: """Current validated value.""" return self._value @value.setter - def value(self, v): + def value(self, v: object) -> None: """Set a new value after validating against the spec.""" # Do nothing if the value is unchanged if self._value == v: @@ -154,14 +159,35 @@ def value(self, v): if parent_datablock is not None: parent_datablock._need_categories_update = True + def _set_value_from_minimizer(self, v: object) -> None: + """ + Set the value from a minimizer, bypassing validation. + + Writes ``_value`` directly — no type or range checks — but still + marks the owning :class:`DatablockItem` dirty so that + ``_update_categories()`` knows work is needed. + + This exists because: + + 1. Physical-range validators (e.g. intensity ≥ 0) would reject + trial values the minimizer needs to explore. 2. Validation + overhead is measurable over thousands of objective-function + evaluations. + """ + self._value = v + parent_datablock = self._datablock_item() + if parent_datablock is not None: + parent_datablock._need_categories_update = True + @property - def description(self): + def description(self) -> str | None: """Optional human-readable description.""" return self._description @property - def parameters(self): - """Return a flat list of parameters contained by this object. + def parameters(self) -> list[GenericDescriptorBase]: + """ + Return a flat list of parameters contained by this object. For a single descriptor, it returns a one-element list with itself. Composite objects override this to flatten nested @@ -174,34 +200,45 @@ def as_cif(self) -> str: """Serialize this descriptor to a CIF-formatted string.""" return param_to_cif(self) - def from_cif(self, block, idx=0): + def from_cif(self, block: object, idx: int = 0) -> None: """Populate this parameter from a CIF block.""" param_from_cif(self, block, idx) +# ====================================================================== + + class GenericStringDescriptor(GenericDescriptorBase): + """Base descriptor that constrains values to strings.""" + _value_type = DataTypes.STRING def __init__( self, - **kwargs: Any, + **kwargs: object, ) -> None: super().__init__(**kwargs) +# ====================================================================== + + class GenericNumericDescriptor(GenericDescriptorBase): + """Base descriptor that constrains values to numbers.""" + _value_type = DataTypes.NUMERIC def __init__( self, *, units: str = '', - **kwargs: Any, + **kwargs: object, ) -> None: super().__init__(**kwargs) self._units: str = units def __str__(self) -> str: + """Return the string representation including units.""" s: str = super().__str__() s = s[1:-1] # strip <> if self.units: @@ -214,8 +251,12 @@ def units(self) -> str: return self._units +# ====================================================================== + + class GenericParameter(GenericNumericDescriptor): - """Numeric descriptor extended with fitting-related attributes. + """ + Numeric descriptor extended with fitting-related attributes. Adds standard attributes used by minimizers: "free" flag, uncertainty, bounds and an optional starting value. Subclasses can @@ -224,24 +265,24 @@ class GenericParameter(GenericNumericDescriptor): def __init__( self, - **kwargs: Any, - ): + **kwargs: object, + ) -> None: super().__init__(**kwargs) # Initial validated states self._free_spec = self._BOOL_SPEC_TEMPLATE self._free = self._free_spec.default self._uncertainty_spec = AttributeSpec( - type_=DataTypes.NUMERIC, - content_validator=RangeValidator(ge=0), + data_type=DataTypes.NUMERIC, + validator=RangeValidator(ge=0), allow_none=True, ) self._uncertainty = self._uncertainty_spec.default - self._fit_min_spec = AttributeSpec(type_=DataTypes.NUMERIC, default=-np.inf) + self._fit_min_spec = AttributeSpec(data_type=DataTypes.NUMERIC, default=-np.inf) self._fit_min = self._fit_min_spec.default - self._fit_max_spec = AttributeSpec(type_=DataTypes.NUMERIC, default=np.inf) + self._fit_max_spec = AttributeSpec(data_type=DataTypes.NUMERIC, default=np.inf) self._fit_max = self._fit_max_spec.default - self._start_value_spec = AttributeSpec(type_=DataTypes.NUMERIC, default=0.0) + self._start_value_spec = AttributeSpec(data_type=DataTypes.NUMERIC, default=0.0) self._start_value = self._start_value_spec.default self._constrained_spec = self._BOOL_SPEC_TEMPLATE self._constrained = self._constrained_spec.default @@ -250,6 +291,7 @@ def __init__( UidMapHandler.get().add_to_uid_map(self) def __str__(self) -> str: + """Return string representation with uncertainty and free.""" s = GenericDescriptorBase.__str__(self) s = s[1:-1] # strip <> if self.uncertainty is not None: @@ -265,136 +307,156 @@ def _generate_uid(length: int = 16) -> str: return ''.join(secrets.choice(letters) for _ in range(length)) @property - def uid(self): + def uid(self) -> str: """Stable random identifier for this descriptor.""" return self._uid @property - def _minimizer_uid(self): + def _minimizer_uid(self) -> str: """Variant of uid that is safe for minimizer engines.""" # return self.unique_name.replace('.', '__') return self.uid @property - def name(self) -> str: - """Local name of the parameter (without category/datablock).""" - return self._name - - @property - def unique_name(self): - """Fully qualified parameter name including its context path.""" - parts = [ - self._identity.datablock_entry_name, - self._identity.category_code, - self._identity.category_entry_name, - self.name, - ] - return '.'.join(filter(None, parts)) - - @property - def constrained(self): + def constrained(self) -> bool: """Whether this parameter is part of a constraint expression.""" return self._constrained + def _set_value_constrained(self, v: object) -> None: + """ + Set the value from a constraint expression. + + Validates against the spec, marks the parent datablock dirty, + and flags the parameter as constrained. Used exclusively by + ``ConstraintsHandler.apply()``. + """ + self.value = v + self._constrained = True + @property - def free(self): + def free(self) -> bool: """Whether this parameter is currently varied during fitting.""" return self._free @free.setter - def free(self, v): + def free(self, v: bool) -> None: """Set the "free" flag after validation.""" self._free = self._free_spec.validated( v, name=f'{self.unique_name}.free', current=self._free ) @property - def uncertainty(self): - """Estimated standard uncertainty of the fitted value, if - available. - """ + def uncertainty(self) -> float | None: + """Estimated standard uncertainty of the fitted value.""" return self._uncertainty @uncertainty.setter - def uncertainty(self, v): + def uncertainty(self, v: float | None) -> None: """Set the uncertainty value (must be non-negative or None).""" self._uncertainty = self._uncertainty_spec.validated( v, name=f'{self.unique_name}.uncertainty', current=self._uncertainty ) @property - def fit_min(self): + def fit_min(self) -> float: """Lower fitting bound.""" return self._fit_min @fit_min.setter - def fit_min(self, v): + def fit_min(self, v: float) -> None: """Set the lower bound for the parameter value.""" self._fit_min = self._fit_min_spec.validated( v, name=f'{self.unique_name}.fit_min', current=self._fit_min ) @property - def fit_max(self): + def fit_max(self) -> float: """Upper fitting bound.""" return self._fit_max @fit_max.setter - def fit_max(self, v): + def fit_max(self, v: float) -> None: """Set the upper bound for the parameter value.""" self._fit_max = self._fit_max_spec.validated( v, name=f'{self.unique_name}.fit_max', current=self._fit_max ) +# ====================================================================== + + class StringDescriptor(GenericStringDescriptor): + """String descriptor bound to a CIF handler.""" + def __init__( self, *, cif_handler: CifHandler, - **kwargs: Any, + **kwargs: object, ) -> None: - """String descriptor bound to a CIF handler. - - Args: - cif_handler: Object that tracks CIF identifiers. - **kwargs: Forwarded to GenericStringDescriptor. + """ + Initialize a string descriptor bound to a CIF handler. + + Parameters + ---------- + cif_handler : CifHandler + Object that tracks CIF identifiers. + **kwargs : object + Forwarded to GenericStringDescriptor. """ super().__init__(**kwargs) self._cif_handler = cif_handler self._cif_handler.attach(self) +# ====================================================================== + + class NumericDescriptor(GenericNumericDescriptor): + """Numeric descriptor bound to a CIF handler.""" + def __init__( self, *, cif_handler: CifHandler, - **kwargs: Any, + **kwargs: object, ) -> None: - """Numeric descriptor bound to a CIF handler. - - Args: - cif_handler: Object that tracks CIF identifiers. - **kwargs: Forwarded to GenericNumericDescriptor. + """ + Numeric descriptor bound to a CIF handler. + + Parameters + ---------- + cif_handler : CifHandler + Object that tracks CIF identifiers. + **kwargs : object + Forwarded to GenericNumericDescriptor. """ super().__init__(**kwargs) self._cif_handler = cif_handler self._cif_handler.attach(self) +# ====================================================================== + + class Parameter(GenericParameter): + """Fittable parameter bound to a CIF handler.""" + def __init__( self, *, cif_handler: CifHandler, - **kwargs: Any, + **kwargs: object, ) -> None: - """Fittable parameter bound to a CIF handler. - - Args: - cif_handler: Object that tracks CIF identifiers. - **kwargs: Forwarded to GenericParameter. + """ + Fittable parameter bound to a CIF handler. + + Parameters + ---------- + cif_handler : CifHandler + Object that tracks CIF identifiers. + **kwargs : object + Forwarded to GenericParameter. """ super().__init__(**kwargs) self._cif_handler = cif_handler diff --git a/src/easydiffraction/crystallography/__init__.py b/src/easydiffraction/crystallography/__init__.py index 429f2648..4e798e20 100644 --- a/src/easydiffraction/crystallography/__init__.py +++ b/src/easydiffraction/crystallography/__init__.py @@ -1,2 +1,2 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause diff --git a/src/easydiffraction/crystallography/crystallography.py b/src/easydiffraction/crystallography/crystallography.py index c7ff6203..bc90383f 100644 --- a/src/easydiffraction/crystallography/crystallography.py +++ b/src/easydiffraction/crystallography/crystallography.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause from typing import Any @@ -21,14 +21,19 @@ def apply_cell_symmetry_constraints( cell: Dict[str, float], name_hm: str, ) -> Dict[str, float]: - """Apply symmetry constraints to unit cell parameters based on space - group. - - Args: - cell: Dictionary containing lattice parameters. - name_hm: Hermann-Mauguin symbol of the space group. - - Returns: + """ + Apply symmetry constraints to unit cell parameters. + + Parameters + ---------- + cell : Dict[str, float] + Dictionary containing lattice parameters. + name_hm : str + Hermann-Mauguin symbol of the space group. + + Returns + ------- + Dict[str, float] The cell dictionary with applied symmetry constraints. """ it_number = get_it_number_by_name_hm_short(name_hm) @@ -90,16 +95,23 @@ def apply_atom_site_symmetry_constraints( coord_code: int, wyckoff_letter: str, ) -> Dict[str, Any]: - """Apply symmetry constraints to atomic coordinates based on site - symmetry. - - Args: - atom_site: Dictionary containing atom position data. - name_hm: Hermann-Mauguin symbol of the space group. - coord_code: Coordinate system code. - wyckoff_letter: Wyckoff position letter. - - Returns: + """ + Apply symmetry constraints to atom site coordinates. + + Parameters + ---------- + atom_site : Dict[str, Any] + Dictionary containing atom position data. + name_hm : str + Hermann-Mauguin symbol of the space group. + coord_code : int + Coordinate system code. + wyckoff_letter : str + Wyckoff position letter. + + Returns + ------- + Dict[str, Any] The atom_site dictionary with applied symmetry constraints. """ it_number = get_it_number_by_name_hm_short(name_hm) diff --git a/src/easydiffraction/crystallography/space_groups.py b/src/easydiffraction/crystallography/space_groups.py index 114b3467..4047b8c5 100644 --- a/src/easydiffraction/crystallography/space_groups.py +++ b/src/easydiffraction/crystallography/space_groups.py @@ -1,6 +1,7 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -"""Space group reference data. +""" +Space group reference data. Loads a gzipped, packaged pickle with crystallographic space-group information. The file is part of the distribution; user input is not @@ -10,11 +11,11 @@ import gzip import pickle # noqa: S403 - trusted internal pickle file (package data only) from pathlib import Path -from typing import Any -def _restricted_pickle_load(file_obj) -> Any: - """Load pickle data from an internal gz file (trusted boundary). +def _restricted_pickle_load(file_obj: object) -> object: + """ + Load pickle data from an internal gz file (trusted boundary). The archive lives in the package; no user-controlled input enters this function. If distribution process changes, revisit. @@ -23,7 +24,7 @@ def _restricted_pickle_load(file_obj) -> Any: return data -def _load(): +def _load() -> object: """Load space-group data from the packaged archive.""" path = Path(__file__).with_name('space_groups.pkl.gz') with gzip.open(path, 'rb') as f: diff --git a/src/easydiffraction/datablocks/__init__.py b/src/easydiffraction/datablocks/__init__.py new file mode 100644 index 00000000..4e798e20 --- /dev/null +++ b/src/easydiffraction/datablocks/__init__.py @@ -0,0 +1,2 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause diff --git a/src/easydiffraction/datablocks/experiment/__init__.py b/src/easydiffraction/datablocks/experiment/__init__.py new file mode 100644 index 00000000..4e798e20 --- /dev/null +++ b/src/easydiffraction/datablocks/experiment/__init__.py @@ -0,0 +1,2 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause diff --git a/src/easydiffraction/datablocks/experiment/categories/__init__.py b/src/easydiffraction/datablocks/experiment/categories/__init__.py new file mode 100644 index 00000000..4e798e20 --- /dev/null +++ b/src/easydiffraction/datablocks/experiment/categories/__init__.py @@ -0,0 +1,2 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause diff --git a/src/easydiffraction/datablocks/experiment/categories/background/__init__.py b/src/easydiffraction/datablocks/experiment/categories/background/__init__.py new file mode 100644 index 00000000..7ffe8f22 --- /dev/null +++ b/src/easydiffraction/datablocks/experiment/categories/background/__init__.py @@ -0,0 +1,9 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + +from easydiffraction.datablocks.experiment.categories.background.chebyshev import ( + ChebyshevPolynomialBackground, +) +from easydiffraction.datablocks.experiment.categories.background.line_segment import ( + LineSegmentBackground, +) diff --git a/src/easydiffraction/experiments/categories/background/base.py b/src/easydiffraction/datablocks/experiment/categories/background/base.py similarity index 75% rename from src/easydiffraction/experiments/categories/background/base.py rename to src/easydiffraction/datablocks/experiment/categories/background/base.py index 78cc5ef1..913cb764 100644 --- a/src/easydiffraction/experiments/categories/background/base.py +++ b/src/easydiffraction/datablocks/experiment/categories/background/base.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause from __future__ import annotations @@ -9,7 +9,8 @@ class BackgroundBase(CategoryCollection): - """Abstract base for background subcategories in experiments. + """ + Abstract base for background subcategories in experiments. Concrete implementations provide parameterized background models and compute background intensities on the experiment grid. diff --git a/src/easydiffraction/experiments/categories/background/chebyshev.py b/src/easydiffraction/datablocks/experiment/categories/background/chebyshev.py similarity index 52% rename from src/easydiffraction/experiments/categories/background/chebyshev.py rename to src/easydiffraction/datablocks/experiment/categories/background/chebyshev.py index 3532e35e..098c4268 100644 --- a/src/easydiffraction/experiments/categories/background/chebyshev.py +++ b/src/easydiffraction/datablocks/experiment/categories/background/chebyshev.py @@ -1,6 +1,7 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -"""Chebyshev polynomial background model. +""" +Chebyshev polynomial background model. Provides a collection of polynomial terms and evaluation helpers. """ @@ -14,14 +15,19 @@ from numpy.polynomial.chebyshev import chebval from easydiffraction.core.category import CategoryItem -from easydiffraction.core.parameters import NumericDescriptor -from easydiffraction.core.parameters import Parameter -from easydiffraction.core.parameters import StringDescriptor +from easydiffraction.core.metadata import CalculatorSupport +from easydiffraction.core.metadata import Compatibility +from easydiffraction.core.metadata import TypeInfo from easydiffraction.core.validation import AttributeSpec -from easydiffraction.core.validation import DataTypes from easydiffraction.core.validation import RangeValidator from easydiffraction.core.validation import RegexValidator -from easydiffraction.experiments.categories.background.base import BackgroundBase +from easydiffraction.core.variable import NumericDescriptor +from easydiffraction.core.variable import Parameter +from easydiffraction.core.variable import StringDescriptor +from easydiffraction.datablocks.experiment.categories.background.base import BackgroundBase +from easydiffraction.datablocks.experiment.categories.background.factory import BackgroundFactory +from easydiffraction.datablocks.experiment.item.enums import BeamModeEnum +from easydiffraction.datablocks.experiment.item.enums import CalculatorEnum from easydiffraction.io.cif.handler import CifHandler from easydiffraction.utils.logging import console from easydiffraction.utils.logging import log @@ -29,7 +35,8 @@ class PolynomialTerm(CategoryItem): - """Chebyshev polynomial term. + """ + Chebyshev polynomial term. New public attribute names: ``order`` and ``coef`` replacing the longer ``chebyshev_order`` / ``chebyshev_coef``. Backward-compatible @@ -37,99 +44,117 @@ class PolynomialTerm(CategoryItem): not break immediately. Tests should migrate to the short names. """ - def __init__( - self, - *, - id=None, # TODO: rename as in the case of data points? - order=None, - coef=None, - ) -> None: + def __init__(self) -> None: super().__init__() self._id = StringDescriptor( name='id', - description='Identifier for this background polynomial term.', + description='Identifier for this background polynomial term', value_spec=AttributeSpec( - type_=DataTypes.STRING, - value=id, default='0', # TODO: the following pattern is valid for dict key # (keywords are not checked). CIF label is less strict. # Do we need conversion between CIF and internal label? - content_validator=RegexValidator(pattern=r'^[A-Za-z0-9_]*$'), - ), - cif_handler=CifHandler( - names=[ - '_pd_background.id', - ] + validator=RegexValidator(pattern=r'^[A-Za-z0-9_]*$'), ), + cif_handler=CifHandler(names=['_pd_background.id']), ) self._order = NumericDescriptor( name='order', description='Order used in a Chebyshev polynomial background term', value_spec=AttributeSpec( - value=order, - type_=DataTypes.NUMERIC, default=0.0, - content_validator=RangeValidator(), - ), - cif_handler=CifHandler( - names=[ - '_pd_background.Chebyshev_order', - ] + validator=RangeValidator(), ), + cif_handler=CifHandler(names=['_pd_background.Chebyshev_order']), ) self._coef = Parameter( name='coef', description='Coefficient used in a Chebyshev polynomial background term', value_spec=AttributeSpec( - value=coef, - type_=DataTypes.NUMERIC, default=0.0, - content_validator=RangeValidator(), - ), - cif_handler=CifHandler( - names=[ - '_pd_background.Chebyshev_coef', - ] + validator=RangeValidator(), ), + cif_handler=CifHandler(names=['_pd_background.Chebyshev_coef']), ) self._identity.category_code = 'background' self._identity.category_entry_name = lambda: str(self._id.value) + # ------------------------------------------------------------------ + # Public properties + # ------------------------------------------------------------------ + @property - def id(self): + def id(self) -> StringDescriptor: + """ + Identifier for this background polynomial term. + + Reading this property returns the underlying + ``StringDescriptor`` object. Assigning to it updates the + parameter value. + """ return self._id @id.setter - def id(self, value): + def id(self, value: str) -> None: self._id.value = value @property - def order(self): + def order(self) -> NumericDescriptor: + """ + Order used in a Chebyshev polynomial background term. + + Reading this property returns the underlying + ``NumericDescriptor`` object. Assigning to it updates the + parameter value. + """ return self._order @order.setter - def order(self, value): + def order(self, value: float) -> None: self._order.value = value @property - def coef(self): + def coef(self) -> Parameter: + """ + Coefficient used in a Chebyshev polynomial background term. + + Reading this property returns the underlying ``Parameter`` + object. Assigning to it updates the parameter value. + """ return self._coef @coef.setter - def coef(self, value): + def coef(self, value: float) -> None: self._coef.value = value +@BackgroundFactory.register class ChebyshevPolynomialBackground(BackgroundBase): - _description: str = 'Chebyshev polynomial background' - - def __init__(self): + """Chebyshev polynomial background model.""" + + type_info = TypeInfo( + tag='chebyshev', + description='Chebyshev polynomial background', + ) + compatibility = Compatibility( + beam_mode=frozenset({ + BeamModeEnum.CONSTANT_WAVELENGTH, + BeamModeEnum.TIME_OF_FLIGHT, + }), + ) + calculator_support = CalculatorSupport( + calculators=frozenset({ + CalculatorEnum.CRYSPY, + CalculatorEnum.CRYSFML, + }), + ) + + def __init__(self) -> None: super().__init__(item_type=PolynomialTerm) - def _update(self, called_by_minimizer=False): + def _update(self, called_by_minimizer: bool = False) -> None: """Evaluate polynomial background over x data.""" del called_by_minimizer @@ -138,14 +163,14 @@ def _update(self, called_by_minimizer=False): if not self._items: log.warning('No background points found. Setting background to zero.') - data._set_bkg(np.zeros_like(x)) + data._set_intensity_bkg(np.zeros_like(x)) return u = (x - x.min()) / (x.max() - x.min()) * 2 - 1 coefs = [term.coef.value for term in self._items] y = chebval(u, coefs) - data._set_bkg(y) + data._set_intensity_bkg(y) def show(self) -> None: """Print a table of polynomial orders and coefficients.""" diff --git a/src/easydiffraction/experiments/categories/background/enums.py b/src/easydiffraction/datablocks/experiment/categories/background/enums.py similarity index 84% rename from src/easydiffraction/experiments/categories/background/enums.py rename to src/easydiffraction/datablocks/experiment/categories/background/enums.py index d7edf42e..9e78effc 100644 --- a/src/easydiffraction/experiments/categories/background/enums.py +++ b/src/easydiffraction/datablocks/experiment/categories/background/enums.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause """Enumerations for background model types.""" @@ -12,7 +12,7 @@ class BackgroundTypeEnum(str, Enum): """Supported background model types.""" LINE_SEGMENT = 'line-segment' - CHEBYSHEV = 'chebyshev polynomial' + CHEBYSHEV = 'chebyshev' @classmethod def default(cls) -> 'BackgroundTypeEnum': diff --git a/src/easydiffraction/datablocks/experiment/categories/background/factory.py b/src/easydiffraction/datablocks/experiment/categories/background/factory.py new file mode 100644 index 00000000..c4d300c8 --- /dev/null +++ b/src/easydiffraction/datablocks/experiment/categories/background/factory.py @@ -0,0 +1,14 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Background factory — delegates entirely to ``FactoryBase``.""" + +from easydiffraction.core.factory import FactoryBase +from easydiffraction.datablocks.experiment.categories.background.enums import BackgroundTypeEnum + + +class BackgroundFactory(FactoryBase): + """Create background collections by tag.""" + + _default_rules = { + frozenset(): BackgroundTypeEnum.LINE_SEGMENT, + } diff --git a/src/easydiffraction/experiments/categories/background/line_segment.py b/src/easydiffraction/datablocks/experiment/categories/background/line_segment.py similarity index 54% rename from src/easydiffraction/experiments/categories/background/line_segment.py rename to src/easydiffraction/datablocks/experiment/categories/background/line_segment.py index bf202ce3..2f0a5496 100644 --- a/src/easydiffraction/experiments/categories/background/line_segment.py +++ b/src/easydiffraction/datablocks/experiment/categories/background/line_segment.py @@ -1,6 +1,7 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -"""Line-segment background model. +""" +Line-segment background model. Interpolate user-specified points to form a background curve. """ @@ -13,14 +14,19 @@ from scipy.interpolate import interp1d from easydiffraction.core.category import CategoryItem -from easydiffraction.core.parameters import NumericDescriptor -from easydiffraction.core.parameters import Parameter -from easydiffraction.core.parameters import StringDescriptor +from easydiffraction.core.metadata import CalculatorSupport +from easydiffraction.core.metadata import Compatibility +from easydiffraction.core.metadata import TypeInfo from easydiffraction.core.validation import AttributeSpec -from easydiffraction.core.validation import DataTypes from easydiffraction.core.validation import RangeValidator from easydiffraction.core.validation import RegexValidator -from easydiffraction.experiments.categories.background.base import BackgroundBase +from easydiffraction.core.variable import NumericDescriptor +from easydiffraction.core.variable import Parameter +from easydiffraction.core.variable import StringDescriptor +from easydiffraction.datablocks.experiment.categories.background.base import BackgroundBase +from easydiffraction.datablocks.experiment.categories.background.factory import BackgroundFactory +from easydiffraction.datablocks.experiment.item.enums import BeamModeEnum +from easydiffraction.datablocks.experiment.item.enums import CalculatorEnum from easydiffraction.io.cif.handler import CifHandler from easydiffraction.utils.logging import console from easydiffraction.utils.logging import log @@ -30,44 +36,27 @@ class LineSegment(CategoryItem): """Single background control point for interpolation.""" - def __init__( - self, - *, - id=None, # TODO: rename as in the case of data points? - x=None, - y=None, - ) -> None: + def __init__(self) -> None: super().__init__() self._id = StringDescriptor( name='id', - description='Identifier for this background line segment.', + description='Identifier for this background line segment', value_spec=AttributeSpec( - type_=DataTypes.STRING, - value=id, default='0', # TODO: the following pattern is valid for dict key # (keywords are not checked). CIF label is less strict. # Do we need conversion between CIF and internal label? - content_validator=RegexValidator(pattern=r'^[A-Za-z0-9_]*$'), - ), - cif_handler=CifHandler( - names=[ - '_pd_background.id', - ] + validator=RegexValidator(pattern=r'^[A-Za-z0-9_]*$'), ), + cif_handler=CifHandler(names=['_pd_background.id']), ) self._x = NumericDescriptor( name='x', - description=( - 'X-coordinates used to create many straight-line segments ' - 'representing the background in a calculated diffractogram.' - ), + description='X-coordinates used to create many straight-line segments', value_spec=AttributeSpec( - value=x, - type_=DataTypes.NUMERIC, default=0.0, - content_validator=RangeValidator(), + validator=RangeValidator(), ), cif_handler=CifHandler( names=[ @@ -78,15 +67,10 @@ def __init__( ) self._y = Parameter( name='y', # TODO: rename to intensity - description=( - 'Intensity used to create many straight-line segments ' - 'representing the background in a calculated diffractogram' - ), + description='Intensity used to create many straight-line segments', value_spec=AttributeSpec( - value=y, - type_=DataTypes.NUMERIC, default=0.0, - content_validator=RangeValidator(), + validator=RangeValidator(), ), # TODO: rename to intensity cif_handler=CifHandler( names=[ @@ -99,38 +83,74 @@ def __init__( self._identity.category_code = 'background' self._identity.category_entry_name = lambda: str(self._id.value) + # ------------------------------------------------------------------ + # Public properties + # ------------------------------------------------------------------ + @property - def id(self): + def id(self) -> StringDescriptor: + """ + Identifier for this background line segment. + + Reading this property returns the underlying + ``StringDescriptor`` object. Assigning to it updates the + parameter value. + """ return self._id @id.setter - def id(self, value): + def id(self, value: str) -> None: self._id.value = value @property - def x(self): + def x(self) -> NumericDescriptor: + """ + X-coordinates used to create many straight-line segments. + + Reading this property returns the underlying + ``NumericDescriptor`` object. Assigning to it updates the + parameter value. + """ return self._x @x.setter - def x(self, value): + def x(self, value: float) -> None: self._x.value = value @property - def y(self): + def y(self) -> Parameter: + """ + Intensity used to create many straight-line segments. + + Reading this property returns the underlying ``Parameter`` + object. Assigning to it updates the parameter value. + """ return self._y @y.setter - def y(self, value): + def y(self, value: float) -> None: self._y.value = value +@BackgroundFactory.register class LineSegmentBackground(BackgroundBase): - _description: str = 'Linear interpolation between points' - - def __init__(self): + """Linear-interpolation background between user-defined points.""" + + type_info = TypeInfo( + tag='line-segment', + description='Linear interpolation between points', + ) + compatibility = Compatibility( + beam_mode=frozenset({BeamModeEnum.CONSTANT_WAVELENGTH, BeamModeEnum.TIME_OF_FLIGHT}), + ) + calculator_support = CalculatorSupport( + calculators=frozenset({CalculatorEnum.CRYSPY, CalculatorEnum.CRYSFML}), + ) + + def __init__(self) -> None: super().__init__(item_type=LineSegment) - def _update(self, called_by_minimizer=False): + def _update(self, called_by_minimizer: bool = False) -> None: """Interpolate background points over x data.""" del called_by_minimizer @@ -139,7 +159,7 @@ def _update(self, called_by_minimizer=False): if not self._items: log.debug('No background points found. Setting background to zero.') - data._set_bkg(np.zeros_like(x)) + data._set_intensity_bkg(np.zeros_like(x)) return segments_x = np.array([point.x.value for point in self._items]) @@ -153,7 +173,7 @@ def _update(self, called_by_minimizer=False): ) y = interp_func(x) - data._set_bkg(y) + data._set_intensity_bkg(y) def show(self) -> None: """Print a table of control points (x, intensity).""" diff --git a/src/easydiffraction/datablocks/experiment/categories/data/__init__.py b/src/easydiffraction/datablocks/experiment/categories/data/__init__.py new file mode 100644 index 00000000..3599f3b5 --- /dev/null +++ b/src/easydiffraction/datablocks/experiment/categories/data/__init__.py @@ -0,0 +1,7 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + +from easydiffraction.datablocks.experiment.categories.data.bragg_pd import PdCwlData +from easydiffraction.datablocks.experiment.categories.data.bragg_pd import PdTofData +from easydiffraction.datablocks.experiment.categories.data.bragg_sc import ReflnData +from easydiffraction.datablocks.experiment.categories.data.total_pd import TotalData diff --git a/src/easydiffraction/datablocks/experiment/categories/data/bragg_pd.py b/src/easydiffraction/datablocks/experiment/categories/data/bragg_pd.py new file mode 100644 index 00000000..883f7f80 --- /dev/null +++ b/src/easydiffraction/datablocks/experiment/categories/data/bragg_pd.py @@ -0,0 +1,625 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + +from __future__ import annotations + +import numpy as np + +from easydiffraction.core.category import CategoryCollection +from easydiffraction.core.category import CategoryItem +from easydiffraction.core.metadata import CalculatorSupport +from easydiffraction.core.metadata import Compatibility +from easydiffraction.core.metadata import TypeInfo +from easydiffraction.core.validation import AttributeSpec +from easydiffraction.core.validation import MembershipValidator +from easydiffraction.core.validation import RangeValidator +from easydiffraction.core.validation import RegexValidator +from easydiffraction.core.variable import NumericDescriptor +from easydiffraction.core.variable import StringDescriptor +from easydiffraction.datablocks.experiment.categories.data.factory import DataFactory +from easydiffraction.datablocks.experiment.item.enums import BeamModeEnum +from easydiffraction.datablocks.experiment.item.enums import CalculatorEnum +from easydiffraction.datablocks.experiment.item.enums import SampleFormEnum +from easydiffraction.datablocks.experiment.item.enums import ScatteringTypeEnum +from easydiffraction.io.cif.handler import CifHandler +from easydiffraction.utils.utils import tof_to_d +from easydiffraction.utils.utils import twotheta_to_d + + +class PdDataPointBaseMixin: + """Single base data point mixin for powder diffraction data.""" + + def __init__(self) -> None: + super().__init__() + + self._point_id = StringDescriptor( + name='point_id', + description='Identifier for this data point in the dataset', + value_spec=AttributeSpec( + default='0', + # TODO: the following pattern is valid for dict key + # (keywords are not checked). CIF label is less strict. + # Do we need conversion between CIF and internal label? + validator=RegexValidator(pattern=r'^[A-Za-z0-9_]*$'), + ), + cif_handler=CifHandler( + names=[ + '_pd_data.point_id', + ] + ), + ) + self._d_spacing = NumericDescriptor( + name='d_spacing', + description='d-spacing value corresponding to this data point', + value_spec=AttributeSpec( + default=0.0, + validator=RangeValidator(ge=0), + ), + cif_handler=CifHandler(names=['_pd_proc.d_spacing']), + ) + self._intensity_meas = NumericDescriptor( + name='intensity_meas', + description='Intensity recorded at each measurement point (angle/time)', + value_spec=AttributeSpec( + default=0.0, + validator=RangeValidator(ge=0), + ), + cif_handler=CifHandler( + names=[ + '_pd_meas.intensity_total', + '_pd_proc.intensity_norm', + ] + ), + ) + self._intensity_meas_su = NumericDescriptor( + name='intensity_meas_su', + description='Standard uncertainty of the measured intensity at this point', + value_spec=AttributeSpec( + default=1.0, + validator=RangeValidator(ge=0), + ), + cif_handler=CifHandler( + names=[ + '_pd_meas.intensity_total_su', + '_pd_proc.intensity_norm_su', + ] + ), + ) + self._intensity_calc = NumericDescriptor( + name='intensity_calc', + description='Intensity of a computed diffractogram at this point', + value_spec=AttributeSpec( + default=0.0, + validator=RangeValidator(ge=0), + ), + cif_handler=CifHandler(names=['_pd_calc.intensity_total']), + ) + self._intensity_bkg = NumericDescriptor( + name='intensity_bkg', + description='Intensity of a computed background at this point', + value_spec=AttributeSpec( + default=0.0, + validator=RangeValidator(ge=0), + ), + cif_handler=CifHandler(names=['_pd_calc.intensity_bkg']), + ) + self._calc_status = StringDescriptor( + name='calc_status', + description='Status code of the data point in the calculation process', + value_spec=AttributeSpec( + default='incl', # TODO: Make Enum + validator=MembershipValidator(allowed=['incl', 'excl']), + ), + cif_handler=CifHandler( + names=[ + '_pd_data.refinement_status', # TODO: rename to calc_status + ] + ), + ) + + # ------------------------------------------------------------------ + # Public properties + # ------------------------------------------------------------------ + + @property + def point_id(self) -> StringDescriptor: + """ + Identifier for this data point in the dataset. + + Reading this property returns the underlying + ``StringDescriptor`` object. + """ + return self._point_id + + @property + def d_spacing(self) -> NumericDescriptor: + """ + d-spacing value corresponding to this data point. + + Reading this property returns the underlying + ``NumericDescriptor`` object. + """ + return self._d_spacing + + @property + def intensity_meas(self) -> NumericDescriptor: + """ + Intensity recorded at each measurement point (angle/time). + + Reading this property returns the underlying + ``NumericDescriptor`` object. + """ + return self._intensity_meas + + @property + def intensity_meas_su(self) -> NumericDescriptor: + """ + Standard uncertainty of the measured intensity at this point. + + Reading this property returns the underlying + ``NumericDescriptor`` object. + """ + return self._intensity_meas_su + + @property + def intensity_calc(self) -> NumericDescriptor: + """ + Intensity of a computed diffractogram at this point. + + Reading this property returns the underlying + ``NumericDescriptor`` object. + """ + return self._intensity_calc + + @property + def intensity_bkg(self) -> NumericDescriptor: + """ + Intensity of a computed background at this point. + + Reading this property returns the underlying + ``NumericDescriptor`` object. + """ + return self._intensity_bkg + + @property + def calc_status(self) -> StringDescriptor: + """ + Status code of the data point in the calculation process. + + Reading this property returns the underlying + ``StringDescriptor`` object. + """ + return self._calc_status + + +class PdCwlDataPointMixin: + """Mixin for CWL powder diffraction data points.""" + + def __init__(self) -> None: + super().__init__() + + self._two_theta = NumericDescriptor( + name='two_theta', + description='Measured 2θ diffraction angle.', + units='deg', + value_spec=AttributeSpec( + default=0.0, + validator=RangeValidator(ge=0, le=180), + ), + cif_handler=CifHandler( + names=[ + '_pd_proc.2theta_scan', + '_pd_meas.2theta_scan', + ] + ), + ) + + # ------------------------------------------------------------------ + # Public properties + # ------------------------------------------------------------------ + + @property + def two_theta(self) -> NumericDescriptor: + """ + Measured 2θ diffraction angle (deg). + + Reading this property returns the underlying + ``NumericDescriptor`` object. + """ + return self._two_theta + + +class PdTofDataPointMixin: + """Mixin for powder diffraction data points with time-of-flight.""" + + def __init__(self) -> None: + super().__init__() + + self._time_of_flight = NumericDescriptor( + name='time_of_flight', + description='Measured time for time-of-flight neutron measurement.', + units='µs', + value_spec=AttributeSpec( + default=0.0, + validator=RangeValidator(ge=0), + ), + cif_handler=CifHandler(names=['_pd_meas.time_of_flight']), + ) + + # ------------------------------------------------------------------ + # Public properties + # ------------------------------------------------------------------ + + @property + def time_of_flight(self) -> NumericDescriptor: + """ + Measured time for time-of-flight neutron measurement (µs). + + Reading this property returns the underlying + ``NumericDescriptor`` object. + """ + return self._time_of_flight + + +class PdCwlDataPoint( + PdDataPointBaseMixin, # TODO: rename to BasePdDataPointMixin??? + PdCwlDataPointMixin, # TODO: rename to CwlPdDataPointMixin??? + CategoryItem, # Must be last to ensure mixins initialized first + # TODO: Check this. AI suggest class + # CwlThompsonCoxHastings( + # PeakBase, # From CategoryItem + # CwlBroadeningMixin, + # FcjAsymmetryMixin, + # ): + # But also says, that in fact, it is just for consistency. And both + # orders work. +): + """Powder diffraction data point for CWL experiments.""" + + def __init__(self) -> None: + super().__init__() + self._identity.category_code = 'pd_data' + self._identity.category_entry_name = lambda: str(self.point_id.value) + + +class PdTofDataPoint( + PdDataPointBaseMixin, + PdTofDataPointMixin, + CategoryItem, # Must be last to ensure mixins initialized first +): + """Powder diffraction data point for time-of-flight experiments.""" + + def __init__(self) -> None: + super().__init__() + self._identity.category_code = 'pd_data' + self._identity.category_entry_name = lambda: str(self.point_id.value) + + +class PdDataBase(CategoryCollection): + """Base class for powder diffraction data collections.""" + + # TODO: ??? + + # Redefine update priority to ensure data updated after other + # categories. Higher number = runs later. Default for other + # categories, e.g., background and excluded regions are 10 by + # default + _update_priority = 100 + + ################# + # Private methods + ################# + + # Should be set only once + + def _set_point_id(self, values: object) -> None: + """Set point IDs.""" + for p, v in zip(self._items, values, strict=True): + p.point_id._value = v + + def _set_intensity_meas(self, values: object) -> None: + """Set measured intensity.""" + for p, v in zip(self._items, values, strict=True): + p.intensity_meas._value = v + + def _set_intensity_meas_su(self, values: object) -> None: + """Set standard uncertainty of measured intensity values.""" + for p, v in zip(self._items, values, strict=True): + p.intensity_meas_su._value = v + + # Can be set multiple times + + def _set_d_spacing(self, values: object) -> None: + """Set d-spacing values.""" + for p, v in zip(self._calc_items, values, strict=True): + p.d_spacing._value = v + + def _set_intensity_calc(self, values: object) -> None: + """Set calculated intensity.""" + for p, v in zip(self._calc_items, values, strict=True): + p.intensity_calc._value = v + + def _set_intensity_bkg(self, values: object) -> None: + """Set background intensity.""" + for p, v in zip(self._calc_items, values, strict=True): + p.intensity_bkg._value = v + + def _set_calc_status(self, values: object) -> None: + """Set refinement status.""" + for p, v in zip(self._items, values, strict=True): + if v: + p.calc_status._value = 'incl' + elif not v: + p.calc_status._value = 'excl' + else: + raise ValueError( + f'Invalid refinement status value: {v}. Expected boolean True/False.' + ) + + @property + def _calc_mask(self) -> np.ndarray: + return self.calc_status == 'incl' + + @property + def _calc_items(self) -> list: + """Get only the items included in calculations.""" + return [item for item, mask in zip(self._items, self._calc_mask, strict=False) if mask] + + # Misc + + def _update(self, called_by_minimizer: bool = False) -> None: + experiment = self._parent + experiments = experiment._parent + project = experiments._parent + structures = project.structures + calculator = experiment.calculator + + initial_calc = np.zeros_like(self.x) + calc = initial_calc + + # TODO: refactor _get_valid_linked_phases to only be responsible + # for returning list. Warning message should be defined here, + # at least some of them. + # TODO: Adapt following the _update method in bragg_sc.py + for linked_phase in experiment._get_valid_linked_phases(structures): + structure_id = linked_phase._identity.category_entry_name + structure_scale = linked_phase.scale.value + structure = structures[structure_id] + + structure_calc = calculator.calculate_pattern( + structure, + experiment, + called_by_minimizer=called_by_minimizer, + ) + + structure_scaled_calc = structure_scale * structure_calc + calc += structure_scaled_calc + + self._set_intensity_calc(calc + self.intensity_bkg) + + ################### + # Public properties + ################### + + @property + def calc_status(self) -> np.ndarray: + """Refinement-status flags for each data point as an array.""" + return np.fromiter( + (p.calc_status.value for p in self._items), + dtype=object, # TODO: needed? DataTypes.NUMERIC? + ) + + @property + def d_spacing(self) -> np.ndarray: + """D-spacing values for active (non-excluded) data points.""" + return np.fromiter( + (p.d_spacing.value for p in self._calc_items), + dtype=float, # TODO: needed? DataTypes.NUMERIC? + ) + + @property + def intensity_meas(self) -> np.ndarray: + """Measured intensities for active data points.""" + return np.fromiter( + (p.intensity_meas.value for p in self._calc_items), + dtype=float, # TODO: needed? DataTypes.NUMERIC? + ) + + @property + def intensity_meas_su(self) -> np.ndarray: + """ + Standard uncertainties of the measured intensities. + + Values smaller than 0.0001 are replaced with 1.0 to prevent + fitting failures. + """ + # TODO: The following is a temporary workaround to handle zero + # or near-zero uncertainties in the data, when dats is loaded + # from CIF files. This is necessary because zero uncertainties + # cause fitting algorithms to fail. + # The current implementation is inefficient. + # In the future, we should extend the functionality of + # the NumericDescriptor to automatically replace the value + # outside of the valid range (`validator`) with a + # default value (`default`), when the value is set. + # BraggPdExperiment._load_ascii_data_to_experiment() handles + # this for ASCII data, but we also need to handle CIF data and + # come up with a consistent approach for both data sources. + original = np.fromiter( + (p.intensity_meas_su.value for p in self._calc_items), + dtype=float, # TODO: needed? DataTypes.NUMERIC? + ) + # Replace values smaller than 0.0001 with 1.0 + modified = np.where(original < 0.0001, 1.0, original) + return modified + + @property + def intensity_calc(self) -> np.ndarray: + """Calculated intensities for active data points.""" + return np.fromiter( + (p.intensity_calc.value for p in self._calc_items), + dtype=float, # TODO: needed? DataTypes.NUMERIC? + ) + + @property + def intensity_bkg(self) -> np.ndarray: + """Background intensities for active data points.""" + return np.fromiter( + (p.intensity_bkg.value for p in self._calc_items), + dtype=float, # TODO: needed? DataTypes.NUMERIC? + ) + + +@DataFactory.register +class PdCwlData(PdDataBase): + """Bragg powder CWL data collection.""" + + # TODO: ??? + # _description: str = 'Powder diffraction data points for + # constant-wavelength experiments.' + type_info = TypeInfo(tag='bragg-pd', description='Bragg powder CWL data') + compatibility = Compatibility( + sample_form=frozenset({SampleFormEnum.POWDER}), + scattering_type=frozenset({ScatteringTypeEnum.BRAGG}), + beam_mode=frozenset({BeamModeEnum.CONSTANT_WAVELENGTH, BeamModeEnum.TIME_OF_FLIGHT}), + ) + calculator_support = CalculatorSupport( + calculators=frozenset({CalculatorEnum.CRYSPY}), + ) + + def __init__(self) -> None: + super().__init__(item_type=PdCwlDataPoint) + + ################# + # Private methods + ################# + + # Should be set only once + + def _create_items_set_xcoord_and_id(self, values: object) -> None: + """Set 2θ values.""" + # TODO: split into multiple methods + + # Create items + self._items = [self._item_type() for _ in range(values.size)] + + # Set two-theta values + for p, v in zip(self._items, values, strict=True): + p.two_theta._value = v + + # Set point IDs + self._set_point_id([str(i + 1) for i in range(values.size)]) + + # Misc + + def _update(self, called_by_minimizer: bool = False) -> None: + super()._update(called_by_minimizer) + + experiment = self._parent + d_spacing = twotheta_to_d( + self.x, + experiment.instrument.setup_wavelength.value, + ) + self._set_d_spacing(d_spacing) + + ################### + # Public properties + ################### + + @property + def two_theta(self) -> np.ndarray: + """Get 2θ values for data points included in calculations.""" + return np.fromiter( + (p.two_theta.value for p in self._calc_items), + dtype=float, # TODO: needed? DataTypes.NUMERIC? + ) + + @property + def x(self) -> np.ndarray: + """Alias for two_theta.""" + return self.two_theta + + @property + def unfiltered_x(self) -> np.ndarray: + """Get the 2θ values for all data points in this collection.""" + return np.fromiter( + (p.two_theta.value for p in self._items), + dtype=float, # TODO: needed? DataTypes.NUMERIC? + ) + + +@DataFactory.register +class PdTofData(PdDataBase): + """Bragg powder TOF data collection.""" + + type_info = TypeInfo(tag='bragg-pd-tof', description='Bragg powder TOF data') + compatibility = Compatibility( + sample_form=frozenset({SampleFormEnum.POWDER}), + scattering_type=frozenset({ScatteringTypeEnum.BRAGG}), + beam_mode=frozenset({BeamModeEnum.TIME_OF_FLIGHT}), + ) + calculator_support = CalculatorSupport( + calculators=frozenset({CalculatorEnum.CRYSPY, CalculatorEnum.CRYSFML}), + ) + + def __init__(self) -> None: + super().__init__(item_type=PdTofDataPoint) + + ################# + # Private methods + ################# + + # Should be set only once + + def _create_items_set_xcoord_and_id(self, values: object) -> None: + """Set time-of-flight values.""" + # TODO: split into multiple methods + + # Create items + self._items = [self._item_type() for _ in range(values.size)] + + # Set time-of-flight values + for p, v in zip(self._items, values, strict=True): + p.time_of_flight._value = v + + # Set point IDs + self._set_point_id([str(i + 1) for i in range(values.size)]) + + # Misc + + def _update(self, called_by_minimizer: bool = False) -> None: + super()._update(called_by_minimizer) + + experiment = self._parent + d_spacing = tof_to_d( + self.x, + experiment.instrument.calib_d_to_tof_offset.value, + experiment.instrument.calib_d_to_tof_linear.value, + experiment.instrument.calib_d_to_tof_quad.value, + ) + self._set_d_spacing(d_spacing) + + ################### + # Public properties + ################### + + @property + def time_of_flight(self) -> np.ndarray: + """Get TOF values for data points included in calculations.""" + return np.fromiter( + (p.time_of_flight.value for p in self._calc_items), + dtype=float, # TODO: needed? DataTypes.NUMERIC? + ) + + @property + def x(self) -> np.ndarray: + """Alias for time_of_flight.""" + return self.time_of_flight + + @property + def unfiltered_x(self) -> np.ndarray: + """Get the TOF values for all data points in this collection.""" + return np.fromiter( + (p.time_of_flight.value for p in self._items), + dtype=float, # TODO: needed? DataTypes.NUMERIC? + ) diff --git a/src/easydiffraction/datablocks/experiment/categories/data/bragg_sc.py b/src/easydiffraction/datablocks/experiment/categories/data/bragg_sc.py new file mode 100644 index 00000000..d19eb75f --- /dev/null +++ b/src/easydiffraction/datablocks/experiment/categories/data/bragg_sc.py @@ -0,0 +1,434 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + +from __future__ import annotations + +import numpy as np + +from easydiffraction.core.category import CategoryCollection +from easydiffraction.core.category import CategoryItem +from easydiffraction.core.metadata import CalculatorSupport +from easydiffraction.core.metadata import Compatibility +from easydiffraction.core.metadata import TypeInfo +from easydiffraction.core.validation import AttributeSpec +from easydiffraction.core.validation import RangeValidator +from easydiffraction.core.validation import RegexValidator +from easydiffraction.core.variable import NumericDescriptor +from easydiffraction.core.variable import StringDescriptor +from easydiffraction.datablocks.experiment.categories.data.factory import DataFactory +from easydiffraction.datablocks.experiment.item.enums import BeamModeEnum +from easydiffraction.datablocks.experiment.item.enums import CalculatorEnum +from easydiffraction.datablocks.experiment.item.enums import SampleFormEnum +from easydiffraction.datablocks.experiment.item.enums import ScatteringTypeEnum +from easydiffraction.io.cif.handler import CifHandler +from easydiffraction.utils.logging import log +from easydiffraction.utils.utils import sin_theta_over_lambda_to_d_spacing + + +class Refln(CategoryItem): + """Single reflection for single-crystal diffraction data.""" + + def __init__(self) -> None: + super().__init__() + + self._id = StringDescriptor( + name='id', + description='Identifier of the reflection', + value_spec=AttributeSpec( + default='0', + # TODO: the following pattern is valid for dict key + # (keywords are not checked). CIF label is less strict. + # Do we need conversion between CIF and internal label? + validator=RegexValidator(pattern=r'^[A-Za-z0-9_]*$'), + ), + cif_handler=CifHandler(names=['_refln.id']), + ) + self._d_spacing = NumericDescriptor( + name='d_spacing', + description='Distance between lattice planes for this reflection', + units='Å', + value_spec=AttributeSpec( + default=0.0, + validator=RangeValidator(ge=0), + ), + cif_handler=CifHandler(names=['_refln.d_spacing']), + ) + self._sin_theta_over_lambda = NumericDescriptor( + name='sin_theta_over_lambda', + description='The sin(θ)/λ value for this reflection', + units='Å⁻¹', + value_spec=AttributeSpec( + default=0.0, + validator=RangeValidator(ge=0), + ), + cif_handler=CifHandler(names=['_refln.sin_theta_over_lambda']), + ) + self._index_h = NumericDescriptor( + name='index_h', + description='Miller index h of a measured reflection', + value_spec=AttributeSpec( + default=0.0, + validator=RangeValidator(), + ), + cif_handler=CifHandler(names=['_refln.index_h']), + ) + self._index_k = NumericDescriptor( + name='index_k', + description='Miller index k of a measured reflection', + value_spec=AttributeSpec( + default=0.0, + validator=RangeValidator(), + ), + cif_handler=CifHandler(names=['_refln.index_k']), + ) + self._index_l = NumericDescriptor( + name='index_l', + description='Miller index l of a measured reflection', + value_spec=AttributeSpec( + default=0.0, + validator=RangeValidator(), + ), + cif_handler=CifHandler(names=['_refln.index_l']), + ) + self._intensity_meas = NumericDescriptor( + name='intensity_meas', + description=' The intensity of the reflection derived from the measurements.', + value_spec=AttributeSpec( + default=0.0, + validator=RangeValidator(ge=0), + ), + cif_handler=CifHandler(names=['_refln.intensity_meas']), + ) + self._intensity_meas_su = NumericDescriptor( + name='intensity_meas_su', + description='Standard uncertainty of the measured intensity.', + value_spec=AttributeSpec( + default=0.0, + validator=RangeValidator(ge=0), + ), + cif_handler=CifHandler(names=['_refln.intensity_meas_su']), + ) + self._intensity_calc = NumericDescriptor( + name='intensity_calc', + description='Intensity of the reflection calculated from atom site data', + value_spec=AttributeSpec( + default=0.0, + validator=RangeValidator(ge=0), + ), + cif_handler=CifHandler(names=['_refln.intensity_calc']), + ) + self._wavelength = NumericDescriptor( + name='wavelength', + description='Mean wavelength of radiation for this reflection', + units='Å', + value_spec=AttributeSpec( + default=0.0, + validator=RangeValidator(ge=0), + ), + cif_handler=CifHandler(names=['_refln.wavelength']), + ) + + self._identity.category_code = 'refln' + self._identity.category_entry_name = lambda: str(self.id.value) + + # ------------------------------------------------------------------ + # Public properties + # ------------------------------------------------------------------ + + @property + def id(self) -> StringDescriptor: + """ + Identifier of the reflection. + + Reading this property returns the underlying + ``StringDescriptor`` object. + """ + return self._id + + @property + def d_spacing(self) -> NumericDescriptor: + """ + Distance between lattice planes for this reflection (Å). + + Reading this property returns the underlying + ``NumericDescriptor`` object. + """ + return self._d_spacing + + @property + def sin_theta_over_lambda(self) -> NumericDescriptor: + """ + The sin(θ)/λ value for this reflection (Å⁻¹). + + Reading this property returns the underlying + ``NumericDescriptor`` object. + """ + return self._sin_theta_over_lambda + + @property + def index_h(self) -> NumericDescriptor: + """ + Miller index h of a measured reflection. + + Reading this property returns the underlying + ``NumericDescriptor`` object. + """ + return self._index_h + + @property + def index_k(self) -> NumericDescriptor: + """ + Miller index k of a measured reflection. + + Reading this property returns the underlying + ``NumericDescriptor`` object. + """ + return self._index_k + + @property + def index_l(self) -> NumericDescriptor: + """ + Miller index l of a measured reflection. + + Reading this property returns the underlying + ``NumericDescriptor`` object. + """ + return self._index_l + + @property + def intensity_meas(self) -> NumericDescriptor: + """ + The intensity of the reflection derived from the measurements. + + Reading this property returns the underlying + ``NumericDescriptor`` object. + """ + return self._intensity_meas + + @property + def intensity_meas_su(self) -> NumericDescriptor: + """ + Standard uncertainty of the measured intensity. + + Reading this property returns the underlying + ``NumericDescriptor`` object. + """ + return self._intensity_meas_su + + @property + def intensity_calc(self) -> NumericDescriptor: + """ + Intensity of the reflection calculated from atom site data. + + Reading this property returns the underlying + ``NumericDescriptor`` object. + """ + return self._intensity_calc + + @property + def wavelength(self) -> NumericDescriptor: + """ + Mean wavelength of radiation for this reflection (Å). + + Reading this property returns the underlying + ``NumericDescriptor`` object. + """ + return self._wavelength + + +@DataFactory.register +class ReflnData(CategoryCollection): + """Collection of reflections for single crystal diffraction data.""" + + type_info = TypeInfo(tag='bragg-sc', description='Bragg single-crystal reflection data') + compatibility = Compatibility( + sample_form=frozenset({SampleFormEnum.SINGLE_CRYSTAL}), + scattering_type=frozenset({ScatteringTypeEnum.BRAGG}), + beam_mode=frozenset({BeamModeEnum.CONSTANT_WAVELENGTH, BeamModeEnum.TIME_OF_FLIGHT}), + ) + calculator_support = CalculatorSupport( + calculators=frozenset({CalculatorEnum.CRYSPY}), + ) + + _update_priority = 100 + + def __init__(self) -> None: + super().__init__(item_type=Refln) + + ################# + # Private methods + ################# + + # Should be set only once + + def _create_items_set_hkl_and_id( + self, + indices_h: object, + indices_k: object, + indices_l: object, + ) -> None: + """Set Miller indices.""" + # TODO: split into multiple methods + + # Create items + self._items = [self._item_type() for _ in range(indices_h.size)] + + # Set indices + for item, index_h, index_k, index_l in zip( + self._items, indices_h, indices_k, indices_l, strict=True + ): + item.index_h._value = index_h + item.index_k._value = index_k + item.index_l._value = index_l + + # Set reflection IDs + self._set_id([str(i + 1) for i in range(indices_h.size)]) + + def _set_id(self, values: object) -> None: + """Set reflection IDs.""" + for p, v in zip(self._items, values, strict=True): + p.id._value = v + + def _set_intensity_meas(self, values: object) -> None: + """Set measured intensity.""" + for p, v in zip(self._items, values, strict=True): + p.intensity_meas._value = v + + def _set_intensity_meas_su(self, values: object) -> None: + """Set standard uncertainty of measured intensity values.""" + for p, v in zip(self._items, values, strict=True): + p.intensity_meas_su._value = v + + def _set_wavelength(self, values: object) -> None: + """Set wavelength.""" + for p, v in zip(self._items, values, strict=True): + p.wavelength._value = v + + # Can be set multiple times + + def _set_d_spacing(self, values: object) -> None: + """Set d-spacing values.""" + for p, v in zip(self._items, values, strict=True): + p.d_spacing._value = v + + def _set_sin_theta_over_lambda(self, values: object) -> None: + """Set sin(theta)/lambda values.""" + for p, v in zip(self._items, values, strict=True): + p.sin_theta_over_lambda._value = v + + def _set_intensity_calc(self, values: object) -> None: + """Set calculated intensity.""" + for p, v in zip(self._items, values, strict=True): + p.intensity_calc._value = v + + # Misc + + def _update(self, called_by_minimizer: bool = False) -> None: + experiment = self._parent + experiments = experiment._parent + project = experiments._parent + structures = project.structures + calculator = experiment.calculator + + linked_crystal = experiment.linked_crystal + linked_crystal_id = experiment.linked_crystal.id.value + + if linked_crystal_id not in structures.names: + log.error( + f"Linked crystal ID '{linked_crystal_id}' not found in " + f'structure IDs {structures.names}.' + ) + return + + structure_id = linked_crystal_id + structure_scale = linked_crystal.scale.value + structure = structures[structure_id] + + stol, raw_calc = calculator.calculate_structure_factors( + structure, + experiment, + called_by_minimizer=called_by_minimizer, + ) + + d_spacing = sin_theta_over_lambda_to_d_spacing(stol) + calc = structure_scale * raw_calc + + self._set_d_spacing(d_spacing) + self._set_sin_theta_over_lambda(stol) + self._set_intensity_calc(calc) + + # ------------------------------------------------------------------ + # Public properties + # ------------------------------------------------------------------ + + @property + def d_spacing(self) -> np.ndarray: + """D-spacing values for all reflection data points.""" + return np.fromiter( + (p.d_spacing.value for p in self._items), + dtype=float, + ) + + @property + def sin_theta_over_lambda(self) -> np.ndarray: + """sinθ/λ values for all reflection data points.""" + return np.fromiter( + (p.sin_theta_over_lambda.value for p in self._items), + dtype=float, + ) + + @property + def index_h(self) -> np.ndarray: + """Miller h indices for all reflection data points.""" + return np.fromiter( + (p.index_h.value for p in self._items), + dtype=float, + ) + + @property + def index_k(self) -> np.ndarray: + """Miller k indices for all reflection data points.""" + return np.fromiter( + (p.index_k.value for p in self._items), + dtype=float, + ) + + @property + def index_l(self) -> np.ndarray: + """Miller l indices for all reflection data points.""" + return np.fromiter( + (p.index_l.value for p in self._items), + dtype=float, + ) + + @property + def intensity_meas(self) -> np.ndarray: + """Measured structure-factor intensities for all reflections.""" + return np.fromiter( + (p.intensity_meas.value for p in self._items), + dtype=float, + ) + + @property + def intensity_meas_su(self) -> np.ndarray: + """Standard uncertainties of the measured intensities.""" + return np.fromiter( + (p.intensity_meas_su.value for p in self._items), + dtype=float, + ) + + @property + def intensity_calc(self) -> np.ndarray: + """Calculated intensities for all reflections.""" + return np.fromiter( + (p.intensity_calc.value for p in self._items), + dtype=float, + ) + + @property + def wavelength(self) -> np.ndarray: + """Wavelengths associated with each reflection.""" + return np.fromiter( + (p.wavelength.value for p in self._items), + dtype=float, + ) diff --git a/src/easydiffraction/datablocks/experiment/categories/data/factory.py b/src/easydiffraction/datablocks/experiment/categories/data/factory.py new file mode 100644 index 00000000..d8cdcf12 --- /dev/null +++ b/src/easydiffraction/datablocks/experiment/categories/data/factory.py @@ -0,0 +1,33 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Data collection factory — delegates to ``FactoryBase``.""" + +from easydiffraction.core.factory import FactoryBase +from easydiffraction.datablocks.experiment.item.enums import BeamModeEnum +from easydiffraction.datablocks.experiment.item.enums import SampleFormEnum +from easydiffraction.datablocks.experiment.item.enums import ScatteringTypeEnum + + +class DataFactory(FactoryBase): + """Factory for creating diffraction data collections.""" + + _default_rules = { + frozenset({ + ('sample_form', SampleFormEnum.POWDER), + ('scattering_type', ScatteringTypeEnum.BRAGG), + ('beam_mode', BeamModeEnum.CONSTANT_WAVELENGTH), + }): 'bragg-pd', + frozenset({ + ('sample_form', SampleFormEnum.POWDER), + ('scattering_type', ScatteringTypeEnum.BRAGG), + ('beam_mode', BeamModeEnum.TIME_OF_FLIGHT), + }): 'bragg-pd-tof', + frozenset({ + ('sample_form', SampleFormEnum.POWDER), + ('scattering_type', ScatteringTypeEnum.TOTAL), + }): 'total-pd', + frozenset({ + ('sample_form', SampleFormEnum.SINGLE_CRYSTAL), + ('scattering_type', ScatteringTypeEnum.BRAGG), + }): 'bragg-sc', + } diff --git a/src/easydiffraction/datablocks/experiment/categories/data/total_pd.py b/src/easydiffraction/datablocks/experiment/categories/data/total_pd.py new file mode 100644 index 00000000..c8d0aa9d --- /dev/null +++ b/src/easydiffraction/datablocks/experiment/categories/data/total_pd.py @@ -0,0 +1,376 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Data categories for total scattering (PDF) experiments.""" + +from __future__ import annotations + +import numpy as np + +from easydiffraction.core.category import CategoryCollection +from easydiffraction.core.category import CategoryItem +from easydiffraction.core.metadata import CalculatorSupport +from easydiffraction.core.metadata import Compatibility +from easydiffraction.core.metadata import TypeInfo +from easydiffraction.core.validation import AttributeSpec +from easydiffraction.core.validation import MembershipValidator +from easydiffraction.core.validation import RangeValidator +from easydiffraction.core.validation import RegexValidator +from easydiffraction.core.variable import NumericDescriptor +from easydiffraction.core.variable import StringDescriptor +from easydiffraction.datablocks.experiment.categories.data.factory import DataFactory +from easydiffraction.datablocks.experiment.item.enums import BeamModeEnum +from easydiffraction.datablocks.experiment.item.enums import CalculatorEnum +from easydiffraction.datablocks.experiment.item.enums import SampleFormEnum +from easydiffraction.datablocks.experiment.item.enums import ScatteringTypeEnum +from easydiffraction.io.cif.handler import CifHandler + + +class TotalDataPoint(CategoryItem): + """ + Total scattering (PDF) data point in r-space (real space). + + Note: PDF data is always in r-space regardless of whether the + original measurement was CWL or TOF. + """ + + def __init__(self) -> None: + super().__init__() + + self._point_id = StringDescriptor( + name='point_id', + description='Identifier for this data point in the dataset', + value_spec=AttributeSpec( + default='0', + validator=RegexValidator(pattern=r'^[A-Za-z0-9_]*$'), + ), + cif_handler=CifHandler( + names=[ + '_pd_data.point_id', # TODO: Use total scattering CIF names + ] + ), + ) + self._r = NumericDescriptor( + name='r', + description='Interatomic distance in real space', + units='Å', + value_spec=AttributeSpec( + default=0.0, + validator=RangeValidator(ge=0), + ), + cif_handler=CifHandler( + names=[ + '_pd_proc.r', # TODO: Use PDF-specific CIF names + ] + ), + ) + self._g_r_meas = NumericDescriptor( + name='g_r_meas', + description='Measured pair distribution function G(r)', + value_spec=AttributeSpec( + default=0.0, + ), + cif_handler=CifHandler( + names=[ + '_pd_meas.intensity_total', # TODO: Use PDF-specific CIF names + ] + ), + ) + self._g_r_meas_su = NumericDescriptor( + name='g_r_meas_su', + description='Standard uncertainty of measured G(r)', + value_spec=AttributeSpec( + default=0.0, + validator=RangeValidator(ge=0), + ), + cif_handler=CifHandler( + names=[ + '_pd_meas.intensity_total_su', # TODO: Use PDF-specific CIF names + ] + ), + ) + self._g_r_calc = NumericDescriptor( + name='g_r_calc', + description='Calculated pair distribution function G(r)', + value_spec=AttributeSpec( + default=0.0, + ), + cif_handler=CifHandler( + names=[ + '_pd_calc.intensity_total', # TODO: Use PDF-specific CIF names + ] + ), + ) + self._calc_status = StringDescriptor( + name='calc_status', + description='Status code of the data point in calculation', + value_spec=AttributeSpec( + default='incl', + validator=MembershipValidator(allowed=['incl', 'excl']), + ), + cif_handler=CifHandler( + names=[ + '_pd_data.refinement_status', # TODO: Use PDF-specific CIF names + ] + ), + ) + + self._identity.category_code = 'total_data' + self._identity.category_entry_name = lambda: str(self.point_id.value) + + # ------------------------------------------------------------------ + # Public properties + # ------------------------------------------------------------------ + + @property + def point_id(self) -> StringDescriptor: + """ + Identifier for this data point in the dataset. + + Reading this property returns the underlying + ``StringDescriptor`` object. + """ + return self._point_id + + @property + def r(self) -> NumericDescriptor: + """ + Interatomic distance in real space (Å). + + Reading this property returns the underlying + ``NumericDescriptor`` object. + """ + return self._r + + @property + def g_r_meas(self) -> NumericDescriptor: + """ + Measured pair distribution function G(r). + + Reading this property returns the underlying + ``NumericDescriptor`` object. + """ + return self._g_r_meas + + @property + def g_r_meas_su(self) -> NumericDescriptor: + """ + Standard uncertainty of measured G(r). + + Reading this property returns the underlying + ``NumericDescriptor`` object. + """ + return self._g_r_meas_su + + @property + def g_r_calc(self) -> NumericDescriptor: + """ + Calculated pair distribution function G(r). + + Reading this property returns the underlying + ``NumericDescriptor`` object. + """ + return self._g_r_calc + + @property + def calc_status(self) -> StringDescriptor: + """ + Status code of the data point in calculation. + + Reading this property returns the underlying + ``StringDescriptor`` object. + """ + return self._calc_status + + +class TotalDataBase(CategoryCollection): + """Base class for total scattering data collections.""" + + _update_priority = 100 + + ################# + # Private methods + ################# + + # Should be set only once + + def _set_point_id(self, values: object) -> None: + """Set point IDs.""" + for p, v in zip(self._items, values, strict=True): + p.point_id._value = v + + def _set_g_r_meas(self, values: object) -> None: + """Set measured G(r).""" + for p, v in zip(self._items, values, strict=True): + p.g_r_meas._value = v + + def _set_g_r_meas_su(self, values: object) -> None: + """Set standard uncertainty of measured G(r) values.""" + for p, v in zip(self._items, values, strict=True): + p.g_r_meas_su._value = v + + # Can be set multiple times + + def _set_g_r_calc(self, values: object) -> None: + """Set calculated G(r).""" + for p, v in zip(self._calc_items, values, strict=True): + p.g_r_calc._value = v + + def _set_calc_status(self, values: object) -> None: + """Set calculation status.""" + for p, v in zip(self._items, values, strict=True): + if v: + p.calc_status._value = 'incl' + elif not v: + p.calc_status._value = 'excl' + else: + raise ValueError( + f'Invalid calculation status value: {v}. Expected boolean True/False.' + ) + + @property + def _calc_mask(self) -> np.ndarray: + return self.calc_status == 'incl' + + @property + def _calc_items(self) -> list: + """Get only the items included in calculations.""" + return [item for item, mask in zip(self._items, self._calc_mask, strict=False) if mask] + + # Misc + + def _update(self, called_by_minimizer: bool = False) -> None: + experiment = self._parent + experiments = experiment._parent + project = experiments._parent + structures = project.structures + calculator = experiment.calculator + + initial_calc = np.zeros_like(self.x) + calc = initial_calc + + # TODO: refactor _get_valid_linked_phases to only be responsible + # for returning list. Warning message should be defined here, + # at least some of them. + # TODO: Adapt following the _update method in bragg_sc.py + for linked_phase in experiment._get_valid_linked_phases(structures): + structure_id = linked_phase._identity.category_entry_name + structure_scale = linked_phase.scale.value + structure = structures[structure_id] + + structure_calc = calculator.calculate_pattern( + structure, + experiment, + called_by_minimizer=called_by_minimizer, + ) + + structure_scaled_calc = structure_scale * structure_calc + calc += structure_scaled_calc + + self._set_g_r_calc(calc) + + # ------------------------------------------------------------------ + # Public properties + # ------------------------------------------------------------------ + + @property + def calc_status(self) -> np.ndarray: + """Refinement-status flags for each data point as an array.""" + return np.fromiter( + (p.calc_status.value for p in self._items), + dtype=object, + ) + + @property + def intensity_meas(self) -> np.ndarray: + """Measured G(r) values for active data points.""" + return np.fromiter( + (p.g_r_meas.value for p in self._calc_items), + dtype=float, + ) + + @property + def intensity_meas_su(self) -> np.ndarray: + """Standard uncertainties of the measured G(r) values.""" + return np.fromiter( + (p.g_r_meas_su.value for p in self._calc_items), + dtype=float, + ) + + @property + def intensity_calc(self) -> np.ndarray: + """Calculated G(r) values for active data points.""" + return np.fromiter( + (p.g_r_calc.value for p in self._calc_items), + dtype=float, + ) + + @property + def intensity_bkg(self) -> np.ndarray: + """Background is always zero for PDF data.""" + return np.zeros_like(self.intensity_calc) + + +@DataFactory.register +class TotalData(TotalDataBase): + """ + Total scattering (PDF) data collection in r-space. + + Note: Works for both CWL and TOF measurements as PDF data is always + transformed to r-space. + """ + + type_info = TypeInfo( + tag='total-pd', + description='Total scattering (PDF) data', + ) + compatibility = Compatibility( + sample_form=frozenset({SampleFormEnum.POWDER}), + scattering_type=frozenset({ScatteringTypeEnum.TOTAL}), + beam_mode=frozenset({BeamModeEnum.CONSTANT_WAVELENGTH, BeamModeEnum.TIME_OF_FLIGHT}), + ) + calculator_support = CalculatorSupport( + calculators=frozenset({CalculatorEnum.PDFFIT}), + ) + + def __init__(self) -> None: + super().__init__(item_type=TotalDataPoint) + + ################# + # Private methods + ################# + + # Should be set only once + + def _create_items_set_xcoord_and_id(self, values: object) -> None: + """Set r values.""" + # TODO: split into multiple methods + + # Create items + self._items = [self._item_type() for _ in range(values.size)] + + # Set r values + for p, v in zip(self._items, values, strict=True): + p.r._value = v + + # Set point IDs + self._set_point_id([str(i + 1) for i in range(values.size)]) + + # ------------------------------------------------------------------ + # Public properties + # ------------------------------------------------------------------ + + @property + def x(self) -> np.ndarray: + """Get the r values for data points included in calculations.""" + return np.fromiter( + (p.r.value for p in self._calc_items), + dtype=float, # TODO: needed? DataTypes.NUMERIC? + ) + + @property + def unfiltered_x(self) -> np.ndarray: + """Get the r values for all data points.""" + return np.fromiter( + (p.r.value for p in self._items), + dtype=float, # TODO: needed? DataTypes.NUMERIC? + ) diff --git a/src/easydiffraction/datablocks/experiment/categories/diffrn/__init__.py b/src/easydiffraction/datablocks/experiment/categories/diffrn/__init__.py new file mode 100644 index 00000000..6c11ee7e --- /dev/null +++ b/src/easydiffraction/datablocks/experiment/categories/diffrn/__init__.py @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + +from easydiffraction.datablocks.experiment.categories.diffrn.default import DefaultDiffrn diff --git a/src/easydiffraction/datablocks/experiment/categories/diffrn/default.py b/src/easydiffraction/datablocks/experiment/categories/diffrn/default.py new file mode 100644 index 00000000..9635fc2e --- /dev/null +++ b/src/easydiffraction/datablocks/experiment/categories/diffrn/default.py @@ -0,0 +1,136 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Default diffraction ambient-conditions category.""" + +from __future__ import annotations + +from easydiffraction.core.category import CategoryItem +from easydiffraction.core.metadata import TypeInfo +from easydiffraction.core.validation import AttributeSpec +from easydiffraction.core.validation import RangeValidator +from easydiffraction.core.variable import NumericDescriptor +from easydiffraction.datablocks.experiment.categories.diffrn.factory import DiffrnFactory +from easydiffraction.io.cif.handler import CifHandler + + +@DiffrnFactory.register +class DefaultDiffrn(CategoryItem): + """Ambient conditions recorded during diffraction measurement.""" + + type_info = TypeInfo( + tag='default', + description='Diffraction ambient conditions', + ) + + def __init__(self) -> None: + super().__init__() + + self._ambient_temperature = NumericDescriptor( + name='ambient_temperature', + description='Mean temperature during measurement', + units='K', + value_spec=AttributeSpec( + default=None, + allow_none=True, + validator=RangeValidator(), + ), + cif_handler=CifHandler(names=['_diffrn.ambient_temperature']), + ) + + self._ambient_pressure = NumericDescriptor( + name='ambient_pressure', + description='Mean hydrostatic pressure during measurement', + units='kPa', + value_spec=AttributeSpec( + default=None, + allow_none=True, + validator=RangeValidator(), + ), + cif_handler=CifHandler(names=['_diffrn.ambient_pressure']), + ) + + self._ambient_magnetic_field = NumericDescriptor( + name='ambient_magnetic_field', + description='Mean magnetic field during measurement', + units='T', + value_spec=AttributeSpec( + default=None, + allow_none=True, + validator=RangeValidator(), + ), + cif_handler=CifHandler(names=['_diffrn.ambient_magnetic_field']), + ) + + self._ambient_electric_field = NumericDescriptor( + name='ambient_electric_field', + description='Mean electric field during measurement', + units='V/m', + value_spec=AttributeSpec( + default=None, + allow_none=True, + validator=RangeValidator(), + ), + cif_handler=CifHandler(names=['_diffrn.ambient_electric_field']), + ) + + self._identity.category_code = 'diffrn' + + # ------------------------------------------------------------------ + # Public properties + # ------------------------------------------------------------------ + + @property + def ambient_temperature(self) -> NumericDescriptor: + """ + Mean temperature during measurement (K). + + Reading this property returns the underlying + ``NumericDescriptor`` object. Assigning to it updates the value. + """ + return self._ambient_temperature + + @ambient_temperature.setter + def ambient_temperature(self, value: float) -> None: + self._ambient_temperature.value = value + + @property + def ambient_pressure(self) -> NumericDescriptor: + """ + Mean hydrostatic pressure during measurement (kPa). + + Reading this property returns the underlying + ``NumericDescriptor`` object. Assigning to it updates the value. + """ + return self._ambient_pressure + + @ambient_pressure.setter + def ambient_pressure(self, value: float) -> None: + self._ambient_pressure.value = value + + @property + def ambient_magnetic_field(self) -> NumericDescriptor: + """ + Mean magnetic field during measurement (T). + + Reading this property returns the underlying + ``NumericDescriptor`` object. Assigning to it updates the value. + """ + return self._ambient_magnetic_field + + @ambient_magnetic_field.setter + def ambient_magnetic_field(self, value: float) -> None: + self._ambient_magnetic_field.value = value + + @property + def ambient_electric_field(self) -> NumericDescriptor: + """ + Mean electric field during measurement (V/m). + + Reading this property returns the underlying + ``NumericDescriptor`` object. Assigning to it updates the value. + """ + return self._ambient_electric_field + + @ambient_electric_field.setter + def ambient_electric_field(self, value: float) -> None: + self._ambient_electric_field.value = value diff --git a/src/easydiffraction/datablocks/experiment/categories/diffrn/factory.py b/src/easydiffraction/datablocks/experiment/categories/diffrn/factory.py new file mode 100644 index 00000000..ef5fb719 --- /dev/null +++ b/src/easydiffraction/datablocks/experiment/categories/diffrn/factory.py @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Factory for diffraction ambient-conditions categories.""" + +from __future__ import annotations + +from easydiffraction.core.factory import FactoryBase + + +class DiffrnFactory(FactoryBase): + """Create diffraction ambient-conditions category instances.""" + + _default_rules = { + frozenset(): 'default', + } diff --git a/src/easydiffraction/datablocks/experiment/categories/excluded_regions/__init__.py b/src/easydiffraction/datablocks/experiment/categories/excluded_regions/__init__.py new file mode 100644 index 00000000..8d232629 --- /dev/null +++ b/src/easydiffraction/datablocks/experiment/categories/excluded_regions/__init__.py @@ -0,0 +1,9 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + +from easydiffraction.datablocks.experiment.categories.excluded_regions.default import ( + ExcludedRegion, +) +from easydiffraction.datablocks.experiment.categories.excluded_regions.default import ( + ExcludedRegions, +) diff --git a/src/easydiffraction/experiments/categories/excluded_regions.py b/src/easydiffraction/datablocks/experiment/categories/excluded_regions/default.py similarity index 60% rename from src/easydiffraction/experiments/categories/excluded_regions.py rename to src/easydiffraction/datablocks/experiment/categories/excluded_regions/default.py index 0ebce0a4..fdf130c2 100644 --- a/src/easydiffraction/experiments/categories/excluded_regions.py +++ b/src/easydiffraction/datablocks/experiment/categories/excluded_regions/default.py @@ -1,19 +1,26 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause """Exclude ranges of x from fitting/plotting (masked regions).""" +from __future__ import annotations + from typing import List import numpy as np from easydiffraction.core.category import CategoryCollection from easydiffraction.core.category import CategoryItem -from easydiffraction.core.parameters import NumericDescriptor -from easydiffraction.core.parameters import StringDescriptor +from easydiffraction.core.metadata import Compatibility +from easydiffraction.core.metadata import TypeInfo from easydiffraction.core.validation import AttributeSpec -from easydiffraction.core.validation import DataTypes from easydiffraction.core.validation import RangeValidator from easydiffraction.core.validation import RegexValidator +from easydiffraction.core.variable import NumericDescriptor +from easydiffraction.core.variable import StringDescriptor +from easydiffraction.datablocks.experiment.categories.excluded_regions.factory import ( + ExcludedRegionsFactory, +) +from easydiffraction.datablocks.experiment.item.enums import SampleFormEnum from easydiffraction.io.cif.handler import CifHandler from easydiffraction.utils.logging import console from easydiffraction.utils.utils import render_table @@ -22,111 +29,119 @@ class ExcludedRegion(CategoryItem): """Closed interval [start, end] to be excluded.""" - def __init__( - self, - *, - id=None, # TODO: rename as in the case of data points? - start=None, - end=None, - ): + def __init__(self) -> None: super().__init__() # TODO: Add point_id as for the background self._id = StringDescriptor( name='id', - description='Identifier for this excluded region.', + description='Identifier for this excluded region', value_spec=AttributeSpec( - type_=DataTypes.STRING, - value=id, default='0', # TODO: the following pattern is valid for dict key # (keywords are not checked). CIF label is less strict. # Do we need conversion between CIF and internal label? - content_validator=RegexValidator(pattern=r'^[A-Za-z0-9_]*$'), - ), - cif_handler=CifHandler( - names=[ - '_excluded_region.id', - ] + validator=RegexValidator(pattern=r'^[A-Za-z0-9_]*$'), ), + cif_handler=CifHandler(names=['_excluded_region.id']), ) self._start = NumericDescriptor( name='start', description='Start of the excluded region.', value_spec=AttributeSpec( - value=start, - type_=DataTypes.NUMERIC, default=0.0, - content_validator=RangeValidator(), - ), - cif_handler=CifHandler( - names=[ - '_excluded_region.start', - ] + validator=RangeValidator(), ), + cif_handler=CifHandler(names=['_excluded_region.start']), ) self._end = NumericDescriptor( name='end', description='End of the excluded region.', value_spec=AttributeSpec( - value=end, - type_=DataTypes.NUMERIC, default=0.0, - content_validator=RangeValidator(), - ), - cif_handler=CifHandler( - names=[ - '_excluded_region.end', - ] + validator=RangeValidator(), ), + cif_handler=CifHandler(names=['_excluded_region.end']), ) - # self._category_entry_attr_name = f'{start}-{end}' - # self._category_entry_attr_name = self.start.name - # self.name = self.start.value self._identity.category_code = 'excluded_regions' self._identity.category_entry_name = lambda: str(self._id.value) + # ------------------------------------------------------------------ + # Public properties + # ------------------------------------------------------------------ + @property - def id(self): + def id(self) -> StringDescriptor: + """ + Identifier for this excluded region. + + Reading this property returns the underlying + ``StringDescriptor`` object. Assigning to it updates the + parameter value. + """ return self._id @id.setter - def id(self, value): + def id(self, value: str) -> None: self._id.value = value @property def start(self) -> NumericDescriptor: + """ + Start of the excluded region. + + Reading this property returns the underlying + ``NumericDescriptor`` object. Assigning to it updates the + parameter value. + """ return self._start @start.setter - def start(self, value: float): + def start(self, value: float) -> None: self._start.value = value @property def end(self) -> NumericDescriptor: + """ + End of the excluded region. + + Reading this property returns the underlying + ``NumericDescriptor`` object. Assigning to it updates the + parameter value. + """ return self._end @end.setter - def end(self, value: float): + def end(self, value: float) -> None: self._end.value = value +@ExcludedRegionsFactory.register class ExcludedRegions(CategoryCollection): - """Collection of ExcludedRegion instances. + """ + Collection of ExcludedRegion instances. Excluded regions define closed intervals [start, end] on the x-axis that are to be excluded from calculations and, as a result, from fitting and plotting. """ - def __init__(self): + type_info = TypeInfo( + tag='default', + description='Excluded x-axis regions for fitting and plotting', + ) + compatibility = Compatibility( + sample_form=frozenset({SampleFormEnum.POWDER}), + ) + + def __init__(self) -> None: super().__init__(item_type=ExcludedRegion) - def _update(self, called_by_minimizer=False): + def _update(self, called_by_minimizer: bool = False) -> None: del called_by_minimizer data = self._parent.data - x = data.all_x + x = data.unfiltered_x # Start with a mask of all False (nothing excluded yet) combined_mask = np.full_like(x, fill_value=False, dtype=bool) diff --git a/src/easydiffraction/datablocks/experiment/categories/excluded_regions/factory.py b/src/easydiffraction/datablocks/experiment/categories/excluded_regions/factory.py new file mode 100644 index 00000000..e12fb0c0 --- /dev/null +++ b/src/easydiffraction/datablocks/experiment/categories/excluded_regions/factory.py @@ -0,0 +1,17 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +""" +Excluded-regions factory — delegates entirely to ``FactoryBase``. +""" + +from __future__ import annotations + +from easydiffraction.core.factory import FactoryBase + + +class ExcludedRegionsFactory(FactoryBase): + """Create excluded-regions collections by tag.""" + + _default_rules = { + frozenset(): 'default', + } diff --git a/src/easydiffraction/datablocks/experiment/categories/experiment_type/__init__.py b/src/easydiffraction/datablocks/experiment/categories/experiment_type/__init__.py new file mode 100644 index 00000000..197b5510 --- /dev/null +++ b/src/easydiffraction/datablocks/experiment/categories/experiment_type/__init__.py @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + +from easydiffraction.datablocks.experiment.categories.experiment_type.default import ExperimentType diff --git a/src/easydiffraction/datablocks/experiment/categories/experiment_type/default.py b/src/easydiffraction/datablocks/experiment/categories/experiment_type/default.py new file mode 100644 index 00000000..1b9d3811 --- /dev/null +++ b/src/easydiffraction/datablocks/experiment/categories/experiment_type/default.py @@ -0,0 +1,142 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +""" +Experiment type descriptor (form, beam, probe, scattering). + +This lightweight container stores the categorical attributes defining an +experiment configuration and handles CIF serialization via +``CifHandler``. +""" + +from __future__ import annotations + +from easydiffraction.core.category import CategoryItem +from easydiffraction.core.metadata import TypeInfo +from easydiffraction.core.validation import AttributeSpec +from easydiffraction.core.validation import MembershipValidator +from easydiffraction.core.variable import StringDescriptor +from easydiffraction.datablocks.experiment.categories.experiment_type.factory import ( + ExperimentTypeFactory, +) +from easydiffraction.datablocks.experiment.item.enums import BeamModeEnum +from easydiffraction.datablocks.experiment.item.enums import RadiationProbeEnum +from easydiffraction.datablocks.experiment.item.enums import SampleFormEnum +from easydiffraction.datablocks.experiment.item.enums import ScatteringTypeEnum +from easydiffraction.io.cif.handler import CifHandler + + +@ExperimentTypeFactory.register +class ExperimentType(CategoryItem): + """Container of attributes defining the experiment type.""" + + type_info = TypeInfo( + tag='default', + description='Experiment type descriptor', + ) + + def __init__(self) -> None: + super().__init__() + + self._sample_form = StringDescriptor( + name='sample_form', + description='Powder diffraction or single crystal diffraction', + value_spec=AttributeSpec( + default=SampleFormEnum.default().value, + validator=MembershipValidator(allowed=[member.value for member in SampleFormEnum]), + ), + cif_handler=CifHandler(names=['_expt_type.sample_form']), + ) + + self._beam_mode = StringDescriptor( + name='beam_mode', + description='Constant wavelength (CW) or time-of-flight (TOF) measurement', + value_spec=AttributeSpec( + default=BeamModeEnum.default().value, + validator=MembershipValidator(allowed=[member.value for member in BeamModeEnum]), + ), + cif_handler=CifHandler(names=['_expt_type.beam_mode']), + ) + self._radiation_probe = StringDescriptor( + name='radiation_probe', + description='Neutron or X-ray diffraction measurement', + value_spec=AttributeSpec( + default=RadiationProbeEnum.default().value, + validator=MembershipValidator( + allowed=[member.value for member in RadiationProbeEnum] + ), + ), + cif_handler=CifHandler(names=['_expt_type.radiation_probe']), + ) + self._scattering_type = StringDescriptor( + name='scattering_type', + description='Conventional Bragg diffraction or total scattering (PDF)', + value_spec=AttributeSpec( + default=ScatteringTypeEnum.default().value, + validator=MembershipValidator( + allowed=[member.value for member in ScatteringTypeEnum] + ), + ), + cif_handler=CifHandler(names=['_expt_type.scattering_type']), + ) + + self._identity.category_code = 'expt_type' + + # ------------------------------------------------------------------ + # Private setters (used by factories and loaders only) + # ------------------------------------------------------------------ + + def _set_sample_form(self, value: str) -> None: + self._sample_form.value = value + + def _set_beam_mode(self, value: str) -> None: + self._beam_mode.value = value + + def _set_radiation_probe(self, value: str) -> None: + self._radiation_probe.value = value + + def _set_scattering_type(self, value: str) -> None: + self._scattering_type.value = value + + # ------------------------------------------------------------------ + # Public read-only properties + # ------------------------------------------------------------------ + + @property + def sample_form(self) -> StringDescriptor: + """ + Powder diffraction or single crystal diffraction. + + Reading this property returns the underlying + ``StringDescriptor`` object. + """ + return self._sample_form + + @property + def beam_mode(self) -> StringDescriptor: + """ + Constant wavelength (CW) or time-of-flight (TOF) measurement. + + Reading this property returns the underlying + ``StringDescriptor`` object. + """ + return self._beam_mode + + @property + def radiation_probe(self) -> StringDescriptor: + """ + Neutron or X-ray diffraction measurement. + + Reading this property returns the underlying + ``StringDescriptor`` object. + """ + return self._radiation_probe + + @property + def scattering_type(self) -> StringDescriptor: + """ + Conventional Bragg diffraction or total scattering (PDF). + + Reading this property returns the underlying + ``StringDescriptor`` object. + """ + return self._scattering_type diff --git a/src/easydiffraction/datablocks/experiment/categories/experiment_type/factory.py b/src/easydiffraction/datablocks/experiment/categories/experiment_type/factory.py new file mode 100644 index 00000000..05f0d2d9 --- /dev/null +++ b/src/easydiffraction/datablocks/experiment/categories/experiment_type/factory.py @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Experiment-type factory — delegates entirely to ``FactoryBase``.""" + +from __future__ import annotations + +from easydiffraction.core.factory import FactoryBase + + +class ExperimentTypeFactory(FactoryBase): + """Create experiment-type descriptors by tag.""" + + _default_rules = { + frozenset(): 'default', + } diff --git a/src/easydiffraction/datablocks/experiment/categories/extinction/__init__.py b/src/easydiffraction/datablocks/experiment/categories/extinction/__init__.py new file mode 100644 index 00000000..3e6fa39a --- /dev/null +++ b/src/easydiffraction/datablocks/experiment/categories/extinction/__init__.py @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + +from easydiffraction.datablocks.experiment.categories.extinction.shelx import ShelxExtinction diff --git a/src/easydiffraction/datablocks/experiment/categories/extinction/factory.py b/src/easydiffraction/datablocks/experiment/categories/extinction/factory.py new file mode 100644 index 00000000..4e4bd9ed --- /dev/null +++ b/src/easydiffraction/datablocks/experiment/categories/extinction/factory.py @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Extinction factory — delegates entirely to ``FactoryBase``.""" + +from __future__ import annotations + +from easydiffraction.core.factory import FactoryBase + + +class ExtinctionFactory(FactoryBase): + """Create extinction correction models by tag.""" + + _default_rules = { + frozenset(): 'shelx', + } diff --git a/src/easydiffraction/datablocks/experiment/categories/extinction/shelx.py b/src/easydiffraction/datablocks/experiment/categories/extinction/shelx.py new file mode 100644 index 00000000..dd736a1a --- /dev/null +++ b/src/easydiffraction/datablocks/experiment/categories/extinction/shelx.py @@ -0,0 +1,94 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Shelx-style isotropic extinction correction.""" + +from __future__ import annotations + +from easydiffraction.core.category import CategoryItem +from easydiffraction.core.metadata import Compatibility +from easydiffraction.core.metadata import TypeInfo +from easydiffraction.core.validation import AttributeSpec +from easydiffraction.core.validation import RangeValidator +from easydiffraction.core.variable import Parameter +from easydiffraction.datablocks.experiment.categories.extinction.factory import ExtinctionFactory +from easydiffraction.datablocks.experiment.item.enums import SampleFormEnum +from easydiffraction.io.cif.handler import CifHandler + + +@ExtinctionFactory.register +class ShelxExtinction(CategoryItem): + """Shelx-style extinction correction for single crystals.""" + + type_info = TypeInfo( + tag='shelx', + description='Shelx-style isotropic extinction correction', + ) + compatibility = Compatibility( + sample_form=frozenset({SampleFormEnum.SINGLE_CRYSTAL}), + ) + + def __init__(self) -> None: + super().__init__() + + self._mosaicity = Parameter( + name='mosaicity', + description='Mosaicity value for extinction correction', + units='deg', + value_spec=AttributeSpec( + default=1.0, + validator=RangeValidator(), + ), + cif_handler=CifHandler( + names=[ + '_extinction.mosaicity', + ] + ), + ) + self._radius = Parameter( + name='radius', + description='Crystal radius for extinction correction', + units='µm', + value_spec=AttributeSpec( + default=1.0, + validator=RangeValidator(), + ), + cif_handler=CifHandler( + names=[ + '_extinction.radius', + ] + ), + ) + + self._identity.category_code = 'extinction' + + # ------------------------------------------------------------------ + # Public properties + # ------------------------------------------------------------------ + + @property + def mosaicity(self) -> Parameter: + """ + Mosaicity value for extinction correction (deg). + + Reading this property returns the underlying ``Parameter`` + object. Assigning to it updates the parameter value. + """ + return self._mosaicity + + @mosaicity.setter + def mosaicity(self, value: float) -> None: + self._mosaicity.value = value + + @property + def radius(self) -> Parameter: + """ + Crystal radius for extinction correction (µm). + + Reading this property returns the underlying ``Parameter`` + object. Assigning to it updates the parameter value. + """ + return self._radius + + @radius.setter + def radius(self, value: float) -> None: + self._radius.value = value diff --git a/src/easydiffraction/datablocks/experiment/categories/instrument/__init__.py b/src/easydiffraction/datablocks/experiment/categories/instrument/__init__.py new file mode 100644 index 00000000..d12b40c3 --- /dev/null +++ b/src/easydiffraction/datablocks/experiment/categories/instrument/__init__.py @@ -0,0 +1,7 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + +from easydiffraction.datablocks.experiment.categories.instrument.cwl import CwlPdInstrument +from easydiffraction.datablocks.experiment.categories.instrument.cwl import CwlScInstrument +from easydiffraction.datablocks.experiment.categories.instrument.tof import TofPdInstrument +from easydiffraction.datablocks.experiment.categories.instrument.tof import TofScInstrument diff --git a/src/easydiffraction/experiments/categories/instrument/base.py b/src/easydiffraction/datablocks/experiment/categories/instrument/base.py similarity index 72% rename from src/easydiffraction/experiments/categories/instrument/base.py rename to src/easydiffraction/datablocks/experiment/categories/instrument/base.py index 0d1c04d5..a2568884 100644 --- a/src/easydiffraction/experiments/categories/instrument/base.py +++ b/src/easydiffraction/datablocks/experiment/categories/instrument/base.py @@ -1,6 +1,7 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -"""Instrument category base definitions for CWL/TOF instruments. +""" +Instrument category base definitions for CWL/TOF instruments. This module provides the shared parent used by concrete instrument implementations under the instrument category. @@ -12,7 +13,8 @@ class InstrumentBase(CategoryItem): - """Base class for instrument category items. + """ + Base class for instrument category items. This class sets the common ``category_code`` and is used as a base for concrete CWL/TOF instrument definitions. diff --git a/src/easydiffraction/datablocks/experiment/categories/instrument/cwl.py b/src/easydiffraction/datablocks/experiment/categories/instrument/cwl.py new file mode 100644 index 00000000..3a3628bd --- /dev/null +++ b/src/easydiffraction/datablocks/experiment/categories/instrument/cwl.py @@ -0,0 +1,127 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + +from easydiffraction.core.metadata import CalculatorSupport +from easydiffraction.core.metadata import Compatibility +from easydiffraction.core.metadata import TypeInfo +from easydiffraction.core.validation import AttributeSpec +from easydiffraction.core.validation import RangeValidator +from easydiffraction.core.variable import Parameter +from easydiffraction.datablocks.experiment.categories.instrument.base import InstrumentBase +from easydiffraction.datablocks.experiment.categories.instrument.factory import InstrumentFactory +from easydiffraction.datablocks.experiment.item.enums import BeamModeEnum +from easydiffraction.datablocks.experiment.item.enums import CalculatorEnum +from easydiffraction.datablocks.experiment.item.enums import SampleFormEnum +from easydiffraction.datablocks.experiment.item.enums import ScatteringTypeEnum +from easydiffraction.io.cif.handler import CifHandler + + +class CwlInstrumentBase(InstrumentBase): + """Base class for constant-wavelength instruments.""" + + def __init__(self) -> None: + super().__init__() + + self._setup_wavelength: Parameter = Parameter( + name='wavelength', + description='Incident neutron or X-ray wavelength', + units='Å', + value_spec=AttributeSpec( + default=1.5406, + validator=RangeValidator(), + ), + cif_handler=CifHandler( + names=[ + '_instr.wavelength', + ] + ), + ) + + @property + def setup_wavelength(self) -> Parameter: + """ + Incident neutron or X-ray wavelength (Å). + + Reading this property returns the underlying ``Parameter`` + object. Assigning to it updates the parameter value. + """ + return self._setup_wavelength + + @setup_wavelength.setter + def setup_wavelength(self, value: float) -> None: + self._setup_wavelength.value = value + + +@InstrumentFactory.register +class CwlScInstrument(CwlInstrumentBase): + """CW single-crystal diffractometer.""" + + type_info = TypeInfo( + tag='cwl-sc', + description='CW single-crystal diffractometer', + ) + compatibility = Compatibility( + scattering_type=frozenset({ScatteringTypeEnum.BRAGG}), + beam_mode=frozenset({BeamModeEnum.CONSTANT_WAVELENGTH}), + sample_form=frozenset({SampleFormEnum.SINGLE_CRYSTAL}), + ) + calculator_support = CalculatorSupport( + calculators=frozenset({CalculatorEnum.CRYSPY}), + ) + + def __init__(self) -> None: + super().__init__() + + +@InstrumentFactory.register +class CwlPdInstrument(CwlInstrumentBase): + """CW powder diffractometer.""" + + type_info = TypeInfo( + tag='cwl-pd', + description='CW powder diffractometer', + ) + compatibility = Compatibility( + scattering_type=frozenset({ScatteringTypeEnum.BRAGG, ScatteringTypeEnum.TOTAL}), + beam_mode=frozenset({BeamModeEnum.CONSTANT_WAVELENGTH}), + sample_form=frozenset({SampleFormEnum.POWDER}), + ) + calculator_support = CalculatorSupport( + calculators=frozenset({ + CalculatorEnum.CRYSPY, + CalculatorEnum.CRYSFML, + CalculatorEnum.PDFFIT, + }), + ) + + def __init__(self) -> None: + super().__init__() + + self._calib_twotheta_offset: Parameter = Parameter( + name='twotheta_offset', + description='Instrument misalignment offset', + units='deg', + value_spec=AttributeSpec( + default=0.0, + validator=RangeValidator(), + ), + cif_handler=CifHandler( + names=[ + '_instr.2theta_offset', + ] + ), + ) + + @property + def calib_twotheta_offset(self) -> Parameter: + """ + Instrument misalignment offset (deg). + + Reading this property returns the underlying ``Parameter`` + object. Assigning to it updates the parameter value. + """ + return self._calib_twotheta_offset + + @calib_twotheta_offset.setter + def calib_twotheta_offset(self, value: float) -> None: + self._calib_twotheta_offset.value = value diff --git a/src/easydiffraction/datablocks/experiment/categories/instrument/factory.py b/src/easydiffraction/datablocks/experiment/categories/instrument/factory.py new file mode 100644 index 00000000..fce8ad5c --- /dev/null +++ b/src/easydiffraction/datablocks/experiment/categories/instrument/factory.py @@ -0,0 +1,30 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Instrument factory — delegates to ``FactoryBase``.""" + +from easydiffraction.core.factory import FactoryBase +from easydiffraction.datablocks.experiment.item.enums import BeamModeEnum +from easydiffraction.datablocks.experiment.item.enums import SampleFormEnum + + +class InstrumentFactory(FactoryBase): + """Create instrument instances for supported modes.""" + + _default_rules = { + frozenset({ + ('beam_mode', BeamModeEnum.CONSTANT_WAVELENGTH), + ('sample_form', SampleFormEnum.POWDER), + }): 'cwl-pd', + frozenset({ + ('beam_mode', BeamModeEnum.CONSTANT_WAVELENGTH), + ('sample_form', SampleFormEnum.SINGLE_CRYSTAL), + }): 'cwl-sc', + frozenset({ + ('beam_mode', BeamModeEnum.TIME_OF_FLIGHT), + ('sample_form', SampleFormEnum.POWDER), + }): 'tof-pd', + frozenset({ + ('beam_mode', BeamModeEnum.TIME_OF_FLIGHT), + ('sample_form', SampleFormEnum.SINGLE_CRYSTAL), + }): 'tof-sc', + } diff --git a/src/easydiffraction/datablocks/experiment/categories/instrument/tof.py b/src/easydiffraction/datablocks/experiment/categories/instrument/tof.py new file mode 100644 index 00000000..7e1db98e --- /dev/null +++ b/src/easydiffraction/datablocks/experiment/categories/instrument/tof.py @@ -0,0 +1,179 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + +from easydiffraction.core.metadata import CalculatorSupport +from easydiffraction.core.metadata import Compatibility +from easydiffraction.core.metadata import TypeInfo +from easydiffraction.core.validation import AttributeSpec +from easydiffraction.core.validation import RangeValidator +from easydiffraction.core.variable import Parameter +from easydiffraction.datablocks.experiment.categories.instrument.base import InstrumentBase +from easydiffraction.datablocks.experiment.categories.instrument.factory import InstrumentFactory +from easydiffraction.datablocks.experiment.item.enums import BeamModeEnum +from easydiffraction.datablocks.experiment.item.enums import CalculatorEnum +from easydiffraction.datablocks.experiment.item.enums import SampleFormEnum +from easydiffraction.datablocks.experiment.item.enums import ScatteringTypeEnum +from easydiffraction.io.cif.handler import CifHandler + + +@InstrumentFactory.register +class TofScInstrument(InstrumentBase): + """TOF single-crystal diffractometer.""" + + type_info = TypeInfo( + tag='tof-sc', + description='TOF single-crystal diffractometer', + ) + compatibility = Compatibility( + scattering_type=frozenset({ScatteringTypeEnum.BRAGG}), + beam_mode=frozenset({BeamModeEnum.TIME_OF_FLIGHT}), + sample_form=frozenset({SampleFormEnum.SINGLE_CRYSTAL}), + ) + calculator_support = CalculatorSupport( + calculators=frozenset({CalculatorEnum.CRYSPY}), + ) + + def __init__(self) -> None: + super().__init__() + + +@InstrumentFactory.register +class TofPdInstrument(InstrumentBase): + """TOF powder diffractometer.""" + + type_info = TypeInfo( + tag='tof-pd', + description='TOF powder diffractometer', + ) + compatibility = Compatibility( + scattering_type=frozenset({ScatteringTypeEnum.BRAGG}), + beam_mode=frozenset({BeamModeEnum.TIME_OF_FLIGHT}), + sample_form=frozenset({SampleFormEnum.POWDER}), + ) + calculator_support = CalculatorSupport( + calculators=frozenset({CalculatorEnum.CRYSPY, CalculatorEnum.CRYSFML}), + ) + + def __init__(self) -> None: + super().__init__() + + self._setup_twotheta_bank: Parameter = Parameter( + name='twotheta_bank', + description='Detector bank position', + units='deg', + value_spec=AttributeSpec( + default=150.0, + validator=RangeValidator(), + ), + cif_handler=CifHandler(names=['_instr.2theta_bank']), + ) + self._calib_d_to_tof_offset: Parameter = Parameter( + name='d_to_tof_offset', + description='TOF offset', + units='µs', + value_spec=AttributeSpec( + default=0.0, + validator=RangeValidator(), + ), + cif_handler=CifHandler(names=['_instr.d_to_tof_offset']), + ) + self._calib_d_to_tof_linear: Parameter = Parameter( + name='d_to_tof_linear', + description='TOF linear conversion', + units='µs/Å', + value_spec=AttributeSpec( + default=10000.0, + validator=RangeValidator(), + ), + cif_handler=CifHandler(names=['_instr.d_to_tof_linear']), + ) + self._calib_d_to_tof_quad: Parameter = Parameter( + name='d_to_tof_quad', + description='TOF quadratic correction', + units='µs/Ų', + value_spec=AttributeSpec( + default=-0.00001, # TODO: Fix CrysPy to accept 0 + validator=RangeValidator(), + ), + cif_handler=CifHandler(names=['_instr.d_to_tof_quad']), + ) + self._calib_d_to_tof_recip: Parameter = Parameter( + name='d_to_tof_recip', + description='TOF reciprocal velocity correction', + units='µs·Å', + value_spec=AttributeSpec( + default=0.0, + validator=RangeValidator(), + ), + cif_handler=CifHandler(names=['_instr.d_to_tof_recip']), + ) + + @property + def setup_twotheta_bank(self) -> Parameter: + """ + Detector bank position (deg). + + Reading this property returns the underlying ``Parameter`` + object. Assigning to it updates the parameter value. + """ + return self._setup_twotheta_bank + + @setup_twotheta_bank.setter + def setup_twotheta_bank(self, value: float) -> None: + self._setup_twotheta_bank.value = value + + @property + def calib_d_to_tof_offset(self) -> Parameter: + """ + TOF offset (µs). + + Reading this property returns the underlying ``Parameter`` + object. Assigning to it updates the parameter value. + """ + return self._calib_d_to_tof_offset + + @calib_d_to_tof_offset.setter + def calib_d_to_tof_offset(self, value: float) -> None: + self._calib_d_to_tof_offset.value = value + + @property + def calib_d_to_tof_linear(self) -> Parameter: + """ + TOF linear conversion (µs/Å). + + Reading this property returns the underlying ``Parameter`` + object. Assigning to it updates the parameter value. + """ + return self._calib_d_to_tof_linear + + @calib_d_to_tof_linear.setter + def calib_d_to_tof_linear(self, value: float) -> None: + self._calib_d_to_tof_linear.value = value + + @property + def calib_d_to_tof_quad(self) -> Parameter: + """ + TOF quadratic correction (µs/Ų). + + Reading this property returns the underlying ``Parameter`` + object. Assigning to it updates the parameter value. + """ + return self._calib_d_to_tof_quad + + @calib_d_to_tof_quad.setter + def calib_d_to_tof_quad(self, value: float) -> None: + self._calib_d_to_tof_quad.value = value + + @property + def calib_d_to_tof_recip(self) -> Parameter: + """ + TOF reciprocal velocity correction (µs·Å). + + Reading this property returns the underlying ``Parameter`` + object. Assigning to it updates the parameter value. + """ + return self._calib_d_to_tof_recip + + @calib_d_to_tof_recip.setter + def calib_d_to_tof_recip(self, value: float) -> None: + self._calib_d_to_tof_recip.value = value diff --git a/src/easydiffraction/datablocks/experiment/categories/linked_crystal/__init__.py b/src/easydiffraction/datablocks/experiment/categories/linked_crystal/__init__.py new file mode 100644 index 00000000..4b93121b --- /dev/null +++ b/src/easydiffraction/datablocks/experiment/categories/linked_crystal/__init__.py @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + +from easydiffraction.datablocks.experiment.categories.linked_crystal.default import LinkedCrystal diff --git a/src/easydiffraction/datablocks/experiment/categories/linked_crystal/default.py b/src/easydiffraction/datablocks/experiment/categories/linked_crystal/default.py new file mode 100644 index 00000000..d441aa9c --- /dev/null +++ b/src/easydiffraction/datablocks/experiment/categories/linked_crystal/default.py @@ -0,0 +1,89 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Default linked-crystal reference (id + scale).""" + +from __future__ import annotations + +from easydiffraction.core.category import CategoryItem +from easydiffraction.core.metadata import Compatibility +from easydiffraction.core.metadata import TypeInfo +from easydiffraction.core.validation import AttributeSpec +from easydiffraction.core.validation import RangeValidator +from easydiffraction.core.validation import RegexValidator +from easydiffraction.core.variable import Parameter +from easydiffraction.core.variable import StringDescriptor +from easydiffraction.datablocks.experiment.categories.linked_crystal.factory import ( + LinkedCrystalFactory, +) +from easydiffraction.datablocks.experiment.item.enums import SampleFormEnum +from easydiffraction.io.cif.handler import CifHandler + + +@LinkedCrystalFactory.register +class LinkedCrystal(CategoryItem): + """Linked crystal reference for single-crystal diffraction.""" + + type_info = TypeInfo( + tag='default', + description='Crystal reference with id and scale factor', + ) + compatibility = Compatibility( + sample_form=frozenset({SampleFormEnum.SINGLE_CRYSTAL}), + ) + + def __init__(self) -> None: + super().__init__() + + self._id = StringDescriptor( + name='id', + description='Identifier of the linked crystal', + value_spec=AttributeSpec( + default='Si', + validator=RegexValidator(pattern=r'^[A-Za-z_][A-Za-z0-9_]*$'), + ), + cif_handler=CifHandler(names=['_sc_crystal_block.id']), + ) + self._scale = Parameter( + name='scale', + description='Scale factor of the linked crystal', + value_spec=AttributeSpec( + default=1.0, + validator=RangeValidator(), + ), + cif_handler=CifHandler(names=['_sc_crystal_block.scale']), + ) + + self._identity.category_code = 'linked_crystal' + + # ------------------------------------------------------------------ + # Public properties + # ------------------------------------------------------------------ + + @property + def id(self) -> StringDescriptor: + """ + Identifier of the linked crystal. + + Reading this property returns the underlying + ``StringDescriptor`` object. Assigning to it updates the + parameter value. + """ + return self._id + + @id.setter + def id(self, value: str) -> None: + self._id.value = value + + @property + def scale(self) -> Parameter: + """ + Scale factor of the linked crystal. + + Reading this property returns the underlying ``Parameter`` + object. Assigning to it updates the parameter value. + """ + return self._scale + + @scale.setter + def scale(self, value: float) -> None: + self._scale.value = value diff --git a/src/easydiffraction/datablocks/experiment/categories/linked_crystal/factory.py b/src/easydiffraction/datablocks/experiment/categories/linked_crystal/factory.py new file mode 100644 index 00000000..b34b8073 --- /dev/null +++ b/src/easydiffraction/datablocks/experiment/categories/linked_crystal/factory.py @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Linked-crystal factory — delegates entirely to ``FactoryBase``.""" + +from __future__ import annotations + +from easydiffraction.core.factory import FactoryBase + + +class LinkedCrystalFactory(FactoryBase): + """Create linked-crystal references by tag.""" + + _default_rules = { + frozenset(): 'default', + } diff --git a/src/easydiffraction/datablocks/experiment/categories/linked_phases/__init__.py b/src/easydiffraction/datablocks/experiment/categories/linked_phases/__init__.py new file mode 100644 index 00000000..dda7d445 --- /dev/null +++ b/src/easydiffraction/datablocks/experiment/categories/linked_phases/__init__.py @@ -0,0 +1,5 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + +from easydiffraction.datablocks.experiment.categories.linked_phases.default import LinkedPhase +from easydiffraction.datablocks.experiment.categories.linked_phases.default import LinkedPhases diff --git a/src/easydiffraction/datablocks/experiment/categories/linked_phases/default.py b/src/easydiffraction/datablocks/experiment/categories/linked_phases/default.py new file mode 100644 index 00000000..97d03c66 --- /dev/null +++ b/src/easydiffraction/datablocks/experiment/categories/linked_phases/default.py @@ -0,0 +1,99 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Linked phases allow combining phases with scale factors.""" + +from __future__ import annotations + +from easydiffraction.core.category import CategoryCollection +from easydiffraction.core.category import CategoryItem +from easydiffraction.core.metadata import Compatibility +from easydiffraction.core.metadata import TypeInfo +from easydiffraction.core.validation import AttributeSpec +from easydiffraction.core.validation import RangeValidator +from easydiffraction.core.validation import RegexValidator +from easydiffraction.core.variable import Parameter +from easydiffraction.core.variable import StringDescriptor +from easydiffraction.datablocks.experiment.categories.linked_phases.factory import ( + LinkedPhasesFactory, +) +from easydiffraction.datablocks.experiment.item.enums import SampleFormEnum +from easydiffraction.io.cif.handler import CifHandler + + +class LinkedPhase(CategoryItem): + """Link to a phase by id with a scale factor.""" + + def __init__(self) -> None: + super().__init__() + + self._id = StringDescriptor( + name='id', + description='Identifier of the linked phase', + value_spec=AttributeSpec( + default='Si', + validator=RegexValidator(pattern=r'^[A-Za-z_][A-Za-z0-9_]*$'), + ), + cif_handler=CifHandler(names=['_pd_phase_block.id']), + ) + self._scale = Parameter( + name='scale', + description='Scale factor of the linked phase.', + value_spec=AttributeSpec( + default=1.0, + validator=RangeValidator(), + ), + cif_handler=CifHandler(names=['_pd_phase_block.scale']), + ) + + self._identity.category_code = 'linked_phases' + self._identity.category_entry_name = lambda: str(self.id.value) + + # ------------------------------------------------------------------ + # Public properties + # ------------------------------------------------------------------ + + @property + def id(self) -> StringDescriptor: + """ + Identifier of the linked phase. + + Reading this property returns the underlying + ``StringDescriptor`` object. Assigning to it updates the + parameter value. + """ + return self._id + + @id.setter + def id(self, value: str) -> None: + self._id.value = value + + @property + def scale(self) -> Parameter: + """ + Scale factor of the linked phase. + + Reading this property returns the underlying ``Parameter`` + object. Assigning to it updates the parameter value. + """ + return self._scale + + @scale.setter + def scale(self, value: float) -> None: + self._scale.value = value + + +@LinkedPhasesFactory.register +class LinkedPhases(CategoryCollection): + """Collection of LinkedPhase instances.""" + + type_info = TypeInfo( + tag='default', + description='Phase references with scale factors', + ) + compatibility = Compatibility( + sample_form=frozenset({SampleFormEnum.POWDER}), + ) + + def __init__(self) -> None: + """Create an empty collection of linked phases.""" + super().__init__(item_type=LinkedPhase) diff --git a/src/easydiffraction/datablocks/experiment/categories/linked_phases/factory.py b/src/easydiffraction/datablocks/experiment/categories/linked_phases/factory.py new file mode 100644 index 00000000..56970ee8 --- /dev/null +++ b/src/easydiffraction/datablocks/experiment/categories/linked_phases/factory.py @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Linked-phases factory — delegates entirely to ``FactoryBase``.""" + +from __future__ import annotations + +from easydiffraction.core.factory import FactoryBase + + +class LinkedPhasesFactory(FactoryBase): + """Create linked-phases collections by tag.""" + + _default_rules = { + frozenset(): 'default', + } diff --git a/src/easydiffraction/datablocks/experiment/categories/peak/__init__.py b/src/easydiffraction/datablocks/experiment/categories/peak/__init__.py new file mode 100644 index 00000000..667bafb4 --- /dev/null +++ b/src/easydiffraction/datablocks/experiment/categories/peak/__init__.py @@ -0,0 +1,10 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + +from easydiffraction.datablocks.experiment.categories.peak.cwl import CwlPseudoVoigt +from easydiffraction.datablocks.experiment.categories.peak.cwl import CwlSplitPseudoVoigt +from easydiffraction.datablocks.experiment.categories.peak.cwl import CwlThompsonCoxHastings +from easydiffraction.datablocks.experiment.categories.peak.tof import TofPseudoVoigt +from easydiffraction.datablocks.experiment.categories.peak.tof import TofPseudoVoigtBackToBack +from easydiffraction.datablocks.experiment.categories.peak.tof import TofPseudoVoigtIkedaCarpenter +from easydiffraction.datablocks.experiment.categories.peak.total import TotalGaussianDampedSinc diff --git a/src/easydiffraction/experiments/categories/peak/base.py b/src/easydiffraction/datablocks/experiment/categories/peak/base.py similarity index 75% rename from src/easydiffraction/experiments/categories/peak/base.py rename to src/easydiffraction/datablocks/experiment/categories/peak/base.py index 5f2654a7..050411b1 100644 --- a/src/easydiffraction/experiments/categories/peak/base.py +++ b/src/easydiffraction/datablocks/experiment/categories/peak/base.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause """Base class for peak profile categories.""" diff --git a/src/easydiffraction/datablocks/experiment/categories/peak/cwl.py b/src/easydiffraction/datablocks/experiment/categories/peak/cwl.py new file mode 100644 index 00000000..a2b4f63b --- /dev/null +++ b/src/easydiffraction/datablocks/experiment/categories/peak/cwl.py @@ -0,0 +1,88 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Constant-wavelength peak profile classes.""" + +from easydiffraction.core.metadata import CalculatorSupport +from easydiffraction.core.metadata import Compatibility +from easydiffraction.core.metadata import TypeInfo +from easydiffraction.datablocks.experiment.categories.peak.base import PeakBase +from easydiffraction.datablocks.experiment.categories.peak.cwl_mixins import CwlBroadeningMixin +from easydiffraction.datablocks.experiment.categories.peak.cwl_mixins import ( + EmpiricalAsymmetryMixin, +) +from easydiffraction.datablocks.experiment.categories.peak.cwl_mixins import FcjAsymmetryMixin +from easydiffraction.datablocks.experiment.categories.peak.factory import PeakFactory +from easydiffraction.datablocks.experiment.item.enums import BeamModeEnum +from easydiffraction.datablocks.experiment.item.enums import CalculatorEnum +from easydiffraction.datablocks.experiment.item.enums import ScatteringTypeEnum + + +@PeakFactory.register +class CwlPseudoVoigt( + PeakBase, + CwlBroadeningMixin, +): + """Constant-wavelength pseudo-Voigt peak shape.""" + + type_info = TypeInfo( + tag='pseudo-voigt', + description='Pseudo-Voigt profile', + ) + compatibility = Compatibility( + scattering_type=frozenset({ScatteringTypeEnum.BRAGG}), + beam_mode=frozenset({BeamModeEnum.CONSTANT_WAVELENGTH}), + ) + calculator_support = CalculatorSupport( + calculators=frozenset({CalculatorEnum.CRYSPY, CalculatorEnum.CRYSFML}), + ) + + def __init__(self) -> None: + super().__init__() + + +@PeakFactory.register +class CwlSplitPseudoVoigt( + PeakBase, + CwlBroadeningMixin, + EmpiricalAsymmetryMixin, +): + """Split pseudo-Voigt (empirical asymmetry) for CWL mode.""" + + type_info = TypeInfo( + tag='split pseudo-voigt', + description='Split pseudo-Voigt with empirical asymmetry correction', + ) + compatibility = Compatibility( + scattering_type=frozenset({ScatteringTypeEnum.BRAGG}), + beam_mode=frozenset({BeamModeEnum.CONSTANT_WAVELENGTH}), + ) + calculator_support = CalculatorSupport( + calculators=frozenset({CalculatorEnum.CRYSPY, CalculatorEnum.CRYSFML}), + ) + + def __init__(self) -> None: + super().__init__() + + +@PeakFactory.register +class CwlThompsonCoxHastings( + PeakBase, + CwlBroadeningMixin, + FcjAsymmetryMixin, +): + """Thompson–Cox–Hastings with FCJ asymmetry for CWL mode.""" + + type_info = TypeInfo( + tag='thompson-cox-hastings', + description='Thompson-Cox-Hastings with FCJ asymmetry correction', + ) + compatibility = Compatibility( + scattering_type=frozenset({ScatteringTypeEnum.BRAGG}), + beam_mode=frozenset({BeamModeEnum.CONSTANT_WAVELENGTH}), + ) + calculator_support = CalculatorSupport( + calculators=frozenset({CalculatorEnum.CRYSPY, CalculatorEnum.CRYSFML}), + ) + + def __init__(self) -> None: + super().__init__() diff --git a/src/easydiffraction/datablocks/experiment/categories/peak/cwl_mixins.py b/src/easydiffraction/datablocks/experiment/categories/peak/cwl_mixins.py new file mode 100644 index 00000000..6e4f29c8 --- /dev/null +++ b/src/easydiffraction/datablocks/experiment/categories/peak/cwl_mixins.py @@ -0,0 +1,314 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +""" +Constant-wavelength (CWL) peak-profile component classes. + +This module provides classes that add broadening and asymmetry +parameters. They are composed into concrete peak classes elsewhere via +multiple inheritance. +""" + +from easydiffraction.core.validation import AttributeSpec +from easydiffraction.core.validation import RangeValidator +from easydiffraction.core.variable import Parameter +from easydiffraction.io.cif.handler import CifHandler + + +class CwlBroadeningMixin: + """CWL Gaussian and Lorentz broadening parameters.""" + + def __init__(self) -> None: + super().__init__() + + self._broad_gauss_u: Parameter = Parameter( + name='broad_gauss_u', + description='Gaussian broadening from sample size and resolution', + units='deg²', + value_spec=AttributeSpec( + default=0.01, + validator=RangeValidator(), + ), + cif_handler=CifHandler(names=['_peak.broad_gauss_u']), + ) + self._broad_gauss_v: Parameter = Parameter( + name='broad_gauss_v', + description='Gaussian broadening instrumental contribution', + units='deg²', + value_spec=AttributeSpec( + default=-0.01, + validator=RangeValidator(), + ), + cif_handler=CifHandler(names=['_peak.broad_gauss_v']), + ) + self._broad_gauss_w: Parameter = Parameter( + name='broad_gauss_w', + description='Gaussian broadening instrumental contribution', + units='deg²', + value_spec=AttributeSpec( + default=0.02, + validator=RangeValidator(), + ), + cif_handler=CifHandler(names=['_peak.broad_gauss_w']), + ) + self._broad_lorentz_x: Parameter = Parameter( + name='broad_lorentz_x', + description='Lorentzian broadening from sample strain effects', + units='deg', + value_spec=AttributeSpec( + default=0.0, + validator=RangeValidator(), + ), + cif_handler=CifHandler(names=['_peak.broad_lorentz_x']), + ) + self._broad_lorentz_y: Parameter = Parameter( + name='broad_lorentz_y', + description='Lorentzian broadening from microstructural defects', + units='deg', + value_spec=AttributeSpec( + default=0.0, + validator=RangeValidator(), + ), + cif_handler=CifHandler(names=['_peak.broad_lorentz_y']), + ) + + # ------------------------------------------------------------------ + # Public properties + # ------------------------------------------------------------------ + + @property + def broad_gauss_u(self) -> Parameter: + """ + Gaussian broadening from sample size and resolution (deg²). + + Reading this property returns the underlying ``Parameter`` + object. Assigning to it updates the parameter value. + """ + return self._broad_gauss_u + + @broad_gauss_u.setter + def broad_gauss_u(self, value: float) -> None: + self._broad_gauss_u.value = value + + @property + def broad_gauss_v(self) -> Parameter: + """ + Gaussian broadening instrumental contribution (deg²). + + Reading this property returns the underlying ``Parameter`` + object. Assigning to it updates the parameter value. + """ + return self._broad_gauss_v + + @broad_gauss_v.setter + def broad_gauss_v(self, value: float) -> None: + self._broad_gauss_v.value = value + + @property + def broad_gauss_w(self) -> Parameter: + """ + Gaussian broadening instrumental contribution (deg²). + + Reading this property returns the underlying ``Parameter`` + object. Assigning to it updates the parameter value. + """ + return self._broad_gauss_w + + @broad_gauss_w.setter + def broad_gauss_w(self, value: float) -> None: + self._broad_gauss_w.value = value + + @property + def broad_lorentz_x(self) -> Parameter: + """ + Lorentzian broadening (sample strain effects) (deg). + + Reading this property returns the underlying ``Parameter`` + object. Assigning to it updates the parameter value. + """ + return self._broad_lorentz_x + + @broad_lorentz_x.setter + def broad_lorentz_x(self, value: float) -> None: + self._broad_lorentz_x.value = value + + @property + def broad_lorentz_y(self) -> Parameter: + """ + Lorentzian broadening from microstructural defects (deg). + + Reading this property returns the underlying ``Parameter`` + object. Assigning to it updates the parameter value. + """ + return self._broad_lorentz_y + + @broad_lorentz_y.setter + def broad_lorentz_y(self, value: float) -> None: + self._broad_lorentz_y.value = value + + +class EmpiricalAsymmetryMixin: + """Empirical CWL peak asymmetry parameters.""" + + def __init__(self) -> None: + super().__init__() + + self._asym_empir_1: Parameter = Parameter( + name='asym_empir_1', + description='Empirical asymmetry coefficient p1', + units='', + value_spec=AttributeSpec( + default=0.1, + validator=RangeValidator(), + ), + cif_handler=CifHandler(names=['_peak.asym_empir_1']), + ) + self._asym_empir_2: Parameter = Parameter( + name='asym_empir_2', + description='Empirical asymmetry coefficient p2', + units='', + value_spec=AttributeSpec( + default=0.2, + validator=RangeValidator(), + ), + cif_handler=CifHandler(names=['_peak.asym_empir_2']), + ) + self._asym_empir_3: Parameter = Parameter( + name='asym_empir_3', + description='Empirical asymmetry coefficient p3', + units='', + value_spec=AttributeSpec( + default=0.3, + validator=RangeValidator(), + ), + cif_handler=CifHandler(names=['_peak.asym_empir_3']), + ) + self._asym_empir_4: Parameter = Parameter( + name='asym_empir_4', + description='Empirical asymmetry coefficient p4', + units='', + value_spec=AttributeSpec( + default=0.4, + validator=RangeValidator(), + ), + cif_handler=CifHandler(names=['_peak.asym_empir_4']), + ) + + # ------------------------------------------------------------------ + # Public properties + # ------------------------------------------------------------------ + + @property + def asym_empir_1(self) -> Parameter: + """ + Empirical asymmetry coefficient p1. + + Reading this property returns the underlying ``Parameter`` + object. Assigning to it updates the parameter value. + """ + return self._asym_empir_1 + + @asym_empir_1.setter + def asym_empir_1(self, value: float) -> None: + self._asym_empir_1.value = value + + @property + def asym_empir_2(self) -> Parameter: + """ + Empirical asymmetry coefficient p2. + + Reading this property returns the underlying ``Parameter`` + object. Assigning to it updates the parameter value. + """ + return self._asym_empir_2 + + @asym_empir_2.setter + def asym_empir_2(self, value: float) -> None: + self._asym_empir_2.value = value + + @property + def asym_empir_3(self) -> Parameter: + """ + Empirical asymmetry coefficient p3. + + Reading this property returns the underlying ``Parameter`` + object. Assigning to it updates the parameter value. + """ + return self._asym_empir_3 + + @asym_empir_3.setter + def asym_empir_3(self, value: float) -> None: + self._asym_empir_3.value = value + + @property + def asym_empir_4(self) -> Parameter: + """ + Empirical asymmetry coefficient p4. + + Reading this property returns the underlying ``Parameter`` + object. Assigning to it updates the parameter value. + """ + return self._asym_empir_4 + + @asym_empir_4.setter + def asym_empir_4(self, value: float) -> None: + self._asym_empir_4.value = value + + +class FcjAsymmetryMixin: + """Finger–Cox–Jephcoat (FCJ) asymmetry parameters.""" + + def __init__(self) -> None: + super().__init__() + + self._asym_fcj_1: Parameter = Parameter( + name='asym_fcj_1', + description='Finger-Cox-Jephcoat asymmetry parameter 1', + units='', + value_spec=AttributeSpec( + default=0.01, + validator=RangeValidator(), + ), + cif_handler=CifHandler(names=['_peak.asym_fcj_1']), + ) + self._asym_fcj_2: Parameter = Parameter( + name='asym_fcj_2', + description='Finger-Cox-Jephcoat asymmetry parameter 2', + units='', + value_spec=AttributeSpec( + default=0.02, + validator=RangeValidator(), + ), + cif_handler=CifHandler(names=['_peak.asym_fcj_2']), + ) + + # ------------------------------------------------------------------ + # Public properties + # ------------------------------------------------------------------ + + @property + def asym_fcj_1(self) -> Parameter: + """ + Finger-Cox-Jephcoat asymmetry parameter 1. + + Reading this property returns the underlying ``Parameter`` + object. Assigning to it updates the parameter value. + """ + return self._asym_fcj_1 + + @asym_fcj_1.setter + def asym_fcj_1(self, value: float) -> None: + self._asym_fcj_1.value = value + + @property + def asym_fcj_2(self) -> Parameter: + """ + Finger-Cox-Jephcoat asymmetry parameter 2. + + Reading this property returns the underlying ``Parameter`` + object. Assigning to it updates the parameter value. + """ + return self._asym_fcj_2 + + @asym_fcj_2.setter + def asym_fcj_2(self, value: float) -> None: + self._asym_fcj_2.value = value diff --git a/src/easydiffraction/datablocks/experiment/categories/peak/factory.py b/src/easydiffraction/datablocks/experiment/categories/peak/factory.py new file mode 100644 index 00000000..ca196748 --- /dev/null +++ b/src/easydiffraction/datablocks/experiment/categories/peak/factory.py @@ -0,0 +1,26 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Peak profile factory — delegates to ``FactoryBase``.""" + +from easydiffraction.core.factory import FactoryBase +from easydiffraction.datablocks.experiment.item.enums import BeamModeEnum +from easydiffraction.datablocks.experiment.item.enums import PeakProfileTypeEnum +from easydiffraction.datablocks.experiment.item.enums import ScatteringTypeEnum + + +class PeakFactory(FactoryBase): + """Factory for creating peak profile objects.""" + + _default_rules = { + frozenset({ + ('scattering_type', ScatteringTypeEnum.BRAGG), + ('beam_mode', BeamModeEnum.CONSTANT_WAVELENGTH), + }): PeakProfileTypeEnum.PSEUDO_VOIGT, + frozenset({ + ('scattering_type', ScatteringTypeEnum.BRAGG), + ('beam_mode', BeamModeEnum.TIME_OF_FLIGHT), + }): PeakProfileTypeEnum.PSEUDO_VOIGT_IKEDA_CARPENTER, + frozenset({ + ('scattering_type', ScatteringTypeEnum.TOTAL), + }): PeakProfileTypeEnum.GAUSSIAN_DAMPED_SINC, + } diff --git a/src/easydiffraction/datablocks/experiment/categories/peak/tof.py b/src/easydiffraction/datablocks/experiment/categories/peak/tof.py new file mode 100644 index 00000000..59c0b9e3 --- /dev/null +++ b/src/easydiffraction/datablocks/experiment/categories/peak/tof.py @@ -0,0 +1,87 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Time-of-flight peak profile classes.""" + +from easydiffraction.core.metadata import CalculatorSupport +from easydiffraction.core.metadata import Compatibility +from easydiffraction.core.metadata import TypeInfo +from easydiffraction.datablocks.experiment.categories.peak.base import PeakBase +from easydiffraction.datablocks.experiment.categories.peak.factory import PeakFactory +from easydiffraction.datablocks.experiment.categories.peak.tof_mixins import ( + IkedaCarpenterAsymmetryMixin, +) +from easydiffraction.datablocks.experiment.categories.peak.tof_mixins import TofBroadeningMixin +from easydiffraction.datablocks.experiment.item.enums import BeamModeEnum +from easydiffraction.datablocks.experiment.item.enums import CalculatorEnum +from easydiffraction.datablocks.experiment.item.enums import ScatteringTypeEnum + + +@PeakFactory.register +class TofPseudoVoigt( + PeakBase, + TofBroadeningMixin, +): + """Time-of-flight pseudo-Voigt peak shape.""" + + type_info = TypeInfo( + tag='tof-pseudo-voigt', + description='TOF pseudo-Voigt profile', + ) + compatibility = Compatibility( + scattering_type=frozenset({ScatteringTypeEnum.BRAGG}), + beam_mode=frozenset({BeamModeEnum.TIME_OF_FLIGHT}), + ) + calculator_support = CalculatorSupport( + calculators=frozenset({CalculatorEnum.CRYSPY, CalculatorEnum.CRYSFML}), + ) + + def __init__(self) -> None: + super().__init__() + + +@PeakFactory.register +class TofPseudoVoigtIkedaCarpenter( + PeakBase, + TofBroadeningMixin, + IkedaCarpenterAsymmetryMixin, +): + """TOF pseudo-Voigt with Ikeda–Carpenter asymmetry.""" + + type_info = TypeInfo( + tag='pseudo-voigt * ikeda-carpenter', + description='Pseudo-Voigt with Ikeda-Carpenter asymmetry correction', + ) + compatibility = Compatibility( + scattering_type=frozenset({ScatteringTypeEnum.BRAGG}), + beam_mode=frozenset({BeamModeEnum.TIME_OF_FLIGHT}), + ) + calculator_support = CalculatorSupport( + calculators=frozenset({CalculatorEnum.CRYSPY, CalculatorEnum.CRYSFML}), + ) + + def __init__(self) -> None: + super().__init__() + + +@PeakFactory.register +class TofPseudoVoigtBackToBack( + PeakBase, + TofBroadeningMixin, + IkedaCarpenterAsymmetryMixin, +): + """TOF back-to-back pseudo-Voigt with asymmetry.""" + + type_info = TypeInfo( + tag='pseudo-voigt * back-to-back', + description='TOF back-to-back pseudo-Voigt with asymmetry', + ) + compatibility = Compatibility( + scattering_type=frozenset({ScatteringTypeEnum.BRAGG}), + beam_mode=frozenset({BeamModeEnum.TIME_OF_FLIGHT}), + ) + calculator_support = CalculatorSupport( + calculators=frozenset({CalculatorEnum.CRYSPY, CalculatorEnum.CRYSFML}), + ) + + def __init__(self) -> None: + super().__init__() diff --git a/src/easydiffraction/datablocks/experiment/categories/peak/tof_mixins.py b/src/easydiffraction/datablocks/experiment/categories/peak/tof_mixins.py new file mode 100644 index 00000000..8093d877 --- /dev/null +++ b/src/easydiffraction/datablocks/experiment/categories/peak/tof_mixins.py @@ -0,0 +1,276 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +""" +Time-of-flight (TOF) peak-profile component classes. + +Defines classes that add Gaussian/Lorentz broadening, mixing, and +Ikeda–Carpenter asymmetry parameters used by TOF peak shapes. This +module provides classes that add broadening and asymmetry parameters. +They are composed into concrete peak classes elsewhere via multiple +inheritance. +""" + +from easydiffraction.core.validation import AttributeSpec +from easydiffraction.core.validation import RangeValidator +from easydiffraction.core.variable import Parameter +from easydiffraction.io.cif.handler import CifHandler + + +class TofBroadeningMixin: + """TOF Gaussian/Lorentz broadening and mixing parameters.""" + + def __init__(self) -> None: + super().__init__() + + self._broad_gauss_sigma_0 = Parameter( + name='gauss_sigma_0', + description='Gaussian broadening (instrumental resolution)', + units='µs²', + value_spec=AttributeSpec( + default=0.0, + validator=RangeValidator(), + ), + cif_handler=CifHandler(names=['_peak.gauss_sigma_0']), + ) + self._broad_gauss_sigma_1 = Parameter( + name='gauss_sigma_1', + description='Gaussian broadening (dependent on d-spacing)', + units='µs/Å', + value_spec=AttributeSpec( + default=0.0, + validator=RangeValidator(), + ), + cif_handler=CifHandler(names=['_peak.gauss_sigma_1']), + ) + self._broad_gauss_sigma_2 = Parameter( + name='gauss_sigma_2', + description='Gaussian broadening (instrument-dependent term)', + units='µs²/Ų', + value_spec=AttributeSpec( + default=0.0, + validator=RangeValidator(), + ), + cif_handler=CifHandler(names=['_peak.gauss_sigma_2']), + ) + self._broad_lorentz_gamma_0 = Parameter( + name='lorentz_gamma_0', + description='Lorentzian broadening (microstrain effects)', + units='µs', + value_spec=AttributeSpec( + default=0.0, + validator=RangeValidator(), + ), + cif_handler=CifHandler(names=['_peak.lorentz_gamma_0']), + ) + self._broad_lorentz_gamma_1 = Parameter( + name='lorentz_gamma_1', + description='Lorentzian broadening (dependent on d-spacing)', + units='µs/Å', + value_spec=AttributeSpec( + default=0.0, + validator=RangeValidator(), + ), + cif_handler=CifHandler(names=['_peak.lorentz_gamma_1']), + ) + self._broad_lorentz_gamma_2 = Parameter( + name='lorentz_gamma_2', + description='Lorentzian broadening (instrument-dependent term)', + units='µs²/Ų', + value_spec=AttributeSpec( + default=0.0, + validator=RangeValidator(), + ), + cif_handler=CifHandler(names=['_peak.lorentz_gamma_2']), + ) + self._broad_mix_beta_0 = Parameter( + name='mix_beta_0', + description='Ratio of Gaussian to Lorentzian contributions', + units='deg', + value_spec=AttributeSpec( + default=0.0, + validator=RangeValidator(), + ), + cif_handler=CifHandler(names=['_peak.mix_beta_0']), + ) + self._broad_mix_beta_1 = Parameter( + name='mix_beta_1', + description='Ratio of Gaussian to Lorentzian contributions', + units='deg', + value_spec=AttributeSpec( + default=0.0, + validator=RangeValidator(), + ), + cif_handler=CifHandler(names=['_peak.mix_beta_1']), + ) + + # ------------------------------------------------------------------ + # Public properties + # ------------------------------------------------------------------ + + @property + def broad_gauss_sigma_0(self) -> Parameter: + """ + Gaussian broadening (instrumental resolution) (µs²). + + Reading this property returns the underlying ``Parameter`` + object. Assigning to it updates the parameter value. + """ + return self._broad_gauss_sigma_0 + + @broad_gauss_sigma_0.setter + def broad_gauss_sigma_0(self, value: float) -> None: + self._broad_gauss_sigma_0.value = value + + @property + def broad_gauss_sigma_1(self) -> Parameter: + """ + Gaussian broadening (dependent on d-spacing) (µs/Å). + + Reading this property returns the underlying ``Parameter`` + object. Assigning to it updates the parameter value. + """ + return self._broad_gauss_sigma_1 + + @broad_gauss_sigma_1.setter + def broad_gauss_sigma_1(self, value: float) -> None: + self._broad_gauss_sigma_1.value = value + + @property + def broad_gauss_sigma_2(self) -> Parameter: + """ + Gaussian broadening (instrument-dependent term) (µs²/Ų). + + Reading this property returns the underlying ``Parameter`` + object. Assigning to it updates the parameter value. + """ + return self._broad_gauss_sigma_2 + + @broad_gauss_sigma_2.setter + def broad_gauss_sigma_2(self, value: float) -> None: + self._broad_gauss_sigma_2.value = value + + @property + def broad_lorentz_gamma_0(self) -> Parameter: + """ + Lorentzian broadening (microstrain effects) (µs). + + Reading this property returns the underlying ``Parameter`` + object. Assigning to it updates the parameter value. + """ + return self._broad_lorentz_gamma_0 + + @broad_lorentz_gamma_0.setter + def broad_lorentz_gamma_0(self, value: float) -> None: + self._broad_lorentz_gamma_0.value = value + + @property + def broad_lorentz_gamma_1(self) -> Parameter: + """ + Lorentzian broadening (dependent on d-spacing) (µs/Å). + + Reading this property returns the underlying ``Parameter`` + object. Assigning to it updates the parameter value. + """ + return self._broad_lorentz_gamma_1 + + @broad_lorentz_gamma_1.setter + def broad_lorentz_gamma_1(self, value: float) -> None: + self._broad_lorentz_gamma_1.value = value + + @property + def broad_lorentz_gamma_2(self) -> Parameter: + """ + Lorentzian broadening (instrument-dependent term) (µs²/Ų). + + Reading this property returns the underlying ``Parameter`` + object. Assigning to it updates the parameter value. + """ + return self._broad_lorentz_gamma_2 + + @broad_lorentz_gamma_2.setter + def broad_lorentz_gamma_2(self, value: float) -> None: + self._broad_lorentz_gamma_2.value = value + + @property + def broad_mix_beta_0(self) -> Parameter: + """ + Ratio of Gaussian to Lorentzian contributions (deg). + + Reading this property returns the underlying ``Parameter`` + object. Assigning to it updates the parameter value. + """ + return self._broad_mix_beta_0 + + @broad_mix_beta_0.setter + def broad_mix_beta_0(self, value: float) -> None: + self._broad_mix_beta_0.value = value + + @property + def broad_mix_beta_1(self) -> Parameter: + """ + Ratio of Gaussian to Lorentzian contributions (deg). + + Reading this property returns the underlying ``Parameter`` + object. Assigning to it updates the parameter value. + """ + return self._broad_mix_beta_1 + + @broad_mix_beta_1.setter + def broad_mix_beta_1(self, value: float) -> None: + self._broad_mix_beta_1.value = value + + +class IkedaCarpenterAsymmetryMixin: + """Ikeda–Carpenter asymmetry parameters.""" + + def __init__(self) -> None: + super().__init__() + + self._asym_alpha_0 = Parameter( + name='asym_alpha_0', + description='Ikeda-Carpenter asymmetry parameter α₀', + units='', # TODO + value_spec=AttributeSpec( + default=0.01, + validator=RangeValidator(), + ), + cif_handler=CifHandler(names=['_peak.asym_alpha_0']), + ) + self._asym_alpha_1 = Parameter( + name='asym_alpha_1', + description='Ikeda-Carpenter asymmetry parameter α₁', + units='', # TODO + value_spec=AttributeSpec( + default=0.02, + validator=RangeValidator(), + ), + cif_handler=CifHandler(names=['_peak.asym_alpha_1']), + ) + + @property + def asym_alpha_0(self) -> Parameter: + """ + Ikeda-Carpenter asymmetry parameter α₀. + + Reading this property returns the underlying ``Parameter`` + object. Assigning to it updates the parameter value. + """ + return self._asym_alpha_0 + + @asym_alpha_0.setter + def asym_alpha_0(self, value: float) -> None: + self._asym_alpha_0.value = value + + @property + def asym_alpha_1(self) -> Parameter: + """ + Ikeda-Carpenter asymmetry parameter α₁. + + Reading this property returns the underlying ``Parameter`` + object. Assigning to it updates the parameter value. + """ + return self._asym_alpha_1 + + @asym_alpha_1.setter + def asym_alpha_1(self, value: float) -> None: + self._asym_alpha_1.value = value diff --git a/src/easydiffraction/datablocks/experiment/categories/peak/total.py b/src/easydiffraction/datablocks/experiment/categories/peak/total.py new file mode 100644 index 00000000..61d5e6fd --- /dev/null +++ b/src/easydiffraction/datablocks/experiment/categories/peak/total.py @@ -0,0 +1,36 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Total-scattering (PDF) peak profile classes.""" + +from easydiffraction.core.metadata import CalculatorSupport +from easydiffraction.core.metadata import Compatibility +from easydiffraction.core.metadata import TypeInfo +from easydiffraction.datablocks.experiment.categories.peak.base import PeakBase +from easydiffraction.datablocks.experiment.categories.peak.factory import PeakFactory +from easydiffraction.datablocks.experiment.categories.peak.total_mixins import TotalBroadeningMixin +from easydiffraction.datablocks.experiment.item.enums import BeamModeEnum +from easydiffraction.datablocks.experiment.item.enums import CalculatorEnum +from easydiffraction.datablocks.experiment.item.enums import ScatteringTypeEnum + + +@PeakFactory.register +class TotalGaussianDampedSinc( + PeakBase, + TotalBroadeningMixin, +): + """Gaussian-damped sinc peak for total scattering (PDF).""" + + type_info = TypeInfo( + tag='gaussian-damped-sinc', + description='Gaussian-damped sinc for pair distribution function analysis', + ) + compatibility = Compatibility( + scattering_type=frozenset({ScatteringTypeEnum.TOTAL}), + beam_mode=frozenset({BeamModeEnum.CONSTANT_WAVELENGTH, BeamModeEnum.TIME_OF_FLIGHT}), + ) + calculator_support = CalculatorSupport( + calculators=frozenset({CalculatorEnum.PDFFIT}), + ) + + def __init__(self) -> None: + super().__init__() diff --git a/src/easydiffraction/datablocks/experiment/categories/peak/total_mixins.py b/src/easydiffraction/datablocks/experiment/categories/peak/total_mixins.py new file mode 100644 index 00000000..bae2c974 --- /dev/null +++ b/src/easydiffraction/datablocks/experiment/categories/peak/total_mixins.py @@ -0,0 +1,170 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +""" +Total scattering / PDF peak-profile component classes. + +This module provides classes that add broadening and asymmetry +parameters. They are composed into concrete peak classes elsewhere via +multiple inheritance. +""" + +from easydiffraction.core.validation import AttributeSpec +from easydiffraction.core.validation import RangeValidator +from easydiffraction.core.variable import Parameter +from easydiffraction.io.cif.handler import CifHandler + + +class TotalBroadeningMixin: + """PDF broadening/damping/sharpening parameters.""" + + def __init__(self) -> None: + super().__init__() + + self._damp_q = Parameter( + name='damp_q', + description='Q-resolution damping for high-r PDF peak amplitude', + units='Å⁻¹', + value_spec=AttributeSpec( + default=0.05, + validator=RangeValidator(), + ), + cif_handler=CifHandler(names=['_peak.damp_q']), + ) + self._broad_q = Parameter( + name='broad_q', + description='Quadratic peak broadening from thermal uncertainty', + units='Å⁻²', + value_spec=AttributeSpec( + default=0.0, + validator=RangeValidator(), + ), + cif_handler=CifHandler(names=['_peak.broad_q']), + ) + self._cutoff_q = Parameter( + name='cutoff_q', + description='Q-value cutoff for Fourier transform', + units='Å⁻¹', + value_spec=AttributeSpec( + default=25.0, + validator=RangeValidator(), + ), + cif_handler=CifHandler(names=['_peak.cutoff_q']), + ) + self._sharp_delta_1 = Parameter( + name='sharp_delta_1', + description='Peak sharpening coefficient (1/r dependence)', + units='Å', + value_spec=AttributeSpec( + default=0.0, + validator=RangeValidator(), + ), + cif_handler=CifHandler(names=['_peak.sharp_delta_1']), + ) + self._sharp_delta_2 = Parameter( + name='sharp_delta_2', + description='Peak sharpening coefficient (1/r² dependence)', + units='Ų', + value_spec=AttributeSpec( + default=0.0, + validator=RangeValidator(), + ), + cif_handler=CifHandler(names=['_peak.sharp_delta_2']), + ) + self._damp_particle_diameter = Parameter( + name='damp_particle_diameter', + description='Particle diameter for spherical envelope damping correction', + units='Å', + value_spec=AttributeSpec( + default=0.0, + validator=RangeValidator(), + ), + cif_handler=CifHandler(names=['_peak.damp_particle_diameter']), + ) + + # ------------------------------------------------------------------ + # Public properties + # ------------------------------------------------------------------ + + @property + def damp_q(self) -> Parameter: + """ + Q-resolution damping for high-r PDF peak amplitude (Å⁻¹). + + Reading this property returns the underlying ``Parameter`` + object. Assigning to it updates the parameter value. + """ + return self._damp_q + + @damp_q.setter + def damp_q(self, value: float) -> None: + self._damp_q.value = value + + @property + def broad_q(self) -> Parameter: + """ + Quadratic peak broadening from thermal uncertainty (Å⁻²). + + Reading this property returns the underlying ``Parameter`` + object. Assigning to it updates the parameter value. + """ + return self._broad_q + + @broad_q.setter + def broad_q(self, value: float) -> None: + self._broad_q.value = value + + @property + def cutoff_q(self) -> Parameter: + """ + Q-value cutoff for Fourier transform (Å⁻¹). + + Reading this property returns the underlying ``Parameter`` + object. Assigning to it updates the parameter value. + """ + return self._cutoff_q + + @cutoff_q.setter + def cutoff_q(self, value: float) -> None: + self._cutoff_q.value = value + + @property + def sharp_delta_1(self) -> Parameter: + """ + PDF peak sharpening coefficient (1/r dependence) (Å). + + Reading this property returns the underlying ``Parameter`` + object. Assigning to it updates the parameter value. + """ + return self._sharp_delta_1 + + @sharp_delta_1.setter + def sharp_delta_1(self, value: float) -> None: + self._sharp_delta_1.value = value + + @property + def sharp_delta_2(self) -> Parameter: + """ + PDF peak sharpening coefficient (1/r² dependence) (Ų). + + Reading this property returns the underlying ``Parameter`` + object. Assigning to it updates the parameter value. + """ + return self._sharp_delta_2 + + @sharp_delta_2.setter + def sharp_delta_2(self, value: float) -> None: + self._sharp_delta_2.value = value + + @property + def damp_particle_diameter(self) -> Parameter: + """ + Particle diameter for spherical envelope damping correction (Å). + + Reading this property returns the underlying ``Parameter`` + object. Assigning to it updates the parameter value. + """ + return self._damp_particle_diameter + + @damp_particle_diameter.setter + def damp_particle_diameter(self, value: float) -> None: + self._damp_particle_diameter.value = value diff --git a/src/easydiffraction/datablocks/experiment/collection.py b/src/easydiffraction/datablocks/experiment/collection.py new file mode 100644 index 00000000..fb30d013 --- /dev/null +++ b/src/easydiffraction/datablocks/experiment/collection.py @@ -0,0 +1,157 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Collection of experiment data blocks.""" + +from typeguard import typechecked + +from easydiffraction.core.datablock import DatablockCollection +from easydiffraction.datablocks.experiment.item.base import ExperimentBase +from easydiffraction.datablocks.experiment.item.factory import ExperimentFactory +from easydiffraction.utils.enums import VerbosityEnum +from easydiffraction.utils.logging import console + + +class Experiments(DatablockCollection): + """ + Collection of Experiment data blocks. + + Provides convenience constructors for common creation patterns and + helper methods for simple presentation of collection contents. + """ + + def __init__(self) -> None: + super().__init__(item_type=ExperimentBase) + + # ------------------------------------------------------------------ + # Public methods + # ------------------------------------------------------------------ + + # TODO: Make abstract in DatablockCollection? + @typechecked + def create( + self, + *, + name: str, + sample_form: str | None = None, + beam_mode: str | None = None, + radiation_probe: str | None = None, + scattering_type: str | None = None, + ) -> None: + """ + Add an experiment without associating a data file. + + Parameters + ---------- + name : str + Experiment identifier. + sample_form : str | None, default=None + Sample form (e.g. ``'powder'``). + beam_mode : str | None, default=None + Beam mode (e.g. ``'constant wavelength'``). + radiation_probe : str | None, default=None + Radiation probe (e.g. ``'neutron'``). + scattering_type : str | None, default=None + Scattering type (e.g. ``'bragg'``). + """ + experiment = ExperimentFactory.from_scratch( + name=name, + sample_form=sample_form, + beam_mode=beam_mode, + radiation_probe=radiation_probe, + scattering_type=scattering_type, + ) + self.add(experiment) + + # TODO: Move to DatablockCollection? + @typechecked + def add_from_cif_str( + self, + cif_str: str, + ) -> None: + """ + Add an experiment from a CIF string. + + Parameters + ---------- + cif_str : str + Full CIF document as a string. + """ + experiment = ExperimentFactory.from_cif_str(cif_str) + self.add(experiment) + + # TODO: Move to DatablockCollection? + @typechecked + def add_from_cif_path( + self, + cif_path: str, + ) -> None: + """ + Add an experiment from a CIF file path. + + Parameters + ---------- + cif_path : str + Path to a CIF document. + """ + experiment = ExperimentFactory.from_cif_path(cif_path) + self.add(experiment) + + @typechecked + def add_from_data_path( + self, + *, + name: str, + data_path: str, + sample_form: str | None = None, + beam_mode: str | None = None, + radiation_probe: str | None = None, + scattering_type: str | None = None, + verbosity: str | None = None, + ) -> None: + """ + Add an experiment from a data file path. + + Parameters + ---------- + name : str + Experiment identifier. + data_path : str + Path to the measured data file. + sample_form : str | None, default=None + Sample form (e.g. ``'powder'``). + beam_mode : str | None, default=None + Beam mode (e.g. ``'constant wavelength'``). + radiation_probe : str | None, default=None + Radiation probe (e.g. ``'neutron'``). + scattering_type : str | None, default=None + Scattering type (e.g. ``'bragg'``). + verbosity : str | None, default=None + Console output verbosity: ``'full'`` for multi-line output, + ``'short'`` for a one-line status message, or ``'silent'`` + for no output. When ``None``, uses ``project.verbosity``. + """ + if verbosity is None and self._parent is not None: + verbosity = self._parent.verbosity + verb = VerbosityEnum(verbosity) if verbosity is not None else VerbosityEnum.FULL + experiment = ExperimentFactory.from_data_path( + name=name, + data_path=data_path, + sample_form=sample_form, + beam_mode=beam_mode, + radiation_probe=radiation_probe, + scattering_type=scattering_type, + verbosity=verb, + ) + self.add(experiment) + + # TODO: Move to DatablockCollection? + def show_names(self) -> None: + """List all experiment names in the collection.""" + console.paragraph('Defined experiments' + ' 🔬') + console.print(self.names) + + # TODO: Move to DatablockCollection? + def show_params(self) -> None: + """Show parameters of all experiments in the collection.""" + for experiment in self.values(): + experiment.show_params() diff --git a/src/easydiffraction/datablocks/experiment/item/__init__.py b/src/easydiffraction/datablocks/experiment/item/__init__.py new file mode 100644 index 00000000..35a64fb1 --- /dev/null +++ b/src/easydiffraction/datablocks/experiment/item/__init__.py @@ -0,0 +1,9 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + +from easydiffraction.datablocks.experiment.item.base import ExperimentBase +from easydiffraction.datablocks.experiment.item.base import PdExperimentBase +from easydiffraction.datablocks.experiment.item.bragg_pd import BraggPdExperiment +from easydiffraction.datablocks.experiment.item.bragg_sc import CwlScExperiment +from easydiffraction.datablocks.experiment.item.bragg_sc import TofScExperiment +from easydiffraction.datablocks.experiment.item.total_pd import TotalPdExperiment diff --git a/src/easydiffraction/datablocks/experiment/item/base.py b/src/easydiffraction/datablocks/experiment/item/base.py new file mode 100644 index 00000000..5c426a94 --- /dev/null +++ b/src/easydiffraction/datablocks/experiment/item/base.py @@ -0,0 +1,768 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Base classes for experiment datablock items.""" + +from __future__ import annotations + +from abc import abstractmethod +from typing import TYPE_CHECKING +from typing import Any +from typing import List + +from easydiffraction.core.datablock import DatablockItem +from easydiffraction.datablocks.experiment.categories.data.factory import DataFactory +from easydiffraction.datablocks.experiment.categories.diffrn.factory import DiffrnFactory +from easydiffraction.datablocks.experiment.categories.excluded_regions.factory import ( + ExcludedRegionsFactory, +) +from easydiffraction.datablocks.experiment.categories.extinction.factory import ExtinctionFactory +from easydiffraction.datablocks.experiment.categories.instrument.factory import InstrumentFactory +from easydiffraction.datablocks.experiment.categories.linked_crystal.factory import ( + LinkedCrystalFactory, +) +from easydiffraction.datablocks.experiment.categories.linked_phases.factory import ( + LinkedPhasesFactory, +) +from easydiffraction.datablocks.experiment.categories.peak.factory import PeakFactory +from easydiffraction.io.cif.serialize import experiment_to_cif +from easydiffraction.utils.logging import console +from easydiffraction.utils.logging import log +from easydiffraction.utils.utils import render_cif + +if TYPE_CHECKING: + from easydiffraction.datablocks.experiment.categories.experiment_type import ExperimentType + from easydiffraction.datablocks.structure.collection import Structures + + +class ExperimentBase(DatablockItem): + """Base class for all experiment datablock items.""" + + def __init__( + self, + *, + name: str, + type: ExperimentType, + ) -> None: + super().__init__() + self._name = name + self._type = type + self._calculator = None + self._calculator_type: str | None = None + self._identity.datablock_entry_name = lambda: self.name + + self._diffrn_type: str = DiffrnFactory.default_tag() + self._diffrn = DiffrnFactory.create(self._diffrn_type) + + @property + def name(self) -> str: + """Human-readable name of the experiment.""" + return self._name + + @name.setter + def name(self, new: str) -> None: + """ + Rename the experiment. + + Parameters + ---------- + new : str + New name for this experiment. + """ + self._name = new + + @property + def type(self) -> object: # TODO: Consider another name + """Experiment type: sample form, probe, beam mode.""" + return self._type + + # ------------------------------------------------------------------ + # Diffrn conditions (switchable-category pattern) + # ------------------------------------------------------------------ + + @property + def diffrn(self) -> object: + """Ambient conditions recorded during measurement.""" + return self._diffrn + + @property + def diffrn_type(self) -> str: + """Tag of the active diffraction conditions type.""" + return self._diffrn_type + + @diffrn_type.setter + def diffrn_type(self, new_type: str) -> None: + """ + Switch to a different diffraction conditions type. + + Parameters + ---------- + new_type : str + Diffrn conditions tag (e.g. ``'default'``). + """ + supported_tags = DiffrnFactory.supported_tags() + if new_type not in supported_tags: + log.warning( + f"Unsupported diffrn type '{new_type}'. " + f'Supported: {supported_tags}. ' + f"For more information, use 'show_supported_diffrn_types()'", + ) + return + + self._diffrn = DiffrnFactory.create(new_type) + self._diffrn_type = new_type + console.paragraph(f"Diffrn type for experiment '{self.name}' changed to") + console.print(new_type) + + def show_supported_diffrn_types(self) -> None: + """Print a table of supported diffraction conditions types.""" + DiffrnFactory.show_supported() + + def show_current_diffrn_type(self) -> None: + """Print the currently used diffraction conditions type.""" + console.paragraph('Current diffrn type') + console.print(self.diffrn_type) + + @property + def as_cif(self) -> str: + """Serialize this experiment to a CIF fragment.""" + return experiment_to_cif(self) + + def show_as_cif(self) -> None: + """Pretty-print the experiment as CIF text.""" + experiment_cif = super().as_cif + paragraph_title: str = f"Experiment 🔬 '{self.name}' as cif" + console.paragraph(paragraph_title) + render_cif(experiment_cif) + + @abstractmethod + def _load_ascii_data_to_experiment(self, data_path: str) -> None: + """ + Load ASCII data from file into the experiment data category. + + Parameters + ---------- + data_path : str + Path to the ASCII file to load. + + Raises + ------ + NotImplementedError + Subclasses must implement this method. + """ + raise NotImplementedError() + + # ------------------------------------------------------------------ + # Calculator (switchable-category pattern) + # ------------------------------------------------------------------ + + @property + def calculator(self) -> object: + """ + The active calculator instance for this experiment. + + Auto-resolved on first access from the experiment's data + category ``calculator_support`` and + ``CalculatorFactory._default_rules``. + """ + if self._calculator is None: + self._resolve_calculator() + return self._calculator + + @property + def calculator_type(self) -> str: + """Tag of the active calculator backend (e.g. ``'cryspy'``).""" + if self._calculator_type is None: + self._resolve_calculator() + return self._calculator_type + + @calculator_type.setter + def calculator_type(self, tag: str) -> None: + """ + Switch to a different calculator backend. + + Parameters + ---------- + tag : str + Calculator tag (e.g. ``'cryspy'``, ``'crysfml'``, + ``'pdffit'``). + """ + from easydiffraction.analysis.calculators.factory import CalculatorFactory + + supported = self._supported_calculator_tags() + if tag not in supported: + log.warning( + f"Unsupported calculator '{tag}' for experiment " + f"'{self.name}'. Supported: {supported}. " + f"For more information, use 'show_supported_calculator_types()'", + ) + return + self._calculator = CalculatorFactory.create(tag) + self._calculator_type = tag + console.paragraph(f"Calculator for experiment '{self.name}' changed to") + console.print(tag) + + def show_supported_calculator_types(self) -> None: + """Print a table of supported calculator backends.""" + from easydiffraction.analysis.calculators.factory import CalculatorFactory + + supported_tags = self._supported_calculator_tags() + all_classes = CalculatorFactory._supported_map() + columns_headers = ['Type', 'Description'] + columns_alignment = ['left', 'left'] + columns_data = [ + [cls.type_info.tag, cls.type_info.description] + for tag, cls in all_classes.items() + if tag in supported_tags + ] + from easydiffraction.utils.utils import render_table + + console.paragraph('Supported calculator types') + render_table( + columns_headers=columns_headers, + columns_alignment=columns_alignment, + columns_data=columns_data, + ) + + def show_current_calculator_type(self) -> None: + """Print the name of the currently active calculator.""" + console.paragraph('Current calculator type') + console.print(self.calculator_type) + + def _resolve_calculator(self) -> None: + """Auto-resolve the default calculator from data category.""" + from easydiffraction.analysis.calculators.factory import CalculatorFactory + + tag = CalculatorFactory.default_tag( + scattering_type=self.type.scattering_type.value, + ) + supported = self._supported_calculator_tags() + if supported and tag not in supported: + tag = supported[0] + self._calculator = CalculatorFactory.create(tag) + self._calculator_type = tag + + def _supported_calculator_tags(self) -> list[str]: + """ + Return calculator tags supported by this experiment. + + Intersects the data category's ``calculator_support`` with + calculators whose engines are importable. + """ + from easydiffraction.analysis.calculators.factory import CalculatorFactory + + available = CalculatorFactory.supported_tags() + data = getattr(self, '_data', None) + if data is not None: + data_support = getattr(data, 'calculator_support', None) + if data_support and data_support.calculators: + return [t for t in available if t in data_support.calculators] + return available + + +class ScExperimentBase(ExperimentBase): + """Base class for all single crystal experiments.""" + + def __init__( + self, + *, + name: str, + type: ExperimentType, + ) -> None: + super().__init__(name=name, type=type) + + self._extinction_type: str = ExtinctionFactory.default_tag() + self._extinction = ExtinctionFactory.create(self._extinction_type) + self._linked_crystal_type: str = LinkedCrystalFactory.default_tag() + self._linked_crystal = LinkedCrystalFactory.create(self._linked_crystal_type) + self._instrument_type: str = InstrumentFactory.default_tag( + scattering_type=self.type.scattering_type.value, + beam_mode=self.type.beam_mode.value, + sample_form=self.type.sample_form.value, + ) + self._instrument = InstrumentFactory.create(self._instrument_type) + self._data_type: str = DataFactory.default_tag( + sample_form=self.type.sample_form.value, + beam_mode=self.type.beam_mode.value, + scattering_type=self.type.scattering_type.value, + ) + self._data = DataFactory.create(self._data_type) + + @abstractmethod + def _load_ascii_data_to_experiment(self, data_path: str) -> None: + """ + Load single crystal data from an ASCII file. + + Parameters + ---------- + data_path : str + Path to data file with columns compatible with the beam + mode. + """ + pass + + # ------------------------------------------------------------------ + # Extinction (switchable-category pattern) + # ------------------------------------------------------------------ + + @property + def extinction(self) -> object: + """Active extinction correction model.""" + return self._extinction + + @property + def extinction_type(self) -> str: + """Tag of the active extinction correction model.""" + return self._extinction_type + + @extinction_type.setter + def extinction_type(self, new_type: str) -> None: + """ + Switch to a different extinction correction model. + + Parameters + ---------- + new_type : str + Extinction tag (e.g. ``'shelx'``). + """ + supported_tags = ExtinctionFactory.supported_tags() + if new_type not in supported_tags: + log.warning( + f"Unsupported extinction type '{new_type}'. " + f'Supported: {supported_tags}. ' + f"For more information, use 'show_supported_extinction_types()'", + ) + return + + self._extinction = ExtinctionFactory.create(new_type) + self._extinction_type = new_type + console.paragraph(f"Extinction type for experiment '{self.name}' changed to") + console.print(new_type) + + def show_supported_extinction_types(self) -> None: + """Print a table of supported extinction correction models.""" + ExtinctionFactory.show_supported() + + def show_current_extinction_type(self) -> None: + """Print the currently used extinction correction model.""" + console.paragraph('Current extinction type') + console.print(self.extinction_type) + + # ------------------------------------------------------------------ + # Linked crystal (switchable-category pattern) + # ------------------------------------------------------------------ + + @property + def linked_crystal(self) -> object: + """Linked crystal model for this experiment.""" + return self._linked_crystal + + @property + def linked_crystal_type(self) -> str: + """Tag of the active linked-crystal reference type.""" + return self._linked_crystal_type + + @linked_crystal_type.setter + def linked_crystal_type(self, new_type: str) -> None: + """ + Switch to a different linked-crystal reference type. + + Parameters + ---------- + new_type : str + Linked-crystal tag (e.g. ``'default'``). + """ + supported_tags = LinkedCrystalFactory.supported_tags() + if new_type not in supported_tags: + log.warning( + f"Unsupported linked crystal type '{new_type}'. " + f'Supported: {supported_tags}. ' + f"For more information, use 'show_supported_linked_crystal_types()'", + ) + return + + self._linked_crystal = LinkedCrystalFactory.create(new_type) + self._linked_crystal_type = new_type + console.paragraph(f"Linked crystal type for experiment '{self.name}' changed to") + console.print(new_type) + + def show_supported_linked_crystal_types(self) -> None: + """Print a table of supported linked-crystal reference types.""" + LinkedCrystalFactory.show_supported() + + def show_current_linked_crystal_type(self) -> None: + """Print the currently used linked-crystal reference type.""" + console.paragraph('Current linked crystal type') + console.print(self.linked_crystal_type) + + # ------------------------------------------------------------------ + # Instrument (switchable-category pattern) + # ------------------------------------------------------------------ + + @property + def instrument(self) -> object: + """Active instrument model for this experiment.""" + return self._instrument + + @property + def instrument_type(self) -> str: + """Tag of the active instrument type.""" + return self._instrument_type + + @instrument_type.setter + def instrument_type(self, new_type: str) -> None: + """ + Switch to a different instrument type. + + Parameters + ---------- + new_type : str + Instrument tag (e.g. ``'cwl-sc'``). + """ + supported = InstrumentFactory.supported_for( + scattering_type=self.type.scattering_type.value, + beam_mode=self.type.beam_mode.value, + sample_form=self.type.sample_form.value, + ) + supported_tags = [k.type_info.tag for k in supported] + if new_type not in supported_tags: + log.warning( + f"Unsupported instrument type '{new_type}'. " + f'Supported: {supported_tags}. ' + f"For more information, use 'show_supported_instrument_types()'", + ) + return + self._instrument = InstrumentFactory.create(new_type) + self._instrument_type = new_type + console.paragraph(f"Instrument type for experiment '{self.name}' changed to") + console.print(new_type) + + def show_supported_instrument_types(self) -> None: + """Print a table of supported instrument types.""" + InstrumentFactory.show_supported( + scattering_type=self.type.scattering_type.value, + beam_mode=self.type.beam_mode.value, + sample_form=self.type.sample_form.value, + ) + + def show_current_instrument_type(self) -> None: + """Print the currently used instrument type.""" + console.paragraph('Current instrument type') + console.print(self.instrument_type) + + # ------------------------------------------------------------------ + # Data (switchable-category pattern) + # ------------------------------------------------------------------ + + @property + def data(self) -> object: + """Data collection for this experiment.""" + return self._data + + @property + def data_type(self) -> str: + """Tag of the active data collection type.""" + return self._data_type + + @data_type.setter + def data_type(self, new_type: str) -> None: + """ + Switch to a different data collection type. + + Parameters + ---------- + new_type : str + Data tag (e.g. ``'bragg-sc'``). + """ + supported_tags = DataFactory.supported_tags() + if new_type not in supported_tags: + log.warning( + f"Unsupported data type '{new_type}'. " + f'Supported: {supported_tags}. ' + f"For more information, use 'show_supported_data_types()'", + ) + return + self._data = DataFactory.create(new_type) + self._data_type = new_type + console.paragraph(f"Data type for experiment '{self.name}' changed to") + console.print(new_type) + + def show_supported_data_types(self) -> None: + """Print a table of supported data collection types.""" + DataFactory.show_supported() + + def show_current_data_type(self) -> None: + """Print the currently used data collection type.""" + console.paragraph('Current data type') + console.print(self.data_type) + + +class PdExperimentBase(ExperimentBase): + """Base class for all powder experiments.""" + + def __init__( + self, + *, + name: str, + type: ExperimentType, + ) -> None: + super().__init__(name=name, type=type) + + self._linked_phases_type: str = LinkedPhasesFactory.default_tag() + self._linked_phases = LinkedPhasesFactory.create(self._linked_phases_type) + self._excluded_regions_type: str = ExcludedRegionsFactory.default_tag() + self._excluded_regions = ExcludedRegionsFactory.create(self._excluded_regions_type) + self._peak_profile_type: str = PeakFactory.default_tag( + scattering_type=self.type.scattering_type.value, + beam_mode=self.type.beam_mode.value, + ) + self._data_type: str = DataFactory.default_tag( + sample_form=self.type.sample_form.value, + beam_mode=self.type.beam_mode.value, + scattering_type=self.type.scattering_type.value, + ) + self._data = DataFactory.create(self._data_type) + self._peak = PeakFactory.create(self._peak_profile_type) + + def _get_valid_linked_phases( + self, + structures: Structures, + ) -> List[Any]: + """ + Get valid linked phases for this experiment. + + Parameters + ---------- + structures : Structures + Collection of structures. + + Returns + ------- + List[Any] + A list of valid linked phases. + """ + if not self.linked_phases: + print('Warning: No linked phases defined. Returning empty pattern.') + return [] + + valid_linked_phases = [] + for linked_phase in self.linked_phases: + if linked_phase._identity.category_entry_name not in structures.names: + print( + f"Warning: Linked phase '{linked_phase.id.value}' not " + f'found in Structures {structures.names}. Skipping it.' + ) + continue + valid_linked_phases.append(linked_phase) + + if not valid_linked_phases: + print( + 'Warning: None of the linked phases found in Structures. Returning empty pattern.' + ) + + return valid_linked_phases + + @abstractmethod + def _load_ascii_data_to_experiment(self, data_path: str) -> int: + """ + Load powder diffraction data from an ASCII file. + + Parameters + ---------- + data_path : str + Path to data file with columns compatible with the beam mode + (e.g. 2θ/I/σ for CWL, TOF/I/σ for TOF). + + Returns + ------- + int + Number of loaded data points. + """ + pass + + @property + def linked_phases(self) -> object: + """Collection of phases linked to this experiment.""" + return self._linked_phases + + @property + def linked_phases_type(self) -> str: + """Tag of the active linked-phases collection type.""" + return self._linked_phases_type + + @linked_phases_type.setter + def linked_phases_type(self, new_type: str) -> None: + """ + Switch to a different linked-phases collection type. + + Parameters + ---------- + new_type : str + Linked-phases tag (e.g. ``'default'``). + """ + supported_tags = LinkedPhasesFactory.supported_tags() + if new_type not in supported_tags: + log.warning( + f"Unsupported linked phases type '{new_type}'. " + f'Supported: {supported_tags}. ' + f"For more information, use 'show_supported_linked_phases_types()'", + ) + return + + self._linked_phases = LinkedPhasesFactory.create(new_type) + self._linked_phases_type = new_type + console.paragraph(f"Linked phases type for experiment '{self.name}' changed to") + console.print(new_type) + + def show_supported_linked_phases_types(self) -> None: + """Print a table of supported linked-phases collection types.""" + LinkedPhasesFactory.show_supported() + + def show_current_linked_phases_type(self) -> None: + """Print the currently used linked-phases collection type.""" + console.paragraph('Current linked phases type') + console.print(self.linked_phases_type) + + @property + def excluded_regions(self) -> object: + """Collection of excluded regions for the x-grid.""" + return self._excluded_regions + + @property + def excluded_regions_type(self) -> str: + """Tag of the active excluded-regions collection type.""" + return self._excluded_regions_type + + @excluded_regions_type.setter + def excluded_regions_type(self, new_type: str) -> None: + """ + Switch to a different excluded-regions collection type. + + Parameters + ---------- + new_type : str + Excluded-regions tag (e.g. ``'default'``). + """ + supported_tags = ExcludedRegionsFactory.supported_tags() + if new_type not in supported_tags: + log.warning( + f"Unsupported excluded regions type '{new_type}'. " + f'Supported: {supported_tags}. ' + f"For more information, use 'show_supported_excluded_regions_types()'", + ) + return + + self._excluded_regions = ExcludedRegionsFactory.create(new_type) + self._excluded_regions_type = new_type + console.paragraph(f"Excluded regions type for experiment '{self.name}' changed to") + console.print(new_type) + + def show_supported_excluded_regions_types(self) -> None: + """Print a table of supported excluded-regions types.""" + ExcludedRegionsFactory.show_supported() + + def show_current_excluded_regions_type(self) -> None: + """Print the currently used excluded-regions collection type.""" + console.paragraph('Current excluded regions type') + console.print(self.excluded_regions_type) + + # ------------------------------------------------------------------ + # Data (switchable-category pattern) + # ------------------------------------------------------------------ + + @property + def data(self) -> object: + """Data collection for this experiment.""" + return self._data + + @property + def data_type(self) -> str: + """Tag of the active data collection type.""" + return self._data_type + + @data_type.setter + def data_type(self, new_type: str) -> None: + """ + Switch to a different data collection type. + + Parameters + ---------- + new_type : str + Data tag (e.g. ``'bragg-pd-cwl'``). + """ + supported_tags = DataFactory.supported_tags() + if new_type not in supported_tags: + log.warning( + f"Unsupported data type '{new_type}'. " + f'Supported: {supported_tags}. ' + f"For more information, use 'show_supported_data_types()'", + ) + return + self._data = DataFactory.create(new_type) + self._data_type = new_type + console.paragraph(f"Data type for experiment '{self.name}' changed to") + console.print(new_type) + + def show_supported_data_types(self) -> None: + """Print a table of supported data collection types.""" + DataFactory.show_supported() + + def show_current_data_type(self) -> None: + """Print the currently used data collection type.""" + console.paragraph('Current data type') + console.print(self.data_type) + + @property + def peak(self) -> object: + """Peak category object with profile parameters and mixins.""" + return self._peak + + @property + def peak_profile_type(self) -> object: + """Currently selected peak profile type enum.""" + return self._peak_profile_type + + @peak_profile_type.setter + def peak_profile_type(self, new_type: str) -> None: + """ + Change the active peak profile type, if supported. + + Parameters + ---------- + new_type : str + New profile type as tag string. + """ + supported = PeakFactory.supported_for( + scattering_type=self.type.scattering_type.value, + beam_mode=self.type.beam_mode.value, + ) + supported_tags = [k.type_info.tag for k in supported] + + if new_type not in supported_tags: + log.warning( + f"Unsupported peak profile '{new_type}'. " + f'Supported peak profiles: {supported_tags}. ' + f"For more information, use 'show_supported_peak_profile_types()'", + ) + return + + if self._peak is not None: + log.warning( + 'Switching peak profile type discards existing peak parameters.', + ) + + self._peak = PeakFactory.create(new_type) + self._peak_profile_type = new_type + console.paragraph(f"Peak profile type for experiment '{self.name}' changed to") + console.print(new_type) + + def show_supported_peak_profile_types(self) -> None: + """Print available peak profile types for this experiment.""" + PeakFactory.show_supported( + scattering_type=self.type.scattering_type.value, + beam_mode=self.type.beam_mode.value, + ) + + def show_current_peak_profile_type(self) -> None: + """Print the currently selected peak profile type.""" + console.paragraph('Current peak profile type') + console.print(self.peak_profile_type) diff --git a/src/easydiffraction/datablocks/experiment/item/bragg_pd.py b/src/easydiffraction/datablocks/experiment/item/bragg_pd.py new file mode 100644 index 00000000..6da9ffe7 --- /dev/null +++ b/src/easydiffraction/datablocks/experiment/item/bragg_pd.py @@ -0,0 +1,219 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import numpy as np + +from easydiffraction.core.metadata import Compatibility +from easydiffraction.core.metadata import TypeInfo +from easydiffraction.datablocks.experiment.categories.background.factory import BackgroundFactory +from easydiffraction.datablocks.experiment.categories.instrument.factory import InstrumentFactory +from easydiffraction.datablocks.experiment.item.base import PdExperimentBase +from easydiffraction.datablocks.experiment.item.enums import BeamModeEnum +from easydiffraction.datablocks.experiment.item.enums import SampleFormEnum +from easydiffraction.datablocks.experiment.item.enums import ScatteringTypeEnum +from easydiffraction.datablocks.experiment.item.factory import ExperimentFactory +from easydiffraction.io.ascii import load_numeric_block +from easydiffraction.utils.logging import console +from easydiffraction.utils.logging import log + +if TYPE_CHECKING: + from easydiffraction.datablocks.experiment.categories.experiment_type import ExperimentType + + +@ExperimentFactory.register +class BraggPdExperiment(PdExperimentBase): + """Standard Bragg powder diffraction experiment.""" + + type_info = TypeInfo( + tag='bragg-pd', + description='Bragg powder diffraction experiment', + ) + compatibility = Compatibility( + scattering_type=frozenset({ScatteringTypeEnum.BRAGG}), + sample_form=frozenset({SampleFormEnum.POWDER}), + beam_mode=frozenset({BeamModeEnum.CONSTANT_WAVELENGTH, BeamModeEnum.TIME_OF_FLIGHT}), + ) + + def __init__( + self, + *, + name: str, + type: ExperimentType, + ) -> None: + super().__init__(name=name, type=type) + + self._instrument_type: str = InstrumentFactory.default_tag( + scattering_type=self.type.scattering_type.value, + beam_mode=self.type.beam_mode.value, + sample_form=self.type.sample_form.value, + ) + self._instrument = InstrumentFactory.create(self._instrument_type) + self._background_type: str = BackgroundFactory.default_tag() + self._background = BackgroundFactory.create(self._background_type) + + def _load_ascii_data_to_experiment( + self, + data_path: str, + ) -> int: + """ + Load (x, y, sy) data from an ASCII file into the data category. + + The file format is space/column separated with 2 or 3 columns: + ``x y [sy]``. If ``sy`` is missing, it is approximated as + ``sqrt(y)``. + + If ``sy`` has values smaller than ``0.0001``, they are replaced + with ``1.0``. + + Parameters + ---------- + data_path : str + Path to the ASCII data file. + + Returns + ------- + int + Number of loaded data points. + """ + data = load_numeric_block(data_path) + + if data.shape[1] < 2: + log.error( + 'Data file must have at least two columns: x and y.', + exc_type=ValueError, + ) + return 0 + + if data.shape[1] < 3: + log.warning('No uncertainty (sy) column provided. Defaulting to sqrt(y).') + + # Extract x, y data + x: np.ndarray = data[:, 0] + y: np.ndarray = data[:, 1] + + # Round x to 4 decimal places + x = np.round(x, 4) + + # Determine sy from column 3 if available, otherwise use sqrt(y) + sy: np.ndarray = data[:, 2] if data.shape[1] > 2 else np.sqrt(y) + + # Replace values smaller than 0.0001 with 1.0 + # TODO: Not used if loading from cif file? + sy = np.where(sy < 0.0001, 1.0, sy) + + # Set the experiment data + self.data._create_items_set_xcoord_and_id(x) + self.data._set_intensity_meas(y) + self.data._set_intensity_meas_su(sy) + + return len(x) + + # ------------------------------------------------------------------ + # Instrument (switchable-category pattern) + # ------------------------------------------------------------------ + + @property + def instrument(self) -> object: + """Active instrument model for this experiment.""" + return self._instrument + + @property + def instrument_type(self) -> str: + """Tag of the active instrument type.""" + return self._instrument_type + + @instrument_type.setter + def instrument_type(self, new_type: str) -> None: + """ + Switch to a different instrument type. + + Parameters + ---------- + new_type : str + Instrument tag (e.g. ``'cwl-pd'``). + """ + supported = InstrumentFactory.supported_for( + scattering_type=self.type.scattering_type.value, + beam_mode=self.type.beam_mode.value, + sample_form=self.type.sample_form.value, + ) + supported_tags = [k.type_info.tag for k in supported] + if new_type not in supported_tags: + log.warning( + f"Unsupported instrument type '{new_type}'. " + f'Supported: {supported_tags}. ' + f"For more information, use 'show_supported_instrument_types()'", + ) + return + self._instrument = InstrumentFactory.create(new_type) + self._instrument_type = new_type + console.paragraph(f"Instrument type for experiment '{self.name}' changed to") + console.print(new_type) + + def show_supported_instrument_types(self) -> None: + """Print a table of supported instrument types.""" + InstrumentFactory.show_supported( + scattering_type=self.type.scattering_type.value, + beam_mode=self.type.beam_mode.value, + sample_form=self.type.sample_form.value, + ) + + def show_current_instrument_type(self) -> None: + """Print the currently used instrument type.""" + console.paragraph('Current instrument type') + console.print(self.instrument_type) + + # ------------------------------------------------------------------ + # Background (switchable-category pattern) + # ------------------------------------------------------------------ + + @property + def background_type(self) -> object: + """Current background type enum value.""" + return self._background_type + + @background_type.setter + def background_type(self, new_type: str) -> None: + """Set a new background type and recreate background object.""" + if self._background_type == new_type: + console.paragraph(f"Background type for experiment '{self.name}' already set to") + console.print(new_type) + return + + supported_tags = BackgroundFactory.supported_tags() + if new_type not in supported_tags: + log.warning( + f"Unsupported background type '{new_type}'. " + f'Supported: {supported_tags}. ' + f"For more information, use 'show_supported_background_types()'", + ) + return + + if len(self._background) > 0: + log.warning( + f'Switching background type discards {len(self._background)} ' + f'existing background point(s).', + ) + + self._background = BackgroundFactory.create(new_type) + self._background_type = new_type + console.paragraph(f"Background type for experiment '{self.name}' changed to") + console.print(new_type) + + @property + def background(self) -> object: + """Active background model for this experiment.""" + return self._background + + def show_supported_background_types(self) -> None: + """Print a table of supported background types.""" + BackgroundFactory.show_supported() + + def show_current_background_type(self) -> None: + """Print the currently used background type.""" + console.paragraph('Current background type') + console.print(self.background_type) diff --git a/src/easydiffraction/datablocks/experiment/item/bragg_sc.py b/src/easydiffraction/datablocks/experiment/item/bragg_sc.py new file mode 100644 index 00000000..f77d3af6 --- /dev/null +++ b/src/easydiffraction/datablocks/experiment/item/bragg_sc.py @@ -0,0 +1,162 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import numpy as np + +from easydiffraction.core.metadata import Compatibility +from easydiffraction.core.metadata import TypeInfo +from easydiffraction.datablocks.experiment.item.base import ScExperimentBase +from easydiffraction.datablocks.experiment.item.enums import BeamModeEnum +from easydiffraction.datablocks.experiment.item.enums import SampleFormEnum +from easydiffraction.datablocks.experiment.item.enums import ScatteringTypeEnum +from easydiffraction.datablocks.experiment.item.factory import ExperimentFactory +from easydiffraction.io.ascii import load_numeric_block +from easydiffraction.utils.logging import log + +if TYPE_CHECKING: + from easydiffraction.datablocks.experiment.categories.experiment_type import ExperimentType + + +@ExperimentFactory.register +class CwlScExperiment(ScExperimentBase): + """Bragg constant-wavelength single-crystal experiment.""" + + type_info = TypeInfo( + tag='bragg-sc-cwl', + description='Bragg CWL single-crystal experiment', + ) + compatibility = Compatibility( + scattering_type=frozenset({ScatteringTypeEnum.BRAGG}), + sample_form=frozenset({SampleFormEnum.SINGLE_CRYSTAL}), + beam_mode=frozenset({BeamModeEnum.CONSTANT_WAVELENGTH}), + ) + + def __init__( + self, + *, + name: str, + type: ExperimentType, + ) -> None: + super().__init__(name=name, type=type) + + def _load_ascii_data_to_experiment(self, data_path: str) -> int: + """ + Load measured data from an ASCII file into the data category. + + The file format is space/column separated with 5 columns: ``h k + l Iobs sIobs``. + + Parameters + ---------- + data_path : str + Path to the ASCII data file. + + Returns + ------- + int + Number of loaded data points. + """ + data = load_numeric_block(data_path) + + if data.shape[1] < 5: + log.error( + 'Data file must have at least 5 columns: h, k, l, Iobs, sIobs.', + exc_type=ValueError, + ) + return 0 + + # Extract Miller indices h, k, l + indices_h: np.ndarray = data[:, 0].astype(int) + indices_k: np.ndarray = data[:, 1].astype(int) + indices_l: np.ndarray = data[:, 2].astype(int) + + # Extract intensities and their standard uncertainties + integrated_intensities: np.ndarray = data[:, 3] + integrated_intensities_su: np.ndarray = data[:, 4] + + # Set the experiment data + self.data._create_items_set_hkl_and_id(indices_h, indices_k, indices_l) + self.data._set_intensity_meas(integrated_intensities) + self.data._set_intensity_meas_su(integrated_intensities_su) + + return len(indices_h) + + +@ExperimentFactory.register +class TofScExperiment(ScExperimentBase): + """Bragg time-of-flight single-crystal experiment.""" + + type_info = TypeInfo( + tag='bragg-sc-tof', + description='Bragg TOF single-crystal experiment', + ) + compatibility = Compatibility( + scattering_type=frozenset({ScatteringTypeEnum.BRAGG}), + sample_form=frozenset({SampleFormEnum.SINGLE_CRYSTAL}), + beam_mode=frozenset({BeamModeEnum.TIME_OF_FLIGHT}), + ) + + def __init__( + self, + *, + name: str, + type: ExperimentType, + ) -> None: + super().__init__(name=name, type=type) + + def _load_ascii_data_to_experiment(self, data_path: str) -> int: + """ + Load measured data from an ASCII file into the data category. + + The file format is space/column separated with 6 columns: ``h k + l Iobs sIobs wavelength``. + + Parameters + ---------- + data_path : str + Path to the ASCII data file. + + Returns + ------- + int + Number of loaded data points. + """ + try: + data = load_numeric_block(data_path) + except IOError as e: + log.error( + f'Failed to read data from {data_path}: {e}', + exc_type=IOError, + ) + return 0 + + if data.shape[1] < 6: + log.error( + 'Data file must have at least 6 columns: h, k, l, Iobs, sIobs, wavelength.', + exc_type=ValueError, + ) + return 0 + + # Extract Miller indices h, k, l + indices_h: np.ndarray = data[:, 0].astype(int) + indices_k: np.ndarray = data[:, 1].astype(int) + indices_l: np.ndarray = data[:, 2].astype(int) + + # Extract intensities and their standard uncertainties + integrated_intensities: np.ndarray = data[:, 3] + integrated_intensities_su: np.ndarray = data[:, 4] + + # Extract wavelength values + wavelength: np.ndarray = data[:, 5] + + # Set the experiment data + self.data._create_items_set_hkl_and_id(indices_h, indices_k, indices_l) + self.data._set_intensity_meas(integrated_intensities) + self.data._set_intensity_meas_su(integrated_intensities_su) + self.data._set_wavelength(wavelength) + + return len(indices_h) diff --git a/src/easydiffraction/experiments/experiment/enums.py b/src/easydiffraction/datablocks/experiment/item/enums.py similarity index 59% rename from src/easydiffraction/experiments/experiment/enums.py rename to src/easydiffraction/datablocks/experiment/item/enums.py index b15220fe..2375c38e 100644 --- a/src/easydiffraction/experiments/experiment/enums.py +++ b/src/easydiffraction/datablocks/experiment/item/enums.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause """Enumerations for experiment configuration (forms, modes, types).""" @@ -13,9 +13,25 @@ class SampleFormEnum(str, Enum): @classmethod def default(cls) -> 'SampleFormEnum': + """ + Return the default sample form (POWDER). + + Returns + ------- + 'SampleFormEnum' + The default enum member. + """ return cls.POWDER def description(self) -> str: + """ + Return a human-readable description of this sample form. + + Returns + ------- + str + Description string for the current enum member. + """ if self is SampleFormEnum.POWDER: return 'Powdered or polycrystalline sample.' elif self is SampleFormEnum.SINGLE_CRYSTAL: @@ -30,9 +46,25 @@ class ScatteringTypeEnum(str, Enum): @classmethod def default(cls) -> 'ScatteringTypeEnum': + """ + Return the default scattering type (BRAGG). + + Returns + ------- + 'ScatteringTypeEnum' + The default enum member. + """ return cls.BRAGG def description(self) -> str: + """ + Return a human-readable description of this scattering type. + + Returns + ------- + str + Description string for the current enum member. + """ if self is ScatteringTypeEnum.BRAGG: return 'Bragg diffraction for conventional structure refinement.' elif self is ScatteringTypeEnum.TOTAL: @@ -47,9 +79,25 @@ class RadiationProbeEnum(str, Enum): @classmethod def default(cls) -> 'RadiationProbeEnum': + """ + Return the default radiation probe (NEUTRON). + + Returns + ------- + 'RadiationProbeEnum' + The default enum member. + """ return cls.NEUTRON def description(self) -> str: + """ + Return a human-readable description of this radiation probe. + + Returns + ------- + str + Description string for the current enum member. + """ if self is RadiationProbeEnum.NEUTRON: return 'Neutron diffraction.' elif self is RadiationProbeEnum.XRAY: @@ -65,15 +113,45 @@ class BeamModeEnum(str, Enum): @classmethod def default(cls) -> 'BeamModeEnum': + """ + Return the default beam mode (CONSTANT_WAVELENGTH). + + Returns + ------- + 'BeamModeEnum' + The default enum member. + """ return cls.CONSTANT_WAVELENGTH def description(self) -> str: + """ + Return a human-readable description of this beam mode. + + Returns + ------- + str + Description string for the current enum member. + """ if self is BeamModeEnum.CONSTANT_WAVELENGTH: return 'Constant wavelength (CW) diffraction.' elif self is BeamModeEnum.TIME_OF_FLIGHT: return 'Time-of-flight (TOF) diffraction.' +class CalculatorEnum(str, Enum): + """Known calculation engine identifiers.""" + + CRYSPY = 'cryspy' + CRYSFML = 'crysfml' + PDFFIT = 'pdffit' + + +# TODO: Can, instead of hardcoding here, this info be auto-extracted +# from the actual peak profile classes defined in peak/cwl.py, tof.py, +# total.py? So that their Enum variable, string representation and +# description are defined in the respective classes? +# TODO: Can supported values be defined based on the structure of peak/? +# TODO: Can the same be reused for other enums in this file? class PeakProfileTypeEnum(str, Enum): """Available peak profile types per scattering and beam mode.""" @@ -90,6 +168,23 @@ def default( scattering_type: ScatteringTypeEnum | None = None, beam_mode: BeamModeEnum | None = None, ) -> 'PeakProfileTypeEnum': + """ + Return the default peak profile type for a given mode. + + Parameters + ---------- + scattering_type : ScatteringTypeEnum | None, default=None + Scattering type; defaults to + ``ScatteringTypeEnum.default()`` when ``None``. + beam_mode : BeamModeEnum | None, default=None + Beam mode; defaults to ``BeamModeEnum.default()`` when + ``None``. + + Returns + ------- + 'PeakProfileTypeEnum' + The default profile type for the given combination. + """ if scattering_type is None: scattering_type = ScatteringTypeEnum.default() if beam_mode is None: @@ -105,6 +200,14 @@ def default( }[(scattering_type, beam_mode)] def description(self) -> str: + """ + Return a human-readable description of this peak profile type. + + Returns + ------- + str + Description string for the current enum member. + """ if self is PeakProfileTypeEnum.PSEUDO_VOIGT: return 'Pseudo-Voigt profile' elif self is PeakProfileTypeEnum.SPLIT_PSEUDO_VOIGT: diff --git a/src/easydiffraction/datablocks/experiment/item/factory.py b/src/easydiffraction/datablocks/experiment/item/factory.py new file mode 100644 index 00000000..5c0b3094 --- /dev/null +++ b/src/easydiffraction/datablocks/experiment/item/factory.py @@ -0,0 +1,277 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +""" +Factory for creating experiment instances from various inputs. + +Provides individual class methods for each creation pathway: +``from_cif_path``, ``from_cif_str``, ``from_data_path``, and +``from_scratch``. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from typeguard import typechecked + +from easydiffraction.core.factory import FactoryBase +from easydiffraction.datablocks.experiment.categories.experiment_type import ExperimentType +from easydiffraction.datablocks.experiment.item.enums import BeamModeEnum +from easydiffraction.datablocks.experiment.item.enums import SampleFormEnum +from easydiffraction.datablocks.experiment.item.enums import ScatteringTypeEnum +from easydiffraction.io.cif.parse import document_from_path +from easydiffraction.io.cif.parse import document_from_string +from easydiffraction.io.cif.parse import name_from_block +from easydiffraction.io.cif.parse import pick_sole_block +from easydiffraction.utils.enums import VerbosityEnum +from easydiffraction.utils.logging import console +from easydiffraction.utils.logging import log + +if TYPE_CHECKING: + import gemmi + + from easydiffraction.datablocks.experiment.item.base import ExperimentBase + + +class ExperimentFactory(FactoryBase): + """Creates Experiment instances with only relevant attributes.""" + + _default_rules = { + frozenset({ + ('scattering_type', ScatteringTypeEnum.BRAGG), + ('sample_form', SampleFormEnum.POWDER), + }): 'bragg-pd', + frozenset({ + ('scattering_type', ScatteringTypeEnum.TOTAL), + ('sample_form', SampleFormEnum.POWDER), + }): 'total-pd', + frozenset({ + ('scattering_type', ScatteringTypeEnum.BRAGG), + ('sample_form', SampleFormEnum.SINGLE_CRYSTAL), + ('beam_mode', BeamModeEnum.CONSTANT_WAVELENGTH), + }): 'bragg-sc-cwl', + frozenset({ + ('scattering_type', ScatteringTypeEnum.BRAGG), + ('sample_form', SampleFormEnum.SINGLE_CRYSTAL), + ('beam_mode', BeamModeEnum.TIME_OF_FLIGHT), + }): 'bragg-sc-tof', + } + + # TODO: Add to core/factory.py? + def __init__(self) -> None: + log.error( + 'Experiment objects must be created using class methods such as ' + '`ExperimentFactory.from_cif_str(...)`, etc.' + ) + + # ------------------------------------------------------------------ + # Private helper methods + # ------------------------------------------------------------------ + + @classmethod + @typechecked + def _create_experiment_type( + cls, + sample_form: str | None = None, + beam_mode: str | None = None, + radiation_probe: str | None = None, + scattering_type: str | None = None, + ) -> ExperimentType: + """Construct ExperimentType with defaults for omitted values.""" + # Note: validation of input values is done via Descriptor setter + # methods + + et = ExperimentType() + + if sample_form is not None: + et._set_sample_form(sample_form) + if beam_mode is not None: + et._set_beam_mode(beam_mode) + if radiation_probe is not None: + et._set_radiation_probe(radiation_probe) + if scattering_type is not None: + et._set_scattering_type(scattering_type) + + return et + + @classmethod + @typechecked + def _resolve_class(cls, expt_type: ExperimentType) -> type: + """Look up the experiment class from the type enums.""" + tag = cls.default_tag( + scattering_type=expt_type.scattering_type.value, + sample_form=expt_type.sample_form.value, + beam_mode=expt_type.beam_mode.value, + ) + return cls._supported_map()[tag] + + @classmethod + # TODO: @typechecked fails to find gemmi? + def _from_gemmi_block( + cls, + block: gemmi.cif.Block, + ) -> ExperimentBase: + """Build a model instance from a single CIF block.""" + name = name_from_block(block) + + expt_type = ExperimentType() + for param in expt_type.parameters: + param.from_cif(block) + + expt_class = cls._resolve_class(expt_type) + expt_obj = expt_class(name=name, type=expt_type) + + for category in expt_obj.categories: + category.from_cif(block) + + return expt_obj + + # ------------------------------------------------------------------ + # Public methods + # ------------------------------------------------------------------ + + @classmethod + @typechecked + def from_scratch( + cls, + *, + name: str, + sample_form: str | None = None, + beam_mode: str | None = None, + radiation_probe: str | None = None, + scattering_type: str | None = None, + ) -> ExperimentBase: + """ + Create an experiment without measured data. + + Parameters + ---------- + name : str + Experiment identifier. + sample_form : str | None, default=None + Sample form (e.g. ``'powder'``). + beam_mode : str | None, default=None + Beam mode (e.g. ``'constant wavelength'``). + radiation_probe : str | None, default=None + Radiation probe (e.g. ``'neutron'``). + scattering_type : str | None, default=None + Scattering type (e.g. ``'bragg'``). + + Returns + ------- + ExperimentBase + An experiment instance with only metadata. + """ + expt_type = cls._create_experiment_type( + sample_form=sample_form, + beam_mode=beam_mode, + radiation_probe=radiation_probe, + scattering_type=scattering_type, + ) + expt_class = cls._resolve_class(expt_type) + expt_obj = expt_class(name=name, type=expt_type) + return expt_obj + + # TODO: add minimal default configuration for missing parameters + @classmethod + @typechecked + def from_cif_str( + cls, + cif_str: str, + ) -> ExperimentBase: + """ + Create an experiment from a CIF string. + + Parameters + ---------- + cif_str : str + Full CIF document as a string. + + Returns + ------- + ExperimentBase + A populated experiment instance. + """ + doc = document_from_string(cif_str) + block = pick_sole_block(doc) + return cls._from_gemmi_block(block) + + # TODO: Read content and call self.from_cif_str + @classmethod + @typechecked + def from_cif_path( + cls, + cif_path: str, + ) -> ExperimentBase: + """ + Create an experiment from a CIF file path. + + Parameters + ---------- + cif_path : str + Path to a CIF file. + + Returns + ------- + ExperimentBase + A populated experiment instance. + """ + doc = document_from_path(cif_path) + block = pick_sole_block(doc) + return cls._from_gemmi_block(block) + + @classmethod + @typechecked + def from_data_path( + cls, + *, + name: str, + data_path: str, + sample_form: str | None = None, + beam_mode: str | None = None, + radiation_probe: str | None = None, + scattering_type: str | None = None, + verbosity: VerbosityEnum = VerbosityEnum.FULL, + ) -> ExperimentBase: + """ + Create an experiment from a raw data ASCII file. + + Parameters + ---------- + name : str + Experiment identifier. + data_path : str + Path to the measured data file. + sample_form : str | None, default=None + Sample form (e.g. ``'powder'``). + beam_mode : str | None, default=None + Beam mode (e.g. ``'constant wavelength'``). + radiation_probe : str | None, default=None + Radiation probe (e.g. ``'neutron'``). + scattering_type : str | None, default=None + Scattering type (e.g. ``'bragg'``). + verbosity : VerbosityEnum, default=VerbosityEnum.FULL + Console output verbosity. + + Returns + ------- + ExperimentBase + An experiment instance with measured data attached. + """ + expt_obj = cls.from_scratch( + name=name, + sample_form=sample_form, + beam_mode=beam_mode, + radiation_probe=radiation_probe, + scattering_type=scattering_type, + ) + + num_points = expt_obj._load_ascii_data_to_experiment(data_path) + + if verbosity is VerbosityEnum.FULL: + console.paragraph('Data loaded successfully') + console.print(f"Experiment 🔬 '{name}'. Number of data points: {num_points}.") + elif verbosity is VerbosityEnum.SHORT: + console.print(f"✅ Data loaded: Experiment 🔬 '{name}'. {num_points} points.") + + return expt_obj diff --git a/src/easydiffraction/datablocks/experiment/item/total_pd.py b/src/easydiffraction/datablocks/experiment/item/total_pd.py new file mode 100644 index 00000000..2ed856ba --- /dev/null +++ b/src/easydiffraction/datablocks/experiment/item/total_pd.py @@ -0,0 +1,92 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import numpy as np + +from easydiffraction.core.metadata import Compatibility +from easydiffraction.core.metadata import TypeInfo +from easydiffraction.datablocks.experiment.item.base import PdExperimentBase +from easydiffraction.datablocks.experiment.item.enums import BeamModeEnum +from easydiffraction.datablocks.experiment.item.enums import SampleFormEnum +from easydiffraction.datablocks.experiment.item.enums import ScatteringTypeEnum +from easydiffraction.datablocks.experiment.item.factory import ExperimentFactory + +if TYPE_CHECKING: + from easydiffraction.datablocks.experiment.categories.experiment_type import ExperimentType + + +@ExperimentFactory.register +class TotalPdExperiment(PdExperimentBase): + """PDF experiment class with specific attributes.""" + + type_info = TypeInfo( + tag='total-pd', + description='Total scattering (PDF) powder experiment', + ) + compatibility = Compatibility( + scattering_type=frozenset({ScatteringTypeEnum.TOTAL}), + sample_form=frozenset({SampleFormEnum.POWDER}), + beam_mode=frozenset({BeamModeEnum.CONSTANT_WAVELENGTH, BeamModeEnum.TIME_OF_FLIGHT}), + ) + + def __init__( + self, + name: str, + type: ExperimentType, + ) -> None: + super().__init__(name=name, type=type) + + def _load_ascii_data_to_experiment(self, data_path: str) -> int: + """ + Load x, y, sy values from an ASCII file into the experiment. + + The file must be structured as: x y sy + + Parameters + ---------- + data_path : str + Path to the ASCII data file. + + Returns + ------- + int + Number of loaded data points. + + Raises + ------ + ImportError + If the ``diffpy`` package is not installed. + IOError + If the data file cannot be read. + ValueError + If the data file has fewer than two columns. + """ + try: + from diffpy.utils.parsers.loaddata import loadData + except ImportError: + raise ImportError('diffpy module not found.') from None + try: + data = loadData(data_path) + except Exception as e: + raise IOError(f'Failed to read data from {data_path}: {e}') from e + + if data.shape[1] < 2: + raise ValueError('Data file must have at least two columns: x and y.') + + default_sy = 0.03 + if data.shape[1] < 3: + print(f'Warning: No uncertainty (sy) column provided. Defaulting to {default_sy}.') + + x = data[:, 0] + y = data[:, 1] + sy = data[:, 2] if data.shape[1] > 2 else np.full_like(y, fill_value=default_sy) + + self.data._create_items_set_xcoord_and_id(x) + self.data._set_g_r_meas(y) + self.data._set_g_r_meas_su(sy) + + return len(x) diff --git a/src/easydiffraction/datablocks/structure/__init__.py b/src/easydiffraction/datablocks/structure/__init__.py new file mode 100644 index 00000000..4e798e20 --- /dev/null +++ b/src/easydiffraction/datablocks/structure/__init__.py @@ -0,0 +1,2 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause diff --git a/src/easydiffraction/datablocks/structure/categories/__init__.py b/src/easydiffraction/datablocks/structure/categories/__init__.py new file mode 100644 index 00000000..4e798e20 --- /dev/null +++ b/src/easydiffraction/datablocks/structure/categories/__init__.py @@ -0,0 +1,2 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause diff --git a/src/easydiffraction/datablocks/structure/categories/atom_sites/__init__.py b/src/easydiffraction/datablocks/structure/categories/atom_sites/__init__.py new file mode 100644 index 00000000..7bd7b9ad --- /dev/null +++ b/src/easydiffraction/datablocks/structure/categories/atom_sites/__init__.py @@ -0,0 +1,5 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + +from easydiffraction.datablocks.structure.categories.atom_sites.default import AtomSite +from easydiffraction.datablocks.structure.categories.atom_sites.default import AtomSites diff --git a/src/easydiffraction/datablocks/structure/categories/atom_sites/default.py b/src/easydiffraction/datablocks/structure/categories/atom_sites/default.py new file mode 100644 index 00000000..265c3c62 --- /dev/null +++ b/src/easydiffraction/datablocks/structure/categories/atom_sites/default.py @@ -0,0 +1,380 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +""" +Atom site category. + +Defines :class:`AtomSite` items and :class:`AtomSites` collection used +in crystallographic structures. +""" + +from __future__ import annotations + +from cryspy.A_functions_base.database import DATABASE + +from easydiffraction.core.category import CategoryCollection +from easydiffraction.core.category import CategoryItem +from easydiffraction.core.metadata import TypeInfo +from easydiffraction.core.validation import AttributeSpec +from easydiffraction.core.validation import MembershipValidator +from easydiffraction.core.validation import RangeValidator +from easydiffraction.core.validation import RegexValidator +from easydiffraction.core.variable import Parameter +from easydiffraction.core.variable import StringDescriptor +from easydiffraction.crystallography import crystallography as ecr +from easydiffraction.datablocks.structure.categories.atom_sites.factory import AtomSitesFactory +from easydiffraction.io.cif.handler import CifHandler + + +class AtomSite(CategoryItem): + """ + Single atom site with fractional coordinates and ADP. + + Attributes are represented by descriptors to support validation and + CIF serialization. + """ + + def __init__(self) -> None: + """Initialise the atom site with default descriptor values.""" + super().__init__() + + self._label = StringDescriptor( + name='label', + description='Unique identifier for the atom site.', + value_spec=AttributeSpec( + default='Si', + # TODO: the following pattern is valid for dict key + # (keywords are not checked). CIF label is less strict. + # Do we need conversion between CIF and internal label? + validator=RegexValidator(pattern=r'^[A-Za-z_][A-Za-z0-9_]*$'), + ), + cif_handler=CifHandler(names=['_atom_site.label']), + ) + self._type_symbol = StringDescriptor( + name='type_symbol', + description='Chemical symbol of the atom at this site.', + value_spec=AttributeSpec( + default='Tb', + validator=MembershipValidator(allowed=self._type_symbol_allowed_values), + ), + cif_handler=CifHandler(names=['_atom_site.type_symbol']), + ) + self._fract_x = Parameter( + name='fract_x', + description='Fractional x-coordinate of the atom site within the unit cell.', + value_spec=AttributeSpec( + default=0.0, + validator=RangeValidator(), + ), + cif_handler=CifHandler(names=['_atom_site.fract_x']), + ) + self._fract_y = Parameter( + name='fract_y', + description='Fractional y-coordinate of the atom site within the unit cell.', + value_spec=AttributeSpec( + default=0.0, + validator=RangeValidator(), + ), + cif_handler=CifHandler(names=['_atom_site.fract_y']), + ) + self._fract_z = Parameter( + name='fract_z', + description='Fractional z-coordinate of the atom site within the unit cell.', + value_spec=AttributeSpec( + default=0.0, + validator=RangeValidator(), + ), + cif_handler=CifHandler(names=['_atom_site.fract_z']), + ) + self._wyckoff_letter = StringDescriptor( + name='wyckoff_letter', + description='Wyckoff letter indicating the symmetry of the ' + 'atom site within the space group.', + value_spec=AttributeSpec( + default=self._wyckoff_letter_default_value, + validator=MembershipValidator(allowed=self._wyckoff_letter_allowed_values), + ), + cif_handler=CifHandler( + names=[ + '_atom_site.Wyckoff_letter', + '_atom_site.Wyckoff_symbol', + ] + ), + ) + self._occupancy = Parameter( + name='occupancy', + description='Occupancy of the atom site, representing the ' + 'fraction of the site occupied by the atom type.', + value_spec=AttributeSpec( + default=1.0, + validator=RangeValidator(), + ), + cif_handler=CifHandler(names=['_atom_site.occupancy']), + ) + self._b_iso = Parameter( + name='b_iso', + description='Isotropic atomic displacement parameter (ADP) for the atom site.', + units='Ų', + value_spec=AttributeSpec( + default=0.0, + validator=RangeValidator(ge=0.0), + ), + cif_handler=CifHandler(names=['_atom_site.B_iso_or_equiv']), + ) + self._adp_type = StringDescriptor( + name='adp_type', + description='Type of atomic displacement parameter (ADP) ' + 'used (e.g., Biso, Uiso, Uani, Bani).', + value_spec=AttributeSpec( + default='Biso', + validator=MembershipValidator(allowed=['Biso']), + ), + cif_handler=CifHandler(names=['_atom_site.adp_type']), + ) + + self._identity.category_code = 'atom_site' + self._identity.category_entry_name = lambda: str(self.label.value) + + # ------------------------------------------------------------------ + # Private helper methods + # ------------------------------------------------------------------ + + @property + def _type_symbol_allowed_values(self) -> list[str]: + """ + Return chemical symbols accepted by *cryspy*. + + Returns + ------- + list[str] + Unique element/isotope symbols from the database. + """ + return list({key[1] for key in DATABASE['Isotopes']}) + + @property + def _wyckoff_letter_allowed_values(self) -> list[str]: + """ + Return allowed Wyckoff-letter symbols. + + Returns + ------- + list[str] + Currently a hard-coded placeholder list. + """ + # TODO: Need to now current space group. How to access it? Via + # parent Cell? Then letters = + # list(SPACE_GROUPS[62, 'cab']['Wyckoff_positions'].keys()) + # Temporarily return hardcoded list: + return ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i'] + + @property + def _wyckoff_letter_default_value(self) -> str: + """ + Return the default Wyckoff letter. + + Returns + ------- + str + First element of the allowed values list. + """ + # TODO: What to pass as default? + return self._wyckoff_letter_allowed_values[0] + + # ------------------------------------------------------------------ + # Public properties + # ------------------------------------------------------------------ + + @property + def label(self) -> StringDescriptor: + """ + Unique identifier for the atom site. + + Reading this property returns the underlying + ``StringDescriptor`` object. Assigning to it updates the + parameter value. + """ + return self._label + + @label.setter + def label(self, value: str) -> None: + self._label.value = value + + @property + def type_symbol(self) -> StringDescriptor: + """ + Chemical symbol of the atom at this site. + + Reading this property returns the underlying + ``StringDescriptor`` object. Assigning to it updates the + parameter value. + """ + return self._type_symbol + + @type_symbol.setter + def type_symbol(self, value: str) -> None: + self._type_symbol.value = value + + @property + def adp_type(self) -> StringDescriptor: + """ + ADP type used (e.g., Biso, Uiso, Uani, Bani). + + Reading this property returns the underlying + ``StringDescriptor`` object. Assigning to it updates the + parameter value. + """ + return self._adp_type + + @adp_type.setter + def adp_type(self, value: str) -> None: + self._adp_type.value = value + + @property + def wyckoff_letter(self) -> StringDescriptor: + """ + Wyckoff letter for the atom site symmetry position. + + Reading this property returns the underlying + ``StringDescriptor`` object. Assigning to it updates the + parameter value. + """ + return self._wyckoff_letter + + @wyckoff_letter.setter + def wyckoff_letter(self, value: str) -> None: + self._wyckoff_letter.value = value + + @property + def fract_x(self) -> Parameter: + """ + Fractional x-coordinate of the atom site within the unit cell. + + Reading this property returns the underlying ``Parameter`` + object. Assigning to it updates the parameter value. + """ + return self._fract_x + + @fract_x.setter + def fract_x(self, value: float) -> None: + self._fract_x.value = value + + @property + def fract_y(self) -> Parameter: + """ + Fractional y-coordinate of the atom site within the unit cell. + + Reading this property returns the underlying ``Parameter`` + object. Assigning to it updates the parameter value. + """ + return self._fract_y + + @fract_y.setter + def fract_y(self, value: float) -> None: + self._fract_y.value = value + + @property + def fract_z(self) -> Parameter: + """ + Fractional z-coordinate of the atom site within the unit cell. + + Reading this property returns the underlying ``Parameter`` + object. Assigning to it updates the parameter value. + """ + return self._fract_z + + @fract_z.setter + def fract_z(self, value: float) -> None: + self._fract_z.value = value + + @property + def occupancy(self) -> Parameter: + """ + Occupancy fraction of the atom type at this site. + + Reading this property returns the underlying ``Parameter`` + object. Assigning to it updates the parameter value. + """ + return self._occupancy + + @occupancy.setter + def occupancy(self, value: float) -> None: + self._occupancy.value = value + + @property + def b_iso(self) -> Parameter: + """ + Isotropic ADP for the atom site (Ų). + + Reading this property returns the underlying ``Parameter`` + object. Assigning to it updates the parameter value. + """ + return self._b_iso + + @b_iso.setter + def b_iso(self, value: float) -> None: + self._b_iso.value = value + + +@AtomSitesFactory.register +class AtomSites(CategoryCollection): + """Collection of :class:`AtomSite` instances.""" + + type_info = TypeInfo( + tag='default', + description='Atom sites collection', + ) + + def __init__(self) -> None: + """Initialise an empty atom-sites collection.""" + super().__init__(item_type=AtomSite) + + # ------------------------------------------------------------------ + # Private helper methods + # ------------------------------------------------------------------ + + def _apply_atomic_coordinates_symmetry_constraints(self) -> None: + """ + Apply symmetry rules to fractional coordinates of every site. + + Uses the parent structure's space-group symbol, IT coordinate + system code and each atom's Wyckoff letter. Atoms without a + Wyckoff letter are silently skipped. + """ + structure = self._parent + space_group_name = structure.space_group.name_h_m.value + space_group_coord_code = structure.space_group.it_coordinate_system_code.value + for atom in self._items: + dummy_atom = { + 'fract_x': atom.fract_x.value, + 'fract_y': atom.fract_y.value, + 'fract_z': atom.fract_z.value, + } + wl = atom.wyckoff_letter.value + if not wl: + # TODO: Decide how to handle this case + continue + ecr.apply_atom_site_symmetry_constraints( + atom_site=dummy_atom, + name_hm=space_group_name, + coord_code=space_group_coord_code, + wyckoff_letter=wl, + ) + atom.fract_x.value = dummy_atom['fract_x'] + atom.fract_y.value = dummy_atom['fract_y'] + atom.fract_z.value = dummy_atom['fract_z'] + + def _update( + self, + called_by_minimizer: bool = False, + ) -> None: + """ + Recalculate atom sites after a change. + + Parameters + ---------- + called_by_minimizer : bool, default=False + Whether the update was triggered by the fitting minimizer. + Currently unused. + """ + del called_by_minimizer + + self._apply_atomic_coordinates_symmetry_constraints() diff --git a/src/easydiffraction/datablocks/structure/categories/atom_sites/factory.py b/src/easydiffraction/datablocks/structure/categories/atom_sites/factory.py new file mode 100644 index 00000000..c91b3dda --- /dev/null +++ b/src/easydiffraction/datablocks/structure/categories/atom_sites/factory.py @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Atom-sites factory — delegates entirely to ``FactoryBase``.""" + +from __future__ import annotations + +from easydiffraction.core.factory import FactoryBase + + +class AtomSitesFactory(FactoryBase): + """Create atom-sites collections by tag.""" + + _default_rules = { + frozenset(): 'default', + } diff --git a/src/easydiffraction/datablocks/structure/categories/cell/__init__.py b/src/easydiffraction/datablocks/structure/categories/cell/__init__.py new file mode 100644 index 00000000..16f6cab2 --- /dev/null +++ b/src/easydiffraction/datablocks/structure/categories/cell/__init__.py @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + +from easydiffraction.datablocks.structure.categories.cell.default import Cell diff --git a/src/easydiffraction/datablocks/structure/categories/cell/default.py b/src/easydiffraction/datablocks/structure/categories/cell/default.py new file mode 100644 index 00000000..53cc98ee --- /dev/null +++ b/src/easydiffraction/datablocks/structure/categories/cell/default.py @@ -0,0 +1,235 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Unit cell parameters category for structures.""" + +from __future__ import annotations + +from easydiffraction.core.category import CategoryItem +from easydiffraction.core.metadata import TypeInfo +from easydiffraction.core.validation import AttributeSpec +from easydiffraction.core.validation import RangeValidator +from easydiffraction.core.variable import Parameter +from easydiffraction.crystallography import crystallography as ecr +from easydiffraction.datablocks.structure.categories.cell.factory import CellFactory +from easydiffraction.io.cif.handler import CifHandler + + +@CellFactory.register +class Cell(CategoryItem): + """ + Unit cell with lengths a, b, c and angles alpha, beta, gamma. + + All six lattice parameters are exposed as :class:`Parameter` + descriptors supporting validation, fitting and CIF serialization. + """ + + type_info = TypeInfo( + tag='default', + description='Unit cell parameters', + ) + + def __init__(self) -> None: + """Initialise the unit cell with default parameter values.""" + super().__init__() + + self._length_a = Parameter( + name='length_a', + description='Length of the a axis of the unit cell', + units='Å', + value_spec=AttributeSpec( + default=10.0, + validator=RangeValidator(ge=0, le=1000), + ), + cif_handler=CifHandler(names=['_cell.length_a']), + ) + self._length_b = Parameter( + name='length_b', + description='Length of the b axis of the unit cell', + units='Å', + value_spec=AttributeSpec( + default=10.0, + validator=RangeValidator(ge=0, le=1000), + ), + cif_handler=CifHandler(names=['_cell.length_b']), + ) + self._length_c = Parameter( + name='length_c', + description='Length of the c axis of the unit cell', + units='Å', + value_spec=AttributeSpec( + default=10.0, + validator=RangeValidator(ge=0, le=1000), + ), + cif_handler=CifHandler(names=['_cell.length_c']), + ) + self._angle_alpha = Parameter( + name='angle_alpha', + description='Angle between edges b and c', + units='deg', + value_spec=AttributeSpec( + default=90.0, + validator=RangeValidator(ge=0, le=180), + ), + cif_handler=CifHandler(names=['_cell.angle_alpha']), + ) + self._angle_beta = Parameter( + name='angle_beta', + description='Angle between edges a and c', + units='deg', + value_spec=AttributeSpec( + default=90.0, + validator=RangeValidator(ge=0, le=180), + ), + cif_handler=CifHandler(names=['_cell.angle_beta']), + ) + self._angle_gamma = Parameter( + name='angle_gamma', + description='Angle between edges a and b', + units='deg', + value_spec=AttributeSpec( + default=90.0, + validator=RangeValidator(ge=0, le=180), + ), + cif_handler=CifHandler(names=['_cell.angle_gamma']), + ) + + self._identity.category_code = 'cell' + + # ------------------------------------------------------------------ + # Private helper methods + # ------------------------------------------------------------------ + + def _apply_cell_symmetry_constraints(self) -> None: + """ + Apply symmetry constraints to cell parameters in place. + + Uses the parent structure's space-group symbol to determine + which lattice parameters are dependent and sets them + accordingly. + """ + dummy_cell = { + 'lattice_a': self.length_a.value, + 'lattice_b': self.length_b.value, + 'lattice_c': self.length_c.value, + 'angle_alpha': self.angle_alpha.value, + 'angle_beta': self.angle_beta.value, + 'angle_gamma': self.angle_gamma.value, + } + space_group_name = self._parent.space_group.name_h_m.value + + ecr.apply_cell_symmetry_constraints( + cell=dummy_cell, + name_hm=space_group_name, + ) + + self.length_a.value = dummy_cell['lattice_a'] + self.length_b.value = dummy_cell['lattice_b'] + self.length_c.value = dummy_cell['lattice_c'] + self.angle_alpha.value = dummy_cell['angle_alpha'] + self.angle_beta.value = dummy_cell['angle_beta'] + self.angle_gamma.value = dummy_cell['angle_gamma'] + + def _update( + self, + called_by_minimizer: bool = False, + ) -> None: + """ + Recalculate cell parameters after a change. + + Parameters + ---------- + called_by_minimizer : bool, default=False + Whether the update was triggered by the fitting minimizer. + Currently unused. + """ + del called_by_minimizer # TODO: ??? + + self._apply_cell_symmetry_constraints() + + # ------------------------------------------------------------------ + # Public properties + # ------------------------------------------------------------------ + + @property + def length_a(self) -> Parameter: + """ + Length of the a axis of the unit cell (Å). + + Reading this property returns the underlying ``Parameter`` + object. Assigning to it updates the parameter value. + """ + return self._length_a + + @length_a.setter + def length_a(self, value: float) -> None: + self._length_a.value = value + + @property + def length_b(self) -> Parameter: + """ + Length of the b axis of the unit cell (Å). + + Reading this property returns the underlying ``Parameter`` + object. Assigning to it updates the parameter value. + """ + return self._length_b + + @length_b.setter + def length_b(self, value: float) -> None: + self._length_b.value = value + + @property + def length_c(self) -> Parameter: + """ + Length of the c axis of the unit cell (Å). + + Reading this property returns the underlying ``Parameter`` + object. Assigning to it updates the parameter value. + """ + return self._length_c + + @length_c.setter + def length_c(self, value: float) -> None: + self._length_c.value = value + + @property + def angle_alpha(self) -> Parameter: + """ + Angle between edges b and c (deg). + + Reading this property returns the underlying ``Parameter`` + object. Assigning to it updates the parameter value. + """ + return self._angle_alpha + + @angle_alpha.setter + def angle_alpha(self, value: float) -> None: + self._angle_alpha.value = value + + @property + def angle_beta(self) -> Parameter: + """ + Angle between edges a and c (deg). + + Reading this property returns the underlying ``Parameter`` + object. Assigning to it updates the parameter value. + """ + return self._angle_beta + + @angle_beta.setter + def angle_beta(self, value: float) -> None: + self._angle_beta.value = value + + @property + def angle_gamma(self) -> Parameter: + """ + Angle between edges a and b (deg). + + Reading this property returns the underlying ``Parameter`` + object. Assigning to it updates the parameter value. + """ + return self._angle_gamma + + @angle_gamma.setter + def angle_gamma(self, value: float) -> None: + self._angle_gamma.value = value diff --git a/src/easydiffraction/datablocks/structure/categories/cell/factory.py b/src/easydiffraction/datablocks/structure/categories/cell/factory.py new file mode 100644 index 00000000..6817b2d7 --- /dev/null +++ b/src/easydiffraction/datablocks/structure/categories/cell/factory.py @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Cell factory — delegates entirely to ``FactoryBase``.""" + +from __future__ import annotations + +from easydiffraction.core.factory import FactoryBase + + +class CellFactory(FactoryBase): + """Create unit-cell categories by tag.""" + + _default_rules = { + frozenset(): 'default', + } diff --git a/src/easydiffraction/datablocks/structure/categories/space_group/__init__.py b/src/easydiffraction/datablocks/structure/categories/space_group/__init__.py new file mode 100644 index 00000000..a8b33e62 --- /dev/null +++ b/src/easydiffraction/datablocks/structure/categories/space_group/__init__.py @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + +from easydiffraction.datablocks.structure.categories.space_group.default import SpaceGroup diff --git a/src/easydiffraction/datablocks/structure/categories/space_group/default.py b/src/easydiffraction/datablocks/structure/categories/space_group/default.py new file mode 100644 index 00000000..a91cc554 --- /dev/null +++ b/src/easydiffraction/datablocks/structure/categories/space_group/default.py @@ -0,0 +1,163 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Space group category for crystallographic structures.""" + +from __future__ import annotations + +from cryspy.A_functions_base.function_2_space_group import ACCESIBLE_NAME_HM_SHORT +from cryspy.A_functions_base.function_2_space_group import ( + get_it_coordinate_system_codes_by_it_number, +) +from cryspy.A_functions_base.function_2_space_group import get_it_number_by_name_hm_short + +from easydiffraction.core.category import CategoryItem +from easydiffraction.core.metadata import TypeInfo +from easydiffraction.core.validation import AttributeSpec +from easydiffraction.core.validation import MembershipValidator +from easydiffraction.core.variable import StringDescriptor +from easydiffraction.datablocks.structure.categories.space_group.factory import SpaceGroupFactory +from easydiffraction.io.cif.handler import CifHandler + + +@SpaceGroupFactory.register +class SpaceGroup(CategoryItem): + """ + Space group with H-M symbol and IT coordinate system code. + + Holds the space-group symbol (``name_h_m``) and the International + Tables coordinate-system qualifier (``it_coordinate_system_code``). + Changing the symbol automatically resets the coordinate-system code + to the first allowed value for the new group. + """ + + type_info = TypeInfo( + tag='default', + description='Space group symmetry', + ) + + def __init__(self) -> None: + """Initialise the space group with default values.""" + super().__init__() + + self._name_h_m = StringDescriptor( + name='name_h_m', + description='Hermann-Mauguin symbol of the space group.', + value_spec=AttributeSpec( + default='P 1', + validator=MembershipValidator( + allowed=lambda: self._name_h_m_allowed_values, + ), + ), + cif_handler=CifHandler( + # TODO: Keep only version with "." and automate ... + names=[ + '_space_group.name_H-M_alt', + '_space_group_name_H-M_alt', + '_symmetry.space_group_name_H-M', + '_symmetry_space_group_name_H-M', + ] + ), + ) + self._it_coordinate_system_code = StringDescriptor( + name='it_coordinate_system_code', + description='A qualifier identifying which setting in IT is used.', + value_spec=AttributeSpec( + default=lambda: self._it_coordinate_system_code_default_value, + validator=MembershipValidator( + allowed=lambda: self._it_coordinate_system_code_allowed_values + ), + ), + cif_handler=CifHandler( + names=[ + '_space_group.IT_coordinate_system_code', + '_space_group_IT_coordinate_system_code', + '_symmetry.IT_coordinate_system_code', + '_symmetry_IT_coordinate_system_code', + ] + ), + ) + + self._identity.category_code = 'space_group' + + # ------------------------------------------------------------------ + # Private helper methods + # ------------------------------------------------------------------ + + def _reset_it_coordinate_system_code(self) -> None: + """Reset IT coordinate system code to default for this group.""" + self._it_coordinate_system_code.value = self._it_coordinate_system_code_default_value + + @property + def _name_h_m_allowed_values(self) -> list[str]: + """ + Return the list of recognised Hermann–Mauguin short symbols. + + Returns + ------- + list[str] + All short H-M symbols known to *cryspy*. + """ + return ACCESIBLE_NAME_HM_SHORT + + @property + def _it_coordinate_system_code_allowed_values(self) -> list[str]: + """ + Return allowed IT coordinate system codes for the current group. + + Returns + ------- + list[str] + Coordinate-system codes, or ``['']`` when none are defined. + """ + name = self.name_h_m.value + it_number = get_it_number_by_name_hm_short(name) + codes = get_it_coordinate_system_codes_by_it_number(it_number) + codes = [str(code) for code in codes] + return codes if codes else [''] + + @property + def _it_coordinate_system_code_default_value(self) -> str: + """ + Return the default IT coordinate system code. + + Returns + ------- + str + First element of the allowed codes list. + """ + return self._it_coordinate_system_code_allowed_values[0] + + # ------------------------------------------------------------------ + # Public properties + # ------------------------------------------------------------------ + + @property + def name_h_m(self) -> StringDescriptor: + """ + Hermann-Mauguin symbol of the space group. + + Reading this property returns the underlying + ``StringDescriptor`` object. Assigning to it updates the + parameter value. + """ + return self._name_h_m + + @name_h_m.setter + def name_h_m(self, value: str) -> None: + self._name_h_m.value = value + self._reset_it_coordinate_system_code() + + @property + def it_coordinate_system_code(self) -> StringDescriptor: + """ + A qualifier identifying which setting in IT is used. + + Reading this property returns the underlying + ``StringDescriptor`` object. Assigning to it updates the + parameter value. + """ + return self._it_coordinate_system_code + + @it_coordinate_system_code.setter + def it_coordinate_system_code(self, value: str) -> None: + self._it_coordinate_system_code.value = value diff --git a/src/easydiffraction/datablocks/structure/categories/space_group/factory.py b/src/easydiffraction/datablocks/structure/categories/space_group/factory.py new file mode 100644 index 00000000..9ef8611d --- /dev/null +++ b/src/easydiffraction/datablocks/structure/categories/space_group/factory.py @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Space-group factory — delegates entirely to ``FactoryBase``.""" + +from __future__ import annotations + +from easydiffraction.core.factory import FactoryBase + + +class SpaceGroupFactory(FactoryBase): + """Create space-group categories by tag.""" + + _default_rules = { + frozenset(): 'default', + } diff --git a/src/easydiffraction/datablocks/structure/collection.py b/src/easydiffraction/datablocks/structure/collection.py new file mode 100644 index 00000000..801b416a --- /dev/null +++ b/src/easydiffraction/datablocks/structure/collection.py @@ -0,0 +1,92 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Collection of structure data blocks.""" + +from typeguard import typechecked + +from easydiffraction.core.datablock import DatablockCollection +from easydiffraction.datablocks.structure.item.base import Structure +from easydiffraction.datablocks.structure.item.factory import StructureFactory +from easydiffraction.utils.logging import console + + +class Structures(DatablockCollection): + """ + Ordered collection of :class:`Structure` instances. + + Provides convenience ``add_from_*`` methods that mirror the + :class:`StructureFactory` classmethods plus a bare :meth:`add` for + inserting pre-built structures. + """ + + def __init__(self) -> None: + """Initialise an empty structures collection.""" + super().__init__(item_type=Structure) + + # ------------------------------------------------------------------ + # Public methods + # ------------------------------------------------------------------ + + # TODO: Make abstract in DatablockCollection? + @typechecked + def create( + self, + *, + name: str, + ) -> None: + """ + Create a minimal structure and add it to the collection. + + Parameters + ---------- + name : str + Identifier for the new structure. + """ + structure = StructureFactory.from_scratch(name=name) + self.add(structure) + + # TODO: Move to DatablockCollection? + @typechecked + def add_from_cif_str( + self, + cif_str: str, + ) -> None: + """ + Create a structure from CIF content and add it. + + Parameters + ---------- + cif_str : str + CIF file content as a string. + """ + structure = StructureFactory.from_cif_str(cif_str) + self.add(structure) + + # TODO: Move to DatablockCollection? + @typechecked + def add_from_cif_path( + self, + cif_path: str, + ) -> None: + """ + Create a structure from a CIF file and add it. + + Parameters + ---------- + cif_path : str + Filesystem path to a CIF file. + """ + structure = StructureFactory.from_cif_path(cif_path) + self.add(structure) + + # TODO: Move to DatablockCollection? + def show_names(self) -> None: + """List all structure names in the collection.""" + console.paragraph('Defined structures' + ' 🧩') + console.print(self.names) + + # TODO: Move to DatablockCollection? + def show_params(self) -> None: + """Show parameters of all structures in the collection.""" + for structure in self.values(): + structure.show_params() diff --git a/src/easydiffraction/datablocks/structure/item/__init__.py b/src/easydiffraction/datablocks/structure/item/__init__.py new file mode 100644 index 00000000..4e798e20 --- /dev/null +++ b/src/easydiffraction/datablocks/structure/item/__init__.py @@ -0,0 +1,2 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause diff --git a/src/easydiffraction/datablocks/structure/item/base.py b/src/easydiffraction/datablocks/structure/item/base.py new file mode 100644 index 00000000..80d8f76a --- /dev/null +++ b/src/easydiffraction/datablocks/structure/item/base.py @@ -0,0 +1,255 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Structure datablock item.""" + +from typeguard import typechecked + +from easydiffraction.core.datablock import DatablockItem +from easydiffraction.datablocks.structure.categories.atom_sites import AtomSites +from easydiffraction.datablocks.structure.categories.atom_sites.factory import AtomSitesFactory +from easydiffraction.datablocks.structure.categories.cell import Cell +from easydiffraction.datablocks.structure.categories.cell.factory import CellFactory +from easydiffraction.datablocks.structure.categories.space_group import SpaceGroup +from easydiffraction.datablocks.structure.categories.space_group.factory import SpaceGroupFactory +from easydiffraction.utils.logging import console +from easydiffraction.utils.logging import log +from easydiffraction.utils.utils import render_cif + + +class Structure(DatablockItem): + """Structure datablock item.""" + + def __init__( + self, + *, + name: str, + ) -> None: + super().__init__() + self._name = name + self._cell_type: str = CellFactory.default_tag() + self._cell = CellFactory.create(self._cell_type) + self._space_group_type: str = SpaceGroupFactory.default_tag() + self._space_group = SpaceGroupFactory.create(self._space_group_type) + self._atom_sites_type: str = AtomSitesFactory.default_tag() + self._atom_sites = AtomSitesFactory.create(self._atom_sites_type) + self._identity.datablock_entry_name = lambda: self.name + + # ------------------------------------------------------------------ + # Public properties + # ------------------------------------------------------------------ + + @property + def name(self) -> str: + """ + Name identifier for this structure. + + Returns + ------- + str + The structure's name. + """ + return self._name + + @name.setter + @typechecked + def name(self, new: str) -> None: + """ + Set the name identifier for this structure. + + Parameters + ---------- + new : str + New name string. + """ + self._name = new + + # ------------------------------------------------------------------ + # Cell (switchable-category pattern) + # ------------------------------------------------------------------ + + @property + def cell(self) -> Cell: + """Unit-cell category for this structure.""" + return self._cell + + @cell.setter + @typechecked + def cell(self, new: Cell) -> None: + """ + Replace the unit-cell category for this structure. + + Parameters + ---------- + new : Cell + New unit-cell instance. + """ + self._cell = new + + @property + def cell_type(self) -> str: + """Tag of the active unit-cell type.""" + return self._cell_type + + @cell_type.setter + def cell_type(self, new_type: str) -> None: + """ + Switch to a different unit-cell type. + + Parameters + ---------- + new_type : str + Cell tag (e.g. ``'default'``). + """ + supported_tags = CellFactory.supported_tags() + if new_type not in supported_tags: + log.warning( + f"Unsupported cell type '{new_type}'. " + f'Supported: {supported_tags}. ' + f"For more information, use 'show_supported_cell_types()'", + ) + return + self._cell = CellFactory.create(new_type) + self._cell_type = new_type + console.paragraph(f"Cell type for structure '{self.name}' changed to") + console.print(new_type) + + def show_supported_cell_types(self) -> None: + """Print a table of supported unit-cell types.""" + CellFactory.show_supported() + + def show_current_cell_type(self) -> None: + """Print the currently used unit-cell type.""" + console.paragraph('Current cell type') + console.print(self.cell_type) + + # ------------------------------------------------------------------ + # Space group (switchable-category pattern) + # ------------------------------------------------------------------ + + @property + def space_group(self) -> SpaceGroup: + """Space-group category for this structure.""" + return self._space_group + + @space_group.setter + @typechecked + def space_group(self, new: SpaceGroup) -> None: + """ + Replace the space-group category for this structure. + + Parameters + ---------- + new : SpaceGroup + New space-group instance. + """ + self._space_group = new + + @property + def space_group_type(self) -> str: + """Tag of the active space-group type.""" + return self._space_group_type + + @space_group_type.setter + def space_group_type(self, new_type: str) -> None: + """ + Switch to a different space-group type. + + Parameters + ---------- + new_type : str + Space-group tag (e.g. ``'default'``). + """ + supported_tags = SpaceGroupFactory.supported_tags() + if new_type not in supported_tags: + log.warning( + f"Unsupported space group type '{new_type}'. " + f'Supported: {supported_tags}. ' + f"For more information, use 'show_supported_space_group_types()'", + ) + return + self._space_group = SpaceGroupFactory.create(new_type) + self._space_group_type = new_type + console.paragraph(f"Space group type for structure '{self.name}' changed to") + console.print(new_type) + + def show_supported_space_group_types(self) -> None: + """Print a table of supported space-group types.""" + SpaceGroupFactory.show_supported() + + def show_current_space_group_type(self) -> None: + """Print the currently used space-group type.""" + console.paragraph('Current space group type') + console.print(self.space_group_type) + + # ------------------------------------------------------------------ + # Atom sites (switchable-category pattern) + # ------------------------------------------------------------------ + + @property + def atom_sites(self) -> AtomSites: + """Atom-sites collection for this structure.""" + return self._atom_sites + + @atom_sites.setter + @typechecked + def atom_sites(self, new: AtomSites) -> None: + """ + Replace the atom-sites collection for this structure. + + Parameters + ---------- + new : AtomSites + New atom-sites collection. + """ + self._atom_sites = new + + @property + def atom_sites_type(self) -> str: + """Tag of the active atom-sites collection type.""" + return self._atom_sites_type + + @atom_sites_type.setter + def atom_sites_type(self, new_type: str) -> None: + """ + Switch to a different atom-sites collection type. + + Parameters + ---------- + new_type : str + Atom-sites tag (e.g. ``'default'``). + """ + supported_tags = AtomSitesFactory.supported_tags() + if new_type not in supported_tags: + log.warning( + f"Unsupported atom sites type '{new_type}'. " + f'Supported: {supported_tags}. ' + f"For more information, use 'show_supported_atom_sites_types()'", + ) + return + self._atom_sites = AtomSitesFactory.create(new_type) + self._atom_sites_type = new_type + console.paragraph(f"Atom sites type for structure '{self.name}' changed to") + console.print(new_type) + + def show_supported_atom_sites_types(self) -> None: + """Print a table of supported atom-sites collection types.""" + AtomSitesFactory.show_supported() + + def show_current_atom_sites_type(self) -> None: + """Print the currently used atom-sites collection type.""" + console.paragraph('Current atom sites type') + console.print(self.atom_sites_type) + + # ------------------------------------------------------------------ + # Public methods + # ------------------------------------------------------------------ + + def show(self) -> None: + """Display an ASCII projection of the structure in 2D.""" + console.paragraph(f"Structure 🧩 '{self.name}'") + console.print('Not implemented yet.') + + def show_as_cif(self) -> None: + """Render the CIF text for this structure in the terminal.""" + console.paragraph(f"Structure 🧩 '{self.name}' as cif") + render_cif(self.as_cif) diff --git a/src/easydiffraction/datablocks/structure/item/factory.py b/src/easydiffraction/datablocks/structure/item/factory.py new file mode 100644 index 00000000..567d26dc --- /dev/null +++ b/src/easydiffraction/datablocks/structure/item/factory.py @@ -0,0 +1,137 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +""" +Factory for creating structure instances from various inputs. + +Provides individual class methods for each creation pathway: +``from_scratch``, ``from_cif_path``, or ``from_cif_str``. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from typeguard import typechecked + +from easydiffraction.datablocks.structure.item.base import Structure +from easydiffraction.io.cif.parse import document_from_path +from easydiffraction.io.cif.parse import document_from_string +from easydiffraction.io.cif.parse import name_from_block +from easydiffraction.io.cif.parse import pick_sole_block +from easydiffraction.utils.logging import log + +if TYPE_CHECKING: + import gemmi + + +class StructureFactory: + """Create :class:`Structure` instances from supported inputs.""" + + def __init__(self) -> None: + log.error( + 'Structure objects must be created using class methods such as ' + '`StructureFactory.from_cif_str(...)`, etc.' + ) + + # ------------------------------------------------------------------ + # Private helper methods + # ------------------------------------------------------------------ + + @classmethod + # TODO: @typechecked fails to find gemmi? + def _from_gemmi_block( + cls, + block: gemmi.cif.Block, + ) -> Structure: + """ + Build a structure from a single *gemmi* CIF block. + + Parameters + ---------- + block : gemmi.cif.Block + Parsed CIF data block. + + Returns + ------- + Structure + A fully populated structure instance. + """ + name = name_from_block(block) + structure = Structure(name=name) + for category in structure.categories: + category.from_cif(block) + return structure + + # ------------------------------------------------------------------ + # Public methods + # ------------------------------------------------------------------ + + @classmethod + @typechecked + def from_scratch( + cls, + *, + name: str, + ) -> Structure: + """ + Create a minimal default structure. + + Parameters + ---------- + name : str + Identifier for the new structure. + + Returns + ------- + Structure + An empty structure with default categories. + """ + return Structure(name=name) + + # TODO: add minimal default configuration for missing parameters + @classmethod + @typechecked + def from_cif_str( + cls, + cif_str: str, + ) -> Structure: + """ + Create a structure by parsing a CIF string. + + Parameters + ---------- + cif_str : str + Raw CIF content. + + Returns + ------- + Structure + A populated structure instance. + """ + doc = document_from_string(cif_str) + block = pick_sole_block(doc) + return cls._from_gemmi_block(block) + + # TODO: Read content and call self.from_cif_str + @classmethod + @typechecked + def from_cif_path( + cls, + cif_path: str, + ) -> Structure: + """ + Create a structure by reading and parsing a CIF file. + + Parameters + ---------- + cif_path : str + Filesystem path to a CIF file. + + Returns + ------- + Structure + A populated structure instance. + """ + doc = document_from_path(cif_path) + block = pick_sole_block(doc) + return cls._from_gemmi_block(block) diff --git a/src/easydiffraction/display/__init__.py b/src/easydiffraction/display/__init__.py index 265560b2..25bccda4 100644 --- a/src/easydiffraction/display/__init__.py +++ b/src/easydiffraction/display/__init__.py @@ -1,14 +1,15 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -"""Display subsystem for tables and plots. +""" +Display subsystem for tables and plots. -This package contains user-facing facades and backend implementations -to render tabular data and plots in different environments. +This package contains user-facing facades and backend implementations to +render tabular data and plots in different environments. - Tables: see :mod:`easydiffraction.display.tables` and the engines in - :mod:`easydiffraction.display.tablers`. -- Plots: see :mod:`easydiffraction.display.plotting` and the engines in - :mod:`easydiffraction.display.plotters`. +:mod:`easydiffraction.display.tablers`. - Plots: see +:mod:`easydiffraction.display.plotting` and the engines in +:mod:`easydiffraction.display.plotters`. """ # TODO: The following works in Jupyter, but breaks MkDocs builds. diff --git a/src/easydiffraction/display/base.py b/src/easydiffraction/display/base.py index c3babcff..6ffc0698 100644 --- a/src/easydiffraction/display/base.py +++ b/src/easydiffraction/display/base.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause """Common base classes for display components and their factories.""" @@ -6,26 +6,26 @@ from abc import ABC from abc import abstractmethod -from typing import Any from typing import List from typing import Tuple import pandas as pd -from easydiffraction.core.singletons import SingletonBase +from easydiffraction.core.singleton import SingletonBase from easydiffraction.utils.logging import console from easydiffraction.utils.logging import log class RendererBase(SingletonBase, ABC): - """Base class for display components with pluggable engines. + """ + Base class for display components with pluggable engines. Subclasses provide a factory and a default engine. This class manages the active backend instance and exposes helpers to inspect supported engines in a table-friendly format. """ - def __init__(self): + def __init__(self) -> None: self._engine = self._default_engine() self._backend = self._factory().create(self._engine) @@ -43,10 +43,27 @@ def _default_engine(cls) -> str: @property def engine(self) -> str: + """ + Return the name of the currently active rendering engine. + + Returns + ------- + str + Identifier of the active engine. + """ return self._engine @engine.setter def engine(self, new_engine: str) -> None: + """ + Switch to a different rendering engine. + + Parameters + ---------- + new_engine : str + Identifier of the engine to activate. Must be a key + returned by ``_factory()._registry()``. + """ if new_engine == self._engine: log.info(f"Engine is already set to '{new_engine}'. No change made.") return @@ -90,18 +107,25 @@ class RendererFactoryBase(ABC): """Base factory that manages discovery and creation of backends.""" @classmethod - def create(cls, engine_name: str) -> Any: - """Create a backend instance for the given engine. + def create(cls, engine_name: str) -> object: + """ + Create a backend instance for the given engine. - Args: - engine_name: Identifier of the engine to instantiate as - listed in ``_registry()``. + Parameters + ---------- + engine_name : str + Identifier of the engine to instantiate as listed in + ``_registry()``. - Returns: + Returns + ------- + object A new backend instance corresponding to ``engine_name``. - Raises: - ValueError: If the engine name is not supported. + Raises + ------ + ValueError + If the engine name is not supported. """ registry = cls._registry() if engine_name not in registry: @@ -117,16 +141,15 @@ def supported_engines(cls) -> List[str]: @classmethod def descriptions(cls) -> List[Tuple[str, str]]: - """Return pairs of engine name and human-friendly - description. - """ + """Return (name, description) pairs for each engine.""" items = cls._registry().items() return [(name, config.get('description')) for name, config in items] @classmethod @abstractmethod def _registry(cls) -> dict: - """Return engine registry. Implementations must provide this. + """ + Return engine registry. Implementations must provide this. The returned mapping should have keys as engine names and values as a config dict with 'description' and 'class'. Lazy imports diff --git a/src/easydiffraction/display/plotters/__init__.py b/src/easydiffraction/display/plotters/__init__.py index 14dae26a..09931ae8 100644 --- a/src/easydiffraction/display/plotters/__init__.py +++ b/src/easydiffraction/display/plotters/__init__.py @@ -1,10 +1,11 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -"""Plotting backends. +""" +Plotting backends. This subpackage implements plotting engines used by the high-level plotting facade: -- :mod:`.ascii` for terminal-friendly ASCII plots. -- :mod:`.plotly` for interactive plots in notebooks or browsers. +- :mod:`.ascii` for terminal-friendly ASCII plots. - :mod:`.plotly` for +interactive plots in notebooks or browsers. """ diff --git a/src/easydiffraction/display/plotters/ascii.py b/src/easydiffraction/display/plotters/ascii.py index f9ef8904..32ee45ed 100644 --- a/src/easydiffraction/display/plotters/ascii.py +++ b/src/easydiffraction/display/plotters/ascii.py @@ -1,13 +1,15 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -"""ASCII plotting backend. +""" +ASCII plotting backend. -Renders compact line charts in the terminal using -``asciichartpy``. This backend is well suited for quick feedback in -CLI environments and keeps a consistent API with other plotters. +Renders compact line charts in the terminal using ``asciichartpy``. This +backend is well suited for quick feedback in CLI environments and keeps +a consistent API with other plotters. """ import asciichartpy +import numpy as np from easydiffraction.display.plotters.base import DEFAULT_HEIGHT from easydiffraction.display.plotters.base import SERIES_CONFIG @@ -24,16 +26,21 @@ class AsciiPlotter(PlotterBase): """Terminal-based plotter using ASCII art.""" - def _get_legend_item(self, label): - """Return a colored legend entry for a given series label. + def _get_legend_item(self, label: str) -> str: + """ + Return a colored legend entry for a given series label. - The legend uses a colored line matching the series color and - the human-readable name from :data:`SERIES_CONFIG`. + The legend uses a colored line matching the series color and the + human-readable name from :data:`SERIES_CONFIG`. - Args: - label: Series identifier (e.g., ``'meas'``). + Parameters + ---------- + label : str + Series identifier (e.g., ``'meas'``). - Returns: + Returns + ------- + str A formatted legend string with color escapes. """ color_start = DEFAULT_COLORS[label] @@ -43,25 +50,36 @@ def _get_legend_item(self, label): item = f'{color_start}{line}{color_end} {name}' return item - def plot( + def plot_powder( self, - x, - y_series, - labels, - axes_labels, - title, - height=None, - ): - """Render a compact ASCII chart in the terminal. - - Args: - x: 1D array-like of x values (only used for range - display). - y_series: Sequence of y arrays to plot. - labels: Series identifiers corresponding to y_series. - axes_labels: Ignored; kept for API compatibility. - title: Figure title printed above the chart. - height: Number of text rows to allocate for the chart. + x: object, + y_series: object, + labels: object, + axes_labels: object, + title: str, + height: int | None = None, + ) -> None: + """ + Render a line plot for powder diffraction data. + + Suitable for powder diffraction data where intensity is plotted + against an x-axis variable (2θ, TOF, d-spacing). Uses ASCII + characters for terminal display. + + Parameters + ---------- + x : object + 1D array-like of x values (only used for range display). + y_series : object + Sequence of y arrays to plot. + labels : object + Series identifiers corresponding to y_series. + axes_labels : object + Ignored; kept for API compatibility. + title : str + Figure title printed above the chart. + height : int | None, default=None + Number of text rows to allocate for the chart. """ # Intentionally unused; kept for a consistent display API del axes_labels @@ -84,3 +102,105 @@ def plot( padded = '\n'.join(' ' + line for line in chart.splitlines()) print(padded) + + def plot_single_crystal( + self, + x_calc: object, + y_meas: object, + y_meas_su: object, + axes_labels: object, + title: str, + height: int | None = None, + ) -> None: + """ + Render a scatter plot for single crystal diffraction data. + + Creates an ASCII scatter plot showing measured vs calculated + values with a diagonal reference line. + + Parameters + ---------- + x_calc : object + 1D array-like of calculated values (x-axis). + y_meas : object + 1D array-like of measured values (y-axis). + y_meas_su : object + 1D array-like of measurement uncertainties (ignored in ASCII + mode). + axes_labels : object + Pair of strings for the x and y titles. + title : str + Figure title. + height : int | None, default=None + Number of text rows for the chart (default: 15). + """ + # Intentionally unused; ASCII scatter doesn't show error bars + del y_meas_su + + if height is None: + height = DEFAULT_HEIGHT + width = 60 # TODO: Make width configurable + + # Determine axis limits + vmin = float(min(np.min(y_meas), np.min(x_calc))) + vmax = float(max(np.max(y_meas), np.max(x_calc))) + pad = 0.05 * (vmax - vmin) if vmax > vmin else 1.0 + vmin -= pad + vmax += pad + + # Create empty grid + grid = [[' ' for _ in range(width)] for _ in range(height)] + + # Draw diagonal line (calc == meas) + for i in range(min(width, height)): + row = height - 1 - int(i * height / width) + col = i + if 0 <= row < height and 0 <= col < width: + grid[row][col] = '·' + + # Plot data points + for xv, yv in zip(x_calc, y_meas, strict=False): + col = int((xv - vmin) / (vmax - vmin) * (width - 1)) + row = height - 1 - int((yv - vmin) / (vmax - vmin) * (height - 1)) + if 0 <= row < height and 0 <= col < width: + grid[row][col] = '●' + + # Build chart string with axes + chart_lines = [] + for row in grid: + label = '│' + chart_lines.append(label + ''.join(row)) + + # X-axis + x_axis = '└' + '─' * width + + # Print output + console.paragraph(f'{title}') + console.print(f'{axes_labels[1]}') + for line in chart_lines: + print(f' {line}') + print(f' {x_axis}') + console.print(f'{" " * (width - 3)}{axes_labels[0]}') + + def plot_scatter( + self, + x: object, + y: object, + sy: object, + axes_labels: object, + title: str, + height: int | None = None, + ) -> None: + """Render a scatter plot with error bars in ASCII.""" + _ = x, sy # ASCII backend does not use x ticks or error bars + + if height is None: + height = DEFAULT_HEIGHT + + config = {'height': height, 'colors': [asciichartpy.blue]} + chart = asciichartpy.plot([list(y)], config) + + console.paragraph(f'{title}') + console.print(f'{axes_labels[1]} vs {axes_labels[0]}') + padded = '\n'.join(' ' + line for line in chart.splitlines()) + print(padded) diff --git a/src/easydiffraction/display/plotters/base.py b/src/easydiffraction/display/plotters/base.py index 43c22767..67fea11f 100644 --- a/src/easydiffraction/display/plotters/base.py +++ b/src/easydiffraction/display/plotters/base.py @@ -1,39 +1,137 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause """Abstract base and shared constants for plotting backends.""" from abc import ABC from abc import abstractmethod +from enum import Enum import numpy as np -from easydiffraction.experiments.experiment.enums import BeamModeEnum -from easydiffraction.experiments.experiment.enums import ScatteringTypeEnum +from easydiffraction.datablocks.experiment.item.enums import BeamModeEnum +from easydiffraction.datablocks.experiment.item.enums import SampleFormEnum +from easydiffraction.datablocks.experiment.item.enums import ScatteringTypeEnum -DEFAULT_HEIGHT = 9 +DEFAULT_HEIGHT = 25 DEFAULT_MIN = -np.inf DEFAULT_MAX = np.inf + +class XAxisType(str, Enum): + """ + X-axis types for diffraction plots. + + Values match attribute names in data models for direct use with + ``getattr(pattern, x_axis)``. + """ + + TWO_THETA = 'two_theta' + TIME_OF_FLIGHT = 'time_of_flight' + R = 'x' + + INTENSITY_CALC = 'intensity_calc' + + D_SPACING = 'd_spacing' + SIN_THETA_OVER_LAMBDA = 'sin_theta_over_lambda' + + +# Map (SampleFormEnum, ScatteringTypeEnum, BeamModeEnum) to default +# x-axis type +DEFAULT_X_AXIS = { + # Powder Bragg diffraction + ( + SampleFormEnum.POWDER, + ScatteringTypeEnum.BRAGG, + BeamModeEnum.CONSTANT_WAVELENGTH, + ): XAxisType.TWO_THETA, + ( + SampleFormEnum.POWDER, + ScatteringTypeEnum.BRAGG, + BeamModeEnum.TIME_OF_FLIGHT, + ): XAxisType.TIME_OF_FLIGHT, + # Powder total scattering (PDF) — always r-space + ( + SampleFormEnum.POWDER, + ScatteringTypeEnum.TOTAL, + BeamModeEnum.CONSTANT_WAVELENGTH, + ): XAxisType.R, + ( + SampleFormEnum.POWDER, + ScatteringTypeEnum.TOTAL, + BeamModeEnum.TIME_OF_FLIGHT, + ): XAxisType.R, + # Single crystal Bragg diffraction + ( + SampleFormEnum.SINGLE_CRYSTAL, + ScatteringTypeEnum.BRAGG, + BeamModeEnum.CONSTANT_WAVELENGTH, + ): XAxisType.INTENSITY_CALC, + ( + SampleFormEnum.SINGLE_CRYSTAL, + ScatteringTypeEnum.BRAGG, + BeamModeEnum.TIME_OF_FLIGHT, + ): XAxisType.INTENSITY_CALC, +} + DEFAULT_AXES_LABELS = { - (ScatteringTypeEnum.BRAGG, BeamModeEnum.CONSTANT_WAVELENGTH): [ + # Powder Bragg diffraction + ( + SampleFormEnum.POWDER, + ScatteringTypeEnum.BRAGG, + XAxisType.TWO_THETA, + ): [ '2θ (degree)', 'Intensity (arb. units)', ], - (ScatteringTypeEnum.BRAGG, BeamModeEnum.TIME_OF_FLIGHT): [ + ( + SampleFormEnum.POWDER, + ScatteringTypeEnum.BRAGG, + XAxisType.TIME_OF_FLIGHT, + ): [ 'TOF (µs)', 'Intensity (arb. units)', ], - (ScatteringTypeEnum.BRAGG, 'd-spacing'): [ + ( + SampleFormEnum.POWDER, + ScatteringTypeEnum.BRAGG, + XAxisType.D_SPACING, + ): [ 'd (Å)', 'Intensity (arb. units)', ], - (ScatteringTypeEnum.TOTAL, BeamModeEnum.CONSTANT_WAVELENGTH): [ - 'r (Å)', - 'G(r) (Å)', + # Powder total scattering (PDF) + ( + SampleFormEnum.POWDER, + ScatteringTypeEnum.TOTAL, + XAxisType.R, + ): [ + 'r (Å)', + 'G(r) (Å)', + ], + # Single crystal Bragg diffraction + ( + SampleFormEnum.SINGLE_CRYSTAL, + ScatteringTypeEnum.BRAGG, + XAxisType.INTENSITY_CALC, + ): [ + 'I²calc', + 'I²meas', ], - (ScatteringTypeEnum.TOTAL, BeamModeEnum.TIME_OF_FLIGHT): [ - 'r (Å)', - 'G(r) (Å)', + ( + SampleFormEnum.SINGLE_CRYSTAL, + ScatteringTypeEnum.BRAGG, + XAxisType.D_SPACING, + ): [ + 'd (Å)', + 'Intensity (arb. units)', + ], + ( + SampleFormEnum.SINGLE_CRYSTAL, + ScatteringTypeEnum.BRAGG, + XAxisType.SIN_THETA_OVER_LAMBDA, + ): [ + 'sin(θ)/λ (Å⁻¹)', + 'Intensity (arb. units)', ], } @@ -54,30 +152,110 @@ class PlotterBase(ABC): - """Abstract base for plotting backends. + """ + Abstract base for plotting backends. Implementations accept x values, multiple y-series, optional labels and render a plot to the chosen medium. + + Two main plot types are supported: - ``plot_powder``: Line plots for + powder diffraction patterns (intensity vs. 2θ/TOF/d-spacing). - + ``plot_single_crystal``: Scatter plots comparing measured vs. + calculated values (e.g., F²meas vs F²calc for single crystal). """ @abstractmethod - def plot( + def plot_powder( self, - x, - y_series, - labels, - axes_labels, - title, - height, - ): - """Render a plot. - - Args: - x: 1D array of x-axis values. - y_series: Sequence of y arrays to plot. - labels: Identifiers corresponding to y_series. - axes_labels: Pair of strings for the x and y titles. - title: Figure title. - height: Backend-specific height (text rows or pixels). + x: object, + y_series: object, + labels: object, + axes_labels: object, + title: str, + height: int | None, + ) -> None: + """ + Render a line plot for powder diffraction data. + + Suitable for powder diffraction data where intensity is plotted + against an x-axis variable (2θ, TOF, d-spacing). + + Parameters + ---------- + x : object + 1D array of x-axis values. + y_series : object + Sequence of y arrays to plot. + labels : object + Identifiers corresponding to y_series. + axes_labels : object + Pair of strings for the x and y titles. + title : str + Figure title. + height : int | None + Backend-specific height (text rows or pixels). + """ + pass + + @abstractmethod + def plot_single_crystal( + self, + x_calc: object, + y_meas: object, + y_meas_su: object, + axes_labels: object, + title: str, + height: int | None, + ) -> None: + """ + Render a scatter plot for single crystal diffraction data. + + Suitable for single crystal diffraction data where measured + values are plotted against calculated values with error bars. + + Parameters + ---------- + x_calc : object + 1D array of calculated values (x-axis). + y_meas : object + 1D array of measured values (y-axis). + y_meas_su : object + 1D array of measurement uncertainties. + axes_labels : object + Pair of strings for the x and y titles. + title : str + Figure title. + height : int | None + Backend-specific height (text rows or pixels). + """ + pass + + @abstractmethod + def plot_scatter( + self, + x: object, + y: object, + sy: object, + axes_labels: object, + title: str, + height: int | None, + ) -> None: + """ + Render a scatter plot with error bars. + + Parameters + ---------- + x : object + 1-D array of x-axis values. + y : object + 1-D array of y-axis values. + sy : object + 1-D array of y uncertainties. + axes_labels : object + Pair of strings for x and y axis titles. + title : str + Figure title. + height : int | None + Backend-specific height (text rows or pixels). """ pass diff --git a/src/easydiffraction/display/plotters/plotly.py b/src/easydiffraction/display/plotters/plotly.py index f91156d1..9d559d02 100644 --- a/src/easydiffraction/display/plotters/plotly.py +++ b/src/easydiffraction/display/plotters/plotly.py @@ -1,6 +1,7 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -"""Plotly plotting backend. +""" +Plotly plotting backend. Provides an interactive plotting implementation using Plotly. In notebooks, figures are displayed inline; in other environments a browser @@ -36,16 +37,27 @@ class PlotlyPlotter(PlotterBase): if in_pycharm(): pio.renderers.default = 'browser' - def _get_trace(self, x, y, label): - """Create a Plotly trace for a single data series. + def _get_powder_trace( + self, + x: object, + y: object, + label: str, + ) -> object: + """ + Create a Plotly trace for powder diffraction data. - Args: - x: 1D array-like of x-axis values. - y: 1D array-like of y-axis values. - label: Series identifier (``'meas'``, ``'calc'``, or - ``'resid'``). + Parameters + ---------- + x : object + 1D array-like of x-axis values. + y : object + 1D array- like of y-axis values. + label : str + Series identifier (``'meas'``, ``'calc'``, or ``'resid'``). - Returns: + Returns + ------- + object A configured :class:`plotly.graph_objects.Scatter` trace. """ mode = SERIES_CONFIG[label]['mode'] @@ -63,34 +75,173 @@ def _get_trace(self, x, y, label): return trace - def plot( + def _get_single_crystal_trace( self, - x, - y_series, - labels, - axes_labels, - title, - height=None, - ): - """Render an interactive Plotly figure. - - Args: - x: 1D array-like of x-axis values. - y_series: Sequence of y arrays to plot. - labels: Series identifiers corresponding to y_series. - axes_labels: Pair of strings for the x and y titles. - title: Figure title. - height: Ignored; Plotly auto-sizes based on renderer. + x_calc: object, + y_meas: object, + y_meas_su: object, + ) -> object: """ - # Intentionally unused; accepted for API compatibility - del height - data = [] - for idx, y in enumerate(y_series): - label = labels[idx] - trace = self._get_trace(x, y, label) - data.append(trace) + Create a Plotly trace for single crystal diffraction data. + + Parameters + ---------- + x_calc : object + 1D array-like of calculated values (x-axis). + y_meas : object + 1D array-like of measured values (y-axis). + y_meas_su : object + 1D array-like of measurement uncertainties. + + Returns + ------- + object + A configured :class:`plotly.graph_objects.Scatter` trace + with markers and error bars. + """ + trace = go.Scatter( + x=x_calc, + y=y_meas, + mode='markers', + marker=dict( + symbol='circle', + size=10, + line=dict(width=0.5), + color=DEFAULT_COLORS['meas'], + ), + error_y=dict( + type='data', + array=y_meas_su, + visible=True, + ), + hovertemplate='calc: %{x}
meas: %{y}
', + ) + + return trace + + def _get_diagonal_shape(self) -> dict: + """ + Create a diagonal reference line shape. + + Returns a y=x diagonal line spanning the plot area using paper + coordinates (0,0) to (1,1). + + Returns + ------- + dict + A dict configuring a diagonal line shape. + """ + return dict( + type='line', + x0=0, + y0=0, + x1=1, + y1=1, + xref='paper', + yref='paper', + layer='below', + line=dict(width=0.5), + ) + + def _get_config(self) -> dict: + """ + Return the Plotly figure configuration. + + Returns + ------- + dict + A dict with display and mode bar settings. + """ + return dict( + displaylogo=False, + modeBarButtonsToRemove=[ + 'select2d', + 'lasso2d', + 'zoomIn2d', + 'zoomOut2d', + 'autoScale2d', + ], + ) + + def _get_figure( + self, + data: object, + layout: object, + ) -> object: + """ + Create and configure a Plotly figure. + + Parameters + ---------- + data : object + List of traces to include in the figure. + layout : object + Layout configuration dict. + + Returns + ------- + object + A configured :class:`plotly.graph_objects.Figure`. + """ + fig = go.Figure(data=data, layout=layout) + # Format axis ticks: + # decimals for small numbers, grouped thousands for large + fig.update_xaxes(tickformat=',.6~g', separatethousands=True) + fig.update_yaxes(tickformat=',.6~g', separatethousands=True) + return fig + + def _show_figure( + self, + fig: object, + ) -> None: + """ + Display a Plotly figure. + + Renders the figure using the appropriate method for the current + environment (browser for PyCharm, inline HTML for Jupyter). + + Parameters + ---------- + fig : object + A :class:`plotly.graph_objects.Figure` to display. + """ + config = self._get_config() + + if in_pycharm() or display is None or HTML is None: + fig.show(config=config) + else: + html_fig = pio.to_html( + fig, + include_plotlyjs='cdn', + full_html=False, + config=config, + ) + display(HTML(html_fig)) - layout = go.Layout( + def _get_layout( + self, + title: str, + axes_labels: object, + **kwargs: object, + ) -> object: + """ + Create a Plotly layout configuration. + + Parameters + ---------- + title : str + Figure title. + axes_labels : object + Pair of strings for the x and y titles. + **kwargs : object + Additional layout parameters (e.g., shapes). + + Returns + ------- + object + A configured :class:`plotly.graph_objects.Layout`. + """ + return go.Layout( margin=dict( autoexpand=True, r=30, @@ -118,38 +269,145 @@ def plot( mirror=True, zeroline=False, ), + **kwargs, ) - config = dict( - displaylogo=False, - modeBarButtonsToRemove=[ - 'select2d', - 'lasso2d', - 'zoomIn2d', - 'zoomOut2d', - 'autoScale2d', - ], - ) + def plot_powder( + self, + x: object, + y_series: object, + labels: object, + axes_labels: object, + title: str, + height: int | None = None, + ) -> None: + """ + Render a line plot for powder diffraction data. - fig = go.Figure( - data=data, - layout=layout, + Suitable for powder diffraction data where intensity is plotted + against an x-axis variable (2θ, TOF, d-spacing). + + Parameters + ---------- + x : object + 1D array-like of x-axis values. + y_series : object + Sequence of y arrays to plot. + labels : object + Series identifiers corresponding to y_series. + axes_labels : object + Pair of strings for the x and y titles. + title : str + Figure title. + height : int | None, default=None + Ignored; Plotly auto-sizes based on renderer. + """ + # Intentionally unused; accepted for API compatibility + del height + + data = [] + for idx, y in enumerate(y_series): + label = labels[idx] + trace = self._get_powder_trace(x, y, label) + data.append(trace) + + layout = self._get_layout( + title, + axes_labels, ) - # Format the axes ticks. - # Keeps decimals for small numbers; groups thousands for large - # ones - fig.update_xaxes(tickformat=',.6~g', separatethousands=True) - fig.update_yaxes(tickformat=',.6~g', separatethousands=True) + fig = self._get_figure(data, layout) + self._show_figure(fig) - # Show the figure - if in_pycharm() or display is None or HTML is None: - fig.show(config=config) - else: - html_fig = pio.to_html( - fig, - include_plotlyjs='cdn', - full_html=False, - config=config, + def plot_single_crystal( + self, + x_calc: object, + y_meas: object, + y_meas_su: object, + axes_labels: object, + title: str, + height: int | None = None, + ) -> None: + """ + Render a scatter plot for single crystal diffraction data. + + Suitable for single crystal diffraction data where measured + values are plotted against calculated values with error bars and + a diagonal reference line. + + Parameters + ---------- + x_calc : object + 1D array-like of calculated values (x-axis). + y_meas : object + 1D array-like of measured values (y-axis). + y_meas_su : object + 1D array-like of measurement uncertainties. + axes_labels : object + Pair of strings for the x and y titles. + title : str + Figure title. + height : int | None, default=None + Ignored; Plotly auto-sizes based on renderer. + """ + # Intentionally unused; accepted for API compatibility + del height + + data = [ + self._get_single_crystal_trace( + x_calc, + y_meas, + y_meas_su, ) - display(HTML(html_fig)) + ] + + layout = self._get_layout( + title, + axes_labels, + shapes=[self._get_diagonal_shape()], + ) + + fig = self._get_figure(data, layout) + self._show_figure(fig) + + def plot_scatter( + self, + x: object, + y: object, + sy: object, + axes_labels: object, + title: str, + height: int | None = None, + ) -> None: + """Render a scatter plot with error bars via Plotly.""" + _ = height # not used by Plotly backend + + trace = go.Scatter( + x=x, + y=y, + mode='markers+lines', + marker=dict( + symbol='circle', + size=10, + line=dict(width=0.5), + color=DEFAULT_COLORS['meas'], + ), + line=dict( + width=1, + color=DEFAULT_COLORS['meas'], + ), + error_y=dict( + type='data', + array=sy, + visible=True, + ), + hovertemplate='x: %{x}
y: %{y}
', + ) + + layout = self._get_layout( + title, + axes_labels, + ) + + fig = self._get_figure(trace, layout) + self._show_figure(fig) diff --git a/src/easydiffraction/display/plotting.py b/src/easydiffraction/display/plotting.py index c0c98deb..e4b1ad34 100644 --- a/src/easydiffraction/display/plotting.py +++ b/src/easydiffraction/display/plotting.py @@ -1,6 +1,7 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -"""Plotting facade for measured and calculated patterns. +""" +Plotting facade for measured and calculated patterns. Uses the common :class:`RendererBase` so plotters and tablers share a consistent configuration surface and engine handling. @@ -18,6 +19,8 @@ from easydiffraction.display.plotters.base import DEFAULT_HEIGHT from easydiffraction.display.plotters.base import DEFAULT_MAX from easydiffraction.display.plotters.base import DEFAULT_MIN +from easydiffraction.display.plotters.base import DEFAULT_X_AXIS +from easydiffraction.display.plotters.base import XAxisType from easydiffraction.display.plotters.plotly import PlotlyPlotter from easydiffraction.display.tables import TableRenderer from easydiffraction.utils.environment import in_jupyter @@ -26,6 +29,8 @@ class PlotterEngineEnum(str, Enum): + """Available plotting engine backends.""" + ASCII = 'asciichartpy' PLOTLY = 'plotly' @@ -50,7 +55,11 @@ def description(self) -> str: class Plotter(RendererBase): """User-facing plotting facade backed by concrete plotters.""" - def __init__(self): + # ------------------------------------------------------------------ + # Private special methods + # ------------------------------------------------------------------ + + def __init__(self) -> None: super().__init__() # X-axis limits self._x_min = DEFAULT_MIN @@ -58,6 +67,10 @@ def __init__(self): # Chart height self.height = DEFAULT_HEIGHT + # ------------------------------------------------------------------ + # Private class methods + # ------------------------------------------------------------------ + @classmethod def _factory(cls) -> type[RendererFactoryBase]: # type: ignore[override] return PlotterFactory @@ -66,32 +79,231 @@ def _factory(cls) -> type[RendererFactoryBase]: # type: ignore[override] def _default_engine(cls) -> str: return PlotterEngineEnum.default().value - def show_config(self): - """Display the current plotting configuration.""" - headers = [ - ('Parameter', 'left'), - ('Value', 'left'), - ] - rows = [ - ['Plotting engine', self.engine], - ['x-axis limits', f'[{self.x_min}, {self.x_max}]'], - ['Chart height', self.height], - ] - df = pd.DataFrame(rows, columns=pd.MultiIndex.from_tuples(headers)) - console.paragraph('Current plotter configuration') - TableRenderer.get().render(df) + # ------------------------------------------------------------------ + # Private helper methods + # ------------------------------------------------------------------ + + def _auto_x_range_for_ascii( + self, + pattern: object, + x_array: object, + x_min: object, + x_max: object, + ) -> tuple: + """ + For the ASCII engine, narrow the range around the tallest peak. + + Parameters + ---------- + pattern : object + Data pattern object (needs ``intensity_meas``). + x_array : object + Full x-axis array. + x_min : object + Current minimum (may be ``None``). + x_max : object + Current maximum (may be ``None``). + + Returns + ------- + tuple + Tuple of ``(x_min, x_max)``, possibly narrowed. + """ + if self._engine == 'asciichartpy' and (x_min is None or x_max is None): + max_intensity_pos = np.argmax(pattern.intensity_meas) + half_range = 50 + start = max(0, max_intensity_pos - half_range) + end = min(len(x_array) - 1, max_intensity_pos + half_range) + x_min = x_array[start] + x_max = x_array[end] + return x_min, x_max + + def _filtered_y_array( + self, + y_array: object, + x_array: object, + x_min: object, + x_max: object, + ) -> object: + """ + Filter an array by the inclusive x-range limits. + + Parameters + ---------- + y_array : object + 1D array-like of y values. + x_array : object + 1D array-like of x values (same length as ``y_array``). + x_min : object + Minimum x limit (or ``None`` to use default). + x_max : object + Maximum x limit (or ``None`` to use default). + + Returns + ------- + object + Filtered ``y_array`` values where ``x_array`` lies within + ``[x_min, x_max]``. + """ + if x_min is None: + x_min = self.x_min + if x_max is None: + x_max = self.x_max + + mask = (x_array >= x_min) & (x_array <= x_max) + filtered_y_array = y_array[mask] + + return filtered_y_array + + def _get_axes_labels( + self, + sample_form: object, + scattering_type: object, + x_axis: object, + ) -> list: + """Look up axis labels for the experiment / x-axis.""" + return DEFAULT_AXES_LABELS[(sample_form, scattering_type, x_axis)] + + def _prepare_powder_data( + self, + pattern: object, + expt_name: str, + expt_type: object, + x_min: object, + x_max: object, + x: object, + need_meas: bool = False, + need_calc: bool = False, + show_residual: bool = False, + ) -> dict | None: + """ + Validate, resolve axes, auto-range, and filter arrays. + + Parameters + ---------- + pattern : object + Data pattern object with intensity arrays. + expt_name : str + Experiment name for error messages. + expt_type : object + Experiment type with sample_form, scattering, and beam + enums. + x_min : object + Optional minimum x-axis limit. + x_max : object + Optional maximum x-axis limit. + x : object + Explicit x-axis type or ``None``. + need_meas : bool, default=False + Whether ``intensity_meas`` is required. + need_calc : bool, default=False + Whether ``intensity_calc`` is required. + show_residual : bool, default=False + If ``True``, compute meas − calc residual. + + Returns + ------- + dict | None + A dict with keys ``x_filtered``, ``y_series``, ``y_labels``, + ``axes_labels``, and ``x_axis``; or ``None`` when a required + array is missing. + """ + x_axis, x_name, sample_form, scattering_type, _ = self._resolve_x_axis(expt_type, x) + + # Get x-array from pattern + x_array = getattr(pattern, x_axis, None) + if x_array is None: + log.error(f'No {x_name} data available for experiment {expt_name}') + return None + + # Validate required intensities + if need_meas and pattern.intensity_meas is None: + log.error(f'No measured data available for experiment {expt_name}') + return None + if need_calc and pattern.intensity_calc is None: + log.error(f'No calculated data available for experiment {expt_name}') + return None + + # Auto-range for ASCII engine + x_min, x_max = self._auto_x_range_for_ascii(pattern, x_array, x_min, x_max) + + # Filter x + x_filtered = self._filtered_y_array(x_array, x_array, x_min, x_max) + + # Filter y arrays and build series / labels + y_series = [] + y_labels = [] + + y_meas = None + if need_meas: + y_meas = self._filtered_y_array(pattern.intensity_meas, x_array, x_min, x_max) + y_series.append(y_meas) + y_labels.append('meas') + + y_calc = None + if need_calc: + y_calc = self._filtered_y_array(pattern.intensity_calc, x_array, x_min, x_max) + y_series.append(y_calc) + y_labels.append('calc') + + if show_residual and y_meas is not None and y_calc is not None: + y_resid = y_meas - y_calc + y_series.append(y_resid) + y_labels.append('resid') + + axes_labels = self._get_axes_labels(sample_form, scattering_type, x_axis) + + return { + 'x_filtered': x_filtered, + 'y_series': y_series, + 'y_labels': y_labels, + 'axes_labels': axes_labels, + 'x_axis': x_axis, + } + + def _resolve_x_axis(self, expt_type: object, x: object) -> tuple: + """ + Determine the x-axis type from experiment metadata. + + Parameters + ---------- + expt_type : object + Experiment type with sample_form, scattering_type, and + beam_mode enums. + x : object + Explicit x-axis type or ``None`` to auto-detect. + + Returns + ------- + tuple + Tuple of ``(x_axis, x_name, sample_form, scattering_type, + beam_mode)``. + """ + sample_form = expt_type.sample_form.value + scattering_type = expt_type.scattering_type.value + beam_mode = expt_type.beam_mode.value + x_axis = DEFAULT_X_AXIS[(sample_form, scattering_type, beam_mode)] if x is None else x + x_name = getattr(x_axis, 'value', x_axis) + return x_axis, x_name, sample_form, scattering_type, beam_mode + + # ------------------------------------------------------------------ + # Public properties + # ------------------------------------------------------------------ @property - def x_min(self): + def x_min(self) -> float: """Minimum x-axis limit.""" return self._x_min @x_min.setter - def x_min(self, value): - """Set the minimum x-axis limit. + def x_min(self, value: object) -> None: + """ + Set the minimum x-axis limit. - Args: - value: Minimum limit or ``None`` to reset to default. + Parameters + ---------- + value : object + Minimum limit or ``None`` to reset to default. """ if value is not None: self._x_min = value @@ -99,16 +311,19 @@ def x_min(self, value): self._x_min = DEFAULT_MIN @property - def x_max(self): + def x_max(self) -> float: """Maximum x-axis limit.""" return self._x_max @x_max.setter - def x_max(self, value): - """Set the maximum x-axis limit. + def x_max(self, value: object) -> None: + """ + Set the maximum x-axis limit. - Args: - value: Maximum limit or ``None`` to reset to default. + Parameters + ---------- + value : object + Maximum limit or ``None`` to reset to default. """ if value is not None: self._x_max = value @@ -116,311 +331,344 @@ def x_max(self, value): self._x_max = DEFAULT_MAX @property - def height(self): + def height(self) -> int: """Plot height (rows for ASCII, pixels for Plotly).""" return self._height @height.setter - def height(self, value): - """Set plot height. + def height(self, value: object) -> None: + """ + Set plot height. - Args: - value: Height value or ``None`` to reset to default. + Parameters + ---------- + value : object + Height value or ``None`` to reset to default. """ if value is not None: self._height = value else: self._height = DEFAULT_HEIGHT + # ------------------------------------------------------------------ + # Public methods + # ------------------------------------------------------------------ + + def show_config(self) -> None: + """Display the current plotting configuration.""" + headers = [ + ('Parameter', 'left'), + ('Value', 'left'), + ] + rows = [ + ['Plotting engine', self.engine], + ['x-axis limits', f'[{self.x_min}, {self.x_max}]'], + ['Chart height', self.height], + ] + df = pd.DataFrame(rows, columns=pd.MultiIndex.from_tuples(headers)) + console.paragraph('Current plotter configuration') + TableRenderer.get().render(df) + def plot_meas( self, - pattern, - expt_name, - expt_type, - x_min=None, - x_max=None, - d_spacing=False, - ): - """Plot measured pattern using the current engine. - - Args: - pattern: Object with ``x`` and ``meas`` arrays (and - ``d`` when ``d_spacing`` is true). - expt_name: Experiment name for the title. - expt_type: Experiment type with scattering/beam enums. - x_min: Optional minimum x-axis limit. - x_max: Optional maximum x-axis limit. - d_spacing: If ``True``, plot against d-spacing values. + pattern: object, + expt_name: str, + expt_type: object, + x_min: object = None, + x_max: object = None, + x: object = None, + ) -> None: """ - if pattern.x is None: - log.error(f'No data available for experiment {expt_name}') - return - if pattern.meas is None: - log.error(f'No measured data available for experiment {expt_name}') - return - - # Select x-axis data based on d-spacing or original x values - x_array = pattern.d if d_spacing else pattern.x - - # For asciichartpy, if x_min or x_max is not provided, center - # around the maximum intensity peak - if self._engine == 'asciichartpy' and (x_min is None or x_max is None): - max_intensity_pos = np.argmax(pattern.meas) - half_range = 50 - start = max(0, max_intensity_pos - half_range) - end = min(len(x_array) - 1, max_intensity_pos + half_range) - x_min = x_array[start] - x_max = x_array[end] - - # Filter x, y_meas, and y_calc based on x_min and x_max - x = self._filtered_y_array( - y_array=x_array, - x_array=x_array, - x_min=x_min, - x_max=x_max, - ) - y_meas = self._filtered_y_array( - y_array=pattern.meas, - x_array=x_array, - x_min=x_min, - x_max=x_max, + Plot measured pattern using the current engine. + + Parameters + ---------- + pattern : object + Object with x-axis arrays (``two_theta``, + ``time_of_flight``, ``d_spacing``) and ``meas`` array. + expt_name : str + Experiment name for the title. + expt_type : object + Experiment type with scattering/beam enums. + x_min : object, default=None + Optional minimum x-axis limit. + x_max : object, default=None + Optional maximum x-axis limit. + x : object, default=None + X-axis type (``'two_theta'``, ``'time_of_flight'``, or + ``'d_spacing'``). If ``None``, auto-detected from beam mode. + """ + ctx = self._prepare_powder_data( + pattern, + expt_name, + expt_type, + x_min, + x_max, + x, + need_meas=True, ) + if ctx is None: + return - y_series = [y_meas] - y_labels = ['meas'] - - if d_spacing: - axes_labels = DEFAULT_AXES_LABELS[ - ( - expt_type.scattering_type.value, - 'd-spacing', - ) - ] - else: - axes_labels = DEFAULT_AXES_LABELS[ - ( - expt_type.scattering_type.value, - expt_type.beam_mode.value, - ) - ] - - # TODO: Before, it was self._plotter.plot. Check what is better. - self._backend.plot( - x=x, - y_series=y_series, - labels=y_labels, - axes_labels=axes_labels, + self._backend.plot_powder( + x=ctx['x_filtered'], + y_series=ctx['y_series'], + labels=ctx['y_labels'], + axes_labels=ctx['axes_labels'], title=f"Measured data for experiment 🔬 '{expt_name}'", height=self.height, ) def plot_calc( self, - pattern, - expt_name, - expt_type, - x_min=None, - x_max=None, - d_spacing=False, - ): - """Plot calculated pattern using the current engine. - - Args: - pattern: Object with ``x`` and ``calc`` arrays (and - ``d`` when ``d_spacing`` is true). - expt_name: Experiment name for the title. - expt_type: Experiment type with scattering/beam enums. - x_min: Optional minimum x-axis limit. - x_max: Optional maximum x-axis limit. - d_spacing: If ``True``, plot against d-spacing values. + pattern: object, + expt_name: str, + expt_type: object, + x_min: object = None, + x_max: object = None, + x: object = None, + ) -> None: """ - if pattern.x is None: - log.error(f'No data available for experiment {expt_name}') - return - if pattern.calc is None: - log.error(f'No calculated data available for experiment {expt_name}') - return - - # Select x-axis data based on d-spacing or original x values - x_array = pattern.d if d_spacing else pattern.x - - # For asciichartpy, if x_min or x_max is not provided, center - # around the maximum intensity peak - if self._engine == 'asciichartpy' and (x_min is None or x_max is None): - max_intensity_pos = np.argmax(pattern.meas) - half_range = 50 - start = max(0, max_intensity_pos - half_range) - end = min(len(x_array) - 1, max_intensity_pos + half_range) - x_min = x_array[start] - x_max = x_array[end] - - # Filter x, y_meas, and y_calc based on x_min and x_max - x = self._filtered_y_array( - y_array=x_array, - x_array=x_array, - x_min=x_min, - x_max=x_max, - ) - y_calc = self._filtered_y_array( - y_array=pattern.calc, - x_array=x_array, - x_min=x_min, - x_max=x_max, + Plot calculated pattern using the current engine. + + Parameters + ---------- + pattern : object + Object with x-axis arrays (``two_theta``, + ``time_of_flight``, ``d_spacing``) and ``calc`` array. + expt_name : str + Experiment name for the title. + expt_type : object + Experiment type with scattering/beam enums. + x_min : object, default=None + Optional minimum x-axis limit. + x_max : object, default=None + Optional maximum x-axis limit. + x : object, default=None + X-axis type (``'two_theta'``, ``'time_of_flight'``, or + ``'d_spacing'``). If ``None``, auto-detected from beam mode. + """ + ctx = self._prepare_powder_data( + pattern, + expt_name, + expt_type, + x_min, + x_max, + x, + need_calc=True, ) + if ctx is None: + return - y_series = [y_calc] - y_labels = ['calc'] - - if d_spacing: - axes_labels = DEFAULT_AXES_LABELS[ - ( - expt_type.scattering_type.value, - 'd-spacing', - ) - ] - else: - axes_labels = DEFAULT_AXES_LABELS[ - ( - expt_type.scattering_type.value, - expt_type.beam_mode.value, - ) - ] - - self._backend.plot( - x=x, - y_series=y_series, - labels=y_labels, - axes_labels=axes_labels, + self._backend.plot_powder( + x=ctx['x_filtered'], + y_series=ctx['y_series'], + labels=ctx['y_labels'], + axes_labels=ctx['axes_labels'], title=f"Calculated data for experiment 🔬 '{expt_name}'", height=self.height, ) def plot_meas_vs_calc( self, - pattern, - expt_name, - expt_type, - x_min=None, - x_max=None, - show_residual=False, - d_spacing=False, - ): - """Plot measured and calculated series and optional residual. - - Args: - pattern: Object with ``x``, ``meas`` and ``calc`` arrays - (and ``d`` when ``d_spacing`` is true). - expt_name: Experiment name for the title. - expt_type: Experiment type with scattering/beam enums. - x_min: Optional minimum x-axis limit. - x_max: Optional maximum x-axis limit. - show_residual: If ``True``, add residual series. - d_spacing: If ``True``, plot against d-spacing values. + pattern: object, + expt_name: str, + expt_type: object, + x_min: object = None, + x_max: object = None, + show_residual: bool = False, + x: object = None, + ) -> None: """ - if pattern.x is None: - log.error(f'No data available for experiment {expt_name}') - return - if pattern.meas is None: + Plot measured and calculated series and optional residual. + + Supports both powder and single crystal data with a unified API. + + For powder diffraction: - x='two_theta', 'time_of_flight', or + 'd_spacing' - Auto-detected from beam mode if not specified + + For single crystal diffraction: - x='intensity_calc' (default): + scatter plot - x='d_spacing' or 'sin_theta_over_lambda': line + plot + + Parameters + ---------- + pattern : object + Data pattern object with meas/calc arrays. + expt_name : str + Experiment name for the title. + expt_type : object + Experiment type with sample_form, scattering, and beam + enums. + x_min : object, default=None + Optional minimum x-axis limit. + x_max : object, default=None + Optional maximum x-axis limit. + show_residual : bool, default=False + If ``True``, add residual series (powder only). + x : object, default=None + X-axis type. If ``None``, auto-detected from sample form and + beam mode. + """ + x_axis, _, sample_form, scattering_type, _ = self._resolve_x_axis(expt_type, x) + + # Validate required data (before x-array check, matching + # original behavior for plot_meas_vs_calc) + if pattern.intensity_meas is None: log.error(f'No measured data available for experiment {expt_name}') return - if pattern.calc is None: + if pattern.intensity_calc is None: log.error(f'No calculated data available for experiment {expt_name}') return - # Select x-axis data based on d-spacing or original x values - x_array = pattern.d if d_spacing else pattern.x - - # For asciichartpy, if x_min or x_max is not provided, center - # around the maximum intensity peak - if self._engine == 'asciichartpy' and (x_min is None or x_max is None): - max_intensity_pos = np.argmax(pattern.meas) - half_range = 50 - start = max(0, max_intensity_pos - half_range) - end = min(len(x_array) - 1, max_intensity_pos + half_range) - x_min = x_array[start] - x_max = x_array[end] + title = f"Measured vs Calculated data for experiment 🔬 '{expt_name}'" + + # Single crystal scatter plot (I²calc vs I²meas) + if x_axis == XAxisType.INTENSITY_CALC or x_axis == 'intensity_calc': + axes_labels = self._get_axes_labels(sample_form, scattering_type, x_axis) + + if pattern.intensity_meas_su is None: + log.warning(f'No measurement uncertainties for experiment {expt_name}') + meas_su = np.zeros_like(pattern.intensity_meas) + else: + meas_su = pattern.intensity_meas_su + + self._backend.plot_single_crystal( + x_calc=pattern.intensity_calc, + y_meas=pattern.intensity_meas, + y_meas_su=meas_su, + axes_labels=axes_labels, + title=f"Measured vs Calculated data for experiment 🔬 '{expt_name}'", + height=self.height, + ) + return - # Filter x, y_meas, and y_calc based on x_min and x_max - x = self._filtered_y_array( - y_array=x_array, - x_array=x_array, - x_min=x_min, - x_max=x_max, - ) - y_meas = self._filtered_y_array( - y_array=pattern.meas, - x_array=x_array, - x_min=x_min, - x_max=x_max, - ) - y_calc = self._filtered_y_array( - y_array=pattern.calc, - x_array=x_array, - x_min=x_min, - x_max=x_max, + # Line plot (PD or SC with d_spacing/sin_theta_over_lambda) + # TODO: Rename from _prepare_powder_data as it also supports + # single crystal line plots + ctx = self._prepare_powder_data( + pattern, + expt_name, + expt_type, + x_min, + x_max, + x, + need_meas=True, + need_calc=True, + show_residual=show_residual, ) + if ctx is None: + return - y_series = [y_meas, y_calc] - y_labels = ['meas', 'calc'] - - if d_spacing: - axes_labels = DEFAULT_AXES_LABELS[ - ( - expt_type.scattering_type.value, - 'd-spacing', - ) - ] - else: - axes_labels = DEFAULT_AXES_LABELS[ - ( - expt_type.scattering_type.value, - expt_type.beam_mode.value, - ) - ] - - if show_residual: - y_resid = y_meas - y_calc - y_series.append(y_resid) - y_labels.append('resid') + self._backend.plot_powder( + x=ctx['x_filtered'], + y_series=ctx['y_series'], + labels=ctx['y_labels'], + axes_labels=ctx['axes_labels'], + title=title, + height=self.height, + ) - self._backend.plot( + def plot_param_series( + self, + unique_name: str, + versus_name: str | None, + experiments: object, + parameter_snapshots: dict[str, dict[str, dict]], + ) -> None: + """ + Plot a parameter's value across sequential fit results. + + Parameters + ---------- + unique_name : str + Unique name of the parameter to plot. + versus_name : str | None + Name of the diffrn descriptor to use as the x-axis (e.g. + ``'ambient_temperature'``). When ``None``, the experiment + sequence index is used instead. + experiments : object + Experiments collection for accessing diffrn conditions. + parameter_snapshots : dict[str, dict[str, dict]] + Per-experiment parameter value snapshots keyed by experiment + name, then by parameter unique name. + """ + x = [] + y = [] + sy = [] + axes_labels = [] + title = '' + + for idx, expt_name in enumerate(parameter_snapshots, start=1): + experiment = experiments[expt_name] + diffrn = experiment.diffrn + + x_axis_param = self._resolve_diffrn_descriptor(diffrn, versus_name) + + if x_axis_param is not None and x_axis_param.value is not None: + value = x_axis_param.value + else: + value = idx + x.append(value) + + param_data = parameter_snapshots[expt_name][unique_name] + y.append(param_data['value']) + sy.append(param_data['uncertainty']) + + if x_axis_param is not None: + axes_labels = [ + x_axis_param.description or x_axis_param.name, + f'Parameter value ({param_data["units"]})', + ] + else: + axes_labels = [ + 'Experiment No.', + f'Parameter value ({param_data["units"]})', + ] + + title = f"Parameter '{unique_name}' across fit results" + + self._backend.plot_scatter( x=x, - y_series=y_series, - labels=y_labels, + y=y, + sy=sy, axes_labels=axes_labels, - title=f"Measured vs Calculated data for experiment 🔬 '{expt_name}'", + title=title, height=self.height, ) - def _filtered_y_array( - self, - y_array, - x_array, - x_min, - x_max, - ): - """Filter an array by the inclusive x-range limits. - - Args: - y_array: 1D array-like of y values. - x_array: 1D array-like of x values (same length as - ``y_array``). - x_min: Minimum x limit (or ``None`` to use default). - x_max: Maximum x limit (or ``None`` to use default). - - Returns: - Filtered ``y_array`` values where ``x_array`` lies within - ``[x_min, x_max]``. + @staticmethod + def _resolve_diffrn_descriptor( + diffrn: object, + name: str | None, + ) -> object | None: """ - if x_min is None: - x_min = self.x_min - if x_max is None: - x_max = self.x_max - - mask = (x_array >= x_min) & (x_array <= x_max) - filtered_y_array = y_array[mask] - - return filtered_y_array + Return the diffrn descriptor matching *name*, or ``None``. + + Parameters + ---------- + diffrn : object + The diffrn category of an experiment. + name : str | None + Descriptor name (e.g. ``'ambient_temperature'``). + + Returns + ------- + object | None + The matching ``NumericDescriptor``, or ``None`` when *name* + is ``None`` or unrecognised. + """ + if name is None: + return None + if name == 'ambient_temperature': + return diffrn.ambient_temperature + if name == 'ambient_pressure': + return diffrn.ambient_pressure + if name == 'ambient_magnetic_field': + return diffrn.ambient_magnetic_field + if name == 'ambient_electric_field': + return diffrn.ambient_electric_field + return None class PlotterFactory(RendererFactoryBase): diff --git a/src/easydiffraction/display/tablers/__init__.py b/src/easydiffraction/display/tablers/__init__.py index 93d84417..6471fbff 100644 --- a/src/easydiffraction/display/tablers/__init__.py +++ b/src/easydiffraction/display/tablers/__init__.py @@ -1,10 +1,11 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -"""Tabular rendering backends. +""" +Tabular rendering backends. -This subpackage provides concrete implementations for rendering -tables in different environments: +This subpackage provides concrete implementations for rendering tables +in different environments: -- :mod:`.rich` for terminal and notebooks using the Rich library. -- :mod:`.pandas` for notebooks using DataFrame Styler. +- :mod:`.rich` for terminal and notebooks using the Rich library. - +:mod:`.pandas` for notebooks using DataFrame Styler. """ diff --git a/src/easydiffraction/display/tablers/base.py b/src/easydiffraction/display/tablers/base.py index 2b710d8f..869c5a17 100644 --- a/src/easydiffraction/display/tablers/base.py +++ b/src/easydiffraction/display/tablers/base.py @@ -1,6 +1,7 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -"""Low-level backends for rendering tables. +""" +Low-level backends for rendering tables. This module defines the abstract base for tabular renderers and small helpers for consistent styling across terminal and notebook outputs. @@ -10,7 +11,6 @@ from abc import ABC from abc import abstractmethod -from typing import Any from IPython import get_ipython from rich.color import Color @@ -19,11 +19,11 @@ class TableBackendBase(ABC): - """Abstract base class for concrete table backends. + """ + Abstract base class for concrete table backends. - Subclasses implement the ``render`` method which receives an - index-aware pandas DataFrame and the alignment for each column - header. + Subclasses implement the ``render`` method which receives an index- + aware pandas DataFrame and the alignment for each column header. """ FLOAT_PRECISION = 5 @@ -34,20 +34,26 @@ def __init__(self) -> None: super().__init__() self._float_fmt = f'{{:.{self.FLOAT_PRECISION}f}}'.format - def _format_value(self, value: Any) -> Any: - """Format floats with fixed precision and others as strings. + def _format_value(self, value: object) -> object: + """ + Format floats with fixed precision and others as strings. - Args: - value: Cell value to format. + Parameters + ---------- + value : object + Cell value to format. - Returns: + Returns + ------- + object A string representation with fixed precision for floats or ``str(value)`` for other types. """ return self._float_fmt(value) if isinstance(value, float) else str(value) def _is_dark_theme(self) -> bool: - """Return True when a dark theme is detected in Jupyter. + """ + Return True when a dark theme is detected in Jupyter. If not running inside Jupyter, return a sane default (True). """ @@ -62,14 +68,18 @@ def _is_dark_theme(self) -> bool: return is_dark() - def _rich_to_hex(self, color): - """Convert a Rich color name to a CSS-style hex string. + def _rich_to_hex(self, color: str) -> str: + """ + Convert a Rich color name to a CSS-style hex string. - Args: - color: Rich color name or specification parsable by - :mod:`rich`. + Parameters + ---------- + color : str + Rich color name or specification parsable by :mod:`rich`. - Returns: + Returns + ------- + str Hex color string in the form ``#RRGGBB``. """ c = Color.parse(color) @@ -90,21 +100,27 @@ def _pandas_border_color(self) -> str: @abstractmethod def render( self, - alignments, - df, - display_handle: Any | None = None, - ) -> Any: - """Render the provided DataFrame with backend-specific styling. - - Args: - alignments: Iterable of column justifications (e.g., - ``'left'`` or ``'center'``) corresponding to the data - columns. - df: Index-aware DataFrame with data to render. - display_handle: Optional environment-specific handle to - enable in-place updates. - - Returns: + alignments: object, + df: object, + display_handle: object | None = None, + ) -> object: + """ + Render the provided DataFrame with backend-specific styling. + + Parameters + ---------- + alignments : object + Iterable of column justifications (e.g., ``'left'`` or + ``'center'``) corresponding to the data columns. + df : object + Index-aware DataFrame with data to render. + display_handle : object | None, default=None + Optional environment-specific handle to enable in-place + updates. + + Returns + ------- + object Backend-defined return value (commonly ``None``). """ pass diff --git a/src/easydiffraction/display/tablers/pandas.py b/src/easydiffraction/display/tablers/pandas.py index da05b5e8..4efef148 100644 --- a/src/easydiffraction/display/tablers/pandas.py +++ b/src/easydiffraction/display/tablers/pandas.py @@ -1,11 +1,9 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause """Pandas-based table renderer for notebooks using DataFrame Styler.""" from __future__ import annotations -from typing import Any - try: from IPython.display import HTML from IPython.display import display @@ -22,13 +20,18 @@ class PandasTableBackend(TableBackendBase): """Render tables using the pandas Styler in Jupyter environments.""" def _build_base_styles(self, color: str) -> list[dict]: - """Return base CSS table styles for a given border color. + """ + Return base CSS table styles for a given border color. - Args: - color: CSS color value (e.g., ``#RRGGBB``) to use for - borders and header accents. + Parameters + ---------- + color : str + CSS color value (e.g., ``#RRGGBB``) to use for borders and + header accents. - Returns: + Returns + ------- + list[dict] A list of ``Styler.set_table_styles`` dictionaries. """ return [ @@ -76,15 +79,21 @@ def _build_base_styles(self, color: str) -> list[dict]: }, ] - def _build_header_alignment_styles(self, df, alignments) -> list[dict]: - """Generate header cell alignment styles per column. - - Args: - df: DataFrame whose columns are being rendered. - alignments: Iterable of text alignment values (e.g., - ``'left'``, ``'center'``) matching ``df`` columns. - - Returns: + def _build_header_alignment_styles(self, df: object, alignments: object) -> list[dict]: + """ + Generate header cell alignment styles per column. + + Parameters + ---------- + df : object + DataFrame whose columns are being rendered. + alignments : object + Iterable of text alignment values (e.g., ``'left'``, + ``'center'``) matching ``df`` columns. + + Returns + ------- + list[dict] A list of CSS rules for header cell alignment. """ return [ @@ -95,15 +104,22 @@ def _build_header_alignment_styles(self, df, alignments) -> list[dict]: for column, align in zip(df.columns, alignments, strict=False) ] - def _apply_styling(self, df, alignments, color: str): - """Build a configured Styler with alignments and base styles. - - Args: - df: DataFrame to style. - alignments: Iterable of text alignment values for columns. - color: CSS color value used for borders/header. - - Returns: + def _apply_styling(self, df: object, alignments: object, color: str) -> object: + """ + Build a configured Styler with alignments and base styles. + + Parameters + ---------- + df : object + DataFrame to style. + alignments : object + Iterable of text alignment values for columns. + color : str + CSS color value used for borders/header. + + Returns + ------- + object A configured pandas Styler ready for display. """ table_styles = self._build_base_styles(color) @@ -120,17 +136,20 @@ def _apply_styling(self, df, alignments, color: str): ) return styler - def _update_display(self, styler, display_handle) -> None: - """Single, consistent update path for Jupyter. + def _update_display(self, styler: object, display_handle: object) -> None: + """ + Single, consistent update path for Jupyter. If a handle with ``update()`` is provided and it's a DisplayHandle, update the output area in-place using HTML. Otherwise, display once via IPython ``display()``. - Args: - styler: Configured DataFrame Styler to be rendered. - display_handle: Optional IPython DisplayHandle used for - in-place updates. + Parameters + ---------- + styler : object + Configured DataFrame Styler to be rendered. + display_handle : object + Optional IPython DisplayHandle used for in-place updates. """ # Handle with update() method if display_handle is not None and hasattr(display_handle, 'update'): @@ -152,17 +171,27 @@ def _update_display(self, styler, display_handle) -> None: def render( self, - alignments, - df, - display_handle: Any | None = None, - ) -> Any: - """Render a styled DataFrame. - - Args: - alignments: Iterable of column justifications (e.g. 'left'). - df: DataFrame whose index is displayed as the first column. - display_handle: Optional IPython DisplayHandle to update an - existing output area in place when running in Jupyter. + alignments: object, + df: object, + display_handle: object | None = None, + ) -> object: + """ + Render a styled DataFrame. + + Parameters + ---------- + alignments : object + Iterable of column justifications (e.g. 'left'). + df : object + DataFrame whose index is displayed as the first column. + display_handle : object | None, default=None + Optional IPython DisplayHandle to update an existing output + area in place when running in Jupyter. + + Returns + ------- + object + Backend-defined return value (commonly ``None``). """ color = self._pandas_border_color styler = self._apply_styling(df, alignments, color) diff --git a/src/easydiffraction/display/tablers/rich.py b/src/easydiffraction/display/tablers/rich.py index e7a8afbb..fba2a400 100644 --- a/src/easydiffraction/display/tablers/rich.py +++ b/src/easydiffraction/display/tablers/rich.py @@ -1,11 +1,10 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause """Rich-based table renderer for terminals and notebooks.""" from __future__ import annotations import io -from typing import Any from rich.box import Box from rich.console import Console @@ -41,16 +40,20 @@ class RichTableBackend(TableBackendBase): """Render tables to terminal or Jupyter using the Rich library.""" def _to_html(self, table: Table) -> str: - """Render a Rich table to HTML using an off-screen console. + """ + Render a Rich table to HTML using an off-screen console. - A fresh ``Console(record=True, file=StringIO())`` avoids - private attribute access and guarantees no visible output - in notebooks. + A fresh ``Console(record=True, file=StringIO())`` avoids private + attribute access and guarantees no visible output in notebooks. - Args: - table: Rich :class:`~rich.table.Table` to export. + Parameters + ---------- + table : Table + Rich :class:`~rich.table.Table` to export. - Returns: + Returns + ------- + str HTML string with inline styles for notebook display. """ tmp = Console(force_jupyter=False, record=True, file=io.StringIO()) @@ -63,15 +66,22 @@ def _to_html(self, table: Table) -> str: ) return html - def _build_table(self, df, alignments, color: str) -> Table: - """Construct a Rich Table with formatted data and alignment. - - Args: - df: DataFrame-like object providing rows to render. - alignments: Iterable of text alignment values for columns. - color: Rich color name used for borders/index style. - - Returns: + def _build_table(self, df: object, alignments: object, color: str) -> Table: + """ + Construct a Rich Table with formatted data and alignment. + + Parameters + ---------- + df : object + DataFrame-like object providing rows to render. + alignments : object + Iterable of text alignment values for columns. + color : str + Rich color name used for borders/index style. + + Returns + ------- + Table A :class:`~rich.table.Table` configured for display. """ table = Table( @@ -96,20 +106,23 @@ def _build_table(self, df, alignments, color: str) -> Table: return table - def _update_display(self, table: Table, display_handle) -> None: - """Single, consistent update path for Jupyter and terminal. - - - With a handle that has ``update()``: - * If it's an IPython DisplayHandle, export to HTML and - update. - * Otherwise, treat it as a terminal/live-like handle and - update with the Rich renderable. - - Without a handle, print once to the shared console. - - Args: - table: Rich :class:`~rich.table.Table` to display. - display_handle: Optional environment-specific handle for - in-place updates (IPython or terminal live). + def _update_display(self, table: Table, display_handle: object) -> None: + """ + Single, consistent update path for Jupyter and terminal. + + - With a handle that has ``update()``: * If it's an IPython + DisplayHandle, export to HTML and update. * Otherwise, treat it + as a terminal/live-like handle and update with the Rich + renderable. - Without a handle, print once to the shared + console. + + Parameters + ---------- + table : Table + Rich :class:`~rich.table.Table` to display. + display_handle : object + Optional environment-specific handle for in- place updates + (IPython or terminal live). """ # Handle with update() method if display_handle is not None and hasattr(display_handle, 'update'): @@ -136,17 +149,26 @@ def _update_display(self, table: Table, display_handle) -> None: def render( self, - alignments, - df, - display_handle=None, - ) -> Any: - """Render a styled table using Rich. - - Args: - alignments: Iterable of text-align values for columns. - df: Index-aware DataFrame to render. - display_handle: Optional environment handle for in-place - updates. + alignments: object, + df: object, + display_handle: object = None, + ) -> object: + """ + Render a styled table using Rich. + + Parameters + ---------- + alignments : object + Iterable of text-align values for columns. + df : object + Index-aware DataFrame to render. + display_handle : object, default=None + Optional environment handle for in-place updates. + + Returns + ------- + object + Backend-defined return value (commonly ``None``). """ color = self._rich_border_color table = self._build_table(df, alignments, color) diff --git a/src/easydiffraction/display/tables.py b/src/easydiffraction/display/tables.py index e149609d..1ae39594 100644 --- a/src/easydiffraction/display/tables.py +++ b/src/easydiffraction/display/tables.py @@ -1,11 +1,10 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause """Table rendering engines: console (Rich) and Jupyter (pandas).""" from __future__ import annotations from enum import Enum -from typing import Any import pandas as pd @@ -19,12 +18,15 @@ class TableEngineEnum(str, Enum): + """Available table rendering backends.""" + RICH = 'rich' PANDAS = 'pandas' @classmethod def default(cls) -> 'TableEngineEnum': - """Select default engine based on environment. + """ + Select default engine based on environment. Returns Pandas when running in Jupyter, otherwise Rich. """ @@ -35,6 +37,14 @@ def default(cls) -> 'TableEngineEnum': return cls.RICH def description(self) -> str: + """ + Return a human-readable description of this table engine. + + Returns + ------- + str + Description string for the current enum member. + """ if self is TableEngineEnum.RICH: return 'Console rendering with Rich' elif self is TableEngineEnum.PANDAS: @@ -65,17 +75,23 @@ def show_config(self) -> None: console.paragraph('Current tabler configuration') TableRenderer.get().render(df) - def render(self, df, display_handle: Any | None = None) -> Any: - """Render a DataFrame as a table using the active backend. - - Args: - df: DataFrame with a two-level column index where the - second level provides per-column alignment. - display_handle: Optional environment-specific handle used - to update an existing output area in-place (e.g., an - IPython DisplayHandle or a terminal live handle). - - Returns: + def render(self, df: object, display_handle: object | None = None) -> object: + """ + Render a DataFrame as a table using the active backend. + + Parameters + ---------- + df : object + DataFrame with a two-level column index where the second + level provides per-column alignment. + display_handle : object | None, default=None + Optional environment-specific handle used to update an + existing output area in-place (e.g., an IPython + DisplayHandle or a terminal live handle). + + Returns + ------- + object Backend-specific return value (usually ``None``). """ # Work on a copy to avoid mutating the original DataFrame @@ -98,11 +114,11 @@ class TableRendererFactory(RendererFactoryBase): @classmethod def _registry(cls) -> dict: - """Build registry, adapting available engines to the - environment. + """ + Build registry, adapting available engines to the environment. - - In Jupyter: expose both 'rich' and 'pandas'. - - In terminal: expose only 'rich' (pandas is notebook-only). + - In Jupyter: expose both 'rich' and 'pandas'. - In terminal: + expose only 'rich' (pandas is notebook-only). """ base = { TableEngineEnum.RICH.value: { diff --git a/src/easydiffraction/display/utils.py b/src/easydiffraction/display/utils.py index 8c06f384..17c6fa94 100644 --- a/src/easydiffraction/display/utils.py +++ b/src/easydiffraction/display/utils.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause from __future__ import annotations @@ -18,9 +18,7 @@ class JupyterScrollManager: - """Ensures that Jupyter output cells are not scrollable (applied - once). - """ + """Ensures Jupyter output cells are not scrollable (once).""" _applied: ClassVar[bool] = False diff --git a/src/easydiffraction/experiments/__init__.py b/src/easydiffraction/experiments/__init__.py deleted file mode 100644 index 429f2648..00000000 --- a/src/easydiffraction/experiments/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors -# SPDX-License-Identifier: BSD-3-Clause diff --git a/src/easydiffraction/experiments/categories/__init__.py b/src/easydiffraction/experiments/categories/__init__.py deleted file mode 100644 index 429f2648..00000000 --- a/src/easydiffraction/experiments/categories/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors -# SPDX-License-Identifier: BSD-3-Clause diff --git a/src/easydiffraction/experiments/categories/background/__init__.py b/src/easydiffraction/experiments/categories/background/__init__.py deleted file mode 100644 index 429f2648..00000000 --- a/src/easydiffraction/experiments/categories/background/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors -# SPDX-License-Identifier: BSD-3-Clause diff --git a/src/easydiffraction/experiments/categories/background/factory.py b/src/easydiffraction/experiments/categories/background/factory.py deleted file mode 100644 index 716260f4..00000000 --- a/src/easydiffraction/experiments/categories/background/factory.py +++ /dev/null @@ -1,66 +0,0 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors -# SPDX-License-Identifier: BSD-3-Clause -"""Background collection entry point (public facade). - -End users should import Background classes from this module. Internals -live under the package -`easydiffraction.experiments.category_collections.background_types` -and are re-exported here for a stable and readable API. -""" - -from __future__ import annotations - -from typing import TYPE_CHECKING -from typing import Optional - -from easydiffraction.experiments.categories.background.enums import BackgroundTypeEnum - -if TYPE_CHECKING: - from easydiffraction.experiments.categories.background import BackgroundBase - - -class BackgroundFactory: - """Create background collections by type.""" - - BT = BackgroundTypeEnum - - @classmethod - def _supported_map(cls) -> dict: - """Return mapping of enum values to concrete background - classes. - """ - # Lazy import to avoid circulars - from easydiffraction.experiments.categories.background.chebyshev import ( - ChebyshevPolynomialBackground, - ) - from easydiffraction.experiments.categories.background.line_segment import ( - LineSegmentBackground, - ) - - return { - cls.BT.LINE_SEGMENT: LineSegmentBackground, - cls.BT.CHEBYSHEV: ChebyshevPolynomialBackground, - } - - @classmethod - def create( - cls, - background_type: Optional[BackgroundTypeEnum] = None, - ) -> BackgroundBase: - """Instantiate a background collection of requested type. - - If type is None, the default enum value is used. - """ - if background_type is None: - background_type = BackgroundTypeEnum.default() - - supported = cls._supported_map() - if background_type not in supported: - supported_types = list(supported.keys()) - raise ValueError( - f"Unsupported background type: '{background_type}'. " - f'Supported background types: {[bt.value for bt in supported_types]}' - ) - - background_class = supported[background_type] - return background_class() diff --git a/src/easydiffraction/experiments/categories/data/bragg_pd.py b/src/easydiffraction/experiments/categories/data/bragg_pd.py deleted file mode 100644 index da92da30..00000000 --- a/src/easydiffraction/experiments/categories/data/bragg_pd.py +++ /dev/null @@ -1,455 +0,0 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors -# SPDX-License-Identifier: BSD-3-Clause - -from __future__ import annotations - -import numpy as np - -from easydiffraction.core.category import CategoryCollection -from easydiffraction.core.category import CategoryItem -from easydiffraction.core.parameters import NumericDescriptor -from easydiffraction.core.parameters import StringDescriptor -from easydiffraction.core.validation import AttributeSpec -from easydiffraction.core.validation import DataTypes -from easydiffraction.core.validation import MembershipValidator -from easydiffraction.core.validation import RangeValidator -from easydiffraction.core.validation import RegexValidator -from easydiffraction.io.cif.handler import CifHandler -from easydiffraction.utils.utils import tof_to_d -from easydiffraction.utils.utils import twotheta_to_d - - -class PdDataPointBaseMixin: - """Single base data point mixin for powder diffraction data.""" - - def __init__(self, **kwargs): - super().__init__(**kwargs) - - self._point_id = StringDescriptor( - name='point_id', - description='Identifier for this data point in the dataset.', - value_spec=AttributeSpec( - type_=DataTypes.STRING, - default='0', - # TODO: the following pattern is valid for dict key - # (keywords are not checked). CIF label is less strict. - # Do we need conversion between CIF and internal label? - content_validator=RegexValidator(pattern=r'^[A-Za-z0-9_]*$'), - ), - cif_handler=CifHandler( - names=[ - '_pd_data.point_id', - ] - ), - ) - self._d_spacing = NumericDescriptor( - name='d_spacing', - description='d-spacing value corresponding to this data point.', - value_spec=AttributeSpec( - type_=DataTypes.NUMERIC, - default=0.0, - content_validator=RangeValidator(ge=0), - ), - cif_handler=CifHandler( - names=[ - '_pd_proc.d_spacing', - ] - ), - ) - self._intensity_meas = NumericDescriptor( - name='intensity_meas', - description='Intensity recorded at each measurement point as a function of angle/time', - value_spec=AttributeSpec( - type_=DataTypes.NUMERIC, - default=0.0, - content_validator=RangeValidator(ge=0), - ), - cif_handler=CifHandler( - names=[ - '_pd_meas.intensity_total', - '_pd_proc.intensity_norm', - ] - ), - ) - self._intensity_meas_su = NumericDescriptor( - name='intensity_meas_su', - description='Standard uncertainty of the measured intensity at this data point.', - value_spec=AttributeSpec( - type_=DataTypes.NUMERIC, - default=1.0, - content_validator=RangeValidator(ge=0), - ), - cif_handler=CifHandler( - names=[ - '_pd_meas.intensity_total_su', - '_pd_proc.intensity_norm_su', - ] - ), - ) - self._intensity_calc = NumericDescriptor( - name='intensity_calc', - description='Intensity value for a computed diffractogram at this data point.', - value_spec=AttributeSpec( - type_=DataTypes.NUMERIC, - default=0.0, - content_validator=RangeValidator(ge=0), - ), - cif_handler=CifHandler( - names=[ - '_pd_calc.intensity_total', - ] - ), - ) - self._intensity_bkg = NumericDescriptor( - name='intensity_bkg', - description='Intensity value for a computed background at this data point.', - value_spec=AttributeSpec( - type_=DataTypes.NUMERIC, - default=0.0, - content_validator=RangeValidator(ge=0), - ), - cif_handler=CifHandler( - names=[ - '_pd_calc.intensity_bkg', - ] - ), - ) - self._calc_status = StringDescriptor( - name='calc_status', - description='Status code of the data point in the calculation process.', - value_spec=AttributeSpec( - type_=DataTypes.STRING, - default='incl', # TODO: Make Enum - content_validator=MembershipValidator(allowed=['incl', 'excl']), - ), - cif_handler=CifHandler( - names=[ - '_pd_data.refinement_status', # TODO: rename to calc_status - ] - ), - ) - - @property - def point_id(self) -> StringDescriptor: - return self._point_id - - @property - def d_spacing(self) -> NumericDescriptor: - return self._d_spacing - - @property - def intensity_meas(self) -> NumericDescriptor: - return self._intensity_meas - - @property - def intensity_meas_su(self) -> NumericDescriptor: - return self._intensity_meas_su - - @property - def intensity_calc(self) -> NumericDescriptor: - return self._intensity_calc - - @property - def intensity_bkg(self) -> NumericDescriptor: - return self._intensity_bkg - - @property - def calc_status(self) -> StringDescriptor: - return self._calc_status - - -class PdCwlDataPointMixin: - """Mixin for powder diffraction data points with constant - wavelength. - """ - - def __init__(self, **kwargs): - super().__init__(**kwargs) - self._two_theta = NumericDescriptor( - name='two_theta', - description='Measured 2θ diffraction angle.', - value_spec=AttributeSpec( - type_=DataTypes.NUMERIC, - default=0.0, - content_validator=RangeValidator(ge=0, le=180), - ), - units='deg', - cif_handler=CifHandler( - names=[ - '_pd_proc.2theta_scan', - '_pd_meas.2theta_scan', - ] - ), - ) - - @property - def two_theta(self) -> NumericDescriptor: - return self._two_theta - - -class PdTofDataPointMixin: - """Mixin for powder diffraction data points with time-of-flight.""" - - def __init__(self, **kwargs): - super().__init__(**kwargs) - self._time_of_flight = NumericDescriptor( - name='time_of_flight', - description='Measured time for time-of-flight neutron measurement.', - value_spec=AttributeSpec( - type_=DataTypes.NUMERIC, - default=0.0, - content_validator=RangeValidator(ge=0), - ), - units='µs', - cif_handler=CifHandler( - names=[ - '_pd_meas.time_of_flight', - ] - ), - ) - - @property - def time_of_flight(self) -> NumericDescriptor: - return self._time_of_flight - - -class PdCwlDataPoint( - PdDataPointBaseMixin, - PdCwlDataPointMixin, - CategoryItem, # Must be last to ensure mixins initialized first -): - """Powder diffraction data point for constant-wavelength - experiments. - """ - - def __init__(self) -> None: - super().__init__() - self._identity.category_code = 'pd_data' - self._identity.category_entry_name = lambda: str(self.point_id.value) - - -class PdTofDataPoint( - PdDataPointBaseMixin, - PdTofDataPointMixin, - CategoryItem, # Must be last to ensure mixins initialized first -): - """Powder diffraction data point for time-of-flight experiments.""" - - def __init__(self) -> None: - super().__init__() - self._identity.category_code = 'pd_data' - self._identity.category_entry_name = lambda: str(self.point_id.value) - - -class PdDataBase(CategoryCollection): - # TODO: ??? - - # Redefine update priority to ensure data updated after other - # categories. Higher number = runs later. Default for other - # categories, e.g., background and excluded regions are 10 by - # default - _update_priority = 100 - - # Should be set only once - - def _set_point_id(self, values) -> None: - """Helper method to set point IDs.""" - for p, v in zip(self._items, values, strict=True): - p.point_id._value = v - - def _set_meas(self, values) -> None: - """Helper method to set measured intensity.""" - for p, v in zip(self._items, values, strict=True): - p.intensity_meas._value = v - - def _set_meas_su(self, values) -> None: - """Helper method to set standard uncertainty of measured - intensity. - """ - for p, v in zip(self._items, values, strict=True): - p.intensity_meas_su._value = v - - # Can be set multiple times - - def _set_d_spacing(self, values) -> None: - """Helper method to set d-spacing values.""" - for p, v in zip(self._calc_items, values, strict=True): - p.d_spacing._value = v - - def _set_calc(self, values) -> None: - """Helper method to set calculated intensity.""" - for p, v in zip(self._calc_items, values, strict=True): - p.intensity_calc._value = v - - def _set_bkg(self, values) -> None: - """Helper method to set background intensity.""" - for p, v in zip(self._calc_items, values, strict=True): - p.intensity_bkg._value = v - - def _set_calc_status(self, values) -> None: - """Helper method to set refinement status.""" - for p, v in zip(self._items, values, strict=True): - if v: - p.calc_status._value = 'incl' - elif not v: - p.calc_status._value = 'excl' - else: - raise ValueError( - f'Invalid refinement status value: {v}. Expected boolean True/False.' - ) - - @property - def _calc_mask(self) -> np.ndarray: - return self.calc_status == 'incl' - - @property - def _calc_items(self): - """Get only the items included in calculations.""" - return [item for item, mask in zip(self._items, self._calc_mask, strict=False) if mask] - - @property - def calc_status(self) -> np.ndarray: - return np.fromiter((p.calc_status.value for p in self._items), dtype=object) - - @property - def d(self) -> np.ndarray: - return np.fromiter((p.d_spacing.value for p in self._calc_items), dtype=float) - - @property - def meas(self) -> np.ndarray: - return np.fromiter((p.intensity_meas.value for p in self._calc_items), dtype=float) - - @property - def meas_su(self) -> np.ndarray: - # TODO: The following is a temporary workaround to handle zero - # or near-zero uncertainties in the data, when dats is loaded - # from CIF files. This is necessary because zero uncertainties - # cause fitting algorithms to fail. - # The current implementation is inefficient. - # In the future, we should extend the functionality of - # the NumericDescriptor to automatically replace the value - # outside of the valid range (`content_validator`) with a - # default value (`default`), when the value is set. - # BraggPdExperiment._load_ascii_data_to_experiment() handles - # this for ASCII data, but we also need to handle CIF data and - # come up with a consistent approach for both data sources. - original = np.fromiter((p.intensity_meas_su.value for p in self._calc_items), dtype=float) - # Replace values smaller than 0.0001 with 1.0 - modified = np.where(original < 0.0001, 1.0, original) - return modified - - @property - def calc(self) -> np.ndarray: - return np.fromiter((p.intensity_calc.value for p in self._calc_items), dtype=float) - - @property - def bkg(self) -> np.ndarray: - return np.fromiter((p.intensity_bkg.value for p in self._calc_items), dtype=float) - - def _update(self, called_by_minimizer=False): - experiment = self._parent - experiments = experiment._parent - project = experiments._parent - sample_models = project.sample_models - # calculator = experiment.calculator # TODO: move from analysis - calculator = project.analysis.calculator - - initial_calc = np.zeros_like(self.x) - calc = initial_calc - for linked_phase in experiment._get_valid_linked_phases(sample_models): - sample_model_id = linked_phase._identity.category_entry_name - sample_model_scale = linked_phase.scale.value - sample_model = sample_models[sample_model_id] - - sample_model_calc = calculator.calculate_pattern( - sample_model, - experiment, - called_by_minimizer=called_by_minimizer, - ) - - sample_model_scaled_calc = sample_model_scale * sample_model_calc - calc += sample_model_scaled_calc - - self._set_calc(calc + self.bkg) - - -class PdCwlData(PdDataBase): - # TODO: ??? - # _description: str = 'Powder diffraction data points for - # constant-wavelength experiments.' - - def __init__(self): - super().__init__(item_type=PdCwlDataPoint) - - # Should be set only once - - def _set_x(self, values) -> None: - """Helper method to set 2θ values.""" - # TODO: split into multiple methods - self._items = [self._item_type() for _ in range(values.size)] - for p, v in zip(self._items, values, strict=True): - p.two_theta._value = v - self._set_point_id([str(i + 1) for i in range(values.size)]) - - @property - def all_x(self) -> np.ndarray: - """Get the 2θ values for all data points in this collection.""" - return np.fromiter((p.two_theta.value for p in self._items), dtype=float) - - @property - def x(self) -> np.ndarray: - """Get the 2θ values for data points included in - calculations. - """ - return np.fromiter((p.two_theta.value for p in self._calc_items), dtype=float) - - def _update(self, called_by_minimizer=False): - super()._update(called_by_minimizer) - - experiment = self._parent - d_spacing = twotheta_to_d( - self.x, - experiment.instrument.setup_wavelength.value, - ) - self._set_d_spacing(d_spacing) - - -class PdTofData(PdDataBase): - # TODO: ??? - # _description: str = 'Powder diffraction data points for - # time-of-flight experiments.' - - def __init__(self): - super().__init__(item_type=PdTofDataPoint) - - def _set_x(self, values) -> None: - """Helper method to set time-of-flight values.""" - # TODO: split into multiple methods - self._items = [self._item_type() for _ in range(values.size)] - for p, v in zip(self._items, values, strict=True): - p.time_of_flight._value = v - self._set_point_id([str(i + 1) for i in range(values.size)]) - - @property - def all_x(self) -> np.ndarray: - """Get the TOF values for all data points in this collection.""" - return np.fromiter((p.time_of_flight.value for p in self._items), dtype=float) - - @property - def x(self) -> np.ndarray: - """Get the TOF values for data points included in - calculations. - """ - return np.fromiter((p.time_of_flight.value for p in self._calc_items), dtype=float) - - def _update(self, called_by_minimizer=False): - super()._update(called_by_minimizer) - - experiment = self._parent - d_spacing = tof_to_d( - self.x, - experiment.instrument.calib_d_to_tof_offset.value, - experiment.instrument.calib_d_to_tof_linear.value, - experiment.instrument.calib_d_to_tof_quad.value, - ) - self._set_d_spacing(d_spacing) diff --git a/src/easydiffraction/experiments/categories/data/bragg_sc.py b/src/easydiffraction/experiments/categories/data/bragg_sc.py deleted file mode 100644 index f738b2df..00000000 --- a/src/easydiffraction/experiments/categories/data/bragg_sc.py +++ /dev/null @@ -1,96 +0,0 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors -# SPDX-License-Identifier: BSD-3-Clause - -from __future__ import annotations - -from easydiffraction.core.category import CategoryCollection -from easydiffraction.core.parameters import NumericDescriptor -from easydiffraction.core.parameters import StringDescriptor -from easydiffraction.core.validation import AttributeSpec -from easydiffraction.core.validation import DataTypes -from easydiffraction.core.validation import RangeValidator -from easydiffraction.io.cif.handler import CifHandler - - -class Refln: - def __init__(self, **kwargs): - super().__init__(**kwargs) - - self._index_h = StringDescriptor( - name='index_h', - description='...', - value_spec=AttributeSpec( - type_=DataTypes.NUMERIC, - default=0.0, - content_validator=RangeValidator(), - ), - cif_handler=CifHandler( - names=[ - '_refln.index_h', - ] - ), - ) - self._index_k = StringDescriptor( - name='index_k', - description='...', - value_spec=AttributeSpec( - type_=DataTypes.NUMERIC, - default=0.0, - content_validator=RangeValidator(), - ), - cif_handler=CifHandler( - names=[ - '_refln.index_k', - ] - ), - ) - self._index_l = StringDescriptor( - name='index_l', - description='...', - value_spec=AttributeSpec( - type_=DataTypes.NUMERIC, - default=0.0, - content_validator=RangeValidator(), - ), - cif_handler=CifHandler( - names=[ - '_refln.index_l', - ] - ), - ) - self._intensity_meas = NumericDescriptor( - name='intensity_meas', - description='...', - value_spec=AttributeSpec( - type_=DataTypes.NUMERIC, - default=0.0, - content_validator=RangeValidator(ge=0), - ), - cif_handler=CifHandler( - names=[ - '_refln.intensity_meas', - ] - ), - ) - self._intensity_meas_su = NumericDescriptor( - name='intensity_meas_su', - description='...', - value_spec=AttributeSpec( - type_=DataTypes.NUMERIC, - default=0.0, - content_validator=RangeValidator(ge=0), - ), - cif_handler=CifHandler( - names=[ - '_refln.intensity_meas_su', - ] - ), - ) - - class ReflnData(CategoryCollection): - """...""" - - _update_priority = 100 - - def __init__(self): - super().__init__(item_type=Refln) diff --git a/src/easydiffraction/experiments/categories/data/factory.py b/src/easydiffraction/experiments/categories/data/factory.py deleted file mode 100644 index 2d60560c..00000000 --- a/src/easydiffraction/experiments/categories/data/factory.py +++ /dev/null @@ -1,76 +0,0 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors -# SPDX-License-Identifier: BSD-3-Clause - -from __future__ import annotations - -from typing import TYPE_CHECKING -from typing import Optional - -from easydiffraction.experiments.categories.data.bragg_pd import PdCwlData -from easydiffraction.experiments.categories.data.bragg_pd import PdTofData -from easydiffraction.experiments.categories.data.total import TotalData -from easydiffraction.experiments.experiment.enums import BeamModeEnum -from easydiffraction.experiments.experiment.enums import SampleFormEnum -from easydiffraction.experiments.experiment.enums import ScatteringTypeEnum - -if TYPE_CHECKING: - from easydiffraction.core.category import CategoryCollection - - -class DataFactory: - """Factory for creating powder diffraction data collections.""" - - _supported = { - SampleFormEnum.POWDER: { - ScatteringTypeEnum.BRAGG: { - BeamModeEnum.CONSTANT_WAVELENGTH: PdCwlData, - BeamModeEnum.TIME_OF_FLIGHT: PdTofData, - }, - ScatteringTypeEnum.TOTAL: { - BeamModeEnum.CONSTANT_WAVELENGTH: TotalData, - BeamModeEnum.TIME_OF_FLIGHT: TotalData, - }, - }, - } - - @classmethod - def create( - cls, - *, - sample_form: Optional[SampleFormEnum] = None, - beam_mode: Optional[BeamModeEnum] = None, - scattering_type: Optional[ScatteringTypeEnum] = None, - ) -> CategoryCollection: - """Create a data collection for the given configuration.""" - if sample_form is None: - sample_form = SampleFormEnum.default() - if beam_mode is None: - beam_mode = BeamModeEnum.default() - if scattering_type is None: - scattering_type = ScatteringTypeEnum.default() - - supported_sample_forms = list(cls._supported.keys()) - if sample_form not in supported_sample_forms: - raise ValueError( - f"Unsupported sample form: '{sample_form}'.\n" - f'Supported sample forms: {supported_sample_forms}' - ) - - supported_scattering_types = list(cls._supported[sample_form].keys()) - if scattering_type not in supported_scattering_types: - raise ValueError( - f"Unsupported scattering type: '{scattering_type}' for sample form: " - f"'{sample_form}'.\n Supported scattering types: '{supported_scattering_types}'" - ) - supported_beam_modes = list(cls._supported[sample_form][scattering_type].keys()) - if beam_mode not in supported_beam_modes: - raise ValueError( - f"Unsupported beam mode: '{beam_mode}' for sample form: " - f"'{sample_form}' and scattering type '{scattering_type}'.\n" - f"Supported beam modes: '{supported_beam_modes}'" - ) - - data_class = cls._supported[sample_form][scattering_type][beam_mode] - data_obj = data_class() - - return data_obj diff --git a/src/easydiffraction/experiments/categories/data/total.py b/src/easydiffraction/experiments/categories/data/total.py deleted file mode 100644 index 5dc69b9d..00000000 --- a/src/easydiffraction/experiments/categories/data/total.py +++ /dev/null @@ -1,267 +0,0 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors -# SPDX-License-Identifier: BSD-3-Clause -"""Data categories for total scattering (PDF) experiments.""" - -from __future__ import annotations - -import numpy as np - -from easydiffraction.core.category import CategoryCollection -from easydiffraction.core.category import CategoryItem -from easydiffraction.core.parameters import NumericDescriptor -from easydiffraction.core.parameters import StringDescriptor -from easydiffraction.core.validation import AttributeSpec -from easydiffraction.core.validation import DataTypes -from easydiffraction.core.validation import MembershipValidator -from easydiffraction.core.validation import RangeValidator -from easydiffraction.core.validation import RegexValidator -from easydiffraction.io.cif.handler import CifHandler - - -class TotalDataPoint(CategoryItem): - """Total scattering (PDF) data point in r-space (real space). - - Note: PDF data is always in r-space regardless of whether the - original measurement was CWL or TOF. - """ - - def __init__(self) -> None: - super().__init__() - - self._point_id = StringDescriptor( - name='point_id', - description='Identifier for this data point in the dataset.', - value_spec=AttributeSpec( - type_=DataTypes.STRING, - default='0', - content_validator=RegexValidator(pattern=r'^[A-Za-z0-9_]*$'), - ), - cif_handler=CifHandler( - names=[ - '_pd_data.point_id', # TODO: Use total scattering CIF names - ] - ), - ) - self._r = NumericDescriptor( - name='r', - description='Interatomic distance in real space.', - value_spec=AttributeSpec( - type_=DataTypes.NUMERIC, - default=0.0, - content_validator=RangeValidator(ge=0), - ), - units='Å', - cif_handler=CifHandler( - names=[ - '_pd_proc.r', # TODO: Use PDF-specific CIF names - ] - ), - ) - self._g_r_meas = NumericDescriptor( - name='g_r_meas', - description='Measured pair distribution function G(r).', - value_spec=AttributeSpec( - type_=DataTypes.NUMERIC, - default=0.0, - ), - cif_handler=CifHandler( - names=[ - '_pd_meas.intensity_total', # TODO: Use PDF-specific CIF names - ] - ), - ) - self._g_r_meas_su = NumericDescriptor( - name='g_r_meas_su', - description='Standard uncertainty of measured G(r).', - value_spec=AttributeSpec( - type_=DataTypes.NUMERIC, - default=0.0, - content_validator=RangeValidator(ge=0), - ), - cif_handler=CifHandler( - names=[ - '_pd_meas.intensity_total_su', # TODO: Use PDF-specific CIF names - ] - ), - ) - self._g_r_calc = NumericDescriptor( - name='g_r_calc', - description='Calculated pair distribution function G(r).', - value_spec=AttributeSpec( - type_=DataTypes.NUMERIC, - default=0.0, - ), - cif_handler=CifHandler( - names=[ - '_pd_calc.intensity_total', # TODO: Use PDF-specific CIF names - ] - ), - ) - self._calc_status = StringDescriptor( - name='calc_status', - description='Status code of the data point in calculation.', - value_spec=AttributeSpec( - type_=DataTypes.STRING, - default='incl', - content_validator=MembershipValidator(allowed=['incl', 'excl']), - ), - cif_handler=CifHandler( - names=[ - '_pd_data.refinement_status', # TODO: Use PDF-specific CIF names - ] - ), - ) - - self._identity.category_code = 'total_data' - self._identity.category_entry_name = lambda: str(self.point_id.value) - - @property - def point_id(self) -> StringDescriptor: - return self._point_id - - @property - def r(self) -> NumericDescriptor: - return self._r - - @property - def g_r_meas(self) -> NumericDescriptor: - return self._g_r_meas - - @property - def g_r_meas_su(self) -> NumericDescriptor: - return self._g_r_meas_su - - @property - def g_r_calc(self) -> NumericDescriptor: - return self._g_r_calc - - @property - def calc_status(self) -> StringDescriptor: - return self._calc_status - - -class TotalDataBase(CategoryCollection): - """Base class for total scattering data collections.""" - - _update_priority = 100 - - # Should be set only once - - def _set_point_id(self, values) -> None: - """Helper method to set point IDs.""" - for p, v in zip(self._items, values, strict=True): - p.point_id._value = v - - def _set_meas(self, values) -> None: - """Helper method to set measured G(r).""" - for p, v in zip(self._items, values, strict=True): - p.g_r_meas._value = v - - def _set_meas_su(self, values) -> None: - """Helper method to set standard uncertainty of measured - G(r). - """ - for p, v in zip(self._items, values, strict=True): - p.g_r_meas_su._value = v - - # Can be set multiple times - - def _set_calc(self, values) -> None: - """Helper method to set calculated G(r).""" - for p, v in zip(self._calc_items, values, strict=True): - p.g_r_calc._value = v - - def _set_calc_status(self, values) -> None: - """Helper method to set calculation status.""" - for p, v in zip(self._items, values, strict=True): - if v: - p.calc_status._value = 'incl' - elif not v: - p.calc_status._value = 'excl' - else: - raise ValueError( - f'Invalid calculation status value: {v}. Expected boolean True/False.' - ) - - @property - def _calc_mask(self) -> np.ndarray: - return self.calc_status == 'incl' - - @property - def _calc_items(self): - """Get only the items included in calculations.""" - return [item for item, mask in zip(self._items, self._calc_mask, strict=False) if mask] - - @property - def calc_status(self) -> np.ndarray: - return np.fromiter((p.calc_status.value for p in self._items), dtype=object) - - @property - def meas(self) -> np.ndarray: - return np.fromiter((p.g_r_meas.value for p in self._calc_items), dtype=float) - - @property - def meas_su(self) -> np.ndarray: - return np.fromiter((p.g_r_meas_su.value for p in self._calc_items), dtype=float) - - @property - def calc(self) -> np.ndarray: - return np.fromiter((p.g_r_calc.value for p in self._calc_items), dtype=float) - - @property - def bkg(self) -> np.ndarray: - """Background is always zero for PDF data.""" - return np.zeros_like(self.calc) - - def _update(self, called_by_minimizer=False): - experiment = self._parent - experiments = experiment._parent - project = experiments._parent - sample_models = project.sample_models - calculator = project.analysis.calculator - - initial_calc = np.zeros_like(self.x) - calc = initial_calc - for linked_phase in experiment._get_valid_linked_phases(sample_models): - sample_model_id = linked_phase._identity.category_entry_name - sample_model_scale = linked_phase.scale.value - sample_model = sample_models[sample_model_id] - - sample_model_calc = calculator.calculate_pattern( - sample_model, - experiment, - called_by_minimizer=called_by_minimizer, - ) - - sample_model_scaled_calc = sample_model_scale * sample_model_calc - calc += sample_model_scaled_calc - - self._set_calc(calc) - - -class TotalData(TotalDataBase): - """Total scattering (PDF) data collection in r-space. - - Note: Works for both CWL and TOF measurements as PDF data - is always transformed to r-space. - """ - - def __init__(self): - super().__init__(item_type=TotalDataPoint) - - def _set_x(self, values) -> None: - """Helper method to set r values.""" - self._items = [self._item_type() for _ in range(values.size)] - for p, v in zip(self._items, values, strict=True): - p.r._value = v - self._set_point_id([str(i + 1) for i in range(values.size)]) - - @property - def all_x(self) -> np.ndarray: - """Get the r values for all data points.""" - return np.fromiter((p.r.value for p in self._items), dtype=float) - - @property - def x(self) -> np.ndarray: - """Get the r values for data points included in calculations.""" - return np.fromiter((p.r.value for p in self._calc_items), dtype=float) diff --git a/src/easydiffraction/experiments/categories/experiment_type.py b/src/easydiffraction/experiments/categories/experiment_type.py deleted file mode 100644 index babe82ee..00000000 --- a/src/easydiffraction/experiments/categories/experiment_type.py +++ /dev/null @@ -1,156 +0,0 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors -# SPDX-License-Identifier: BSD-3-Clause -"""Experiment type descriptor (form, beam, probe, scattering). - -This lightweight container stores the categorical attributes defining -an experiment configuration and handles CIF serialization via -``CifHandler``. -""" - -from easydiffraction.core.category import CategoryItem -from easydiffraction.core.parameters import StringDescriptor -from easydiffraction.core.validation import AttributeSpec -from easydiffraction.core.validation import DataTypes -from easydiffraction.core.validation import MembershipValidator -from easydiffraction.experiments.experiment.enums import BeamModeEnum -from easydiffraction.experiments.experiment.enums import RadiationProbeEnum -from easydiffraction.experiments.experiment.enums import SampleFormEnum -from easydiffraction.experiments.experiment.enums import ScatteringTypeEnum -from easydiffraction.io.cif.handler import CifHandler - - -class ExperimentType(CategoryItem): - """Container of categorical attributes defining experiment flavor. - - Args: - sample_form: Powder or Single crystal. - beam_mode: Constant wavelength (CW) or time-of-flight (TOF). - radiation_probe: Neutrons or X-rays. - scattering_type: Bragg or Total. - """ - - def __init__( - self, - *, - sample_form=None, - beam_mode=None, - radiation_probe=None, - scattering_type=None, - ): - super().__init__() - - self._sample_form: StringDescriptor = StringDescriptor( - name='sample_form', - description='Specifies whether the diffraction data corresponds to ' - 'powder diffraction or single crystal diffraction', - value_spec=AttributeSpec( - value=sample_form, - type_=DataTypes.STRING, - default=SampleFormEnum.default().value, - content_validator=MembershipValidator( - allowed=[member.value for member in SampleFormEnum] - ), - ), - cif_handler=CifHandler( - names=[ - '_expt_type.sample_form', - ] - ), - ) - - self._beam_mode: StringDescriptor = StringDescriptor( - name='beam_mode', - description='Defines whether the measurement is performed with a ' - 'constant wavelength (CW) or time-of-flight (TOF) method', - value_spec=AttributeSpec( - value=beam_mode, - type_=DataTypes.STRING, - default=BeamModeEnum.default().value, - content_validator=MembershipValidator( - allowed=[member.value for member in BeamModeEnum] - ), - ), - cif_handler=CifHandler( - names=[ - '_expt_type.beam_mode', - ] - ), - ) - self._radiation_probe: StringDescriptor = StringDescriptor( - name='radiation_probe', - description='Specifies whether the measurement uses neutrons or X-rays', - value_spec=AttributeSpec( - value=radiation_probe, - type_=DataTypes.STRING, - default=RadiationProbeEnum.default().value, - content_validator=MembershipValidator( - allowed=[member.value for member in RadiationProbeEnum] - ), - ), - cif_handler=CifHandler( - names=[ - '_expt_type.radiation_probe', - ] - ), - ) - self._scattering_type: StringDescriptor = StringDescriptor( - name='scattering_type', - description='Specifies whether the experiment uses Bragg scattering ' - '(for conventional structure refinement) or total scattering ' - '(for pair distribution function analysis - PDF)', - value_spec=AttributeSpec( - value=scattering_type, - type_=DataTypes.STRING, - default=ScatteringTypeEnum.default().value, - content_validator=MembershipValidator( - allowed=[member.value for member in ScatteringTypeEnum] - ), - ), - cif_handler=CifHandler( - names=[ - '_expt_type.scattering_type', - ] - ), - ) - - self._identity.category_code = 'expt_type' - - @property - def sample_form(self): - """Sample form descriptor (powder/single crystal).""" - return self._sample_form - - @sample_form.setter - def sample_form(self, value): - """Set sample form value.""" - self._sample_form.value = value - - @property - def beam_mode(self): - """Beam mode descriptor (CW/TOF).""" - return self._beam_mode - - @beam_mode.setter - def beam_mode(self, value): - """Set beam mode value.""" - self._beam_mode.value = value - - @property - def radiation_probe(self): - """Radiation probe descriptor (neutrons/X-rays).""" - return self._radiation_probe - - @radiation_probe.setter - def radiation_probe(self, value): - """Set radiation probe value.""" - self._radiation_probe.value = value - - @property - def scattering_type(self): - """Scattering type descriptor (Bragg/Total).""" - return self._scattering_type - - @scattering_type.setter - def scattering_type(self, value): - """Set scattering type value.""" - self._scattering_type.value = value diff --git a/src/easydiffraction/experiments/categories/instrument/__init__.py b/src/easydiffraction/experiments/categories/instrument/__init__.py deleted file mode 100644 index 429f2648..00000000 --- a/src/easydiffraction/experiments/categories/instrument/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors -# SPDX-License-Identifier: BSD-3-Clause diff --git a/src/easydiffraction/experiments/categories/instrument/cwl.py b/src/easydiffraction/experiments/categories/instrument/cwl.py deleted file mode 100644 index 0653f93c..00000000 --- a/src/easydiffraction/experiments/categories/instrument/cwl.py +++ /dev/null @@ -1,72 +0,0 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors -# SPDX-License-Identifier: BSD-3-Clause - -from easydiffraction.core.parameters import Parameter -from easydiffraction.core.validation import AttributeSpec -from easydiffraction.core.validation import DataTypes -from easydiffraction.core.validation import RangeValidator -from easydiffraction.experiments.categories.instrument.base import InstrumentBase -from easydiffraction.io.cif.handler import CifHandler - - -class CwlInstrument(InstrumentBase): - def __init__( - self, - *, - setup_wavelength=None, - calib_twotheta_offset=None, - ) -> None: - super().__init__() - - self._setup_wavelength: Parameter = Parameter( - name='wavelength', - description='Incident neutron or X-ray wavelength', - value_spec=AttributeSpec( - value=setup_wavelength, - type_=DataTypes.NUMERIC, - default=1.5406, - content_validator=RangeValidator(), - ), - units='Å', - cif_handler=CifHandler( - names=[ - '_instr.wavelength', - ] - ), - ) - self._calib_twotheta_offset: Parameter = Parameter( - name='twotheta_offset', - description='Instrument misalignment offset', - value_spec=AttributeSpec( - value=calib_twotheta_offset, - type_=DataTypes.NUMERIC, - default=0.0, - content_validator=RangeValidator(), - ), - units='deg', - cif_handler=CifHandler( - names=[ - '_instr.2theta_offset', - ] - ), - ) - - @property - def setup_wavelength(self): - """Incident wavelength parameter (Å).""" - return self._setup_wavelength - - @setup_wavelength.setter - def setup_wavelength(self, value): - """Set incident wavelength value (Å).""" - self._setup_wavelength.value = value - - @property - def calib_twotheta_offset(self): - """Instrument misalignment two-theta offset (deg).""" - return self._calib_twotheta_offset - - @calib_twotheta_offset.setter - def calib_twotheta_offset(self, value): - """Set two-theta offset value (deg).""" - self._calib_twotheta_offset.value = value diff --git a/src/easydiffraction/experiments/categories/instrument/factory.py b/src/easydiffraction/experiments/categories/instrument/factory.py deleted file mode 100644 index 443957a1..00000000 --- a/src/easydiffraction/experiments/categories/instrument/factory.py +++ /dev/null @@ -1,74 +0,0 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors -# SPDX-License-Identifier: BSD-3-Clause -"""Factory for instrument category items. - -Provides a stable entry point for creating instrument objects from the -experiment's scattering type and beam mode. -""" - -from __future__ import annotations - -from typing import TYPE_CHECKING -from typing import Optional -from typing import Type - -from easydiffraction.experiments.experiment.enums import BeamModeEnum -from easydiffraction.experiments.experiment.enums import ScatteringTypeEnum - -if TYPE_CHECKING: - from easydiffraction.experiments.categories.instrument.base import InstrumentBase - - -class InstrumentFactory: - """Create instrument instances for supported modes. - - The factory hides implementation details and lazy-loads concrete - instrument classes to avoid circular imports. - """ - - ST = ScatteringTypeEnum - BM = BeamModeEnum - - @classmethod - def _supported_map(cls) -> dict: - # Lazy import to avoid circulars - from easydiffraction.experiments.categories.instrument.cwl import CwlInstrument - from easydiffraction.experiments.categories.instrument.tof import TofInstrument - - return { - cls.ST.BRAGG: { - cls.BM.CONSTANT_WAVELENGTH: CwlInstrument, - cls.BM.TIME_OF_FLIGHT: TofInstrument, - } - } - - @classmethod - def create( - cls, - scattering_type: Optional[ScatteringTypeEnum] = None, - beam_mode: Optional[BeamModeEnum] = None, - ) -> InstrumentBase: - if beam_mode is None: - beam_mode = BeamModeEnum.default() - if scattering_type is None: - scattering_type = ScatteringTypeEnum.default() - - supported = cls._supported_map() - - supported_scattering_types = list(supported.keys()) - if scattering_type not in supported_scattering_types: - raise ValueError( - f"Unsupported scattering type: '{scattering_type}'.\n " - f'Supported scattering types: {supported_scattering_types}' - ) - - supported_beam_modes = list(supported[scattering_type].keys()) - if beam_mode not in supported_beam_modes: - raise ValueError( - f"Unsupported beam mode: '{beam_mode}' for scattering type: " - f"'{scattering_type}'.\n " - f'Supported beam modes: {supported_beam_modes}' - ) - - instrument_class: Type[InstrumentBase] = supported[scattering_type][beam_mode] - return instrument_class() diff --git a/src/easydiffraction/experiments/categories/instrument/tof.py b/src/easydiffraction/experiments/categories/instrument/tof.py deleted file mode 100644 index ede52d3c..00000000 --- a/src/easydiffraction/experiments/categories/instrument/tof.py +++ /dev/null @@ -1,153 +0,0 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors -# SPDX-License-Identifier: BSD-3-Clause - -from easydiffraction.core.parameters import Parameter -from easydiffraction.core.validation import AttributeSpec -from easydiffraction.core.validation import DataTypes -from easydiffraction.core.validation import RangeValidator -from easydiffraction.experiments.categories.instrument.base import InstrumentBase -from easydiffraction.io.cif.handler import CifHandler - - -class TofInstrument(InstrumentBase): - def __init__( - self, - *, - setup_twotheta_bank=None, - calib_d_to_tof_offset=None, - calib_d_to_tof_linear=None, - calib_d_to_tof_quad=None, - calib_d_to_tof_recip=None, - ) -> None: - super().__init__() - - self._setup_twotheta_bank: Parameter = Parameter( - name='twotheta_bank', - description='Detector bank position', - value_spec=AttributeSpec( - value=setup_twotheta_bank, - type_=DataTypes.NUMERIC, - default=150.0, - content_validator=RangeValidator(), - ), - units='deg', - cif_handler=CifHandler( - names=[ - '_instr.2theta_bank', - ] - ), - ) - self._calib_d_to_tof_offset: Parameter = Parameter( - name='d_to_tof_offset', - description='TOF offset', - value_spec=AttributeSpec( - value=calib_d_to_tof_offset, - type_=DataTypes.NUMERIC, - default=0.0, - content_validator=RangeValidator(), - ), - units='µs', - cif_handler=CifHandler( - names=[ - '_instr.d_to_tof_offset', - ] - ), - ) - self._calib_d_to_tof_linear: Parameter = Parameter( - name='d_to_tof_linear', - description='TOF linear conversion', - value_spec=AttributeSpec( - value=calib_d_to_tof_linear, - type_=DataTypes.NUMERIC, - default=10000.0, - content_validator=RangeValidator(), - ), - units='µs/Å', - cif_handler=CifHandler( - names=[ - '_instr.d_to_tof_linear', - ] - ), - ) - self._calib_d_to_tof_quad: Parameter = Parameter( - name='d_to_tof_quad', - description='TOF quadratic correction', - value_spec=AttributeSpec( - value=calib_d_to_tof_quad, - type_=DataTypes.NUMERIC, - default=-0.00001, - content_validator=RangeValidator(), - ), - units='µs/Ų', - cif_handler=CifHandler( - names=[ - '_instr.d_to_tof_quad', - ] - ), - ) - self._calib_d_to_tof_recip: Parameter = Parameter( - name='d_to_tof_recip', - description='TOF reciprocal velocity correction', - value_spec=AttributeSpec( - value=calib_d_to_tof_recip, - type_=DataTypes.NUMERIC, - default=0.0, - content_validator=RangeValidator(), - ), - units='µs·Å', - cif_handler=CifHandler( - names=[ - '_instr.d_to_tof_recip', - ] - ), - ) - - @property - def setup_twotheta_bank(self): - """Detector bank two-theta position (deg).""" - return self._setup_twotheta_bank - - @setup_twotheta_bank.setter - def setup_twotheta_bank(self, value): - """Set detector bank two-theta position (deg).""" - self._setup_twotheta_bank.value = value - - @property - def calib_d_to_tof_offset(self): - """TOF offset calibration parameter (µs).""" - return self._calib_d_to_tof_offset - - @calib_d_to_tof_offset.setter - def calib_d_to_tof_offset(self, value): - """Set TOF offset (µs).""" - self._calib_d_to_tof_offset.value = value - - @property - def calib_d_to_tof_linear(self): - """Linear d to TOF conversion coefficient (µs/Å).""" - return self._calib_d_to_tof_linear - - @calib_d_to_tof_linear.setter - def calib_d_to_tof_linear(self, value): - """Set linear d to TOF coefficient (µs/Å).""" - self._calib_d_to_tof_linear.value = value - - @property - def calib_d_to_tof_quad(self): - """Quadratic d to TOF correction coefficient (µs/Ų).""" - return self._calib_d_to_tof_quad - - @calib_d_to_tof_quad.setter - def calib_d_to_tof_quad(self, value): - """Set quadratic d to TOF correction (µs/Ų).""" - self._calib_d_to_tof_quad.value = value - - @property - def calib_d_to_tof_recip(self): - """Reciprocal-velocity d to TOF correction (µs·Å).""" - return self._calib_d_to_tof_recip - - @calib_d_to_tof_recip.setter - def calib_d_to_tof_recip(self, value): - """Set reciprocal-velocity d to TOF correction (µs·Å).""" - self._calib_d_to_tof_recip.value = value diff --git a/src/easydiffraction/experiments/categories/linked_phases.py b/src/easydiffraction/experiments/categories/linked_phases.py deleted file mode 100644 index 3ff827d5..00000000 --- a/src/easydiffraction/experiments/categories/linked_phases.py +++ /dev/null @@ -1,86 +0,0 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors -# SPDX-License-Identifier: BSD-3-Clause -"""Linked phases allow combining phases with scale factors.""" - -from easydiffraction.core.category import CategoryCollection -from easydiffraction.core.category import CategoryItem -from easydiffraction.core.parameters import Parameter -from easydiffraction.core.parameters import StringDescriptor -from easydiffraction.core.validation import AttributeSpec -from easydiffraction.core.validation import DataTypes -from easydiffraction.core.validation import RangeValidator -from easydiffraction.core.validation import RegexValidator -from easydiffraction.io.cif.handler import CifHandler - - -class LinkedPhase(CategoryItem): - """Link to a phase by id with a scale factor.""" - - def __init__( - self, - *, - id=None, # TODO: need new name instead of id - scale=None, - ): - super().__init__() - - self._id = StringDescriptor( - name='id', - description='Identifier of the linked phase.', - value_spec=AttributeSpec( - value=id, - type_=DataTypes.STRING, - default='Si', - content_validator=RegexValidator(pattern=r'^[A-Za-z_][A-Za-z0-9_]*$'), - ), - cif_handler=CifHandler( - names=[ - '_pd_phase_block.id', - ] - ), - ) - self._scale = Parameter( - name='scale', - description='Scale factor of the linked phase.', - value_spec=AttributeSpec( - value=scale, - type_=DataTypes.NUMERIC, - default=1.0, - content_validator=RangeValidator(), - ), - cif_handler=CifHandler( - names=[ - '_pd_phase_block.scale', - ] - ), - ) - self._identity.category_code = 'linked_phases' - self._identity.category_entry_name = lambda: str(self.id.value) - - @property - def id(self) -> StringDescriptor: - """Identifier of the linked phase.""" - return self._id - - @id.setter - def id(self, value: str): - """Set the linked phase identifier.""" - self._id.value = value - - @property - def scale(self) -> Parameter: - """Scale factor parameter.""" - return self._scale - - @scale.setter - def scale(self, value: float): - """Set scale factor value.""" - self._scale.value = value - - -class LinkedPhases(CategoryCollection): - """Collection of LinkedPhase instances.""" - - def __init__(self): - """Create an empty collection of linked phases.""" - super().__init__(item_type=LinkedPhase) diff --git a/src/easydiffraction/experiments/categories/peak/__init__.py b/src/easydiffraction/experiments/categories/peak/__init__.py deleted file mode 100644 index 429f2648..00000000 --- a/src/easydiffraction/experiments/categories/peak/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors -# SPDX-License-Identifier: BSD-3-Clause diff --git a/src/easydiffraction/experiments/categories/peak/cwl.py b/src/easydiffraction/experiments/categories/peak/cwl.py deleted file mode 100644 index 76777a44..00000000 --- a/src/easydiffraction/experiments/categories/peak/cwl.py +++ /dev/null @@ -1,45 +0,0 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors -# SPDX-License-Identifier: BSD-3-Clause -"""Constant-wavelength peak profile classes.""" - -from easydiffraction.experiments.categories.peak.base import PeakBase -from easydiffraction.experiments.categories.peak.cwl_mixins import CwlBroadeningMixin -from easydiffraction.experiments.categories.peak.cwl_mixins import EmpiricalAsymmetryMixin -from easydiffraction.experiments.categories.peak.cwl_mixins import FcjAsymmetryMixin - - -class CwlPseudoVoigt( - PeakBase, - CwlBroadeningMixin, -): - """Constant-wavelength pseudo-Voigt peak shape.""" - - def __init__(self) -> None: - super().__init__() - self._add_constant_wavelength_broadening() - - -class CwlSplitPseudoVoigt( - PeakBase, - CwlBroadeningMixin, - EmpiricalAsymmetryMixin, -): - """Split pseudo-Voigt (empirical asymmetry) for CWL mode.""" - - def __init__(self) -> None: - super().__init__() - self._add_constant_wavelength_broadening() - self._add_empirical_asymmetry() - - -class CwlThompsonCoxHastings( - PeakBase, - CwlBroadeningMixin, - FcjAsymmetryMixin, -): - """Thompson–Cox–Hastings with FCJ asymmetry for CWL mode.""" - - def __init__(self) -> None: - super().__init__() - self._add_constant_wavelength_broadening() - self._add_fcj_asymmetry() diff --git a/src/easydiffraction/experiments/categories/peak/cwl_mixins.py b/src/easydiffraction/experiments/categories/peak/cwl_mixins.py deleted file mode 100644 index 47d48636..00000000 --- a/src/easydiffraction/experiments/categories/peak/cwl_mixins.py +++ /dev/null @@ -1,332 +0,0 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors -# SPDX-License-Identifier: BSD-3-Clause -"""Constant-wavelength (CWL) peak-profile mixins. - -This module provides mixins that add broadening and asymmetry parameters -for constant-wavelength powder diffraction peak profiles. They are -composed into concrete peak classes elsewhere. -""" - -from easydiffraction.core.parameters import Parameter -from easydiffraction.core.validation import AttributeSpec -from easydiffraction.core.validation import DataTypes -from easydiffraction.core.validation import RangeValidator -from easydiffraction.io.cif.handler import CifHandler - - -class CwlBroadeningMixin: - """Mixin that adds CWL Gaussian and Lorentz broadening - parameters. - """ - - # TODO: Rename to cwl. Check other mixins for naming consistency. - def _add_constant_wavelength_broadening(self) -> None: - """Create CWL broadening parameters and attach them to the - class. - - Defines Gaussian (U, V, W) and Lorentz (X, Y) terms - often used in the TCH formulation. Values are stored as - ``Parameter`` objects. - """ - self._broad_gauss_u: Parameter = Parameter( - name='broad_gauss_u', - description='Gaussian broadening coefficient (dependent on ' - 'sample size and instrument resolution)', - value_spec=AttributeSpec( - value=0.01, - type_=DataTypes.NUMERIC, - default=0.01, - content_validator=RangeValidator(), - ), - units='deg²', - cif_handler=CifHandler( - names=[ - '_peak.broad_gauss_u', - ] - ), - ) - self._broad_gauss_v: Parameter = Parameter( - name='broad_gauss_v', - description='Gaussian broadening coefficient (instrumental broadening contribution)', - value_spec=AttributeSpec( - value=-0.01, - type_=DataTypes.NUMERIC, - default=-0.01, - content_validator=RangeValidator(), - ), - units='deg²', - cif_handler=CifHandler( - names=[ - '_peak.broad_gauss_v', - ] - ), - ) - self._broad_gauss_w: Parameter = Parameter( - name='broad_gauss_w', - description='Gaussian broadening coefficient (instrumental broadening contribution)', - value_spec=AttributeSpec( - value=0.02, - type_=DataTypes.NUMERIC, - default=0.02, - content_validator=RangeValidator(), - ), - units='deg²', - cif_handler=CifHandler( - names=[ - '_peak.broad_gauss_w', - ] - ), - ) - self._broad_lorentz_x: Parameter = Parameter( - name='broad_lorentz_x', - description='Lorentzian broadening coefficient (dependent on sample strain effects)', - value_spec=AttributeSpec( - value=0.0, - type_=DataTypes.NUMERIC, - default=0.0, - content_validator=RangeValidator(), - ), - units='deg', - cif_handler=CifHandler( - names=[ - '_peak.broad_lorentz_x', - ] - ), - ) - self._broad_lorentz_y: Parameter = Parameter( - name='broad_lorentz_y', - description='Lorentzian broadening coefficient (dependent on ' - 'microstructural defects and strain)', - value_spec=AttributeSpec( - value=0.0, - type_=DataTypes.NUMERIC, - default=0.0, - content_validator=RangeValidator(), - ), - units='deg', - cif_handler=CifHandler( - names=[ - '_peak.broad_lorentz_y', - ] - ), - ) - - @property - def broad_gauss_u(self) -> Parameter: - """Get Gaussian U broadening parameter.""" - return self._broad_gauss_u - - @broad_gauss_u.setter - def broad_gauss_u(self, value: float) -> None: - """Set Gaussian U broadening parameter.""" - self._broad_gauss_u.value = value - - @property - def broad_gauss_v(self) -> Parameter: - """Get Gaussian V broadening parameter.""" - return self._broad_gauss_v - - @broad_gauss_v.setter - def broad_gauss_v(self, value: float) -> None: - """Set Gaussian V broadening parameter.""" - self._broad_gauss_v.value = value - - @property - def broad_gauss_w(self) -> Parameter: - """Get Gaussian W broadening parameter.""" - return self._broad_gauss_w - - @broad_gauss_w.setter - def broad_gauss_w(self, value: float) -> None: - """Set Gaussian W broadening parameter.""" - self._broad_gauss_w.value = value - - @property - def broad_lorentz_x(self) -> Parameter: - """Get Lorentz X broadening parameter.""" - return self._broad_lorentz_x - - @broad_lorentz_x.setter - def broad_lorentz_x(self, value: float) -> None: - """Set Lorentz X broadening parameter.""" - self._broad_lorentz_x.value = value - - @property - def broad_lorentz_y(self) -> Parameter: - """Get Lorentz Y broadening parameter.""" - return self._broad_lorentz_y - - @broad_lorentz_y.setter - def broad_lorentz_y(self, value: float) -> None: - """Set Lorentz Y broadening parameter.""" - self._broad_lorentz_y.value = value - - -class EmpiricalAsymmetryMixin: - """Mixin that adds empirical CWL peak asymmetry parameters.""" - - def _add_empirical_asymmetry(self) -> None: - """Create empirical asymmetry parameters p1..p4.""" - self._asym_empir_1: Parameter = Parameter( - name='asym_empir_1', - description='Empirical asymmetry coefficient p1', - value_spec=AttributeSpec( - value=0.1, - type_=DataTypes.NUMERIC, - default=0.1, - content_validator=RangeValidator(), - ), - units='', - cif_handler=CifHandler( - names=[ - '_peak.asym_empir_1', - ] - ), - ) - self._asym_empir_2: Parameter = Parameter( - name='asym_empir_2', - description='Empirical asymmetry coefficient p2', - value_spec=AttributeSpec( - value=0.2, - type_=DataTypes.NUMERIC, - default=0.2, - content_validator=RangeValidator(), - ), - units='', - cif_handler=CifHandler( - names=[ - '_peak.asym_empir_2', - ] - ), - ) - self._asym_empir_3: Parameter = Parameter( - name='asym_empir_3', - description='Empirical asymmetry coefficient p3', - value_spec=AttributeSpec( - value=0.3, - type_=DataTypes.NUMERIC, - default=0.3, - content_validator=RangeValidator(), - ), - units='', - cif_handler=CifHandler( - names=[ - '_peak.asym_empir_3', - ] - ), - ) - self._asym_empir_4: Parameter = Parameter( - name='asym_empir_4', - description='Empirical asymmetry coefficient p4', - value_spec=AttributeSpec( - value=0.4, - type_=DataTypes.NUMERIC, - default=0.4, - content_validator=RangeValidator(), - ), - units='', - cif_handler=CifHandler( - names=[ - '_peak.asym_empir_4', - ] - ), - ) - - @property - def asym_empir_1(self) -> Parameter: - """Get empirical asymmetry coefficient p1.""" - return self._asym_empir_1 - - @asym_empir_1.setter - def asym_empir_1(self, value: float) -> None: - """Set empirical asymmetry coefficient p1.""" - self._asym_empir_1.value = value - - @property - def asym_empir_2(self) -> Parameter: - """Get empirical asymmetry coefficient p2.""" - return self._asym_empir_2 - - @asym_empir_2.setter - def asym_empir_2(self, value: float) -> None: - """Set empirical asymmetry coefficient p2.""" - self._asym_empir_2.value = value - - @property - def asym_empir_3(self) -> Parameter: - """Get empirical asymmetry coefficient p3.""" - return self._asym_empir_3 - - @asym_empir_3.setter - def asym_empir_3(self, value: float) -> None: - """Set empirical asymmetry coefficient p3.""" - self._asym_empir_3.value = value - - @property - def asym_empir_4(self) -> Parameter: - """Get empirical asymmetry coefficient p4.""" - return self._asym_empir_4 - - @asym_empir_4.setter - def asym_empir_4(self, value: float) -> None: - """Set empirical asymmetry coefficient p4.""" - self._asym_empir_4.value = value - - -class FcjAsymmetryMixin: - """Mixin that adds Finger–Cox–Jephcoat (FCJ) asymmetry params.""" - - def _add_fcj_asymmetry(self) -> None: - """Create FCJ asymmetry parameters.""" - self._asym_fcj_1: Parameter = Parameter( - name='asym_fcj_1', - description='Finger-Cox-Jephcoat asymmetry parameter 1', - value_spec=AttributeSpec( - value=0.01, - type_=DataTypes.NUMERIC, - default=0.01, - content_validator=RangeValidator(), - ), - units='', - cif_handler=CifHandler( - names=[ - '_peak.asym_fcj_1', - ] - ), - ) - self._asym_fcj_2: Parameter = Parameter( - name='asym_fcj_2', - description='Finger-Cox-Jephcoat asymmetry parameter 2', - value_spec=AttributeSpec( - value=0.02, - type_=DataTypes.NUMERIC, - default=0.02, - content_validator=RangeValidator(), - ), - units='', - cif_handler=CifHandler( - names=[ - '_peak.asym_fcj_2', - ] - ), - ) - - @property - def asym_fcj_1(self) -> Parameter: - """Get FCJ asymmetry parameter 1.""" - return self._asym_fcj_1 - - @asym_fcj_1.setter - def asym_fcj_1(self, value: float) -> None: - """Set FCJ asymmetry parameter 1.""" - self._asym_fcj_1.value = value - - @property - def asym_fcj_2(self) -> Parameter: - """Get FCJ asymmetry parameter 2.""" - return self._asym_fcj_2 - - @asym_fcj_2.setter - def asym_fcj_2(self, value: float) -> None: - """Set FCJ asymmetry parameter 2.""" - self._asym_fcj_2.value = value diff --git a/src/easydiffraction/experiments/categories/peak/factory.py b/src/easydiffraction/experiments/categories/peak/factory.py deleted file mode 100644 index a0aabf10..00000000 --- a/src/easydiffraction/experiments/categories/peak/factory.py +++ /dev/null @@ -1,130 +0,0 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors -# SPDX-License-Identifier: BSD-3-Clause - -from typing import Optional - -from easydiffraction.experiments.experiment.enums import BeamModeEnum -from easydiffraction.experiments.experiment.enums import PeakProfileTypeEnum -from easydiffraction.experiments.experiment.enums import ScatteringTypeEnum - - -# TODO: Consider inheriting from FactoryBase -class PeakFactory: - """Factory for creating peak profile objects. - - Lazily imports implementations to avoid circular dependencies and - selects the appropriate class based on scattering type, beam mode - and requested profile type. - """ - - ST = ScatteringTypeEnum - BM = BeamModeEnum - PPT = PeakProfileTypeEnum - _supported = None # type: ignore[var-annotated] - - @classmethod - def _supported_map(cls): - """Return nested mapping of supported profile classes. - - Structure: - ``{ScatteringType: {BeamMode: {ProfileType: Class}}}``. - """ - # Lazy import to avoid circular imports between - # base and cw/tof/pdf modules - if cls._supported is None: - from easydiffraction.experiments.categories.peak.cwl import CwlPseudoVoigt as CwPv - from easydiffraction.experiments.categories.peak.cwl import ( - CwlSplitPseudoVoigt as CwSpv, - ) - from easydiffraction.experiments.categories.peak.cwl import ( - CwlThompsonCoxHastings as CwTch, - ) - from easydiffraction.experiments.categories.peak.tof import TofPseudoVoigt as TofPv - from easydiffraction.experiments.categories.peak.tof import ( - TofPseudoVoigtBackToBack as TofBtb, - ) - from easydiffraction.experiments.categories.peak.tof import ( - TofPseudoVoigtIkedaCarpenter as TofIc, - ) - from easydiffraction.experiments.categories.peak.total import ( - TotalGaussianDampedSinc as PdfGds, - ) - - cls._supported = { - cls.ST.BRAGG: { - cls.BM.CONSTANT_WAVELENGTH: { - cls.PPT.PSEUDO_VOIGT: CwPv, - cls.PPT.SPLIT_PSEUDO_VOIGT: CwSpv, - cls.PPT.THOMPSON_COX_HASTINGS: CwTch, - }, - cls.BM.TIME_OF_FLIGHT: { - cls.PPT.PSEUDO_VOIGT: TofPv, - cls.PPT.PSEUDO_VOIGT_IKEDA_CARPENTER: TofIc, - cls.PPT.PSEUDO_VOIGT_BACK_TO_BACK: TofBtb, - }, - }, - cls.ST.TOTAL: { - cls.BM.CONSTANT_WAVELENGTH: { - cls.PPT.GAUSSIAN_DAMPED_SINC: PdfGds, - }, - cls.BM.TIME_OF_FLIGHT: { - cls.PPT.GAUSSIAN_DAMPED_SINC: PdfGds, - }, - }, - } - return cls._supported - - @classmethod - def create( - cls, - scattering_type: Optional[ScatteringTypeEnum] = None, - beam_mode: Optional[BeamModeEnum] = None, - profile_type: Optional[PeakProfileTypeEnum] = None, - ): - """Instantiate a peak profile for the given configuration. - - Args: - scattering_type: Bragg or Total. Defaults to library - default. - beam_mode: CW or TOF. Defaults to library default. - profile_type: Concrete profile within the mode. If omitted, - a sensible default is chosen based on the other args. - - Returns: - A newly created peak profile object. - - Raises: - ValueError: If a requested option is not supported. - """ - if beam_mode is None: - beam_mode = BeamModeEnum.default() - if scattering_type is None: - scattering_type = ScatteringTypeEnum.default() - if profile_type is None: - profile_type = PeakProfileTypeEnum.default(scattering_type, beam_mode) - supported = cls._supported_map() - supported_scattering_types = list(supported.keys()) - if scattering_type not in supported_scattering_types: - raise ValueError( - f"Unsupported scattering type: '{scattering_type}'.\n" - f'Supported scattering types: {supported_scattering_types}' - ) - - supported_beam_modes = list(supported[scattering_type].keys()) - if beam_mode not in supported_beam_modes: - raise ValueError( - f"Unsupported beam mode: '{beam_mode}' for scattering type: " - f"'{scattering_type}'.\n Supported beam modes: '{supported_beam_modes}'" - ) - - supported_profile_types = list(supported[scattering_type][beam_mode].keys()) - if profile_type not in supported_profile_types: - raise ValueError( - f"Unsupported profile type '{profile_type}' for beam mode '{beam_mode}'.\n" - f'Supported profile types: {supported_profile_types}' - ) - - peak_class = supported[scattering_type][beam_mode][profile_type] - peak_obj = peak_class() - - return peak_obj diff --git a/src/easydiffraction/experiments/categories/peak/tof.py b/src/easydiffraction/experiments/categories/peak/tof.py deleted file mode 100644 index b38c4548..00000000 --- a/src/easydiffraction/experiments/categories/peak/tof.py +++ /dev/null @@ -1,44 +0,0 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors -# SPDX-License-Identifier: BSD-3-Clause -"""Time-of-flight peak profile classes.""" - -from easydiffraction.experiments.categories.peak.base import PeakBase -from easydiffraction.experiments.categories.peak.tof_mixins import IkedaCarpenterAsymmetryMixin -from easydiffraction.experiments.categories.peak.tof_mixins import TofBroadeningMixin - - -class TofPseudoVoigt( - PeakBase, - TofBroadeningMixin, -): - """Time-of-flight pseudo-Voigt peak shape.""" - - def __init__(self) -> None: - super().__init__() - self._add_time_of_flight_broadening() - - -class TofPseudoVoigtIkedaCarpenter( - PeakBase, - TofBroadeningMixin, - IkedaCarpenterAsymmetryMixin, -): - """TOF pseudo-Voigt with Ikeda–Carpenter asymmetry.""" - - def __init__(self) -> None: - super().__init__() - self._add_time_of_flight_broadening() - self._add_ikeda_carpenter_asymmetry() - - -class TofPseudoVoigtBackToBack( - PeakBase, - TofBroadeningMixin, - IkedaCarpenterAsymmetryMixin, -): - """TOF back-to-back pseudo-Voigt with asymmetry.""" - - def __init__(self) -> None: - super().__init__() - self._add_time_of_flight_broadening() - self._add_ikeda_carpenter_asymmetry() diff --git a/src/easydiffraction/experiments/categories/peak/tof_mixins.py b/src/easydiffraction/experiments/categories/peak/tof_mixins.py deleted file mode 100644 index ca459754..00000000 --- a/src/easydiffraction/experiments/categories/peak/tof_mixins.py +++ /dev/null @@ -1,293 +0,0 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors -# SPDX-License-Identifier: BSD-3-Clause -"""Time-of-flight (TOF) peak-profile mixins. - -Defines mixins that add Gaussian/Lorentz broadening, mixing, and -Ikeda–Carpenter asymmetry parameters used by TOF peak shapes. -""" - -from easydiffraction.core.parameters import Parameter -from easydiffraction.core.validation import AttributeSpec -from easydiffraction.core.validation import DataTypes -from easydiffraction.core.validation import RangeValidator -from easydiffraction.io.cif.handler import CifHandler - - -class TofBroadeningMixin: - """Mixin that adds TOF Gaussian/Lorentz broadening and mixing - terms. - """ - - def _add_time_of_flight_broadening(self) -> None: - """Create TOF broadening and mixing parameters.""" - self._broad_gauss_sigma_0: Parameter = Parameter( - name='gauss_sigma_0', - description='Gaussian broadening coefficient (instrumental resolution)', - value_spec=AttributeSpec( - value=0.0, - type_=DataTypes.NUMERIC, - default=0.0, - content_validator=RangeValidator(), - ), - units='µs²', - cif_handler=CifHandler( - names=[ - '_peak.gauss_sigma_0', - ] - ), - ) - self._broad_gauss_sigma_1: Parameter = Parameter( - name='gauss_sigma_1', - description='Gaussian broadening coefficient (dependent on d-spacing)', - value_spec=AttributeSpec( - value=0.0, - type_=DataTypes.NUMERIC, - default=0.0, - content_validator=RangeValidator(), - ), - units='µs/Å', - cif_handler=CifHandler( - names=[ - '_peak.gauss_sigma_1', - ] - ), - ) - self._broad_gauss_sigma_2: Parameter = Parameter( - name='gauss_sigma_2', - description='Gaussian broadening coefficient (instrument-dependent term)', - value_spec=AttributeSpec( - value=0.0, - type_=DataTypes.NUMERIC, - default=0.0, - content_validator=RangeValidator(), - ), - units='µs²/Ų', - cif_handler=CifHandler( - names=[ - '_peak.gauss_sigma_2', - ] - ), - ) - self._broad_lorentz_gamma_0: Parameter = Parameter( - name='lorentz_gamma_0', - description='Lorentzian broadening coefficient (dependent on microstrain effects)', - value_spec=AttributeSpec( - value=0.0, - type_=DataTypes.NUMERIC, - default=0.0, - content_validator=RangeValidator(), - ), - units='µs', - cif_handler=CifHandler( - names=[ - '_peak.lorentz_gamma_0', - ] - ), - ) - self._broad_lorentz_gamma_1: Parameter = Parameter( - name='lorentz_gamma_1', - description='Lorentzian broadening coefficient (dependent on d-spacing)', - value_spec=AttributeSpec( - value=0.0, - type_=DataTypes.NUMERIC, - default=0.0, - content_validator=RangeValidator(), - ), - units='µs/Å', - cif_handler=CifHandler( - names=[ - '_peak.lorentz_gamma_1', - ] - ), - ) - self._broad_lorentz_gamma_2: Parameter = Parameter( - name='lorentz_gamma_2', - description='Lorentzian broadening coefficient (instrument-dependent term)', - value_spec=AttributeSpec( - value=0.0, - type_=DataTypes.NUMERIC, - default=0.0, - content_validator=RangeValidator(), - ), - units='µs²/Ų', - cif_handler=CifHandler( - names=[ - '_peak.lorentz_gamma_2', - ] - ), - ) - self._broad_mix_beta_0: Parameter = Parameter( - name='mix_beta_0', - description='Mixing parameter. Defines the ratio of Gaussian ' - 'to Lorentzian contributions in TOF profiles', - value_spec=AttributeSpec( - value=0.0, - type_=DataTypes.NUMERIC, - default=0.0, - content_validator=RangeValidator(), - ), - units='deg', - cif_handler=CifHandler( - names=[ - '_peak.mix_beta_0', - ] - ), - ) - self._broad_mix_beta_1: Parameter = Parameter( - name='mix_beta_1', - description='Mixing parameter. Defines the ratio of Gaussian ' - 'to Lorentzian contributions in TOF profiles', - value_spec=AttributeSpec( - value=0.0, - type_=DataTypes.NUMERIC, - default=0.0, - content_validator=RangeValidator(), - ), - units='deg', - cif_handler=CifHandler( - names=[ - '_peak.mix_beta_1', - ] - ), - ) - - @property - def broad_gauss_sigma_0(self) -> Parameter: - """Get Gaussian sigma_0 parameter.""" - return self._broad_gauss_sigma_0 - - @broad_gauss_sigma_0.setter - def broad_gauss_sigma_0(self, value: float) -> None: - """Set Gaussian sigma_0 parameter.""" - self._broad_gauss_sigma_0.value = value - - @property - def broad_gauss_sigma_1(self) -> Parameter: - """Get Gaussian sigma_1 parameter.""" - return self._broad_gauss_sigma_1 - - @broad_gauss_sigma_1.setter - def broad_gauss_sigma_1(self, value: float) -> None: - """Set Gaussian sigma_1 parameter.""" - self._broad_gauss_sigma_1.value = value - - @property - def broad_gauss_sigma_2(self) -> Parameter: - """Get Gaussian sigma_2 parameter.""" - return self._broad_gauss_sigma_2 - - @broad_gauss_sigma_2.setter - def broad_gauss_sigma_2(self, value: float) -> None: - """Set Gaussian sigma_2 parameter.""" - self._broad_gauss_sigma_2.value = value - - @property - def broad_lorentz_gamma_0(self) -> Parameter: - """Get Lorentz gamma_0 parameter.""" - return self._broad_lorentz_gamma_0 - - @broad_lorentz_gamma_0.setter - def broad_lorentz_gamma_0(self, value: float) -> None: - """Set Lorentz gamma_0 parameter.""" - self._broad_lorentz_gamma_0.value = value - - @property - def broad_lorentz_gamma_1(self) -> Parameter: - """Get Lorentz gamma_1 parameter.""" - return self._broad_lorentz_gamma_1 - - @broad_lorentz_gamma_1.setter - def broad_lorentz_gamma_1(self, value: float) -> None: - """Set Lorentz gamma_1 parameter.""" - self._broad_lorentz_gamma_1.value = value - - @property - def broad_lorentz_gamma_2(self) -> Parameter: - """Get Lorentz gamma_2 parameter.""" - return self._broad_lorentz_gamma_2 - - @broad_lorentz_gamma_2.setter - def broad_lorentz_gamma_2(self, value: float) -> None: - """Set Lorentz gamma_2 parameter.""" - self._broad_lorentz_gamma_2.value = value - - @property - def broad_mix_beta_0(self) -> Parameter: - """Get mixing parameter beta_0.""" - return self._broad_mix_beta_0 - - @broad_mix_beta_0.setter - def broad_mix_beta_0(self, value: float) -> None: - """Set mixing parameter beta_0.""" - self._broad_mix_beta_0.value = value - - @property - def broad_mix_beta_1(self) -> Parameter: - """Get mixing parameter beta_1.""" - return self._broad_mix_beta_1 - - @broad_mix_beta_1.setter - def broad_mix_beta_1(self, value: float) -> None: - """Set mixing parameter beta_1.""" - self._broad_mix_beta_1.value = value - - -class IkedaCarpenterAsymmetryMixin: - """Mixin that adds Ikeda–Carpenter asymmetry parameters.""" - - def _add_ikeda_carpenter_asymmetry(self) -> None: - """Create Ikeda–Carpenter asymmetry parameters alpha_0 and - alpha_1. - """ - self._asym_alpha_0: Parameter = Parameter( - name='asym_alpha_0', - description='Ikeda-Carpenter asymmetry parameter α₀', - value_spec=AttributeSpec( - value=0.01, - type_=DataTypes.NUMERIC, - default=0.01, - content_validator=RangeValidator(), - ), - units='', - cif_handler=CifHandler( - names=[ - '_peak.asym_alpha_0', - ] - ), - ) - self._asym_alpha_1: Parameter = Parameter( - name='asym_alpha_1', - description='Ikeda-Carpenter asymmetry parameter α₁', - value_spec=AttributeSpec( - value=0.02, - type_=DataTypes.NUMERIC, - default=0.02, - content_validator=RangeValidator(), - ), - units='', - cif_handler=CifHandler( - names=[ - '_peak.asym_alpha_1', - ] - ), - ) - - @property - def asym_alpha_0(self) -> Parameter: - """Get Ikeda–Carpenter asymmetry alpha_0.""" - return self._asym_alpha_0 - - @asym_alpha_0.setter - def asym_alpha_0(self, value: float) -> None: - """Set Ikeda–Carpenter asymmetry alpha_0.""" - self._asym_alpha_0.value = value - - @property - def asym_alpha_1(self) -> Parameter: - """Get Ikeda–Carpenter asymmetry alpha_1.""" - return self._asym_alpha_1 - - @asym_alpha_1.setter - def asym_alpha_1(self, value: float) -> None: - """Set Ikeda–Carpenter asymmetry alpha_1.""" - self._asym_alpha_1.value = value diff --git a/src/easydiffraction/experiments/categories/peak/total.py b/src/easydiffraction/experiments/categories/peak/total.py deleted file mode 100644 index e1f7c28c..00000000 --- a/src/easydiffraction/experiments/categories/peak/total.py +++ /dev/null @@ -1,17 +0,0 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors -# SPDX-License-Identifier: BSD-3-Clause -"""Total-scattering (PDF) peak profile classes.""" - -from easydiffraction.experiments.categories.peak.base import PeakBase -from easydiffraction.experiments.categories.peak.total_mixins import TotalBroadeningMixin - - -class TotalGaussianDampedSinc( - PeakBase, - TotalBroadeningMixin, -): - """Gaussian-damped sinc peak for total scattering (PDF).""" - - def __init__(self) -> None: - super().__init__() - self._add_pair_distribution_function_broadening() diff --git a/src/easydiffraction/experiments/categories/peak/total_mixins.py b/src/easydiffraction/experiments/categories/peak/total_mixins.py deleted file mode 100644 index 03907ffa..00000000 --- a/src/easydiffraction/experiments/categories/peak/total_mixins.py +++ /dev/null @@ -1,181 +0,0 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors -# SPDX-License-Identifier: BSD-3-Clause -"""Total scattering/PDF peak-profile mixins. - -Adds damping, broadening, sharpening and envelope parameters used in -pair distribution function (PDF) modeling. -""" - -from easydiffraction.core.parameters import Parameter -from easydiffraction.core.validation import AttributeSpec -from easydiffraction.core.validation import DataTypes -from easydiffraction.core.validation import RangeValidator -from easydiffraction.io.cif.handler import CifHandler - - -class TotalBroadeningMixin: - """Mixin adding PDF broadening/damping/sharpening parameters.""" - - def _add_pair_distribution_function_broadening(self): - """Create PDF parameters: damp_q, broad_q, cutoff_q, - sharp deltas, and particle diameter envelope. - """ - self._damp_q: Parameter = Parameter( - name='damp_q', - description='Instrumental Q-resolution damping factor ' - '(affects high-r PDF peak amplitude)', - value_spec=AttributeSpec( - value=0.05, - type_=DataTypes.NUMERIC, - default=0.05, - content_validator=RangeValidator(), - ), - units='Å⁻¹', - cif_handler=CifHandler( - names=[ - '_peak.damp_q', - ] - ), - ) - self._broad_q: Parameter = Parameter( - name='broad_q', - description='Quadratic PDF peak broadening coefficient ' - '(thermal and model uncertainty contribution)', - value_spec=AttributeSpec( - value=0.0, - type_=DataTypes.NUMERIC, - default=0.0, - content_validator=RangeValidator(), - ), - units='Å⁻²', - cif_handler=CifHandler( - names=[ - '_peak.broad_q', - ] - ), - ) - self._cutoff_q: Parameter = Parameter( - name='cutoff_q', - description='Q-value cutoff applied to model PDF for Fourier ' - 'transform (controls real-space resolution)', - value_spec=AttributeSpec( - value=25.0, - type_=DataTypes.NUMERIC, - default=25.0, - content_validator=RangeValidator(), - ), - units='Å⁻¹', - cif_handler=CifHandler( - names=[ - '_peak.cutoff_q', - ] - ), - ) - self._sharp_delta_1: Parameter = Parameter( - name='sharp_delta_1', - description='PDF peak sharpening coefficient (1/r dependence)', - value_spec=AttributeSpec( - value=0.0, - type_=DataTypes.NUMERIC, - default=0.0, - content_validator=RangeValidator(), - ), - units='Å', - cif_handler=CifHandler( - names=[ - '_peak.sharp_delta_1', - ] - ), - ) - self._sharp_delta_2: Parameter = Parameter( - name='sharp_delta_2', - description='PDF peak sharpening coefficient (1/r² dependence)', - value_spec=AttributeSpec( - value=0.0, - type_=DataTypes.NUMERIC, - default=0.0, - content_validator=RangeValidator(), - ), - units='Ų', - cif_handler=CifHandler( - names=[ - '_peak.sharp_delta_2', - ] - ), - ) - self._damp_particle_diameter: Parameter = Parameter( - name='damp_particle_diameter', - description='Particle diameter for spherical envelope damping correction in PDF', - value_spec=AttributeSpec( - value=0.0, - type_=DataTypes.NUMERIC, - default=0.0, - content_validator=RangeValidator(), - ), - units='Å', - cif_handler=CifHandler( - names=[ - '_peak.damp_particle_diameter', - ] - ), - ) - - @property - def damp_q(self) -> Parameter: - """Get Q-resolution damping factor.""" - return self._damp_q - - @damp_q.setter - def damp_q(self, value: float) -> None: - """Set Q-resolution damping factor.""" - self._damp_q.value = value - - @property - def broad_q(self) -> Parameter: - """Get quadratic PDF broadening coefficient.""" - return self._broad_q - - @broad_q.setter - def broad_q(self, value: float) -> None: - """Set quadratic PDF broadening coefficient.""" - self._broad_q.value = value - - @property - def cutoff_q(self) -> Parameter: - """Get Q cutoff used for Fourier transform.""" - return self._cutoff_q - - @cutoff_q.setter - def cutoff_q(self, value: float) -> None: - """Set Q cutoff used for Fourier transform.""" - self._cutoff_q.value = value - - @property - def sharp_delta_1(self) -> Parameter: - """Get sharpening coefficient with 1/r dependence.""" - return self._sharp_delta_1 - - @sharp_delta_1.setter - def sharp_delta_1(self, value: float) -> None: - """Set sharpening coefficient with 1/r dependence.""" - self._sharp_delta_1.value = value - - @property - def sharp_delta_2(self) -> Parameter: - """Get sharpening coefficient with 1/r^2 dependence.""" - return self._sharp_delta_2 - - @sharp_delta_2.setter - def sharp_delta_2(self, value: float) -> None: - """Set sharpening coefficient with 1/r^2 dependence.""" - self._sharp_delta_2.value = value - - @property - def damp_particle_diameter(self) -> Parameter: - """Get particle diameter for spherical envelope damping.""" - return self._damp_particle_diameter - - @damp_particle_diameter.setter - def damp_particle_diameter(self, value: float) -> None: - """Set particle diameter for spherical envelope damping.""" - self._damp_particle_diameter.value = value diff --git a/src/easydiffraction/experiments/experiment/__init__.py b/src/easydiffraction/experiments/experiment/__init__.py deleted file mode 100644 index a2a39eea..00000000 --- a/src/easydiffraction/experiments/experiment/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors -# SPDX-License-Identifier: BSD-3-Clause - -from easydiffraction.experiments.experiment.base import ExperimentBase -from easydiffraction.experiments.experiment.base import PdExperimentBase -from easydiffraction.experiments.experiment.bragg_pd import BraggPdExperiment -from easydiffraction.experiments.experiment.bragg_sc import BraggScExperiment -from easydiffraction.experiments.experiment.total_pd import TotalPdExperiment - -__all__ = [ - 'ExperimentBase', - 'PdExperimentBase', - 'BraggPdExperiment', - 'TotalPdExperiment', - 'BraggScExperiment', -] diff --git a/src/easydiffraction/experiments/experiment/base.py b/src/easydiffraction/experiments/experiment/base.py deleted file mode 100644 index 35ed7b1d..00000000 --- a/src/easydiffraction/experiments/experiment/base.py +++ /dev/null @@ -1,264 +0,0 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors -# SPDX-License-Identifier: BSD-3-Clause - -from __future__ import annotations - -from abc import abstractmethod -from typing import TYPE_CHECKING -from typing import Any -from typing import List - -from easydiffraction.core.datablock import DatablockItem -from easydiffraction.experiments.categories.data.factory import DataFactory -from easydiffraction.experiments.categories.excluded_regions import ExcludedRegions -from easydiffraction.experiments.categories.linked_phases import LinkedPhases -from easydiffraction.experiments.categories.peak.factory import PeakFactory -from easydiffraction.experiments.categories.peak.factory import PeakProfileTypeEnum -from easydiffraction.io.cif.serialize import experiment_to_cif -from easydiffraction.utils.logging import console -from easydiffraction.utils.logging import log -from easydiffraction.utils.utils import render_cif -from easydiffraction.utils.utils import render_table - -if TYPE_CHECKING: - from easydiffraction.experiments.categories.experiment_type import ExperimentType - from easydiffraction.sample_models.sample_models import SampleModels - - -class ExperimentBase(DatablockItem): - """Base class for all experiments with only core attributes. - - Wraps experiment type and instrument. - """ - - def __init__( - self, - *, - name: str, - type: ExperimentType, - ): - super().__init__() - self._name = name - self._type = type - # TODO: Should return default calculator based on experiment - # type - from easydiffraction.analysis.calculators.factory import CalculatorFactory - - self._calculator = CalculatorFactory.create_calculator('cryspy') - self._identity.datablock_entry_name = lambda: self.name - - @property - def name(self) -> str: - """Human-readable name of the experiment.""" - return self._name - - @name.setter - def name(self, new: str) -> None: - """Rename the experiment. - - Args: - new: New name for this experiment. - """ - self._name = new - - @property - def type(self): # TODO: Consider another name - """Experiment type descriptor (sample form, probe, beam - mode). - """ - return self._type - - @property - def calculator(self): - """Calculator engine used for pattern calculations.""" - return self._calculator - - @property - def as_cif(self) -> str: - """Serialize this experiment to a CIF fragment.""" - return experiment_to_cif(self) - - def show_as_cif(self) -> None: - """Pretty-print the experiment as CIF text.""" - experiment_cif = super().as_cif - paragraph_title: str = f"Experiment 🔬 '{self.name}' as cif" - console.paragraph(paragraph_title) - render_cif(experiment_cif) - - @abstractmethod - def _load_ascii_data_to_experiment(self, data_path: str) -> None: - """Load ASCII data from file into the experiment data category. - - Args: - data_path: Path to the ASCII file to load. - """ - raise NotImplementedError() - - -class PdExperimentBase(ExperimentBase): - """Base class for all powder experiments.""" - - def __init__( - self, - *, - name: str, - type: ExperimentType, - ) -> None: - super().__init__(name=name, type=type) - - self._linked_phases: LinkedPhases = LinkedPhases() - self._excluded_regions: ExcludedRegions = ExcludedRegions() - - self._peak_profile_type: PeakProfileTypeEnum = PeakProfileTypeEnum.default( - self.type.scattering_type.value, - self.type.beam_mode.value, - ) - self._peak = PeakFactory.create( - scattering_type=self.type.scattering_type.value, - beam_mode=self.type.beam_mode.value, - profile_type=self._peak_profile_type, - ) - - self._data = DataFactory.create( - sample_form=self.type.sample_form.value, - beam_mode=self.type.beam_mode.value, - scattering_type=self.type.scattering_type.value, - ) - - def _get_valid_linked_phases( - self, - sample_models: SampleModels, - ) -> List[Any]: - """Get valid linked phases for this experiment. - - Args: - sample_models: Collection of sample models. - - Returns: - A list of valid linked phases. - """ - if not self.linked_phases: - print('Warning: No linked phases defined. Returning empty pattern.') - return [] - - valid_linked_phases = [] - for linked_phase in self.linked_phases: - if linked_phase._identity.category_entry_name not in sample_models.names: - print( - f"Warning: Linked phase '{linked_phase.id.value}' not " - f'found in Sample Models {sample_models.names}. Skipping it.' - ) - continue - valid_linked_phases.append(linked_phase) - - if not valid_linked_phases: - print( - 'Warning: None of the linked phases found in Sample ' - 'Models. Returning empty pattern.' - ) - - return valid_linked_phases - - @abstractmethod - def _load_ascii_data_to_experiment(self, data_path: str) -> None: - """Load powder diffraction data from an ASCII file. - - Args: - data_path: Path to data file with columns compatible with - the beam mode (e.g. 2θ/I/σ for CWL, TOF/I/σ for TOF). - """ - pass - - @property - def linked_phases(self): - """Collection of phases linked to this experiment.""" - return self._linked_phases - - @property - def excluded_regions(self): - """Collection of excluded regions for the x-grid.""" - return self._excluded_regions - - @property - def peak(self) -> str: - """Peak category object with profile parameters and mixins.""" - return self._peak - - @peak.setter - def peak(self, value): - """Replace the peak model used for this powder experiment. - - Args: - value: New peak object created by the `PeakFactory`. - """ - self._peak = value - - @property - def data(self): - return self._data - - @property - def peak_profile_type(self): - """Currently selected peak profile type enum.""" - return self._peak_profile_type - - @peak_profile_type.setter - def peak_profile_type(self, new_type: str | PeakProfileTypeEnum): - """Change the active peak profile type, if supported. - - Args: - new_type: New profile type as enum or its string value. - """ - if isinstance(new_type, str): - try: - new_type = PeakProfileTypeEnum(new_type) - except ValueError: - log.warning(f"Unknown peak profile type '{new_type}'") - return - - supported_types = list( - PeakFactory._supported[self.type.scattering_type.value][ - self.type.beam_mode.value - ].keys() - ) - - if new_type not in supported_types: - log.warning( - f"Unsupported peak profile '{new_type.value}', " - f'Supported peak profiles: {supported_types}', - "For more information, use 'show_supported_peak_profile_types()'", - ) - return - - self._peak = PeakFactory.create( - scattering_type=self.type.scattering_type.value, - beam_mode=self.type.beam_mode.value, - profile_type=new_type, - ) - self._peak_profile_type = new_type - console.paragraph(f"Peak profile type for experiment '{self.name}' changed to") - console.print(new_type.value) - - def show_supported_peak_profile_types(self): - """Print available peak profile types for this experiment.""" - columns_headers = ['Peak profile type', 'Description'] - columns_alignment = ['left', 'left'] - columns_data = [] - - scattering_type = self.type.scattering_type.value - beam_mode = self.type.beam_mode.value - - for profile_type in PeakFactory._supported[scattering_type][beam_mode]: - columns_data.append([profile_type.value, profile_type.description()]) - - console.paragraph('Supported peak profile types') - render_table( - columns_headers=columns_headers, - columns_alignment=columns_alignment, - columns_data=columns_data, - ) - - def show_current_peak_profile_type(self): - """Print the currently selected peak profile type.""" - console.paragraph('Current peak profile type') - console.print(self.peak_profile_type) diff --git a/src/easydiffraction/experiments/experiment/bragg_pd.py b/src/easydiffraction/experiments/experiment/bragg_pd.py deleted file mode 100644 index 3821419a..00000000 --- a/src/easydiffraction/experiments/experiment/bragg_pd.py +++ /dev/null @@ -1,134 +0,0 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors -# SPDX-License-Identifier: BSD-3-Clause - -from __future__ import annotations - -from typing import TYPE_CHECKING - -import numpy as np - -from easydiffraction.experiments.categories.background.enums import BackgroundTypeEnum -from easydiffraction.experiments.categories.background.factory import BackgroundFactory -from easydiffraction.experiments.experiment.base import PdExperimentBase -from easydiffraction.experiments.experiment.instrument_mixin import InstrumentMixin -from easydiffraction.utils.logging import console -from easydiffraction.utils.logging import log -from easydiffraction.utils.utils import render_table - -if TYPE_CHECKING: - from easydiffraction.experiments.categories.experiment_type import ExperimentType - - -class BraggPdExperiment(InstrumentMixin, PdExperimentBase): - """Powder diffraction experiment. - - Wraps background model, peak profile and linked phases for Bragg PD. - """ - - def __init__( - self, - *, - name: str, - type: ExperimentType, - ) -> None: - super().__init__(name=name, type=type) - - self._background_type: BackgroundTypeEnum = BackgroundTypeEnum.default() - self._background = BackgroundFactory.create(background_type=self.background_type) - - @property - def background(self): - return self._background - - @background.setter - def background(self, value): - self._background = value - - # ------------- - # Measured data - # ------------- - - def _load_ascii_data_to_experiment(self, data_path: str) -> None: - """Load (x, y, sy) data from an ASCII file into the data - category. - - The file format is space/column separated with 2 or 3 columns: - ``x y [sy]``. If ``sy`` is missing, it is approximated as - ``sqrt(y)`` with small values clamped to ``1.0``. - """ - try: - data = np.loadtxt(data_path) - except Exception as e: - raise IOError(f'Failed to read data from {data_path}: {e}') from e - - if data.shape[1] < 2: - raise ValueError('Data file must have at least two columns: x and y.') - - if data.shape[1] < 3: - print('Warning: No uncertainty (sy) column provided. Defaulting to sqrt(y).') - - # Extract x, y data - x: np.ndarray = data[:, 0] - y: np.ndarray = data[:, 1] - - # Round x to 4 decimal places - x = np.round(x, 4) - - # Determine sy from column 3 if available, otherwise use sqrt(y) - sy: np.ndarray = data[:, 2] if data.shape[1] > 2 else np.sqrt(y) - - # Replace values smaller than 0.0001 with 1.0 - sy = np.where(sy < 0.0001, 1.0, sy) - - # Set the experiment data - self.data._set_x(x) - self.data._set_meas(y) - self.data._set_meas_su(sy) - - console.paragraph('Data loaded successfully') - console.print(f"Experiment 🔬 '{self.name}'. Number of data points: {len(x)}") - - @property - def background_type(self): - """Current background type enum value.""" - return self._background_type - - @background_type.setter - def background_type(self, new_type): - """Set and apply a new background type. - - Falls back to printing supported types if the new value is not - supported. - """ - if new_type not in BackgroundFactory._supported_map(): - supported_types = list(BackgroundFactory._supported_map().keys()) - log.warning( - f"Unknown background type '{new_type}'. " - f'Supported background types: {[bt.value for bt in supported_types]}. ' - f"For more information, use 'show_supported_background_types()'" - ) - return - self.background = BackgroundFactory.create(new_type) - self._background_type = new_type - console.paragraph(f"Background type for experiment '{self.name}' changed to") - console.print(new_type) - - def show_supported_background_types(self): - """Print a table of supported background types.""" - columns_headers = ['Background type', 'Description'] - columns_alignment = ['left', 'left'] - columns_data = [] - for bt in BackgroundFactory._supported_map(): - columns_data.append([bt.value, bt.description()]) - - console.paragraph('Supported background types') - render_table( - columns_headers=columns_headers, - columns_alignment=columns_alignment, - columns_data=columns_data, - ) - - def show_current_background_type(self): - """Print the currently used background type.""" - console.paragraph('Current background type') - console.print(self.background_type) diff --git a/src/easydiffraction/experiments/experiment/bragg_sc.py b/src/easydiffraction/experiments/experiment/bragg_sc.py deleted file mode 100644 index 6b95d559..00000000 --- a/src/easydiffraction/experiments/experiment/bragg_sc.py +++ /dev/null @@ -1,28 +0,0 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors -# SPDX-License-Identifier: BSD-3-Clause -"""Single crystal experiment types and helpers.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -from easydiffraction.experiments.experiment.base import ExperimentBase - -if TYPE_CHECKING: - from easydiffraction.experiments.categories.experiment_type import ExperimentType - - -class BraggScExperiment(ExperimentBase): - """Single crystal experiment class with specific attributes.""" - - def __init__( - self, - *, - name: str, - type: ExperimentType, - ) -> None: - super().__init__(name=name, type=type) - self.linked_crystal = None - - def show_meas_chart(self) -> None: - print('Showing measured data chart is not implemented yet.') diff --git a/src/easydiffraction/experiments/experiment/factory.py b/src/easydiffraction/experiments/experiment/factory.py deleted file mode 100644 index 0db5e347..00000000 --- a/src/easydiffraction/experiments/experiment/factory.py +++ /dev/null @@ -1,181 +0,0 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors -# SPDX-License-Identifier: BSD-3-Clause - -from __future__ import annotations - -from typing import TYPE_CHECKING - -from easydiffraction.core.factory import FactoryBase -from easydiffraction.experiments.categories.experiment_type import ExperimentType -from easydiffraction.experiments.experiment import BraggPdExperiment -from easydiffraction.experiments.experiment import BraggScExperiment -from easydiffraction.experiments.experiment import TotalPdExperiment -from easydiffraction.experiments.experiment.enums import BeamModeEnum -from easydiffraction.experiments.experiment.enums import RadiationProbeEnum -from easydiffraction.experiments.experiment.enums import SampleFormEnum -from easydiffraction.experiments.experiment.enums import ScatteringTypeEnum -from easydiffraction.io.cif.parse import document_from_path -from easydiffraction.io.cif.parse import document_from_string -from easydiffraction.io.cif.parse import name_from_block -from easydiffraction.io.cif.parse import pick_sole_block - -if TYPE_CHECKING: - import gemmi - - from easydiffraction.experiments.experiment.base import ExperimentBase - - -class ExperimentFactory(FactoryBase): - """Creates Experiment instances with only relevant attributes.""" - - _ALLOWED_ARG_SPECS = [ - {'required': ['cif_path'], 'optional': []}, - {'required': ['cif_str'], 'optional': []}, - { - 'required': ['name', 'data_path'], - 'optional': ['sample_form', 'beam_mode', 'radiation_probe', 'scattering_type'], - }, - { - 'required': ['name'], - 'optional': ['sample_form', 'beam_mode', 'radiation_probe', 'scattering_type'], - }, - ] - - _SUPPORTED = { - ScatteringTypeEnum.BRAGG: { - SampleFormEnum.POWDER: BraggPdExperiment, - SampleFormEnum.SINGLE_CRYSTAL: BraggScExperiment, - }, - ScatteringTypeEnum.TOTAL: { - SampleFormEnum.POWDER: TotalPdExperiment, - }, - } - - @classmethod - def _make_experiment_type(cls, kwargs): - """Helper to construct an ExperimentType from keyword arguments, - using defaults as needed. - """ - # TODO: Defaults are already in the experiment type... - # TODO: Merging with experiment_type_from_block from - # io.cif.parse - return ExperimentType( - sample_form=kwargs.get('sample_form', SampleFormEnum.default().value), - beam_mode=kwargs.get('beam_mode', BeamModeEnum.default().value), - radiation_probe=kwargs.get('radiation_probe', RadiationProbeEnum.default().value), - scattering_type=kwargs.get('scattering_type', ScatteringTypeEnum.default().value), - ) - - # TODO: Move to a common CIF utility module? io.cif.parse? - @classmethod - def _create_from_gemmi_block( - cls, - block: gemmi.cif.Block, - ) -> ExperimentBase: - """Build a model instance from a single CIF block.""" - name = name_from_block(block) - - # TODO: move to io.cif.parse? - expt_type = ExperimentType() - for param in expt_type.parameters: - param.from_cif(block) - - # Create experiment instance of appropriate class - # TODO: make helper method to create experiment from type - scattering_type = expt_type.scattering_type.value - sample_form = expt_type.sample_form.value - expt_class = cls._SUPPORTED[scattering_type][sample_form] - expt_obj = expt_class(name=name, type=expt_type) - - # Read all categories from CIF block - # TODO: move to io.cif.parse? - for category in expt_obj.categories: - category.from_cif(block) - - return expt_obj - - @classmethod - def _create_from_cif_path( - cls, - cif_path: str, - ) -> ExperimentBase: - """Create an experiment from a CIF file path.""" - doc = document_from_path(cif_path) - block = pick_sole_block(doc) - return cls._create_from_gemmi_block(block) - - @classmethod - def _create_from_cif_str( - cls, - cif_str: str, - ) -> ExperimentBase: - """Create an experiment from a CIF string.""" - doc = document_from_string(cif_str) - block = pick_sole_block(doc) - return cls._create_from_gemmi_block(block) - - @classmethod - def _create_from_data_path(cls, kwargs): - """Create an experiment from a raw data ASCII file. - - Loads the experiment and attaches measured data from the - specified file. - """ - expt_type = cls._make_experiment_type(kwargs) - scattering_type = expt_type.scattering_type.value - sample_form = expt_type.sample_form.value - expt_class = cls._SUPPORTED[scattering_type][sample_form] - expt_name = kwargs['name'] - expt_obj = expt_class(name=expt_name, type=expt_type) - data_path = kwargs['data_path'] - expt_obj._load_ascii_data_to_experiment(data_path) - return expt_obj - - @classmethod - def _create_without_data(cls, kwargs): - """Create an experiment without measured data. - - Returns an experiment instance with only metadata and - configuration. - """ - expt_type = cls._make_experiment_type(kwargs) - scattering_type = expt_type.scattering_type.value - sample_form = expt_type.sample_form.value - expt_class = cls._SUPPORTED[scattering_type][sample_form] - expt_name = kwargs['name'] - expt_obj = expt_class(name=expt_name, type=expt_type) - return expt_obj - - @classmethod - def create(cls, **kwargs): - """Create an `ExperimentBase` using a validated argument - combination. - """ - # TODO: move to FactoryBase - # Check for valid argument combinations - user_args = {k for k, v in kwargs.items() if v is not None} - cls._validate_args( - present=user_args, - allowed_specs=cls._ALLOWED_ARG_SPECS, - factory_name=cls.__name__, # TODO: move to FactoryBase - ) - - # Validate enum arguments if provided - if 'sample_form' in kwargs: - SampleFormEnum(kwargs['sample_form']) - if 'beam_mode' in kwargs: - BeamModeEnum(kwargs['beam_mode']) - if 'radiation_probe' in kwargs: - RadiationProbeEnum(kwargs['radiation_probe']) - if 'scattering_type' in kwargs: - ScatteringTypeEnum(kwargs['scattering_type']) - - # Dispatch to the appropriate creation method - if 'cif_path' in kwargs: - return cls._create_from_cif_path(kwargs['cif_path']) - elif 'cif_str' in kwargs: - return cls._create_from_cif_str(kwargs['cif_str']) - elif 'data_path' in kwargs: - return cls._create_from_data_path(kwargs) - elif 'name' in kwargs: - return cls._create_without_data(kwargs) diff --git a/src/easydiffraction/experiments/experiment/instrument_mixin.py b/src/easydiffraction/experiments/experiment/instrument_mixin.py deleted file mode 100644 index c83c07bc..00000000 --- a/src/easydiffraction/experiments/experiment/instrument_mixin.py +++ /dev/null @@ -1,46 +0,0 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors -# SPDX-License-Identifier: BSD-3-Clause - -from __future__ import annotations - -from typing import TYPE_CHECKING - -from typeguard import typechecked - -from easydiffraction.experiments.categories.instrument.factory import InstrumentFactory - -if TYPE_CHECKING: - from easydiffraction.experiments.categories.instrument.base import InstrumentBase - - -class InstrumentMixin: - """Mixin that wires an experiment to an instrument category. - - Creates a default instrument via `InstrumentFactory` using the - experiment type (scattering type and beam mode) at initialization. - """ - - def __init__(self, *args, **kwargs): - expt_type = kwargs.get('type') - super().__init__(*args, **kwargs) - self._instrument = InstrumentFactory.create( - scattering_type=expt_type.scattering_type.value, - beam_mode=expt_type.beam_mode.value, - ) - - @property - def instrument(self): - """Instrument category object associated with the experiment.""" - return self._instrument - - @instrument.setter - @typechecked - def instrument(self, new_instrument: InstrumentBase): - """Replace the instrument and re-parent it to this experiment. - - Args: - new_instrument: Instrument instance compatible with the - experiment type. - """ - self._instrument = new_instrument - self._instrument._parent = self diff --git a/src/easydiffraction/experiments/experiment/total_pd.py b/src/easydiffraction/experiments/experiment/total_pd.py deleted file mode 100644 index 2f2f5f89..00000000 --- a/src/easydiffraction/experiments/experiment/total_pd.py +++ /dev/null @@ -1,59 +0,0 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors -# SPDX-License-Identifier: BSD-3-Clause - -from __future__ import annotations - -from typing import TYPE_CHECKING - -import numpy as np - -from easydiffraction.experiments.experiment.base import PdExperimentBase -from easydiffraction.utils.logging import console - -if TYPE_CHECKING: - from easydiffraction.experiments.categories.experiment_type import ExperimentType - - -class TotalPdExperiment(PdExperimentBase): - """PDF experiment class with specific attributes.""" - - def __init__( - self, - name: str, - type: ExperimentType, - ): - super().__init__(name=name, type=type) - - def _load_ascii_data_to_experiment(self, data_path): - """Loads x, y, sy values from an ASCII data file into the - experiment. - - The file must be structured as: - x y sy - """ - try: - from diffpy.utils.parsers.loaddata import loadData - except ImportError: - raise ImportError('diffpy module not found.') from None - try: - data = loadData(data_path) - except Exception as e: - raise IOError(f'Failed to read data from {data_path}: {e}') from e - - if data.shape[1] < 2: - raise ValueError('Data file must have at least two columns: x and y.') - - default_sy = 0.03 - if data.shape[1] < 3: - print(f'Warning: No uncertainty (sy) column provided. Defaulting to {default_sy}.') - - x = data[:, 0] - y = data[:, 1] - sy = data[:, 2] if data.shape[1] > 2 else np.full_like(y, fill_value=default_sy) - - self.data._set_x(x) - self.data._set_meas(y) - self.data._set_meas_su(sy) - - console.paragraph('Data loaded successfully') - console.print(f"Experiment 🔬 '{self.name}'. Number of data points: {len(x)}") diff --git a/src/easydiffraction/experiments/experiments.py b/src/easydiffraction/experiments/experiments.py deleted file mode 100644 index 9f58a3b7..00000000 --- a/src/easydiffraction/experiments/experiments.py +++ /dev/null @@ -1,134 +0,0 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors -# SPDX-License-Identifier: BSD-3-Clause - -from typeguard import typechecked - -from easydiffraction.core.datablock import DatablockCollection -from easydiffraction.experiments.experiment.base import ExperimentBase -from easydiffraction.experiments.experiment.factory import ExperimentFactory -from easydiffraction.utils.logging import console - - -class Experiments(DatablockCollection): - """Collection of Experiment data blocks. - - Provides convenience constructors for common creation patterns and - helper methods for simple presentation of collection contents. - """ - - def __init__(self) -> None: - super().__init__(item_type=ExperimentBase) - - # -------------------- - # Add / Remove methods - # -------------------- - - # TODO: Move to DatablockCollection? - # TODO: Disallow args and only allow kwargs? - def add(self, **kwargs): - experiment = kwargs.pop('experiment', None) - - if experiment is None: - experiment = ExperimentFactory.create(**kwargs) - - self._add(experiment) - - # @typechecked - # def add_from_cif_path(self, cif_path: str): - # """Add an experiment from a CIF file path. - # - # Args: - # cif_path: Path to a CIF document. - # """ - # experiment = ExperimentFactory.create(cif_path=cif_path) - # self.add(experiment) - - # @typechecked - # def add_from_cif_str(self, cif_str: str): - # """Add an experiment from a CIF string. - # - # Args: - # cif_str: Full CIF document as a string. - # """ - # experiment = ExperimentFactory.create(cif_str=cif_str) - # self.add(experiment) - - # @typechecked - # def add_from_data_path( - # self, - # name: str, - # data_path: str, - # sample_form: str = SampleFormEnum.default().value, - # beam_mode: str = BeamModeEnum.default().value, - # radiation_probe: str = RadiationProbeEnum.default().value, - # scattering_type: str = ScatteringTypeEnum.default().value, - # ): - # """Add an experiment from a data file path. - # - # Args: - # name: Experiment identifier. - # data_path: Path to the measured data file. - # sample_form: Sample form (powder or single crystal). - # beam_mode: Beam mode (constant wavelength or TOF). - # radiation_probe: Radiation probe (neutron or xray). - # scattering_type: Scattering type (bragg or total). - # """ - # experiment = ExperimentFactory.create( - # name=name, - # data_path=data_path, - # sample_form=sample_form, - # beam_mode=beam_mode, - # radiation_probe=radiation_probe, - # scattering_type=scattering_type, - # ) - # self.add(experiment) - - # @typechecked - # def add_without_data( - # self, - # name: str, - # sample_form: str = SampleFormEnum.default().value, - # beam_mode: str = BeamModeEnum.default().value, - # radiation_probe: str = RadiationProbeEnum.default().value, - # scattering_type: str = ScatteringTypeEnum.default().value, - # ): - # """Add an experiment without associating a data file. - # - # Args: - # name: Experiment identifier. - # sample_form: Sample form (powder or single crystal). - # beam_mode: Beam mode (constant wavelength or TOF). - # radiation_probe: Radiation probe (neutron or xray). - # scattering_type: Scattering type (bragg or total). - # """ - # experiment = ExperimentFactory.create( - # name=name, - # sample_form=sample_form, - # beam_mode=beam_mode, - # radiation_probe=radiation_probe, - # scattering_type=scattering_type, - # ) - # self.add(experiment) - - # TODO: Move to DatablockCollection? - @typechecked - def remove(self, name: str) -> None: - """Remove an experiment by name if it exists.""" - if name in self: - del self[name] - - # ------------ - # Show methods - # ------------ - - # TODO: Move to DatablockCollection? - def show_names(self) -> None: - """Print the list of experiment names.""" - console.paragraph('Defined experiments' + ' 🔬') - console.print(self.names) - - # TODO: Move to DatablockCollection? - def show_params(self) -> None: - """Print parameters for each experiment in the collection.""" - for exp in self.values(): - exp.show_params() diff --git a/src/easydiffraction/io/__init__.py b/src/easydiffraction/io/__init__.py index 429f2648..6ce45a95 100644 --- a/src/easydiffraction/io/__init__.py +++ b/src/easydiffraction/io/__init__.py @@ -1,2 +1,7 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause + +from easydiffraction.io.ascii import extract_data_paths_from_dir +from easydiffraction.io.ascii import extract_data_paths_from_zip +from easydiffraction.io.ascii import extract_metadata +from easydiffraction.io.ascii import load_numeric_block diff --git a/src/easydiffraction/io/ascii.py b/src/easydiffraction/io/ascii.py new file mode 100644 index 00000000..6987d217 --- /dev/null +++ b/src/easydiffraction/io/ascii.py @@ -0,0 +1,182 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Helpers for loading numeric data from ASCII files.""" + +from __future__ import annotations + +import tempfile +import zipfile +from io import StringIO +from pathlib import Path + +import numpy as np + + +def extract_data_paths_from_zip(zip_path: str | Path) -> list[str]: + """ + Extract all files from a ZIP archive and return their paths. + + Files are extracted into a temporary directory that persists for the + lifetime of the process. The returned paths are sorted + lexicographically by file name so that numbered data files (e.g. + ``scan_001.dat``, ``scan_002.dat``) appear in natural order. Hidden + files and directories (names starting with ``'.'`` or ``'__'``) are + excluded. + + Parameters + ---------- + zip_path : str | Path + Path to the ZIP archive. + + Returns + ------- + list[str] + Sorted absolute paths to the extracted data files. + + Raises + ------ + FileNotFoundError + If *zip_path* does not exist. + ValueError + If the archive contains no usable data files. + """ + zip_path = Path(zip_path) + if not zip_path.exists(): + raise FileNotFoundError(f'ZIP file not found: {zip_path}') + + # TODO: Unify mkdir with other uses in the code + extract_dir = Path(tempfile.mkdtemp(prefix='ed_zip_')) + + with zipfile.ZipFile(zip_path, 'r') as zf: + zf.extractall(extract_dir) + + paths = sorted( + str(p) + for p in extract_dir.rglob('*') + if p.is_file() and not p.name.startswith('.') and not p.name.startswith('__') + ) + + if not paths: + raise ValueError(f'No data files found in ZIP archive: {zip_path}') + + return paths + + +def extract_data_paths_from_dir( + dir_path: str | Path, + file_pattern: str = '*', +) -> list[str]: + """ + List data files in a directory and return their sorted paths. + + Hidden files (names starting with ``'.'`` or ``'__'``) are excluded. + The returned paths are sorted lexicographically by file name. + + Parameters + ---------- + dir_path : str | Path + Path to the directory containing data files. + file_pattern : str, default='*' + Glob pattern to filter files (e.g. ``'*.dat'``, ``'*.xye'``). + + Returns + ------- + list[str] + Sorted absolute paths to the matching data files. + + Raises + ------ + FileNotFoundError + If *dir_path* does not exist or is not a directory. + ValueError + If no matching data files are found. + """ + dir_path = Path(dir_path) + if not dir_path.is_dir(): + raise FileNotFoundError(f'Directory not found: {dir_path}') + + paths = sorted( + str(p) + for p in dir_path.glob(file_pattern) + if p.is_file() and not p.name.startswith('.') and not p.name.startswith('__') + ) + + if not paths: + raise ValueError(f"No files matching '{file_pattern}' found in directory: {dir_path}") + + return paths + + +def extract_metadata( + file_path: str | Path, + pattern: str, +) -> float | None: + """ + Extract a single numeric value from a file using a regex pattern. + + The entire file content is searched (not just the header). The + **first** match is used. The regex must contain exactly one capture + group whose match is convertible to ``float``. + + Parameters + ---------- + file_path : str | Path + Path to the input file. + pattern : str + Regex with one capture group that matches the numeric value. + + Returns + ------- + float | None + The extracted value, or ``None`` if the pattern did not match or + the captured text could not be converted to float. + """ + import re + + content = Path(file_path).read_text(encoding='utf-8', errors='ignore') + match = re.search(pattern, content, re.MULTILINE) + if match is None: + return None + try: + return float(match.group(1)) + except (ValueError, IndexError): + return None + + +def load_numeric_block(data_path: str | Path) -> np.ndarray: + """ + Load a numeric block from an ASCII file, skipping header lines. + + Read the file and try ``numpy.loadtxt`` starting from the first + line, then the second, etc., until the load succeeds. This allows + files with an arbitrary number of non-numeric header lines to be + parsed without prior knowledge of the format. + + Parameters + ---------- + data_path : str | Path + Path to the ASCII data file. + + Returns + ------- + np.ndarray + 2-D array of the parsed numeric data. + + Raises + ------ + IOError + If no contiguous numeric block can be found in the file. + """ + data_path = Path(data_path) + lines = data_path.read_text().splitlines() + + last_error: Exception | None = None + for start in range(len(lines)): + try: + return np.loadtxt(StringIO('\n'.join(lines[start:]))) + except Exception as e: # noqa: BLE001 + last_error = e + + raise IOError( + f'Failed to read numeric data from {data_path}: {last_error}', + ) from last_error diff --git a/src/easydiffraction/io/cif/__init__.py b/src/easydiffraction/io/cif/__init__.py index 429f2648..4e798e20 100644 --- a/src/easydiffraction/io/cif/__init__.py +++ b/src/easydiffraction/io/cif/__init__.py @@ -1,2 +1,2 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause diff --git a/src/easydiffraction/io/cif/handler.py b/src/easydiffraction/io/cif/handler.py index 16ed4343..0cd3b62b 100644 --- a/src/easydiffraction/io/cif/handler.py +++ b/src/easydiffraction/io/cif/handler.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause """Minimal CIF tag handler used by descriptors/parameters.""" @@ -6,7 +6,8 @@ class CifHandler: - """Canonical CIF handler used by descriptors/parameters. + """ + Canonical CIF handler used by descriptors/parameters. Holds CIF tags (names) and attaches to an owning descriptor so it can derive a stable uid if needed. @@ -16,7 +17,7 @@ def __init__(self, *, names: list[str]) -> None: self._names = names self._owner = None # set by attach - def attach(self, owner): + def attach(self, owner: object) -> None: """Attach to a descriptor or parameter instance.""" self._owner = owner diff --git a/src/easydiffraction/io/cif/parse.py b/src/easydiffraction/io/cif/parse.py index ad0d736d..a320cf60 100644 --- a/src/easydiffraction/io/cif/parse.py +++ b/src/easydiffraction/io/cif/parse.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause import gemmi diff --git a/src/easydiffraction/io/cif/serialize.py b/src/easydiffraction/io/cif/serialize.py index 877f6ab1..f885aed7 100644 --- a/src/easydiffraction/io/cif/serialize.py +++ b/src/easydiffraction/io/cif/serialize.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause from __future__ import annotations @@ -19,15 +19,19 @@ from easydiffraction.core.category import CategoryCollection from easydiffraction.core.category import CategoryItem - from easydiffraction.core.parameters import GenericDescriptorBase + from easydiffraction.core.variable import GenericDescriptorBase -def format_value(value) -> str: - """Format a single CIF value, quoting strings with whitespace, and - format floats with global precision. +def format_value(value: object) -> str: """ - width = 8 - precision = 4 + Format a single CIF value for output. + + .. note:: The precision must be high enough so that the + minimizer's finite-difference Jacobian probes (typically ~1e-8 + relative) survive the float→string→float round-trip through CIF. + """ + width = 12 + precision = 8 # Converting @@ -56,8 +60,9 @@ def format_value(value) -> str: ################## -def param_to_cif(param) -> str: - """Render a single descriptor/parameter to a CIF line. +def param_to_cif(param: object) -> str: + """ + Render a single descriptor/parameter to a CIF line. Expects ``param`` to expose ``_cif_handler.names`` and ``value``. """ @@ -66,8 +71,9 @@ def param_to_cif(param) -> str: return f'{main_key} {format_value(param.value)}' -def category_item_to_cif(item) -> str: - """Render a CategoryItem-like object to CIF text. +def category_item_to_cif(item: object) -> str: + """ + Render a CategoryItem-like object to CIF text. Expects ``item.parameters`` iterable of params with ``_cif_handler.names`` and ``value``. @@ -79,10 +85,11 @@ def category_item_to_cif(item) -> str: def category_collection_to_cif( - collection, + collection: object, max_display: Optional[int] = 20, ) -> str: - """Render a CategoryCollection-like object to CIF text. + """ + Render a CategoryCollection-like object to CIF text. Uses first item to build loop header, then emits rows for each item. """ @@ -120,8 +127,9 @@ def category_collection_to_cif( return '\n'.join(lines) -def datablock_item_to_cif(datablock) -> str: - """Render a DatablockItem-like object to CIF text. +def datablock_item_to_cif(datablock: object) -> str: + """ + Render a DatablockItem-like object to CIF text. Emits a data_ header and then concatenates category CIF sections. """ @@ -145,15 +153,13 @@ def datablock_item_to_cif(datablock) -> str: return '\n\n'.join(parts) -def datablock_collection_to_cif(collection) -> str: +def datablock_collection_to_cif(collection: object) -> str: """Render a collection of datablocks by joining their CIF blocks.""" return '\n\n'.join([block.as_cif for block in collection.values()]) -def project_info_to_cif(info) -> str: - """Render ProjectInfo to CIF text (id, title, description, - dates). - """ +def project_info_to_cif(info: object) -> str: + """Render ProjectInfo to CIF text (id, title, description).""" name = f'{info.name}' title = f'{info.title}' @@ -179,13 +185,13 @@ def project_info_to_cif(info) -> str: ) -def project_to_cif(project) -> str: +def project_to_cif(project: object) -> str: """Render a whole project by concatenating sections when present.""" parts: list[str] = [] if hasattr(project, 'info'): parts.append(project.info.as_cif) - if getattr(project, 'sample_models', None): - parts.append(project.sample_models.as_cif) + if getattr(project, 'structures', None): + parts.append(project.structures.as_cif) if getattr(project, 'experiments', None): parts.append(project.experiments.as_cif) if getattr(project, 'analysis', None): @@ -195,26 +201,29 @@ def project_to_cif(project) -> str: return '\n\n'.join([p for p in parts if p]) -def experiment_to_cif(experiment) -> str: +def experiment_to_cif(experiment: object) -> str: """Render an experiment: datablock part plus measured data.""" return datablock_item_to_cif(experiment) -def analysis_to_cif(analysis) -> str: +def analysis_to_cif(analysis: object) -> str: """Render analysis metadata, aliases, and constraints to CIF.""" cur_min = format_value(analysis.current_minimizer) lines: list[str] = [] - lines.append(f'_analysis.calculator_engine {format_value(analysis.current_calculator)}') lines.append(f'_analysis.fitting_engine {cur_min}') - lines.append(f'_analysis.fit_mode {format_value(analysis.fit_mode)}') + lines.append(analysis.fit_mode.as_cif) lines.append('') lines.append(analysis.aliases.as_cif) lines.append('') lines.append(analysis.constraints.as_cif) + jfe_cif = analysis.joint_fit_experiments.as_cif + if jfe_cif: + lines.append('') + lines.append(jfe_cif) return '\n'.join(lines) -def summary_to_cif(_summary) -> str: +def summary_to_cif(_summary: object) -> str: """Render a summary CIF block (placeholder for now).""" return 'To be added...' @@ -231,6 +240,18 @@ def param_from_cif( block: gemmi.cif.Block, idx: int = 0, ) -> None: + """ + Populate a single descriptor from a CIF block. + + Parameters + ---------- + self : GenericDescriptorBase + The descriptor instance to populate. + block : gemmi.cif.Block + Parsed CIF block to read values from. + idx : int, default=0 + Row index used when the tag belongs to a loop. + """ found_values: list[Any] = [] # Try to find the value(s) from the CIF block iterating over @@ -282,6 +303,21 @@ def category_collection_from_cif( self: CategoryCollection, block: gemmi.cif.Block, ) -> None: + """ + Populate a CategoryCollection from a CIF loop. + + Parameters + ---------- + self : CategoryCollection + The collection instance to populate. + block : gemmi.cif.Block + Parsed CIF block to read the loop from. + + Raises + ------ + ValueError + If the collection has no ``_item_type`` defined. + """ # TODO: Find a better way and then remove TODO in the AtomSite # class # TODO: Rename to _item_cls? @@ -294,7 +330,7 @@ def category_collection_from_cif( # Iterate over category parameters and their possible CIF names # trying to find the whole loop it belongs to inside the CIF block - def _get_loop(block, category_item): + def _get_loop(block: object, category_item: object) -> object | None: for param in category_item.parameters: for name in param._cif_handler.names: loop = block.find_loop(name).get_loop() diff --git a/src/easydiffraction/project/__init__.py b/src/easydiffraction/project/__init__.py index 429f2648..4e798e20 100644 --- a/src/easydiffraction/project/__init__.py +++ b/src/easydiffraction/project/__init__.py @@ -1,2 +1,2 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause diff --git a/src/easydiffraction/project/project.py b/src/easydiffraction/project/project.py index 980f680b..61b2cd5d 100644 --- a/src/easydiffraction/project/project.py +++ b/src/easydiffraction/project/project.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause """Project facade to orchestrate models, experiments, and analysis.""" @@ -10,22 +10,23 @@ from easydiffraction.analysis.analysis import Analysis from easydiffraction.core.guard import GuardedBase +from easydiffraction.datablocks.experiment.collection import Experiments +from easydiffraction.datablocks.structure.collection import Structures from easydiffraction.display.plotting import Plotter from easydiffraction.display.tables import TableRenderer -from easydiffraction.experiments.experiments import Experiments from easydiffraction.io.cif.serialize import project_to_cif from easydiffraction.project.project_info import ProjectInfo -from easydiffraction.sample_models.sample_models import SampleModels from easydiffraction.summary.summary import Summary +from easydiffraction.utils.enums import VerbosityEnum from easydiffraction.utils.logging import console from easydiffraction.utils.logging import log class Project(GuardedBase): - """Central API for managing a diffraction data analysis project. + """ + Central API for managing a diffraction data analysis project. - Provides access to sample models, experiments, analysis, and - summary. + Provides access to structures, experiments, analysis, and summary. """ # ------------------------------------------------------------------ @@ -40,7 +41,7 @@ def __init__( super().__init__() self._info: ProjectInfo = ProjectInfo(name, title, description) - self._sample_models = SampleModels() + self._structures = Structures() self._experiments = Experiments() self._tabler = TableRenderer.get() self._plotter = Plotter() @@ -48,6 +49,7 @@ def __init__( self._summary = Summary(self) self._saved = False self._varname = varname() + self._verbosity: VerbosityEnum = VerbosityEnum.FULL # ------------------------------------------------------------------ # Dunder methods @@ -56,11 +58,11 @@ def __str__(self) -> str: """Human-readable representation.""" class_name = self.__class__.__name__ project_name = self.name - sample_models_count = len(self.sample_models) + structures_count = len(self.structures) experiments_count = len(self.experiments) return ( f"{class_name} '{project_name}' " - f'({sample_models_count} sample models, ' + f'({structures_count} structures, ' f'{experiments_count} experiments)' ) @@ -75,86 +77,113 @@ def info(self) -> ProjectInfo: @property def name(self) -> str: - """Convenience property to access the project's name - directly. - """ + """Convenience property for the project name.""" return self._info.name @property def full_name(self) -> str: + """ + Return the full project name (alias for :attr:`name`). + + Returns + ------- + str + The project name. + """ return self.name @property - def sample_models(self) -> SampleModels: - """Collection of sample models in the project.""" - return self._sample_models + def structures(self) -> Structures: + """Collection of structures in the project.""" + return self._structures - @sample_models.setter + @structures.setter @typechecked - def sample_models(self, sample_models: SampleModels) -> None: - self._sample_models = sample_models + def structures(self, structures: Structures) -> None: + self._structures = structures @property - def experiments(self): + def experiments(self) -> Experiments: """Collection of experiments in the project.""" return self._experiments @experiments.setter @typechecked - def experiments(self, experiments: Experiments): + def experiments(self, experiments: Experiments) -> None: self._experiments = experiments @property - def plotter(self): + def plotter(self) -> Plotter: """Plotting facade bound to the project.""" return self._plotter @property - def tabler(self): + def tabler(self) -> TableRenderer: """Tables rendering facade bound to the project.""" return self._tabler @property - def analysis(self): + def analysis(self) -> Analysis: """Analysis entry-point bound to the project.""" return self._analysis @property - def summary(self): + def summary(self) -> Summary: """Summary report builder bound to the project.""" return self._summary @property - def parameters(self): - """Return parameters from all components (TBD).""" - # To be implemented: return all parameters in the project - return [] + def parameters(self) -> list: + """Return parameters from all structures and experiments.""" + return self.structures.parameters + self.experiments.parameters @property - def as_cif(self): + def as_cif(self) -> str: """Export whole project as CIF text.""" # Concatenate sections using centralized CIF serializers return project_to_cif(self) + @property + def verbosity(self) -> str: + """ + Project-wide console output verbosity. + + Returns + ------- + str + One of ``'full'``, ``'short'``, or ``'silent'``. + """ + return self._verbosity.value + + @verbosity.setter + def verbosity(self, value: str) -> None: + """ + Set project-wide console output verbosity. + + Parameters + ---------- + value : str + ``'full'`` for multi-line output, ``'short'`` for one-line + status messages, or ``'silent'`` for no output. + """ + self._verbosity = VerbosityEnum(value) + # ------------------------------------------ # Project File I/O # ------------------------------------------ def load(self, dir_path: str) -> None: - """Load a project from a given directory. + """ + Load a project from a given directory. - Loads project info, sample models, experiments, etc. + Loads project info, structures, experiments, etc. """ - console.paragraph('Loading project 📦 from') - console.print(dir_path) - self._info.path = dir_path # TODO: load project components from files inside dir_path - console.print('Loading project is not implemented yet.') - self._saved = True + raise NotImplementedError('Project.load() is not implemented yet.') def save(self) -> None: """Save the project into the existing project directory.""" - if not self._info.path: + if self._info.path is None: log.error('Project path not specified. Use save_as() to define the path first.') return @@ -169,26 +198,24 @@ def save(self) -> None: f.write(self._info.as_cif()) console.print('├── 📄 project.cif') - # Save sample models - sm_dir = self._info.path / 'sample_models' + # Save structures + sm_dir = self._info.path / 'structures' sm_dir.mkdir(parents=True, exist_ok=True) - # Iterate over sample model objects (MutableMapping iter gives - # keys) - for model in self.sample_models.values(): - file_name: str = f'{model.name}.cif' + console.print('├── 📁 structures/') + for structure in self.structures.values(): + file_name: str = f'{structure.name}.cif' file_path = sm_dir / file_name - console.print('├── 📁 sample_models') with file_path.open('w') as f: - f.write(model.as_cif) + f.write(structure.as_cif) console.print(f'│ └── 📄 {file_name}') # Save experiments expt_dir = self._info.path / 'experiments' expt_dir.mkdir(parents=True, exist_ok=True) + console.print('├── 📁 experiments/') for experiment in self.experiments.values(): file_name: str = f'{experiment.name}.cif' file_path = expt_dir / file_name - console.print('├── 📁 experiments') with file_path.open('w') as f: f.write(experiment.as_cif) console.print(f'│ └── 📄 {file_name}') @@ -222,20 +249,34 @@ def save_as( # Plotting # ------------------------------------------ - def _update_categories(self, expt_name) -> None: - for sample_model in self.sample_models: - sample_model._update_categories() + def _update_categories(self, expt_name: str) -> None: + for structure in self.structures: + structure._update_categories() self.analysis._update_categories() experiment = self.experiments[expt_name] experiment._update_categories() def plot_meas( self, - expt_name, - x_min=None, - x_max=None, - d_spacing=False, - ): + expt_name: str, + x_min: float | None = None, + x_max: float | None = None, + x: object | None = None, + ) -> None: + """ + Plot measured diffraction data for an experiment. + + Parameters + ---------- + expt_name : str + Name of the experiment to plot. + x_min : float | None, default=None + Lower bound for the x-axis range. + x_max : float | None, default=None + Upper bound for the x-axis range. + x : object | None, default=None + Optional explicit x-axis data to override stored values. + """ self._update_categories(expt_name) experiment = self.experiments[expt_name] @@ -245,16 +286,30 @@ def plot_meas( experiment.type, x_min=x_min, x_max=x_max, - d_spacing=d_spacing, + x=x, ) def plot_calc( self, - expt_name, - x_min=None, - x_max=None, - d_spacing=False, - ): + expt_name: str, + x_min: float | None = None, + x_max: float | None = None, + x: object | None = None, + ) -> None: + """ + Plot calculated diffraction pattern for an experiment. + + Parameters + ---------- + expt_name : str + Name of the experiment to plot. + x_min : float | None, default=None + Lower bound for the x-axis range. + x_max : float | None, default=None + Upper bound for the x-axis range. + x : object | None, default=None + Optional explicit x-axis data to override stored values. + """ self._update_categories(expt_name) experiment = self.experiments[expt_name] @@ -264,17 +319,33 @@ def plot_calc( experiment.type, x_min=x_min, x_max=x_max, - d_spacing=d_spacing, + x=x, ) def plot_meas_vs_calc( self, - expt_name, - x_min=None, - x_max=None, - show_residual=False, - d_spacing=False, - ): + expt_name: str, + x_min: float | None = None, + x_max: float | None = None, + show_residual: bool = False, + x: object | None = None, + ) -> None: + """ + Plot measured vs calculated data for an experiment. + + Parameters + ---------- + expt_name : str + Name of the experiment to plot. + x_min : float | None, default=None + Lower bound for the x-axis range. + x_max : float | None, default=None + Upper bound for the x-axis range. + show_residual : bool, default=False + When ``True``, include the residual (difference) curve. + x : object | None, default=None + Optional explicit x-axis data to override stored values. + """ self._update_categories(expt_name) experiment = self.experiments[expt_name] @@ -285,5 +356,29 @@ def plot_meas_vs_calc( x_min=x_min, x_max=x_max, show_residual=show_residual, - d_spacing=d_spacing, + x=x, + ) + + def plot_param_series(self, param: object, versus: object | None = None) -> None: + """ + Plot a parameter's value across sequential fit results. + + Parameters + ---------- + param : object + Parameter descriptor whose ``unique_name`` identifies the + values to plot. + versus : object | None, default=None + A diffrn descriptor (e.g. + ``expt.diffrn.ambient_temperature``) whose value is used as + the x-axis for each experiment. When ``None``, the + experiment sequence number is used instead. + """ + unique_name = param.unique_name + versus_name = versus.name if versus is not None else None + self.plotter.plot_param_series( + unique_name, + versus_name, + self.experiments, + self.analysis._parameter_snapshots, ) diff --git a/src/easydiffraction/project/project_info.py b/src/easydiffraction/project/project_info.py index 998ba200..dcba2fba 100644 --- a/src/easydiffraction/project/project_info.py +++ b/src/easydiffraction/project/project_info.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause """Project metadata container used by Project.""" @@ -12,9 +12,7 @@ class ProjectInfo(GuardedBase): - """Stores metadata about the project, such as name, title, - description, and file paths. - """ + """Store project metadata: name, title, description, paths.""" def __init__( self, @@ -27,7 +25,7 @@ def __init__( self._name = name self._title = title self._description = description - self._path: pathlib.Path = pathlib.Path.cwd() + self._path: pathlib.Path | None = None # pathlib.Path.cwd() self._created: datetime.datetime = datetime.datetime.now() self._last_modified: datetime.datetime = datetime.datetime.now() @@ -38,6 +36,14 @@ def name(self) -> str: @name.setter def name(self, value: str) -> None: + """ + Set the project name. + + Parameters + ---------- + value : str + New project name. + """ self._name = value @property @@ -52,6 +58,14 @@ def title(self) -> str: @title.setter def title(self, value: str) -> None: + """ + Set the project title. + + Parameters + ---------- + value : str + New project title. + """ self._title = value @property @@ -61,15 +75,31 @@ def description(self) -> str: @description.setter def description(self, value: str) -> None: + """ + Set the project description (whitespace normalized). + + Parameters + ---------- + value : str + New description text. + """ self._description = ' '.join(value.split()) @property - def path(self) -> pathlib.Path: + def path(self) -> pathlib.Path | None: """Return the project path as a Path object.""" return self._path @path.setter - def path(self, value) -> None: + def path(self, value: object) -> None: + """ + Set the project directory path. + + Parameters + ---------- + value : object + New path as a :class:`str` or :class:`pathlib.Path`. + """ # Accept str or Path; normalize to Path self._path = pathlib.Path(value) @@ -87,8 +117,8 @@ def update_last_modified(self) -> None: """Update the last modified timestamp.""" self._last_modified = datetime.datetime.now() - def parameters(self): - """Placeholder for parameter listing.""" + def parameters(self) -> None: + """List parameters (not implemented).""" pass # TODO: Consider moving to io.cif.serialize diff --git a/src/easydiffraction/sample_models/__init__.py b/src/easydiffraction/sample_models/__init__.py deleted file mode 100644 index 429f2648..00000000 --- a/src/easydiffraction/sample_models/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors -# SPDX-License-Identifier: BSD-3-Clause diff --git a/src/easydiffraction/sample_models/categories/__init__.py b/src/easydiffraction/sample_models/categories/__init__.py deleted file mode 100644 index 429f2648..00000000 --- a/src/easydiffraction/sample_models/categories/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors -# SPDX-License-Identifier: BSD-3-Clause diff --git a/src/easydiffraction/sample_models/categories/atom_sites.py b/src/easydiffraction/sample_models/categories/atom_sites.py deleted file mode 100644 index 955c62df..00000000 --- a/src/easydiffraction/sample_models/categories/atom_sites.py +++ /dev/null @@ -1,334 +0,0 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors -# SPDX-License-Identifier: BSD-3-Clause -"""Atom site category. - -Defines AtomSite items and AtomSites collection used in sample models. -Only documentation was added; behavior remains unchanged. -""" - -from cryspy.A_functions_base.database import DATABASE - -from easydiffraction.core.category import CategoryCollection -from easydiffraction.core.category import CategoryItem -from easydiffraction.core.parameters import Parameter -from easydiffraction.core.parameters import StringDescriptor -from easydiffraction.core.validation import AttributeSpec -from easydiffraction.core.validation import DataTypes -from easydiffraction.core.validation import MembershipValidator -from easydiffraction.core.validation import RangeValidator -from easydiffraction.core.validation import RegexValidator -from easydiffraction.crystallography import crystallography as ecr -from easydiffraction.io.cif.handler import CifHandler - - -class AtomSite(CategoryItem): - """Single atom site with fractional coordinates and ADP. - - Attributes are represented by descriptors to support validation and - CIF serialization. - """ - - def __init__( - self, - *, - label=None, - type_symbol=None, - fract_x=None, - fract_y=None, - fract_z=None, - wyckoff_letter=None, - occupancy=None, - b_iso=None, - adp_type=None, - ) -> None: - super().__init__() - - self._label: StringDescriptor = StringDescriptor( - name='label', - description='Unique identifier for the atom site.', - value_spec=AttributeSpec( - value=label, - type_=DataTypes.STRING, - default='Si', - # TODO: the following pattern is valid for dict key - # (keywords are not checked). CIF label is less strict. - # Do we need conversion between CIF and internal label? - content_validator=RegexValidator(pattern=r'^[A-Za-z_][A-Za-z0-9_]*$'), - ), - cif_handler=CifHandler( - names=[ - '_atom_site.label', - ] - ), - ) - self._type_symbol: StringDescriptor = StringDescriptor( - name='type_symbol', - description='Chemical symbol of the atom at this site.', - value_spec=AttributeSpec( - value=type_symbol, - type_=DataTypes.STRING, - default='Tb', - content_validator=MembershipValidator(allowed=self._type_symbol_allowed_values), - ), - cif_handler=CifHandler( - names=[ - '_atom_site.type_symbol', - ] - ), - ) - self._fract_x: Parameter = Parameter( - name='fract_x', - description='Fractional x-coordinate of the atom site within the unit cell.', - value_spec=AttributeSpec( - value=fract_x, - type_=DataTypes.NUMERIC, - default=0.0, - content_validator=RangeValidator(), - ), - cif_handler=CifHandler( - names=[ - '_atom_site.fract_x', - ] - ), - ) - self._fract_y: Parameter = Parameter( - name='fract_y', - description='Fractional y-coordinate of the atom site within the unit cell.', - value_spec=AttributeSpec( - value=fract_y, - type_=DataTypes.NUMERIC, - default=0.0, - content_validator=RangeValidator(), - ), - cif_handler=CifHandler( - names=[ - '_atom_site.fract_y', - ] - ), - ) - self._fract_z: Parameter = Parameter( - name='fract_z', - description='Fractional z-coordinate of the atom site within the unit cell.', - value_spec=AttributeSpec( - value=fract_z, - type_=DataTypes.NUMERIC, - default=0.0, - content_validator=RangeValidator(), - ), - cif_handler=CifHandler( - names=[ - '_atom_site.fract_z', - ] - ), - ) - self._wyckoff_letter: StringDescriptor = StringDescriptor( - name='wyckoff_letter', - description='Wyckoff letter indicating the symmetry of the ' - 'atom site within the space group.', - value_spec=AttributeSpec( - value=wyckoff_letter, - type_=DataTypes.STRING, - default=self._wyckoff_letter_default_value, - content_validator=MembershipValidator(allowed=self._wyckoff_letter_allowed_values), - ), - cif_handler=CifHandler( - names=[ - '_atom_site.Wyckoff_letter', - '_atom_site.Wyckoff_symbol', - ] - ), - ) - self._occupancy: Parameter = Parameter( - name='occupancy', - description='Occupancy of the atom site, representing the ' - 'fraction of the site occupied by the atom type.', - value_spec=AttributeSpec( - value=occupancy, - type_=DataTypes.NUMERIC, - default=1.0, - content_validator=RangeValidator(), - ), - cif_handler=CifHandler( - names=[ - '_atom_site.occupancy', - ] - ), - ) - self._b_iso: Parameter = Parameter( - name='b_iso', - description='Isotropic atomic displacement parameter (ADP) for the atom site.', - value_spec=AttributeSpec( - value=b_iso, - type_=DataTypes.NUMERIC, - default=0.0, - content_validator=RangeValidator(ge=0.0), - ), - units='Ų', - cif_handler=CifHandler( - names=[ - '_atom_site.B_iso_or_equiv', - ] - ), - ) - self._adp_type: StringDescriptor = StringDescriptor( - name='adp_type', - description='Type of atomic displacement parameter (ADP) ' - 'used (e.g., Biso, Uiso, Uani, Bani).', - value_spec=AttributeSpec( - value=adp_type, - type_=DataTypes.STRING, - default='Biso', - content_validator=MembershipValidator(allowed=['Biso']), - ), - cif_handler=CifHandler( - names=[ - '_atom_site.adp_type', - ] - ), - ) - - self._identity.category_code = 'atom_site' - self._identity.category_entry_name = lambda: str(self.label.value) - - @property - def _type_symbol_allowed_values(self): - return list({key[1] for key in DATABASE['Isotopes']}) - - @property - def _wyckoff_letter_allowed_values(self): - # TODO: Need to now current space group. How to access it? Via - # parent Cell? Then letters = - # list(SPACE_GROUPS[62, 'cab']['Wyckoff_positions'].keys()) - # Temporarily return hardcoded list: - return ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i'] - - @property - def _wyckoff_letter_default_value(self): - # TODO: What to pass as default? - return self._wyckoff_letter_allowed_values[0] - - @property - def label(self): - """Label descriptor for the site (unique key).""" - return self._label - - @label.setter - def label(self, value): - self._label.value = value - - @property - def type_symbol(self): - """Chemical symbol descriptor (e.g. 'Si').""" - return self._type_symbol - - @type_symbol.setter - def type_symbol(self, value): - self._type_symbol.value = value - - @property - def adp_type(self): - """ADP type descriptor (e.g. 'Biso').""" - return self._adp_type - - @adp_type.setter - def adp_type(self, value): - self._adp_type.value = value - - @property - def wyckoff_letter(self): - """Wyckoff letter descriptor (space-group position).""" - return self._wyckoff_letter - - @wyckoff_letter.setter - def wyckoff_letter(self, value): - self._wyckoff_letter.value = value - - @property - def fract_x(self): - """Fractional x coordinate descriptor.""" - return self._fract_x - - @fract_x.setter - def fract_x(self, value): - self._fract_x.value = value - - @property - def fract_y(self): - """Fractional y coordinate descriptor.""" - return self._fract_y - - @fract_y.setter - def fract_y(self, value): - self._fract_y.value = value - - @property - def fract_z(self): - """Fractional z coordinate descriptor.""" - return self._fract_z - - @fract_z.setter - def fract_z(self, value): - self._fract_z.value = value - - @property - def occupancy(self): - """Occupancy descriptor (0..1).""" - return self._occupancy - - @occupancy.setter - def occupancy(self, value): - self._occupancy.value = value - - @property - def b_iso(self): - """Isotropic ADP descriptor in Ų.""" - return self._b_iso - - @b_iso.setter - def b_iso(self, value): - self._b_iso.value = value - - -class AtomSites(CategoryCollection): - """Collection of AtomSite instances.""" - - def __init__(self): - super().__init__(item_type=AtomSite) - - def _apply_atomic_coordinates_symmetry_constraints(self): - """Apply symmetry rules to fractional coordinates of atom - sites. - """ - sample_model = self._parent - space_group_name = sample_model.space_group.name_h_m.value - space_group_coord_code = sample_model.space_group.it_coordinate_system_code.value - for atom in self._items: - dummy_atom = { - 'fract_x': atom.fract_x.value, - 'fract_y': atom.fract_y.value, - 'fract_z': atom.fract_z.value, - } - wl = atom.wyckoff_letter.value - if not wl: - # TODO: Decide how to handle this case - # For now, we just skip applying constraints if wyckoff - # letter is not set. Alternatively, could raise an - # error or warning - # print(f"Warning: Wyckoff letter is not ...") - # raise ValueError("Wyckoff letter is not ...") - continue - ecr.apply_atom_site_symmetry_constraints( - atom_site=dummy_atom, - name_hm=space_group_name, - coord_code=space_group_coord_code, - wyckoff_letter=wl, - ) - atom.fract_x.value = dummy_atom['fract_x'] - atom.fract_y.value = dummy_atom['fract_y'] - atom.fract_z.value = dummy_atom['fract_z'] - - def _update(self, called_by_minimizer=False): - """Update atom sites by applying symmetry constraints.""" - del called_by_minimizer - - self._apply_atomic_coordinates_symmetry_constraints() diff --git a/src/easydiffraction/sample_models/categories/cell.py b/src/easydiffraction/sample_models/categories/cell.py deleted file mode 100644 index 80c0b144..00000000 --- a/src/easydiffraction/sample_models/categories/cell.py +++ /dev/null @@ -1,188 +0,0 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors -# SPDX-License-Identifier: BSD-3-Clause -"""Unit cell parameters category for sample models.""" - -from typing import Optional - -from easydiffraction.core.category import CategoryItem -from easydiffraction.core.parameters import Parameter -from easydiffraction.core.validation import AttributeSpec -from easydiffraction.core.validation import DataTypes -from easydiffraction.core.validation import RangeValidator -from easydiffraction.crystallography import crystallography as ecr -from easydiffraction.io.cif.handler import CifHandler - - -class Cell(CategoryItem): - """Unit cell with lengths a, b, c and angles alpha, beta, gamma.""" - - def __init__( - self, - *, - length_a: Optional[int | float] = None, - length_b: Optional[int | float] = None, - length_c: Optional[int | float] = None, - angle_alpha: Optional[int | float] = None, - angle_beta: Optional[int | float] = None, - angle_gamma: Optional[int | float] = None, - ) -> None: - super().__init__() - - self._length_a: Parameter = Parameter( - name='length_a', - description='Length of the a axis of the unit cell.', - value_spec=AttributeSpec( - value=length_a, - type_=DataTypes.NUMERIC, - default=10.0, - content_validator=RangeValidator(ge=0, le=1000), - ), - units='Å', - cif_handler=CifHandler(names=['_cell.length_a']), - ) - self._length_b: Parameter = Parameter( - name='length_b', - description='Length of the b axis of the unit cell.', - value_spec=AttributeSpec( - value=length_b, - type_=DataTypes.NUMERIC, - default=10.0, - content_validator=RangeValidator(ge=0, le=1000), - ), - units='Å', - cif_handler=CifHandler(names=['_cell.length_b']), - ) - self._length_c: Parameter = Parameter( - name='length_c', - description='Length of the c axis of the unit cell.', - value_spec=AttributeSpec( - value=length_c, - type_=DataTypes.NUMERIC, - default=10.0, - content_validator=RangeValidator(ge=0, le=1000), - ), - units='Å', - cif_handler=CifHandler(names=['_cell.length_c']), - ) - self._angle_alpha: Parameter = Parameter( - name='angle_alpha', - description='Angle between edges b and c.', - value_spec=AttributeSpec( - value=angle_alpha, - type_=DataTypes.NUMERIC, - default=90.0, - content_validator=RangeValidator(ge=0, le=180), - ), - units='deg', - cif_handler=CifHandler(names=['_cell.angle_alpha']), - ) - self._angle_beta: Parameter = Parameter( - name='angle_beta', - description='Angle between edges a and c.', - value_spec=AttributeSpec( - value=angle_beta, - type_=DataTypes.NUMERIC, - default=90.0, - content_validator=RangeValidator(ge=0, le=180), - ), - units='deg', - cif_handler=CifHandler(names=['_cell.angle_beta']), - ) - self._angle_gamma: Parameter = Parameter( - name='angle_gamma', - description='Angle between edges a and b.', - value_spec=AttributeSpec( - value=angle_gamma, - type_=DataTypes.NUMERIC, - default=90.0, - content_validator=RangeValidator(ge=0, le=180), - ), - units='deg', - cif_handler=CifHandler(names=['_cell.angle_gamma']), - ) - - self._identity.category_code = 'cell' - - @property - def length_a(self): - """Descriptor for a-axis length in Å.""" - return self._length_a - - @length_a.setter - def length_a(self, value): - self._length_a.value = value - - @property - def length_b(self): - """Descriptor for b-axis length in Å.""" - return self._length_b - - @length_b.setter - def length_b(self, value): - self._length_b.value = value - - @property - def length_c(self): - """Descriptor for c-axis length in Å.""" - return self._length_c - - @length_c.setter - def length_c(self, value): - self._length_c.value = value - - @property - def angle_alpha(self): - """Descriptor for angle alpha in degrees.""" - return self._angle_alpha - - @angle_alpha.setter - def angle_alpha(self, value): - self._angle_alpha.value = value - - @property - def angle_beta(self): - """Descriptor for angle beta in degrees.""" - return self._angle_beta - - @angle_beta.setter - def angle_beta(self, value): - self._angle_beta.value = value - - @property - def angle_gamma(self): - """Descriptor for angle gamma in degrees.""" - return self._angle_gamma - - @angle_gamma.setter - def angle_gamma(self, value): - self._angle_gamma.value = value - - def _apply_cell_symmetry_constraints(self): - """Apply symmetry constraints to cell parameters.""" - dummy_cell = { - 'lattice_a': self.length_a.value, - 'lattice_b': self.length_b.value, - 'lattice_c': self.length_c.value, - 'angle_alpha': self.angle_alpha.value, - 'angle_beta': self.angle_beta.value, - 'angle_gamma': self.angle_gamma.value, - } - space_group_name = self._parent.space_group.name_h_m.value - - ecr.apply_cell_symmetry_constraints( - cell=dummy_cell, - name_hm=space_group_name, - ) - - self.length_a.value = dummy_cell['lattice_a'] - self.length_b.value = dummy_cell['lattice_b'] - self.length_c.value = dummy_cell['lattice_c'] - self.angle_alpha.value = dummy_cell['angle_alpha'] - self.angle_beta.value = dummy_cell['angle_beta'] - self.angle_gamma.value = dummy_cell['angle_gamma'] - - def _update(self, called_by_minimizer=False): - """Update cell parameters by applying symmetry constraints.""" - del called_by_minimizer - - self._apply_cell_symmetry_constraints() diff --git a/src/easydiffraction/sample_models/categories/space_group.py b/src/easydiffraction/sample_models/categories/space_group.py deleted file mode 100644 index 3e726b17..00000000 --- a/src/easydiffraction/sample_models/categories/space_group.py +++ /dev/null @@ -1,107 +0,0 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors -# SPDX-License-Identifier: BSD-3-Clause -"""Space group category for crystallographic sample models.""" - -from cryspy.A_functions_base.function_2_space_group import ACCESIBLE_NAME_HM_SHORT -from cryspy.A_functions_base.function_2_space_group import ( - get_it_coordinate_system_codes_by_it_number, -) -from cryspy.A_functions_base.function_2_space_group import get_it_number_by_name_hm_short - -from easydiffraction.core.category import CategoryItem -from easydiffraction.core.parameters import StringDescriptor -from easydiffraction.core.validation import AttributeSpec -from easydiffraction.core.validation import DataTypes -from easydiffraction.core.validation import MembershipValidator -from easydiffraction.io.cif.handler import CifHandler - - -class SpaceGroup(CategoryItem): - """Space group with Hermann–Mauguin symbol and IT code.""" - - def __init__( - self, - *, - name_h_m: str = None, - it_coordinate_system_code: str = None, - ) -> None: - super().__init__() - self._name_h_m: StringDescriptor = StringDescriptor( - name='name_h_m', - description='Hermann-Mauguin symbol of the space group.', - value_spec=AttributeSpec( - value=name_h_m, - type_=DataTypes.STRING, - default='P 1', - content_validator=MembershipValidator( - allowed=lambda: self._name_h_m_allowed_values - ), - ), - cif_handler=CifHandler( - names=[ - '_space_group.name_H-M_alt', - '_space_group_name_H-M_alt', - '_symmetry.space_group_name_H-M', - '_symmetry_space_group_name_H-M', - ] - ), - ) - self._it_coordinate_system_code: StringDescriptor = StringDescriptor( - name='it_coordinate_system_code', - description='A qualifier identifying which setting in IT is used.', - value_spec=AttributeSpec( - value=it_coordinate_system_code, - type_=DataTypes.STRING, - default=lambda: self._it_coordinate_system_code_default_value, - content_validator=MembershipValidator( - allowed=lambda: self._it_coordinate_system_code_allowed_values - ), - ), - cif_handler=CifHandler( - names=[ - '_space_group.IT_coordinate_system_code', - '_space_group_IT_coordinate_system_code', - '_symmetry.IT_coordinate_system_code', - '_symmetry_IT_coordinate_system_code', - ] - ), - ) - self._identity.category_code = 'space_group' - - def _reset_it_coordinate_system_code(self): - self._it_coordinate_system_code.value = self._it_coordinate_system_code_default_value - - @property - def _name_h_m_allowed_values(self): - return ACCESIBLE_NAME_HM_SHORT - - @property - def _it_coordinate_system_code_allowed_values(self): - name = self.name_h_m.value - it_number = get_it_number_by_name_hm_short(name) - codes = get_it_coordinate_system_codes_by_it_number(it_number) - codes = [str(code) for code in codes] - return codes if codes else [''] - - @property - def _it_coordinate_system_code_default_value(self): - return self._it_coordinate_system_code_allowed_values[0] - - @property - def name_h_m(self): - """Descriptor for Hermann–Mauguin symbol.""" - return self._name_h_m - - @name_h_m.setter - def name_h_m(self, value): - self._name_h_m.value = value - self._reset_it_coordinate_system_code() - - @property - def it_coordinate_system_code(self): - """Descriptor for IT coordinate system code.""" - return self._it_coordinate_system_code - - @it_coordinate_system_code.setter - def it_coordinate_system_code(self, value): - self._it_coordinate_system_code.value = value diff --git a/src/easydiffraction/sample_models/sample_model/__init__.py b/src/easydiffraction/sample_models/sample_model/__init__.py deleted file mode 100644 index 429f2648..00000000 --- a/src/easydiffraction/sample_models/sample_model/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors -# SPDX-License-Identifier: BSD-3-Clause diff --git a/src/easydiffraction/sample_models/sample_model/base.py b/src/easydiffraction/sample_models/sample_model/base.py deleted file mode 100644 index 7ae135de..00000000 --- a/src/easydiffraction/sample_models/sample_model/base.py +++ /dev/null @@ -1,183 +0,0 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors -# SPDX-License-Identifier: BSD-3-Clause - -from easydiffraction.core.datablock import DatablockItem -from easydiffraction.crystallography import crystallography as ecr -from easydiffraction.sample_models.categories.atom_sites import AtomSites -from easydiffraction.sample_models.categories.cell import Cell -from easydiffraction.sample_models.categories.space_group import SpaceGroup -from easydiffraction.utils.logging import console -from easydiffraction.utils.utils import render_cif - - -class SampleModelBase(DatablockItem): - """Base sample model and container for structural information. - - Holds space group, unit cell and atom-site categories. The - factory is responsible for creating rich instances from CIF; - this base accepts just the ``name`` and exposes helpers for - applying symmetry. - """ - - def __init__( - self, - *, - name, - ) -> None: - super().__init__() - self._name = name - self._cell: Cell = Cell() - self._space_group: SpaceGroup = SpaceGroup() - self._atom_sites: AtomSites = AtomSites() - self._identity.datablock_entry_name = lambda: self.name - - def __str__(self) -> str: - """Human-readable representation of this component.""" - name = self._log_name - items = ', '.join( - f'{k}={v}' - for k, v in { - 'cell': self.cell, - 'space_group': self.space_group, - 'atom_sites': self.atom_sites, - }.items() - ) - return f'<{name} ({items})>' - - @property - def name(self) -> str: - """Model name. - - Returns: - The user-facing identifier for this model. - """ - return self._name - - @name.setter - def name(self, new: str) -> None: - """Update model name.""" - self._name = new - - @property - def cell(self) -> Cell: - """Unit-cell category object.""" - return self._cell - - @cell.setter - def cell(self, new: Cell) -> None: - """Replace the unit-cell category object.""" - self._cell = new - - @property - def space_group(self) -> SpaceGroup: - """Space-group category object.""" - return self._space_group - - @space_group.setter - def space_group(self, new: SpaceGroup) -> None: - """Replace the space-group category object.""" - self._space_group = new - - @property - def atom_sites(self) -> AtomSites: - """Atom-sites collection for this model.""" - return self._atom_sites - - @atom_sites.setter - def atom_sites(self, new: AtomSites) -> None: - """Replace the atom-sites collection.""" - self._atom_sites = new - - # -------------------- - # Symmetry constraints - # -------------------- - - def _apply_cell_symmetry_constraints(self): - """Apply symmetry rules to unit-cell parameters in place.""" - dummy_cell = { - 'lattice_a': self.cell.length_a.value, - 'lattice_b': self.cell.length_b.value, - 'lattice_c': self.cell.length_c.value, - 'angle_alpha': self.cell.angle_alpha.value, - 'angle_beta': self.cell.angle_beta.value, - 'angle_gamma': self.cell.angle_gamma.value, - } - space_group_name = self.space_group.name_h_m.value - ecr.apply_cell_symmetry_constraints(cell=dummy_cell, name_hm=space_group_name) - self.cell.length_a.value = dummy_cell['lattice_a'] - self.cell.length_b.value = dummy_cell['lattice_b'] - self.cell.length_c.value = dummy_cell['lattice_c'] - self.cell.angle_alpha.value = dummy_cell['angle_alpha'] - self.cell.angle_beta.value = dummy_cell['angle_beta'] - self.cell.angle_gamma.value = dummy_cell['angle_gamma'] - - def _apply_atomic_coordinates_symmetry_constraints(self): - """Apply symmetry rules to fractional coordinates of atom - sites. - """ - space_group_name = self.space_group.name_h_m.value - space_group_coord_code = self.space_group.it_coordinate_system_code.value - for atom in self.atom_sites: - dummy_atom = { - 'fract_x': atom.fract_x.value, - 'fract_y': atom.fract_y.value, - 'fract_z': atom.fract_z.value, - } - wl = atom.wyckoff_letter.value - if not wl: - # TODO: Decide how to handle this case - # For now, we just skip applying constraints if wyckoff - # letter is not set. Alternatively, could raise an - # error or warning - # print(f"Warning: Wyckoff letter is not ...") - # raise ValueError("Wyckoff letter is not ...") - continue - ecr.apply_atom_site_symmetry_constraints( - atom_site=dummy_atom, - name_hm=space_group_name, - coord_code=space_group_coord_code, - wyckoff_letter=wl, - ) - atom.fract_x.value = dummy_atom['fract_x'] - atom.fract_y.value = dummy_atom['fract_y'] - atom.fract_z.value = dummy_atom['fract_z'] - - def _apply_atomic_displacement_symmetry_constraints(self): - """Placeholder for ADP symmetry constraints (not - implemented). - """ - pass - - def _apply_symmetry_constraints(self): - """Apply all available symmetry constraints to this model.""" - self._apply_cell_symmetry_constraints() - self._apply_atomic_coordinates_symmetry_constraints() - self._apply_atomic_displacement_symmetry_constraints() - - # ------------ - # Show methods - # ------------ - - def show_structure(self): - """Show an ASCII projection of the structure on a 2D plane.""" - console.paragraph(f"Sample model 🧩 '{self.name}' structure view") - console.print('Not implemented yet.') - - def show_params(self): - """Display structural parameters (space group, cell, atom - sites). - """ - console.print(f'\nSampleModel ID: {self.name}') - console.print(f'Space group: {self.space_group.name_h_m}') - console.print(f'Cell parameters: {self.cell.as_dict}') - console.print('Atom sites:') - self.atom_sites.show() - - def show_as_cif(self) -> None: - """Render the CIF text for this model in a terminal-friendly - view. - """ - cif_text: str = self.as_cif - paragraph_title: str = f"Sample model 🧩 '{self.name}' as cif" - console.paragraph(paragraph_title) - render_cif(cif_text) diff --git a/src/easydiffraction/sample_models/sample_model/factory.py b/src/easydiffraction/sample_models/sample_model/factory.py deleted file mode 100644 index 7a03b0f0..00000000 --- a/src/easydiffraction/sample_models/sample_model/factory.py +++ /dev/null @@ -1,103 +0,0 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors -# SPDX-License-Identifier: BSD-3-Clause -"""Factory for creating sample models from simple inputs or CIF. - -Supports three argument combinations: ``name``, ``cif_path``, or -``cif_str``. Returns a minimal ``SampleModelBase`` populated from CIF -when provided, or an empty model with the given name. -""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -from easydiffraction.core.factory import FactoryBase -from easydiffraction.io.cif.parse import document_from_path -from easydiffraction.io.cif.parse import document_from_string -from easydiffraction.io.cif.parse import name_from_block -from easydiffraction.io.cif.parse import pick_sole_block -from easydiffraction.sample_models.sample_model.base import SampleModelBase - -if TYPE_CHECKING: - import gemmi - - -class SampleModelFactory(FactoryBase): - """Create ``SampleModelBase`` instances from supported inputs.""" - - _ALLOWED_ARG_SPECS = [ - {'required': ['name'], 'optional': []}, - {'required': ['cif_path'], 'optional': []}, - {'required': ['cif_str'], 'optional': []}, - ] - - @classmethod - def _create_from_gemmi_block( - cls, - block: gemmi.cif.Block, - ) -> SampleModelBase: - """Build a model instance from a single CIF block.""" - name = name_from_block(block) - sample_model = SampleModelBase(name=name) - for category in sample_model.categories: - category.from_cif(block) - return sample_model - - @classmethod - def _create_from_cif_path( - cls, - cif_path: str, - ) -> SampleModelBase: - """Create a model by reading and parsing a CIF file.""" - doc = document_from_path(cif_path) - block = pick_sole_block(doc) - return cls._create_from_gemmi_block(block) - - @classmethod - def _create_from_cif_str( - cls, - cif_str: str, - ) -> SampleModelBase: - """Create a model by parsing a CIF string.""" - doc = document_from_string(cif_str) - block = pick_sole_block(doc) - return cls._create_from_gemmi_block(block) - - @classmethod - def _create_minimal( - cls, - name: str, - ) -> SampleModelBase: - """Create a minimal default model with just a name.""" - return SampleModelBase(name=name) - - @classmethod - def create(cls, **kwargs): - """Create a model based on a validated argument combination. - - Keyword Args: - name: Name of the sample model to create. - cif_path: Path to a CIF file to parse. - cif_str: Raw CIF string to parse. - **kwargs: Extra args are ignored if None; only the above - three keys are supported. - - Returns: - SampleModelBase: A populated or empty model instance. - """ - # TODO: move to FactoryBase - # Check for valid argument combinations - user_args = {k for k, v in kwargs.items() if v is not None} - cls._validate_args( - present=user_args, - allowed_specs=cls._ALLOWED_ARG_SPECS, - factory_name=cls.__name__, # TODO: move to FactoryBase - ) - - # Dispatch to the appropriate creation method - if 'cif_path' in kwargs: - return cls._create_from_cif_path(kwargs['cif_path']) - elif 'cif_str' in kwargs: - return cls._create_from_cif_str(kwargs['cif_str']) - elif 'name' in kwargs: - return cls._create_minimal(kwargs['name']) diff --git a/src/easydiffraction/sample_models/sample_models.py b/src/easydiffraction/sample_models/sample_models.py deleted file mode 100644 index bb85d2e9..00000000 --- a/src/easydiffraction/sample_models/sample_models.py +++ /dev/null @@ -1,87 +0,0 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors -# SPDX-License-Identifier: BSD-3-Clause - -from typeguard import typechecked - -from easydiffraction.core.datablock import DatablockCollection -from easydiffraction.sample_models.sample_model.base import SampleModelBase -from easydiffraction.sample_models.sample_model.factory import SampleModelFactory -from easydiffraction.utils.logging import console - - -class SampleModels(DatablockCollection): - """Collection manager for multiple SampleModel instances.""" - - def __init__(self) -> None: - super().__init__(item_type=SampleModelBase) - - # -------------------- - # Add / Remove methods - # -------------------- - - # TODO: Move to DatablockCollection? - # TODO: Disallow args and only allow kwargs? - def add(self, **kwargs): - sample_model = kwargs.pop('sample_model', None) - - if sample_model is None: - sample_model = SampleModelFactory.create(**kwargs) - - self._add(sample_model) - - # @typechecked - # def add_from_cif_path(self, cif_path: str) -> None: - # """Create and add a model from a CIF file path.# - # - # Args: - # cif_path: Path to a CIF file. - # """ - # sample_model = SampleModelFactory.create(cif_path=cif_path) - # self.add(sample_model) - - # @typechecked - # def add_from_cif_str(self, cif_str: str) -> None: - # """Create and add a model from CIF content (string). - # - # Args: - # cif_str: CIF file content. - # """ - # sample_model = SampleModelFactory.create(cif_str=cif_str) - # self.add(sample_model) - - # @typechecked - # def add_minimal(self, name: str) -> None: - # """Create and add a minimal model (defaults, no atoms). - # - # Args: - # name: Identifier to assign to the new model. - # """ - # sample_model = SampleModelFactory.create(name=name) - # self.add(sample_model) - - # TODO: Move to DatablockCollection? - @typechecked - def remove(self, name: str) -> None: - """Remove a sample model by its ID. - - Args: - name: ID of the model to remove. - """ - if name in self: - del self[name] - - # ------------ - # Show methods - # ------------ - - # TODO: Move to DatablockCollection? - def show_names(self) -> None: - """List all model names in the collection.""" - console.paragraph('Defined sample models' + ' 🧩') - console.print(self.names) - - # TODO: Move to DatablockCollection? - def show_params(self) -> None: - """Show parameters of all sample models in the collection.""" - for model in self.values(): - model.show_params() diff --git a/src/easydiffraction/summary/__init__.py b/src/easydiffraction/summary/__init__.py index 429f2648..4e798e20 100644 --- a/src/easydiffraction/summary/__init__.py +++ b/src/easydiffraction/summary/__init__.py @@ -1,2 +1,2 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause diff --git a/src/easydiffraction/summary/summary.py b/src/easydiffraction/summary/summary.py index 7d28c875..9d715bac 100644 --- a/src/easydiffraction/summary/summary.py +++ b/src/easydiffraction/summary/summary.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause from textwrap import wrap @@ -9,17 +9,21 @@ class Summary: - """Generates reports and exports results from the project. + """ + Generates reports and exports results from the project. This class collects and presents all relevant information about the fitted model, experiments, and analysis results. """ - def __init__(self, project) -> None: - """Initialize the summary with a reference to the project. + def __init__(self, project: object) -> None: + """ + Initialize the summary with a reference to the project. - Args: - project: The Project instance this summary belongs to. + Parameters + ---------- + project : object + The Project instance this summary belongs to. """ self.project = project @@ -28,6 +32,7 @@ def __init__(self, project) -> None: # ------------------------------------------ def show_report(self) -> None: + """Print a full project report covering all sections.""" self.show_project_info() self.show_crystallographic_data() self.show_experimental_data() @@ -52,12 +57,10 @@ def show_project_info(self) -> None: print('\n'.join(desc_lines)) def show_crystallographic_data(self) -> None: - """Print crystallographic data including phase datablocks, space - groups, cell parameters, and atom sites. - """ + """Print crystallographic data for all phases.""" console.section('Crystallographic data') - for model in self.project.sample_models.values(): + for model in self.project.structures.values(): console.paragraph('Phase datablock') console.print(f'🧩 {model.name}') @@ -114,9 +117,7 @@ def show_crystallographic_data(self) -> None: ) def show_experimental_data(self) -> None: - """Print experimental data including experiment datablocks, - types, instrument settings, and peak profile information. - """ + """Print experimental data for all experiments.""" console.section('Experiments') for expt in self.project.experiments.values(): @@ -174,13 +175,12 @@ def show_experimental_data(self) -> None: ) def show_fitting_details(self) -> None: - """Print fitting details including calculation and minimization - engines, and fit quality metrics. - """ + """Print fitting details including engines and metrics.""" console.section('Fitting') console.paragraph('Calculation engine') - console.print(self.project.analysis.current_calculator) + for expt in self.project.experiments.values(): + console.print(f' {expt.name}: {expt.calculator_type}') console.paragraph('Minimization engine') console.print(self.project.analysis.current_minimizer) @@ -205,9 +205,7 @@ def show_fitting_details(self) -> None: # ------------------------------------------ def as_cif(self) -> str: - """Export the final fitted data and analysis results as CIF - format. - """ + """Export fitted data and analysis results as CIF.""" from easydiffraction.io.cif.serialize import summary_to_cif return summary_to_cif(self) diff --git a/src/easydiffraction/utils/__init__.py b/src/easydiffraction/utils/__init__.py index e40e0816..53fde2c6 100644 --- a/src/easydiffraction/utils/__init__.py +++ b/src/easydiffraction/utils/__init__.py @@ -1,10 +1,5 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause from easydiffraction.utils.utils import _is_dev_version from easydiffraction.utils.utils import stripped_package_version - -__all__ = [ - '_is_dev_version', - 'stripped_package_version', -] diff --git a/src/easydiffraction/utils/_vendored/__init__.py b/src/easydiffraction/utils/_vendored/__init__.py index 20506363..c124e1af 100644 --- a/src/easydiffraction/utils/_vendored/__init__.py +++ b/src/easydiffraction/utils/_vendored/__init__.py @@ -1,23 +1,22 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -"""Vendored third-party modules. +""" +Vendored third-party modules. This package contains third-party code that has been vendored into the project to avoid external dependencies not available on conda-forge. -Packages: - jupyter_dark_detect/ - Vendored copy of jupyter_dark_detect (MIT License). - Check jupyter_dark_detect.__version__ for vendored version. +Packages: jupyter_dark_detect/ Vendored copy of +jupyter_dark_detect (MIT License). Check +jupyter_dark_detect.__version__ for vendored version. - To update, replace these files from upstream: - - https://github.com/OpenMined/jupyter-dark-detect/blob/main/jupyter_dark_detect/__init__.py - - https://github.com/OpenMined/jupyter-dark-detect/blob/main/jupyter_dark_detect/detector.py +To update, replace these files from upstream: - +https://github.com/OpenMined/jupyter-dark-detect/blob/main/jupyter_dark_detect/__init__.py +- +https://github.com/OpenMined/jupyter-dark-detect/blob/main/jupyter_dark_detect/detector.py - Then run 'pixi run fix' to format and check for issues. +Then run 'pixi run fix' to format and check for issues. -Modules: - theme_detect: - Custom wrapper around jupyter_dark_detect with optimized - detection order for EasyDiffraction's use case. +Modules: theme_detect: Custom wrapper around jupyter_dark_detect with +optimized detection order for EasyDiffraction's use case. """ diff --git a/src/easydiffraction/utils/_vendored/jupyter_dark_detect/__init__.py b/src/easydiffraction/utils/_vendored/jupyter_dark_detect/__init__.py index 11ef7b95..640883dd 100644 --- a/src/easydiffraction/utils/_vendored/jupyter_dark_detect/__init__.py +++ b/src/easydiffraction/utils/_vendored/jupyter_dark_detect/__init__.py @@ -1,7 +1,7 @@ -"""jupyter-dark-detect: Detect dark mode in Jupyter environments. +"""Jupyter-dark-detect: Detect dark mode in Jupyter environments. -This package provides a simple API to detect whether Jupyter Notebook/Lab -is running in dark mode across different environments. +This package provides a simple API to detect whether Jupyter +Notebook/Lab is running in dark mode across different environments. """ from .detector import is_dark diff --git a/src/easydiffraction/utils/_vendored/jupyter_dark_detect/detector.py b/src/easydiffraction/utils/_vendored/jupyter_dark_detect/detector.py index 8f34b5af..e247ba87 100644 --- a/src/easydiffraction/utils/_vendored/jupyter_dark_detect/detector.py +++ b/src/easydiffraction/utils/_vendored/jupyter_dark_detect/detector.py @@ -14,14 +14,11 @@ def is_dark() -> bool: """Check if Jupyter Notebook/Lab is running in dark mode. - This function attempts multiple detection strategies: - 1. JupyterLab theme settings files - 2. VS Code settings (when running in VS Code) - 3. JavaScript DOM inspection - 4. System preferences (macOS) - - Returns: - bool: True if dark mode is detected, False otherwise + This function attempts multiple detection strategies: 1. JupyterLab + theme settings files 2. VS Code settings (when running in VS Code) + 3. JavaScript DOM inspection 4. System preferences (macOS) + + Returns: bool: True if dark mode is detected, False otherwise """ # Try JupyterLab settings first result = _check_jupyterlab_settings() diff --git a/src/easydiffraction/utils/_vendored/theme_detect.py b/src/easydiffraction/utils/_vendored/theme_detect.py index 1d555f0a..9f75ee72 100644 --- a/src/easydiffraction/utils/_vendored/theme_detect.py +++ b/src/easydiffraction/utils/_vendored/theme_detect.py @@ -1,6 +1,7 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -"""Jupyter theme detection with custom detection order. +""" +Jupyter theme detection with custom detection order. This module wraps the vendored jupyter_dark_detect package and provides a custom detection order optimized for EasyDiffraction's use case. @@ -12,15 +13,12 @@ 3. JavaScript DOM inspection (for browser-based environments) 4. System preferences (macOS, Windows) - fallback only -Note: - The detection order differs from upstream jupyter_dark_detect. - We prioritize JavaScript DOM inspection over system preferences - because the Jupyter theme may differ from the system theme. +Note: The detection order differs from upstream jupyter_dark_detect. We +prioritize JavaScript DOM inspection over system preferences because the +Jupyter theme may differ from the system theme. -Example: - >>> from easydiffraction.utils._vendored.theme_detect import is_dark - >>> if is_dark(): - ... print('Dark mode detected') +Example: >>> from easydiffraction.utils._vendored.theme_detect import +is_dark >>> if is_dark(): ... print('Dark mode detected') """ from __future__ import annotations @@ -37,19 +35,22 @@ def is_dark() -> bool: - """Check if the Jupyter environment is running in dark mode. + """ + Check if the Jupyter environment is running in dark mode. This function uses a custom detection order that prioritizes Jupyter-specific detection over system preferences. Detection order: - 1. JupyterLab settings files (most reliable for JupyterLab) - 2. VS Code settings (when running in VS Code) - 3. JavaScript DOM inspection (for browser-based Jupyter) - 4. System preferences (fallback - may differ from Jupyter theme) + 1. JupyterLab settings files (most reliable for JupyterLab) 2. VS + Code settings (when running in VS Code) 3. JavaScript DOM inspection + (for browser-based Jupyter) 4. System preferences (fallback - may + differ from Jupyter theme) - Returns: + Returns + ------- + bool True if dark mode is detected, False otherwise. """ # Try Jupyter-specific methods first @@ -77,11 +78,14 @@ def is_dark() -> bool: def get_detection_result() -> dict[str, Optional[bool]]: - """Get results from all detection methods for debugging. + """ + Get results from all detection methods for debugging. - Returns: - Dictionary with detection method names as keys and their - results (True/False/None) as values. + Returns + ------- + dict[str, Optional[bool]] + Dictionary with detection method names as keys and their results + (True/False/None) as values. """ return { 'jupyterlab_settings': _check_jupyterlab_settings(), diff --git a/src/easydiffraction/utils/enums.py b/src/easydiffraction/utils/enums.py new file mode 100644 index 00000000..c4aac164 --- /dev/null +++ b/src/easydiffraction/utils/enums.py @@ -0,0 +1,30 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""General-purpose enumerations shared across the library.""" + +from __future__ import annotations + +from enum import Enum + + +class VerbosityEnum(str, Enum): + """ + Console output verbosity level. + + Controls how much information is printed during operations such as + data loading, fitting, and saving. + + Members + ------- + FULL Multi-line output with headers, tables, and details. SHORT + Single-line status messages per action. SILENT No console output. + """ + + FULL = 'full' + SHORT = 'short' + SILENT = 'silent' + + @classmethod + def default(cls) -> VerbosityEnum: + """Return the default verbosity (FULL).""" + return cls.FULL diff --git a/src/easydiffraction/utils/environment.py b/src/easydiffraction/utils/environment.py index aa7de5a6..2c518092 100644 --- a/src/easydiffraction/utils/environment.py +++ b/src/easydiffraction/utils/environment.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause from __future__ import annotations @@ -9,27 +9,50 @@ def in_pytest() -> bool: + """ + Determine whether the code is running inside a pytest session. + + Returns + ------- + bool + True if pytest is loaded, False otherwise. + """ return 'pytest' in sys.modules def in_warp() -> bool: + """ + Determine whether the terminal is the Warp terminal emulator. + + Returns + ------- + bool + True if the TERM_PROGRAM environment variable equals + ``'WarpTerminal'``, False otherwise. + """ return os.getenv('TERM_PROGRAM') == 'WarpTerminal' def in_pycharm() -> bool: - """Determines if the current environment is PyCharm. + """ + Check whether the current environment is PyCharm. - Returns: - bool: True if running inside PyCharm, False otherwise. + Returns + ------- + bool + True if running inside PyCharm, False otherwise. """ return os.environ.get('PYCHARM_HOSTED') == '1' def in_colab() -> bool: - """Determines if the current environment is Google Colab. + """ + Check whether the current environment is Google Colab. - Returns: - bool: True if running in Google Colab, False otherwise. + Returns + ------- + bool + True if running in Google Colab, False otherwise. """ try: return find_spec('google.colab') is not None @@ -38,10 +61,13 @@ def in_colab() -> bool: def in_jupyter() -> bool: - """Return True when running inside a Jupyter Notebook. + """ + Return True when running inside a Jupyter Notebook. - Returns: - bool: True if inside a Jupyter Notebook, False otherwise. + Returns + ------- + bool + True if inside a Jupyter Notebook, False otherwise. """ try: import IPython # type: ignore[import-not-found] @@ -76,11 +102,13 @@ def in_jupyter() -> bool: def in_github_ci() -> bool: - """Return True when running under GitHub Actions CI. + """ + Return True when running under GitHub Actions CI. - Returns: - bool: True if env var ``GITHUB_ACTIONS`` is set, False - otherwise. + Returns + ------- + bool + True if env var ``GITHUB_ACTIONS`` is set, False otherwise. """ return os.environ.get('GITHUB_ACTIONS') is not None @@ -91,12 +119,13 @@ def in_github_ci() -> bool: def is_ipython_display_handle(obj: object) -> bool: - """Return True if ``obj`` is an IPython DisplayHandle instance. + """ + Return True if ``obj`` is an IPython DisplayHandle instance. Tries to import ``IPython.display.DisplayHandle`` and uses - ``isinstance`` when available. Falls back to a conservative - module name heuristic if IPython is missing. Any errors result - in ``False``. + ``isinstance`` when available. Falls back to a conservative module + name heuristic if IPython is missing. Any errors result in + ``False``. """ try: # Fast path when IPython is available from IPython.display import DisplayHandle # type: ignore[import-not-found] @@ -115,7 +144,8 @@ def is_ipython_display_handle(obj: object) -> bool: def can_update_ipython_display() -> bool: - """Return True if IPython HTML display utilities are available. + """ + Return True if IPython HTML display utilities are available. This indicates we can safely construct ``IPython.display.HTML`` and update a display handle. @@ -129,7 +159,8 @@ def can_update_ipython_display() -> bool: def can_use_ipython_display(handle: object) -> bool: - """Return True if we can update the given IPython DisplayHandle. + """ + Return True if we can update the given IPython DisplayHandle. Combines type checking of the handle with availability of IPython HTML utilities. diff --git a/src/easydiffraction/utils/logging.py b/src/easydiffraction/utils/logging.py index 056d650b..876d7956 100644 --- a/src/easydiffraction/utils/logging.py +++ b/src/easydiffraction/utils/logging.py @@ -1,6 +1,7 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -"""Project-wide logging utilities built on top of Rich. +""" +Project-wide logging utilities built on top of Rich. Provides a shared Rich console, a compact/verbose logger with consistent formatting, Jupyter traceback handling, and a small printing façade @@ -43,9 +44,7 @@ class IconifiedRichHandler(RichHandler): - """RichHandler that uses icons for log levels in compact mode, Rich - default in verbose mode. - """ + """RichHandler using icons (compact) or names (verbose).""" _icons = { logging.CRITICAL: '💀', @@ -55,11 +54,24 @@ class IconifiedRichHandler(RichHandler): logging.INFO: 'ℹ️', } - def __init__(self, *args, mode: str = 'compact', **kwargs): + def __init__(self, *args: object, mode: str = 'compact', **kwargs: object) -> None: super().__init__(*args, **kwargs) self.mode = mode def get_level_text(self, record: logging.LogRecord) -> Text: + """ + Return an icon or level name for the log record. + + Parameters + ---------- + record : logging.LogRecord + The log record being rendered. + + Returns + ------- + Text + A Rich Text object with the level indicator. + """ if self.mode == 'compact': icon = self._icons.get(record.levelno, record.levelname) if in_warp() and not in_jupyter() and icon in ['⚠️', '⚙️', 'ℹ️']: @@ -70,9 +82,21 @@ def get_level_text(self, record: logging.LogRecord) -> Text: return super().get_level_text(record) def render_message(self, record: logging.LogRecord, message: str) -> Text: - # In compact mode, let the icon come from get_level_text and - # keep the message body unadorned. In verbose mode, defer to - # RichHandler. + """ + Render the log message body as a Rich Text object. + + Parameters + ---------- + record : logging.LogRecord + The log record being rendered. + message : str + Pre-formatted log message string. + + Returns + ------- + Text + A Rich Text object with the rendered message. + """ if self.mode == 'compact': try: return Text.from_markup(message) @@ -94,9 +118,12 @@ class ConsoleManager: @staticmethod def _detect_width() -> int: - """Detect a suitable console width for the shared Console. + """ + Detect a suitable console width for the shared Console. - Returns: + Returns + ------- + int The detected terminal width, clamped at ``_MIN_CONSOLE_WIDTH`` to avoid cramped layouts. """ @@ -134,13 +161,19 @@ def setup_handlers( rich_tracebacks: bool, mode: str = 'compact', ) -> None: - """Install Rich handler and optional Jupyter traceback support. - - Args: - logger: Logger instance to attach handlers to. - level: Minimum log level to emit. - rich_tracebacks: Whether to enable Rich tracebacks. - mode: Output mode name ("compact" or "verbose"). + """ + Install Rich handler and optional Jupyter traceback support. + + Parameters + ---------- + logger : logging.Logger + Logger instance to attach handlers to. + level : int + Minimum log level to emit. + rich_tracebacks : bool + Whether to enable Rich tracebacks. + mode : str, default='compact' + Output mode name ("compact" or "verbose"). """ logger.handlers.clear() logger.propagate = False @@ -175,13 +208,19 @@ def configure( level: 'Logger.Level', rich_tracebacks: bool, ) -> None: - """Configure the logger with RichHandler and exception hooks. - - Args: - logger: Logger instance to configure. - mode: Output mode (compact or verbose). - level: Minimum log level to emit. - rich_tracebacks: Whether to enable Rich tracebacks. + """ + Configure the logger with RichHandler and exception hooks. + + Parameters + ---------- + logger : logging.Logger + Logger instance to configure. + mode : 'Logger.Mode' + Output mode (compact or verbose). + level : 'Logger.Level' + Minimum log level to emit. + rich_tracebacks : bool + Whether to enable Rich tracebacks. """ LoggerConfig.setup_handlers( logger, @@ -204,10 +243,13 @@ class ExceptionHookManager: @staticmethod def install_verbose_hook(logger: logging.Logger) -> None: - """Install a verbose exception hook that prints rich tracebacks. + """ + Install a verbose exception hook that prints rich tracebacks. - Args: - logger: Logger used to emit the exception information. + Parameters + ---------- + logger : logging.Logger + Logger used to emit the exception information. """ if not hasattr(Logger, '_orig_excepthook'): Logger._orig_excepthook = sys.excepthook # type: ignore[attr-defined] @@ -217,6 +259,7 @@ def aligned_excepthook( exc: BaseException, tb: 'TracebackType | None', ) -> None: + """Log the exception with full traceback via Rich.""" original_args = getattr(exc, 'args', tuple()) message = str(exc) with suppress(Exception): @@ -233,10 +276,13 @@ def aligned_excepthook( @staticmethod def install_compact_hook(logger: logging.Logger) -> None: - """Install a compact exception hook that logs message-only. + """ + Install a compact exception hook that logs message-only. - Args: - logger: Logger used to emit the error message. + Parameters + ---------- + logger : logging.Logger + Logger used to emit the error message. """ if not hasattr(Logger, '_orig_excepthook'): Logger._orig_excepthook = sys.excepthook # type: ignore[attr-defined] @@ -246,33 +292,39 @@ def compact_excepthook( exc: BaseException, _tb: 'TracebackType | None', ) -> None: + """Log the exception message and exit.""" logger.error(str(exc)) raise SystemExit(1) sys.excepthook = compact_excepthook # type: ignore[assignment] @staticmethod - def restore_original_hook(): + def restore_original_hook() -> None: """Restore the original sys.excepthook if it was overridden.""" if hasattr(Logger, '_orig_excepthook'): sys.excepthook = Logger._orig_excepthook # type: ignore[attr-defined] # Jupyter-specific traceback suppression (inlined here) @staticmethod - def _suppress_traceback(logger): - """Build a Jupyter custom exception callback that logs only the - message. + def _suppress_traceback(logger: object) -> object: + """ + Build a Jupyter exception callback that logs the message only. - Args: - logger: Logger used to emit error messages. + Parameters + ---------- + logger : object + Logger used to emit error messages. - Returns: + Returns + ------- + object A callable suitable for IPython's set_custom_exc that suppresses full tracebacks and logs only the exception message. """ - def suppress_jupyter_traceback(*args, **kwargs): + def suppress_jupyter_traceback(*args: object, **kwargs: object) -> None: + """Log only the exception message.""" try: _evalue = ( args[2] if len(args) > 2 else kwargs.get('_evalue') or kwargs.get('evalue') @@ -286,11 +338,13 @@ def suppress_jupyter_traceback(*args, **kwargs): @staticmethod def install_jupyter_traceback_suppressor(logger: logging.Logger) -> None: - """Install a Jupyter/IPython custom exception handler that - suppresses tracebacks. + """ + Install a Jupyter/IPython exception handler for tracebacks. - Args: - logger: Logger used to emit error messages. + Parameters + ---------- + logger : logging.Logger + Logger used to emit error messages. """ try: from IPython import get_ipython @@ -311,11 +365,11 @@ def install_jupyter_traceback_suppressor(logger: logging.Logger) -> None: class Logger: - """Centralized logging with Rich formatting and two modes. + """ + Centralized logging with Rich formatting and two modes. - Environment variables: - ED_LOG_MODE: set default mode ('verbose' or 'compact') - ED_LOG_LEVEL: set default level ('DEBUG', 'INFO', etc.) + Environment variables: ED_LOG_MODE: set default mode ('verbose' or + 'compact') ED_LOG_LEVEL: set default level ('DEBUG', 'INFO', etc.) """ # --- Enums --- @@ -326,7 +380,8 @@ class Mode(Enum): COMPACT = 'compact' # single line; no traceback @classmethod - def default(cls): + def default(cls) -> Logger.Mode: + """Return the default output mode (compact).""" return cls.COMPACT class Level(IntEnum): @@ -339,7 +394,8 @@ class Level(IntEnum): CRITICAL = logging.CRITICAL @classmethod - def default(cls): + def default(cls) -> Logger.Level: + """Return the default log level (WARNING).""" return cls.WARNING class Reaction(Enum): @@ -349,7 +405,8 @@ class Reaction(Enum): WARN = auto() @classmethod - def default(cls): + def default(cls) -> Logger.Reaction: + """Return the default error reaction (RAISE).""" return cls.RAISE # --- Internal state --- @@ -369,15 +426,15 @@ def configure( reaction: Reaction | None = None, rich_tracebacks: bool | None = None, ) -> None: - """Configure logger. + """ + Configure logger. - mode: default COMPACT in Jupyter else VERBOSE - level: minimum log level - rich_tracebacks: override automatic choice + mode: default COMPACT in Jupyter else VERBOSE level: minimum log + level rich_tracebacks: override automatic choice - Environment variables: - ED_LOG_MODE: set default mode ('verbose' or 'compact') - ED_LOG_LEVEL: set default level ('DEBUG', 'INFO', etc.) + Environment variables: ED_LOG_MODE: set default mode ('verbose' + or 'compact') ED_LOG_LEVEL: set default level ('DEBUG', 'INFO', + etc.) """ env_mode = os.getenv('ED_LOG_MODE') env_level = os.getenv('ED_LOG_LEVEL') @@ -418,22 +475,44 @@ def configure( @classmethod def _install_jupyter_traceback_suppressor(cls) -> None: - """Install traceback suppressor in Jupyter, safely and lint- - clean. - """ + """Install the Jupyter traceback suppressor safely.""" ExceptionHookManager.install_jupyter_traceback_suppressor(cls._logger) # ===== Helpers ===== @classmethod def set_mode(cls, mode: Mode) -> None: + """ + Set the output mode and reconfigure the logger. + + Parameters + ---------- + mode : Mode + The desired output mode (VERBOSE or COMPACT). + """ cls.configure(mode=mode, level=cls.Level(cls._logger.level)) @classmethod def set_level(cls, level: Level) -> None: + """ + Set the minimum log level and reconfigure the logger. + + Parameters + ---------- + level : Level + The desired minimum log level. + """ cls.configure(mode=cls._mode, level=level) @classmethod def mode(cls) -> Mode: + """ + Return the currently active output mode. + + Returns + ------- + Mode + The current Logger.Mode value. + """ return cls._mode @classmethod @@ -478,22 +557,68 @@ def handle( @classmethod def debug(cls, *messages: str) -> None: + """ + Log one or more messages at DEBUG level. + + Parameters + ---------- + *messages : str + Message parts joined with a space before logging. + """ cls.handle(*messages, level=cls.Level.DEBUG, exc_type=None) @classmethod def info(cls, *messages: str) -> None: + """ + Log one or more messages at INFO level. + + Parameters + ---------- + *messages : str + Message parts joined with a space before logging. + """ cls.handle(*messages, level=cls.Level.INFO, exc_type=None) @classmethod def warning(cls, *messages: str, exc_type: type[BaseException] | None = None) -> None: + """ + Log one or more messages at WARNING level. + + Parameters + ---------- + *messages : str + Message parts joined with a space before logging. + exc_type : type[BaseException] | None, default=None + If provided, raise this exception type instead of logging. + """ cls.handle(*messages, level=cls.Level.WARNING, exc_type=exc_type) @classmethod def error(cls, *messages: str, exc_type: type[BaseException] = AttributeError) -> None: + """ + Log one or more messages at ERROR level. + + Parameters + ---------- + *messages : str + Message parts joined with a space before logging. + exc_type : type[BaseException], default=AttributeError + Exception type to raise in VERBOSE/COMPACT mode. + """ cls.handle(*messages, level=cls.Level.ERROR, exc_type=exc_type) @classmethod def critical(cls, *messages: str, exc_type: type[BaseException] = RuntimeError) -> None: + """ + Log one or more messages at CRITICAL level. + + Parameters + ---------- + *messages : str + Message parts joined with a space before logging. + exc_type : type[BaseException], default=RuntimeError + Exception type to raise in VERBOSE/COMPACT mode. + """ cls.handle(*messages, level=cls.Level.CRITICAL, exc_type=exc_type) @@ -503,20 +628,18 @@ def critical(cls, *messages: str, exc_type: type[BaseException] = RuntimeError) class ConsolePrinter: - """Printer utility that prints objects to the shared console with - left padding. - """ + """Printer utility for the shared console with left padding.""" _console = ConsoleManager.get() @classmethod - def print(cls, *objects, **kwargs): - """Print objects to the console with left padding. + def print(cls, *objects: object, **kwargs: object) -> None: + """ + Print objects to the console with left padding. - Renderables (Rich types like Text, Table, Panel, etc.) are - kept as-is. - - Non-renderables (ints, floats, Path, etc.) are converted to - str(). + kept as-is. - Non-renderables (ints, floats, Path, etc.) are + converted to str(). """ safe_objects = [] for obj in objects: @@ -538,6 +661,15 @@ def print(cls, *objects, **kwargs): @classmethod def paragraph(cls, title: str) -> None: + """ + Print a bold blue paragraph heading. + + Parameters + ---------- + title : str + Heading text; substrings enclosed in single quotes are + rendered without the bold-blue style. + """ parts = re.split(r"('.*?')", title) text = Text() for part in parts: @@ -552,7 +684,7 @@ def paragraph(cls, title: str) -> None: @classmethod def section(cls, title: str) -> None: - """Formats a section header with bold green text.""" + """Format a section header with bold green text.""" full_title = f'{title.upper()}' line = '—' * len(full_title) formatted = f'[bold green]{line}\n{full_title}\n{line}[/bold green]' @@ -562,9 +694,7 @@ def section(cls, title: str) -> None: @classmethod def chapter(cls, title: str) -> None: - """Formats a chapter header with bold magenta text, uppercase, - and padding. - """ + """Format a chapter header in bold magenta, uppercase.""" width = ConsoleManager._detect_width() symbol = '—' full_title = f' {title.upper()} ' diff --git a/src/easydiffraction/utils/utils.py b/src/easydiffraction/utils/utils.py index 2b6f9c50..5527629f 100644 --- a/src/easydiffraction/utils/utils.py +++ b/src/easydiffraction/utils/utils.py @@ -1,13 +1,12 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause from __future__ import annotations +import functools import json import pathlib -import re import urllib.request -from functools import lru_cache from importlib.metadata import PackageNotFoundError from importlib.metadata import version from typing import List @@ -30,13 +29,18 @@ def _validate_url(url: str) -> None: - """Validate that a URL uses only safe HTTP/HTTPS schemes. + """ + Validate that a URL uses only safe HTTP/HTTPS schemes. - Args: - url: The URL to validate. + Parameters + ---------- + url : str + The URL to validate. - Raises: - ValueError: If the URL scheme is not HTTP or HTTPS. + Raises + ------ + ValueError + If the URL scheme is not HTTP or HTTPS. """ parsed = urlparse(url) if parsed.scheme not in ('http', 'https'): @@ -44,16 +48,15 @@ def _validate_url(url: str) -> None: def _filename_for_id_from_url(data_id: int | str, url: str) -> str: - """Return local filename like 'ed-12.xye' using extension from the - URL. - """ + """Return local filename using the extension from the URL.""" suffix = pathlib.Path(urlparse(url).path).suffix # includes leading dot ('.cif', '.xye', ...) # If URL has no suffix, fall back to no extension. return f'ed-{data_id}{suffix}' def _normalize_known_hash(value: str | None) -> str | None: - """Return pooch-compatible known_hash or None. + """ + Return pooch-compatible known_hash or None. Treat placeholder values like 'sha256:...' as unset. """ @@ -65,16 +68,13 @@ def _normalize_known_hash(value: str | None) -> str | None: return value -@lru_cache(maxsize=1) def _fetch_data_index() -> dict: - """Fetch & cache the diffraction data index.json and return it as - dict. - """ + """Fetch and cache the diffraction data index.json.""" index_url = 'https://raw.githubusercontent.com/easyscience/data/refs/heads/master/diffraction/index.json' _validate_url(index_url) # macOS: sha256sum index.json - index_hash = 'sha256:9aceaf51d298992058c80903283c9a83543329a063692d49b7aaee1156e76884' + index_hash = 'sha256:f421aab32ec532782dc62f4440a97320e5cec23b9e64f5ae3f8a3e818d013430' destination_dirname = 'easydiffraction' destination_fname = 'data-index.json' cache_dir = pooch.os_cache(destination_dirname) @@ -91,20 +91,22 @@ def _fetch_data_index() -> dict: return json.load(f) -@lru_cache(maxsize=1) +@functools.lru_cache(maxsize=1) def _fetch_tutorials_index() -> dict: - """Fetch & cache the tutorials index.json from gh-pages and return - it as dict. + """ + Fetch and cache the tutorials index.json from gh-pages. The index is fetched from: https://easyscience.github.io/diffraction-lib/{version}/tutorials/index.json - For released versions, {version} is the public version string - (e.g., '0.8.0.post1'). For development versions, 'dev' is used. + For released versions, {version} is the public version string (e.g., + '0.8.0.post1'). For development versions, 'dev' is used. - Returns: - dict: The tutorials index as a dictionary, or empty dict if - fetch fails. + Returns + ------- + dict + The tutorials index as a dictionary, or empty dict if fetch + fails. """ version = _get_version_for_url() index_url = f'https://easyscience.github.io/diffraction-lib/{version}/tutorials/index.json' @@ -126,24 +128,29 @@ def download_data( destination: str = 'data', overwrite: bool = False, ) -> str: - """Download a dataset by numeric ID using the remote diffraction - index. - - Example: - path = download_data(id=12, destination="data") + """ + Download a dataset by numeric ID using the remote diffraction index. - Args: - id: Numeric dataset id (e.g. 12). - destination: Directory to save the file into (created if - missing). - overwrite: Whether to overwrite the file if it already exists. + Example: path = download_data(id=12, destination="data") - Returns: - str: Full path to the downloaded file as string. + Parameters + ---------- + id : int | str + Numeric dataset id (e.g. 12). + destination : str, default='data' + Directory to save the file into (created if missing). + overwrite : bool, default=False + Whether to overwrite the file if it already exists. + + Returns + ------- + str + Full path to the downloaded file as string. - Raises: - KeyError: If the id is not found in the index. - ValueError: If the resolved URL is not HTTP/HTTPS. + Raises + ------ + KeyError + If the id is not found in the index. """ index = _fetch_data_index() key = str(id) @@ -196,14 +203,19 @@ def download_data( def package_version(package_name: str) -> str | None: - """Get the installed version string of the specified package. + """ + Get the installed version string of the specified package. - Args: - package_name (str): The name of the package to query. + Parameters + ---------- + package_name : str + The name of the package to query. - Returns: - str | None: The raw version string (may include local part, - e.g., '1.2.3+abc123'), or None if the package is not installed. + Returns + ------- + str | None + The raw version string (may include local part, e.g., + '1.2.3+abc123'), or None if the package is not installed. """ try: return version(package_name) @@ -212,18 +224,22 @@ def package_version(package_name: str) -> str | None: def stripped_package_version(package_name: str) -> str | None: - """Get the installed version of the specified package, stripped of - any local version part. + """ + Get installed package version, stripped of local version parts. Returns only the public version segment (e.g., '1.2.3' or '1.2.3.post4'), omitting any local segment (e.g., '+d136'). - Args: - package_name (str): The name of the package to query. + Parameters + ---------- + package_name : str + The name of the package to query. - Returns: - str | None: The public version string, or None if the package - is not installed. + Returns + ------- + str | None + The public version string, or None if the package is not + installed. """ v_str = package_version(package_name) if v_str is None: @@ -236,21 +252,22 @@ def stripped_package_version(package_name: str) -> str | None: def _is_dev_version(package_name: str) -> bool: - """Check if the installed package version is a development/local - version. + """ + Check if the installed package version is a dev version. - A version is considered "dev" if: - - The raw version contains '+dev', '+dirty', or '+devdirty' (local - suffixes from versioningit) - - The public version is '999.0.0' (versioningit default-tag - fallback) + A version is considered "dev" if: - The raw version contains '+dev', + '+dirty', or '+devdirty' (local suffixes from versioningit) - The + public version is '999.0.0' (versioningit default-tag fallback) - Args: - package_name (str): The name of the package to query. + Parameters + ---------- + package_name : str + The name of the package to query. - Returns: - bool: True if the version is a development version, False - otherwise. + Returns + ------- + bool + True if the version is a development version, False otherwise. """ raw_version = package_version(package_name) if raw_version is None: @@ -266,26 +283,31 @@ def _is_dev_version(package_name: str) -> bool: def _get_version_for_url(package_name: str = 'easydiffraction') -> str: - """Get the version string to use in URLs for fetching remote - resources. + """ + Get the version string to use in URLs for fetching remote resources. Returns the public version for released versions, or 'dev' for development/local versions. - Args: - package_name (str): The name of the package to query. + Parameters + ---------- + package_name : str, default='easydiffraction' + The name of the package to query. - Returns: - str: The version string to use in URLs ('dev' or a version like - '0.8.0.post1'). + Returns + ------- + str + The version string to use in URLs ('dev' or a version like + '0.8.0.post1'). """ if _is_dev_version(package_name): return 'dev' return stripped_package_version(package_name) or 'dev' -def _safe_urlopen(request_or_url): # type: ignore[no-untyped-def] - """Wrapper for urlopen with prior validation. +def _safe_urlopen(request_or_url: object) -> object: # type: ignore[no-untyped-def] + """ + Open a URL with prior validation. Centralises lint suppression for validated HTTPS requests. """ @@ -302,25 +324,29 @@ def _safe_urlopen(request_or_url): # type: ignore[no-untyped-def] def _resolve_tutorial_url(url_template: str) -> str: - """Replace {version} placeholder in URL template with actual - version. + """ + Replace {version} placeholder in URL template with actual version. - Args: - url_template (str): URL template containing {version} - placeholder. + Parameters + ---------- + url_template : str + URL template containing {version} placeholder. - Returns: - str: URL with {version} replaced by actual version string. + Returns + ------- + str + URL with {version} replaced by actual version string. """ version = _get_version_for_url() return url_template.replace('{version}', version) def list_tutorials() -> None: - """Display a table of available tutorial notebooks. + """ + Display a table of available tutorial notebooks. - Shows tutorial ID, filename, title, and description for all - tutorials available for the current version of easydiffraction. + Shows tutorial ID, filename and title for all tutorials available + for the current version of easydiffraction. """ index = _fetch_tutorials_index() if not index: @@ -328,19 +354,17 @@ def list_tutorials() -> None: return version = _get_version_for_url() - console.print(f'Tutorials available for easydiffraction v{version}:') - console.print('') + console.paragraph(f'Tutorials available for easydiffraction v{version}:') - columns_headers = ['id', 'file', 'title', 'description'] - columns_alignment = ['right', 'left', 'left', 'left'] + columns_headers = ['id', 'file', 'title'] + columns_alignment = ['right', 'left', 'left'] columns_data = [] - for tutorial_id in sorted(index.keys(), key=lambda x: int(x) if x.isdigit() else x): + for tutorial_id in index: record = index[tutorial_id] filename = f'ed-{tutorial_id}.ipynb' title = record.get('title', '') - description = record.get('description', '') - columns_data.append([tutorial_id, filename, title, description]) + columns_data.append([tutorial_id, filename, title]) render_table( columns_headers=columns_headers, @@ -354,23 +378,29 @@ def download_tutorial( destination: str = 'tutorials', overwrite: bool = False, ) -> str: - """Download a tutorial notebook by numeric ID. - - Example: - path = download_tutorial(id=1, destination="tutorials") + """ + Download a tutorial notebook by numeric ID. - Args: - id: Numeric tutorial id (e.g. 1). - destination: Directory to save the file into (created if - missing). - overwrite: Whether to overwrite the file if it already exists. + Example: path = download_tutorial(id=1, destination="tutorials") - Returns: - str: Full path to the downloaded file as string. + Parameters + ---------- + id : int | str + Numeric tutorial id (e.g. 1). + destination : str, default='tutorials' + Directory to save the file into (created if missing). + overwrite : bool, default=False + Whether to overwrite the file if it already exists. + + Returns + ------- + str + Full path to the downloaded file as string. - Raises: - KeyError: If the id is not found in the index. - ValueError: If the resolved URL is not HTTP/HTTPS. + Raises + ------ + KeyError + If the id is not found in the index. """ index = _fetch_tutorials_index() key = str(id) @@ -421,18 +451,22 @@ def download_all_tutorials( destination: str = 'tutorials', overwrite: bool = False, ) -> list[str]: - """Download all available tutorial notebooks. + """ + Download all available tutorial notebooks. - Example: - paths = download_all_tutorials(destination="tutorials") + Example: paths = download_all_tutorials(destination="tutorials") - Args: - destination: Directory to save the files into (created if - missing). - overwrite: Whether to overwrite files if they already exist. + Parameters + ---------- + destination : str, default='tutorials' + Directory to save the files into (created if missing). + overwrite : bool, default=False + Whether to overwrite files if they already exist. - Returns: - list[str]: List of full paths to the downloaded files. + Returns + ------- + list[str] + List of full paths to the downloaded files. """ index = _fetch_tutorials_index() if not index: @@ -459,11 +493,7 @@ def download_all_tutorials( def show_version() -> None: - """Print the installed version of the easydiffraction package. - - Args: - None - """ + """Print the installed version of the easydiffraction package.""" current_ed_version = package_version('easydiffraction') console.print(f'Current easydiffraction v{current_ed_version}') @@ -471,11 +501,27 @@ def show_version() -> None: # TODO: This is a temporary utility function. Complete migration to # TableRenderer (as e.g. in show_all_params) and remove this. def render_table( - columns_data, - columns_alignment, - columns_headers=None, - display_handle=None, -): + columns_data: object, + columns_alignment: object, + columns_headers: object = None, + display_handle: object = None, +) -> None: + """ + Render tabular data to the active display backend. + + Parameters + ---------- + columns_data : object + A list of rows, where each row is a list of cell values. + columns_alignment : object + A list of alignment strings (e.g. ``'left'``, ``'right'``, + ``'center'``) matching the number of columns. + columns_headers : object, default=None + Optional list of column header strings. + display_handle : object, default=None + Optional display handle for in-place updates (e.g. in Jupyter or + a terminal Live context). + """ headers = [ (col, align) for col, align in zip(columns_headers, columns_alignment, strict=False) ] @@ -485,12 +531,14 @@ def render_table( tabler.render(df, display_handle=display_handle) -def render_cif(cif_text) -> None: - """Display the CIF text as a formatted table in Jupyter Notebook or - terminal. +def render_cif(cif_text: str) -> None: + """ + Display CIF text as a formatted table in Jupyter or terminal. - Args: - cif_text: The CIF text to display. + Parameters + ---------- + cif_text : str + The CIF text to display. """ # Split into lines lines: List[str] = [line for line in cif_text.splitlines()] @@ -511,37 +559,42 @@ def tof_to_d( offset: float, linear: float, quad: float, - quad_eps=1e-20, + quad_eps: float = 1e-20, ) -> np.ndarray: - """Convert time-of-flight (TOF) to d-spacing using a quadratic - calibration. - - Model: - TOF = offset + linear * d + quad * d² - - The function: - - Uses a linear fallback when the quadratic term is effectively - zero. - - Solves the quadratic for d and selects the smallest positive, - finite root. - - Returns NaN where no valid solution exists. - - Expects ``tof`` as a NumPy array; output matches its shape. - - Args: - tof (np.ndarray): Time-of-flight values (µs). Must be a NumPy - array. - offset (float): Calibration offset (µs). - linear (float): Linear calibration coefficient (µs/Å). - quad (float): Quadratic calibration coefficient (µs/Ų). - quad_eps (float, optional): Threshold to treat ``quad`` as zero. - Defaults to 1e-20. - - Returns: - np.ndarray: d-spacing values (Å), NaN where invalid. - - Raises: - TypeError: If ``tof`` is not a NumPy array or coefficients are - not real numbers. + """ + Convert time-of-flight to d-spacing using quadratic calibration. + + Model: TOF = offset + linear * d + quad * d² + + The function: - Uses a linear fallback when the quadratic term is + effectively zero. - Solves the quadratic for d and selects the + smallest positive, finite root. - Returns NaN where no valid + solution exists. - Expects ``tof`` as a NumPy array; output matches + its shape. + + Parameters + ---------- + tof : np.ndarray + Time-of-flight values (µs). Must be a NumPy array. + offset : float + Calibration offset (µs). + linear : float + Linear calibration coefficient (µs/Å). + quad : float + Quadratic calibration coefficient (µs/Ų). + quad_eps : float, default=1e-20 + Threshold to treat ``quad`` as zero. + + Returns + ------- + np.ndarray + d-spacing values (Å), NaN where invalid. + + Raises + ------ + TypeError + If ``tof`` is not a NumPy array or coefficients are not real + numbers. """ # Type checks if not isinstance(tof, np.ndarray): @@ -595,15 +648,21 @@ def tof_to_d( return d_out -def twotheta_to_d(twotheta, wavelength): - """Convert 2-theta to d-spacing using Bragg's law. +def twotheta_to_d(twotheta: object, wavelength: float) -> object: + """ + Convert 2-theta to d-spacing using Bragg's law. - Parameters: - twotheta (float or np.ndarray): 2-theta angle in degrees. - wavelength (float): Wavelength in Å. + Parameters + ---------- + twotheta : object + 2-theta angle in degrees (float or np.ndarray). + wavelength : float + Wavelength in Å. - Returns: - d (float or np.ndarray): d-spacing in Å. + Returns + ------- + object + d-spacing in Å (float or np.ndarray). """ # Convert twotheta from degrees to radians theta_rad = np.radians(twotheta / 2) @@ -614,62 +673,53 @@ def twotheta_to_d(twotheta, wavelength): return d -def get_value_from_xye_header(file_path, key): - """Extracts a floating point value from the first line of the file, - corresponding to the given key. - - Parameters: - file_path (str): Path to the input file. - key (str): The key to extract ('DIFC' or 'two_theta'). +def sin_theta_over_lambda_to_d_spacing(sin_theta_over_lambda: object) -> object: + """ + Convert sin(theta)/lambda to d-spacing. - Returns: - float: The extracted value. + Parameters + ---------- + sin_theta_over_lambda : object + sin(theta)/lambda in 1/Å (float or np.ndarray). - Raises: - ValueError: If the key is not found. + Returns + ------- + object + d-spacing in Å (float or np.ndarray). """ - pattern = rf'{key}\s*=\s*([-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?)' + # Avoid division by zero + with np.errstate(divide='ignore', invalid='ignore'): + d = 1 / (2 * sin_theta_over_lambda) + # Set non-positive inputs to NaN + d = np.where(sin_theta_over_lambda > 0, d, np.nan) + return d - with pathlib.Path(file_path).open('r') as f: - first_line = f.readline() - match = re.search(pattern, first_line) - if match: - return float(match.group(1)) - else: - raise ValueError(f'{key} not found in the header.') +def str_to_ufloat(s: Optional[str], default: Optional[float] = None) -> UFloat: + """ + Parse a CIF-style numeric string into a ufloat. + Examples of supported input: - "3.566" → ufloat(3.566, nan) - + "3.566(2)" → ufloat(3.566, 0.002) - None → ufloat(default, nan) -def str_to_ufloat(s: Optional[str], default: Optional[float] = None) -> UFloat: - """Parse a CIF-style numeric string into a `ufloat` with an optional - uncertainty. - - Examples of supported input: - - "3.566" → ufloat(3.566, nan) - - "3.566(2)" → ufloat(3.566, 0.002) - - None → ufloat(default, nan) - - Behavior: - - If the input string contains a value with parentheses (e.g. - "3.566(2)"), the number in parentheses is interpreted as an - estimated standard deviation (esd) in the last digit(s). - - If the input string has no parentheses, an uncertainty of NaN is - assigned to indicate "no esd provided". - - If parsing fails, the function falls back to the given `default` - value with uncertainty NaN. + Behavior: - If the input string contains a value with parentheses + (e.g. "3.566(2)"), the number in parentheses is interpreted as an + estimated standard deviation (esd) in the last digit(s). - If the + input string has no parentheses, an uncertainty of NaN is assigned + to indicate "no esd provided". - If parsing fails, the function + falls back to the given ``default`` value with uncertainty NaN. Parameters ---------- - s : str or None + s : Optional[str] Numeric string in CIF format (e.g. "3.566", "3.566(2)") or None. - default : float or None, optional - Default value to use if `s` is None or parsing fails. - Defaults to None. + default : Optional[float], default=None + Default value to use if ``s`` is None or parsing fails. - Returns: + Returns ------- UFloat - An `uncertainties.UFloat` object with the parsed value and + An ``uncertainties.UFloat`` object with the parsed value and uncertainty. The uncertainty will be NaN if not specified or parsing failed. """ diff --git a/tests/integration/fitting/test_multi.py b/tests/integration/fitting/test_multi.py new file mode 100644 index 00000000..6b1a0c8f --- /dev/null +++ b/tests/integration/fitting/test_multi.py @@ -0,0 +1,236 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + +import tempfile + +from numpy.testing import assert_almost_equal + +import easydiffraction as ed +from easydiffraction import ExperimentFactory +from easydiffraction import Project +from easydiffraction import StructureFactory +from easydiffraction import download_data + +TEMP_DIR = tempfile.gettempdir() + + +def test_single_fit_neutron_pd_tof_mcstas_lbco_si() -> None: + # Set structures + model_1 = StructureFactory.from_scratch(name='lbco') + model_1.space_group.name_h_m = 'P m -3 m' + model_1.space_group.it_coordinate_system_code = '1' + model_1.cell.length_a = 3.8909 + model_1.atom_sites.create( + label='La', + type_symbol='La', + fract_x=0, + fract_y=0, + fract_z=0, + wyckoff_letter='a', + b_iso=0.2, + occupancy=0.5, + ) + model_1.atom_sites.create( + label='Ba', + type_symbol='Ba', + fract_x=0, + fract_y=0, + fract_z=0, + wyckoff_letter='a', + b_iso=0.2, + occupancy=0.5, + ) + model_1.atom_sites.create( + label='Co', + type_symbol='Co', + fract_x=0.5, + fract_y=0.5, + fract_z=0.5, + wyckoff_letter='b', + b_iso=0.2567, + ) + model_1.atom_sites.create( + label='O', + type_symbol='O', + fract_x=0, + fract_y=0.5, + fract_z=0.5, + wyckoff_letter='c', + b_iso=1.4041, + ) + + model_2 = StructureFactory.from_scratch(name='si') + model_2.space_group.name_h_m = 'F d -3 m' + model_2.space_group.it_coordinate_system_code = '2' + model_2.cell.length_a = 5.43146 + model_2.atom_sites.create( + label='Si', + type_symbol='Si', + fract_x=0.0, + fract_y=0.0, + fract_z=0.0, + wyckoff_letter='a', + b_iso=0.0, + ) + + # Set experiment + data_path = download_data(id=8, destination=TEMP_DIR) + expt = ExperimentFactory.from_data_path( + name='mcstas', + data_path=data_path, + beam_mode='time-of-flight', + ) + expt.instrument.setup_twotheta_bank = 94.90931761529106 + expt.instrument.calib_d_to_tof_offset = 0.0 + expt.instrument.calib_d_to_tof_linear = 58724.76869981215 + expt.instrument.calib_d_to_tof_quad = -0.00001 + expt.peak_profile_type = 'pseudo-voigt * ikeda-carpenter' + expt.peak.broad_gauss_sigma_0 = 45137 + expt.peak.broad_gauss_sigma_1 = -52394 + expt.peak.broad_gauss_sigma_2 = 22998 + expt.peak.broad_mix_beta_0 = 0.0055 + expt.peak.broad_mix_beta_1 = 0.0041 + expt.peak.asym_alpha_0 = 0.0 + expt.peak.asym_alpha_1 = 0.0097 + expt.linked_phases.create(id='lbco', scale=4.0) + expt.linked_phases.create(id='si', scale=0.2) + for x in range(45000, 115000, 5000): + expt.background.create(id=str(x), x=x, y=0.2) + + # Create project + project = Project() + project.structures.add(model_1) + project.structures.add(model_2) + project.experiments.add(expt) + + # Exclude regions from fitting + project.experiments['mcstas'].excluded_regions.create(start=108000, end=200000) + + # Prepare for fitting + project.analysis.current_minimizer = 'lmfit' + + # Select fitting parameters + model_1.cell.length_a.free = True + model_1.atom_sites['La'].b_iso.free = True + model_1.atom_sites['Ba'].b_iso.free = True + model_1.atom_sites['Co'].b_iso.free = True + model_1.atom_sites['O'].b_iso.free = True + model_2.cell.length_a.free = True + model_2.atom_sites['Si'].b_iso.free = True + expt.linked_phases['lbco'].scale.free = True + expt.linked_phases['si'].scale.free = True + expt.peak.broad_gauss_sigma_0.free = True + expt.peak.broad_gauss_sigma_1.free = True + expt.peak.broad_gauss_sigma_2.free = True + expt.peak.asym_alpha_1.free = True + expt.peak.broad_mix_beta_0.free = True + expt.peak.broad_mix_beta_1.free = True + for point in expt.background: + point.y.free = True + + # Perform fit + project.analysis.fit() + + # Compare fit quality + assert_almost_equal( + project.analysis.fit_results.reduced_chi_square, + desired=2.87, + decimal=1, + ) + + +def _test_joint_fit_bragg_pdf_neutron_pd_tof_si() -> None: + # Set structure (shared between Bragg and PDF experiments) + model = StructureFactory.from_scratch(name='si') + model.space_group.name_h_m = 'F d -3 m' + model.space_group.it_coordinate_system_code = '2' + model.cell.length_a = 5.431 + model.atom_sites.create( + label='Si', + type_symbol='Si', + fract_x=0.125, + fract_y=0.125, + fract_z=0.125, + b_iso=0.5, + ) + + # Set Bragg experiment (SEPD, TOF) + bragg_data_path = download_data(id=7, destination=TEMP_DIR) + bragg_expt = ExperimentFactory.from_data_path( + name='sepd', + data_path=bragg_data_path, + beam_mode='time-of-flight', + ) + bragg_expt.instrument.setup_twotheta_bank = 144.845 + bragg_expt.instrument.calib_d_to_tof_offset = 0.0 + bragg_expt.instrument.calib_d_to_tof_linear = 7476.91 + bragg_expt.instrument.calib_d_to_tof_quad = -1.54 + bragg_expt.peak_profile_type = 'pseudo-voigt * ikeda-carpenter' + bragg_expt.peak.broad_gauss_sigma_0 = 3.0 + bragg_expt.peak.broad_gauss_sigma_1 = 40.0 + bragg_expt.peak.broad_gauss_sigma_2 = 2.0 + bragg_expt.peak.broad_mix_beta_0 = 0.04221 + bragg_expt.peak.broad_mix_beta_1 = 0.00946 + bragg_expt.peak.asym_alpha_0 = 0.0 + bragg_expt.peak.asym_alpha_1 = 0.5971 + bragg_expt.linked_phases.create(id='si', scale=10.0) + for x in range(0, 35000, 5000): + bragg_expt.background.create(id=str(x), x=x, y=200) + + # Set PDF experiment (NOMAD, TOF) + pdf_data_path = ed.download_data(id=5, destination=TEMP_DIR) + pdf_expt = ExperimentFactory.from_data_path( + name='nomad', + data_path=pdf_data_path, + beam_mode='time-of-flight', + scattering_type='total', + ) + pdf_expt.peak.damp_q = 0.02 + pdf_expt.peak.broad_q = 0.03 + pdf_expt.peak.cutoff_q = 35.0 + pdf_expt.peak.sharp_delta_1 = 0.0 + pdf_expt.peak.sharp_delta_2 = 4.0 + pdf_expt.peak.damp_particle_diameter = 0 + pdf_expt.linked_phases.create(id='si', scale=1.0) + + # Create project + project = Project() + project.structures.add(model) + project.experiments.add(bragg_expt) + project.experiments.add(pdf_expt) + + # Prepare for fitting + project.analysis.fit_mode.mode = 'joint' + project.analysis.current_minimizer = 'lmfit' + + # Select fitting parameters — shared structure + model.cell.length_a.free = True + model.atom_sites['Si'].b_iso.free = True + + # Select fitting parameters — Bragg experiment + bragg_expt.linked_phases['si'].scale.free = True + bragg_expt.instrument.calib_d_to_tof_offset.free = True + for point in bragg_expt.background: + point.y.free = True + + # Select fitting parameters — PDF experiment + pdf_expt.linked_phases['si'].scale.free = True + pdf_expt.peak.damp_q.free = True + pdf_expt.peak.broad_q.free = True + pdf_expt.peak.sharp_delta_1.free = True + pdf_expt.peak.sharp_delta_2.free = True + + # Perform fit + project.analysis.fit() + + # Compare fit quality + assert_almost_equal( + project.analysis.fit_results.reduced_chi_square, + desired=8978.39, + decimal=-2, + ) + + +if __name__ == '__main__': + test_single_fit_neutron_pd_tof_mcstas_lbco_si() + # test_joint_fit_bragg_pdf_neutron_pd_tof_si() diff --git a/tests/integration/fitting/test_pair-distribution-function.py b/tests/integration/fitting/test_pair-distribution-function.py index 6af36e52..066e727e 100644 --- a/tests/integration/fitting/test_pair-distribution-function.py +++ b/tests/integration/fitting/test_pair-distribution-function.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause import tempfile @@ -14,13 +14,13 @@ def test_single_fit_pdf_xray_pd_cw_nacl() -> None: project = ed.Project() - # Set sample model - project.sample_models.add(name='nacl') - sample_model = project.sample_models['nacl'] - sample_model.space_group.name_h_m = 'F m -3 m' - sample_model.space_group.it_coordinate_system_code = '1' - sample_model.cell.length_a = 5.6018 - sample_model.atom_sites.add( + # Set structure + project.structures.create(name='nacl') + structure = project.structures['nacl'] + structure.space_group.name_h_m = 'F m -3 m' + structure.space_group.it_coordinate_system_code = '1' + structure.cell.length_a = 5.6018 + structure.atom_sites.create( label='Na', type_symbol='Na', fract_x=0, @@ -29,7 +29,7 @@ def test_single_fit_pdf_xray_pd_cw_nacl() -> None: wyckoff_letter='a', b_iso=1.1053, ) - sample_model.atom_sites.add( + structure.atom_sites.create( label='Cl', type_symbol='Cl', fract_x=0.5, @@ -41,7 +41,7 @@ def test_single_fit_pdf_xray_pd_cw_nacl() -> None: # Set experiment data_path = ed.download_data(id=4, destination=TEMP_DIR) - project.experiments.add( + project.experiments.add_from_data_path( name='xray_pdf', data_path=data_path, sample_form='powder', @@ -57,18 +57,17 @@ def test_single_fit_pdf_xray_pd_cw_nacl() -> None: experiment.peak.sharp_delta_1 = 0 experiment.peak.sharp_delta_2 = 3.5041 experiment.peak.damp_particle_diameter = 0 - experiment.linked_phases.add(id='nacl', scale=0.4254) + experiment.linked_phases.create(id='nacl', scale=0.4254) # Select fitting parameters - sample_model.cell.length_a.free = True - sample_model.atom_sites['Na'].b_iso.free = True - sample_model.atom_sites['Cl'].b_iso.free = True + structure.cell.length_a.free = True + structure.atom_sites['Na'].b_iso.free = True + structure.atom_sites['Cl'].b_iso.free = True experiment.linked_phases['nacl'].scale.free = True experiment.peak.damp_q.free = True experiment.peak.sharp_delta_2.free = True # Perform fit - project.analysis.current_calculator = 'pdffit' project.analysis.fit() # Compare fit quality @@ -80,13 +79,13 @@ def test_single_fit_pdf_xray_pd_cw_nacl() -> None: def test_single_fit_pdf_neutron_pd_cw_ni(): project = ed.Project() - # Set sample model - project.sample_models.add(name='ni') - sample_model = project.sample_models['ni'] - sample_model.space_group.name_h_m.value = 'F m -3 m' - sample_model.space_group.it_coordinate_system_code = '1' - sample_model.cell.length_a = 3.526 - sample_model.atom_sites.add( + # Set structure + project.structures.create(name='ni') + structure = project.structures['ni'] + structure.space_group.name_h_m.value = 'F m -3 m' + structure.space_group.it_coordinate_system_code = '1' + structure.cell.length_a = 3.526 + structure.atom_sites.create( label='Ni', type_symbol='Ni', fract_x=0, @@ -98,7 +97,7 @@ def test_single_fit_pdf_neutron_pd_cw_ni(): # Set experiment data_path = ed.download_data(id=6, destination=TEMP_DIR) - project.experiments.add( + project.experiments.add_from_data_path( name='pdf', data_path=data_path, sample_form='powder', @@ -113,17 +112,16 @@ def test_single_fit_pdf_neutron_pd_cw_ni(): experiment.peak.sharp_delta_1 = 0 experiment.peak.sharp_delta_2 = 2.5587 experiment.peak.damp_particle_diameter = 0 - experiment.linked_phases.add(id='ni', scale=0.9892) + experiment.linked_phases.create(id='ni', scale=0.9892) # Select fitting parameters - sample_model.cell.length_a.free = True - sample_model.atom_sites['Ni'].b_iso.free = True + structure.cell.length_a.free = True + structure.atom_sites['Ni'].b_iso.free = True experiment.linked_phases['ni'].scale.free = True experiment.peak.broad_q.free = True experiment.peak.sharp_delta_2.free = True # Perform fit - project.analysis.current_calculator = 'pdffit' project.analysis.fit() # Compare fit quality @@ -134,13 +132,13 @@ def test_single_fit_pdf_neutron_pd_cw_ni(): def test_single_fit_pdf_neutron_pd_tof_si(): project = ed.Project() - # Set sample model - project.sample_models.add(name='si') - sample_model = project.sample_models['si'] - sample_model.space_group.name_h_m.value = 'F d -3 m' - sample_model.space_group.it_coordinate_system_code = '1' - sample_model.cell.length_a = 5.4306 - sample_model.atom_sites.add( + # Set structure + project.structures.create(name='si') + structure = project.structures['si'] + structure.space_group.name_h_m.value = 'F d -3 m' + structure.space_group.it_coordinate_system_code = '1' + structure.cell.length_a = 5.4306 + structure.atom_sites.create( label='Si', type_symbol='Si', fract_x=0, @@ -152,7 +150,7 @@ def test_single_fit_pdf_neutron_pd_tof_si(): # Set experiment data_path = ed.download_data(id=5, destination=TEMP_DIR) - project.experiments.add( + project.experiments.add_from_data_path( name='nomad', data_path=data_path, sample_form='powder', @@ -167,11 +165,11 @@ def test_single_fit_pdf_neutron_pd_tof_si(): experiment.peak.sharp_delta_1 = 2.54 experiment.peak.sharp_delta_2 = -1.7525 experiment.peak.damp_particle_diameter = 0 - experiment.linked_phases.add(id='si', scale=1.2728) + experiment.linked_phases.create(id='si', scale=1.2728) # Select fitting parameters - project.sample_models['si'].cell.length_a.free = True - project.sample_models['si'].atom_sites['Si'].b_iso.free = True + project.structures['si'].cell.length_a.free = True + project.structures['si'].atom_sites['Si'].b_iso.free = True experiment.linked_phases['si'].scale.free = True experiment.peak.damp_q.free = True experiment.peak.broad_q.free = True @@ -179,7 +177,6 @@ def test_single_fit_pdf_neutron_pd_tof_si(): experiment.peak.sharp_delta_2.free = True # Perform fit - project.analysis.current_calculator = 'pdffit' project.analysis.fit() # Compare fit quality diff --git a/tests/integration/fitting/test_powder-diffraction_constant-wavelength.py b/tests/integration/fitting/test_powder-diffraction_constant-wavelength.py index bba3b2d2..7a98c15d 100644 --- a/tests/integration/fitting/test_powder-diffraction_constant-wavelength.py +++ b/tests/integration/fitting/test_powder-diffraction_constant-wavelength.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause import tempfile @@ -8,18 +8,18 @@ from easydiffraction import ExperimentFactory from easydiffraction import Project -from easydiffraction import SampleModelFactory +from easydiffraction import StructureFactory from easydiffraction import download_data TEMP_DIR = tempfile.gettempdir() def test_single_fit_neutron_pd_cwl_lbco() -> None: - # Set sample model - model = SampleModelFactory.create(name='lbco') + # Set structure + model = StructureFactory.from_scratch(name='lbco') model.space_group.name_h_m = 'P m -3 m' model.cell.length_a = 3.88 - model.atom_sites.add( + model.atom_sites.create( label='La', type_symbol='La', fract_x=0, @@ -29,7 +29,7 @@ def test_single_fit_neutron_pd_cwl_lbco() -> None: occupancy=0.5, b_iso=0.1, ) - model.atom_sites.add( + model.atom_sites.create( label='Ba', type_symbol='Ba', fract_x=0, @@ -39,7 +39,7 @@ def test_single_fit_neutron_pd_cwl_lbco() -> None: occupancy=0.5, b_iso=0.1, ) - model.atom_sites.add( + model.atom_sites.create( label='Co', type_symbol='Co', fract_x=0.5, @@ -48,7 +48,7 @@ def test_single_fit_neutron_pd_cwl_lbco() -> None: wyckoff_letter='b', b_iso=0.1, ) - model.atom_sites.add( + model.atom_sites.create( label='O', type_symbol='O', fract_x=0, @@ -61,7 +61,7 @@ def test_single_fit_neutron_pd_cwl_lbco() -> None: # Set experiment data_path = download_data(id=3, destination=TEMP_DIR) - expt = ExperimentFactory.create( + expt = ExperimentFactory.from_data_path( name='hrpt', data_path=data_path, ) @@ -75,19 +75,18 @@ def test_single_fit_neutron_pd_cwl_lbco() -> None: expt.peak.broad_lorentz_x = 0 expt.peak.broad_lorentz_y = 0 - expt.linked_phases.add(id='lbco', scale=5.0) + expt.linked_phases.create(id='lbco', scale=5.0) - expt.background.add(id='1', x=10, y=170) - expt.background.add(id='2', x=165, y=170) + expt.background.create(id='1', x=10, y=170) + expt.background.create(id='2', x=165, y=170) # Create project project = Project() - project.sample_models.add(sample_model=model) - project.experiments.add(experiment=expt) + project.structures.add(model) + project.experiments.add(expt) # Prepare for fitting - project.analysis.current_calculator = 'cryspy' - project.analysis.current_minimizer = 'lmfit (leastsq)' + project.analysis.current_minimizer = 'lmfit' # ------------ 1st fitting ------------ @@ -147,8 +146,8 @@ def test_single_fit_neutron_pd_cwl_lbco() -> None: @pytest.mark.fast def test_single_fit_neutron_pd_cwl_lbco_with_constraints() -> None: - # Set sample model - model = SampleModelFactory.create(name='lbco') + # Set structure + model = StructureFactory.from_scratch(name='lbco') space_group = model.space_group space_group.name_h_m = 'P m -3 m' @@ -157,7 +156,7 @@ def test_single_fit_neutron_pd_cwl_lbco_with_constraints() -> None: cell.length_a = 3.8909 atom_sites = model.atom_sites - atom_sites.add( + atom_sites.create( label='La', type_symbol='La', fract_x=0, @@ -167,7 +166,7 @@ def test_single_fit_neutron_pd_cwl_lbco_with_constraints() -> None: b_iso=1.0, occupancy=0.5, ) - atom_sites.add( + atom_sites.create( label='Ba', type_symbol='Ba', fract_x=0, @@ -177,7 +176,7 @@ def test_single_fit_neutron_pd_cwl_lbco_with_constraints() -> None: b_iso=1.0, occupancy=0.5, ) - atom_sites.add( + atom_sites.create( label='Co', type_symbol='Co', fract_x=0.5, @@ -186,7 +185,7 @@ def test_single_fit_neutron_pd_cwl_lbco_with_constraints() -> None: wyckoff_letter='b', b_iso=1.0, ) - atom_sites.add( + atom_sites.create( label='O', type_symbol='O', fract_x=0, @@ -199,7 +198,7 @@ def test_single_fit_neutron_pd_cwl_lbco_with_constraints() -> None: # Set experiment data_path = download_data(id=3, destination=TEMP_DIR) - expt = ExperimentFactory.create( + expt = ExperimentFactory.from_data_path( name='hrpt', data_path=data_path, ) @@ -216,27 +215,26 @@ def test_single_fit_neutron_pd_cwl_lbco_with_constraints() -> None: peak.broad_lorentz_y = 0.0797 background = expt.background - background.add(id='10', x=10, y=174.3) - background.add(id='20',x=20, y=159.8) - background.add(id='30',x=30, y=167.9) - background.add(id='50',x=50, y=166.1) - background.add(id='70',x=70, y=172.3) - background.add(id='90',x=90, y=171.1) - background.add(id='110',x=110, y=172.4) - background.add(id='130',x=130, y=182.5) - background.add(id='150',x=150, y=173.0) - background.add(id='165',x=165, y=171.1) - - expt.linked_phases.add(id='lbco', scale=9.0976) + background.create(id='10', x=10, y=174.3) + background.create(id='20', x=20, y=159.8) + background.create(id='30', x=30, y=167.9) + background.create(id='50', x=50, y=166.1) + background.create(id='70', x=70, y=172.3) + background.create(id='90', x=90, y=171.1) + background.create(id='110', x=110, y=172.4) + background.create(id='130', x=130, y=182.5) + background.create(id='150', x=150, y=173.0) + background.create(id='165', x=165, y=171.1) + + expt.linked_phases.create(id='lbco', scale=9.0976) # Create project project = Project() - project.sample_models.add(sample_model=model) - project.experiments.add(experiment=expt) + project.structures.add(model) + project.experiments.add(expt) # Prepare for fitting - project.analysis.current_calculator = 'cryspy' - project.analysis.current_minimizer = 'lmfit (leastsq)' + project.analysis.current_minimizer = 'lmfit' # ------------ 1st fitting ------------ @@ -277,18 +275,26 @@ def test_single_fit_neutron_pd_cwl_lbco_with_constraints() -> None: # ------------ 2nd fitting ------------ # Set aliases for parameters - project.analysis.aliases.add(label='biso_La', param_uid=atom_sites['La'].b_iso.uid) - project.analysis.aliases.add(label='biso_Ba', param_uid=atom_sites['Ba'].b_iso.uid) - project.analysis.aliases.add( - label='occ_La', param_uid=atom_sites['La'].occupancy.uid + project.analysis.aliases.create( + label='biso_La', + param_uid=atom_sites['La'].b_iso.uid, ) - project.analysis.aliases.add( - label='occ_Ba', param_uid=atom_sites['Ba'].occupancy.uid + project.analysis.aliases.create( + label='biso_Ba', + param_uid=atom_sites['Ba'].b_iso.uid, + ) + project.analysis.aliases.create( + label='occ_La', + param_uid=atom_sites['La'].occupancy.uid, + ) + project.analysis.aliases.create( + label='occ_Ba', + param_uid=atom_sites['Ba'].occupancy.uid, ) # Set constraints - project.analysis.constraints.add(lhs_alias='biso_Ba', rhs_expr='biso_La') - project.analysis.constraints.add(lhs_alias='occ_Ba', rhs_expr='1 - occ_La') + project.analysis.constraints.create(expression='biso_Ba = biso_La') + project.analysis.constraints.create(expression='occ_Ba = 1 - occ_La') # Apply constraints project.analysis.apply_constraints() @@ -313,13 +319,13 @@ def test_single_fit_neutron_pd_cwl_lbco_with_constraints() -> None: def test_fit_neutron_pd_cwl_hs() -> None: - # Set sample model - model = SampleModelFactory.create(name='hs') + # Set structure + model = StructureFactory.from_scratch(name='hs') model.space_group.name_h_m = 'R -3 m' model.space_group.it_coordinate_system_code = 'h' model.cell.length_a = 6.8615 model.cell.length_c = 14.136 - model.atom_sites.add( + model.atom_sites.create( label='Zn', type_symbol='Zn', fract_x=0, @@ -328,7 +334,7 @@ def test_fit_neutron_pd_cwl_hs() -> None: wyckoff_letter='b', b_iso=0.1, ) - model.atom_sites.add( + model.atom_sites.create( label='Cu', type_symbol='Cu', fract_x=0.5, @@ -337,7 +343,7 @@ def test_fit_neutron_pd_cwl_hs() -> None: wyckoff_letter='e', b_iso=1.2, ) - model.atom_sites.add( + model.atom_sites.create( label='O', type_symbol='O', fract_x=0.206, @@ -346,7 +352,7 @@ def test_fit_neutron_pd_cwl_hs() -> None: wyckoff_letter='h', b_iso=0.7, ) - model.atom_sites.add( + model.atom_sites.create( label='Cl', type_symbol='Cl', fract_x=0, @@ -355,7 +361,7 @@ def test_fit_neutron_pd_cwl_hs() -> None: wyckoff_letter='c', b_iso=1.1, ) - model.atom_sites.add( + model.atom_sites.create( label='H', type_symbol='2H', fract_x=0.132, @@ -368,7 +374,7 @@ def test_fit_neutron_pd_cwl_hs() -> None: # Set experiment data_path = download_data(id=11, destination=TEMP_DIR) - expt = ExperimentFactory.create(name='hrpt', data_path=data_path) + expt = ExperimentFactory.from_data_path(name='hrpt', data_path=data_path) expt.instrument.setup_wavelength = 1.89 expt.instrument.calib_twotheta_offset = 0.0 @@ -379,26 +385,25 @@ def test_fit_neutron_pd_cwl_hs() -> None: expt.peak.broad_lorentz_x = 0.2927 expt.peak.broad_lorentz_y = 0 - expt.background.add(id='1', x=4.4196, y=648.413) - expt.background.add(id='2', x=6.6207, y=523.788) - expt.background.add(id='3', x=10.4918, y=454.938) - expt.background.add(id='4', x=15.4634, y=435.913) - expt.background.add(id='5', x=45.6041, y=472.972) - expt.background.add(id='6', x=74.6844, y=486.606) - expt.background.add(id='7', x=103.4187, y=472.409) - expt.background.add(id='8', x=121.6311, y=496.734) - expt.background.add(id='9', x=159.4116, y=473.146) + expt.background.create(id='1', x=4.4196, y=648.413) + expt.background.create(id='2', x=6.6207, y=523.788) + expt.background.create(id='3', x=10.4918, y=454.938) + expt.background.create(id='4', x=15.4634, y=435.913) + expt.background.create(id='5', x=45.6041, y=472.972) + expt.background.create(id='6', x=74.6844, y=486.606) + expt.background.create(id='7', x=103.4187, y=472.409) + expt.background.create(id='8', x=121.6311, y=496.734) + expt.background.create(id='9', x=159.4116, y=473.146) - expt.linked_phases.add(id='hs', scale=0.492) + expt.linked_phases.create(id='hs', scale=0.492) # Create project project = Project() - project.sample_models.add(sample_model=model) - project.experiments.add(experiment=expt) + project.structures.add(model) + project.experiments.add(expt) # Prepare for fitting - project.analysis.current_calculator = 'cryspy' - project.analysis.current_minimizer = 'lmfit (leastsq)' + project.analysis.current_minimizer = 'lmfit' # ------------ 1st fitting ------------ diff --git a/tests/integration/fitting/test_powder-diffraction_joint-fit.py b/tests/integration/fitting/test_powder-diffraction_joint-fit.py index 904b2e1c..fb94a8ff 100644 --- a/tests/integration/fitting/test_powder-diffraction_joint-fit.py +++ b/tests/integration/fitting/test_powder-diffraction_joint-fit.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause import tempfile @@ -8,7 +8,7 @@ from easydiffraction import ExperimentFactory from easydiffraction import Project -from easydiffraction import SampleModelFactory +from easydiffraction import StructureFactory from easydiffraction import download_data TEMP_DIR = tempfile.gettempdir() @@ -16,13 +16,13 @@ @pytest.mark.fast def test_joint_fit_split_dataset_neutron_pd_cwl_pbso4() -> None: - # Set sample model - model = SampleModelFactory.create(name='pbso4') + # Set structure + model = StructureFactory.from_scratch(name='pbso4') model.space_group.name_h_m = 'P n m a' model.cell.length_a = 8.47 model.cell.length_b = 5.39 model.cell.length_c = 6.95 - model.atom_sites.add( + model.atom_sites.create( label='Pb', type_symbol='Pb', fract_x=0.1876, @@ -31,7 +31,7 @@ def test_joint_fit_split_dataset_neutron_pd_cwl_pbso4() -> None: wyckoff_letter='c', b_iso=1.37, ) - model.atom_sites.add( + model.atom_sites.create( label='S', type_symbol='S', fract_x=0.0654, @@ -40,7 +40,7 @@ def test_joint_fit_split_dataset_neutron_pd_cwl_pbso4() -> None: wyckoff_letter='c', b_iso=0.3777, ) - model.atom_sites.add( + model.atom_sites.create( label='O1', type_symbol='O', fract_x=0.9082, @@ -49,7 +49,7 @@ def test_joint_fit_split_dataset_neutron_pd_cwl_pbso4() -> None: wyckoff_letter='c', b_iso=1.9764, ) - model.atom_sites.add( + model.atom_sites.create( label='O2', type_symbol='O', fract_x=0.1935, @@ -58,7 +58,7 @@ def test_joint_fit_split_dataset_neutron_pd_cwl_pbso4() -> None: wyckoff_letter='c', b_iso=1.4456, ) - model.atom_sites.add( + model.atom_sites.create( label='O3', type_symbol='O', fract_x=0.0811, @@ -70,7 +70,7 @@ def test_joint_fit_split_dataset_neutron_pd_cwl_pbso4() -> None: # Set experiments data_path = download_data(id=14, destination=TEMP_DIR) - expt1 = ExperimentFactory.create(name='npd1', data_path=data_path) + expt1 = ExperimentFactory.from_data_path(name='npd1', data_path=data_path) expt1.instrument.setup_wavelength = 1.91 expt1.instrument.calib_twotheta_offset = -0.1406 expt1.peak.broad_gauss_u = 0.139 @@ -78,7 +78,7 @@ def test_joint_fit_split_dataset_neutron_pd_cwl_pbso4() -> None: expt1.peak.broad_gauss_w = 0.386 expt1.peak.broad_lorentz_x = 0 expt1.peak.broad_lorentz_y = 0.0878 - expt1.linked_phases.add(id='pbso4', scale=1.46) + expt1.linked_phases.create(id='pbso4', scale=1.46) expt1.background_type = 'line-segment' for id, x, y in [ ('1', 11.0, 206.1624), @@ -90,10 +90,10 @@ def test_joint_fit_split_dataset_neutron_pd_cwl_pbso4() -> None: ('7', 120.0, 244.4525), ('8', 153.0, 226.0595), ]: - expt1.background.add(id=id, x=x, y=y) + expt1.background.create(id=id, x=x, y=y) data_path = download_data(id=15, destination=TEMP_DIR) - expt2 = ExperimentFactory.create(name='npd2', data_path=data_path) + expt2 = ExperimentFactory.from_data_path(name='npd2', data_path=data_path) expt2.instrument.setup_wavelength = 1.91 expt2.instrument.calib_twotheta_offset = -0.1406 expt2.peak.broad_gauss_u = 0.139 @@ -101,7 +101,7 @@ def test_joint_fit_split_dataset_neutron_pd_cwl_pbso4() -> None: expt2.peak.broad_gauss_w = 0.386 expt2.peak.broad_lorentz_x = 0 expt2.peak.broad_lorentz_y = 0.0878 - expt2.linked_phases.add(id='pbso4', scale=1.46) + expt2.linked_phases.create(id='pbso4', scale=1.46) expt2.background_type = 'line-segment' for id, x, y in [ ('1', 11.0, 206.1624), @@ -113,18 +113,17 @@ def test_joint_fit_split_dataset_neutron_pd_cwl_pbso4() -> None: ('7', 120.0, 244.4525), ('8', 153.0, 226.0595), ]: - expt2.background.add(id=id, x=x, y=y) + expt2.background.create(id=id, x=x, y=y) # Create project project = Project() - project.sample_models.add(sample_model=model) - project.experiments.add(experiment=expt1) - project.experiments.add(experiment=expt2) + project.structures.add(model) + project.experiments.add(expt1) + project.experiments.add(expt2) # Prepare for fitting - project.analysis.current_calculator = 'cryspy' - project.analysis.current_minimizer = 'lmfit (leastsq)' - project.analysis.fit_mode = 'joint' + project.analysis.current_minimizer = 'lmfit' + project.analysis.fit_mode.mode = 'joint' # Select fitting parameters model.cell.length_a.free = True @@ -144,13 +143,13 @@ def test_joint_fit_split_dataset_neutron_pd_cwl_pbso4() -> None: @pytest.mark.fast def test_joint_fit_neutron_xray_pd_cwl_pbso4() -> None: - # Set sample model - model = SampleModelFactory.create(name='pbso4') + # Set structure + model = StructureFactory.from_scratch(name='pbso4') model.space_group.name_h_m = 'P n m a' model.cell.length_a = 8.47 model.cell.length_b = 5.39 model.cell.length_c = 6.95 - model.atom_sites.add( + model.atom_sites.create( label='Pb', type_symbol='Pb', fract_x=0.1876, @@ -159,7 +158,7 @@ def test_joint_fit_neutron_xray_pd_cwl_pbso4() -> None: wyckoff_letter='c', b_iso=1.37, ) - model.atom_sites.add( + model.atom_sites.create( label='S', type_symbol='S', fract_x=0.0654, @@ -168,7 +167,7 @@ def test_joint_fit_neutron_xray_pd_cwl_pbso4() -> None: wyckoff_letter='c', b_iso=0.3777, ) - model.atom_sites.add( + model.atom_sites.create( label='O1', type_symbol='O', fract_x=0.9082, @@ -177,7 +176,7 @@ def test_joint_fit_neutron_xray_pd_cwl_pbso4() -> None: wyckoff_letter='c', b_iso=1.9764, ) - model.atom_sites.add( + model.atom_sites.create( label='O2', type_symbol='O', fract_x=0.1935, @@ -186,7 +185,7 @@ def test_joint_fit_neutron_xray_pd_cwl_pbso4() -> None: wyckoff_letter='c', b_iso=1.4456, ) - model.atom_sites.add( + model.atom_sites.create( label='O3', type_symbol='O', fract_x=0.0811, @@ -198,7 +197,7 @@ def test_joint_fit_neutron_xray_pd_cwl_pbso4() -> None: # Set experiments data_path = download_data(id=13, destination=TEMP_DIR) - expt1 = ExperimentFactory.create( + expt1 = ExperimentFactory.from_data_path( name='npd', data_path=data_path, radiation_probe='neutron', @@ -210,7 +209,7 @@ def test_joint_fit_neutron_xray_pd_cwl_pbso4() -> None: expt1.peak.broad_gauss_w = 0.386 expt1.peak.broad_lorentz_x = 0 expt1.peak.broad_lorentz_y = 0.088 - expt1.linked_phases.add(id='pbso4', scale=1.5) + expt1.linked_phases.create(id='pbso4', scale=1.5) for id, x, y in [ ('1', 11.0, 206.1624), ('2', 15.0, 194.75), @@ -221,10 +220,10 @@ def test_joint_fit_neutron_xray_pd_cwl_pbso4() -> None: ('7', 120.0, 244.4525), ('8', 153.0, 226.0595), ]: - expt1.background.add(id=id, x=x, y=y) + expt1.background.create(id=id, x=x, y=y) data_path = download_data(id=16, destination=TEMP_DIR) - expt2 = ExperimentFactory.create( + expt2 = ExperimentFactory.from_data_path( name='xrd', data_path=data_path, radiation_probe='xray', @@ -236,7 +235,7 @@ def test_joint_fit_neutron_xray_pd_cwl_pbso4() -> None: expt2.peak.broad_gauss_w = 0.021272 expt2.peak.broad_lorentz_x = 0 expt2.peak.broad_lorentz_y = 0.057691 - expt2.linked_phases.add(id='pbso4', scale=0.001) + expt2.linked_phases.create(id='pbso4', scale=0.001) for id, x, y in [ ('1', 11.0, 141.8516), ('2', 13.0, 102.8838), @@ -247,17 +246,16 @@ def test_joint_fit_neutron_xray_pd_cwl_pbso4() -> None: ('7', 90.0, 113.7473), ('8', 110.0, 132.4643), ]: - expt2.background.add(id=id, x=x, y=y) + expt2.background.create(id=id, x=x, y=y) # Create project project = Project() - project.sample_models.add(sample_model=model) - project.experiments.add(experiment=expt1) - project.experiments.add(experiment=expt2) + project.structures.add(model) + project.experiments.add(expt1) + project.experiments.add(expt2) # Prepare for fitting - project.analysis.current_calculator = 'cryspy' - project.analysis.current_minimizer = 'lmfit (leastsq)' + project.analysis.current_minimizer = 'lmfit' # Select fitting parameters model.cell.length_a.free = True @@ -269,7 +267,6 @@ def test_joint_fit_neutron_xray_pd_cwl_pbso4() -> None: # ------------ 1st fitting ------------ # Perform fit - project.analysis.fit_mode = 'single' # Default project.analysis.fit() # Compare fit quality @@ -282,7 +279,7 @@ def test_joint_fit_neutron_xray_pd_cwl_pbso4() -> None: # ------------ 2nd fitting ------------ # Perform fit - project.analysis.fit_mode = 'joint' + project.analysis.fit_mode.mode = 'joint' project.analysis.fit() # Compare fit quality @@ -297,7 +294,6 @@ def test_joint_fit_neutron_xray_pd_cwl_pbso4() -> None: # Perform fit project.analysis.joint_fit_experiments['xrd'].weight = 0.5 # Default project.analysis.joint_fit_experiments['npd'].weight = 0.5 # Default - project.analysis.fit_mode = 'joint' project.analysis.fit() # Compare fit quality @@ -312,7 +308,6 @@ def test_joint_fit_neutron_xray_pd_cwl_pbso4() -> None: # Perform fit project.analysis.joint_fit_experiments['xrd'].weight = 0.3 project.analysis.joint_fit_experiments['npd'].weight = 0.7 - project.analysis.fit_mode = 'joint' project.analysis.fit() # Compare fit quality diff --git a/tests/integration/fitting/test_powder-diffraction_multiphase.py b/tests/integration/fitting/test_powder-diffraction_multiphase.py deleted file mode 100644 index 2880eb2b..00000000 --- a/tests/integration/fitting/test_powder-diffraction_multiphase.py +++ /dev/null @@ -1,143 +0,0 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors -# SPDX-License-Identifier: BSD-3-Clause - -import tempfile - -from numpy.testing import assert_almost_equal - -from easydiffraction import ExperimentFactory -from easydiffraction import Project -from easydiffraction import SampleModelFactory -from easydiffraction import download_data - -TEMP_DIR = tempfile.gettempdir() - - -def test_single_fit_neutron_pd_tof_mcstas_lbco_si() -> None: - # Set sample models - model_1 = SampleModelFactory.create(name='lbco') - model_1.space_group.name_h_m = 'P m -3 m' - model_1.space_group.it_coordinate_system_code = '1' - model_1.cell.length_a = 3.8909 - model_1.atom_sites.add( - label='La', - type_symbol='La', - fract_x=0, - fract_y=0, - fract_z=0, - wyckoff_letter='a', - b_iso=0.2, - occupancy=0.5, - ) - model_1.atom_sites.add( - label='Ba', - type_symbol='Ba', - fract_x=0, - fract_y=0, - fract_z=0, - wyckoff_letter='a', - b_iso=0.2, - occupancy=0.5, - ) - model_1.atom_sites.add( - label='Co', - type_symbol='Co', - fract_x=0.5, - fract_y=0.5, - fract_z=0.5, - wyckoff_letter='b', - b_iso=0.2567, - ) - model_1.atom_sites.add( - label='O', - type_symbol='O', - fract_x=0, - fract_y=0.5, - fract_z=0.5, - wyckoff_letter='c', - b_iso=1.4041, - ) - - model_2 = SampleModelFactory.create(name='si') - model_2.space_group.name_h_m = 'F d -3 m' - model_2.space_group.it_coordinate_system_code = '2' - model_2.cell.length_a = 5.43146 - model_2.atom_sites.add( - label='Si', - type_symbol='Si', - fract_x=0.0, - fract_y=0.0, - fract_z=0.0, - wyckoff_letter='a', - b_iso=0.0, - ) - - # Set experiment - data_path = download_data(id=8, destination=TEMP_DIR) - expt = ExperimentFactory.create( - name='mcstas', - data_path=data_path, - beam_mode='time-of-flight', - ) - expt.instrument.setup_twotheta_bank = 94.90931761529106 - expt.instrument.calib_d_to_tof_offset = 0.0 - expt.instrument.calib_d_to_tof_linear = 58724.76869981215 - expt.instrument.calib_d_to_tof_quad = -0.00001 - expt.peak_profile_type = 'pseudo-voigt * ikeda-carpenter' - expt.peak.broad_gauss_sigma_0 = 45137 - expt.peak.broad_gauss_sigma_1 = -52394 - expt.peak.broad_gauss_sigma_2 = 22998 - expt.peak.broad_mix_beta_0 = 0.0055 - expt.peak.broad_mix_beta_1 = 0.0041 - expt.peak.asym_alpha_0 = 0.0 - expt.peak.asym_alpha_1 = 0.0097 - expt.linked_phases.add(id='lbco', scale=4.0) - expt.linked_phases.add(id='si', scale=0.2) - for x in range(45000, 115000, 5000): - expt.background.add(id=str(x), x=x, y=0.2) - - # Create project - project = Project() - project.sample_models.add(sample_model=model_1) - project.sample_models.add(sample_model=model_2) - project.experiments.add(experiment=expt) - - # Exclude regions from fitting - project.experiments['mcstas'].excluded_regions.add(start=108000, end=200000) - - # Prepare for fitting - project.analysis.current_calculator = 'cryspy' - project.analysis.current_minimizer = 'lmfit (leastsq)' - - # Select fitting parameters - model_1.cell.length_a.free = True - model_1.atom_sites['La'].b_iso.free = True - model_1.atom_sites['Ba'].b_iso.free = True - model_1.atom_sites['Co'].b_iso.free = True - model_1.atom_sites['O'].b_iso.free = True - model_2.cell.length_a.free = True - model_2.atom_sites['Si'].b_iso.free = True - expt.linked_phases['lbco'].scale.free = True - expt.linked_phases['si'].scale.free = True - expt.peak.broad_gauss_sigma_0.free = True - expt.peak.broad_gauss_sigma_1.free = True - expt.peak.broad_gauss_sigma_2.free = True - expt.peak.asym_alpha_1.free = True - expt.peak.broad_mix_beta_0.free = True - expt.peak.broad_mix_beta_1.free = True - for point in expt.background: - point.y.free = True - - # Perform fit - project.analysis.fit() - - # Compare fit quality - assert_almost_equal( - project.analysis.fit_results.reduced_chi_square, - desired=2.87, - decimal=1, - ) - - -if __name__ == '__main__': - test_single_fit_neutron_pd_tof_mcstas_lbco_si() diff --git a/tests/integration/fitting/test_powder-diffraction_time-of-flight.py b/tests/integration/fitting/test_powder-diffraction_time-of-flight.py index 129fb7ff..18123254 100644 --- a/tests/integration/fitting/test_powder-diffraction_time-of-flight.py +++ b/tests/integration/fitting/test_powder-diffraction_time-of-flight.py @@ -1,26 +1,25 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -import os import tempfile from numpy.testing import assert_almost_equal from easydiffraction import ExperimentFactory from easydiffraction import Project -from easydiffraction import SampleModelFactory +from easydiffraction import StructureFactory from easydiffraction import download_data TEMP_DIR = tempfile.gettempdir() def test_single_fit_neutron_pd_tof_si() -> None: - # Set sample model - model = SampleModelFactory.create(name='si') + # Set structure + model = StructureFactory.from_scratch(name='si') model.space_group.name_h_m = 'F d -3 m' model.space_group.it_coordinate_system_code = '2' model.cell.length_a = 5.4315 - model.atom_sites.add( + model.atom_sites.create( label='Si', type_symbol='Si', fract_x=0.125, @@ -32,7 +31,7 @@ def test_single_fit_neutron_pd_tof_si() -> None: # Set experiment data_path = download_data(id=7, destination=TEMP_DIR) - expt = ExperimentFactory.create( + expt = ExperimentFactory.from_data_path( name='sepd', data_path=data_path, beam_mode='time-of-flight', @@ -49,18 +48,17 @@ def test_single_fit_neutron_pd_tof_si() -> None: expt.peak.broad_mix_beta_1 = 0.00946 expt.peak.asym_alpha_0 = 0.0 expt.peak.asym_alpha_1 = 0.5971 - expt.linked_phases.add(id='si', scale=14.92) + expt.linked_phases.create(id='si', scale=14.92) for x in range(0, 35000, 5000): - expt.background.add(id=str(x), x=x, y=200) + expt.background.create(id=str(x), x=x, y=200) # Create project project = Project() - project.sample_models.add(sample_model=model) - project.experiments.add(experiment=expt) + project.structures.add(model) + project.experiments.add(expt) # Prepare for fitting - project.analysis.current_calculator = 'cryspy' - project.analysis.current_minimizer = 'lmfit (leastsq)' + project.analysis.current_minimizer = 'lmfit' # Select fitting parameters model.cell.length_a.free = True @@ -82,12 +80,12 @@ def test_single_fit_neutron_pd_tof_si() -> None: def test_single_fit_neutron_pd_tof_ncaf() -> None: - # Set sample model - model = SampleModelFactory.create(name='ncaf') + # Set structure + model = StructureFactory.from_scratch(name='ncaf') model.space_group.name_h_m = 'I 21 3' model.space_group.it_coordinate_system_code = '1' model.cell.length_a = 10.250256 - model.atom_sites.add( + model.atom_sites.create( label='Ca', type_symbol='Ca', fract_x=0.4661, @@ -96,7 +94,7 @@ def test_single_fit_neutron_pd_tof_ncaf() -> None: wyckoff_letter='b', b_iso=0.9, ) - model.atom_sites.add( + model.atom_sites.create( label='Al', type_symbol='Al', fract_x=0.25171, @@ -105,7 +103,7 @@ def test_single_fit_neutron_pd_tof_ncaf() -> None: wyckoff_letter='a', b_iso=0.66, ) - model.atom_sites.add( + model.atom_sites.create( label='Na', type_symbol='Na', fract_x=0.08481, @@ -114,7 +112,7 @@ def test_single_fit_neutron_pd_tof_ncaf() -> None: wyckoff_letter='a', b_iso=1.9, ) - model.atom_sites.add( + model.atom_sites.create( label='F1', type_symbol='F', fract_x=0.1375, @@ -123,7 +121,7 @@ def test_single_fit_neutron_pd_tof_ncaf() -> None: wyckoff_letter='c', b_iso=0.9, ) - model.atom_sites.add( + model.atom_sites.create( label='F2', type_symbol='F', fract_x=0.3626, @@ -132,7 +130,7 @@ def test_single_fit_neutron_pd_tof_ncaf() -> None: wyckoff_letter='c', b_iso=1.28, ) - model.atom_sites.add( + model.atom_sites.create( label='F3', type_symbol='F', fract_x=0.4612, @@ -144,13 +142,13 @@ def test_single_fit_neutron_pd_tof_ncaf() -> None: # Set experiment data_path = download_data(id=9, destination=TEMP_DIR) - expt = ExperimentFactory.create( + expt = ExperimentFactory.from_data_path( name='wish', data_path=data_path, beam_mode='time-of-flight', ) - expt.excluded_regions.add(id='1', start=0, end=9000) - expt.excluded_regions.add(id='2', start=100010, end=200000) + expt.excluded_regions.create(id='1', start=0, end=9000) + expt.excluded_regions.create(id='2', start=100010, end=200000) expt.instrument.setup_twotheta_bank = 152.827 expt.instrument.calib_d_to_tof_offset = -13.7123 expt.instrument.calib_d_to_tof_linear = 20773.1 @@ -163,7 +161,7 @@ def test_single_fit_neutron_pd_tof_ncaf() -> None: expt.peak.broad_mix_beta_1 = 0.0099 expt.peak.asym_alpha_0 = -0.009 expt.peak.asym_alpha_1 = 0.1085 - expt.linked_phases.add(id='ncaf', scale=1.0928) + expt.linked_phases.create(id='ncaf', scale=1.0928) for x, y in [ (9162, 465), (11136, 593), @@ -194,16 +192,15 @@ def test_single_fit_neutron_pd_tof_ncaf() -> None: (91958, 268), (102712, 262), ]: - expt.background.add(id=str(x), x=x, y=y) + expt.background.create(id=str(x), x=x, y=y) # Create project project = Project() - project.sample_models.add(sample_model=model) - project.experiments.add(experiment=expt) + project.structures.add(model) + project.experiments.add(expt) # Prepare for fitting - project.analysis.current_calculator = 'cryspy' - project.analysis.current_minimizer = 'lmfit (leastsq)' + project.analysis.current_minimizer = 'lmfit' # Select fitting parameters expt.linked_phases['ncaf'].scale.free = True diff --git a/tests/integration/fitting/test_single-crystal-diffraction.py b/tests/integration/fitting/test_single-crystal-diffraction.py new file mode 100644 index 00000000..f5273fdd --- /dev/null +++ b/tests/integration/fitting/test_single-crystal-diffraction.py @@ -0,0 +1,90 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + +import tempfile + +import pytest + +import easydiffraction as ed + +TEMP_DIR = tempfile.gettempdir() + + +@pytest.mark.fast +def test_single_fit_neut_sc_cwl_tbti() -> None: + project = ed.Project() + + # Set structure + model_path = ed.download_data(id=20, destination=TEMP_DIR) + project.structures.add_from_cif_path(model_path) + + # Set experiment + data_path = ed.download_data(id=19, destination=TEMP_DIR) + project.experiments.add_from_data_path( + name='heidi', + data_path=data_path, + sample_form='single crystal', + beam_mode='constant wavelength', + radiation_probe='neutron', + scattering_type='bragg', + ) + experiment = project.experiments['heidi'] + experiment.linked_crystal.id = 'tbti' + experiment.linked_crystal.scale = 3 + experiment.instrument.setup_wavelength = 0.793 + experiment.extinction.mosaicity = 29820 + experiment.extinction.radius = 27 + + # Select fitting parameters (experiment only) + # Structure parameters are selected in the loaded CIF file + experiment.linked_crystal.scale.free = True + experiment.extinction.radius.free = True + + # Perform fit + project.analysis.fit() + + # Compare fit quality + chi2 = project.analysis.fit_results.reduced_chi_square + assert chi2 == pytest.approx(expected=12.9, abs=0.1) + + +@pytest.mark.fast +def test_single_fit_neut_sc_tof_taurine() -> None: + project = ed.Project() + + # Set structure + model_path = ed.download_data(id=21, destination=TEMP_DIR) + project.structures.add_from_cif_path(model_path) + + # Set experiment + data_path = ed.download_data(id=22, destination=TEMP_DIR) + project.experiments.add_from_data_path( + name='senju', + data_path=data_path, + sample_form='single crystal', + beam_mode='time-of-flight', + radiation_probe='neutron', + scattering_type='bragg', + ) + experiment = project.experiments['senju'] + experiment.linked_crystal.id = 'taurine' + experiment.linked_crystal.scale = 1.4 + experiment.extinction.mosaicity = 1000.0 + experiment.extinction.radius = 2.0 + + # Select fitting parameters (experiment only) + # Structure parameters are selected in the loaded CIF file + experiment.linked_crystal.scale.free = True + experiment.extinction.radius.free = True + + # Perform fit + project.analysis.fit() + + # Compare fit quality + chi2 = project.analysis.fit_results.reduced_chi_square + assert chi2 == pytest.approx(expected=23.6, abs=0.1) + + +if __name__ == '__main__': + test_single_fit_neut_sc_cwl_tbti() + test_single_fit_neut_sc_tof_taurine() diff --git a/tests/integration/scipp-analysis/dream/conftest.py b/tests/integration/scipp-analysis/dream/conftest.py index 56aabf1f..b16da0a1 100644 --- a/tests/integration/scipp-analysis/dream/conftest.py +++ b/tests/integration/scipp-analysis/dream/conftest.py @@ -1,5 +1,6 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -# Copyright (c) 2026 DMSC + """Shared fixtures for DREAM scipp-analysis integration tests. This module provides pytest fixtures for downloading and parsing diff --git a/tests/integration/scipp-analysis/dream/test_analyze_reduced_data.py b/tests/integration/scipp-analysis/dream/test_analyze_reduced_data.py index eb528ff5..23821ee2 100644 --- a/tests/integration/scipp-analysis/dream/test_analyze_reduced_data.py +++ b/tests/integration/scipp-analysis/dream/test_analyze_reduced_data.py @@ -1,10 +1,11 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -# Copyright (c) 2026 DMSC + """Tests for analyzing reduced diffraction data using easydiffraction. These tests verify the complete workflow: 1. Define project -2. Add sample model manually defined +2. Add structure manually defined 3. Modify experiment CIF file 4. Add experiment from modified CIF file 5. Modify default experiment configuration @@ -56,11 +57,11 @@ def prepared_cif_path( def project_with_data( prepared_cif_path: str, ) -> ed.Project: - """Create project with sample model, experiment data, and + """Create project with structure, experiment data, and configuration. 1. Define project - 2. Add sample model manually defined + 2. Add structure manually defined 3. Modify experiment CIF file 4. Add experiment from modified CIF file 5. Modify default experiment configuration @@ -68,16 +69,16 @@ def project_with_data( # Step 1: Define Project project = ed.Project() - # Step 2: Define Sample Model manually - project.sample_models.add(name='si') - sample_model = project.sample_models['si'] + # Step 2: Define Structure manually + project.structures.create(name='si') + structure = project.structures['si'] - sample_model.space_group.name_h_m = 'F d -3 m' - sample_model.space_group.it_coordinate_system_code = '1' + structure.space_group.name_h_m = 'F d -3 m' + structure.space_group.it_coordinate_system_code = '1' - sample_model.cell.length_a = 5.43146 + structure.cell.length_a = 5.43146 - sample_model.atom_sites.add( + structure.atom_sites.create( label='Si', type_symbol='Si', fract_x=0.125, @@ -88,12 +89,12 @@ def project_with_data( ) # Step 3: Add experiment from modified CIF file - project.experiments.add(cif_path=prepared_cif_path) + project.experiments.add_from_cif_path(prepared_cif_path) experiment = project.experiments['reduced_tof'] # Step 4: Configure experiment # Link phase - experiment.linked_phases.add(id='si', scale=0.8) + experiment.linked_phases.create(id='si', scale=0.8) # Instrument setup experiment.instrument.setup_twotheta_bank = 90.0 @@ -109,8 +110,8 @@ def project_with_data( experiment.peak.asym_alpha_1 = 0.26 # Excluded regions - experiment.excluded_regions.add(id='1', start=0, end=10000) - experiment.excluded_regions.add(id='2', start=70000, end=200000) + experiment.excluded_regions.create(id='1', start=0, end=10000) + experiment.excluded_regions.create(id='2', start=70000, end=200000) # Background points background_points = [ @@ -124,7 +125,7 @@ def project_with_data( ('9', 70000, 0.6), ] for id_, x, y in background_points: - experiment.background.add(id=id_, x=x, y=y) + experiment.background.create(id=id_, x=x, y=y) return project @@ -139,12 +140,12 @@ def fitted_project( 7. Do fitting """ project = project_with_data - sample_model = project.sample_models['si'] + structure = project.structures['si'] experiment = project.experiments['reduced_tof'] # Step 5: Select parameters to be fitted - # Set free parameters for sample model - sample_model.atom_sites['Si'].b_iso.free = True + # Set free parameters for structure + structure.atom_sites['Si'].b_iso.free = True # Set free parameters for experiment experiment.linked_phases['si'].scale.free = True diff --git a/tests/integration/scipp-analysis/dream/test_package_import.py b/tests/integration/scipp-analysis/dream/test_package_import.py index 7c10d02b..03125806 100644 --- a/tests/integration/scipp-analysis/dream/test_package_import.py +++ b/tests/integration/scipp-analysis/dream/test_package_import.py @@ -1,5 +1,6 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -# Copyright (c) 2026 DMSC + """Tests for verifying package installation and version consistency. These tests check that easydiffraction and essdiffraction packages are diff --git a/tests/integration/scipp-analysis/dream/test_read_reduced_data.py b/tests/integration/scipp-analysis/dream/test_read_reduced_data.py index 616c9876..db589354 100644 --- a/tests/integration/scipp-analysis/dream/test_read_reduced_data.py +++ b/tests/integration/scipp-analysis/dream/test_read_reduced_data.py @@ -1,5 +1,6 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -# Copyright (c) 2026 DMSC + """Tests for reading reduced data from CIF files. These tests verify that the CIF file can be fetched, read as text, diff --git a/tests/integration/scipp-analysis/dream/test_validate_meta_data.py b/tests/integration/scipp-analysis/dream/test_validate_meta_data.py index 7712dbc3..6f07845a 100644 --- a/tests/integration/scipp-analysis/dream/test_validate_meta_data.py +++ b/tests/integration/scipp-analysis/dream/test_validate_meta_data.py @@ -1,5 +1,6 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -# Copyright (c) 2026 DMSC + """Tests for validating metadata structure in CIF files. These tests verify that the CIF file contains the expected data blocks, diff --git a/tests/integration/scipp-analysis/dream/test_validate_physical_data.py b/tests/integration/scipp-analysis/dream/test_validate_physical_data.py index f1be5eb3..a8e5d4fb 100644 --- a/tests/integration/scipp-analysis/dream/test_validate_physical_data.py +++ b/tests/integration/scipp-analysis/dream/test_validate_physical_data.py @@ -1,5 +1,6 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -# Copyright (c) 2026 DMSC + """Tests for validating physical data values in CIF files. These tests verify that numerical data columns contain valid, diff --git a/tests/unit/easydiffraction/analysis/calculators/test_base.py b/tests/unit/easydiffraction/analysis/calculators/test_base.py index 6483af6e..5070b9a3 100644 --- a/tests/unit/easydiffraction/analysis/calculators/test_base.py +++ b/tests/unit/easydiffraction/analysis/calculators/test_base.py @@ -1,6 +1,7 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause + def test_module_import(): import easydiffraction.analysis.calculators.base as MUT diff --git a/tests/unit/easydiffraction/analysis/calculators/test_crysfml.py b/tests/unit/easydiffraction/analysis/calculators/test_crysfml.py index 866d4008..a6a1371c 100644 --- a/tests/unit/easydiffraction/analysis/calculators/test_crysfml.py +++ b/tests/unit/easydiffraction/analysis/calculators/test_crysfml.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause import pytest @@ -17,7 +17,7 @@ def test_crysfml_engine_flag_and_structure_factors_raises(): # engine_imported is a boolean flag; it may be False in our env assert isinstance(calc.engine_imported, bool) with pytest.raises(NotImplementedError): - calc.calculate_structure_factors(sample_models=None, experiments=None) + calc.calculate_structure_factors(structures=None, experiments=None) def test_crysfml_adjust_pattern_length_truncates(): diff --git a/tests/unit/easydiffraction/analysis/calculators/test_cryspy.py b/tests/unit/easydiffraction/analysis/calculators/test_cryspy.py index 9289c15e..8593bfe0 100644 --- a/tests/unit/easydiffraction/analysis/calculators/test_cryspy.py +++ b/tests/unit/easydiffraction/analysis/calculators/test_cryspy.py @@ -1,6 +1,7 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause + def test_module_import(): import easydiffraction.analysis.calculators.cryspy as MUT @@ -21,5 +22,5 @@ class DummySample: def as_cif(self): return 'data_x' - # _convert_sample_model_to_cryspy_cif returns input as_cif - assert calc._convert_sample_model_to_cryspy_cif(DummySample()) == 'data_x' + # _convert_structure_to_cryspy_cif returns input as_cif + assert calc._convert_structure_to_cryspy_cif(DummySample()) == 'data_x' diff --git a/tests/unit/easydiffraction/analysis/calculators/test_factory.py b/tests/unit/easydiffraction/analysis/calculators/test_factory.py index 7cf9fc25..681299fa 100644 --- a/tests/unit/easydiffraction/analysis/calculators/test_factory.py +++ b/tests/unit/easydiffraction/analysis/calculators/test_factory.py @@ -1,41 +1,22 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -def test_list_and_show_supported_calculators_do_not_crash(capsys, monkeypatch): +import pytest + + +def test_supported_tags_and_show_supported(capsys): from easydiffraction.analysis.calculators.factory import CalculatorFactory - # Simulate no engines available by forcing engine_imported to False - class DummyCalc: - def __call__(self): - return self - - @property - def engine_imported(self): - return False - - monkeypatch = monkeypatch # keep name - monkeypatch.setitem( - CalculatorFactory._potential_calculators, - 'dummy', - { - 'description': 'Dummy calc', - 'class': DummyCalc, - }, - ) - - lst = CalculatorFactory.list_supported_calculators() - assert isinstance(lst, list) - - CalculatorFactory.show_supported_calculators() + tags = CalculatorFactory.supported_tags() + assert isinstance(tags, list) + + CalculatorFactory.show_supported() out = capsys.readouterr().out - # Should print the paragraph title - assert 'Supported calculators' in out + assert 'Supported types' in out -def test_create_calculator_unknown_returns_none(capsys): +def test_create_unknown_raises(): from easydiffraction.analysis.calculators.factory import CalculatorFactory - obj = CalculatorFactory.create_calculator('this_is_unknown') - assert obj is None - out = capsys.readouterr().out - assert 'Unknown calculator' in out + with pytest.raises(ValueError): + CalculatorFactory.create('this_is_unknown') diff --git a/tests/unit/easydiffraction/analysis/calculators/test_pdffit.py b/tests/unit/easydiffraction/analysis/calculators/test_pdffit.py index e7662d43..9dce59f5 100644 --- a/tests/unit/easydiffraction/analysis/calculators/test_pdffit.py +++ b/tests/unit/easydiffraction/analysis/calculators/test_pdffit.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause import numpy as np @@ -16,7 +16,7 @@ def test_pdffit_engine_flag_and_hkl_message(capsys): calc = PdffitCalculator() assert isinstance(calc.engine_imported, bool) # calculate_structure_factors prints fixed message and returns [] by contract - out = calc.calculate_structure_factors(sample_models=None, experiments=None) + out = calc.calculate_structure_factors(structures=None, experiments=None) assert out == [] # The method prints a note printed = capsys.readouterr().out @@ -53,7 +53,7 @@ def __init__(self): self.type = type('T', (), {'radiation_probe': type('P', (), {'value': 'neutron'})()})() self.linked_phases = DummyLinkedPhases() - class DummySampleModel: + class DummyStructure: name = 'PhaseA' @property @@ -93,6 +93,6 @@ def parse(self, text): calc = PdffitCalculator() pattern = calc.calculate_pattern( - DummySampleModel(), DummyExperiment(), called_by_minimizer=False + DummyStructure(), DummyExperiment(), called_by_minimizer=False ) assert isinstance(pattern, np.ndarray) and pattern.shape[0] == 5 diff --git a/tests/unit/easydiffraction/analysis/categories/test_aliases.py b/tests/unit/easydiffraction/analysis/categories/test_aliases.py index 73c7b9ab..2545218a 100644 --- a/tests/unit/easydiffraction/analysis/categories/test_aliases.py +++ b/tests/unit/easydiffraction/analysis/categories/test_aliases.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause from easydiffraction.analysis.categories.aliases import Alias @@ -6,10 +6,12 @@ def test_alias_creation_and_collection(): - a = Alias(label='x', param_uid='p1') + a = Alias() + a.label = 'x' + a.param_uid = 'p1' assert a.label.value == 'x' coll = Aliases() - coll.add(label='x', param_uid='p1') + coll.create(label='x', param_uid='p1') # Collections index by entry name; check via names or direct indexing assert 'x' in coll.names assert coll['x'].param_uid.value == 'p1' diff --git a/tests/unit/easydiffraction/analysis/categories/test_constraints.py b/tests/unit/easydiffraction/analysis/categories/test_constraints.py index 542a9f52..15dddc4f 100644 --- a/tests/unit/easydiffraction/analysis/categories/test_constraints.py +++ b/tests/unit/easydiffraction/analysis/categories/test_constraints.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause from easydiffraction.analysis.categories.constraints import Constraint @@ -6,9 +6,11 @@ def test_constraint_creation_and_collection(): - c = Constraint(lhs_alias='a', rhs_expr='b + c') - assert c.lhs_alias.value == 'a' + c = Constraint() + c.expression = 'a = b + c' + assert c.lhs_alias == 'a' + assert c.rhs_expr == 'b + c' coll = Constraints() - coll.add(lhs_alias='a', rhs_expr='b + c') + coll.create(expression='a = b + c') assert 'a' in coll.names - assert coll['a'].rhs_expr.value == 'b + c' + assert coll['a'].rhs_expr == 'b + c' diff --git a/tests/unit/easydiffraction/analysis/categories/test_joint_fit_experiments.py b/tests/unit/easydiffraction/analysis/categories/test_joint_fit_experiments.py index 6a134ea0..51777f11 100644 --- a/tests/unit/easydiffraction/analysis/categories/test_joint_fit_experiments.py +++ b/tests/unit/easydiffraction/analysis/categories/test_joint_fit_experiments.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause from easydiffraction.analysis.categories.joint_fit_experiments import JointFitExperiment @@ -6,10 +6,12 @@ def test_joint_fit_experiment_and_collection(): - j = JointFitExperiment(id='ex1', weight=0.5) + j = JointFitExperiment() + j.id = 'ex1' + j.weight = 0.5 assert j.id.value == 'ex1' assert j.weight.value == 0.5 coll = JointFitExperiments() - coll.add(id='ex1', weight=0.5) + coll.create(id='ex1', weight=0.5) assert 'ex1' in coll.names assert coll['ex1'].weight.value == 0.5 diff --git a/tests/unit/easydiffraction/analysis/fit_helpers/test_metrics.py b/tests/unit/easydiffraction/analysis/fit_helpers/test_metrics.py index 2af9f167..2fc968f8 100644 --- a/tests/unit/easydiffraction/analysis/fit_helpers/test_metrics.py +++ b/tests/unit/easydiffraction/analysis/fit_helpers/test_metrics.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause import numpy as np @@ -31,9 +31,9 @@ def test_get_reliability_inputs_collects_arrays_with_default_su(): # Minimal fakes for experiments class DS: def __init__(self): - self.meas = np.array([1.0, 2.0]) - self.meas_su = None # triggers default ones - self.calc = np.array([1.1, 1.9]) + self.intensity_meas = np.array([1.0, 2.0]) + self.intensity_meas_su = None # triggers default ones + self.intensity_calc = np.array([1.1, 1.9]) class Expt: def __init__(self): @@ -46,9 +46,9 @@ class Expts(dict): def values(self): return [Expt()] - class SampleModels(dict): + class DummyStructures(dict): pass - y_obs, y_calc, y_err = M.get_reliability_inputs(SampleModels(), Expts()) + y_obs, y_calc, y_err = M.get_reliability_inputs(DummyStructures(), Expts()) assert y_obs.shape == (2,) and y_calc.shape == (2,) and y_err.shape == (2,) assert np.allclose(y_err, 1.0) diff --git a/tests/unit/easydiffraction/analysis/fit_helpers/test_reporting.py b/tests/unit/easydiffraction/analysis/fit_helpers/test_reporting.py index 458519ce..ee8014f2 100644 --- a/tests/unit/easydiffraction/analysis/fit_helpers/test_reporting.py +++ b/tests/unit/easydiffraction/analysis/fit_helpers/test_reporting.py @@ -1,6 +1,7 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause + def test_module_import(): import easydiffraction.analysis.fit_helpers.reporting as MUT diff --git a/tests/unit/easydiffraction/analysis/fit_helpers/test_tracking.py b/tests/unit/easydiffraction/analysis/fit_helpers/test_tracking.py index bf3873ab..561eb54c 100644 --- a/tests/unit/easydiffraction/analysis/fit_helpers/test_tracking.py +++ b/tests/unit/easydiffraction/analysis/fit_helpers/test_tracking.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause import numpy as np diff --git a/tests/unit/easydiffraction/analysis/minimizers/test_base.py b/tests/unit/easydiffraction/analysis/minimizers/test_base.py index dca36ee1..501a2a98 100644 --- a/tests/unit/easydiffraction/analysis/minimizers/test_base.py +++ b/tests/unit/easydiffraction/analysis/minimizers/test_base.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause import numpy as np @@ -49,7 +49,7 @@ def _check_success(self, raw_result): # Provide residuals implementation used by _objective_function def _compute_residuals( - self, engine_params, parameters, sample_models, experiments, calculator + self, engine_params, parameters, structures, experiments, calculator ): # Minimal residuals; verify engine params passed through assert engine_params == {'ok': True} @@ -62,7 +62,7 @@ def _compute_residuals( # Wrap minimizer's objective creator to simulate higher-level usage objective = minim._create_objective_function( parameters=params, - sample_models=None, + structures=None, experiments=None, calculator=None, ) @@ -94,14 +94,14 @@ def _check_success(self, raw_result): return True def _compute_residuals( - self, engine_params, parameters, sample_models, experiments, calculator + self, engine_params, parameters, structures, experiments, calculator ): # Return a deterministic vector to assert against return np.array([1.0, 2.0, 3.0]) m = M() f = m._create_objective_function( - parameters=[], sample_models=None, experiments=None, calculator=None + parameters=[], structures=None, experiments=None, calculator=None ) out = f({}) assert np.allclose(out, np.array([1.0, 2.0, 3.0])) diff --git a/tests/unit/easydiffraction/analysis/minimizers/test_dfols.py b/tests/unit/easydiffraction/analysis/minimizers/test_dfols.py index f0a9ea9d..72d01949 100644 --- a/tests/unit/easydiffraction/analysis/minimizers/test_dfols.py +++ b/tests/unit/easydiffraction/analysis/minimizers/test_dfols.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause import numpy as np @@ -15,11 +15,22 @@ def test_dfols_prepare_run_and_sync(monkeypatch): class P: def __init__(self, v, lo=-np.inf, hi=np.inf): - self.value = v + self._value = v self.fit_min = lo self.fit_max = hi self.uncertainty = None + @property + def value(self): + return self._value + + @value.setter + def value(self, v): + self._value = v + + def _set_value_from_minimizer(self, v): + self._value = v + class FakeRes: EXIT_SUCCESS = 0 diff --git a/tests/unit/easydiffraction/analysis/minimizers/test_factory.py b/tests/unit/easydiffraction/analysis/minimizers/test_factory.py index 940143a6..961ef782 100644 --- a/tests/unit/easydiffraction/analysis/minimizers/test_factory.py +++ b/tests/unit/easydiffraction/analysis/minimizers/test_factory.py @@ -1,37 +1,42 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause + def test_minimizer_factory_list_and_show(capsys): from easydiffraction.analysis.minimizers.factory import MinimizerFactory - lst = MinimizerFactory.list_available_minimizers() + lst = MinimizerFactory.supported_tags() assert isinstance(lst, list) and len(lst) >= 1 - MinimizerFactory.show_available_minimizers() + MinimizerFactory.show_supported() out = capsys.readouterr().out - assert 'Supported minimizers' in out + assert 'Supported types' in out def test_minimizer_factory_unknown_raises(): from easydiffraction.analysis.minimizers.factory import MinimizerFactory try: - MinimizerFactory.create_minimizer('___unknown___') + MinimizerFactory.create('___unknown___') except ValueError as e: - assert 'Unknown minimizer' in str(e) + assert 'Unsupported type' in str(e) else: assert False, 'Expected ValueError' -def test_minimizer_factory_create_known_and_register(monkeypatch): +def test_minimizer_factory_create_known_and_register(): from easydiffraction.analysis.minimizers.base import MinimizerBase from easydiffraction.analysis.minimizers.factory import MinimizerFactory + from easydiffraction.core.metadata import TypeInfo - # Create a known minimizer instance (lmfit (leastsq) exists) - m = MinimizerFactory.create_minimizer('lmfit (leastsq)') + # Create a known minimizer instance (lmfit exists) + m = MinimizerFactory.create('lmfit') assert isinstance(m, MinimizerBase) # Register a custom minimizer and create it + @MinimizerFactory.register class Custom(MinimizerBase): + type_info = TypeInfo(tag='custom-test', description='x') + def _prepare_solver_args(self, parameters): return {} @@ -44,8 +49,5 @@ def _sync_result_to_parameters(self, raw_result, parameters): def _check_success(self, raw_result): return True - MinimizerFactory.register_minimizer( - name='custom-test', minimizer_cls=Custom, method=None, description='x' - ) - created = MinimizerFactory.create_minimizer('custom-test') + created = MinimizerFactory.create('custom-test') assert isinstance(created, Custom) diff --git a/tests/unit/easydiffraction/analysis/minimizers/test_lmfit.py b/tests/unit/easydiffraction/analysis/minimizers/test_lmfit.py index c2385ee4..69005bd1 100644 --- a/tests/unit/easydiffraction/analysis/minimizers/test_lmfit.py +++ b/tests/unit/easydiffraction/analysis/minimizers/test_lmfit.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause import types @@ -32,6 +32,9 @@ def value(self): def value(self, v): self._value = v + def _set_value_from_minimizer(self, v): + self._value = v + # Fake lmfit.Parameters and result structure class FakeParam: def __init__(self, value, stderr=None): diff --git a/tests/unit/easydiffraction/analysis/test_analysis.py b/tests/unit/easydiffraction/analysis/test_analysis.py index 4a16d206..19bc3c2a 100644 --- a/tests/unit/easydiffraction/analysis/test_analysis.py +++ b/tests/unit/easydiffraction/analysis/test_analysis.py @@ -1,6 +1,7 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause + def test_module_import(): import easydiffraction.analysis.analysis as MUT @@ -20,68 +21,96 @@ def names(self): class P: experiments = ExpCol(names) - sample_models = object() + structures = object() _varname = 'proj' return P() -def test_show_current_calculator_and_minimizer_prints(capsys): +def test_show_current_minimizer_prints(capsys): from easydiffraction.analysis.analysis import Analysis a = Analysis(project=_make_project_with_names([])) - a.show_current_calculator() a.show_current_minimizer() out = capsys.readouterr().out - assert 'Current calculator' in out - assert 'cryspy' in out assert 'Current minimizer' in out - assert 'lmfit (leastsq)' in out + assert 'lmfit' in out -def test_current_calculator_setter_success_and_unknown(monkeypatch, capsys): - from easydiffraction.analysis import calculators as calc_pkg +def test_fit_mode_category_and_joint_fit_experiments(monkeypatch, capsys): + from easydiffraction.analysis.analysis import Analysis + + a = Analysis(project=_make_project_with_names(['e1', 'e2'])) + + # Default fit mode is 'single' + assert a.fit_mode.mode.value == 'single' + + # Switch to joint + a.fit_mode.mode = 'joint' + assert a.fit_mode.mode.value == 'joint' + + # joint_fit_experiments exists but is empty until fit() populates it + assert len(a.joint_fit_experiments) == 0 + + +def test_fit_mode_type_getter(capsys): from easydiffraction.analysis.analysis import Analysis a = Analysis(project=_make_project_with_names([])) + assert a.fit_mode_type == 'default' + + +def test_show_supported_fit_mode_types(capsys): + from easydiffraction.analysis.analysis import Analysis + + a = Analysis(project=_make_project_with_names([])) + a.show_supported_fit_mode_types() + out = capsys.readouterr().out + assert 'default' in out - # Success path - monkeypatch.setattr( - calc_pkg.factory.CalculatorFactory, - 'create_calculator', - lambda name: object(), - ) - a.current_calculator = 'pdffit' + +def test_show_current_fit_mode_type(capsys): + from easydiffraction.analysis.analysis import Analysis + + a = Analysis(project=_make_project_with_names([])) + a.show_current_fit_mode_type() out = capsys.readouterr().out - assert 'Current calculator changed to' in out - assert a.current_calculator == 'pdffit' + assert 'Current fit-mode type' in out + assert 'default' in out - # Unknown path (create_calculator returns None): no change - monkeypatch.setattr( - calc_pkg.factory.CalculatorFactory, - 'create_calculator', - lambda name: None, - ) - a.current_calculator = 'unknown' - assert a.current_calculator == 'pdffit' + +def test_fit_mode_type_setter_valid(capsys): + from easydiffraction.analysis.analysis import Analysis + + a = Analysis(project=_make_project_with_names([])) + a.fit_mode_type = 'default' + assert a.fit_mode_type == 'default' -def test_fit_modes_show_and_switch_to_joint(monkeypatch, capsys): +def test_fit_mode_type_setter_invalid(capsys): from easydiffraction.analysis.analysis import Analysis - a = Analysis(project=_make_project_with_names(['e1', 'e2'])) + a = Analysis(project=_make_project_with_names([])) + a.fit_mode_type = 'nonexistent' + out = capsys.readouterr().out + assert 'Unsupported' in out + # Type should remain unchanged + assert a.fit_mode_type == 'default' - a.show_available_fit_modes() - a.show_current_fit_mode() - out1 = capsys.readouterr().out - assert 'Available fit modes' in out1 - assert 'Current fit mode' in out1 - assert 'single' in out1 - a.fit_mode = 'joint' - out2 = capsys.readouterr().out - assert 'Current fit mode changed to' in out2 - assert a.fit_mode == 'joint' +def test_analysis_help(capsys): + from easydiffraction.analysis.analysis import Analysis + + a = Analysis(project=_make_project_with_names([])) + a.help() + out = capsys.readouterr().out + assert "Help for 'Analysis'" in out + assert 'fit_mode' in out + assert 'current_minimizer' in out + assert 'Properties' in out + assert 'Methods' in out + assert 'fit()' in out + assert 'show_fit_results()' in out def test_show_fit_results_warns_when_no_results(capsys): @@ -105,13 +134,13 @@ def test_show_fit_results_calls_process_fit_results(monkeypatch): # Track if _process_fit_results was called process_called = {'called': False, 'args': None} - def mock_process_fit_results(sample_models, experiments): + def mock_process_fit_results(structures, experiments): process_called['called'] = True - process_called['args'] = (sample_models, experiments) + process_called['args'] = (structures, experiments) - # Create a mock project with sample_models and experiments + # Create a mock project with structures and experiments class MockProject: - sample_models = object() + structures = object() experiments = object() _varname = 'proj' @@ -121,7 +150,7 @@ class experiments_cls: experiments = experiments_cls() project = MockProject() - project.sample_models = object() + project.structures = object() project.experiments.names = [] a = Analysis(project=project) diff --git a/tests/unit/easydiffraction/analysis/test_analysis_access_params.py b/tests/unit/easydiffraction/analysis/test_analysis_access_params.py index df8827ea..bdd5ead0 100644 --- a/tests/unit/easydiffraction/analysis/test_analysis_access_params.py +++ b/tests/unit/easydiffraction/analysis/test_analysis_access_params.py @@ -1,21 +1,22 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause + def test_how_to_access_parameters_prints_paths_and_uids(capsys, monkeypatch): + import easydiffraction.analysis.analysis as analysis_mod from easydiffraction.analysis.analysis import Analysis - from easydiffraction.core.parameters import Parameter from easydiffraction.core.validation import AttributeSpec - from easydiffraction.core.validation import DataTypes + from easydiffraction.core.variable import Parameter from easydiffraction.io.cif.handler import CifHandler - import easydiffraction.analysis.analysis as analysis_mod # Build two parameters with identity metadata set directly def make_param(db, cat, entry, name, val): p = Parameter( name=name, - value_spec=AttributeSpec(value=val, type_=DataTypes.NUMERIC, default=0.0), + value_spec=AttributeSpec(default=0.0), cif_handler=CifHandler(names=[f'_{cat}.{name}']), ) + p.value = val # Inject identity metadata (avoid parent chain) p._identity.datablock_entry_name = lambda: db p._identity.category_code = cat @@ -36,7 +37,7 @@ class Project: _varname = 'proj' def __init__(self): - self.sample_models = Coll([p1]) + self.structures = Coll([p1]) self.experiments = Coll([p2]) # Capture the table payload by monkeypatching render_table to avoid @@ -63,7 +64,7 @@ def fake_render_table(**kwargs): flat_rows = [' '.join(map(str, row)) for row in data] # Python access paths - assert any("proj.sample_models['db1'].catA.alpha" in r for r in flat_rows) + assert any("proj.structures['db1'].catA.alpha" in r for r in flat_rows) assert any("proj.experiments['db2'].catB['row1'].beta" in r for r in flat_rows) # Now check CIF unique identifiers via the new API diff --git a/tests/unit/easydiffraction/analysis/test_analysis_show_empty.py b/tests/unit/easydiffraction/analysis/test_analysis_show_empty.py index 8503398c..7f2895b4 100644 --- a/tests/unit/easydiffraction/analysis/test_analysis_show_empty.py +++ b/tests/unit/easydiffraction/analysis/test_analysis_show_empty.py @@ -1,6 +1,7 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause + def test_show_params_empty_branches(capsys): from easydiffraction.analysis.analysis import Analysis @@ -18,7 +19,7 @@ def free_parameters(self): return [] class P: - sample_models = Empty() + structures = Empty() experiments = Empty() _varname = 'proj' diff --git a/tests/unit/easydiffraction/analysis/test_fitting.py b/tests/unit/easydiffraction/analysis/test_fitting.py index 990cabaf..ed9ea419 100644 --- a/tests/unit/easydiffraction/analysis/test_fitting.py +++ b/tests/unit/easydiffraction/analysis/test_fitting.py @@ -1,6 +1,7 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause + def test_module_import(): import easydiffraction.analysis.fitting as MUT @@ -25,13 +26,13 @@ def names(self): class DummyMin: tracker = type('T', (), {'track': staticmethod(lambda a, b: a)})() - def fit(self, params, obj): + def fit(self, params, obj, verbosity=None): return None f = Fitter() # Avoid creating a real minimizer f.minimizer = DummyMin() - f.fit(sample_models=DummyCollection(), experiments=DummyCollection()) + f.fit(structures=DummyCollection(), experiments=DummyCollection()) out = capsys.readouterr().out assert 'No parameters selected for fitting' in out @@ -65,7 +66,7 @@ class MockFitResults: class DummyMin: tracker = type('T', (), {'track': staticmethod(lambda a, b: a)})() - def fit(self, params, obj): + def fit(self, params, obj, verbosity=None): return MockFitResults() def _sync_result_to_parameters(self, params, engine_params): @@ -83,7 +84,7 @@ def mock_process(*args, **kwargs): monkeypatch.setattr(f, '_process_fit_results', mock_process) - f.fit(sample_models=DummyCollection(), experiments=DummyCollection()) + f.fit(structures=DummyCollection(), experiments=DummyCollection()) assert not process_called['called'], ( 'Fitter.fit() should not call _process_fit_results automatically. ' diff --git a/tests/unit/easydiffraction/core/test_category.py b/tests/unit/easydiffraction/core/test_category.py index 6143c479..632d96f1 100644 --- a/tests/unit/easydiffraction/core/test_category.py +++ b/tests/unit/easydiffraction/core/test_category.py @@ -1,26 +1,24 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause from easydiffraction.core.category import CategoryCollection from easydiffraction.core.category import CategoryItem -from easydiffraction.core.parameters import StringDescriptor from easydiffraction.core.validation import AttributeSpec -from easydiffraction.core.validation import DataTypes +from easydiffraction.core.variable import StringDescriptor from easydiffraction.io.cif.handler import CifHandler class SimpleItem(CategoryItem): - def __init__(self, entry_name): + def __init__(self): super().__init__() self._identity.category_code = 'simple' - self._identity.category_entry_name = entry_name object.__setattr__( self, '_a', StringDescriptor( name='a', description='', - value_spec=AttributeSpec(value='x', type_=DataTypes.STRING, default=''), + value_spec=AttributeSpec(default='_'), cif_handler=CifHandler(names=['_simple.a']), ), ) @@ -30,19 +28,28 @@ def __init__(self, entry_name): StringDescriptor( name='b', description='', - value_spec=AttributeSpec(value='y', type_=DataTypes.STRING, default=''), + value_spec=AttributeSpec(default='_'), cif_handler=CifHandler(names=['_simple.b']), ), ) + self._identity.category_entry_name = lambda: str(self._a.value) @property def a(self): return self._a + @a.setter + def a(self, value): + self._a.value = value + @property def b(self): return self._b + @b.setter + def b(self, value): + self._b.value = value + class SimpleCollection(CategoryCollection): def __init__(self): @@ -50,7 +57,8 @@ def __init__(self): def test_category_item_str_and_properties(): - it = SimpleItem('name1') + it = SimpleItem() + it.a = 'name1' s = str(it) assert '<' in s and 'a=' in s and 'b=' in s assert it.unique_name.endswith('.simple.name1') or it.unique_name == 'simple.name1' @@ -59,9 +67,33 @@ def test_category_item_str_and_properties(): def test_category_collection_str_and_cif_calls(): c = SimpleCollection() - c.add('n1') - c.add('n2') + c.create(a='n1') + c.create(a='n2') s = str(c) assert 'collection' in s and '2 items' in s # as_cif delegates to serializer; should be a string (possibly empty) assert isinstance(c.as_cif, str) + + +def test_category_item_help(capsys): + it = SimpleItem() + it.a = 'name1' + it.help() + out = capsys.readouterr().out + assert 'Help for' in out + assert 'Parameters' in out + assert 'string' in out # Type column + assert '✓' in out # a and b are writable + assert 'Methods' in out + + +def test_category_collection_help(capsys): + c = SimpleCollection() + c.create(a='n1') + c.create(a='n2') + c.help() + out = capsys.readouterr().out + assert 'Help for' in out + assert 'Items (2)' in out + assert 'n1' in out + assert 'n2' in out diff --git a/tests/unit/easydiffraction/core/test_collection.py b/tests/unit/easydiffraction/core/test_collection.py index 9d89afea..bbbd9c65 100644 --- a/tests/unit/easydiffraction/core/test_collection.py +++ b/tests/unit/easydiffraction/core/test_collection.py @@ -1,6 +1,7 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause + def test_collection_add_get_delete_and_names(): from easydiffraction.core.collection import CollectionBase from easydiffraction.core.identity import Identity @@ -29,3 +30,136 @@ def as_cif(self) -> str: assert c['a'] is a2 and len(list(c.keys())) == 2 del c['b'] assert list(c.names) == ['a'] + + +def test_collection_contains(): + from easydiffraction.core.collection import CollectionBase + from easydiffraction.core.identity import Identity + + class Item: + def __init__(self, name): + self._identity = Identity(owner=self, category_entry=lambda: name) + + class MyCollection(CollectionBase): + @property + def parameters(self): + return [] + + @property + def as_cif(self) -> str: + return '' + + c = MyCollection(item_type=Item) + c['x'] = Item('x') + assert 'x' in c + assert 'y' not in c + + +def test_collection_remove(): + import pytest + + from easydiffraction.core.collection import CollectionBase + from easydiffraction.core.identity import Identity + + class Item: + def __init__(self, name): + self._identity = Identity(owner=self, category_entry=lambda: name) + + class MyCollection(CollectionBase): + @property + def parameters(self): + return [] + + @property + def as_cif(self) -> str: + return '' + + c = MyCollection(item_type=Item) + c['a'] = Item('a') + c['b'] = Item('b') + c.remove('a') + assert 'a' not in c + assert len(c) == 1 + with pytest.raises(KeyError): + c.remove('nonexistent') + + +def test_collection_getitem_by_int_index(): + """Verify items can be retrieved by positional index.""" + import pytest + + from easydiffraction.core.collection import CollectionBase + from easydiffraction.core.identity import Identity + + class Item: + def __init__(self, name): + self._identity = Identity(owner=self, category_entry=lambda: name) + + class MyCollection(CollectionBase): + @property + def parameters(self): + return [] + + @property + def as_cif(self) -> str: + return '' + + c = MyCollection(item_type=Item) + a = Item('a') + b = Item('b') + c['a'] = a + c['b'] = b + + # Forward indexing + assert c[0] is a + assert c[1] is b + + # Negative indexing + assert c[-1] is b + assert c[-2] is a + + # Out of range + with pytest.raises(IndexError): + c[2] + + # Invalid key type + with pytest.raises(TypeError): + c[3.14] + + +def test_collection_datablock_keyed_items(): + """Verify __setitem__/__delitem__/__contains__ work for datablock-keyed items.""" + from easydiffraction.core.collection import CollectionBase + from easydiffraction.core.identity import Identity + + class DbItem: + def __init__(self, name): + self._identity = Identity(owner=self, datablock_entry=lambda: name) + + class MyCollection(CollectionBase): + @property + def parameters(self): + return [] + + @property + def as_cif(self) -> str: + return '' + + c = MyCollection(item_type=DbItem) + a = DbItem('alpha') + b = DbItem('beta') + c['alpha'] = a + c['beta'] = b + assert 'alpha' in c + assert c['alpha'] is a + + # Replace + a2 = DbItem('alpha') + c['alpha'] = a2 + assert c['alpha'] is a2 + assert len(c) == 2 + + # Delete + del c['beta'] + assert 'beta' not in c + assert len(c) == 1 diff --git a/tests/unit/easydiffraction/core/test_datablock.py b/tests/unit/easydiffraction/core/test_datablock.py index 91d35663..b41f68b7 100644 --- a/tests/unit/easydiffraction/core/test_datablock.py +++ b/tests/unit/easydiffraction/core/test_datablock.py @@ -1,13 +1,13 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause + def test_datablock_collection_add_and_filters_with_real_parameters(): from easydiffraction.core.category import CategoryItem from easydiffraction.core.datablock import DatablockCollection from easydiffraction.core.datablock import DatablockItem - from easydiffraction.core.parameters import Parameter from easydiffraction.core.validation import AttributeSpec - from easydiffraction.core.validation import DataTypes + from easydiffraction.core.variable import Parameter from easydiffraction.io.cif.handler import CifHandler class Cat(CategoryItem): @@ -19,17 +19,20 @@ def __init__(self): self._p1 = Parameter( name='p1', description='', - value_spec=AttributeSpec(value=1.0, type_=DataTypes.NUMERIC, default=0.0), + value_spec=AttributeSpec(default=0.0), units='', cif_handler=CifHandler(names=['_cat.p1']), ) self._p2 = Parameter( name='p2', description='', - value_spec=AttributeSpec(value=2.0, type_=DataTypes.NUMERIC, default=0.0), + value_spec=AttributeSpec(default=0.0), units='', cif_handler=CifHandler(names=['_cat.p2']), ) + # Set actual values via setter + self._p1.value = 1.0 + self._p2.value = 2.0 # Make p2 constrained and not free self._p2._constrained = True self._p2._free = False @@ -59,8 +62,8 @@ def cat(self): coll = DatablockCollection(item_type=Block) a = Block('A') b = Block('B') - coll._add(a) - coll._add(b) + coll.add(a) + coll.add(b) # parameters collection aggregates from both blocks (p1 & p2 each) params = coll.parameters assert len(params) == 4 @@ -71,3 +74,63 @@ def cat(self): # free is subset of fittable where free=True (true for p1) free_params = coll.free_parameters assert free_params == fittable + + +def test_datablock_item_help(capsys): + from easydiffraction.core.category import CategoryItem + from easydiffraction.core.datablock import DatablockItem + from easydiffraction.core.validation import AttributeSpec + from easydiffraction.core.variable import Parameter + from easydiffraction.io.cif.handler import CifHandler + + class Cat(CategoryItem): + def __init__(self): + super().__init__() + self._identity.category_code = 'cat' + self._identity.category_entry_name = 'e1' + self._p1 = Parameter( + name='p1', + description='', + value_spec=AttributeSpec(default=0.0), + units='', + cif_handler=CifHandler(names=['_cat.p1']), + ) + + @property + def p1(self): + return self._p1 + + class Block(DatablockItem): + def __init__(self): + super().__init__() + self._identity.datablock_entry_name = lambda: 'blk' + self._cat = Cat() + + @property + def cat(self): + return self._cat + + b = Block() + b.help() + out = capsys.readouterr().out + assert 'Help for' in out + assert 'Categories' in out + assert 'cat' in out + + +def test_datablock_collection_help(capsys): + from easydiffraction.core.datablock import DatablockCollection + from easydiffraction.core.datablock import DatablockItem + + class Block(DatablockItem): + def __init__(self, name): + super().__init__() + self._identity.datablock_entry_name = lambda: name + + coll = DatablockCollection(item_type=Block) + a = Block('A') + coll.add(a) + coll.help() + out = capsys.readouterr().out + assert 'Items (1)' in out + assert 'A' in out diff --git a/tests/unit/easydiffraction/core/test_diagnostic.py b/tests/unit/easydiffraction/core/test_diagnostic.py index 98e96320..cda7ce98 100644 --- a/tests/unit/easydiffraction/core/test_diagnostic.py +++ b/tests/unit/easydiffraction/core/test_diagnostic.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause import pytest diff --git a/tests/unit/easydiffraction/core/test_factory.py b/tests/unit/easydiffraction/core/test_factory.py index 22ffad86..78150ea5 100644 --- a/tests/unit/easydiffraction/core/test_factory.py +++ b/tests/unit/easydiffraction/core/test_factory.py @@ -1,29 +1,2 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause - -import pytest - - -def test_module_import(): - import easydiffraction.core.factory as MUT - - expected_module_name = 'easydiffraction.core.factory' - actual_module_name = MUT.__name__ - assert expected_module_name == actual_module_name - - -def test_validate_args_valid_and_invalid(): - import easydiffraction.core.factory as MUT - - specs = [ - {'required': ['a'], 'optional': ['b']}, - {'required': ['x', 'y'], 'optional': []}, - ] - # valid: only required - MUT.FactoryBase._validate_args({'a'}, specs, 'Thing') - # valid: required + optional subset - MUT.FactoryBase._validate_args({'a', 'b'}, specs, 'Thing') - MUT.FactoryBase._validate_args({'x', 'y'}, specs, 'Thing') - # invalid: unknown key - with pytest.raises(ValueError): - MUT.FactoryBase._validate_args({'a', 'c'}, specs, 'Thing') diff --git a/tests/unit/easydiffraction/core/test_guard.py b/tests/unit/easydiffraction/core/test_guard.py index 1cd9dd15..64d46680 100644 --- a/tests/unit/easydiffraction/core/test_guard.py +++ b/tests/unit/easydiffraction/core/test_guard.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause import pytest @@ -51,3 +51,51 @@ def as_cif(self) -> str: # Unknown attribute should raise AttributeError under current logging mode with pytest.raises(AttributeError): p.child.unknown_attr = 1 + + +def test_help_lists_public_properties(capsys): + from easydiffraction.core.guard import GuardedBase + + class Obj(GuardedBase): + @property + def parameters(self): + return [] + + @property + def as_cif(self) -> str: + return '' + + @property + def name(self): + """Human-readable name.""" + return 'test' + + @property + def score(self): + """Computed score.""" + return 42 + + @score.setter + def score(self, v): + pass + + obj = Obj() + obj.help() + out = capsys.readouterr().out + assert "Help for 'Obj'" in out + assert 'name' in out + assert 'score' in out + assert 'Properties' in out + assert 'Methods' in out + assert '✓' in out # score is writable + assert '✗' in out # name is read-only + + +def test_first_sentence_extracts_first_paragraph(): + from easydiffraction.core.guard import GuardedBase + + assert GuardedBase._first_sentence(None) == '' + assert GuardedBase._first_sentence('') == '' + assert GuardedBase._first_sentence('One liner.') == 'One liner.' + assert GuardedBase._first_sentence('First.\n\nSecond.') == 'First.' + assert GuardedBase._first_sentence('Line one\ncontinued.') == 'Line one continued.' diff --git a/tests/unit/easydiffraction/core/test_identity.py b/tests/unit/easydiffraction/core/test_identity.py index 584135d7..61da0723 100644 --- a/tests/unit/easydiffraction/core/test_identity.py +++ b/tests/unit/easydiffraction/core/test_identity.py @@ -1,6 +1,7 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause + def test_identity_direct_and_parent_resolution(): from easydiffraction.core.identity import Identity diff --git a/tests/unit/easydiffraction/core/test_parameters.py b/tests/unit/easydiffraction/core/test_parameters.py index d16603b8..4bf46f0d 100644 --- a/tests/unit/easydiffraction/core/test_parameters.py +++ b/tests/unit/easydiffraction/core/test_parameters.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause import numpy as np @@ -6,36 +6,35 @@ def test_module_import(): - import easydiffraction.core.parameters as MUT + import easydiffraction.core.variable as MUT - assert MUT.__name__ == 'easydiffraction.core.parameters' + assert MUT.__name__ == 'easydiffraction.core.variable' def test_string_descriptor_type_override_raises_type_error(): # Creating a StringDescriptor with a NUMERIC spec should raise via Diagnostics - from easydiffraction.core.parameters import StringDescriptor from easydiffraction.core.validation import AttributeSpec from easydiffraction.core.validation import DataTypes + from easydiffraction.core.variable import StringDescriptor from easydiffraction.io.cif.handler import CifHandler with pytest.raises(TypeError): StringDescriptor( name='title', - value_spec=AttributeSpec(value='abc', type_=DataTypes.NUMERIC, default='x'), + value_spec=AttributeSpec(data_type=DataTypes.NUMERIC, default='x'), description='Title text', cif_handler=CifHandler(names=['_proj.title']), ) def test_numeric_descriptor_str_includes_units(): - from easydiffraction.core.parameters import NumericDescriptor from easydiffraction.core.validation import AttributeSpec - from easydiffraction.core.validation import DataTypes + from easydiffraction.core.variable import NumericDescriptor from easydiffraction.io.cif.handler import CifHandler d = NumericDescriptor( name='w', - value_spec=AttributeSpec(value=1.23, type_=DataTypes.NUMERIC, default=0.0), + value_spec=AttributeSpec(default=1.23), units='deg', cif_handler=CifHandler(names=['_x.w']), ) @@ -44,17 +43,17 @@ def test_numeric_descriptor_str_includes_units(): def test_parameter_string_repr_and_as_cif_and_flags(): - from easydiffraction.core.parameters import Parameter from easydiffraction.core.validation import AttributeSpec - from easydiffraction.core.validation import DataTypes + from easydiffraction.core.variable import Parameter from easydiffraction.io.cif.handler import CifHandler p = Parameter( name='a', - value_spec=AttributeSpec(value=2.5, type_=DataTypes.NUMERIC, default=0.0), + value_spec=AttributeSpec(default=0.0), units='A', cif_handler=CifHandler(names=['_param.a']), ) + p.value = 2.5 # Update extra attributes p.uncertainty = 0.1 p.free = True @@ -63,21 +62,20 @@ def test_parameter_string_repr_and_as_cif_and_flags(): assert '± 0.1' in s and 'A' in s and '(free=True)' in s # CIF line is ` ` - assert p.as_cif == '_param.a 2.5000' + assert p.as_cif == '_param.a 2.50000000' # CifHandler uid is owner's unique_name (parameter name here) assert p._cif_handler.uid == p.unique_name == 'a' def test_parameter_uncertainty_must_be_non_negative(): - from easydiffraction.core.parameters import Parameter from easydiffraction.core.validation import AttributeSpec - from easydiffraction.core.validation import DataTypes + from easydiffraction.core.variable import Parameter from easydiffraction.io.cif.handler import CifHandler p = Parameter( name='b', - value_spec=AttributeSpec(value=1.0, type_=DataTypes.NUMERIC, default=0.0), + value_spec=AttributeSpec(default=1.0), cif_handler=CifHandler(names=['_param.b']), ) with pytest.raises(TypeError): @@ -85,14 +83,13 @@ def test_parameter_uncertainty_must_be_non_negative(): def test_parameter_fit_bounds_assign_and_read(): - from easydiffraction.core.parameters import Parameter from easydiffraction.core.validation import AttributeSpec - from easydiffraction.core.validation import DataTypes + from easydiffraction.core.variable import Parameter from easydiffraction.io.cif.handler import CifHandler p = Parameter( name='c', - value_spec=AttributeSpec(value=0.0, type_=DataTypes.NUMERIC, default=0.0), + value_spec=AttributeSpec(default=0.0), cif_handler=CifHandler(names=['_param.c']), ) p.fit_min = -1.0 diff --git a/tests/unit/easydiffraction/core/test_singletons.py b/tests/unit/easydiffraction/core/test_singletons.py index d2f8fa3a..ba69f07a 100644 --- a/tests/unit/easydiffraction/core/test_singletons.py +++ b/tests/unit/easydiffraction/core/test_singletons.py @@ -1,11 +1,11 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause import pytest def test_uid_map_handler_rejects_non_descriptor(): - from easydiffraction.core.singletons import UidMapHandler + from easydiffraction.core.singleton import UidMapHandler h = UidMapHandler.get() with pytest.raises(TypeError): diff --git a/tests/unit/easydiffraction/core/test_validation.py b/tests/unit/easydiffraction/core/test_validation.py index 64150f33..3bbd0ac3 100644 --- a/tests/unit/easydiffraction/core/test_validation.py +++ b/tests/unit/easydiffraction/core/test_validation.py @@ -1,6 +1,7 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause + def test_module_import(): import easydiffraction.core.validation as MUT @@ -9,7 +10,7 @@ def test_module_import(): assert expected_module_name == actual_module_name -def test_type_validator_accepts_and_rejects(monkeypatch): +def test_data_type_validator_accepts_and_rejects(monkeypatch): from easydiffraction.core.validation import AttributeSpec from easydiffraction.core.validation import DataTypes from easydiffraction.utils.logging import log @@ -17,7 +18,7 @@ def test_type_validator_accepts_and_rejects(monkeypatch): # So that errors do not raise in test process log.configure(reaction=log.Reaction.WARN) - spec = AttributeSpec(type_=DataTypes.STRING, default='abc') + spec = AttributeSpec(data_type=DataTypes.STRING, default='abc') # valid expected = 'xyz' actual = spec.validated('xyz', name='p') @@ -36,7 +37,7 @@ def test_range_validator_bounds(monkeypatch): log.configure(reaction=log.Reaction.WARN) spec = AttributeSpec( - type_=DataTypes.NUMERIC, default=1.0, content_validator=RangeValidator(ge=0, le=2) + data_type=DataTypes.NUMERIC, default=1.0, validator=RangeValidator(ge=0, le=2) ) # inside range expected = 1.5 @@ -55,11 +56,11 @@ def test_membership_and_regex_validators(monkeypatch): from easydiffraction.utils.logging import log log.configure(reaction=log.Reaction.WARN) - mspec = AttributeSpec(default='b', content_validator=MembershipValidator(['a', 'b'])) + mspec = AttributeSpec(default='b', validator=MembershipValidator(['a', 'b'])) assert mspec.validated('a', name='m') == 'a' # reject -> fallback default assert mspec.validated('c', name='m') == 'b' - rspec = AttributeSpec(default='a1', content_validator=RegexValidator(r'^[a-z]\d$')) + rspec = AttributeSpec(default='a1', validator=RegexValidator(r'^[a-z]\d$')) assert rspec.validated('b2', name='r') == 'b2' assert rspec.validated('BAD', name='r') == 'a1' diff --git a/tests/unit/easydiffraction/crystallography/test_crystallography.py b/tests/unit/easydiffraction/crystallography/test_crystallography.py index 3c2bf7df..73e1a134 100644 --- a/tests/unit/easydiffraction/crystallography/test_crystallography.py +++ b/tests/unit/easydiffraction/crystallography/test_crystallography.py @@ -1,6 +1,7 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause + def test_module_import(): import easydiffraction.crystallography.crystallography as MUT diff --git a/tests/unit/easydiffraction/crystallography/test_space_groups.py b/tests/unit/easydiffraction/crystallography/test_space_groups.py index 5629f633..dd1482cd 100644 --- a/tests/unit/easydiffraction/crystallography/test_space_groups.py +++ b/tests/unit/easydiffraction/crystallography/test_space_groups.py @@ -1,6 +1,7 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause + def test_module_import(): import easydiffraction.crystallography.space_groups as MUT diff --git a/tests/unit/easydiffraction/experiments/categories/background/test_base.py b/tests/unit/easydiffraction/datablocks/experiment/categories/background/test_base.py similarity index 72% rename from tests/unit/easydiffraction/experiments/categories/background/test_base.py rename to tests/unit/easydiffraction/datablocks/experiment/categories/background/test_base.py index 57f1bfeb..88049e83 100644 --- a/tests/unit/easydiffraction/experiments/categories/background/test_base.py +++ b/tests/unit/easydiffraction/datablocks/experiment/categories/background/test_base.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause import numpy as np @@ -7,23 +7,30 @@ def test_background_base_minimal_impl_and_collection_cif(): from easydiffraction.core.category import CategoryItem from easydiffraction.core.collection import CollectionBase - from easydiffraction.core.parameters import Parameter from easydiffraction.core.validation import AttributeSpec from easydiffraction.core.validation import DataTypes - from easydiffraction.experiments.categories.background.base import BackgroundBase + from easydiffraction.core.variable import Parameter + from easydiffraction.datablocks.experiment.categories.background.base import BackgroundBase from easydiffraction.io.cif.handler import CifHandler class ConstantBackground(CategoryItem): - def __init__(self, name: str, value: float): - # CategoryItem doesn't define __init__; call GuardedBase via super() + def __init__(self): super().__init__() self._identity.category_code = 'background' - self._identity.category_entry_name = name self._level = Parameter( name='level', - value_spec=AttributeSpec(value=value, type_=DataTypes.NUMERIC, default=0.0), + value_spec=AttributeSpec(data_type=DataTypes.NUMERIC, default=0.0), cif_handler=CifHandler(names=['_bkg.level']), ) + self._identity.category_entry_name = lambda: str(self._level.value) + + @property + def level(self): + return self._level + + @level.setter + def level(self, value): + self._level.value = value def calculate(self, x_data): return np.full_like(np.asarray(x_data), fill_value=self._level.value, dtype=float) @@ -48,9 +55,10 @@ def show(self) -> None: # pragma: no cover - trivial return None coll = BackgroundCollection() - a = ConstantBackground('a', 1.0) - coll.add('a', 1.0) - coll.add('b', 2.0) + a = ConstantBackground() + a.level = 1.0 + coll.create(level=1.0) + coll.create(level=2.0) # calculate sums two backgrounds externally (out of scope), here just verify item.calculate x = np.array([0.0, 1.0, 2.0]) diff --git a/tests/unit/easydiffraction/experiments/categories/background/test_chebyshev.py b/tests/unit/easydiffraction/datablocks/experiment/categories/background/test_chebyshev.py similarity index 69% rename from tests/unit/easydiffraction/experiments/categories/background/test_chebyshev.py rename to tests/unit/easydiffraction/datablocks/experiment/categories/background/test_chebyshev.py index d2de6aa7..d10a3c40 100644 --- a/tests/unit/easydiffraction/experiments/categories/background/test_chebyshev.py +++ b/tests/unit/easydiffraction/datablocks/experiment/categories/background/test_chebyshev.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause import numpy as np @@ -7,14 +7,14 @@ def test_chebyshev_background_calculate_and_cif(): from types import SimpleNamespace - from easydiffraction.experiments.categories.background.chebyshev import ( + from easydiffraction.datablocks.experiment.categories.background.chebyshev import ( ChebyshevPolynomialBackground, ) # Create mock parent with data x = np.linspace(0.0, 1.0, 5) mock_data = SimpleNamespace(x=x, _bkg=None) - mock_data._set_bkg = lambda y: setattr(mock_data, '_bkg', y) + mock_data._set_intensity_bkg = lambda y: setattr(mock_data, '_bkg', y) mock_parent = SimpleNamespace(data=mock_data) cb = ChebyshevPolynomialBackground() @@ -25,7 +25,7 @@ def test_chebyshev_background_calculate_and_cif(): assert np.allclose(mock_data._bkg, 0.0) # Add two terms and verify CIF contains expected tags - cb.add(order=0, coef=1.0) - cb.add(order=1, coef=0.5) + cb.create(order=0, coef=1.0) + cb.create(order=1, coef=0.5) cif = cb.as_cif assert '_pd_background.Chebyshev_order' in cif and '_pd_background.Chebyshev_coef' in cif diff --git a/tests/unit/easydiffraction/datablocks/experiment/categories/background/test_enums.py b/tests/unit/easydiffraction/datablocks/experiment/categories/background/test_enums.py new file mode 100644 index 00000000..b37a22e6 --- /dev/null +++ b/tests/unit/easydiffraction/datablocks/experiment/categories/background/test_enums.py @@ -0,0 +1,17 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + + +def test_background_type_info(): + from easydiffraction.datablocks.experiment.categories.background.chebyshev import ( + ChebyshevPolynomialBackground, + ) + from easydiffraction.datablocks.experiment.categories.background.line_segment import ( + LineSegmentBackground, + ) + + assert LineSegmentBackground.type_info.tag == 'line-segment' + assert LineSegmentBackground.type_info.description == 'Linear interpolation between points' + + assert ChebyshevPolynomialBackground.type_info.tag == 'chebyshev' + assert ChebyshevPolynomialBackground.type_info.description == 'Chebyshev polynomial background' diff --git a/tests/unit/easydiffraction/datablocks/experiment/categories/background/test_factory.py b/tests/unit/easydiffraction/datablocks/experiment/categories/background/test_factory.py new file mode 100644 index 00000000..09679489 --- /dev/null +++ b/tests/unit/easydiffraction/datablocks/experiment/categories/background/test_factory.py @@ -0,0 +1,22 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + +import pytest + + +def test_background_factory_default_and_errors(): + from easydiffraction.datablocks.experiment.categories.background.factory import ( + BackgroundFactory, + ) + + # Default via default_tag() + obj = BackgroundFactory.create(BackgroundFactory.default_tag()) + assert obj.__class__.__name__.endswith('LineSegmentBackground') + + # Explicit type by tag + obj2 = BackgroundFactory.create('chebyshev') + assert obj2.__class__.__name__.endswith('ChebyshevPolynomialBackground') + + # Unsupported tag should raise ValueError + with pytest.raises(ValueError): + BackgroundFactory.create('nonexistent') diff --git a/tests/unit/easydiffraction/experiments/categories/background/test_line_segment.py b/tests/unit/easydiffraction/datablocks/experiment/categories/background/test_line_segment.py similarity index 72% rename from tests/unit/easydiffraction/experiments/categories/background/test_line_segment.py rename to tests/unit/easydiffraction/datablocks/experiment/categories/background/test_line_segment.py index 483873db..ff231943 100644 --- a/tests/unit/easydiffraction/experiments/categories/background/test_line_segment.py +++ b/tests/unit/easydiffraction/datablocks/experiment/categories/background/test_line_segment.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause import numpy as np @@ -7,14 +7,14 @@ def test_line_segment_background_calculate_and_cif(): from types import SimpleNamespace - from easydiffraction.experiments.categories.background.line_segment import ( + from easydiffraction.datablocks.experiment.categories.background.line_segment import ( LineSegmentBackground, ) # Create mock parent with data x = np.array([0.0, 1.0, 2.0]) mock_data = SimpleNamespace(x=x, _bkg=None) - mock_data._set_bkg = lambda y: setattr(mock_data, '_bkg', y) + mock_data._set_intensity_bkg = lambda y: setattr(mock_data, '_bkg', y) mock_parent = SimpleNamespace(data=mock_data) bkg = LineSegmentBackground() @@ -25,8 +25,8 @@ def test_line_segment_background_calculate_and_cif(): assert np.allclose(mock_data._bkg, [0.0, 0.0, 0.0]) # Add two points -> linear interpolation - bkg.add(id='1', x=0.0, y=0.0) - bkg.add(id='2', x=2.0, y=4.0) + bkg.create(id='1', x=0.0, y=0.0) + bkg.create(id='2', x=2.0, y=4.0) bkg._update() assert np.allclose(mock_data._bkg, [0.0, 2.0, 4.0]) diff --git a/tests/unit/easydiffraction/datablocks/experiment/categories/data/test_bragg_pd.py b/tests/unit/easydiffraction/datablocks/experiment/categories/data/test_bragg_pd.py new file mode 100644 index 00000000..1a3f233c --- /dev/null +++ b/tests/unit/easydiffraction/datablocks/experiment/categories/data/test_bragg_pd.py @@ -0,0 +1,144 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + +import numpy as np + + +def test_pd_cwl_data_point_defaults(): + from easydiffraction.datablocks.experiment.categories.data.bragg_pd import PdCwlDataPoint + + pt = PdCwlDataPoint() + assert pt.point_id.value == '0' + assert pt.d_spacing.value == 0.0 + assert pt.two_theta.value == 0.0 + assert pt.intensity_meas.value == 0.0 + assert pt.intensity_meas_su.value == 1.0 + assert pt.intensity_calc.value == 0.0 + assert pt.intensity_bkg.value == 0.0 + assert pt.calc_status.value == 'incl' + assert pt._identity.category_code == 'pd_data' + + +def test_pd_tof_data_point_defaults(): + from easydiffraction.datablocks.experiment.categories.data.bragg_pd import PdTofDataPoint + + pt = PdTofDataPoint() + assert pt.point_id.value == '0' + assert pt.d_spacing.value == 0.0 + assert pt.time_of_flight.value == 0.0 + assert pt.intensity_meas.value == 0.0 + assert pt.intensity_meas_su.value == 1.0 + assert pt.intensity_calc.value == 0.0 + assert pt.intensity_bkg.value == 0.0 + assert pt.calc_status.value == 'incl' + assert pt._identity.category_code == 'pd_data' + + +def test_pd_cwl_data_collection_create_and_properties(): + from easydiffraction.datablocks.experiment.categories.data.bragg_pd import PdCwlData + + coll = PdCwlData() + + # Create items with x-coordinate (two_theta) values + x_vals = np.array([10.0, 20.0, 30.0]) + coll._create_items_set_xcoord_and_id(x_vals) + + assert len(coll._items) == 3 + + # Check two_theta property (returns calc items only, all included) + np.testing.assert_array_almost_equal(coll.two_theta, x_vals) + + # Check x is alias for two_theta + np.testing.assert_array_almost_equal(coll.x, coll.two_theta) + + # Check unfiltered_x returns all items + np.testing.assert_array_almost_equal(coll.unfiltered_x, x_vals) + + # Set and read measured intensities + meas = np.array([100.0, 200.0, 300.0]) + coll._set_intensity_meas(meas) + np.testing.assert_array_almost_equal(coll.intensity_meas, meas) + + # Set and read standard uncertainties + su = np.array([10.0, 20.0, 30.0]) + coll._set_intensity_meas_su(su) + np.testing.assert_array_almost_equal(coll.intensity_meas_su, su) + + # Check point IDs are set + assert coll._items[0].point_id.value == '1' + assert coll._items[1].point_id.value == '2' + assert coll._items[2].point_id.value == '3' + + +def test_pd_tof_data_collection_create_and_properties(): + from easydiffraction.datablocks.experiment.categories.data.bragg_pd import PdTofData + + coll = PdTofData() + + # Create items with x-coordinate (time_of_flight) values + x_vals = np.array([1000.0, 2000.0, 3000.0]) + coll._create_items_set_xcoord_and_id(x_vals) + + assert len(coll._items) == 3 + + # Check time_of_flight property + np.testing.assert_array_almost_equal(coll.time_of_flight, x_vals) + + # Check x is alias for time_of_flight + np.testing.assert_array_almost_equal(coll.x, coll.time_of_flight) + + # Check unfiltered_x returns all items + np.testing.assert_array_almost_equal(coll.unfiltered_x, x_vals) + + # Check point IDs are set + assert coll._items[0].point_id.value == '1' + assert coll._items[2].point_id.value == '3' + + +def test_pd_data_calc_status_exclusion(): + from easydiffraction.datablocks.experiment.categories.data.bragg_pd import PdCwlData + + coll = PdCwlData() + + x_vals = np.array([10.0, 20.0, 30.0, 40.0]) + coll._create_items_set_xcoord_and_id(x_vals) + coll._set_intensity_meas(np.array([100.0, 200.0, 300.0, 400.0])) + coll._set_intensity_meas_su(np.array([10.0, 20.0, 30.0, 40.0])) + + # Exclude the second and third points + coll._set_calc_status([True, False, False, True]) + + # calc_status should reflect the change + assert np.array_equal(coll.calc_status, np.array(['incl', 'excl', 'excl', 'incl'])) + + # x should only return included points + np.testing.assert_array_almost_equal(coll.x, np.array([10.0, 40.0])) + + # intensity_meas should only return included points + np.testing.assert_array_almost_equal(coll.intensity_meas, np.array([100.0, 400.0])) + + +def test_pd_cwl_data_type_info(): + from easydiffraction.datablocks.experiment.categories.data.bragg_pd import PdCwlData + from easydiffraction.datablocks.experiment.categories.data.bragg_pd import PdTofData + + assert PdCwlData.type_info.tag == 'bragg-pd' + assert PdCwlData.type_info.description == 'Bragg powder CWL data' + + assert PdTofData.type_info.tag == 'bragg-pd-tof' + assert PdTofData.type_info.description == 'Bragg powder TOF data' + + +def test_pd_data_intensity_meas_su_zero_replacement(): + from easydiffraction.datablocks.experiment.categories.data.bragg_pd import PdCwlData + + coll = PdCwlData() + x_vals = np.array([10.0, 20.0, 30.0]) + coll._create_items_set_xcoord_and_id(x_vals) + + # Set su with near-zero values — those should be replaced by 1.0 + coll._set_intensity_meas_su(np.array([0.0, 0.00001, 5.0])) + su = coll.intensity_meas_su + assert su[0] == 1.0 # replaced + assert su[1] == 1.0 # replaced + assert su[2] == 5.0 # kept diff --git a/tests/unit/easydiffraction/datablocks/experiment/categories/data/test_bragg_sc.py b/tests/unit/easydiffraction/datablocks/experiment/categories/data/test_bragg_sc.py new file mode 100644 index 00000000..534fd192 --- /dev/null +++ b/tests/unit/easydiffraction/datablocks/experiment/categories/data/test_bragg_sc.py @@ -0,0 +1,92 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + +import numpy as np + + +def test_refln_data_point_defaults(): + from easydiffraction.datablocks.experiment.categories.data.bragg_sc import Refln + + pt = Refln() + assert pt.id.value == '0' + assert pt.d_spacing.value == 0.0 + assert pt.sin_theta_over_lambda.value == 0.0 + assert pt.index_h.value == 0.0 + assert pt.index_k.value == 0.0 + assert pt.index_l.value == 0.0 + assert pt.intensity_meas.value == 0.0 + assert pt.intensity_meas_su.value == 0.0 + assert pt.intensity_calc.value == 0.0 + assert pt.wavelength.value == 0.0 + assert pt._identity.category_code == 'refln' + + +def test_refln_data_collection_create_and_properties(): + from easydiffraction.datablocks.experiment.categories.data.bragg_sc import ReflnData + + coll = ReflnData() + + # Create items with hkl + h = np.array([1.0, 2.0, 0.0]) + k = np.array([0.0, 1.0, 0.0]) + l = np.array([0.0, 0.0, 2.0]) + coll._create_items_set_hkl_and_id(h, k, l) + + assert len(coll._items) == 3 + + # Check hkl arrays + np.testing.assert_array_almost_equal(coll.index_h, h) + np.testing.assert_array_almost_equal(coll.index_k, k) + np.testing.assert_array_almost_equal(coll.index_l, l) + + # Check IDs are sequential + assert coll._items[0].id.value == '1' + assert coll._items[1].id.value == '2' + assert coll._items[2].id.value == '3' + + # Set and read measured intensities + meas = np.array([50.0, 100.0, 150.0]) + coll._set_intensity_meas(meas) + np.testing.assert_array_almost_equal(coll.intensity_meas, meas) + + # Set and read su + su = np.array([5.0, 10.0, 15.0]) + coll._set_intensity_meas_su(su) + np.testing.assert_array_almost_equal(coll.intensity_meas_su, su) + + # Set wavelength + wl = np.array([0.84, 0.84, 0.84]) + coll._set_wavelength(wl) + np.testing.assert_array_almost_equal(coll.wavelength, wl) + + # Set and read calculated intensities + calc = np.array([48.0, 102.0, 148.0]) + coll._set_intensity_calc(calc) + np.testing.assert_array_almost_equal(coll.intensity_calc, calc) + + +def test_refln_data_d_spacing_and_stol(): + from easydiffraction.datablocks.experiment.categories.data.bragg_sc import ReflnData + + coll = ReflnData() + h = np.array([1.0, 2.0]) + k = np.array([0.0, 0.0]) + l = np.array([0.0, 0.0]) + coll._create_items_set_hkl_and_id(h, k, l) + + # Set d-spacing + d = np.array([5.43, 2.715]) + coll._set_d_spacing(d) + np.testing.assert_array_almost_equal(coll.d_spacing, d) + + # Set sin(theta)/lambda + stol = np.array([0.092, 0.184]) + coll._set_sin_theta_over_lambda(stol) + np.testing.assert_array_almost_equal(coll.sin_theta_over_lambda, stol) + + +def test_refln_data_type_info(): + from easydiffraction.datablocks.experiment.categories.data.bragg_sc import ReflnData + + assert ReflnData.type_info.tag == 'bragg-sc' + assert ReflnData.type_info.description == 'Bragg single-crystal reflection data' diff --git a/tests/unit/easydiffraction/datablocks/experiment/categories/data/test_factory.py b/tests/unit/easydiffraction/datablocks/experiment/categories/data/test_factory.py new file mode 100644 index 00000000..6f52cdff --- /dev/null +++ b/tests/unit/easydiffraction/datablocks/experiment/categories/data/test_factory.py @@ -0,0 +1,85 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + +import pytest + + +def test_data_factory_default_and_errors(): + # Ensure concrete classes are registered + from easydiffraction.datablocks.experiment.categories.data import bragg_pd # noqa: F401 + from easydiffraction.datablocks.experiment.categories.data import bragg_sc # noqa: F401 + from easydiffraction.datablocks.experiment.categories.data import total_pd # noqa: F401 + from easydiffraction.datablocks.experiment.categories.data.factory import DataFactory + + # Explicit type by tag + obj = DataFactory.create('bragg-pd') + assert obj.__class__.__name__ == 'PdCwlData' + + # Explicit type by tag + obj2 = DataFactory.create('bragg-pd-tof') + assert obj2.__class__.__name__ == 'PdTofData' + + obj3 = DataFactory.create('bragg-sc') + assert obj3.__class__.__name__ == 'ReflnData' + + obj4 = DataFactory.create('total-pd') + assert obj4.__class__.__name__ == 'TotalData' + + # Unsupported tag should raise ValueError + with pytest.raises(ValueError): + DataFactory.create('nonexistent') + + +def test_data_factory_default_tag_resolution(): + # Ensure concrete classes are registered + from easydiffraction.datablocks.experiment.categories.data import bragg_pd # noqa: F401 + from easydiffraction.datablocks.experiment.categories.data import bragg_sc # noqa: F401 + from easydiffraction.datablocks.experiment.categories.data import total_pd # noqa: F401 + from easydiffraction.datablocks.experiment.categories.data.factory import DataFactory + from easydiffraction.datablocks.experiment.item.enums import BeamModeEnum + from easydiffraction.datablocks.experiment.item.enums import SampleFormEnum + from easydiffraction.datablocks.experiment.item.enums import ScatteringTypeEnum + + # Context-dependent default: Bragg powder CWL + tag = DataFactory.default_tag( + sample_form=SampleFormEnum.POWDER, + scattering_type=ScatteringTypeEnum.BRAGG, + beam_mode=BeamModeEnum.CONSTANT_WAVELENGTH, + ) + assert tag == 'bragg-pd' + + # Context-dependent default: Bragg powder TOF + tag = DataFactory.default_tag( + sample_form=SampleFormEnum.POWDER, + scattering_type=ScatteringTypeEnum.BRAGG, + beam_mode=BeamModeEnum.TIME_OF_FLIGHT, + ) + assert tag == 'bragg-pd-tof' + + # Context-dependent default: total scattering + tag = DataFactory.default_tag( + sample_form=SampleFormEnum.POWDER, + scattering_type=ScatteringTypeEnum.TOTAL, + ) + assert tag == 'total-pd' + + # Context-dependent default: single crystal + tag = DataFactory.default_tag( + sample_form=SampleFormEnum.SINGLE_CRYSTAL, + scattering_type=ScatteringTypeEnum.BRAGG, + ) + assert tag == 'bragg-sc' + + +def test_data_factory_supported_tags(): + # Ensure concrete classes are registered + from easydiffraction.datablocks.experiment.categories.data import bragg_pd # noqa: F401 + from easydiffraction.datablocks.experiment.categories.data import bragg_sc # noqa: F401 + from easydiffraction.datablocks.experiment.categories.data import total_pd # noqa: F401 + from easydiffraction.datablocks.experiment.categories.data.factory import DataFactory + + tags = DataFactory.supported_tags() + assert 'bragg-pd' in tags + assert 'bragg-pd-tof' in tags + assert 'bragg-sc' in tags + assert 'total-pd' in tags diff --git a/tests/unit/easydiffraction/datablocks/experiment/categories/data/test_total_pd.py b/tests/unit/easydiffraction/datablocks/experiment/categories/data/test_total_pd.py new file mode 100644 index 00000000..41e638e5 --- /dev/null +++ b/tests/unit/easydiffraction/datablocks/experiment/categories/data/test_total_pd.py @@ -0,0 +1,91 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + +import numpy as np + + +def test_total_data_point_defaults(): + from easydiffraction.datablocks.experiment.categories.data.total_pd import TotalDataPoint + + pt = TotalDataPoint() + assert pt.point_id.value == '0' + assert pt.r.value == 0.0 + assert pt.g_r_meas.value == 0.0 + assert pt.g_r_meas_su.value == 0.0 + assert pt.g_r_calc.value == 0.0 + assert pt.calc_status.value == 'incl' + assert pt._identity.category_code == 'total_data' + + +def test_total_data_collection_create_and_properties(): + from easydiffraction.datablocks.experiment.categories.data.total_pd import TotalData + + coll = TotalData() + + # Create items with r values + r_vals = np.array([1.0, 2.0, 3.0, 4.0]) + coll._create_items_set_xcoord_and_id(r_vals) + + assert len(coll._items) == 4 + + # Check x property (returns calc items, all included) + np.testing.assert_array_almost_equal(coll.x, r_vals) + + # Check unfiltered_x returns all items + np.testing.assert_array_almost_equal(coll.unfiltered_x, r_vals) + + # Set and read measured G(r) + g_meas = np.array([0.1, 0.5, 0.3, 0.2]) + coll._set_g_r_meas(g_meas) + np.testing.assert_array_almost_equal(coll.intensity_meas, g_meas) + + # Set and read su + g_su = np.array([0.01, 0.05, 0.03, 0.02]) + coll._set_g_r_meas_su(g_su) + np.testing.assert_array_almost_equal(coll.intensity_meas_su, g_su) + + # Point IDs + assert coll._items[0].point_id.value == '1' + assert coll._items[3].point_id.value == '4' + + +def test_total_data_calc_status_and_exclusion(): + from easydiffraction.datablocks.experiment.categories.data.total_pd import TotalData + + coll = TotalData() + r_vals = np.array([1.0, 2.0, 3.0, 4.0]) + coll._create_items_set_xcoord_and_id(r_vals) + coll._set_g_r_meas(np.array([0.1, 0.5, 0.3, 0.2])) + + # Exclude the second and third points + coll._set_calc_status([True, False, False, True]) + + assert np.array_equal(coll.calc_status, np.array(['incl', 'excl', 'excl', 'incl'])) + + # x should only return included points + np.testing.assert_array_almost_equal(coll.x, np.array([1.0, 4.0])) + + # intensity_meas should only return included points + np.testing.assert_array_almost_equal(coll.intensity_meas, np.array([0.1, 0.2])) + + +def test_total_data_intensity_bkg_always_zero(): + from easydiffraction.datablocks.experiment.categories.data.total_pd import TotalData + + coll = TotalData() + r_vals = np.array([1.0, 2.0, 3.0]) + coll._create_items_set_xcoord_and_id(r_vals) + + # Set calc G(r) so intensity_calc is non-empty + coll._set_g_r_calc(np.array([0.5, 0.6, 0.7])) + + # Background should always be zeros + bkg = coll.intensity_bkg + np.testing.assert_array_almost_equal(bkg, np.zeros(3)) + + +def test_total_data_type_info(): + from easydiffraction.datablocks.experiment.categories.data.total_pd import TotalData + + assert TotalData.type_info.tag == 'total-pd' + assert TotalData.type_info.description == 'Total scattering (PDF) data' diff --git a/tests/unit/easydiffraction/experiments/categories/instrument/test_base.py b/tests/unit/easydiffraction/datablocks/experiment/categories/instrument/test_base.py similarity index 57% rename from tests/unit/easydiffraction/experiments/categories/instrument/test_base.py rename to tests/unit/easydiffraction/datablocks/experiment/categories/instrument/test_base.py index 047d314b..38bcb8c7 100644 --- a/tests/unit/easydiffraction/experiments/categories/instrument/test_base.py +++ b/tests/unit/easydiffraction/datablocks/experiment/categories/instrument/test_base.py @@ -1,8 +1,9 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause + def test_instrument_base_sets_category_code(): - from easydiffraction.experiments.categories.instrument.base import InstrumentBase + from easydiffraction.datablocks.experiment.categories.instrument.base import InstrumentBase class DummyInstr(InstrumentBase): def __init__(self): diff --git a/tests/unit/easydiffraction/experiments/categories/instrument/test_cwl.py b/tests/unit/easydiffraction/datablocks/experiment/categories/instrument/test_cwl.py similarity index 54% rename from tests/unit/easydiffraction/experiments/categories/instrument/test_cwl.py rename to tests/unit/easydiffraction/datablocks/experiment/categories/instrument/test_cwl.py index dcefa62e..0816f6e7 100644 --- a/tests/unit/easydiffraction/experiments/categories/instrument/test_cwl.py +++ b/tests/unit/easydiffraction/datablocks/experiment/categories/instrument/test_cwl.py @@ -1,11 +1,11 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -from easydiffraction.experiments.categories.instrument.cwl import CwlInstrument +from easydiffraction.datablocks.experiment.categories.instrument.cwl import CwlPdInstrument def test_cwl_instrument_parameters_settable(): - instr = CwlInstrument() + instr = CwlPdInstrument() instr.setup_wavelength = 2.0 instr.calib_twotheta_offset = 0.1 assert instr.setup_wavelength.value == 2.0 diff --git a/tests/unit/easydiffraction/datablocks/experiment/categories/instrument/test_factory.py b/tests/unit/easydiffraction/datablocks/experiment/categories/instrument/test_factory.py new file mode 100644 index 00000000..04117aa9 --- /dev/null +++ b/tests/unit/easydiffraction/datablocks/experiment/categories/instrument/test_factory.py @@ -0,0 +1,37 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + +import pytest + + +def test_instrument_factory_default_and_errors(): + try: + from easydiffraction.datablocks.experiment.categories.instrument.factory import ( + InstrumentFactory, + ) + from easydiffraction.datablocks.experiment.item.enums import BeamModeEnum + from easydiffraction.datablocks.experiment.item.enums import SampleFormEnum + except ImportError as e: # pragma: no cover - environment-specific circular import + pytest.skip(f'InstrumentFactory import triggers circular import in this context: {e}') + return + + # By tag + inst = InstrumentFactory.create('cwl-pd') + assert inst.__class__.__name__ == 'CwlPdInstrument' + + # By tag + inst2 = InstrumentFactory.create('cwl-pd') + assert inst2.__class__.__name__ == 'CwlPdInstrument' + inst3 = InstrumentFactory.create('tof-pd') + assert inst3.__class__.__name__ == 'TofPdInstrument' + + # Context-dependent default + tag = InstrumentFactory.default_tag( + beam_mode=BeamModeEnum.TIME_OF_FLIGHT, + sample_form=SampleFormEnum.POWDER, + ) + assert tag == 'tof-pd' + + # Invalid tag + with pytest.raises(ValueError): + InstrumentFactory.create('nonexistent') diff --git a/tests/unit/easydiffraction/experiments/categories/instrument/test_tof.py b/tests/unit/easydiffraction/datablocks/experiment/categories/instrument/test_tof.py similarity index 86% rename from tests/unit/easydiffraction/experiments/categories/instrument/test_tof.py rename to tests/unit/easydiffraction/datablocks/experiment/categories/instrument/test_tof.py index 2d4e48b1..bfd6cc12 100644 --- a/tests/unit/easydiffraction/experiments/categories/instrument/test_tof.py +++ b/tests/unit/easydiffraction/datablocks/experiment/categories/instrument/test_tof.py @@ -1,13 +1,13 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause import numpy as np def test_tof_instrument_defaults_and_setters_and_parameters_and_cif(): - from easydiffraction.experiments.categories.instrument.tof import TofInstrument + from easydiffraction.datablocks.experiment.categories.instrument.tof import TofPdInstrument - inst = TofInstrument() + inst = TofPdInstrument() # Defaults assert np.isclose(inst.setup_twotheta_bank.value, 150.0) diff --git a/tests/unit/easydiffraction/experiments/categories/peak/test_base.py b/tests/unit/easydiffraction/datablocks/experiment/categories/peak/test_base.py similarity index 56% rename from tests/unit/easydiffraction/experiments/categories/peak/test_base.py rename to tests/unit/easydiffraction/datablocks/experiment/categories/peak/test_base.py index 737c7e17..229fc734 100644 --- a/tests/unit/easydiffraction/experiments/categories/peak/test_base.py +++ b/tests/unit/easydiffraction/datablocks/experiment/categories/peak/test_base.py @@ -1,7 +1,7 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -from easydiffraction.experiments.categories.peak.base import PeakBase +from easydiffraction.datablocks.experiment.categories.peak.base import PeakBase def test_peak_base_identity_code(): diff --git a/tests/unit/easydiffraction/experiments/categories/peak/test_cwl.py b/tests/unit/easydiffraction/datablocks/experiment/categories/peak/test_cwl.py similarity index 72% rename from tests/unit/easydiffraction/experiments/categories/peak/test_cwl.py rename to tests/unit/easydiffraction/datablocks/experiment/categories/peak/test_cwl.py index b3b16d4e..d941afe9 100644 --- a/tests/unit/easydiffraction/experiments/categories/peak/test_cwl.py +++ b/tests/unit/easydiffraction/datablocks/experiment/categories/peak/test_cwl.py @@ -1,10 +1,11 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause + def test_cwl_peak_classes_expose_expected_parameters_and_category(): - from easydiffraction.experiments.categories.peak.cwl import CwlPseudoVoigt - from easydiffraction.experiments.categories.peak.cwl import CwlSplitPseudoVoigt - from easydiffraction.experiments.categories.peak.cwl import CwlThompsonCoxHastings + from easydiffraction.datablocks.experiment.categories.peak.cwl import CwlPseudoVoigt + from easydiffraction.datablocks.experiment.categories.peak.cwl import CwlSplitPseudoVoigt + from easydiffraction.datablocks.experiment.categories.peak.cwl import CwlThompsonCoxHastings pv = CwlPseudoVoigt() spv = CwlSplitPseudoVoigt() diff --git a/tests/unit/easydiffraction/experiments/categories/peak/test_cwl_mixins.py b/tests/unit/easydiffraction/datablocks/experiment/categories/peak/test_cwl_mixins.py similarity index 65% rename from tests/unit/easydiffraction/experiments/categories/peak/test_cwl_mixins.py rename to tests/unit/easydiffraction/datablocks/experiment/categories/peak/test_cwl_mixins.py index ffde4deb..19026f50 100644 --- a/tests/unit/easydiffraction/experiments/categories/peak/test_cwl_mixins.py +++ b/tests/unit/easydiffraction/datablocks/experiment/categories/peak/test_cwl_mixins.py @@ -1,14 +1,14 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -from easydiffraction.experiments.categories.peak.cwl import CwlPseudoVoigt -from easydiffraction.experiments.categories.peak.cwl import CwlSplitPseudoVoigt -from easydiffraction.experiments.categories.peak.cwl import CwlThompsonCoxHastings +from easydiffraction.datablocks.experiment.categories.peak.cwl import CwlPseudoVoigt +from easydiffraction.datablocks.experiment.categories.peak.cwl import CwlSplitPseudoVoigt +from easydiffraction.datablocks.experiment.categories.peak.cwl import CwlThompsonCoxHastings def test_cwl_pseudo_voigt_params_exist_and_settable(): peak = CwlPseudoVoigt() - # Created by _add_constant_wavelength_broadening + # CwlBroadening parameters assert peak.broad_gauss_u.name == 'broad_gauss_u' peak.broad_gauss_u = 0.123 assert peak.broad_gauss_u.value == 0.123 diff --git a/tests/unit/easydiffraction/datablocks/experiment/categories/peak/test_factory.py b/tests/unit/easydiffraction/datablocks/experiment/categories/peak/test_factory.py new file mode 100644 index 00000000..4b0ccd35 --- /dev/null +++ b/tests/unit/easydiffraction/datablocks/experiment/categories/peak/test_factory.py @@ -0,0 +1,54 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + +import pytest + + +def test_peak_factory_default_and_combinations_and_errors(): + from easydiffraction.datablocks.experiment.categories.peak.factory import PeakFactory + from easydiffraction.datablocks.experiment.item.enums import BeamModeEnum + from easydiffraction.datablocks.experiment.item.enums import ScatteringTypeEnum + + # Explicit valid combos by tag + p = PeakFactory.create('pseudo-voigt') + assert p._identity.category_code == 'peak' + + # Explicit valid combos by tag + p1 = PeakFactory.create('pseudo-voigt') + assert p1.__class__.__name__ == 'CwlPseudoVoigt' + + p2 = PeakFactory.create('pseudo-voigt * ikeda-carpenter') + assert p2.__class__.__name__ == 'TofPseudoVoigtIkedaCarpenter' + + p3 = PeakFactory.create('gaussian-damped-sinc') + assert p3.__class__.__name__ == 'TotalGaussianDampedSinc' + + # Context-dependent defaults + tag_bragg_cwl = PeakFactory.default_tag( + scattering_type=ScatteringTypeEnum.BRAGG, + beam_mode=BeamModeEnum.CONSTANT_WAVELENGTH, + ) + assert tag_bragg_cwl == 'pseudo-voigt' + + tag_bragg_tof = PeakFactory.default_tag( + scattering_type=ScatteringTypeEnum.BRAGG, + beam_mode=BeamModeEnum.TIME_OF_FLIGHT, + ) + assert tag_bragg_tof == 'pseudo-voigt * ikeda-carpenter' + + tag_total = PeakFactory.default_tag( + scattering_type=ScatteringTypeEnum.TOTAL, + ) + assert tag_total == 'gaussian-damped-sinc' + + # supported_for filtering + cwl_profiles = PeakFactory.supported_for( + scattering_type=ScatteringTypeEnum.BRAGG, + beam_mode=BeamModeEnum.CONSTANT_WAVELENGTH, + ) + assert len(cwl_profiles) == 3 + assert all(k.type_info.tag for k in cwl_profiles) + + # Invalid tag + with pytest.raises(ValueError): + PeakFactory.create('nonexistent-profile') diff --git a/tests/unit/easydiffraction/experiments/categories/peak/test_tof.py b/tests/unit/easydiffraction/datablocks/experiment/categories/peak/test_tof.py similarity index 62% rename from tests/unit/easydiffraction/experiments/categories/peak/test_tof.py rename to tests/unit/easydiffraction/datablocks/experiment/categories/peak/test_tof.py index 0ace3beb..78e25d52 100644 --- a/tests/unit/easydiffraction/experiments/categories/peak/test_tof.py +++ b/tests/unit/easydiffraction/datablocks/experiment/categories/peak/test_tof.py @@ -1,9 +1,9 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -from easydiffraction.experiments.categories.peak.tof import TofPseudoVoigt -from easydiffraction.experiments.categories.peak.tof import TofPseudoVoigtBackToBack -from easydiffraction.experiments.categories.peak.tof import TofPseudoVoigtIkedaCarpenter +from easydiffraction.datablocks.experiment.categories.peak.tof import TofPseudoVoigt +from easydiffraction.datablocks.experiment.categories.peak.tof import TofPseudoVoigtBackToBack +from easydiffraction.datablocks.experiment.categories.peak.tof import TofPseudoVoigtIkedaCarpenter def test_tof_pseudo_voigt_has_broadening_params(): diff --git a/tests/unit/easydiffraction/experiments/categories/peak/test_tof_mixins.py b/tests/unit/easydiffraction/datablocks/experiment/categories/peak/test_tof_mixins.py similarity index 58% rename from tests/unit/easydiffraction/experiments/categories/peak/test_tof_mixins.py rename to tests/unit/easydiffraction/datablocks/experiment/categories/peak/test_tof_mixins.py index c54fdc93..c2f114a0 100644 --- a/tests/unit/easydiffraction/experiments/categories/peak/test_tof_mixins.py +++ b/tests/unit/easydiffraction/datablocks/experiment/categories/peak/test_tof_mixins.py @@ -1,19 +1,23 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause import numpy as np def test_tof_broadening_and_asymmetry_mixins(): - from easydiffraction.experiments.categories.peak.base import PeakBase - from easydiffraction.experiments.categories.peak.tof_mixins import IkedaCarpenterAsymmetryMixin - from easydiffraction.experiments.categories.peak.tof_mixins import TofBroadeningMixin + from easydiffraction.datablocks.experiment.categories.peak.base import PeakBase + from easydiffraction.datablocks.experiment.categories.peak.tof_mixins import ( + IkedaCarpenterAsymmetryMixin, + ) + from easydiffraction.datablocks.experiment.categories.peak.tof_mixins import TofBroadeningMixin - class TofPeak(PeakBase, TofBroadeningMixin, IkedaCarpenterAsymmetryMixin): + class TofPeak( + PeakBase, + TofBroadeningMixin, + IkedaCarpenterAsymmetryMixin, + ): def __init__(self): super().__init__() - self._add_time_of_flight_broadening() - self._add_ikeda_carpenter_asymmetry() p = TofPeak() names = {param.name for param in p.parameters} diff --git a/tests/unit/easydiffraction/experiments/categories/peak/test_total.py b/tests/unit/easydiffraction/datablocks/experiment/categories/peak/test_total.py similarity index 82% rename from tests/unit/easydiffraction/experiments/categories/peak/test_total.py rename to tests/unit/easydiffraction/datablocks/experiment/categories/peak/test_total.py index 7529d875..46a6497b 100644 --- a/tests/unit/easydiffraction/experiments/categories/peak/test_total.py +++ b/tests/unit/easydiffraction/datablocks/experiment/categories/peak/test_total.py @@ -1,11 +1,11 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause import numpy as np def test_total_gaussian_damped_sinc_parameters_and_setters(): - from easydiffraction.experiments.categories.peak.total import TotalGaussianDampedSinc + from easydiffraction.datablocks.experiment.categories.peak.total import TotalGaussianDampedSinc p = TotalGaussianDampedSinc() assert p._identity.category_code == 'peak' diff --git a/tests/unit/easydiffraction/experiments/categories/peak/test_total_mixins.py b/tests/unit/easydiffraction/datablocks/experiment/categories/peak/test_total_mixins.py similarity index 57% rename from tests/unit/easydiffraction/experiments/categories/peak/test_total_mixins.py rename to tests/unit/easydiffraction/datablocks/experiment/categories/peak/test_total_mixins.py index 475ab781..c3534847 100644 --- a/tests/unit/easydiffraction/experiments/categories/peak/test_total_mixins.py +++ b/tests/unit/easydiffraction/datablocks/experiment/categories/peak/test_total_mixins.py @@ -1,7 +1,7 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -from easydiffraction.experiments.categories.peak.total import TotalGaussianDampedSinc +from easydiffraction.datablocks.experiment.categories.peak.total import TotalGaussianDampedSinc def test_total_gaussian_damped_sinc_params(): diff --git a/tests/unit/easydiffraction/experiments/categories/test_excluded_regions.py b/tests/unit/easydiffraction/datablocks/experiment/categories/test_excluded_regions.py similarity index 83% rename from tests/unit/easydiffraction/experiments/categories/test_excluded_regions.py rename to tests/unit/easydiffraction/datablocks/experiment/categories/test_excluded_regions.py index 7aa3e833..8f34b8de 100644 --- a/tests/unit/easydiffraction/experiments/categories/test_excluded_regions.py +++ b/tests/unit/easydiffraction/datablocks/experiment/categories/test_excluded_regions.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause import numpy as np @@ -7,14 +7,14 @@ def test_excluded_regions_add_updates_datastore_and_cif(): from types import SimpleNamespace - from easydiffraction.experiments.categories.excluded_regions import ExcludedRegions + from easydiffraction.datablocks.experiment.categories.excluded_regions import ExcludedRegions # Minimal fake datastore full_x = np.array([0.0, 1.0, 2.0, 3.0]) full_meas = np.array([10.0, 11.0, 12.0, 13.0]) full_meas_su = np.array([1.0, 1.0, 1.0, 1.0]) ds = SimpleNamespace( - all_x=full_x, # ExcludedRegions._update uses all_x not full_x + unfiltered_x=full_x, full_x=full_x, full_meas=full_meas, full_meas_su=full_meas_su, @@ -23,7 +23,7 @@ def test_excluded_regions_add_updates_datastore_and_cif(): meas=full_meas.copy(), meas_su=full_meas_su.copy(), ) - + def set_calc_status(status): # _set_calc_status sets excluded to the inverse ds.excluded = ~status @@ -31,14 +31,14 @@ def set_calc_status(status): ds.x = ds.full_x[status] ds.meas = ds.full_meas[status] ds.meas_su = ds.full_meas_su[status] - + ds._set_calc_status = set_calc_status coll = ExcludedRegions() # stitch in a parent with data object.__setattr__(coll, '_parent', SimpleNamespace(data=ds)) - coll.add(start=1.0, end=2.0) + coll.create(start=1.0, end=2.0) # Call _update() to apply exclusions coll._update() diff --git a/tests/unit/easydiffraction/datablocks/experiment/categories/test_experiment_type.py b/tests/unit/easydiffraction/datablocks/experiment/categories/test_experiment_type.py new file mode 100644 index 00000000..9190071e --- /dev/null +++ b/tests/unit/easydiffraction/datablocks/experiment/categories/test_experiment_type.py @@ -0,0 +1,37 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + + +def test_module_import(): + import easydiffraction.datablocks.experiment.categories.experiment_type as MUT + + expected_module_name = 'easydiffraction.datablocks.experiment.categories.experiment_type' + actual_module_name = MUT.__name__ + assert expected_module_name == actual_module_name + + +def test_experiment_type_properties_and_validation(monkeypatch): + from easydiffraction.datablocks.experiment.categories.experiment_type import ExperimentType + from easydiffraction.datablocks.experiment.item.enums import BeamModeEnum + from easydiffraction.datablocks.experiment.item.enums import RadiationProbeEnum + from easydiffraction.datablocks.experiment.item.enums import SampleFormEnum + from easydiffraction.datablocks.experiment.item.enums import ScatteringTypeEnum + from easydiffraction.utils.logging import log + + log.configure(reaction=log.Reaction.WARN) + + et = ExperimentType() + et._set_sample_form(SampleFormEnum.POWDER.value) + et._set_beam_mode(BeamModeEnum.CONSTANT_WAVELENGTH.value) + et._set_radiation_probe(RadiationProbeEnum.NEUTRON.value) + et._set_scattering_type(ScatteringTypeEnum.BRAGG.value) + + # getters nominal + assert et.sample_form.value == SampleFormEnum.POWDER.value + assert et.beam_mode.value == BeamModeEnum.CONSTANT_WAVELENGTH.value + assert et.radiation_probe.value == RadiationProbeEnum.NEUTRON.value + assert et.scattering_type.value == ScatteringTypeEnum.BRAGG.value + + # public setters are blocked (read-only properties via GuardedBase) + et.sample_form = 'single crystal' + assert et.sample_form.value == SampleFormEnum.POWDER.value diff --git a/tests/unit/easydiffraction/datablocks/experiment/categories/test_extinction.py b/tests/unit/easydiffraction/datablocks/experiment/categories/test_extinction.py new file mode 100644 index 00000000..dcee6f6e --- /dev/null +++ b/tests/unit/easydiffraction/datablocks/experiment/categories/test_extinction.py @@ -0,0 +1,76 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + + +def test_module_import(): + import easydiffraction.datablocks.experiment.categories.extinction.shelx as MUT + + expected_module_name = 'easydiffraction.datablocks.experiment.categories.extinction.shelx' + actual_module_name = MUT.__name__ + assert expected_module_name == actual_module_name + + +def test_extinction_defaults(): + from easydiffraction.datablocks.experiment.categories.extinction.shelx import ShelxExtinction + + ext = ShelxExtinction() + assert ext.mosaicity.value == 1.0 + assert ext.radius.value == 1.0 + assert ext._identity.category_code == 'extinction' + + +def test_extinction_property_setters(): + from easydiffraction.datablocks.experiment.categories.extinction.shelx import ShelxExtinction + + ext = ShelxExtinction() + + ext.mosaicity = 0.5 + assert ext.mosaicity.value == 0.5 + + ext.radius = 10.0 + assert ext.radius.value == 10.0 + + +def test_extinction_cif_handler_names(): + from easydiffraction.datablocks.experiment.categories.extinction.shelx import ShelxExtinction + + ext = ShelxExtinction() + + mosaicity_cif_names = ext._mosaicity._cif_handler.names + assert '_extinction.mosaicity' in mosaicity_cif_names + + radius_cif_names = ext._radius._cif_handler.names + assert '_extinction.radius' in radius_cif_names + + +def test_extinction_type_info(): + from easydiffraction.datablocks.experiment.categories.extinction.shelx import ShelxExtinction + + assert ShelxExtinction.type_info.tag == 'shelx' + assert ShelxExtinction.type_info.description != '' + + +def test_extinction_factory_registration(): + from easydiffraction.datablocks.experiment.categories.extinction.factory import ( + ExtinctionFactory, + ) + + assert 'shelx' in ExtinctionFactory.supported_tags() + + +def test_extinction_factory_create(): + from easydiffraction.datablocks.experiment.categories.extinction.factory import ( + ExtinctionFactory, + ) + from easydiffraction.datablocks.experiment.categories.extinction.shelx import ShelxExtinction + + ext = ExtinctionFactory.create('shelx') + assert isinstance(ext, ShelxExtinction) + + +def test_extinction_factory_default_tag(): + from easydiffraction.datablocks.experiment.categories.extinction.factory import ( + ExtinctionFactory, + ) + + assert ExtinctionFactory.default_tag() == 'shelx' diff --git a/tests/unit/easydiffraction/datablocks/experiment/categories/test_linked_crystal.py b/tests/unit/easydiffraction/datablocks/experiment/categories/test_linked_crystal.py new file mode 100644 index 00000000..69f96e7b --- /dev/null +++ b/tests/unit/easydiffraction/datablocks/experiment/categories/test_linked_crystal.py @@ -0,0 +1,88 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + + +def test_module_import(): + import easydiffraction.datablocks.experiment.categories.linked_crystal.default as MUT + + expected_module_name = ( + 'easydiffraction.datablocks.experiment.categories.linked_crystal.default' + ) + actual_module_name = MUT.__name__ + assert expected_module_name == actual_module_name + + +def test_linked_crystal_defaults(): + from easydiffraction.datablocks.experiment.categories.linked_crystal.default import ( + LinkedCrystal, + ) + + lc = LinkedCrystal() + assert lc.id.value == 'Si' + assert lc.scale.value == 1.0 + assert lc._identity.category_code == 'linked_crystal' + + +def test_linked_crystal_property_setters(): + from easydiffraction.datablocks.experiment.categories.linked_crystal.default import ( + LinkedCrystal, + ) + + lc = LinkedCrystal() + + lc.id = 'Ge' + assert lc.id.value == 'Ge' + + lc.scale = 2.5 + assert lc.scale.value == 2.5 + + +def test_linked_crystal_cif_handler_names(): + from easydiffraction.datablocks.experiment.categories.linked_crystal.default import ( + LinkedCrystal, + ) + + lc = LinkedCrystal() + + id_cif_names = lc._id._cif_handler.names + assert '_sc_crystal_block.id' in id_cif_names + + scale_cif_names = lc._scale._cif_handler.names + assert '_sc_crystal_block.scale' in scale_cif_names + + +def test_linked_crystal_type_info(): + from easydiffraction.datablocks.experiment.categories.linked_crystal.default import ( + LinkedCrystal, + ) + + assert LinkedCrystal.type_info.tag == 'default' + assert LinkedCrystal.type_info.description != '' + + +def test_linked_crystal_factory_registration(): + from easydiffraction.datablocks.experiment.categories.linked_crystal.factory import ( + LinkedCrystalFactory, + ) + + assert 'default' in LinkedCrystalFactory.supported_tags() + + +def test_linked_crystal_factory_create(): + from easydiffraction.datablocks.experiment.categories.linked_crystal.default import ( + LinkedCrystal, + ) + from easydiffraction.datablocks.experiment.categories.linked_crystal.factory import ( + LinkedCrystalFactory, + ) + + lc = LinkedCrystalFactory.create('default') + assert isinstance(lc, LinkedCrystal) + + +def test_linked_crystal_factory_default_tag(): + from easydiffraction.datablocks.experiment.categories.linked_crystal.factory import ( + LinkedCrystalFactory, + ) + + assert LinkedCrystalFactory.default_tag() == 'default' diff --git a/tests/unit/easydiffraction/datablocks/experiment/categories/test_linked_phases.py b/tests/unit/easydiffraction/datablocks/experiment/categories/test_linked_phases.py new file mode 100644 index 00000000..3b4b9fa7 --- /dev/null +++ b/tests/unit/easydiffraction/datablocks/experiment/categories/test_linked_phases.py @@ -0,0 +1,19 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + + +def test_linked_phases_add_and_cif_headers(): + from easydiffraction.datablocks.experiment.categories.linked_phases import LinkedPhase + from easydiffraction.datablocks.experiment.categories.linked_phases import LinkedPhases + + lp = LinkedPhase() + lp.id = 'Si' + lp.scale = 2.0 + assert lp.id.value == 'Si' and lp.scale.value == 2.0 + + coll = LinkedPhases() + coll.create(id='Si', scale=2.0) + + # CIF loop header presence + cif = coll.as_cif + assert 'loop_' in cif and '_pd_phase_block.id' in cif and '_pd_phase_block.scale' in cif diff --git a/tests/unit/easydiffraction/datablocks/experiment/item/test_base.py b/tests/unit/easydiffraction/datablocks/experiment/item/test_base.py new file mode 100644 index 00000000..b666374a --- /dev/null +++ b/tests/unit/easydiffraction/datablocks/experiment/item/test_base.py @@ -0,0 +1,38 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + + +def test_module_import(): + import easydiffraction.datablocks.experiment.item.base as MUT + + expected_module_name = 'easydiffraction.datablocks.experiment.item.base' + actual_module_name = MUT.__name__ + assert expected_module_name == actual_module_name + + +def test_pd_experiment_peak_profile_type_switch(capsys): + from easydiffraction.datablocks.experiment.categories.experiment_type import ExperimentType + from easydiffraction.datablocks.experiment.item.base import PdExperimentBase + from easydiffraction.datablocks.experiment.item.enums import BeamModeEnum + from easydiffraction.datablocks.experiment.item.enums import RadiationProbeEnum + from easydiffraction.datablocks.experiment.item.enums import SampleFormEnum + from easydiffraction.datablocks.experiment.item.enums import ScatteringTypeEnum + + class ConcretePd(PdExperimentBase): + def _load_ascii_data_to_experiment(self, data_path: str) -> int: + return 0 + + et = ExperimentType() + et._set_sample_form(SampleFormEnum.POWDER.value) + et._set_beam_mode(BeamModeEnum.CONSTANT_WAVELENGTH.value) + et._set_radiation_probe(RadiationProbeEnum.NEUTRON.value) + et._set_scattering_type(ScatteringTypeEnum.BRAGG.value) + + ex = ConcretePd(name='ex1', type=et) + # valid switch using tag string + ex.peak_profile_type = 'pseudo-voigt' + assert ex.peak_profile_type == 'pseudo-voigt' + # invalid string should warn and keep previous + ex.peak_profile_type = 'non-existent' + captured = capsys.readouterr().out + assert 'Unsupported' in captured or 'Unknown' in captured diff --git a/tests/unit/easydiffraction/experiments/experiment/test_bragg_pd.py b/tests/unit/easydiffraction/datablocks/experiment/item/test_bragg_pd.py similarity index 58% rename from tests/unit/easydiffraction/experiments/experiment/test_bragg_pd.py rename to tests/unit/easydiffraction/datablocks/experiment/item/test_bragg_pd.py index f070d4fd..89c763dc 100644 --- a/tests/unit/easydiffraction/experiments/experiment/test_bragg_pd.py +++ b/tests/unit/easydiffraction/datablocks/experiment/item/test_bragg_pd.py @@ -1,39 +1,39 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause import numpy as np import pytest -from easydiffraction.experiments.categories.background.enums import BackgroundTypeEnum -from easydiffraction.experiments.categories.experiment_type import ExperimentType -from easydiffraction.experiments.experiment.bragg_pd import BraggPdExperiment -from easydiffraction.experiments.experiment.enums import BeamModeEnum -from easydiffraction.experiments.experiment.enums import RadiationProbeEnum -from easydiffraction.experiments.experiment.enums import SampleFormEnum -from easydiffraction.experiments.experiment.enums import ScatteringTypeEnum +from easydiffraction.datablocks.experiment.categories.background.factory import BackgroundFactory +from easydiffraction.datablocks.experiment.categories.experiment_type import ExperimentType +from easydiffraction.datablocks.experiment.item.bragg_pd import BraggPdExperiment +from easydiffraction.datablocks.experiment.item.enums import BeamModeEnum +from easydiffraction.datablocks.experiment.item.enums import RadiationProbeEnum +from easydiffraction.datablocks.experiment.item.enums import SampleFormEnum +from easydiffraction.datablocks.experiment.item.enums import ScatteringTypeEnum def _mk_type_powder_cwl_bragg(): - return ExperimentType( - sample_form=SampleFormEnum.POWDER.value, - beam_mode=BeamModeEnum.CONSTANT_WAVELENGTH.value, - radiation_probe=RadiationProbeEnum.NEUTRON.value, - scattering_type=ScatteringTypeEnum.BRAGG.value, - ) + et = ExperimentType() + et._set_sample_form(SampleFormEnum.POWDER.value) + et._set_beam_mode(BeamModeEnum.CONSTANT_WAVELENGTH.value) + et._set_radiation_probe(RadiationProbeEnum.NEUTRON.value) + et._set_scattering_type(ScatteringTypeEnum.BRAGG.value) + return et def test_background_defaults_and_change(): expt = BraggPdExperiment(name='e1', type=_mk_type_powder_cwl_bragg()) # default background type - assert expt.background_type == BackgroundTypeEnum.default() + assert expt.background_type == BackgroundFactory.default_tag() # change to a supported type - expt.background_type = BackgroundTypeEnum.CHEBYSHEV - assert expt.background_type == BackgroundTypeEnum.CHEBYSHEV + expt.background_type = 'chebyshev' + assert expt.background_type == 'chebyshev' # unknown type keeps previous type and prints warnings (no raise) expt.background_type = 'not-a-type' # invalid string - assert expt.background_type == BackgroundTypeEnum.CHEBYSHEV + assert expt.background_type == 'chebyshev' def test_load_ascii_data_rounds_and_defaults_sy(tmp_path: pytest.TempPathFactory): @@ -53,7 +53,7 @@ def test_load_ascii_data_rounds_and_defaults_sy(tmp_path: pytest.TempPathFactory # sy = sqrt(y) with values < 1e-4 replaced by 1.0 expected_sy = np.sqrt(y) expected_sy = np.where(expected_sy < 1e-4, 1.0, expected_sy) - assert np.allclose(expt.data.meas_su, expected_sy) + assert np.allclose(expt.data.intensity_meas_su, expected_sy) # Check that data array shapes match assert len(expt.data.x) == len(x) @@ -64,7 +64,7 @@ def test_load_ascii_data_rounds_and_defaults_sy(tmp_path: pytest.TempPathFactory np.savetxt(p3, data3) expt._load_ascii_data_to_experiment(str(p3)) expected_sy3 = np.where(sy < 1e-4, 1.0, sy) - assert np.allclose(expt.data.meas_su, expected_sy3) + assert np.allclose(expt.data.intensity_meas_su, expected_sy3) # Case 3: invalid shape -> currently triggers an exception (IndexError on shape[1]) pinv = tmp_path / 'invalid.dat' diff --git a/tests/unit/easydiffraction/datablocks/experiment/item/test_bragg_sc.py b/tests/unit/easydiffraction/datablocks/experiment/item/test_bragg_sc.py new file mode 100644 index 00000000..014f65fe --- /dev/null +++ b/tests/unit/easydiffraction/datablocks/experiment/item/test_bragg_sc.py @@ -0,0 +1,36 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + +import pytest + +from easydiffraction.datablocks.experiment.categories.experiment_type import ExperimentType +from easydiffraction.datablocks.experiment.item.bragg_sc import CwlScExperiment +from easydiffraction.datablocks.experiment.item.enums import BeamModeEnum +from easydiffraction.datablocks.experiment.item.enums import RadiationProbeEnum +from easydiffraction.datablocks.experiment.item.enums import SampleFormEnum +from easydiffraction.datablocks.experiment.item.enums import ScatteringTypeEnum +from easydiffraction.utils.logging import Logger + + +def _mk_type_sc_bragg(): + et = ExperimentType() + et._set_sample_form(SampleFormEnum.SINGLE_CRYSTAL.value) + et._set_beam_mode(BeamModeEnum.CONSTANT_WAVELENGTH.value) + et._set_radiation_probe(RadiationProbeEnum.NEUTRON.value) + et._set_scattering_type(ScatteringTypeEnum.BRAGG.value) + return et + + +class _ConcreteCwlSc(CwlScExperiment): + def _load_ascii_data_to_experiment(self, data_path: str) -> int: + # Not used in this test + return 0 + + +def test_init_and_placeholder_no_crash(monkeypatch: pytest.MonkeyPatch): + # Prevent logger from raising on attribute errors inside __init__ + monkeypatch.setattr(Logger, '_reaction', Logger.Reaction.WARN, raising=True) + expt = _ConcreteCwlSc(name='sc1', type=_mk_type_sc_bragg()) + # Verify that experiment was created successfully with expected properties + assert expt.name == 'sc1' + assert expt.type is not None diff --git a/tests/unit/easydiffraction/experiments/experiment/test_enums.py b/tests/unit/easydiffraction/datablocks/experiment/item/test_enums.py similarity index 61% rename from tests/unit/easydiffraction/experiments/experiment/test_enums.py rename to tests/unit/easydiffraction/datablocks/experiment/item/test_enums.py index 2df2dbb5..983f991b 100644 --- a/tests/unit/easydiffraction/experiments/experiment/test_enums.py +++ b/tests/unit/easydiffraction/datablocks/experiment/item/test_enums.py @@ -1,16 +1,17 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause + def test_module_import(): - import easydiffraction.experiments.experiment.enums as MUT + import easydiffraction.datablocks.experiment.item.enums as MUT - expected_module_name = 'easydiffraction.experiments.experiment.enums' + expected_module_name = 'easydiffraction.datablocks.experiment.item.enums' actual_module_name = MUT.__name__ assert expected_module_name == actual_module_name def test_default_enums_consistency(): - import easydiffraction.experiments.experiment.enums as MUT + import easydiffraction.datablocks.experiment.item.enums as MUT assert MUT.SampleFormEnum.default() in list(MUT.SampleFormEnum) assert MUT.ScatteringTypeEnum.default() in list(MUT.ScatteringTypeEnum) diff --git a/tests/unit/easydiffraction/datablocks/experiment/item/test_factory.py b/tests/unit/easydiffraction/datablocks/experiment/item/test_factory.py new file mode 100644 index 00000000..c185ed30 --- /dev/null +++ b/tests/unit/easydiffraction/datablocks/experiment/item/test_factory.py @@ -0,0 +1,28 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + + +def test_module_import(): + import easydiffraction.datablocks.experiment.item.factory as MUT + + expected_module_name = 'easydiffraction.datablocks.experiment.item.factory' + actual_module_name = MUT.__name__ + assert expected_module_name == actual_module_name + + +def test_experiment_factory_from_scratch(): + import easydiffraction.datablocks.experiment.item.factory as EF + from easydiffraction.datablocks.experiment.item.enums import BeamModeEnum + from easydiffraction.datablocks.experiment.item.enums import RadiationProbeEnum + from easydiffraction.datablocks.experiment.item.enums import SampleFormEnum + from easydiffraction.datablocks.experiment.item.enums import ScatteringTypeEnum + + ex = EF.ExperimentFactory.from_scratch( + name='ex1', + sample_form=SampleFormEnum.POWDER.value, + beam_mode=BeamModeEnum.CONSTANT_WAVELENGTH.value, + radiation_probe=RadiationProbeEnum.NEUTRON.value, + scattering_type=ScatteringTypeEnum.BRAGG.value, + ) + # Instance should be created (BraggPdExperiment) + assert hasattr(ex, 'type') and ex.type.sample_form.value == SampleFormEnum.POWDER.value diff --git a/tests/unit/easydiffraction/experiments/experiment/test_total_pd.py b/tests/unit/easydiffraction/datablocks/experiment/item/test_total_pd.py similarity index 50% rename from tests/unit/easydiffraction/experiments/experiment/test_total_pd.py rename to tests/unit/easydiffraction/datablocks/experiment/item/test_total_pd.py index 5a4fe416..d021dc34 100644 --- a/tests/unit/easydiffraction/experiments/experiment/test_total_pd.py +++ b/tests/unit/easydiffraction/datablocks/experiment/item/test_total_pd.py @@ -1,24 +1,24 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause import numpy as np import pytest -from easydiffraction.experiments.categories.experiment_type import ExperimentType -from easydiffraction.experiments.experiment.enums import BeamModeEnum -from easydiffraction.experiments.experiment.enums import RadiationProbeEnum -from easydiffraction.experiments.experiment.enums import SampleFormEnum -from easydiffraction.experiments.experiment.enums import ScatteringTypeEnum -from easydiffraction.experiments.experiment.total_pd import TotalPdExperiment +from easydiffraction.datablocks.experiment.categories.experiment_type import ExperimentType +from easydiffraction.datablocks.experiment.item.enums import BeamModeEnum +from easydiffraction.datablocks.experiment.item.enums import RadiationProbeEnum +from easydiffraction.datablocks.experiment.item.enums import SampleFormEnum +from easydiffraction.datablocks.experiment.item.enums import ScatteringTypeEnum +from easydiffraction.datablocks.experiment.item.total_pd import TotalPdExperiment def _mk_type_powder_total(): - return ExperimentType( - sample_form=SampleFormEnum.POWDER.value, - beam_mode=BeamModeEnum.CONSTANT_WAVELENGTH.value, - radiation_probe=RadiationProbeEnum.NEUTRON.value, - scattering_type=ScatteringTypeEnum.TOTAL.value, - ) + et = ExperimentType() + et._set_sample_form(SampleFormEnum.POWDER.value) + et._set_beam_mode(BeamModeEnum.CONSTANT_WAVELENGTH.value) + et._set_radiation_probe(RadiationProbeEnum.NEUTRON.value) + et._set_scattering_type(ScatteringTypeEnum.TOTAL.value) + return et def test_load_ascii_data_pdf(tmp_path: pytest.TempPathFactory): @@ -47,5 +47,5 @@ def test_load_ascii_data_pdf(tmp_path: pytest.TempPathFactory): # With diffpy available, load should succeed expt._load_ascii_data_to_experiment(str(f)) assert np.allclose(expt.data.x, data[:, 0]) - assert np.allclose(expt.data.meas, data[:, 1]) - assert np.allclose(expt.data.meas_su, data[:, 2]) + assert np.allclose(expt.data.intensity_meas, data[:, 1]) + assert np.allclose(expt.data.intensity_meas_su, data[:, 2]) diff --git a/tests/unit/easydiffraction/experiments/test_experiments.py b/tests/unit/easydiffraction/datablocks/experiment/test_collection.py similarity index 65% rename from tests/unit/easydiffraction/experiments/test_experiments.py rename to tests/unit/easydiffraction/datablocks/experiment/test_collection.py index 89dd874e..3d5819f6 100644 --- a/tests/unit/easydiffraction/experiments/test_experiments.py +++ b/tests/unit/easydiffraction/datablocks/experiment/test_collection.py @@ -1,17 +1,18 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause + def test_module_import(): - import easydiffraction.experiments.experiments as MUT + import easydiffraction.datablocks.experiment.collection as MUT - expected_module_name = 'easydiffraction.experiments.experiments' + expected_module_name = 'easydiffraction.datablocks.experiment.collection' actual_module_name = MUT.__name__ assert expected_module_name == actual_module_name def test_experiments_show_and_remove(monkeypatch, capsys): - from easydiffraction.experiments.experiment.base import ExperimentBase - from easydiffraction.experiments.experiments import Experiments + from easydiffraction.datablocks.experiment.collection import Experiments + from easydiffraction.datablocks.experiment.item.base import ExperimentBase class DummyType: def __init__(self): @@ -22,12 +23,12 @@ class DummyExp(ExperimentBase): def __init__(self, name='e1'): super().__init__(name=name, type=DummyType()) - def _load_ascii_data_to_experiment(self, data_path: str) -> None: - pass + def _load_ascii_data_to_experiment(self, data_path: str) -> int: + return 0 exps = Experiments() - exps.add(experiment=DummyExp('a')) - exps.add(experiment=DummyExp('b')) + exps.add(DummyExp('a')) + exps.add(DummyExp('b')) exps.show_names() out = capsys.readouterr().out assert 'Defined experiments' in out diff --git a/tests/unit/easydiffraction/sample_models/categories/test_space_group.py b/tests/unit/easydiffraction/datablocks/structure/categories/test_space_group.py similarity index 73% rename from tests/unit/easydiffraction/sample_models/categories/test_space_group.py rename to tests/unit/easydiffraction/datablocks/structure/categories/test_space_group.py index d1a0238c..4025bd83 100644 --- a/tests/unit/easydiffraction/sample_models/categories/test_space_group.py +++ b/tests/unit/easydiffraction/datablocks/structure/categories/test_space_group.py @@ -1,7 +1,7 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -from easydiffraction.sample_models.categories.space_group import SpaceGroup +from easydiffraction.datablocks.structure.categories.space_group import SpaceGroup def test_space_group_name_updates_it_code(): diff --git a/tests/unit/easydiffraction/datablocks/structure/item/test_base.py b/tests/unit/easydiffraction/datablocks/structure/item/test_base.py new file mode 100644 index 00000000..154d0405 --- /dev/null +++ b/tests/unit/easydiffraction/datablocks/structure/item/test_base.py @@ -0,0 +1,12 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + +from easydiffraction.datablocks.structure.item.base import Structure + + +def test_structure_base_str_and_properties(): + m = Structure(name='m1') + m.name = 'm2' + assert m.name == 'm2' + s = str(m) + assert 'Structure' in s or '<' in s diff --git a/tests/unit/easydiffraction/datablocks/structure/item/test_factory.py b/tests/unit/easydiffraction/datablocks/structure/item/test_factory.py new file mode 100644 index 00000000..148e010a --- /dev/null +++ b/tests/unit/easydiffraction/datablocks/structure/item/test_factory.py @@ -0,0 +1,9 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + +from easydiffraction.datablocks.structure.item.factory import StructureFactory + + +def test_from_scratch(): + m = StructureFactory.from_scratch(name='abc') + assert m.name == 'abc' diff --git a/tests/unit/easydiffraction/datablocks/structure/test_collection.py b/tests/unit/easydiffraction/datablocks/structure/test_collection.py new file mode 100644 index 00000000..4e798e20 --- /dev/null +++ b/tests/unit/easydiffraction/datablocks/structure/test_collection.py @@ -0,0 +1,2 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause diff --git a/tests/unit/easydiffraction/display/plotters/test_ascii.py b/tests/unit/easydiffraction/display/plotters/test_ascii.py index 1ad07bd3..e0c04f8b 100644 --- a/tests/unit/easydiffraction/display/plotters/test_ascii.py +++ b/tests/unit/easydiffraction/display/plotters/test_ascii.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause import numpy as np @@ -18,6 +18,33 @@ def test_ascii_plotter_plot_minimal(capsys): x = np.array([0.0, 1.0, 2.0]) y = np.array([1.0, 2.0, 3.0]) p = AsciiPlotter() - p.plot(x=x, y_series=[y], labels=['meas'], axes_labels=['x', 'y'], title='T', height=5) + p.plot_powder(x=x, y_series=[y], labels=['meas'], axes_labels=['x', 'y'], title='T', height=5) out = capsys.readouterr().out assert 'Displaying data for selected x-range' in out + + +def test_ascii_plotter_plot_single_crystal(capsys): + from easydiffraction.display.plotters.ascii import AsciiPlotter + + x_calc = np.array([1.0, 2.0, 3.0, 4.0, 5.0]) + y_meas = np.array([1.1, 1.9, 3.2, 3.8, 5.1]) + y_meas_su = np.array([0.1, 0.1, 0.1, 0.1, 0.1]) + + p = AsciiPlotter() + p.plot_single_crystal( + x_calc=x_calc, + y_meas=y_meas, + y_meas_su=y_meas_su, + axes_labels=['F²calc', 'F²meas'], + title='SC Test', + height=10, + ) + out = capsys.readouterr().out + # Verify title and axes labels appear + assert 'SC Test' in out + assert 'F²calc' in out + assert 'F²meas' in out + # Verify scatter points are plotted (● character) + assert '●' in out + # Verify diagonal reference line (· character) + assert '·' in out diff --git a/tests/unit/easydiffraction/display/plotters/test_base.py b/tests/unit/easydiffraction/display/plotters/test_base.py index ecc241c6..b1029d2c 100644 --- a/tests/unit/easydiffraction/display/plotters/test_base.py +++ b/tests/unit/easydiffraction/display/plotters/test_base.py @@ -1,9 +1,8 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -import importlib -import types import sys +import types def test_module_import(): @@ -32,8 +31,38 @@ def test_default_engine_switches_with_notebook(monkeypatch): def test_default_axes_labels_keys_present(): import easydiffraction.display.plotters.base as pb - from easydiffraction.experiments.experiment.enums import BeamModeEnum - from easydiffraction.experiments.experiment.enums import ScatteringTypeEnum + from easydiffraction.datablocks.experiment.item.enums import SampleFormEnum + from easydiffraction.datablocks.experiment.item.enums import ScatteringTypeEnum - assert (ScatteringTypeEnum.BRAGG, BeamModeEnum.CONSTANT_WAVELENGTH) in pb.DEFAULT_AXES_LABELS - assert (ScatteringTypeEnum.BRAGG, BeamModeEnum.TIME_OF_FLIGHT) in pb.DEFAULT_AXES_LABELS + # Powder Bragg + assert ( + SampleFormEnum.POWDER, + ScatteringTypeEnum.BRAGG, + pb.XAxisType.TWO_THETA, + ) in pb.DEFAULT_AXES_LABELS + assert ( + SampleFormEnum.POWDER, + ScatteringTypeEnum.BRAGG, + pb.XAxisType.TIME_OF_FLIGHT, + ) in pb.DEFAULT_AXES_LABELS + assert ( + SampleFormEnum.POWDER, + ScatteringTypeEnum.BRAGG, + pb.XAxisType.D_SPACING, + ) in pb.DEFAULT_AXES_LABELS + # Single crystal Bragg + assert ( + SampleFormEnum.SINGLE_CRYSTAL, + ScatteringTypeEnum.BRAGG, + pb.XAxisType.INTENSITY_CALC, + ) in pb.DEFAULT_AXES_LABELS + assert ( + SampleFormEnum.SINGLE_CRYSTAL, + ScatteringTypeEnum.BRAGG, + pb.XAxisType.D_SPACING, + ) in pb.DEFAULT_AXES_LABELS + assert ( + SampleFormEnum.SINGLE_CRYSTAL, + ScatteringTypeEnum.BRAGG, + pb.XAxisType.SIN_THETA_OVER_LAMBDA, + ) in pb.DEFAULT_AXES_LABELS diff --git a/tests/unit/easydiffraction/display/plotters/test_plotly.py b/tests/unit/easydiffraction/display/plotters/test_plotly.py index f0ea6a61..5a1549f0 100644 --- a/tests/unit/easydiffraction/display/plotters/test_plotly.py +++ b/tests/unit/easydiffraction/display/plotters/test_plotly.py @@ -1,6 +1,7 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause + def test_module_import(): import easydiffraction.display.plotters.plotly as MUT @@ -66,15 +67,15 @@ def __init__(self, html): plotter = pp.PlotlyPlotter() - # Exercise _get_trace + # Exercise _get_powder_trace x = [0, 1, 2] y = [1, 2, 3] - trace = plotter._get_trace(x, y, label='calc') + trace = plotter._get_powder_trace(x, y, label='calc') assert hasattr(trace, 'kwargs') assert trace.kwargs['x'] == x and trace.kwargs['y'] == y - # Exercise plot (non-PyCharm, display path) - plotter.plot( + # Exercise plot_powder (non-PyCharm, display path) + plotter.plot_powder( x, y_series=[y], labels=['calc'], @@ -85,3 +86,89 @@ def __init__(self, html): # One HTML display call expected assert dummy_display_calls['count'] == 1 or shown['count'] == 1 + + +def test_plotly_single_crystal_trace_and_plot(monkeypatch): + import easydiffraction.display.plotters.plotly as pp + + # Arrange: force non-PyCharm branch + monkeypatch.setattr(pp, 'in_pycharm', lambda: False) + + shown = {'count': 0} + + class DummyFig: + def update_xaxes(self, **kwargs): + pass + + def update_yaxes(self, **kwargs): + pass + + def show(self, **kwargs): + shown['count'] += 1 + + class DummyScatter: + def __init__(self, **kwargs): + self.kwargs = kwargs + + class DummyGO: + class Scatter(DummyScatter): + pass + + class Figure(DummyFig): + def __init__(self, data=None, layout=None): + self.data = data + self.layout = layout + + class Layout: + def __init__(self, **kwargs): + self.kwargs = kwargs + + class DummyPIO: + @staticmethod + def to_html(fig, include_plotlyjs=None, full_html=None, config=None): + return '
plot
' + + dummy_display_calls = {'count': 0} + + def dummy_display(obj): + dummy_display_calls['count'] += 1 + + class DummyHTML: + def __init__(self, html): + self.html = html + + monkeypatch.setattr(pp, 'go', DummyGO) + monkeypatch.setattr(pp, 'pio', DummyPIO) + monkeypatch.setattr(pp, 'display', dummy_display) + monkeypatch.setattr(pp, 'HTML', DummyHTML) + + plotter = pp.PlotlyPlotter() + + # Exercise _get_single_crystal_trace + x_calc = [1.0, 2.0, 3.0] + y_meas = [1.1, 1.9, 3.2] + y_meas_su = [0.1, 0.1, 0.1] + trace = plotter._get_single_crystal_trace(x_calc, y_meas, y_meas_su) + assert hasattr(trace, 'kwargs') + assert trace.kwargs['x'] == x_calc + assert trace.kwargs['y'] == y_meas + assert trace.kwargs['mode'] == 'markers' + assert 'error_y' in trace.kwargs + + # Exercise _get_diagonal_shape + shape = plotter._get_diagonal_shape() + assert shape['type'] == 'line' + assert shape['xref'] == 'paper' + assert shape['yref'] == 'paper' + + # Exercise plot_single_crystal + plotter.plot_single_crystal( + x_calc=x_calc, + y_meas=y_meas, + y_meas_su=y_meas_su, + axes_labels=['F²calc', 'F²meas'], + title='SC Test', + height=None, + ) + # One display call expected + assert dummy_display_calls['count'] == 1 or shown['count'] == 1 diff --git a/tests/unit/easydiffraction/display/test_plotting.py b/tests/unit/easydiffraction/display/test_plotting.py index 8210f481..be5b4239 100644 --- a/tests/unit/easydiffraction/display/test_plotting.py +++ b/tests/unit/easydiffraction/display/test_plotting.py @@ -1,6 +1,7 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause + def test_module_import(): import easydiffraction.display.plotting as MUT @@ -53,52 +54,60 @@ def test_plotter_factory_supported_and_unsupported(): def test_plotter_error_paths_and_filtering(capsys): - from easydiffraction.experiments.experiment.enums import BeamModeEnum - from easydiffraction.experiments.experiment.enums import ScatteringTypeEnum + from easydiffraction.datablocks.experiment.item.enums import BeamModeEnum + from easydiffraction.datablocks.experiment.item.enums import SampleFormEnum + from easydiffraction.datablocks.experiment.item.enums import ScatteringTypeEnum from easydiffraction.display.plotting import Plotter class Ptn: - def __init__(self, x=None, meas=None, calc=None, d=None): - self.x = x - self.meas = meas - self.calc = calc - self.d = d if d is not None else x + def __init__( + self, two_theta=None, intensity_meas=None, intensity_calc=None, d_spacing=None + ): + self.two_theta = two_theta + self.intensity_meas = intensity_meas + self.intensity_calc = intensity_calc + self.d_spacing = d_spacing if d_spacing is not None else two_theta class ExptType: def __init__(self): + self.sample_form = type('SF', (), {'value': SampleFormEnum.POWDER}) self.scattering_type = type('S', (), {'value': ScatteringTypeEnum.BRAGG}) self.beam_mode = type('B', (), {'value': BeamModeEnum.CONSTANT_WAVELENGTH}) p = Plotter() # Error paths (now log errors via console; messages are printed) - p.plot_meas(Ptn(x=None, meas=None), 'E', ExptType()) + p.plot_meas(Ptn(two_theta=None, intensity_meas=None), 'E', ExptType()) out = capsys.readouterr().out - assert 'No data available for experiment E' in out + assert 'No two_theta data available for experiment E' in out - p.plot_meas(Ptn(x=[1], meas=None), 'E', ExptType()) + p.plot_meas(Ptn(two_theta=[1], intensity_meas=None), 'E', ExptType()) out = capsys.readouterr().out assert 'No measured data available for experiment E' in out - p.plot_calc(Ptn(x=None, calc=None), 'E', ExptType()) + p.plot_calc(Ptn(two_theta=None, intensity_calc=None), 'E', ExptType()) out = capsys.readouterr().out - assert 'No data available for experiment E' in out + assert 'No two_theta data available for experiment E' in out - p.plot_calc(Ptn(x=[1], calc=None), 'E', ExptType()) + p.plot_calc(Ptn(two_theta=[1], intensity_calc=None), 'E', ExptType()) out = capsys.readouterr().out assert 'No calculated data available for experiment E' in out - p.plot_meas_vs_calc(Ptn(x=None), 'E', ExptType()) + p.plot_meas_vs_calc( + Ptn(two_theta=None, intensity_meas=None, intensity_calc=None), 'E', ExptType() + ) out = capsys.readouterr().out - assert 'No data available for experiment E' in out - p.plot_meas_vs_calc(Ptn(x=[1], meas=None, calc=[1]), 'E', ExptType()) + assert 'No measured data available for experiment E' in out + p.plot_meas_vs_calc( + Ptn(two_theta=[1], intensity_meas=None, intensity_calc=[1]), 'E', ExptType() + ) out = capsys.readouterr().out assert 'No measured data available for experiment E' in out - p.plot_meas_vs_calc(Ptn(x=[1], meas=[1], calc=None), 'E', ExptType()) + p.plot_meas_vs_calc( + Ptn(two_theta=[1], intensity_meas=[1], intensity_calc=None), 'E', ExptType() + ) out = capsys.readouterr().out assert 'No calculated data available for experiment E' in out - # TODO: Update assertions with new logging-based error handling - # in the above line and elsewhere as needed. # Filtering import numpy as np @@ -113,27 +122,29 @@ def test_plotter_routes_to_ascii_plotter(monkeypatch): import numpy as np import easydiffraction.display.plotters.ascii as ascii_mod - from easydiffraction.experiments.experiment.enums import BeamModeEnum - from easydiffraction.experiments.experiment.enums import ScatteringTypeEnum + from easydiffraction.datablocks.experiment.item.enums import BeamModeEnum + from easydiffraction.datablocks.experiment.item.enums import SampleFormEnum + from easydiffraction.datablocks.experiment.item.enums import ScatteringTypeEnum from easydiffraction.display.plotting import Plotter called = {} - def fake_plot(self, x, y_series, labels, axes_labels, title, height=None): + def fake_plot_powder(self, x, y_series, labels, axes_labels, title, height=None): called['labels'] = tuple(labels) called['axes'] = tuple(axes_labels) called['title'] = title - monkeypatch.setattr(ascii_mod.AsciiPlotter, 'plot', fake_plot) + monkeypatch.setattr(ascii_mod.AsciiPlotter, 'plot_powder', fake_plot_powder) class Ptn: def __init__(self): - self.x = np.array([0.0, 1.0]) - self.meas = np.array([1.0, 2.0]) - self.d = self.x + self.two_theta = np.array([0.0, 1.0]) + self.intensity_meas = np.array([1.0, 2.0]) + self.d_spacing = self.two_theta class ExptType: def __init__(self): + self.sample_form = type('SF', (), {'value': SampleFormEnum.POWDER}) self.scattering_type = type('S', (), {'value': ScatteringTypeEnum.BRAGG}) self.beam_mode = type('B', (), {'value': BeamModeEnum.CONSTANT_WAVELENGTH}) diff --git a/tests/unit/easydiffraction/experiments/categories/background/test_enums.py b/tests/unit/easydiffraction/experiments/categories/background/test_enums.py deleted file mode 100644 index 4a64fb29..00000000 --- a/tests/unit/easydiffraction/experiments/categories/background/test_enums.py +++ /dev/null @@ -1,11 +0,0 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors -# SPDX-License-Identifier: BSD-3-Clause - -def test_background_enum_default_and_descriptions(): - import easydiffraction.experiments.categories.background.enums as MUT - - assert MUT.BackgroundTypeEnum.default() == MUT.BackgroundTypeEnum.LINE_SEGMENT - assert ( - MUT.BackgroundTypeEnum.LINE_SEGMENT.description() == 'Linear interpolation between points' - ) - assert MUT.BackgroundTypeEnum.CHEBYSHEV.description() == 'Chebyshev polynomial background' diff --git a/tests/unit/easydiffraction/experiments/categories/background/test_factory.py b/tests/unit/easydiffraction/experiments/categories/background/test_factory.py deleted file mode 100644 index 57308f95..00000000 --- a/tests/unit/easydiffraction/experiments/categories/background/test_factory.py +++ /dev/null @@ -1,24 +0,0 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors -# SPDX-License-Identifier: BSD-3-Clause - -import pytest - - -def test_background_factory_default_and_errors(): - from easydiffraction.experiments.categories.background.enums import BackgroundTypeEnum - from easydiffraction.experiments.categories.background.factory import BackgroundFactory - - # Default should produce a LineSegmentBackground - obj = BackgroundFactory.create() - assert obj.__class__.__name__.endswith('LineSegmentBackground') - - # Explicit type - obj2 = BackgroundFactory.create(BackgroundTypeEnum.CHEBYSHEV) - assert obj2.__class__.__name__.endswith('ChebyshevPolynomialBackground') - - # Unsupported enum (fake) should raise ValueError - class FakeEnum: - value = 'x' - - with pytest.raises(ValueError): - BackgroundFactory.create(FakeEnum) # type: ignore[arg-type] diff --git a/tests/unit/easydiffraction/experiments/categories/instrument/test_factory.py b/tests/unit/easydiffraction/experiments/categories/instrument/test_factory.py deleted file mode 100644 index 79e97077..00000000 --- a/tests/unit/easydiffraction/experiments/categories/instrument/test_factory.py +++ /dev/null @@ -1,37 +0,0 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors -# SPDX-License-Identifier: BSD-3-Clause - -import pytest - - -def test_instrument_factory_default_and_errors(): - try: - from easydiffraction.experiments.categories.instrument.factory import InstrumentFactory - from easydiffraction.experiments.experiment.enums import BeamModeEnum - from easydiffraction.experiments.experiment.enums import ScatteringTypeEnum - except ImportError as e: # pragma: no cover - environment-specific circular import - pytest.skip(f'InstrumentFactory import triggers circular import in this context: {e}') - return - - inst = InstrumentFactory.create() # defaults - assert inst.__class__.__name__ in {'CwlInstrument', 'TofInstrument'} - - # Valid combinations - inst2 = InstrumentFactory.create(ScatteringTypeEnum.BRAGG, BeamModeEnum.CONSTANT_WAVELENGTH) - assert inst2.__class__.__name__ == 'CwlInstrument' - inst3 = InstrumentFactory.create(ScatteringTypeEnum.BRAGG, BeamModeEnum.TIME_OF_FLIGHT) - assert inst3.__class__.__name__ == 'TofInstrument' - - # Invalid scattering type - class FakeST: - pass - - with pytest.raises(ValueError): - InstrumentFactory.create(FakeST, BeamModeEnum.CONSTANT_WAVELENGTH) # type: ignore[arg-type] - - # Invalid beam mode - class FakeBM: - pass - - with pytest.raises(ValueError): - InstrumentFactory.create(ScatteringTypeEnum.BRAGG, FakeBM) # type: ignore[arg-type] diff --git a/tests/unit/easydiffraction/experiments/categories/peak/test_factory.py b/tests/unit/easydiffraction/experiments/categories/peak/test_factory.py deleted file mode 100644 index bc474949..00000000 --- a/tests/unit/easydiffraction/experiments/categories/peak/test_factory.py +++ /dev/null @@ -1,58 +0,0 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors -# SPDX-License-Identifier: BSD-3-Clause - -import pytest - - -def test_peak_factory_default_and_combinations_and_errors(): - from easydiffraction.experiments.categories.peak.factory import PeakFactory - from easydiffraction.experiments.experiment.enums import BeamModeEnum - from easydiffraction.experiments.experiment.enums import PeakProfileTypeEnum - from easydiffraction.experiments.experiment.enums import ScatteringTypeEnum - - # Defaults -> valid object for default enums - p = PeakFactory.create() - assert p._identity.category_code == 'peak' - - # Explicit valid combos - p1 = PeakFactory.create( - ScatteringTypeEnum.BRAGG, - BeamModeEnum.CONSTANT_WAVELENGTH, - PeakProfileTypeEnum.PSEUDO_VOIGT, - ) - assert p1.__class__.__name__ == 'CwlPseudoVoigt' - p2 = PeakFactory.create( - ScatteringTypeEnum.BRAGG, - BeamModeEnum.TIME_OF_FLIGHT, - PeakProfileTypeEnum.PSEUDO_VOIGT_IKEDA_CARPENTER, - ) - assert p2.__class__.__name__ == 'TofPseudoVoigtIkedaCarpenter' - p3 = PeakFactory.create( - ScatteringTypeEnum.TOTAL, - BeamModeEnum.CONSTANT_WAVELENGTH, - PeakProfileTypeEnum.GAUSSIAN_DAMPED_SINC, - ) - assert p3.__class__.__name__ == 'TotalGaussianDampedSinc' - - # Invalid scattering type - class FakeST: - pass - - with pytest.raises(ValueError): - PeakFactory.create( - FakeST, BeamModeEnum.CONSTANT_WAVELENGTH, PeakProfileTypeEnum.PSEUDO_VOIGT - ) # type: ignore[arg-type] - - # Invalid beam mode - class FakeBM: - pass - - with pytest.raises(ValueError): - PeakFactory.create(ScatteringTypeEnum.BRAGG, FakeBM, PeakProfileTypeEnum.PSEUDO_VOIGT) # type: ignore[arg-type] - - # Invalid profile type - class FakePPT: - pass - - with pytest.raises(ValueError): - PeakFactory.create(ScatteringTypeEnum.BRAGG, BeamModeEnum.CONSTANT_WAVELENGTH, FakePPT) # type: ignore[arg-type] diff --git a/tests/unit/easydiffraction/experiments/categories/test_experiment_type.py b/tests/unit/easydiffraction/experiments/categories/test_experiment_type.py deleted file mode 100644 index 429eb419..00000000 --- a/tests/unit/easydiffraction/experiments/categories/test_experiment_type.py +++ /dev/null @@ -1,36 +0,0 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors -# SPDX-License-Identifier: BSD-3-Clause - -def test_module_import(): - import easydiffraction.experiments.categories.experiment_type as MUT - - expected_module_name = 'easydiffraction.experiments.categories.experiment_type' - actual_module_name = MUT.__name__ - assert expected_module_name == actual_module_name - - -def test_experiment_type_properties_and_validation(monkeypatch): - from easydiffraction.experiments.categories.experiment_type import ExperimentType - from easydiffraction.experiments.experiment.enums import BeamModeEnum - from easydiffraction.experiments.experiment.enums import RadiationProbeEnum - from easydiffraction.experiments.experiment.enums import SampleFormEnum - from easydiffraction.experiments.experiment.enums import ScatteringTypeEnum - from easydiffraction.utils.logging import log - - log.configure(reaction=log.Reaction.WARN) - - et = ExperimentType( - sample_form=SampleFormEnum.POWDER.value, - beam_mode=BeamModeEnum.CONSTANT_WAVELENGTH.value, - radiation_probe=RadiationProbeEnum.NEUTRON.value, - scattering_type=ScatteringTypeEnum.BRAGG.value, - ) - # getters nominal - assert et.sample_form.value == SampleFormEnum.POWDER.value - assert et.beam_mode.value == BeamModeEnum.CONSTANT_WAVELENGTH.value - assert et.radiation_probe.value == RadiationProbeEnum.NEUTRON.value - assert et.scattering_type.value == ScatteringTypeEnum.BRAGG.value - - # try invalid value should fall back to previous (membership validator) - et.sample_form = 'invalid' - assert et.sample_form.value == SampleFormEnum.POWDER.value diff --git a/tests/unit/easydiffraction/experiments/categories/test_linked_phases.py b/tests/unit/easydiffraction/experiments/categories/test_linked_phases.py deleted file mode 100644 index 2b7de53c..00000000 --- a/tests/unit/easydiffraction/experiments/categories/test_linked_phases.py +++ /dev/null @@ -1,16 +0,0 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors -# SPDX-License-Identifier: BSD-3-Clause - -def test_linked_phases_add_and_cif_headers(): - from easydiffraction.experiments.categories.linked_phases import LinkedPhase - from easydiffraction.experiments.categories.linked_phases import LinkedPhases - - lp = LinkedPhase(id='Si', scale=2.0) - assert lp.id.value == 'Si' and lp.scale.value == 2.0 - - coll = LinkedPhases() - coll.add(id='Si', scale=2.0) - - # CIF loop header presence - cif = coll.as_cif - assert 'loop_' in cif and '_pd_phase_block.id' in cif and '_pd_phase_block.scale' in cif diff --git a/tests/unit/easydiffraction/experiments/experiment/test_base.py b/tests/unit/easydiffraction/experiments/experiment/test_base.py deleted file mode 100644 index bd781afb..00000000 --- a/tests/unit/easydiffraction/experiments/experiment/test_base.py +++ /dev/null @@ -1,38 +0,0 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors -# SPDX-License-Identifier: BSD-3-Clause - -def test_module_import(): - import easydiffraction.experiments.experiment.base as MUT - - expected_module_name = 'easydiffraction.experiments.experiment.base' - actual_module_name = MUT.__name__ - assert expected_module_name == actual_module_name - - -def test_pd_experiment_peak_profile_type_switch(capsys): - from easydiffraction.experiments.categories.experiment_type import ExperimentType - from easydiffraction.experiments.experiment.base import PdExperimentBase - from easydiffraction.experiments.experiment.enums import BeamModeEnum - from easydiffraction.experiments.experiment.enums import PeakProfileTypeEnum - from easydiffraction.experiments.experiment.enums import RadiationProbeEnum - from easydiffraction.experiments.experiment.enums import SampleFormEnum - from easydiffraction.experiments.experiment.enums import ScatteringTypeEnum - - class ConcretePd(PdExperimentBase): - def _load_ascii_data_to_experiment(self, data_path: str) -> None: - pass - - et = ExperimentType( - sample_form=SampleFormEnum.POWDER.value, - beam_mode=BeamModeEnum.CONSTANT_WAVELENGTH.value, - radiation_probe=RadiationProbeEnum.NEUTRON.value, - scattering_type=ScatteringTypeEnum.BRAGG.value, - ) - ex = ConcretePd(name='ex1', type=et) - # valid switch using enum - ex.peak_profile_type = PeakProfileTypeEnum.PSEUDO_VOIGT - assert ex.peak_profile_type == PeakProfileTypeEnum.PSEUDO_VOIGT - # invalid string should warn and keep previous - ex.peak_profile_type = 'non-existent' - captured = capsys.readouterr().out - assert 'Unsupported' in captured or 'Unknown' in captured diff --git a/tests/unit/easydiffraction/experiments/experiment/test_bragg_sc.py b/tests/unit/easydiffraction/experiments/experiment/test_bragg_sc.py deleted file mode 100644 index bfbb3e0e..00000000 --- a/tests/unit/easydiffraction/experiments/experiment/test_bragg_sc.py +++ /dev/null @@ -1,35 +0,0 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors -# SPDX-License-Identifier: BSD-3-Clause - -import pytest - -from easydiffraction.experiments.categories.experiment_type import ExperimentType -from easydiffraction.experiments.experiment.bragg_sc import BraggScExperiment -from easydiffraction.experiments.experiment.enums import BeamModeEnum -from easydiffraction.experiments.experiment.enums import RadiationProbeEnum -from easydiffraction.experiments.experiment.enums import SampleFormEnum -from easydiffraction.experiments.experiment.enums import ScatteringTypeEnum -from easydiffraction.utils.logging import Logger - - -def _mk_type_sc_bragg(): - return ExperimentType( - sample_form=SampleFormEnum.SINGLE_CRYSTAL.value, - beam_mode=BeamModeEnum.CONSTANT_WAVELENGTH.value, - radiation_probe=RadiationProbeEnum.NEUTRON.value, - scattering_type=ScatteringTypeEnum.BRAGG.value, - ) - - -class _ConcreteBraggSc(BraggScExperiment): - def _load_ascii_data_to_experiment(self, data_path: str) -> None: - # Not used in this test - pass - - -def test_init_and_placeholder_no_crash(monkeypatch: pytest.MonkeyPatch): - # Prevent logger from raising on attribute errors inside __init__ - monkeypatch.setattr(Logger, '_reaction', Logger.Reaction.WARN, raising=True) - expt = _ConcreteBraggSc(name='sc1', type=_mk_type_sc_bragg()) - # show_meas_chart just prints placeholder text; ensure no exception - expt.show_meas_chart() diff --git a/tests/unit/easydiffraction/experiments/experiment/test_factory.py b/tests/unit/easydiffraction/experiments/experiment/test_factory.py deleted file mode 100644 index d7838b04..00000000 --- a/tests/unit/easydiffraction/experiments/experiment/test_factory.py +++ /dev/null @@ -1,35 +0,0 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors -# SPDX-License-Identifier: BSD-3-Clause - -import pytest - - - -def test_module_import(): - import easydiffraction.experiments.experiment.factory as MUT - - expected_module_name = 'easydiffraction.experiments.experiment.factory' - actual_module_name = MUT.__name__ - assert expected_module_name == actual_module_name - - -def test_experiment_factory_create_without_data_and_invalid_combo(): - import easydiffraction.experiments.experiment.factory as EF - from easydiffraction.experiments.experiment.enums import BeamModeEnum - from easydiffraction.experiments.experiment.enums import RadiationProbeEnum - from easydiffraction.experiments.experiment.enums import SampleFormEnum - from easydiffraction.experiments.experiment.enums import ScatteringTypeEnum - - ex = EF.ExperimentFactory.create( - name='ex1', - sample_form=SampleFormEnum.POWDER.value, - beam_mode=BeamModeEnum.CONSTANT_WAVELENGTH.value, - radiation_probe=RadiationProbeEnum.NEUTRON.value, - scattering_type=ScatteringTypeEnum.BRAGG.value, - ) - # Instance should be created (BraggPdExperiment) - assert hasattr(ex, 'type') and ex.type.sample_form.value == SampleFormEnum.POWDER.value - - # invalid combination: unexpected key - with pytest.raises(ValueError): - EF.ExperimentFactory.create(name='ex2', unexpected=True) diff --git a/tests/unit/easydiffraction/experiments/experiment/test_instrument_mixin.py b/tests/unit/easydiffraction/experiments/experiment/test_instrument_mixin.py deleted file mode 100644 index 89975b7d..00000000 --- a/tests/unit/easydiffraction/experiments/experiment/test_instrument_mixin.py +++ /dev/null @@ -1,9 +0,0 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors -# SPDX-License-Identifier: BSD-3-Clause - -def test_module_import(): - import easydiffraction.experiments.experiment.instrument_mixin as MUT - - expected_module_name = 'easydiffraction.experiments.experiment.instrument_mixin' - actual_module_name = MUT.__name__ - assert expected_module_name == actual_module_name diff --git a/tests/unit/easydiffraction/io/cif/test_handler.py b/tests/unit/easydiffraction/io/cif/test_handler.py index ea31b833..a9c1be85 100644 --- a/tests/unit/easydiffraction/io/cif/test_handler.py +++ b/tests/unit/easydiffraction/io/cif/test_handler.py @@ -1,6 +1,7 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause + def test_cif_handler_names_and_uid(): import easydiffraction.io.cif.handler as H diff --git a/tests/unit/easydiffraction/io/cif/test_serialize.py b/tests/unit/easydiffraction/io/cif/test_serialize.py index 2cf82f31..6b92a0e4 100644 --- a/tests/unit/easydiffraction/io/cif/test_serialize.py +++ b/tests/unit/easydiffraction/io/cif/test_serialize.py @@ -1,6 +1,7 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause + def test_module_import(): import easydiffraction.io.cif.serialize as MUT @@ -12,8 +13,8 @@ def test_module_import(): def test_format_value_quotes_whitespace_strings(): import easydiffraction.io.cif.serialize as MUT - assert MUT.format_value('a b') == ' "a b"' - assert MUT.format_value('ab') == ' ab' + assert MUT.format_value('a b') == ' "a b"' + assert MUT.format_value('ab') == ' ab' def test_param_to_cif_minimal(): @@ -26,7 +27,7 @@ def __init__(self): self.value = 3 p = P() - assert MUT.param_to_cif(p) == '_x.y 3.0000' + assert MUT.param_to_cif(p) == '_x.y 3.00000000' def test_category_collection_to_cif_empty_and_one_row(): @@ -73,7 +74,7 @@ def as_cif(self): class Project: def __init__(self): self.info = Obj('I') - self.sample_models = None + self.structures = None self.experiments = Obj('E') self.analysis = None self.summary = None diff --git a/tests/unit/easydiffraction/io/cif/test_serialize_more.py b/tests/unit/easydiffraction/io/cif/test_serialize_more.py index 3c197a75..fc8ef714 100644 --- a/tests/unit/easydiffraction/io/cif/test_serialize_more.py +++ b/tests/unit/easydiffraction/io/cif/test_serialize_more.py @@ -1,9 +1,6 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -import numpy as np -import pytest - def test_datablock_item_to_cif_includes_item_and_collection(): import easydiffraction.io.cif.serialize as MUT @@ -112,11 +109,13 @@ def as_cif(self): assert '_k' in out_with and '1' in out_with out_without = MUT.experiment_to_cif(Exp('')) - assert out_without.startswith('data_expA') and out_without.endswith('1.0000') + assert out_without.startswith('data_expA') and out_without.endswith('1.00000000') def test_analysis_to_cif_renders_all_sections(): import easydiffraction.io.cif.serialize as MUT + from easydiffraction.analysis.categories.fit_mode import FitMode + from easydiffraction.analysis.categories.joint_fit_experiments import JointFitExperiments class Obj: def __init__(self, t): @@ -127,16 +126,14 @@ def as_cif(self): return self._t class A: - current_calculator = 'cryspy engine' - current_minimizer = 'lmfit (leastsq)' - fit_mode = 'single' + current_minimizer = 'lmfit' + fit_mode = FitMode() + joint_fit_experiments = JointFitExperiments() aliases = Obj('ALIASES') constraints = Obj('CONSTRAINTS') out = MUT.analysis_to_cif(A()) lines = out.splitlines() - assert lines[0].startswith('_analysis.calculator_engine') - assert '"cryspy engine"' in lines[0] - assert lines[1].startswith('_analysis.fitting_engine') and '"lmfit (leastsq)"' in lines[1] - assert lines[2].startswith('_analysis.fit_mode') and 'single' in lines[2] + assert lines[0].startswith('_analysis.fitting_engine') and 'lmfit' in lines[0] + assert lines[1].startswith('_analysis.fit_mode') and 'single' in lines[1] assert 'ALIASES' in out and 'CONSTRAINTS' in out diff --git a/tests/unit/easydiffraction/project/test_project.py b/tests/unit/easydiffraction/project/test_project.py index 046a44f0..aedd24a8 100644 --- a/tests/unit/easydiffraction/project/test_project.py +++ b/tests/unit/easydiffraction/project/test_project.py @@ -1,9 +1,51 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause + def test_module_import(): import easydiffraction.project.project as MUT expected_module_name = 'easydiffraction.project.project' actual_module_name = MUT.__name__ assert expected_module_name == actual_module_name + + +def test_project_help(capsys): + from easydiffraction.project.project import Project + + p = Project() + p.help() + out = capsys.readouterr().out + assert "Help for 'Project'" in out + assert 'experiments' in out + assert 'analysis' in out + assert 'summary' in out + + +def test_project_verbosity_default(): + from easydiffraction.project.project import Project + + p = Project() + assert p.verbosity == 'full' + + +def test_project_verbosity_setter(): + from easydiffraction.project.project import Project + + p = Project() + p.verbosity = 'short' + assert p.verbosity == 'short' + p.verbosity = 'silent' + assert p.verbosity == 'silent' + p.verbosity = 'full' + assert p.verbosity == 'full' + + +def test_project_verbosity_invalid(): + import pytest + + from easydiffraction.project.project import Project + + p = Project() + with pytest.raises(ValueError): + p.verbosity = 'verbose' diff --git a/tests/unit/easydiffraction/project/test_project_info.py b/tests/unit/easydiffraction/project/test_project_info.py index ef8b061f..8c3455ab 100644 --- a/tests/unit/easydiffraction/project/test_project_info.py +++ b/tests/unit/easydiffraction/project/test_project_info.py @@ -1,6 +1,7 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause + def test_module_import(): import easydiffraction.project.project_info as MUT diff --git a/tests/unit/easydiffraction/project/test_project_load_and_summary_wrap.py b/tests/unit/easydiffraction/project/test_project_load_and_summary_wrap.py index d3e0b2d6..cdeafd35 100644 --- a/tests/unit/easydiffraction/project/test_project_load_and_summary_wrap.py +++ b/tests/unit/easydiffraction/project/test_project_load_and_summary_wrap.py @@ -1,16 +1,16 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause + def test_project_load_prints_and_sets_path(tmp_path, capsys): + import pytest + from easydiffraction.project.project import Project p = Project() dir_path = tmp_path / 'pdir' - p.load(str(dir_path)) - out = capsys.readouterr().out - assert 'Loading project' in out and str(dir_path) in out - # Path should be set on ProjectInfo - assert p.info.path == dir_path + with pytest.raises(NotImplementedError, match='not implemented yet'): + p.load(str(dir_path)) def test_summary_show_project_info_wraps_description(capsys): diff --git a/tests/unit/easydiffraction/project/test_project_save.py b/tests/unit/easydiffraction/project/test_project_save.py index 2879f85c..ac8b9895 100644 --- a/tests/unit/easydiffraction/project/test_project_save.py +++ b/tests/unit/easydiffraction/project/test_project_save.py @@ -1,15 +1,16 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause + def test_project_save_uses_cwd_when_no_explicit_path(monkeypatch, tmp_path, capsys): - # Default ProjectInfo.path is cwd; ensure save writes into a temp cwd, not repo root + # ProjectInfo.path defaults to None; save() requires save_as() first from easydiffraction.project.project import Project monkeypatch.chdir(tmp_path) p = Project() - p.save() + p.save_as(str(tmp_path)) out = capsys.readouterr().out - # It should announce saving and create the three core files in cwd + # It should announce saving and create the three core files assert 'Saving project' in out assert (tmp_path / 'project.cif').exists() assert (tmp_path / 'analysis.cif').exists() @@ -35,5 +36,5 @@ def test_project_save_as_writes_core_files(tmp_path, monkeypatch): assert (target / 'project.cif').is_file() assert (target / 'analysis.cif').is_file() assert (target / 'summary.cif').is_file() - assert (target / 'sample_models').is_dir() + assert (target / 'structures').is_dir() assert (target / 'experiments').is_dir() diff --git a/tests/unit/easydiffraction/sample_models/categories/test_atom_sites.py b/tests/unit/easydiffraction/sample_models/categories/test_atom_sites.py deleted file mode 100644 index db4c22ed..00000000 --- a/tests/unit/easydiffraction/sample_models/categories/test_atom_sites.py +++ /dev/null @@ -1,28 +0,0 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors -# SPDX-License-Identifier: BSD-3-Clause - -from easydiffraction.sample_models.categories.atom_sites import AtomSite -from easydiffraction.sample_models.categories.atom_sites import AtomSites - - -def test_atom_site_defaults_and_setters(): - a = AtomSite(label='Si1', type_symbol='Si') - a.fract_x = 0.1 - a.fract_y = 0.2 - a.fract_z = 0.3 - a.occupancy = 0.9 - a.b_iso = 1.5 - a.adp_type = 'Biso' - assert a.label.value == 'Si1' - assert a.type_symbol.value == 'Si' - assert (a.fract_x.value, a.fract_y.value, a.fract_z.value) == (0.1, 0.2, 0.3) - assert a.occupancy.value == 0.9 - assert a.b_iso.value == 1.5 - assert a.adp_type.value == 'Biso' - - -def test_atom_sites_collection_adds_by_label(): - sites = AtomSites() - sites.add(label='O1', type_symbol='O') - assert 'O1' in sites.names - assert sites['O1'].type_symbol.value == 'O' diff --git a/tests/unit/easydiffraction/sample_models/categories/test_cell.py b/tests/unit/easydiffraction/sample_models/categories/test_cell.py deleted file mode 100644 index 8de1da42..00000000 --- a/tests/unit/easydiffraction/sample_models/categories/test_cell.py +++ /dev/null @@ -1,43 +0,0 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors -# SPDX-License-Identifier: BSD-3-Clause - -import pytest - - -def test_cell_defaults_and_overrides(): - from easydiffraction.sample_models.categories.cell import Cell - - c = Cell() - # Defaults from AttributeSpec in implementation - assert pytest.approx(c.length_a.value) == 10.0 - assert pytest.approx(c.length_b.value) == 10.0 - assert pytest.approx(c.length_c.value) == 10.0 - assert pytest.approx(c.angle_alpha.value) == 90.0 - assert pytest.approx(c.angle_beta.value) == 90.0 - assert pytest.approx(c.angle_gamma.value) == 90.0 - - # Override through constructor - c2 = Cell(length_a=12.3, angle_beta=100.0) - assert pytest.approx(c2.length_a.value) == 12.3 - assert pytest.approx(c2.angle_beta.value) == 100.0 - - -def test_cell_setters_apply_validation_and_units(): - from easydiffraction.sample_models.categories.cell import Cell - - c = Cell() - # Set valid values within range - c.length_a = 5.5 - c.angle_gamma = 120.0 - assert pytest.approx(c.length_a.value) == 5.5 - assert pytest.approx(c.angle_gamma.value) == 120.0 - # Check units are preserved on parameter objects - assert c.length_a.units == 'Å' - assert c.angle_gamma.units == 'deg' - - -def test_cell_identity_category_code(): - from easydiffraction.sample_models.categories.cell import Cell - - c = Cell() - assert c._identity.category_code == 'cell' diff --git a/tests/unit/easydiffraction/sample_models/sample_model/test_base.py b/tests/unit/easydiffraction/sample_models/sample_model/test_base.py deleted file mode 100644 index 61c4b3db..00000000 --- a/tests/unit/easydiffraction/sample_models/sample_model/test_base.py +++ /dev/null @@ -1,12 +0,0 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors -# SPDX-License-Identifier: BSD-3-Clause - -from easydiffraction.sample_models.sample_model.base import SampleModelBase - - -def test_sample_model_base_str_and_properties(): - m = SampleModelBase(name='m1') - m.name = 'm2' - assert m.name == 'm2' - s = str(m) - assert 'SampleModelBase' in s or '<' in s diff --git a/tests/unit/easydiffraction/sample_models/sample_model/test_factory.py b/tests/unit/easydiffraction/sample_models/sample_model/test_factory.py deleted file mode 100644 index aa9fd9a0..00000000 --- a/tests/unit/easydiffraction/sample_models/sample_model/test_factory.py +++ /dev/null @@ -1,16 +0,0 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors -# SPDX-License-Identifier: BSD-3-Clause - -import pytest - -from easydiffraction.sample_models.sample_model.factory import SampleModelFactory - - -def test_create_minimal_by_name(): - m = SampleModelFactory.create(name='abc') - assert m.name == 'abc' - - -def test_invalid_arg_combo_raises(): - with pytest.raises(ValueError): - SampleModelFactory.create(name=None, cif_path=None) diff --git a/tests/unit/easydiffraction/sample_models/test_sample_models.py b/tests/unit/easydiffraction/sample_models/test_sample_models.py deleted file mode 100644 index eed0ea84..00000000 --- a/tests/unit/easydiffraction/sample_models/test_sample_models.py +++ /dev/null @@ -1,6 +0,0 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors -# SPDX-License-Identifier: BSD-3-Clause - -import pytest - -from easydiffraction.sample_models.sample_models import SampleModels diff --git a/tests/unit/easydiffraction/summary/test_summary.py b/tests/unit/easydiffraction/summary/test_summary.py index e6388cbf..b57b5598 100644 --- a/tests/unit/easydiffraction/summary/test_summary.py +++ b/tests/unit/easydiffraction/summary/test_summary.py @@ -1,6 +1,7 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause + def test_summary_as_cif_returns_placeholder_string(): from easydiffraction.summary.summary import Summary @@ -23,11 +24,10 @@ class Info: class Project: def __init__(self): self.info = Info() - self.sample_models = {} # empty mapping to exercise loops safely + self.structures = {} # empty mapping to exercise loops safely self.experiments = {} # empty mapping to exercise loops safely class A: - current_calculator = 'cryspy' current_minimizer = 'lmfit' class R: @@ -47,11 +47,6 @@ class R: assert 'FITTING' in out - - - - - def test_module_import(): import easydiffraction.summary.summary as MUT diff --git a/tests/unit/easydiffraction/summary/test_summary_details.py b/tests/unit/easydiffraction/summary/test_summary_details.py index 8965c5ed..d0e0c97b 100644 --- a/tests/unit/easydiffraction/summary/test_summary_details.py +++ b/tests/unit/easydiffraction/summary/test_summary_details.py @@ -1,10 +1,11 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause + def test_summary_crystallographic_and_experimental_sections(capsys): from easydiffraction.summary.summary import Summary - # Build a minimal sample model stub that exposes required attributes + # Build a minimal structure stub that exposes required attributes class Val: def __init__(self, v): self.value = v @@ -96,11 +97,10 @@ class Info: class Project: def __init__(self): self.info = Info() - self.sample_models = {'phaseA': Model()} + self.structures = {'phaseA': Model()} self.experiments = {'exp1': Expt()} class A: - current_calculator = 'cryspy' current_minimizer = 'lmfit' class R: diff --git a/tests/unit/easydiffraction/test___init__.py b/tests/unit/easydiffraction/test___init__.py index 714ffc4d..e42e801c 100644 --- a/tests/unit/easydiffraction/test___init__.py +++ b/tests/unit/easydiffraction/test___init__.py @@ -1,7 +1,6 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -# Focused tests for package __init__: lazy attributes and error path import importlib from pathlib import Path @@ -14,11 +13,11 @@ def test_lazy_attributes_resolve_and_are_accessible(): # Access a few lazy attributes; just ensure they exist and are callable/class-like assert hasattr(ed, 'Project') assert hasattr(ed, 'ExperimentFactory') - assert hasattr(ed, 'SampleModelFactory') + assert hasattr(ed, 'StructureFactory') # Access utility functions from utils via lazy getattr assert callable(ed.show_version) - assert callable(ed.get_value_from_xye_header) + assert callable(ed.extract_metadata) # Import once to exercise __getattr__; subsequent access should be cached by Python _ = ed.Project diff --git a/tests/unit/easydiffraction/test___main__.py b/tests/unit/easydiffraction/test___main__.py index 885f73ef..76ba7cec 100644 --- a/tests/unit/easydiffraction/test___main__.py +++ b/tests/unit/easydiffraction/test___main__.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause from typer.testing import CliRunner diff --git a/tests/unit/easydiffraction/utils/test_enums.py b/tests/unit/easydiffraction/utils/test_enums.py new file mode 100644 index 00000000..9d970540 --- /dev/null +++ b/tests/unit/easydiffraction/utils/test_enums.py @@ -0,0 +1,27 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + +import pytest + +from easydiffraction.utils.enums import VerbosityEnum + + +def test_verbosity_enum_members(): + assert VerbosityEnum.FULL == 'full' + assert VerbosityEnum.SHORT == 'short' + assert VerbosityEnum.SILENT == 'silent' + + +def test_verbosity_enum_from_string(): + assert VerbosityEnum('full') is VerbosityEnum.FULL + assert VerbosityEnum('short') is VerbosityEnum.SHORT + assert VerbosityEnum('silent') is VerbosityEnum.SILENT + + +def test_verbosity_enum_invalid_string(): + with pytest.raises(ValueError): + VerbosityEnum('verbose') + + +def test_verbosity_enum_default(): + assert VerbosityEnum.default() is VerbosityEnum.FULL diff --git a/tests/unit/easydiffraction/utils/test_logging.py b/tests/unit/easydiffraction/utils/test_logging.py index 8503e178..6dd520c2 100644 --- a/tests/unit/easydiffraction/utils/test_logging.py +++ b/tests/unit/easydiffraction/utils/test_logging.py @@ -1,6 +1,7 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause + def test_module_import(): import easydiffraction.utils.logging as MUT diff --git a/tests/unit/easydiffraction/utils/test_theme_detect.py b/tests/unit/easydiffraction/utils/test_theme_detect.py index e9899fb9..c3277a0a 100644 --- a/tests/unit/easydiffraction/utils/test_theme_detect.py +++ b/tests/unit/easydiffraction/utils/test_theme_detect.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause """Unit tests for theme detection module.""" @@ -23,12 +23,7 @@ def test_dark_theme_detection(self, tmp_path: Path) -> None: ) settings_dir = ( - tmp_path - / '.jupyter' - / 'lab' - / 'user-settings' - / '@jupyterlab' - / 'apputils-extension' + tmp_path / '.jupyter' / 'lab' / 'user-settings' / '@jupyterlab' / 'apputils-extension' ) settings_dir.mkdir(parents=True) @@ -45,12 +40,7 @@ def test_light_theme_detection(self, tmp_path: Path) -> None: ) settings_dir = ( - tmp_path - / '.jupyter' - / 'lab' - / 'user-settings' - / '@jupyterlab' - / 'apputils-extension' + tmp_path / '.jupyter' / 'lab' / 'user-settings' / '@jupyterlab' / 'apputils-extension' ) settings_dir.mkdir(parents=True) @@ -76,12 +66,7 @@ def test_comments_in_settings(self, tmp_path: Path) -> None: ) settings_dir = ( - tmp_path - / '.jupyter' - / 'lab' - / 'user-settings' - / '@jupyterlab' - / 'apputils-extension' + tmp_path / '.jupyter' / 'lab' / 'user-settings' / '@jupyterlab' / 'apputils-extension' ) settings_dir.mkdir(parents=True) @@ -119,9 +104,7 @@ def test_vscode_dark_theme(self, tmp_path: Path) -> None: vscode_dir.mkdir() settings_file = vscode_dir / 'settings.json' - settings_file.write_text( - json.dumps({'workbench.colorTheme': 'One Dark Pro'}) - ) + settings_file.write_text(json.dumps({'workbench.colorTheme': 'One Dark Pro'})) with mock.patch.dict(os.environ, {'VSCODE_PID': '12345'}): with mock.patch.object(Path, 'cwd', return_value=tmp_path): @@ -137,9 +120,7 @@ def test_vscode_light_theme(self, tmp_path: Path) -> None: vscode_dir.mkdir() settings_file = vscode_dir / 'settings.json' - settings_file.write_text( - json.dumps({'workbench.colorTheme': 'Light+ (default light)'}) - ) + settings_file.write_text(json.dumps({'workbench.colorTheme': 'Light+ (default light)'})) with mock.patch.dict(os.environ, {'VSCODE_PID': '12345'}): with mock.patch.object(Path, 'cwd', return_value=tmp_path): @@ -160,9 +141,7 @@ def test_vscode_nls_config_dark(self) -> None: class TestCheckSystemPreferences: """Tests for _check_system_preferences function.""" - @pytest.mark.skipif( - not os.sys.platform.startswith('darwin'), reason='macOS only test' - ) + @pytest.mark.skipif(not os.sys.platform.startswith('darwin'), reason='macOS only test') def test_macos_dark_mode(self) -> None: """Test macOS dark mode detection.""" from easydiffraction.utils._vendored.jupyter_dark_detect.detector import ( @@ -174,9 +153,7 @@ def test_macos_dark_mode(self) -> None: mock_run.return_value.stdout = 'Dark' assert _check_system_preferences() is True - @pytest.mark.skipif( - not os.sys.platform.startswith('darwin'), reason='macOS only test' - ) + @pytest.mark.skipif(not os.sys.platform.startswith('darwin'), reason='macOS only test') def test_macos_light_mode(self) -> None: """Test macOS light mode detection.""" from easydiffraction.utils._vendored.jupyter_dark_detect.detector import ( @@ -195,86 +172,82 @@ def test_default_to_false(self) -> None: """Test that is_dark defaults to False when no detection works.""" from easydiffraction.utils._vendored.theme_detect import is_dark - with mock.patch( - 'easydiffraction.utils._vendored.theme_detect.' - '_check_jupyterlab_settings', - return_value=None, - ): - with mock.patch( - 'easydiffraction.utils._vendored.theme_detect.' - '_check_vscode_settings', + with ( + mock.patch( + 'easydiffraction.utils._vendored.theme_detect._check_jupyterlab_settings', + return_value=None, + ), + mock.patch( + 'easydiffraction.utils._vendored.theme_detect._check_vscode_settings', return_value=None, - ): - with mock.patch( - 'easydiffraction.utils._vendored.theme_detect.' - '_check_javascript_detection', - return_value=None, - ): - with mock.patch( - 'easydiffraction.utils._vendored.theme_detect.' - '_check_system_preferences', - return_value=None, - ): - assert is_dark() is False + ), + mock.patch( + 'easydiffraction.utils._vendored.theme_detect._check_javascript_detection', + return_value=None, + ), + mock.patch( + 'easydiffraction.utils._vendored.theme_detect._check_system_preferences', + return_value=None, + ), + ): + assert is_dark() is False def test_jupyterlab_priority(self) -> None: """Test that JupyterLab settings take priority.""" from easydiffraction.utils._vendored.theme_detect import is_dark - with mock.patch( - 'easydiffraction.utils._vendored.theme_detect.' - '_check_jupyterlab_settings', - return_value=True, - ): - with mock.patch( - 'easydiffraction.utils._vendored.theme_detect.' - '_check_vscode_settings', + with ( + mock.patch( + 'easydiffraction.utils._vendored.theme_detect._check_jupyterlab_settings', + return_value=True, + ), + mock.patch( + 'easydiffraction.utils._vendored.theme_detect._check_vscode_settings', return_value=False, - ): - assert is_dark() is True + ), + ): + assert is_dark() is True def test_vscode_second_priority(self) -> None: """Test that VS Code settings are checked after JupyterLab.""" from easydiffraction.utils._vendored.theme_detect import is_dark - with mock.patch( - 'easydiffraction.utils._vendored.theme_detect.' - '_check_jupyterlab_settings', - return_value=None, - ): - with mock.patch( - 'easydiffraction.utils._vendored.theme_detect.' - '_check_vscode_settings', + with ( + mock.patch( + 'easydiffraction.utils._vendored.theme_detect._check_jupyterlab_settings', + return_value=None, + ), + mock.patch( + 'easydiffraction.utils._vendored.theme_detect._check_vscode_settings', return_value=True, - ): - assert is_dark() is True + ), + ): + assert is_dark() is True def test_javascript_before_system(self) -> None: """Test that JS detection comes before system preferences.""" from easydiffraction.utils._vendored.theme_detect import is_dark - with mock.patch( - 'easydiffraction.utils._vendored.theme_detect.' - '_check_jupyterlab_settings', - return_value=None, - ): - with mock.patch( - 'easydiffraction.utils._vendored.theme_detect.' - '_check_vscode_settings', + with ( + mock.patch( + 'easydiffraction.utils._vendored.theme_detect._check_jupyterlab_settings', + return_value=None, + ), + mock.patch( + 'easydiffraction.utils._vendored.theme_detect._check_vscode_settings', return_value=None, - ): - with mock.patch( - 'easydiffraction.utils._vendored.theme_detect.' - '_check_javascript_detection', - return_value=True, - ): - with mock.patch( - 'easydiffraction.utils._vendored.theme_detect.' - '_check_system_preferences', - return_value=False, - ): - # JS detection should win over system prefs - assert is_dark() is True + ), + mock.patch( + 'easydiffraction.utils._vendored.theme_detect._check_javascript_detection', + return_value=True, + ), + mock.patch( + 'easydiffraction.utils._vendored.theme_detect._check_system_preferences', + return_value=False, + ), + ): + # JS detection should win over system prefs + assert is_dark() is True class TestGetDetectionResult: @@ -284,37 +257,35 @@ def test_returns_dict_with_all_methods(self) -> None: """Test that get_detection_result returns all detection methods.""" from easydiffraction.utils._vendored.theme_detect import get_detection_result - with mock.patch( - 'easydiffraction.utils._vendored.theme_detect.' - '_check_jupyterlab_settings', - return_value=True, - ): - with mock.patch( - 'easydiffraction.utils._vendored.theme_detect.' - '_check_vscode_settings', + with ( + mock.patch( + 'easydiffraction.utils._vendored.theme_detect._check_jupyterlab_settings', + return_value=True, + ), + mock.patch( + 'easydiffraction.utils._vendored.theme_detect._check_vscode_settings', + return_value=None, + ), + mock.patch( + 'easydiffraction.utils._vendored.theme_detect._check_javascript_detection', return_value=None, - ): - with mock.patch( - 'easydiffraction.utils._vendored.theme_detect.' - '_check_javascript_detection', - return_value=None, - ): - with mock.patch( - 'easydiffraction.utils._vendored.theme_detect.' - '_check_system_preferences', - return_value=False, - ): - result = get_detection_result() - - assert 'jupyterlab_settings' in result - assert 'vscode_settings' in result - assert 'javascript_dom' in result - assert 'system_preferences' in result - - assert result['jupyterlab_settings'] is True - assert result['vscode_settings'] is None - assert result['javascript_dom'] is None - assert result['system_preferences'] is False + ), + mock.patch( + 'easydiffraction.utils._vendored.theme_detect._check_system_preferences', + return_value=False, + ), + ): + result = get_detection_result() + + assert 'jupyterlab_settings' in result + assert 'vscode_settings' in result + assert 'javascript_dom' in result + assert 'system_preferences' in result + + assert result['jupyterlab_settings'] is True + assert result['vscode_settings'] is None + assert result['javascript_dom'] is None + assert result['system_preferences'] is False class TestImports: diff --git a/tests/unit/easydiffraction/utils/test_utils.py b/tests/unit/easydiffraction/utils/test_utils.py index 183178bd..ab2c9bab 100644 --- a/tests/unit/easydiffraction/utils/test_utils.py +++ b/tests/unit/easydiffraction/utils/test_utils.py @@ -1,11 +1,10 @@ -# SPDX-FileCopyrightText: 2021-2026 EasyDiffraction contributors +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause import numpy as np import pytest - def test_module_import(): import easydiffraction.utils.utils as MUT @@ -69,20 +68,18 @@ def test_str_to_ufloat_no_esd_defaults_nan(): assert np.isclose(expected_value, actual_value) and np.isnan(u.std_dev) -def test_get_value_from_xye_header(tmp_path): - import easydiffraction.utils.utils as MUT +def test_extract_metadata(tmp_path): + import easydiffraction.io.ascii as ascii_io - text = 'DIFC = 123.45 two_theta = 67.89\nrest of file\n' + text = '# DIFC = 123.45 two_theta = 67.89\nrest of file\n' p = tmp_path / 'file.xye' p.write_text(text) - expected_difc = 123.45 - expected_two_theta = 67.89 - actual = np.array([ - MUT.get_value_from_xye_header(p, 'DIFC'), - MUT.get_value_from_xye_header(p, 'two_theta'), - ]) - expected = np.array([expected_difc, expected_two_theta]) - assert np.allclose(expected, actual) + difc = ascii_io.extract_metadata(str(p), r'DIFC\s*=\s*([-+]?\d*\.?\d+(?:[eE][-+]?\d+)?)') + two_theta = ascii_io.extract_metadata( + str(p), r'two_theta\s*=\s*([-+]?\d*\.?\d+(?:[eE][-+]?\d+)?)' + ) + assert np.isclose(difc, 123.45) + assert np.isclose(two_theta, 67.89) def test_validate_url_rejects_non_http_https(): @@ -129,6 +126,7 @@ def test_is_pycharm_and_is_colab(monkeypatch): def test_render_table_terminal_branch(capsys, monkeypatch): import easydiffraction.utils.utils as MUT + # Ensure non-notebook rendering; on CI/default env it's terminal anyway. MUT.render_table( columns_data=[[1, 2], [3, 4]], @@ -354,7 +352,6 @@ def __exit__(self, *args): def test_resolve_tutorial_url(): - import easydiffraction.utils.utils as MUT # Test with a specific version url_template = 'https://example.com/{version}/tutorials/ed-1/ed-1.ipynb' @@ -362,4 +359,3 @@ def test_resolve_tutorial_url(): # So we just test that the function exists and replaces {version} result = url_template.replace('{version}', '0.8.0') assert result == 'https://example.com/0.8.0/tutorials/ed-1/ed-1.ipynb' - diff --git a/tmp/__validator.py b/tmp/__validator.py new file mode 100644 index 00000000..359ecd1a --- /dev/null +++ b/tmp/__validator.py @@ -0,0 +1,87 @@ +# %% +import easydiffraction as ed +import numpy as np + +# %% +project = ed.Project() + +# + +project.experiments.add_from_data_path(name='aaa', data_path=23) + +exit() + +# %% +model_path = ed.download_data(id=1, destination='data') +project.structures.add_from_cif_path(cif_path=model_path) + +#project.structures.add_from_scratch(name='qwe') +#project.structures['qwe'] = 6 +#print(project.structures['qwe'].name.value) +#struct = project.structures['qwe'] +#struct.cell = "cell" +#print(struct.cell) + +#exit() + + + +# %% +expt_path = ed.download_data(id=2, destination='data') +project.experiments.add_from_cif_path(cif_path=expt_path) +#project.experiments.add_from_cif_path(cif_path=77) + +#expt = ed.ExperimentFactory.from_scratch(name='expt', scattering_type='total2') +#print(expt) +exit() + +print('\nStructure:') +print(project.structures['lbco']) + +print('\nExperiment:') +print(project.experiments['hrpt']) + + +exit() + + + +# %% +#sample = project.sample_models.get(id=1) +#sample = project.sample_models.get(name='Fe2O3') +sample = project.sample_models['lbco'] + +# %% +print() +print("=== Testing cell.length_a ===") +sample.cell.length_a = 3 +print(sample.cell.length_a, type(sample.cell.length_a.value)) +sample.cell.length_a = np.int64(4) +print(sample.cell.length_a, type(sample.cell.length_a.value)) +sample.cell.length_a = np.float64(5.5) +print(sample.cell.length_a, type(sample.cell.length_a.value)) +###sample.cell.length_a = "6.0" +###sample.cell.length_a = -7.0 +###sample.cell.length_a = None +print(sample.cell.length_a, type(sample.cell.length_a.value)) + +# %% +print() +print("=== Testing space_group ===") +sample.space_group.name_h_m = 'P n m a' +print(sample.space_group.name_h_m) +print(sample.space_group.it_coordinate_system_code) +###sample.space_group.name_h_m = 'P x y z' +print(sample.space_group.name_h_m) +###sample.space_group.name_h_m = 4500 +print(sample.space_group.name_h_m) +sample.space_group.it_coordinate_system_code = 'cab' +print(sample.space_group.it_coordinate_system_code) + +# %% +print() +print("=== Testing atom_sites ===") +sample.atom_sites.add(label2='O5', type_symbol='O') + +# %% +sample.show_as_cif() \ No newline at end of file diff --git a/tmp/basic_single-fit_pd-neut-cwl_LBCO-HRPT.py b/tmp/basic_single-fit_pd-neut-cwl_LBCO-HRPT.py index cf21bcc8..18db2b22 100644 --- a/tmp/basic_single-fit_pd-neut-cwl_LBCO-HRPT.py +++ b/tmp/basic_single-fit_pd-neut-cwl_LBCO-HRPT.py @@ -375,22 +375,22 @@ # # #### Set Calculator # -# Show supported calculation engines. +# Show supported calculation engines for this experiment. # %% -project.analysis.show_supported_calculators() +project.experiments['hrpt'].show_supported_calculator_types() # %% [markdown] -# Show current calculation engine. +# Show current calculation engine for this experiment. # %% -project.analysis.show_current_calculator() +project.experiments['hrpt'].show_current_calculator_type() # %% [markdown] # Select the desired calculation engine. # %% -project.analysis.current_calculator = 'cryspy' +project.experiments['hrpt'].calculator_type = 'cryspy' # %% [markdown] # #### Show Calculated Data @@ -435,23 +435,9 @@ # %% [markdown] # #### Set Fit Mode -# -# Show supported fit modes. - -# %% -project.analysis.show_available_fit_modes() - -# %% [markdown] -# Show current fit mode. - -# %% -project.analysis.show_current_fit_mode() - -# %% [markdown] -# Select desired fit mode. # %% -project.analysis.fit_mode = 'single' +project.analysis.fit_mode.mode = 'single' # %% [markdown] # #### Set Minimizer diff --git a/tmp/cryst-struct_pd-neut-tof_multiphase-BSFTO-HRPT.py b/tmp/cryst-struct_pd-neut-tof_multiphase-BSFTO-HRPT.py index b4ee7e15..e7e6c1d6 100644 --- a/tmp/cryst-struct_pd-neut-tof_multiphase-BSFTO-HRPT.py +++ b/tmp/cryst-struct_pd-neut-tof_multiphase-BSFTO-HRPT.py @@ -220,12 +220,6 @@ # This section outlines the analysis process, including how to configure # calculation and fitting engines. # -# #### Set Calculator - -# %% -project.analysis.current_calculator = 'cryspy' - -# %% [markdown] # #### Set Minimizer # %% diff --git a/tmp/short7.py b/tmp/short7.py index dfec2cd8..285704e9 100644 --- a/tmp/short7.py +++ b/tmp/short7.py @@ -76,7 +76,6 @@ def single_fit_neutron_pd_cwl_lbco() -> None: expt.show_as_cif() # Prepare for fitting - project.analysis.current_calculator = 'cryspy' project.analysis.current_minimizer = 'lmfit (leastsq)' # ------------ 1st fitting ------------ diff --git a/tmp/show_d401.py b/tmp/show_d401.py new file mode 100644 index 00000000..8ac806fc --- /dev/null +++ b/tmp/show_d401.py @@ -0,0 +1,26 @@ +files_lines = [ + ('src/easydiffraction/analysis/calculators/crysfml.py', [108,160,205]), + ('src/easydiffraction/analysis/calculators/cryspy.py', [357,377]), + ('src/easydiffraction/analysis/calculators/pdffit.py', [61]), + ('src/easydiffraction/analysis/minimizers/dfols.py', [55,77]), + ('src/easydiffraction/analysis/minimizers/lmfit.py', [40,66,96,117,140]), + ('src/easydiffraction/core/singleton.py', [28,45,49,65,107,116,138]), + ('src/easydiffraction/core/variable.py', [386]), + ('src/easydiffraction/datablocks/experiment/categories/data/bragg_pd.py', [312,317,329,334,339,344,494,567]), + ('src/easydiffraction/datablocks/experiment/categories/data/bragg_sc.py', [268,286,291,301,308,313,318]), + ('src/easydiffraction/datablocks/experiment/categories/data/total_pd.py', [195,200,212,217,340]), + ('src/easydiffraction/project/project_info.py', [119]), + ('src/easydiffraction/utils/environment.py', [35,47]), + ('src/easydiffraction/utils/logging.py', [685]), + ('src/easydiffraction/utils/utils.py', [308]), +] +for fname, lines in files_lines: + with open(fname) as f: + content = f.readlines() + for ln in lines: + lo = max(0, ln-2) + hi = min(len(content), ln+2) + for i, l in enumerate(content[lo:hi]): + print(f'{fname}:{lo+i+1}: {l}', end='') + print('---') + diff --git a/tmp/show_w505.py b/tmp/show_w505.py new file mode 100644 index 00000000..6e606803 --- /dev/null +++ b/tmp/show_w505.py @@ -0,0 +1,27 @@ +files_lines = [ + ('src/easydiffraction/datablocks/experiment/categories/data/bragg_pd.py', [196,277,324,527,602]), + ('src/easydiffraction/datablocks/experiment/categories/data/bragg_sc.py', [29,298,422]), + ('src/easydiffraction/datablocks/experiment/categories/data/total_pd.py', [207]), + ('src/easydiffraction/datablocks/experiment/categories/experiment_type/default.py', [30]), + ('src/easydiffraction/datablocks/experiment/categories/extinction/shelx.py', [20]), + ('src/easydiffraction/datablocks/experiment/item/base.py', [71,604]), + ('src/easydiffraction/datablocks/experiment/item/factory.py', [78]), + ('src/easydiffraction/datablocks/structure/item/base.py', [248]), + ('src/easydiffraction/display/base.py', [144]), + ('src/easydiffraction/display/utils.py', [21]), + ('src/easydiffraction/io/cif/serialize.py', [162]), + ('src/easydiffraction/project/project.py', [78]), + ('src/easydiffraction/summary/summary.py', [208]), + ('src/easydiffraction/utils/logging.py', [327,478]), + ('src/easydiffraction/utils/utils.py', [52,73]), +] +for fname, lines in files_lines: + with open(fname) as f: + content = f.readlines() + for ln in lines: + lo = max(0, ln-2) + hi = min(len(content), ln+1) + for i, l in enumerate(content[lo:hi]): + print(f'{fname}:{lo+i+1}: {l}', end='') + print('---') + diff --git a/tools/add_assets_to_docs.sh b/tools/add_assets_to_docs.sh deleted file mode 100755 index e13eb40a..00000000 --- a/tools/add_assets_to_docs.sh +++ /dev/null @@ -1,16 +0,0 @@ -echo "📥 Add files from ../assets-docs" -cp -R ../assets-docs/docs/assets/ docs/assets/ -cp -R ../assets-docs/includes/ includes/ -cp -R ../assets-docs/overrides/ overrides/ - -echo "📥 Add files from ../assets-branding" -mkdir -p docs/assets/images/ -cp ../assets-branding/easydiffraction/hero/dark.png docs/assets/images/hero_dark.png -cp ../assets-branding/easydiffraction/hero/light.png docs/assets/images/hero_light.png -cp ../assets-branding/easydiffraction/logos/dark.svg docs/assets/images/logo_dark.svg -cp ../assets-branding/easydiffraction/logos/light.svg docs/assets/images/logo_light.svg -cp ../assets-branding/easydiffraction/icons/color.png docs/assets/images/favicon.png - -mkdir -p overrides/.icons/ -cp ../assets-branding/easydiffraction/icons/bw.svg overrides/.icons/easydiffraction.svg -cp ../assets-branding/easyscience-org/icons/eso-icon_bw.svg overrides/.icons/easyscience.svg diff --git a/tools/cleanup_docs.sh b/tools/cleanup_docs.sh deleted file mode 100755 index 757ec3e4..00000000 --- a/tools/cleanup_docs.sh +++ /dev/null @@ -1,10 +0,0 @@ -echo "🧹 Clean up after building documentation" -rm -rf site/ -rm -rf docs/assets/javascripts -rm -rf docs/assets/stylesheets -rm -rf docs/assets/images/*.png -rm -rf docs/assets/images/*.svg -rm -rf includes/ -rm -rf overrides/ -rm -rf docs/tutorials/*.ipynb -rm mkdocs.yml diff --git a/tools/convert_google_docstrings_to_numpy.py b/tools/convert_google_docstrings_to_numpy.py new file mode 100644 index 00000000..5fcb14e2 --- /dev/null +++ b/tools/convert_google_docstrings_to_numpy.py @@ -0,0 +1,539 @@ +#!/usr/bin/env python3 +"""Convert Google-style Python docstrings to numpydoc style.""" + +from __future__ import annotations + +import ast +import inspect +import re +import sys +import textwrap +from pathlib import Path + +from docstring_parser import DocstringStyle +from docstring_parser import compose +from docstring_parser import parse +from format_docstring.docstring_rewriter import calc_abs_pos +from format_docstring.docstring_rewriter import calc_line_starts +from format_docstring.docstring_rewriter import find_docstring +from format_docstring.docstring_rewriter import rebuild_literal + +SECTION_NAMES = ( + 'Args', + 'Arguments', + 'Returns', + 'Raises', + 'Yields', + 'Attributes', + 'Examples', + 'Notes', +) +GOOGLE_SECTION_RE = re.compile( + r'(?m)^(?P[ \t]*)(?P
' + + '|'.join(SECTION_NAMES) + + r'):\s*(?P\S.*)?$' +) +NUMPY_SECTION_RE = re.compile(r'(?m)^[^\n]+\n-+\n') +SECTION_KINDS_WITH_ITEMS = {'Args', 'Arguments', 'Attributes'} +PRESERVE_BLOCK_SECTIONS = {'Examples', 'Notes'} +GENERIC_ITEM_SECTIONS = {'Raises', 'Returns', 'Yields'} +GENERIC_ITEM_RE = re.compile( + r'(?[A-Za-z_][A-Za-z0-9_\.\[\], \|\(\)]{0,80}?)\s*:' +) + + +def _iter_python_files(paths: list[Path]) -> list[Path]: + files: list[Path] = [] + for path in paths: + if path.is_file() and path.suffix == '.py': + files.append(path) + continue + + if not path.exists(): + continue + + for file_path in sorted(path.rglob('*.py')): + if '_vendored' in file_path.parts: + continue + if '.pixi' in file_path.parts: + continue + files.append(file_path) + + return files + + +def _collect_names(node: ast.AST) -> list[str]: + names: list[str] = [] + + if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)): + args = list(node.args.posonlyargs) + list(node.args.args) + args += list(node.args.kwonlyargs) + names.extend(arg.arg for arg in args) + if node.args.vararg is not None: + names.append(node.args.vararg.arg) + if node.args.kwarg is not None: + names.append(node.args.kwarg.arg) + return [name for name in names if name not in {'self', 'cls'}] + + if isinstance(node, ast.ClassDef): + init_method = next( + ( + stmt + for stmt in node.body + if isinstance(stmt, ast.FunctionDef) and stmt.name == '__init__' + ), + None, + ) + if init_method is not None: + names.extend(_collect_names(init_method)) + + for stmt in node.body: + if isinstance(stmt, ast.AnnAssign) and isinstance(stmt.target, ast.Name): + names.append(stmt.target.id) + elif isinstance(stmt, ast.Assign): + for target in stmt.targets: + if isinstance(target, ast.Name): + names.append(target.id) + + return list(dict.fromkeys(names)) + + +def _strip_blank_edges(lines: list[str]) -> list[str]: + start = 0 + end = len(lines) + while start < end and not lines[start].strip(): + start += 1 + while end > start and not lines[end - 1].strip(): + end -= 1 + return lines[start:end] + + +def _join_wrapped_lines(lines: list[str]) -> str: + parts: list[str] = [] + for line in lines: + text = re.sub(r'\s+', ' ', line.strip()) + if not text: + continue + if parts and parts[-1].endswith('-') and not parts[-1].endswith(' -'): + parts[-1] = parts[-1][:-1] + text + else: + parts.append(text) + return ' '.join(parts) + + +def _collapse_whitespace(lines: list[str]) -> str: + return _join_wrapped_lines(lines) + + +def _repair_named_items(block_lines: list[str], names: list[str]) -> list[str] | None: + flat = _collapse_whitespace(block_lines) + if not flat or not names: + return None + + label_pattern = '|'.join(re.escape(name) for name in sorted(set(names), key=len, reverse=True)) + item_re = re.compile( + rf'(?\*{{0,2}}(?:{label_pattern})(?:\s*\([^)]*\))?)\s*:' + ) + matches = list(item_re.finditer(flat)) + if not matches or matches[0].start() != 0: + return None + + repaired: list[str] = [] + for index, match in enumerate(matches): + start = match.end() + end = matches[index + 1].start() if index + 1 < len(matches) else len(flat) + description = flat[start:end].strip() + repaired.append(f' {match.group("label")}: {description}' if description else f' {match.group("label")}:') + return repaired + + +def _repair_generic_items(block_lines: list[str]) -> list[str] | None: + flat = _collapse_whitespace(block_lines) + if not flat: + return None + + matches = list(GENERIC_ITEM_RE.finditer(flat)) + if not matches or matches[0].start() != 0: + return None + + repaired: list[str] = [] + for index, match in enumerate(matches): + start = match.end() + end = matches[index + 1].start() if index + 1 < len(matches) else len(flat) + description = flat[start:end].strip() + repaired.append(f' {match.group("label")}: {description}' if description else f' {match.group("label")}:') + return repaired + + +def _repair_section(section: str, block_lines: list[str], names: list[str]) -> list[str]: + stripped = _strip_blank_edges(block_lines) + if not stripped: + return [] + + if section in SECTION_KINDS_WITH_ITEMS: + flat = _collapse_whitespace(stripped).lower().rstrip('.') + if flat == 'none': + return [] + repaired = _repair_named_items(stripped, names) + if repaired is not None: + return repaired + + if section in GENERIC_ITEM_SECTIONS: + repaired = _repair_generic_items(stripped) + if repaired is not None: + return repaired + + if section in PRESERVE_BLOCK_SECTIONS: + return [f' {line}' if line else '' for line in stripped] + + flat = _collapse_whitespace(stripped) + return [f' {flat}'] if flat else [] + + +def _repair_inline_sections(docstring: str, names: list[str]) -> str: + cleaned = inspect.cleandoc(docstring.replace('\r\n', '\n')) + lines = cleaned.split('\n') + out: list[str] = [] + index = 0 + + while index < len(lines): + raw_line = lines[index] + heading = GOOGLE_SECTION_RE.match(raw_line) + if heading is None: + out.append(raw_line.rstrip()) + index += 1 + continue + + section = heading.group('section') + section_name = 'Args' if section == 'Arguments' else section + out.append(f'{section_name}:') + + block_lines: list[str] = [] + rest = heading.group('rest') + if rest: + block_lines.append(rest) + + index += 1 + while index < len(lines): + next_line = lines[index] + if GOOGLE_SECTION_RE.match(next_line): + break + if ( + section_name not in PRESERVE_BLOCK_SECTIONS + and not next_line.strip() + and index + 1 < len(lines) + and lines[index + 1].strip() + and GOOGLE_SECTION_RE.match(lines[index + 1]) is None + ): + break + block_lines.append(next_line.rstrip()) + index += 1 + + out.extend(_repair_section(section_name, block_lines, names)) + + return '\n'.join(out) + + +def _looks_google(docstring: str) -> bool: + return bool(GOOGLE_SECTION_RE.search(docstring)) + + +def _looks_numpydoc(docstring: str) -> bool: + return bool(NUMPY_SECTION_RE.search(docstring)) + + +def _meta_kinds(parsed) -> set[str]: + kinds: set[str] = set() + for meta in parsed.meta: + args = getattr(meta, 'args', None) or [] + if not args: + continue + kinds.add(str(args[0]).lower()) + return kinds + + +def _contains_unparsed_sections(parsed) -> bool: + for text in (parsed.short_description, parsed.long_description): + if text and GOOGLE_SECTION_RE.search(text): + return True + return False + + +def _has_section_heading(docstring: str, section: str) -> bool: + return re.search(rf'(?m)^[ \t]*{re.escape(section)}:\s*(?:\S.*)?$', docstring) is not None + + +def _is_safe_conversion(docstring: str, parsed) -> bool: + if '::' in docstring: + return False + + kinds = _meta_kinds(parsed) + if _contains_unparsed_sections(parsed): + return False + + expectations = { + 'Args': 'param', + 'Arguments': 'param', + 'Attributes': 'attribute', + 'Returns': 'returns', + 'Raises': 'raises', + 'Yields': 'yields', + 'Examples': 'examples', + } + for section, expected_kind in expectations.items(): + if _has_section_heading(docstring, section) and expected_kind not in kinds: + return False + + return True + + +def _is_section_header(lines: list[str], index: int) -> bool: + return index + 1 < len(lines) and bool(lines[index].strip()) and set(lines[index + 1].strip()) == {'-'} + + +def _wrap_paragraph(lines: list[str], width: int, indent: str = '') -> list[str]: + if not lines: + return [] + + text = _join_wrapped_lines(lines) + if not text: + return [''] if lines else [] + + return textwrap.wrap( + text, + width=width, + initial_indent=indent, + subsequent_indent=indent, + break_long_words=False, + break_on_hyphens=False, + ) + + +def _format_freeform_block(lines: list[str], width: int = 72, indent: str = '') -> list[str]: + stripped = _strip_blank_edges(lines) + if not stripped: + return [] + + formatted: list[str] = [] + paragraph: list[str] = [] + for line in stripped: + if not line.strip(): + if paragraph: + formatted.extend(_wrap_paragraph(paragraph, width=width, indent=indent)) + paragraph = [] + if formatted and formatted[-1] != '': + formatted.append('') + continue + + content = line.strip() + if content.startswith(('>>>', '...')): + if paragraph: + formatted.extend(_wrap_paragraph(paragraph, width=width, indent=indent)) + paragraph = [] + formatted.append(f'{indent}{content}') + continue + + paragraph.append(content) + + if paragraph: + formatted.extend(_wrap_paragraph(paragraph, width=width, indent=indent)) + + return formatted + + +def _format_named_section(block_lines: list[str]) -> list[str]: + lines = _strip_blank_edges(block_lines) + if not lines: + return [] + + formatted: list[str] = [] + index = 0 + while index < len(lines): + if not lines[index].strip(): + index += 1 + continue + + header = lines[index].strip() + formatted.append(header) + index += 1 + + description: list[str] = [] + while index < len(lines): + line = lines[index] + if not line.strip(): + index += 1 + if description: + break + continue + if not line.startswith(' ') and not line.startswith('\t'): + break + description.append(line.strip()) + index += 1 + + if description: + formatted.extend(_wrap_paragraph(description, width=68, indent=' ')) + elif formatted and formatted[-1] != '': + formatted.append('') + + if formatted and formatted[-1] == '': + formatted.pop() + return formatted + + +def _format_return_like_section(block_lines: list[str]) -> list[str]: + lines = _strip_blank_edges(block_lines) + if not lines: + return [] + + first = next((line for line in lines if line.strip()), '') + if first.startswith((' ', '\t')): + return _format_freeform_block(lines, width=68, indent=' ') + + return _format_named_section(lines) + + +def _format_numpydoc_output(docstring: str) -> str: + lines = docstring.strip('\n').splitlines() + formatted: list[str] = [] + index = 0 + + preamble: list[str] = [] + while index < len(lines) and not _is_section_header(lines, index): + preamble.append(lines[index]) + index += 1 + formatted.extend(_format_freeform_block(preamble)) + + while index < len(lines): + if not _is_section_header(lines, index): + index += 1 + continue + + if formatted and formatted[-1] != '': + formatted.append('') + heading = lines[index].strip() + underline = lines[index + 1].strip() + formatted.extend([heading, underline]) + index += 2 + + block: list[str] = [] + while index < len(lines) and not _is_section_header(lines, index): + block.append(lines[index]) + index += 1 + + if heading in {'Parameters', 'Attributes'}: + formatted.extend(_format_named_section(block)) + elif heading in {'Returns', 'Raises', 'Yields'}: + formatted.extend(_format_return_like_section(block)) + else: + formatted.extend(_format_freeform_block(block)) + + return '\n'.join(_strip_blank_edges(formatted)) + + +def _convert_docstring(docstring: str, names: list[str]) -> str | None: + cleaned = inspect.cleandoc(docstring) + if not _looks_google(cleaned): + return None + + repaired = _repair_inline_sections(cleaned, names) + + try: + parsed = parse(repaired, style=DocstringStyle.GOOGLE) + except Exception: + return None + + if not _is_safe_conversion(repaired, parsed): + return None + + converted = _format_numpydoc_output(compose(parsed, style=DocstringStyle.NUMPYDOC)) + return converted if converted != cleaned else None + + +def _reformat_numpydoc_docstring(docstring: str) -> str | None: + cleaned = inspect.cleandoc(docstring) + if not _looks_numpydoc(cleaned): + return None + + formatted = _format_numpydoc_output(cleaned) + return formatted if formatted != cleaned else None + + +def _format_multiline_docstring(content: str, indent: int) -> str: + indent_str = ' ' * indent + lines = content.strip('\n').splitlines() + body = '\n'.join(f'{indent_str}{line}' if line else '' for line in lines) + return f'\n{body}\n{indent_str}' + + +def _convert_file(path: Path) -> bool: + source_code = path.read_text() + tree = ast.parse(source_code, type_comments=True) + line_starts = calc_line_starts(source_code) + replacements: list[tuple[int, int, str]] = [] + + nodes: list[ast.AST] = [tree] + nodes.extend(ast.walk(tree)) + + for node in nodes: + if not isinstance(node, (ast.Module, ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)): + continue + + docstring_obj = find_docstring(node) + if docstring_obj is None: + continue + + value = docstring_obj.value + end_lineno = getattr(value, 'end_lineno', None) + end_col_offset = getattr(value, 'end_col_offset', None) + if end_lineno is None or end_col_offset is None: + continue + + docstring = ast.get_docstring(node, clean=False) + if docstring is None: + continue + + converted = _convert_docstring(docstring, _collect_names(node)) + if converted is None: + converted = _reformat_numpydoc_docstring(docstring) + if converted is None: + continue + + start = calc_abs_pos(source_code, line_starts, value.lineno, value.col_offset) + end = calc_abs_pos(source_code, line_starts, end_lineno, end_col_offset) + original_literal = source_code[start:end] + leading_indent = getattr(value, 'col_offset', 0) + formatted = _format_multiline_docstring(converted, leading_indent) + new_literal = rebuild_literal(original_literal, formatted) + if new_literal is None or new_literal == original_literal: + continue + + replacements.append((start, end, new_literal)) + + if not replacements: + return False + + replacements.sort(reverse=True) + new_source = source_code + for start, end, replacement in replacements: + new_source = new_source[:start] + replacement + new_source[end:] + + compile(new_source, str(path), 'exec') + path.write_text(new_source) + return True + + +def main(argv: list[str]) -> int: + input_paths = [Path(arg) for arg in argv] if argv else [Path('src'), Path('tools')] + changed = 0 + + for path in _iter_python_files(input_paths): + if _convert_file(path): + changed += 1 + print(f'Converted {path}') + + print(f'Converted docstrings in {changed} file(s).') + return 0 + + +if __name__ == '__main__': + raise SystemExit(main(sys.argv[1:])) diff --git a/tools/create_mkdocs_yml.py b/tools/create_mkdocs_yml.py deleted file mode 100644 index fb2b8685..00000000 --- a/tools/create_mkdocs_yml.py +++ /dev/null @@ -1,158 +0,0 @@ -import os -import re -from pathlib import Path -from typing import Any -from typing import Dict -from typing import List - -import material.extensions.emoji # side-effect: register emoji tag -import pymdownx.superfences # side-effect: register superfence tag -import yaml - -# Side-effect imports above ensure tagged YAML constructors -# (e.g., !!python/name:...) can be resolved during load. - - -def _activate_yaml_tag_side_effects() -> None: # pragma: no cover - trivial - """Access imported modules' attributes so Ruff sees them as used. - - The primary purpose of importing these packages is to ensure the - tagged constructors are importable during YAML load. Accessing the - attributes makes the side-effect explicit without needing noqa. - """ - _ = material.extensions.emoji.twemoji # type: ignore[attr-defined] - _ = pymdownx.superfences.fence_code_format # type: ignore[attr-defined] - - -_activate_yaml_tag_side_effects() - - -def load_yaml_with_env_variables(file_path: str) -> Dict[str, Any]: - """Load YAML resolving env variables declared as !ENV ${VAR_NAME}. - - Args: - file_path (str): Path to the YAML file. - - Returns: - dict: Parsed YAML content with environment variables replaced. - """ - tag = '!ENV' - pattern = re.compile(r'.*?\${([A-Z0-9_]+)}.*?') - - def constructor_env_variables(loader, node): # type: ignore[all] - """YAML constructor replacing !ENV markers with values.""" - value = loader.construct_scalar(node) # type: ignore[attr-defined] - for var in pattern.findall(value): - value = value.replace(f'${{{var}}}', os.environ.get(var, var)) - return value - - loader = yaml.FullLoader - loader.add_implicit_resolver(tag, pattern, None) - loader.add_constructor(tag, constructor_env_variables) - - with Path(file_path).open('r', encoding='utf-8') as fh: - return yaml.full_load(fh) - - -def merge_yaml(base_config: Dict[str, Any], override_config: Dict[str, Any]) -> Dict[str, Any]: - """Deep merge two YAML dicts; override has priority.""" - if not isinstance(base_config, dict): - return override_config - - merged_config = base_config.copy() - - for key, override_value in override_config.items(): - if key in merged_config: - base_value = merged_config[key] - if isinstance(base_value, dict) and isinstance(override_value, dict): - merged_config[key] = merge_yaml(base_value, override_value) - elif isinstance(base_value, list) and isinstance(override_value, list): - merged_config[key] = merge_lists(base_value, override_value) - else: - merged_config[key] = override_value - else: - merged_config[key] = override_value - - return merged_config - - -def merge_lists(base_list: List[Any], override_list: List[Any]) -> List[Any]: - """Merge two lists with handling of single-key dict items. - - Single-key dicts sharing a key are deep-merged; other items are - appended if not already present. - - Args: - base_list (list): The base list. - override_list (list): The overriding list. - - Returns: - list: The merged list. - """ - merged_list = [] - seen_items = {} - - for item in base_list + override_list: - if isinstance(item, dict) and len(item) == 1: - key = next(iter(item)) # Extract dictionary key (e.g., "mkdocs-jupyter") - if key in seen_items: - seen_items[key] = merge_yaml(seen_items[key], item[key]) # Merge dictionaries - else: - seen_items[key] = item[key] - elif item not in merged_list: - merged_list.append(item) - - # Convert merged dictionary values back into list format - for key, value in seen_items.items(): - merged_list.append({key: value}) - - return merged_list - - -def save_yaml(data: Dict[str, Any], output_file: str) -> None: - """Write YAML preserving !!python/name tags and Unicode.""" - - class CustomDumper(yaml.Dumper): - """Custom dumper avoiding unnecessary anchors & quotes.""" - - def ignore_aliases(self, _): # noqa: D401 - simple override - return True - - out_path = Path(output_file) - with out_path.open('w', encoding='utf-8') as f: - f.write('# WARNING: This file is auto-generated during the build process.\n') - f.write('# DO NOT EDIT THIS FILE MANUALLY.\n') - f.write('# It is created by merging:\n') - f.write('# - Generic YAML file: ../assets-docs/mkdocs.yml\n') - f.write('# - Project specific YAML file: docs/mkdocs.yml\n\n') - - with out_path.open('a', encoding='utf-8') as f: - yaml.dump( - data, - f, - Dumper=CustomDumper, # Use custom dumper - allow_unicode=True, # Ensure Unicode characters like © are preserved - default_flow_style=False, # - sort_keys=False, # Preserve the order of keys - ) - - -def main() -> None: - """Main function to read, merge, and save YAML configurations.""" - generic_config_path = '../assets-docs/mkdocs.yml' - specific_config_path = 'docs/mkdocs.yml' - output_path = 'mkdocs.yml' - - print(f'Reading generic config: {generic_config_path}') - base_config = load_yaml_with_env_variables(generic_config_path) - - print(f'Reading project specific config: {specific_config_path}') - override_config = load_yaml_with_env_variables(specific_config_path) - - print(f'Saving merged config: {output_path}') - merged_config = merge_yaml(base_config, override_config) - save_yaml(merged_config, output_path) - - -if __name__ == '__main__': - main() diff --git a/tools/license_headers.py b/tools/license_headers.py new file mode 100644 index 00000000..47d23524 --- /dev/null +++ b/tools/license_headers.py @@ -0,0 +1,315 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Add, remove, or check SPDX headers in Python files.""" + +from __future__ import annotations + +import argparse +import fnmatch +import tomllib +from datetime import datetime +from pathlib import Path +from typing import Any +from typing import Optional +from typing import Union + +from git import Repo +from spdx_headers.core import find_repository_root +from spdx_headers.core import get_copyright_info +from spdx_headers.core import has_spdx_header +from spdx_headers.data import load_license_data +from spdx_headers.operations import add_header_to_single_file +from spdx_headers.operations import remove_header_from_single_file + +LICENSE_DATABASE = load_license_data() + + +def load_pyproject(repo_path: Union[str, Path]) -> dict[str, Any]: + """Load and return parsed ``pyproject.toml`` data for the repository.""" + repo_root = find_repository_root(repo_path) + pyproject_path = repo_root / 'pyproject.toml' + + with pyproject_path.open('rb') as file_handle: + return tomllib.load(file_handle) + + +def get_pyproject_value(pyproject_data: dict[str, Any], dotted_key: str) -> Any: + """Return a nested ``pyproject.toml`` value from a dotted key.""" + value: Any = pyproject_data + for part in dotted_key.split('.'): + if not isinstance(value, dict) or part not in value: + raise KeyError(dotted_key) + value = value[part] + return value + + +def normalize_pattern(pattern: str) -> str: + """Normalize an exclude pattern to a POSIX-style relative path.""" + normalized = Path(pattern).as_posix() + if normalized.startswith('./'): + normalized = normalized[2:] + return normalized.rstrip('/') + + +def get_exclude_patterns( + repo_path: Union[str, Path], + exclude_values: list[str], + exclude_from_pyproject_toml: Optional[str], +) -> list[str]: + """Return normalized exclude patterns from CLI and ``pyproject.toml``.""" + pyproject_data = load_pyproject(repo_path) + patterns: list[str] = [] + + if exclude_from_pyproject_toml: + value = get_pyproject_value(pyproject_data, exclude_from_pyproject_toml) + if not isinstance(value, list) or not all(isinstance(item, str) for item in value): + raise ValueError( + f'{exclude_from_pyproject_toml} in pyproject.toml must be a list of strings.', + ) + patterns.extend(value) + + for item in exclude_values: + try: + value = get_pyproject_value(pyproject_data, item) + except KeyError: + patterns.append(item) + continue + + if not isinstance(value, list) or not all(isinstance(entry, str) for entry in value): + raise ValueError(f'{item} in pyproject.toml must be a list of strings.') + patterns.extend(value) + + normalized_patterns: list[str] = [] + seen: set[str] = set() + for pattern in patterns: + normalized = normalize_pattern(pattern) + if normalized and normalized not in seen: + normalized_patterns.append(normalized) + seen.add(normalized) + + return normalized_patterns + + +def get_file_creation_year(file_path: Union[str, Path]) -> str: + """Return the year the file was first added to Git history. + + If the year cannot be determined, fall back to the current year. + """ + file_path = Path(file_path) + + repo = Repo(file_path, search_parent_directories=True) + root = Path(repo.working_tree_dir).resolve() + rel_path = file_path.resolve().relative_to(root) + + rel_path_git = rel_path.as_posix() + + log_output = repo.git.log( + '--follow', + '--diff-filter=A', + '--reverse', + '--format=%ad', + '--date=format:%Y', + '--', + rel_path_git, + ).strip() + + year = log_output.splitlines()[0].strip() if log_output else '' + + return year or str(datetime.now().year) + + +def get_org_url(repo_path: Union[str, Path]) -> str: + """Return the organization URL derived from the repository source URL.""" + pyproject_data = load_pyproject(repo_path) + repo_url = pyproject_data['project']['urls']['Source Code'] + return repo_url.rsplit('/', 1)[0] + + +def get_project_license(repo_path: Union[str, Path]) -> str: + """Return the project license value from ``pyproject.toml``.""" + pyproject_data = load_pyproject(repo_path) + return pyproject_data['project']['license'] + + +def get_copyright_holder(repo_path: Union[str, Path]) -> str: + """Return the repository copyright holder name.""" + _, name, _ = get_copyright_info(repo_path) + return name + + +def add_spdx_header( + target_file: Union[str, Path], + *, + license_key: str, + copyright_holder: str, + org_url: str, +) -> None: + """Add SPDX headers to one file.""" + year = get_file_creation_year(target_file) + + add_header_to_single_file( + filepath=target_file, + license_key=license_key, + license_data=LICENSE_DATABASE, + year=year, + name=copyright_holder, + email=org_url, + ) + + +def is_excluded(relative_path: str, exclude_patterns: list[str]) -> bool: + """Return whether a relative path should be excluded.""" + for pattern in exclude_patterns: + if fnmatch.fnmatch(relative_path, pattern): + return True + if relative_path == pattern: + return True + if relative_path.startswith(f'{pattern}/'): + return True + return False + + +def iter_python_files( + paths: list[str], + *, + repo_root: Path, + exclude_patterns: list[str], + parser: argparse.ArgumentParser, +) -> list[Path]: + """Collect Python files under the given paths after exclusions.""" + files: list[Path] = [] + seen: set[Path] = set() + + for base_dir in paths: + base_path = Path(base_dir) + if not base_path.exists(): + parser.error(f'Path does not exist: {base_dir}') + + if base_path.is_file(): + candidates = [base_path] if base_path.suffix == '.py' else [] + else: + candidates = sorted(base_path.rglob('*.py')) + + for py_file in candidates: + resolved = py_file.resolve() + try: + relative_path = resolved.relative_to(repo_root).as_posix() + except ValueError: + relative_path = py_file.as_posix() + + if is_excluded(relative_path, exclude_patterns): + continue + + if resolved not in seen: + files.append(py_file) + seen.add(resolved) + + return files + + +def run_add( + files: list[Path], + *, + license_key: str, + copyright_holder: str, + org_url: str, +) -> int: + """Add SPDX headers to all selected files.""" + for py_file in files: + add_spdx_header( + py_file, + license_key=license_key, + copyright_holder=copyright_holder, + org_url=org_url, + ) + return 0 + + +def run_remove(files: list[Path]) -> int: + """Remove SPDX headers from all selected files.""" + for py_file in files: + remove_header_from_single_file(py_file) + return 0 + + +def run_check(files: list[Path]) -> int: + """Check SPDX headers in all selected files.""" + missing_files = [py_file for py_file in files if not has_spdx_header(py_file)] + + if not missing_files: + print('✓ All Python files have valid SPDX headers.') + return 0 + + print('✗ The following files are missing SPDX headers:') + for py_file in missing_files: + print(f' - {py_file.as_posix()}') + print(f'\nFound {len(missing_files)} files without SPDX headers.') + return 1 + + +def build_parser() -> argparse.ArgumentParser: + """Build the CLI argument parser.""" + parser = argparse.ArgumentParser( + description='Add, remove, or check SPDX headers in Python files.', + ) + subparsers = parser.add_subparsers(dest='command', required=True) + + for command_name in ('check', 'remove', 'add'): + command_parser = subparsers.add_parser(command_name) + command_parser.add_argument( + 'paths', + nargs='+', + help='Relative paths to scan (e.g. src tests)', + ) + command_parser.add_argument( + '--exclude', + nargs='*', + default=[], + help='Exclude paths, glob patterns, or pyproject dotted keys.', + ) + command_parser.add_argument( + '--exclude-from-pyproject-toml', + help='Read exclude patterns from a dotted key in pyproject.toml.', + ) + + return parser + + +def main(argv: Optional[list[str]] = None) -> int: + """Run the SPDX header CLI.""" + parser = build_parser() + args = parser.parse_args(argv) + + repo_path = Path('.').resolve() + repo_root = find_repository_root(repo_path).resolve() + exclude_patterns = get_exclude_patterns( + repo_path, + args.exclude, + args.exclude_from_pyproject_toml, + ) + files = iter_python_files( + args.paths, + repo_root=repo_root, + exclude_patterns=exclude_patterns, + parser=parser, + ) + + if args.command == 'check': + return run_check(files) + + if args.command == 'remove': + return run_remove(files) + + license_key = get_project_license(repo_path) + copyright_holder = get_copyright_holder(repo_path) + org_url = get_org_url(repo_path) + return run_add( + files, + license_key=license_key, + copyright_holder=copyright_holder, + org_url=org_url, + ) + + +if __name__ == '__main__': + raise SystemExit(main()) diff --git a/tools/param_consistency.py b/tools/param_consistency.py new file mode 100644 index 00000000..cd63989c --- /dev/null +++ b/tools/param_consistency.py @@ -0,0 +1,677 @@ +"""Check and fix consistency between Parameter/Descriptor +definitions and their public property docstrings and type +annotations. + +Usage:: + + python param_consistency.py --check + python param_consistency.py --fix + python param_consistency.py src/mypackage/ --check + +Template (see architecture.md §9.8 for the full spec) +------------------------------------------------------ +Given ``description='Length of the a axis of the unit +cell.'``, ``units='Å'``, and type ``Parameter``: + +Writable:: + + @property + def length_a(self) -> Parameter: + \"""Length of the a axis of the unit cell (Å). + + Reading this property returns the underlying + ``Parameter`` object. Assigning to it updates + the parameter value. + \""" + return self._length_a + + @length_a.setter + def length_a(self, value: float) -> None: + self._length_a.value = value + +Read-only:: + + @property + def length_a(self) -> Parameter: + \"""Length of the a axis of the unit cell (Å). + + Reading this property returns the underlying + ``Parameter`` object. + \""" + return self._length_a + +Rules: + +- ``{desc}`` = description without trailing period + (single source of truth). +- ``{units}`` = units string; omit ``({units})`` when + absent or empty. +- Getter summary: ``{desc} ({units}).`` or ``{desc}.`` +- Getter body mentions the descriptor class and, for + writable properties, notes that assignment updates + the value. +- Setter has **no** docstring. +- Getter return annotation: the descriptor class name. +- Setter value annotation: ``float`` for numeric, + ``str`` for string. +- Setter return annotation: ``None``. + +Exit code 0 when all checks pass (or fix succeeds), +1 otherwise. +""" + +from __future__ import annotations + +import argparse +import ast +import sys +from dataclasses import dataclass +from dataclasses import field +from pathlib import Path + +# --------------------------------------------------------- +# Constants +# --------------------------------------------------------- + +_SRC_ROOT = ( + Path(__file__).resolve().parents[1] + / 'src' + / 'easydiffraction' +) + +_DESCRIPTOR_TYPES = frozenset( + {'Parameter', 'NumericDescriptor', 'StringDescriptor'} +) + +# Canonical setter value annotation per descriptor family. +_SETTER_ANN: dict[str, str] = { + 'Parameter': 'float', + 'NumericDescriptor': 'float', + 'StringDescriptor': 'str', +} + + +# --------------------------------------------------------- +# Data structures +# --------------------------------------------------------- + + +@dataclass +class DescriptorInfo: + """Descriptor definition from ``__init__``.""" + + attr_name: str # e.g. '_length_a' + prop_name: str # e.g. 'length_a' + type_name: str # 'Parameter' | … + description: str # e.g. 'Length of …' + units: str | None # e.g. 'Å', or None + + +@dataclass +class PropertyInfo: + """Property getter / setter AST nodes.""" + + name: str + getter: ast.FunctionDef + setter: ast.FunctionDef | None = None + + +@dataclass +class Edit: + """A source-level edit. + + Replace ``lines[start:end]`` with *new_text*. + When ``start == end`` the edit is an insertion + before that line. + """ + + start: int # 0-based inclusive + end: int # 0-based exclusive + new_text: str + + +@dataclass +class FileResult: + """Analysis result for one source file.""" + + path: Path + issues: list[str] = field(default_factory=list) + edits: list[Edit] = field(default_factory=list) + + +# --------------------------------------------------------- +# Template helpers +# --------------------------------------------------------- + + +def _strip_dot(s: str) -> str: + """Remove trailing period and whitespace.""" + s = s.rstrip() + if s.endswith('.'): + s = s[:-1].rstrip() + return s + + +def _getter_docstring( + desc: str, + units: str | None, + type_name: str, + has_setter: bool, + indent: str, +) -> str: + """Build the expected getter docstring.""" + d = _strip_dot(desc) + summary = f'{d} ({units}).' if units else f'{d}.' + + if has_setter: + body = ( + f'Reading this property returns the underlying ' + f'``{type_name}`` object. ' + f'Assigning to it updates the parameter value.' + ) + else: + body = ( + f'Reading this property returns the underlying ' + f'``{type_name}`` object.' + ) + + return ( + f'{indent}"""{summary}\n' + f'\n' + f'{indent}{body}\n' + f'{indent}"""\n' + ) + + +def _normalize(text: str) -> str: + """Collapse whitespace for comparison.""" + return ' '.join(text.split()).lower() + + +# --------------------------------------------------------- +# AST helpers +# --------------------------------------------------------- + + +def _call_name(node: ast.Call) -> str | None: + """Return the simple name of a Call's func.""" + if isinstance(node.func, ast.Name): + return node.func.id + if isinstance(node.func, ast.Attribute): + return node.func.attr + return None + + +def _kwarg_str( + call: ast.Call, + name: str, +) -> str | None: + """Extract a string keyword argument.""" + for kw in call.keywords: + if ( + kw.arg == name + and isinstance(kw.value, ast.Constant) + and isinstance(kw.value.value, str) + ): + return kw.value.value + return None + + +def _ann_str(ann: ast.expr | None) -> str | None: + """Return annotation as a source string.""" + if ann is None: + return None + if isinstance(ann, ast.Name): + return ann.id + if isinstance(ann, ast.Constant) and isinstance( + ann.value, str + ): + return ann.value # forward reference + return ast.unparse(ann) + + +def _body_indent( + func: ast.FunctionDef, + lines: list[str], +) -> str: + """Compute the indentation for the body.""" + def_line = lines[func.lineno - 1] + return ' ' * ( + len(def_line) - len(def_line.lstrip()) + 4 + ) + + +def _def_line_range( + func: ast.FunctionDef, + lines: list[str], +) -> tuple[int, int]: + """Return 0-based [start, end) of the def.""" + start = func.lineno - 1 + for i in range(start, min(start + 10, len(lines))): + if lines[i].rstrip().endswith(':'): + return start, i + 1 + if func.body and i + 1 >= func.body[0].lineno: + break + return start, start + 1 + + +def _docstring_range( + func: ast.FunctionDef, +) -> tuple[str | None, int, int]: + """Return (text, start_0, end_exclusive_0).""" + if not func.body: + return None, -1, -1 + first = func.body[0] + if ( + isinstance(first, ast.Expr) + and isinstance(first.value, ast.Constant) + and isinstance(first.value.value, str) + ): + # end_lineno is 1-based inclusive + return ( + first.value.value, + first.lineno - 1, + first.end_lineno, + ) + return None, -1, -1 + + +# --------------------------------------------------------- +# Extraction +# --------------------------------------------------------- + + +def _extract_descriptors( + cls: ast.ClassDef, +) -> dict[str, DescriptorInfo]: + """Find self._xxx = DescriptorType(...) in init.""" + result: dict[str, DescriptorInfo] = {} + + init = next( + ( + n + for n in cls.body + if isinstance(n, ast.FunctionDef) + and n.name == '__init__' + ), + None, + ) + if init is None: + return result + + for stmt in ast.walk(init): + if ( + isinstance(stmt, ast.Assign) + and len(stmt.targets) == 1 + ): + target = stmt.targets[0] + value = stmt.value + elif ( + isinstance(stmt, ast.AnnAssign) + and stmt.value is not None + ): + target = stmt.target + value = stmt.value + else: + continue + + # Target must be self._xxx + if not ( + isinstance(target, ast.Attribute) + and isinstance(target.value, ast.Name) + and target.value.id == 'self' + and target.attr.startswith('_') + ): + continue + + if not isinstance(value, ast.Call): + continue + + name = _call_name(value) + if name not in _DESCRIPTOR_TYPES: + continue + + desc_str = _kwarg_str(value, 'description') + if not desc_str or not _strip_dot(desc_str): + continue + + units = _kwarg_str(value, 'units') or None + prop = target.attr.lstrip('_') + result[prop] = DescriptorInfo( + target.attr, prop, name, desc_str, units + ) + + return result + + +def _extract_properties( + cls: ast.ClassDef, +) -> dict[str, PropertyInfo]: + """Find property getters and setters.""" + result: dict[str, PropertyInfo] = {} + + for item in cls.body: + if not isinstance(item, ast.FunctionDef): + continue + for dec in item.decorator_list: + # @property + if ( + isinstance(dec, ast.Name) + and dec.id == 'property' + ): + result[item.name] = PropertyInfo( + item.name, item + ) + break + # @xxx.setter + if ( + isinstance(dec, ast.Attribute) + and dec.attr == 'setter' + and isinstance(dec.value, ast.Name) + and dec.value.id in result + ): + result[dec.value.id].setter = item + break + + return result + + +# --------------------------------------------------------- +# Analysis (shared by --check and --fix) +# --------------------------------------------------------- + + +def _analyze_file(path: Path) -> FileResult: + """Analyze one file, return issues and edits.""" + result = FileResult(path) + try: + source = path.read_text(encoding='utf-8') + except Exception: # noqa: BLE001 + return result + + lines = source.splitlines(keepends=True) + + try: + tree = ast.parse(source, filename=str(path)) + except SyntaxError: + return result + + for node in tree.body: + if not isinstance(node, ast.ClassDef): + continue + + descriptors = _extract_descriptors(node) + properties = _extract_properties(node) + + for prop_name, prop in properties.items(): + if prop_name not in descriptors: + continue + desc = descriptors[prop_name] + _analyze_property( + node.name, prop, desc, lines, result + ) + + return result + + +def _analyze_property( + cls_name: str, + prop: PropertyInfo, + desc: DescriptorInfo, + lines: list[str], + result: FileResult, +) -> None: + """Check one property against the template.""" + loc = f'{cls_name}.{prop.name}' + indent = _body_indent(prop.getter, lines) + has_setter = prop.setter is not None + + # --- Getter return annotation --- + actual_ret = _ann_str(prop.getter.returns) + expected_ret = desc.type_name + if actual_ret != expected_ret: + result.issues.append( + f'{loc}: getter annotation ' + f'-> {actual_ret} (expected {expected_ret})' + ) + ds, de = _def_line_range(prop.getter, lines) + def_indent = lines[ds][ + : len(lines[ds]) - len(lines[ds].lstrip()) + ] + new_def = ( + f'{def_indent}def {prop.name}' + f'(self) -> {expected_ret}:\n' + ) + result.edits.append(Edit(ds, de, new_def)) + + # --- Getter docstring --- + expected_doc = _getter_docstring( + desc.description, + desc.units, + desc.type_name, + has_setter, + indent, + ) + actual_doc_text, doc_s, doc_e = _docstring_range( + prop.getter + ) + + if actual_doc_text is None: + result.issues.append( + f'{loc}: getter missing docstring' + ) + _, def_end = _def_line_range(prop.getter, lines) + result.edits.append( + Edit(def_end, def_end, expected_doc) + ) + else: + actual_src = ''.join(lines[doc_s:doc_e]) + if _normalize(actual_src) != _normalize( + expected_doc + ): + result.issues.append( + f'{loc}: getter docstring ' + 'does not match template' + ) + result.edits.append( + Edit(doc_s, doc_e, expected_doc) + ) + + # --- Setter --- + if prop.setter is None: + return + + # Setter def-line annotations + setter_args = prop.setter.args.args + setter_param = ( + setter_args[1].arg + if len(setter_args) >= 2 + else 'value' + ) + expected_ann = _SETTER_ANN[desc.type_name] + + actual_val_ann = None + if ( + len(setter_args) >= 2 + and setter_args[1].annotation + ): + actual_val_ann = _ann_str( + setter_args[1].annotation + ) + + actual_ret_ann = _ann_str(prop.setter.returns) + + if ( + actual_val_ann != expected_ann + or actual_ret_ann != 'None' + ): + parts: list[str] = [] + if actual_val_ann != expected_ann: + parts.append( + f'value: {actual_val_ann} ' + f'(expected {expected_ann})' + ) + if actual_ret_ann != 'None': + parts.append( + f'return: {actual_ret_ann} ' + '(expected None)' + ) + result.issues.append( + f'{loc}: setter annotation ' + f'— {", ".join(parts)}' + ) + + ds, de = _def_line_range(prop.setter, lines) + def_indent = lines[ds][ + : len(lines[ds]) - len(lines[ds].lstrip()) + ] + new_def = ( + f'{def_indent}def {prop.name}' + f'(self, {setter_param}: ' + f'{expected_ann}) -> None:\n' + ) + result.edits.append(Edit(ds, de, new_def)) + + # Setter docstring — should not exist + setter_doc_text, sd_s, sd_e = _docstring_range( + prop.setter + ) + if setter_doc_text is not None: + result.issues.append( + f'{loc}: setter has docstring ' + '(should have none)' + ) + result.edits.append(Edit(sd_s, sd_e, '')) + + +# --------------------------------------------------------- +# Apply edits +# --------------------------------------------------------- + + +def _apply_edits( + lines: list[str], + edits: list[Edit], +) -> list[str]: + """Apply edits bottom-up to preserve line numbers.""" + sorted_edits = sorted( + edits, key=lambda e: e.start, reverse=True + ) + result = list(lines) + for edit in sorted_edits: + new_lines = edit.new_text.splitlines(keepends=True) + result[edit.start : edit.end] = new_lines + return result + + +# --------------------------------------------------------- +# Entry point +# --------------------------------------------------------- + + +def _collect_py_files(paths: list[str]) -> list[Path]: + """Resolve paths to a sorted list of .py files. + + Each entry can be a directory (recursively globbed) + or a single .py file. When *paths* is empty, + defaults to ``_SRC_ROOT``. + """ + if not paths: + return sorted(_SRC_ROOT.rglob('*.py')) + + result: list[Path] = [] + for raw in paths: + p = Path(raw).resolve() + if p.is_dir(): + result.extend(p.rglob('*.py')) + elif p.is_file() and p.suffix == '.py': + result.append(p) + return sorted(set(result)) + + +def main() -> int: + """Run param-consistency check or fix.""" + parser = argparse.ArgumentParser( + description=( + 'Parameter / property consistency: ' + 'docstrings and type hints.' + ), + ) + parser.add_argument( + 'paths', + nargs='*', + help=( + 'Directories or .py files to scan ' + '(default: src/easydiffraction/)' + ), + ) + group = parser.add_mutually_exclusive_group() + group.add_argument( + '--check', + action='store_true', + help='Validate consistency (default)', + ) + group.add_argument( + '--fix', + action='store_true', + help='Auto-fix docstrings and type hints', + ) + args = parser.parse_args() + + py_files = _collect_py_files(args.paths) + repo_root = Path(__file__).resolve().parents[1] + total_issues = 0 + total_fixed = 0 + files_touched = 0 + + for path in py_files: + result = _analyze_file(path) + if not result.issues: + continue + + try: + rel = path.relative_to(repo_root) + except ValueError: + rel = path + + if args.fix: + source_lines = path.read_text( + encoding='utf-8' + ).splitlines(keepends=True) + fixed_lines = _apply_edits( + source_lines, result.edits + ) + path.write_text( + ''.join(fixed_lines), encoding='utf-8' + ) + count = len(result.issues) + total_fixed += count + files_touched += 1 + print( + f'📝 {rel}: fixed {count} issue(s)' + ) + else: + for issue in result.issues: + print(f' ❌ {rel}: {issue}') + total_issues += len(result.issues) + + # Summary + print() + if args.fix: + print( + f'✅ Fixed {total_fixed} issue(s) ' + f'in {files_touched} file(s).' + ) + return 0 + if total_issues: + print( + f'❌ {total_issues} consistency ' + 'issue(s) found.' + ) + return 1 + print('✅ All properties match the template.') + return 0 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/tools/test_scripts.py b/tools/test_scripts.py index 327bafd2..2dcca083 100644 --- a/tools/test_scripts.py +++ b/tools/test_scripts.py @@ -1,3 +1,5 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause """Test runner for tutorial scripts in the 'tutorials' directory. This test discovers and executes all Python scripts located under the @@ -16,15 +18,13 @@ import pytest -# Mark this module as 'integration' so it's excluded by default -# (see pytest.ini) -pytestmark = pytest.mark.integration - _repo_root = Path(__file__).resolve().parents[1] _src_root = _repo_root / 'src' # Discover tutorial scripts, excluding temporary checkpoint files -TUTORIALS = [p for p in Path('tutorials').rglob('*.py') if '.ipynb_checkpoints' not in p.parts] +TUTORIALS = [ + p for p in Path('docs/docs/tutorials').rglob('*.py') if '.ipynb_checkpoints' not in p.parts +] @pytest.mark.parametrize('script_path', TUTORIALS) @@ -51,6 +51,7 @@ def test_script_runs(script_path: Path): env=env, capture_output=True, text=True, + encoding='utf-8', ) if result.returncode != 0: details = (result.stdout or '') + (result.stderr or '') diff --git a/tools/update_docs_assets.py b/tools/update_docs_assets.py new file mode 100644 index 00000000..b274e038 --- /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 = 'easydiffraction' + +# 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/tools/update_spdx.py b/tools/update_spdx.py deleted file mode 100644 index df9236af..00000000 --- a/tools/update_spdx.py +++ /dev/null @@ -1,109 +0,0 @@ -"""Update or insert SPDX headers in Python files. - -- Ensures SPDX-FileCopyrightText has the current year. -- Ensures SPDX-License-Identifier is set to BSD-3-Clause. -""" - -import datetime -import fnmatch -import re -from pathlib import Path - -CURRENT_YEAR = datetime.datetime.now().year -COPYRIGHT_TEXT = ( - f'# SPDX-FileCopyrightText: 2021-{CURRENT_YEAR} EasyDiffraction contributors ' - '' -) -LICENSE_TEXT = '# SPDX-License-Identifier: BSD-3-Clause' - -# Patterns to exclude from SPDX header updates (vendored code) -EXCLUDE_PATTERNS = [ - '*/_vendored/jupyter_dark_detect/*', -] - - -def should_exclude(file_path: Path) -> bool: - """Check if a file should be excluded from SPDX header updates.""" - path_str = str(file_path) - return any(fnmatch.fnmatch(path_str, pattern) for pattern in EXCLUDE_PATTERNS) - - -def update_spdx_header(file_path: Path): - # Use Path.open to satisfy lint rule PTH123. - with file_path.open('r', encoding='utf-8') as f: - original_lines = f.readlines() - - # Regexes for SPDX lines - copy_re = re.compile(r'^#\s*SPDX-FileCopyrightText:.*$') - lic_re = re.compile(r'^#\s*SPDX-License-Identifier:.*$') - - # 1) Preserve any leading shebang / coding cookie lines - prefix = [] - body_start = 0 - if original_lines: - # Shebang line like "#!/usr/bin/env python3" - if original_lines[0].startswith('#!'): - prefix.append(original_lines[0]) - body_start = 1 - # PEP 263 coding cookie on first or second line - # e.g. "# -*- coding: utf-8 -*-" or "# coding: utf-8" - for _ in range(2): # at most one more line to inspect - if body_start < len(original_lines): - line = original_lines[body_start] - if re.match(r'^#.*coding[:=]\s*[-\w.]+', line): - prefix.append(line) - body_start += 1 - else: - break - - # 2) Work on the remaining body - body = original_lines[body_start:] - - # Remove any existing SPDX lines anywhere in the body - body = [ln for ln in body if not (copy_re.match(ln) or lic_re.match(ln))] - - # Strip leading blank lines in the body so header is tight - while body and not body[0].strip(): - body.pop(0) - - # 3) Build canonical SPDX block: two lines + exactly one blank - spdx_block = [ - COPYRIGHT_TEXT + '\n', - LICENSE_TEXT + '\n', - '\n', - ] - - # 4) New content: prefix + SPDX + body - new_lines = prefix + spdx_block + body - - # 5) Normalize: collapse any extra blank lines immediately after - # LICENSE to exactly one. This keeps the script idempotent. - # Find the index of LICENSE we just inserted (prefix may be 0, 1, - # or 2 lines) - lic_idx = len(prefix) + 1 # spdx_block[1] is the license line - # Ensure exactly one blank line after LICENSE - # Remove all blank lines after lic_idx, then insert a single blank. - j = lic_idx + 1 - # Remove any number of blank lines following - while j < len(new_lines) and not new_lines[j].strip(): - new_lines.pop(j) - # Insert exactly one blank line at this position - new_lines.insert(j, '\n') - - with file_path.open('w', encoding='utf-8') as f: - f.writelines(new_lines) - - -def main(): - """Recursively update or insert SPDX headers in all Python files - under the 'src' and 'tests' directories. - """ - for base_dir in ('src', 'tests'): - for py_file in Path(base_dir).rglob('*.py'): - if should_exclude(py_file): - continue - update_spdx_header(py_file) - - -if __name__ == '__main__': - main()