diff --git a/.codecov.yml b/.codecov.yml new file mode 100644 index 000000000..d001c41da --- /dev/null +++ b/.codecov.yml @@ -0,0 +1,26 @@ +# Codecov configuration +# https://docs.codecov.com/docs/codecov-yaml + +github_checks: + annotations: true + +comment: + layout: 'reach, diff, flags, files' + behavior: default + require_changes: false + require_base: false + require_head: true + +coverage: + status: + project: + default: + target: auto + threshold: 1% + # Make project coverage informational (won't block PR) + informational: true + patch: + default: + target: auto + # Require patch coverage but with threshold + threshold: 1% diff --git a/.copier-answers.yml b/.copier-answers.yml index 929409549..009e6acc4 100644 --- a/.copier-answers.yml +++ b/.copier-answers.yml @@ -1,6 +1,6 @@ # WARNING: Do not edit this file manually. # Any changes will be overwritten by Copier. -_commit: v0.11.0-7-gb5113cf +_commit: v0.11.4 _src_path: gh:easyscience/templates app_docs_url: https://easyscience.github.io/diffraction-app app_doi: 10.5281/zenodo.18163581 diff --git a/.github/actions/setup-pixi/action.yml b/.github/actions/setup-pixi/action.yml index ec7d7ba7b..dc3232c61 100644 --- a/.github/actions/setup-pixi/action.yml +++ b/.github/actions/setup-pixi/action.yml @@ -33,7 +33,7 @@ inputs: runs: using: 'composite' steps: - - uses: prefix-dev/setup-pixi@v0.9.4 + - uses: prefix-dev/setup-pixi@v0.9.5 with: environments: ${{ inputs.environments }} activate-environment: ${{ inputs.activate-environment }} diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 18b01c478..46a066d2b 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -34,7 +34,10 @@ - One class per file when substantial; group small related classes. - No `**kwargs` — use explicit keyword arguments. - No string-based dispatch (e.g. `getattr(self, f'_{name}')`); write - named methods (`_set_sample_form`, `_set_beam_mode`). + named methods (`_set_sample_form`, `_set_beam_mode`). Narrow framework + metadata lookups are allowed when the attribute name is a class-level + declaration, is not user input, and is validated in one central place; + for example, `CategoryItem._category_entry_name`. - Public attrs are either editable (getter+setter property) or read-only (getter only). For internal mutation of read-only props, use a private `_set_` method, not a public setter. @@ -77,7 +80,7 @@ ## Testing - Every new module, class, or bug fix ships with tests. See - `docs/architecture/architecture.md` §10 for the full strategy. + `docs/dev/adrs/accepted/test-strategy.md` for the full strategy. - Unit tests mirror the source tree: `src/easydiffraction//.py` → `tests/unit/easydiffraction//test_.py`. Verify with @@ -102,8 +105,11 @@ - Before any structural/design change (new categories, factories, switchable-category wiring, datablocks, CIF serialisation), read - `docs/architecture/architecture.md` and follow documented patterns. - Localised bug fixes or test updates need only this file. + `docs/dev/adrs/index.md` and the relevant accepted ADRs. Localised bug + fixes or test updates need only this file. +- Development documentation lives under `docs/dev/`. Use + `docs/dev/adrs/index.md` as the architecture and decision navigation + surface; there is no separate `architecture.md` source of truth. - Project is in beta: no legacy shims, no deprecation warnings — update tests and tutorials to the current API. - Minimal diffs; don't reformat working code. Fix only what's asked; @@ -112,7 +118,10 @@ resolves them. - Never remove or replace existing functionality without explicit confirmation — highlight every removal and wait for approval. -- When renaming, grep the entire project (code, tests, tutorials, docs). +- When renaming or auditing usages, search the entire project (code, + tests, tutorials, docs). Use `git grep -n` because all contributors + have Git; do not assume `rg` is installed. If `git grep` is + unavailable, fall back to `find ... -type f` plus `grep -n`. - Each change is atomic and single-commit-sized: make one change, suggest the commit message, then stop and wait for confirmation. - When in doubt, ask. @@ -137,35 +146,51 @@ Non-trivial changes use a two-phase workflow: -- **Phase 1 — Implementation.** Code, docs, and architecture updates - only. Do not create or run tests unless the user explicitly asks. When - done, present for review and iterate until approved. +- **Phase 1 — Implementation.** Code and docs updates only. Update ADRs + when the change affects architecture or documented decisions. Do not + create or run tests unless the user explicitly asks. When done, + present for review and iterate until approved. - **Phase 2 — Verification.** Add/update tests, then run `pixi run fix`, `pixi run check`, `pixi run unit-tests`, `pixi run integration-tests`, `pixi run script-tests`. Notes: -- `pixi run fix` regenerates `docs/architecture/package-structure-*.md` - automatically — never edit those by hand. Don't review auto-fixes; - accept and move on. Then `pixi run check` until clean. +- `pixi run fix` regenerates `docs/dev/package-structure/full.md` and + `docs/dev/package-structure/short.md` automatically — never edit those + by hand. Don't review auto-fixes; accept and move on. Then + `pixi run check` until clean. +- When a check command needs saved output for analysis, capture the log + and preserve the command exit code with a zsh-safe variable name: + `pixi run check > /tmp/easydiffraction-check.log 2>&1; check_exit_code=$?; tail -n 200 /tmp/easydiffraction-check.log; exit $check_exit_code`. + Never assign to `status` in zsh; it is readonly. Use task-specific + names such as `check_exit_code`, `unit_tests_exit_code`, or + `script_tests_exit_code`. - Open issues / design questions / planned improvements live in - `docs/architecture/issues_open.md` (priority-ordered). On resolution, - move to `docs/architecture/issues_closed.md` and update - `architecture.md` if affected. + `docs/dev/issues/open.md` (priority-ordered). On resolution, move to + `docs/dev/issues/closed.md` and update the relevant ADR or + `docs/dev/adrs/index.md` if affected. ### Planning When asked to create a plan: +- Start the plan by referencing this file: + `.github/copilot-instructions.md`. State any deliberate exception to + these instructions in the plan itself. - First gather enough repository context to make the plan concrete. Ask all ambiguous or unclear questions in one concise batch; record unresolved questions in the plan if the user wants it saved before answering them. -- Save plans as `docs/dev/plan_.md` (lowercase, - dash-separated, e.g. `plan_background-refactor.md`). Use the same - `` for the implementation branch - (`feature/`). Do not push the branch unless asked. +- Save plans as `docs/dev/plans/.md` (lowercase, + dash-separated, e.g. `docs/dev/plans/background-refactor.md`). When a + plan implements one ADR, use the same slug as the ADR file; for + example, `docs/dev/adrs/suggestions/foo.md` maps to + `docs/dev/plans/foo.md`. If a plan has no corresponding ADR or spans + multiple ADRs, choose a concise feature slug and list all related ADRs + in the plan. Use the same `` for the implementation + branch (`feature/`). Do not push the branch unless + asked. - Include a status checklist with `[ ]` items; mark `[x]` as completed during implementation. - Apply the two-phase workflow (Phase 1 implementation, Phase 2 @@ -185,6 +210,12 @@ When asked to create a plan: files likely to change, decisions already made, open questions, verification commands for Phase 2, and a short suggested commit message or branch name when useful. +- Verification commands in plans must include the zsh-safe log-capture + pattern from **Workflow** whenever saved output is needed for later + analysis. +- Before saving a plan, verify that referenced files, directories, + scripts, and task names exist locally when that is practical. If a + referenced tool is optional or missing, include an available fallback. - End every plan with a "Suggested Pull Request" section containing a short PR title and a brief end-user-oriented description. Keep this section non-technical enough for scientists and other users to diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 95c190c63..d96e5b87d 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -5,8 +5,6 @@ on: push: branches: - develop - # 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 diff --git a/.github/workflows/dashboard.yml b/.github/workflows/dashboard.yml index 9d1f2b0bd..573e30868 100644 --- a/.github/workflows/dashboard.yml +++ b/.github/workflows/dashboard.yml @@ -32,9 +32,13 @@ jobs: - name: Set up pixi uses: ./.github/actions/setup-pixi + # Install badgery into the active Pixi environment without modifying + # pixi.toml/pixi.lock. Using `pixi add` here re-solves the whole project + # and rebuilds the local editable package, which can cause intermittent + # Linux CI failures (`Text file busy`, os error 26). - name: Install badgery shell: bash - run: pixi add --pypi --git https://github.com/enhantica/badgery badgery + run: pixi run python -m pip install git+https://github.com/enhantica/badgery - name: Run docstring coverage and code complexity/maintainability checks run: | diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 117416a3f..ea6bdad11 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -27,12 +27,16 @@ on: # Allows you to run this workflow manually from the Actions tab workflow_dispatch: -# Allow only one concurrent deployment to gh-pages at a time. -# All docs workflows share the same concurrency group to prevent race conditions -# when multiple branches/tags trigger simultaneous deployments. +# - Non-tagged pushes and pull requests all use `docs-dev` group, so +# they cancel each other. +# - Tagged pushes use their own group like docs-v1.2.3, so they do not +# cancel non-tagged runs, and non-tagged runs do not cancel them. concurrency: - group: docs-gh-pages-deploy - cancel-in-progress: false + group: >- + ${{ startsWith(github.ref, 'refs/tags/v') + && format('docs-{0}', github.ref_name) + || 'docs-dev' }} + cancel-in-progress: true # Set the environment variables to be used in all jobs defined in this workflow env: @@ -115,7 +119,7 @@ jobs: # Uses multiple cores for parallel execution to speed up the process. - name: Run notebooks # if: false # Temporarily disabled to speed up the docs build - run: pixi run notebook-exec + run: pixi run notebook-exec-ci # Build the static files for the documentation site for local inspection # Input: docs/ directory containing the Markdown files diff --git a/.github/workflows/issues-labels.yml b/.github/workflows/issues-labels.yml index 31a69ad20..4fbf4fab1 100644 --- a/.github/workflows/issues-labels.yml +++ b/.github/workflows/issues-labels.yml @@ -1,6 +1,5 @@ -# 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. +# Verifies if the current issue has at least one `[scope]` label and one +# `[priority]` label. If either is missing, the workflow adds a reminder label. name: Issue labels check @@ -13,8 +12,6 @@ permissions: jobs: check-labels: - if: github.actor != 'easyscience[bot]' - runs-on: ubuntu-latest concurrency: @@ -22,30 +19,22 @@ jobs: cancel-in-progress: true steps: - - name: Checkout repository - uses: actions/checkout@v6 - - - name: Setup easyscience[bot] - id: bot - uses: ./.github/actions/setup-easyscience-bot + - name: Ensure [scope] label + uses: Rindrics/expect-label-prefix@v1.2.1 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 + repository_full_name: ${{ github.repository }} + token: ${{ github.token }} + label_prefix: '[scope]' + label_separator: ' ' + add_label: 'true' + default_label: '[scope] ⚠️ label needed' + + - name: Ensure [priority] label + uses: Rindrics/expect-label-prefix@v1.2.1 with: - secret: ${{ steps.bot.outputs.token }} - prefix: '[priority]' - labelSeparator: ' ' - addLabel: true - defaultLabel: '[priority] ⚠️ label needed' + repository_full_name: ${{ github.repository }} + token: ${{ github.token }} + label_prefix: '[priority]' + label_separator: ' ' + add_label: 'true' + default_label: '[priority] ⚠️ label needed' diff --git a/.github/workflows/pr-labels.yml b/.github/workflows/pr-labels.yml index 642cd3184..fcb8a78cb 100644 --- a/.github/workflows/pr-labels.yml +++ b/.github/workflows/pr-labels.yml @@ -1,14 +1,9 @@ # Verifies if a pull request has at least one label from a set of valid # labels before it can be merged. # -# NOTE: -# This workflow may be triggered twice in quick succession when a PR is -# created: -# 1) `opened` — when the pull request is initially created -# 2) `labeled` — if labels are added immediately after creation -# (e.g. by manual labeling, another workflow, or GitHub App). -# -# These are separate GitHub events, so two workflow runs can be started. +# The label validation is delegated to `mheap/github-action-required-labels`, +# which checks the current PR labels via the GitHub API and can add or update a +# PR comment when the required label set is missing. name: PR labels check @@ -17,40 +12,34 @@ on: types: [opened, labeled, unlabeled, synchronize] permissions: - pull-requests: read + issues: write + pull-requests: write jobs: check-labels: runs-on: ubuntu-latest steps: - - name: Check for valid labels - run: | - PR_LABELS=$(echo '${{ toJson(github.event.pull_request.labels.*.name) }}' | jq -r '.[]') - - echo "Current PR labels: $PR_LABELS" - VALID_LABELS=( - "[bot] release" - "[scope] bug" - "[scope] documentation" - "[scope] enhancement" - "[scope] maintenance" - "[scope] significant" - ) - - found=false - for label in "${VALID_LABELS[@]}"; do - if echo "$PR_LABELS" | grep -Fxq "$label"; then - echo "✅ Found valid label: $label" - found=true - break - fi - done - - if [ "$found" = false ]; then - echo "ERROR: PR must have at least one of the following labels:" - for label in "${VALID_LABELS[@]}"; do - echo " - $label" - done - exit 1 - fi + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Setup easyscience[bot] + id: bot + uses: ./.github/actions/setup-easyscience-bot + with: + app-id: ${{ vars.EASYSCIENCE_APP_ID }} + private-key: ${{ secrets.EASYSCIENCE_APP_KEY }} + + - uses: mheap/github-action-required-labels@v5 + with: + token: ${{ steps.bot.outputs.token }} + add_comment: true + mode: minimum + count: 1 + labels: | + [bot] release + [scope] bug + [scope] documentation + [scope] enhancement + [scope] maintenance + [scope] significant diff --git a/.github/workflows/release-pr.yml b/.github/workflows/release-pr.yml index 0e716c27e..3fa073b90 100644 --- a/.github/workflows/release-pr.yml +++ b/.github/workflows/release-pr.yml @@ -48,7 +48,7 @@ jobs: gh pr create \ --base ${{ env.DEFAULT_BRANCH }} \ --head ${{ env.SOURCE_BRANCH }} \ - --title "Release: merge ${{ env.SOURCE_BRANCH }} into ${{ env.DEFAULT_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 }}\`. diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d0298f95b..86bfa6063 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -248,10 +248,10 @@ jobs: exit 1 fi - whl_url="file://$(python -c 'import os,sys; print(os.path.abspath(sys.argv[1]))' "${whl_path[0]}")" + whl_abs_path="$(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 "Adding easydiffraction from: $whl_abs_path" + pixi add --pypi "easydiffraction[dev] @ ${whl_abs_path}" echo "Exiting pixi project directory" cd .. diff --git a/.gitignore b/.gitignore index 28df1a0ac..ec2e0d270 100644 --- a/.gitignore +++ b/.gitignore @@ -49,4 +49,5 @@ CMakeLists.txt.user* # Used to fetch tutorials data during their runtime. Need to have '/' at # the beginning to avoid ignoring 'data' module in the src/. /data/ +/projects/ /tmp/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e8dc420f2..9e5b93cff 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -249,6 +249,12 @@ or to run only Python linting checks: pixi run py-lint-check ``` +To add missing license headers: + +```bash +pixi run spdx-add +``` + Some formatting issues can be fixed automatically: ```bash diff --git a/codecov.yml b/codecov.yml deleted file mode 100644 index f62b13ab4..000000000 --- a/codecov.yml +++ /dev/null @@ -1,13 +0,0 @@ -# Codecov configuration -# https://docs.codecov.com/docs/codecovyml-reference - -coverage: - status: - project: - default: - # Make project coverage informational (won't block PR) - informational: true - patch: - default: - # Require patch coverage but with threshold - threshold: 1% diff --git a/docs/dev/adp_implementation.md b/docs/dev/adp_implementation.md deleted file mode 100644 index 4d66e0b74..000000000 --- a/docs/dev/adp_implementation.md +++ /dev/null @@ -1,321 +0,0 @@ -# ADP Implementation Plan - -**Date:** 2026-04-12 **Status:** Design approved — awaiting -implementation - ---- - -## 1. Goal - -Extend ADP (Atomic Displacement Parameter) support from the current -Biso-only implementation to all four CIF-standard types: **Biso**, -**Uiso**, **Bani**, **Uani**. The design uses type-neutral parameter -names (`adp_iso`, `adp_11`…`adp_23`) so that switching ADP type is a -one-line operation on `adp_type` without creating or destroying -parameters. - ---- - -## 2. Design Summary - -### 2.1 Two Sibling Collections on Structure - -Following CIF conventions (`_atom_site` + `_atom_site_aniso` are -separate loops), the structure owns two sibling collections: - -``` -Structure -├── cell (CategoryItem) -├── space_group (CategoryItem) -├── atom_sites (CategoryCollection of AtomSite) -└── atom_site_aniso (CategoryCollection of AtomSiteAniso) -``` - -Every atom always has an entry in both collections, kept in sync by -`Structure._update_categories()`. This eliminates conditional existence -checks throughout the codebase. - -### 2.2 Type-Neutral Parameter Names - -Parameters on `AtomSite` and `AtomSiteAniso` use type-neutral names -whose physical meaning is determined by `atom_site.adp_type`: - -| Parameter | Location | CIF names (order depends on `adp_type`) | -| ---------- | --------------- | -------------------------------------------------------- | -| `adp_type` | `AtomSite` | `_atom_site.adp_type` | -| `adp_iso` | `AtomSite` | `_atom_site.B_iso_or_equiv`, `_atom_site.U_iso_or_equiv` | -| `adp_11` | `AtomSiteAniso` | `_atom_site_aniso.B_11`, `_atom_site_aniso.U_11` | -| `adp_22` | `AtomSiteAniso` | `_atom_site_aniso.B_22`, `_atom_site_aniso.U_22` | -| `adp_33` | `AtomSiteAniso` | `_atom_site_aniso.B_33`, `_atom_site_aniso.U_33` | -| `adp_12` | `AtomSiteAniso` | `_atom_site_aniso.B_12`, `_atom_site_aniso.U_12` | -| `adp_13` | `AtomSiteAniso` | `_atom_site_aniso.B_13`, `_atom_site_aniso.U_13` | -| `adp_23` | `AtomSiteAniso` | `_atom_site_aniso.B_23`, `_atom_site_aniso.U_23` | - -### 2.3 Dual CIF Names — Static Read, Reordered Write - -Each parameter's `CifHandler` carries both CIF name variants. The -existing infrastructure handles this: - -- **Reading (deserialization):** `param_from_cif()` and - `category_collection_from_cif()` iterate `_cif_handler.names` and stop - at the first match. A CIF file with `_atom_site.U_iso_or_equiv` is - read correctly regardless of name order. -- **Writing (serialization):** `param_to_cif()` and - `category_collection_to_cif()` always use `names[0]`. The `adp_type` - setter reorders the `names` list so that the correct CIF tag is - emitted first. - -Example — when `adp_type` changes from `'Biso'` to `'Uiso'`: - -```python -# adp_type setter reorders CIF names on adp_iso -self._adp_iso._cif_handler._names = [ - '_atom_site.U_iso_or_equiv', - '_atom_site.B_iso_or_equiv', -] -``` - -No new core CIF infrastructure is needed. - -### 2.4 ADP Type Enum - -```python -class AdpTypeEnum(str, Enum): - BISO = 'Biso' - UISO = 'Uiso' - BANI = 'Bani' - UANI = 'Uani' -``` - -`adp_type` on `AtomSite` is a `StringDescriptor` validated by -`MembershipValidator(allowed=...)` using all four enum values. - -### 2.5 Auto-Conversion on Type Switch - -Setting `adp_type` triggers value conversion. The physics: - -- **B ↔ U (isotropic):** `B = 8π²U` -- **Iso → Ani:** diagonal `adp_11 = adp_22 = adp_33 = adp_iso`, - off-diagonal `adp_12 = adp_13 = adp_23 = 0` -- **Ani → Iso:** `adp_iso = (adp_11 + adp_22 + adp_33) / 3` - -The `adp_type` setter on `AtomSite` performs the conversion and updates -both the isotropic parameter and the aniso parameters on the sibling -collection. - -### 2.6 Collection Sync via `_update_categories()` - -`Structure._update_categories()` reconciles the two collections: - -| Event | Sync action | -| ---------------------------------- | --------------------------------------------------------- | -| Atom added to `atom_sites` | Create matching `AtomSiteAniso` entry with defaults (0.0) | -| Atom removed from `atom_sites` | Remove matching `AtomSiteAniso` entry | -| Atom label renamed in `atom_sites` | Rekey the matching `AtomSiteAniso` entry | - -The sync is driven by the dirty flag — any parameter or collection -change sets `_need_categories_update = True`, and the next -serialization, plot, or fit call triggers `_update_categories()`. - -### 2.7 Inactive Aniso Values - -When `adp_type` is `'Biso'` or `'Uiso'`, the aniso parameters exist with -value `0.0` but are not read by calculators. This avoids introducing -`None` into the `float`-based `Parameter` system. - ---- - -## 3. User-Facing API - -### 3.1 Parameter Access Pattern - -Parameters are accessed via the standard two-level pattern: - -```python -# CategoryItem.Parameter -structure.cell.length_a = 3.88 - -# CategoryCollection[item_id].Parameter -structure.atom_sites['Si'].adp_type = 'Biso' -structure.atom_sites['Si'].adp_iso = 0.47 -structure.atom_site_aniso['Si'].adp_11 = 0.05 -``` - -### 3.2 Switching ADP Type - -```python -# Switch from Biso to Uiso — auto-converts value -structure.atom_sites['Si'].adp_type = 'Uiso' -# adp_iso now holds U_iso value (B / 8π²) - -# Switch to anisotropic — seeds diagonal from iso -structure.atom_sites['Si'].adp_type = 'Uani' -# atom_site_aniso['Si'].adp_11/22/33 seeded from adp_iso -# adp_iso recalculated as mean of diagonal - -# Switch back to isotropic — collapses tensor to scalar -structure.atom_sites['Si'].adp_type = 'Biso' -# adp_iso = mean(adp_11, adp_22, adp_33), converted B→U if needed -``` - -### 3.3 Creating Atoms - -```python -# adp_iso replaces the old b_iso keyword -structure.atom_sites.create( - label='Si', - type_symbol='Si', - fract_x=0.0, - fract_y=0.0, - fract_z=0.0, - adp_type='Biso', - adp_iso=0.47, -) -# atom_site_aniso['Si'] is auto-created by _update_categories() -``` - -### 3.4 CIF Output - -```cif -loop_ -_atom_site.label -_atom_site.type_symbol -_atom_site.fract_x -_atom_site.fract_y -_atom_site.fract_z -_atom_site.adp_type -_atom_site.B_iso_or_equiv -_atom_site.occupancy -Si Si 0.00000000 0.00000000 0.00000000 Biso 0.47000000 1.00000000 - -loop_ -_atom_site_aniso.label -_atom_site_aniso.B_11 -_atom_site_aniso.B_22 -_atom_site_aniso.B_33 -_atom_site_aniso.B_12 -_atom_site_aniso.B_13 -_atom_site_aniso.B_23 -Si 0.00000000 0.00000000 0.00000000 0.00000000 0.00000000 0.00000000 -``` - -When `adp_type = 'Uiso'`, the CIF tag becomes -`_atom_site.U_iso_or_equiv` and the aniso loop uses -`_atom_site_aniso.U_*` tags. - ---- - -## 4. Implementation Phases - -### Phase 1: Rename `b_iso` → `adp_iso` and Expand `adp_type` - -**Files to modify:** - -1. **`atom_sites/default.py`** — `AtomSite`: - - Rename `_b_iso` Parameter to `_adp_iso` with dual CIF names - `['_atom_site.B_iso_or_equiv', '_atom_site.U_iso_or_equiv']`. - - Expand `_adp_type` validator to accept all four values from - `AdpTypeEnum`. - - Add `adp_type` setter logic: reorder CIF names on `_adp_iso`, - perform B↔U conversion. - - Rename property `b_iso` → `adp_iso`. - -2. **`atom_sites/enums.py`** (new) — `AdpTypeEnum` with BISO, UISO, - BANI, UANI members plus `default()` and `description()` methods. - -3. **Calculator bridges** — update `cryspy.py` and `crysfml.py` to read - `atom.adp_iso.value` instead of `atom.b_iso.value`, and pass the - correct type based on `atom.adp_type.value`. - -4. **CIF data files** — update all `.cif` files in `data/` that - reference `b_iso`. - -5. **Tutorials** — update all `*.py` scripts that use `b_iso`. - -6. **Tests** — update all unit/functional/integration tests. - -### Phase 2: Add `AtomSiteAniso` Sibling Collection - -**Files to create:** - -1. **`atom_site_aniso/`** package under - `datablocks/structure/categories/`: - - `__init__.py` — imports `AtomSiteAniso` and - `AtomSiteAnisoCollection`. - - `default.py` — `AtomSiteAniso` (CategoryItem) with `label` - (StringDescriptor) and six Parameters (`adp_11`…`adp_23`) each with - dual CIF names. `AtomSiteAnisoCollection` (CategoryCollection). - - `factory.py` — `AtomSiteAnisoFactory`. - - `enums.py` — if needed (likely shared with Phase 1 enum). - -2. **`structure/item/base.py`** — add `_atom_site_aniso` attribute and - `atom_site_aniso` read-only property on Structure. - -3. **`structure/item/base.py`** — override `_update_categories()` to - reconcile `atom_sites` and `atom_site_aniso` collections (add - missing, remove stale, rekey on label change). - -**Files to modify:** - -4. **`atom_sites/default.py`** — `adp_type` setter also reorders CIF - names on all six aniso parameters (accessed via parent structure's - `atom_site_aniso` collection). - -5. **Calculator bridges** — when `adp_type` is `'Bani'` or `'Uani'`, - read from `atom_site_aniso[label]` instead of `adp_iso`. - -6. **CIF serialization** — works automatically (Structure's `as_cif` - emits all CategoryCollections found in `vars(self)`). - -### Phase 3: ADP Symmetry Constraints - -1. **`crystallography.py`** — add symmetry constraint functions for - anisotropic ADPs based on space group and Wyckoff position. - -2. **`AtomSites._update()`** — call aniso symmetry constraints in - addition to coordinate constraints. - ---- - -## 5. Breaking Changes - -| Change | Scope | Migration | -| ----------------------------------------- | --------------------------------- | ---------------------------------- | -| `b_iso` → `adp_iso` | All code referencing `atom.b_iso` | Mechanical rename (greppable) | -| `b_iso=0.5` → `adp_iso=0.5` in `create()` | Tutorials, tests, user scripts | Mechanical rename | -| New `atom_site_aniso` on Structure | Structure API surface grows | Additive — no existing code breaks | - -The project is in beta, so no deprecation path is needed. - ---- - -## 6. Design Decisions - -### Why type-neutral names (`adp_iso`) instead of type-specific (`b_iso`, `u_iso`)? - -With type-specific names, switching ADP type would require -creating/destroying parameters, migrating constraints and free flags, -and updating every reference. Type-neutral names make switching a -one-liner on `adp_type` — the parameter object stays the same, only its -value and CIF tag change. - -### Why always-present aniso collection instead of on-demand? - -Conditional existence of `atom_site_aniso` would require guards -everywhere: serialization, calculators, parameter tables, constraint -wiring, UI. Always-present with 0.0 defaults eliminates all those -branches. - -### Why sync via `_update_categories()` instead of coupled add/remove? - -Loose coupling: `AtomSites` and `AtomSiteAnisoCollection` don't know -about each other. `Structure` coordinates them at update time. This -follows the existing dirty-flag pattern and keeps categories -independent. - -### Why reorder `_cif_handler.names` instead of dynamic CIF handler? - -The existing serialization/deserialization pipeline uses `names[0]` for -writing and iterates all names for reading. Reordering the list is a -2-line operation in the `adp_type` setter with zero core infrastructure -changes. diff --git a/docs/dev/adrs/accepted/analysis-cif-fit-state.md b/docs/dev/adrs/accepted/analysis-cif-fit-state.md new file mode 100644 index 000000000..a38f0e7c7 --- /dev/null +++ b/docs/dev/adrs/accepted/analysis-cif-fit-state.md @@ -0,0 +1,193 @@ +# ADR: Analysis CIF Fit State + +## Status + +Accepted current design. + +## Date + +2026-05-18 + +## Group + +Analysis and fitting. + +## Context + +`analysis/analysis.cif` already persists analysis configuration such as +`_fitting.minimizer_type`, `_fitting.mode_type`, aliases, constraints, +and active fit-mode settings. That configuration alone is not enough to +reopen a saved project and continue the same fit-result, plotting, and +command-line workflow. + +Analysis-owned fit state needs to persist: + +- fit bounds and bound provenance +- pre-fit scalar snapshots for recovery workflows +- compact status metadata for the latest saved fit projection +- deterministic correlation summaries +- Bayesian summary metadata and manifests for bulk array sidecars +- plot-ready Bayesian caches so restored posterior displays do not need + to recompute on first use + +Committed model parameter values and uncertainties already persist in +structure and experiment CIF files through the accepted free-flag CIF +encoding. Those committed values must remain the source of truth for the +current model state. + +The accepted runtime fit-results ADR keeps backend runtime objects +runtime-only unless a narrower persistence ADR defines a saved +projection. This ADR defines that narrower saved projection. + +## Decision + +Persist analysis-owned fit state as explicit sibling categories in +`analysis/analysis.cif`, with large Bayesian arrays stored in +`analysis/results.h5`. + +Do not add a dedicated `_fit_state` category or +`_fit_state.schema_version`. Persisted fit state is detected from +`_fit_result` and the related fit-state categories. + +### Common fit-state categories + +Persist these common categories for any saved fit projection: + +- `_fit_parameter` +- `_fit_result` +- `_fit_parameter_correlation` + +`_fit_parameter` stores analysis-owned per-parameter fit controls and +pre-fit scalar snapshots: + +- `param_unique_name` +- `fit_min` +- `fit_max` +- `fit_bounds_uncertainty_multiplier` +- `start_value` +- `start_uncertainty` + +`_fit_result` stores the latest saved fit header: + +- `result_kind` +- `success` +- `message` +- `iterations` +- `fitting_time` +- `reduced_chi_square` + +`_fit_parameter_correlation` stores pairwise deterministic or posterior +correlation summaries keyed by a persisted `id`. Only unique parameter +pairs are stored. + +### Deterministic fit projection + +Deterministic fits persist `_deterministic_result` in addition to the +common categories above. + +`_deterministic_result` stores compact optimizer metadata and counts: + +- `optimizer_name` +- `method_name` +- `objective_name` +- `objective_value` +- `n_data_points` +- `n_parameters` +- `n_free_parameters` +- `degrees_of_freedom` +- `covariance_available` +- `correlation_available` + +Do not persist a `_deterministic_parameter_result` category. Final +deterministic parameter values and uncertainties already persist in the +model CIF files, and restored deterministic ordering comes from +`_fit_parameter`. + +### Bayesian fit projection + +Bayesian fits persist these additional categories: + +- `_bayesian_result` +- `_bayesian_sampler` +- `_bayesian_convergence` +- `_bayesian_parameter_posterior` +- `_bayesian_distribution_cache` +- `_bayesian_pair_cache` +- `_bayesian_predictive_dataset` + +`_bayesian_result` stores the saved Bayesian header and sidecar flags, +including `sidecar_file`, `has_posterior_samples`, +`has_distribution_cache`, `has_pair_cache`, and +`has_posterior_predictive`. + +`_bayesian_sampler` stores the resolved sampler settings used for the +run. `parallel` persists the resolved non-negative worker count as an +integer. + +`_bayesian_convergence` stores convergence metadata and posterior array +shape counts. + +`_bayesian_parameter_posterior` stores one summary row per sampled +parameter, including credible intervals, uncertainty, ESS, and R-hat. +Its row order defines the saved posterior parameter order. + +`_bayesian_distribution_cache`, `_bayesian_pair_cache`, and +`_bayesian_predictive_dataset` store manifest rows for plot-ready +posterior caches. Distribution and predictive caches are persisted for +any Bayesian fit with posterior samples, including single-parameter +fits. Pair caches and posterior correlation summaries are only persisted +when more than one parameter was sampled. + +`parameter.posterior` is not part of this accepted design. This ADR +persists analysis-level posterior summaries and caches only. Any future +parameter-level posterior API remains a separate decision. + +### Bayesian sidecar + +Persist large Bayesian arrays in `analysis/results.h5` using `h5py`. +This includes canonical posterior arrays and any saved distribution, +pair, and predictive cache arrays referenced by the CIF manifests. + +The persisted `sidecar_file` value is a local file name only. It must +resolve to a basename inside the project `analysis/` directory. Absolute +paths and traversal paths are rejected and fall back to `results.h5`. + +If the sidecar is missing on load, summary rows in +`analysis/analysis.cif` still restore fit tables and metadata. Features +that require missing bulk arrays must warn clearly instead of failing +silently. + +### Save and restore behavior + +After a fit completes, project save writes the fit-state projection +before the project is considered fully persisted. For Bayesian fits, +that includes the prepared summaries and saved plot caches used by +posterior displays. + +Load order is: + +1. standard analysis configuration +2. common fit-state categories +3. deterministic or Bayesian fit-specific categories according to + `_fit_result.result_kind` +4. Bayesian sidecar arrays when a Bayesian sidecar is expected + +Persist backend runtime objects, optimizer instances, and raw driver +payloads nowhere in this design. + +## Consequences + +Saved projects reopen with enough fit-state context to display the last +saved result and rerun fits without rebuilding analysis-owned bounds by +hand. + +Deterministic persistence stays compact because committed parameter +values remain in the model CIF files instead of being duplicated in a +second deterministic per-parameter result loop. + +Bayesian persistence spans CIF metadata and an HDF5 sidecar, so save and +load must validate consistency between manifest rows and bulk datasets. + +The accepted runtime fit-results ADR should now be read as runtime-only +except where this narrower projection explicitly persists fit-state +metadata, summaries, and cache arrays. diff --git a/docs/dev/adrs/accepted/category-owner-sections.md b/docs/dev/adrs/accepted/category-owner-sections.md new file mode 100644 index 000000000..46f9b3db0 --- /dev/null +++ b/docs/dev/adrs/accepted/category-owner-sections.md @@ -0,0 +1,206 @@ +# ADR: Category Owners and Real Datablocks + +## Status + +Accepted and implemented. + +## Date + +2026-05-17 + +## Context + +The library has two different kinds of objects that expose CIF-like +categories: + +- real datablocks such as structures and experiments +- singleton project sections such as analysis and project configuration + +Real datablocks map to CIF `data_` blocks and therefore need a +datablock identity plus a `data_` header. Singleton sections do not. + +Before this change, `DatablockItem` mixed two responsibilities: + +1. owning and updating flat categories +2. representing a real CIF data block with a `data_` header + +That made it tempting to move `Analysis` onto `DatablockItem` just to +reuse category discovery, parameter enumeration, dirty tracking, and CIF +serialization. Doing so would have weakened the meaning of "datablock" +in the architecture and encouraged fake identities such as +`datablock_entry_name = "analysis"`. + +## Decision + +Introduce `CategoryOwner` as the shared abstraction for objects that own +flat CIF-like categories. + +### 1. Real datablocks remain `DatablockItem`-based + +`DatablockItem` now inherits from `CategoryOwner` and keeps only the +behavior specific to real CIF data blocks: + +- `data_` header serialization +- datablock identity via `datablock_entry_name` +- participation in `DatablockCollection` + +The real datablock families remain: + +- `Structure` +- `ExperimentBase` subclasses + +They continue to serialize as independent CIF data blocks with +`data_` headers. + +### 2. `Analysis` is a category-owning singleton section + +`Analysis` inherits from `CategoryOwner`, not `DatablockItem`. + +It reuses shared behavior for: + +- category discovery +- parameter aggregation +- category update orchestration +- dirty-flag tracking +- help display +- headerless CIF body serialization + +`Analysis` remains a singleton section without a fake `data_` header. It +uses an owner-level `_serializable_categories()` policy so that only the +active sibling categories are written for the current fitting mode. +Inactive mode-specific categories remain accessible but are not +serialized. + +### 3. Project configuration is a category-owning singleton section + +Project-level configuration follows the same pattern via a private +`ProjectConfig(CategoryOwner)` object. + +Its current children are: + +- `ProjectInfo` +- `Rendering` + +The public API stays flat and user-facing: + +- `project.info` +- `project.rendering` + +Saved `project.cif` remains a section file without a `data_` header. It +serializes the `_project.*` metadata category and the `_rendering.*` +configuration category without pretending that the project config is a +real datablock. + +### 4. CIF serialization is split by responsibility + +Serialization is separated into two layers: + +- `category_owner_to_cif(owner)` renders category bodies without a + `data_` header +- `datablock_item_to_cif(datablock)` renders the `data_` header and + then the category-owner body + +This keeps the meaning of `DatablockItem` precise while letting +singleton sections reuse the same category serialization logic. + +### 5. Dirty-flag propagation is generalized to `CategoryOwner` + +Descriptor changes now mark the nearest `CategoryOwner` ancestor dirty +instead of depending on `DatablockItem` specifically. Structures, +experiments, analysis, and project configuration now share the same +owner-level dirty/update contract. + +## Resulting Hierarchy + +```text +GuardedBase +|-- CategoryItem +|-- CollectionBase +| |-- CategoryCollection +| `-- DatablockCollection +`-- CategoryOwner + |-- DatablockItem + | |-- Structure + | `-- ExperimentBase + |-- Analysis + `-- ProjectConfig +``` + +## Consequences + +### Positive + +- The term "datablock" remains semantically precise. +- `Analysis` and project configuration reuse the standard category-owner + behavior without becoming fake data blocks. +- CIF serialization is clearer because category-body rendering is + separated from `data_` header rendering. +- Dirty-flag handling is consistent across all category owners. +- Project-level singleton sections now follow the same architectural + pattern as analysis. + +### Trade-offs + +- The core model gains a new abstraction that must be understood and + documented. +- Owner-level serialization policy now lives in explicit hooks such as + `_serializable_categories()` instead of falling out of the raw object + layout. + +### Compatibility Outcomes + +The implemented design preserves these contracts: + +- `structure.as_cif` starts with `data_` +- `experiment.as_cif` starts with `data_` +- `analysis.as_cif` does not start with `data_` +- `project.cif` does not emit `data_project` +- `project.parameters` remains fit-focused and does not include analysis + configuration parameters +- saved project layout remains compatible + +## Alternatives Considered + +### Make `Analysis` inherit from `DatablockItem` + +Rejected. + +This would have been the smallest code change, but it would make +"datablock" mean both real CIF data blocks and singleton project +sections. + +### Add `emit_data_header = False` to `DatablockItem` + +Rejected. + +This would keep reuse through inheritance but encode two different +concepts in one class and force datablock behavior to branch on whether +the object is "real enough" to emit a header. + +### Keep `Analysis` fully ad hoc + +Rejected. + +That would preserve current behavior but keep duplicated logic for +category discovery, category updates, parameter enumeration, and CIF +section serialization. + +### Make `Project` itself a `CategoryOwner` + +Rejected. + +`Project` is a top-level facade that coordinates structures, +experiments, analysis, display, summary, and file I/O. A smaller private +`ProjectConfig(CategoryOwner)` keeps the category-owning concern local +to the singleton project-section surface instead of mixing it into the +facade itself. + +## Verification + +This decision is fully implemented and was verified with: + +- `pixi run fix` +- `pixi run check` +- `pixi run unit-tests` +- `pixi run integration-tests` +- `pixi run script-tests` diff --git a/docs/dev/adrs/accepted/category-parameter-access.md b/docs/dev/adrs/accepted/category-parameter-access.md new file mode 100644 index 000000000..8c4b17d39 --- /dev/null +++ b/docs/dev/adrs/accepted/category-parameter-access.md @@ -0,0 +1,44 @@ +# ADR: Two-Level Category Parameter Access + +## Status + +Accepted. + +## Date + +2026-05-17 + +## Group + +Core model. + +## Context + +EasyDiffraction models CIF concepts as datablocks, categories, category +collections, and parameters. Users need predictable navigation paths +from a structure or experiment to any parameter. + +## Decision + +Use at most two navigation levels from a datablock to a parameter: + +```python +structure.cell.length_a = 3.88 +experiment.instrument.setup_wavelength = 1.494 +structure.atom_sites['Si'].adp_iso = 0.47 +experiment.background['10'].y = 170 +``` + +The general forms are: + +- `DATABLOCK.CATEGORY_ITEM.PARAMETER` +- `DATABLOCK.CATEGORY_COLLECTION[ITEM_ID].PARAMETER` + +Categories are flat siblings under their owner. A category must not own +another category of a different type. + +## Consequences + +API paths remain short, regular, and close to CIF category structure. +Nested category designs are rejected because they make parameter access +depth depend on the domain area and obscure the CIF-like model. diff --git a/docs/dev/adrs/accepted/development-docs-structure.md b/docs/dev/adrs/accepted/development-docs-structure.md new file mode 100644 index 000000000..223fb71e3 --- /dev/null +++ b/docs/dev/adrs/accepted/development-docs-structure.md @@ -0,0 +1,66 @@ +# ADR: Development Documentation Structure + +## Status + +Accepted. + +## Date + +2026-05-17 + +## Group + +Documentation. + +## Context + +Development notes, ADRs, roadmap material, package-structure snapshots, +and issue backlogs were all kept directly under `docs/dev` or under +mixed-case directories. That made the development documentation harder +to scan and created several competing naming conventions. + +The repository also contains `docs/docs`, which is the MkDocs source for +published user documentation. Development-only material should not be +mixed into that tree unless it is intentionally published. + +## Decision + +Keep development-only documentation under `docs/dev`. + +Use this structure: + +```text +docs/dev/ +|-- index.md +|-- adrs/ +| |-- index.md +| |-- accepted/ +| `-- suggestions/ +|-- issues/ +| |-- open.md +| `-- closed.md +|-- package-structure/ +| |-- full.md +| `-- short.md +|-- plans/ +`-- roadmap/ + `-- ROADMAP.md +``` + +Use lowercase directory names for new development-doc folders. Keep +`ROADMAP.md` uppercase because it may later be copied into published +user documentation where the conventional filename is useful. + +Use `docs/dev/adrs/index.md` as the architecture and decision navigation +surface. Do not keep a separate architecture overview that duplicates +ADR content. + +## Consequences + +- Development documentation remains under the documentation tree without + being part of the published MkDocs source by default. +- ADRs have one entry point for architecture navigation and separate + accepted and proposed areas. +- Package-structure snapshots have stable, script-friendly paths. +- Roadmap publication can later be implemented as a build-time copy from + `docs/dev/roadmap/ROADMAP.md` into `docs/docs`. diff --git a/docs/dev/adrs/accepted/display-ux.md b/docs/dev/adrs/accepted/display-ux.md new file mode 100644 index 000000000..23a780812 --- /dev/null +++ b/docs/dev/adrs/accepted/display-ux.md @@ -0,0 +1,244 @@ +# ADR: Display UX Facade + +## Status + +Accepted and implemented. + +## Context + +The previous user-facing display API mixed presentation actions, +analysis reports, and renderer configuration: + +```python +project.display.plotter.plot_meas(expt_name='hrpt') +project.display.plotter.plot_calc(expt_name='hrpt') +project.display.plotter.plot_meas_vs_calc(expt_name='hrpt') +project.display.plotter.plot_param_correlations() +project.display.plotter.plot_posterior_pairs() +project.display.plotter.plot_param_distribution(param) +project.display.plotter.plot_posterior_predictive(expt_name='hrpt') + +project.analysis.display.free_params() +project.analysis.display.fit_results() +``` + +This has several UX problems: + +- `plot_` is redundant below any display or chart object. +- `plotter` and `tabler` expose backend implementation language to + scientists using notebooks. +- `project.display` and `project.analysis.display` overlap without a + clear user-facing rule. +- `plot_meas`, `plot_calc`, and `plot_meas_vs_calc` force users to + choose a plot state that the project can often infer. +- Bayesian and deterministic chart names are not systematic. +- The previous `project.display` category was serialized to CIF, so it + should not also become a broad transient display facade. + +EasyDiffraction is aimed at scientists, often non-programmers, so the +display API should prioritize discoverability, clear names, and safe +defaults. + +## Decision + +Use `project.display` as the user-facing facade for display actions. +Move serialized renderer settings out of that facade and into a separate +project category named `project.rendering`. + +Renderer settings: + +```python +project.rendering.chart_engine = 'plotly' +project.rendering.table_engine = 'pandas' +project.rendering.show_chart_engines() +project.rendering.show_table_engines() +project.rendering.show_config() +``` + +CIF names: + +- `_rendering.chart_engine` +- `_rendering.table_engine` + +No legacy loader is required for `_display.plotter_type` or +`_display.tabler_type`. The project is in beta, so this cleanup may +break old project files rather than carrying compatibility code. + +The selected display API is grouped: + +```python +project.display.pattern(expt_name='hrpt') + +project.display.parameters.free() +project.display.parameters.fittable() +project.display.parameters.all() +project.display.parameters.access() +project.display.parameters.cif_uids() + +project.display.fit.results() +project.display.fit.correlations() +project.display.fit.series(param, versus='diffrn.ambient_temperature') + +project.display.posterior.pairs() +project.display.posterior.distribution(param) +project.display.posterior.predictive(expt_name='hrpt') + +project.display.show_pattern_options(expt_name='hrpt') +``` + +`project.analysis.display` is removed from the primary public API. Its +current responsibilities move to clearer homes, while the implementation +may keep the existing helpers as internal delegation targets: + +| Current method | New home | +| ---------------------------- | -------------------------------------------------------------- | +| `all_params()` | `project.display.parameters.all()` | +| `fittable_params()` | `project.display.parameters.fittable()` | +| `free_params()` | `project.display.parameters.free()` | +| `how_to_access_parameters()` | `project.display.parameters.access()` | +| `parameter_cif_uids()` | `project.display.parameters.cif_uids()` | +| `fit_results()` | `project.display.fit.results()` | +| `constraints()` | `project.analysis.constraints.show()` | +| `as_cif()` | `project.analysis.as_cif` and `project.analysis.show_as_cif()` | + +`project.analysis` and `project.info` follow the same CIF display +pattern as structures and experiments: + +- `as_cif` is a read-only property returning CIF text as a string. +- `show_as_cif()` pretty-prints the CIF text with a header. + +## Pattern Display + +Use `pattern()` as the main experiment chart: + +```python +project.display.pattern(expt_name='hrpt') +project.display.pattern(expt_name='hrpt', x_min=40, x_max=55) +``` + +By default, `pattern()` uses `include='auto'` and displays as much +useful information as the project state supports: + +- measured data if present +- calculated data if linked structure state and calculated intensities + are available +- background if powder Bragg measured and calculated data plus defined + background points are available +- Bragg ticks if powder Bragg measured and calculated data plus + reflection rows are available +- residual if both measured and calculated data are available and the + experiment type supports a residual panel +- excluded regions if available on the experiment +- uncertainty bands where posterior predictive data exists and the chart + engine supports them + +Specific subsets are selected with `include`: + +```python +project.display.pattern(expt_name='hrpt', include='auto') +project.display.pattern(expt_name='hrpt', include='measured') +project.display.pattern(expt_name='hrpt', include='calculated') +project.display.pattern( + expt_name='hrpt', + include=('measured', 'calculated', 'background', 'residual', 'bragg'), +) +``` + +`include` was chosen over alternatives: + +| Name | Reason not selected | +| ------------- | ----------------------------------------------- | +| `layers` | Sounds graphical rather than user intent. | +| `components` | Precise, but longer. | +| `content` | Too broad. | +| `view` | Better for presets than arbitrary combinations. | +| `series` | Does not fit residual rows or Bragg ticks well. | +| boolean flags | Explicit, but scales poorly. | + +Add discovery for supported pattern content: + +```python +project.display.show_pattern_options(expt_name='hrpt') +``` + +The table shows option name, description, availability for the +experiment, whether `include='auto'` includes it, and the reason an +option is unavailable. + +Pattern option names: + +- `auto` +- `measured` +- `calculated` +- `background` +- `residual` +- `bragg` +- `excluded` +- `uncertainty` + +`uncertainty` is available where posterior predictive data exists for a +supported experiment and the active chart engine can render bands. It is +unavailable, with a clear reason, when no posterior predictive data is +present. + +Explicit combinations are validated against the same project state used +by `include='auto'`. `background`, `bragg`, and `residual` require both +measured and calculated data in the same view. `excluded` requires +measured, calculated, or uncertainty content in the same view, and +excluded-region overlays currently require the experiment's default +x-axis. + +## Deterministic And Bayesian Consistency + +Use these naming rules: + +- `pattern()` shows the current point-estimate experiment view. +- `fit.results()` reports the latest fit result. +- `fit.correlations()` shows parameter relationships from the latest + fit. +- `fit.series(param, versus=...)` shows fitted parameter values across a + sequence of fit results or experiments, using a persisted `diffrn.*` + path for `versus`. +- `posterior.*` names are used only when posterior samples are required. + +## Rejected Alternatives + +Flat display facade: + +```python +project.display.pattern(expt_name='hrpt') +project.display.parameters(scope='free') +project.display.fit_results() +project.display.correlations() +project.display.parameter_series(param, versus='diffrn.ambient_temperature') +project.display.posterior_pairs() +project.display.posterior_distribution(param) +project.display.posterior_predictive(expt_name='hrpt') +``` + +This is shorter but would make `project.display` grow into a long flat +list. + +Separate `charts` and `tables` namespaces were also rejected because +users should not need to decide the output type before asking for +information. Some outputs may render as a chart or a table depending on +backend and state. + +Separate `measured()` and `calculated()` methods were rejected because +they duplicate `pattern(..., include=...)`. + +## Consequences + +- The main display workflow becomes more discoverable through grouped + namespaces and tab completion. +- Renderer configuration becomes clearly separate from display actions. +- Existing tutorials and public API docs must be updated to the selected + API. +- Constraints remain owned by the analysis constraints category. +- There is no legacy CIF compatibility path for `_display.plotter_type` + or `_display.tabler_type`. +- `project.analysis` and `project.info` CIF access is standardized for + consistency with structure and experiment objects. +- Pattern option availability is computed from live project state, + linked structures, calculated intensities, and experiment-specific + content instead of placeholder arrays alone. diff --git a/docs/dev/adrs/accepted/enum-backed-closed-values.md b/docs/dev/adrs/accepted/enum-backed-closed-values.md new file mode 100644 index 000000000..1e89cc7cc --- /dev/null +++ b/docs/dev/adrs/accepted/enum-backed-closed-values.md @@ -0,0 +1,34 @@ +# ADR: Enum-Backed Closed Value Sets + +## Status + +Accepted. + +## Date + +2026-05-17 + +## Group + +Core model. + +## Context + +Many attributes accept a finite set of values: experiment axes, factory +tags, fit modes, calculators, minimizers, and rendering engines. +String-only dispatch is hard to grep and easy to mistype. + +## Decision + +Represent every finite closed set with a `(str, Enum)` class. + +Use enum members as the internal source of truth for validation, +dispatch, default rules, and descriptions. User-facing setters may +accept either enum members or their string values, but internal code +compares against enum members. + +## Consequences + +Finite choices are discoverable, type-checkable, and greppable. +Validation can use enum membership instead of hand-written string +patterns. diff --git a/docs/dev/adrs/accepted/factory-contracts.md b/docs/dev/adrs/accepted/factory-contracts.md new file mode 100644 index 000000000..154f092c0 --- /dev/null +++ b/docs/dev/adrs/accepted/factory-contracts.md @@ -0,0 +1,44 @@ +# ADR: Factory Contracts and Metadata + +## Status + +Accepted. + +## Date + +2026-05-17 + +## Group + +Factories. + +## Context + +Many domain categories have multiple implementations, and some currently +have only one implementation but may gain more later. Construction, +default selection, compatibility filtering, and supported-option display +need a common contract. + +## Decision + +Every category that can be constructed by framework code is created +through a factory, even when it currently has only one implementation. + +Concrete factory-created classes carry metadata appropriate to their +role: + +- `type_info` for stable tag lookup and user descriptions. +- `compatibility` for experiment-axis compatibility where relevant. +- `calculator_support` for calculation-engine support where relevant. + +Child rows that only exist inside a collection do not carry factory +metadata; the collection carries the metadata. + +Factory registration is triggered by explicit imports in package +`__init__.py` files. + +## Consequences + +Construction, default resolution, support tables, and compatibility +filtering stay uniform. Single-implementation categories are ready for +future alternatives without changing the public construction pattern. diff --git a/docs/dev/adrs/accepted/factory-tag-naming.md b/docs/dev/adrs/accepted/factory-tag-naming.md new file mode 100644 index 000000000..edf8a724a --- /dev/null +++ b/docs/dev/adrs/accepted/factory-tag-naming.md @@ -0,0 +1,48 @@ +# ADR: Factory Tag Naming + +## Status + +Accepted. + +## Date + +2026-05-17 + +## Group + +Naming. + +## Context + +Factory tags are persisted and used for public selection. Inconsistent +abbreviations or ordering would make saved files and user code harder to +read. + +## Decision + +Canonical factory tags are: + +- lowercase +- hyphen-separated +- semantically ordered from general to specific +- unique within a factory + +Use standard abbreviations: + +| Concept | Abbreviation | +| ------------------- | ------------ | +| Powder | `pd` | +| Single crystal | `sc` | +| Constant wavelength | `cwl` | +| Time-of-flight | `tof` | +| Bragg scattering | `bragg` | +| Total scattering | `total` | + +Context-local aliases are allowed when the owning object already +disambiguates the choice, for example `pseudo-voigt` inside a CWL +experiment. + +## Consequences + +Saved tags are stable and greppable, while user-facing APIs can remain +short where context makes the canonical prefix redundant. diff --git a/docs/dev/adrs/accepted/fit-mode-categories.md b/docs/dev/adrs/accepted/fit-mode-categories.md new file mode 100644 index 000000000..278d3752a --- /dev/null +++ b/docs/dev/adrs/accepted/fit-mode-categories.md @@ -0,0 +1,781 @@ +# ADR: Fit Mode Categories and Fit Execution API + +## Status + +Accepted and implemented. + +## Date + +2026-05-16 + +## Context + +The current analysis API uses `project.analysis.fit` as both a category +and a callable execution entry point: + +```python +project.analysis.fit.minimizer_type = 'lmfit (leastsq)' +project.analysis.fit.mode = 'joint' +project.analysis.fit() +``` + +This creates three problems: + +- the same name, `fit`, represents both configuration and execution +- mode-specific configuration is not part of the normal switchable + category pattern used elsewhere in the API +- `project.analysis.help()` always shows `joint_fit_experiments`, even + when the active fit mode is not joint + +The separate `fit_sequential(...)` method creates another inconsistency. +Sequential fitting is selected by `fit.mode = 'sequential'`, but the +actual run must be started through `fit_sequential(...)` because +sequential-specific settings such as `data_dir`, `max_workers`, +`chunk_size`, `file_pattern`, and `reverse` are currently method +arguments instead of persisted analysis configuration. + +The rest of the API already has a clearer convention for switchable +categories. For example, peak profile selection is owned by the +experiment: + +```python +project.experiments['hrpt'].show_peak_profile_types() +project.experiments['hrpt'].peak_profile_type = 'pseudo-voigt' +project.experiments['hrpt'].peak.broad_gauss_u = 0.1 +``` + +The owner-level selector switches or configures the active category +shape, and the active category exposes only the parameters that make +sense for that selected type. + +Fit modes should follow the same conceptual pattern: + +- the `Analysis` owner selects the fit mode +- common fitting configuration lives in a stable category +- mode-specific settings live in mode-specific sibling categories +- help output and CIF serialization reflect the active mode + +### Precedent: how `atom_site_aniso` handles a conditional category + +The repository already has one closely related precedent: the +`atom_site_aniso` collection on a structure is only meaningful when at +least one `atom_site` has `adp_type` set to `Bani` or `Uani`. Its +implementation is instructive because it deliberately does **not** hide +the category from public discovery: + +- `Structure.atom_site_aniso` is always present as a property and always + appears in help output. +- When inactive, the collection is simply empty. +- A private `_sync_atom_site_aniso()` reconciles its contents from + `atom_sites` whenever categories update: rows for anisotropic atoms + are added, rows for isotropic or stale atoms are removed. +- CIF serialization naturally drops the empty loop, so there is no + separate "serialize only when active" rule. + +This is a viable alternative pattern for fit modes, and it is +intentionally rejected by this ADR (see _Alternatives Considered_). The +key differences that motivate a new pattern for fit modes are: + +- `atom_site_aniso` rows are **derived** from a per-atom selector + (`atom_site.adp_type`). For fit modes, the selector is owner-level + (`Analysis.fitting_mode_type`) and the mode-specific categories + (`joint_fit`, `sequential_fit`, `sequential_fit_extract`) carry + independent, user-edited settings that cannot be derived from anything + else. +- `atom_site_aniso` has one conditional category. Fit modes introduce a + family of mutually exclusive categories; showing all of them as + always-present empty surfaces would clutter `help()` output and invite + users to configure a mode that is not active. +- For sequential fitting, configuration must be authoritative for CLI + workflows. "Empty when inactive" is ambiguous on reload — was the mode + never used, or was it cleared on a previous run? + +Fit modes therefore call for an explicit **active-sibling** pattern (see +§2 and §7) rather than the auto-synced always-present pattern used by +`atom_site_aniso`. + +This ADR intentionally does not preserve the existing public API as a +compatibility surface. The follow-up migration plan may describe file, +test, and documentation changes, but the target design is not required +to keep legacy runtime aliases. + +## Decision + +### 1. Split fitting configuration from fit execution + +`Analysis.fit()` becomes the public operation that executes the current +fit mode. + +Common fitting configuration moves to a dedicated category: + +```python +project.analysis.fitting.minimizer_type = 'lmfit (leastsq)' +project.analysis.fit() +``` + +`project.analysis.fit` is no longer a category. It is an action method. + +The common `fitting` category owns configuration shared by all fit +modes. Initially this includes: + +- `minimizer_type` + +Additional settings that apply to all fit modes can be added here later. +Verbosity remains a call-level or project-level concern and does not +need to be persisted in this category. + +**Single source of truth.** `Analysis.fitting_mode_type` is the only +writable surface for the active mode, and the only place the mode is +stored at runtime. The CIF field `_fitting.mode_type` (§8) is +synthesized directly from `analysis.fitting_mode_type` at serialization +time and applied back to the selector on load. There is no mirror +descriptor on the `fitting` category. This keeps the runtime model free +of duplicated state. + +### 2. Add an owner-level fitting-mode selector + +`Analysis` owns the fitting-mode selector, following the existing +switchable-category style used by experiment categories. + +The selector name must start with the public category name. This mirrors +`peak_profile_type` and `show_peak_profile_types()`: the category is +`peak`, and the selected aspect is the peak profile. For fitting, the +category is `fitting`, and the selected aspect is the fitting mode. + +```python +project.analysis.show_fitting_mode_types() +project.analysis.fitting_mode_type = 'sequential' +``` + +The selector is backed by `FitModeEnum` and accepts: + +- `single` +- `joint` +- `sequential` + +`show_fitting_mode_types()` should show all fitting modes, mark the +current mode, and describe the execution requirements for each mode. It +should not hide `sequential` simply because the project currently has +only one experiment. Sequential fitting uses one template experiment +plus files from `sequential_fit.data_dir`, so filtering it out based on +experiment count is misleading. + +The selector changes the active fit mode and controls which +mode-specific public categories are visible and serialized. + +Note that this is **not** the same mechanism as `peak_profile_type`. +`peak_profile_type` swaps the concrete class behind a single category +(`peak`); `fitting_mode_type` swaps which _sibling_ category +(`joint_fit` / `sequential_fit`) is active and visible. The `fitting` +category itself does not change shape. This is a new pattern — call it +the **active-sibling selector** — and it is documented here as a +first-class convention for owners that gate sibling categories on a +run-time choice. Future categories with the same shape should follow the +same naming and lifecycle rules. + +### 3. Keep mode-specific categories as flat Analysis siblings + +Mode-specific configuration lives in direct children of `Analysis`. +These categories are not nested under `fitting`. + +Public API: + +```python +project.analysis.fitting_mode_type = 'joint' +project.analysis.joint_fit.create(experiment_id='sepd', weight=0.7) +project.analysis.joint_fit.create(experiment_id='nomad', weight=0.3) +project.analysis.fit() +``` + +```python +project.analysis.fitting_mode_type = 'sequential' +project.analysis.sequential_fit.data_dir = 'data/d20_scan' +project.analysis.sequential_fit.file_pattern = '*.xye' +project.analysis.sequential_fit.max_workers = 'auto' +project.analysis.sequential_fit.reverse = True +project.analysis.fit() +``` + +There is no `single_fit` category initially because single fitting has +no mode-specific persisted settings. If single-mode settings are added +later, they should be placed in a `single_fit` category using the same +pattern. + +### 4. Replace `joint_fit_experiments` with `joint_fit` + +The user-facing joint-mode category is named `joint_fit`. + +It is a collection keyed by experiment id. Each item contains: + +- `experiment_id` +- `weight` + +Suggested API: + +```python +project.analysis.joint_fit.create(experiment_id='sepd', weight=0.7) +project.analysis.joint_fit.create(experiment_id='nomad', weight=0.3) +project.analysis.joint_fit['sepd'].weight = 0.8 +``` + +When joint mode is active, `joint_fit` represents the experiments +participating in the joint fit. Auto-population and validation are +specified deterministically: + +- On `fit()` in joint mode, any project experiment without a + corresponding `joint_fit` row is added automatically with + `weight = 1.0`. +- A `joint_fit` row whose `experiment_id` does not match any project + experiment raises an error before fitting starts. It is not silently + pruned, because that would mask user typos. +- Switching `fitting_mode_type` to `joint` does **not** auto-populate. + Auto-population happens only at execution time so that intermediate + configuration states are never silently mutated. + +Execution requirements for joint fitting: + +- at least two project experiments +- every joint-fit row references an existing experiment +- every participating experiment has one weight (after auto-population) + +Weights remain relative weights. The fitting implementation may +normalize them internally. + +### 5. Add a `sequential_fit` category + +The user-facing sequential-mode category is named `sequential_fit`. + +It is a single-item category with these persisted fields: + +- `data_dir` +- `file_pattern` +- `max_workers` +- `chunk_size` +- `reverse` + +Suggested defaults: + +- `data_dir`: unset, required before execution +- `file_pattern`: `*` +- `max_workers`: `1` +- `chunk_size`: unset, resolved from `max_workers` at runtime +- `reverse`: `false` + +`max_workers` accepts either a positive integer or the token `auto`. It +is stored as a single descriptor and normalized to a positive integer by +a runtime resolver before being passed to the worker pool; consumers +never see the raw `auto` token. Whether the descriptor type is a +dedicated union descriptor or a string descriptor with validation is an +implementation detail. + +`chunk_size` allows an unset value and serializes that unset value as +CIF null (`.`). + +Relative `data_dir` values are resolved relative to the project +directory when the project has a saved path. For an unsaved project, +`fit()` raises a clear error if `data_dir` is relative — sequential +fitting requires either a saved project or an absolute `data_dir`. This +keeps saved projects portable across Python and CLI workflows and +rejects the ambiguous CWD-dependent case explicitly. + +`reverse` is represented by a boolean descriptor. If the current +descriptor layer has no dedicated boolean descriptor, one is introduced +rather than storing boolean state as an arbitrary string. (Introducing a +boolean descriptor is a small prerequisite for this ADR; the +implementation plan should call it out separately.) + +Execution requirements for sequential fitting: + +- exactly one project structure +- exactly one template project experiment +- project has a saved path +- at least one free parameter +- `sequential_fit.data_dir` is set and resolves to input data files + +### 6. Add a `sequential_fit_extract` category + +Sequential fits often need per-file metadata such as temperature, +pressure, field strength, or other scan coordinates. This information is +scientifically important and is also used by parameter-series plots. + +The current `extract_diffrn` callback solves this in Python notebooks, +but a Python callable cannot be serialized in a portable way or invoked +from the generic CLI. Replace that callback with a persisted extraction +rule collection named `sequential_fit_extract`. + +Suggested API: + +```python +project.analysis.sequential_fit_extract.create( + id='temperature', + target='diffrn.ambient_temperature', + pattern=r'^TEMP\s+([0-9.]+)', +) +project.analysis.sequential_fit_extract.create( + id='pressure', + target='diffrn.ambient_pressure', + pattern=r'^PRESSURE\s+([0-9.]+)', +) +``` + +Each extraction rule contains: + +- `id` +- `target` +- `pattern` +- `required` + +`target` is a descriptor path relative to the template experiment, for +example `diffrn.ambient_temperature`. It replaces the weaker name +`param_suffix`, because the rule is assigning to a known descriptor and +to a known output CSV column. Initial supported targets are numeric +descriptors under `experiment.diffrn`. `target` is validated at +`create()` time against the current template experiment and rejected if +it does not resolve to a writable numeric descriptor; this fails fast +rather than waiting until the first sequential run. + +`pattern` is a regular expression applied **line by line** to the input +data file (`re.search` on each line until the first match). The regex +must contain exactly one capture group, and the captured text must be +convertible to `float`. To bound worst-case behaviour on untrusted CIF +input, patterns are validated at `create()` time and rejected if they +contain backreferences or nested quantifiers; a future revision may +adopt a timeout-based engine. + +`required` controls failure behavior. If `required` is false and the +pattern is not found, the target value is left empty for that file. If +`required` is true, the file result is marked failed with a clear error. + +Extracted values are written to `analysis/results.csv` under the column +name `diffrn.` (dots are preserved). Downstream consumers such as +`display.fit.series(...)` must use that exact column name. + +The corresponding CIF fragment is: + +```cif +loop_ +_sequential_fit_extract.id +_sequential_fit_extract.target +_sequential_fit_extract.pattern +_sequential_fit_extract.required +temperature diffrn.ambient_temperature "^TEMP\s+([0-9.]+)" false +pressure diffrn.ambient_pressure "^PRESSURE\s+([0-9.]+)" false +``` + +During sequential fitting, each worker should: + +1. load the data file +2. apply all `sequential_fit_extract` rules +3. assign extracted values to the target descriptors on the worker + experiment +4. include those values in the result row as `diffrn.` columns +5. run the fit + +Dataset replay should also apply `diffrn.*` values from +`analysis/results.csv` back onto the template experiment. This keeps: + +```python +temperature = 'diffrn.ambient_temperature' +project.display.fit.series(param, versus=temperature) +``` + +working after a sequential fit and after reloading a saved project. + +A runtime-only Python hook may still be useful for advanced notebook +workflows, but it is not part of the persisted CLI-ready contract +defined by this ADR. + +### 7. Make help output instance-aware + +Help rendering should support instance-aware filtering through a common +hook rather than special-casing `Analysis` alone. Class-level MRO +discovery remains the default, but an object may filter or extend the +properties and methods shown by `help()` based on its current state. + +The implementation should add an optional help-filter hook to the common +help path used by `render_object_help()`, `GuardedBase.help()`, and +`CategoryItem.help()`. The exact function names can be chosen during +implementation, but the contract is: + +- The hook can only **hide** members that class-MRO discovery already + produced; it cannot inject members that do not exist on the class. + This keeps `help()` consistent with `dir()` and IDE completion. +- Hidden members remain programmatically accessible. Direct attribute + access to an inactive mode-specific category (e.g. reading + `analysis.sequential_fit` while in `joint` mode) returns the + underlying object unchanged. Mutating it does not raise, but its + values are not serialized while the mode is inactive (§8). This avoids + surprising errors in notebooks where a user is iterating on + configuration before switching modes. + +This hook is useful beyond fitting. Any object with conditional workflow +surfaces, backend-dependent options, or selected-type-specific +categories can use the same mechanism later. + +For this ADR, `Analysis.help()` must use instance-aware filtering. It +should not only inspect class-level properties because mode-specific +categories are conditional workflow surfaces. + +The help output should show common analysis properties and only the +category relevant to the active fit mode. + +For `single` mode, help should show fitting configuration and the +`fit()` operation, but no joint or sequential category: + +```text +Properties +fitting +display + +Methods +fit() +show_fitting_mode_types() +``` + +For `joint` mode, help should additionally show: + +```text +joint_fit +``` + +For `sequential` mode, help should additionally show: + +```text +sequential_fit +sequential_fit_extract +``` + +Inactive mode categories should not be advertised in help output. Direct +access to an inactive mode category may either raise a clear mode error +or remain an internal implementation detail, but the public discovery +surface should only show categories relevant to the selected mode. + +### 8. Serialize common and active mode-specific categories + +Persist common fitting configuration in `analysis/analysis.cif` using a +category name that matches the new Python category: + +```cif +_fitting.minimizer_type "lmfit (leastsq)" +_fitting.mode_type sequential +``` + +Persist only the active mode-specific category. + +Sequential example: + +```cif +_fitting.minimizer_type "lmfit (leastsq)" +_fitting.mode_type sequential + +_sequential_fit.data_dir "data/d20_scan" +_sequential_fit.file_pattern "*.xye" +_sequential_fit.max_workers auto +_sequential_fit.chunk_size . +_sequential_fit.reverse true + +loop_ +_sequential_fit_extract.id +_sequential_fit_extract.target +_sequential_fit_extract.pattern +_sequential_fit_extract.required +temperature diffrn.ambient_temperature "^TEMP\s+([0-9.]+)" false +``` + +Joint example: + +```cif +_fitting.minimizer_type "lmfit (leastsq)" +_fitting.mode_type joint + +loop_ +_joint_fit.experiment_id +_joint_fit.weight +sepd 0.7 +nomad 0.3 +``` + +Single example: + +```cif +_fitting.minimizer_type "lmfit (leastsq)" +_fitting.mode_type single +``` + +Inactive mode-specific categories should not be serialized. This avoids +stale settings from a previously selected mode affecting CLI behavior +after reload. Because `sequential_fit_extract` is part of the sequential +workflow, it is serialized only when the active fitting mode is +`sequential`. + +### 9. Restore mode before mode-specific settings + +Deserialization order must be: + +1. restore the common `fitting` category +2. read `_fitting.mode_type` +3. set `analysis.fitting_mode_type` +4. restore the active mode-specific category, if present +5. restore active child collections such as `sequential_fit_extract` +6. restore other analysis categories such as aliases and constraints + +This mirrors the switchable-category restoration pattern used by +experiment categories: the active mode is known before mode-specific +fields are loaded. + +### 10. The CLI runs the configured fit mode + +The top-level CLI `fit` command should load the project and execute: + +```python +project.analysis.fit() +``` + +It should not need a separate `fit-sequential` command for the normal +case. Sequential mode can be run from CLI because its required settings +are persisted in `analysis/analysis.cif`. + +CLI options may override saved configuration for one invocation, for +example `--fitting-mode`, `--data-dir`, or `--max-workers`, but the core +model is that the saved project contains the selected mode and its +mode-specific settings. CLI overrides are **per-invocation only** and +are never written back to the project on disk. Persisting a new mode or +new settings requires an explicit save step. + +## Consequences + +### Positive + +- `fit()` has one meaning: execute fitting. +- `fitting` has one meaning: common fitting configuration. +- Fit modes follow the same owner-level selection style as existing + switchable categories. +- `joint_fit` and `sequential_fit` are visible only when relevant. +- Sequential fitting becomes runnable from CLI without a special Python + method call. +- Sequential scan metadata becomes serializable and CLI-friendly through + `sequential_fit_extract`. +- Instance-aware help becomes a reusable capability for conditional + public surfaces. +- CIF structure is flat, explicit, and aligned with public API names. +- Mode-specific configuration can grow independently without polluting + the common fitting category. + +### Trade-offs + +- Help rendering needs an instance-aware extension hook instead of pure + class-MRO discovery. +- Switching fit mode changes the visible public surface of `Analysis`, + which requires clear help and error messages. +- The new API intentionally breaks the current `analysis.fit` category + and `fit_sequential(...)` method shape. +- Regex extraction rules cover common file-header metadata but are less + flexible than an arbitrary Python callback. + +### Compatibility + +This ADR does not require runtime compatibility aliases. Consistent with +the project's beta-stage policy of no legacy shims, loading an old +project that still contains `_fit.minimizer_type`, `_fit.mode`, or +`_joint_fit_experiments.*` raises a clear error pointing at the new CIF +names. There is no silent auto-migration on load. + +The following public API shapes are replaced by the new design: + +- `project.analysis.fit.minimizer_type` +- `project.analysis.fit.mode` +- `project.analysis.fit_sequential(...)` +- `project.analysis.joint_fit_experiments` + +The replacement API is: + +- `project.analysis.fitting.minimizer_type` +- `project.analysis.fitting_mode_type` +- `project.analysis.joint_fit` +- `project.analysis.sequential_fit` +- `project.analysis.sequential_fit_extract` + +The `project.analysis.fit()` spelling remains, but it changes from a +callable category invocation to a real `Analysis` method. + +## Alternatives Considered + +### Name the selector `fit_mode_type` + +Rejected. + +`fit_mode_type` and `show_fit_mode_types()` would imply a public +`fit_mode` category, which does not exist. The project convention is +that switchable-category selector names begin with the public category +name (`peak_profile_type` for `peak`, `background_type` for +`background`). For a selector hosted on `Analysis` that gates the +`fitting`-related siblings, `fitting_mode_type` is the closest fit. + +### Mirror `atom_site_aniso`: always present, auto-synced, empty when inactive + +Rejected for fit modes (see _Context \u2014 Precedent_ for the +comparison). + +`atom_site_aniso` keeps the category always visible and derives its +contents from a per-atom selector. Applying the same shape to fit modes +would mean `joint_fit`, `sequential_fit`, and `sequential_fit_extract` +always appear in `help()` and CIF, with auto-sync rules that try to +reconcile them with `fitting_mode_type`. + +This is rejected because: + +- The mode-specific categories carry independent user-edited state that + cannot be derived from any other object. +- Three always-visible mutually exclusive categories make `help()` + output and CIF files harder to read than a single active sibling. +- For CLI workflows, \"empty category\" and \"unused mode\" must be + distinguishable on reload; explicit serialization of only the active + category preserves that distinction. + +The precedent is still informative: `atom_site_aniso` shows that the +codebase accepts non-uniform category visibility patterns when they fit +the underlying data model. The active-sibling pattern introduced here is +the right tool for an owner-level mode selector. + +### Keep `analysis.fit` as a callable category + +Rejected. + +It keeps the current naming ambiguity where `fit` is both a category and +an operation. It also leaves no clean place for mode-specific +configuration without either nesting categories under `fit` or adding +always-visible sibling categories. + +### Put all mode-specific settings into `fitting` + +Rejected. + +This would make `fitting` contain `data_dir`, `max_workers`, and +joint-fit weights even when those fields do not apply to the active +mode. It weakens help output and makes CIF harder to read. + +### Use `analysis.fitting.mode` as the public selector + +Rejected for the public API. + +Although `_fitting.mode_type` is the CIF spelling, the public selector +should follow the existing switchable-category owner style: + +```python +project.analysis.fitting_mode_type = 'sequential' +``` + +A separate `fitting.mode` descriptor on the runtime `fitting` category +is also rejected: it would duplicate state already held by +`fitting_mode_type`. `_fitting.mode_type` is synthesized at +serialization time instead of being mirrored on a runtime object. + +### Replace the `fitting` category object per fit mode + +Rejected. + +A concrete `SequentialFitting` object could expose sequential fields +directly, but switching by assigning a property on the object being +replaced creates stale-reference hazards: + +```python +fitting = project.analysis.fitting +project.analysis.fitting_mode_type = 'sequential' +# fitting may now point to the old object +``` + +Keeping `fitting` stable and adding active sibling mode categories gives +better long-term API stability. + +### Persist inactive mode-specific categories + +Rejected. + +Persisting inactive categories would make saved projects ambiguous for +CLI workflows. The selected mode should determine which mode-specific +category is authoritative. + +## Follow-up Questions + +The core design in this ADR is implemented. The questions below are +follow-up design topics that may need future ADRs if behaviour changes. + +### Architectural / API + +- **Direct access to inactive mode categories.** \u00a77 specifies the + lenient behaviour: reading `analysis.sequential_fit` in `joint` mode + returns the underlying object, mutation does not raise, but values are + not serialized. Open: is this the right trade-off, or should access + raise a `ModeError` to prevent silent data loss on save? + +### Data model + +- **`joint_fit` and experiment lifecycle.** Stale rows raise at `fit()` + time. Open: should `joint_fit` also listen for experiment-collection + changes and prune (or warn) on experiment deletion, or remain passive + until execution? +- **`joint_fit` weight bounds.** Default weight is `1.0`. Open: is + `weight = 0` allowed (effective exclusion), and what is the upper + bound, if any? Should weights share the validator used by free + parameters? +- **`sequential_fit_extract` target scope.** Targets are initially + numeric descriptors under `experiment.diffrn`. Open: + - are nested descriptors (`diffrn.foo.bar`) allowed, or only one + level? + - is the same target reachable from two rules (last wins, error, or + merge)? + - how is the supported-prefix list extended when new + sample-environment categories appear? +- **`sequential_fit_extract` failure aggregation.** A failed `required` + rule marks the file failed. Open: does one failure abort the whole + sequential run, just exclude that file from results, or apply a + max-failure threshold? +- **Extraction caching.** Files may be re-read on resume or partial + replay. Open: is extraction re-run each time, or cached alongside + results in `analysis/results.csv`? + +### Persistence & CLI + +- **CIF round-trip for `auto` `max_workers`.** The on-disk value is the + token `auto`. Open: when CLI overrides resolve `auto` to a concrete + integer for one run, is that integer ever written back, or is the + token always preserved on disk regardless of runtime resolution? +- **Serialization order for `_fitting.*`.** \u00a79 specifies + deserialization order. Open: pin serialization order too (mode first, + then `minimizer_type`, then mode-specific siblings) so generated files + are stable for diffing? +- **Failure mid-sequential-run.** Open: if `fit()` fails partway through + a sequential scan, what is the state of `analysis/results.csv` and the + persisted `sequential_fit` \u2014 resumable, discarded, or left as-is + for manual recovery? +- **CLI override of `sequential_fit_extract`.** Overrides are listed for + `--fitting-mode`, `--data-dir`, `--max-workers`. Open: are extraction + rules overridable from the CLI (for example + `--extract id=temperature:target=...:pattern=...`), or only via the + project file? + +### Help & discovery + +- **`dir()` consistency.** The hook hides members from `help()` only. + Open: should `dir(analysis)` likewise hide inactive categories, or + always reflect the full class surface (affects tab completion)? + +### Scope + +- **`single` mode \"future-proofing.\"** \u00a73 leaves `single_fit` as + optional future work. Open: is there any current setting that would + qualify \u2014 for example, per-experiment selection when a project + contains multiple experiments and the user wants to run `single` + against one of them? +- **Migration error timing.** Compatibility says loading old CIF raises. + Open: does \"raises\" mean at load of `project.cif`, or at first + access of `analysis`? This affects how users discover the break and + whether a project can be partially loaded for inspection. + +## Deferred Work + +- Optional `single_fit` category if single-mode-specific settings are + introduced. +- A separate ADR for changing switchable category selectors globally + from owner-level names such as `peak_profile_type` toward + category-owned selectors such as `peak.profile_type`. diff --git a/docs/dev/adrs/accepted/free-flag-cif-encoding.md b/docs/dev/adrs/accepted/free-flag-cif-encoding.md new file mode 100644 index 000000000..e9e7509bf --- /dev/null +++ b/docs/dev/adrs/accepted/free-flag-cif-encoding.md @@ -0,0 +1,39 @@ +# ADR: Free-Flag CIF Encoding + +## Status + +Accepted. + +## Date + +2026-05-17 + +## Group + +Persistence. + +## Context + +Fitting needs to persist whether a parameter is fixed or free. A +separate free-parameter list would duplicate state already attached to +individual parameter values and could become stale. + +## Decision + +Encode a parameter's free/fixed status in the CIF value uncertainty +syntax: + +```text +3.89 fixed +3.89(2) free with estimated standard deviation +3.89() free without estimated standard deviation +``` + +Do not persist a separate list of free parameters as the source of +truth. + +## Consequences + +Each parameter carries its own fit-state information in the same place +as the value. CIF round-trips are simpler because there is no separate +state table to reconcile. diff --git a/docs/dev/adrs/accepted/guarded-public-properties.md b/docs/dev/adrs/accepted/guarded-public-properties.md new file mode 100644 index 000000000..15fd6308d --- /dev/null +++ b/docs/dev/adrs/accepted/guarded-public-properties.md @@ -0,0 +1,44 @@ +# ADR: Guarded Public Properties + +## Status + +Accepted. + +## Date + +2026-05-17 + +## Group + +Core model. + +## Context + +Most EasyDiffraction domain objects inherit from `GuardedBase`. +Descriptors and parameters are stored privately and exposed through +public properties. The public API needs a clear rule for whether a user +can assign to an attribute, and internal construction code still needs a +way to populate read-only values. + +## Decision + +Treat the presence of a property setter as the public writability +contract. + +- Public properties with setters are user-editable. +- Public properties without setters are read-only. +- Internal mutation of read-only properties uses explicit private + methods such as `_set_sample_form`. +- Unknown public attributes are rejected by `GuardedBase.__setattr__` + with diagnostics. + +Do not add a public setter only for internal use, because that makes the +attribute user-writable. + +## Consequences + +The public API stays predictable: reading a property returns the live +descriptor or parameter object, and assigning to the property is allowed +only when the class explicitly declares that assignment as part of the +API. Internal loaders and factories remain greppable because private +mutator methods are named directly. diff --git a/docs/dev/adrs/accepted/help-discoverability.md b/docs/dev/adrs/accepted/help-discoverability.md new file mode 100644 index 000000000..8fd019f39 --- /dev/null +++ b/docs/dev/adrs/accepted/help-discoverability.md @@ -0,0 +1,65 @@ +# ADR: Help Method Discoverability + +## Status + +Accepted and implemented. + +## Context + +EasyDiffraction is used by scientists who often explore the API in +notebooks. The main object graph already exposes many focused objects: +projects, project metadata, structures, experiments, categories, +parameters, analysis helpers, summaries, and display facades. Users need +a consistent way to discover the next useful operation from any of these +objects without reading source code. + +Most model objects inherit `GuardedBase`, `CategoryItem`, +`CategoryCollection`, `DatablockItem`, or `DatablockCollection`, which +already provide `help()` output. Plain facade classes such as display +namespaces and summaries do not inherit those base classes, so they need +the same discovery behavior explicitly. + +## Decision + +Every primary public object should provide a `help()` method. This +includes: + +- parameters and descriptors +- category items and category collections +- datablock items and datablock collections +- project-level objects such as `Project`, `ProjectInfo`, `Analysis`, + `Summary`, and `Rendering` +- display facades such as `project.display`, + `project.display.parameters`, `project.display.fit`, + `project.display.posterior`, and `analysis.display` + +`help()` output uses the existing console/table presentation style. It +lists public properties and methods discovered from the class MRO, uses +the first docstring paragraph as the description, and skips private +names. Specialized containers can append domain-specific tables, such as +collection items or datablock categories, after the generic section. + +Plain helper and facade classes use `render_object_help()` so their +output stays consistent with `GuardedBase.help()` without forcing those +classes into the guarded object hierarchy. + +## Consequences + +Users can call `help()` while navigating through the object graph: + +```python +project.help() +project.display.help() +project.display.parameters.help() +project.analysis.display.help() +project.summary.help() +project.experiments.help() +project.experiments['hrpt'].help() +project.experiments['hrpt'].background.help() +project.experiments['hrpt'].background['1'].help() +``` + +New user-facing objects should either inherit an existing help-capable +base class or define `help()` by delegating to `render_object_help()`. +When a class represents a collection or owner, its help output should +guide users to the next object level where practical. diff --git a/docs/dev/adrs/accepted/immutable-experiment-type.md b/docs/dev/adrs/accepted/immutable-experiment-type.md new file mode 100644 index 000000000..9740b1107 --- /dev/null +++ b/docs/dev/adrs/accepted/immutable-experiment-type.md @@ -0,0 +1,35 @@ +# ADR: Immutable Experiment Type + +## Status + +Accepted. + +## Date + +2026-05-17 + +## Group + +Experiment model. + +## Context + +An experiment is defined by four orthogonal axes: sample form, +scattering type, beam mode, and radiation probe. Changing those axes +after creation can require replacing categories, calculators, data +collections, and validation rules. + +## Decision + +Experiment type is set at creation time and is immutable afterwards. + +Factory and CIF-loading code may set the type through private +construction methods, but users cannot mutate the type axes on an +existing experiment. + +## Consequences + +The model avoids partial transformations between fundamentally different +experiment configurations. Runtime switching remains available for +category implementations, such as background or peak profile, where the +state replacement is bounded and explicit. diff --git a/docs/dev/adrs/accepted/lint-complexity-thresholds.md b/docs/dev/adrs/accepted/lint-complexity-thresholds.md new file mode 100644 index 000000000..d2ba16b87 --- /dev/null +++ b/docs/dev/adrs/accepted/lint-complexity-thresholds.md @@ -0,0 +1,33 @@ +# ADR: Lint Complexity Thresholds + +## Status + +Accepted. + +## Date + +2026-05-17 + +## Group + +Quality. + +## Context + +The repository enforces ruff PLR complexity rules. Raising thresholds or +adding local suppressions would make complex functions easier to merge +without addressing maintainability. + +## Decision + +Keep ruff's default PLR thresholds, except set `max-args` and +`max-positional-args` to 6 because ruff counts `self` and `cls`. + +Do not raise complexity thresholds or add `# noqa` comments to silence +complexity rules. Refactor code instead. + +## Consequences + +Complexity failures are treated as design feedback. Large refactors that +change public API should be planned explicitly instead of bypassing the +guardrail. diff --git a/docs/dev/adrs/accepted/loop-category-key-identity.md b/docs/dev/adrs/accepted/loop-category-key-identity.md new file mode 100644 index 000000000..4334b4e13 --- /dev/null +++ b/docs/dev/adrs/accepted/loop-category-key-identity.md @@ -0,0 +1,163 @@ +# ADR: Loop Category Keys and Identity Naming + +**Status:** Accepted +**Date:** 2026-05-18 + +## Context + +CIF dictionaries can declare the key column for a loop category through +`_category_key.name`. For example, crystallographic categories commonly +use domain-specific identity tags such as `_atom_site.label`, while +other loop categories may use an explicit `id` tag. + +EasyDiffraction models the same runtime concept with +`item._identity.category_entry_name`. `CategoryCollection` uses this +value as the collection key, and category items use it in their +`unique_name` path: + +```text +.. +``` + +The design question was whether the `category_entry_name` approach was +enough, and how closely Python-facing identity names should follow CIF +key tags. + +## Assessment + +The `category_entry_name` approach is directionally good. It gives every +loop item a stable collection key without hard-coding the key field into +`CategoryCollection`, and most current loop categories derive that key +from the same field that is serialized into CIF. + +Before this decision it was not explicit enough. The key field was +encoded as a lambda on each item, not as declarative metadata on the +category or descriptor. Nothing validated that the runtime key +corresponded to a serialized CIF field. The main visible example was +`Constraint`: the collection key was derived from the left-hand side of +`_constraint.expression`, but no separate `_constraint.id` field was +persisted. + +## Decision + +Keep `category_entry_name` as the runtime analogue of CIF +`_category_key.name`, and formalize the rule: + +1. Every concrete `CategoryCollection` should have a documented key + field. +2. The key should normally be a public descriptor on the item. +3. The key descriptor should normally be serialized to CIF. +4. Standard CIF categories should keep CIF names in Python where those + names are already meaningful to scientists. +5. Custom categories should prefer `id` only for opaque machine + identity. Use `label` or `*_id` when the value has clearer domain + meaning. + +The `constraint` category has an explicit `id` field and uses it as the +collection key: + +```text +analysis.constraints[id].id -> _constraint.id +``` + +`Constraints.create()` accepts an optional `id` argument. A +user-provided `id` is used whenever it is not `None`; otherwise the +method derives the default `id` from `lhs_alias`, preserving the old +`create(expression=...)` behavior. + +The existing `lhs_alias` and `rhs_expr` properties remain derived +helpers from `_constraint.expression`, not the row identity. + +This argues against a blanket Python API change to use `id` everywhere. +For scientists moving between notebooks and saved CIF, `atom_site.label` +in Python and `_atom_site.label` in CIF is less surprising than +`atom_site.id` in Python and `_atom_site.label` in CIF. + +## Loop Category Keys + +Rows are sorted by the chosen Python key style: `id`, then `*_id`, then +`label`. This table lists fields that drive `category_entry_name`; +identifier-like fields that are not collection keys are listed in the +next section. + +| Python key | Area | Collection class | Category code | CIF key tag | Source | Decision | +| --------------- | ---------- | ------------------------------------------- | ------------------------ | ---------------------------- | --------------------------- | ------------------------------------------------------------------------------------------------------ | +| `id` | Analysis | `Constraints` | `constraint` | `_constraint.id` | Custom category | Add this key. Previous implementations derived the key from the left side of `_constraint.expression`. | +| `id` | Analysis | `SequentialFitExtractCollection` | `sequential_fit_extract` | `_sequential_fit_extract.id` | Custom category | Keep. It is an explicit row identifier for extraction rules. | +| `id` | Experiment | `LineSegmentBackground` | `background` | `_pd_background.id` | Powder CIF-style category | Keep. The row identity is opaque and already serialized. | +| `id` | Experiment | `ChebyshevPolynomialBackground` | `background` | `_pd_background.id` | Powder CIF-style category | Keep. The row identity is opaque and shared with other background variants. | +| `id` | Experiment | `LinkedPhases` | `linked_phases` | `_pd_phase_block.id` | Powder CIF-style category | Keep. Consider `phase_id` only if the public API later standardizes foreign-key names. | +| `id` | Experiment | `ExcludedRegions` | `excluded_regions` | `_excluded_region.id` | Custom category | Keep. It is a simple custom loop row identifier. | +| `id` | Experiment | `ReflnData` | `refln` | `_refln.id` | CIF-style category | Keep for the current reflection table shape. | +| `id` | Experiment | `PowderCwlReflnData` / `PowderTofReflnData` | `refln` | `_refln.id` | CIF-style category | Keep; `phase_id` remains a separate field, not the row key. | +| `experiment_id` | Analysis | `JointFitCollection` | `joint_fit` | `_joint_fit.experiment_id` | Custom category | Keep. The key is a reference to an experiment, so `id` alone would lose context. | +| `point_id` | Experiment | `PdCwlData` / `PdTofData` | `pd_data` | `_pd_data.point_id` | Powder CIF-style category | Keep. It is clearer than `id` for dense measured/calculated data points. | +| `point_id` | Experiment | `TotalData` | `total_data` | `_pd_data.point_id` | Current powder-data mapping | Keep. Revisit the CIF tag only when total-scattering-specific CIF tags are introduced. | +| `label` | Analysis | `Aliases` | `alias` | `_alias.label` | Custom category | Keep. It is the user-visible symbol referenced by expressions, not an opaque row id. | +| `label` | Structure | `AtomSites` | `atom_site` | `_atom_site.label` | CIF core category | Keep. This is a well-known crystallographic identity field. | +| `label` | Structure | `AtomSiteAnisoCollection` | `atom_site_aniso` | `_atom_site_aniso.label` | CIF core category | Keep. It intentionally matches and references the atom-site label. | + +## Non-Key Identity And Reference Fields + +These fields are serialized in loop rows and look identity-like, but +they do not define the collection key. + +| Python field | Area | Collection class | Category code | CIF tag | Role | +| ------------------- | ---------- | ------------------------------------------- | ------------- | -------------------------- | ------------------------------------------------------------------------------------- | +| `phase_id` | Experiment | `PowderCwlReflnData` / `PowderTofReflnData` | `refln` | `_refln.phase_id` | References the linked phase for a calculated reflection. Row key remains `_refln.id`. | +| `param_unique_name` | Analysis | `Aliases` | `alias` | `_alias.param_unique_name` | References the target parameter. Row key remains `_alias.label`. | + +## Naming Guidance + +Use `label` when the identity is a user-visible, domain-specific symbol. +This applies to atom sites and aliases. A label is not weaker than an +`id` if the category defines it as the key. + +Use `id` when the identity is an opaque row identifier without a richer +domain word. This applies to excluded regions, background terms, +sequential-fit extraction rules, and current reflection rows. + +Use `*_id` when the identity is primarily a reference to another object. +This applies to `experiment_id` and `point_id`. It keeps the public API +clearer than a generic `id` while still expressing uniqueness within the +collection. + +Avoid renaming standard CIF identity fields in Python unless the CIF +name is actively hostile to the user-facing model. Each intentional +Python/CIF mismatch adds translation cost for users who move between +Jupyter, CLI output, and saved CIF files. + +## Implementation Notes + +The implementation uses class-level `_category_code` and +`_category_entry_name` declarations on concrete `CategoryItem` +subclasses. `CategoryItem` resolves the declared `_category_entry_name` +lazily from the named public attribute, and `Identity` exposes the +resolved value through `item._identity.category_entry_name`. + +For constraints, a descriptor-backed `id` property is serialized as +`_constraint.id`, and `category_entry_name` resolves from that +descriptor. `_constraint.expression` stores the full equation. +`lhs_alias` and `rhs_expr` remain derived convenience properties. + +When reading older CIF files that only contain `_constraint.expression`, +the loader derives a deterministic fallback `id` from the old +`lhs_alias` key after row values are loaded, then writes +`_constraint.id` on the next save. + +## Consequences + +Keeping CIF names where possible improves notebook-to-CIF continuity and +makes saved files easier to inspect. It also reduces the amount of +documentation needed to explain common crystallographic terms such as +`atom_site.label`. + +Allowing custom categories to use `id`, `label`, or `*_id` means the API +will not be mechanically uniform, but it will be semantically clearer. +Uniformity should come from documented key metadata and predictable +collection behavior, not from forcing every row key to be named `id`. + +## References + +- [COMCIFS `cif_core.dic`](https://github.com/COMCIFS/cif_core/blob/main/cif_core.dic) +- [IUCr Core CIF dictionary browser](https://www.iucr.org/resources/cif/dictionaries/browse/cif_core) diff --git a/docs/dev/adrs/accepted/notebook-generation.md b/docs/dev/adrs/accepted/notebook-generation.md new file mode 100644 index 000000000..47faa3b67 --- /dev/null +++ b/docs/dev/adrs/accepted/notebook-generation.md @@ -0,0 +1,34 @@ +# ADR: Notebook Generation Source of Truth + +## Status + +Accepted. + +## Date + +2026-05-17 + +## Group + +Documentation. + +## Context + +Tutorial notebooks are published as `.ipynb` files, but editing notebook +JSON by hand creates noisy diffs and inconsistent formatting. + +## Decision + +Treat tutorial `.py` files under `docs/docs/tutorials/` as the editable +source of truth. Regenerate notebooks with: + +```shell +pixi run notebook-prepare +``` + +Do not edit generated `.ipynb` tutorial files by hand. + +## Consequences + +Tutorial diffs stay reviewable, and notebooks are regenerated through +the same script path used by the documentation workflow. diff --git a/docs/dev/adrs/accepted/parameter-correlation-persistence.md b/docs/dev/adrs/accepted/parameter-correlation-persistence.md new file mode 100644 index 000000000..9b51c0362 --- /dev/null +++ b/docs/dev/adrs/accepted/parameter-correlation-persistence.md @@ -0,0 +1,104 @@ +# ADR: Parameter Correlation Persistence + +## Status + +Accepted current design. + +## Date + +2026-05-19 + +## Group + +Analysis and fitting. + +## Context + +`plot_param_correlations()` can visualize either deterministic parameter +correlations derived from engine covariance or Bayesian correlations +derived from posterior samples. + +Reloaded projects still need correlation heatmaps even when the raw +runtime covariance or posterior arrays are unavailable. The broader +fit-state layout is defined in `analysis-cif-fit-state.md`; this ADR +records the narrower persisted correlation-summary projection within +that accepted design. + +Correlation data is analysis-owned derived state, not model state. It +therefore belongs in `analysis/analysis.cif`, not in structure or +experiment CIF files. + +## Decision + +Persist pairwise parameter correlations in `_fit_parameter_correlation` +rows inside `analysis/analysis.cif`. + +### Correlation summary schema + +Store one row per unique parameter pair with these fields: + +- `id` +- `source_kind` +- `param_unique_name_i` +- `param_unique_name_j` +- `correlation` + +Example: + +```cif +loop_ +_fit_parameter_correlation.id +_fit_parameter_correlation.source_kind +_fit_parameter_correlation.param_unique_name_i +_fit_parameter_correlation.param_unique_name_j +_fit_parameter_correlation.correlation +1 posterior cosio.atom_site.Co1.adp_iso cosio.atom_site.Co2.adp_iso 0.87 +``` + +Normalize each row to the upper triangle excluding the diagonal. +`param_unique_name_i` and `param_unique_name_j` use a stable ordering so +only one unordered pair is stored. The diagonal is omitted because it is +always `1.0` and can be rebuilt on load. + +Use the same loop for deterministic and Bayesian projections. The source +is carried by `source_kind`, currently `deterministic` or `posterior`. + +### Summary-only role + +`_fit_parameter_correlation` is a persisted summary. It does not replace +posterior samples, posterior pair densities, or covariance matrices. + +This summary is enough to restore correlation heatmaps. It is not enough +to restore richer pair-plot density surfaces or covariance-specific +workflows. + +### Restore behavior + +On load, restore `_fit_parameter_correlation` rows into an analysis- +owned correlation collection. + +When runtime covariance or posterior samples are unavailable, +`project.display.plotter.plot_param_correlations()` may rebuild a square +correlation matrix from the persisted rows and the restored fit-result +parameter ordering. + +### Relation to the broader fit-state ADR + +`analysis-cif-fit-state.md` remains the source of truth for the full +fit-state projection, save/load ordering, and Bayesian sidecar layout. +This ADR records the implemented correlation-specific piece of that +accepted design. + +## Consequences + +- Deterministic and Bayesian correlation summaries survive reload. +- Correlation heatmaps no longer depend entirely on runtime-only data. +- The schema stays compact and fit-type-agnostic. +- Posterior pair plots and covariance-specific workflows still need + richer runtime or persisted data. + +## Deferred Work + +- optional storage of covariance matrices in addition to correlations +- multiple named correlation sources for the same saved project +- full correlation restoration for pair-plot density surfaces diff --git a/docs/dev/adrs/accepted/project-facade-and-persistence.md b/docs/dev/adrs/accepted/project-facade-and-persistence.md new file mode 100644 index 000000000..3b922f423 --- /dev/null +++ b/docs/dev/adrs/accepted/project-facade-and-persistence.md @@ -0,0 +1,80 @@ +# ADR: Project Facade and Persistence Layout + +## Status + +Accepted current design. + +## Date + +2026-05-17 + +## Group + +Persistence. + +## Context + +`Project` is the top-level user facade. It owns project metadata, +structures, experiments, rendering preferences, display helpers, +analysis, summaries, verbosity, and save/load behavior. + +A later proposal considered renaming this facade to `Workspace` so that +`project` could be reserved for the scientific project information +category. The final naming decision is to keep `Project` as the public +root because it matches scientific user language and the saved project +container. + +The persisted project directory needs to separate real CIF datablocks +from singleton project sections. + +## Decision + +Use `Project` as the top-level facade and persist projects as a +directory of CIF files: + +```text +project_dir/ +|-- project.cif +|-- summary.cif +|-- structures/ +|-- experiments/ +`-- analysis/ + `-- analysis.cif +``` + +Real structures and experiments serialize as `data_` datablocks. +Singleton sections such as project configuration, analysis, and summary +serialize without fake `data_` headers. + +Keep project information available as `project.info`. The Python name +avoids a confusing `project.project` access path, while the persisted +CIF category remains the semantic `_project.*` category: + +```cif +_project.id +_project.title +_project.description +_project.created +_project.last_modified +``` + +Do not introduce `_meta.*` tags for project information. The category is +scientific project information, not generic metadata. Any future change +from `_project.*` to another category name must be a separate explicit +persistence decision. + +Keep `project.cif` as the primary singleton project configuration file +while `Project` remains the root facade. Do not rename it to +`workspace.cif` as a side effect of category cleanup. + +The saved project directory path is runtime file-I/O state, not a +serialized project-information field. If the path is exposed in Python, +it must not emit a `_project.path` CIF item. + +## Consequences + +The saved layout mirrors the current object graph while preserving the +semantic difference between real datablocks and singleton sections. The +`Workspace` rename proposal is rejected; ADR examples should continue to +use `Project`, `project.info`, and `project.cif` unless a later accepted +ADR changes a narrower part of this design. diff --git a/docs/dev/adrs/accepted/property-docstring-template.md b/docs/dev/adrs/accepted/property-docstring-template.md new file mode 100644 index 000000000..e78bd457a --- /dev/null +++ b/docs/dev/adrs/accepted/property-docstring-template.md @@ -0,0 +1,39 @@ +# ADR: Descriptor Property Docstring Template + +## Status + +Accepted. + +## Date + +2026-05-17 + +## Group + +Documentation. + +## Context + +Public properties backed by `Parameter`, `NumericDescriptor`, or +`StringDescriptor` need consistent documentation and type hints. +Duplicating free-form text across docstrings and descriptor descriptions +creates drift. + +## Decision + +Use descriptor `description` fields as the source of truth for property +docstrings. Public properties follow the architecture template for: + +- getter summary text +- writable versus read-only getter body +- getter return annotation +- setter value annotation + +Setter docstrings are omitted because they are not rendered by the API +documentation pipeline. + +## Consequences + +Generated API documentation stays consistent with descriptor metadata. +The `param-consistency` tool can validate and repair property docstrings +mechanically. diff --git a/docs/dev/adrs/accepted/runtime-fit-results.md b/docs/dev/adrs/accepted/runtime-fit-results.md new file mode 100644 index 000000000..f7ee8389e --- /dev/null +++ b/docs/dev/adrs/accepted/runtime-fit-results.md @@ -0,0 +1,36 @@ +# ADR: Runtime Fit Results + +## Status + +Accepted current design. + +## Date + +2026-05-17 + +## Group + +Analysis and fitting. + +## Context + +Analysis settings are persisted in `analysis/analysis.cif`, but full fit +outputs can include large arrays, posterior samples, predictive caches, +and diagnostics. Persisting all of that by default would make project +directories heavy and would require a broader result schema. + +## Decision + +Persist fit configuration, not full runtime fit results. + +Per-experiment calculator selection lives in experiment files. Common +fit configuration and fit-mode settings live in `analysis/analysis.cif`. +Runtime fit outputs such as `analysis.fit_results`, posterior samples, +posterior predictive arrays, summaries, and diagnostics remain +runtime-only unless a later ADR narrows the persisted projection. + +## Consequences + +Saved projects remain focused on reusable model and analysis settings. +Result persistence can be added later through specific ADRs without +making the initial project layout carry large runtime artifacts. diff --git a/docs/dev/adrs/accepted/selector-families.md b/docs/dev/adrs/accepted/selector-families.md new file mode 100644 index 000000000..698991649 --- /dev/null +++ b/docs/dev/adrs/accepted/selector-families.md @@ -0,0 +1,40 @@ +# ADR: Selector Families + +## Status + +Accepted. + +## Date + +2026-05-17 + +## Group + +User-facing API. + +## Context + +Several public names use a selector-like shape, but they do not all mean +the same thing. Treating every `_type` as a switchable category +would blur distinct concepts. + +## Decision + +Recognize three selector families: + +| Family | User intent | Examples | +| ---------------------------- | ------------------------------- | --------------------------------------------------------------------------------- | +| Backend selector | Pick an execution backend | `fitting.minimizer_type`, `calculation.calculator_type`, `rendering.chart_engine` | +| Switchable-category selector | Swap a category implementation | `experiment.background_type`, `experiment.peak_profile_type` | +| Active-sibling selector | Pick the active sibling surface | `analysis.fitting_mode_type` | + +Backend selectors live on dedicated configuration categories. +Switchable-category selectors live on the host because they replace a +category instance. Active-sibling selectors live on the owner and decide +which sibling categories are visible, authoritative, and serialized. + +## Consequences + +Selector names stay familiar without forcing one implementation pattern +onto different concepts. Future selectors should declare which family +they belong to before adding API. diff --git a/docs/dev/adrs/accepted/string-paths-and-live-descriptors.md b/docs/dev/adrs/accepted/string-paths-and-live-descriptors.md new file mode 100644 index 000000000..fa8815a95 --- /dev/null +++ b/docs/dev/adrs/accepted/string-paths-and-live-descriptors.md @@ -0,0 +1,45 @@ +# ADR: String Paths and Live Descriptors + +## Status + +Accepted. + +## Date + +2026-05-17 + +## Group + +User-facing API. + +## Context + +Some APIs refer to persisted CIF-style fields, while other APIs refer to +one concrete live model parameter. Accepting both forms everywhere would +make call sites ambiguous and harder to validate. + +## Decision + +Use CIF-style string paths for setup-time, schema-level, and +cross-experiment selectors: + +```python +sequential_fit_extract.create(target='diffrn.ambient_temperature') +project.display.fit.series( + param=structure.cell.length_a, + versus='diffrn.ambient_temperature', +) +``` + +Use live descriptor or parameter objects when the call targets one exact +model quantity and needs its `unique_name`, description, units, or +runtime value. + +Do not add new APIs that accept both a string path and a live descriptor +unless a later ADR defines a stronger reason. + +## Consequences + +Persisted fields and live model parameters stay distinct. APIs that +operate across experiments can round-trip through CIF paths, while APIs +that operate on one fitted parameter keep direct object references. diff --git a/docs/dev/adrs/accepted/switchable-category-api.md b/docs/dev/adrs/accepted/switchable-category-api.md new file mode 100644 index 000000000..629f01f9b --- /dev/null +++ b/docs/dev/adrs/accepted/switchable-category-api.md @@ -0,0 +1,45 @@ +# ADR: Switchable Category API + +## Status + +Accepted. + +## Date + +2026-05-17 + +## Group + +User-facing API. + +## Context + +Some categories have multiple concrete implementations that users can +switch at runtime, such as background, peak profile, and extinction. +Other categories are fixed by experiment type or have only one current +implementation. + +## Decision + +For multi-type switchable categories, expose the selector on the owner: + +```python +experiment.background_type = 'chebyshev' +experiment.peak_profile_type = 'pseudo-voigt' +``` + +The category object itself remains a read-only property. Switching the +owner-level type replaces the underlying category object. + +Expose `show__types()` on the owner so supported choices can +be filtered by the owner context. + +Do not expose public `_type` selectors for fixed-at-creation categories +or single-implementation categories. Their factories and internal type +attributes may still exist for consistency. + +## Consequences + +Users can tell when a change replaces a whole category implementation. +The public API stays smaller for single-type categories while preserving +the internal factory pattern. diff --git a/docs/dev/adrs/accepted/test-strategy.md b/docs/dev/adrs/accepted/test-strategy.md new file mode 100644 index 000000000..17194c51c --- /dev/null +++ b/docs/dev/adrs/accepted/test-strategy.md @@ -0,0 +1,41 @@ +# ADR: Test Strategy + +## Status + +Accepted. + +## Date + +2026-05-17 + +## Group + +Quality. + +## Context + +EasyDiffraction combines core model logic, factories, calculators, +display helpers, tutorials, notebooks, and filesystem persistence. +Regressions can appear at several levels. + +## Decision + +Use layered tests: + +- unit tests for isolated classes and functions +- functional tests for multi-component workflows without heavy external + dependencies +- integration tests for end-to-end behavior with real calculation + engines and data +- script tests for tutorial `.py` files +- notebook tests for generated tutorials + +The unit-test tree mirrors the source tree where practical. The +`test-structure-check` script tracks expected test locations and known +aliases. + +## Consequences + +New features should add focused tests at the lowest useful layer and +broader tests when behavior crosses module boundaries. The mirrored +structure makes missing coverage easier to spot. diff --git a/docs/dev/adrs/accepted/type-neutral-adp-parameters.md b/docs/dev/adrs/accepted/type-neutral-adp-parameters.md new file mode 100644 index 000000000..5609c7a29 --- /dev/null +++ b/docs/dev/adrs/accepted/type-neutral-adp-parameters.md @@ -0,0 +1,42 @@ +# ADR: Type-Neutral ADP Parameters + +## Status + +Accepted. + +## Date + +2026-05-17 + +## Group + +Structure model. + +## Context + +Atomic displacement parameters support CIF-standard B/U and +isotropic/anisotropic variants. Type-specific public names such as +`b_iso` and `u_iso` would require replacing parameter objects when +`adp_type` changes, breaking references, constraints, and free flags. + +## Decision + +Use type-neutral parameter names: + +- `atom_site.adp_iso` +- `atom_site_aniso.adp_11` +- `atom_site_aniso.adp_22` +- `atom_site_aniso.adp_33` +- `atom_site_aniso.adp_12` +- `atom_site_aniso.adp_13` +- `atom_site_aniso.adp_23` + +Use `atom_site.adp_type` to determine whether those values are B or U, +isotropic or anisotropic. Keep `atom_site_aniso` as an always-present +sibling collection and synchronize it from `atom_sites`. + +## Consequences + +Parameter object identity remains stable across ADP type switches. +Serialization and calculator code can branch on `adp_type` without +replacing public parameter objects. diff --git a/docs/dev/adrs/index.md b/docs/dev/adrs/index.md new file mode 100644 index 000000000..50a287db9 --- /dev/null +++ b/docs/dev/adrs/index.md @@ -0,0 +1,46 @@ +# Architecture Decision Records + +Project-specific ADRs are kept in this repository. Organization-wide ADR +indexing may exist separately, but repository decisions should stay near +the code they explain. + +The index follows the EasyScience discussion guidance from +[discussion #47](https://github.com/orgs/easyscience/discussions/47): +group decisions by topic first, then use titles and short descriptions +to keep the list easy to scan. If a group becomes too broad, split it; +if a group stays small, keep it as a group rather than adding deeper +folders. + +## ADR Index + +| Group | Status | Title | Short description | Link | +| -------------------- | ---------- | ----------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | +| Analysis and fitting | Accepted | Fit Mode Categories and Fit Execution API | Splits fitting configuration from execution and defines active sibling fit-mode categories. | [`fit-mode-categories.md`](accepted/fit-mode-categories.md) | +| Analysis and fitting | Accepted | Runtime Fit Results | Keeps full fit outputs runtime-only in the current design unless a narrower persistence ADR is accepted. | [`runtime-fit-results.md`](accepted/runtime-fit-results.md) | +| Analysis and fitting | Accepted | Analysis CIF Fit State | Defines the persisted fit-state projection in `analysis/analysis.cif` and `analysis/results.h5`. | [`analysis-cif-fit-state.md`](accepted/analysis-cif-fit-state.md) | +| Analysis and fitting | Accepted | Parameter Correlation Persistence | Persists deterministic and posterior correlation summaries in `_fit_parameter_correlation` | [`parameter-correlation-persistence.md`](accepted/parameter-correlation-persistence.md) | +| Analysis and fitting | Suggestion | Fit Output Files and Data Exports | Narrows remaining archive/export questions after adopting `results.csv` and `results.h5`. | [`fit-output-files-and-data-exports.md`](suggestions/fit-output-files-and-data-exports.md) | +| Analysis and fitting | Suggestion | Parameter-Level Posterior Projection | Narrows the still-open `parameter.posterior` API after analysis-level posterior summaries were accepted. | [`parameter-posterior-summary.md`](suggestions/parameter-posterior-summary.md) | +| Analysis and fitting | Suggestion | Undo Fit | Builds rollback semantics and CLI behavior on already-persisted pre-fit scalar snapshots. | [`undo-fit.md`](suggestions/undo-fit.md) | +| Core model | Accepted | Category Owners and Real Datablocks | Introduces `CategoryOwner` so singleton sections do not pretend to be real CIF datablocks. | [`category-owner-sections.md`](accepted/category-owner-sections.md) | +| Core model | Accepted | Enum-Backed Closed Value Sets | Requires finite option sets to use `(str, Enum)` classes for validation and dispatch. | [`enum-backed-closed-values.md`](accepted/enum-backed-closed-values.md) | +| Core model | Accepted | Guarded Public Properties | Uses property setters as the public writability contract for guarded objects. | [`guarded-public-properties.md`](accepted/guarded-public-properties.md) | +| Core model | Accepted | Two-Level Category Parameter Access | Keeps parameter access to `datablock.category.parameter` or `datablock.collection[id].parameter`. | [`category-parameter-access.md`](accepted/category-parameter-access.md) | +| Documentation | Accepted | Descriptor Property Docstring Template | Makes descriptor metadata the source of truth for public property docstrings and annotations. | [`property-docstring-template.md`](accepted/property-docstring-template.md) | +| Documentation | Accepted | Development Documentation Structure | Defines the `docs/dev` layout for ADRs, issues, plans, package structure, and roadmap. | [`development-docs-structure.md`](accepted/development-docs-structure.md) | +| Documentation | Accepted | Help Method Discoverability | Requires primary public objects and facades to expose consistent `help()` output. | [`help-discoverability.md`](accepted/help-discoverability.md) | +| Documentation | Accepted | Notebook Generation Source of Truth | Treats tutorial `.py` files as editable sources and notebooks as generated artifacts. | [`notebook-generation.md`](accepted/notebook-generation.md) | +| Experiment model | Accepted | Immutable Experiment Type | Makes experiment type axes creation-time state rather than mutable runtime state. | [`immutable-experiment-type.md`](accepted/immutable-experiment-type.md) | +| Factories | Accepted | Factory Contracts and Metadata | Standardizes factory construction, metadata, compatibility, and registration behavior. | [`factory-contracts.md`](accepted/factory-contracts.md) | +| Naming | Accepted | Factory Tag Naming | Defines canonical factory tag style and standard abbreviations. | [`factory-tag-naming.md`](accepted/factory-tag-naming.md) | +| Persistence | Accepted | Free-Flag CIF Encoding | Encodes fit free/fixed state through CIF uncertainty syntax instead of a separate free list. | [`free-flag-cif-encoding.md`](accepted/free-flag-cif-encoding.md) | +| Persistence | Accepted | Loop Category Keys and Identity Naming | Documents loop collection keys and naming rules aligned with CIF category keys. | [`loop-category-key-identity.md`](accepted/loop-category-key-identity.md) | +| Persistence | Accepted | Project Facade and Persistence Layout | Documents the current `Project` facade and saved directory layout. | [`project-facade-and-persistence.md`](accepted/project-facade-and-persistence.md) | +| Persistence | Suggestion | Python and CIF Category Correspondence | Compares current Python paths and CIF tags, then proposes scoped one-to-one mapping for project-level categories. | [`python-cif-category-correspondence.md`](suggestions/python-cif-category-correspondence.md) | +| Quality | Accepted | Lint Complexity Thresholds | Treats ruff PLR complexity limits as design guardrails that should not be bypassed. | [`lint-complexity-thresholds.md`](accepted/lint-complexity-thresholds.md) | +| Quality | Accepted | Test Strategy | Defines layered unit, functional, integration, script, and notebook testing. | [`test-strategy.md`](accepted/test-strategy.md) | +| Structure model | Accepted | Type-Neutral ADP Parameters | Keeps ADP parameter object identities stable across B/U and iso/ani switches. | [`type-neutral-adp-parameters.md`](accepted/type-neutral-adp-parameters.md) | +| User-facing API | Accepted | Display UX Facade | Defines `project.display` and `project.rendering` responsibilities and display method names. | [`display-ux.md`](accepted/display-ux.md) | +| User-facing API | Accepted | Selector Families | Distinguishes backend selectors, switchable-category selectors, and active-sibling selectors. | [`selector-families.md`](accepted/selector-families.md) | +| User-facing API | Accepted | String Paths and Live Descriptors | Separates persisted field selectors from references to live model parameters. | [`string-paths-and-live-descriptors.md`](accepted/string-paths-and-live-descriptors.md) | +| User-facing API | Accepted | Switchable Category API | Places multi-type category selectors on the owner and omits public selectors for fixed or single-type categories. | [`switchable-category-api.md`](accepted/switchable-category-api.md) | diff --git a/docs/dev/adrs/suggestions/fit-output-files-and-data-exports.md b/docs/dev/adrs/suggestions/fit-output-files-and-data-exports.md new file mode 100644 index 000000000..8705fe355 --- /dev/null +++ b/docs/dev/adrs/suggestions/fit-output-files-and-data-exports.md @@ -0,0 +1,172 @@ +# ADR: Fit Output Files and Data Exports + +**Status:** Proposed **Date:** 2026-05-18 + +## Status Note + +The current branch already adopts two pieces of this naming scheme: + +- sequential deterministic results stay in `analysis/results.csv` +- Bayesian arrays and plot caches use `analysis/results.h5` + +Those decisions now live in +[Analysis CIF Fit State](../accepted/analysis-cif-fit-state.md). This +proposal is therefore narrowed to the still-open roles for +`analysis/data.h5`, `analysis/exports/`, and any extra deterministic +convenience exports. + +## Context + +Different fit modes still produce different kinds of reusable output: + +- sequential deterministic fits produce a rectangular parameter + evolution table, already saved as `analysis/results.csv` +- Bayesian fits produce posterior samples, diagnostics, predictive + arrays, and plot caches, which are too large and structured for CIF or + CSV +- deterministic single and joint fits produce fitted model state, + calculated data, reflection tables, residuals, and optional + covariance/correlation summaries + +The accepted fit-state ADR already standardizes the canonical saved fit +projection in `analysis/analysis.cif` plus `analysis/results.h5` for +Bayesian sidecars. What remains open here is whether project save should +also produce optional archives or user-facing export files beyond that +accepted baseline. + +The project should keep naming consistent and avoid making users extract +ordinary plotting data from CIF when a clearer CSV export is possible. +At the same time, CIF remains the canonical model/configuration format, +and large numerical arrays should not be embedded in +`analysis/analysis.cif`. + +## Decision + +### 1. Keep the implemented results baseline + +The accepted baseline is: + +- `analysis/results.csv` for sequential deterministic fit tables +- `analysis/results.h5` for large Bayesian arrays and result-derived + caches + +Any future change to those canonical filenames would need a follow-up +ADR. + +### 2. Reserve separate roles for archives and exports + +If extra persisted files are added under `analysis/`, keep their roles +separate: + +- `analysis/data.h5` for optional archived input or measured data. +- `analysis/exports/` for optional user-facing CSV files intended for + external plotting and inspection. + +The fit type and saved fit-state manifests stay recorded in +`analysis/analysis.cif`, principally through `_fit_result.result_kind` +and the related fit-state categories. + +### 3. Sequential deterministic results stay CSV + +Sequential deterministic fitting should keep `analysis/results.csv` as +the canonical table for parameter evolution and extracted metadata. + +This file is intentionally CSV because: + +- each row naturally corresponds to one sequential fit step +- users often inspect, filter, and plot it outside EasyDiffraction +- it should remain easy to diff, copy, and load in spreadsheets + +Sequential measured input data may optionally be archived in +`analysis/data.h5`, but that archive is data, not results. It must not +replace `analysis/results.csv`. + +### 4. Bayesian arrays use `analysis/results.h5` + +Single Bayesian fits should store posterior samples, log posterior +arrays, predictive arrays, and prepared plot caches in +`analysis/results.h5`. + +The previous candidate name `analysis/bayesian_data.h5` remains rejected +because it mixes fit type with file role and blurs result arrays with +input data. Bayesian-specific meaning belongs in the CIF manifest and +HDF5 groups, not the sidecar filename. + +### 5. Deterministic single and joint fits may gain CSV exports + +For single, joint, and sequential deterministic fits, EasyDiffraction +should consider optional CSV exports for ordinary plotting data: + +- measured data +- calculated data +- residuals +- reflection tables / `refln` categories + +These exports are not canonical persistence. They are convenience files +for users who want to plot or analyze results in external software +without parsing CIF. + +Suggested first layout: + +```text +analysis/ + analysis.cif + results.csv # sequential deterministic only, when applicable + results.h5 # Bayesian and other structured result arrays + data.h5 # optional archived measured/input data + exports/ + _measured.csv + _calculated.csv + _residual.csv + _reflections.csv +``` + +## Fit-Type Mapping + +| Fit type | Canonical fit state | Tabular results | Large arrays / caches | Optional data archive | Optional exports | +| ------------------------ | ------------------------------------------------ | ---------------------------- | --------------------- | --------------------- | ------------------------------- | +| single deterministic | `analysis/analysis.cif` | open question | none initially | none initially | `analysis/exports/*.csv` | +| joint deterministic | `analysis/analysis.cif` | open question | none initially | none initially | `analysis/exports/*.csv` | +| sequential deterministic | `analysis/analysis.cif` + `analysis/results.csv` | `analysis/results.csv` | none initially | `analysis/data.h5` | `analysis/exports/*.csv` | +| single Bayesian | `analysis/analysis.cif` + `analysis/results.h5` | optional summary export only | `analysis/results.h5` | none initially | optional summary/predictive CSV | + +## Open Questions + +- Should single and joint deterministic fits write a one-row + `analysis/results.csv`, or is their result projection in + `analysis/analysis.cif` enough? +- Should CSV exports be written automatically after fit/save, or only by + an explicit export command? +- What exact CSV column schemas should be used for measured, calculated, + residual, and reflection exports? +- Should exported `refln` CSVs mirror CIF tag names exactly, or use + shorter user-facing column names? +- Should sequential measured data archival in `analysis/data.h5` be + opt-in, automatic below a size threshold, or always disabled unless + requested? +- What size threshold and compression policy should control the optional + `analysis/data.h5`, and does `analysis/results.h5` need a matching + convention? +- Should external CSV exports be regenerated from canonical CIF/HDF5 on + demand rather than stored persistently? + +## Consequences + +### Positive + +- Fit output filenames become role-based and consistent across fit + types. +- Sequential parameter evolution keeps its simple CSV workflow. +- Bayesian arrays get a generic result sidecar name that can also serve + future structured result types. +- External plotting data can be exposed as plain CSV without weakening + CIF as the canonical model format. + +### Trade-offs + +- The project gains more output-file roles under `analysis/`, so save + and cleanup behavior must be explicit. +- Export CSVs are derived data and must be invalidated or regenerated + when the project changes. +- HDF5 archives introduce size and compression choices that should be + resolved before implementation. diff --git a/docs/dev/adrs/suggestions/parameter-posterior-summary.md b/docs/dev/adrs/suggestions/parameter-posterior-summary.md new file mode 100644 index 000000000..36c0b979b --- /dev/null +++ b/docs/dev/adrs/suggestions/parameter-posterior-summary.md @@ -0,0 +1,337 @@ +# ADR: Parameter-Level Posterior Projection + +**Status:** Proposed **Date:** 2026-05-13 + +## Status Note + +This proposal is narrower than its original draft. The accepted +[Analysis CIF Fit State](../accepted/analysis-cif-fit-state.md) ADR now +persists analysis-level posterior summaries in +`_bayesian_parameter_posterior`, and current Bayesian fits already +commit the best posterior sample to `parameter.value`. What remains +undecided is whether parameters should also expose a convenience +`parameter.posterior` projection and dedicated internal helpers for +updating fit-derived metadata atomically. + +## Context + +`GenericParameter` already stores fit-adjacent helper data such as +`uncertainty`, `fit_min`, and `fit_max`, while `value` remains the +single scalar used by calculations. + +Bayesian DREAM currently keeps posterior state only on +`analysis.fit_results` via `BayesianFitResults`, including +`posterior_samples`, `posterior_parameter_summaries`, +`posterior_predictive`, diagnostics, and sampler settings. The accepted +runtime-fit-results ADR originally described this state as runtime-only, +but the accepted fit-state ADR now persists analysis-level posterior +summaries and cache manifests as a narrower saved projection. + +`analysis.fit_results` already changes by analysis type: deterministic +fits use `FitResults`, while posterior-capable fits such as DREAM use +`BayesianFitResults`. This ADR preserves that result-model split. + +The user-facing need is more local: when a posterior-capable fit has +completed, each parameter should expose a compact Bayesian summary for +inspection without forcing users to traverse `analysis.fit_results`. At +the same time, `parameter.value` must remain the only scalar used by the +live model, minimizer setup, constraints, and category updates. + +There is also a staleness problem. After a manual parameter edit or a +new fit, fit-derived helper data must be cleared or replaced as a group. +Keeping old posterior summaries after the active parameter state changes +would mislead users. + +One more implementation constraint matters: minimizers currently apply +final fitted values through `_set_value_from_minimizer(...)` and then +write `param.uncertainty = ...` directly. If `uncertainty` and posterior +metadata become read-only fit outputs and the public `value` setter +starts clearing stale fit metadata on manual edits, fit-result +application must move to a dedicated internal update path so that valid +fit outputs are installed atomically instead of being cleared +accidentally. + +## Decision + +### 1. Add one optional posterior object to each parameter + +Each `GenericParameter` will gain a read-only `posterior` property whose +default value is `None`. + +This property is convenience metadata only. It does not participate in +calculations. `parameter.value` remains the only scalar used by the live +model. + +### 2. Reuse the existing Bayesian summary container + +Do not add separate flat parameter attributes such as `median`, +`best_sample_value`, `interval_95`, `r_hat`, or `ess_bulk`. + +Instead, the parameter-level projection reuses the existing +`PosteriorParameterSummary` object already produced for +`BayesianFitResults`. This keeps one grouped summary shape for display, +inspection, and restore from persisted analysis state. + +The summary object currently provides the right level of detail: + +- `best_sample_value` +- `median` +- `uncertainty` +- `interval_68` +- `interval_95` +- `r_hat` +- `ess_bulk` + +`unique_name` and `display_name` remain redundant but acceptable when +the object is attached to a parameter. + +Different communities use different names for this scalar spread term: +`standard deviation`, `estimated standard deviation` (`e.s.d.`), and +`standard uncertainty` (`s.u.` / `su`). The public EasyDiffraction API +selects `uncertainty` as the canonical name because it already matches +the existing parameter API, aligns better with CIF terminology, and can +be used consistently for both deterministic and Bayesian results. For +Bayesian summaries, this `uncertainty` value is computed from the +posterior standard deviation. + +The internal field names stay compact and code-oriented. User-facing +tables, summaries, and plot annotations should use these friendly +labels: + +- `best_sample_value` -> `Best posterior sample` +- `median` -> `Median` +- `uncertainty` -> `Standard uncertainty` +- `interval_68` -> `68% credible interval` +- `interval_95` -> `95% credible interval` +- `r_hat` -> `R-hat` +- `ess_bulk` -> `Bulk ESS` + +Asymmetric interval information remains explicit through the stored +lower and upper bounds. For example, `parameter.posterior.interval_95` +returns a `(lower, upper)` tuple rather than one symmetric uncertainty +value. + +Example user access: + +```python +param = project.phases['lbco'].cell.length_a + +current_value = param.value +current_uncertainty = param.uncertainty + +if param.posterior is not None: + best_sample_value = param.posterior.best_sample_value + median = param.posterior.median + uncertainty = param.posterior.uncertainty + low95, high95 = param.posterior.interval_95 + r_hat = param.posterior.r_hat + ess_bulk = param.posterior.ess_bulk +``` + +### 3. Make fit outputs read-only for the user + +`uncertainty` becomes a read-only fit output on the public parameter +API. `posterior` is also read-only. + +Users may inspect these properties, but only internal fit-application +paths may set or clear them. + +This keeps the writable public parameter state focused on user intent: +`value`, `free`, `fit_min`, `fit_max`, and related configuration. + +### 4. Populate it only for posterior-capable fit methods + +Only fit methods that actually produce posterior summaries may populate +`parameter.posterior`. + +At present this means only `bumps (dream)`. + +Do not add a generic minimizer-capability abstraction yet. That would +introduce a new abstraction before there is a second concrete posterior +fit method. When another posterior-capable minimizer exists, the shared +contract can be extracted then. + +### 5. Treat parameter-level posterior data as a projection, not the canonical result + +The canonical Bayesian result remains `analysis.fit_results`. + +`parameter.posterior` is a synchronized projection of the current fit +result for user convenience. It must never be the only place where +Bayesian information lives, because it cannot represent joint posterior +arrays, predictive summaries, or cross-parameter correlations. + +### 6. Clear or replace fit-derived metadata as a group + +Fit-derived helper data on a parameter must be coherent. + +The following policy applies: + +- Manual edit of `parameter.value` clears `parameter.uncertainty` and + `parameter.posterior`. +- A deterministic fit replaces `value` and `uncertainty`, then clears + `posterior`. +- A posterior fit replaces `value`, `uncertainty`, and `posterior` + together. + +Manual user edits of `uncertainty` and `posterior` are not supported, +because both are read-only fit outputs. + +Configuration attributes such as `free`, `fit_min`, `fit_max`, units, +and `fit_bounds_uncertainty_multiplier` are not cleared by this policy. +They are parameter configuration or user intent, not posterior output. + +### 7. Add a dedicated internal fit-application path + +To support the clearing policy above, fit application must not rely on +public setters alone. + +Implementation should add private helpers on `GenericParameter` to set +and clear fit-derived state explicitly, for example: + +- `_set_uncertainty(...)` +- `_set_posterior(...)` +- `_clear_fit_metadata()` +- `_apply_deterministic_fit_update(...)` +- `_apply_posterior_fit_update(...)` + +The exact helper names can be refined during implementation, but the +design requirement is fixed: manual user edits clear stale metadata, +while internal fit application installs fresh metadata atomically. + +### 8. Commit best posterior sample to `parameter.value` after Bayesian fits + +After a posterior-capable fit, `parameter.value` is committed from the +best posterior sample. + +Current DREAM support already follows this rule. + +The best posterior sample is chosen because it is a coherent joint point +estimate across all free parameters. Marginal medians remain available +on `parameter.posterior`, but they are summary data rather than the +active live model state. + +### 9. Keep `uncertainty` as a convenience scalar after Bayesian fits + +`parameter.uncertainty` remains a single convenience scalar even after +posterior fits. + +For posterior-capable fits, populate it from posterior standard +deviation and expose it as `uncertainty`. This preserves one scalar API +term across deterministic and Bayesian results while keeping the +calculation itself statistically conventional. + +Asymmetric interval information is not squeezed into +`parameter.uncertainty`; it remains available only via +`parameter.posterior`. + +### 10. Rebuild posterior from analysis-level state + +Canonical Bayesian state is owned by `analysis.fit_results`, not by +individual parameters. The saved fit-state format and restore order are +defined in `analysis-cif-fit-state.md`. + +The accepted fit-state ADR currently rebuilds analysis-level posterior +state only. This proposal would add a parameter-level convenience +projection on top of that restored analysis state. + +`parameter.posterior` is never serialized as a per-parameter property. +It is rebuilt from the analysis-level saved result projection when that +projection is available. + +Two restore levels matter for the parameter API: + +- summary-only restore can populate `parameter.posterior` and fit-result + tables +- full restore can also support posterior plots and predictive plots + +If the saved project has no analysis-level posterior summary for a +parameter, `parameter.posterior` remains `None`. + +Example user access after load: + +```python +project = Project.load('/path/to/project') + +param = project.phases['lbco'].cell.length_a +posterior = param.posterior + +if posterior is not None: + print(posterior.best_sample_value) + print(posterior.uncertainty) + print(posterior.interval_68) + +project.analysis.display.fit_results() +``` + +Posterior plot availability after load follows the fit-state restore +level defined in `analysis-cif-fit-state.md`. + +### 11. Keep parameter posterior data rebuilt, not duplicated + +`parameter.posterior` is always rebuilt from analysis-level fit state. + +Do not serialize posterior summaries again inside each parameter's own +CIF representation. Duplicating the same posterior summary data across +structure and experiment files would create multiple sources of truth. + +## Consequences + +### Positive + +- Users get a compact Bayesian overview directly from each parameter. +- The parameter API stays tidy because posterior helpers are grouped + into one optional object rather than many flat attributes. +- `uncertainty` and posterior metadata become clearly fit-owned rather + than mixed user-editable and fit-editable state. +- `analysis.fit_results` remains the canonical runtime source for full + Bayesian state. +- Partial restore can still expose parameter summaries when + analysis-level saved state contains them. +- The design matches the current rule that `value` is the only active + scalar used for calculations. + +### Trade-offs + +- `GenericParameter` now holds an optional reference to analysis-derived + metadata. +- Any manual user edit invalidates fit-derived metadata more eagerly + than today. +- `uncertainty` becomes a read-only public property, so fit-result + application code must be updated to use dedicated internal helpers + rather than a mix of `_set_value_from_minimizer(...)` and public + uncertainty assignment. + +## Layering and Ownership + +To keep `core/` free of Bayesian computations, the parameter object must +not compute posterior summaries itself. + +The summary object is created by posterior-capable fit code and then +attached to parameters as already-computed metadata. `core/variable.py` +should avoid eager runtime imports of Bayesian helper modules; a +type-only import is acceptable. + +## Deferred Work + +This ADR defines the parameter-level posterior projection. It defers: + +- persistence for future posterior-capable minimizers beyond DREAM +- enabling currently unsupported single-crystal predictive draw plots + +## Implementation Notes + +- The first implementation should only populate `parameter.posterior` + for DREAM. +- Existing `PosteriorParameterSummary` instances should be reused rather + than copied into a second summary type unless implementation reveals a + concrete layering problem that cannot be resolved cleanly. +- `parameter.posterior` should always be rebuilt from analysis-level + Bayesian data rather than serialized redundantly at parameter level. + +## Chosen Defaults + +- `parameter.value` remains committed to the best posterior sample after + posterior fits. +- If a project is loaded with only posterior summaries, restoring + `parameter.posterior` is acceptable for table display and parameter + inspection. diff --git a/docs/dev/adrs/suggestions/python-cif-category-correspondence.md b/docs/dev/adrs/suggestions/python-cif-category-correspondence.md new file mode 100644 index 000000000..191f18e59 --- /dev/null +++ b/docs/dev/adrs/suggestions/python-cif-category-correspondence.md @@ -0,0 +1,338 @@ +# ADR: Python and CIF Category Correspondence + +**Status:** Proposed +**Date:** 2026-05-17 + +## Context + +EasyDiffraction exposes a Python object graph and persists state in CIF +files. The public Python API should be easy for scientists to predict, +while CIF output should remain readable and semantically useful. + +The current public root is `Project`, and project-level configuration is +saved in: + +```text +project.cif +``` + +Inside that file, generic category names such as `_info.*`, +`_rendering.*`, and `_verbosity.*` are less ambiguous than they would be +in a single monolithic CIF file. This opens the option of a strict +one-to-one correspondence for project-owned singleton categories: + +```text +project.info.title -> project.cif: _info.title +project.rendering.engine -> project.cif: _rendering.engine +project.verbosity.fit -> project.cif: _verbosity.fit +``` + +The design question is whether this rule should be applied only to +project-level configuration, or more broadly across analysis, +experiments, structures, and calculated data. + +The accepted project-facade decision keeps `Project` as the public root +and keeps `project.cif` as the singleton project configuration file. It +also keeps `_project.*` as the semantic CIF category for scientific +project information and rejects `_meta.*` for that purpose. This ADR +therefore must not reintroduce the rejected `Workspace` rename, +`workspace.cif`, or `_meta.project_*` tags as incidental cleanup. + +## Scope Of Comparison + +The comparison below is category-level and public-API oriented. It lists +all currently persisted Python category surfaces found in the source +code, with complete field sets where the category is small and compact +field groups where a CIF loop has many repeated parameters. + +Shorthand names such as `analysis`, `experiment`, and `structure` refer +to objects reached from the current `Project` root, for example +`project.analysis`, `project.experiments[name]`, and +`project.structures[name]`. + +## Current Persistence Layout + +| Current Python surface | Current saved location | Current CIF block form | Notes | +| ----------------------------------- | ------------------------ | ---------------------- | ----------------------------------------------------------------------------------- | +| `project.info`, `project.rendering` | `project.cif` | bare categories | Project-level singleton config. | +| `project.verbosity` | `project.cif` | bare category | Project-owned fit-output verbosity category backed by `VerbosityEnum`. | +| `project.structures[name]` | `structures/.cif` | `data_` | Each structure is one CIF data block. | +| `project.experiments[name]` | `experiments/.cif` | `data_` | Each experiment is one CIF data block. | +| `project.analysis` | `analysis/analysis.cif` | bare categories | Loader also accepts legacy root-level `analysis.cif`. | +| `project.summary` | `summary.cif` | placeholder text | Summary persistence exists as a file but `summary_to_cif()` is not implemented yet. | + +## Current Correspondence + +### Project-Level Configuration + +| Current Python path | Current CIF path | Match? | Notes | +| -------------------------------- | ------------------------- | ------ | -------------------------------------------------------------------------------------------------- | +| `project.info.name` | `_project.id` | No | Python uses user-facing `name`; CIF uses `id`; category is `info` in Python but `_project` in CIF. | +| `project.info.title` | `_project.title` | Partly | Field name matches, category name does not. | +| `project.info.description` | `_project.description` | Partly | Field name matches, category name does not. | +| `project.info.created` | `_project.created` | Partly | Field name matches, category name does not. | +| `project.info.last_modified` | `_project.last_modified` | Partly | Field name matches, category name does not. | +| `project.info.path` | none | No | Runtime storage path, not a CIF field. | +| `project.rendering.chart_engine` | `_rendering.chart_engine` | Yes | Direct category and field mapping. | +| `project.rendering.table_engine` | `_rendering.table_engine` | Yes | Direct category and field mapping. | +| `project.verbosity.fit` | `_verbosity.fit` | Yes | Direct category and field mapping for fitting process output verbosity. | + +### Analysis Configuration + +| Current Python path | Current CIF path | Match? | Notes | +| ------------------------------------------------- | ---------------------------------- | ------ | ------------------------------------------------------------------------------------------------ | +| `analysis.fitting.minimizer_type` | `_fitting.minimizer_type` | Yes | Direct category mapping. | +| `analysis.fitting_mode_type` | `_fitting.mode_type` | No | Public selector is owner-level state serialized into the `_fitting` category. | +| `analysis.joint_fit[experiment_id].experiment_id` | `_joint_fit.experiment_id` | Yes | Collection key is also stored as a field. | +| `analysis.joint_fit[experiment_id].weight` | `_joint_fit.weight` | Yes | Direct field mapping. | +| `analysis.sequential_fit.data_dir` | `_sequential_fit.data_dir` | Yes | Direct category mapping. | +| `analysis.sequential_fit.file_pattern` | `_sequential_fit.file_pattern` | Yes | Direct category mapping. | +| `analysis.sequential_fit.max_workers` | `_sequential_fit.max_workers` | Yes | Direct category mapping. | +| `analysis.sequential_fit.chunk_size` | `_sequential_fit.chunk_size` | Yes | Direct category mapping. | +| `analysis.sequential_fit.reverse` | `_sequential_fit.reverse` | Yes | Direct category mapping. | +| `analysis.sequential_fit_extract[id].id` | `_sequential_fit_extract.id` | Yes | Direct collection mapping. | +| `analysis.sequential_fit_extract[id].target` | `_sequential_fit_extract.target` | Yes | Direct collection mapping. | +| `analysis.sequential_fit_extract[id].pattern` | `_sequential_fit_extract.pattern` | Yes | Direct collection mapping. | +| `analysis.sequential_fit_extract[id].required` | `_sequential_fit_extract.required` | Yes | Direct collection mapping. | +| `analysis.aliases[label].label` | `_alias.label` | Partly | Python collection is plural; CIF row category is singular. | +| `analysis.aliases[label].param_unique_name` | `_alias.param_unique_name` | Partly | Python collection is plural; CIF row category is singular. | +| `analysis.constraints[lhs_alias].expression` | `_constraint.expression` | Partly | Collection key is derived from the expression; there is no separate `_constraint.lhs_alias` tag. | + +### Experiment Configuration + +| Current Python path | Current CIF path | Match? | Notes | +| --------------------------------------------- | ---------------------------------------------------------------------------------- | ------ | ------------------------------------------------------------------------------------ | +| `experiment.type.sample_form` | `_expt_type.sample_form` | Partly | Python uses the user-facing word `type`; CIF uses abbreviated `_expt_type`. | +| `experiment.type.beam_mode` | `_expt_type.beam_mode` | Partly | Python uses the user-facing word `type`; CIF uses abbreviated `_expt_type`. | +| `experiment.type.radiation_probe` | `_expt_type.radiation_probe` | Partly | Python uses the user-facing word `type`; CIF uses abbreviated `_expt_type`. | +| `experiment.type.scattering_type` | `_expt_type.scattering_type` | Partly | Python uses the user-facing word `type`; CIF uses abbreviated `_expt_type`. | +| `experiment.calculation.calculator_type` | `_calculation.calculator_type` | Yes | Direct category mapping. | +| `experiment.diffrn.ambient_temperature` | `_diffrn.ambient_temperature` | Yes | Direct category mapping. | +| `experiment.diffrn.ambient_pressure` | `_diffrn.ambient_pressure` | Yes | Direct category mapping. | +| `experiment.diffrn.ambient_magnetic_field` | `_diffrn.ambient_magnetic_field` | Yes | Direct category mapping. | +| `experiment.diffrn.ambient_electric_field` | `_diffrn.ambient_electric_field` | Yes | Direct category mapping. | +| `experiment.instrument.setup_wavelength` | `_instr.wavelength` | Partly | Python name exposes setup role; CIF tag uses compact instrument name. | +| `experiment.instrument.calib_twotheta_offset` | `_instr.2theta_offset` | Partly | Python name exposes calibration role; CIF tag uses compact instrument name. | +| `experiment.instrument.setup_twotheta_bank` | `_instr.2theta_bank` | Partly | Python name exposes setup role; CIF tag uses compact instrument name. | +| `experiment.instrument.calib_d_to_tof_offset` | `_instr.d_to_tof_offset` | Partly | Python name exposes calibration role; CIF tag uses compact instrument name. | +| `experiment.instrument.calib_d_to_tof_linear` | `_instr.d_to_tof_linear` | Partly | Python name exposes calibration role; CIF tag uses compact instrument name. | +| `experiment.instrument.calib_d_to_tof_quad` | `_instr.d_to_tof_quad` | Partly | Python name exposes calibration role; CIF tag uses compact instrument name. | +| `experiment.instrument.calib_d_to_tof_recip` | `_instr.d_to_tof_recip` | Partly | Python name exposes calibration role; CIF tag uses compact instrument name. | +| `experiment.peak.profile_type` | `_peak.profile_type` | Yes | The active peak category stores its own type tag. | +| `experiment.peak_profile_type` | `_peak.profile_type` | No | Public selector is an owner-level convenience alias. | +| `experiment.peak.broad_gauss_u` | `_peak.broad_gauss_u` | Yes | CWL peak field. | +| `experiment.peak.broad_gauss_v` | `_peak.broad_gauss_v` | Yes | CWL peak field. | +| `experiment.peak.broad_gauss_w` | `_peak.broad_gauss_w` | Yes | CWL peak field. | +| `experiment.peak.broad_lorentz_x` | `_peak.broad_lorentz_x` | Yes | CWL peak field. | +| `experiment.peak.broad_lorentz_y` | `_peak.broad_lorentz_y` | Yes | CWL peak field. | +| `experiment.peak.asym_empir_1..4` | `_peak.asym_empir_1..4` | Yes | CWL peak field group. | +| `experiment.peak.asym_fcj_1..2` | `_peak.asym_fcj_1..2` | Yes | CWL peak field group. | +| `experiment.peak.broad_gauss_sigma_0..2` | `_peak.gauss_sigma_0..2` | Partly | Python prefixes the family with `broad_`; CIF tags omit that grouping prefix. | +| `experiment.peak.broad_lorentz_gamma_0..2` | `_peak.lorentz_gamma_0..2` | Partly | Python prefixes the family with `broad_`; CIF tags omit that grouping prefix. | +| `experiment.peak.exp_rise_alpha_0..1` | `_peak.rise_alpha_0..1` | Partly | Python prefixes the family with `exp_`; CIF tags omit that grouping prefix. | +| `experiment.peak.exp_decay_beta_0..1` | `_peak.decay_beta_0..1` | Partly | Python prefixes the family with `exp_`; CIF tags omit that grouping prefix. | +| `experiment.peak.dexp_*` | `_peak.dexp_*` | Yes | TOF double-exponential peak field group. | +| `experiment.peak.damp_q` | `_peak.damp_q` | Yes | Total-scattering peak field. | +| `experiment.peak.broad_q` | `_peak.broad_q` | Yes | Total-scattering peak field. | +| `experiment.peak.cutoff_q` | `_peak.cutoff_q` | Yes | Total-scattering peak field. | +| `experiment.peak.sharp_delta_1` | `_peak.sharp_delta_1` | Yes | Total-scattering peak field. | +| `experiment.peak.sharp_delta_2` | `_peak.sharp_delta_2` | Yes | Total-scattering peak field. | +| `experiment.peak.damp_particle_diameter` | `_peak.damp_particle_diameter` | Yes | Total-scattering peak field. | +| `experiment.background[id].id` line segment | `_pd_background.id` | Partly | Python category is `background`; CIF uses powder-background category. | +| `experiment.background[id].x` line segment | `_pd_background.line_segment_X` or `_pd_background_line_segment_X` | Partly | Python uses compact `x`; CIF tag encodes powder-background line-segment meaning. | +| `experiment.background[id].y` line segment | `_pd_background.line_segment_intensity` or `_pd_background_line_segment_intensity` | Partly | Python uses compact `y`; CIF tag encodes powder-background line-segment meaning. | +| `experiment.background[id].id` Chebyshev | `_pd_background.id` | Partly | Python category is `background`; CIF uses powder-background category. | +| `experiment.background[id].order` Chebyshev | `_pd_background.Chebyshev_order` | Partly | CIF tag encodes polynomial type and uses CIF-style capitalization. | +| `experiment.background[id].coef` Chebyshev | `_pd_background.Chebyshev_coef` | Partly | CIF tag encodes polynomial type and uses CIF-style capitalization. | +| `experiment.background_type` | implied by active background category | No | There is no standalone selector tag. | +| `experiment.extinction.model` | `_extinction.model` | Yes | Direct category mapping. | +| `experiment.extinction.mosaicity` | `_extinction.mosaicity` | Yes | Direct category mapping. | +| `experiment.extinction.radius` | `_extinction.radius` | Yes | Direct category mapping. | +| `experiment.extinction_type` | `_extinction.model` | Partly | Public selector chooses the category; persisted model tag lives inside the category. | +| `experiment.linked_phases[id].id` | `_pd_phase_block.id` | Partly | Python name is user-facing; CIF tag follows powder phase-block convention. | +| `experiment.linked_phases[id].scale` | `_pd_phase_block.scale` | Partly | Python name is user-facing; CIF tag follows powder phase-block convention. | +| `experiment.linked_crystal.id` | `_sc_crystal_block.id` | Partly | Python name is user-facing; CIF tag follows single-crystal block convention. | +| `experiment.linked_crystal.scale` | `_sc_crystal_block.scale` | Partly | Python name is user-facing; CIF tag follows single-crystal block convention. | +| `experiment.excluded_regions[id].id` | `_excluded_region.id` | Partly | Python collection is plural; CIF row category is singular. | +| `experiment.excluded_regions[id].start` | `_excluded_region.start` | Partly | Python collection is plural; CIF row category is singular. | +| `experiment.excluded_regions[id].end` | `_excluded_region.end` | Partly | Python collection is plural; CIF row category is singular. | + +### Experiment Data And Calculated Results + +| Current Python path | Current CIF path | Match? | Notes | +| ---------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | ------ | -------------------------------------------------------------------- | +| `experiment.data[point_id].point_id` | `_pd_data.point_id` | Partly | Python row key maps to a powder-data CIF tag. | +| `experiment.data[point_id].d_spacing` Bragg powder | `_pd_proc.d_spacing` | Partly | Python name is analysis-oriented; CIF tag is processed powder data. | +| `experiment.data[point_id].intensity_meas` Bragg powder | `_pd_meas.intensity_total` or `_pd_proc.intensity_norm` | Partly | CIF can use measured or processed intensity tags. | +| `experiment.data[point_id].intensity_meas_su` Bragg powder | `_pd_meas.intensity_total_su` or `_pd_proc.intensity_norm_su` | Partly | CIF can use measured or processed uncertainty tags. | +| `experiment.data[point_id].intensity_calc` Bragg powder | `_pd_calc.intensity_total` | Partly | Python name is analysis-oriented; CIF tag is calculated powder data. | +| `experiment.data[point_id].intensity_bkg` Bragg powder | `_pd_calc.intensity_bkg` | Partly | Python name is analysis-oriented; CIF tag is calculated background. | +| `experiment.data[point_id].calc_status` Bragg powder | `_pd_data.refinement_status` | Partly | Python says calculation status; CIF tag says refinement status. | +| `experiment.data[point_id].two_theta` Bragg powder | `_pd_proc.2theta_scan` or `_pd_meas.2theta_scan` | Partly | CIF can use processed or measured x-coordinate tags. | +| `experiment.data[point_id].time_of_flight` Bragg powder | `_pd_meas.time_of_flight` | Partly | CIF tag is measurement-specific. | +| `experiment.data[point_id].r` total powder | `_pd_proc.r` | Partly | Current code comments say PDF-specific CIF names are still needed. | +| `experiment.data[point_id].g_r_meas` total powder | `_pd_meas.intensity_total` | Partly | Current code comments say PDF-specific CIF names are still needed. | +| `experiment.data[point_id].g_r_meas_su` total powder | `_pd_meas.intensity_total_su` | Partly | Current code comments say PDF-specific CIF names are still needed. | +| `experiment.data[point_id].g_r_calc` total powder | `_pd_calc.intensity_total` | Partly | Current code comments say PDF-specific CIF names are still needed. | +| `experiment.data[point_id].calc_status` total powder | `_pd_data.refinement_status` | Partly | Current code comments say PDF-specific CIF names are still needed. | +| `experiment.refln[id]` single crystal | `_refln.{id,d_spacing,sin_theta_over_lambda,index_h,index_k,index_l,intensity_meas,intensity_meas_su,intensity_calc,wavelength}` | Yes | Direct reflection-loop mapping. | +| `experiment.refln[id]` powder calculated reflections | `_refln.{id,phase_id,d_spacing,sin_theta_over_lambda,index_h,index_k,index_l,f_calc,f_squared_calc,two_theta,time_of_flight}` | Yes | Direct calculated-reflection loop mapping. | + +### Structure Configuration + +| Current Python path | Current CIF path | Match? | Notes | +| ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | --------------------------------------------------------------------------------- | +| `structure.cell.length_a` | `_cell.length_a` | Yes | Direct category mapping. | +| `structure.cell.length_b` | `_cell.length_b` | Yes | Direct category mapping. | +| `structure.cell.length_c` | `_cell.length_c` | Yes | Direct category mapping. | +| `structure.cell.angle_alpha` | `_cell.angle_alpha` | Yes | Direct category mapping. | +| `structure.cell.angle_beta` | `_cell.angle_beta` | Yes | Direct category mapping. | +| `structure.cell.angle_gamma` | `_cell.angle_gamma` | Yes | Direct category mapping. | +| `structure.space_group.name_h_m` | `_space_group.name_H-M_alt`, `_space_group_name_H-M_alt`, `_symmetry.space_group_name_H-M`, or `_symmetry_space_group_name_H-M` | Partly | CIF naming follows crystallographic conventions and supports legacy alternatives. | +| `structure.space_group.it_coordinate_system_code` | `_space_group.IT_coordinate_system_code`, `_space_group_IT_coordinate_system_code`, `_symmetry.IT_coordinate_system_code`, or `_symmetry_IT_coordinate_system_code` | Partly | CIF naming follows crystallographic conventions and supports legacy alternatives. | +| `structure.atom_sites[label].label` | `_atom_site.label` | Yes | Direct row-field mapping. | +| `structure.atom_sites[label].type_symbol` | `_atom_site.type_symbol` | Yes | Direct row-field mapping. | +| `structure.atom_sites[label].fract_x` | `_atom_site.fract_x` | Yes | Direct row-field mapping. | +| `structure.atom_sites[label].fract_y` | `_atom_site.fract_y` | Yes | Direct row-field mapping. | +| `structure.atom_sites[label].fract_z` | `_atom_site.fract_z` | Yes | Direct row-field mapping. | +| `structure.atom_sites[label].wyckoff_letter` | `_atom_site.Wyckoff_letter` or `_atom_site.Wyckoff_symbol` | Partly | CIF uses capitalized/legacy Wyckoff tags. | +| `structure.atom_sites[label].occupancy` | `_atom_site.occupancy` | Yes | Direct row-field mapping. | +| `structure.atom_sites[label].adp_iso` | `_atom_site.B_iso_or_equiv` or `_atom_site.U_iso_or_equiv` | No | Python uses type-neutral ADP name; CIF uses B/U-specific tags. | +| `structure.atom_sites[label].adp_type` | `_atom_site.adp_type` | Yes | Direct row-field mapping. | +| `structure.atom_site_aniso[label].label` | `_atom_site_aniso.label` | Yes | Direct row-field mapping. | +| `structure.atom_site_aniso[label].adp_11` | `_atom_site_aniso.B_11` or `_atom_site_aniso.U_11` | No | Python uses type-neutral ADP name; CIF uses B/U-specific tags. | +| `structure.atom_site_aniso[label].adp_22` | `_atom_site_aniso.B_22` or `_atom_site_aniso.U_22` | No | Python uses type-neutral ADP name; CIF uses B/U-specific tags. | +| `structure.atom_site_aniso[label].adp_33` | `_atom_site_aniso.B_33` or `_atom_site_aniso.U_33` | No | Python uses type-neutral ADP name; CIF uses B/U-specific tags. | +| `structure.atom_site_aniso[label].adp_12` | `_atom_site_aniso.B_12` or `_atom_site_aniso.U_12` | No | Python uses type-neutral ADP name; CIF uses B/U-specific tags. | +| `structure.atom_site_aniso[label].adp_13` | `_atom_site_aniso.B_13` or `_atom_site_aniso.U_13` | No | Python uses type-neutral ADP name; CIF uses B/U-specific tags. | +| `structure.atom_site_aniso[label].adp_23` | `_atom_site_aniso.B_23` or `_atom_site_aniso.U_23` | No | Python uses type-neutral ADP name; CIF uses B/U-specific tags. | + +### Not Yet Mapped + +| Current Python path | Current CIF status | Notes | +| ------------------- | --------------------- | -------------------------------------------- | +| `project.summary` | placeholder text only | `summary_to_cif()` currently returns a stub. | + +## Decision To Discuss + +Adopt a scoped one-to-one rule for project-level configuration: + +```text +project.. -> project.cif: _. +``` + +This ADR does not propose renaming the public root object. The current +root object is already `Project`; the proposal is about category and tag +correspondence inside project-owned singleton configuration. + +The accepted baseline is: + +```text +project.info. -> project.cif: _project. +``` + +Future one-to-one correspondence work may still discuss whether the +public identity field should be `name` or `id`, whether verbosity should +gain additional coverage-specific fields, and whether rendering should +keep separate chart and table engine fields. + +Possible strict-correspondence target if a future ADR explicitly changes +the accepted `_project.*` baseline: + +| Python path | Target CIF path | Current state | +| -------------------------------- | ------------------------- | ------------------------------------------------ | +| `project.info.name` | `_info.name` | Currently `_project.id`. | +| `project.info.title` | `_info.title` | Currently `_project.title`. | +| `project.info.description` | `_info.description` | Currently `_project.description`. | +| `project.info.created` | `_info.created` | Currently `_project.created`. | +| `project.info.last_modified` | `_info.last_modified` | Currently `_project.last_modified`. | +| `project.rendering.chart_engine` | `_rendering.chart_engine` | Already matches. | +| `project.rendering.table_engine` | `_rendering.table_engine` | Already matches. | +| `project.verbosity.fit` | `_verbosity.fit` | Implemented direct fit-output verbosity mapping. | + +Alternative target if the project identity field should be called `id` +rather than `name`: + +```text +project.info.id -> _info.id +``` + +Do not force strict one-to-one correspondence globally where CIF-domain +names are clearer or where the Python API intentionally abstracts over +CIF details. + +## Rationale + +### Project-Level Categories Are Repository-Owned + +Project-level configuration categories are not external crystallographic +CIF categories. They are EasyDiffraction project-file categories, so the +repository can optimize them for API/persistence symmetry. + +### `project.cif` Scopes Generic Categories + +`_info.title` is generic in isolation, but inside `project.cif` it reads +as project information. This is similar to `_verbosity.fit`: the file +scope tells the reader this is project-level verbosity, and the field +name identifies the fitting-process coverage. + +### The Current `Project` Root Already Matches User Language + +The current public root object is already `Project`. Keeping it avoids a +broad user-facing root rename and aligns with scientific workflows where +a project is the container for structures, experiments, analysis, and +saved files. + +### `_project.*` Is More Semantic Than `_meta.*` + +The project-information category stores the scientific project identity, +title, description, and timestamps. `_project.id` and `_project.title` +say that directly, while `_meta.project_id` and `_meta.project_title` +make the CIF less domain-oriented and repeat the concept in every item +name. + +### Scientific CIF/Domain Categories Should Stay Domain-Oriented + +For structures, experiments, measured data, and calculated results, many +CIF names are standard or domain-specific. Exact Python mirroring would +make those tags less meaningful to CIF readers and could weaken +compatibility with scientific conventions. + +### Some Python Names Are Deliberate Abstractions + +Type-neutral ADP parameters, owner-level switchable selectors, +active-sibling selectors, and analysis-friendly data names intentionally +do not mirror individual CIF fields. These should remain exceptions +unless a separate ADR changes the underlying API pattern. + +## Consequences + +### Positive + +- Project-level configuration becomes easier to explain and inspect. +- Users can predict project-level CIF tags from Python paths. +- The decision can focus on project-owned singleton config without + forcing scientific CIF categories to mirror Python convenience names. + +### Trade-Offs + +- `_info.*` is less self-describing if copied out of `project.cif`. +- Existing `_project.*` project files would need migration or a + deliberate compatibility decision. +- Persisted verbosity is now a category object. The initial field is + `project.verbosity.fit`, leaving room for future coverage-specific + verbosity fields. +- Collapsing rendering to `project.rendering.engine` would simplify the + API, but only if chart and table renderers are intended to share one + backend choice. + +## Open Questions + +- Should the project identity remain `project.info.name`, or should it + become `project.info.id` to mirror the saved identifier field? +- Should `project.rendering.chart_engine` and + `project.rendering.table_engine` remain separate, or should the public + API and CIF collapse to one `engine` field? +- Should `project.verbosity = 'short'` remain as a convenience alias for + `project.verbosity.fit = 'short'`, or should strict correspondence + remove the alias? diff --git a/docs/dev/adrs/suggestions/undo-fit.md b/docs/dev/adrs/suggestions/undo-fit.md new file mode 100644 index 000000000..cd658aba9 --- /dev/null +++ b/docs/dev/adrs/suggestions/undo-fit.md @@ -0,0 +1,126 @@ +# ADR: Undo Fit + +**Status:** Proposed +**Date:** 2026-05-18 + +## Status Note + +The rollback anchors described here are already persisted and restored. +Current code saves `_fit_parameter.start_value` and +`_fit_parameter.start_uncertainty` in `analysis/analysis.cif`, and the +CLI already reserves `PROJECT_DIR undo`, but no rollback operation is +implemented yet. + +## Context + +The accepted fit-state persistence design now stores +`_fit_parameter.start_value` and `_fit_parameter.start_uncertainty` in +`analysis/analysis.cif`. Those fields capture the last committed pre-fit +scalar state for each fitted parameter and are the essential rollback +anchors for any undo feature. + +This branch also introduced project-first CLI routing and reserved a +top-level `undo` command shape, but the command is still only a +placeholder. The actual rollback semantics are still undecided. + +Parameter-level posterior access remains a separate proposal. Undo must +not depend on `parameter.posterior` existing. + +## Decision + +### 1. Add an analysis-owned `undo_fit()` operation + +The rollback operation belongs on `Analysis`: + +```python +project.analysis.undo_fit() +``` + +`Analysis` owns fit execution, fit metadata, and the persisted fit-state +projection, so it is the correct public owner. + +### 2. Initial undo scope is scalar rollback plus fit-state clear + +The first undo implementation restores each fitted parameter's saved +pre-fit scalar state and clears fit-derived state that belongs only to +the discarded fit. + +After `undo_fit()`: + +- `parameter.value` is restored from `_fit_parameter.start_value` +- `parameter.uncertainty` is restored from + `_fit_parameter.start_uncertainty` +- `analysis.fit_results` is cleared +- persisted fit-state summaries and Bayesian caches for the discarded + fit are cleared + +If a future `parameter.posterior` API exists, undo should clear that +projection too. It is not a prerequisite for the initial implementation. + +If an older saved project lacks `start_uncertainty`, clearing +`parameter.uncertainty` remains an acceptable compatibility fallback. + +### 3. Undo does not roll back user configuration + +The initial undo operation does not revert: + +- aliases +- constraints +- fit bounds +- minimizer type +- fit mode +- joint-fit weights + +These belong to analysis configuration, not fit output. + +### 4. Undo is single-level for now + +Only the latest saved pre-fit snapshot is addressable. Multi-level undo +and redo require a dedicated snapshot-history design and remain +deferred. + +### 5. CLI exposure follows the project-first command style + +The command-line surface should follow the current CLI style: + +```bash +python -m easydiffraction PROJECT_DIR undo +``` + +This command should: + +- load the saved project from `PROJECT_DIR` +- execute `project.analysis.undo_fit()` +- save the recovered state back to the same project directory by default +- support `--dry` to preview the rollback without overwriting files +- fail with a clear non-zero exit status when no usable undo snapshot is + available + +Compatibility aliases may remain if the CLI supports them, but the +project-first form is the canonical user-facing syntax. + +## Consequences + +### Positive + +- The accepted fit-state persistence already provides the minimum saved + anchors required for cross-session undo. +- Users gain a predictable recovery path after a poor fit without + needing full historical fit snapshots. +- The feature aligns naturally with saved-project workflows in both + Python and the CLI. + +### Trade-offs + +- Undo restores visible scalar parameter state, not a full historical + runtime result object. +- Older saved projects may still need the uncertainty-clearing fallback. +- Multi-level undo remains unsupported. + +## Deferred Work + +- exact restoration of previous posterior-derived displays beyond the + scalar rollback anchors +- multi-level undo and redo +- confirmation or preview UX beyond `--dry` +- any dependency on a future `parameter.posterior` API diff --git a/docs/dev/architecture.md b/docs/dev/architecture.md deleted file mode 100644 index 6cb8c67e2..000000000 --- a/docs/dev/architecture.md +++ /dev/null @@ -1,1442 +0,0 @@ -# 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 - -**Parameter access pattern.** Users reach any parameter through at most -two levels of navigation from the datablock: - -```python -# CategoryItem → Parameter -structure.cell.length_a = 3.88 -experiment.instrument.setup_wavelength = 1.494 - -# CategoryCollection[item_id] → Parameter -structure.atom_sites['Si'].adp_iso = 0.47 -experiment.background['10'].y = 170 -``` - -The general forms are: - -- `DATABLOCK.CATEGORY_ITEM.PARAMETER` -- `DATABLOCK.CATEGORY_COLLECTION[ITEM_ID].PARAMETER` - -This two-level convention (category then parameter) is a deliberate -design constraint. Categories are never nested inside other categories -(see § 9.7), which keeps the path depth uniform and the API predictable. - -| 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, symmetry_fixed -``` - -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 (powder / total only) -experiment.refln # CategoryCollection (Bragg powder + single crystal) - -# 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 = 'becker-coppens' # triggers ExtinctionFactory.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, atom_site_aniso -``` - -A `Structure` contains four categories: - -- `Cell` — unit cell parameters (`CategoryItem`) -- `SpaceGroup` — symmetry information (`CategoryItem`) -- `AtomSites` — atomic positions and isotropic ADP - (`CategoryCollection`) -- `AtomSiteAniso` — anisotropic ADP tensor components - (`CategoryCollection`) - -Symmetry constraints (cell metric, atomic coordinates, ADPs) are applied -via the `crystallography` module during `_update_categories()`. - -Parameters that are fully determined by symmetry (e.g. `lattice_b` in -cubic, `fract_y` of an atom on a 4-fold axis, off-diagonal ADPs forced -to zero by site symmetry) are flagged as `symmetry_fixed = True` on the -`Parameter`. This forces `free = False`; any subsequent attempt to set -`free = True` on such a parameter is ignored with a warning. Flags are -recomputed on every `_update_categories()` so that changing the space -group, Wyckoff letter, or ADP type re-evaluates which parameters are -fixed. Surface helpers `cell_symmetry_fixed_flags(...)` and -`atom_site_symmetry_fixed_flags(...)` in `crystallography` expose the -per-key flags. - -### 4.2 Atomic Displacement Parameters (ADP) - -ADP support covers four CIF-standard types: **Biso**, **Uiso**, -**Bani**, **Uani**. The design uses **type-neutral parameter names** so -that switching type is a one-line operation on `adp_type`. - -**Two sibling collections.** Following CIF conventions (`_atom_site` and -`_atom_site_aniso` are separate loops), isotropic and anisotropic data -live in separate collections on `Structure`. Every atom always has an -entry in both collections; when `adp_type` is isotropic, the aniso -parameters hold `0.0` and are ignored by calculators. - -**Type-neutral names.** `atom_site.adp_iso` is the type-neutral -isotropic ADP parameter. Its physical meaning (B or U) is determined by -`atom_site.adp_type`. Similarly, `atom_site_aniso.adp_11`…`adp_23` are -type-neutral tensor components. - -**Dual CIF names.** Each parameter's `CifHandler` carries both CIF name -variants (e.g. -`['_atom_site.B_iso_or_equiv', '_atom_site.U_iso_or_equiv']`). Reading -tries each name until a match is found; writing uses `names[0]`. The -`adp_type` setter reorders the list so the correct tag is emitted. - -**Auto-conversion.** Setting `adp_type` triggers value conversion: B ↔ U -via `B = 8π²U`; iso → ani seeds the diagonal; ani → iso averages the -diagonal. - -**Collection sync.** `Structure._update_categories()` reconciles the two -collections: adds missing aniso entries, removes stale ones, and rekeys -on label rename. - -See [`adp_implementation.md`](adp_implementation.md) for the full -implementation plan. - ---- - -## 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.CWL_PSEUDO_VOIGT, - frozenset({ - ('scattering_type', ScatteringTypeEnum.BRAGG), - ('beam_mode', BeamModeEnum.TIME_OF_FLIGHT), - }): PeakProfileTypeEnum.TOF_JORGENSEN, - frozenset({ - ('scattering_type', ScatteringTypeEnum.TOTAL), - }): PeakProfileTypeEnum.TOTAL_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=PeakProfileTypeEnum.CWL_PSEUDO_VOIGT.value, - description=PeakProfileTypeEnum.CWL_PSEUDO_VOIGT.description(), - ) - 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`, `TofJorgensen`, `TofJorgensenVonDreele`, … | -| `InstrumentFactory` | Instruments | `CwlPdInstrument`, `TofPdInstrument`, … | -| `DataFactory` | Data collections | `PdCwlData`, `PdTofData`, `TotalData` | -| `ReflnFactory` | Reflection collections | `ReflnData`, `PowderCwlReflnData`, `PowderTofReflnData` | -| `ExtinctionFactory` | Extinction models | `BeckerCoppensExtinction` | -| `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` | -| `AtomSiteAnisoFactory` | Anisotropic ADPs | `AtomSiteAnisoCollection` | -| `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`, `LmfitLeastsqMinimizer`, `LmfitLeastSquaresMinimizer`, `DfolsMinimizer`, `BumpsMinimizer`, `BumpsLmMinimizer`, `BumpsAmoebaMinimizer`, `BumpsDEMinimizer` | - -> **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 - -Canonical tags are the stable identifiers for factory lookup and -serialisation. User-facing APIs may expose context-local aliases when -the owner object already provides enough context to disambiguate the -choice (for example, `experiment.peak_profile_type = 'pseudo-voigt'` -inside a CWL or TOF experiment). Canonical tags 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** - -Canonical peak tags are globally unique within `PeakFactory`. The -experiment-facing `peak_profile_type` getter, setter, and supported-type -display use context-local aliases so users do not need to type `cwl-`, -`tof-`, or `total-` when the experiment context already disambiguates -the choice. - -| Canonical tag | Local alias | Class | -| -------------------------------------- | ------------------------------------ | ---------------------------------- | -| `cwl-pseudo-voigt` | `pseudo-voigt` | `CwlPseudoVoigt` | -| `cwl-pseudo-voigt-empirical-asymmetry` | `pseudo-voigt + empirical asymmetry` | `CwlPseudoVoigtEmpiricalAsymmetry` | -| `cwl-thompson-cox-hastings` | `thompson-cox-hastings` | `CwlThompsonCoxHastings` | -| `tof-pseudo-voigt` | `pseudo-voigt` | `TofPseudoVoigt` | -| `tof-jorgensen` | `jorgensen` | `TofJorgensen` | -| `tof-jorgensen-von-dreele` | `jorgensen-von-dreele` | `TofJorgensenVonDreele` | -| `tof-double-jorgensen-von-dreele` | `double-jorgensen-von-dreele` | `TofDoubleJorgensenVonDreele` | -| `total-gaussian-damped-sinc` | `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` | `PdCwlData` | -| `bragg-pd-tof` | `PdTofData` | -| `total-pd` | `TotalData` | - -**Refln tags** - -| Tag | Class | -| -------------------- | -------------------- | -| `bragg-sc` | `ReflnData` | -| `bragg-pd-refln` | `PowderCwlReflnData` | -| `bragg-pd-tof-refln` | `PowderTofReflnData` | - -**Extinction tags** - -| Tag | Class | -| ---------------- | ------------------------- | -| `becker-coppens` | `BeckerCoppensExtinction` | - -**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)` | `LmfitLeastsqMinimizer` | -| `lmfit (least_squares)` | `LmfitLeastSquaresMinimizer` | -| `dfols` | `DfolsMinimizer` | -| `bumps` | `BumpsMinimizer` | -| `bumps (lm)` | `BumpsLmMinimizer` | -| `bumps (amoeba)` | `BumpsAmoebaMinimizer` | -| `bumps (de)` | `BumpsDEMinimizer` | - -### 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` | -| `CwlPseudoVoigtEmpiricalAsymmetry` | `PeakFactory` | -| `CwlThompsonCoxHastings` | `PeakFactory` | -| `TofJorgensen` | `PeakFactory` | -| `TofJorgensenVonDreele` | `PeakFactory` | -| `TofDoubleJorgensenVonDreele` | `PeakFactory` | -| `TotalGaussianDampedSinc` | `PeakFactory` | -| `BeckerCoppensExtinction` | `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` | `ReflnFactory` | -| `PowderCwlReflnData` | `ReflnFactory` | -| `PowderTofReflnData` | `ReflnFactory` | -| `ExcludedRegions` | `ExcludedRegionsFactory` | -| `LinkedPhases` | `LinkedPhasesFactory` | -| `AtomSites` | `AtomSitesFactory` | -| `AtomSiteAnisoCollection` | `AtomSiteAnisoFactory` | -| `Aliases` | `AliasesFactory` | -| `Constraints` | `ConstraintsFactory` | -| `JointFitExperiments` | `JointFitExperimentsFactory` | - -#### CategoryItems that are ONLY children of collections (NO metadata) - -| Class | Parent collection | -| -------------------- | ------------------------------- | -| `LineSegment` | `LineSegmentBackground` | -| `PolynomialTerm` | `ChebyshevPolynomialBackground` | -| `AtomSite` | `AtomSites` | -| `AtomSiteAniso` | `AtomSiteAnisoCollection` | -| `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) | -| `BumpsMinimizer` | `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 experiment's -active support category (`data` for powder, `refln` for Bragg -single-crystal) `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 a dedicated `calculation` category: - -- `calculation.calculator_type` — getter + setter for the calculator - backend tag -- `calculation.calculator` — read-only access to the live backend - instance -- `calculation.show_calculator_types()` — filtered by the active support - category and marks the current type - -### 6.2 Minimiser - -The minimiser drives the optimisation loop. `MinimizerFactory` creates -instances by tag (e.g. `'lmfit'`, `'dfols'`, `'bumps'`). - -### 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: - -- Fit configuration: `fit` (`CategoryItem` with `minimizer_type` and - `mode` descriptors). `fit.minimizer_type` selects the minimizer - backend. `fit.mode` stores whether fitting is `'single'`, `'joint'`, - or `'sequential'`. `fit.show_minimizer_types()` lists supported - minimizers; `fit.show_modes()` filters modes by experiment count (≤1 → - only `single`; >1 → all three). -- Joint-fit weights: `joint_fit_experiments` (`CategoryCollection` of - per-experiment weight entries); sibling of `fit`, not a child. -- Parameter tables: `show_all_params()`, `show_fittable_params()`, - `show_free_params()`, `how_to_access_parameters()` -- Fitting: `fit()` dispatches single/joint through the callable `fit` - category; `fit_sequential()` handles sequential mode (sets `fit.mode` - to `'sequential'` internally). `display.fit_results()` shows results. -- Aliases and constraints (single-type categories; no public `_type` - getter or setter) - ---- - -## 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.display` | `Display` | Plot/table engine selection and facades | -| `project.analysis` | `Analysis` | Minimiser, fitting, aliases, constraints | -| `project.summary` | `Summary` | Report generation | -| `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 + Display preferences -├── summary.cif # Summary report -├── structures/ -│ └── lbco.cif # One file per structure -├── experiments/ -│ └── hrpt.cif # One file per experiment -└── analysis/ - └── analysis.cif # Analysis settings -``` - -`project.cif` carries both the `_project.*` metadata and the -`_display.*` engine preferences (`plotter_type`, `tabler_type`), so a -saved project re-opens with the same display backends. Per-experiment -calculator selection (`_calculation.calculator_type`) lives in each -experiment file, and fit configuration (`_fit.minimizer_type`, -`_fit.mode`) lives in `analysis/analysis.cif`. - -### 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-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', - adp_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_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_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'].calculation.show_calculator_types() -project.experiments['hrpt'].calculation.calculator_type = 'cryspy' -project.analysis.fit.minimizer_type = 'lmfit' - -# Plot before fitting -project.display.plotter.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.display.free_params() - -# Fit and show results -project.analysis.fit() -project.analysis.display.fit_results() - -# Plot after fitting -project.display.plotter.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 = 'jorgensen' -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. -- **Free-flag encoding**: A parameter's free/fixed status is encoded in - CIF via uncertainty brackets. `3.89` = fixed, `3.89(2)` = free with - esd, `3.89()` = free without esd. There is no separate list of free - parameters; the brackets are the single source of truth. - -### 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 uniform construction, -consistent metadata, and makes adding a second implementation trivial. - -For categories with **multiple implementations** (multi-type), the owner -exposes the full switchable API: - -| 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 types | `show__types()` | `expt.show_background_types()` | - -Multi-type categories: - -- **Experiment:** `background_type`, `peak_profile_type`, - `extinction_type`. - -Categories that are **fixed at creation** (determined by the experiment -type and never changed) expose only a read-only `` property -with no `_type` getter, setter, or show methods: - -- **Experiment:** `instrument`, `data`, `refln`. - -For categories with **only one implementation** (single-type), the -`_type` getter, setter, and show methods are omitted from the public API -to avoid clutter. The factory and `_type` attribute still exist -internally for consistency and future extensibility. - -Single-type categories (no public `_type` property): - -- **Experiment:** `diffrn`, `linked_crystal`, `excluded_regions`, - `linked_phases`. -- **Structure:** `cell`, `space_group`, `atom_sites`, `atom_site_aniso`. -- **Analysis:** `aliases`, `constraints`. - -`fit` is a dedicated analysis category. Its public selector surface is -`fit.minimizer_type` and `fit.mode`; there is no separate owner-level -proxy API. Likewise, `calculation` is a dedicated experiment category -that owns calculator selection — -`experiment.calculation.calculator_type` and -`experiment.calculation.show_calculator_types()` — instead of the -selector being exposed at the experiment owner level. The same pattern -applies to `display` on `Project`, which owns `plotter_type` and -`tabler_type` (see §9.4.1). - -**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.4.1 Selector Families - -Not every `_type` attribute represents the same kind of choice. The API -recognises three distinct selector families. They share a similar -`_type` shape so the user can inspect and set them uniformly, but -their intent and ownership differ: - -| Family | User intent | Examples | CIF | -| ---------------------------------- | ------------------------------- | --------------------------------------------------------------------------- | ------------------------------------------------------------------------------ | -| Backend selector | Pick an execution backend | `fit.minimizer_type`, `calculation.calculator_type`, `display.plotter_type` | `_fit.minimizer_type`, `_calculation.calculator_type`, `_display.plotter_type` | -| Switchable-category impl. selector | Swap a category implementation | `experiment.background_type`, `experiment.peak_profile_type` | category-owned type tag such as `_peak.profile_type` | -| Semantic value selector | Pick a scientific/analysis mode | `fit.mode` | `_fit.mode` | - -Backend selectors and semantic value selectors live on a dedicated -configuration category (`fit`, `calculation`, `display`). Switchable- -category implementation selectors are owned by the host (typically the -experiment) because switching them replaces the category instance, as -described in §9.3. - -### 9.5 Discoverable Supported Options - -The user can always discover what is supported for the current -experiment: - -```python -expt.show_peak_profile_types() -expt.show_background_types() -expt.calculation.show_calculator_types() -expt.show_extinction_types() -project.analysis.fit.show_minimizer_types() -project.analysis.fit.show_modes() -project.display.show_plotter_types() -project.display.show_tabler_types() -``` - -Available calculators are filtered by `engine_imported` (whether the -library is installed) and by the experiment's active support 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`, `FitModeEnum.SEQUENTIAL`). - -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.value == FitModeEnum.JOINT: - -# ❌ Wrong — compare with raw string -if self._fit.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` and `joint_fit_experiments`:** `fit` is a -`CategoryItem` holding the active minimizer and fitting mode. -`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 = 'joint' -project.analysis.joint_fit_experiments['npd'].weight = 0.7 - -# ❌ Wrong — joint_fit_experiments as a child of fit -project.analysis.fit.joint_fit_experiments['npd'].weight = 0.7 -``` - -In CIF output, sibling categories appear as independent blocks: - -``` -_fit.minimizer_type lmfit -_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. - -### 9.9 Lint Complexity Thresholds - -The Pylint-style complexity limits in `pyproject.toml` are **intentional -code-quality guardrails**, not arbitrary numbers. A violation is a -signal that the function or class needs refactoring — not that the -threshold needs raising. - -The project uses **ruff's defaults** for all PLR thresholds, with one -exception: `max-args` and `max-positional-args` are set to **6** instead -of the ruff default of 5, because ruff counts `self`/`cls` while -traditional pylint does not. Setting 6 in ruff matches pylint's standard -limit of 5 real parameters per function. - -| Threshold | Value | Rule | -| --------------------- | ----- | ------- | -| `max-args` | 6 | PLR0913 | -| `max-positional-args` | 6 | PLR0917 | -| `max-branches` | 12 | PLR0912 | -| `max-statements` | 50 | PLR0915 | -| `max-locals` | 15 | PLR0914 | -| `max-nested-blocks` | 5 | PLR1702 | -| `max-returns` | 6 | PLR0911 | -| `max-public-methods` | 20 | PLR0904 | - -**Rules:** - -- **Do not raise thresholds.** The current values represent the - project's design intent for maximum acceptable complexity. -- **Do not add `# noqa` comments** (or any other mechanism) to silence - complexity rules such as `PLR0912`, `PLR0913`, `PLR0914`, `PLR0915`, - `PLR0917`, `PLR1702`. -- **Refactor the code instead:** extract helper functions, introduce - parameter objects, flatten nesting, use early returns, etc. -- **For complex refactors** that touch many lines or change public API, - propose a refactoring plan and wait for approval before proceeding. - ---- - -## 10. Test Strategy - -Every new feature, category, factory, or bug fix must ship with tests. -The project enforces a multi-layered testing approach that catches -regressions at different levels of abstraction. - -### 10.1 Test Layers - -| Layer | Location | Runner command | Scope | -| --------------------- | ----------------------- | ---------------------------- | -------------------------------------------------------------------------------------------------------------------- | -| **Unit** | `tests/unit/` | `pixi run unit-tests` | Single class or function in isolation. Fast, no I/O, no external engines. | -| **Functional** | `tests/functional/` | `pixi run functional-tests` | Multi-component workflows (e.g. create experiment → load data → fit). No external data files beyond tiny test stubs. | -| **Integration** | `tests/integration/` | `pixi run integration-tests` | End-to-end pipelines using real calculation engines (cryspy, crysfml, pdffit2) and real data files from `data/`. | -| **Script (tutorial)** | `tools/test_scripts.py` | `pixi run script-tests` | Runs each tutorial `*.py` script under `docs/docs/tutorials/` as a subprocess and checks for a zero exit code. | -| **Notebook** | `docs/docs/tutorials/` | `pixi run notebook-tests` | Executes every Jupyter notebook end-to-end via `nbmake`. | - -### 10.2 Directory Structure Convention - -The unit-test tree **mirrors** the source tree: - -``` -src/easydiffraction//.py - → tests/unit/easydiffraction//test_.py -``` - -Two additional patterns are recognised: - -1. **Supplementary coverage files** — `test__coverage.py`, - `test__more.py`, etc. sit beside the main test file and add - extra scenarios. -2. **Parent-level roll-up** — for category packages that contain only - `default.py` and `factory.py`, a single `test_.py` one - directory up covers the whole package (e.g. - `categories/test_experiment_type.py` covers - `categories/experiment_type/default.py` and - `categories/experiment_type/factory.py`). - -The CI tool `pixi run test-structure-check` validates that every source -module has a corresponding test file and reports any gaps. Explicit name -aliases (e.g. `variable.py` tested by `test_parameters.py`) are declared -in `KNOWN_ALIASES` inside the tool script. - -### 10.3 What to Test per Source Module Type - -| Source module type | Required tests | -| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | -| **Core base class** (`core/`) | Instantiation, public properties, validation edge cases, identity wiring. | -| **Factory** (`factory.py`) | Registration check, `supported_tags()`, `default_tag()`, `create()` for each tag, `show_supported()` output, invalid-tag handling. | -| **Category** (`default.py`) | Instantiation, all public properties (read + write where applicable), CIF round-trip (`as_cif` → `from_cif`), parameter enumeration. | -| **Enum** (`enums.py`) | Membership of all members, `default()` method, `description()` for every member, `StrEnum` string equality. | -| **Datablock item** (`base.py`) | Construction, switchable-category full API (``, `_type` get/set, `show__types`), `show`/`show_as_cif`. | -| **Collection** (`collection.py`) | `create`, `add`, `remove`, `names`, `show_names`, `show_params`, iteration, duplicate-name handling. | -| **Calculator / Minimizer** | `can_handle()` with compatible and incompatible experiment types, `_compute()` stub or mock. | -| **Display / IO** | Input → output for representative cases; file-not-found and malformed-input error paths. | - -### 10.4 Test Conventions - -- **No test-ordering dependence.** Each test must be self-contained. Use - `monkeypatch` to set `Logger._reaction` when the test expects a raised - exception (another test may have leaked WARN mode via the global - `Logger` singleton). -- **Error paths are tested explicitly.** Use `pytest.raises()` (with - `monkeypatch` for Logger RAISE mode) for `log.error()` calls that - specify `exc_type`. -- **`@typechecked` setters raise `typeguard.TypeCheckError`**, not - `TypeError`. Tests must catch the correct exception. -- **Use `capsys` / `capfd`** for asserting console output from `show_*` - methods. -- **Prefer `tmp_path`** (pytest fixture) for file-system tests. -- **No sleeping, no network calls, no real calculation engines** in unit - tests. -- Test files carry the SPDX license header and a module-level docstring. - They are exempt from most lint rules (ANN, D, DOC, INP001, S101, etc.) - per `pyproject.toml`. - -### 10.5 Coverage Threshold - -The minimum line-coverage threshold is **70 %** (`fail_under = 70` in -`pyproject.toml`). The project aspires to test every code path; the -threshold is a safety net, not a target. - -Run `pixi run unit-tests-coverage` for a per-module report. - ---- - -## 11. 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/dev/benchmarking/20260519-103251_darwin-arm64_py314_tutorial-benchmarks.csv b/docs/dev/benchmarking/20260519-103251_darwin-arm64_py314_tutorial-benchmarks.csv new file mode 100644 index 000000000..edb8e3f63 --- /dev/null +++ b/docs/dev/benchmarking/20260519-103251_darwin-arm64_py314_tutorial-benchmarks.csv @@ -0,0 +1,24 @@ +tutorial_name,elapsed_seconds,status,return_code +ed-1.py,13.789,ok,0 +ed-10.py,39.098,ok,0 +ed-11.py,10.343,ok,0 +ed-12.py,8.331,ok,0 +ed-13.py,21.745,ok,0 +ed-14.py,6.158,ok,0 +ed-15.py,240.151,ok,0 +ed-16.py,58.481,ok,0 +ed-17.py,152.214,ok,0 +ed-18.py,6.114,ok,0 +ed-2.py,18.322,ok,0 +ed-20.py,36.422,ok,0 +ed-21.py,197.610,ok,0 +ed-22.py,194.351,ok,0 +ed-23.py,6.060,ok,0 +ed-24.py,4.749,ok,0 +ed-3.py,19.050,ok,0 +ed-4.py,4.480,ok,0 +ed-5.py,36.605,ok,0 +ed-6.py,61.846,ok,0 +ed-7.py,115.147,ok,0 +ed-8.py,101.442,ok,0 +ed-9.py,9.214,ok,0 diff --git a/docs/dev/benchmarking/20260519-103500_darwin-arm64_py314_tutorial-benchmarks.csv b/docs/dev/benchmarking/20260519-103500_darwin-arm64_py314_tutorial-benchmarks.csv new file mode 100644 index 000000000..608944cb5 --- /dev/null +++ b/docs/dev/benchmarking/20260519-103500_darwin-arm64_py314_tutorial-benchmarks.csv @@ -0,0 +1,24 @@ +tutorial_name,elapsed_seconds,status,return_code +ed-1.py,13.979,ok,0 +ed-10.py,38.764,ok,0 +ed-11.py,10.606,ok,0 +ed-12.py,9.044,ok,0 +ed-13.py,23.157,ok,0 +ed-14.py,6.585,ok,0 +ed-15.py,258.188,ok,0 +ed-16.py,60.097,ok,0 +ed-17.py,69.418,ok,0 +ed-18.py,6.181,ok,0 +ed-2.py,18.844,ok,0 +ed-20.py,39.157,ok,0 +ed-21.py,96.730,ok,0 +ed-22.py,73.480,ok,0 +ed-23.py,5.984,ok,0 +ed-24.py,4.942,ok,0 +ed-3.py,20.782,ok,0 +ed-4.py,5.780,ok,0 +ed-5.py,37.716,ok,0 +ed-6.py,66.911,ok,0 +ed-7.py,119.645,ok,0 +ed-8.py,103.887,ok,0 +ed-9.py,8.891,ok,0 diff --git a/docs/dev/benchmarking/20260519-121524_darwin-arm64_py314_tutorial-benchmarks.csv b/docs/dev/benchmarking/20260519-121524_darwin-arm64_py314_tutorial-benchmarks.csv new file mode 100644 index 000000000..ff8eda19a --- /dev/null +++ b/docs/dev/benchmarking/20260519-121524_darwin-arm64_py314_tutorial-benchmarks.csv @@ -0,0 +1,24 @@ +tutorial_name,elapsed_seconds,status,return_code +ed-1.py,15.557,ok,0 +ed-10.py,40.860,ok,0 +ed-11.py,10.823,ok,0 +ed-12.py,8.861,ok,0 +ed-13.py,24.128,ok,0 +ed-14.py,6.722,ok,0 +ed-15.py,28.243,ok,0 +ed-16.py,59.218,ok,0 +ed-17.py,70.816,ok,0 +ed-18.py,6.944,ok,0 +ed-2.py,20.385,ok,0 +ed-20.py,39.513,ok,0 +ed-21.py,96.953,ok,0 +ed-22.py,75.390,ok,0 +ed-23.py,6.115,ok,0 +ed-24.py,5.159,ok,0 +ed-3.py,34.082,ok,0 +ed-4.py,8.215,ok,0 +ed-5.py,61.949,ok,0 +ed-6.py,83.857,ok,0 +ed-7.py,120.332,ok,0 +ed-8.py,103.831,ok,0 +ed-9.py,9.270,ok,0 diff --git a/docs/dev/cryspy-dwf-bug.md b/docs/dev/cryspy-dwf-bug.md deleted file mode 100644 index 5f475ecc6..000000000 --- a/docs/dev/cryspy-dwf-bug.md +++ /dev/null @@ -1,126 +0,0 @@ -# CrysPy Bug: Wrong Rotation Matrix Indices in `calc_power_dwf_aniso` - -## Summary - -`calc_power_dwf_aniso` in CrysPy 0.7.8 reads the rotation matrix from -the wrong slice of `reduced_symm_elems`, producing incorrect -Debye–Waller factors for every anisotropic atom. - -## Affected Version - -CrysPy **0.7.8** (and likely all versions sharing this code path). - -## Location - -`cryspy/A_functions_base/debye_waller_factor.py`, function -`calc_power_dwf_aniso`, around line 177. - -## Root Cause - -`reduced_symm_elems` has shape `(13, N_symmetry_operations)` with the -following layout per column: - -| Indices | Content | -| ------- | ----------------------------------- | -| 0–3 | Translation: $b_1, b_2, b_3, b_d$ | -| 4–12 | Rotation matrix $R$ (row-major 3×3) | - -The buggy code extracts the rotation matrix from indices **0–8** (i.e. -`symm_elems_r[0:9]`), which mixes the translation vector into the -rotation matrix: - -```python -# BUGGY (current code) -r_11, r_12, r_13 = symm_elems_r[0], symm_elems_r[1], symm_elems_r[2] -r_21, r_22, r_23 = symm_elems_r[3], symm_elems_r[4], symm_elems_r[5] -r_31, r_32, r_33 = symm_elems_r[6], symm_elems_r[7], symm_elems_r[8] -``` - -The correct indices are **4–12**: - -```python -# FIXED -r_11, r_12, r_13 = symm_elems_r[4], symm_elems_r[5], symm_elems_r[6] -r_21, r_22, r_23 = symm_elems_r[7], symm_elems_r[8], symm_elems_r[9] -r_31, r_32, r_33 = symm_elems_r[10], symm_elems_r[11], symm_elems_r[12] -``` - -For comparison, `calc_pr1` in the same codebase (structure factor phase -calculation) correctly uses indices 4–12 for the rotation. - -## Impact - -Every structure-factor calculation that uses anisotropic ADPs produces -wrong results. The isotropic DWF (`calc_power_dwf_iso`) is unaffected -and works correctly, so the bug only manifests when anisotropic -displacement parameters ($B_\text{ani}$ or $U_\text{ani}$) are used. - -## Verification - -We verified the fix by comparing isotropic and anisotropic calculations -for the LBCO structure (space group $I\,4/m\,m\,m$) where $B_\text{iso}$ -and the equivalent diagonal anisotropic tensor must give identical -$|F|^2$ values: - -| Metric | Buggy code | Patched code | -| --------------------------- | --------------------------- | ------------ | ------------------------------ | ---------------------- | -| $\max | y*\text{iso} - y*\text{ani} | $ | ~11 (first peak: 72.1 vs 61.0) | $1.78 \times 10^{-13}$ | -| $\chi^2$ (Biso) | 14.98 | 14.98 | -| $\chi^2$ (Bani, equivalent) | 184.10 | 14.98 | - -## Workaround - -Monkey-patch at import time (used in EasyDiffraction): - -```python -from cryspy.A_functions_base import debye_waller_factor as _dwf_mod - -def _patched_calc_power_dwf_aniso(index_hkl, beta, symm_elems_r, flag_beta=False): - b_11, b_22, b_33 = beta[0], beta[1], beta[2] - b_12, b_13, b_23 = beta[3], beta[4], beta[5] - h, k, l = index_hkl[0], index_hkl[1], index_hkl[2] - # Corrected indices: rotation starts at 4, not 0 - r_11, r_12, r_13 = symm_elems_r[4], symm_elems_r[5], symm_elems_r[6] - r_21, r_22, r_23 = symm_elems_r[7], symm_elems_r[8], symm_elems_r[9] - r_31, r_32, r_33 = symm_elems_r[10], symm_elems_r[11], symm_elems_r[12] - h_s = h * r_11 + k * r_21 + l * r_31 - k_s = h * r_12 + k * r_22 + l * r_32 - l_s = h * r_13 + k * r_23 + l * r_33 - power = ( - b_11 * np.square(h_s) + b_22 * np.square(k_s) + b_33 * np.square(l_s) - + 2.0 * b_12 * h_s * k_s - + 2.0 * b_13 * h_s * l_s - + 2.0 * b_23 * k_s * l_s - ) - dder = {} - if flag_beta: - ones_b = np.ones_like(b_11) - dder['beta'] = np.stack([ - ones_b * np.square(h_s), - ones_b * np.square(k_s), - ones_b * np.square(l_s), - ones_b * 2.0 * h_s * k_s, - ones_b * 2.0 * h_s * l_s, - ones_b * 2.0 * k_s * l_s, - ], axis=0) - return power, dder - -_dwf_mod.calc_power_dwf_aniso = _patched_calc_power_dwf_aniso -``` - -## Suggested Fix - -In `cryspy/A_functions_base/debye_waller_factor.py`, function -`calc_power_dwf_aniso`, change: - -```diff -- r_11, r_12, r_13 = symm_elems_r[0], symm_elems_r[1], symm_elems_r[2] -- r_21, r_22, r_23 = symm_elems_r[3], symm_elems_r[4], symm_elems_r[5] -- r_31, r_32, r_33 = symm_elems_r[6], symm_elems_r[7], symm_elems_r[8] -+ r_11, r_12, r_13 = symm_elems_r[4], symm_elems_r[5], symm_elems_r[6] -+ r_21, r_22, r_23 = symm_elems_r[7], symm_elems_r[8], symm_elems_r[9] -+ r_31, r_32, r_33 = symm_elems_r[10], symm_elems_r[11], symm_elems_r[12] -``` - -The same correction must be applied to the analytical-derivative branch -of the same function if it shares the same index convention. diff --git a/docs/dev/index.md b/docs/dev/index.md new file mode 100644 index 000000000..424140f2b --- /dev/null +++ b/docs/dev/index.md @@ -0,0 +1,29 @@ +# Development Documentation + +This directory contains development-only project documentation. Keep it +under `docs/dev` rather than a repository-root `dev/` directory so all +documentation lives under one tree, while published user documentation +remains isolated under `docs/docs`. + +## Structure + +| Path | Purpose | +| ---------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | +| [`adrs/index.md`](adrs/index.md) | Architecture and decision navigation, grouped by topic. | +| [`issues/open.md`](issues/open.md) | Prioritized open development issues and design questions. | +| [`issues/closed.md`](issues/closed.md) | Closed development issues retained for history. | +| [`package-structure/short.md`](package-structure/short.md) | Generated compact package tree. | +| [`package-structure/full.md`](package-structure/full.md) | Generated package tree with top-level classes. | +| [`plans/`](plans/) | Implementation plans for larger migrations. | +| [`roadmap/ROADMAP.md`](roadmap/ROADMAP.md) | Development roadmap. This may later be copied into `docs/docs` during the published-docs build. | + +## Rules + +- Put architecture and decision history in ADR files. +- Put proposed decisions in `adrs/suggestions/` until accepted. +- Move accepted suggestions to `adrs/accepted/` and update + `adrs/index.md`. +- Do not edit generated package-structure files by hand; run + `pixi run update-package-diagrams`. +- Keep development documentation out of `docs/docs` unless it is meant + to become user-facing documentation. diff --git a/docs/dev/issues_closed.md b/docs/dev/issues/closed.md similarity index 81% rename from docs/dev/issues_closed.md rename to docs/dev/issues/closed.md index a5ae2621a..c2e60653a 100644 --- a/docs/dev/issues_closed.md +++ b/docs/dev/issues/closed.md @@ -4,6 +4,19 @@ Issues that have been fully resolved. Kept for historical reference. --- +## 77. Add Help Methods to Public Discovery Facades + +Added consistent `help()` methods for plain user-facing facade classes +that do not inherit the guarded object hierarchy: `project.display`, +`project.display.parameters`, `project.display.fit`, +`project.display.posterior`, `analysis.display`, and `project.summary`. +Introduced `render_object_help()` so these helpers share the same +property and method table style as `GuardedBase.help()`. Documented the +convention in +[`help-discoverability.md`](../adrs/accepted/help-discoverability.md). + +--- + ## Restore Minimiser Variant Support Used thin subclasses (approach A) to restore lmfit algorithm variants. @@ -50,6 +63,19 @@ user-facing path and bypassed during fitting --- +## Introduce CategoryOwner for Analysis and Datablocks + +Added `CategoryOwner` as the shared base class for flat CIF-like +category owners. `DatablockItem` now extends `CategoryOwner`, keeping +real `data_` header behavior for structures and experiments. +`Analysis` also extends `CategoryOwner`, reusing shared category +discovery, parameter aggregation, and dirty tracking while remaining a +singleton section without a `data_` header. CIF serialization now splits +category-body rendering from datablock header rendering via +`category_owner_to_cif()`. + +--- + ## Move Calculator from Global to Per-Experiment Each experiment owns its calculator, auto-resolved on first access from diff --git a/docs/dev/issues_open.md b/docs/dev/issues/open.md similarity index 66% rename from docs/dev/issues_open.md rename to docs/dev/issues/open.md index 014aa4675..04d97ab85 100644 --- a/docs/dev/issues_open.md +++ b/docs/dev/issues/open.md @@ -3,8 +3,8 @@ 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. +implemented, remove it from this file and update +[`adrs/index.md`](../adrs/index.md) or the relevant ADR if needed. **Legend:** 🔴 High · 🟡 Medium · 🟢 Low @@ -14,31 +14,14 @@ needed. **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. +`joint_fit` 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. - ---- - -## 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. +**Fix:** rebuild or validate `joint_fit` at the start of every joint +fit. At minimum, `fit()` should assert that the weight keys exactly +match `project.experiments.names`. **Depends on:** nothing. @@ -100,7 +83,7 @@ with joint-fit workflows. 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). +**Depends on:** benefits from the CategoryOwner migration. --- @@ -161,6 +144,156 @@ is needed. --- +## 15. 🟡 Decide Whether Inactive Fit-Mode Categories Stay Lenient + +**Type:** API design + +`Analysis` currently allows direct access to inactive mode-specific +categories such as `joint_fit` or `sequential_fit`. The values remain +editable, but inactive sections are hidden from help and dropped during +serialization. + +**Fix:** confirm whether this lenient access is the long-term contract, +or replace it with a dedicated mode error to prevent silent state loss +on save. + +**Depends on:** nothing. + +--- + +## 16. 🟡 Clarify `joint_fit` Lifecycle Outside Execution + +**Type:** Fragility + +`joint_fit` is validated and auto-populated at `fit()` time, but it does +not react when experiments are later renamed or removed. + +**Fix:** decide whether `joint_fit` should stay passive until execution, +or listen for experiment lifecycle changes and prune or warn earlier. + +**Depends on:** nothing. + +--- + +## 17. 🟡 Define `joint_fit.weight` Bounds + +**Type:** Data model + +Joint-fit rows currently allow any non-negative weight, but the public +contract is still unclear about whether `0` means exclusion and whether +an upper bound should exist. + +**Fix:** define the supported range and validator semantics for +`joint_fit.weight`. + +**Depends on:** nothing. + +--- + +## 18. 🟡 Define `sequential_fit_extract` Target Scope + +**Type:** Data model + +Sequential extract rules currently target one numeric descriptor under +`experiment.diffrn`. Open questions remain around nested targets, +duplicate rules writing the same target, and how additional supported +prefixes should be introduced when new environment categories appear. + +**Fix:** pin the allowed target grammar and duplicate-target behaviour +in an ADR and validation rules. + +**Depends on:** nothing. + +--- + +## 19. 🟡 Decide Sequential Extraction Failure Policy + +**Type:** Runtime behaviour + +Today a failed required extract rule marks that file as failed and the +run continues. The overall aggregation policy is still undefined. + +**Fix:** decide whether one failed file should abort the whole run, +remain an isolated row-level failure, or count toward a configurable +failure threshold. + +**Depends on:** nothing. + +--- + +## 20. 🟢 Decide Whether Sequential Extraction Should Be Cached + +**Type:** Performance + +Sequential metadata extraction currently re-reads input files when the +run is repeated or resumed. + +**Fix:** decide whether extracted `diffrn.*` values should be cached in +`analysis/results.csv` only, or also in a dedicated reusable cache. + +**Depends on:** nothing. + +--- + +## 21. 🟢 Decide How Mid-Run Sequential Failures Persist + +**Type:** Recovery design + +If a sequential fit fails partway through, the recovery and persistence +contract for `analysis/results.csv` is not fully specified. + +**Fix:** define whether partial CSV output is authoritative for resume, +left untouched for manual recovery, or replaced on the next run. + +**Depends on:** nothing. + +--- + +## 22. 🟢 Decide Whether CLI Should Override Extract Rules + +**Type:** CLI design + +The CLI can override mode and worker settings, but persisted +`sequential_fit_extract` rules are not yet overridable from the command +line. + +**Fix:** decide whether extraction rules stay project-file-only or gain +an explicit CLI override syntax. + +**Depends on:** nothing. + +--- + +## 23. 🟢 Align `dir()` With Help Filtering + +**Type:** Discoverability + +`help()` now hides inactive analysis categories by fitting mode, while +`dir()` and tab completion still expose the full class surface. + +**Fix:** decide whether `dir()` should mirror the help filter or remain +an always-complete developer surface. + +**Depends on:** nothing. + +--- + +## 24. 🟢 Decide Whether `single_fit` Needs a Future Category + +**Type:** Scope planning + +Single mode currently has no dedicated persisted category. Future +single-mode settings could require one, but the threshold is not yet +defined. + +**Fix:** decide what concrete single-mode behaviour would justify a +`single_fit` category instead of keeping the mode configuration on the +owner only. + +**Depends on:** nothing. + +--- + ## 15. 🟡 Validate Joint-Fit Weights Before Residual Normalisation **Type:** Correctness @@ -180,6 +313,30 @@ minimiser. --- +## 16. 🟢 Add Serial Pattern-Generation Benchmarks + +**Type:** Performance + +The dev environment previously installed `pytest-benchmark`, but the +repository does not currently define any benchmark tests. At the same +time, the integration, script, and notebook pytest tasks all run with +`pytest-xdist`, so benchmark plugins only add warning noise and do not +provide reliable performance regression coverage. + +Performance regressions are still worth tracking, especially for single +diffraction-pattern calculation where backend or profile changes can +quietly slow interactive workflows. + +**Fix:** add a dedicated serial benchmark task outside the normal +parallel pytest suite. Benchmark representative single-pattern +calculations on fixed datasets and calculators, run without `-n auto`, +and define regression thresholds only after measurements are stable on a +controlled runner. + +**Depends on:** nothing. + +--- + ## 17. 🟢 Use PDF-Specific CIF Names for Total Scattering **Type:** Naming @@ -665,12 +822,13 @@ the archived planning notes left two follow-up questions open: --- -## 40. 🟢 Implement Resetting `.constrained` to `False` +## 40. 🟢 Implement Resetting `.user_constrained` to `False` **Type:** Feature -`ConstraintsHandler` has a TODO to implement changing the `.constrained` -attribute back to `False` when constraints are removed. +`ConstraintsHandler` has a TODO to implement changing the +`.user_constrained` attribute back to `False` when constraints are +removed. **TODOs:** @@ -759,18 +917,18 @@ formatting for `StringDescriptor` values. --- -## 46. 🟢 Rename `JointFitExperiments` ID and Improve Descriptions +## 46. 🟢 Improve `JointFitItem` Descriptions **Type:** Naming -`JointFitExperiments` uses `name='id'` with a TODO suggesting a better -name, and two description fields are incomplete. +`JointFitItem` uses `name='experiment_id'`, but two description fields +are still incomplete. **TODOs:** -- [default.py](src/easydiffraction/analysis/categories/joint_fit_experiments/default.py#L33) -- [default.py](src/easydiffraction/analysis/categories/joint_fit_experiments/default.py#L34) -- [default.py](src/easydiffraction/analysis/categories/joint_fit_experiments/default.py#L43) +- [default.py](src/easydiffraction/analysis/categories/joint_fit/default.py#L31) +- [default.py](src/easydiffraction/analysis/categories/joint_fit/default.py#L32) +- [default.py](src/easydiffraction/analysis/categories/joint_fit/default.py#L41) **Depends on:** nothing. @@ -1239,27 +1397,6 @@ deviate: e.g. `show_minimizer_types()` instead of --- -## 77. 🟡 Add `help()` to `Project` and Enrich Existing `help()` Methods - -**Type:** API discoverability - -`help()` exists on `CategoryItem`, `CollectionBase`, `DatablockItem`, -and `Analysis`, but **not on `Project`**. The user's primary entry point -lacks discoverability. Additionally, each `help()` level should guide -the user to the next level: - -1. `project.help()` → attributes: info, experiments, structures, - analysis, summary. -2. `project.experiments.help()` → list experiments and how to select. -3. `project.experiments['name'].help()` → list categories. -4. `experiment.peak.help()` → list public attributes. -5. `experiment.background.help()` → list items + array accessors. -6. `experiment.background['id'].help()` → list attributes. - -**Depends on:** nothing. - ---- - ## 79. 🟢 Verify Completeness of Analysis CIF Serialisation **Type:** Correctness @@ -1315,8 +1452,8 @@ Two manual workflow steps are required between releases/changes: 2. `pixi run notebook-prepare` — regenerate tutorial notebooks from scripts. -Document these in `CONTRIBUTING.md` or the architecture doc so they are -not forgotten. +Document these in `CONTRIBUTING.md` or a relevant ADR so they are not +forgotten. **Depends on:** nothing. @@ -1420,6 +1557,40 @@ threaded because each step's output is the next step's input. --- +## 95. 🟡 Re-Enable DREAM Multiprocessing in Direct Python Scripts + +**Type:** Performance / Script runtime + +On macOS and other spawn-based platforms, direct Bayesian tutorial +execution via `python script.py` or wrappers such as +`pixi run tutorial docs/docs/tutorials/ed-21.py` can fail during BUMPS +`MPMapper` startup because worker processes re-import `__main__` and +re-execute top-level tutorial code. The current defensive workaround is +to fall back to serial execution for these direct-script entry points, +which avoids the crash but disables DREAM multiprocessing and causes a +large performance drop. + +Observed behavior for `ed-21` today: + +- Jupyter execution and `easydiffraction PROJECT_DIR fit` both appear to + use working parallel DREAM and complete `361/361` in about 40 seconds. +- Direct Python-script execution of the same tutorial runs `361/361` in + about 220 seconds, consistent with the serial fallback path. + +**Possible solution:** keep the existing tracker-state cleanup before +pickling and mapper startup, but replace the blanket serial fallback +with an EasyDiffraction-controlled multiprocessing context policy. For +direct Python script entry points, prefer a `fork` context when +available so workers do not re-import the tutorial top level. Keep the +existing behavior for import-safe module entry points such as +`easydiffraction PROJECT_DIR fit` and for platforms where `fork` is +unavailable. Document the tradeoff clearly because `fork` on macOS is +less conservative than `spawn`. + +**Depends on:** related to issue 89, but independent. + +--- + ## 90. 🟢 Show Experiment Number/Total During Sequential Fitting **Type:** UX @@ -1438,7 +1609,7 @@ Should print: **Type:** CI / Tooling CodeFactor flags TODO comments as unresolved issues (rule C100) in PRs. -Since TODOs are tracked in `issues_open.md`, the CodeFactor check adds +Since TODOs are tracked in `issues/open.md`, the CodeFactor check adds noise. Disable the C100 rule or configure CodeFactor to ignore TODO comments. @@ -1458,91 +1629,158 @@ operation is possible (e.g. in automated pipelines or tests). --- +## 93. 🟡 Eliminate Flicker in Live Progress Tables + +**Type:** UX + +The shared `ActivityIndicator` / Rich `Live` region used by single fit, +sequential fit, and DREAM sampling visibly flickers in terminals +whenever the live renderable grows (new rows appended) or is updated at +a moderate rate. The effect is most pronounced in sequential fit because +rows are added more frequently than in single fit. + +**Findings from current investigation:** + +- Both single fit (`FitProgressTracker._refresh_activity_indicator`) and + sequential fit (`_report_chunk_progress`) push a fresh + `build_table_renderable(...)` into + `ActivityIndicator.update(content=...)` on each progress event. The + Rich `Table` instance is rebuilt from scratch every time. +- `_TerminalLiveHandle` / `ActivityIndicator` start `rich.live.Live` + with `auto_refresh=True`, + `refresh_per_second=1/_SPINNER_FRAME_SECONDS` (≈10 Hz), and + `vertical_overflow='visible'`. At every refresh tick, Rich re-renders + the full multi-line region (table + spinner line), which on many + terminals causes a visible flicker that scales with row count. +- Earlier attempts to mitigate this in sequential fit by switching to a + single-line spinner-only `Live` and printing rows above it (so Rich's + print-above-live mechanism handled them) removed flicker entirely, but + produced a different visual style from single fit and could not show + the closing border during the run. That approach was reverted for + consistency with single fit; flicker came back with it. +- `vertical_overflow='visible'` is required so the growing table is not + clipped, but it also forces Rich to repaint the whole region rather + than scroll/append. +- The spinner animation itself drives the refresh rate; lowering + `refresh_per_second` reduces flicker frequency but makes the spinner + feel sluggish. +- Single fit appears smoother in practice mainly because content changes + are throttled (`FIT_PROGRESS_UPDATE_SECONDS = 5.0`) and rows grow + slowly; the underlying mechanism is the same and it still flickers + when many iterations are appended quickly. + +**Possible directions (not yet evaluated):** + +- Decouple spinner refresh from content refresh: drive `Live` at a low + `refresh_per_second` (e.g. 2–4 Hz) and update content explicitly only + when a new row arrives, while animating the spinner via the label + string rather than Rich's renderable diff. +- Render the table once as static `console.print(...)` above a + single-line spinner-only `Live`, and re-print only the _new_ row(s) on + each update — restore the streaming approach but emit the bottom + border at the end (accept the trade-off that the closing border is not + visible during the run, or print it as part of every update with ANSI + cursor movement). +- Use `rich.live.Live(transient=False, auto_refresh=False)` and call + `live.refresh()` manually only when content changes; let the spinner + animate via a separate background timer or label updates. +- Investigate `rich.progress.Progress` with custom columns and a table + panel — Rich has optimised diff rendering there. +- Evaluate the actual cause on macOS Terminal / iTerm2 / VS Code + terminal separately — flicker behaviour differs across emulators. + +**Depends on:** nothing. Affects single fit, sequential fit, and DREAM +sampler progress displays — any fix should keep their visuals consistent +(issue #93 should be solved for all three at once). + +--- + ## Summary -| # | Issue | Severity | Type | -| --- | ------------------------------------------------ | -------- | ---------------- | -| 3 | Rebuild joint-fit weights | 🟡 Med | Fragility | -| 5 | `Analysis` as `DatablockItem` | 🟡 Med | Consistency | -| 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 | -| 13 | Suppress redundant dirty-flag sets | 🟢 Low | Performance | -| 14 | Finer-grained change tracking | 🟢 Low | Performance | -| 15 | Validate joint-fit weights | 🟡 Med | Correctness | -| 17 | Use PDF-specific CIF names | 🟢 Low | Naming | -| 18 | Move CIF v2→v1 conversion out of calculator | 🟢 Low | Maintainability | -| 19 | Debug-mode logging for calculator imports | 🟢 Low | Diagnostics | -| 20 | Redirect/suppress CrysPy stderr | 🟢 Low | UX | -| 21 | Clarify CrysPy TOF background CIF tags | 🟡 Med | Correctness | -| 22 | Check SC instrument mapping in CrysPy | 🟢 Low | Correctness | -| 23 | Investigate PyCrysFML pattern length discrepancy | 🟢 Low | Correctness | -| 24 | Process defaults on experiment creation | 🟢 Low | Design | -| 25 | Refactor data `_update` methods | 🟡 Med | Maintainability | -| 26 | Clarify `dtype` usage in data arrays | 🟢 Low | Cleanup | -| 27 | Handle zero uncertainty in Bragg PD | 🟢 Low | Correctness | -| 28 | Clarify Bragg PD data collection description | 🟢 Low | Cleanup | -| 29 | Standardise CIF ID validator pattern | 🟡 Med | Consistency | -| 30 | Make `refinement_status` default an Enum | 🟢 Low | Design | -| 31 | Rename PD data point mixins | 🟢 Low | Naming | -| 32 | Move common methods to `DatablockCollection` | 🟡 Med | Maintainability | -| 33 | Make `_update_categories` abstract | 🟡 Med | Design | -| 34 | Auto-extract `PeakProfileTypeEnum` | 🟢 Low | Design | -| 35 | Rename `BeamModeEnum` members to CWL/TOF | 🟢 Low | Naming | -| 36 | Common `EnumBase` class | 🟢 Low | Design | -| 37 | Rename experiment `.type` property | 🟢 Low | Naming | -| 38 | Fix `@typechecked`/gemmi in factories | 🟡 Med | Bug | -| 39 | Improve `_update_priority` handling | 🟢 Low | Design | -| 40 | Implement resetting `.constrained` to `False` | 🟢 Low | Feature | -| 41 | Check `_mark_dirty` in `_set_value` | 🟢 Low | Cleanup | -| 42 | MkDocs type unpacking in validation | 🟢 Low | Docs | -| 43 | Fix summary display inconsistencies | 🟢 Low | UX | -| 44 | Merge parameter record construction | 🟢 Low | Cleanup | -| 45 | Decide alias/constraint descriptor default | 🟢 Low | Design | -| 46 | Rename `JointFitExperiments` id + descriptions | 🟢 Low | Naming | -| 47 | Improve error handling in crystallography | 🟢 Low | Diagnostics | -| 48 | Fix CrysPy TOF instrument default | 🟢 Low | Bug workaround | -| 49 | Automate space group CIF name variants | 🟢 Low | Maintainability | -| 50 | Clarify `Cell._update` minimizer param | 🟢 Low | Cleanup | -| 51 | Access space group for Wyckoff letters | 🟢 Low | Design | -| 52 | Rename line-segment `y` to `intensity` | 🟢 Low | Naming | -| 53 | Move `show()` to `CategoryCollection` | 🟢 Low | Maintainability | -| 54 | Add `point_id` to excluded regions | 🟢 Low | Completeness | -| 55 | Fix Jupyter scroll disabling for MkDocs | 🟢 Low | Docs / UX | -| 56 | Make ASCII plot width configurable | 🟢 Low | UX | -| 57 | Clean up CIF deserialisation helpers | 🟢 Low | Maintainability | -| 58 | Move `ProjectInfo` CIF methods to `serialize` | 🟢 Low | Maintainability | -| 59 | Add CIF name validation in parse | 🟢 Low | Robustness | -| 60 | Unify `mkdir` usage | 🟢 Low | Cleanup | -| 61 | Clarify logger default reaction mode | 🟢 Low | Design | -| 62 | Complete `render_table` → `TableRenderer` | 🟢 Low | Cleanup | -| 63 | Fix calculator `calculate_pattern` signature | 🟢 Low | Design | -| 64 | Check unused-if-loading-from-CIF code | 🟢 Low | Cleanup | -| 65 | Replace all bare `print()` with logging | 🟡 Med | Code quality | -| 66 | Error-handling strategy: `log.error` vs `raise` | 🟡 Med | Design | -| 67 | Custom validation for params and category types | 🟡 Med | Design | -| 68 | `@typechecked` on all public methods? | 🟢 Low | Design | -| 69 | Shorter public API names via `__init__` | 🟢 Low | API ergonomics | -| 70 | Standardise class member ordering + headers | 🟡 Med | Code style | -| 71 | `_update_priority` reference table | 🟢 Low | Documentation | -| 72 | Warn on all switchable-category type changes | 🟡 Med | UX | -| 73 | Unify setter parameter naming | 🟢 Low | Code style | -| 74 | Sync property type hints + custom lint rules | 🟡 Med | Tooling | -| 75 | `show_supported_calculators()` on Analysis | 🟢 Low | API completeness | -| 76 | Consistent `_type` suffix in switchable APIs | 🟡 Med | Naming | -| 77 | Add `help()` to Project + enrich existing | 🟡 Med | Discoverability | -| 79 | Verify analysis CIF serialisation completeness | 🟢 Low | Correctness | -| 80 | Resolve `Any` vs `object` annotation policy | 🟢 Low | Code style | -| 81 | Enforce docstrings on all public methods | 🟡 Med | Code quality | -| 82 | Document `param-docstring-fix` workflow | 🟢 Low | Documentation | -| 83 | Remove redundant parameter listing | 🟢 Low | Cleanup | -| 84 | Serialise `None` as `.` in CIF output | 🟡 Med | Correctness | -| 85 | Retain per-experiment fitted params for plotting | 🟡 Med | Correctness | -| 86 | Auto-resolve `plot_param` x-axis + add units | 🟢 Low | UX | -| 87 | Redesign tutorial grouping/categorisation | 🟢 Low | Documentation | -| 88 | Fix Dataset 26 description (47 not 57) | 🟢 Low | Data | -| 89 | Parallel independent fits for single mode | 🟡 Med | Performance | -| 90 | Show experiment number during sequential fitting | 🟢 Low | UX | -| 91 | Disable TODO checks in CodeFactor PRs | 🟢 Low | CI / Tooling | -| 92 | Make `save()` respect verbosity | 🟢 Low | UX | +| # | Issue | Severity | Type | +| --- | ------------------------------------------------- | -------- | ---------------------------- | +| 3 | Rebuild joint-fit weights | 🟡 Med | Fragility | +| 5 | `Analysis` as `DatablockItem` | 🟡 Med | Consistency | +| 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 | +| 13 | Suppress redundant dirty-flag sets | 🟢 Low | Performance | +| 14 | Finer-grained change tracking | 🟢 Low | Performance | +| 15 | Validate joint-fit weights | 🟡 Med | Correctness | +| 17 | Use PDF-specific CIF names | 🟢 Low | Naming | +| 18 | Move CIF v2→v1 conversion out of calculator | 🟢 Low | Maintainability | +| 19 | Debug-mode logging for calculator imports | 🟢 Low | Diagnostics | +| 20 | Redirect/suppress CrysPy stderr | 🟢 Low | UX | +| 21 | Clarify CrysPy TOF background CIF tags | 🟡 Med | Correctness | +| 22 | Check SC instrument mapping in CrysPy | 🟢 Low | Correctness | +| 23 | Investigate PyCrysFML pattern length discrepancy | 🟢 Low | Correctness | +| 24 | Process defaults on experiment creation | 🟢 Low | Design | +| 25 | Refactor data `_update` methods | 🟡 Med | Maintainability | +| 26 | Clarify `dtype` usage in data arrays | 🟢 Low | Cleanup | +| 27 | Handle zero uncertainty in Bragg PD | 🟢 Low | Correctness | +| 28 | Clarify Bragg PD data collection description | 🟢 Low | Cleanup | +| 29 | Standardise CIF ID validator pattern | 🟡 Med | Consistency | +| 30 | Make `refinement_status` default an Enum | 🟢 Low | Design | +| 31 | Rename PD data point mixins | 🟢 Low | Naming | +| 32 | Move common methods to `DatablockCollection` | 🟡 Med | Maintainability | +| 33 | Make `_update_categories` abstract | 🟡 Med | Design | +| 34 | Auto-extract `PeakProfileTypeEnum` | 🟢 Low | Design | +| 35 | Rename `BeamModeEnum` members to CWL/TOF | 🟢 Low | Naming | +| 36 | Common `EnumBase` class | 🟢 Low | Design | +| 37 | Rename experiment `.type` property | 🟢 Low | Naming | +| 38 | Fix `@typechecked`/gemmi in factories | 🟡 Med | Bug | +| 39 | Improve `_update_priority` handling | 🟢 Low | Design | +| 40 | Reset `.user_constrained` to `False` | 🟢 Low | Feature | +| 41 | Check `_mark_dirty` in `_set_value` | 🟢 Low | Cleanup | +| 42 | MkDocs type unpacking in validation | 🟢 Low | Docs | +| 43 | Fix summary display inconsistencies | 🟢 Low | UX | +| 44 | Merge parameter record construction | 🟢 Low | Cleanup | +| 45 | Decide alias/constraint descriptor default | 🟢 Low | Design | +| 46 | Improve `JointFitItem` descriptions | 🟢 Low | Naming | +| 47 | Improve error handling in crystallography | 🟢 Low | Diagnostics | +| 48 | Fix CrysPy TOF instrument default | 🟢 Low | Bug workaround | +| 49 | Automate space group CIF name variants | 🟢 Low | Maintainability | +| 50 | Clarify `Cell._update` minimizer param | 🟢 Low | Cleanup | +| 51 | Access space group for Wyckoff letters | 🟢 Low | Design | +| 52 | Rename line-segment `y` to `intensity` | 🟢 Low | Naming | +| 53 | Move `show()` to `CategoryCollection` | 🟢 Low | Maintainability | +| 54 | Add `point_id` to excluded regions | 🟢 Low | Completeness | +| 55 | Fix Jupyter scroll disabling for MkDocs | 🟢 Low | Docs / UX | +| 56 | Make ASCII plot width configurable | 🟢 Low | UX | +| 57 | Clean up CIF deserialisation helpers | 🟢 Low | Maintainability | +| 58 | Move `ProjectInfo` CIF methods to `serialize` | 🟢 Low | Maintainability | +| 59 | Add CIF name validation in parse | 🟢 Low | Robustness | +| 60 | Unify `mkdir` usage | 🟢 Low | Cleanup | +| 61 | Clarify logger default reaction mode | 🟢 Low | Design | +| 62 | Complete `render_table` → `TableRenderer` | 🟢 Low | Cleanup | +| 63 | Fix calculator `calculate_pattern` signature | 🟢 Low | Design | +| 64 | Check unused-if-loading-from-CIF code | 🟢 Low | Cleanup | +| 65 | Replace all bare `print()` with logging | 🟡 Med | Code quality | +| 66 | Error-handling strategy: `log.error` vs `raise` | 🟡 Med | Design | +| 67 | Custom validation for params and category types | 🟡 Med | Design | +| 68 | `@typechecked` on all public methods? | 🟢 Low | Design | +| 69 | Shorter public API names via `__init__` | 🟢 Low | API ergonomics | +| 70 | Standardise class member ordering + headers | 🟡 Med | Code style | +| 71 | `_update_priority` reference table | 🟢 Low | Documentation | +| 72 | Warn on all switchable-category type changes | 🟡 Med | UX | +| 73 | Unify setter parameter naming | 🟢 Low | Code style | +| 74 | Sync property type hints + custom lint rules | 🟡 Med | Tooling | +| 75 | `show_supported_calculators()` on Analysis | 🟢 Low | API completeness | +| 76 | Consistent `_type` suffix in switchable APIs | 🟡 Med | Naming | +| 79 | Verify analysis CIF serialisation completeness | 🟢 Low | Correctness | +| 80 | Resolve `Any` vs `object` annotation policy | 🟢 Low | Code style | +| 81 | Enforce docstrings on all public methods | 🟡 Med | Code quality | +| 82 | Document `param-docstring-fix` workflow | 🟢 Low | Documentation | +| 83 | Remove redundant parameter listing | 🟢 Low | Cleanup | +| 84 | Serialise `None` as `.` in CIF output | 🟡 Med | Correctness | +| 85 | Retain per-experiment fitted params for plotting | 🟡 Med | Correctness | +| 86 | Auto-resolve `plot_param` x-axis + add units | 🟢 Low | UX | +| 87 | Redesign tutorial grouping/categorisation | 🟢 Low | Documentation | +| 88 | Fix Dataset 26 description (47 not 57) | 🟢 Low | Data | +| 89 | Parallel independent fits for single mode | 🟡 Med | Performance | +| 95 | Re-enable DREAM multiprocessing in direct scripts | 🟡 Med | Performance / Script runtime | +| 90 | Show experiment number during sequential fitting | 🟢 Low | UX | +| 91 | Disable TODO checks in CodeFactor PRs | 🟢 Low | CI / Tooling | +| 92 | Make `save()` respect verbosity | 🟢 Low | UX | +| 93 | Eliminate flicker in live progress tables | 🟡 Med | UX | diff --git a/docs/dev/package-structure-full.md b/docs/dev/package-structure-full.md deleted file mode 100644 index ae233b21d..000000000 --- a/docs/dev/package-structure-full.md +++ /dev/null @@ -1,412 +0,0 @@ -# Package Structure (full) - -``` -📦 easydiffraction -├── 📁 analysis -│ ├── 📁 calculators -│ │ ├── 📄 __init__.py -│ │ ├── 📄 base.py -│ │ │ └── 🏷️ class CalculatorBase -│ │ ├── 📄 crysfml.py -│ │ │ └── 🏷️ class CrysfmlCalculator -│ │ ├── 📄 cryspy.py -│ │ │ └── 🏷️ class CryspyCalculator -│ │ ├── 📄 factory.py -│ │ │ └── 🏷️ class CalculatorFactory -│ │ └── 📄 pdffit.py -│ │ └── 🏷️ class PdffitCalculator -│ ├── 📁 categories -│ │ ├── 📁 aliases -│ │ │ ├── 📄 __init__.py -│ │ │ ├── 📄 default.py -│ │ │ │ ├── 🏷️ class Alias -│ │ │ │ └── 🏷️ class Aliases -│ │ │ └── 📄 factory.py -│ │ │ └── 🏷️ class AliasesFactory -│ │ ├── 📁 constraints -│ │ │ ├── 📄 __init__.py -│ │ │ ├── 📄 default.py -│ │ │ │ ├── 🏷️ class Constraint -│ │ │ │ └── 🏷️ class Constraints -│ │ │ └── 📄 factory.py -│ │ │ └── 🏷️ class ConstraintsFactory -│ │ ├── 📁 fit -│ │ │ ├── 📄 __init__.py -│ │ │ ├── 📄 default.py -│ │ │ │ └── 🏷️ class Fit -│ │ │ ├── 📄 enums.py -│ │ │ │ └── 🏷️ class FitModeEnum -│ │ │ └── 📄 factory.py -│ │ │ └── 🏷️ class FitFactory -│ │ ├── 📁 joint_fit_experiments -│ │ │ ├── 📄 __init__.py -│ │ │ ├── 📄 default.py -│ │ │ │ ├── 🏷️ class JointFitExperiment -│ │ │ │ └── 🏷️ class JointFitExperiments -│ │ │ └── 📄 factory.py -│ │ │ └── 🏷️ class JointFitExperimentsFactory -│ │ └── 📄 __init__.py -│ ├── 📁 fit_helpers -│ │ ├── 📄 __init__.py -│ │ ├── 📄 metrics.py -│ │ ├── 📄 reporting.py -│ │ │ └── 🏷️ class FitResults -│ │ └── 📄 tracking.py -│ │ ├── 🏷️ class _TerminalLiveHandle -│ │ └── 🏷️ class FitProgressTracker -│ ├── 📁 minimizers -│ │ ├── 📄 __init__.py -│ │ ├── 📄 base.py -│ │ │ └── 🏷️ class MinimizerBase -│ │ ├── 📄 bumps.py -│ │ │ ├── 🏷️ class _EasyDiffractionFitness -│ │ │ └── 🏷️ class BumpsMinimizer -│ │ ├── 📄 bumps_amoeba.py -│ │ │ └── 🏷️ class BumpsAmoebaMinimizer -│ │ ├── 📄 bumps_de.py -│ │ │ └── 🏷️ class BumpsDEMinimizer -│ │ ├── 📄 bumps_lm.py -│ │ │ └── 🏷️ class BumpsLmMinimizer -│ │ ├── 📄 dfols.py -│ │ │ └── 🏷️ class DfolsMinimizer -│ │ ├── 📄 enums.py -│ │ │ └── 🏷️ class MinimizerTypeEnum -│ │ ├── 📄 factory.py -│ │ │ └── 🏷️ class MinimizerFactory -│ │ ├── 📄 lmfit.py -│ │ │ └── 🏷️ class LmfitMinimizer -│ │ ├── 📄 lmfit_least_squares.py -│ │ │ └── 🏷️ class LmfitLeastSquaresMinimizer -│ │ └── 📄 lmfit_leastsq.py -│ │ └── 🏷️ class LmfitLeastsqMinimizer -│ ├── 📄 __init__.py -│ ├── 📄 analysis.py -│ │ ├── 🏷️ class AnalysisDisplay -│ │ └── 🏷️ class Analysis -│ ├── 📄 fitting.py -│ │ └── 🏷️ class Fitter -│ └── 📄 sequential.py -│ └── 🏷️ class SequentialFitTemplate -├── 📁 core -│ ├── 📄 __init__.py -│ ├── 📄 category.py -│ │ ├── 🏷️ class CategoryItem -│ │ └── 🏷️ class CategoryCollection -│ ├── 📄 collection.py -│ │ └── 🏷️ class CollectionBase -│ ├── 📄 datablock.py -│ │ ├── 🏷️ class DatablockItem -│ │ └── 🏷️ class DatablockCollection -│ ├── 📄 diagnostic.py -│ │ └── 🏷️ class Diagnostics -│ ├── 📄 factory.py -│ │ └── 🏷️ class FactoryBase -│ ├── 📄 guard.py -│ │ └── 🏷️ class GuardedBase -│ ├── 📄 identity.py -│ │ └── 🏷️ class Identity -│ ├── 📄 metadata.py -│ │ ├── 🏷️ class TypeInfo -│ │ ├── 🏷️ class Compatibility -│ │ └── 🏷️ class CalculatorSupport -│ ├── 📄 singleton.py -│ │ ├── 🏷️ class SingletonBase -│ │ └── 🏷️ class ConstraintsHandler -│ ├── 📄 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 -│ └── 🏷️ class _RestrictedUnpickler -├── 📁 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 -│ │ │ ├── 📁 calculation -│ │ │ │ ├── 📄 __init__.py -│ │ │ │ ├── 📄 default.py -│ │ │ │ │ └── 🏷️ class Calculation -│ │ │ │ └── 📄 factory.py -│ │ │ │ └── 🏷️ class CalculationFactory -│ │ │ ├── 📁 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 -│ │ │ ├── 📁 diffrn -│ │ │ │ ├── 📄 __init__.py -│ │ │ │ ├── 📄 default.py -│ │ │ │ │ └── 🏷️ class DefaultDiffrn -│ │ │ │ └── 📄 factory.py -│ │ │ │ └── 🏷️ class DiffrnFactory -│ │ │ ├── 📁 excluded_regions -│ │ │ │ ├── 📄 __init__.py -│ │ │ │ ├── 📄 default.py -│ │ │ │ │ ├── 🏷️ class ExcludedRegion -│ │ │ │ │ └── 🏷️ class ExcludedRegions -│ │ │ │ └── 📄 factory.py -│ │ │ │ └── 🏷️ class ExcludedRegionsFactory -│ │ │ ├── 📁 experiment_type -│ │ │ │ ├── 📄 __init__.py -│ │ │ │ ├── 📄 default.py -│ │ │ │ │ └── 🏷️ class ExperimentType -│ │ │ │ └── 📄 factory.py -│ │ │ │ └── 🏷️ class ExperimentTypeFactory -│ │ │ ├── 📁 extinction -│ │ │ │ ├── 📄 __init__.py -│ │ │ │ ├── 📄 becker_coppens.py -│ │ │ │ │ └── 🏷️ class BeckerCoppensExtinction -│ │ │ │ └── 📄 factory.py -│ │ │ │ └── 🏷️ class ExtinctionFactory -│ │ │ ├── 📁 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 -│ │ │ ├── 📁 linked_crystal -│ │ │ │ ├── 📄 __init__.py -│ │ │ │ ├── 📄 default.py -│ │ │ │ │ └── 🏷️ class LinkedCrystal -│ │ │ │ └── 📄 factory.py -│ │ │ │ └── 🏷️ class LinkedCrystalFactory -│ │ │ ├── 📁 linked_phases -│ │ │ │ ├── 📄 __init__.py -│ │ │ │ ├── 📄 default.py -│ │ │ │ │ ├── 🏷️ class LinkedPhase -│ │ │ │ │ └── 🏷️ class LinkedPhases -│ │ │ │ └── 📄 factory.py -│ │ │ │ └── 🏷️ class LinkedPhasesFactory -│ │ │ ├── 📁 peak -│ │ │ │ ├── 📄 __init__.py -│ │ │ │ ├── 📄 base.py -│ │ │ │ │ └── 🏷️ class PeakBase -│ │ │ │ ├── 📄 cwl.py -│ │ │ │ │ ├── 🏷️ class CwlPseudoVoigt -│ │ │ │ │ ├── 🏷️ class CwlPseudoVoigtEmpiricalAsymmetry -│ │ │ │ │ └── 🏷️ class CwlThompsonCoxHastings -│ │ │ │ ├── 📄 cwl_mixins.py -│ │ │ │ │ ├── 🏷️ class CwlBroadeningMixin -│ │ │ │ │ ├── 🏷️ class EmpiricalAsymmetryMixin -│ │ │ │ │ └── 🏷️ class FcjAsymmetryMixin -│ │ │ │ ├── 📄 factory.py -│ │ │ │ │ └── 🏷️ class PeakFactory -│ │ │ │ ├── 📄 tof.py -│ │ │ │ │ ├── 🏷️ class TofPseudoVoigt -│ │ │ │ │ ├── 🏷️ class TofJorgensen -│ │ │ │ │ ├── 🏷️ class TofJorgensenVonDreele -│ │ │ │ │ └── 🏷️ class TofDoubleJorgensenVonDreele -│ │ │ │ ├── 📄 tof_mixins.py -│ │ │ │ │ ├── 🏷️ class TofGaussianBroadeningMixin -│ │ │ │ │ ├── 🏷️ class TofLorentzianBroadeningMixin -│ │ │ │ │ ├── 🏷️ class TofBackToBackExponentialMixin -│ │ │ │ │ └── 🏷️ class TofDoubleExponentialMixin -│ │ │ │ ├── 📄 total.py -│ │ │ │ │ └── 🏷️ class TotalGaussianDampedSinc -│ │ │ │ └── 📄 total_mixins.py -│ │ │ │ └── 🏷️ class TotalBroadeningMixin -│ │ │ └── 📄 __init__.py -│ │ ├── 📁 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 -│ │ │ │ └── 🏷️ class ExtinctionModelEnum -│ │ │ ├── 📄 factory.py -│ │ │ │ └── 🏷️ class ExperimentFactory -│ │ │ └── 📄 total_pd.py -│ │ │ └── 🏷️ class TotalPdExperiment -│ │ ├── 📄 __init__.py -│ │ └── 📄 collection.py -│ │ └── 🏷️ class Experiments -│ ├── 📁 structure -│ │ ├── 📁 categories -│ │ │ ├── 📁 atom_site_aniso -│ │ │ │ ├── 📄 __init__.py -│ │ │ │ ├── 📄 default.py -│ │ │ │ │ ├── 🏷️ class AtomSiteAniso -│ │ │ │ │ └── 🏷️ class AtomSiteAnisoCollection -│ │ │ │ └── 📄 factory.py -│ │ │ │ └── 🏷️ class AtomSiteAnisoFactory -│ │ │ ├── 📁 atom_sites -│ │ │ │ ├── 📄 __init__.py -│ │ │ │ ├── 📄 default.py -│ │ │ │ │ ├── 🏷️ class AtomSite -│ │ │ │ │ └── 🏷️ class AtomSites -│ │ │ │ ├── 📄 enums.py -│ │ │ │ │ └── 🏷️ class AdpTypeEnum -│ │ │ │ └── 📄 factory.py -│ │ │ │ └── 🏷️ class AtomSitesFactory -│ │ │ ├── 📁 cell -│ │ │ │ ├── 📄 __init__.py -│ │ │ │ ├── 📄 default.py -│ │ │ │ │ └── 🏷️ class Cell -│ │ │ │ └── 📄 factory.py -│ │ │ │ └── 🏷️ class CellFactory -│ │ │ ├── 📁 space_group -│ │ │ │ ├── 📄 __init__.py -│ │ │ │ ├── 📄 default.py -│ │ │ │ │ └── 🏷️ class SpaceGroup -│ │ │ │ └── 📄 factory.py -│ │ │ │ └── 🏷️ class SpaceGroupFactory -│ │ │ └── 📄 __init__.py -│ │ ├── 📁 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 BraggTickSet -│ │ │ ├── 🏷️ class PowderMeasVsCalcSpec -│ │ │ ├── 🏷️ class XAxisType -│ │ │ └── 🏷️ class PlotterBase -│ │ └── 📄 plotly.py -│ │ ├── 🏷️ class PowderCompositeRows -│ │ └── 🏷️ class PlotlyPlotter -│ ├── 📁 tablers -│ │ ├── 📄 __init__.py -│ │ ├── 📄 base.py -│ │ │ └── 🏷️ class TableBackendBase -│ │ ├── 📄 pandas.py -│ │ │ └── 🏷️ class PandasTableBackend -│ │ └── 📄 rich.py -│ │ └── 🏷️ class RichTableBackend -│ ├── 📄 __init__.py -│ ├── 📄 base.py -│ │ ├── 🏷️ class RendererBase -│ │ └── 🏷️ class RendererFactoryBase -│ ├── 📄 plotting.py -│ │ ├── 🏷️ class PlotterEngineEnum -│ │ ├── 🏷️ class _MeasVsCalcPlotOptions -│ │ ├── 🏷️ class Plotter -│ │ └── 🏷️ class PlotterFactory -│ ├── 📄 tables.py -│ │ ├── 🏷️ class TableEngineEnum -│ │ ├── 🏷️ class TableRenderer -│ │ └── 🏷️ class TableRendererFactory -│ └── 📄 utils.py -│ └── 🏷️ class JupyterScrollManager -├── 📁 io -│ ├── 📁 cif -│ │ ├── 📄 __init__.py -│ │ ├── 📄 handler.py -│ │ │ └── 🏷️ class CifHandler -│ │ ├── 📄 parse.py -│ │ └── 📄 serialize.py -│ ├── 📄 __init__.py -│ └── 📄 ascii.py -├── 📁 project -│ ├── 📁 categories -│ │ ├── 📁 display -│ │ │ ├── 📄 __init__.py -│ │ │ ├── 📄 default.py -│ │ │ │ └── 🏷️ class Display -│ │ │ └── 📄 factory.py -│ │ │ └── 🏷️ class DisplayFactory -│ │ └── 📄 __init__.py -│ ├── 📄 __init__.py -│ ├── 📄 project.py -│ │ └── 🏷️ class Project -│ └── 📄 project_info.py -│ └── 🏷️ class ProjectInfo -├── 📁 summary -│ ├── 📄 __init__.py -│ └── 📄 summary.py -│ └── 🏷️ class Summary -├── 📁 utils -│ ├── 📁 _vendored -│ │ ├── 📁 jupyter_dark_detect -│ │ │ ├── 📄 __init__.py -│ │ │ └── 📄 detector.py -│ │ ├── 📄 __init__.py -│ │ └── 📄 theme_detect.py -│ ├── 📄 __init__.py -│ ├── 📄 enums.py -│ │ └── 🏷️ class VerbosityEnum -│ ├── 📄 environment.py -│ ├── 📄 logging.py -│ │ ├── 🏷️ class IconifiedRichHandler -│ │ ├── 🏷️ class ConsoleManager -│ │ ├── 🏷️ class LoggerConfig -│ │ ├── 🏷️ class ExceptionHookManager -│ │ ├── 🏷️ class Logger -│ │ └── 🏷️ class ConsolePrinter -│ └── 📄 utils.py -├── 📄 __init__.py -└── 📄 __main__.py -``` diff --git a/docs/dev/package-structure-short.md b/docs/dev/package-structure-short.md deleted file mode 100644 index 30b4daf72..000000000 --- a/docs/dev/package-structure-short.md +++ /dev/null @@ -1,220 +0,0 @@ -# Package Structure (short) - -``` -📦 easydiffraction -├── 📁 analysis -│ ├── 📁 calculators -│ │ ├── 📄 __init__.py -│ │ ├── 📄 base.py -│ │ ├── 📄 crysfml.py -│ │ ├── 📄 cryspy.py -│ │ ├── 📄 factory.py -│ │ └── 📄 pdffit.py -│ ├── 📁 categories -│ │ ├── 📁 aliases -│ │ │ ├── 📄 __init__.py -│ │ │ ├── 📄 default.py -│ │ │ └── 📄 factory.py -│ │ ├── 📁 constraints -│ │ │ ├── 📄 __init__.py -│ │ │ ├── 📄 default.py -│ │ │ └── 📄 factory.py -│ │ ├── 📁 fit -│ │ │ ├── 📄 __init__.py -│ │ │ ├── 📄 default.py -│ │ │ ├── 📄 enums.py -│ │ │ └── 📄 factory.py -│ │ ├── 📁 joint_fit_experiments -│ │ │ ├── 📄 __init__.py -│ │ │ ├── 📄 default.py -│ │ │ └── 📄 factory.py -│ │ └── 📄 __init__.py -│ ├── 📁 fit_helpers -│ │ ├── 📄 __init__.py -│ │ ├── 📄 metrics.py -│ │ ├── 📄 reporting.py -│ │ └── 📄 tracking.py -│ ├── 📁 minimizers -│ │ ├── 📄 __init__.py -│ │ ├── 📄 base.py -│ │ ├── 📄 bumps.py -│ │ ├── 📄 bumps_amoeba.py -│ │ ├── 📄 bumps_de.py -│ │ ├── 📄 bumps_lm.py -│ │ ├── 📄 dfols.py -│ │ ├── 📄 enums.py -│ │ ├── 📄 factory.py -│ │ ├── 📄 lmfit.py -│ │ ├── 📄 lmfit_least_squares.py -│ │ └── 📄 lmfit_leastsq.py -│ ├── 📄 __init__.py -│ ├── 📄 analysis.py -│ ├── 📄 fitting.py -│ └── 📄 sequential.py -├── 📁 core -│ ├── 📄 __init__.py -│ ├── 📄 category.py -│ ├── 📄 collection.py -│ ├── 📄 datablock.py -│ ├── 📄 diagnostic.py -│ ├── 📄 factory.py -│ ├── 📄 guard.py -│ ├── 📄 identity.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 -│ │ │ ├── 📁 calculation -│ │ │ │ ├── 📄 __init__.py -│ │ │ │ ├── 📄 default.py -│ │ │ │ └── 📄 factory.py -│ │ │ ├── 📁 data -│ │ │ │ ├── 📄 __init__.py -│ │ │ │ ├── 📄 bragg_pd.py -│ │ │ │ ├── 📄 bragg_sc.py -│ │ │ │ ├── 📄 factory.py -│ │ │ │ └── 📄 total_pd.py -│ │ │ ├── 📁 diffrn -│ │ │ │ ├── 📄 __init__.py -│ │ │ │ ├── 📄 default.py -│ │ │ │ └── 📄 factory.py -│ │ │ ├── 📁 excluded_regions -│ │ │ │ ├── 📄 __init__.py -│ │ │ │ ├── 📄 default.py -│ │ │ │ └── 📄 factory.py -│ │ │ ├── 📁 experiment_type -│ │ │ │ ├── 📄 __init__.py -│ │ │ │ ├── 📄 default.py -│ │ │ │ └── 📄 factory.py -│ │ │ ├── 📁 extinction -│ │ │ │ ├── 📄 __init__.py -│ │ │ │ ├── 📄 becker_coppens.py -│ │ │ │ └── 📄 factory.py -│ │ │ ├── 📁 instrument -│ │ │ │ ├── 📄 __init__.py -│ │ │ │ ├── 📄 base.py -│ │ │ │ ├── 📄 cwl.py -│ │ │ │ ├── 📄 factory.py -│ │ │ │ └── 📄 tof.py -│ │ │ ├── 📁 linked_crystal -│ │ │ │ ├── 📄 __init__.py -│ │ │ │ ├── 📄 default.py -│ │ │ │ └── 📄 factory.py -│ │ │ ├── 📁 linked_phases -│ │ │ │ ├── 📄 __init__.py -│ │ │ │ ├── 📄 default.py -│ │ │ │ └── 📄 factory.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 -│ │ ├── 📁 item -│ │ │ ├── 📄 __init__.py -│ │ │ ├── 📄 base.py -│ │ │ ├── 📄 bragg_pd.py -│ │ │ ├── 📄 bragg_sc.py -│ │ │ ├── 📄 enums.py -│ │ │ ├── 📄 factory.py -│ │ │ └── 📄 total_pd.py -│ │ ├── 📄 __init__.py -│ │ └── 📄 collection.py -│ ├── 📁 structure -│ │ ├── 📁 categories -│ │ │ ├── 📁 atom_site_aniso -│ │ │ │ ├── 📄 __init__.py -│ │ │ │ ├── 📄 default.py -│ │ │ │ └── 📄 factory.py -│ │ │ ├── 📁 atom_sites -│ │ │ │ ├── 📄 __init__.py -│ │ │ │ ├── 📄 default.py -│ │ │ │ ├── 📄 enums.py -│ │ │ │ └── 📄 factory.py -│ │ │ ├── 📁 cell -│ │ │ │ ├── 📄 __init__.py -│ │ │ │ ├── 📄 default.py -│ │ │ │ └── 📄 factory.py -│ │ │ ├── 📁 space_group -│ │ │ │ ├── 📄 __init__.py -│ │ │ │ ├── 📄 default.py -│ │ │ │ └── 📄 factory.py -│ │ │ └── 📄 __init__.py -│ │ ├── 📁 item -│ │ │ ├── 📄 __init__.py -│ │ │ ├── 📄 base.py -│ │ │ └── 📄 factory.py -│ │ ├── 📄 __init__.py -│ │ └── 📄 collection.py -│ └── 📄 __init__.py -├── 📁 display -│ ├── 📁 plotters -│ │ ├── 📄 __init__.py -│ │ ├── 📄 ascii.py -│ │ ├── 📄 base.py -│ │ └── 📄 plotly.py -│ ├── 📁 tablers -│ │ ├── 📄 __init__.py -│ │ ├── 📄 base.py -│ │ ├── 📄 pandas.py -│ │ └── 📄 rich.py -│ ├── 📄 __init__.py -│ ├── 📄 base.py -│ ├── 📄 plotting.py -│ ├── 📄 tables.py -│ └── 📄 utils.py -├── 📁 io -│ ├── 📁 cif -│ │ ├── 📄 __init__.py -│ │ ├── 📄 handler.py -│ │ ├── 📄 parse.py -│ │ └── 📄 serialize.py -│ ├── 📄 __init__.py -│ └── 📄 ascii.py -├── 📁 project -│ ├── 📁 categories -│ │ ├── 📁 display -│ │ │ ├── 📄 __init__.py -│ │ │ ├── 📄 default.py -│ │ │ └── 📄 factory.py -│ │ └── 📄 __init__.py -│ ├── 📄 __init__.py -│ ├── 📄 project.py -│ └── 📄 project_info.py -├── 📁 summary -│ ├── 📄 __init__.py -│ └── 📄 summary.py -├── 📁 utils -│ ├── 📁 _vendored -│ │ ├── 📁 jupyter_dark_detect -│ │ │ ├── 📄 __init__.py -│ │ │ └── 📄 detector.py -│ │ ├── 📄 __init__.py -│ │ └── 📄 theme_detect.py -│ ├── 📄 __init__.py -│ ├── 📄 enums.py -│ ├── 📄 environment.py -│ ├── 📄 logging.py -│ └── 📄 utils.py -├── 📄 __init__.py -└── 📄 __main__.py -``` diff --git a/docs/architecture/package-structure-full.md b/docs/dev/package-structure/full.md similarity index 69% rename from docs/architecture/package-structure-full.md rename to docs/dev/package-structure/full.md index be35bb076..677f5851d 100644 --- a/docs/architecture/package-structure-full.md +++ b/docs/dev/package-structure/full.md @@ -24,6 +24,54 @@ │ │ │ │ └── 🏷️ class Aliases │ │ │ └── 📄 factory.py │ │ │ └── 🏷️ class AliasesFactory +│ │ ├── 📁 bayesian_convergence +│ │ │ ├── 📄 __init__.py +│ │ │ ├── 📄 default.py +│ │ │ │ └── 🏷️ class BayesianConvergence +│ │ │ └── 📄 factory.py +│ │ │ └── 🏷️ class BayesianConvergenceFactory +│ │ ├── 📁 bayesian_distribution_caches +│ │ │ ├── 📄 __init__.py +│ │ │ ├── 📄 default.py +│ │ │ │ ├── 🏷️ class BayesianDistributionCacheItem +│ │ │ │ └── 🏷️ class BayesianDistributionCaches +│ │ │ └── 📄 factory.py +│ │ │ └── 🏷️ class BayesianDistributionCachesFactory +│ │ ├── 📁 bayesian_pair_caches +│ │ │ ├── 📄 __init__.py +│ │ │ ├── 📄 default.py +│ │ │ │ ├── 🏷️ class BayesianPairCachePaths +│ │ │ │ ├── 🏷️ class BayesianPairCacheItem +│ │ │ │ └── 🏷️ class BayesianPairCaches +│ │ │ └── 📄 factory.py +│ │ │ └── 🏷️ class BayesianPairCachesFactory +│ │ ├── 📁 bayesian_parameter_posteriors +│ │ │ ├── 📄 __init__.py +│ │ │ ├── 📄 default.py +│ │ │ │ ├── 🏷️ class BayesianParameterPosteriorItem +│ │ │ │ └── 🏷️ class BayesianParameterPosteriors +│ │ │ └── 📄 factory.py +│ │ │ └── 🏷️ class BayesianParameterPosteriorsFactory +│ │ ├── 📁 bayesian_predictive_datasets +│ │ │ ├── 📄 __init__.py +│ │ │ ├── 📄 default.py +│ │ │ │ ├── 🏷️ class BayesianPredictiveDatasetPaths +│ │ │ │ ├── 🏷️ class BayesianPredictiveDatasetItem +│ │ │ │ └── 🏷️ class BayesianPredictiveDatasets +│ │ │ └── 📄 factory.py +│ │ │ └── 🏷️ class BayesianPredictiveDatasetsFactory +│ │ ├── 📁 bayesian_result +│ │ │ ├── 📄 __init__.py +│ │ │ ├── 📄 default.py +│ │ │ │ └── 🏷️ class BayesianResult +│ │ │ └── 📄 factory.py +│ │ │ └── 🏷️ class BayesianResultFactory +│ │ ├── 📁 bayesian_sampler +│ │ │ ├── 📄 __init__.py +│ │ │ ├── 📄 default.py +│ │ │ │ └── 🏷️ class BayesianSampler +│ │ │ └── 📄 factory.py +│ │ │ └── 🏷️ class BayesianSamplerFactory │ │ ├── 📁 constraints │ │ │ ├── 📄 __init__.py │ │ │ ├── 📄 default.py @@ -31,47 +79,98 @@ │ │ │ │ └── 🏷️ class Constraints │ │ │ └── 📄 factory.py │ │ │ └── 🏷️ class ConstraintsFactory -│ │ ├── 📁 fit +│ │ ├── 📁 deterministic_result │ │ │ ├── 📄 __init__.py │ │ │ ├── 📄 default.py -│ │ │ │ └── 🏷️ class Fit -│ │ │ ├── 📄 enums.py -│ │ │ │ └── 🏷️ class FitModeEnum +│ │ │ │ └── 🏷️ class DeterministicResult │ │ │ └── 📄 factory.py -│ │ │ └── 🏷️ class FitFactory -│ │ ├── 📁 joint_fit_experiments +│ │ │ └── 🏷️ class DeterministicResultFactory +│ │ ├── 📁 fit_parameter_correlations │ │ │ ├── 📄 __init__.py │ │ │ ├── 📄 default.py -│ │ │ │ ├── 🏷️ class JointFitExperiment -│ │ │ │ └── 🏷️ class JointFitExperiments +│ │ │ │ ├── 🏷️ class FitParameterCorrelationItem +│ │ │ │ └── 🏷️ class FitParameterCorrelations │ │ │ └── 📄 factory.py -│ │ │ └── 🏷️ class JointFitExperimentsFactory +│ │ │ └── 🏷️ class FitParameterCorrelationsFactory +│ │ ├── 📁 fit_parameters +│ │ │ ├── 📄 __init__.py +│ │ │ ├── 📄 default.py +│ │ │ │ ├── 🏷️ class FitParameterItem +│ │ │ │ └── 🏷️ class FitParameters +│ │ │ └── 📄 factory.py +│ │ │ └── 🏷️ class FitParametersFactory +│ │ ├── 📁 fit_result +│ │ │ ├── 📄 __init__.py +│ │ │ ├── 📄 default.py +│ │ │ │ └── 🏷️ class FitResult +│ │ │ └── 📄 factory.py +│ │ │ └── 🏷️ class FitResultFactory +│ │ ├── 📁 fit_state +│ │ ├── 📁 fitting +│ │ │ ├── 📄 __init__.py +│ │ │ ├── 📄 default.py +│ │ │ │ └── 🏷️ class Fitting +│ │ │ └── 📄 factory.py +│ │ │ └── 🏷️ class FittingFactory +│ │ ├── 📁 joint_fit +│ │ │ ├── 📄 __init__.py +│ │ │ ├── 📄 default.py +│ │ │ │ ├── 🏷️ class JointFitItem +│ │ │ │ └── 🏷️ class JointFitCollection +│ │ │ └── 📄 factory.py +│ │ │ └── 🏷️ class JointFitFactory +│ │ ├── 📁 sequential_fit +│ │ │ ├── 📄 __init__.py +│ │ │ ├── 📄 default.py +│ │ │ │ └── 🏷️ class SequentialFit +│ │ │ └── 📄 factory.py +│ │ │ └── 🏷️ class SequentialFitFactory +│ │ ├── 📁 sequential_fit_extract +│ │ │ ├── 📄 __init__.py +│ │ │ ├── 📄 default.py +│ │ │ │ ├── 🏷️ class SequentialFitExtractItem +│ │ │ │ └── 🏷️ class SequentialFitExtractCollection +│ │ │ └── 📄 factory.py +│ │ │ └── 🏷️ class SequentialFitExtractFactory │ │ └── 📄 __init__.py │ ├── 📁 fit_helpers │ │ ├── 📄 __init__.py +│ │ ├── 📄 bayesian.py +│ │ │ ├── 🏷️ class PosteriorParameterSummary +│ │ │ ├── 🏷️ class PosteriorPredictiveSummary +│ │ │ ├── 🏷️ class PosteriorSamples +│ │ │ └── 🏷️ class BayesianFitResults │ │ ├── 📄 metrics.py │ │ ├── 📄 reporting.py │ │ │ └── 🏷️ class FitResults │ │ └── 📄 tracking.py -│ │ ├── 🏷️ class _TerminalLiveHandle +│ │ ├── 🏷️ class SamplerProgressUpdate │ │ └── 🏷️ class FitProgressTracker │ ├── 📁 minimizers │ │ ├── 📄 __init__.py │ │ ├── 📄 base.py │ │ │ └── 🏷️ class MinimizerBase │ │ ├── 📄 bumps.py +│ │ │ ├── 🏷️ class _BumpsEvaluationLimitError │ │ │ ├── 🏷️ class _EasyDiffractionFitness +│ │ │ ├── 🏷️ class _BumpsProgressMonitor │ │ │ └── 🏷️ class BumpsMinimizer │ │ ├── 📄 bumps_amoeba.py │ │ │ └── 🏷️ class BumpsAmoebaMinimizer │ │ ├── 📄 bumps_de.py │ │ │ └── 🏷️ class BumpsDEMinimizer +│ │ ├── 📄 bumps_dream.py +│ │ │ ├── 🏷️ class _DreamRunContext +│ │ │ ├── 🏷️ class _DreamDriverResult +│ │ │ ├── 🏷️ class _DreamProgressMonitor +│ │ │ └── 🏷️ class BumpsDreamMinimizer │ │ ├── 📄 bumps_lm.py │ │ │ └── 🏷️ class BumpsLmMinimizer │ │ ├── 📄 dfols.py │ │ │ └── 🏷️ class DfolsMinimizer │ │ ├── 📄 enums.py -│ │ │ └── 🏷️ class MinimizerTypeEnum +│ │ │ ├── 🏷️ class MinimizerTypeEnum +│ │ │ └── 🏷️ class DreamPopulationInitializationEnum │ │ ├── 📄 factory.py │ │ │ └── 🏷️ class MinimizerFactory │ │ ├── 📄 lmfit.py @@ -83,16 +182,29 @@ │ ├── 📄 __init__.py │ ├── 📄 analysis.py │ │ ├── 🏷️ class AnalysisDisplay +│ │ ├── 🏷️ class _AnalysisOwnerAccessorsMixin +│ │ ├── 🏷️ class _AnalysisPersistedCategoryAccessorsMixin │ │ └── 🏷️ class Analysis +│ ├── 📄 enums.py +│ │ ├── 🏷️ class FitModeEnum +│ │ ├── 🏷️ class FitResultKindEnum +│ │ └── 🏷️ class FitCorrelationSourceEnum │ ├── 📄 fitting.py │ │ └── 🏷️ class Fitter │ └── 📄 sequential.py -│ └── 🏷️ class SequentialFitTemplate +│ ├── 🏷️ class SequentialFitExtractRule +│ ├── 🏷️ class SequentialFitTemplate +│ ├── 🏷️ class SequentialProgressState +│ ├── 🏷️ class SequentialProgressContext +│ ├── 🏷️ class _ChunkProgressMetrics +│ └── 🏷️ class SequentialRunPlan ├── 📁 core │ ├── 📄 __init__.py │ ├── 📄 category.py │ │ ├── 🏷️ class CategoryItem │ │ └── 🏷️ class CategoryCollection +│ ├── 📄 category_owner.py +│ │ └── 🏷️ class CategoryOwner │ ├── 📄 collection.py │ │ └── 🏷️ class CollectionBase │ ├── 📄 datablock.py @@ -126,10 +238,14 @@ │ └── 📄 variable.py │ ├── 🏷️ class GenericDescriptorBase │ ├── 🏷️ class GenericStringDescriptor +│ ├── 🏷️ class GenericBoolDescriptor │ ├── 🏷️ class GenericNumericDescriptor +│ ├── 🏷️ class GenericIntegerDescriptor │ ├── 🏷️ class GenericParameter │ ├── 🏷️ class StringDescriptor +│ ├── 🏷️ class BoolDescriptor │ ├── 🏷️ class NumericDescriptor +│ ├── 🏷️ class IntegerDescriptor │ └── 🏷️ class Parameter ├── 📁 crystallography │ ├── 📄 __init__.py @@ -364,10 +480,19 @@ │ │ └── 🏷️ class RendererFactoryBase │ ├── 📄 plotting.py │ │ ├── 🏷️ class PlotterEngineEnum +│ │ ├── 🏷️ class PosteriorPairPlotStyleEnum │ │ ├── 🏷️ class _MeasVsCalcPlotOptions │ │ ├── 🏷️ class _PowderMeasVsCalcSeries +│ │ ├── 🏷️ class _PosteriorDistributionContext +│ │ ├── 🏷️ class _PosteriorPairsContext +│ │ ├── 🏷️ class _CorrelationHeatmapContext +│ │ ├── 🏷️ class _PosteriorPairsLegendState │ │ ├── 🏷️ class Plotter │ │ └── 🏷️ class PlotterFactory +│ ├── 📄 progress.py +│ │ ├── 🏷️ class _TerminalLiveHandle +│ │ ├── 🏷️ class ActivityIndicator +│ │ └── 🏷️ class _ActivityIndicatorContext │ ├── 📄 tables.py │ │ ├── 🏷️ class TableEngineEnum │ │ ├── 🏷️ class TableRenderer @@ -382,21 +507,41 @@ │ │ ├── 📄 parse.py │ │ └── 📄 serialize.py │ ├── 📄 __init__.py -│ └── 📄 ascii.py +│ ├── 📄 ascii.py +│ └── 📄 results_sidecar.py ├── 📁 project │ ├── 📁 categories -│ │ ├── 📁 display +│ │ ├── 📁 info +│ │ │ ├── 📄 __init__.py +│ │ │ ├── 📄 default.py +│ │ │ │ └── 🏷️ class ProjectInfo +│ │ │ └── 📄 factory.py +│ │ │ └── 🏷️ class ProjectInfoFactory +│ │ ├── 📁 rendering +│ │ │ ├── 📄 __init__.py +│ │ │ ├── 📄 default.py +│ │ │ │ └── 🏷️ class Rendering +│ │ │ └── 📄 factory.py +│ │ │ └── 🏷️ class RenderingFactory +│ │ ├── 📁 verbosity │ │ │ ├── 📄 __init__.py │ │ │ ├── 📄 default.py -│ │ │ │ └── 🏷️ class Display +│ │ │ │ └── 🏷️ class Verbosity │ │ │ └── 📄 factory.py -│ │ │ └── 🏷️ class DisplayFactory +│ │ │ └── 🏷️ class VerbosityFactory │ │ └── 📄 __init__.py │ ├── 📄 __init__.py +│ ├── 📄 display.py +│ │ ├── 🏷️ class PatternOptionStatus +│ │ ├── 🏷️ class ParameterDisplay +│ │ ├── 🏷️ class FitDisplay +│ │ ├── 🏷️ class PosteriorDisplay +│ │ └── 🏷️ class ProjectDisplay │ ├── 📄 project.py │ │ └── 🏷️ class Project +│ ├── 📄 project_config.py +│ │ └── 🏷️ class ProjectConfig │ └── 📄 project_info.py -│ └── 🏷️ class ProjectInfo ├── 📁 summary │ ├── 📄 __init__.py │ └── 📄 summary.py diff --git a/docs/architecture/package-structure-short.md b/docs/dev/package-structure/short.md similarity index 73% rename from docs/architecture/package-structure-short.md rename to docs/dev/package-structure/short.md index ba2a0b33b..367a36a51 100644 --- a/docs/architecture/package-structure-short.md +++ b/docs/dev/package-structure/short.md @@ -15,22 +15,75 @@ │ │ │ ├── 📄 __init__.py │ │ │ ├── 📄 default.py │ │ │ └── 📄 factory.py +│ │ ├── 📁 bayesian_convergence +│ │ │ ├── 📄 __init__.py +│ │ │ ├── 📄 default.py +│ │ │ └── 📄 factory.py +│ │ ├── 📁 bayesian_distribution_caches +│ │ │ ├── 📄 __init__.py +│ │ │ ├── 📄 default.py +│ │ │ └── 📄 factory.py +│ │ ├── 📁 bayesian_pair_caches +│ │ │ ├── 📄 __init__.py +│ │ │ ├── 📄 default.py +│ │ │ └── 📄 factory.py +│ │ ├── 📁 bayesian_parameter_posteriors +│ │ │ ├── 📄 __init__.py +│ │ │ ├── 📄 default.py +│ │ │ └── 📄 factory.py +│ │ ├── 📁 bayesian_predictive_datasets +│ │ │ ├── 📄 __init__.py +│ │ │ ├── 📄 default.py +│ │ │ └── 📄 factory.py +│ │ ├── 📁 bayesian_result +│ │ │ ├── 📄 __init__.py +│ │ │ ├── 📄 default.py +│ │ │ └── 📄 factory.py +│ │ ├── 📁 bayesian_sampler +│ │ │ ├── 📄 __init__.py +│ │ │ ├── 📄 default.py +│ │ │ └── 📄 factory.py │ │ ├── 📁 constraints │ │ │ ├── 📄 __init__.py │ │ │ ├── 📄 default.py │ │ │ └── 📄 factory.py -│ │ ├── 📁 fit +│ │ ├── 📁 deterministic_result │ │ │ ├── 📄 __init__.py │ │ │ ├── 📄 default.py -│ │ │ ├── 📄 enums.py │ │ │ └── 📄 factory.py -│ │ ├── 📁 joint_fit_experiments +│ │ ├── 📁 fit_parameter_correlations +│ │ │ ├── 📄 __init__.py +│ │ │ ├── 📄 default.py +│ │ │ └── 📄 factory.py +│ │ ├── 📁 fit_parameters +│ │ │ ├── 📄 __init__.py +│ │ │ ├── 📄 default.py +│ │ │ └── 📄 factory.py +│ │ ├── 📁 fit_result +│ │ │ ├── 📄 __init__.py +│ │ │ ├── 📄 default.py +│ │ │ └── 📄 factory.py +│ │ ├── 📁 fit_state +│ │ ├── 📁 fitting +│ │ │ ├── 📄 __init__.py +│ │ │ ├── 📄 default.py +│ │ │ └── 📄 factory.py +│ │ ├── 📁 joint_fit +│ │ │ ├── 📄 __init__.py +│ │ │ ├── 📄 default.py +│ │ │ └── 📄 factory.py +│ │ ├── 📁 sequential_fit +│ │ │ ├── 📄 __init__.py +│ │ │ ├── 📄 default.py +│ │ │ └── 📄 factory.py +│ │ ├── 📁 sequential_fit_extract │ │ │ ├── 📄 __init__.py │ │ │ ├── 📄 default.py │ │ │ └── 📄 factory.py │ │ └── 📄 __init__.py │ ├── 📁 fit_helpers │ │ ├── 📄 __init__.py +│ │ ├── 📄 bayesian.py │ │ ├── 📄 metrics.py │ │ ├── 📄 reporting.py │ │ └── 📄 tracking.py @@ -40,6 +93,7 @@ │ │ ├── 📄 bumps.py │ │ ├── 📄 bumps_amoeba.py │ │ ├── 📄 bumps_de.py +│ │ ├── 📄 bumps_dream.py │ │ ├── 📄 bumps_lm.py │ │ ├── 📄 dfols.py │ │ ├── 📄 enums.py @@ -49,11 +103,13 @@ │ │ └── 📄 lmfit_leastsq.py │ ├── 📄 __init__.py │ ├── 📄 analysis.py +│ ├── 📄 enums.py │ ├── 📄 fitting.py │ └── 📄 sequential.py ├── 📁 core │ ├── 📄 __init__.py │ ├── 📄 category.py +│ ├── 📄 category_owner.py │ ├── 📄 collection.py │ ├── 📄 datablock.py │ ├── 📄 diagnostic.py @@ -184,6 +240,7 @@ │ ├── 📄 __init__.py │ ├── 📄 base.py │ ├── 📄 plotting.py +│ ├── 📄 progress.py │ ├── 📄 tables.py │ └── 📄 utils.py ├── 📁 io @@ -193,16 +250,27 @@ │ │ ├── 📄 parse.py │ │ └── 📄 serialize.py │ ├── 📄 __init__.py -│ └── 📄 ascii.py +│ ├── 📄 ascii.py +│ └── 📄 results_sidecar.py ├── 📁 project │ ├── 📁 categories -│ │ ├── 📁 display +│ │ ├── 📁 info +│ │ │ ├── 📄 __init__.py +│ │ │ ├── 📄 default.py +│ │ │ └── 📄 factory.py +│ │ ├── 📁 rendering +│ │ │ ├── 📄 __init__.py +│ │ │ ├── 📄 default.py +│ │ │ └── 📄 factory.py +│ │ ├── 📁 verbosity │ │ │ ├── 📄 __init__.py │ │ │ ├── 📄 default.py │ │ │ └── 📄 factory.py │ │ └── 📄 __init__.py │ ├── 📄 __init__.py +│ ├── 📄 display.py │ ├── 📄 project.py +│ ├── 📄 project_config.py │ └── 📄 project_info.py ├── 📁 summary │ ├── 📄 __init__.py diff --git a/docs/dev/plan_powder-chart-y-range.md b/docs/dev/plan_powder-chart-y-range.md deleted file mode 100644 index c987fe47a..000000000 --- a/docs/dev/plan_powder-chart-y-range.md +++ /dev/null @@ -1,138 +0,0 @@ -# Powder Chart Y-Range Fix Plan - -**Date:** 2026-05-06 **Status:** Phase 2 verified — complete - ---- - -## 1. Goal - -Fix the Plotly composite powder measured-vs-calculated chart so the main -intensity row is not anchored to zero. The y-axis range should be -derived from all displayed main-row intensity series: measured -(`Imeas`), calculated (`Icalc`), and background (`Ibkg`) when present. - -The intended display range is: - -```text -lower = min(Imeas, Icalc, Ibkg) - margin -upper = max(Imeas, Icalc, Ibkg) + margin -``` - -where `margin` is controlled by a dedicated constant of about 5% of the -main intensity span. The lower bound must use `min - margin`, not -`min + margin`, so the lowest displayed point remains visible with -padding below it. - ---- - -## 2. Current Findings - -- The affected code is `PlotlyPlotter._get_main_intensity_range()` in - `src/easydiffraction/display/plotters/plotly.py`. -- It currently uses only `y_meas` and `y_calc`, then forces - `lower_limit = min(0.0, main_y_min)`. That explains positive powder - charts being truncated to a `0..max` range. -- `PowderMeasVsCalcSpec` already carries optional `y_bkg`, and - `Plotter._plot_meas_vs_calc_data()` already filters - `pattern.intensity_bkg` into the spec for powder Bragg plots. -- Existing Plotly unit tests assert the current `0.0..max` y-range in - the residual scale-match tests, so those expectations must change. -- Repository memory notes confirm the background line is part of the - composite powder plot and should be considered display data. - ---- - -## 3. Scope - -### In Scope - -- Add a module-level constant in - `src/easydiffraction/display/plotters/plotly.py`, likely - `MAIN_INTENSITY_RANGE_MARGIN_FRACTION = 0.05`. -- Update `_get_main_intensity_range()` to compute min/max over `y_meas`, - `y_calc`, and non-empty `y_bkg` when present. -- Apply symmetric visual padding outside the data range using the new - constant. -- Preserve the existing empty-filtered-range behavior: empty required - series should still return a harmless fallback range. -- Preserve residual scale matching by letting `_get_residual_limit()` - use the newly padded main range and the existing - `residual_height_fraction`, so the residual row remains adjusted to - the main row size as it is now. -- Add/update focused unit tests for range calculation and affected - residual-scale expectations. - -### Out of Scope - -- No public plotting API changes. -- No user-configurable y-axis margin in this step. -- No changes to ASCII plotting unless a later review shows the same main - view problem exists there. -- No refactor of plot layout, Bragg tick sizing, hover templates, or - facade routing. - ---- - -## 4. Decisions - -- Use `min(Imeas, Icalc, Ibkg) - margin` for the lower y-axis bound and - `max(Imeas, Icalc, Ibkg) + margin` for the upper y-axis bound. -- Keep the residual plot scaled to the main intensity row, preserving - the current matched-scale behavior after the main range gains padding. - ---- - -## 5. Implementation Checklist - -- [ ] Create branch `feature/powder-chart-y-range` if requested. -- [x] In `src/easydiffraction/display/plotters/plotly.py`, add the - dedicated 5% y-range margin constant near the other Plotly layout - constants. -- [x] Update `_get_main_intensity_range()` so it includes background - intensity when available and uses the padded min/max range instead - of anchoring positive data to zero. -- [x] Keep zero-span data explicit and stable, using a small fallback - range around the datum because a percentage margin is undefined. -- [x] Confirm `_get_residual_limit()` continues to scale the residual - row from the updated main y-range and existing residual height - fraction. -- [x] Stop after Phase 1 and request review before adding or running - tests, following the repo workflow. - ---- - -## 6. Phase 2 Verification Checklist - -- [x] Add or update tests in - `tests/unit/easydiffraction/display/plotters/test_plotly.py` for: - - positive-only `Imeas`/`Icalc` data no longer starting at zero; - - `Ibkg` lowering or raising the main y-range when present; - - 5% padding on both ends of the main row; - - residual scale-match expectations after padding changes the main row - span; - - empty filtered arrays retaining the existing fallback behavior. -- [x] Keep the existing facade propagation test in - `tests/unit/easydiffraction/display/test_plotting.py` unless the - implementation reveals a missing background handoff case. -- [x] Run `pixi run fix`. -- [x] Run `pixi run check` until clean. -- [x] Run `pixi run unit-tests`. -- [x] Run `pixi run integration-tests`. -- [x] Run `pixi run script-tests`. - ---- - -## 7. Likely Files - -- `src/easydiffraction/display/plotters/plotly.py` -- `tests/unit/easydiffraction/display/plotters/test_plotly.py` -- `tests/unit/easydiffraction/display/test_plotting.py` only if a - facade-level test gap is discovered during verification. - ---- - -## 8. Suggested Commit Message - -```text -Fix powder chart y-axis range -``` diff --git a/docs/dev/ROADMAP.md b/docs/dev/roadmap/ROADMAP.md similarity index 100% rename from docs/dev/ROADMAP.md rename to docs/dev/roadmap/ROADMAP.md diff --git a/docs/docs/cli/index.md b/docs/docs/cli/index.md index 5fc13c771..46c283879 100644 --- a/docs/docs/cli/index.md +++ b/docs/docs/cli/index.md @@ -48,6 +48,41 @@ List all available tutorial notebooks: python -m easydiffraction list-tutorials ``` +### List Example Data + +List all available example data files and downloadable project archives: + +```bash +python -m easydiffraction list-data +``` + +The table includes the data ID, file name, record kind, and description. + +### Download Example Data + +Download a specific example data record by ID: + +```bash +python -m easydiffraction download-data 3 +``` + +For downloadable saved projects, the ZIP archive is extracted +automatically, the ZIP file is removed, and the extracted project path +is reported: + +```bash +python -m easydiffraction download-data 30 +``` + +This makes it possible to go straight from download to a project-first +CLI command such as: + +```bash +python -m easydiffraction EXTRACTED_PROJECT_DIR display +``` + +Use the extracted path printed by `download-data`. + ### Download Tutorials Download a specific tutorial by ID: @@ -66,12 +101,16 @@ Both commands accept `--destination` (`-d`) to specify the output directory (default: `tutorials/`) and `--overwrite` (`-o`) to replace existing files. +`download-data` also accepts `--destination` (`-d`) and `--overwrite` +(`-o`). For project archives, `--overwrite` replaces the extracted +project directory before downloading and unpacking a fresh copy. + ### Fit a Project Load a saved project and run structural refinement: ```bash -python -m easydiffraction fit PROJECT_DIR +python -m easydiffraction PROJECT_DIR fit ``` `PROJECT_DIR` is the path to a project directory previously created by @@ -86,5 +125,35 @@ Use the `--dry` flag to run the fit **without overwriting** the project files: ```bash -python -m easydiffraction fit PROJECT_DIR --dry +python -m easydiffraction PROJECT_DIR fit --dry ``` + +EasyDiffraction also accepts the legacy subcommand-first form +`python -m easydiffraction fit PROJECT_DIR`, but the project-first form +is recommended because it makes it easy to rerun the same command and +swap only the action. + +### Display a Project + +Load a saved project and show the outputs that match its current fit +state and rendering backend: + +```bash +python -m easydiffraction PROJECT_DIR display +``` + +For typical non-sequential projects this includes the latest fit +results, parameter correlations, default pattern views, and when the +saved state is Bayesian also posterior distributions and predictive +checks. Plotly-only views such as posterior pair plots are shown only +when the active chart engine is Plotly. + +### Undo the Last Fit + +The CLI already reserves the project-first undo command shape: + +```bash +python -m easydiffraction PROJECT_DIR undo +``` + +This command currently reports that undo support is not implemented yet. diff --git a/docs/docs/installation-and-setup/index.md b/docs/docs/installation-and-setup/index.md index fdea87bfc..ee86679ba 100644 --- a/docs/docs/installation-and-setup/index.md +++ b/docs/docs/installation-and-setup/index.md @@ -244,17 +244,18 @@ once using the command line, as shown below. - Navigate to your existing Pixi project, created as described in the [Installing with Pixi](#installing-with-pixi) section. -- Add JupyterLab and the Pixi kernel for Jupyter: +- Add JupyterLab, Interactive Python shell and the Pixi kernel for + Jupyter: ```txt - pixi add --pypi jupyterlab pixi-kernel + pixi add --pypi jupyterlab ipython pixi-kernel ``` - Download all the EasyDiffraction tutorials to the `tutorials/` directory: ```txt pixi run easydiffraction download-all-tutorials ``` -- Start JupyterLab in the `tutorials/` directory to access the - notebooks: +- Start the JupyterLab server in the `tutorials/` directory to access + the notebooks: ```txt pixi run jupyter lab tutorials/ ``` @@ -263,9 +264,10 @@ once using the command line, as shown below. ### Classical Run Tutorials Locally -- Install Jupyter Notebook and IPython kernel: +- Install Jupyter Notebook, Interactive Python shell and the IPython + kernel: ```txt - pip install notebook ipykernel + pip install notebook ipython ipykernel ``` - Add the virtual environment as a Jupyter kernel: ```txt @@ -276,13 +278,14 @@ once using the command line, as shown below. ```txt python -m easydiffraction download-all-tutorials ``` -- Launch the Jupyter Notebook server (opens browser automatically at - `http://localhost:8888/`): +- Start the Jupyter Notebook server in the `tutorials/` directory to + access the notebooks: ```txt jupyter notebook tutorials/ ``` -- Open one of the `*.ipynb` files and select the - `EasyDiffraction Python kernel` to get started. +- Your web browser should open automatically. Click on one of the + `*.ipynb` files and select the `EasyDiffraction Python kernel` to get + started. ### Run Tutorials via Google Colab diff --git a/docs/docs/quick-reference/index.md b/docs/docs/quick-reference/index.md new file mode 100644 index 000000000..9c4d715bd --- /dev/null +++ b/docs/docs/quick-reference/index.md @@ -0,0 +1,430 @@ +--- +icon: material/clipboard-text-outline +--- + +# :material-clipboard-text-outline: Quick Reference + +This page is a short refresher for day-to-day EasyDiffraction work. It +collects the commands you are most likely to need when returning to a +project, preparing a quick refinement, or checking how to inspect +parameters and results. + +For complete explanations, use the [User Guide](../user-guide/index.md) +and [Tutorials](../tutorials/index.md). + +## Start a Session + +Import the package and create or load a project: + +```python +import easydiffraction as ed + +project = ed.Project(name='lbco_hrpt') +``` + +```python +from easydiffraction import Project + +project = Project.load('lbco_hrpt') +``` + +Check the installed version: + +```python +ed.show_version() +``` + +## Get Example Data + +Download a dataset by ID into a local directory: + +```python +ed.list_data() + +structure_path = ed.download_data(id=1, destination='data') +data_path = ed.download_data(id=3, destination='data') +``` + +Project archives are extracted automatically, and `download_data()` +returns the extracted project directory path. + +For tutorial notebooks: + +```python +ed.list_tutorials() +ed.download_tutorial(id=1, destination='tutorials') +ed.download_all_tutorials(destination='tutorials') +``` + +## Build a Project + +Load a structure from CIF: + +```python +project.structures.add_from_cif_path(structure_path) +project.structures.show_names() + +structure = project.structures['lbco'] +``` + +Create a structure directly: + +```python +project.structures.create(name='lbco') +structure = project.structures['lbco'] + +structure.space_group.name_h_m = 'P m -3 m' +structure.space_group.it_coordinate_system_code = '1' +structure.cell.length_a = 3.88 +``` + +Add an atom site: + +```python +structure.atom_sites.create( + label='O', + type_symbol='O', + fract_x=0, + fract_y=0.5, + fract_z=0.5, + wyckoff_letter='c', + adp_iso=0.5, +) +``` + +Load an experiment from measured data: + +```python +project.experiments.add_from_data_path( + name='hrpt', + data_path=data_path, + sample_form='powder', + beam_mode='constant wavelength', + radiation_probe='neutron', +) + +experiment = project.experiments['hrpt'] +``` + +Set common experiment parameters: + +```python +experiment.instrument.setup_wavelength = 1.494 +experiment.instrument.calib_twotheta_offset = 0.6 + +experiment.peak.broad_gauss_u = 0.1 +experiment.peak.broad_gauss_v = -0.1 +experiment.peak.broad_gauss_w = 0.1 +experiment.peak.broad_lorentz_y = 0.1 +``` + +Add background points and excluded regions: + +```python +experiment.background.create(id='1', x=10, y=170) +experiment.background.create(id='2', x=30, y=170) + +experiment.excluded_regions.create(id='1', start=0, end=5) +experiment.excluded_regions.create(id='2', start=165, end=180) +``` + +Link a structure to an experiment: + +```python +experiment.linked_phases.create(id='lbco', scale=10.0) +``` + +## Inspect the Project + +Show names, CIF text, and plotting options: + +```python +project.structures.show_names() +project.experiments.show_names() + +structure.show_as_cif() +experiment.show_as_cif() + +project.display.show_pattern_options(expt_name='hrpt') +``` + +Open the main display views: + +```python +project.display.pattern(expt_name='hrpt') +project.display.parameters.all() +project.display.parameters.fittable() +project.display.parameters.free() +project.display.parameters.access() +project.display.parameters.cif_uids() +``` + +## Show Tables and Select Types + +EasyDiffraction uses two related display patterns: + +- `show_*()` usually lists supported choices or displays a current + configuration. +- `.show()` on a loop-style object usually prints rows you have already + created. + +Show created loop contents: + +```python +experiment.background.show() +experiment.excluded_regions.show() +project.analysis.constraints.show() +``` + +List supported type choices. The current selection is marked in the +output: + +```python +experiment.show_peak_profile_types() +experiment.show_background_types() +experiment.calculation.show_calculator_types() + +project.analysis.show_fitting_mode_types() +project.analysis.fitting.show_minimizer_types() + +project.rendering.show_chart_engines() +project.rendering.show_table_engines() +project.rendering.show_config() +``` + +Change the active type by assigning the corresponding `*_type` property: + +```python +experiment.peak_profile_type = 'pseudo-voigt' +experiment.background_type = 'line-segment' +experiment.calculation.calculator_type = 'cryspy' + +project.analysis.fitting_mode_type = 'single' +project.analysis.fitting.minimizer_type = 'lmfit' + +project.rendering.chart_engine = 'plotly' +project.rendering.table_engine = 'rich' +``` + +For single-crystal experiments, extinction uses the same pattern: + +```python +experiment.show_extinction_types() +experiment.extinction_type = 'becker-coppens' +``` + +## Find Commands with Help + +Use help methods when you do not remember the exact command. The most +useful starting points are the project-level display and analysis +facades: + +```python +project.display.help() +project.display.parameters.help() +project.display.fit.help() +project.analysis.help() +``` + +Drill into project collections to see what they contain: + +```python +project.structures.help() +project.experiments.help() +``` + +Then inspect one structure or experiment: + +```python +structure = project.structures['lbco'] +experiment = project.experiments['hrpt'] + +structure.help() +experiment.help() +``` + +Category-level help shows available parameters and methods. This is +often the fastest way to remember exact names: + +```python +structure.cell.help() +structure.atom_sites.help() +structure.atom_sites['O'].help() + +experiment.instrument.help() +experiment.peak.help() +experiment.background.help() +experiment.background['1'].help() +``` + +Individual parameters also expose help. Use this when you need to check +whether a parameter is writable, free, constrained, or has fit bounds: + +```python +structure.cell.length_a.help() +structure.atom_sites['O'].adp_iso.help() + +experiment.instrument.calib_twotheta_offset.help() +experiment.linked_phases['lbco'].scale.help() +``` + +The usual navigation pattern is: + +```text +project → structures/experiments → structure/experiment → category → item → parameter +``` + +## Refine Parameters + +Mark parameters as free: + +```python +structure.cell.length_a.free = True +structure.atom_sites['O'].adp_iso.free = True + +experiment.instrument.calib_twotheta_offset.free = True +experiment.peak.broad_gauss_u.free = True +experiment.background['1'].y.free = True +experiment.linked_phases['lbco'].scale.free = True +``` + +Choose calculators and minimizers: + +```python +experiment.calculation.show_calculator_types() +experiment.calculation.calculator_type = 'cryspy' + +project.analysis.show_fitting_mode_types() +project.analysis.fitting_mode_type = 'single' + +project.analysis.fitting.show_minimizer_types() +project.analysis.fitting.minimizer_type = 'lmfit' +``` + +Run a fit and inspect the result: + +```python +project.analysis.fit() + +project.display.fit.results() +project.display.fit.correlations() +project.display.pattern(expt_name='hrpt') +``` + +Run a sequential fit over a scan directory and plot parameter evolution: + +```python +scan_data_dir = 'path/to/scan-directory' +temperature = 'diffrn.ambient_temperature' + +project.analysis.sequential_fit_extract.create( + id='temperature', + target=temperature, + pattern=r'^TEMP\s+([0-9.]+)', + required=True, +) + +project.analysis.fitting_mode_type = 'sequential' +project.analysis.sequential_fit.data_dir = scan_data_dir +project.analysis.sequential_fit.max_workers = 'auto' + +project.analysis.fit() +project.display.fit.series(structure.cell.length_a, versus=temperature) +project.display.fit.series(versus=temperature) + +project.apply_params_from_csv(row_index=0) +project.display.pattern(expt_name='d20') +``` + +Use the same persisted `diffrn.*` path for both `target` and `versus`. +`project.display.fit.series(versus=temperature)` plots every fitted +parameter one after another. Sequential fitting writes per-dataset +results to `analysis/results.csv`, so inspect them with `fit.series()` +and `apply_params_from_csv()` rather than `display.fit.results()`. + +After a Bayesian fit, inspect posterior displays: + +```python +project.display.posterior.distribution(param) +project.display.posterior.distribution() +project.display.posterior.pairs() +project.display.posterior.predictive(expt_name='hrpt') +``` + +Call `project.display.posterior.distribution()` without `param` to plot +the marginal distribution for each free parameter one by one. + +## Add Simple Constraints + +Create aliases from parameter objects, then define a constraint +expression using those aliases: + +```python +project.analysis.aliases.create( + label='biso_la', + param=project.structures['lbco'].atom_sites['La'].adp_iso, +) +project.analysis.aliases.create( + label='biso_ba', + param=project.structures['lbco'].atom_sites['Ba'].adp_iso, +) + +project.analysis.constraints.create(expression='biso_ba = biso_la') +``` + +Show the created constraints: + +```python +project.analysis.constraints.show() +``` + +Then fit again: + +```python +project.analysis.fit() +project.display.fit.results() +``` + +## Save and Reuse Work + +Save a project directory for later: + +```python +project.save_as(dir_path='lbco_hrpt') +project.save() +``` + +Load it again: + +```python +project = ed.Project.load('lbco_hrpt') +``` + +Run a saved project from the command line: + +```bash +python -m easydiffraction lbco_hrpt fit +python -m easydiffraction lbco_hrpt fit --dry +python -m easydiffraction lbco_hrpt display +``` + +Load a saved example project straight from `download_data()`: + +```python +saved_project_dir = ed.download_data(id=30, destination='projects') +project = ed.Project.load(saved_project_dir) +``` + +## Command-Line Reminders + +```bash +python -m easydiffraction --help +python -m easydiffraction --version +python -m easydiffraction list-data +python -m easydiffraction download-data 30 --destination projects +python -m easydiffraction list-tutorials +python -m easydiffraction download-tutorial 1 --destination tutorials +python -m easydiffraction download-all-tutorials --destination tutorials +python -m easydiffraction PROJECT_DIR fit +python -m easydiffraction PROJECT_DIR display +``` diff --git a/docs/docs/tutorials/ed-1.ipynb b/docs/docs/tutorials/ed-1.ipynb index 53635a4c0..f108a808b 100644 --- a/docs/docs/tutorials/ed-1.ipynb +++ b/docs/docs/tutorials/ed-1.ipynb @@ -167,7 +167,7 @@ "outputs": [], "source": [ "# Show fit results summary\n", - "project.analysis.display.fit_results()" + "project.display.fit.results()" ] }, { @@ -178,7 +178,7 @@ "outputs": [], "source": [ "# Show parameter correlations\n", - "project.display.plotter.plot_param_correlations()" + "project.display.fit.correlations()" ] }, { @@ -234,7 +234,7 @@ "outputs": [], "source": [ "# Show fit results summary\n", - "project.analysis.display.fit_results()" + "project.display.fit.results()" ] }, { @@ -245,7 +245,7 @@ "outputs": [], "source": [ "# Show parameter correlations\n", - "project.display.plotter.plot_param_correlations()" + "project.display.fit.correlations()" ] }, { @@ -267,7 +267,7 @@ "outputs": [], "source": [ "# Plot measured vs. calculated diffraction patterns\n", - "project.display.plotter.plot_meas_vs_calc(expt_name='hrpt')" + "project.display.pattern(expt_name='hrpt')" ] } ], diff --git a/docs/docs/tutorials/ed-1.py b/docs/docs/tutorials/ed-1.py index 0e2a5e553..e6312d4ac 100644 --- a/docs/docs/tutorials/ed-1.py +++ b/docs/docs/tutorials/ed-1.py @@ -61,11 +61,11 @@ # %% # Show fit results summary -project.analysis.display.fit_results() +project.display.fit.results() # %% # Show parameter correlations -project.display.plotter.plot_param_correlations() +project.display.fit.correlations() # %% [markdown] # ## Step 5: Perform Analysis (with constraints) @@ -95,11 +95,11 @@ # %% # Show fit results summary -project.analysis.display.fit_results() +project.display.fit.results() # %% # Show parameter correlations -project.display.plotter.plot_param_correlations() +project.display.fit.correlations() # %% # Show defined experiment names @@ -107,4 +107,4 @@ # %% # Plot measured vs. calculated diffraction patterns -project.display.plotter.plot_meas_vs_calc(expt_name='hrpt') +project.display.pattern(expt_name='hrpt') diff --git a/docs/docs/tutorials/ed-10.ipynb b/docs/docs/tutorials/ed-10.ipynb index 1e14322b2..2b23af884 100644 --- a/docs/docs/tutorials/ed-10.ipynb +++ b/docs/docs/tutorials/ed-10.ipynb @@ -207,8 +207,8 @@ "outputs": [], "source": [ "project.analysis.fit()\n", - "project.analysis.display.fit_results()\n", - "project.display.plotter.plot_param_correlations(threshold=0.75)" + "project.display.fit.results()\n", + "project.display.fit.correlations(threshold=0.75)" ] }, { @@ -226,7 +226,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='pdf', show_residual=True)" + "project.display.pattern(expt_name='pdf')" ] } ], diff --git a/docs/docs/tutorials/ed-10.py b/docs/docs/tutorials/ed-10.py index 08f33f0ce..9fb0b9c61 100644 --- a/docs/docs/tutorials/ed-10.py +++ b/docs/docs/tutorials/ed-10.py @@ -82,11 +82,11 @@ # %% project.analysis.fit() -project.analysis.display.fit_results() -project.display.plotter.plot_param_correlations(threshold=0.75) +project.display.fit.results() +project.display.fit.correlations(threshold=0.75) # %% [markdown] # ## Plot Measured vs Calculated # %% -project.display.plotter.plot_meas_vs_calc(expt_name='pdf', show_residual=True) +project.display.pattern(expt_name='pdf') diff --git a/docs/docs/tutorials/ed-11.ipynb b/docs/docs/tutorials/ed-11.ipynb index ff9669a26..963894d86 100644 --- a/docs/docs/tutorials/ed-11.ipynb +++ b/docs/docs/tutorials/ed-11.ipynb @@ -82,8 +82,8 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.show_supported_engines()\n", - "project.display.plotter.show_current_engine()" + "project.rendering.show_chart_engines()\n", + "project.rendering.show_config()" ] }, { @@ -94,7 +94,7 @@ "outputs": [], "source": [ "# Set global plot range for plots\n", - "project.display.plotter.x_max = 40" + "project.rendering.plotter.x_max = 40" ] }, { @@ -238,8 +238,8 @@ "outputs": [], "source": [ "project.analysis.fit()\n", - "project.analysis.display.fit_results()\n", - "project.display.plotter.plot_param_correlations()" + "project.display.fit.results()\n", + "project.display.fit.correlations()" ] }, { @@ -257,7 +257,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='nomad', show_residual=False)" + "project.display.pattern(expt_name='nomad', include=('measured', 'calculated'))" ] } ], diff --git a/docs/docs/tutorials/ed-11.py b/docs/docs/tutorials/ed-11.py index a48d2fd68..e4a1ec991 100644 --- a/docs/docs/tutorials/ed-11.py +++ b/docs/docs/tutorials/ed-11.py @@ -21,12 +21,12 @@ # ## Set Plotting Engine # %% -project.display.plotter.show_supported_engines() -project.display.plotter.show_current_engine() +project.rendering.show_chart_engines() +project.rendering.show_config() # %% # Set global plot range for plots -project.display.plotter.x_max = 40 +project.rendering.plotter.x_max = 40 # %% [markdown] # ## Add Structure @@ -94,11 +94,11 @@ # %% project.analysis.fit() -project.analysis.display.fit_results() -project.display.plotter.plot_param_correlations() +project.display.fit.results() +project.display.fit.correlations() # %% [markdown] # ## Plot Measured vs Calculated # %% -project.display.plotter.plot_meas_vs_calc(expt_name='nomad', show_residual=False) +project.display.pattern(expt_name='nomad', include=('measured', 'calculated')) diff --git a/docs/docs/tutorials/ed-12.ipynb b/docs/docs/tutorials/ed-12.ipynb index 77e648e0b..176ab1475 100644 --- a/docs/docs/tutorials/ed-12.ipynb +++ b/docs/docs/tutorials/ed-12.ipynb @@ -87,7 +87,7 @@ "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.display.plotter.engine = 'plotly'" + "# project.rendering.chart_engine = 'plotly'" ] }, { @@ -98,8 +98,8 @@ "outputs": [], "source": [ "# Set global plot range for plots\n", - "project.display.plotter.x_min = 2.0\n", - "project.display.plotter.x_max = 30.0" + "project.rendering.plotter.x_min = 2.0\n", + "project.rendering.plotter.x_max = 30.0" ] }, { @@ -278,8 +278,8 @@ "outputs": [], "source": [ "project.analysis.fit()\n", - "project.analysis.display.fit_results()\n", - "project.display.plotter.plot_param_correlations()" + "project.display.fit.results()\n", + "project.display.fit.correlations()" ] }, { @@ -297,7 +297,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='xray_pdf')" + "project.display.pattern(expt_name='xray_pdf')" ] } ], diff --git a/docs/docs/tutorials/ed-12.py b/docs/docs/tutorials/ed-12.py index 78c722ced..090f948b6 100644 --- a/docs/docs/tutorials/ed-12.py +++ b/docs/docs/tutorials/ed-12.py @@ -26,12 +26,12 @@ # %% # Keep the auto-selected engine. Alternatively, you can uncomment the # line below to explicitly set the engine to the required one. -# project.display.plotter.engine = 'plotly' +# project.rendering.chart_engine = 'plotly' # %% # Set global plot range for plots -project.display.plotter.x_min = 2.0 -project.display.plotter.x_max = 30.0 +project.rendering.plotter.x_min = 2.0 +project.rendering.plotter.x_max = 30.0 # %% [markdown] # ## Add Structure @@ -113,11 +113,11 @@ # %% project.analysis.fit() -project.analysis.display.fit_results() -project.display.plotter.plot_param_correlations() +project.display.fit.results() +project.display.fit.correlations() # %% [markdown] # ## Plot Measured vs Calculated # %% -project.display.plotter.plot_meas_vs_calc(expt_name='xray_pdf') +project.display.pattern(expt_name='xray_pdf') diff --git a/docs/docs/tutorials/ed-13.ipynb b/docs/docs/tutorials/ed-13.ipynb index 4d605962c..ca636d82b 100644 --- a/docs/docs/tutorials/ed-13.ipynb +++ b/docs/docs/tutorials/ed-13.ipynb @@ -266,8 +266,8 @@ "[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." + "To visualize the measured data, we can use the `pattern` method of\n", + "the project's `display` facade with `include='measured'`." ] }, { @@ -277,7 +277,7 @@ "metadata": {}, "outputs": [], "source": [ - "project_1.display.plotter.plot_meas(expt_name='sim_si')" + "project_1.display.pattern(expt_name='sim_si', include='measured')" ] }, { @@ -330,8 +330,8 @@ "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." + "the measured data again. The excluded region will be highlighted on\n", + "the plot and is not used in the fitting process." ] }, { @@ -341,7 +341,7 @@ "metadata": {}, "outputs": [], "source": [ - "project_1.display.plotter.plot_meas(expt_name='sim_si')" + "project_1.display.pattern(expt_name='sim_si', include=('measured', 'excluded'))" ] }, { @@ -979,7 +979,7 @@ "#### Show Free Parameters\n", "\n", "We can check which parameters are free to be refined by calling the\n", - "`free_params` method of the `analysis.display` object of the project." + "`free` method of the `display.parameters` object of the project." ] }, { @@ -1002,7 +1002,7 @@ "metadata": {}, "outputs": [], "source": [ - "project_1.analysis.display.free_params()" + "project_1.display.parameters.free()" ] }, { @@ -1016,8 +1016,8 @@ "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." + "measured data. The `pattern` method of the project's `display`\n", + "facade allows this comparison." ] }, { @@ -1027,7 +1027,7 @@ "metadata": {}, "outputs": [], "source": [ - "project_1.display.plotter.plot_meas_vs_calc(expt_name='sim_si')" + "project_1.display.pattern(expt_name='sim_si')" ] }, { @@ -1059,7 +1059,7 @@ "outputs": [], "source": [ "project_1.analysis.fit()\n", - "project_1.analysis.display.fit_results()" + "project_1.display.fit.results()" ] }, { @@ -1101,7 +1101,7 @@ "metadata": {}, "outputs": [], "source": [ - "project_1.display.plotter.plot_meas_vs_calc(expt_name='sim_si')" + "project_1.display.pattern(expt_name='sim_si')" ] }, { @@ -1132,9 +1132,9 @@ "`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`." + "The `pattern` method of the project's `display` facade allows us to\n", + "plot the measured and calculated diffraction patterns in the\n", + "d-spacing axis by setting `x='d_spacing'`." ] }, { @@ -1144,7 +1144,7 @@ "metadata": {}, "outputs": [], "source": [ - "project_1.display.plotter.plot_meas_vs_calc(expt_name='sim_si', x='d_spacing')" + "project_1.display.pattern(expt_name='sim_si', x='d_spacing')" ] }, { @@ -1348,12 +1348,12 @@ "metadata": {}, "outputs": [], "source": [ - "project_2.display.plotter.plot_meas(expt_name='sim_lbco')\n", + "project_2.display.pattern(expt_name='sim_lbco', include='measured')\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.display.plotter.plot_meas(expt_name='sim_lbco')" + "project_2.display.pattern(expt_name='sim_lbco', include=('measured', 'excluded'))" ] }, { @@ -1938,10 +1938,10 @@ "id": "146", "metadata": {}, "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." + "Use the `pattern` method of the project's `display` facade to\n", + "visualize the measured and calculated diffraction patterns before\n", + "fitting. Then, use the `fit` method of the `analysis` object of the\n", + "project to perform the fitting process." ] }, { @@ -1959,10 +1959,10 @@ "metadata": {}, "outputs": [], "source": [ - "project_2.display.plotter.plot_meas_vs_calc(expt_name='sim_lbco')\n", + "project_2.display.pattern(expt_name='sim_lbco')\n", "\n", "project_2.analysis.fit()\n", - "project_2.analysis.display.fit_results()" + "project_2.display.fit.results()" ] }, { @@ -2036,7 +2036,7 @@ "metadata": {}, "outputs": [], "source": [ - "project_2.display.plotter.plot_meas_vs_calc(expt_name='sim_lbco')" + "project_2.display.pattern(expt_name='sim_lbco')" ] }, { @@ -2090,9 +2090,9 @@ "project_2.structures['lbco'].cell.length_a.free = True\n", "\n", "project_2.analysis.fit()\n", - "project_2.analysis.display.fit_results()\n", + "project_2.display.fit.results()\n", "\n", - "project_2.display.plotter.plot_meas_vs_calc(expt_name='sim_lbco')" + "project_2.display.pattern(expt_name='sim_lbco')" ] }, { @@ -2132,8 +2132,8 @@ "id": "163", "metadata": {}, "source": [ - "Use the `plot_meas_vs_calc` method of the project and set the\n", - "`d_spacing` parameter to `True`." + "Use the `pattern` method of the project's `display` facade and set\n", + "`x='d_spacing'`." ] }, { @@ -2151,7 +2151,7 @@ "metadata": {}, "outputs": [], "source": [ - "project_2.display.plotter.plot_meas_vs_calc(expt_name='sim_lbco', x='d_spacing')" + "project_2.display.pattern(expt_name='sim_lbco', x='d_spacing')" ] }, { @@ -2180,9 +2180,7 @@ "metadata": {}, "outputs": [], "source": [ - "project_2.display.plotter.plot_meas_vs_calc(\n", - " expt_name='sim_lbco', x='d_spacing', x_min=1.35, x_max=1.40\n", - ")" + "project_2.display.pattern(expt_name='sim_lbco', x='d_spacing', x_min=1.35, x_max=1.40)" ] }, { @@ -2242,11 +2240,9 @@ "project_2.experiments['sim_lbco'].peak.exp_rise_alpha_1.free = True\n", "\n", "project_2.analysis.fit()\n", - "project_2.analysis.display.fit_results()\n", + "project_2.display.fit.results()\n", "\n", - "project_2.display.plotter.plot_meas_vs_calc(\n", - " expt_name='sim_lbco', x='d_spacing', x_min=1.35, x_max=1.40\n", - ")" + "project_2.display.pattern(expt_name='sim_lbco', x='d_spacing', x_min=1.35, x_max=1.40)" ] }, { @@ -2296,9 +2292,7 @@ "metadata": {}, "outputs": [], "source": [ - "project_2.display.plotter.plot_meas_vs_calc(\n", - " expt_name='sim_lbco', x='d_spacing', x_min=1.53, x_max=1.7\n", - ")" + "project_2.display.pattern(expt_name='sim_lbco', x='d_spacing', x_min=1.53, x_max=1.7)" ] }, { @@ -2420,10 +2414,8 @@ "metadata": {}, "outputs": [], "source": [ - "project_1.display.plotter.plot_meas_vs_calc(expt_name='sim_si', x='d_spacing', x_min=1, x_max=1.7)\n", - "project_2.display.plotter.plot_meas_vs_calc(\n", - " expt_name='sim_lbco', x='d_spacing', x_min=1, x_max=1.7\n", - ")" + "project_1.display.pattern(expt_name='sim_si', x='d_spacing', x_min=1, x_max=1.7)\n", + "project_2.display.pattern(expt_name='sim_lbco', x='d_spacing', x_min=1, x_max=1.7)" ] }, { @@ -2520,10 +2512,10 @@ "id": "196", "metadata": {}, "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." + "You can use the `pattern` method of the project's `display` facade to\n", + "visualize the patterns. Then, set the `free` attribute of the `scale`\n", + "parameter of the Si phase to `True` to allow the fitting process to\n", + "adjust the scale factor." ] }, { @@ -2544,7 +2536,7 @@ "# 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.display.plotter.plot_meas_vs_calc(expt_name='sim_lbco')\n", + "project_2.display.pattern(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", @@ -2554,14 +2546,14 @@ "\n", "# Now we can perform the fit with both phases included.\n", "project_2.analysis.fit()\n", - "project_2.analysis.display.fit_results()\n", + "project_2.display.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.display.plotter.plot_meas_vs_calc(expt_name='sim_lbco')\n", - "project_2.display.plotter.plot_meas_vs_calc(expt_name='sim_lbco', x_min=88000, x_max=101000)" + "project_2.display.pattern(expt_name='sim_lbco')\n", + "project_2.display.pattern(expt_name='sim_lbco', x_min=88000, x_max=101000)" ] }, { @@ -2665,7 +2657,7 @@ ], "metadata": { "jupytext": { - "cell_metadata_filter": "tags,title,-all", + "cell_metadata_filter": "title,tags,-all", "main_language": "python", "notebook_metadata_filter": "-all" } diff --git a/docs/docs/tutorials/ed-13.py b/docs/docs/tutorials/ed-13.py index e89f42887..436c13b93 100644 --- a/docs/docs/tutorials/ed-13.py +++ b/docs/docs/tutorials/ed-13.py @@ -144,11 +144,11 @@ # [documentation](https://docs.easydiffraction.org/lib/user-guide/analysis-workflow/experiment/#measured-data-category) # for more details about the measured data and its format. # -# To visualize the measured data, we can use the `plot_meas` method of -# the project. +# To visualize the measured data, we can use the `pattern` method of +# the project's `display` facade with `include='measured'`. # %% -project_1.display.plotter.plot_meas(expt_name='sim_si') +project_1.display.pattern(expt_name='sim_si', include='measured') # %% [markdown] # If you zoom in on the highest TOF peak (around 120,000 μs), you will @@ -179,11 +179,11 @@ # %% [markdown] # To visualize the effect of excluding the high TOF region, we can plot -# the measured data again. The excluded region will be omitted from the -# plot and is not used in the fitting process. +# the measured data again. The excluded region will be highlighted on +# the plot and is not used in the fitting process. # %% -project_1.display.plotter.plot_meas(expt_name='sim_si') +project_1.display.pattern(expt_name='sim_si', include=('measured', 'excluded')) # %% [markdown] # #### Set Instrument Parameters @@ -583,7 +583,7 @@ # #### Show Free Parameters # # We can check which parameters are free to be refined by calling the -# `free_params` method of the `analysis.display` object of the project. +# `free` method of the `display.parameters` object of the project. # %% [markdown] tags=["doc-link"] # 📖 See @@ -594,7 +594,7 @@ # - show only free parameters of the project. # %% -project_1.analysis.display.free_params() +project_1.display.parameters.free() # %% [markdown] # #### Visualize Diffraction Patterns @@ -603,11 +603,11 @@ # diffraction pattern with the calculated diffraction pattern based on # 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. +# measured data. The `pattern` method of the project's `display` +# facade allows this comparison. # %% -project_1.display.plotter.plot_meas_vs_calc(expt_name='sim_si') +project_1.display.pattern(expt_name='sim_si') # %% [markdown] # #### Run Fitting @@ -622,7 +622,7 @@ # %% project_1.analysis.fit() -project_1.analysis.display.fit_results() +project_1.display.fit.results() # %% [markdown] # #### Check Fit Results @@ -647,7 +647,7 @@ # pattern is now based on the refined parameters. # %% -project_1.display.plotter.plot_meas_vs_calc(expt_name='sim_si') +project_1.display.pattern(expt_name='sim_si') # %% [markdown] # #### TOF vs d-spacing @@ -673,12 +673,12 @@ # `quad` terms were not part of the data reduction and are therefore set # to 0 by default. # -# The `plot_meas_vs_calc` method of the project allows us to plot the -# measured and calculated diffraction patterns in the d-spacing axis by -# setting the `d_spacing` parameter to `True`. +# The `pattern` method of the project's `display` facade allows us to +# plot the measured and calculated diffraction patterns in the +# d-spacing axis by setting `x='d_spacing'`. # %% -project_1.display.plotter.plot_meas_vs_calc(expt_name='sim_si', x='d_spacing') +project_1.display.pattern(expt_name='sim_si', x='d_spacing') # %% [markdown] # As you can see, the calculated diffraction pattern now matches the @@ -789,12 +789,12 @@ # **Solution:** # %% tags=["solution", "hide-input"] -project_2.display.plotter.plot_meas(expt_name='sim_lbco') +project_2.display.pattern(expt_name='sim_lbco', include='measured') 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.display.plotter.plot_meas(expt_name='sim_lbco') +project_2.display.pattern(expt_name='sim_lbco', include=('measured', 'excluded')) # %% [markdown] # #### Exercise 2.2: Set Instrument Parameters @@ -1106,19 +1106,19 @@ # **Hint:** # %% [markdown] tags=["dmsc-school-hint"] -# Use the `plot_meas_vs_calc` method of the project to visualize the -# measured and calculated diffraction patterns before fitting. Then, use -# the `fit` method of the `analysis` object of the project to perform -# the fitting process. +# Use the `pattern` method of the project's `display` facade to +# visualize the measured and calculated diffraction patterns before +# fitting. Then, use the `fit` method of the `analysis` object of the +# project to perform the fitting process. # %% [markdown] # **Solution:** # %% tags=["solution", "hide-input"] -project_2.display.plotter.plot_meas_vs_calc(expt_name='sim_lbco') +project_2.display.pattern(expt_name='sim_lbco') project_2.analysis.fit() -project_2.analysis.display.fit_results() +project_2.display.fit.results() # %% [markdown] # #### Exercise 5.3: Find the Misfit in the Fit @@ -1160,7 +1160,7 @@ # peak positions. # %% tags=["solution", "hide-input"] -project_2.display.plotter.plot_meas_vs_calc(expt_name='sim_lbco') +project_2.display.pattern(expt_name='sim_lbco') # %% [markdown] # #### Exercise 5.4: Refine the LBCO Lattice Parameter @@ -1187,9 +1187,9 @@ project_2.structures['lbco'].cell.length_a.free = True project_2.analysis.fit() -project_2.analysis.display.fit_results() +project_2.display.fit.results() -project_2.display.plotter.plot_meas_vs_calc(expt_name='sim_lbco') +project_2.display.pattern(expt_name='sim_lbco') # %% [markdown] # One of the main goals of this study was to refine the lattice @@ -1209,14 +1209,14 @@ # **Hint:** # %% [markdown] tags=["dmsc-school-hint"] -# Use the `plot_meas_vs_calc` method of the project and set the -# `d_spacing` parameter to `True`. +# Use the `pattern` method of the project's `display` facade and set +# `x='d_spacing'`. # %% [markdown] # **Solution:** # %% tags=["solution", "hide-input"] -project_2.display.plotter.plot_meas_vs_calc(expt_name='sim_lbco', x='d_spacing') +project_2.display.pattern(expt_name='sim_lbco', x='d_spacing') # %% [markdown] # #### Exercise 5.6: Refine the Peak Profile Parameters @@ -1233,9 +1233,7 @@ # perfectly describe the peak at about 1.38 Å, as can be seen below: # %% -project_2.display.plotter.plot_meas_vs_calc( - expt_name='sim_lbco', x='d_spacing', x_min=1.35, x_max=1.40 -) +project_2.display.pattern(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 @@ -1268,11 +1266,9 @@ project_2.experiments['sim_lbco'].peak.exp_rise_alpha_1.free = True project_2.analysis.fit() -project_2.analysis.display.fit_results() +project_2.display.fit.results() -project_2.display.plotter.plot_meas_vs_calc( - expt_name='sim_lbco', x='d_spacing', x_min=1.35, x_max=1.40 -) +project_2.display.pattern(expt_name='sim_lbco', x='d_spacing', x_min=1.35, x_max=1.40) # %% [markdown] # #### Exercise 5.7: Find Undefined Features @@ -1295,9 +1291,7 @@ # **Solution:** # %% tags=["solution", "hide-input"] -project_2.display.plotter.plot_meas_vs_calc( - expt_name='sim_lbco', x='d_spacing', x_min=1.53, x_max=1.7 -) +project_2.display.pattern(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 @@ -1362,10 +1356,8 @@ # confirm this hypothesis. # %% tags=["solution", "hide-input"] -project_1.display.plotter.plot_meas_vs_calc(expt_name='sim_si', x='d_spacing', x_min=1, x_max=1.7) -project_2.display.plotter.plot_meas_vs_calc( - expt_name='sim_lbco', x='d_spacing', x_min=1, x_max=1.7 -) +project_1.display.pattern(expt_name='sim_si', x='d_spacing', x_min=1, x_max=1.7) +project_2.display.pattern(expt_name='sim_lbco', x='d_spacing', x_min=1, x_max=1.7) # %% [markdown] # #### Exercise 5.10: Create a Second Structure – Si as Impurity @@ -1420,10 +1412,10 @@ # **Hint:** # %% [markdown] tags=["dmsc-school-hint"] -# You can use the `plot_meas_vs_calc` method of the project to visualize -# the patterns. Then, set the `free` attribute of the `scale` parameter -# of the Si phase to `True` to allow the fitting process to adjust the -# scale factor. +# You can use the `pattern` method of the project's `display` facade to +# visualize the patterns. Then, set the `free` attribute of the `scale` +# parameter of the Si phase to `True` to allow the fitting process to +# adjust the scale factor. # %% [markdown] # **Solution:** @@ -1432,7 +1424,7 @@ # Before optimizing the parameters, we can visualize the measured # diffraction pattern and the calculated diffraction pattern based on # the two phases: LBCO and Si. -project_2.display.plotter.plot_meas_vs_calc(expt_name='sim_lbco') +project_2.display.pattern(expt_name='sim_lbco') # As you can see, the calculated pattern is now the sum of both phases, # and Si peaks are visible in the calculated pattern. However, their @@ -1442,14 +1434,14 @@ # Now we can perform the fit with both phases included. project_2.analysis.fit() -project_2.analysis.display.fit_results() +project_2.display.fit.results() # Let's plot the measured diffraction pattern and the calculated # diffraction pattern both for the full range and for a zoomed-in region # around the previously unexplained peak near 95,000 μs. The calculated # pattern will be the sum of the two phases. -project_2.display.plotter.plot_meas_vs_calc(expt_name='sim_lbco') -project_2.display.plotter.plot_meas_vs_calc(expt_name='sim_lbco', x_min=88000, x_max=101000) +project_2.display.pattern(expt_name='sim_lbco') +project_2.display.pattern(expt_name='sim_lbco', x_min=88000, x_max=101000) # %% [markdown] # All previously unexplained peaks are now accounted for in the pattern, diff --git a/docs/docs/tutorials/ed-14.ipynb b/docs/docs/tutorials/ed-14.ipynb index 75e6805b0..48665fa53 100644 --- a/docs/docs/tutorials/ed-14.ipynb +++ b/docs/docs/tutorials/ed-14.ipynb @@ -254,7 +254,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='heidi')" + "project.display.pattern(expt_name='heidi')" ] }, { @@ -307,7 +307,7 @@ "outputs": [], "source": [ "# Show fit results summary\n", - "project.analysis.display.fit_results()" + "project.display.fit.results()" ] }, { @@ -337,7 +337,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='heidi')" + "project.display.pattern(expt_name='heidi')" ] }, { @@ -405,7 +405,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.display.fit_results()" + "project.display.fit.results()" ] }, { @@ -415,7 +415,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_param_correlations()" + "project.display.fit.correlations()" ] }, { @@ -425,7 +425,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='heidi')" + "project.display.pattern(expt_name='heidi')" ] }, { diff --git a/docs/docs/tutorials/ed-14.py b/docs/docs/tutorials/ed-14.py index c29d3eb88..3ff5f25f0 100644 --- a/docs/docs/tutorials/ed-14.py +++ b/docs/docs/tutorials/ed-14.py @@ -85,7 +85,7 @@ # ## Step 4: Perform Analysis I (ADP iso) # %% -project.display.plotter.plot_meas_vs_calc(expt_name='heidi') +project.display.pattern(expt_name='heidi') # %% structure.atom_sites['O1'].fract_x.free = True @@ -110,7 +110,7 @@ # %% # Show fit results summary -project.analysis.display.fit_results() +project.display.fit.results() # %% structure.show_as_cif() @@ -119,7 +119,7 @@ project.experiments.show_names() # %% -project.display.plotter.plot_meas_vs_calc(expt_name='heidi') +project.display.pattern(expt_name='heidi') # %% [markdown] # ## Step 5: Perform Analysis (ADP aniso) @@ -147,13 +147,13 @@ project.analysis.fit() # %% -project.analysis.display.fit_results() +project.display.fit.results() # %% -project.display.plotter.plot_param_correlations() +project.display.fit.correlations() # %% -project.display.plotter.plot_meas_vs_calc(expt_name='heidi') +project.display.pattern(expt_name='heidi') # %% structure.show_as_cif() diff --git a/docs/docs/tutorials/ed-15.ipynb b/docs/docs/tutorials/ed-15.ipynb index d1ece4eb6..e79bdd876 100644 --- a/docs/docs/tutorials/ed-15.ipynb +++ b/docs/docs/tutorials/ed-15.ipynb @@ -208,7 +208,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='senju')" + "project.display.pattern(expt_name='senju')" ] }, { @@ -229,7 +229,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.fit.show_minimizer_types()" + "project.analysis.fitting.show_minimizer_types()" ] }, { @@ -239,7 +239,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.fit.minimizer_type = 'bumps'" + "project.analysis.fitting.minimizer_type = 'bumps'" ] }, { @@ -248,6 +248,17 @@ "id": "23", "metadata": {}, "outputs": [], + "source": [ + "# Limit number of iterations to prevent long calculation time in this tutorial.\n", + "project.analysis.fitting.minimizer.max_iterations = 500" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "24", + "metadata": {}, + "outputs": [], "source": [ "# Start refinement. All parameters, which have standard uncertainties\n", "# in the input CIF files, are refined by default.\n", @@ -257,18 +268,18 @@ { "cell_type": "code", "execution_count": null, - "id": "24", + "id": "25", "metadata": {}, "outputs": [], "source": [ "# Show fit results summary\n", - "project.analysis.display.fit_results()" + "project.display.fit.results()" ] }, { "cell_type": "code", "execution_count": null, - "id": "25", + "id": "26", "metadata": {}, "outputs": [], "source": [ @@ -278,7 +289,7 @@ { "cell_type": "code", "execution_count": null, - "id": "26", + "id": "27", "metadata": {}, "outputs": [], "source": [ @@ -288,16 +299,16 @@ { "cell_type": "code", "execution_count": null, - "id": "27", + "id": "28", "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='senju')" + "project.display.pattern(expt_name='senju')" ] }, { "cell_type": "markdown", - "id": "28", + "id": "29", "metadata": {}, "source": [ "## Step 5: Perform Analysis (ADP aniso)" @@ -306,7 +317,7 @@ { "cell_type": "code", "execution_count": null, - "id": "29", + "id": "30", "metadata": {}, "outputs": [], "source": [ @@ -317,7 +328,7 @@ { "cell_type": "code", "execution_count": null, - "id": "30", + "id": "31", "metadata": {}, "outputs": [], "source": [ @@ -330,7 +341,7 @@ { "cell_type": "code", "execution_count": null, - "id": "31", + "id": "32", "metadata": {}, "outputs": [], "source": [ @@ -340,17 +351,17 @@ { "cell_type": "code", "execution_count": null, - "id": "32", + "id": "33", "metadata": {}, "outputs": [], "source": [ - "project.analysis.display.free_params()" + "project.display.parameters.free()" ] }, { "cell_type": "code", "execution_count": null, - "id": "33", + "id": "34", "metadata": {}, "outputs": [], "source": [ @@ -360,37 +371,37 @@ { "cell_type": "code", "execution_count": null, - "id": "34", + "id": "35", "metadata": {}, "outputs": [], "source": [ - "project.analysis.display.fit_results()" + "project.display.fit.results()" ] }, { "cell_type": "code", "execution_count": null, - "id": "35", + "id": "36", "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_param_correlations()" + "project.display.fit.correlations()" ] }, { "cell_type": "code", "execution_count": null, - "id": "36", + "id": "37", "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='senju')" + "project.display.pattern(expt_name='senju')" ] }, { "cell_type": "code", "execution_count": null, - "id": "37", + "id": "38", "metadata": {}, "outputs": [], "source": [ diff --git a/docs/docs/tutorials/ed-15.py b/docs/docs/tutorials/ed-15.py index c198fc234..03dc35fa7 100644 --- a/docs/docs/tutorials/ed-15.py +++ b/docs/docs/tutorials/ed-15.py @@ -67,17 +67,21 @@ # ## Step 4: Perform Analysis I (ADP iso) # %% -project.display.plotter.plot_meas_vs_calc(expt_name='senju') +project.display.pattern(expt_name='senju') # %% experiment.linked_crystal.scale.free = True experiment.extinction.radius.free = True # %% -project.analysis.fit.show_minimizer_types() +project.analysis.fitting.show_minimizer_types() # %% -project.analysis.fit.minimizer_type = 'bumps' +project.analysis.fitting.minimizer_type = 'bumps' + +# %% +# Limit number of iterations to prevent long calculation time in this tutorial. +project.analysis.fitting.minimizer.max_iterations = 500 # %% # Start refinement. All parameters, which have standard uncertainties @@ -86,7 +90,7 @@ # %% # Show fit results summary -project.analysis.display.fit_results() +project.display.fit.results() # %% structure.show_as_cif() @@ -95,7 +99,7 @@ project.experiments.show_names() # %% -project.display.plotter.plot_meas_vs_calc(expt_name='senju') +project.display.pattern(expt_name='senju') # %% [markdown] # ## Step 5: Perform Analysis (ADP aniso) @@ -114,19 +118,19 @@ structure.show_as_cif() # %% -project.analysis.display.free_params() +project.display.parameters.free() # %% project.analysis.fit() # %% -project.analysis.display.fit_results() +project.display.fit.results() # %% -project.display.plotter.plot_param_correlations() +project.display.fit.correlations() # %% -project.display.plotter.plot_meas_vs_calc(expt_name='senju') +project.display.pattern(expt_name='senju') # %% structure.show_as_cif() diff --git a/docs/docs/tutorials/ed-16.ipynb b/docs/docs/tutorials/ed-16.ipynb index f7a839dae..b30be9754 100644 --- a/docs/docs/tutorials/ed-16.ipynb +++ b/docs/docs/tutorials/ed-16.ipynb @@ -435,9 +435,9 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.fit.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)" + "project.analysis.fitting_mode_type = 'joint'\n", + "project.analysis.joint_fit.create(experiment_id='sepd', weight=0.7)\n", + "project.analysis.joint_fit.create(experiment_id='nomad', weight=0.3)" ] }, { @@ -455,7 +455,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='sepd')" + "project.display.pattern(expt_name='sepd')" ] }, { @@ -465,7 +465,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='nomad')" + "project.display.pattern(expt_name='nomad')" ] }, { @@ -551,7 +551,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.display.free_params()" + "project.display.parameters.free()" ] }, { @@ -570,8 +570,8 @@ "outputs": [], "source": [ "project.analysis.fit()\n", - "project.analysis.display.fit_results()\n", - "project.display.plotter.plot_param_correlations()" + "project.display.fit.results()\n", + "project.display.fit.correlations()" ] }, { @@ -589,7 +589,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='sepd')" + "project.display.pattern(expt_name='sepd')" ] }, { @@ -599,7 +599,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='nomad')" + "project.display.pattern(expt_name='nomad')" ] } ], diff --git a/docs/docs/tutorials/ed-16.py b/docs/docs/tutorials/ed-16.py index 91d48b1e6..e5520bfe3 100644 --- a/docs/docs/tutorials/ed-16.py +++ b/docs/docs/tutorials/ed-16.py @@ -182,18 +182,18 @@ # #### Set Fit Mode and Weights # %% -project.analysis.fit.mode = 'joint' -project.analysis.joint_fit_experiments.create(id='sepd', weight=0.7) -project.analysis.joint_fit_experiments.create(id='nomad', weight=0.3) +project.analysis.fitting_mode_type = 'joint' +project.analysis.joint_fit.create(experiment_id='sepd', weight=0.7) +project.analysis.joint_fit.create(experiment_id='nomad', weight=0.3) # %% [markdown] # #### Plot Measured vs Calculated (Before Fit) # %% -project.display.plotter.plot_meas_vs_calc(expt_name='sepd') +project.display.pattern(expt_name='sepd') # %% -project.display.plotter.plot_meas_vs_calc(expt_name='nomad') +project.display.pattern(expt_name='nomad') # %% [markdown] # #### Set Fitting Parameters @@ -231,21 +231,21 @@ # #### Show Free Parameters # %% -project.analysis.display.free_params() +project.display.parameters.free() # %% [markdown] # #### Run Fitting # %% project.analysis.fit() -project.analysis.display.fit_results() -project.display.plotter.plot_param_correlations() +project.display.fit.results() +project.display.fit.correlations() # %% [markdown] # #### Plot Measured vs Calculated (After Fit) # %% -project.display.plotter.plot_meas_vs_calc(expt_name='sepd') +project.display.pattern(expt_name='sepd') # %% -project.display.plotter.plot_meas_vs_calc(expt_name='nomad') +project.display.pattern(expt_name='nomad') diff --git a/docs/docs/tutorials/ed-17.ipynb b/docs/docs/tutorials/ed-17.ipynb index d2ee917d1..e471d99a7 100644 --- a/docs/docs/tutorials/ed-17.ipynb +++ b/docs/docs/tutorials/ed-17.ipynb @@ -29,7 +29,7 @@ "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 is performed against a\n", - "temperature scan using `fit_sequential`, which processes each data\n", + "temperature scan using sequential fitting, which processes each data\n", "file independently without loading all datasets into memory at once." ] }, @@ -58,7 +58,8 @@ "source": [ "## Step 1: Define Project\n", "\n", - "The project object manages structures, experiments, and analysis." + "The project object manages structures, experiments, analysis, display,\n", + "and other related components." ] }, { @@ -68,7 +69,9 @@ "metadata": {}, "outputs": [], "source": [ - "project = ed.Project()" + "project = ed.Project(name='cosio_d20')\n", + "analysis = project.analysis\n", + "display = project.display" ] }, { @@ -87,7 +90,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.save_as('data/cosio_project', temporary=False)" + "project.save_as(dir_path='projects/cosio_d20')" ] }, { @@ -111,7 +114,7 @@ "outputs": [], "source": [ "project.structures.create(name='cosio')\n", - "structure = project.structures['cosio']" + "struct = project.structures['cosio']" ] }, { @@ -129,8 +132,8 @@ "metadata": {}, "outputs": [], "source": [ - "structure.space_group.name_h_m = 'P n m a'\n", - "structure.space_group.it_coordinate_system_code = 'abc'" + "struct.space_group.name_h_m = 'P n m a'\n", + "struct.space_group.it_coordinate_system_code = 'abc'" ] }, { @@ -148,9 +151,9 @@ "metadata": {}, "outputs": [], "source": [ - "structure.cell.length_a = 10.31\n", - "structure.cell.length_b = 6.0\n", - "structure.cell.length_c = 4.79" + "struct.cell.length_a = 10.31\n", + "struct.cell.length_b = 6.0\n", + "struct.cell.length_c = 4.79" ] }, { @@ -168,7 +171,7 @@ "metadata": {}, "outputs": [], "source": [ - "structure.atom_sites.create(\n", + "struct.atom_sites.create(\n", " label='Co1',\n", " type_symbol='Co',\n", " fract_x=0,\n", @@ -177,7 +180,7 @@ " wyckoff_letter='a',\n", " adp_iso=0.3,\n", ")\n", - "structure.atom_sites.create(\n", + "struct.atom_sites.create(\n", " label='Co2',\n", " type_symbol='Co',\n", " fract_x=0.279,\n", @@ -186,7 +189,7 @@ " wyckoff_letter='c',\n", " adp_iso=0.3,\n", ")\n", - "structure.atom_sites.create(\n", + "struct.atom_sites.create(\n", " label='Si',\n", " type_symbol='Si',\n", " fract_x=0.094,\n", @@ -195,7 +198,7 @@ " wyckoff_letter='c',\n", " adp_iso=0.34,\n", ")\n", - "structure.atom_sites.create(\n", + "struct.atom_sites.create(\n", " label='O1',\n", " type_symbol='O',\n", " fract_x=0.091,\n", @@ -204,7 +207,7 @@ " wyckoff_letter='c',\n", " adp_iso=0.63,\n", ")\n", - "structure.atom_sites.create(\n", + "struct.atom_sites.create(\n", " label='O2',\n", " type_symbol='O',\n", " fract_x=0.448,\n", @@ -213,7 +216,7 @@ " wyckoff_letter='c',\n", " adp_iso=0.59,\n", ")\n", - "structure.atom_sites.create(\n", + "struct.atom_sites.create(\n", " label='O3',\n", " type_symbol='O',\n", " fract_x=0.164,\n", @@ -246,7 +249,7 @@ "metadata": {}, "outputs": [], "source": [ - "zip_path = ed.download_data(id=27, destination='data')" + "zip_path = ed.download_data(id=25, destination='data')" ] }, { @@ -264,8 +267,11 @@ "metadata": {}, "outputs": [], "source": [ - "data_dir = 'data/d20_scan'\n", - "data_paths = ed.extract_data_paths_from_zip(zip_path, destination=data_dir)" + "scan_data_dir = 'experiments/d20_scan'\n", + "data_paths = ed.extract_data_paths_from_zip(\n", + " zip_path,\n", + " destination=project.info.path / scan_data_dir,\n", + ")" ] }, { @@ -424,28 +430,28 @@ "metadata": {}, "outputs": [], "source": [ - "structure.cell.length_a.free = True\n", - "structure.cell.length_b.free = True\n", - "structure.cell.length_c.free = True\n", + "struct.cell.length_a.free = True\n", + "struct.cell.length_b.free = True\n", + "struct.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", + "struct.atom_sites['Co2'].fract_x.free = True\n", + "struct.atom_sites['Co2'].fract_z.free = True\n", + "struct.atom_sites['Si'].fract_x.free = True\n", + "struct.atom_sites['Si'].fract_z.free = True\n", + "struct.atom_sites['O1'].fract_x.free = True\n", + "struct.atom_sites['O1'].fract_z.free = True\n", + "struct.atom_sites['O2'].fract_x.free = True\n", + "struct.atom_sites['O2'].fract_z.free = True\n", + "struct.atom_sites['O3'].fract_x.free = True\n", + "struct.atom_sites['O3'].fract_y.free = True\n", + "struct.atom_sites['O3'].fract_z.free = True\n", "\n", - "structure.atom_sites['Co1'].adp_iso.free = True\n", - "structure.atom_sites['Co2'].adp_iso.free = True\n", - "structure.atom_sites['Si'].adp_iso.free = True\n", - "structure.atom_sites['O1'].adp_iso.free = True\n", - "structure.atom_sites['O2'].adp_iso.free = True\n", - "structure.atom_sites['O3'].adp_iso.free = True" + "struct.atom_sites['Co1'].adp_iso.free = True\n", + "struct.atom_sites['Co2'].adp_iso.free = True\n", + "struct.atom_sites['Si'].adp_iso.free = True\n", + "struct.atom_sites['O1'].adp_iso.free = True\n", + "struct.atom_sites['O2'].adp_iso.free = True\n", + "struct.atom_sites['O3'].adp_iso.free = True" ] }, { @@ -485,13 +491,13 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.aliases.create(\n", + "analysis.aliases.create(\n", " label='biso_Co1',\n", - " param=structure.atom_sites['Co1'].adp_iso,\n", + " param=struct.atom_sites['Co1'].adp_iso,\n", ")\n", - "project.analysis.aliases.create(\n", + "analysis.aliases.create(\n", " label='biso_Co2',\n", - " param=structure.atom_sites['Co2'].adp_iso,\n", + " param=struct.atom_sites['Co2'].adp_iso,\n", ")" ] }, @@ -510,7 +516,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.constraints.create(expression='biso_Co2 = biso_Co1')" + "analysis.constraints.create(expression='biso_Co2 = biso_Co1')" ] }, { @@ -528,7 +534,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.fit.minimizer_type = 'bumps (lm)'" + "analysis.fitting.minimizer_type = 'bumps (lm)'" ] }, { @@ -551,13 +557,23 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.fit()" + "analysis.fit()" ] }, { - "cell_type": "markdown", + "cell_type": "code", + "execution_count": null, "id": "44", "metadata": {}, + "outputs": [], + "source": [ + "display.fit.results()" + ] + }, + { + "cell_type": "markdown", + "id": "45", + "metadata": {}, "source": [ "#### Show parameter correlations" ] @@ -565,16 +581,16 @@ { "cell_type": "code", "execution_count": null, - "id": "45", + "id": "46", "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_param_correlations()" + "display.fit.correlations()" ] }, { "cell_type": "markdown", - "id": "46", + "id": "47", "metadata": {}, "source": [ "#### Compare measured and calculated patterns for the first fit." @@ -583,16 +599,16 @@ { "cell_type": "code", "execution_count": null, - "id": "47", + "id": "48", "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='d20')" + "display.pattern(expt_name='d20')" ] }, { "cell_type": "markdown", - "id": "48", + "id": "49", "metadata": {}, "source": [ "#### Run Sequential Fitting\n", @@ -604,7 +620,7 @@ { "cell_type": "code", "execution_count": null, - "id": "49", + "id": "50", "metadata": {}, "outputs": [], "source": [ @@ -613,33 +629,65 @@ }, { "cell_type": "markdown", - "id": "50", + "id": "51", "metadata": { "lines_to_next_cell": 2 }, "source": [ "\n", - "Define a callback that extracts the temperature from each data file." + "Create a persisted extract rule that reads the temperature from each\n", + "data file." ] }, { "cell_type": "code", "execution_count": null, - "id": "51", + "id": "52", + "metadata": {}, + "outputs": [], + "source": [ + "temperature = 'diffrn.ambient_temperature'" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "53", "metadata": {}, "outputs": [], "source": [ - "def extract_diffrn(file_path):\n", - " temperature = ed.extract_metadata(\n", - " file_path=file_path,\n", - " pattern=r'^TEMP\\s+([0-9.]+)',\n", - " )\n", - " return {'ambient_temperature': temperature}" + "analysis.sequential_fit_extract.create(\n", + " id='temperature',\n", + " target=temperature,\n", + " pattern=r'^TEMP\\s+([0-9.]+)',\n", + " required=True,\n", + ")" ] }, { "cell_type": "markdown", - "id": "52", + "id": "54", + "metadata": {}, + "source": [ + "Set the sequential fitting parameters." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "55", + "metadata": {}, + "outputs": [], + "source": [ + "analysis.fitting_mode_type = 'sequential'\n", + "analysis.sequential_fit.data_dir = scan_data_dir\n", + "analysis.sequential_fit.max_workers = 'auto'\n", + "analysis.sequential_fit.reverse = True" + ] + }, + { + "cell_type": "markdown", + "id": "56", "metadata": {}, "source": [ "Run the sequential fit over all data files in the scan directory." @@ -648,21 +696,16 @@ { "cell_type": "code", "execution_count": null, - "id": "53", + "id": "57", "metadata": {}, "outputs": [], "source": [ - "project.analysis.fit_sequential(\n", - " data_dir=data_dir,\n", - " extract_diffrn=extract_diffrn,\n", - " max_workers='auto',\n", - " reverse=True,\n", - ")" + "analysis.fit()" ] }, { "cell_type": "markdown", - "id": "54", + "id": "58", "metadata": {}, "source": [ "#### Replay a Dataset\n", @@ -673,17 +716,17 @@ { "cell_type": "code", "execution_count": null, - "id": "55", + "id": "59", "metadata": {}, "outputs": [], "source": [ "project.apply_params_from_csv(row_index=0)\n", - "project.display.plotter.plot_meas_vs_calc(expt_name='d20')" + "display.pattern(expt_name='d20')" ] }, { "cell_type": "markdown", - "id": "56", + "id": "60", "metadata": {}, "source": [ "\n", @@ -693,37 +736,47 @@ { "cell_type": "code", "execution_count": null, - "id": "57", + "id": "61", "metadata": {}, "outputs": [], "source": [ "project.apply_params_from_csv(row_index=-1)\n", - "project.display.plotter.plot_meas_vs_calc(expt_name='d20')" + "display.pattern(expt_name='d20')" ] }, { "cell_type": "markdown", - "id": "58", + "id": "62", "metadata": {}, "source": [ "#### Plot Parameter Evolution\n", "\n", - "Define the quantity to use as the x-axis in the following plots." + "Reuse the extracted diffrn path as the x-axis in the following plots." + ] + }, + { + "cell_type": "markdown", + "id": "63", + "metadata": {}, + "source": [ + "Plot fit quality metrics vs. temperature." ] }, { "cell_type": "code", "execution_count": null, - "id": "59", + "id": "64", "metadata": {}, "outputs": [], "source": [ - "temperature = expt.diffrn.ambient_temperature" + "display.fit.series(analysis.fit_result.success, versus=temperature)\n", + "display.fit.series(analysis.fit_result.reduced_chi_square, versus=temperature)\n", + "display.fit.series(analysis.fit_result.iterations, versus=temperature)" ] }, { "cell_type": "markdown", - "id": "60", + "id": "65", "metadata": {}, "source": [ "Plot unit cell parameters vs. temperature." @@ -732,18 +785,18 @@ { "cell_type": "code", "execution_count": null, - "id": "61", + "id": "66", "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_param_series(structure.cell.length_a, versus=temperature)\n", - "project.display.plotter.plot_param_series(structure.cell.length_b, versus=temperature)\n", - "project.display.plotter.plot_param_series(structure.cell.length_c, versus=temperature)" + "display.fit.series(struct.cell.length_a, versus=temperature)\n", + "display.fit.series(struct.cell.length_b, versus=temperature)\n", + "display.fit.series(struct.cell.length_c, versus=temperature)" ] }, { "cell_type": "markdown", - "id": "62", + "id": "67", "metadata": {}, "source": [ "Plot isotropic displacement parameters vs. temperature." @@ -752,35 +805,20 @@ { "cell_type": "code", "execution_count": null, - "id": "63", + "id": "68", "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_param_series(\n", - " structure.atom_sites['Co1'].adp_iso,\n", - " versus=temperature,\n", - ")\n", - "project.display.plotter.plot_param_series(\n", - " structure.atom_sites['Si'].adp_iso,\n", - " versus=temperature,\n", - ")\n", - "project.display.plotter.plot_param_series(\n", - " structure.atom_sites['O1'].adp_iso,\n", - " versus=temperature,\n", - ")\n", - "project.display.plotter.plot_param_series(\n", - " structure.atom_sites['O2'].adp_iso,\n", - " versus=temperature,\n", - ")\n", - "project.display.plotter.plot_param_series(\n", - " structure.atom_sites['O3'].adp_iso,\n", - " versus=temperature,\n", - ")" + "display.fit.series(struct.atom_sites['Co1'].adp_iso, versus=temperature)\n", + "display.fit.series(struct.atom_sites['Si'].adp_iso, versus=temperature)\n", + "display.fit.series(struct.atom_sites['O1'].adp_iso, versus=temperature)\n", + "display.fit.series(struct.atom_sites['O2'].adp_iso, versus=temperature)\n", + "display.fit.series(struct.atom_sites['O3'].adp_iso, versus=temperature)" ] }, { "cell_type": "markdown", - "id": "64", + "id": "69", "metadata": {}, "source": [ "Plot selected fractional coordinates vs. temperature." @@ -789,30 +827,15 @@ { "cell_type": "code", "execution_count": null, - "id": "65", + "id": "70", "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_param_series(\n", - " structure.atom_sites['Co2'].fract_x,\n", - " versus=temperature,\n", - ")\n", - "project.display.plotter.plot_param_series(\n", - " structure.atom_sites['Co2'].fract_z,\n", - " versus=temperature,\n", - ")\n", - "project.display.plotter.plot_param_series(\n", - " structure.atom_sites['O1'].fract_z,\n", - " versus=temperature,\n", - ")\n", - "project.display.plotter.plot_param_series(\n", - " structure.atom_sites['O2'].fract_z,\n", - " versus=temperature,\n", - ")\n", - "project.display.plotter.plot_param_series(\n", - " structure.atom_sites['O3'].fract_z,\n", - " versus=temperature,\n", - ")" + "display.fit.series(struct.atom_sites['Co2'].fract_x, versus=temperature)\n", + "display.fit.series(struct.atom_sites['Co2'].fract_z, versus=temperature)\n", + "display.fit.series(struct.atom_sites['O1'].fract_z, versus=temperature)\n", + "display.fit.series(struct.atom_sites['O2'].fract_z, versus=temperature)\n", + "display.fit.series(struct.atom_sites['O3'].fract_z, versus=temperature)" ] } ], diff --git a/docs/docs/tutorials/ed-17.py b/docs/docs/tutorials/ed-17.py index f929cd47c..93f691fb7 100644 --- a/docs/docs/tutorials/ed-17.py +++ b/docs/docs/tutorials/ed-17.py @@ -4,7 +4,7 @@ # 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 is performed against a -# temperature scan using `fit_sequential`, which processes each data +# temperature scan using sequential fitting, which processes each data # file independently without loading all datasets into memory at once. # %% [markdown] @@ -16,17 +16,20 @@ # %% [markdown] # ## Step 1: Define Project # -# The project object manages structures, experiments, and analysis. +# The project object manages structures, experiments, analysis, display, +# and other related components. # %% -project = ed.Project() +project = ed.Project(name='cosio_d20') +analysis = project.analysis +display = project.display # %% [markdown] # The project must be saved before running sequential fitting, so that # results can be written to `analysis/results.csv`. # %% -project.save_as('data/cosio_project', temporary=False) +project.save_as(dir_path='projects/cosio_d20') # %% [markdown] # ## Step 2: Define Crystal Structure @@ -38,28 +41,28 @@ # %% project.structures.create(name='cosio') -structure = project.structures['cosio'] +struct = 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' +struct.space_group.name_h_m = 'P n m a' +struct.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 +struct.cell.length_a = 10.31 +struct.cell.length_b = 6.0 +struct.cell.length_c = 4.79 # %% [markdown] # #### Set Atom Sites # %% -structure.atom_sites.create( +struct.atom_sites.create( label='Co1', type_symbol='Co', fract_x=0, @@ -68,7 +71,7 @@ wyckoff_letter='a', adp_iso=0.3, ) -structure.atom_sites.create( +struct.atom_sites.create( label='Co2', type_symbol='Co', fract_x=0.279, @@ -77,7 +80,7 @@ wyckoff_letter='c', adp_iso=0.3, ) -structure.atom_sites.create( +struct.atom_sites.create( label='Si', type_symbol='Si', fract_x=0.094, @@ -86,7 +89,7 @@ wyckoff_letter='c', adp_iso=0.34, ) -structure.atom_sites.create( +struct.atom_sites.create( label='O1', type_symbol='O', fract_x=0.091, @@ -95,7 +98,7 @@ wyckoff_letter='c', adp_iso=0.63, ) -structure.atom_sites.create( +struct.atom_sites.create( label='O2', type_symbol='O', fract_x=0.448, @@ -104,7 +107,7 @@ wyckoff_letter='c', adp_iso=0.59, ) -structure.atom_sites.create( +struct.atom_sites.create( label='O3', type_symbol='O', fract_x=0.164, @@ -125,14 +128,17 @@ # #### Download Measured Data # %% -zip_path = ed.download_data(id=27, destination='data') +zip_path = ed.download_data(id=25, destination='data') # %% [markdown] # #### Extract Data Files # %% -data_dir = 'data/d20_scan' -data_paths = ed.extract_data_paths_from_zip(zip_path, destination=data_dir) +scan_data_dir = 'experiments/d20_scan' +data_paths = ed.extract_data_paths_from_zip( + zip_path, + destination=project.info.path / scan_data_dir, +) # %% [markdown] # #### Create Template Experiment from the First File @@ -202,28 +208,28 @@ # #### Set Free Parameters # %% -structure.cell.length_a.free = True -structure.cell.length_b.free = True -structure.cell.length_c.free = True +struct.cell.length_a.free = True +struct.cell.length_b.free = True +struct.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 +struct.atom_sites['Co2'].fract_x.free = True +struct.atom_sites['Co2'].fract_z.free = True +struct.atom_sites['Si'].fract_x.free = True +struct.atom_sites['Si'].fract_z.free = True +struct.atom_sites['O1'].fract_x.free = True +struct.atom_sites['O1'].fract_z.free = True +struct.atom_sites['O2'].fract_x.free = True +struct.atom_sites['O2'].fract_z.free = True +struct.atom_sites['O3'].fract_x.free = True +struct.atom_sites['O3'].fract_y.free = True +struct.atom_sites['O3'].fract_z.free = True -structure.atom_sites['Co1'].adp_iso.free = True -structure.atom_sites['Co2'].adp_iso.free = True -structure.atom_sites['Si'].adp_iso.free = True -structure.atom_sites['O1'].adp_iso.free = True -structure.atom_sites['O2'].adp_iso.free = True -structure.atom_sites['O3'].adp_iso.free = True +struct.atom_sites['Co1'].adp_iso.free = True +struct.atom_sites['Co2'].adp_iso.free = True +struct.atom_sites['Si'].adp_iso.free = True +struct.atom_sites['O1'].adp_iso.free = True +struct.atom_sites['O2'].adp_iso.free = True +struct.atom_sites['O3'].adp_iso.free = True # %% expt.linked_phases['cosio'].scale.free = True @@ -244,26 +250,26 @@ # Set aliases for parameters. # %% -project.analysis.aliases.create( +analysis.aliases.create( label='biso_Co1', - param=structure.atom_sites['Co1'].adp_iso, + param=struct.atom_sites['Co1'].adp_iso, ) -project.analysis.aliases.create( +analysis.aliases.create( label='biso_Co2', - param=structure.atom_sites['Co2'].adp_iso, + param=struct.atom_sites['Co2'].adp_iso, ) # %% [markdown] # Set constraints. # %% -project.analysis.constraints.create(expression='biso_Co2 = biso_Co1') +analysis.constraints.create(expression='biso_Co2 = biso_Co1') # %% [markdown] # #### Set Minimizer # %% -project.analysis.fit.minimizer_type = 'bumps (lm)' +analysis.fitting.minimizer_type = 'bumps (lm)' # %% [markdown] # #### Run Single Fitting @@ -274,19 +280,22 @@ # if the initial parameters are far from optimal. # %% -project.analysis.fit() +analysis.fit() + +# %% +display.fit.results() # %% [markdown] # #### Show parameter correlations # %% -project.display.plotter.plot_param_correlations() +display.fit.correlations() # %% [markdown] # #### Compare measured and calculated patterns for the first fit. # %% -project.display.plotter.plot_meas_vs_calc(expt_name='d20') +display.pattern(expt_name='d20') # %% [markdown] # #### Run Sequential Fitting @@ -299,28 +308,35 @@ # %% [markdown] # -# Define a callback that extracts the temperature from each data file. +# Create a persisted extract rule that reads the temperature from each +# data file. # %% -def extract_diffrn(file_path): - temperature = ed.extract_metadata( - file_path=file_path, - pattern=r'^TEMP\s+([0-9.]+)', - ) - return {'ambient_temperature': temperature} +temperature = 'diffrn.ambient_temperature' +# %% +analysis.sequential_fit_extract.create( + id='temperature', + target=temperature, + pattern=r'^TEMP\s+([0-9.]+)', + required=True, +) + +# %% [markdown] +# Set the sequential fitting parameters. + +# %% +analysis.fitting_mode_type = 'sequential' +analysis.sequential_fit.data_dir = scan_data_dir +analysis.sequential_fit.max_workers = 'auto' +analysis.sequential_fit.reverse = True # %% [markdown] # Run the sequential fit over all data files in the scan directory. # %% -project.analysis.fit_sequential( - data_dir=data_dir, - extract_diffrn=extract_diffrn, - max_workers='auto', - reverse=True, -) +analysis.fit() # %% [markdown] # #### Replay a Dataset @@ -329,7 +345,7 @@ def extract_diffrn(file_path): # %% project.apply_params_from_csv(row_index=0) -project.display.plotter.plot_meas_vs_calc(expt_name='d20') +display.pattern(expt_name='d20') # %% [markdown] # @@ -337,70 +353,45 @@ def extract_diffrn(file_path): # %% project.apply_params_from_csv(row_index=-1) -project.display.plotter.plot_meas_vs_calc(expt_name='d20') +display.pattern(expt_name='d20') # %% [markdown] # #### Plot Parameter Evolution # -# Define the quantity to use as the x-axis in the following plots. +# Reuse the extracted diffrn path as the x-axis in the following plots. + +# %% [markdown] +# Plot fit quality metrics vs. temperature. # %% -temperature = expt.diffrn.ambient_temperature +display.fit.series(analysis.fit_result.success, versus=temperature) +display.fit.series(analysis.fit_result.reduced_chi_square, versus=temperature) +display.fit.series(analysis.fit_result.iterations, versus=temperature) # %% [markdown] # Plot unit cell parameters vs. temperature. # %% -project.display.plotter.plot_param_series(structure.cell.length_a, versus=temperature) -project.display.plotter.plot_param_series(structure.cell.length_b, versus=temperature) -project.display.plotter.plot_param_series(structure.cell.length_c, versus=temperature) +display.fit.series(struct.cell.length_a, versus=temperature) +display.fit.series(struct.cell.length_b, versus=temperature) +display.fit.series(struct.cell.length_c, versus=temperature) # %% [markdown] # Plot isotropic displacement parameters vs. temperature. # %% -project.display.plotter.plot_param_series( - structure.atom_sites['Co1'].adp_iso, - versus=temperature, -) -project.display.plotter.plot_param_series( - structure.atom_sites['Si'].adp_iso, - versus=temperature, -) -project.display.plotter.plot_param_series( - structure.atom_sites['O1'].adp_iso, - versus=temperature, -) -project.display.plotter.plot_param_series( - structure.atom_sites['O2'].adp_iso, - versus=temperature, -) -project.display.plotter.plot_param_series( - structure.atom_sites['O3'].adp_iso, - versus=temperature, -) +display.fit.series(struct.atom_sites['Co1'].adp_iso, versus=temperature) +display.fit.series(struct.atom_sites['Si'].adp_iso, versus=temperature) +display.fit.series(struct.atom_sites['O1'].adp_iso, versus=temperature) +display.fit.series(struct.atom_sites['O2'].adp_iso, versus=temperature) +display.fit.series(struct.atom_sites['O3'].adp_iso, versus=temperature) # %% [markdown] # Plot selected fractional coordinates vs. temperature. # %% -project.display.plotter.plot_param_series( - structure.atom_sites['Co2'].fract_x, - versus=temperature, -) -project.display.plotter.plot_param_series( - structure.atom_sites['Co2'].fract_z, - versus=temperature, -) -project.display.plotter.plot_param_series( - structure.atom_sites['O1'].fract_z, - versus=temperature, -) -project.display.plotter.plot_param_series( - structure.atom_sites['O2'].fract_z, - versus=temperature, -) -project.display.plotter.plot_param_series( - structure.atom_sites['O3'].fract_z, - versus=temperature, -) +display.fit.series(struct.atom_sites['Co2'].fract_x, versus=temperature) +display.fit.series(struct.atom_sites['Co2'].fract_z, versus=temperature) +display.fit.series(struct.atom_sites['O1'].fract_z, versus=temperature) +display.fit.series(struct.atom_sites['O2'].fract_z, versus=temperature) +display.fit.series(struct.atom_sites['O3'].fract_z, versus=temperature) diff --git a/docs/docs/tutorials/ed-18.ipynb b/docs/docs/tutorials/ed-18.ipynb index 47a25154a..7473bc6e5 100644 --- a/docs/docs/tutorials/ed-18.ipynb +++ b/docs/docs/tutorials/ed-18.ipynb @@ -53,8 +53,7 @@ "outputs": [], "source": [ "from easydiffraction import Project\n", - "from easydiffraction import download_data\n", - "from easydiffraction import extract_project_from_zip" + "from easydiffraction import download_data" ] }, { @@ -62,7 +61,7 @@ "id": "4", "metadata": {}, "source": [ - "## Download Project Archive" + "## Download Saved Project" ] }, { @@ -72,31 +71,13 @@ "metadata": {}, "outputs": [], "source": [ - "zip_path = download_data(id=30, destination='data')" + "project_dir = download_data(id=36, destination='projects')" ] }, { "cell_type": "markdown", "id": "6", "metadata": {}, - "source": [ - "## Extract Project" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "7", - "metadata": {}, - "outputs": [], - "source": [ - "project_dir = extract_project_from_zip(zip_path, destination='data')" - ] - }, - { - "cell_type": "markdown", - "id": "8", - "metadata": {}, "source": [ "## Load Project" ] @@ -104,7 +85,7 @@ { "cell_type": "code", "execution_count": null, - "id": "9", + "id": "7", "metadata": {}, "outputs": [], "source": [ @@ -113,7 +94,7 @@ }, { "cell_type": "markdown", - "id": "10", + "id": "8", "metadata": {}, "source": [ "## Perform Analysis" @@ -122,7 +103,7 @@ { "cell_type": "code", "execution_count": null, - "id": "11", + "id": "9", "metadata": {}, "outputs": [], "source": [ @@ -131,7 +112,7 @@ }, { "cell_type": "markdown", - "id": "12", + "id": "10", "metadata": {}, "source": [ "## Show Results" @@ -140,31 +121,31 @@ { "cell_type": "code", "execution_count": null, - "id": "13", + "id": "11", "metadata": {}, "outputs": [], "source": [ - "project.analysis.display.fit_results()" + "project.display.fit.results()" ] }, { "cell_type": "code", "execution_count": null, - "id": "14", + "id": "12", "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_param_correlations()" + "project.display.fit.correlations()" ] }, { "cell_type": "code", "execution_count": null, - "id": "15", + "id": "13", "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='hrpt')" + "project.display.pattern(expt_name='hrpt')" ] } ], diff --git a/docs/docs/tutorials/ed-18.py b/docs/docs/tutorials/ed-18.py index fb44972fa..31e903c04 100644 --- a/docs/docs/tutorials/ed-18.py +++ b/docs/docs/tutorials/ed-18.py @@ -17,19 +17,12 @@ # %% from easydiffraction import Project from easydiffraction import download_data -from easydiffraction import extract_project_from_zip # %% [markdown] -# ## Download Project Archive +# ## Download Saved Project # %% -zip_path = download_data(id=30, destination='data') - -# %% [markdown] -# ## Extract Project - -# %% -project_dir = extract_project_from_zip(zip_path, destination='data') +project_dir = download_data(id=36, destination='projects') # %% [markdown] # ## Load Project @@ -47,10 +40,10 @@ # ## Show Results # %% -project.analysis.display.fit_results() +project.display.fit.results() # %% -project.display.plotter.plot_param_correlations() +project.display.fit.correlations() # %% -project.display.plotter.plot_meas_vs_calc(expt_name='hrpt') +project.display.pattern(expt_name='hrpt') diff --git a/docs/docs/tutorials/ed-2.ipynb b/docs/docs/tutorials/ed-2.ipynb index 4a52b569c..1212cbcc7 100644 --- a/docs/docs/tutorials/ed-2.ipynb +++ b/docs/docs/tutorials/ed-2.ipynb @@ -345,7 +345,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.display.fit_results()" + "project.display.fit.results()" ] }, { @@ -355,7 +355,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_param_correlations()" + "project.display.fit.correlations()" ] }, { @@ -365,7 +365,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='hrpt')" + "project.display.pattern(expt_name='hrpt')" ] }, { @@ -408,8 +408,8 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.fit.show_minimizer_types()\n", - "project.analysis.fit.minimizer_type = 'lmfit'" + "project.analysis.fitting.show_minimizer_types()\n", + "project.analysis.fitting.minimizer_type = 'lmfit'" ] }, { @@ -429,7 +429,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.display.fit_results()" + "project.display.fit.results()" ] }, { @@ -439,7 +439,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_param_correlations()" + "project.display.fit.correlations()" ] }, { @@ -449,7 +449,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='hrpt')" + "project.display.pattern(expt_name='hrpt')" ] }, { @@ -497,7 +497,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.display.fit_results()" + "project.display.fit.results()" ] }, { @@ -507,7 +507,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_param_correlations()" + "project.display.fit.correlations()" ] }, { @@ -517,7 +517,25 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='hrpt')" + "project.display.pattern(expt_name='hrpt')" + ] + }, + { + "cell_type": "markdown", + "id": "42", + "metadata": {}, + "source": [ + "## Step 7: Save Project" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "43", + "metadata": {}, + "outputs": [], + "source": [ + "project.save_as('projects/lbco_hrpt')" ] } ], diff --git a/docs/docs/tutorials/ed-2.py b/docs/docs/tutorials/ed-2.py index fa4918c05..220ebae9c 100644 --- a/docs/docs/tutorials/ed-2.py +++ b/docs/docs/tutorials/ed-2.py @@ -162,13 +162,13 @@ project.analysis.fit() # %% -project.analysis.display.fit_results() +project.display.fit.results() # %% -project.display.plotter.plot_param_correlations() +project.display.fit.correlations() # %% -project.display.plotter.plot_meas_vs_calc(expt_name='hrpt') +project.display.pattern(expt_name='hrpt') # %% [markdown] # ## Step 5: Perform Analysis (with constraints) @@ -192,20 +192,20 @@ project.analysis.constraints.create(expression='biso_Ba = biso_La') # %% -project.analysis.fit.show_minimizer_types() -project.analysis.fit.minimizer_type = 'lmfit' +project.analysis.fitting.show_minimizer_types() +project.analysis.fitting.minimizer_type = 'lmfit' # %% project.analysis.fit() # %% -project.analysis.display.fit_results() +project.display.fit.results() # %% -project.display.plotter.plot_param_correlations() +project.display.fit.correlations() # %% -project.display.plotter.plot_meas_vs_calc(expt_name='hrpt') +project.display.pattern(expt_name='hrpt') # %% [markdown] # ## Step 6: Switch calculator engine @@ -220,10 +220,16 @@ project.analysis.fit() # %% -project.analysis.display.fit_results() +project.display.fit.results() # %% -project.display.plotter.plot_param_correlations() +project.display.fit.correlations() # %% -project.display.plotter.plot_meas_vs_calc(expt_name='hrpt') +project.display.pattern(expt_name='hrpt') + +# %% [markdown] +# ## Step 7: Save Project + +# %% +project.save_as('projects/lbco_hrpt') diff --git a/docs/docs/tutorials/ed-20.ipynb b/docs/docs/tutorials/ed-20.ipynb index b9a47a281..086d7d185 100644 --- a/docs/docs/tutorials/ed-20.ipynb +++ b/docs/docs/tutorials/ed-20.ipynb @@ -459,7 +459,7 @@ "outputs": [], "source": [ "project = Project(name='beer')\n", - "project.save_as(dir_path='beer_mcstas')" + "project.save_as(dir_path='projects/beer_mcstas')" ] }, { @@ -515,7 +515,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='expt_s2')" + "project.display.pattern(expt_name='expt_s2')" ] }, { @@ -525,7 +525,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='expt_n2')" + "project.display.pattern(expt_name='expt_n2')" ] }, { @@ -548,7 +548,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.fit.show_modes()" + "project.analysis.show_fitting_mode_types()" ] }, { @@ -558,7 +558,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.fit.mode = 'joint'" + "project.analysis.fitting_mode_type = 'joint'" ] }, { @@ -576,7 +576,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.display.fittable_params()" + "project.display.parameters.fittable()" ] }, { @@ -731,8 +731,8 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.display.fit_results()\n", - "project.display.plotter.plot_param_correlations()" + "project.display.fit.results()\n", + "project.display.fit.correlations()" ] }, { @@ -752,7 +752,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='expt_s2')" + "project.display.pattern(expt_name='expt_s2')" ] }, { @@ -762,7 +762,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='expt_n2')" + "project.display.pattern(expt_name='expt_n2')" ] }, { @@ -780,7 +780,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(\n", + "project.display.pattern(\n", " expt_name='expt_s2',\n", " x='d_spacing',\n", " x_min=2.08,\n", @@ -795,7 +795,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(\n", + "project.display.pattern(\n", " expt_name='expt_n2',\n", " x='d_spacing',\n", " x_min=2.08,\n", diff --git a/docs/docs/tutorials/ed-20.py b/docs/docs/tutorials/ed-20.py index 0ac55fdbf..1fcc6d24a 100644 --- a/docs/docs/tutorials/ed-20.py +++ b/docs/docs/tutorials/ed-20.py @@ -225,7 +225,7 @@ # %% project = Project(name='beer') -project.save_as(dir_path='beer_mcstas') +project.save_as(dir_path='projects/beer_mcstas') # %% [markdown] # #### Add Structures @@ -245,10 +245,10 @@ # #### Plot Measured vs Calculated # %% -project.display.plotter.plot_meas_vs_calc(expt_name='expt_s2') +project.display.pattern(expt_name='expt_s2') # %% -project.display.plotter.plot_meas_vs_calc(expt_name='expt_n2') +project.display.pattern(expt_name='expt_n2') # %% [markdown] # ## Perform Analysis @@ -259,16 +259,16 @@ # #### Set Fit Mode # %% -project.analysis.fit.show_modes() +project.analysis.show_fitting_mode_types() # %% -project.analysis.fit.mode = 'joint' +project.analysis.fitting_mode_type = 'joint' # %% [markdown] # #### Set Free Parameters # %% -project.analysis.display.fittable_params() +project.display.parameters.fittable() # %% ferrite.atom_sites['Fe'].adp_iso.free = True @@ -347,8 +347,8 @@ # Show fit results and parameter correlations. # %% -project.analysis.display.fit_results() -project.display.plotter.plot_param_correlations() +project.display.fit.results() +project.display.fit.correlations() # %% [markdown] # #### Plot Measured vs Calculated @@ -356,16 +356,16 @@ # Show full range in TOF. # %% -project.display.plotter.plot_meas_vs_calc(expt_name='expt_s2') +project.display.pattern(expt_name='expt_s2') # %% -project.display.plotter.plot_meas_vs_calc(expt_name='expt_n2') +project.display.pattern(expt_name='expt_n2') # %% [markdown] # Show selected peaks in d-spacing. # %% -project.display.plotter.plot_meas_vs_calc( +project.display.pattern( expt_name='expt_s2', x='d_spacing', x_min=2.08, @@ -373,7 +373,7 @@ ) # %% -project.display.plotter.plot_meas_vs_calc( +project.display.pattern( expt_name='expt_n2', x='d_spacing', x_min=2.08, diff --git a/docs/docs/tutorials/ed-21.ipynb b/docs/docs/tutorials/ed-21.ipynb new file mode 100644 index 000000000..689c3d57a --- /dev/null +++ b/docs/docs/tutorials/ed-21.ipynb @@ -0,0 +1,763 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "0", + "metadata": { + "tags": [ + "hide-in-docs" + ] + }, + "outputs": [], + "source": [ + "# Check whether easydiffraction is installed; install it if needed.\n", + "# Required for remote environments such as Google Colab.\n", + "import importlib.util\n", + "\n", + "if importlib.util.find_spec('easydiffraction') is None:\n", + " %pip install easydiffraction" + ] + }, + { + "cell_type": "markdown", + "id": "1", + "metadata": {}, + "source": [ + "# Bayesian Analysis: LBCO, HRPT\n", + "\n", + "This tutorial demonstrates a practical two-stage workflow for powder\n", + "diffraction analysis with EasyDiffraction.\n", + "\n", + "In the first stage, we run a fast local refinement to obtain a sensible\n", + "point estimate and parameter uncertainties. In the second stage, we use\n", + "these refined values to define fit bounds and then sample the posterior\n", + "distribution with DREAM.\n", + "\n", + "The example uses constant-wavelength neutron powder diffraction data\n", + "for La0.5Ba0.5CoO3 measured on HRPT at PSI.\n", + "\n", + "The goal is not only to obtain a good fit, but also to answer Bayesian\n", + "questions such as:\n", + "\n", + "- Which parameter values are most probable?\n", + "- How broad are the credible intervals?\n", + "- Which parameters are strongly correlated?\n", + "- How much uncertainty propagates into the calculated diffraction\n", + " pattern?" + ] + }, + { + "cell_type": "markdown", + "id": "2", + "metadata": {}, + "source": [ + "## Import Library" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3", + "metadata": {}, + "outputs": [], + "source": [ + "import easydiffraction as ed" + ] + }, + { + "cell_type": "markdown", + "id": "4", + "metadata": {}, + "source": [ + "## Step 1: Create a Project Container\n", + "\n", + "The project object keeps structures, experiments, fit settings, and\n", + "plotting utilities together in a single place. We will build the full\n", + "workflow inside this object.\n", + "\n", + "Save the project to a directory early on so that you can easily reload\n", + "it later if needed." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5", + "metadata": {}, + "outputs": [], + "source": [ + "project = ed.Project()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6", + "metadata": {}, + "outputs": [], + "source": [ + "project.save_as('projects/lbco_hrpt_bayesian')" + ] + }, + { + "cell_type": "markdown", + "id": "7", + "metadata": {}, + "source": [ + "## Step 2: Build the Structural Model\n", + "\n", + "We define a simple cubic perovskite model for LBCO. La and Ba share the\n", + "same crystallographic site with equal occupancy, while Co and O occupy\n", + "the remaining ideal perovskite positions." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8", + "metadata": {}, + "outputs": [], + "source": [ + "project.structures.create(name='lbco')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9", + "metadata": {}, + "outputs": [], + "source": [ + "structure = project.structures['lbco']" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "10", + "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": "11", + "metadata": {}, + "outputs": [], + "source": [ + "structure.cell.length_a = 3.88" + ] + }, + { + "cell_type": "markdown", + "id": "12", + "metadata": {}, + "source": [ + "The atom-site definitions below form the starting structural model. The\n", + "parameters are intentionally reasonable rather than fully optimized,\n", + "because the refinement step will improve them." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "13", + "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", + " adp_type='Biso',\n", + " adp_iso=0.5151,\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", + " adp_type='Biso',\n", + " adp_iso=0.5151,\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", + " adp_type='Biso',\n", + " adp_iso=0.2190,\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", + " adp_type='Biso',\n", + " adp_iso=1.3916,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "14", + "metadata": {}, + "source": [ + "## Step 3: Define the Diffraction Experiment\n", + "\n", + "Next we download the measured powder pattern, create a neutron powder\n", + "experiment, and configure the instrument, profile, background, and\n", + "excluded regions." + ] + }, + { + "cell_type": "markdown", + "id": "15", + "metadata": {}, + "source": [ + "Download the measured data from the repository. Alternatively, you\n", + "could use your own data file by providing the path to it instead of\n", + "downloading from the repository." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "16", + "metadata": {}, + "outputs": [], + "source": [ + "data_path = ed.download_data(id=3, destination='data')" + ] + }, + { + "cell_type": "markdown", + "id": "17", + "metadata": {}, + "source": [ + "Create the experiment object and specify the sample form, beam mode,\n", + "and radiation probe." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "18", + "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": "19", + "metadata": {}, + "outputs": [], + "source": [ + "experiment = project.experiments['hrpt']" + ] + }, + { + "cell_type": "markdown", + "id": "20", + "metadata": {}, + "source": [ + "Link the structural phase to the experiment." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "21", + "metadata": {}, + "outputs": [], + "source": [ + "experiment.linked_phases.create(id='lbco', scale=9.1351)" + ] + }, + { + "cell_type": "markdown", + "id": "22", + "metadata": {}, + "source": [ + "Set instrument and peak profile parameters.\n", + "\n", + "These values provide the initial instrument description for the local\n", + "refinement. Later, a subset of them will be refined." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "23", + "metadata": {}, + "outputs": [], + "source": [ + "experiment.instrument.setup_wavelength = 1.494\n", + "experiment.instrument.calib_twotheta_offset = 0.0" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "24", + "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.1204\n", + "experiment.peak.broad_lorentz_y = 0.0844" + ] + }, + { + "cell_type": "markdown", + "id": "25", + "metadata": {}, + "source": [ + "Add background points and excluded regions.\n", + "\n", + "The line-segment background is defined by a few anchor points. We also\n", + "exclude regions that are not intended to contribute to the fit." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "26", + "metadata": {}, + "outputs": [], + "source": [ + "experiment.background.create(id='1', x=10, y=168.5585)\n", + "experiment.background.create(id='2', x=30, y=164.3357)\n", + "experiment.background.create(id='3', x=50, y=166.8881)\n", + "experiment.background.create(id='4', x=110, y=175.4006)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "27", + "metadata": {}, + "outputs": [], + "source": [ + "experiment.excluded_regions.create(id='1', start=0, end=10)\n", + "experiment.excluded_regions.create(id='2', start=100, end=180)" + ] + }, + { + "cell_type": "markdown", + "id": "28", + "metadata": {}, + "source": [ + "## Step 4: Run an Initial Local Refinement\n", + "\n", + "Before Bayesian sampling, it is useful to run a deterministic fit. This\n", + "gives us:\n", + "\n", + "- a good point estimate near the best-fit region,\n", + "- uncertainties from the local optimizer,\n", + "- a quick check that the model and experiment are configured\n", + " sensibly.\n", + "\n", + "In this tutorial we refine only a small set of parameters that are easy\n", + "to interpret in the later Bayesian stage." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "29", + "metadata": {}, + "outputs": [], + "source": [ + "structure.cell.length_a.free = True" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "30", + "metadata": {}, + "outputs": [], + "source": [ + "experiment.linked_phases['lbco'].scale.free = True\n", + "experiment.peak.broad_gauss_u.free = True\n", + "experiment.peak.broad_gauss_v.free = True\n", + "experiment.instrument.calib_twotheta_offset.free = True" + ] + }, + { + "cell_type": "markdown", + "id": "31", + "metadata": {}, + "source": [ + "We choose the BUMPS Levenberg-Marquardt minimizer as a fast local\n", + "optimizer. Its main purpose here is to provide a stable starting point\n", + "and uncertainty estimates for the Bayesian run." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "32", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.fitting.show_minimizer_types()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "33", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.fitting.minimizer_type = 'bumps (lm)'" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "34", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.fit()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "35", + "metadata": {}, + "outputs": [], + "source": [ + "project.display.fit.results()" + ] + }, + { + "cell_type": "markdown", + "id": "36", + "metadata": {}, + "source": [ + "The correlation plot shows how strongly the fitted parameters move\n", + "together in the local refinement. The measured-vs-calculated plots show\n", + "how well the refined model reproduces the data globally and in a zoomed\n", + "region." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "37", + "metadata": {}, + "outputs": [], + "source": [ + "project.display.fit.correlations()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "38", + "metadata": {}, + "outputs": [], + "source": [ + "project.display.pattern(expt_name='hrpt')" + ] + }, + { + "cell_type": "markdown", + "id": "39", + "metadata": {}, + "source": [ + "## Step 5: Prepare for Bayesian Sampling\n", + "\n", + "DREAM requires finite bounds for the free parameters. Instead of\n", + "setting them manually, we derive them from the uncertainties estimated\n", + "in the local refinement.\n", + "\n", + "The helper method `set_fit_bounds_from_uncertainty` centers the bounds\n", + "on the current parameter value and expands them by a chosen multiple of\n", + "the reported uncertainty.\n", + "\n", + "The default `multiplier` is 4. If the local refinement is very tight,\n", + "or if you expect a broader posterior, increase it explicitly.\n", + "\n", + "Show unset fit bounds before setting them from the local refinement uncertainties." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "40", + "metadata": {}, + "outputs": [], + "source": [ + "project.display.parameters.free()" + ] + }, + { + "cell_type": "markdown", + "id": "41", + "metadata": {}, + "source": [ + "Set fit bounds for all free parameters using the default multiplier of\n", + "4. In this tutorial that means the posterior pair plot will later\n", + "refer to a `±4 × uncertainty` region in its title. To use a different\n", + "region, pass another value, for example `multiplier=6`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "42", + "metadata": {}, + "outputs": [], + "source": [ + "for param in project.free_parameters:\n", + " param.set_fit_bounds_from_uncertainty()" + ] + }, + { + "cell_type": "markdown", + "id": "43", + "metadata": {}, + "source": [ + "Displaying the free parameters again is a convenient way to confirm\n", + "that the fit bounds have been assigned as expected before launching the\n", + "sampler." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "44", + "metadata": {}, + "outputs": [], + "source": [ + "project.display.parameters.free()" + ] + }, + { + "cell_type": "markdown", + "id": "45", + "metadata": {}, + "source": [ + "## Step 6: Configure and Run DREAM\n", + "\n", + "We now switch from the local minimizer to the Bayesian DREAM sampler.\n", + "\n", + "The settings below are intentionally small so the tutorial runs\n", + "quickly. For production analysis you would usually increase the number\n", + "of steps (`steps`) and often the burn-in (`burn`) as well. When\n", + "needed, the DREAM API also lets you tune how chains are initialized\n", + "through the `init` setting. Other sampler settings such as `thin` and\n", + "`pop` can be adjusted as well. The current EasyDiffraction defaults\n", + "use `steps=3000`, `init='lhs'`, and `parallel=0`, which tells\n", + "BUMPS-DREAM to use all available CPUs for population evaluations.\n", + "\n", + "The `burn` setting is auto-resolved when left unset. With the default\n", + "`steps=3000` this gives `burn=600`, but if you override `steps` and\n", + "keep `burn=None`, the effective burn-in is recomputed automatically.\n", + "Here we use a much smaller step count to keep the tutorial fast, but\n", + "this is not recommended for production analysis." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "46", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.fitting.show_minimizer_types()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "47", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.fitting.minimizer_type = 'bumps (dream)'" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "48", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.fitting.minimizer.steps = 100 # lower than the default 3000\n", + "project.analysis.fitting.minimizer.burn = 20 # lower than the default 600" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "49", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.fit()" + ] + }, + { + "cell_type": "markdown", + "id": "50", + "metadata": {}, + "source": [ + "## Step 7: Inspect Bayesian Results\n", + "\n", + "The fit-results display now includes sampler settings, convergence\n", + "diagnostics, committed parameter values, and posterior summary\n", + "statistics." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "51", + "metadata": {}, + "outputs": [], + "source": [ + "project.display.fit.results()" + ] + }, + { + "cell_type": "markdown", + "id": "52", + "metadata": {}, + "source": [ + "The correlation and posterior-pair plots are complementary:\n", + "\n", + "- `plot_param_correlations` summarizes pairwise structure in a compact\n", + " matrix.\n", + "- `plot_posterior_pairs` shows marginal densities on the diagonal and\n", + " posterior contours off-diagonal. In this tutorial its title also\n", + " reminds you that the display region follows the `±4 × uncertainty`\n", + " bounds defined above, while numeric subplot ranges are omitted to\n", + " keep the grid readable." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "53", + "metadata": {}, + "outputs": [], + "source": [ + "project.display.fit.correlations()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "54", + "metadata": {}, + "outputs": [], + "source": [ + "project.display.posterior.pairs()" + ] + }, + { + "cell_type": "markdown", + "id": "55", + "metadata": {}, + "source": [ + "The one-dimensional posterior distributions below make it easier to\n", + "inspect individual parameters in isolation, including asymmetry or\n", + "multimodality." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "56", + "metadata": {}, + "outputs": [], + "source": [ + "project.display.posterior.distribution()" + ] + }, + { + "cell_type": "markdown", + "id": "57", + "metadata": {}, + "source": [ + "Finally, the posterior predictive plot propagates the sampled parameter\n", + "uncertainty into the calculated diffraction pattern. Comparing this to\n", + "the zoomed measured-vs-calculated view helps assess whether the sampled\n", + "model family explains the data in the region of interest." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "58", + "metadata": {}, + "outputs": [], + "source": [ + "project.display.posterior.predictive(expt_name='hrpt')" + ] + }, + { + "cell_type": "markdown", + "id": "59", + "metadata": {}, + "source": [ + "A final zoomed measured-vs-calculated plot is useful for checking how\n", + "the posterior-supported model behaves in a narrow region of the pattern\n", + "after the Bayesian run." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "60", + "metadata": {}, + "outputs": [], + "source": [ + "project.display.posterior.predictive(expt_name='hrpt', x_min=92, x_max=93)" + ] + } + ], + "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-21.py b/docs/docs/tutorials/ed-21.py new file mode 100644 index 000000000..df32bcabe --- /dev/null +++ b/docs/docs/tutorials/ed-21.py @@ -0,0 +1,356 @@ +# %% [markdown] +# # Bayesian Analysis: LBCO, HRPT +# +# This tutorial demonstrates a practical two-stage workflow for powder +# diffraction analysis with EasyDiffraction. +# +# In the first stage, we run a fast local refinement to obtain a sensible +# point estimate and parameter uncertainties. In the second stage, we use +# these refined values to define fit bounds and then sample the posterior +# distribution with DREAM. +# +# The example uses constant-wavelength neutron powder diffraction data +# for La0.5Ba0.5CoO3 measured on HRPT at PSI. +# +# The goal is not only to obtain a good fit, but also to answer Bayesian +# questions such as: +# +# - Which parameter values are most probable? +# - How broad are the credible intervals? +# - Which parameters are strongly correlated? +# - How much uncertainty propagates into the calculated diffraction +# pattern? + +# %% [markdown] +# ## Import Library + +# %% +import easydiffraction as ed + +# %% [markdown] +# ## Step 1: Create a Project Container +# +# The project object keeps structures, experiments, fit settings, and +# plotting utilities together in a single place. We will build the full +# workflow inside this object. +# +# Save the project to a directory early on so that you can easily reload +# it later if needed. + +# %% +project = ed.Project() + +# %% +project.save_as('projects/lbco_hrpt_bayesian') + +# %% [markdown] +# ## Step 2: Build the Structural Model +# +# We define a simple cubic perovskite model for LBCO. La and Ba share the +# same crystallographic site with equal occupancy, while Co and O occupy +# the remaining ideal perovskite positions. + +# %% +project.structures.create(name='lbco') + +# %% +structure = project.structures['lbco'] + +# %% +structure.space_group.name_h_m = 'P m -3 m' +structure.space_group.it_coordinate_system_code = '1' + +# %% +structure.cell.length_a = 3.88 + +# %% [markdown] +# The atom-site definitions below form the starting structural model. The +# parameters are intentionally reasonable rather than fully optimized, +# because the refinement step will improve them. + +# %% +structure.atom_sites.create( + label='La', + type_symbol='La', + fract_x=0, + fract_y=0, + fract_z=0, + wyckoff_letter='a', + adp_type='Biso', + adp_iso=0.5151, + occupancy=0.5, +) +structure.atom_sites.create( + label='Ba', + type_symbol='Ba', + fract_x=0, + fract_y=0, + fract_z=0, + wyckoff_letter='a', + adp_type='Biso', + adp_iso=0.5151, + occupancy=0.5, +) +structure.atom_sites.create( + label='Co', + type_symbol='Co', + fract_x=0.5, + fract_y=0.5, + fract_z=0.5, + wyckoff_letter='b', + adp_type='Biso', + adp_iso=0.2190, +) +structure.atom_sites.create( + label='O', + type_symbol='O', + fract_x=0, + fract_y=0.5, + fract_z=0.5, + wyckoff_letter='c', + adp_type='Biso', + adp_iso=1.3916, +) + +# %% [markdown] +# ## Step 3: Define the Diffraction Experiment +# +# Next we download the measured powder pattern, create a neutron powder +# experiment, and configure the instrument, profile, background, and +# excluded regions. + +# %% [markdown] +# Download the measured data from the repository. Alternatively, you +# could use your own data file by providing the path to it instead of +# downloading from the repository. + +# %% +data_path = ed.download_data(id=3, destination='data') + +# %% [markdown] +# Create the experiment object and specify the sample form, beam mode, +# and radiation probe. + +# %% +project.experiments.add_from_data_path( + name='hrpt', + data_path=data_path, + sample_form='powder', + beam_mode='constant wavelength', + radiation_probe='neutron', +) + +# %% +experiment = project.experiments['hrpt'] + +# %% [markdown] +# Link the structural phase to the experiment. + +# %% +experiment.linked_phases.create(id='lbco', scale=9.1351) + +# %% [markdown] +# Set instrument and peak profile parameters. +# +# These values provide the initial instrument description for the local +# refinement. Later, a subset of them will be refined. + +# %% +experiment.instrument.setup_wavelength = 1.494 +experiment.instrument.calib_twotheta_offset = 0.0 + +# %% +experiment.peak.broad_gauss_u = 0.1 +experiment.peak.broad_gauss_v = -0.1 +experiment.peak.broad_gauss_w = 0.1204 +experiment.peak.broad_lorentz_y = 0.0844 + +# %% [markdown] +# Add background points and excluded regions. +# +# The line-segment background is defined by a few anchor points. We also +# exclude regions that are not intended to contribute to the fit. + +# %% +experiment.background.create(id='1', x=10, y=168.5585) +experiment.background.create(id='2', x=30, y=164.3357) +experiment.background.create(id='3', x=50, y=166.8881) +experiment.background.create(id='4', x=110, y=175.4006) + +# %% +experiment.excluded_regions.create(id='1', start=0, end=10) +experiment.excluded_regions.create(id='2', start=100, end=180) + +# %% [markdown] +# ## Step 4: Run an Initial Local Refinement +# +# Before Bayesian sampling, it is useful to run a deterministic fit. This +# gives us: +# +# - a good point estimate near the best-fit region, +# - uncertainties from the local optimizer, +# - a quick check that the model and experiment are configured +# sensibly. +# +# In this tutorial we refine only a small set of parameters that are easy +# to interpret in the later Bayesian stage. + +# %% +structure.cell.length_a.free = True + +# %% +experiment.linked_phases['lbco'].scale.free = True +experiment.peak.broad_gauss_u.free = True +experiment.peak.broad_gauss_v.free = True +experiment.instrument.calib_twotheta_offset.free = True + +# %% [markdown] +# We choose the BUMPS Levenberg-Marquardt minimizer as a fast local +# optimizer. Its main purpose here is to provide a stable starting point +# and uncertainty estimates for the Bayesian run. + +# %% +project.analysis.fitting.show_minimizer_types() + +# %% +project.analysis.fitting.minimizer_type = 'bumps (lm)' + +# %% +project.analysis.fit() + +# %% +project.display.fit.results() + +# %% [markdown] +# The correlation plot shows how strongly the fitted parameters move +# together in the local refinement. The measured-vs-calculated plots show +# how well the refined model reproduces the data globally and in a zoomed +# region. + +# %% +project.display.fit.correlations() + +# %% +project.display.pattern(expt_name='hrpt') + +# %% [markdown] +# ## Step 5: Prepare for Bayesian Sampling +# +# DREAM requires finite bounds for the free parameters. Instead of +# setting them manually, we derive them from the uncertainties estimated +# in the local refinement. +# +# The helper method `set_fit_bounds_from_uncertainty` centers the bounds +# on the current parameter value and expands them by a chosen multiple of +# the reported uncertainty. +# +# The default `multiplier` is 4. If the local refinement is very tight, +# or if you expect a broader posterior, increase it explicitly. +# +# Show unset fit bounds before setting them from the local refinement uncertainties. + +# %% +project.display.parameters.free() + +# %% [markdown] +# Set fit bounds for all free parameters using the default multiplier of +# 4. In this tutorial that means the posterior pair plot will later +# refer to a `±4 × uncertainty` region in its title. To use a different +# region, pass another value, for example `multiplier=6`. + +# %% +for param in project.free_parameters: + param.set_fit_bounds_from_uncertainty() + +# %% [markdown] +# Displaying the free parameters again is a convenient way to confirm +# that the fit bounds have been assigned as expected before launching the +# sampler. + +# %% +project.display.parameters.free() + +# %% [markdown] +# ## Step 6: Configure and Run DREAM +# +# We now switch from the local minimizer to the Bayesian DREAM sampler. +# +# The settings below are intentionally small so the tutorial runs +# quickly. For production analysis you would usually increase the number +# of steps (`steps`) and often the burn-in (`burn`) as well. When +# needed, the DREAM API also lets you tune how chains are initialized +# through the `init` setting. Other sampler settings such as `thin` and +# `pop` can be adjusted as well. The current EasyDiffraction defaults +# use `steps=3000`, `init='lhs'`, and `parallel=0`, which tells +# BUMPS-DREAM to use all available CPUs for population evaluations. +# +# The `burn` setting is auto-resolved when left unset. With the default +# `steps=3000` this gives `burn=600`, but if you override `steps` and +# keep `burn=None`, the effective burn-in is recomputed automatically. +# Here we use a much smaller step count to keep the tutorial fast, but +# this is not recommended for production analysis. + +# %% +project.analysis.fitting.show_minimizer_types() + +# %% +project.analysis.fitting.minimizer_type = 'bumps (dream)' + +# %% +project.analysis.fitting.minimizer.steps = 100 # lower than the default 3000 +project.analysis.fitting.minimizer.burn = 20 # lower than the default 600 + +# %% +project.analysis.fit() + +# %% [markdown] +# ## Step 7: Inspect Bayesian Results +# +# The fit-results display now includes sampler settings, convergence +# diagnostics, committed parameter values, and posterior summary +# statistics. + +# %% +project.display.fit.results() + +# %% [markdown] +# The correlation and posterior-pair plots are complementary: +# +# - `plot_param_correlations` summarizes pairwise structure in a compact +# matrix. +# - `plot_posterior_pairs` shows marginal densities on the diagonal and +# posterior contours off-diagonal. In this tutorial its title also +# reminds you that the display region follows the `±4 × uncertainty` +# bounds defined above, while numeric subplot ranges are omitted to +# keep the grid readable. + +# %% +project.display.fit.correlations() + +# %% +project.display.posterior.pairs() + +# %% [markdown] +# The one-dimensional posterior distributions below make it easier to +# inspect individual parameters in isolation, including asymmetry or +# multimodality. + +# %% +project.display.posterior.distribution() + +# %% [markdown] +# Finally, the posterior predictive plot propagates the sampled parameter +# uncertainty into the calculated diffraction pattern. Comparing this to +# the zoomed measured-vs-calculated view helps assess whether the sampled +# model family explains the data in the region of interest. + +# %% +project.display.posterior.predictive(expt_name='hrpt') + +# %% [markdown] +# A final zoomed measured-vs-calculated plot is useful for checking how +# the posterior-supported model behaves in a narrow region of the pattern +# after the Bayesian run. + +# %% +project.display.posterior.predictive(expt_name='hrpt', x_min=92, x_max=93) diff --git a/docs/docs/tutorials/ed-22.ipynb b/docs/docs/tutorials/ed-22.ipynb new file mode 100644 index 000000000..a8d511132 --- /dev/null +++ b/docs/docs/tutorials/ed-22.ipynb @@ -0,0 +1,607 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "0", + "metadata": { + "tags": [ + "hide-in-docs" + ] + }, + "outputs": [], + "source": [ + "# Check whether easydiffraction is installed; install it if needed.\n", + "# Required for remote environments such as Google Colab.\n", + "import importlib.util\n", + "\n", + "if importlib.util.find_spec('easydiffraction') is None:\n", + " %pip install easydiffraction" + ] + }, + { + "cell_type": "markdown", + "id": "1", + "metadata": {}, + "source": [ + "# Bayesian Analysis: Tb2TiO7, HEiDi\n", + "\n", + "This tutorial demonstrates a practical two-stage workflow for single-crystal\n", + "diffraction analysis with EasyDiffraction.\n", + "\n", + "In the first stage, we run a fast local refinement to obtain a sensible\n", + "point estimate and parameter uncertainties. In the second stage, we use\n", + "these refined values to define fit bounds and then sample the posterior\n", + "distribution with BUMPS-DREAM.\n", + "\n", + "The example uses constant-wavelength neutron single-crystal diffraction data\n", + "for Tb2TiO7 measured on HEiDi at FRM II.\n", + "\n", + "The goal is not only to obtain a good fit, but also to answer Bayesian\n", + "questions such as:\n", + "\n", + "- Which parameter values are most probable?\n", + "- How broad are the credible intervals?\n", + "- Which parameters are strongly correlated?\n", + "- How much uncertainty propagates into the calculated reflection\n", + " intensities?" + ] + }, + { + "cell_type": "markdown", + "id": "2", + "metadata": {}, + "source": [ + "## Import Library" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3", + "metadata": {}, + "outputs": [], + "source": [ + "import easydiffraction as ed" + ] + }, + { + "cell_type": "markdown", + "id": "4", + "metadata": {}, + "source": [ + "## Step 1: Create a Project Container\n", + "\n", + "The project object keeps structures, experiments, fit settings, and\n", + "plotting utilities together in a single place. We will build the full\n", + "workflow inside this object." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5", + "metadata": {}, + "outputs": [], + "source": [ + "project = ed.Project()" + ] + }, + { + "cell_type": "markdown", + "id": "6", + "metadata": {}, + "source": [ + "## Step 2: Build the Structural Model\n", + "\n", + "For this example we start from a CIF file describing the Tb2TiO7\n", + "pyrochlore structure. Loading the structure from CIF is convenient\n", + "because it preserves a realistic starting\n", + "model without rebuilding the full structure by hand." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7", + "metadata": {}, + "outputs": [], + "source": [ + "structure_path = ed.download_data(id=20, destination='data')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8", + "metadata": {}, + "outputs": [], + "source": [ + "project.structures.add_from_cif_path(structure_path)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9", + "metadata": {}, + "outputs": [], + "source": [ + "structure = project.structures['tbti']" + ] + }, + { + "cell_type": "markdown", + "id": "10", + "metadata": {}, + "source": [ + "## Step 3: Define the Diffraction Experiment\n", + "\n", + "Next we download the measured reflection data, create a neutron\n", + "single-crystal experiment, and configure the crystal link,\n", + "wavelength, and extinction model." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "11", + "metadata": {}, + "outputs": [], + "source": [ + "data_path = ed.download_data(id=19, destination='data')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "12", + "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": "13", + "metadata": {}, + "outputs": [], + "source": [ + "experiment = project.experiments['heidi']" + ] + }, + { + "cell_type": "markdown", + "id": "14", + "metadata": {}, + "source": [ + "Link the crystal structure to the experiment and set its scale factor." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "15", + "metadata": {}, + "outputs": [], + "source": [ + "experiment.linked_crystal.id = 'tbti'\n", + "experiment.linked_crystal.scale = 1.0" + ] + }, + { + "cell_type": "markdown", + "id": "16", + "metadata": {}, + "source": [ + "Set the instrument wavelength and starting extinction parameters.\n", + "These values provide the initial experiment description for the local\n", + "refinement." + ] + }, + { + "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 = 35000\n", + "experiment.extinction.radius = 10" + ] + }, + { + "cell_type": "markdown", + "id": "19", + "metadata": {}, + "source": [ + "## Step 4: Run an Initial Local Refinement\n", + "\n", + "Before Bayesian sampling, it is useful to run a deterministic fit. This\n", + "gives us:\n", + "\n", + "- a good point estimate near the best-fit region,\n", + "- uncertainties from the local optimizer,\n", + "- a quick check that the model and experiment are configured\n", + " sensibly.\n", + "\n", + "In this tutorial we refine a small set of structural and extinction\n", + "parameters while keeping occupancies fixed." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "20", + "metadata": {}, + "outputs": [], + "source": [ + "structure.atom_sites['O1'].fract_x.free = True\n", + "\n", + "structure.atom_sites['Ti'].occupancy.free = False\n", + "structure.atom_sites['O1'].occupancy.free = False\n", + "structure.atom_sites['O2'].occupancy.free = False\n", + "\n", + "structure.atom_sites['Tb'].adp_iso.free = True\n", + "structure.atom_sites['Ti'].adp_iso.free = True\n", + "structure.atom_sites['O1'].adp_iso.free = True\n", + "structure.atom_sites['O2'].adp_iso.free = True" + ] + }, + { + "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": "markdown", + "id": "22", + "metadata": {}, + "source": [ + "We keep using the default LMFIT Levenberg-Marquardt minimizer as a fast local\n", + "optimizer. Its main purpose here is to provide a stable starting point\n", + "and uncertainty estimates for the Bayesian run." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "23", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.fitting.show_minimizer_types()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "24", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.fit()" + ] + }, + { + "cell_type": "markdown", + "id": "25", + "metadata": {}, + "source": [ + "The fit-results display summarizes the locally refined values and their\n", + "estimated uncertainties." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "26", + "metadata": {}, + "outputs": [], + "source": [ + "project.display.fit.results()" + ] + }, + { + "cell_type": "markdown", + "id": "27", + "metadata": {}, + "source": [ + "The correlation plot shows how strongly the refined parameters move\n", + "together in the local refinement. The measured-vs-calculated plot shows\n", + "how well the refined crystal model reproduces the measured reflection\n", + "intensities." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "28", + "metadata": {}, + "outputs": [], + "source": [ + "project.display.fit.correlations()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "29", + "metadata": {}, + "outputs": [], + "source": [ + "project.display.pattern(expt_name='heidi')" + ] + }, + { + "cell_type": "markdown", + "id": "30", + "metadata": {}, + "source": [ + "## Step 5: Prepare for Bayesian Sampling\n", + "\n", + "DREAM requires finite bounds for the free parameters. Instead of\n", + "setting them manually, we derive them from the uncertainties estimated\n", + "in the local refinement.\n", + "\n", + "The helper method `set_fit_bounds_from_uncertainty` centers the bounds\n", + "on the current parameter value and expands them by a chosen multiple of\n", + "the reported uncertainty.\n", + "\n", + "The default `multiplier` is 4. In this single-crystal tutorial we use\n", + "a tighter value of `1.5` to keep the sampling window closer to the\n", + "locally refined solution.\n", + "\n", + "Show unset fit bounds before setting them from the local refinement\n", + "uncertainties." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "31", + "metadata": {}, + "outputs": [], + "source": [ + "project.display.parameters.free()" + ] + }, + { + "cell_type": "markdown", + "id": "32", + "metadata": {}, + "source": [ + "Set fit bounds for all free parameters using `multiplier=1.5`. In this\n", + "tutorial that means the posterior pair plot will later refer to a\n", + "`±1.5 × uncertainty` region in its title. To widen the sampling window,\n", + "increase the multiplier explicitly." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "33", + "metadata": {}, + "outputs": [], + "source": [ + "for param in project.free_parameters:\n", + " param.set_fit_bounds_from_uncertainty(multiplier=1.5)" + ] + }, + { + "cell_type": "markdown", + "id": "34", + "metadata": {}, + "source": [ + "Displaying the free parameters again is a convenient way to confirm\n", + "that the fit bounds have been assigned as expected before launching the\n", + "sampler." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "35", + "metadata": {}, + "outputs": [], + "source": [ + "project.display.parameters.free()" + ] + }, + { + "cell_type": "markdown", + "id": "36", + "metadata": {}, + "source": [ + "## Step 6: Configure and Run DREAM\n", + "\n", + "We now switch from the local minimizer to the Bayesian DREAM sampler.\n", + "\n", + "The settings below are intentionally small so the tutorial runs\n", + "quickly. For production analysis you would usually increase the number\n", + "of steps (`steps`) and often the burn-in (`burn`) as well. When\n", + "needed, the DREAM API also lets you tune how chains are initialized\n", + "through the `init` setting. Other sampler settings such as `thin` and\n", + "`pop` can be adjusted as well. The current EasyDiffraction defaults\n", + "use `steps=3000`, `init='lhs'`, and `parallel=0`, which tells\n", + "BUMPS-DREAM to use all available CPUs for population evaluations.\n", + "\n", + "The `burn` setting is auto-resolved when left unset. Here we override\n", + "`steps` with a smaller value to keep the tutorial fast, and the\n", + "effective burn-in is recomputed automatically." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "37", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.fitting.show_minimizer_types()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "38", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.fitting.minimizer_type = 'bumps (dream)'" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "39", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.fitting.minimizer.steps = 100 # lower than the default 3000\n", + "project.analysis.fitting.minimizer.burn = 20 # lower than the default 600" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "40", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.fit()" + ] + }, + { + "cell_type": "markdown", + "id": "41", + "metadata": {}, + "source": [ + "## Step 7: Inspect Bayesian Results\n", + "\n", + "The fit-results display now includes sampler settings, convergence\n", + "diagnostics, committed parameter values, and posterior summary\n", + "statistics." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "42", + "metadata": {}, + "outputs": [], + "source": [ + "project.display.fit.results()" + ] + }, + { + "cell_type": "markdown", + "id": "43", + "metadata": {}, + "source": [ + "The correlation and posterior-pair plots are complementary:\n", + "\n", + "- `plot_param_correlations` summarizes pairwise structure in a compact\n", + " matrix.\n", + "- `plot_posterior_pairs` shows marginal densities on the diagonal and\n", + " posterior contours off-diagonal. In this tutorial its title also\n", + " reminds you that the display region follows the `±1.5 × uncertainty`\n", + " bounds defined above, while numeric subplot ranges are omitted to\n", + " keep the grid readable." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "44", + "metadata": {}, + "outputs": [], + "source": [ + "project.display.fit.correlations()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "45", + "metadata": {}, + "outputs": [], + "source": [ + "project.display.posterior.pairs()" + ] + }, + { + "cell_type": "markdown", + "id": "46", + "metadata": {}, + "source": [ + "The one-dimensional posterior distributions below make it easier to\n", + "inspect individual parameters in isolation, including asymmetry or\n", + "multimodality." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "47", + "metadata": {}, + "outputs": [], + "source": [ + "project.display.posterior.distribution()" + ] + }, + { + "cell_type": "markdown", + "id": "48", + "metadata": {}, + "source": [ + "Finally, the posterior predictive plot propagates the sampled\n", + "parameter uncertainty into the calculated single-crystal reflection\n", + "intensities." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "49", + "metadata": {}, + "outputs": [], + "source": [ + "project.display.posterior.predictive(expt_name='heidi')" + ] + } + ], + "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-22.py b/docs/docs/tutorials/ed-22.py new file mode 100644 index 000000000..cbc08f74e --- /dev/null +++ b/docs/docs/tutorials/ed-22.py @@ -0,0 +1,269 @@ +# %% [markdown] +# # Bayesian Analysis: Tb2TiO7, HEiDi +# +# This tutorial demonstrates a practical two-stage workflow for single-crystal +# diffraction analysis with EasyDiffraction. +# +# In the first stage, we run a fast local refinement to obtain a sensible +# point estimate and parameter uncertainties. In the second stage, we use +# these refined values to define fit bounds and then sample the posterior +# distribution with BUMPS-DREAM. +# +# The example uses constant-wavelength neutron single-crystal diffraction data +# for Tb2TiO7 measured on HEiDi at FRM II. +# +# The goal is not only to obtain a good fit, but also to answer Bayesian +# questions such as: +# +# - Which parameter values are most probable? +# - How broad are the credible intervals? +# - Which parameters are strongly correlated? +# - How much uncertainty propagates into the calculated reflection +# intensities? + +# %% [markdown] +# ## Import Library + +# %% +import easydiffraction as ed + +# %% [markdown] +# ## Step 1: Create a Project Container +# +# The project object keeps structures, experiments, fit settings, and +# plotting utilities together in a single place. We will build the full +# workflow inside this object. + +# %% +project = ed.Project() + +# %% [markdown] +# ## Step 2: Build the Structural Model +# +# For this example we start from a CIF file describing the Tb2TiO7 +# pyrochlore structure. Loading the structure from CIF is convenient +# because it preserves a realistic starting +# model without rebuilding the full structure by hand. + +# %% +structure_path = ed.download_data(id=20, destination='data') + +# %% +project.structures.add_from_cif_path(structure_path) + +# %% +structure = project.structures['tbti'] + +# %% [markdown] +# ## Step 3: Define the Diffraction Experiment +# +# Next we download the measured reflection data, create a neutron +# single-crystal experiment, and configure the crystal link, +# wavelength, and extinction model. + +# %% +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'] + +# %% [markdown] +# Link the crystal structure to the experiment and set its scale factor. + +# %% +experiment.linked_crystal.id = 'tbti' +experiment.linked_crystal.scale = 1.0 + +# %% [markdown] +# Set the instrument wavelength and starting extinction parameters. +# These values provide the initial experiment description for the local +# refinement. + +# %% +experiment.instrument.setup_wavelength = 0.793 + +# %% +experiment.extinction.mosaicity = 35000 +experiment.extinction.radius = 10 + +# %% [markdown] +# ## Step 4: Run an Initial Local Refinement +# +# Before Bayesian sampling, it is useful to run a deterministic fit. This +# gives us: +# +# - a good point estimate near the best-fit region, +# - uncertainties from the local optimizer, +# - a quick check that the model and experiment are configured +# sensibly. +# +# In this tutorial we refine a small set of structural and extinction +# parameters while keeping occupancies fixed. + +# %% +structure.atom_sites['O1'].fract_x.free = True + +structure.atom_sites['Ti'].occupancy.free = False +structure.atom_sites['O1'].occupancy.free = False +structure.atom_sites['O2'].occupancy.free = False + +structure.atom_sites['Tb'].adp_iso.free = True +structure.atom_sites['Ti'].adp_iso.free = True +structure.atom_sites['O1'].adp_iso.free = True +structure.atom_sites['O2'].adp_iso.free = True + +# %% +experiment.linked_crystal.scale.free = True +experiment.extinction.radius.free = True + +# %% [markdown] +# We keep using the default LMFIT Levenberg-Marquardt minimizer as a fast local +# optimizer. Its main purpose here is to provide a stable starting point +# and uncertainty estimates for the Bayesian run. + +# %% +project.analysis.fitting.show_minimizer_types() + +# %% +project.analysis.fit() + +# %% [markdown] +# The fit-results display summarizes the locally refined values and their +# estimated uncertainties. + +# %% +project.display.fit.results() + +# %% [markdown] +# The correlation plot shows how strongly the refined parameters move +# together in the local refinement. The measured-vs-calculated plot shows +# how well the refined crystal model reproduces the measured reflection +# intensities. + +# %% +project.display.fit.correlations() + +# %% +project.display.pattern(expt_name='heidi') + +# %% [markdown] +# ## Step 5: Prepare for Bayesian Sampling +# +# DREAM requires finite bounds for the free parameters. Instead of +# setting them manually, we derive them from the uncertainties estimated +# in the local refinement. +# +# The helper method `set_fit_bounds_from_uncertainty` centers the bounds +# on the current parameter value and expands them by a chosen multiple of +# the reported uncertainty. +# +# The default `multiplier` is 4. In this single-crystal tutorial we use +# a tighter value of `1.5` to keep the sampling window closer to the +# locally refined solution. +# +# Show unset fit bounds before setting them from the local refinement +# uncertainties. + +# %% +project.display.parameters.free() + +# %% [markdown] +# Set fit bounds for all free parameters using `multiplier=1.5`. In this +# tutorial that means the posterior pair plot will later refer to a +# `±1.5 × uncertainty` region in its title. To widen the sampling window, +# increase the multiplier explicitly. + +# %% +for param in project.free_parameters: + param.set_fit_bounds_from_uncertainty(multiplier=1.5) + +# %% [markdown] +# Displaying the free parameters again is a convenient way to confirm +# that the fit bounds have been assigned as expected before launching the +# sampler. + +# %% +project.display.parameters.free() + +# %% [markdown] +# ## Step 6: Configure and Run DREAM +# +# We now switch from the local minimizer to the Bayesian DREAM sampler. +# +# The settings below are intentionally small so the tutorial runs +# quickly. For production analysis you would usually increase the number +# of steps (`steps`) and often the burn-in (`burn`) as well. When +# needed, the DREAM API also lets you tune how chains are initialized +# through the `init` setting. Other sampler settings such as `thin` and +# `pop` can be adjusted as well. The current EasyDiffraction defaults +# use `steps=3000`, `init='lhs'`, and `parallel=0`, which tells +# BUMPS-DREAM to use all available CPUs for population evaluations. +# +# The `burn` setting is auto-resolved when left unset. Here we override +# `steps` with a smaller value to keep the tutorial fast, and the +# effective burn-in is recomputed automatically. + +# %% +project.analysis.fitting.show_minimizer_types() + +# %% +project.analysis.fitting.minimizer_type = 'bumps (dream)' + +# %% +project.analysis.fitting.minimizer.steps = 100 # lower than the default 3000 +project.analysis.fitting.minimizer.burn = 20 # lower than the default 600 + +# %% +project.analysis.fit() + +# %% [markdown] +# ## Step 7: Inspect Bayesian Results +# +# The fit-results display now includes sampler settings, convergence +# diagnostics, committed parameter values, and posterior summary +# statistics. + +# %% +project.display.fit.results() + +# %% [markdown] +# The correlation and posterior-pair plots are complementary: +# +# - `plot_param_correlations` summarizes pairwise structure in a compact +# matrix. +# - `plot_posterior_pairs` shows marginal densities on the diagonal and +# posterior contours off-diagonal. In this tutorial its title also +# reminds you that the display region follows the `±1.5 × uncertainty` +# bounds defined above, while numeric subplot ranges are omitted to +# keep the grid readable. + +# %% +project.display.fit.correlations() + +# %% +project.display.posterior.pairs() + +# %% [markdown] +# The one-dimensional posterior distributions below make it easier to +# inspect individual parameters in isolation, including asymmetry or +# multimodality. + +# %% +project.display.posterior.distribution() + +# %% [markdown] +# Finally, the posterior predictive plot propagates the sampled +# parameter uncertainty into the calculated single-crystal reflection +# intensities. + +# %% +project.display.posterior.predictive(expt_name='heidi') diff --git a/docs/docs/tutorials/ed-23.ipynb b/docs/docs/tutorials/ed-23.ipynb new file mode 100644 index 000000000..11a28c791 --- /dev/null +++ b/docs/docs/tutorials/ed-23.ipynb @@ -0,0 +1,236 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "0", + "metadata": { + "tags": [ + "hide-in-docs" + ] + }, + "outputs": [], + "source": [ + "# Check whether easydiffraction is installed; install it if needed.\n", + "# Required for remote environments such as Google Colab.\n", + "import importlib.util\n", + "\n", + "if importlib.util.find_spec('easydiffraction') is None:\n", + " %pip install easydiffraction" + ] + }, + { + "cell_type": "markdown", + "id": "1", + "metadata": {}, + "source": [ + "# Structure Refinement: Co2SiO4, D20 (T-scan, resumed)\n", + "\n", + "This example loads a previously saved Co2SiO4 project after a\n", + "sequential refinement was stopped before all scan files were\n", + "processed. If `analysis/results.csv` already contains completed rows,\n", + "running `project.analysis.fit()` again resumes from the remaining\n", + "datasets and appends the missing results." + ] + }, + { + "cell_type": "markdown", + "id": "2", + "metadata": {}, + "source": [ + "## Import Library" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3", + "metadata": {}, + "outputs": [], + "source": [ + "import easydiffraction as ed" + ] + }, + { + "cell_type": "markdown", + "id": "4", + "metadata": {}, + "source": [ + "## Download Saved Project\n", + "\n", + "The returned path points directly to the saved project directory with\n", + "a partially completed sequential fit, including\n", + "`analysis/results.csv`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5", + "metadata": {}, + "outputs": [], + "source": [ + "project_dir = ed.download_data(id=37, destination='projects')" + ] + }, + { + "cell_type": "markdown", + "id": "6", + "metadata": {}, + "source": [ + "## Load Saved Project" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7", + "metadata": {}, + "outputs": [], + "source": [ + "project = ed.Project.load(project_dir)" + ] + }, + { + "cell_type": "markdown", + "id": "8", + "metadata": {}, + "source": [ + "## Resume Sequential Analysis\n", + "\n", + "This project already stores the template experiment, sequential-fit\n", + "settings, and the partial `analysis/results.csv` from the previous\n", + "run. Running the fit again skips datasets already present in the CSV\n", + "and continues from the remaining files." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.fit()" + ] + }, + { + "cell_type": "markdown", + "id": "10", + "metadata": {}, + "source": [ + "## Replay Fitted Datasets\n", + "\n", + "Apply fitted parameters from the first CSV row and plot the result." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "11", + "metadata": {}, + "outputs": [], + "source": [ + "project.apply_params_from_csv(row_index=0)\n", + "project.display.pattern(expt_name='d20')" + ] + }, + { + "cell_type": "markdown", + "id": "12", + "metadata": {}, + "source": [ + "\n", + "Apply fitted parameters from the last CSV row and plot the result." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "13", + "metadata": {}, + "outputs": [], + "source": [ + "project.apply_params_from_csv(row_index=-1)\n", + "project.display.pattern(expt_name='d20')" + ] + }, + { + "cell_type": "markdown", + "id": "14", + "metadata": {}, + "source": [ + "## Plot Parameter Evolution\n", + "\n", + "Use the same persisted diffrn path stored in `analysis/results.csv`\n", + "for the x-axis." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "15", + "metadata": {}, + "outputs": [], + "source": [ + "temperature = 'diffrn.ambient_temperature'" + ] + }, + { + "cell_type": "markdown", + "id": "16", + "metadata": {}, + "source": [ + "Plot fit quality metrics vs. temperature." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "17", + "metadata": {}, + "outputs": [], + "source": [ + "project.display.fit.series(\n", + " project.analysis.fit_result.success,\n", + " versus=temperature,\n", + ")\n", + "project.display.fit.series(\n", + " project.analysis.fit_result.reduced_chi_square,\n", + " versus=temperature,\n", + ")\n", + "project.display.fit.series(\n", + " project.analysis.fit_result.iterations,\n", + " versus=temperature,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "18", + "metadata": {}, + "source": [ + "Omitting `param` plots every fitted parameter one after another." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "19", + "metadata": {}, + "outputs": [], + "source": [ + "project.display.fit.series(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-23.py b/docs/docs/tutorials/ed-23.py new file mode 100644 index 000000000..a4dbc8ed7 --- /dev/null +++ b/docs/docs/tutorials/ed-23.py @@ -0,0 +1,90 @@ +# %% [markdown] +# # Structure Refinement: Co2SiO4, D20 (T-scan, resumed) +# +# This example loads a previously saved Co2SiO4 project after a +# sequential refinement was stopped before all scan files were +# processed. If `analysis/results.csv` already contains completed rows, +# running `project.analysis.fit()` again resumes from the remaining +# datasets and appends the missing results. + +# %% [markdown] +# ## Import Library + +# %% +import easydiffraction as ed + +# %% [markdown] +# ## Download Saved Project +# +# The returned path points directly to the saved project directory with +# a partially completed sequential fit, including +# `analysis/results.csv`. + +# %% +project_dir = ed.download_data(id=37, destination='projects') + +# %% [markdown] +# ## Load Saved Project + +# %% +project = ed.Project.load(project_dir) + +# %% [markdown] +# ## Resume Sequential Analysis +# +# This project already stores the template experiment, sequential-fit +# settings, and the partial `analysis/results.csv` from the previous +# run. Running the fit again skips datasets already present in the CSV +# and continues from the remaining files. + +# %% +project.analysis.fit() + +# %% [markdown] +# ## Replay Fitted Datasets +# +# Apply fitted parameters from the first CSV row and plot the result. + +# %% +project.apply_params_from_csv(row_index=0) +project.display.pattern(expt_name='d20') + +# %% [markdown] +# +# Apply fitted parameters from the last CSV row and plot the result. + +# %% +project.apply_params_from_csv(row_index=-1) +project.display.pattern(expt_name='d20') + +# %% [markdown] +# ## Plot Parameter Evolution +# +# Use the same persisted diffrn path stored in `analysis/results.csv` +# for the x-axis. + +# %% +temperature = 'diffrn.ambient_temperature' + +# %% [markdown] +# Plot fit quality metrics vs. temperature. + +# %% +project.display.fit.series( + project.analysis.fit_result.success, + versus=temperature, +) +project.display.fit.series( + project.analysis.fit_result.reduced_chi_square, + versus=temperature, +) +project.display.fit.series( + project.analysis.fit_result.iterations, + versus=temperature, +) + +# %% [markdown] +# Omitting `param` plots every fitted parameter one after another. + +# %% +project.display.fit.series(versus=temperature) diff --git a/docs/docs/tutorials/ed-24.ipynb b/docs/docs/tutorials/ed-24.ipynb new file mode 100644 index 000000000..745d962f8 --- /dev/null +++ b/docs/docs/tutorials/ed-24.ipynb @@ -0,0 +1,223 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "0", + "metadata": { + "tags": [ + "hide-in-docs" + ] + }, + "outputs": [], + "source": [ + "# Check whether easydiffraction is installed; install it if needed.\n", + "# Required for remote environments such as Google Colab.\n", + "import importlib.util\n", + "\n", + "if importlib.util.find_spec('easydiffraction') is None:\n", + " %pip install easydiffraction" + ] + }, + { + "cell_type": "markdown", + "id": "1", + "metadata": {}, + "source": [ + "# Load Saved Bayesian Project: LBCO, HRPT\n", + "\n", + "This tutorial shows how to reopen the Bayesian project created in\n", + "`ed-21.py` and inspect the saved fit results without rerunning DREAM.\n", + "\n", + "The project already contains posterior samples together with cached\n", + "posterior density, pair, and predictive data, so the plots below are\n", + "restored directly from disk." + ] + }, + { + "cell_type": "markdown", + "id": "2", + "metadata": {}, + "source": [ + "## Import Library" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3", + "metadata": {}, + "outputs": [], + "source": [ + "import easydiffraction as ed" + ] + }, + { + "cell_type": "markdown", + "id": "4", + "metadata": {}, + "source": [ + "## Download Saved Project\n", + "\n", + "The returned path points directly to the saved project directory with\n", + "the completed Bayesian fit and persisted posterior samples and plot\n", + "caches." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5", + "metadata": {}, + "outputs": [], + "source": [ + "project_dir = ed.download_data(id=35, destination='projects')" + ] + }, + { + "cell_type": "markdown", + "id": "6", + "metadata": {}, + "source": [ + "## Load the Saved Bayesian Project\n", + "\n", + "Loading restores the persisted fit state, posterior samples, and plot\n", + "caches. No new fit is launched in this tutorial." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7", + "metadata": {}, + "outputs": [], + "source": [ + "project = ed.Project.load(project_dir)" + ] + }, + { + "cell_type": "markdown", + "id": "8", + "metadata": {}, + "source": [ + "## Review the Saved Fit Summary\n", + "\n", + "The fit summary reports the committed point estimate, sampler\n", + "settings, convergence diagnostics, and posterior parameter summaries\n", + "from the saved Bayesian run." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9", + "metadata": {}, + "outputs": [], + "source": [ + "project.display.fit.results()" + ] + }, + { + "cell_type": "markdown", + "id": "10", + "metadata": {}, + "source": [ + "## Show Correlations\n", + "\n", + "The correlation matrix is restored from the saved project state." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "11", + "metadata": {}, + "outputs": [], + "source": [ + "project.display.fit.correlations()" + ] + }, + { + "cell_type": "markdown", + "id": "12", + "metadata": {}, + "source": [ + "## Inspect Posterior Densities and Pair Structure\n", + "\n", + "The pair plot and one-dimensional posterior distributions now load\n", + "from the persisted caches generated when the Bayesian fit was saved." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "13", + "metadata": {}, + "outputs": [], + "source": [ + "project.display.posterior.pairs()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "14", + "metadata": {}, + "outputs": [], + "source": [ + "project.display.posterior.distribution()" + ] + }, + { + "cell_type": "markdown", + "id": "15", + "metadata": {}, + "source": [ + "## Plot Posterior Predictive Checks\n", + "\n", + "The posterior predictive view reuses the cached predictive summary\n", + "stored in the project rather than recalculating it on first display.\n", + "It overlays the 95% credible interval propagated from the posterior\n", + "samples." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "16", + "metadata": {}, + "outputs": [], + "source": [ + "project.display.posterior.predictive(expt_name='hrpt')" + ] + }, + { + "cell_type": "markdown", + "id": "17", + "metadata": {}, + "source": [ + "A zoomed view is useful for checking the propagated uncertainty in a\n", + "narrow region of the diffraction pattern." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "18", + "metadata": {}, + "outputs": [], + "source": [ + "project.display.posterior.predictive(expt_name='hrpt', x_min=92, x_max=93)" + ] + } + ], + "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-24.py b/docs/docs/tutorials/ed-24.py new file mode 100644 index 000000000..9e5d40f87 --- /dev/null +++ b/docs/docs/tutorials/ed-24.py @@ -0,0 +1,82 @@ +# %% [markdown] +# # Load Saved Bayesian Project: LBCO, HRPT +# +# This tutorial shows how to reopen the Bayesian project created in +# `ed-21.py` and inspect the saved fit results without rerunning DREAM. +# +# The project already contains posterior samples together with cached +# posterior density, pair, and predictive data, so the plots below are +# restored directly from disk. + +# %% [markdown] +# ## Import Library + +# %% +import easydiffraction as ed + +# %% [markdown] +# ## Download Saved Project +# +# The returned path points directly to the saved project directory with +# the completed Bayesian fit and persisted posterior samples and plot +# caches. + +# %% +project_dir = ed.download_data(id=35, destination='projects') + +# %% [markdown] +# ## Load the Saved Bayesian Project +# +# Loading restores the persisted fit state, posterior samples, and plot +# caches. No new fit is launched in this tutorial. + +# %% +project = ed.Project.load(project_dir) + +# %% [markdown] +# ## Review the Saved Fit Summary +# +# The fit summary reports the committed point estimate, sampler +# settings, convergence diagnostics, and posterior parameter summaries +# from the saved Bayesian run. + +# %% +project.display.fit.results() + +# %% [markdown] +# ## Show Correlations +# +# The correlation matrix is restored from the saved project state. + +# %% +project.display.fit.correlations() + +# %% [markdown] +# ## Inspect Posterior Densities and Pair Structure +# +# The pair plot and one-dimensional posterior distributions now load +# from the persisted caches generated when the Bayesian fit was saved. + +# %% +project.display.posterior.pairs() + +# %% +project.display.posterior.distribution() + +# %% [markdown] +# ## Plot Posterior Predictive Checks +# +# The posterior predictive view reuses the cached predictive summary +# stored in the project rather than recalculating it on first display. +# It overlays the 95% credible interval propagated from the posterior +# samples. + +# %% +project.display.posterior.predictive(expt_name='hrpt') + +# %% [markdown] +# A zoomed view is useful for checking the propagated uncertainty in a +# narrow region of the diffraction pattern. + +# %% +project.display.posterior.predictive(expt_name='hrpt', x_min=92, x_max=93) diff --git a/docs/docs/tutorials/ed-3.ipynb b/docs/docs/tutorials/ed-3.ipynb index f99298a2f..357607f26 100644 --- a/docs/docs/tutorials/ed-3.ipynb +++ b/docs/docs/tutorials/ed-3.ipynb @@ -175,7 +175,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.show_supported_engines()" + "project.rendering.show_chart_engines()" ] }, { @@ -193,7 +193,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.show_config()" + "project.rendering.show_config()" ] }, { @@ -492,7 +492,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas(expt_name='hrpt')" + "project.display.pattern(expt_name='hrpt', include='measured')" ] }, { @@ -774,7 +774,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_calc(expt_name='hrpt')" + "project.display.pattern(expt_name='hrpt', include='calculated')" ] }, { @@ -792,7 +792,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='hrpt')" + "project.display.pattern(expt_name='hrpt')" ] }, { @@ -802,7 +802,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', x_min=38, x_max=41)" + "project.display.pattern(expt_name='hrpt', x_min=38, x_max=41)" ] }, { @@ -822,7 +822,7 @@ "metadata": {}, "outputs": [], "source": [ - "# project.analysis.display.all_params()" + "project.display.parameters.all()" ] }, { @@ -840,7 +840,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.display.fittable_params()" + "project.display.parameters.fittable()" ] }, { @@ -858,7 +858,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.display.free_params()" + "project.display.parameters.free()" ] }, { @@ -876,7 +876,7 @@ "metadata": {}, "outputs": [], "source": [ - "# project.analysis.display.how_to_access_parameters()" + "project.display.parameters.access()" ] }, { @@ -896,7 +896,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.fit.show_modes()" + "project.analysis.show_fitting_mode_types()" ] }, { @@ -914,7 +914,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.fit.mode = 'single'" + "project.analysis.fitting_mode_type = 'single'" ] }, { @@ -934,7 +934,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.fit.show_minimizer_types()" + "project.analysis.fitting.show_minimizer_types()" ] }, { @@ -952,7 +952,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.fit.minimizer_type = 'lmfit'" + "project.analysis.fitting.minimizer_type = 'lmfit'" ] }, { @@ -1014,7 +1014,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.display.free_params()" + "project.display.parameters.free()" ] }, { @@ -1033,7 +1033,7 @@ "outputs": [], "source": [ "project.analysis.fit()\n", - "project.analysis.display.fit_results()" + "project.display.fit.results()" ] }, { @@ -1051,7 +1051,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='hrpt')" + "project.display.pattern(expt_name='hrpt')" ] }, { @@ -1061,31 +1061,13 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', x_min=38, x_max=41)" + "project.display.pattern(expt_name='hrpt', x_min=38, x_max=41)" ] }, { "cell_type": "markdown", "id": "103", "metadata": {}, - "source": [ - "#### Save Project State" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "104", - "metadata": {}, - "outputs": [], - "source": [ - "project.save_as(dir_path='lbco_hrpt', temporary=True)" - ] - }, - { - "cell_type": "markdown", - "id": "105", - "metadata": {}, "source": [ "### Perform Fit 2/5\n", "\n", @@ -1095,7 +1077,7 @@ { "cell_type": "code", "execution_count": null, - "id": "106", + "id": "104", "metadata": {}, "outputs": [], "source": [ @@ -1107,7 +1089,7 @@ }, { "cell_type": "markdown", - "id": "107", + "id": "105", "metadata": {}, "source": [ "Show free parameters after selection." @@ -1116,16 +1098,16 @@ { "cell_type": "code", "execution_count": null, - "id": "108", + "id": "106", "metadata": {}, "outputs": [], "source": [ - "project.analysis.display.free_params()" + "project.display.parameters.free()" ] }, { "cell_type": "markdown", - "id": "109", + "id": "107", "metadata": {}, "source": [ "#### Run Fitting" @@ -1134,17 +1116,17 @@ { "cell_type": "code", "execution_count": null, - "id": "110", + "id": "108", "metadata": {}, "outputs": [], "source": [ "project.analysis.fit()\n", - "project.analysis.display.fit_results()" + "project.display.fit.results()" ] }, { "cell_type": "markdown", - "id": "111", + "id": "109", "metadata": {}, "source": [ "#### Plot Measured vs Calculated" @@ -1153,26 +1135,26 @@ { "cell_type": "code", "execution_count": null, - "id": "112", + "id": "110", "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='hrpt')" + "project.display.pattern(expt_name='hrpt')" ] }, { "cell_type": "code", "execution_count": null, - "id": "113", + "id": "111", "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', x_min=38, x_max=41)" + "project.display.pattern(expt_name='hrpt', x_min=38, x_max=41)" ] }, { "cell_type": "markdown", - "id": "114", + "id": "112", "metadata": {}, "source": [ "#### Save Project State" @@ -1181,16 +1163,16 @@ { "cell_type": "code", "execution_count": null, - "id": "115", + "id": "113", "metadata": {}, "outputs": [], "source": [ - "project.save_as(dir_path='lbco_hrpt', temporary=True)" + "project.save()" ] }, { "cell_type": "markdown", - "id": "116", + "id": "114", "metadata": {}, "source": [ "### Perform Fit 3/5\n", @@ -1201,7 +1183,7 @@ { "cell_type": "code", "execution_count": null, - "id": "117", + "id": "115", "metadata": {}, "outputs": [], "source": [ @@ -1213,7 +1195,7 @@ }, { "cell_type": "markdown", - "id": "118", + "id": "116", "metadata": {}, "source": [ "Show free parameters after selection." @@ -1222,16 +1204,16 @@ { "cell_type": "code", "execution_count": null, - "id": "119", + "id": "117", "metadata": {}, "outputs": [], "source": [ - "project.analysis.display.free_params()" + "project.display.parameters.free()" ] }, { "cell_type": "markdown", - "id": "120", + "id": "118", "metadata": {}, "source": [ "#### Run Fitting" @@ -1240,17 +1222,17 @@ { "cell_type": "code", "execution_count": null, - "id": "121", + "id": "119", "metadata": {}, "outputs": [], "source": [ "project.analysis.fit()\n", - "project.analysis.display.fit_results()" + "project.display.fit.results()" ] }, { "cell_type": "markdown", - "id": "122", + "id": "120", "metadata": {}, "source": [ "#### Plot Measured vs Calculated" @@ -1259,44 +1241,26 @@ { "cell_type": "code", "execution_count": null, - "id": "123", - "metadata": {}, - "outputs": [], - "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='hrpt')" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "124", + "id": "121", "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', x_min=38, x_max=41)" - ] - }, - { - "cell_type": "markdown", - "id": "125", - "metadata": {}, - "source": [ - "#### Save Project State" + "project.display.pattern(expt_name='hrpt')" ] }, { "cell_type": "code", "execution_count": null, - "id": "126", + "id": "122", "metadata": {}, "outputs": [], "source": [ - "project.save_as(dir_path='lbco_hrpt', temporary=True)" + "project.display.pattern(expt_name='hrpt', x_min=38, x_max=41)" ] }, { "cell_type": "markdown", - "id": "127", + "id": "123", "metadata": {}, "source": [ "### Perform Fit 4/5\n", @@ -1309,7 +1273,7 @@ { "cell_type": "code", "execution_count": null, - "id": "128", + "id": "124", "metadata": {}, "outputs": [], "source": [ @@ -1325,7 +1289,7 @@ }, { "cell_type": "markdown", - "id": "129", + "id": "125", "metadata": {}, "source": [ "Set constraints." @@ -1334,7 +1298,7 @@ { "cell_type": "code", "execution_count": null, - "id": "130", + "id": "126", "metadata": {}, "outputs": [], "source": [ @@ -1343,7 +1307,7 @@ }, { "cell_type": "markdown", - "id": "131", + "id": "127", "metadata": {}, "source": [ "Show defined constraints." @@ -1352,16 +1316,16 @@ { "cell_type": "code", "execution_count": null, - "id": "132", + "id": "128", "metadata": {}, "outputs": [], "source": [ - "project.analysis.display.constraints()" + "project.analysis.constraints.show()" ] }, { "cell_type": "markdown", - "id": "133", + "id": "129", "metadata": {}, "source": [ "Show free parameters." @@ -1370,16 +1334,16 @@ { "cell_type": "code", "execution_count": null, - "id": "134", + "id": "130", "metadata": {}, "outputs": [], "source": [ - "project.analysis.display.free_params()" + "project.display.parameters.free()" ] }, { "cell_type": "markdown", - "id": "135", + "id": "131", "metadata": {}, "source": [ "#### Run Fitting" @@ -1388,17 +1352,17 @@ { "cell_type": "code", "execution_count": null, - "id": "136", + "id": "132", "metadata": {}, "outputs": [], "source": [ "project.analysis.fit()\n", - "project.analysis.display.fit_results()" + "project.display.fit.results()" ] }, { "cell_type": "markdown", - "id": "137", + "id": "133", "metadata": {}, "source": [ "#### Plot Measured vs Calculated" @@ -1407,44 +1371,26 @@ { "cell_type": "code", "execution_count": null, - "id": "138", - "metadata": {}, - "outputs": [], - "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='hrpt')" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "139", + "id": "134", "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', x_min=38, x_max=41)" - ] - }, - { - "cell_type": "markdown", - "id": "140", - "metadata": {}, - "source": [ - "#### Save Project State" + "project.display.pattern(expt_name='hrpt')" ] }, { "cell_type": "code", "execution_count": null, - "id": "141", + "id": "135", "metadata": {}, "outputs": [], "source": [ - "project.save_as(dir_path='lbco_hrpt', temporary=True)" + "project.display.pattern(expt_name='hrpt', x_min=38, x_max=41)" ] }, { "cell_type": "markdown", - "id": "142", + "id": "136", "metadata": {}, "source": [ "### Perform Fit 5/5\n", @@ -1457,7 +1403,7 @@ { "cell_type": "code", "execution_count": null, - "id": "143", + "id": "137", "metadata": {}, "outputs": [], "source": [ @@ -1473,7 +1419,7 @@ }, { "cell_type": "markdown", - "id": "144", + "id": "138", "metadata": {}, "source": [ "Set more constraints." @@ -1482,7 +1428,7 @@ { "cell_type": "code", "execution_count": null, - "id": "145", + "id": "139", "metadata": {}, "outputs": [], "source": [ @@ -1493,7 +1439,7 @@ }, { "cell_type": "markdown", - "id": "146", + "id": "140", "metadata": {}, "source": [ "Show defined constraints." @@ -1502,18 +1448,18 @@ { "cell_type": "code", "execution_count": null, - "id": "147", + "id": "141", "metadata": { "lines_to_next_cell": 2 }, "outputs": [], "source": [ - "project.analysis.display.constraints()" + "project.analysis.constraints.show()" ] }, { "cell_type": "markdown", - "id": "148", + "id": "142", "metadata": {}, "source": [ "Set structure parameters to be refined." @@ -1522,7 +1468,7 @@ { "cell_type": "code", "execution_count": null, - "id": "149", + "id": "143", "metadata": {}, "outputs": [], "source": [ @@ -1531,7 +1477,7 @@ }, { "cell_type": "markdown", - "id": "150", + "id": "144", "metadata": {}, "source": [ "Show free parameters after selection." @@ -1540,16 +1486,16 @@ { "cell_type": "code", "execution_count": null, - "id": "151", + "id": "145", "metadata": {}, "outputs": [], "source": [ - "project.analysis.display.free_params()" + "project.display.parameters.free()" ] }, { "cell_type": "markdown", - "id": "152", + "id": "146", "metadata": {}, "source": [ "#### Run Fitting" @@ -1558,18 +1504,18 @@ { "cell_type": "code", "execution_count": null, - "id": "153", + "id": "147", "metadata": {}, "outputs": [], "source": [ "project.analysis.fit()\n", - "project.analysis.display.fit_results()\n", - "project.display.plotter.plot_param_correlations()" + "project.display.fit.results()\n", + "project.display.fit.correlations()" ] }, { "cell_type": "markdown", - "id": "154", + "id": "148", "metadata": {}, "source": [ "#### Plot Measured vs Calculated" @@ -1578,26 +1524,26 @@ { "cell_type": "code", "execution_count": null, - "id": "155", + "id": "149", "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='hrpt')" + "project.display.pattern(expt_name='hrpt')" ] }, { "cell_type": "code", "execution_count": null, - "id": "156", + "id": "150", "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', x_min=38, x_max=41)" + "project.display.pattern(expt_name='hrpt', x_min=38, x_max=41)" ] }, { "cell_type": "markdown", - "id": "157", + "id": "151", "metadata": {}, "source": [ "#### Save Project State" @@ -1606,16 +1552,16 @@ { "cell_type": "code", "execution_count": null, - "id": "158", + "id": "152", "metadata": {}, "outputs": [], "source": [ - "project.save_as(dir_path='lbco_hrpt', temporary=True)" + "project.save()" ] }, { "cell_type": "markdown", - "id": "159", + "id": "153", "metadata": {}, "source": [ "## Step 5: Summary\n", @@ -1625,7 +1571,7 @@ }, { "cell_type": "markdown", - "id": "160", + "id": "154", "metadata": {}, "source": [ "#### Show Project Summary" @@ -1634,7 +1580,7 @@ { "cell_type": "code", "execution_count": null, - "id": "161", + "id": "155", "metadata": {}, "outputs": [], "source": [ diff --git a/docs/docs/tutorials/ed-3.py b/docs/docs/tutorials/ed-3.py index de1f74c63..7a194acd2 100644 --- a/docs/docs/tutorials/ed-3.py +++ b/docs/docs/tutorials/ed-3.py @@ -68,13 +68,13 @@ # Show supported plotting engines. # %% -project.display.plotter.show_supported_engines() +project.rendering.show_chart_engines() # %% [markdown] # Show current plotting configuration. # %% -project.display.plotter.show_config() +project.rendering.show_config() # %% [markdown] # ## Step 2: Define Structure @@ -219,7 +219,7 @@ # #### Show Measured Data # %% -project.display.plotter.plot_meas(expt_name='hrpt') +project.display.pattern(expt_name='hrpt', include='measured') # %% [markdown] # #### Set Instrument @@ -328,16 +328,16 @@ # #### Show Calculated Data # %% -project.display.plotter.plot_calc(expt_name='hrpt') +project.display.pattern(expt_name='hrpt', include='calculated') # %% [markdown] # #### Plot Measured vs Calculated # %% -project.display.plotter.plot_meas_vs_calc(expt_name='hrpt') +project.display.pattern(expt_name='hrpt') # %% -project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', x_min=38, x_max=41) +project.display.pattern(expt_name='hrpt', x_min=38, x_max=41) # %% [markdown] # #### Show Parameters @@ -345,25 +345,25 @@ # Show all parameters of the project. # %% -# project.analysis.display.all_params() +project.display.parameters.all() # %% [markdown] # Show all fittable parameters. # %% -project.analysis.display.fittable_params() +project.display.parameters.fittable() # %% [markdown] # Show only free parameters. # %% -project.analysis.display.free_params() +project.display.parameters.free() # %% [markdown] # Show how to access parameters in the code. # %% -# project.analysis.display.how_to_access_parameters() +project.display.parameters.access() # %% [markdown] # #### Set Fit Mode @@ -371,13 +371,13 @@ # Show supported fit modes. # %% -project.analysis.fit.show_modes() +project.analysis.show_fitting_mode_types() # %% [markdown] # Select desired fit mode. # %% -project.analysis.fit.mode = 'single' +project.analysis.fitting_mode_type = 'single' # %% [markdown] # #### Set Minimizer @@ -385,13 +385,13 @@ # Show supported fitting engines. # %% -project.analysis.fit.show_minimizer_types() +project.analysis.fitting.show_minimizer_types() # %% [markdown] # Select desired fitting engine. # %% -project.analysis.fit.minimizer_type = 'lmfit' +project.analysis.fitting.minimizer_type = 'lmfit' # %% [markdown] # ### Perform Fit 1/5 @@ -417,29 +417,23 @@ # Show free parameters after selection. # %% -project.analysis.display.free_params() +project.display.parameters.free() # %% [markdown] # #### Run Fitting # %% project.analysis.fit() -project.analysis.display.fit_results() +project.display.fit.results() # %% [markdown] # #### Plot Measured vs Calculated # %% -project.display.plotter.plot_meas_vs_calc(expt_name='hrpt') +project.display.pattern(expt_name='hrpt') # %% -project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', x_min=38, x_max=41) - -# %% [markdown] -# #### Save Project State - -# %% -project.save_as(dir_path='lbco_hrpt', temporary=True) +project.display.pattern(expt_name='hrpt', x_min=38, x_max=41) # %% [markdown] # ### Perform Fit 2/5 @@ -456,29 +450,29 @@ # Show free parameters after selection. # %% -project.analysis.display.free_params() +project.display.parameters.free() # %% [markdown] # #### Run Fitting # %% project.analysis.fit() -project.analysis.display.fit_results() +project.display.fit.results() # %% [markdown] # #### Plot Measured vs Calculated # %% -project.display.plotter.plot_meas_vs_calc(expt_name='hrpt') +project.display.pattern(expt_name='hrpt') # %% -project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', x_min=38, x_max=41) +project.display.pattern(expt_name='hrpt', x_min=38, x_max=41) # %% [markdown] # #### Save Project State # %% -project.save_as(dir_path='lbco_hrpt', temporary=True) +project.save() # %% [markdown] # ### Perform Fit 3/5 @@ -495,29 +489,23 @@ # Show free parameters after selection. # %% -project.analysis.display.free_params() +project.display.parameters.free() # %% [markdown] # #### Run Fitting # %% project.analysis.fit() -project.analysis.display.fit_results() +project.display.fit.results() # %% [markdown] # #### Plot Measured vs Calculated # %% -project.display.plotter.plot_meas_vs_calc(expt_name='hrpt') - -# %% -project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', x_min=38, x_max=41) - -# %% [markdown] -# #### Save Project State +project.display.pattern(expt_name='hrpt') # %% -project.save_as(dir_path='lbco_hrpt', temporary=True) +project.display.pattern(expt_name='hrpt', x_min=38, x_max=41) # %% [markdown] # ### Perform Fit 4/5 @@ -546,35 +534,29 @@ # Show defined constraints. # %% -project.analysis.display.constraints() +project.analysis.constraints.show() # %% [markdown] # Show free parameters. # %% -project.analysis.display.free_params() +project.display.parameters.free() # %% [markdown] # #### Run Fitting # %% project.analysis.fit() -project.analysis.display.fit_results() +project.display.fit.results() # %% [markdown] # #### Plot Measured vs Calculated # %% -project.display.plotter.plot_meas_vs_calc(expt_name='hrpt') +project.display.pattern(expt_name='hrpt') # %% -project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', x_min=38, x_max=41) - -# %% [markdown] -# #### Save Project State - -# %% -project.save_as(dir_path='lbco_hrpt', temporary=True) +project.display.pattern(expt_name='hrpt', x_min=38, x_max=41) # %% [markdown] # ### Perform Fit 5/5 @@ -605,7 +587,7 @@ # Show defined constraints. # %% -project.analysis.display.constraints() +project.analysis.constraints.show() # %% [markdown] @@ -618,30 +600,30 @@ # Show free parameters after selection. # %% -project.analysis.display.free_params() +project.display.parameters.free() # %% [markdown] # #### Run Fitting # %% project.analysis.fit() -project.analysis.display.fit_results() -project.display.plotter.plot_param_correlations() +project.display.fit.results() +project.display.fit.correlations() # %% [markdown] # #### Plot Measured vs Calculated # %% -project.display.plotter.plot_meas_vs_calc(expt_name='hrpt') +project.display.pattern(expt_name='hrpt') # %% -project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', x_min=38, x_max=41) +project.display.pattern(expt_name='hrpt', x_min=38, x_max=41) # %% [markdown] # #### Save Project State # %% -project.save_as(dir_path='lbco_hrpt', temporary=True) +project.save() # %% [markdown] # ## Step 5: Summary diff --git a/docs/docs/tutorials/ed-4.ipynb b/docs/docs/tutorials/ed-4.ipynb index 3a77ab8bb..ec70ad9e1 100644 --- a/docs/docs/tutorials/ed-4.ipynb +++ b/docs/docs/tutorials/ed-4.ipynb @@ -576,7 +576,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.fit.mode = 'joint'" + "project.analysis.fitting_mode_type = 'joint'" ] }, { @@ -594,7 +594,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.fit.minimizer_type = 'lmfit'" + "project.analysis.fitting.minimizer_type = 'lmfit'" ] }, { @@ -687,7 +687,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='npd', x_min=35.5, x_max=38.3)" + "project.display.pattern(expt_name='npd', x_min=35.5, x_max=38.3)" ] }, { @@ -697,7 +697,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='xrd', x_min=29.0, x_max=30.4)" + "project.display.pattern(expt_name='xrd', x_min=29.0, x_max=30.4)" ] } ], diff --git a/docs/docs/tutorials/ed-4.py b/docs/docs/tutorials/ed-4.py index 8a868277f..e0362a8c2 100644 --- a/docs/docs/tutorials/ed-4.py +++ b/docs/docs/tutorials/ed-4.py @@ -264,13 +264,13 @@ # #### Set Fit Mode # %% -project.analysis.fit.mode = 'joint' +project.analysis.fitting_mode_type = 'joint' # %% [markdown] # #### Set Minimizer # %% -project.analysis.fit.minimizer_type = 'lmfit' +project.analysis.fitting.minimizer_type = 'lmfit' # %% [markdown] # #### Set Fitting Parameters @@ -315,7 +315,7 @@ # #### Plot Measured vs Calculated # %% -project.display.plotter.plot_meas_vs_calc(expt_name='npd', x_min=35.5, x_max=38.3) +project.display.pattern(expt_name='npd', x_min=35.5, x_max=38.3) # %% -project.display.plotter.plot_meas_vs_calc(expt_name='xrd', x_min=29.0, x_max=30.4) +project.display.pattern(expt_name='xrd', x_min=29.0, x_max=30.4) diff --git a/docs/docs/tutorials/ed-5.ipynb b/docs/docs/tutorials/ed-5.ipynb index 02ef6045c..3fa6294b9 100644 --- a/docs/docs/tutorials/ed-5.ipynb +++ b/docs/docs/tutorials/ed-5.ipynb @@ -426,7 +426,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='d20')" + "project.display.pattern(expt_name='d20')" ] }, { @@ -436,7 +436,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='d20', x_min=41, x_max=54)" + "project.display.pattern(expt_name='d20', x_min=41, x_max=54)" ] }, { @@ -507,7 +507,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.display.free_params()" + "project.display.parameters.free()" ] }, { @@ -582,7 +582,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.display.fit_results()" + "project.display.fit.results()" ] }, { @@ -592,7 +592,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_param_correlations()" + "project.display.fit.correlations()" ] }, { @@ -610,7 +610,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='d20')" + "project.display.pattern(expt_name='d20')" ] }, { @@ -620,7 +620,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='d20', x_min=42, x_max=52)" + "project.display.pattern(expt_name='d20', x_min=42, x_max=52)" ] }, { diff --git a/docs/docs/tutorials/ed-5.py b/docs/docs/tutorials/ed-5.py index 72e54feb9..a82990a32 100644 --- a/docs/docs/tutorials/ed-5.py +++ b/docs/docs/tutorials/ed-5.py @@ -200,10 +200,10 @@ # #### Plot Measured vs Calculated # %% -project.display.plotter.plot_meas_vs_calc(expt_name='d20') +project.display.pattern(expt_name='d20') # %% -project.display.plotter.plot_meas_vs_calc(expt_name='d20', x_min=41, x_max=54) +project.display.pattern(expt_name='d20', x_min=41, x_max=54) # %% [markdown] # #### Set Free Parameters @@ -243,7 +243,7 @@ # Show free parameters after selection. # %% -project.analysis.display.free_params() +project.display.parameters.free() # %% [markdown] # #### Set Constraints @@ -274,19 +274,19 @@ project.analysis.fit() # %% -project.analysis.display.fit_results() +project.display.fit.results() # %% -project.display.plotter.plot_param_correlations() +project.display.fit.correlations() # %% [markdown] # #### Plot Measured vs Calculated # %% -project.display.plotter.plot_meas_vs_calc(expt_name='d20') +project.display.pattern(expt_name='d20') # %% -project.display.plotter.plot_meas_vs_calc(expt_name='d20', x_min=42, x_max=52) +project.display.pattern(expt_name='d20', x_min=42, x_max=52) # %% [markdown] # ## Summary diff --git a/docs/docs/tutorials/ed-6.ipynb b/docs/docs/tutorials/ed-6.ipynb index ae8f5a84d..3c89362d1 100644 --- a/docs/docs/tutorials/ed-6.ipynb +++ b/docs/docs/tutorials/ed-6.ipynb @@ -385,7 +385,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='hrpt')" + "project.display.pattern(expt_name='hrpt')" ] }, { @@ -395,7 +395,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', x_min=48, x_max=51)" + "project.display.pattern(expt_name='hrpt', x_min=48, x_max=51)" ] }, { @@ -437,7 +437,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.display.free_params()" + "project.display.parameters.free()" ] }, { @@ -465,7 +465,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.display.fit_results()" + "project.display.fit.results()" ] }, { @@ -483,7 +483,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='hrpt')" + "project.display.pattern(expt_name='hrpt')" ] }, { @@ -493,7 +493,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', x_min=48, x_max=51)" + "project.display.pattern(expt_name='hrpt', x_min=48, x_max=51)" ] }, { @@ -537,7 +537,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.display.free_params()" + "project.display.parameters.free()" ] }, { @@ -565,7 +565,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.display.fit_results()" + "project.display.fit.results()" ] }, { @@ -583,7 +583,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='hrpt')" + "project.display.pattern(expt_name='hrpt')" ] }, { @@ -593,7 +593,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', x_min=48, x_max=51)" + "project.display.pattern(expt_name='hrpt', x_min=48, x_max=51)" ] }, { @@ -635,7 +635,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.display.free_params()" + "project.display.parameters.free()" ] }, { @@ -663,7 +663,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.display.fit_results()" + "project.display.fit.results()" ] }, { @@ -681,7 +681,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='hrpt')" + "project.display.pattern(expt_name='hrpt')" ] }, { @@ -691,7 +691,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', x_min=48, x_max=51)" + "project.display.pattern(expt_name='hrpt', x_min=48, x_max=51)" ] }, { @@ -738,7 +738,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.display.free_params()" + "project.display.parameters.free()" ] }, { @@ -766,7 +766,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.display.fit_results()" + "project.display.fit.results()" ] }, { @@ -776,7 +776,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_param_correlations()" + "project.display.fit.correlations()" ] }, { @@ -794,7 +794,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='hrpt')" + "project.display.pattern(expt_name='hrpt')" ] }, { @@ -804,7 +804,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', x_min=48, x_max=51)" + "project.display.pattern(expt_name='hrpt', x_min=48, x_max=51)" ] }, { diff --git a/docs/docs/tutorials/ed-6.py b/docs/docs/tutorials/ed-6.py index 341178e61..14d93515e 100644 --- a/docs/docs/tutorials/ed-6.py +++ b/docs/docs/tutorials/ed-6.py @@ -179,10 +179,10 @@ # #### Plot Measured vs Calculated # %% -project.display.plotter.plot_meas_vs_calc(expt_name='hrpt') +project.display.pattern(expt_name='hrpt') # %% -project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', x_min=48, x_max=51) +project.display.pattern(expt_name='hrpt', x_min=48, x_max=51) # %% [markdown] # ### Perform Fit 1/4 @@ -200,7 +200,7 @@ # Show free parameters after selection. # %% -project.analysis.display.free_params() +project.display.parameters.free() # %% [markdown] # #### Run Fitting @@ -209,16 +209,16 @@ project.analysis.fit() # %% -project.analysis.display.fit_results() +project.display.fit.results() # %% [markdown] # #### Plot Measured vs Calculated # %% -project.display.plotter.plot_meas_vs_calc(expt_name='hrpt') +project.display.pattern(expt_name='hrpt') # %% -project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', x_min=48, x_max=51) +project.display.pattern(expt_name='hrpt', x_min=48, x_max=51) # %% [markdown] # ### Perform Fit 2/4 @@ -238,7 +238,7 @@ # Show free parameters after selection. # %% -project.analysis.display.free_params() +project.display.parameters.free() # %% [markdown] # #### Run Fitting @@ -247,16 +247,16 @@ project.analysis.fit() # %% -project.analysis.display.fit_results() +project.display.fit.results() # %% [markdown] # #### Plot Measured vs Calculated # %% -project.display.plotter.plot_meas_vs_calc(expt_name='hrpt') +project.display.pattern(expt_name='hrpt') # %% -project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', x_min=48, x_max=51) +project.display.pattern(expt_name='hrpt', x_min=48, x_max=51) # %% [markdown] # ### Perform Fit 3/4 @@ -274,7 +274,7 @@ # Show free parameters after selection. # %% -project.analysis.display.free_params() +project.display.parameters.free() # %% [markdown] # #### Run Fitting @@ -283,16 +283,16 @@ project.analysis.fit() # %% -project.analysis.display.fit_results() +project.display.fit.results() # %% [markdown] # #### Plot Measured vs Calculated # %% -project.display.plotter.plot_meas_vs_calc(expt_name='hrpt') +project.display.pattern(expt_name='hrpt') # %% -project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', x_min=48, x_max=51) +project.display.pattern(expt_name='hrpt', x_min=48, x_max=51) # %% [markdown] # ### Perform Fit 4/4 @@ -315,7 +315,7 @@ # Show free parameters after selection. # %% -project.analysis.display.free_params() +project.display.parameters.free() # %% [markdown] # #### Run Fitting @@ -324,19 +324,19 @@ project.analysis.fit() # %% -project.analysis.display.fit_results() +project.display.fit.results() # %% -project.display.plotter.plot_param_correlations() +project.display.fit.correlations() # %% [markdown] # #### Plot Measured vs Calculated # %% -project.display.plotter.plot_meas_vs_calc(expt_name='hrpt') +project.display.pattern(expt_name='hrpt') # %% -project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', x_min=48, x_max=51) +project.display.pattern(expt_name='hrpt', x_min=48, x_max=51) # %% [markdown] # ## Summary diff --git a/docs/docs/tutorials/ed-7.ipynb b/docs/docs/tutorials/ed-7.ipynb index 7a66c6f66..31294b586 100644 --- a/docs/docs/tutorials/ed-7.ipynb +++ b/docs/docs/tutorials/ed-7.ipynb @@ -345,8 +345,8 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='sepd')\n", - "project.display.plotter.plot_meas_vs_calc(expt_name='sepd', x_min=23200, x_max=23700)" + "project.display.pattern(expt_name='sepd')\n", + "project.display.pattern(expt_name='sepd', x_min=23200, x_max=23700)" ] }, { @@ -387,7 +387,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.display.free_params()" + "project.display.parameters.free()" ] }, { @@ -406,7 +406,7 @@ "outputs": [], "source": [ "project.analysis.fit()\n", - "project.analysis.display.fit_results()" + "project.display.fit.results()" ] }, { @@ -424,7 +424,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='sepd')" + "project.display.pattern(expt_name='sepd')" ] }, { @@ -434,7 +434,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='sepd', x_min=23200, x_max=23700)" + "project.display.pattern(expt_name='sepd', x_min=23200, x_max=23700)" ] }, { @@ -473,7 +473,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.display.free_params()" + "project.display.parameters.free()" ] }, { @@ -492,7 +492,7 @@ "outputs": [], "source": [ "project.analysis.fit()\n", - "project.analysis.display.fit_results()" + "project.display.fit.results()" ] }, { @@ -510,7 +510,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='sepd')" + "project.display.pattern(expt_name='sepd')" ] }, { @@ -520,7 +520,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='sepd', x_min=23200, x_max=23700)" + "project.display.pattern(expt_name='sepd', x_min=23200, x_max=23700)" ] }, { @@ -579,7 +579,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.display.free_params()" + "project.display.parameters.free()" ] }, { @@ -598,7 +598,7 @@ "outputs": [], "source": [ "project.analysis.fit()\n", - "project.analysis.display.fit_results()" + "project.display.fit.results()" ] }, { @@ -616,7 +616,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='sepd')" + "project.display.pattern(expt_name='sepd')" ] }, { @@ -626,7 +626,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='sepd', x_min=23200, x_max=23700)" + "project.display.pattern(expt_name='sepd', x_min=23200, x_max=23700)" ] }, { @@ -668,7 +668,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.display.free_params()" + "project.display.parameters.free()" ] }, { @@ -687,7 +687,7 @@ "outputs": [], "source": [ "project.analysis.fit()\n", - "project.analysis.display.fit_results()" + "project.display.fit.results()" ] }, { @@ -705,7 +705,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_param_correlations()" + "project.display.fit.correlations()" ] }, { @@ -723,7 +723,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='sepd')" + "project.display.pattern(expt_name='sepd')" ] }, { @@ -733,7 +733,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='sepd', x_min=23200, x_max=23700)" + "project.display.pattern(expt_name='sepd', x_min=23200, x_max=23700)" ] }, { @@ -745,7 +745,7 @@ }, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='sepd', x='d_spacing')" + "project.display.pattern(expt_name='sepd', x='d_spacing')" ] }, { @@ -860,7 +860,7 @@ "outputs": [], "source": [ "project.analysis.fit()\n", - "project.analysis.display.fit_results()" + "project.display.fit.results()" ] }, { @@ -878,7 +878,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_param_correlations()" + "project.display.fit.correlations()" ] }, { @@ -896,7 +896,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='sepd', x_min=23200, x_max=23700)" + "project.display.pattern(expt_name='sepd', x_min=23200, x_max=23700)" ] }, { @@ -906,7 +906,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='sepd', x='d_spacing')" + "project.display.pattern(expt_name='sepd', x='d_spacing')" ] } ], diff --git a/docs/docs/tutorials/ed-7.py b/docs/docs/tutorials/ed-7.py index 5305549a7..ed3dcdaec 100644 --- a/docs/docs/tutorials/ed-7.py +++ b/docs/docs/tutorials/ed-7.py @@ -140,8 +140,8 @@ # #### Plot Measured vs Calculated # %% -project.display.plotter.plot_meas_vs_calc(expt_name='sepd') -project.display.plotter.plot_meas_vs_calc(expt_name='sepd', x_min=23200, x_max=23700) +project.display.pattern(expt_name='sepd') +project.display.pattern(expt_name='sepd', x_min=23200, x_max=23700) # %% [markdown] # ### Perform Fit 1/5 @@ -158,23 +158,23 @@ # Show free parameters after selection. # %% -project.analysis.display.free_params() +project.display.parameters.free() # %% [markdown] # #### Run Fitting # %% project.analysis.fit() -project.analysis.display.fit_results() +project.display.fit.results() # %% [markdown] # #### Plot Measured vs Calculated # %% -project.display.plotter.plot_meas_vs_calc(expt_name='sepd') +project.display.pattern(expt_name='sepd') # %% -project.display.plotter.plot_meas_vs_calc(expt_name='sepd', x_min=23200, x_max=23700) +project.display.pattern(expt_name='sepd', x_min=23200, x_max=23700) # %% [markdown] # ### Perform Fit 2/5 @@ -189,23 +189,23 @@ # Show free parameters after selection. # %% -project.analysis.display.free_params() +project.display.parameters.free() # %% [markdown] # #### Run Fitting # %% project.analysis.fit() -project.analysis.display.fit_results() +project.display.fit.results() # %% [markdown] # #### Plot Measured vs Calculated # %% -project.display.plotter.plot_meas_vs_calc(expt_name='sepd') +project.display.pattern(expt_name='sepd') # %% -project.display.plotter.plot_meas_vs_calc(expt_name='sepd', x_min=23200, x_max=23700) +project.display.pattern(expt_name='sepd', x_min=23200, x_max=23700) # %% [markdown] # ### Perform Fit 3/5 @@ -228,23 +228,23 @@ # Show free parameters after selection. # %% -project.analysis.display.free_params() +project.display.parameters.free() # %% [markdown] # #### Run Fitting # %% project.analysis.fit() -project.analysis.display.fit_results() +project.display.fit.results() # %% [markdown] # #### Plot Measured vs Calculated # %% -project.display.plotter.plot_meas_vs_calc(expt_name='sepd') +project.display.pattern(expt_name='sepd') # %% -project.display.plotter.plot_meas_vs_calc(expt_name='sepd', x_min=23200, x_max=23700) +project.display.pattern(expt_name='sepd', x_min=23200, x_max=23700) # %% [markdown] # ### Perform Fit 4/5 @@ -262,32 +262,32 @@ # Show free parameters after selection. # %% -project.analysis.display.free_params() +project.display.parameters.free() # %% [markdown] # #### Run Fitting # %% project.analysis.fit() -project.analysis.display.fit_results() +project.display.fit.results() # %% [markdown] # #### Show parameter correlations # %% -project.display.plotter.plot_param_correlations() +project.display.fit.correlations() # %% [markdown] # #### Plot Measured vs Calculated # %% -project.display.plotter.plot_meas_vs_calc(expt_name='sepd') +project.display.pattern(expt_name='sepd') # %% -project.display.plotter.plot_meas_vs_calc(expt_name='sepd', x_min=23200, x_max=23700) +project.display.pattern(expt_name='sepd', x_min=23200, x_max=23700) # %% -project.display.plotter.plot_meas_vs_calc(expt_name='sepd', x='d_spacing') +project.display.pattern(expt_name='sepd', x='d_spacing') # %% [markdown] @@ -334,19 +334,19 @@ # %% project.analysis.fit() -project.analysis.display.fit_results() +project.display.fit.results() # %% [markdown] # #### Show parameter correlations # %% -project.display.plotter.plot_param_correlations() +project.display.fit.correlations() # %% [markdown] # #### Plot Measured vs Calculated # %% -project.display.plotter.plot_meas_vs_calc(expt_name='sepd', x_min=23200, x_max=23700) +project.display.pattern(expt_name='sepd', x_min=23200, x_max=23700) # %% -project.display.plotter.plot_meas_vs_calc(expt_name='sepd', x='d_spacing') +project.display.pattern(expt_name='sepd', x='d_spacing') diff --git a/docs/docs/tutorials/ed-8.ipynb b/docs/docs/tutorials/ed-8.ipynb index 5df563205..528eee528 100644 --- a/docs/docs/tutorials/ed-8.ipynb +++ b/docs/docs/tutorials/ed-8.ipynb @@ -564,8 +564,8 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.fit.show_modes()\n", - "project.analysis.fit.mode = 'joint'" + "project.analysis.show_fitting_mode_types()\n", + "project.analysis.fitting_mode_type = 'joint'" ] }, { @@ -630,7 +630,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='wish_5_6')" + "project.display.pattern(expt_name='wish_5_6')" ] }, { @@ -640,7 +640,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='wish_4_7')" + "project.display.pattern(expt_name='wish_4_7')" ] }, { @@ -659,8 +659,8 @@ "outputs": [], "source": [ "project.analysis.fit()\n", - "project.analysis.display.fit_results()\n", - "project.display.plotter.plot_param_correlations()" + "project.display.fit.results()\n", + "project.display.fit.correlations()" ] }, { @@ -678,7 +678,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='wish_5_6')" + "project.display.pattern(expt_name='wish_5_6')" ] }, { @@ -688,7 +688,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='wish_4_7')" + "project.display.pattern(expt_name='wish_4_7')" ] }, { diff --git a/docs/docs/tutorials/ed-8.py b/docs/docs/tutorials/ed-8.py index a9adb7c8f..7491684ba 100644 --- a/docs/docs/tutorials/ed-8.py +++ b/docs/docs/tutorials/ed-8.py @@ -298,8 +298,8 @@ # #### Set Fit Mode # %% -project.analysis.fit.show_modes() -project.analysis.fit.mode = 'joint' +project.analysis.show_fitting_mode_types() +project.analysis.fitting_mode_type = 'joint' # %% [markdown] # #### Set Free Parameters @@ -333,27 +333,27 @@ # #### Plot Measured vs Calculated # %% -project.display.plotter.plot_meas_vs_calc(expt_name='wish_5_6') +project.display.pattern(expt_name='wish_5_6') # %% -project.display.plotter.plot_meas_vs_calc(expt_name='wish_4_7') +project.display.pattern(expt_name='wish_4_7') # %% [markdown] # #### Run Fitting # %% project.analysis.fit() -project.analysis.display.fit_results() -project.display.plotter.plot_param_correlations() +project.display.fit.results() +project.display.fit.correlations() # %% [markdown] # #### Plot Measured vs Calculated # %% -project.display.plotter.plot_meas_vs_calc(expt_name='wish_5_6') +project.display.pattern(expt_name='wish_5_6') # %% -project.display.plotter.plot_meas_vs_calc(expt_name='wish_4_7') +project.display.pattern(expt_name='wish_4_7') # %% [markdown] # ## Summary diff --git a/docs/docs/tutorials/ed-9.ipynb b/docs/docs/tutorials/ed-9.ipynb index 4c4d7eb5b..0bfd09490 100644 --- a/docs/docs/tutorials/ed-9.ipynb +++ b/docs/docs/tutorials/ed-9.ipynb @@ -509,7 +509,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas(expt_name='mcstas')" + "project.display.pattern(expt_name='mcstas', include='measured')" ] }, { @@ -564,7 +564,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas(expt_name='mcstas')" + "project.display.pattern(expt_name='mcstas', include=('measured', 'excluded'))" ] }, { @@ -660,8 +660,8 @@ "outputs": [], "source": [ "project.analysis.fit()\n", - "project.analysis.display.fit_results()\n", - "project.display.plotter.plot_param_correlations()" + "project.display.fit.results()\n", + "project.display.fit.correlations()" ] }, { @@ -679,7 +679,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='mcstas')" + "project.display.pattern(expt_name='mcstas')" ] } ], diff --git a/docs/docs/tutorials/ed-9.py b/docs/docs/tutorials/ed-9.py index c9108b5f0..ffae09108 100644 --- a/docs/docs/tutorials/ed-9.py +++ b/docs/docs/tutorials/ed-9.py @@ -227,7 +227,7 @@ # Show measured data as loaded from the file. # %% -project.display.plotter.plot_meas(expt_name='mcstas') +project.display.pattern(expt_name='mcstas', include='measured') # %% [markdown] # Add excluded regions. @@ -246,7 +246,7 @@ # Show measured data after adding excluded regions. # %% -project.display.plotter.plot_meas(expt_name='mcstas') +project.display.pattern(expt_name='mcstas', include=('measured', 'excluded')) # %% [markdown] # Show experiment as CIF. @@ -294,11 +294,11 @@ # %% project.analysis.fit() -project.analysis.display.fit_results() -project.display.plotter.plot_param_correlations() +project.display.fit.results() +project.display.fit.correlations() # %% [markdown] # #### Plot Measured vs Calculated # %% -project.display.plotter.plot_meas_vs_calc(expt_name='mcstas') +project.display.pattern(expt_name='mcstas') diff --git a/docs/docs/tutorials/index.json b/docs/docs/tutorials/index.json index dcd0cb870..40145a15e 100644 --- a/docs/docs/tutorials/index.json +++ b/docs/docs/tutorials/index.json @@ -131,5 +131,19 @@ "title": "Instrument calibration: BEER at ESS", "description": "Instrument calibration of BEER at ESS using neutron powder diffraction data simulated with McStas", "level": "intermediate" + }, + "21": { + "url": "https://easyscience.github.io/diffraction-lib/{version}/tutorials/ed-21/ed-21.ipynb", + "original_name": "", + "title": "Bayesian Analysis: LBCO, HRPT", + "description": "Bayesian analysis of the La0.5Ba0.5CoO3 crystal structure - Markov Chain Monte Carlo (MCMC) sampling.", + "level": "advanced" + }, + "22": { + "url": "https://easyscience.github.io/diffraction-lib/{version}/tutorials/ed-22/ed-22.ipynb", + "original_name": "", + "title": "Bayesian Analysis: Tb2TiO7, HEiDi", + "description": "Bayesian analysis of the Tb2TiO7 crystal structure - Markov Chain Monte Carlo (MCMC) sampling.", + "level": "advanced" } } diff --git a/docs/docs/tutorials/index.md b/docs/docs/tutorials/index.md index 69b9d0ca9..b789645d0 100644 --- a/docs/docs/tutorials/index.md +++ b/docs/docs/tutorials/index.md @@ -17,10 +17,6 @@ The tutorials are organized into the following categories: ## Getting Started -- [LBCO `quick` `load`](ed-18.ipynb) – The most minimal example showing - how to load a previously saved project from a directory and run - refinement. Useful when a project has already been set up and saved in - a prior session. - [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 @@ -42,6 +38,19 @@ The tutorials are organized into the following categories: descriptions of every step, making it suitable for users who are new to EasyDiffraction or those who prefer a more guided approach. +## Load Project + +- [LBCO Single Fit](ed-18.ipynb) – The most minimal example showing how + to load a previously saved project from a directory and continue + working with it. +- [Co2SiO4 Sequential Fit](ed-23.ipynb) – Resumes a sequential + refinement from an existing `analysis/results.csv` after an incomplete + previous run. +- [LBCO Bayesian Display](ed-24.ipynb) – Shows how to load the saved + project after a Bayesian analysis and inspect the persisted fit + summary, correlation matrix, posterior plots, and predictive checks + without rerunning MCMC sampling. + ## Powder Diffraction - [Co2SiO4 `pd-neut-cwl`](ed-5.ipynb) – Demonstrates a Rietveld @@ -98,6 +107,22 @@ The tutorials are organized into the following categories: - [BEER McStas](ed-20.ipynb) – Rietveld refinement based on the data simulated with McStas for the BEER instrument at ESS. +## Bayesian Analysis + +- [LBCO Bayesian](ed-21.ipynb) – Demonstrates how to perform a Bayesian + analysis of the La0.5Ba0.5CoO3 crystal structure using constant + wavelength neutron powder diffraction data from HRPT at PSI. This + tutorial covers the use of Markov Chain Monte Carlo (MCMC) sampling to + explore the posterior distribution of the refined parameters, + providing insights into parameter uncertainties and correlations. +- [Tb2TiO7 Bayesian](ed-22.ipynb) – Another example of a Bayesian + analysis. This tutorial focuses on the Tb2TiO7 crystal structure using + constant wavelength neutron single crystal diffraction data from HEiDi + at FRM II. Similar to the LBCO Bayesian tutorial, it covers the use of + MCMC sampling to explore the posterior distribution of the refined + parameters, providing insights into parameter uncertainties and + correlations in the context of single crystal diffraction data. + ## Workshops & Schools - [DMSC Summer School](ed-13.ipynb) – A workshop tutorial that diff --git a/docs/docs/user-guide/analysis-workflow/analysis.md b/docs/docs/user-guide/analysis-workflow/analysis.md index bf1d4ee87..f2efb06ac 100644 --- a/docs/docs/user-guide/analysis-workflow/analysis.md +++ b/docs/docs/user-guide/analysis-workflow/analysis.md @@ -134,7 +134,7 @@ derivatives of the objective. To show the supported minimizers: ```python -project.analysis.fit.show_minimizer_types() +project.analysis.fitting.show_minimizer_types() ``` The example of the output is: @@ -151,7 +151,7 @@ Supported minimizers To select the desired minimizer, e.g., 'lmfit': ```python -project.analysis.fit.minimizer_type = 'lmfit' +project.analysis.fitting.minimizer_type = 'lmfit' ``` ### Fit Mode @@ -168,16 +168,16 @@ The supported fit modes are: | 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 on the `fit` category: +You can set the fit mode on the analysis owner: ```python -project.analysis.fit.mode = 'joint' +project.analysis.fitting_mode_type = 'joint' ``` To check the current fit mode: ```python -print(project.analysis.fit.mode.value) +print(project.analysis.fitting_mode_type) ``` ### Perform Fit @@ -247,11 +247,11 @@ 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: +To plot the measured and calculated data after the fit, you can use the +`pattern` method of the `display` object: ```python -project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', show_residual=True) +project.display.pattern(expt_name='hrpt') ``` ## Constraints @@ -319,7 +319,7 @@ To view the defined constraints, you can use the `show_constraints` method: ```python -project.analysis.display.constraints() +project.analysis.constraints.show() ``` The example of the output is: @@ -336,16 +336,16 @@ User defined constraints To inspect an analysis configuration in CIF format, use: ```python -# Show structure as CIF -project.structures['lbco'].show_as_cif() +# Show analysis as CIF +project.analysis.show_as_cif() ``` Example output: ``` ╒════════════════════════════════════════════════╕ -│ _fit.minimizer_type "lmfit (leastsq)" │ -│ _fit.mode single │ +│ _fitting.minimizer_type "lmfit (leastsq)" │ +│ _fitting.mode_type single │ │ │ │ loop_ │ │ _alias.label │ diff --git a/docs/docs/user-guide/analysis-workflow/project.md b/docs/docs/user-guide/analysis-workflow/project.md index 7959f9f85..72f77dc18 100644 --- a/docs/docs/user-guide/analysis-workflow/project.md +++ b/docs/docs/user-guide/analysis-workflow/project.md @@ -14,7 +14,7 @@ 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). +- **Load an existing saved project** from its project directory. Below are instructions on how to set up a project in EasyDiffraction. It is assumed that you have already imported the `easydiffraction` package, @@ -55,14 +55,15 @@ you can just call the `save`: project.save() ``` -## Loading a Project from CIF +## Loading a Saved Project -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 saved project, load it from the project +directory created by `project.save_as()` or `project.save()`. This is +useful for continuing a previous session or reusing a downloaded saved +project. ```python -project.load('data/lbco_hrpt.cif') +project = ed.Project.load('lbco_hrpt') ``` ## Project Structure @@ -82,9 +83,10 @@ The example below illustrates a typical **project structure** for a ├── 📁 experiments - Folder with experiment settings and measured data. │ ├── 📄 hrpt.cif - Instrumental parameters, calculator selection and measured data from HRPT@PSI. │ └── ... -├── 📄 analysis.cif - Settings for data analysis (minimizer, fit mode, etc.). -└── 📁 summary - └── 📄 report.cif - Summary report after structure refinement. +├── 📁 analysis - Analysis settings and optional persisted Bayesian arrays. +│ ├── 📄 analysis.cif - Settings for data analysis (minimizer, fit mode, constraints, persisted fit state). +│ └── 📄 results.h5 - Optional Bayesian sidecar with posterior and predictive arrays. +└── 📄 summary.cif - Summary report after structure refinement. @@ -110,13 +112,16 @@ This file stores project-level metadata and display configuration.
-data_La0.5Ba0.5CoO3
-
+_project.id          lbco_hrpt
 _project.title       "La0.5Ba0.5CoO3 from neutron diffraction at HRPT@PSI"
 _project.description "neutrons, powder, constant wavelength, HRPT@PSI"
 
-_display.plotter_type  asciichartpy
-_display.tabler_type   rich
+_project.created     "18 May 2026 10:15:00"
+_project.last_modified "18 May 2026 10:20:00"
+
+_rendering.chart_engine auto
+_rendering.table_engine auto
+_verbosity.fit         full
 
@@ -233,7 +238,7 @@ loop_ -### 4. analysis.cif +### 4. analysis / analysis.cif This file contains settings used for data analysis, including the choice of **calculation** and **fitting** engines, as well as user defined @@ -243,8 +248,8 @@ of **calculation** and **fitting** engines, as well as user defined
-_fit.minimizer_type          "lmfit (leastsq)"
-_fit.mode                    single
+_fitting.mode_type              single
+_fitting.minimizer_type         lmfit
 
 loop_
 _alias.label
@@ -263,6 +268,9 @@ loop_
 
 
 
+When a Bayesian fit stores persisted posterior or predictive arrays, the
+same `analysis/` directory also contains `results.h5`.
+
 
--- diff --git a/docs/docs/user-guide/first-steps.md b/docs/docs/user-guide/first-steps.md index b8f69070d..a41bcee68 100644 --- a/docs/docs/user-guide/first-steps.md +++ b/docs/docs/user-guide/first-steps.md @@ -117,7 +117,7 @@ You can also check the available minimizers using the `show_minimizer_types()` method: ```python -project.analysis.fit.show_minimizer_types() +project.analysis.fitting.show_minimizer_types() ``` ### Available parameters @@ -125,23 +125,22 @@ project.analysis.fit.show_minimizer_types() EasyDiffraction provides several methods for showing the available parameters grouped in different categories. For example, you can use: -- `project.analysis.display.all_params()` – to display all available +- `project.display.parameters.all()` – to display all available parameters for the analysis step. -- `project.analysis.display.fittable_params()` – to display only the +- `project.display.parameters.fittable()` – to display only the parameters that can be fitted during the analysis. -- `project.analysis.display.free_params()` – to display the parameters - that are currently free to be adjusted during the fitting process. +- `project.display.parameters.free()` – to display the parameters that + are currently free to be adjusted during the fitting process. -Finally, you can use the -`project.analysis.display.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. +Finally, you can use the `project.display.parameters.access()` 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.display.how_to_access_parameters()` method is: +An example of the output for the `project.display.parameters.access()` +method is: | | Code variable | Unique ID for CIF | | --- | --------------------------------------------------- | -------------------------- | @@ -160,7 +159,7 @@ To see the available plotters, you can use the `display` category on the `Project` instance: ```python -project.display.show_plotter_types() +project.rendering.show_chart_engines() ``` An example of the output is: diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 45c2ce5fb..02f862023 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -148,8 +148,10 @@ plugins: python: paths: ['src'] # Change 'src' to your actual sources directory options: + annotations_path: source docstring_style: numpy - group_by_category: false + group_by_category: true + members_order: source heading_level: 1 show_root_heading: true show_root_full_path: false @@ -191,10 +193,13 @@ nav: - Tutorials: - Tutorials: tutorials/index.md - Getting Started: - - LBCO quick load: tutorials/ed-18.ipynb - LBCO quick code: tutorials/ed-2.ipynb - LBCO basic load: tutorials/ed-1.ipynb - LBCO complete: tutorials/ed-3.ipynb + - Load Project: + - LBCO Single: tutorials/ed-18.ipynb + - Co2SiO4 Sequential: tutorials/ed-23.ipynb + - LBCO Bayesian: tutorials/ed-24.ipynb - Powder Diffraction: - Co2SiO4 pd-neut-cwl: tutorials/ed-5.ipynb - HS pd-neut-cwl: tutorials/ed-6.ipynb @@ -214,8 +219,13 @@ nav: - Simulated Data: - LBCO+Si McStas: tutorials/ed-9.ipynb - BEER McStas: tutorials/ed-20.ipynb + - Bayesian Analysis: + - LBCO Bayesian: tutorials/ed-21.ipynb + - Tb2TiO7 Bayesian: tutorials/ed-22.ipynb - Workshops & Schools: - DMSC Summer School: tutorials/ed-13.ipynb + - Command-Line: + - Command-Line: cli/index.md - API Reference: - API Reference: api-reference/index.md - analysis: api-reference/analysis.md @@ -229,5 +239,5 @@ nav: - project: api-reference/project.md - summary: api-reference/summary.md - utils: api-reference/utils.md - - Command-Line Interface: - - Command-Line Interface: cli/index.md + - Quick Reference: + - Quick Reference: quick-reference/index.md diff --git a/pixi.lock b/pixi.lock index 36a2ecb00..974cbe77e 100644 --- a/pixi.lock +++ b/pixi.lock @@ -1,53 +1,100 @@ -version: 6 +version: 7 +platforms: +- name: linux-64 +- name: osx-arm64 +- name: win-64 environments: default: channels: + - url: https://conda.anaconda.org/nodefaults/ - 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/_openmp_mutex-4.5-20_gnu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/argon2-cffi-bindings-25.1.0-py314h5bd0f2a_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.2.0-py314h3de4e8d_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_9.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/c-ares-1.34.6-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cffi-2.0.0-py314h4a8dc5f_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/debugpy-1.8.20-py314h42812f9_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-78.3-h33c6efd_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.3-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.22.2-ha1258a1_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45.1-default_hbd61a6d_102.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libabseil-20260107.1-cxx17_h7b12aa8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libblas-3.11.0-7_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-7_h0358290_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20250104-pl5321h7949ede_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libev-4.33-hd590300_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.8.0-hecca717_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h3435931_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-he0feb66_19.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.2.0-h69a702a_19.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.2.0-h69a702a_19.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.2.0-h68bc16d_19.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-he0feb66_19.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.3-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libnghttp2-1.68.1-h877daf1_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.33-pthreads_h94d23a6_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsodium-1.0.21-h280c20c_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.53.1-h0c1763c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h934c35e_19.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.42-h5347b49_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libuv-1.51.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.2-h25fd6f3_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.3-py314h67df5f8_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/msgspec-0.21.1-py314h5bd0f2a_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.6-hdb14827_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/nodejs-25.8.2-he4ff34a_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.2-h35e630c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/psutil-7.2.2-py314h0f05182_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.14.4-habeac84_100_cp314.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.3-py314h67df5f8_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pyzmq-27.1.0-py312hda471dd_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.3-h853b02a_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/rpds-py-0.30.0-py314h2e6c369_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h366c992_103.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tornado-6.5.5-py314h5bd0f2a_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h280c20c_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zeromq-4.3.5-h41580af_10.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.13.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/argon2-cffi-25.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/argon2-cffi-bindings-25.1.0-py314h5bd0f2a_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/arrow-1.4.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.18.0-pyhcf101f3_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/backports.zstd-1.3.0-py314h680f03e_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/backports.zstd-1.5.0-py314h680f03e_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.14.3-pyha770c72_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.3.0-pyhcf101f3_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-with-css-6.3.0-hbca2aae_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.2.0-py314h3de4e8d_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_9.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/c-ares-1.34.6-hb03c661_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.4.22-hbd8a1cb_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cached-property-1.5.2-hd8ed1ab_1.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/cached_property-1.5.2-pyha770c72_1.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2026.4.22-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/cffi-2.0.0-py314h4a8dc5f_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.7-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.3-pyhe01879c_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.14.4-py314hd8ed1ab_100.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/debugpy-1.8.20-py314h42812f9_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/decorator-5.2.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/defusedxml-0.7.1-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/fqdn-1.5.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/gsl-2.8-hbf7d49c_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/h11-0.16.0-pyhcf101f3_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/httpcore-1.0.9-pyh29332c3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/httpx-0.28.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/icu-78.3-h33c6efd_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.13-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.8.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.2.0-pyha191276_1.conda @@ -65,84 +112,45 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.8.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.9.1-pyhc90fa1f_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_events-0.12.1-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.17.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.18.2-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server_terminals-0.5.4-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.5.7-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_pygments-0.3.0-pyhd8ed1ab_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_server-2.28.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.3-hb9d3cd8_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.22.2-ha1258a1_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/lark-1.3.1-pyhd8ed1ab_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-6_h0358290_openblas.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20250104-pl5321h7949ede_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libev-4.33-hd590300_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.5-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.3-hb03c661_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb03c661_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libnghttp2-1.68.1-h877daf1_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.32-pthreads_h94d23a6_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libsodium-1.0.21-h280c20c_3.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.53.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.42-h5347b49_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libuv-1.51.0-hb03c661_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.2-h25fd6f3_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.3-py314h67df5f8_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.2.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mistune-3.2.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/msgspec-0.21.1-py314h5bd0f2a_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.2.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mistune-3.2.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/nbclient-0.10.4-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/nbconvert-core-7.17.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/nbformat-5.10.4-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.6-hdb14827_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/nodejs-25.8.2-he4ff34a_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/notebook-shim-0.2.4-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.2-h35e630c_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/overrides-7.7.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.2-pyhc364b38_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pandocfilters-1.5.0-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.6-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.7-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pexpect-4.9.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pixi-kernel-0.7.1-pyhbbac1ac_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.9.6-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/prometheus_client-0.25.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.52-pyha770c72_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/psutil-7.2.2-py314h0f05182_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ptyprocess-0.7.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.14.4-habeac84_100_cp314.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.21.2-pyhe01879c_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.14.4-h4df99d1_100.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-json-logger-3.2.1-pyh332efcf_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2026.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.14-8_cp314.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.3-py314h67df5f8_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/pyzmq-27.1.0-py312hda471dd_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.3-h853b02a_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.37.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.33.1-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.34.2-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/returns-0.27.0-pyhc364b38_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3339-validator-0.1.4-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3986-validator-0.1.1-pyh9f0ad1d_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3987-syntax-1.1.0-pyhe01879c_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/rpds-py-0.30.0-py314h2e6c369_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/send2trash-2.1.0-pyha191276_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-82.0.1-pyh332efcf_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda @@ -151,241 +159,231 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyhc90fa1f_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tinycss2-1.4.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h366c992_103.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.1-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/tornado-6.5.5-py314h5bd0f2a_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.15.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.15.0-h396c80c_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing_utils-0.1.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/uri-template-1.3.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.6.3-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.6.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.7.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.7.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/webcolors-25.10.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/webencodings-0.5.1-pyhd8ed1ab_3.conda - conda: https://conda.anaconda.org/conda-forge/noarch/websocket-client-1.9.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h280c20c_3.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/zeromq-4.3.5-h41580af_10.conda - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.1-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda + - pypi: . + - pypi: https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/04/5f/9ff93450ba96b09c7c2b3f81c94de31c89f92292f1380261bd7195bea4ea/contourpy-1.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/07/c7/deb8c5e604404dbf10a3808a858946ca3547692ff6316b698945bb72177e/python_socketio-5.16.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0b/2e/7eea398450457ecb54e18e9d10110993fa65561c4f3add5e8eccd2b9cd41/scipy-1.17.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/0b/f9/f15c95d6b200167cb22c5eca5eecfa9d28a8ee3f74095f1cd2345c71f2f9/pydoclint-0.8.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0d/12/bbce9472f489cb5c4c23b0d13e5c59c37c1aab11b7ac637dfe6bbdccebe7/copier-9.15.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0d/1f/d398de1612f7a611e22d743280339c9af4903675635e41be3370091c704b/arviz_stats-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0d/fe/6bea5c9162869c5beba5d9c8abbed835ec85bf1ec1fba05a3822325c45f3/build-1.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/bd/c9/989f4034fb46841208de7aeeac2c6d8300745ab4f28c42f629ba77c2d916/aiohttp-3.13.5-cp314-cp314-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/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/11/8c/c9138d881c79aa0ea9ed83cbd58d5ca75624378b38cee225dcf5c42cc91f/griffelib-2.0.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/11/8d/d2532ad2a603ca2b93ad9f5135732124e57811d0168155852f37fbce2458/pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/12/aa/fb2a0649fdeef5ab7072d221e8f4df164098792c813af6c87e2581cfa860/mpltoolbox-26.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/12/c9/6869a1dcf4aaf309b9543ec070be3ec3adebee7c9bec9af8c230494134b9/interrogate-1.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/13/95/cf3f7fe4910cf0365fa8ea0c731f4b8a624d97cd76ea777913ac8d0868e2/mkdocs_jupyter-0.26.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/19/d4/225027a913621a879b429a043674aa35220e6ce67785acad4f7bd0c4ff33/xarray_einstats-0.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1c/59/964ecb8008722d27d8a835baea81f56a91cea8e097b3be992bc6ccde6367/versioningit-3.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1d/77/928ea2e70641ca177a11140062cc5840d421795f2e82749d408d0cce900a/narwhals-2.21.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/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/20/16/e777eadfa0c0305878c36fae1d5e6db474fbb15dae202b9ec378809dfb4d/nbstripout-0.9.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/20/7a/1c6e3562dfd8950adbb11ffbc65d21e7c89d01a6e4f137fa981056de25c5/gitpython-3.1.50-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-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/28/88/4789719fbbe166d12d345b3ac66b96105f10001b16e00a9765ba29261a21/nbqa-1.9.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/28/de/a3e710469772c6a89595fc52816da05c1e164b4c866a89e3cb82fb1b67c5/mkdocs_autorefs-1.4.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2a/2d/d4bf65e47cea8ff2c794a600c4fd1273a7902f268757c531e0ee9f18aa58/pooch-1.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2b/76/7f1bfd6afff4c5e38e36a3c6d68eb5f4b7311ea80baf693db78d95b603c4/propcache-0.5.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/2c/01/bc663630c510822c95c47a66af9fa7a443c295b47d5f041e5e6ae62ef659/mkdocs_material-9.7.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2e/84/efc7c0bf3a1c5eef81d397f6fddac855becdbb11cb38ff957888603014a7/sqlalchemy-2.0.49-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/2f/40/dc34d1a8d5f1e51fc64640b62b191684da52ca469da9cd74e84936ffa4a6/msgpack-1.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/2f/c8/005d1de3af80f54411703d1263a0b9d31276411ec9f273d9432c59b17299/arviz_plots-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/32/28/79f0f8de97cce916d5ae88a7bee1ad724855e83e6019c0b4d5b3fabc80f3/mkdocstrings_python-2.0.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/32/5d/f7e914f7d9325abff4057cee62c0fa70263683189f774473cbfb534cd13b/matplotlib-3.10.9-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/33/b2/986d1220f6ee931e338d272bc1f3ec02cfe5f9b5fad84e95afdad57f1ebc/format_docstring-0.2.7-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/33/eb/f9f1ded8e4db9638f9530c3782eb01f5ab04945f4cb9e597a51c203fa4c5/diffpy_pdffit2-1.6.0.tar.gz + - 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/36/e1/a8933a72c45a87177fbde2696e0d0755c8c9062f8c077a961c6215fa27b1/fonttools-4.63.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/38/7e/7b91c89a4cf0f543a83be978657afb20c86af6d725253e319589dcc4ce52/lmfit-1.3.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/38/8b/7ec325b4e9e78beefc2d025b01ee8a2fde771ef7c957c3bff99b9e1fbffa/xraydb-4.5.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3a/eb/fea4d1d51c49832120f7f285d07306db3960f423a2612c6057caf3e8196f/pip-26.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3c/26/1062c7ec1b053db9e499b4d2d5bc231743201b74051c973dadeac80a8f43/questionary-2.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3e/17/1f31d8562e6f970d64911f1abc330d233bc0c0601411cf7e19c1292be6da/spdx_headers-1.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3e/5c/fb93d3092640a24dfb7bd7727a24016d7c01774ca013e60efd3f683c8002/backrefs-7.0-py314-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3f/0e/fa3b193432cfc60c93b42f3be03365f5f909d2b3ea410295cf36df739e31/widgetsnbextension-4.0.15-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3f/d0/7b958df957e4827837b590944008f0b28078f552b451f7407b4b3d54f574/asciichartpy-1.5.25-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/42/2b/fef67d616931055bf3d6764885990a3ac647d68734a2d6a9e1d13de437a2/yarl-1.23.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/42/d9/27b13bc9419bf5dae02905b348f16ca827646cd76244ddd326f1a8139a6a/cyclebane-24.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/43/80/1315439acedd8398319bac177d6de3d48ab39c62cc0c810f74f0a9a73996/numpy-2.4.5-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/43/fe/ad0ecbe2393cb690a4b3100a8fea47ecfdb49f6e06f40cf2f626635adc0c/scipp-26.3.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/46/b4/0887c88ddfaba1d7140ea335144eb904af97550786ee58bdb295ff10d255/crysfml-0.6.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/4a/f3/00bb1e867fba351e2d784170955713bee200c43ea306c59f30bd7e748192/dask-2026.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4c/65/b4b86e5fa07543bfbbcdc6c9f7f9f561e66a5f3539992e3009973f2b1314/jupytext-1.19.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/52/59/0782e51887ac6b07ffd1570e0364cf901ebc36345fea669969d2084baebb/simple_websocket-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/56/6d/0d9848617b9f753b87f214f1c682592f7ca42de085f564352f10f0843026/ipywidgets-8.1.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/58/3b/1cdec6772bdbaf7b25dab360c59f03cadf05492dd724c6540af905389b07/pandas-3.0.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/58/e0/f1871f520c359e4e3a2eb7437c9e7e792bb6c356414e8617937561167caf/pycifrw-5.0.1.tar.gz + - pypi: https://files.pythonhosted.org/packages/5b/29/74eeb4d3f3ae61ca096b018ad486b3b3c74b17bec09ab4edab721cbefec3/typeguard-4.5.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5b/69/93b34728cc386efdde0c342f8c680b9187dea7beb7adaf6b58a0713be101/mpld3-0.5.12-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5c/01/6cb4d63c6b6933be4b7945b2f64638336420f04ea71ca5b9a7539c008bc5/scippnexus-26.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/61/2b/e260d50e64690d2a9e405d52ccd18a63c286c5088937dd0107cb23eb3195/diffpy_utils-3.7.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/63/9f/724f66a48309dd97a2ff58f491d6ffd925f35d1278a5e55dc9a5ac6a156b/scippneutron-26.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/64/a8/c09fbe44b12fa919c5bfe0afb71e60d1231a7dc93405e54c30496c57c9d3/arviz-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/69/d1/705e6c19b437a4105bf3b9ae7945fcfc3ad2abb73d14bae0a3f2d58b305b/arviz_base-1.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/6c/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/9e/43/53afb8ba17218f19b77c7834128566c5bbb100a0ad9ba2e8e89d089d7079/autopep8-2.3.2-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/3e/5c/fb93d3092640a24dfb7bd7727a24016d7c01774ca013e60efd3f683c8002/backrefs-7.0-py314-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/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/0d/fe/6bea5c9162869c5beba5d9c8abbed835ec85bf1ec1fba05a3822325c45f3/build-1.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6e/94/be70f8ee9c45f2f62b39a1f0e9303bc20e138a8f3b8e50ffd89498e177e1/mkdocstrings-1.0.4-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/72/b9/313e8f2f2e9517ae050a692ae7b3e4b3f17cc5e6dfea0db51fe14e586580/jinja2_ansible_filters-1.3.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/76/8e/56ccb09c7232a55403a7637caa21922f3b65901a37f5e8bdb405d0de0946/mike-2.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/78/3c/2a612b95ddbb9a6bdcb47b7a93c4884f74c6ff22356b2f7b213b16e65c35/pycifstar-0.3.0.tar.gz + - pypi: https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.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/7b/67/b1944235474aac3f0b0e1b232ce49547f9f9461ca4b943df1b88da5d3f1d/bumps-1.0.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/c2/20/193faab46a68ea550587331a698c3dca8099f8901d10937c4443135c7ed9/chardet-7.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7b/e3/d81b065a2d866a33a541ac63a2a4cc5737e03ce2379ac3191c98bb8867e3/trove_classifiers-2026.5.7.17-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7e/85/545a951eecc270fcd688288c600017e2050a1aacb56c711d208586d3e470/pymdown_extensions-10.21.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/85/d7/9b6ac05350ab7f7d3a730ff143ff3e2cada54514117c37be37e26dc91242/docstripy-0.7.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/04/5f/9ff93450ba96b09c7c2b3f81c94de31c89f92292f1380261bd7195bea4ea/contourpy-1.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/98/ba/fc20faf5b2bb04615fa906f8daecfe896e6f7a56b34debd93c4a4d63e6e5/copier-9.15.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/22/e5/06b1f88f42a5a99df42ce61208bdec3bddb3d261412874280a19796fc09c/coverage-7.13.5-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/b2/e6/65abe97bbd42eb6ef73d3a58566ce89b097ae049511b7d9708288714a798/crysfml-0.6.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/8a/06/2c1bd1ee9eee3e65b0b6395dcf960e71b11576995eae3b4ab9ec63d89bea/essreduce-26.4.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8a/a1/8d812e53a5da1687abb10445275d41a8b13adb781bbf7196ddbcf8d88505/lazy_loader-0.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8d/c0/fdf9d3ee103ce66a55f0532835ad5e154226c5222423c6636ba049dc42fc/traittypes-0.2.3-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8f/5e/f1e1dd319e35e962a4e00b33150a8868b6329cc1d19fd533436ba5488f09/uncertainties-3.2.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/90/ad/cba91b3bcf04073e4d1655a5c1710ef3f457f56f7d1b79dcc3d72f4dd912/plotly-6.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/91/4c/e0ce1ef95d4000ebc1c11801f9b944fa5910ecc15b5e351865763d8657f8/graphviz-0.21-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/93/f7/d00d9b4a0313a6be3a3e0818e6375e15da6d7076f4ae47d1324e7ca986a1/radon-6.0.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/96/5d/0c59079aa7ef34980a5925a06a90ad2b7c94e486c194b3527d557cabb042/cryspy-0.11.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/4a/f3/00bb1e867fba351e2d784170955713bee200c43ea306c59f30bd7e748192/dask-2026.3.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/c7/a0/5ff05d1919ca249508012cad89f08fdc6cfbdaa15b41651c5fe6dffaf1d3/dfo_ls-1.6.5-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/33/eb/f9f1ded8e4db9638f9530c3782eb01f5ab04945f4cb9e597a51c203fa4c5/diffpy_pdffit2-1.6.0.tar.gz + - pypi: https://files.pythonhosted.org/packages/99/31/6cf181011dc738c33bf6ba7aea2e8e1d3c1f71b7dab1942f3054f66f6202/asteval-1.0.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/99/37/e8730c3587a65eb5645d4aba2d27aae48e8003614d6aaf15dda67f702f1f/bidict-0.23.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9e/43/53afb8ba17218f19b77c7834128566c5bbb100a0ad9ba2e8e89d089d7079/autopep8-2.3.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a3/58/35da89ee790598a0700ea49b2a66594140f44dec458c07e8e3d4979137fc/ply-3.11-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a3/59/1bd6d7428d6ed9106efbb8c52310c60fd04f6672490f452aeaa3829aa436/greenlet-3.5.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/a4/39/3a0ae5b0edb66e61bb0e8bc53a503495cba5892297ae21faf6ba0525e681/varname-1.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a4/ce/3b6fee91c85626eaf769d617f1be9d2e15c1cca027bbdeb2e0d751469355/verspec-0.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/a8/4e/c09876f08fa9faaa5e1178f3d77b7af3f343258689bd6f3b72593b2f74e3/mkdocs_markdownextradata_plugin-0.2.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/aa/54/0cce26da03a981f949bb8449c9778537f75f5917c172e1d2992ff25cb57d/python_engineio-4.13.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ab/b5/36c712098e6191d1b4e349304ef73a8d06aed77e56ceaac8c0a306c7bda1/jupyterlab_widgets-3.0.16-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/af/46/661159ad844034ba8b3f4e0516215c41e4ee17db4213d13a82227670764f/sciline-25.11.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/52/bc858b1665d0dec3a2511f4e6f5c18ea85c0977563d624d597c95d6d0fd7/jupyterquiz-2.9.6.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b4/5e/bf11645aebb9af7d8d35927c40d3855816a0855c799e8156eeca8d632c90/diffpy_structure-3.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/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/b7/6f/a05a317a66fee0aad270011461f1a63a453ed12471249f172f7d2e2bc7b4/python_discovery-1.3.1-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/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bd/c9/989f4034fb46841208de7aeeac2c6d8300745ab4f28c42f629ba77c2d916/aiohttp-3.13.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/bf/34/1fe99124be59579ebd24316522e1da780979c856977b142c0dcd878b0a2d/spglib-2.6.0.tar.gz - pypi: https://files.pythonhosted.org/packages/bf/50/98b146aea0f1cd7531d25f12bea69fa9ce8d1662124f93fb30dc4511b65e/docstring_parser_fork-0.0.14-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/85/d7/9b6ac05350ab7f7d3a730ff143ff3e2cada54514117c37be37e26dc91242/docstripy-0.7.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/fa/bc/8b8ec5a4bfc5b9cf3ce27a118339e994f88410be5677c96493e0ea28e76d/dunamai-1.26.1-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/c1/d4/59e74daffcb57a07668852eeeb6035af9f32cbfd7a1d2511f17d2fe6a738/smmap-5.0.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c2/20/193faab46a68ea550587331a698c3dca8099f8901d10937c4443135c7ed9/chardet-7.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/c4/6d/82e65254354ba651dc966775270a9bbc02414a3eb3f1704e6c87dab2ea83/essdiffraction-26.5.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/8a/06/2c1bd1ee9eee3e65b0b6395dcf960e71b11576995eae3b4ab9ec63d89bea/essreduce-26.4.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/28/63/cd0c3b26afe60995a5295f37c246a93d454023726c3261cfbb3559969bb9/fonttools-4.62.1-cp314-cp314-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/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/d5/0c/043d5e551459da400957a1395e0febbf771446ff34291afcbe3d8be2a279/fsspec-2026.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c6/3d/020a6b6248c3d4a37797db068256f0b3f15b01bc481327ba888c50309aa8/mkdocs_plugin_inline_svg-0.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c7/a0/5ff05d1919ca249508012cad89f08fdc6cfbdaa15b41651c5fe6dffaf1d3/dfo_ls-1.6.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-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/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-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/d5/0c/043d5e551459da400957a1395e0febbf771446ff34291afcbe3d8be2a279/fsspec-2026.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d7/51/ec641c26e6dca1b25a7d2035ba6ecb7c884ef1a100a9e42fbe4ce4405139/coverage-7.14.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/d8/8b/e2bbeb42068f0c48899e8eddd34902afc0f7429d4d2a152d2dc2670dc661/pythreejs-2.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/db/bc/83e112abc66cd466c6b83f99118035867cecd41802f8d044638aa78a106e/locket-1.0.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/dc/83/6d810a8a9ebc9c307989b418840c20e46907c74d707beb67ab566773e6fc/xarray-2026.4.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/dc/b7/901d837999a9350a7773289f7760cb473d4ba01fdc6ebae0ff2553d269ac/ncrystal_python-4.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e4/bc/daa30c02069eeac5b9198985ba42f5d65ca71bed6705b18329e51d352b7c/plopp-26.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e5/04/c5bb20d64417d20cba0105277235c51969444fa873000fbc26ac0a3fc5a8/gemmi-0.7.5-cp314-cp314-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/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/fd/6f/b842bfa6f21d6f87c57f9abf7194225e55279d96d869775e19e9f7236fc5/gitpython-3.1.49-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/a3/59/1bd6d7428d6ed9106efbb8c52310c60fd04f6672490f452aeaa3829aa436/greenlet-3.5.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/11/8c/c9138d881c79aa0ea9ed83cbd58d5ca75624378b38cee225dcf5c42cc91f/griffelib-2.0.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e7/f9/b06c934a6aa8bc91f566bd2a214fd04c30506c2d9e2b6b171953216a65b6/kiwisolver-1.5.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/e8/31/bf1a0803d077e679cfeee5f2f67290a0fa79c7385b5d9a8c17b9db2c48f0/ruff-0.15.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/eb/be/b257e12f9710819fde40adc972578bee6b72c5992da1bc8369bef2597756/nbmake-1.5.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ee/8c/83087ebc47ab0396ce092363001fa37c17153119ee282700c0713a195853/prettytable-3.17.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f1/16/d905e7f53e661ce2c24686c38048d8e2b750ffc4350009d41c4e6c6c9826/h5py-3.16.0-cp314-cp314-manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/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/f1/2c/3850985d4c64048dec7b826f8a803e135b52b11b4c81c9cd4326b1ca15ab/ncrystal_core-4.4.2-py3-none-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/f1/5b/e63c877c4c94382b66de5045e08ec8cd960e8a4d22f0d62a4dfb1f9e5ac6/ipydatawidgets-4.3.5-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/56/6d/0d9848617b9f753b87f214f1c682592f7ca42de085f564352f10f0843026/ipywidgets-8.1.8-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/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.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/f4/34/a9dbe051de88a63eb7408ea66630bac38e72f7f6077d4be58737106860d9/virtualenv-21.3.3-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/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/e7/f9/b06c934a6aa8bc91f566bd2a214fd04c30506c2d9e2b6b171953216a65b6/kiwisolver-1.5.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/8a/a1/8d812e53a5da1687abb10445275d41a8b13adb781bbf7196ddbcf8d88505/lazy_loader-0.5-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/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/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/32/5d/f7e914f7d9325abff4057cee62c0fa70263683189f774473cbfb534cd13b/matplotlib-3.10.9-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/fb/86/dd6e5db36df29e76c7a7699123569a4a18c1623ce68d826ed96c62643cae/mdit_py_plugins-0.5.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/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/76/8e/56ccb09c7232a55403a7637caa21922f3b65901a37f5e8bdb405d0de0946/mike-2.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/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/13/95/cf3f7fe4910cf0365fa8ea0c731f4b8a624d97cd76ea777913ac8d0868e2/mkdocs_jupyter-0.26.3-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/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/6e/94/be70f8ee9c45f2f62b39a1f0e9303bc20e138a8f3b8e50ffd89498e177e1/mkdocstrings-1.0.4-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/2f/40/dc34d1a8d5f1e51fc64640b62b191684da52ca469da9cd74e84936ffa4a6/msgpack-1.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/fe/3b/8ec5074bcfc450fe84273713b4b0a0dd47c0249358f5d82eb8104ffe2520/multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/d0/69/f24d3d1c38ad69e256138b4ec2452a8c7cf66be49dc214771ae99dd4f0a0/narwhals-2.20.0-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/20/16/e777eadfa0c0305878c36fae1d5e6db474fbb15dae202b9ec378809dfb4d/nbstripout-0.9.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/1a/66/8ce2af42feeba7a85b573e615a360ca1e204c75f6c8d4c5641140b1bfd17/ncrystal-4.3.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/0b/71/3b54e97c28cdf4993c8317d62ac1be655667455f129cf6162591d56aed89/ncrystal_core-4.3.4-py3-none-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/74/10/6434f57fa65651672534ce34a1f40ddbc7b880658ce50a8f9d7ab0830719/ncrystal_python-4.3.4-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/98/7c/21252050676612625449b4807d6b695b9ce8a7c9e1c197ee6216c8a65c7c/numpy-2.4.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.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/88/3cdd54fa279341afa10acf8d2b503556b1375245dccc9315659f795dd2e9/pandas-3.0.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/11/8d/d2532ad2a603ca2b93ad9f5135732124e57811d0168155852f37fbce2458/pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/70/7a/be4bd8bcbb24ea475856dd68159d78b03b2bb53dae369f69c9606b8888f5/pip-26.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e4/bc/daa30c02069eeac5b9198985ba42f5d65ca71bed6705b18329e51d352b7c/plopp-26.4.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/90/ad/cba91b3bcf04073e4d1655a5c1710ef3f457f56f7d1b79dcc3d72f4dd912/plotly-6.7.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/2a/2d/d4bf65e47cea8ff2c794a600c4fd1273a7902f268757c531e0ee9f18aa58/pooch-1.9.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ee/8c/83087ebc47ab0396ce092363001fa37c17153119ee282700c0713a195853/prettytable-3.17.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b2/f2/889ad4b2408f72fe1a4f6a19491177b30ea7bf1a0fd5f17050ca08cfc882/propcache-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/f6/f0/10642828a8dfb741e5f3fbaac830550a518a775c7fff6f04a007259b0548/py-1.11.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/25/7d/cea3531f77df694ac7f169378250d85f19f69b09a5f4fa45f650837ae7cc/py3dmol-2.5.4-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e0/a9/023730ba63db1e494a271cb018dcd361bd2c917ba7004c3e49d5daf795a2/py_cpuinfo-9.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/58/e0/f1871f520c359e4e3a2eb7437c9e7e792bb6c356414e8617937561167caf/pycifrw-5.0.1.tar.gz - - 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/f3/0a/fd7d723f8f8153418fb40cf9c940e82004fce7e987026b08a68a36dd3fe7/pydantic-2.13.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/fd/86/ef96a4c6e79e7a2d0410826a68fbc0eccc0fd44aa733be199d5fcac3bb87/pydantic_core-2.46.3-cp314-cp314-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/f7/27/a2fc51a4a122dfd1015e921ae9d22fee3d20b0b8080d9a704578bf9deece/pymdown_extensions-10.21.2-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/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/33/29/e756e715a48959f1c0045342088d7ca9762a2f509b945f362a316e9412b7/pytest_benchmark-5.2.3-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/d8/db/795879cc3ddfe338599bddea6388cc5100b088db0a4caf6e6c1af1c27e04/python_discovery-1.2.2-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/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/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.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/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e8/39/c61d193b8a1daaa8977f7dea9e8d8ba866e02ea7b65d32f6861693aa4c12/ruff-0.15.12-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/43/fe/ad0ecbe2393cb690a4b3100a8fea47ecfdb49f6e06f40cf2f626635adc0c/scipp-26.3.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/82/9b/cf7e6f157c53d2c8bc0165580350c53939aea3b2a8390ec8e07f0adb82de/scippneutron-26.4.1-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/0b/2e/7eea398450457ecb54e18e9d10110993fa65561c4f3add5e8eccd2b9cd41/scipy-1.17.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.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/c1/d4/59e74daffcb57a07668852eeeb6035af9f32cbfd7a1d2511f17d2fe6a738/smmap-5.0.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/bf/34/1fe99124be59579ebd24316522e1da780979c856977b142c0dcd878b0a2d/spglib-2.6.0.tar.gz - - pypi: https://files.pythonhosted.org/packages/2e/84/efc7c0bf3a1c5eef81d397f6fddac855becdbb11cb38ff957888603014a7/sqlalchemy-2.0.49-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.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/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/f6/f0/10642828a8dfb741e5f3fbaac830550a518a775c7fff6f04a007259b0548/py-1.11.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f7/00/bbca25f8a2372465cdf93138c1e1e38dff045fb0afef1488f395d0afcb3b/ncrystal-4.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fa/bc/8b8ec5a4bfc5b9cf3ce27a118339e994f88410be5677c96493e0ea28e76d/dunamai-1.26.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/8d/c0/fdf9d3ee103ce66a55f0532835ad5e154226c5222423c6636ba049dc42fc/traittypes-0.2.3-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d6/bb/fbdc4e57731efb86b4209ffdd2519520782bf27b3c961beac3e5c20d2b87/trove_classifiers-2026.4.28.13-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/91/88/b55b3117287a8540b76dbdd87733808d4d01c8067a3b339408c250bb3600/typeguard-4.5.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-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/b7/ee/e9c95cda829131f71a8dff5ce0406059fd16e591c074414e31ada19ba7c3/validate_pyproject-0.25-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a4/39/3a0ae5b0edb66e61bb0e8bc53a503495cba5892297ae21faf6ba0525e681/varname-1.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/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/4b/eb/03bfb1299d4c4510329e470f13f9a4ce793df7fcb5a2fd3510f911066f61/virtualenv-21.3.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/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/42/2b/fef67d616931055bf3d6764885990a3ac647d68734a2d6a9e1d13de437a2/yarl-1.23.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - - pypi: ./ + - pypi: https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fe/3b/8ec5074bcfc450fe84273713b4b0a0dd47c0249358f5d82eb8104ffe2520/multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl 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/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.13.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/appnope-0.1.4-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/argon2-cffi-25.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/argon2-cffi-bindings-25.1.0-py314h0612a62_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/arrow-1.4.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.18.0-pyhcf101f3_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/backports.zstd-1.3.0-py314h680f03e_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/backports.zstd-1.5.0-py314h680f03e_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.14.3-pyha770c72_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.3.0-pyhcf101f3_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-with-css-6.3.0-hbca2aae_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-python-1.2.0-py314h3daef5d_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-hd037594_9.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/c-ares-1.34.6-hc919400_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.4.22-hbd8a1cb_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cached-property-1.5.2-hd8ed1ab_1.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/cached_property-1.5.2-pyha770c72_1.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2026.4.22-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cffi-2.0.0-py314h44086f9_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.7-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.3-pyhe01879c_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.14.4-py314hd8ed1ab_100.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/debugpy-1.8.20-py314he609de1_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/decorator-5.2.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/defusedxml-0.7.1-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/fqdn-1.5.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/gsl-2.8-h8d0574d_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/h11-0.16.0-pyhcf101f3_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/httpcore-1.0.9-pyh29332c3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/httpx-0.28.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/icu-78.3-hef89b57_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.13-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.8.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.2.0-pyh5552912_1.conda @@ -403,82 +401,45 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.8.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.9.1-pyhc90fa1f_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_events-0.12.1-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.17.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.18.2-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server_terminals-0.5.4-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.5.7-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_pygments-0.3.0-pyhd8ed1ab_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_server-2.28.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/krb5-1.22.2-h385eeb1_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/lark-1.3.1-pyhd8ed1ab_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-6_hb0561ab_openblas.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-22.1.4-h55c6f16_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libedit-3.1.20250104-pl5321hafb1f1b_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libev-4.33-h93a5062_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.7.5-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.3-h8088a28_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libmpdec-4.0.0-h84a0fba_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libnghttp2-1.68.1-h8f3e76b_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopenblas-0.3.32-openmp_he657e61_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsodium-1.0.21-h1a92334_3.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.53.0-h1b79a29_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libuv-1.51.0-h6caf38d_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.2-h8088a28_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/llvm-openmp-22.1.4-hc7d1edf_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/markupsafe-3.0.3-py314h6e9b3f0_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.2.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mistune-3.2.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/msgspec-0.21.1-py314h6c2aa35_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.2.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mistune-3.2.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/nbclient-0.10.4-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/nbconvert-core-7.17.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/nbformat-5.10.4-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.6-h1d4f5a5_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/nodejs-25.8.2-h7039424_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/notebook-shim-0.2.4-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.6.2-hd24854e_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/overrides-7.7.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.2-pyhc364b38_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pandocfilters-1.5.0-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.6-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.7-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pexpect-4.9.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pixi-kernel-0.7.1-pyhbbac1ac_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.9.6-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/prometheus_client-0.25.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.52-pyha770c72_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/psutil-7.2.2-py314ha14b1ff_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ptyprocess-0.7.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyobjc-core-12.1-py314h3a4d195_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyobjc-framework-cocoa-12.1-py314h36abed7_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.14.4-h4c637c5_100_cp314.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.21.2-pyhe01879c_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.14.4-h4df99d1_100.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-json-logger-3.2.1-pyh332efcf_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2026.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.14-8_cp314.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyyaml-6.0.3-py314h6e9b3f0_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyzmq-27.1.0-py312h022ad19_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.3-h46df422_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.37.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.33.1-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.34.2-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/returns-0.27.0-pyhc364b38_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3339-validator-0.1.4-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3986-validator-0.1.1-pyh9f0ad1d_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3987-syntax-1.1.0-pyhe01879c_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/rpds-py-0.30.0-py314haad56a0_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/send2trash-2.1.0-pyh5552912_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-82.0.1-pyh332efcf_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda @@ -487,231 +448,275 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyhc90fa1f_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tinycss2-1.4.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h010d191_3.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.1-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tornado-6.5.5-py314h6c2aa35_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.15.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.15.0-h396c80c_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing_utils-0.1.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/uri-template-1.3.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.6.3-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.6.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.7.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.7.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/webcolors-25.10.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/webencodings-0.5.1-pyhd8ed1ab_3.conda - conda: https://conda.anaconda.org/conda-forge/noarch/websocket-client-1.9.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/_openmp_mutex-4.5-7_kmp_llvm.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/argon2-cffi-bindings-25.1.0-py314h0612a62_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-python-1.2.0-py314h3daef5d_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-hd037594_9.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/c-ares-1.34.6-hc919400_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cffi-2.0.0-py314h44086f9_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/debugpy-1.8.20-py314he609de1_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-78.3-hef89b57_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/krb5-1.22.2-h385eeb1_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libabseil-20260107.1-cxx17_h2062a1b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libblas-3.11.0-7_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-7_hb0561ab_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-22.1.5-h55c6f16_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libedit-3.1.20250104-pl5321hafb1f1b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libev-4.33-h93a5062_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.8.0-hf6b4638_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.5.2-hcf2aa1b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgcc-15.2.0-hcbb3090_19.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran-15.2.0-h07b0088_19.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran5-15.2.0-hdae7583_19.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.3-h8088a28_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libmpdec-4.0.0-h84a0fba_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libnghttp2-1.68.1-h8f3e76b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopenblas-0.3.33-openmp_he657e61_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsodium-1.0.21-h1a92334_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.53.1-h1b79a29_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libuv-1.51.0-h6caf38d_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.2-h8088a28_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/llvm-openmp-22.1.5-hc7d1edf_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/markupsafe-3.0.3-py314h6e9b3f0_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/msgspec-0.21.1-py314h6c2aa35_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.6-h1d4f5a5_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/nodejs-25.8.2-h7039424_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.6.2-hd24854e_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/psutil-7.2.2-py314ha14b1ff_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyobjc-core-12.1-py314h3a4d195_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyobjc-framework-cocoa-12.1-py314h36abed7_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.14.4-h4c637c5_100_cp314.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyyaml-6.0.3-py314h6e9b3f0_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyzmq-27.1.0-py312h022ad19_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.3-h46df422_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/rpds-py-0.30.0-py314haad56a0_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h010d191_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tornado-6.5.5-py314h6c2aa35_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/yaml-0.2.5-h925e9cb_3.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zeromq-4.3.5-h4818236_10.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.1-pyhcf101f3_0.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/9b/91/cc8cc78a111826c54743d88651e1687008133c37e5ee615fee9b57990fac/aiohttp-3.13.5-cp314-cp314-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/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/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/9e/43/53afb8ba17218f19b77c7834128566c5bbb100a0ad9ba2e8e89d089d7079/autopep8-2.3.2-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/3e/5c/fb93d3092640a24dfb7bd7727a24016d7c01774ca013e60efd3f683c8002/backrefs-7.0-py314-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/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/0d/fe/6bea5c9162869c5beba5d9c8abbed835ec85bf1ec1fba05a3822325c45f3/build-1.5.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/7b/67/b1944235474aac3f0b0e1b232ce49547f9f9461ca4b943df1b88da5d3f1d/bumps-1.0.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/51/ac/b9d68ebddfe1b02c77af5bf81120e12b036b4432dc6af7a303d90e2bc38b/chardet-7.4.3-cp314-cp314-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-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/fd/e1/3542a9cb596cadd76fcef413f19c79216e002623158befe6daa03dbfa88c/contourpy-1.3.3-cp314-cp314-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/98/ba/fc20faf5b2bb04615fa906f8daecfe896e6f7a56b34debd93c4a4d63e6e5/copier-9.15.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e2/3e/51dff36d99ae14639a133d9b164d63e628532e2974d8b1edb99dd1ebc733/coverage-7.13.5-cp314-cp314-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/d0/42/bf7fb4d923a15b99b678ecb3bdcc02d336ee34baa876c6f41c5c55038b9c/crysfml-0.6.1-cp314-cp314-macosx_14_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/96/5d/0c59079aa7ef34980a5925a06a90ad2b7c94e486c194b3527d557cabb042/cryspy-0.11.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/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/4a/f3/00bb1e867fba351e2d784170955713bee200c43ea306c59f30bd7e748192/dask-2026.3.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/c7/a0/5ff05d1919ca249508012cad89f08fdc6cfbdaa15b41651c5fe6dffaf1d3/dfo_ls-1.6.5-py3-none-any.whl + - pypi: . + - pypi: https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/f1/58c14b37525dc075f3bdf149251f079723049a9f1c82eb48835a0e6b8db3/diffpy_pdffit2-1.6.0-cp314-cp314-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/b4/5e/bf11645aebb9af7d8d35927c40d3855816a0855c799e8156eeca8d632c90/diffpy_structure-3.4.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/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/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/85/d7/9b6ac05350ab7f7d3a730ff143ff3e2cada54514117c37be37e26dc91242/docstripy-0.7.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/fa/bc/8b8ec5a4bfc5b9cf3ce27a118339e994f88410be5677c96493e0ea28e76d/dunamai-1.26.1-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/c4/6d/82e65254354ba651dc966775270a9bbc02414a3eb3f1704e6c87dab2ea83/essdiffraction-26.5.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/8a/06/2c1bd1ee9eee3e65b0b6395dcf960e71b11576995eae3b4ab9ec63d89bea/essreduce-26.4.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/36/f0/2888cdac391807d68d90dcb16ef858ddc1b5309bfc6966195a459dd326e2/fonttools-4.62.1-cp314-cp314-macosx_10_15_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/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/d5/0c/043d5e551459da400957a1395e0febbf771446ff34291afcbe3d8be2a279/fsspec-2026.4.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d5/08/c2409cb01d5368dcfedcbaffa7d044cc8957d57a9d0855244a5eb4709d30/funcy-2.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/06/41/4e70dea1d0311016c0b0b1c53a24a266f9f8a34c6bc1af0f17cfca20aa1d/gemmi-0.7.5-cp314-cp314-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/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/fd/6f/b842bfa6f21d6f87c57f9abf7194225e55279d96d869775e19e9f7236fc5/gitpython-3.1.49-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/07/c7/deb8c5e604404dbf10a3808a858946ca3547692ff6316b698945bb72177e/python_socketio-5.16.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0b/f9/f15c95d6b200167cb22c5eca5eecfa9d28a8ee3f74095f1cd2345c71f2f9/pydoclint-0.8.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0d/12/bbce9472f489cb5c4c23b0d13e5c59c37c1aab11b7ac637dfe6bbdccebe7/copier-9.15.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0d/1f/d398de1612f7a611e22d743280339c9af4903675635e41be3370091c704b/arviz_stats-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0d/fe/6bea5c9162869c5beba5d9c8abbed835ec85bf1ec1fba05a3822325c45f3/build-1.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0f/0e/0eb94e64f5badef67f11fe1e448dde2a44f00940d8949f4adf71d560552e/scipp-26.3.1-cp314-cp314-macosx_14_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/11/8c/c9138d881c79aa0ea9ed83cbd58d5ca75624378b38cee225dcf5c42cc91f/griffelib-2.0.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/6a/b7/9366ed44ced9b7ef357ab48c94205280276db9d7f064aa3012a97227e966/h5py-3.16.0-cp314-cp314-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/12/aa/fb2a0649fdeef5ab7072d221e8f4df164098792c813af6c87e2581cfa860/mpltoolbox-26.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/12/c9/6869a1dcf4aaf309b9543ec070be3ec3adebee7c9bec9af8c230494134b9/interrogate-1.7.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/56/6d/0d9848617b9f753b87f214f1c682592f7ca42de085f564352f10f0843026/ipywidgets-8.1.8-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/f4/a4/61adb19f3c74b0dc0e411de4f06ebef564b1f179928f9dffcbd4b378f2ef/jupyter_notebook_parser-0.1.4-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ab/b5/36c712098e6191d1b4e349304ef73a8d06aed77e56ceaac8c0a306c7bda1/jupyterlab_widgets-3.0.16-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/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/49/b2/97980f3ad4fae37dd7fe31626e2bf75fbf8bdf5d303950ec1fab39a12da8/kiwisolver-1.5.0-cp314-cp314-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/8a/a1/8d812e53a5da1687abb10445275d41a8b13adb781bbf7196ddbcf8d88505/lazy_loader-0.5-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/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/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/41/86/86231232fff41c9f8e4a1a7d7a597d349a02527109c3af7d618366122139/matplotlib-3.10.9-cp314-cp314-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/fb/86/dd6e5db36df29e76c7a7699123569a4a18c1623ce68d826ed96c62643cae/mdit_py_plugins-0.5.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/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/76/8e/56ccb09c7232a55403a7637caa21922f3b65901a37f5e8bdb405d0de0946/mike-2.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/13/95/cf3f7fe4910cf0365fa8ea0c731f4b8a624d97cd76ea777913ac8d0868e2/mkdocs_jupyter-0.26.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/19/d4/225027a913621a879b429a043674aa35220e6ce67785acad4f7bd0c4ff33/xarray_einstats-0.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1b/9f/38ff9e57a2eade7bf9dfee5eae17f39fc0e998658050279cbb14d97d36d9/msgpack-1.1.2-cp314-cp314-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/1c/59/964ecb8008722d27d8a835baea81f56a91cea8e097b3be992bc6ccde6367/versioningit-3.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1d/77/928ea2e70641ca177a11140062cc5840d421795f2e82749d408d0cce900a/narwhals-2.21.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/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1f/7e/c2cfe0bdbec1f5ce2bd92e03311038e1c491dfd54824606f38a61167a3f0/crysfml-0.6.2-cp314-cp314-macosx_14_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/20/16/e777eadfa0c0305878c36fae1d5e6db474fbb15dae202b9ec378809dfb4d/nbstripout-0.9.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/20/7a/1c6e3562dfd8950adbb11ffbc65d21e7c89d01a6e4f137fa981056de25c5/gitpython-3.1.50-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/24/f9/e8242b68362bffe6fb536c8db5076861466fc780f0f1b479fc4ffbebb128/yarl-1.23.0-cp314-cp314-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/25/1c/196c610ff4c6782d697ba780ebdc1616be143213701bf22c1a270f3bf7dd/numpy-2.4.5-cp314-cp314-macosx_14_0_arm64.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/27/d2/23d25e3f247b328be58d04a4c9f894178a0d1eda7d42867cfb388adaf416/fonttools-4.63.0-cp314-cp314-macosx_10_15_universal2.whl + - pypi: https://files.pythonhosted.org/packages/28/88/4789719fbbe166d12d345b3ac66b96105f10001b16e00a9765ba29261a21/nbqa-1.9.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/28/de/a3e710469772c6a89595fc52816da05c1e164b4c866a89e3cb82fb1b67c5/mkdocs_autorefs-1.4.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/13/95/cf3f7fe4910cf0365fa8ea0c731f4b8a624d97cd76ea777913ac8d0868e2/mkdocs_jupyter-0.26.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a8/4e/c09876f08fa9faaa5e1178f3d77b7af3f343258689bd6f3b72593b2f74e3/mkdocs_markdownextradata_plugin-0.2.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2a/2d/d4bf65e47cea8ff2c794a600c4fd1273a7902f268757c531e0ee9f18aa58/pooch-1.9.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/01/bc663630c510822c95c47a66af9fa7a443c295b47d5f041e5e6ae62ef659/mkdocs_material-9.7.6-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/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/6e/94/be70f8ee9c45f2f62b39a1f0e9303bc20e138a8f3b8e50ffd89498e177e1/mkdocstrings-1.0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2f/c8/005d1de3af80f54411703d1263a0b9d31276411ec9f273d9432c59b17299/arviz_plots-1.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/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/1b/9f/38ff9e57a2eade7bf9dfee5eae17f39fc0e998658050279cbb14d97d36d9/msgpack-1.1.2-cp314-cp314-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/f1/4f/733c48f270565d78b4544f2baddc2fb2a245e5a8640254b12c36ac7ac68e/multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/d0/69/f24d3d1c38ad69e256138b4ec2452a8c7cf66be49dc214771ae99dd4f0a0/narwhals-2.20.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/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/20/16/e777eadfa0c0305878c36fae1d5e6db474fbb15dae202b9ec378809dfb4d/nbstripout-0.9.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/1a/66/8ce2af42feeba7a85b573e615a360ca1e204c75f6c8d4c5641140b1bfd17/ncrystal-4.3.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a0/0d/df49ae8af94215db241701f692786a2e85c3ac4557aafd829270c54fb1fa/ncrystal_core-4.3.4-py3-none-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/74/10/6434f57fa65651672534ce34a1f40ddbc7b880658ce50a8f9d7ab0830719/ncrystal_python-4.3.4-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/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/33/b2/986d1220f6ee931e338d272bc1f3ec02cfe5f9b5fad84e95afdad57f1ebc/format_docstring-0.2.7-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/33/f0/3fe8c6e69135a845f4106f2ff8b6805638d4e85c264e70114e8126689587/tokenize_rt-6.2.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/38/7e/7b91c89a4cf0f543a83be978657afb20c86af6d725253e319589dcc4ce52/lmfit-1.3.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/38/8b/7ec325b4e9e78beefc2d025b01ee8a2fde771ef7c957c3bff99b9e1fbffa/xraydb-4.5.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3a/eb/fea4d1d51c49832120f7f285d07306db3960f423a2612c6057caf3e8196f/pip-26.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3c/26/1062c7ec1b053db9e499b4d2d5bc231743201b74051c973dadeac80a8f43/questionary-2.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3e/17/1f31d8562e6f970d64911f1abc330d233bc0c0601411cf7e19c1292be6da/spdx_headers-1.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3e/5c/fb93d3092640a24dfb7bd7727a24016d7c01774ca013e60efd3f683c8002/backrefs-7.0-py314-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3f/0e/fa3b193432cfc60c93b42f3be03365f5f909d2b3ea410295cf36df739e31/widgetsnbextension-4.0.15-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3f/d0/7b958df957e4827837b590944008f0b28078f552b451f7407b4b3d54f574/asciichartpy-1.5.25-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/41/86/86231232fff41c9f8e4a1a7d7a597d349a02527109c3af7d618366122139/matplotlib-3.10.9-cp314-cp314-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/42/d9/27b13bc9419bf5dae02905b348f16ca827646cd76244ddd326f1a8139a6a/cyclebane-24.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/49/b2/97980f3ad4fae37dd7fe31626e2bf75fbf8bdf5d303950ec1fab39a12da8/kiwisolver-1.5.0-cp314-cp314-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/4a/f3/00bb1e867fba351e2d784170955713bee200c43ea306c59f30bd7e748192/dask-2026.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4c/65/b4b86e5fa07543bfbbcdc6c9f7f9f561e66a5f3539992e3009973f2b1314/jupytext-1.19.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/51/ac/b9d68ebddfe1b02c77af5bf81120e12b036b4432dc6af7a303d90e2bc38b/chardet-7.4.3-cp314-cp314-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/52/59/0782e51887ac6b07ffd1570e0364cf901ebc36345fea669969d2084baebb/simple_websocket-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/55/33/bf28f618c0a9597d14e0b9ee7d1e0622faff738d44fe986ee287cdf1b8d0/sqlalchemy-2.0.49-cp314-cp314-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/56/6d/0d9848617b9f753b87f214f1c682592f7ca42de085f564352f10f0843026/ipywidgets-8.1.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/58/e0/f1871f520c359e4e3a2eb7437c9e7e792bb6c356414e8617937561167caf/pycifrw-5.0.1.tar.gz + - pypi: https://files.pythonhosted.org/packages/5b/29/74eeb4d3f3ae61ca096b018ad486b3b3c74b17bec09ab4edab721cbefec3/typeguard-4.5.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5b/69/93b34728cc386efdde0c342f8c680b9187dea7beb7adaf6b58a0713be101/mpld3-0.5.12-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5c/01/6cb4d63c6b6933be4b7945b2f64638336420f04ea71ca5b9a7539c008bc5/scippnexus-26.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/61/2b/e260d50e64690d2a9e405d52ccd18a63c286c5088937dd0107cb23eb3195/diffpy_utils-3.7.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/63/9f/724f66a48309dd97a2ff58f491d6ffd925f35d1278a5e55dc9a5ac6a156b/scippneutron-26.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/63/b1/4260d67d6bd85e58a66b72d54ce15d5de789b6f3870cc6bedf8ff9667401/propcache-0.5.2-cp314-cp314-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/64/a8/c09fbe44b12fa919c5bfe0afb71e60d1231a7dc93405e54c30496c57c9d3/arviz-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/68/10/bf2d6738d72748b961a3751ab89522d58c54efc36a8e1a12161216cd45cf/pandas-3.0.3-cp314-cp314-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/69/d1/705e6c19b437a4105bf3b9ae7945fcfc3ad2abb73d14bae0a3f2d58b305b/arviz_base-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6a/b7/9366ed44ced9b7ef357ab48c94205280276db9d7f064aa3012a97227e966/h5py-3.16.0-cp314-cp314-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/6c/25/4f103d1bedb3593718713b3f743df7b3ff3fc68d36d6666c30265ef59c8a/ase-3.28.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6e/94/be70f8ee9c45f2f62b39a1f0e9303bc20e138a8f3b8e50ffd89498e177e1/mkdocstrings-1.0.4-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/72/b9/313e8f2f2e9517ae050a692ae7b3e4b3f17cc5e6dfea0db51fe14e586580/jinja2_ansible_filters-1.3.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/76/8e/56ccb09c7232a55403a7637caa21922f3b65901a37f5e8bdb405d0de0946/mike-2.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/78/3c/2a612b95ddbb9a6bdcb47b7a93c4884f74c6ff22356b2f7b213b16e65c35/pycifstar-0.3.0.tar.gz + - pypi: https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.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/7b/67/b1944235474aac3f0b0e1b232ce49547f9f9461ca4b943df1b88da5d3f1d/bumps-1.0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7b/e3/d81b065a2d866a33a541ac63a2a4cc5737e03ce2379ac3191c98bb8867e3/trove_classifiers-2026.5.7.17-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7e/85/545a951eecc270fcd688288c600017e2050a1aacb56c711d208586d3e470/pymdown_extensions-10.21.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/85/d7/9b6ac05350ab7f7d3a730ff143ff3e2cada54514117c37be37e26dc91242/docstripy-0.7.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/91/fb/287076b2614e1d1044235f50f03748f31fa287e3dbe6abeb35cdfa351eca/numpy-2.4.4-cp314-cp314-macosx_14_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/8a/06/2c1bd1ee9eee3e65b0b6395dcf960e71b11576995eae3b4ab9ec63d89bea/essreduce-26.4.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8a/a1/8d812e53a5da1687abb10445275d41a8b13adb781bbf7196ddbcf8d88505/lazy_loader-0.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8d/c0/fdf9d3ee103ce66a55f0532835ad5e154226c5222423c6636ba049dc42fc/traittypes-0.2.3-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8f/5e/f1e1dd319e35e962a4e00b33150a8868b6329cc1d19fd533436ba5488f09/uncertainties-3.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/95/25/bdb9326c3b5455f8d4d3549fce7abcf967259de146fe2cf7a82368141948/pandas-3.0.2-cp314-cp314-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ba/8c/1a9e46228571de18f8e28f16fabdfc20212a5d019f3e3303452b3f0a580d/pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/70/7a/be4bd8bcbb24ea475856dd68159d78b03b2bb53dae369f69c9606b8888f5/pip-26.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e4/bc/daa30c02069eeac5b9198985ba42f5d65ca71bed6705b18329e51d352b7c/plopp-26.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/90/ad/cba91b3bcf04073e4d1655a5c1710ef3f457f56f7d1b79dcc3d72f4dd912/plotly-6.7.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/91/4c/e0ce1ef95d4000ebc1c11801f9b944fa5910ecc15b5e351865763d8657f8/graphviz-0.21-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/93/f7/d00d9b4a0313a6be3a3e0818e6375e15da6d7076f4ae47d1324e7ca986a1/radon-6.0.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/96/5d/0c59079aa7ef34980a5925a06a90ad2b7c94e486c194b3527d557cabb042/cryspy-0.11.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/99/31/6cf181011dc738c33bf6ba7aea2e8e1d3c1f71b7dab1942f3054f66f6202/asteval-1.0.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/99/37/e8730c3587a65eb5645d4aba2d27aae48e8003614d6aaf15dda67f702f1f/bidict-0.23.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9b/91/cc8cc78a111826c54743d88651e1687008133c37e5ee615fee9b57990fac/aiohttp-3.13.5-cp314-cp314-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9e/43/53afb8ba17218f19b77c7834128566c5bbb100a0ad9ba2e8e89d089d7079/autopep8-2.3.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-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/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a3/58/35da89ee790598a0700ea49b2a66594140f44dec458c07e8e3d4979137fc/ply-3.11-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/2a/2d/d4bf65e47cea8ff2c794a600c4fd1273a7902f268757c531e0ee9f18aa58/pooch-1.9.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ee/8c/83087ebc47ab0396ce092363001fa37c17153119ee282700c0713a195853/prettytable-3.17.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b2/fa/89a8ef0468d5833a23fff277b143d0573897cf75bd56670a6d28126c7d68/propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.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/25/7d/cea3531f77df694ac7f169378250d85f19f69b09a5f4fa45f650837ae7cc/py3dmol-2.5.4-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e0/a9/023730ba63db1e494a271cb018dcd361bd2c917ba7004c3e49d5daf795a2/py_cpuinfo-9.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/58/e0/f1871f520c359e4e3a2eb7437c9e7e792bb6c356414e8617937561167caf/pycifrw-5.0.1.tar.gz - - 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/f3/0a/fd7d723f8f8153418fb40cf9c940e82004fce7e987026b08a68a36dd3fe7/pydantic-2.13.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/24/35/e4066358a22e3e99519db370494c7528f5a2aa1367370e80e27e20283543/pydantic_core-2.46.3-cp314-cp314-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/f7/27/a2fc51a4a122dfd1015e921ae9d22fee3d20b0b8080d9a704578bf9deece/pymdown_extensions-10.21.2-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/a4/39/3a0ae5b0edb66e61bb0e8bc53a503495cba5892297ae21faf6ba0525e681/varname-1.0.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/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.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/aa/54/0cce26da03a981f949bb8449c9778537f75f5917c172e1d2992ff25cb57d/python_engineio-4.13.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ab/b5/36c712098e6191d1b4e349304ef73a8d06aed77e56ceaac8c0a306c7bda1/jupyterlab_widgets-3.0.16-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/af/46/661159ad844034ba8b3f4e0516215c41e4ee17db4213d13a82227670764f/sciline-25.11.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/52/bc858b1665d0dec3a2511f4e6f5c18ea85c0977563d624d597c95d6d0fd7/jupyterquiz-2.9.6.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b4/5e/bf11645aebb9af7d8d35927c40d3855816a0855c799e8156eeca8d632c90/diffpy_structure-3.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/6f/a05a317a66fee0aad270011461f1a63a453ed12471249f172f7d2e2bc7b4/python_discovery-1.3.1-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/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ba/8c/1a9e46228571de18f8e28f16fabdfc20212a5d019f3e3303452b3f0a580d/pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.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/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/33/29/e756e715a48959f1c0045342088d7ca9762a2f509b945f362a316e9412b7/pytest_benchmark-5.2.3-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/bf/34/1fe99124be59579ebd24316522e1da780979c856977b142c0dcd878b0a2d/spglib-2.6.0.tar.gz + - pypi: https://files.pythonhosted.org/packages/bf/50/98b146aea0f1cd7531d25f12bea69fa9ce8d1662124f93fb30dc4511b65e/docstring_parser_fork-0.0.14-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c1/d4/59e74daffcb57a07668852eeeb6035af9f32cbfd7a1d2511f17d2fe6a738/smmap-5.0.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c4/6d/82e65254354ba651dc966775270a9bbc02414a3eb3f1704e6c87dab2ea83/essdiffraction-26.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c6/3d/020a6b6248c3d4a37797db068256f0b3f15b01bc481327ba888c50309aa8/mkdocs_plugin_inline_svg-0.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c7/a0/5ff05d1919ca249508012cad89f08fdc6cfbdaa15b41651c5fe6dffaf1d3/dfo_ls-1.6.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c7/ea/7988934c8e3e3418aa043f70421817df28d06aef50bfd85f5ad3ec6e70f1/ncrystal_core-4.4.2-py3-none-macosx_11_0_arm64.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/d8/db/795879cc3ddfe338599bddea6388cc5100b088db0a4caf6e6c1af1c27e04/python_discovery-1.2.2-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/07/c7/deb8c5e604404dbf10a3808a858946ca3547692ff6316b698945bb72177e/python_socketio-5.16.1-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/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-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/d3/19/43f5f2e568dddde567fc41f8471f9432c09563e19d3e617a48cfa52f8f0a/ruff-0.15.13-py3-none-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d5/08/c2409cb01d5368dcfedcbaffa7d044cc8957d57a9d0855244a5eb4709d30/funcy-2.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d5/0c/043d5e551459da400957a1395e0febbf771446ff34291afcbe3d8be2a279/fsspec-2026.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.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/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.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/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/aa/a4/f828e9718d3dce1f5f11c39c4f65afd32783c8b2aebb2e3d259e492c47bd/ruff-0.15.12-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/0f/0e/0eb94e64f5badef67f11fe1e448dde2a44f00940d8949f4adf71d560552e/scipp-26.3.1-cp314-cp314-macosx_14_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/82/9b/cf7e6f157c53d2c8bc0165580350c53939aea3b2a8390ec8e07f0adb82de/scippneutron-26.4.1-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/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/db/7b/8624a203326675d7746a254083a187398090a179335b2e4a20e2ddc46e83/scipy-1.17.1-cp314-cp314-macosx_14_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz + - pypi: https://files.pythonhosted.org/packages/db/bc/83e112abc66cd466c6b83f99118035867cecd41802f8d044638aa78a106e/locket-1.0.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/dc/83/6d810a8a9ebc9c307989b418840c20e46907c74d707beb67ab566773e6fc/xarray-2026.4.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/dc/b7/901d837999a9350a7773289f7760cb473d4ba01fdc6ebae0ff2553d269ac/ncrystal_python-4.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-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/c1/d4/59e74daffcb57a07668852eeeb6035af9f32cbfd7a1d2511f17d2fe6a738/smmap-5.0.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/bf/34/1fe99124be59579ebd24316522e1da780979c856977b142c0dcd878b0a2d/spglib-2.6.0.tar.gz - - pypi: https://files.pythonhosted.org/packages/55/33/bf28f618c0a9597d14e0b9ee7d1e0622faff738d44fe986ee287cdf1b8d0/sqlalchemy-2.0.49-cp314-cp314-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.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/e4/bc/daa30c02069eeac5b9198985ba42f5d65ca71bed6705b18329e51d352b7c/plopp-26.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/eb/be/b257e12f9710819fde40adc972578bee6b72c5992da1bc8369bef2597756/nbmake-1.5.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ee/8c/83087ebc47ab0396ce092363001fa37c17153119ee282700c0713a195853/prettytable-3.17.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f1/4f/733c48f270565d78b4544f2baddc2fb2a245e5a8640254b12c36ac7ac68e/multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/f1/5b/e63c877c4c94382b66de5045e08ec8cd960e8a4d22f0d62a4dfb1f9e5ac6/ipydatawidgets-4.3.5-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.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/f3/9b/4165a1d56ddc302a0e2d518fd9d412a4fd0b57562618c78c5f21c57194f5/coverage-7.14.0-cp314-cp314-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/f4/34/a9dbe051de88a63eb7408ea66630bac38e72f7f6077d4be58737106860d9/virtualenv-21.3.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f4/a4/61adb19f3c74b0dc0e411de4f06ebef564b1f179928f9dffcbd4b378f2ef/jupyter_notebook_parser-0.1.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f5/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/f6/f0/10642828a8dfb741e5f3fbaac830550a518a775c7fff6f04a007259b0548/py-1.11.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f7/00/bbca25f8a2372465cdf93138c1e1e38dff045fb0afef1488f395d0afcb3b/ncrystal-4.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fa/bc/8b8ec5a4bfc5b9cf3ce27a118339e994f88410be5677c96493e0ea28e76d/dunamai-1.26.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/8d/c0/fdf9d3ee103ce66a55f0532835ad5e154226c5222423c6636ba049dc42fc/traittypes-0.2.3-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d6/bb/fbdc4e57731efb86b4209ffdd2519520782bf27b3c961beac3e5c20d2b87/trove_classifiers-2026.4.28.13-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/91/88/b55b3117287a8540b76dbdd87733808d4d01c8067a3b339408c250bb3600/typeguard-4.5.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-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/b7/ee/e9c95cda829131f71a8dff5ce0406059fd16e591c074414e31ada19ba7c3/validate_pyproject-0.25-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a4/39/3a0ae5b0edb66e61bb0e8bc53a503495cba5892297ae21faf6ba0525e681/varname-1.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/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/4b/eb/03bfb1299d4c4510329e470f13f9a4ce793df7fcb5a2fd3510f911066f61/virtualenv-21.3.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz - - 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/24/f9/e8242b68362bffe6fb536c8db5076861466fc780f0f1b479fc4ffbebb128/yarl-1.23.0-cp314-cp314-macosx_11_0_arm64.whl - - pypi: ./ + - pypi: https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fd/e1/3542a9cb596cadd76fcef413f19c79216e002623158befe6daa03dbfa88c/contourpy-1.3.3-cp314-cp314-macosx_11_0_arm64.whl win-64: - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.13.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/argon2-cffi-25.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/argon2-cffi-bindings-25.1.0-py314h5a2d7ad_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/arrow-1.4.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.18.0-pyhcf101f3_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/backports.zstd-1.3.0-py314h680f03e_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/backports.zstd-1.5.0-py314h680f03e_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.14.3-pyha770c72_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.3.0-pyhcf101f3_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-with-css-6.3.0-hbca2aae_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/brotli-python-1.2.0-py314he701e3d_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h0ad9c76_9.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.4.22-h4c7d964_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cached-property-1.5.2-hd8ed1ab_1.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/cached_property-1.5.2-pyha770c72_1.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2026.4.22-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/cffi-2.0.0-py314h5a2d7ad_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.7-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.3-pyhe01879c_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.14.4-py314hd8ed1ab_100.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/debugpy-1.8.20-py314hb98de8c_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/decorator-5.2.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/defusedxml-0.7.1-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/fqdn-1.5.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/gsl-2.8-h5b8d9c4_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/h11-0.16.0-pyhcf101f3_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda @@ -735,670 +740,385 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.8.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.9.1-pyh6dadd2b_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_events-0.12.1-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.17.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.18.2-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server_terminals-0.5.4-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.5.7-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_pygments-0.3.0-pyhd8ed1ab_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_server-2.28.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/krb5-1.22.2-h0ea6238_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/lark-1.3.1-pyhd8ed1ab_0.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.5-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.3-hfd05255_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libmpdec-4.0.0-hfd05255_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libsodium-1.0.21-h6a83c73_3.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.53.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.3-h692994f_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libxml2-2.15.3-hbc0d294_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.4-h4fa8253_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/markupsafe-3.0.3-py314h2359020_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.2.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mistune-3.2.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/mkl-2025.3.1-hac47afa_12.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/msgspec-0.21.1-py314h5a2d7ad_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.2.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mistune-3.2.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/nbclient-0.10.4-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/nbconvert-core-7.17.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/nbformat-5.10.4-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/nodejs-25.8.2-h80d1838_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/notebook-shim-0.2.4-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/onemkl-license-2025.3.1-h57928b3_12.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.6.2-hf411b9b_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/overrides-7.7.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.2-pyhc364b38_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pandocfilters-1.5.0-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.6-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.7-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pixi-kernel-0.7.1-pyhbbac1ac_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.9.6-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/prometheus_client-0.25.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.52-pyha770c72_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/psutil-7.2.2-py314hc5dbbe4_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyh09c184e_7.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/python-3.14.4-h4b44e0e_100_cp314.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.21.2-pyhe01879c_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.14.4-h4df99d1_100.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-json-logger-3.2.1-pyh332efcf_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2026.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.14-8_cp314.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/pywin32-311-py314h8f8f202_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/pywinpty-2.0.15-py314h51f0985_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/pyyaml-6.0.3-py314h2359020_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/pyzmq-27.1.0-py312h343a6d4_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.37.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.33.1-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.34.2-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/returns-0.27.0-pyhc364b38_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3339-validator-0.1.4-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3986-validator-0.1.1-pyh9f0ad1d_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3987-syntax-1.1.0-pyhe01879c_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/rpds-py-0.30.0-py314h9f07db2_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/send2trash-2.1.0-pyh6dadd2b_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-82.0.1-pyh332efcf_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sniffio-1.3.1-pyhd8ed1ab_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.8.3-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/tbb-2022.3.0-h3155e25_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyh6dadd2b_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tinycss2-1.4.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h6ed50ae_3.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.1-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/tornado-6.5.5-py314h5a2d7ad_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.15.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.15.0-h396c80c_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing_utils-0.1.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.26100.0-h57928b3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/uri-template-1.3.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.6.3-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-h41ae7f8_34.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.44.35208-h818238b_34.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.44.35208-h818238b_34.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.6.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.7.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.7.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/webcolors-25.10.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/webencodings-0.5.1-pyhd8ed1ab_3.conda - conda: https://conda.anaconda.org/conda-forge/noarch/websocket-client-1.9.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/win_inet_pton-1.1.0-pyh7428d3b_8.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/argon2-cffi-bindings-25.1.0-py314h5a2d7ad_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/brotli-python-1.2.0-py314he701e3d_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h0ad9c76_9.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/cffi-2.0.0-py314h5a2d7ad_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/debugpy-1.8.20-py314hb98de8c_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/krb5-1.22.2-h0ea6238_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libblas-3.11.0-7_h8455456_mkl.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libcblas-3.11.0-7_h2a3cdd5_mkl.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.8.0-hac47afa_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libffi-3.5.2-h3d046cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libhwloc-2.13.0-default_h049141e_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.3-hfd05255_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libmpdec-4.0.0-hfd05255_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libsodium-1.0.21-h6a83c73_3.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.53.1-hf5d6505_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libwinpthread-12.0.0.r4.gg4f2fc60ca-h57928b3_10.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libxml2-16-2.15.3-h692994f_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libxml2-2.15.3-hbc0d294_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.5-h4fa8253_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/markupsafe-3.0.3-py314h2359020_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/mkl-2026.0.0-hac47afa_906.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/msgspec-0.21.1-py314h5a2d7ad_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/nodejs-25.8.2-h80d1838_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/onemkl-license-2026.0.0-h57928b3_906.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.6.2-hf411b9b_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/psutil-7.2.2-py314hc5dbbe4_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/python-3.14.4-h4b44e0e_100_cp314.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pywin32-311-py314h8f8f202_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pywinpty-2.0.15-py314h51f0985_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pyyaml-6.0.3-py314h2359020_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pyzmq-27.1.0-py312h343a6d4_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/rpds-py-0.30.0-py314h9f07db2_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/tbb-2023.0.0-hd3d4ead_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/win-64/tornado-6.5.5-py314h5a2d7ad_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.26100.0-h57928b3_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-h41ae7f8_36.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.44.35208-h818238b_36.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.44.35208-h818238b_36.conda - conda: https://conda.anaconda.org/conda-forge/win-64/winpty-0.4.3-4.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/win-64/yaml-0.2.5-h6a83c73_3.conda - conda: https://conda.anaconda.org/conda-forge/win-64/zeromq-4.3.5-h507cc87_10.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_6.conda + - pypi: . + - pypi: https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/07/c7/deb8c5e604404dbf10a3808a858946ca3547692ff6316b698945bb72177e/python_socketio-5.16.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0b/f9/f15c95d6b200167cb22c5eca5eecfa9d28a8ee3f74095f1cd2345c71f2f9/pydoclint-0.8.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0c/53/b50773ecf1d1e4a5858ee13011e30317ba02639ae4a1411a34967951fc9b/crysfml-0.6.2-cp314-cp314-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/0d/12/bbce9472f489cb5c4c23b0d13e5c59c37c1aab11b7ac637dfe6bbdccebe7/copier-9.15.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0d/1f/d398de1612f7a611e22d743280339c9af4903675635e41be3370091c704b/arviz_stats-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0d/fe/6bea5c9162869c5beba5d9c8abbed835ec85bf1ec1fba05a3822325c45f3/build-1.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/22/4d/eaedff67fc805aeba4ba746aec891b4b24cebb1a7d078084b6300f79d063/aiohttp-3.13.5-cp314-cp314-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/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/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/9e/43/53afb8ba17218f19b77c7834128566c5bbb100a0ad9ba2e8e89d089d7079/autopep8-2.3.2-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/3e/5c/fb93d3092640a24dfb7bd7727a24016d7c01774ca013e60efd3f683c8002/backrefs-7.0-py314-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/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/0d/fe/6bea5c9162869c5beba5d9c8abbed835ec85bf1ec1fba05a3822325c45f3/build-1.5.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/7b/67/b1944235474aac3f0b0e1b232ce49547f9f9461ca4b943df1b88da5d3f1d/bumps-1.0.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b1/2c/cad8b5e3623a987f3c930b68e2bdd06cfc388cd91cd42ed05f1227701b73/chardet-7.4.3-cp314-cp314-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/7d/c2/57f54b03d0f22d4044b8afb9ca0e184f8b1afd57b4f735c2fa70883dc601/contourpy-1.3.3-cp314-cp314-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/98/ba/fc20faf5b2bb04615fa906f8daecfe896e6f7a56b34debd93c4a4d63e6e5/copier-9.15.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/8d/b7/4758d4f73fb536347cc5e4ad63662f9d60ba9118cb6785e9616b2ce5d7fa/coverage-7.13.5-cp314-cp314-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/d9/1a/8a81b3a66f36969c8456f8af3a12f7d601fdd9cfed2ad5b4e72a2fb7ea8d/crysfml-0.6.1-cp314-cp314-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/96/5d/0c59079aa7ef34980a5925a06a90ad2b7c94e486c194b3527d557cabb042/cryspy-0.11.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/4a/f3/00bb1e867fba351e2d784170955713bee200c43ea306c59f30bd7e748192/dask-2026.3.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/c7/a0/5ff05d1919ca249508012cad89f08fdc6cfbdaa15b41651c5fe6dffaf1d3/dfo_ls-1.6.5-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/82/d0/26c81ffbe588f936d05f395da34046c66322e8067c9fd331c788c4f682f2/diffpy_pdffit2-1.6.0-cp314-cp314-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/b4/5e/bf11645aebb9af7d8d35927c40d3855816a0855c799e8156eeca8d632c90/diffpy_structure-3.4.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/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/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/85/d7/9b6ac05350ab7f7d3a730ff143ff3e2cada54514117c37be37e26dc91242/docstripy-0.7.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/fa/bc/8b8ec5a4bfc5b9cf3ce27a118339e994f88410be5677c96493e0ea28e76d/dunamai-1.26.1-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/c4/6d/82e65254354ba651dc966775270a9bbc02414a3eb3f1704e6c87dab2ea83/essdiffraction-26.5.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/8a/06/2c1bd1ee9eee3e65b0b6395dcf960e71b11576995eae3b4ab9ec63d89bea/essreduce-26.4.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/6b/67/74b070029043186b5dd13462c958cb7c7f811be0d2e634309d9a1ffb1505/fonttools-4.62.1-cp314-cp314-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/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/d5/0c/043d5e551459da400957a1395e0febbf771446ff34291afcbe3d8be2a279/fsspec-2026.4.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d5/08/c2409cb01d5368dcfedcbaffa7d044cc8957d57a9d0855244a5eb4709d30/funcy-2.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ff/1c/a28b27effb13a381fe077ea3e3e78f6debd6315f2b3edff67bbb93d0ef51/gemmi-0.7.5-cp314-cp314-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/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/fd/6f/b842bfa6f21d6f87c57f9abf7194225e55279d96d869775e19e9f7236fc5/gitpython-3.1.49-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/20/6b165108058767ee643c55c5c4904d591a830ee2b3c7dbd359828fbc829f/greenlet-3.5.0-cp314-cp314-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/11/8c/c9138d881c79aa0ea9ed83cbd58d5ca75624378b38cee225dcf5c42cc91f/griffelib-2.0.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/3e/14/615a450205e1b56d16c6783f5ccd116cde05550faad70ae077c955654a75/h5py-3.16.0-cp314-cp314-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/12/aa/fb2a0649fdeef5ab7072d221e8f4df164098792c813af6c87e2581cfa860/mpltoolbox-26.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/12/c9/6869a1dcf4aaf309b9543ec070be3ec3adebee7c9bec9af8c230494134b9/interrogate-1.7.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/56/6d/0d9848617b9f753b87f214f1c682592f7ca42de085f564352f10f0843026/ipywidgets-8.1.8-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/f4/a4/61adb19f3c74b0dc0e411de4f06ebef564b1f179928f9dffcbd4b378f2ef/jupyter_notebook_parser-0.1.4-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ab/b5/36c712098e6191d1b4e349304ef73a8d06aed77e56ceaac8c0a306c7bda1/jupyterlab_widgets-3.0.16-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/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/a3/36/4e551e8aa55c9188bca9abb5096805edbf7431072b76e2298e34fd3a3008/kiwisolver-1.5.0-cp314-cp314-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/8a/a1/8d812e53a5da1687abb10445275d41a8b13adb781bbf7196ddbcf8d88505/lazy_loader-0.5-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/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/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/ab/1a/5a4f747a8b271cbb024946d2dd3c913ab5032ba430626f8c3528ada96b4b/matplotlib-3.10.9-cp314-cp314-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/fb/86/dd6e5db36df29e76c7a7699123569a4a18c1623ce68d826ed96c62643cae/mdit_py_plugins-0.5.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/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/76/8e/56ccb09c7232a55403a7637caa21922f3b65901a37f5e8bdb405d0de0946/mike-2.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/13/95/cf3f7fe4910cf0365fa8ea0c731f4b8a624d97cd76ea777913ac8d0868e2/mkdocs_jupyter-0.26.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/19/d4/225027a913621a879b429a043674aa35220e6ce67785acad4f7bd0c4ff33/xarray_einstats-0.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1c/59/964ecb8008722d27d8a835baea81f56a91cea8e097b3be992bc6ccde6367/versioningit-3.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1d/77/928ea2e70641ca177a11140062cc5840d421795f2e82749d408d0cce900a/narwhals-2.21.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/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1f/28/3f8aa247d29d010547d52207395cb057ebd0a40b88f64bc1dbac9e17a729/scipp-26.3.1-cp314-cp314-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/20/16/e777eadfa0c0305878c36fae1d5e6db474fbb15dae202b9ec378809dfb4d/nbstripout-0.9.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/20/7a/1c6e3562dfd8950adbb11ffbc65d21e7c89d01a6e4f137fa981056de25c5/gitpython-3.1.50-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/22/4d/eaedff67fc805aeba4ba746aec891b4b24cebb1a7d078084b6300f79d063/aiohttp-3.13.5-cp314-cp314-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-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/28/88/4789719fbbe166d12d345b3ac66b96105f10001b16e00a9765ba29261a21/nbqa-1.9.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/28/de/a3e710469772c6a89595fc52816da05c1e164b4c866a89e3cb82fb1b67c5/mkdocs_autorefs-1.4.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/13/95/cf3f7fe4910cf0365fa8ea0c731f4b8a624d97cd76ea777913ac8d0868e2/mkdocs_jupyter-0.26.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a8/4e/c09876f08fa9faaa5e1178f3d77b7af3f343258689bd6f3b72593b2f74e3/mkdocs_markdownextradata_plugin-0.2.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2a/2d/d4bf65e47cea8ff2c794a600c4fd1273a7902f268757c531e0ee9f18aa58/pooch-1.9.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/01/bc663630c510822c95c47a66af9fa7a443c295b47d5f041e5e6ae62ef659/mkdocs_material-9.7.6-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/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/6e/94/be70f8ee9c45f2f62b39a1f0e9303bc20e138a8f3b8e50ffd89498e177e1/mkdocstrings-1.0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2f/c8/005d1de3af80f54411703d1263a0b9d31276411ec9f273d9432c59b17299/arviz_plots-1.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/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/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/33/b2/986d1220f6ee931e338d272bc1f3ec02cfe5f9b5fad84e95afdad57f1ebc/format_docstring-0.2.7-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/33/f0/3fe8c6e69135a845f4106f2ff8b6805638d4e85c264e70114e8126689587/tokenize_rt-6.2.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/38/55/792619469bab9882d8bbd5865d45a72f6478762d04a9af4bf0d08c503e95/pandas-3.0.3-cp314-cp314-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/38/7e/7b91c89a4cf0f543a83be978657afb20c86af6d725253e319589dcc4ce52/lmfit-1.3.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/38/8b/7ec325b4e9e78beefc2d025b01ee8a2fde771ef7c957c3bff99b9e1fbffa/xraydb-4.5.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3a/eb/fea4d1d51c49832120f7f285d07306db3960f423a2612c6057caf3e8196f/pip-26.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3c/26/1062c7ec1b053db9e499b4d2d5bc231743201b74051c973dadeac80a8f43/questionary-2.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3e/14/615a450205e1b56d16c6783f5ccd116cde05550faad70ae077c955654a75/h5py-3.16.0-cp314-cp314-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/3e/17/1f31d8562e6f970d64911f1abc330d233bc0c0601411cf7e19c1292be6da/spdx_headers-1.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3e/5c/fb93d3092640a24dfb7bd7727a24016d7c01774ca013e60efd3f683c8002/backrefs-7.0-py314-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3f/0e/fa3b193432cfc60c93b42f3be03365f5f909d2b3ea410295cf36df739e31/widgetsnbextension-4.0.15-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3f/d0/7b958df957e4827837b590944008f0b28078f552b451f7407b4b3d54f574/asciichartpy-1.5.25-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/42/d9/27b13bc9419bf5dae02905b348f16ca827646cd76244ddd326f1a8139a6a/cyclebane-24.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4a/f3/00bb1e867fba351e2d784170955713bee200c43ea306c59f30bd7e748192/dask-2026.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4b/39/f0e8ea762a764a9dc52aa7dabcfad51a354819de1f0d4652b6a1122424d6/scipy-1.17.1-cp314-cp314-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/4c/65/b4b86e5fa07543bfbbcdc6c9f7f9f561e66a5f3539992e3009973f2b1314/jupytext-1.19.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/52/59/0782e51887ac6b07ffd1570e0364cf901ebc36345fea669969d2084baebb/simple_websocket-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/56/6d/0d9848617b9f753b87f214f1c682592f7ca42de085f564352f10f0843026/ipywidgets-8.1.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/58/e0/f1871f520c359e4e3a2eb7437c9e7e792bb6c356414e8617937561167caf/pycifrw-5.0.1.tar.gz + - pypi: https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/5b/29/74eeb4d3f3ae61ca096b018ad486b3b3c74b17bec09ab4edab721cbefec3/typeguard-4.5.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5b/69/93b34728cc386efdde0c342f8c680b9187dea7beb7adaf6b58a0713be101/mpld3-0.5.12-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5c/01/6cb4d63c6b6933be4b7945b2f64638336420f04ea71ca5b9a7539c008bc5/scippnexus-26.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/61/2b/e260d50e64690d2a9e405d52ccd18a63c286c5088937dd0107cb23eb3195/diffpy_utils-3.7.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/61/d2/45c9defbaa1ea297035d9d4cce9e8f80daafbf19319c6007f157c6256ea9/propcache-0.5.2-cp314-cp314-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/63/9f/724f66a48309dd97a2ff58f491d6ffd925f35d1278a5e55dc9a5ac6a156b/scippneutron-26.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/64/a8/c09fbe44b12fa919c5bfe0afb71e60d1231a7dc93405e54c30496c57c9d3/arviz-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/69/d1/705e6c19b437a4105bf3b9ae7945fcfc3ad2abb73d14bae0a3f2d58b305b/arviz_base-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6c/25/4f103d1bedb3593718713b3f743df7b3ff3fc68d36d6666c30265ef59c8a/ase-3.28.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6e/94/be70f8ee9c45f2f62b39a1f0e9303bc20e138a8f3b8e50ffd89498e177e1/mkdocstrings-1.0.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/6e/f1/abd09c2ae91228c5f3998dbd7f41353def9eac64253de3c8105efa2082f7/msgpack-1.1.2-cp314-cp314-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/e0/bf/52f25716bbe93745595800f36fb17b73711f14da59ed0bb2eba141bc9f0f/multidict-6.7.1-cp314-cp314-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/d0/69/f24d3d1c38ad69e256138b4ec2452a8c7cf66be49dc214771ae99dd4f0a0/narwhals-2.20.0-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/20/16/e777eadfa0c0305878c36fae1d5e6db474fbb15dae202b9ec378809dfb4d/nbstripout-0.9.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/1a/66/8ce2af42feeba7a85b573e615a360ca1e204c75f6c8d4c5641140b1bfd17/ncrystal-4.3.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ab/14/d708fb7c6bf7e7be586c625840cc078a45e64dd6bffe8d60a7b17a22b24e/ncrystal_core-4.3.4-py3-none-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/74/10/6434f57fa65651672534ce34a1f40ddbc7b880658ce50a8f9d7ab0830719/ncrystal_python-4.3.4-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/fa/22/4cc31a62a6c7b74a8730e31a4274c5dc80e005751e277a2ce38e675e4923/numpy-2.4.4-cp314-cp314-win_amd64.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/db/60/aba6a38de456e7341285102bede27514795c1eaa353bc0e7638b6b785356/pandas-3.0.2-cp314-cp314-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/cd/8b/601e6566b957ca50e28725cb6c355c59c2c8609751efbecd980db44e0349/pillow-12.2.0-cp314-cp314-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/70/7a/be4bd8bcbb24ea475856dd68159d78b03b2bb53dae369f69c9606b8888f5/pip-26.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e4/bc/daa30c02069eeac5b9198985ba42f5d65ca71bed6705b18329e51d352b7c/plopp-26.4.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/90/ad/cba91b3bcf04073e4d1655a5c1710ef3f457f56f7d1b79dcc3d72f4dd912/plotly-6.7.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/72/b9/313e8f2f2e9517ae050a692ae7b3e4b3f17cc5e6dfea0db51fe14e586580/jinja2_ansible_filters-1.3.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/76/8e/56ccb09c7232a55403a7637caa21922f3b65901a37f5e8bdb405d0de0946/mike-2.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/78/3c/2a612b95ddbb9a6bdcb47b7a93c4884f74c6ff22356b2f7b213b16e65c35/pycifstar-0.3.0.tar.gz + - pypi: https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.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/2a/2d/d4bf65e47cea8ff2c794a600c4fd1273a7902f268757c531e0ee9f18aa58/pooch-1.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7b/67/b1944235474aac3f0b0e1b232ce49547f9f9461ca4b943df1b88da5d3f1d/bumps-1.0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7b/e3/d81b065a2d866a33a541ac63a2a4cc5737e03ce2379ac3191c98bb8867e3/trove_classifiers-2026.5.7.17-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7d/c2/57f54b03d0f22d4044b8afb9ca0e184f8b1afd57b4f735c2fa70883dc601/contourpy-1.3.3-cp314-cp314-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/7e/85/545a951eecc270fcd688288c600017e2050a1aacb56c711d208586d3e470/pymdown_extensions-10.21.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ee/8c/83087ebc47ab0396ce092363001fa37c17153119ee282700c0713a195853/prettytable-3.17.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/0c/2a/a758b47de253636e1b8aef181c0b4f4f204bf0dd964914fb2af90a95b49b/propcache-0.4.1-cp314-cp314-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/f6/f0/10642828a8dfb741e5f3fbaac830550a518a775c7fff6f04a007259b0548/py-1.11.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/25/7d/cea3531f77df694ac7f169378250d85f19f69b09a5f4fa45f650837ae7cc/py3dmol-2.5.4-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e0/a9/023730ba63db1e494a271cb018dcd361bd2c917ba7004c3e49d5daf795a2/py_cpuinfo-9.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/58/e0/f1871f520c359e4e3a2eb7437c9e7e792bb6c356414e8617937561167caf/pycifrw-5.0.1.tar.gz - - 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/f3/0a/fd7d723f8f8153418fb40cf9c940e82004fce7e987026b08a68a36dd3fe7/pydantic-2.13.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/20/eb/59980e5f1ae54a3b86372bd9f0fa373ea2d402e8cdcd3459334430f91e91/pydantic_core-2.46.3-cp314-cp314-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/f7/27/a2fc51a4a122dfd1015e921ae9d22fee3d20b0b8080d9a704578bf9deece/pymdown_extensions-10.21.2-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/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/33/29/e756e715a48959f1c0045342088d7ca9762a2f509b945f362a316e9412b7/pytest_benchmark-5.2.3-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/d8/db/795879cc3ddfe338599bddea6388cc5100b088db0a4caf6e6c1af1c27e04/python_discovery-1.2.2-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/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/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.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/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/33/f1/9614e03e1cdcbf9437570b5400ced8a720b5db22b28d8e0f1bda429f660d/ruff-0.15.12-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/1f/28/3f8aa247d29d010547d52207395cb057ebd0a40b88f64bc1dbac9e17a729/scipp-26.3.1-cp314-cp314-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/82/9b/cf7e6f157c53d2c8bc0165580350c53939aea3b2a8390ec8e07f0adb82de/scippneutron-26.4.1-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/4b/39/f0e8ea762a764a9dc52aa7dabcfad51a354819de1f0d4652b6a1122424d6/scipy-1.17.1-cp314-cp314-win_amd64.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/c1/d4/59e74daffcb57a07668852eeeb6035af9f32cbfd7a1d2511f17d2fe6a738/smmap-5.0.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/bf/34/1fe99124be59579ebd24316522e1da780979c856977b142c0dcd878b0a2d/spglib-2.6.0.tar.gz - - pypi: https://files.pythonhosted.org/packages/cf/4f/8297e4ed88e80baa1f5aa3c484a0ee29ef3c69c7582f206c916973b75057/sqlalchemy-2.0.49-cp314-cp314-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.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/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/82/d0/26c81ffbe588f936d05f395da34046c66322e8067c9fd331c788c4f682f2/diffpy_pdffit2-1.6.0-cp314-cp314-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/85/d7/9b6ac05350ab7f7d3a730ff143ff3e2cada54514117c37be37e26dc91242/docstripy-0.7.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8a/06/2c1bd1ee9eee3e65b0b6395dcf960e71b11576995eae3b4ab9ec63d89bea/essreduce-26.4.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8a/a1/8d812e53a5da1687abb10445275d41a8b13adb781bbf7196ddbcf8d88505/lazy_loader-0.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8d/c0/fdf9d3ee103ce66a55f0532835ad5e154226c5222423c6636ba049dc42fc/traittypes-0.2.3-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d6/bb/fbdc4e57731efb86b4209ffdd2519520782bf27b3c961beac3e5c20d2b87/trove_classifiers-2026.4.28.13-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/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-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/8f/5e/f1e1dd319e35e962a4e00b33150a8868b6329cc1d19fd533436ba5488f09/uncertainties-3.2.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/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/90/ad/cba91b3bcf04073e4d1655a5c1710ef3f457f56f7d1b79dcc3d72f4dd912/plotly-6.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/91/20/6b165108058767ee643c55c5c4904d591a830ee2b3c7dbd359828fbc829f/greenlet-3.5.0-cp314-cp314-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/91/4c/e0ce1ef95d4000ebc1c11801f9b944fa5910ecc15b5e351865763d8657f8/graphviz-0.21-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/93/f7/d00d9b4a0313a6be3a3e0818e6375e15da6d7076f4ae47d1324e7ca986a1/radon-6.0.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/96/5d/0c59079aa7ef34980a5925a06a90ad2b7c94e486c194b3527d557cabb042/cryspy-0.11.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/99/31/6cf181011dc738c33bf6ba7aea2e8e1d3c1f71b7dab1942f3054f66f6202/asteval-1.0.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/99/37/e8730c3587a65eb5645d4aba2d27aae48e8003614d6aaf15dda67f702f1f/bidict-0.23.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9e/43/53afb8ba17218f19b77c7834128566c5bbb100a0ad9ba2e8e89d089d7079/autopep8-2.3.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a3/36/4e551e8aa55c9188bca9abb5096805edbf7431072b76e2298e34fd3a3008/kiwisolver-1.5.0-cp314-cp314-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/a3/58/35da89ee790598a0700ea49b2a66594140f44dec458c07e8e3d4979137fc/ply-3.11-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a4/39/3a0ae5b0edb66e61bb0e8bc53a503495cba5892297ae21faf6ba0525e681/varname-1.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/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/4b/eb/03bfb1299d4c4510329e470f13f9a4ce793df7fcb5a2fd3510f911066f61/virtualenv-21.3.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/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/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.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/a9/5b/9b92f54c784c26e2a422e55a8d2607ab15b7ea3349e28359282f84f01d43/yarl-1.23.0-cp314-cp314-win_amd64.whl - - pypi: ./ + - pypi: https://files.pythonhosted.org/packages/aa/54/0cce26da03a981f949bb8449c9778537f75f5917c172e1d2992ff25cb57d/python_engineio-4.13.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/aa/92/d063df4d63d988b20d881856c74df76c0c1786229bb870f3a52af0981d4d/numpy-2.4.5-cp314-cp314-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/ab/1a/5a4f747a8b271cbb024946d2dd3c913ab5032ba430626f8c3528ada96b4b/matplotlib-3.10.9-cp314-cp314-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ab/a6/870a3e8a50590bb92be184ad928c2922f088b00d9dc5c5ec7b924ee08c22/ruff-0.15.13-py3-none-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/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/af/46/661159ad844034ba8b3f4e0516215c41e4ee17db4213d13a82227670764f/sciline-25.11.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b1/2c/cad8b5e3623a987f3c930b68e2bdd06cfc388cd91cd42ed05f1227701b73/chardet-7.4.3-cp314-cp314-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/52/bc858b1665d0dec3a2511f4e6f5c18ea85c0977563d624d597c95d6d0fd7/jupyterquiz-2.9.6.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b4/5e/bf11645aebb9af7d8d35927c40d3855816a0855c799e8156eeca8d632c90/diffpy_structure-3.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/6f/a05a317a66fee0aad270011461f1a63a453ed12471249f172f7d2e2bc7b4/python_discovery-1.3.1-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/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bf/34/1fe99124be59579ebd24316522e1da780979c856977b142c0dcd878b0a2d/spglib-2.6.0.tar.gz + - pypi: https://files.pythonhosted.org/packages/bf/50/98b146aea0f1cd7531d25f12bea69fa9ce8d1662124f93fb30dc4511b65e/docstring_parser_fork-0.0.14-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c1/d4/59e74daffcb57a07668852eeeb6035af9f32cbfd7a1d2511f17d2fe6a738/smmap-5.0.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c3/d4/98078064ccc76b45cb0f6c002452011e93c4bd26f6850344f0951cc1fe89/fonttools-4.63.0-cp314-cp314-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/c4/6d/82e65254354ba651dc966775270a9bbc02414a3eb3f1704e6c87dab2ea83/essdiffraction-26.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c6/3d/020a6b6248c3d4a37797db068256f0b3f15b01bc481327ba888c50309aa8/mkdocs_plugin_inline_svg-0.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c7/6b/6c02f55c2ce2f137ccca0986be7dd89bea31d5bee4346b4377fa3b8586df/ncrystal_core-4.4.2-py3-none-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/c7/a0/5ff05d1919ca249508012cad89f08fdc6cfbdaa15b41651c5fe6dffaf1d3/dfo_ls-1.6.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/cd/8b/601e6566b957ca50e28725cb6c355c59c2c8609751efbecd980db44e0349/pillow-12.2.0-cp314-cp314-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/cf/4f/8297e4ed88e80baa1f5aa3c484a0ee29ef3c69c7582f206c916973b75057/sqlalchemy-2.0.49-cp314-cp314-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/d2/f0/834e479e47e499b6478e807fb57b31cc2db696c4db30557bb6f5aea4a90b/mando-0.7.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d5/08/c2409cb01d5368dcfedcbaffa7d044cc8957d57a9d0855244a5eb4709d30/funcy-2.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d5/0c/043d5e551459da400957a1395e0febbf771446ff34291afcbe3d8be2a279/fsspec-2026.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d8/8b/e2bbeb42068f0c48899e8eddd34902afc0f7429d4d2a152d2dc2670dc661/pythreejs-2.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.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/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/dc/83/6d810a8a9ebc9c307989b418840c20e46907c74d707beb67ab566773e6fc/xarray-2026.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/dc/b7/901d837999a9350a7773289f7760cb473d4ba01fdc6ebae0ff2553d269ac/ncrystal_python-4.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e0/bf/52f25716bbe93745595800f36fb17b73711f14da59ed0bb2eba141bc9f0f/multidict-6.7.1-cp314-cp314-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e4/bc/daa30c02069eeac5b9198985ba42f5d65ca71bed6705b18329e51d352b7c/plopp-26.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/eb/be/b257e12f9710819fde40adc972578bee6b72c5992da1bc8369bef2597756/nbmake-1.5.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ee/8c/83087ebc47ab0396ce092363001fa37c17153119ee282700c0713a195853/prettytable-3.17.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f1/5b/e63c877c4c94382b66de5045e08ec8cd960e8a4d22f0d62a4dfb1f9e5ac6/ipydatawidgets-4.3.5-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.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/f4/34/a9dbe051de88a63eb7408ea66630bac38e72f7f6077d4be58737106860d9/virtualenv-21.3.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f4/a4/61adb19f3c74b0dc0e411de4f06ebef564b1f179928f9dffcbd4b378f2ef/jupyter_notebook_parser-0.1.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f5/57/2a154a69d6642860300bf8eb205d13131104991f2b1065bbb9075ac5c32e/tof-26.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f6/f0/10642828a8dfb741e5f3fbaac830550a518a775c7fff6f04a007259b0548/py-1.11.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f7/00/bbca25f8a2372465cdf93138c1e1e38dff045fb0afef1488f395d0afcb3b/ncrystal-4.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f9/58/6e1b8f52fdc3184b47dc5037f5070d83a3d11042db1594b02d2a44d786c8/coverage-7.14.0-cp314-cp314-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/fa/bc/8b8ec5a4bfc5b9cf3ce27a118339e994f88410be5677c96493e0ea28e76d/dunamai-1.26.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ff/1c/a28b27effb13a381fe077ea3e3e78f6debd6315f2b3edff67bbb93d0ef51/gemmi-0.7.5-cp314-cp314-win_amd64.whl py-312-env: channels: + - url: https://conda.anaconda.org/nodefaults/ - 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/_openmp_mutex-4.5-20_gnu.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.13.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/argon2-cffi-25.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/argon2-cffi-bindings-25.1.0-py312h4c3975b_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/arrow-1.4.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.3.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.18.0-pyhcf101f3_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/backports.zstd-1.3.0-py312h90b7ffd_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.14.3-pyha770c72_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.3.0-pyhcf101f3_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-with-css-6.3.0-hbca2aae_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/backports.zstd-1.5.0-py312h90b7ffd_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.2.0-py312hdb49522_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_9.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/c-ares-1.34.6-hb03c661_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.4.22-hbd8a1cb_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/cached-property-1.5.2-hd8ed1ab_1.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/cached_property-1.5.2-pyha770c72_1.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2026.4.22-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/cffi-2.0.0-py312h460c074_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.7-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.3-pyhe01879c_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.12.13-py312hd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/debugpy-1.8.20-py312h8285ef7_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/decorator-5.2.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/defusedxml-0.7.1-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/fqdn-1.5.1-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/gsl-2.8-hbf7d49c_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/h11-0.16.0-pyhcf101f3_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/httpcore-1.0.9-pyh29332c3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/httpx-0.28.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/icu-78.3-h33c6efd_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.13-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.8.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.2.0-pyha191276_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.13.0-pyh53cf698_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ipython_pygments_lexers-1.1.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/isoduration-20.11.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jedi-0.19.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/json5-0.14.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jsonpointer-3.1.1-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.26.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.9.1-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-with-format-nongpl-4.26.0-hcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter-lsp-2.3.1-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.8.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.9.1-pyhc90fa1f_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_events-0.12.1-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.17.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server_terminals-0.5.4-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.5.7-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_pygments-0.3.0-pyhd8ed1ab_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_server-2.28.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.3-hb9d3cd8_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.22.2-ha1258a1_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/lark-1.3.1-pyhd8ed1ab_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/libblas-3.11.0-7_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-6_h0358290_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.11.0-7_h0358290_openblas.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20250104-pl5321h7949ede_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libev-4.33-hd590300_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.5-hecca717_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.8.0-hecca717_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h3435931_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-he0feb66_18.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/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/libgcc-15.2.0-he0feb66_19.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.2.0-h69a702a_19.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.2.0-h69a702a_19.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.2.0-h68bc16d_19.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-he0feb66_19.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.3-hb03c661_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libnghttp2-1.68.1-h877daf1_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hb9d3cd8_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.32-pthreads_h94d23a6_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.33-pthreads_h94d23a6_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libsodium-1.0.21-h280c20c_3.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.53.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/libsqlite-3.53.1-h0c1763c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h934c35e_19.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.42-h5347b49_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libuv-1.51.0-hb03c661_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.2-h25fd6f3_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.3-py312h8a5da7c_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.2.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mistune-3.2.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/msgspec-0.21.1-py312h4c3975b_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/nbclient-0.10.4-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/nbconvert-core-7.17.1-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/nbformat-5.10.4-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.6-hdb14827_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/nodejs-25.8.2-he4ff34a_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/notebook-shim-0.2.4-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.2-h35e630c_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/overrides-7.7.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.2-pyhc364b38_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pandocfilters-1.5.0-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.6-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pexpect-4.9.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pixi-kernel-0.7.1-pyhbbac1ac_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.9.6-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/prometheus_client-0.25.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.52-pyha770c72_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/psutil-7.2.2-py312h5253ce2_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ptyprocess-0.7.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.12.13-hd63d673_0_cpython.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.21.2-pyhe01879c_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.12.13-hd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-json-logger-3.2.1-pyh332efcf_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2026.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.12-8_cp312.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.3-py312h8a5da7c_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pyzmq-27.1.0-py312hda471dd_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.3-h853b02a_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.37.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.33.1-pyhcf101f3_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/returns-0.27.0-pyhc364b38_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3339-validator-0.1.4-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3986-validator-0.1.1-pyh9f0ad1d_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3987-syntax-1.1.0-pyhe01879c_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/rpds-py-0.30.0-py312h868fb18_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/send2trash-2.1.0-pyha191276_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-82.0.1-pyh332efcf_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sniffio-1.3.1-pyhd8ed1ab_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.8.3-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyhc90fa1f_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tinycss2-1.4.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h366c992_103.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/tornado-6.5.5-py312h4c3975b_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.3-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.15.0-h396c80c_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/typing_utils-0.1.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/uri-template-1.3.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.6.3-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.6.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/webcolors-25.10.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/webencodings-0.5.1-pyhd8ed1ab_3.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/websocket-client-1.9.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h280c20c_3.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/zeromq-4.3.5-h41580af_10.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.1-pyhcf101f3_0.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/57/d8/8d44036d7eb7b6a8ec4c5494ea0c8c8b94fbc0ed3991c1a7adf230df03bf/aiohttp-3.13.5-cp312-cp312-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/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/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/9e/43/53afb8ba17218f19b77c7834128566c5bbb100a0ad9ba2e8e89d089d7079/autopep8-2.3.2-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/36/da/87912ddec6e06feffbaa3d7aa18fc6352bee2e8f1fee185d7d1690f8f4e8/backrefs-7.0-py312-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/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/0d/fe/6bea5c9162869c5beba5d9c8abbed835ec85bf1ec1fba05a3822325c45f3/build-1.5.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/7b/67/b1944235474aac3f0b0e1b232ce49547f9f9461ca4b943df1b88da5d3f1d/bumps-1.0.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e5/59/a32a241d861cf180853a11c8e5a67641cb1b2af13c3a5ccce83ec07e2c9f/chardet-7.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/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/cc/8f/ec6289987824b29529d0dfda0d74a07cec60e54b9c92f3c9da4c0ac732de/contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/98/ba/fc20faf5b2bb04615fa906f8daecfe896e6f7a56b34debd93c4a4d63e6e5/copier-9.15.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/8c/49/cd14b789536ac6a4778c453c6a2338bc0a2fb60c5a5a41b4008328b9acc1/coverage-7.13.5-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/06/fe/2a936f465588dc7ef28793c2917b60a1c0bd26b0b716f4e43b228763c74b/crysfml-0.6.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/96/5d/0c59079aa7ef34980a5925a06a90ad2b7c94e486c194b3527d557cabb042/cryspy-0.11.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/4a/f3/00bb1e867fba351e2d784170955713bee200c43ea306c59f30bd7e748192/dask-2026.3.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/c7/a0/5ff05d1919ca249508012cad89f08fdc6cfbdaa15b41651c5fe6dffaf1d3/dfo_ls-1.6.5-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/33/eb/f9f1ded8e4db9638f9530c3782eb01f5ab04945f4cb9e597a51c203fa4c5/diffpy_pdffit2-1.6.0.tar.gz - - pypi: https://files.pythonhosted.org/packages/b4/5e/bf11645aebb9af7d8d35927c40d3855816a0855c799e8156eeca8d632c90/diffpy_structure-3.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/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/85/d7/9b6ac05350ab7f7d3a730ff143ff3e2cada54514117c37be37e26dc91242/docstripy-0.7.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/fa/bc/8b8ec5a4bfc5b9cf3ce27a118339e994f88410be5677c96493e0ea28e76d/dunamai-1.26.1-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/c4/6d/82e65254354ba651dc966775270a9bbc02414a3eb3f1704e6c87dab2ea83/essdiffraction-26.5.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/8a/06/2c1bd1ee9eee3e65b0b6395dcf960e71b11576995eae3b4ab9ec63d89bea/essreduce-26.4.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/9b/8a/99c8b3c3888c5c474c08dbfd7c8899786de9604b727fcefb055b42c84bba/fonttools-4.62.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/33/b2/986d1220f6ee931e338d272bc1f3ec02cfe5f9b5fad84e95afdad57f1ebc/format_docstring-0.2.7-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/d5/0c/043d5e551459da400957a1395e0febbf771446ff34291afcbe3d8be2a279/fsspec-2026.4.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/e8/88/5a431cd1ea7587408a66947384b39beb2ab2bcc1c87b7c4082f05036719f/gemmi-0.7.5-cp312-cp312-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/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/fd/6f/b842bfa6f21d6f87c57f9abf7194225e55279d96d869775e19e9f7236fc5/gitpython-3.1.49-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/2b/e0/2e13df68f367e2f9960616927d60857dd7e56aaadd59a47c644216b2f920/greenlet-3.5.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/11/8c/c9138d881c79aa0ea9ed83cbd58d5ca75624378b38cee225dcf5c42cc91f/griffelib-2.0.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/9e/e9/1a19e42cd43cc1365e127db6aae85e1c671da1d9a5d746f4d34a50edb577/h5py-3.16.0-cp312-cp312-manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/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/f1/5b/e63c877c4c94382b66de5045e08ec8cd960e8a4d22f0d62a4dfb1f9e5ac6/ipydatawidgets-4.3.5-py2.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/72/b9/313e8f2f2e9517ae050a692ae7b3e4b3f17cc5e6dfea0db51fe14e586580/jinja2_ansible_filters-1.3.2-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/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/c4/13/680c54afe3e65767bed7ec1a15571e1a2f1257128733851ade24abcefbcc/kiwisolver-1.5.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/8a/a1/8d812e53a5da1687abb10445275d41a8b13adb781bbf7196ddbcf8d88505/lazy_loader-0.5-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/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/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/32/91/d024616abdba99e83120e07a20658976f6a343646710760c4a51df126029/matplotlib-3.10.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/fb/86/dd6e5db36df29e76c7a7699123569a4a18c1623ce68d826ed96c62643cae/mdit_py_plugins-0.5.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/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/76/8e/56ccb09c7232a55403a7637caa21922f3b65901a37f5e8bdb405d0de0946/mike-2.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/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/13/95/cf3f7fe4910cf0365fa8ea0c731f4b8a624d97cd76ea777913ac8d0868e2/mkdocs_jupyter-0.26.3-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/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/6e/94/be70f8ee9c45f2f62b39a1f0e9303bc20e138a8f3b8e50ffd89498e177e1/mkdocstrings-1.0.4-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/65/92/a5100f7185a800a5d29f8d14041f61475b9de465ffcc0f3b9fba606e4505/msgpack-1.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/f3/8d/5e5be3ced1d12966fefb5c4ea3b2a5b480afcea36406559442c6e31d4a48/multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/d0/69/f24d3d1c38ad69e256138b4ec2452a8c7cf66be49dc214771ae99dd4f0a0/narwhals-2.20.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/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/20/16/e777eadfa0c0305878c36fae1d5e6db474fbb15dae202b9ec378809dfb4d/nbstripout-0.9.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/1a/66/8ce2af42feeba7a85b573e615a360ca1e204c75f6c8d4c5641140b1bfd17/ncrystal-4.3.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/0b/71/3b54e97c28cdf4993c8317d62ac1be655667455f129cf6162591d56aed89/ncrystal_core-4.3.4-py3-none-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/74/10/6434f57fa65651672534ce34a1f40ddbc7b880658ce50a8f9d7ab0830719/ncrystal_python-4.3.4-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/0a/0d/0e3ecece05b7a7e87ab9fb587855548da437a061326fff64a223b6dcb78a/numpy-2.4.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.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/c4/a8/3a61a721472959ab0ce865ef05d10b0d6bfe27ce8801c99f33d4fa996e65/pandas-3.0.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/43/e3/fdc657359e919462369869f1c9f0e973f353f9a9ee295a39b1fea8ee1a77/pillow-12.2.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/70/7a/be4bd8bcbb24ea475856dd68159d78b03b2bb53dae369f69c9606b8888f5/pip-26.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e4/bc/daa30c02069eeac5b9198985ba42f5d65ca71bed6705b18329e51d352b7c/plopp-26.4.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/90/ad/cba91b3bcf04073e4d1655a5c1710ef3f457f56f7d1b79dcc3d72f4dd912/plotly-6.7.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/2a/2d/d4bf65e47cea8ff2c794a600c4fd1273a7902f268757c531e0ee9f18aa58/pooch-1.9.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ee/8c/83087ebc47ab0396ce092363001fa37c17153119ee282700c0713a195853/prettytable-3.17.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/46/4b/3aae6835b8e5f44ea6a68348ad90f78134047b503765087be2f9912140ea/propcache-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/f6/f0/10642828a8dfb741e5f3fbaac830550a518a775c7fff6f04a007259b0548/py-1.11.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/25/7d/cea3531f77df694ac7f169378250d85f19f69b09a5f4fa45f650837ae7cc/py3dmol-2.5.4-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e0/a9/023730ba63db1e494a271cb018dcd361bd2c917ba7004c3e49d5daf795a2/py_cpuinfo-9.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ae/61/3c1ea8c10bf4f6bf83c33a7f5b4a3143f4cc1f979859dec5498b6cc31900/pycifrw-5.0.1-cp312-cp312-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/f3/0a/fd7d723f8f8153418fb40cf9c940e82004fce7e987026b08a68a36dd3fe7/pydantic-2.13.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/5e/d5/e3d9717c9eba10855325650afd2a9cba8e607321697f18953af9d562da2f/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/87/6f/cc2b231dc78d8c3aaa674a676db190b8f8071c50134af8f8cf39b9b8e8df/pydoclint-0.8.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f7/27/a2fc51a4a122dfd1015e921ae9d22fee3d20b0b8080d9a704578bf9deece/pymdown_extensions-10.21.2-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/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/33/29/e756e715a48959f1c0045342088d7ca9762a2f509b945f362a316e9412b7/pytest_benchmark-5.2.3-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/d8/db/795879cc3ddfe338599bddea6388cc5100b088db0a4caf6e6c1af1c27e04/python_discovery-1.2.2-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/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/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.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/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e8/39/c61d193b8a1daaa8977f7dea9e8d8ba866e02ea7b65d32f6861693aa4c12/ruff-0.15.12-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/1e/e7/cd78635d0ece7e4d3393f2c1d2ebabf6ff4bd615da142891b1d42ad58abf/scipp-26.3.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/82/9b/cf7e6f157c53d2c8bc0165580350c53939aea3b2a8390ec8e07f0adb82de/scippneutron-26.4.1-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/01/8e/1e35281b8ab6d5d72ebe9911edcdffa3f36b04ed9d51dec6dd140396e220/scipy-1.17.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.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/c1/d4/59e74daffcb57a07668852eeeb6035af9f32cbfd7a1d2511f17d2fe6a738/smmap-5.0.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/33/75/98a7eb100dc5cfd20b019046452f08d5e67dfbacc71d8f28763d32426fd3/spglib-2.6.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/2c/fa/65fcae2ed62f84ab72cf89536c7c3217a156e71a2c111b1305ab6f0690e2/sqlalchemy-2.0.49-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.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/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/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-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/d6/bb/fbdc4e57731efb86b4209ffdd2519520782bf27b3c961beac3e5c20d2b87/trove_classifiers-2026.4.28.13-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/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-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/8f/5e/f1e1dd319e35e962a4e00b33150a8868b6329cc1d19fd533436ba5488f09/uncertainties-3.2.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/a4/39/3a0ae5b0edb66e61bb0e8bc53a503495cba5892297ae21faf6ba0525e681/varname-1.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/1c/59/964ecb8008722d27d8a835baea81f56a91cea8e097b3be992bc6ccde6367/versioningit-3.3.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a4/ce/3b6fee91c85626eaf769d617f1be9d2e15c1cca027bbdeb2e0d751469355/verspec-0.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/4b/eb/03bfb1299d4c4510329e470f13f9a4ce793df7fcb5a2fd3510f911066f61/virtualenv-21.3.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/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/66/3e/868e5c3364b6cee19ff3e1a122194fa4ce51def02c61023970442162859e/yarl-1.23.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_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/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.13.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/appnope-0.1.4-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/argon2-cffi-25.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/argon2-cffi-bindings-25.1.0-py312h4409184_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/arrow-1.4.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.18.0-pyhcf101f3_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/backports.zstd-1.3.0-py312h44dc372_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.14.3-pyha770c72_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.3.0-pyhcf101f3_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-with-css-6.3.0-hbca2aae_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-python-1.2.0-py312h0dfefe5_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-hd037594_9.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/c-ares-1.34.6-hc919400_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.4.22-hbd8a1cb_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cached-property-1.5.2-hd8ed1ab_1.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/cached_property-1.5.2-pyha770c72_1.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2026.4.22-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cffi-2.0.0-py312h1b4d9a2_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.7-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.3-pyhe01879c_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.12.13-py312hd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/debugpy-1.8.20-py312h6510ced_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/decorator-5.2.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/defusedxml-0.7.1-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/fqdn-1.5.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/gsl-2.8-h8d0574d_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/h11-0.16.0-pyhcf101f3_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/httpcore-1.0.9-pyh29332c3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/httpx-0.28.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/icu-78.3-hef89b57_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.13-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.8.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.2.0-pyh5552912_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.2.0-pyha191276_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.13.0-pyh53cf698_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ipython_pygments_lexers-1.1.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/isoduration-20.11.0-pyhd8ed1ab_1.conda @@ -1413,82 +1133,46 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.8.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.9.1-pyhc90fa1f_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_events-0.12.1-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.17.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.18.2-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server_terminals-0.5.4-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.5.7-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_pygments-0.3.0-pyhd8ed1ab_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_server-2.28.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/krb5-1.22.2-h385eeb1_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/lark-1.3.1-pyhd8ed1ab_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-6_hb0561ab_openblas.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-22.1.4-h55c6f16_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libedit-3.1.20250104-pl5321hafb1f1b_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libev-4.33-h93a5062_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.7.5-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.3-h8088a28_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libnghttp2-1.68.1-h8f3e76b_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopenblas-0.3.32-openmp_he657e61_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsodium-1.0.21-h1a92334_3.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.53.0-h1b79a29_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libuv-1.51.0-h6caf38d_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.2-h8088a28_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/llvm-openmp-22.1.4-hc7d1edf_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/markupsafe-3.0.3-py312h04c11ed_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.2.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mistune-3.2.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/msgspec-0.21.1-py312h2bbb03f_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.2.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mistune-3.2.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/nbclient-0.10.4-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/nbconvert-core-7.17.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/nbformat-5.10.4-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.6-h1d4f5a5_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/nodejs-25.8.2-h7039424_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/notebook-shim-0.2.4-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.6.2-hd24854e_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/overrides-7.7.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.2-pyhc364b38_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pandocfilters-1.5.0-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.6-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.7-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pexpect-4.9.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pixi-kernel-0.7.1-pyhbbac1ac_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.9.6-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/prometheus_client-0.25.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.52-pyha770c72_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/psutil-7.2.2-py312hb3ab3e3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ptyprocess-0.7.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyobjc-core-12.1-py312h19bbe71_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyobjc-framework-cocoa-12.1-py312h1de3e18_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.12.13-h8561d8f_0_cpython.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.21.2-pyhe01879c_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.12.13-hd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-json-logger-3.2.1-pyh332efcf_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2026.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.12-8_cp312.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyyaml-6.0.3-py312h04c11ed_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyzmq-27.1.0-py312h022ad19_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.3-h46df422_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.37.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.33.1-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.34.2-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/returns-0.27.0-pyhc364b38_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3339-validator-0.1.4-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3986-validator-0.1.1-pyh9f0ad1d_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3987-syntax-1.1.0-pyhe01879c_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/rpds-py-0.30.0-py312h6ef9ec0_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/send2trash-2.1.0-pyh5552912_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/send2trash-2.1.0-pyha191276_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-82.0.1-pyh332efcf_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sniffio-1.3.1-pyhd8ed1ab_2.conda @@ -1496,231 +1180,224 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyhc90fa1f_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tinycss2-1.4.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h010d191_3.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.1-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tornado-6.5.5-py312h2bbb03f_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.15.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.15.0-h396c80c_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing_utils-0.1.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/uri-template-1.3.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.6.3-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.6.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.7.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.7.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/webcolors-25.10.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/webencodings-0.5.1-pyhd8ed1ab_3.conda - conda: https://conda.anaconda.org/conda-forge/noarch/websocket-client-1.9.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/yaml-0.2.5-h925e9cb_3.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zeromq-4.3.5-h4818236_10.conda - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.1-pyhcf101f3_0.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/29/47/7be41556bfbb6917069d6a6634bb7dd5e163ba445b783a90d40f5ac7e3a7/aiohttp-3.13.5-cp312-cp312-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/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/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/9e/43/53afb8ba17218f19b77c7834128566c5bbb100a0ad9ba2e8e89d089d7079/autopep8-2.3.2-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/36/da/87912ddec6e06feffbaa3d7aa18fc6352bee2e8f1fee185d7d1690f8f4e8/backrefs-7.0-py312-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/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl + - pypi: . + - pypi: https://files.pythonhosted.org/packages/01/8e/1e35281b8ab6d5d72ebe9911edcdffa3f36b04ed9d51dec6dd140396e220/scipy-1.17.1-cp312-cp312-manylinux_2_27_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/07/c7/deb8c5e604404dbf10a3808a858946ca3547692ff6316b698945bb72177e/python_socketio-5.16.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0b/f9/f15c95d6b200167cb22c5eca5eecfa9d28a8ee3f74095f1cd2345c71f2f9/pydoclint-0.8.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0d/12/bbce9472f489cb5c4c23b0d13e5c59c37c1aab11b7ac637dfe6bbdccebe7/copier-9.15.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0d/1f/d398de1612f7a611e22d743280339c9af4903675635e41be3370091c704b/arviz_stats-1.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/0d/fe/6bea5c9162869c5beba5d9c8abbed835ec85bf1ec1fba05a3822325c45f3/build-1.5.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/7b/67/b1944235474aac3f0b0e1b232ce49547f9f9461ca4b943df1b88da5d3f1d/bumps-1.0.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/9c/2f/4c5af01fd1a7506a1d5375403d68925eac70289229492db5aa68b58103d8/chardet-7.4.3-cp312-cp312-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-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/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/98/ba/fc20faf5b2bb04615fa906f8daecfe896e6f7a56b34debd93c4a4d63e6e5/copier-9.15.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a6/16/a68a19e5384e93f811dccc51034b1fd0b865841c390e3c931dcc4699e035/coverage-7.13.5-cp312-cp312-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/36/28/46c4b0dfc8eb52c4fe8c902601b5e0acfc6b943f97cf8c53447ef9e1c66c/crysfml-0.6.1-cp312-cp312-macosx_14_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/96/5d/0c59079aa7ef34980a5925a06a90ad2b7c94e486c194b3527d557cabb042/cryspy-0.11.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/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/4a/f3/00bb1e867fba351e2d784170955713bee200c43ea306c59f30bd7e748192/dask-2026.3.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/c7/a0/5ff05d1919ca249508012cad89f08fdc6cfbdaa15b41651c5fe6dffaf1d3/dfo_ls-1.6.5-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/01/5c/87b5fefdd3c4b157c8a16833f2236723136806814584c4589610217252f0/diffpy_pdffit2-1.6.0-cp312-cp312-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/b4/5e/bf11645aebb9af7d8d35927c40d3855816a0855c799e8156eeca8d632c90/diffpy_structure-3.4.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/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/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/85/d7/9b6ac05350ab7f7d3a730ff143ff3e2cada54514117c37be37e26dc91242/docstripy-0.7.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/fa/bc/8b8ec5a4bfc5b9cf3ce27a118339e994f88410be5677c96493e0ea28e76d/dunamai-1.26.1-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/c4/6d/82e65254354ba651dc966775270a9bbc02414a3eb3f1704e6c87dab2ea83/essdiffraction-26.5.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/8a/06/2c1bd1ee9eee3e65b0b6395dcf960e71b11576995eae3b4ab9ec63d89bea/essreduce-26.4.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/47/d4/dbacced3953544b9a93088cc10ef2b596d348c983d5c67a404fa41ec51ba/fonttools-4.62.1-cp312-cp312-macosx_10_13_universal2.whl - - pypi: https://files.pythonhosted.org/packages/33/b2/986d1220f6ee931e338d272bc1f3ec02cfe5f9b5fad84e95afdad57f1ebc/format_docstring-0.2.7-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/d5/0c/043d5e551459da400957a1395e0febbf771446ff34291afcbe3d8be2a279/fsspec-2026.4.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d5/08/c2409cb01d5368dcfedcbaffa7d044cc8957d57a9d0855244a5eb4709d30/funcy-2.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b0/3e/a6497e1c2c9bc6ed2b79e0f2d31a4ce509fd2a9eed4e4f7ac63eda8113cb/gemmi-0.7.5-cp312-cp312-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/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/fd/6f/b842bfa6f21d6f87c57f9abf7194225e55279d96d869775e19e9f7236fc5/gitpython-3.1.49-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/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/11/8c/c9138d881c79aa0ea9ed83cbd58d5ca75624378b38cee225dcf5c42cc91f/griffelib-2.0.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b0/42/c84efcc1d4caebafb1ecd8be4643f39c85c47a80fe254d92b8b43b1eadaf/h5py-3.16.0-cp312-cp312-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/12/aa/fb2a0649fdeef5ab7072d221e8f4df164098792c813af6c87e2581cfa860/mpltoolbox-26.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/12/c9/6869a1dcf4aaf309b9543ec070be3ec3adebee7c9bec9af8c230494134b9/interrogate-1.7.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/56/6d/0d9848617b9f753b87f214f1c682592f7ca42de085f564352f10f0843026/ipywidgets-8.1.8-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/f4/a4/61adb19f3c74b0dc0e411de4f06ebef564b1f179928f9dffcbd4b378f2ef/jupyter_notebook_parser-0.1.4-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ab/b5/36c712098e6191d1b4e349304ef73a8d06aed77e56ceaac8c0a306c7bda1/jupyterlab_widgets-3.0.16-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/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/99/9f/795fedf35634f746151ca8839d05681ceb6287fbed6cc1c9bf235f7887c2/kiwisolver-1.5.0-cp312-cp312-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/8a/a1/8d812e53a5da1687abb10445275d41a8b13adb781bbf7196ddbcf8d88505/lazy_loader-0.5-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/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/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/b7/18/4880dd762e40cd360c1bf06e890c5a97b997e91cb324602b1a19950ad5ce/matplotlib-3.10.9-cp312-cp312-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/fb/86/dd6e5db36df29e76c7a7699123569a4a18c1623ce68d826ed96c62643cae/mdit_py_plugins-0.5.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/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/76/8e/56ccb09c7232a55403a7637caa21922f3b65901a37f5e8bdb405d0de0946/mike-2.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/13/95/cf3f7fe4910cf0365fa8ea0c731f4b8a624d97cd76ea777913ac8d0868e2/mkdocs_jupyter-0.26.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/14/15/5574111ae50dd6e879456888c0eadd4c5a869959775854e18e18a6b345f3/propcache-0.5.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/19/d4/225027a913621a879b429a043674aa35220e6ce67785acad4f7bd0c4ff33/xarray_einstats-0.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1c/59/964ecb8008722d27d8a835baea81f56a91cea8e097b3be992bc6ccde6367/versioningit-3.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1d/77/928ea2e70641ca177a11140062cc5840d421795f2e82749d408d0cce900a/narwhals-2.21.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/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1e/e7/cd78635d0ece7e4d3393f2c1d2ebabf6ff4bd615da142891b1d42ad58abf/scipp-26.3.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/20/16/e777eadfa0c0305878c36fae1d5e6db474fbb15dae202b9ec378809dfb4d/nbstripout-0.9.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/20/7a/1c6e3562dfd8950adbb11ffbc65d21e7c89d01a6e4f137fa981056de25c5/gitpython-3.1.50-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-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/28/88/4789719fbbe166d12d345b3ac66b96105f10001b16e00a9765ba29261a21/nbqa-1.9.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/28/de/a3e710469772c6a89595fc52816da05c1e164b4c866a89e3cb82fb1b67c5/mkdocs_autorefs-1.4.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/13/95/cf3f7fe4910cf0365fa8ea0c731f4b8a624d97cd76ea777913ac8d0868e2/mkdocs_jupyter-0.26.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a8/4e/c09876f08fa9faaa5e1178f3d77b7af3f343258689bd6f3b72593b2f74e3/mkdocs_markdownextradata_plugin-0.2.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2a/2d/d4bf65e47cea8ff2c794a600c4fd1273a7902f268757c531e0ee9f18aa58/pooch-1.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2b/e0/2e13df68f367e2f9960616927d60857dd7e56aaadd59a47c644216b2f920/greenlet-3.5.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/2c/01/bc663630c510822c95c47a66af9fa7a443c295b47d5f041e5e6ae62ef659/mkdocs_material-9.7.6-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/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/6e/94/be70f8ee9c45f2f62b39a1f0e9303bc20e138a8f3b8e50ffd89498e177e1/mkdocstrings-1.0.4-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/2c/fa/65fcae2ed62f84ab72cf89536c7c3217a156e71a2c111b1305ab6f0690e2/sqlalchemy-2.0.49-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/2f/c8/005d1de3af80f54411703d1263a0b9d31276411ec9f273d9432c59b17299/arviz_plots-1.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/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/32/91/d024616abdba99e83120e07a20658976f6a343646710760c4a51df126029/matplotlib-3.10.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/33/75/98a7eb100dc5cfd20b019046452f08d5e67dfbacc71d8f28763d32426fd3/spglib-2.6.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_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/33/eb/f9f1ded8e4db9638f9530c3782eb01f5ab04945f4cb9e597a51c203fa4c5/diffpy_pdffit2-1.6.0.tar.gz + - 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/36/da/87912ddec6e06feffbaa3d7aa18fc6352bee2e8f1fee185d7d1690f8f4e8/backrefs-7.0-py312-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/38/8b/7ec325b4e9e78beefc2d025b01ee8a2fde771ef7c957c3bff99b9e1fbffa/xraydb-4.5.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3a/eb/fea4d1d51c49832120f7f285d07306db3960f423a2612c6057caf3e8196f/pip-26.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3c/26/1062c7ec1b053db9e499b4d2d5bc231743201b74051c973dadeac80a8f43/questionary-2.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3e/17/1f31d8562e6f970d64911f1abc330d233bc0c0601411cf7e19c1292be6da/spdx_headers-1.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3f/0e/fa3b193432cfc60c93b42f3be03365f5f909d2b3ea410295cf36df739e31/widgetsnbextension-4.0.15-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3f/d0/7b958df957e4827837b590944008f0b28078f552b451f7407b4b3d54f574/asciichartpy-1.5.25-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/42/d9/27b13bc9419bf5dae02905b348f16ca827646cd76244ddd326f1a8139a6a/cyclebane-24.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/34/68/ba4f155f793a74c1483d4bdef136e1023f7bcba557f0db4ef3db3c665cf1/msgpack-1.1.2-cp312-cp312-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/a9/65/1caac9d4cd32e8433908683446eebc953e82d22b03d10d41a5f0fefe991b/multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/d0/69/f24d3d1c38ad69e256138b4ec2452a8c7cf66be49dc214771ae99dd4f0a0/narwhals-2.20.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/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/20/16/e777eadfa0c0305878c36fae1d5e6db474fbb15dae202b9ec378809dfb4d/nbstripout-0.9.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/1a/66/8ce2af42feeba7a85b573e615a360ca1e204c75f6c8d4c5641140b1bfd17/ncrystal-4.3.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a0/0d/df49ae8af94215db241701f692786a2e85c3ac4557aafd829270c54fb1fa/ncrystal_core-4.3.4-py3-none-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/74/10/6434f57fa65651672534ce34a1f40ddbc7b880658ce50a8f9d7ab0830719/ncrystal_python-4.3.4-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/43/e3/fdc657359e919462369869f1c9f0e973f353f9a9ee295a39b1fea8ee1a77/pillow-12.2.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/4a/f3/00bb1e867fba351e2d784170955713bee200c43ea306c59f30bd7e748192/dask-2026.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4c/65/b4b86e5fa07543bfbbcdc6c9f7f9f561e66a5f3539992e3009973f2b1314/jupytext-1.19.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/52/59/0782e51887ac6b07ffd1570e0364cf901ebc36345fea669969d2084baebb/simple_websocket-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/56/6d/0d9848617b9f753b87f214f1c682592f7ca42de085f564352f10f0843026/ipywidgets-8.1.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/57/d8/8d44036d7eb7b6a8ec4c5494ea0c8c8b94fbc0ed3991c1a7adf230df03bf/aiohttp-3.13.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/5b/29/74eeb4d3f3ae61ca096b018ad486b3b3c74b17bec09ab4edab721cbefec3/typeguard-4.5.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5b/69/93b34728cc386efdde0c342f8c680b9187dea7beb7adaf6b58a0713be101/mpld3-0.5.12-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5c/01/6cb4d63c6b6933be4b7945b2f64638336420f04ea71ca5b9a7539c008bc5/scippnexus-26.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/61/2b/e260d50e64690d2a9e405d52ccd18a63c286c5088937dd0107cb23eb3195/diffpy_utils-3.7.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/63/9f/724f66a48309dd97a2ff58f491d6ffd925f35d1278a5e55dc9a5ac6a156b/scippneutron-26.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/64/a8/c09fbe44b12fa919c5bfe0afb71e60d1231a7dc93405e54c30496c57c9d3/arviz-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/65/92/a5100f7185a800a5d29f8d14041f61475b9de465ffcc0f3b9fba606e4505/msgpack-1.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/65/b6/09b01cdbc15224e2850365192d17b7bdebb8bdbd8780ed221fcdf0d9a515/pandas-3.0.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/66/3e/868e5c3364b6cee19ff3e1a122194fa4ce51def02c61023970442162859e/yarl-1.23.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/69/d1/705e6c19b437a4105bf3b9ae7945fcfc3ad2abb73d14bae0a3f2d58b305b/arviz_base-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/69/ff/6699e7b71e60d3049eb2bdcbc95ee3f35707b2b0e48f32e9e63d3ce30c08/coverage-7.14.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/6c/25/4f103d1bedb3593718713b3f743df7b3ff3fc68d36d6666c30265ef59c8a/ase-3.28.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6e/94/be70f8ee9c45f2f62b39a1f0e9303bc20e138a8f3b8e50ffd89498e177e1/mkdocstrings-1.0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-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/76/8e/56ccb09c7232a55403a7637caa21922f3b65901a37f5e8bdb405d0de0946/mike-2.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/77/c7/2342da9830e3e9d4870305ca5d2091d2a83284f2953079b7bdd3b5e029d8/fonttools-4.63.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/78/3c/2a612b95ddbb9a6bdcb47b7a93c4884f74c6ff22356b2f7b213b16e65c35/pycifstar-0.3.0.tar.gz + - pypi: https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.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/7b/67/b1944235474aac3f0b0e1b232ce49547f9f9461ca4b943df1b88da5d3f1d/bumps-1.0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7b/e3/d81b065a2d866a33a541ac63a2a4cc5737e03ce2379ac3191c98bb8867e3/trove_classifiers-2026.5.7.17-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7e/85/545a951eecc270fcd688288c600017e2050a1aacb56c711d208586d3e470/pymdown_extensions-10.21.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/85/d7/9b6ac05350ab7f7d3a730ff143ff3e2cada54514117c37be37e26dc91242/docstripy-0.7.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/9b/fd/e5ecca1e78c05106d98028114f5c00d3eddb41207686b2b7de3e477b0e22/numpy-2.4.4-cp312-cp312-macosx_14_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/8a/06/2c1bd1ee9eee3e65b0b6395dcf960e71b11576995eae3b4ab9ec63d89bea/essreduce-26.4.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8a/a1/8d812e53a5da1687abb10445275d41a8b13adb781bbf7196ddbcf8d88505/lazy_loader-0.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8d/c0/fdf9d3ee103ce66a55f0532835ad5e154226c5222423c6636ba049dc42fc/traittypes-0.2.3-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8f/5e/f1e1dd319e35e962a4e00b33150a8868b6329cc1d19fd533436ba5488f09/uncertainties-3.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/35/d0/4831af68ce30cc2d03c697bea8450e3225a835ef497d0d70f31b8cdde965/pandas-3.0.2-cp312-cp312-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d8/95/0a351b9289c2b5cbde0bacd4a83ebc44023e835490a727b2a3bd60ddc0f4/pillow-12.2.0-cp312-cp312-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/70/7a/be4bd8bcbb24ea475856dd68159d78b03b2bb53dae369f69c9606b8888f5/pip-26.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e4/bc/daa30c02069eeac5b9198985ba42f5d65ca71bed6705b18329e51d352b7c/plopp-26.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/90/ad/cba91b3bcf04073e4d1655a5c1710ef3f457f56f7d1b79dcc3d72f4dd912/plotly-6.7.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/91/4c/e0ce1ef95d4000ebc1c11801f9b944fa5910ecc15b5e351865763d8657f8/graphviz-0.21-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/93/f7/d00d9b4a0313a6be3a3e0818e6375e15da6d7076f4ae47d1324e7ca986a1/radon-6.0.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/96/5d/0c59079aa7ef34980a5925a06a90ad2b7c94e486c194b3527d557cabb042/cryspy-0.11.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/97/37/ce5c3ef2595dac2be35039f7b91a0691ef643aa3d954815b3b51e026e0ab/crysfml-0.6.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/99/31/6cf181011dc738c33bf6ba7aea2e8e1d3c1f71b7dab1942f3054f66f6202/asteval-1.0.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/99/37/e8730c3587a65eb5645d4aba2d27aae48e8003614d6aaf15dda67f702f1f/bidict-0.23.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9e/43/53afb8ba17218f19b77c7834128566c5bbb100a0ad9ba2e8e89d089d7079/autopep8-2.3.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9e/e9/1a19e42cd43cc1365e127db6aae85e1c671da1d9a5d746f4d34a50edb577/h5py-3.16.0-cp312-cp312-manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a3/58/35da89ee790598a0700ea49b2a66594140f44dec458c07e8e3d4979137fc/ply-3.11-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/2a/2d/d4bf65e47cea8ff2c794a600c4fd1273a7902f268757c531e0ee9f18aa58/pooch-1.9.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ee/8c/83087ebc47ab0396ce092363001fa37c17153119ee282700c0713a195853/prettytable-3.17.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/0a/b6/5c9a0e42df4d00bfb4a3cbbe5cf9f54260300c88a0e9af1f47ca5ce17ac0/propcache-0.4.1-cp312-cp312-macosx_11_0_arm64.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/25/7d/cea3531f77df694ac7f169378250d85f19f69b09a5f4fa45f650837ae7cc/py3dmol-2.5.4-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e0/a9/023730ba63db1e494a271cb018dcd361bd2c917ba7004c3e49d5daf795a2/py_cpuinfo-9.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/32/a0/37fb236da6040e337381dd656cafb97d09eacb998c5db3057547f5ffddd9/pycifrw-5.0.1-cp312-cp312-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/f3/0a/fd7d723f8f8153418fb40cf9c940e82004fce7e987026b08a68a36dd3fe7/pydantic-2.13.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a1/4f/2fb62c2267cae99b815bbf4a7b9283812c88ca3153ef29f7707200f1d4e5/pydantic_core-2.46.3-cp312-cp312-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/87/6f/cc2b231dc78d8c3aaa674a676db190b8f8071c50134af8f8cf39b9b8e8df/pydoclint-0.8.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f7/27/a2fc51a4a122dfd1015e921ae9d22fee3d20b0b8080d9a704578bf9deece/pymdown_extensions-10.21.2-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/a4/39/3a0ae5b0edb66e61bb0e8bc53a503495cba5892297ae21faf6ba0525e681/varname-1.0.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/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.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/aa/54/0cce26da03a981f949bb8449c9778537f75f5917c172e1d2992ff25cb57d/python_engineio-4.13.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ab/b5/36c712098e6191d1b4e349304ef73a8d06aed77e56ceaac8c0a306c7bda1/jupyterlab_widgets-3.0.16-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ae/61/3c1ea8c10bf4f6bf83c33a7f5b4a3143f4cc1f979859dec5498b6cc31900/pycifrw-5.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.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/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/52/bc858b1665d0dec3a2511f4e6f5c18ea85c0977563d624d597c95d6d0fd7/jupyterquiz-2.9.6.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b4/5e/bf11645aebb9af7d8d35927c40d3855816a0855c799e8156eeca8d632c90/diffpy_structure-3.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/b7/6f/a05a317a66fee0aad270011461f1a63a453ed12471249f172f7d2e2bc7b4/python_discovery-1.3.1-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/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-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/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/33/29/e756e715a48959f1c0045342088d7ca9762a2f509b945f362a316e9412b7/pytest_benchmark-5.2.3-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/bf/50/98b146aea0f1cd7531d25f12bea69fa9ce8d1662124f93fb30dc4511b65e/docstring_parser_fork-0.0.14-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c1/d4/59e74daffcb57a07668852eeeb6035af9f32cbfd7a1d2511f17d2fe6a738/smmap-5.0.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c4/13/680c54afe3e65767bed7ec1a15571e1a2f1257128733851ade24abcefbcc/kiwisolver-1.5.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/c4/6d/82e65254354ba651dc966775270a9bbc02414a3eb3f1704e6c87dab2ea83/essdiffraction-26.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c6/3d/020a6b6248c3d4a37797db068256f0b3f15b01bc481327ba888c50309aa8/mkdocs_plugin_inline_svg-0.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c7/a0/5ff05d1919ca249508012cad89f08fdc6cfbdaa15b41651c5fe6dffaf1d3/dfo_ls-1.6.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d8/db/795879cc3ddfe338599bddea6388cc5100b088db0a4caf6e6c1af1c27e04/python_discovery-1.2.2-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/07/c7/deb8c5e604404dbf10a3808a858946ca3547692ff6316b698945bb72177e/python_socketio-5.16.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/cc/8f/ec6289987824b29529d0dfda0d74a07cec60e54b9c92f3c9da4c0ac732de/contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/d1/0b/b905ae82d9419dc38123523862db64978ca2954b69609c3ae8fdaca1084c/numpy-2.4.5-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.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/d2/f0/834e479e47e499b6478e807fb57b31cc2db696c4db30557bb6f5aea4a90b/mando-0.7.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d5/08/c2409cb01d5368dcfedcbaffa7d044cc8957d57a9d0855244a5eb4709d30/funcy-2.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d5/0c/043d5e551459da400957a1395e0febbf771446ff34291afcbe3d8be2a279/fsspec-2026.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.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/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.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/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/aa/a4/f828e9718d3dce1f5f11c39c4f65afd32783c8b2aebb2e3d259e492c47bd/ruff-0.15.12-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/44/7b/537a61906eac58d94131273084d21d4eb219f5453f0ed30de3aca580a2b4/scipp-26.3.1-cp312-cp312-macosx_14_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/82/9b/cf7e6f157c53d2c8bc0165580350c53939aea3b2a8390ec8e07f0adb82de/scippneutron-26.4.1-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/cf/a9/599c28631bad314d219cf9ffd40e985b24d603fc8a2f4ccc5ae8419a535b/scipy-1.17.1-cp312-cp312-macosx_14_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/db/bc/83e112abc66cd466c6b83f99118035867cecd41802f8d044638aa78a106e/locket-1.0.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/dc/83/6d810a8a9ebc9c307989b418840c20e46907c74d707beb67ab566773e6fc/xarray-2026.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/dc/b7/901d837999a9350a7773289f7760cb473d4ba01fdc6ebae0ff2553d269ac/ncrystal_python-4.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-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/c1/d4/59e74daffcb57a07668852eeeb6035af9f32cbfd7a1d2511f17d2fe6a738/smmap-5.0.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/bd/8c/d4907ad4f6bdc5bf79462d8767728713a7b316918a7444df372958a0e417/spglib-2.6.0-cp312-cp312-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/49/b3/2de412451330756aaaa72d27131db6dde23995efe62c941184e15242a5fa/sqlalchemy-2.0.49-cp312-cp312-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.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/e4/bc/daa30c02069eeac5b9198985ba42f5d65ca71bed6705b18329e51d352b7c/plopp-26.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e5/59/a32a241d861cf180853a11c8e5a67641cb1b2af13c3a5ccce83ec07e2c9f/chardet-7.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e8/31/bf1a0803d077e679cfeee5f2f67290a0fa79c7385b5d9a8c17b9db2c48f0/ruff-0.15.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/e8/88/5a431cd1ea7587408a66947384b39beb2ab2bcc1c87b7c4082f05036719f/gemmi-0.7.5-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/eb/be/b257e12f9710819fde40adc972578bee6b72c5992da1bc8369bef2597756/nbmake-1.5.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ee/8c/83087ebc47ab0396ce092363001fa37c17153119ee282700c0713a195853/prettytable-3.17.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f1/2c/3850985d4c64048dec7b826f8a803e135b52b11b4c81c9cd4326b1ca15ab/ncrystal_core-4.4.2-py3-none-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/f1/5b/e63c877c4c94382b66de5045e08ec8cd960e8a4d22f0d62a4dfb1f9e5ac6/ipydatawidgets-4.3.5-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.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/f3/8d/5e5be3ced1d12966fefb5c4ea3b2a5b480afcea36406559442c6e31d4a48/multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/f4/34/a9dbe051de88a63eb7408ea66630bac38e72f7f6077d4be58737106860d9/virtualenv-21.3.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f4/a4/61adb19f3c74b0dc0e411de4f06ebef564b1f179928f9dffcbd4b378f2ef/jupyter_notebook_parser-0.1.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f5/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/f6/f0/10642828a8dfb741e5f3fbaac830550a518a775c7fff6f04a007259b0548/py-1.11.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f7/00/bbca25f8a2372465cdf93138c1e1e38dff045fb0afef1488f395d0afcb3b/ncrystal-4.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fa/bc/8b8ec5a4bfc5b9cf3ce27a118339e994f88410be5677c96493e0ea28e76d/dunamai-1.26.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/8d/c0/fdf9d3ee103ce66a55f0532835ad5e154226c5222423c6636ba049dc42fc/traittypes-0.2.3-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d6/bb/fbdc4e57731efb86b4209ffdd2519520782bf27b3c961beac3e5c20d2b87/trove_classifiers-2026.4.28.13-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/91/88/b55b3117287a8540b76dbdd87733808d4d01c8067a3b339408c250bb3600/typeguard-4.5.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-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/b7/ee/e9c95cda829131f71a8dff5ce0406059fd16e591c074414e31ada19ba7c3/validate_pyproject-0.25-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a4/39/3a0ae5b0edb66e61bb0e8bc53a503495cba5892297ae21faf6ba0525e681/varname-1.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/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/4b/eb/03bfb1299d4c4510329e470f13f9a4ce793df7fcb5a2fd3510f911066f61/virtualenv-21.3.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.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/19/2a/725ecc166d53438bc88f76822ed4b1e3b10756e790bafd7b523fe97c322d/yarl-1.23.0-cp312-cp312-macosx_11_0_arm64.whl - - pypi: ./ - win-64: + - pypi: https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl + osx-arm64: - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.13.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/appnope-0.1.4-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/argon2-cffi-25.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/argon2-cffi-bindings-25.1.0-py312he06e257_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/arrow-1.4.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.18.0-pyhcf101f3_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/backports.zstd-1.3.0-py312h06d0912_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.14.3-pyha770c72_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.3.0-pyhcf101f3_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-with-css-6.3.0-hbca2aae_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/brotli-python-1.2.0-py312hc6d9e41_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h0ad9c76_9.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.4.22-h4c7d964_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.4.22-hbd8a1cb_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cached-property-1.5.2-hd8ed1ab_1.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/cached_property-1.5.2-pyha770c72_1.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2026.4.22-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/cffi-2.0.0-py312he06e257_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.7-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.3-pyhe01879c_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.12.13-py312hd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/debugpy-1.8.20-py312ha1a9051_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/decorator-5.2.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/defusedxml-0.7.1-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/fqdn-1.5.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/gsl-2.8-h5b8d9c4_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/h11-0.16.0-pyhcf101f3_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda @@ -1729,8 +1406,8 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.13-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.8.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.2.0-pyh6dadd2b_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.13.0-pyhe2676ad_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.2.0-pyh5552912_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.13.0-pyh53cf698_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ipython_pygments_lexers-1.1.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/isoduration-20.11.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jedi-0.19.2-pyhd8ed1ab_1.conda @@ -1742,333 +1419,333 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-with-format-nongpl-4.26.0-hcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter-lsp-2.3.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.8.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.9.1-pyh6dadd2b_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.9.1-pyhc90fa1f_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_events-0.12.1-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.17.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.18.2-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server_terminals-0.5.4-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.5.7-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_pygments-0.3.0-pyhd8ed1ab_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_server-2.28.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/krb5-1.22.2-h0ea6238_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/lark-1.3.1-pyhd8ed1ab_0.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.5-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.3-hfd05255_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libsodium-1.0.21-h6a83c73_3.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.53.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.3-h692994f_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libxml2-2.15.3-hbc0d294_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.4-h4fa8253_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/markupsafe-3.0.3-py312h05f76fc_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.2.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mistune-3.2.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/mkl-2025.3.1-hac47afa_12.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/msgspec-0.21.1-py312he06e257_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.2.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mistune-3.2.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/nbclient-0.10.4-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/nbconvert-core-7.17.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/nbformat-5.10.4-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/nodejs-25.8.2-h80d1838_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/notebook-shim-0.2.4-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/onemkl-license-2025.3.1-h57928b3_12.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.6.2-hf411b9b_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/overrides-7.7.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.2-pyhc364b38_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pandocfilters-1.5.0-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.6-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.7-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pexpect-4.9.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pixi-kernel-0.7.1-pyhbbac1ac_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.9.6-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/prometheus_client-0.25.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.52-pyha770c72_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/psutil-7.2.2-py312he5662c2_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ptyprocess-0.7.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyh09c184e_7.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/python-3.12.13-h0159041_0_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.21.2-pyhe01879c_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.12.13-hd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-json-logger-3.2.1-pyh332efcf_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2026.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.12-8_cp312.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/pywin32-311-py312h829343e_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/pywinpty-2.0.15-py312h275cf98_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/pyyaml-6.0.3-py312h05f76fc_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/pyzmq-27.1.0-py312h343a6d4_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.37.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.33.1-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.34.2-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/returns-0.27.0-pyhc364b38_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3339-validator-0.1.4-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3986-validator-0.1.1-pyh9f0ad1d_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3987-syntax-1.1.0-pyhe01879c_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/rpds-py-0.30.0-py312hdabe01f_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/send2trash-2.1.0-pyh6dadd2b_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/send2trash-2.1.0-pyh5552912_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-82.0.1-pyh332efcf_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sniffio-1.3.1-pyhd8ed1ab_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.8.3-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/tbb-2022.3.0-h3155e25_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyh6dadd2b_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyhc90fa1f_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tinycss2-1.4.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h6ed50ae_3.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.1-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/tornado-6.5.5-py312he06e257_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.15.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.15.0-h396c80c_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing_utils-0.1.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.26100.0-h57928b3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/uri-template-1.3.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.6.3-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-h41ae7f8_34.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.44.35208-h818238b_34.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.44.35208-h818238b_34.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.6.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.7.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.7.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/webcolors-25.10.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/webencodings-0.5.1-pyhd8ed1ab_3.conda - conda: https://conda.anaconda.org/conda-forge/noarch/websocket-client-1.9.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/win_inet_pton-1.1.0-pyh7428d3b_8.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/winpty-0.4.3-4.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/win-64/yaml-0.2.5-h6a83c73_3.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/zeromq-4.3.5-h507cc87_10.conda - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.1-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_6.conda + - 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/argon2-cffi-bindings-25.1.0-py312h4409184_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/backports.zstd-1.5.0-py312h87c4bb7_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-python-1.2.0-py312h0dfefe5_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-hd037594_9.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/c-ares-1.34.6-hc919400_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cffi-2.0.0-py312h1b4d9a2_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/debugpy-1.8.20-py312h6510ced_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-78.3-hef89b57_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/krb5-1.22.2-h385eeb1_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libabseil-20260107.1-cxx17_h2062a1b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libblas-3.11.0-7_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-7_hb0561ab_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-22.1.5-h55c6f16_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libedit-3.1.20250104-pl5321hafb1f1b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libev-4.33-h93a5062_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.8.0-hf6b4638_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.5.2-hcf2aa1b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgcc-15.2.0-hcbb3090_19.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran-15.2.0-h07b0088_19.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran5-15.2.0-hdae7583_19.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.3-h8088a28_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libnghttp2-1.68.1-h8f3e76b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopenblas-0.3.33-openmp_he657e61_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsodium-1.0.21-h1a92334_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.53.1-h1b79a29_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libuv-1.51.0-h6caf38d_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.2-h8088a28_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/llvm-openmp-22.1.5-hc7d1edf_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/markupsafe-3.0.3-py312h04c11ed_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/msgspec-0.21.1-py312h2bbb03f_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.6-h1d4f5a5_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/nodejs-25.8.2-h7039424_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.6.2-hd24854e_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/psutil-7.2.2-py312hb3ab3e3_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyobjc-core-12.1-py312h19bbe71_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyobjc-framework-cocoa-12.1-py312h1de3e18_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.12.13-h8561d8f_0_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyyaml-6.0.3-py312h04c11ed_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyzmq-27.1.0-py312h022ad19_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.3-h46df422_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/rpds-py-0.30.0-py312h6ef9ec0_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h010d191_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tornado-6.5.5-py312h2bbb03f_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/yaml-0.2.5-h925e9cb_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zeromq-4.3.5-h4818236_10.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hbf9d68e_6.conda + - pypi: . + - pypi: https://files.pythonhosted.org/packages/01/5c/87b5fefdd3c4b157c8a16833f2236723136806814584c4589610217252f0/diffpy_pdffit2-1.6.0-cp312-cp312-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/07/c7/deb8c5e604404dbf10a3808a858946ca3547692ff6316b698945bb72177e/python_socketio-5.16.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/08/ef/b3c6b9b5be2f82416d73fe2ed2e96e2793cd80e7510bd6a17ca79cdd88ec/fonttools-4.63.0-cp312-cp312-macosx_10_13_universal2.whl + - pypi: https://files.pythonhosted.org/packages/0b/f9/f15c95d6b200167cb22c5eca5eecfa9d28a8ee3f74095f1cd2345c71f2f9/pydoclint-0.8.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0d/12/bbce9472f489cb5c4c23b0d13e5c59c37c1aab11b7ac637dfe6bbdccebe7/copier-9.15.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0d/1f/d398de1612f7a611e22d743280339c9af4903675635e41be3370091c704b/arviz_stats-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0d/fe/6bea5c9162869c5beba5d9c8abbed835ec85bf1ec1fba05a3822325c45f3/build-1.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/aa/ca/eadf6f9c8fa5e31d40993e3db153fb5ed0b11008ad5d9de98a95045bed84/aiohttp-3.13.5-cp312-cp312-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/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/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/9e/43/53afb8ba17218f19b77c7834128566c5bbb100a0ad9ba2e8e89d089d7079/autopep8-2.3.2-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/36/da/87912ddec6e06feffbaa3d7aa18fc6352bee2e8f1fee185d7d1690f8f4e8/backrefs-7.0-py312-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/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/0d/fe/6bea5c9162869c5beba5d9c8abbed835ec85bf1ec1fba05a3822325c45f3/build-1.5.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/7b/67/b1944235474aac3f0b0e1b232ce49547f9f9461ca4b943df1b88da5d3f1d/bumps-1.0.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/32/60/fca69c534602a7ced04280c952a246ad1edde2a6ca3a164f65d32ac41fe7/chardet-7.4.3-cp312-cp312-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/19/e8/6026ed58a64563186a9ee3f29f41261fd1828f527dd93d33b60feca63352/contourpy-1.3.3-cp312-cp312-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/98/ba/fc20faf5b2bb04615fa906f8daecfe896e6f7a56b34debd93c4a4d63e6e5/copier-9.15.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/29/3d/821a9a5799fac2556bcf0bd37a70d1d11fa9e49784b6d22e92e8b2f85f18/coverage-7.13.5-cp312-cp312-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/2c/58/d025d34259682d555db962eab098f5add29187443c31081cbaf5c7ec4bea/crysfml-0.6.1-cp312-cp312-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/96/5d/0c59079aa7ef34980a5925a06a90ad2b7c94e486c194b3527d557cabb042/cryspy-0.11.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/4a/f3/00bb1e867fba351e2d784170955713bee200c43ea306c59f30bd7e748192/dask-2026.3.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/c7/a0/5ff05d1919ca249508012cad89f08fdc6cfbdaa15b41651c5fe6dffaf1d3/dfo_ls-1.6.5-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/6f/0c/8297c8d978c919ad6318011631a6123082d5da940da5f8612e75a247d739/diffpy_pdffit2-1.6.0-cp312-cp312-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/b4/5e/bf11645aebb9af7d8d35927c40d3855816a0855c799e8156eeca8d632c90/diffpy_structure-3.4.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/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/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/85/d7/9b6ac05350ab7f7d3a730ff143ff3e2cada54514117c37be37e26dc91242/docstripy-0.7.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/fa/bc/8b8ec5a4bfc5b9cf3ce27a118339e994f88410be5677c96493e0ea28e76d/dunamai-1.26.1-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/c4/6d/82e65254354ba651dc966775270a9bbc02414a3eb3f1704e6c87dab2ea83/essdiffraction-26.5.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/8a/06/2c1bd1ee9eee3e65b0b6395dcf960e71b11576995eae3b4ab9ec63d89bea/essreduce-26.4.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/28/b1/0c2ab56a16f409c6c8a68816e6af707827ad5d629634691ff60a52879792/fonttools-4.62.1-cp312-cp312-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/33/b2/986d1220f6ee931e338d272bc1f3ec02cfe5f9b5fad84e95afdad57f1ebc/format_docstring-0.2.7-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/d5/0c/043d5e551459da400957a1395e0febbf771446ff34291afcbe3d8be2a279/fsspec-2026.4.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d5/08/c2409cb01d5368dcfedcbaffa7d044cc8957d57a9d0855244a5eb4709d30/funcy-2.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/eb/f2/53be7a4ba5816e13c39be0f728facac4bcb39cf4903ceeec54b006511c8f/gemmi-0.7.5-cp312-cp312-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/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/fd/6f/b842bfa6f21d6f87c57f9abf7194225e55279d96d869775e19e9f7236fc5/gitpython-3.1.49-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/4c/fe/4fb3a0805bd5165da5ebf858da7cc01cce8061674106d2cf5bdab32cbfde/greenlet-3.5.0-cp312-cp312-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/11/8c/c9138d881c79aa0ea9ed83cbd58d5ca75624378b38cee225dcf5c42cc91f/griffelib-2.0.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/03/c1/0976b235cf29ead553e22f2fb6385a8252b533715e00d0ae52ed7b900582/h5py-3.16.0-cp312-cp312-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/12/aa/fb2a0649fdeef5ab7072d221e8f4df164098792c813af6c87e2581cfa860/mpltoolbox-26.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/12/c9/6869a1dcf4aaf309b9543ec070be3ec3adebee7c9bec9af8c230494134b9/interrogate-1.7.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/56/6d/0d9848617b9f753b87f214f1c682592f7ca42de085f564352f10f0843026/ipywidgets-8.1.8-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/f4/a4/61adb19f3c74b0dc0e411de4f06ebef564b1f179928f9dffcbd4b378f2ef/jupyter_notebook_parser-0.1.4-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ab/b5/36c712098e6191d1b4e349304ef73a8d06aed77e56ceaac8c0a306c7bda1/jupyterlab_widgets-3.0.16-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/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/ad/cf/0348374369ca588f8fe9c338fae49fa4e16eeb10ffb3d012f23a54578a9e/kiwisolver-1.5.0-cp312-cp312-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/8a/a1/8d812e53a5da1687abb10445275d41a8b13adb781bbf7196ddbcf8d88505/lazy_loader-0.5-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/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/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/04/a1/4571fc46e7702de8d0c2dc54ad1b2f8e29328dea3ee90831181f7353d93c/matplotlib-3.10.9-cp312-cp312-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/fb/86/dd6e5db36df29e76c7a7699123569a4a18c1623ce68d826ed96c62643cae/mdit_py_plugins-0.5.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/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/76/8e/56ccb09c7232a55403a7637caa21922f3b65901a37f5e8bdb405d0de0946/mike-2.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/13/95/cf3f7fe4910cf0365fa8ea0c731f4b8a624d97cd76ea777913ac8d0868e2/mkdocs_jupyter-0.26.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/15/1d/9f9e30d76300b0150afaa8b37fab9a0194d44fd4f6b1e5038aca4a1440ed/crysfml-0.6.2-cp312-cp312-macosx_14_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/19/2a/725ecc166d53438bc88f76822ed4b1e3b10756e790bafd7b523fe97c322d/yarl-1.23.0-cp312-cp312-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/19/d4/225027a913621a879b429a043674aa35220e6ce67785acad4f7bd0c4ff33/xarray_einstats-0.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1c/59/964ecb8008722d27d8a835baea81f56a91cea8e097b3be992bc6ccde6367/versioningit-3.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1d/77/928ea2e70641ca177a11140062cc5840d421795f2e82749d408d0cce900a/narwhals-2.21.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/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/20/16/e777eadfa0c0305878c36fae1d5e6db474fbb15dae202b9ec378809dfb4d/nbstripout-0.9.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/20/7a/1c6e3562dfd8950adbb11ffbc65d21e7c89d01a6e4f137fa981056de25c5/gitpython-3.1.50-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-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/28/88/4789719fbbe166d12d345b3ac66b96105f10001b16e00a9765ba29261a21/nbqa-1.9.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/28/de/a3e710469772c6a89595fc52816da05c1e164b4c866a89e3cb82fb1b67c5/mkdocs_autorefs-1.4.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/13/95/cf3f7fe4910cf0365fa8ea0c731f4b8a624d97cd76ea777913ac8d0868e2/mkdocs_jupyter-0.26.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a8/4e/c09876f08fa9faaa5e1178f3d77b7af3f343258689bd6f3b72593b2f74e3/mkdocs_markdownextradata_plugin-0.2.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/29/47/7be41556bfbb6917069d6a6634bb7dd5e163ba445b783a90d40f5ac7e3a7/aiohttp-3.13.5-cp312-cp312-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/2a/2d/d4bf65e47cea8ff2c794a600c4fd1273a7902f268757c531e0ee9f18aa58/pooch-1.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.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/6e/94/be70f8ee9c45f2f62b39a1f0e9303bc20e138a8f3b8e50ffd89498e177e1/mkdocstrings-1.0.4-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/2c/7d/49777a3e20b55863d4794384a38acd460c04157b0a00f8602b0d508b8431/propcache-0.5.2-cp312-cp312-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/2f/c8/005d1de3af80f54411703d1263a0b9d31276411ec9f273d9432c59b17299/arviz_plots-1.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/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/32/a0/37fb236da6040e337381dd656cafb97d09eacb998c5db3057547f5ffddd9/pycifrw-5.0.1-cp312-cp312-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/33/b2/986d1220f6ee931e338d272bc1f3ec02cfe5f9b5fad84e95afdad57f1ebc/format_docstring-0.2.7-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/33/f0/3fe8c6e69135a845f4106f2ff8b6805638d4e85c264e70114e8126689587/tokenize_rt-6.2.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/34/23/35c7aea1274aef7525bdd2dc92f710bdde6d11652239d71d1ec450067939/coverage-7.14.0-cp312-cp312-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/34/68/ba4f155f793a74c1483d4bdef136e1023f7bcba557f0db4ef3db3c665cf1/msgpack-1.1.2-cp312-cp312-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/36/da/87912ddec6e06feffbaa3d7aa18fc6352bee2e8f1fee185d7d1690f8f4e8/backrefs-7.0-py312-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/38/8b/7ec325b4e9e78beefc2d025b01ee8a2fde771ef7c957c3bff99b9e1fbffa/xraydb-4.5.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3a/eb/fea4d1d51c49832120f7f285d07306db3960f423a2612c6057caf3e8196f/pip-26.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3c/26/1062c7ec1b053db9e499b4d2d5bc231743201b74051c973dadeac80a8f43/questionary-2.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3e/17/1f31d8562e6f970d64911f1abc330d233bc0c0601411cf7e19c1292be6da/spdx_headers-1.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3f/0e/fa3b193432cfc60c93b42f3be03365f5f909d2b3ea410295cf36df739e31/widgetsnbextension-4.0.15-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3f/d0/7b958df957e4827837b590944008f0b28078f552b451f7407b4b3d54f574/asciichartpy-1.5.25-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/42/d9/27b13bc9419bf5dae02905b348f16ca827646cd76244ddd326f1a8139a6a/cyclebane-24.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/8c/ec/d431eb7941fb55a31dd6ca3404d41fbb52d99172df2e7707754488390910/msgpack-1.1.2-cp312-cp312-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/80/31/0b2517913687895f5904325c2069d6a3b78f66cc641a86a2baf75a05dcbb/multidict-6.7.1-cp312-cp312-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/d0/69/f24d3d1c38ad69e256138b4ec2452a8c7cf66be49dc214771ae99dd4f0a0/narwhals-2.20.0-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/20/16/e777eadfa0c0305878c36fae1d5e6db474fbb15dae202b9ec378809dfb4d/nbstripout-0.9.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/1a/66/8ce2af42feeba7a85b573e615a360ca1e204c75f6c8d4c5641140b1bfd17/ncrystal-4.3.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ab/14/d708fb7c6bf7e7be586c625840cc078a45e64dd6bffe8d60a7b17a22b24e/ncrystal_core-4.3.4-py3-none-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/74/10/6434f57fa65651672534ce34a1f40ddbc7b880658ce50a8f9d7ab0830719/ncrystal_python-4.3.4-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/2d/5b/e1deebf88ff431b01b7406ca3583ab2bbb90972bbe1c568732e49c844f7e/numpy-2.4.4-cp312-cp312-win_amd64.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/7b/8b/721a9cff6fa6a91b162eb51019c6243b82b3226c71bb6c8ef4a9bd65cbc6/pandas-3.0.2-cp312-cp312-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/5d/7b/25a221d2c761c6a8ae21bfa3874988ff2583e19cf8a27bf2fee358df7942/pillow-12.2.0-cp312-cp312-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/70/7a/be4bd8bcbb24ea475856dd68159d78b03b2bb53dae369f69c9606b8888f5/pip-26.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e4/bc/daa30c02069eeac5b9198985ba42f5d65ca71bed6705b18329e51d352b7c/plopp-26.4.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/90/ad/cba91b3bcf04073e4d1655a5c1710ef3f457f56f7d1b79dcc3d72f4dd912/plotly-6.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/44/7b/537a61906eac58d94131273084d21d4eb219f5453f0ed30de3aca580a2b4/scipp-26.3.1-cp312-cp312-macosx_14_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/47/cc/ddaf3af9c46966fef5be879256f213d85a0c56c75d07a3b7defec7cf6b4c/numpy-2.4.5-cp312-cp312-macosx_14_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/49/b3/2de412451330756aaaa72d27131db6dde23995efe62c941184e15242a5fa/sqlalchemy-2.0.49-cp312-cp312-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/4a/f3/00bb1e867fba351e2d784170955713bee200c43ea306c59f30bd7e748192/dask-2026.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4c/65/b4b86e5fa07543bfbbcdc6c9f7f9f561e66a5f3539992e3009973f2b1314/jupytext-1.19.2-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/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/56/6d/0d9848617b9f753b87f214f1c682592f7ca42de085f564352f10f0843026/ipywidgets-8.1.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5b/29/74eeb4d3f3ae61ca096b018ad486b3b3c74b17bec09ab4edab721cbefec3/typeguard-4.5.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5b/69/93b34728cc386efdde0c342f8c680b9187dea7beb7adaf6b58a0713be101/mpld3-0.5.12-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/5c/01/6cb4d63c6b6933be4b7945b2f64638336420f04ea71ca5b9a7539c008bc5/scippnexus-26.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/61/2b/e260d50e64690d2a9e405d52ccd18a63c286c5088937dd0107cb23eb3195/diffpy_utils-3.7.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/63/9f/724f66a48309dd97a2ff58f491d6ffd925f35d1278a5e55dc9a5ac6a156b/scippneutron-26.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/64/a8/c09fbe44b12fa919c5bfe0afb71e60d1231a7dc93405e54c30496c57c9d3/arviz-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/69/d1/705e6c19b437a4105bf3b9ae7945fcfc3ad2abb73d14bae0a3f2d58b305b/arviz_base-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6c/25/4f103d1bedb3593718713b3f743df7b3ff3fc68d36d6666c30265ef59c8a/ase-3.28.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6e/94/be70f8ee9c45f2f62b39a1f0e9303bc20e138a8f3b8e50ffd89498e177e1/mkdocstrings-1.0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-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/76/8e/56ccb09c7232a55403a7637caa21922f3b65901a37f5e8bdb405d0de0946/mike-2.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/78/3c/2a612b95ddbb9a6bdcb47b7a93c4884f74c6ff22356b2f7b213b16e65c35/pycifstar-0.3.0.tar.gz + - pypi: https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.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/2a/2d/d4bf65e47cea8ff2c794a600c4fd1273a7902f268757c531e0ee9f18aa58/pooch-1.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7b/67/b1944235474aac3f0b0e1b232ce49547f9f9461ca4b943df1b88da5d3f1d/bumps-1.0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7b/e3/d81b065a2d866a33a541ac63a2a4cc5737e03ce2379ac3191c98bb8867e3/trove_classifiers-2026.5.7.17-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7e/85/545a951eecc270fcd688288c600017e2050a1aacb56c711d208586d3e470/pymdown_extensions-10.21.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ee/8c/83087ebc47ab0396ce092363001fa37c17153119ee282700c0713a195853/prettytable-3.17.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/54/09/d19cff2a5aaac632ec8fc03737b223597b1e347416934c1b3a7df079784c/propcache-0.4.1-cp312-cp312-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/f6/f0/10642828a8dfb741e5f3fbaac830550a518a775c7fff6f04a007259b0548/py-1.11.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/25/7d/cea3531f77df694ac7f169378250d85f19f69b09a5f4fa45f650837ae7cc/py3dmol-2.5.4-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e0/a9/023730ba63db1e494a271cb018dcd361bd2c917ba7004c3e49d5daf795a2/py_cpuinfo-9.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/28/55/5733807f4af131ea6194309ac0f43eb5b05463c676d036ef948f3143c1f2/pycifrw-5.0.1-cp312-cp312-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/f3/0a/fd7d723f8f8153418fb40cf9c940e82004fce7e987026b08a68a36dd3fe7/pydantic-2.13.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/57/be/6b5e757b859013ebfbd7adba02f23b428f37c86dcbf78b5bb0b4ffd36e99/pydantic_core-2.46.3-cp312-cp312-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/87/6f/cc2b231dc78d8c3aaa674a676db190b8f8071c50134af8f8cf39b9b8e8df/pydoclint-0.8.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f7/27/a2fc51a4a122dfd1015e921ae9d22fee3d20b0b8080d9a704578bf9deece/pymdown_extensions-10.21.2-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/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/33/29/e756e715a48959f1c0045342088d7ca9762a2f509b945f362a316e9412b7/pytest_benchmark-5.2.3-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/d8/db/795879cc3ddfe338599bddea6388cc5100b088db0a4caf6e6c1af1c27e04/python_discovery-1.2.2-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/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/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.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/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/33/f1/9614e03e1cdcbf9437570b5400ced8a720b5db22b28d8e0f1bda429f660d/ruff-0.15.12-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/1a/1f/86b4d15221096cb5500bcd73bf350745749e3ba056cdd7a7f75f126f154e/scipp-26.3.1-cp312-cp312-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/82/9b/cf7e6f157c53d2c8bc0165580350c53939aea3b2a8390ec8e07f0adb82de/scippneutron-26.4.1-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/a2/84/dc08d77fbf3d87d3ee27f6a0c6dcce1de5829a64f2eae85a0ecc1f0daa73/scipy-1.17.1-cp312-cp312-win_amd64.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/c1/d4/59e74daffcb57a07668852eeeb6035af9f32cbfd7a1d2511f17d2fe6a738/smmap-5.0.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/8f/82/b54e646be7b938fcbdda10030c6533bd2bb1a59930a1381cc83d6050a49c/spglib-2.6.0-cp312-cp312-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/47/9e/fd90114059175cac64e4fafa9bf3ac20584384d66de40793ae2e2f26f3bb/sqlalchemy-2.0.49-cp312-cp312-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.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/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/85/d7/9b6ac05350ab7f7d3a730ff143ff3e2cada54514117c37be37e26dc91242/docstripy-0.7.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8a/06/2c1bd1ee9eee3e65b0b6395dcf960e71b11576995eae3b4ab9ec63d89bea/essreduce-26.4.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8a/a1/8d812e53a5da1687abb10445275d41a8b13adb781bbf7196ddbcf8d88505/lazy_loader-0.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8d/c0/fdf9d3ee103ce66a55f0532835ad5e154226c5222423c6636ba049dc42fc/traittypes-0.2.3-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d6/bb/fbdc4e57731efb86b4209ffdd2519520782bf27b3c961beac3e5c20d2b87/trove_classifiers-2026.4.28.13-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/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-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/8f/5e/f1e1dd319e35e962a4e00b33150a8868b6329cc1d19fd533436ba5488f09/uncertainties-3.2.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/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/90/ad/cba91b3bcf04073e4d1655a5c1710ef3f457f56f7d1b79dcc3d72f4dd912/plotly-6.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/91/4c/e0ce1ef95d4000ebc1c11801f9b944fa5910ecc15b5e351865763d8657f8/graphviz-0.21-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/93/f7/d00d9b4a0313a6be3a3e0818e6375e15da6d7076f4ae47d1324e7ca986a1/radon-6.0.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/96/5d/0c59079aa7ef34980a5925a06a90ad2b7c94e486c194b3527d557cabb042/cryspy-0.11.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/99/31/6cf181011dc738c33bf6ba7aea2e8e1d3c1f71b7dab1942f3054f66f6202/asteval-1.0.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/99/37/e8730c3587a65eb5645d4aba2d27aae48e8003614d6aaf15dda67f702f1f/bidict-0.23.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/99/9f/795fedf35634f746151ca8839d05681ceb6287fbed6cc1c9bf235f7887c2/kiwisolver-1.5.0-cp312-cp312-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/9c/2f/4c5af01fd1a7506a1d5375403d68925eac70289229492db5aa68b58103d8/chardet-7.4.3-cp312-cp312-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9e/43/53afb8ba17218f19b77c7834128566c5bbb100a0ad9ba2e8e89d089d7079/autopep8-2.3.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a3/58/35da89ee790598a0700ea49b2a66594140f44dec458c07e8e3d4979137fc/ply-3.11-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a4/39/3a0ae5b0edb66e61bb0e8bc53a503495cba5892297ae21faf6ba0525e681/varname-1.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/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/4b/eb/03bfb1299d4c4510329e470f13f9a4ce793df7fcb5a2fd3510f911066f61/virtualenv-21.3.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/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/f5/be/25216a49daeeb7af2bec0db22d5e7df08ed1d7c9f65d78b14f3b74fd72fc/yarl-1.23.0-cp312-cp312-win_amd64.whl - - pypi: ./ - py-314-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/_openmp_mutex-4.5-20_gnu.conda + - pypi: https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.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/a9/65/1caac9d4cd32e8433908683446eebc953e82d22b03d10d41a5f0fefe991b/multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/aa/54/0cce26da03a981f949bb8449c9778537f75f5917c172e1d2992ff25cb57d/python_engineio-4.13.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ab/b5/36c712098e6191d1b4e349304ef73a8d06aed77e56ceaac8c0a306c7bda1/jupyterlab_widgets-3.0.16-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/af/46/661159ad844034ba8b3f4e0516215c41e4ee17db4213d13a82227670764f/sciline-25.11.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b0/3e/a6497e1c2c9bc6ed2b79e0f2d31a4ce509fd2a9eed4e4f7ac63eda8113cb/gemmi-0.7.5-cp312-cp312-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/b0/42/c84efcc1d4caebafb1ecd8be4643f39c85c47a80fe254d92b8b43b1eadaf/h5py-3.16.0-cp312-cp312-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/52/bc858b1665d0dec3a2511f4e6f5c18ea85c0977563d624d597c95d6d0fd7/jupyterquiz-2.9.6.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b4/5e/bf11645aebb9af7d8d35927c40d3855816a0855c799e8156eeca8d632c90/diffpy_structure-3.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/18/4880dd762e40cd360c1bf06e890c5a97b997e91cb324602b1a19950ad5ce/matplotlib-3.10.9-cp312-cp312-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/b7/6f/a05a317a66fee0aad270011461f1a63a453ed12471249f172f7d2e2bc7b4/python_discovery-1.3.1-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/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bd/8c/d4907ad4f6bdc5bf79462d8767728713a7b316918a7444df372958a0e417/spglib-2.6.0-cp312-cp312-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/bf/50/98b146aea0f1cd7531d25f12bea69fa9ce8d1662124f93fb30dc4511b65e/docstring_parser_fork-0.0.14-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c1/d4/59e74daffcb57a07668852eeeb6035af9f32cbfd7a1d2511f17d2fe6a738/smmap-5.0.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c4/6d/82e65254354ba651dc966775270a9bbc02414a3eb3f1704e6c87dab2ea83/essdiffraction-26.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c6/3d/020a6b6248c3d4a37797db068256f0b3f15b01bc481327ba888c50309aa8/mkdocs_plugin_inline_svg-0.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c7/a0/5ff05d1919ca249508012cad89f08fdc6cfbdaa15b41651c5fe6dffaf1d3/dfo_ls-1.6.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c7/ea/7988934c8e3e3418aa043f70421817df28d06aef50bfd85f5ad3ec6e70f1/ncrystal_core-4.4.2-py3-none-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/cf/3d/b16412745651e855f357e5e66930248688378853a6e2698a214e331fba1f/pandas-3.0.3-cp312-cp312-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/cf/a9/599c28631bad314d219cf9ffd40e985b24d603fc8a2f4ccc5ae8419a535b/scipy-1.17.1-cp312-cp312-macosx_14_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-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/d3/19/43f5f2e568dddde567fc41f8471f9432c09563e19d3e617a48cfa52f8f0a/ruff-0.15.13-py3-none-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d5/08/c2409cb01d5368dcfedcbaffa7d044cc8957d57a9d0855244a5eb4709d30/funcy-2.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d5/0c/043d5e551459da400957a1395e0febbf771446ff34291afcbe3d8be2a279/fsspec-2026.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d8/8b/e2bbeb42068f0c48899e8eddd34902afc0f7429d4d2a152d2dc2670dc661/pythreejs-2.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d8/95/0a351b9289c2b5cbde0bacd4a83ebc44023e835490a727b2a3bd60ddc0f4/pillow-12.2.0-cp312-cp312-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/db/bc/83e112abc66cd466c6b83f99118035867cecd41802f8d044638aa78a106e/locket-1.0.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/dc/83/6d810a8a9ebc9c307989b418840c20e46907c74d707beb67ab566773e6fc/xarray-2026.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/dc/b7/901d837999a9350a7773289f7760cb473d4ba01fdc6ebae0ff2553d269ac/ncrystal_python-4.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e4/bc/daa30c02069eeac5b9198985ba42f5d65ca71bed6705b18329e51d352b7c/plopp-26.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/eb/be/b257e12f9710819fde40adc972578bee6b72c5992da1bc8369bef2597756/nbmake-1.5.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ee/8c/83087ebc47ab0396ce092363001fa37c17153119ee282700c0713a195853/prettytable-3.17.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f1/5b/e63c877c4c94382b66de5045e08ec8cd960e8a4d22f0d62a4dfb1f9e5ac6/ipydatawidgets-4.3.5-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.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/f4/34/a9dbe051de88a63eb7408ea66630bac38e72f7f6077d4be58737106860d9/virtualenv-21.3.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f4/a4/61adb19f3c74b0dc0e411de4f06ebef564b1f179928f9dffcbd4b378f2ef/jupyter_notebook_parser-0.1.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f5/57/2a154a69d6642860300bf8eb205d13131104991f2b1065bbb9075ac5c32e/tof-26.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f6/f0/10642828a8dfb741e5f3fbaac830550a518a775c7fff6f04a007259b0548/py-1.11.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f7/00/bbca25f8a2372465cdf93138c1e1e38dff045fb0afef1488f395d0afcb3b/ncrystal-4.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fa/bc/8b8ec5a4bfc5b9cf3ce27a118339e994f88410be5677c96493e0ea28e76d/dunamai-1.26.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl + win-64: - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.13.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/argon2-cffi-25.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/argon2-cffi-bindings-25.1.0-py314h5bd0f2a_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/arrow-1.4.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.18.0-pyhcf101f3_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/backports.zstd-1.3.0-py314h680f03e_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.14.3-pyha770c72_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.3.0-pyhcf101f3_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-with-css-6.3.0-hbca2aae_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.2.0-py314h3de4e8d_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_9.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/c-ares-1.34.6-hb03c661_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.4.22-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.4.22-h4c7d964_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cached-property-1.5.2-hd8ed1ab_1.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/cached_property-1.5.2-pyha770c72_1.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2026.4.22-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/cffi-2.0.0-py314h4a8dc5f_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.7-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.3-pyhe01879c_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.14.4-py314hd8ed1ab_100.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/debugpy-1.8.20-py314h42812f9_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.12.13-py312hd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/decorator-5.2.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/defusedxml-0.7.1-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/fqdn-1.5.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/gsl-2.8-hbf7d49c_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/h11-0.16.0-pyhcf101f3_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/httpcore-1.0.9-pyh29332c3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/httpx-0.28.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/icu-78.3-h33c6efd_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.13-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.8.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.2.0-pyha191276_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.13.0-pyh53cf698_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.2.0-pyh6dadd2b_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.13.0-pyhe2676ad_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ipython_pygments_lexers-1.1.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/isoduration-20.11.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jedi-0.19.2-pyhd8ed1ab_1.conda @@ -2080,332 +1757,386 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-with-format-nongpl-4.26.0-hcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter-lsp-2.3.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.8.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.9.1-pyhc90fa1f_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.9.1-pyh6dadd2b_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_events-0.12.1-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.17.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.18.2-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server_terminals-0.5.4-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.5.7-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_pygments-0.3.0-pyhd8ed1ab_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_server-2.28.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.3-hb9d3cd8_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.22.2-ha1258a1_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/lark-1.3.1-pyhd8ed1ab_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-6_h0358290_openblas.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20250104-pl5321h7949ede_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libev-4.33-hd590300_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.5-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.3-hb03c661_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb03c661_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libnghttp2-1.68.1-h877daf1_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.32-pthreads_h94d23a6_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libsodium-1.0.21-h280c20c_3.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.53.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.42-h5347b49_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libuv-1.51.0-hb03c661_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.2-h25fd6f3_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.3-py314h67df5f8_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.2.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mistune-3.2.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/msgspec-0.21.1-py314h5bd0f2a_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.2.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mistune-3.2.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/nbclient-0.10.4-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/nbconvert-core-7.17.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/nbformat-5.10.4-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.6-hdb14827_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/nodejs-25.8.2-he4ff34a_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/notebook-shim-0.2.4-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.2-h35e630c_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/overrides-7.7.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.2-pyhc364b38_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pandocfilters-1.5.0-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.6-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pexpect-4.9.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.7-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pixi-kernel-0.7.1-pyhbbac1ac_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.9.6-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/prometheus_client-0.25.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.52-pyha770c72_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/psutil-7.2.2-py314h0f05182_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ptyprocess-0.7.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.14.4-habeac84_100_cp314.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyh09c184e_7.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.21.2-pyhe01879c_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.14.4-h4df99d1_100.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.12.13-hd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-json-logger-3.2.1-pyh332efcf_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2026.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.14-8_cp314.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.3-py314h67df5f8_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/pyzmq-27.1.0-py312hda471dd_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.3-h853b02a_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.12-8_cp312.conda - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.37.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.33.1-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.34.2-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/returns-0.27.0-pyhc364b38_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3339-validator-0.1.4-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3986-validator-0.1.1-pyh9f0ad1d_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3987-syntax-1.1.0-pyhe01879c_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/rpds-py-0.30.0-py314h2e6c369_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/send2trash-2.1.0-pyha191276_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/send2trash-2.1.0-pyh6dadd2b_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-82.0.1-pyh332efcf_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sniffio-1.3.1-pyhd8ed1ab_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.8.3-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyhc90fa1f_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyh6dadd2b_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tinycss2-1.4.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h366c992_103.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.1-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/tornado-6.5.5-py314h5bd0f2a_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.15.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.15.0-h396c80c_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing_utils-0.1.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/uri-template-1.3.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.6.3-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.6.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.7.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.7.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/webcolors-25.10.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/webencodings-0.5.1-pyhd8ed1ab_3.conda - conda: https://conda.anaconda.org/conda-forge/noarch/websocket-client-1.9.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h280c20c_3.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/zeromq-4.3.5-h41580af_10.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/win_inet_pton-1.1.0-pyh7428d3b_8.conda - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.1-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/argon2-cffi-bindings-25.1.0-py312he06e257_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/backports.zstd-1.5.0-py312h06d0912_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/brotli-python-1.2.0-py312hc6d9e41_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h0ad9c76_9.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/cffi-2.0.0-py312he06e257_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/debugpy-1.8.20-py312ha1a9051_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/krb5-1.22.2-h0ea6238_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libblas-3.11.0-7_h8455456_mkl.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libcblas-3.11.0-7_h2a3cdd5_mkl.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.8.0-hac47afa_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libffi-3.5.2-h3d046cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libhwloc-2.13.0-default_h049141e_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.3-hfd05255_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libsodium-1.0.21-h6a83c73_3.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.53.1-hf5d6505_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libwinpthread-12.0.0.r4.gg4f2fc60ca-h57928b3_10.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libxml2-16-2.15.3-h692994f_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libxml2-2.15.3-hbc0d294_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.5-h4fa8253_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/markupsafe-3.0.3-py312h05f76fc_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/mkl-2026.0.0-hac47afa_906.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/msgspec-0.21.1-py312he06e257_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/nodejs-25.8.2-h80d1838_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/onemkl-license-2026.0.0-h57928b3_906.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.6.2-hf411b9b_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/psutil-7.2.2-py312he5662c2_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/python-3.12.13-h0159041_0_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pywin32-311-py312h829343e_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pywinpty-2.0.15-py312h275cf98_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pyyaml-6.0.3-py312h05f76fc_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pyzmq-27.1.0-py312h343a6d4_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/rpds-py-0.30.0-py312hdabe01f_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/tbb-2023.0.0-hd3d4ead_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/win-64/tornado-6.5.5-py312he06e257_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.26100.0-h57928b3_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-h41ae7f8_36.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.44.35208-h818238b_36.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.44.35208-h818238b_36.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/winpty-0.4.3-4.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/win-64/yaml-0.2.5-h6a83c73_3.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/zeromq-4.3.5-h507cc87_10.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_6.conda + - pypi: . + - pypi: https://files.pythonhosted.org/packages/03/c1/0976b235cf29ead553e22f2fb6385a8252b533715e00d0ae52ed7b900582/h5py-3.16.0-cp312-cp312-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/04/a1/4571fc46e7702de8d0c2dc54ad1b2f8e29328dea3ee90831181f7353d93c/matplotlib-3.10.9-cp312-cp312-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/0b/f9/f15c95d6b200167cb22c5eca5eecfa9d28a8ee3f74095f1cd2345c71f2f9/pydoclint-0.8.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0d/12/bbce9472f489cb5c4c23b0d13e5c59c37c1aab11b7ac637dfe6bbdccebe7/copier-9.15.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0d/1f/d398de1612f7a611e22d743280339c9af4903675635e41be3370091c704b/arviz_stats-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0d/fe/6bea5c9162869c5beba5d9c8abbed835ec85bf1ec1fba05a3822325c45f3/build-1.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/bd/c9/989f4034fb46841208de7aeeac2c6d8300745ab4f28c42f629ba77c2d916/aiohttp-3.13.5-cp314-cp314-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/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/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/9e/43/53afb8ba17218f19b77c7834128566c5bbb100a0ad9ba2e8e89d089d7079/autopep8-2.3.2-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/3e/5c/fb93d3092640a24dfb7bd7727a24016d7c01774ca013e60efd3f683c8002/backrefs-7.0-py314-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/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/0d/fe/6bea5c9162869c5beba5d9c8abbed835ec85bf1ec1fba05a3822325c45f3/build-1.5.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/7b/67/b1944235474aac3f0b0e1b232ce49547f9f9461ca4b943df1b88da5d3f1d/bumps-1.0.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/c2/20/193faab46a68ea550587331a698c3dca8099f8901d10937c4443135c7ed9/chardet-7.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/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/04/5f/9ff93450ba96b09c7c2b3f81c94de31c89f92292f1380261bd7195bea4ea/contourpy-1.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/98/ba/fc20faf5b2bb04615fa906f8daecfe896e6f7a56b34debd93c4a4d63e6e5/copier-9.15.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/22/e5/06b1f88f42a5a99df42ce61208bdec3bddb3d261412874280a19796fc09c/coverage-7.13.5-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/b2/e6/65abe97bbd42eb6ef73d3a58566ce89b097ae049511b7d9708288714a798/crysfml-0.6.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/96/5d/0c59079aa7ef34980a5925a06a90ad2b7c94e486c194b3527d557cabb042/cryspy-0.11.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/4a/f3/00bb1e867fba351e2d784170955713bee200c43ea306c59f30bd7e748192/dask-2026.3.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/c7/a0/5ff05d1919ca249508012cad89f08fdc6cfbdaa15b41651c5fe6dffaf1d3/dfo_ls-1.6.5-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/33/eb/f9f1ded8e4db9638f9530c3782eb01f5ab04945f4cb9e597a51c203fa4c5/diffpy_pdffit2-1.6.0.tar.gz - - pypi: https://files.pythonhosted.org/packages/b4/5e/bf11645aebb9af7d8d35927c40d3855816a0855c799e8156eeca8d632c90/diffpy_structure-3.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/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/85/d7/9b6ac05350ab7f7d3a730ff143ff3e2cada54514117c37be37e26dc91242/docstripy-0.7.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/fa/bc/8b8ec5a4bfc5b9cf3ce27a118339e994f88410be5677c96493e0ea28e76d/dunamai-1.26.1-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/c4/6d/82e65254354ba651dc966775270a9bbc02414a3eb3f1704e6c87dab2ea83/essdiffraction-26.5.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/8a/06/2c1bd1ee9eee3e65b0b6395dcf960e71b11576995eae3b4ab9ec63d89bea/essreduce-26.4.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/28/63/cd0c3b26afe60995a5295f37c246a93d454023726c3261cfbb3559969bb9/fonttools-4.62.1-cp314-cp314-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/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/d5/0c/043d5e551459da400957a1395e0febbf771446ff34291afcbe3d8be2a279/fsspec-2026.4.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d5/08/c2409cb01d5368dcfedcbaffa7d044cc8957d57a9d0855244a5eb4709d30/funcy-2.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e5/04/c5bb20d64417d20cba0105277235c51969444fa873000fbc26ac0a3fc5a8/gemmi-0.7.5-cp314-cp314-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/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/fd/6f/b842bfa6f21d6f87c57f9abf7194225e55279d96d869775e19e9f7236fc5/gitpython-3.1.49-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/a3/59/1bd6d7428d6ed9106efbb8c52310c60fd04f6672490f452aeaa3829aa436/greenlet-3.5.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/11/8c/c9138d881c79aa0ea9ed83cbd58d5ca75624378b38cee225dcf5c42cc91f/griffelib-2.0.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f1/16/d905e7f53e661ce2c24686c38048d8e2b750ffc4350009d41c4e6c6c9826/h5py-3.16.0-cp314-cp314-manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/12/aa/fb2a0649fdeef5ab7072d221e8f4df164098792c813af6c87e2581cfa860/mpltoolbox-26.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/12/c9/6869a1dcf4aaf309b9543ec070be3ec3adebee7c9bec9af8c230494134b9/interrogate-1.7.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/56/6d/0d9848617b9f753b87f214f1c682592f7ca42de085f564352f10f0843026/ipywidgets-8.1.8-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/f4/a4/61adb19f3c74b0dc0e411de4f06ebef564b1f179928f9dffcbd4b378f2ef/jupyter_notebook_parser-0.1.4-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ab/b5/36c712098e6191d1b4e349304ef73a8d06aed77e56ceaac8c0a306c7bda1/jupyterlab_widgets-3.0.16-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/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/e7/f9/b06c934a6aa8bc91f566bd2a214fd04c30506c2d9e2b6b171953216a65b6/kiwisolver-1.5.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/8a/a1/8d812e53a5da1687abb10445275d41a8b13adb781bbf7196ddbcf8d88505/lazy_loader-0.5-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/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/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/32/5d/f7e914f7d9325abff4057cee62c0fa70263683189f774473cbfb534cd13b/matplotlib-3.10.9-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/fb/86/dd6e5db36df29e76c7a7699123569a4a18c1623ce68d826ed96c62643cae/mdit_py_plugins-0.5.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/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/76/8e/56ccb09c7232a55403a7637caa21922f3b65901a37f5e8bdb405d0de0946/mike-2.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/13/95/cf3f7fe4910cf0365fa8ea0c731f4b8a624d97cd76ea777913ac8d0868e2/mkdocs_jupyter-0.26.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/19/d4/225027a913621a879b429a043674aa35220e6ce67785acad4f7bd0c4ff33/xarray_einstats-0.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/19/e8/6026ed58a64563186a9ee3f29f41261fd1828f527dd93d33b60feca63352/contourpy-1.3.3-cp312-cp312-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/1a/1f/86b4d15221096cb5500bcd73bf350745749e3ba056cdd7a7f75f126f154e/scipp-26.3.1-cp312-cp312-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/1a/c7/78200c18404ded028758b28b588aa1f4f3acd851271a74156a2a3db9eadf/crysfml-0.6.2-cp312-cp312-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/1c/59/964ecb8008722d27d8a835baea81f56a91cea8e097b3be992bc6ccde6367/versioningit-3.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1d/77/928ea2e70641ca177a11140062cc5840d421795f2e82749d408d0cce900a/narwhals-2.21.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/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/20/16/e777eadfa0c0305878c36fae1d5e6db474fbb15dae202b9ec378809dfb4d/nbstripout-0.9.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/20/7a/1c6e3562dfd8950adbb11ffbc65d21e7c89d01a6e4f137fa981056de25c5/gitpython-3.1.50-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-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/28/55/5733807f4af131ea6194309ac0f43eb5b05463c676d036ef948f3143c1f2/pycifrw-5.0.1-cp312-cp312-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/28/88/4789719fbbe166d12d345b3ac66b96105f10001b16e00a9765ba29261a21/nbqa-1.9.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/28/de/a3e710469772c6a89595fc52816da05c1e164b4c866a89e3cb82fb1b67c5/mkdocs_autorefs-1.4.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/13/95/cf3f7fe4910cf0365fa8ea0c731f4b8a624d97cd76ea777913ac8d0868e2/mkdocs_jupyter-0.26.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a8/4e/c09876f08fa9faaa5e1178f3d77b7af3f343258689bd6f3b72593b2f74e3/mkdocs_markdownextradata_plugin-0.2.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2a/2d/d4bf65e47cea8ff2c794a600c4fd1273a7902f268757c531e0ee9f18aa58/pooch-1.9.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/01/bc663630c510822c95c47a66af9fa7a443c295b47d5f041e5e6ae62ef659/mkdocs_material-9.7.6-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/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/6e/94/be70f8ee9c45f2f62b39a1f0e9303bc20e138a8f3b8e50ffd89498e177e1/mkdocstrings-1.0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2f/c8/005d1de3af80f54411703d1263a0b9d31276411ec9f273d9432c59b17299/arviz_plots-1.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/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/32/60/fca69c534602a7ced04280c952a246ad1edde2a6ca3a164f65d32ac41fe7/chardet-7.4.3-cp312-cp312-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/33/b2/986d1220f6ee931e338d272bc1f3ec02cfe5f9b5fad84e95afdad57f1ebc/format_docstring-0.2.7-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/33/f0/3fe8c6e69135a845f4106f2ff8b6805638d4e85c264e70114e8126689587/tokenize_rt-6.2.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/36/da/87912ddec6e06feffbaa3d7aa18fc6352bee2e8f1fee185d7d1690f8f4e8/backrefs-7.0-py312-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/38/8b/7ec325b4e9e78beefc2d025b01ee8a2fde771ef7c957c3bff99b9e1fbffa/xraydb-4.5.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3a/eb/fea4d1d51c49832120f7f285d07306db3960f423a2612c6057caf3e8196f/pip-26.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3c/26/1062c7ec1b053db9e499b4d2d5bc231743201b74051c973dadeac80a8f43/questionary-2.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3c/f0/cbf5d391b0b3a5e8cad264603e2fae256b0bde8ce43566b13b78faedc659/numpy-2.4.5-cp312-cp312-win_amd64.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/3f/0e/fa3b193432cfc60c93b42f3be03365f5f909d2b3ea410295cf36df739e31/widgetsnbextension-4.0.15-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3f/d0/7b958df957e4827837b590944008f0b28078f552b451f7407b4b3d54f574/asciichartpy-1.5.25-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/42/d9/27b13bc9419bf5dae02905b348f16ca827646cd76244ddd326f1a8139a6a/cyclebane-24.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/2f/40/dc34d1a8d5f1e51fc64640b62b191684da52ca469da9cd74e84936ffa4a6/msgpack-1.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/fe/3b/8ec5074bcfc450fe84273713b4b0a0dd47c0249358f5d82eb8104ffe2520/multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/d0/69/f24d3d1c38ad69e256138b4ec2452a8c7cf66be49dc214771ae99dd4f0a0/narwhals-2.20.0-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/20/16/e777eadfa0c0305878c36fae1d5e6db474fbb15dae202b9ec378809dfb4d/nbstripout-0.9.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/1a/66/8ce2af42feeba7a85b573e615a360ca1e204c75f6c8d4c5641140b1bfd17/ncrystal-4.3.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/0b/71/3b54e97c28cdf4993c8317d62ac1be655667455f129cf6162591d56aed89/ncrystal_core-4.3.4-py3-none-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/74/10/6434f57fa65651672534ce34a1f40ddbc7b880658ce50a8f9d7ab0830719/ncrystal_python-4.3.4-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/47/9e/fd90114059175cac64e4fafa9bf3ac20584384d66de40793ae2e2f26f3bb/sqlalchemy-2.0.49-cp312-cp312-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/4c/65/b4b86e5fa07543bfbbcdc6c9f7f9f561e66a5f3539992e3009973f2b1314/jupytext-1.19.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4c/fe/4fb3a0805bd5165da5ebf858da7cc01cce8061674106d2cf5bdab32cbfde/greenlet-3.5.0-cp312-cp312-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/52/59/0782e51887ac6b07ffd1570e0364cf901ebc36345fea669969d2084baebb/simple_websocket-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/56/6d/0d9848617b9f753b87f214f1c682592f7ca42de085f564352f10f0843026/ipywidgets-8.1.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5b/29/74eeb4d3f3ae61ca096b018ad486b3b3c74b17bec09ab4edab721cbefec3/typeguard-4.5.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5b/69/93b34728cc386efdde0c342f8c680b9187dea7beb7adaf6b58a0713be101/mpld3-0.5.12-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5c/01/6cb4d63c6b6933be4b7945b2f64638336420f04ea71ca5b9a7539c008bc5/scippnexus-26.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5d/7b/25a221d2c761c6a8ae21bfa3874988ff2583e19cf8a27bf2fee358df7942/pillow-12.2.0-cp312-cp312-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/61/2b/e260d50e64690d2a9e405d52ccd18a63c286c5088937dd0107cb23eb3195/diffpy_utils-3.7.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/61/7c/5c0d34aa3024694d6dcb9271cdbdd08c4e47c1c0ad95ec7e7bc74cdea145/propcache-0.5.2-cp312-cp312-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/63/9f/724f66a48309dd97a2ff58f491d6ffd925f35d1278a5e55dc9a5ac6a156b/scippneutron-26.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/64/a8/c09fbe44b12fa919c5bfe0afb71e60d1231a7dc93405e54c30496c57c9d3/arviz-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/69/d1/705e6c19b437a4105bf3b9ae7945fcfc3ad2abb73d14bae0a3f2d58b305b/arviz_base-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6c/25/4f103d1bedb3593718713b3f743df7b3ff3fc68d36d6666c30265ef59c8a/ase-3.28.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6e/94/be70f8ee9c45f2f62b39a1f0e9303bc20e138a8f3b8e50ffd89498e177e1/mkdocstrings-1.0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6f/0c/8297c8d978c919ad6318011631a6123082d5da940da5f8612e75a247d739/diffpy_pdffit2-1.6.0-cp312-cp312-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-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/76/8e/56ccb09c7232a55403a7637caa21922f3b65901a37f5e8bdb405d0de0946/mike-2.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/78/3c/2a612b95ddbb9a6bdcb47b7a93c4884f74c6ff22356b2f7b213b16e65c35/pycifstar-0.3.0.tar.gz + - pypi: https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.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/7b/67/b1944235474aac3f0b0e1b232ce49547f9f9461ca4b943df1b88da5d3f1d/bumps-1.0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7b/e3/d81b065a2d866a33a541ac63a2a4cc5737e03ce2379ac3191c98bb8867e3/trove_classifiers-2026.5.7.17-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7e/85/545a951eecc270fcd688288c600017e2050a1aacb56c711d208586d3e470/pymdown_extensions-10.21.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/80/31/0b2517913687895f5904325c2069d6a3b78f66cc641a86a2baf75a05dcbb/multidict-6.7.1-cp312-cp312-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/85/19/93853133df2cb371083285ef6a93982a0173e7a233b0f61373ba9fd30eb2/coverage-7.14.0-cp312-cp312-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/85/d7/9b6ac05350ab7f7d3a730ff143ff3e2cada54514117c37be37e26dc91242/docstripy-0.7.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/87/36/cccb9bc2a6ab63d1b2980374f0dca72ce95ae267c9b4cfe77455bb70d0d4/fonttools-4.63.0-cp312-cp312-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/98/7c/21252050676612625449b4807d6b695b9ce8a7c9e1c197ee6216c8a65c7c/numpy-2.4.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/8a/06/2c1bd1ee9eee3e65b0b6395dcf960e71b11576995eae3b4ab9ec63d89bea/essreduce-26.4.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8a/a1/8d812e53a5da1687abb10445275d41a8b13adb781bbf7196ddbcf8d88505/lazy_loader-0.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8c/ec/d431eb7941fb55a31dd6ca3404d41fbb52d99172df2e7707754488390910/msgpack-1.1.2-cp312-cp312-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/8d/c0/fdf9d3ee103ce66a55f0532835ad5e154226c5222423c6636ba049dc42fc/traittypes-0.2.3-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8f/5e/f1e1dd319e35e962a4e00b33150a8868b6329cc1d19fd533436ba5488f09/uncertainties-3.2.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8f/82/b54e646be7b938fcbdda10030c6533bd2bb1a59930a1381cc83d6050a49c/spglib-2.6.0-cp312-cp312-win_amd64.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/88/3cdd54fa279341afa10acf8d2b503556b1375245dccc9315659f795dd2e9/pandas-3.0.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/11/8d/d2532ad2a603ca2b93ad9f5135732124e57811d0168155852f37fbce2458/pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/70/7a/be4bd8bcbb24ea475856dd68159d78b03b2bb53dae369f69c9606b8888f5/pip-26.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e4/bc/daa30c02069eeac5b9198985ba42f5d65ca71bed6705b18329e51d352b7c/plopp-26.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/90/ad/cba91b3bcf04073e4d1655a5c1710ef3f457f56f7d1b79dcc3d72f4dd912/plotly-6.7.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/91/4c/e0ce1ef95d4000ebc1c11801f9b944fa5910ecc15b5e351865763d8657f8/graphviz-0.21-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/93/f7/d00d9b4a0313a6be3a3e0818e6375e15da6d7076f4ae47d1324e7ca986a1/radon-6.0.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/96/5d/0c59079aa7ef34980a5925a06a90ad2b7c94e486c194b3527d557cabb042/cryspy-0.11.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/99/31/6cf181011dc738c33bf6ba7aea2e8e1d3c1f71b7dab1942f3054f66f6202/asteval-1.0.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/99/37/e8730c3587a65eb5645d4aba2d27aae48e8003614d6aaf15dda67f702f1f/bidict-0.23.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9e/43/53afb8ba17218f19b77c7834128566c5bbb100a0ad9ba2e8e89d089d7079/autopep8-2.3.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a2/84/dc08d77fbf3d87d3ee27f6a0c6dcce1de5829a64f2eae85a0ecc1f0daa73/scipy-1.17.1-cp312-cp312-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/a3/58/35da89ee790598a0700ea49b2a66594140f44dec458c07e8e3d4979137fc/ply-3.11-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/2a/2d/d4bf65e47cea8ff2c794a600c4fd1273a7902f268757c531e0ee9f18aa58/pooch-1.9.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-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/b2/f2/889ad4b2408f72fe1a4f6a19491177b30ea7bf1a0fd5f17050ca08cfc882/propcache-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/f6/f0/10642828a8dfb741e5f3fbaac830550a518a775c7fff6f04a007259b0548/py-1.11.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/25/7d/cea3531f77df694ac7f169378250d85f19f69b09a5f4fa45f650837ae7cc/py3dmol-2.5.4-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e0/a9/023730ba63db1e494a271cb018dcd361bd2c917ba7004c3e49d5daf795a2/py_cpuinfo-9.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/58/e0/f1871f520c359e4e3a2eb7437c9e7e792bb6c356414e8617937561167caf/pycifrw-5.0.1.tar.gz - - 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/f3/0a/fd7d723f8f8153418fb40cf9c940e82004fce7e987026b08a68a36dd3fe7/pydantic-2.13.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/fd/86/ef96a4c6e79e7a2d0410826a68fbc0eccc0fd44aa733be199d5fcac3bb87/pydantic_core-2.46.3-cp314-cp314-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/f7/27/a2fc51a4a122dfd1015e921ae9d22fee3d20b0b8080d9a704578bf9deece/pymdown_extensions-10.21.2-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/a4/39/3a0ae5b0edb66e61bb0e8bc53a503495cba5892297ae21faf6ba0525e681/varname-1.0.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/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.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/aa/54/0cce26da03a981f949bb8449c9778537f75f5917c172e1d2992ff25cb57d/python_engineio-4.13.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/aa/ca/eadf6f9c8fa5e31d40993e3db153fb5ed0b11008ad5d9de98a95045bed84/aiohttp-3.13.5-cp312-cp312-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ab/a6/870a3e8a50590bb92be184ad928c2922f088b00d9dc5c5ec7b924ee08c22/ruff-0.15.13-py3-none-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/ad/cf/0348374369ca588f8fe9c338fae49fa4e16eeb10ffb3d012f23a54578a9e/kiwisolver-1.5.0-cp312-cp312-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/af/46/661159ad844034ba8b3f4e0516215c41e4ee17db4213d13a82227670764f/sciline-25.11.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/52/bc858b1665d0dec3a2511f4e6f5c18ea85c0977563d624d597c95d6d0fd7/jupyterquiz-2.9.6.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b4/5e/bf11645aebb9af7d8d35927c40d3855816a0855c799e8156eeca8d632c90/diffpy_structure-3.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/6f/a05a317a66fee0aad270011461f1a63a453ed12471249f172f7d2e2bc7b4/python_discovery-1.3.1-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/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/b9/c5/fc1b368f303087d20e8c9bf3d6ceb186263cfac0ade735cd938538bea839/pandas-3.0.3-cp312-cp312-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/33/29/e756e715a48959f1c0045342088d7ca9762a2f509b945f362a316e9412b7/pytest_benchmark-5.2.3-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/bf/50/98b146aea0f1cd7531d25f12bea69fa9ce8d1662124f93fb30dc4511b65e/docstring_parser_fork-0.0.14-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c1/d4/59e74daffcb57a07668852eeeb6035af9f32cbfd7a1d2511f17d2fe6a738/smmap-5.0.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c4/6d/82e65254354ba651dc966775270a9bbc02414a3eb3f1704e6c87dab2ea83/essdiffraction-26.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c6/3d/020a6b6248c3d4a37797db068256f0b3f15b01bc481327ba888c50309aa8/mkdocs_plugin_inline_svg-0.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c7/6b/6c02f55c2ce2f137ccca0986be7dd89bea31d5bee4346b4377fa3b8586df/ncrystal_core-4.4.2-py3-none-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/c7/a0/5ff05d1919ca249508012cad89f08fdc6cfbdaa15b41651c5fe6dffaf1d3/dfo_ls-1.6.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d8/db/795879cc3ddfe338599bddea6388cc5100b088db0a4caf6e6c1af1c27e04/python_discovery-1.2.2-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/07/c7/deb8c5e604404dbf10a3808a858946ca3547692ff6316b698945bb72177e/python_socketio-5.16.1-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/d2/f0/834e479e47e499b6478e807fb57b31cc2db696c4db30557bb6f5aea4a90b/mando-0.7.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d5/08/c2409cb01d5368dcfedcbaffa7d044cc8957d57a9d0855244a5eb4709d30/funcy-2.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d5/0c/043d5e551459da400957a1395e0febbf771446ff34291afcbe3d8be2a279/fsspec-2026.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.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/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.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/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e8/39/c61d193b8a1daaa8977f7dea9e8d8ba866e02ea7b65d32f6861693aa4c12/ruff-0.15.12-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/43/fe/ad0ecbe2393cb690a4b3100a8fea47ecfdb49f6e06f40cf2f626635adc0c/scipp-26.3.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/82/9b/cf7e6f157c53d2c8bc0165580350c53939aea3b2a8390ec8e07f0adb82de/scippneutron-26.4.1-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/0b/2e/7eea398450457ecb54e18e9d10110993fa65561c4f3add5e8eccd2b9cd41/scipy-1.17.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/db/bc/83e112abc66cd466c6b83f99118035867cecd41802f8d044638aa78a106e/locket-1.0.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/dc/83/6d810a8a9ebc9c307989b418840c20e46907c74d707beb67ab566773e6fc/xarray-2026.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/dc/b7/901d837999a9350a7773289f7760cb473d4ba01fdc6ebae0ff2553d269ac/ncrystal_python-4.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-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/c1/d4/59e74daffcb57a07668852eeeb6035af9f32cbfd7a1d2511f17d2fe6a738/smmap-5.0.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/bf/34/1fe99124be59579ebd24316522e1da780979c856977b142c0dcd878b0a2d/spglib-2.6.0.tar.gz - - pypi: https://files.pythonhosted.org/packages/2e/84/efc7c0bf3a1c5eef81d397f6fddac855becdbb11cb38ff957888603014a7/sqlalchemy-2.0.49-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.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/e4/bc/daa30c02069eeac5b9198985ba42f5d65ca71bed6705b18329e51d352b7c/plopp-26.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/eb/be/b257e12f9710819fde40adc972578bee6b72c5992da1bc8369bef2597756/nbmake-1.5.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/eb/f2/53be7a4ba5816e13c39be0f728facac4bcb39cf4903ceeec54b006511c8f/gemmi-0.7.5-cp312-cp312-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/ee/8c/83087ebc47ab0396ce092363001fa37c17153119ee282700c0713a195853/prettytable-3.17.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f1/5b/e63c877c4c94382b66de5045e08ec8cd960e8a4d22f0d62a4dfb1f9e5ac6/ipydatawidgets-4.3.5-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.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/f4/34/a9dbe051de88a63eb7408ea66630bac38e72f7f6077d4be58737106860d9/virtualenv-21.3.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f4/a4/61adb19f3c74b0dc0e411de4f06ebef564b1f179928f9dffcbd4b378f2ef/jupyter_notebook_parser-0.1.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f5/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/f5/be/25216a49daeeb7af2bec0db22d5e7df08ed1d7c9f65d78b14f3b74fd72fc/yarl-1.23.0-cp312-cp312-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/f6/f0/10642828a8dfb741e5f3fbaac830550a518a775c7fff6f04a007259b0548/py-1.11.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f7/00/bbca25f8a2372465cdf93138c1e1e38dff045fb0afef1488f395d0afcb3b/ncrystal-4.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fa/bc/8b8ec5a4bfc5b9cf3ce27a118339e994f88410be5677c96493e0ea28e76d/dunamai-1.26.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/8d/c0/fdf9d3ee103ce66a55f0532835ad5e154226c5222423c6636ba049dc42fc/traittypes-0.2.3-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d6/bb/fbdc4e57731efb86b4209ffdd2519520782bf27b3c961beac3e5c20d2b87/trove_classifiers-2026.4.28.13-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/91/88/b55b3117287a8540b76dbdd87733808d4d01c8067a3b339408c250bb3600/typeguard-4.5.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-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/b7/ee/e9c95cda829131f71a8dff5ce0406059fd16e591c074414e31ada19ba7c3/validate_pyproject-0.25-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a4/39/3a0ae5b0edb66e61bb0e8bc53a503495cba5892297ae21faf6ba0525e681/varname-1.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/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/4b/eb/03bfb1299d4c4510329e470f13f9a4ce793df7fcb5a2fd3510f911066f61/virtualenv-21.3.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/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/42/2b/fef67d616931055bf3d6764885990a3ac647d68734a2d6a9e1d13de437a2/yarl-1.23.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - - pypi: ./ - osx-arm64: - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/_openmp_mutex-4.5-7_kmp_llvm.conda + - pypi: https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl + py-314-env: + channels: + - url: https://conda.anaconda.org/nodefaults/ + - url: https://conda.anaconda.org/conda-forge/ + indexes: + - https://pypi.org/simple + packages: + linux-64: + - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/argon2-cffi-bindings-25.1.0-py314h5bd0f2a_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.2.0-py314h3de4e8d_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_9.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/c-ares-1.34.6-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cffi-2.0.0-py314h4a8dc5f_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/debugpy-1.8.20-py314h42812f9_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-78.3-h33c6efd_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.3-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.22.2-ha1258a1_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45.1-default_hbd61a6d_102.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libabseil-20260107.1-cxx17_h7b12aa8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libblas-3.11.0-7_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-7_h0358290_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20250104-pl5321h7949ede_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libev-4.33-hd590300_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.8.0-hecca717_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h3435931_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-he0feb66_19.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.2.0-h69a702a_19.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.2.0-h69a702a_19.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.2.0-h68bc16d_19.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-he0feb66_19.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.3-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libnghttp2-1.68.1-h877daf1_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.33-pthreads_h94d23a6_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsodium-1.0.21-h280c20c_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.53.1-h0c1763c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h934c35e_19.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.42-h5347b49_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libuv-1.51.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.2-h25fd6f3_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.3-py314h67df5f8_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/msgspec-0.21.1-py314h5bd0f2a_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.6-hdb14827_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/nodejs-25.8.2-he4ff34a_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.2-h35e630c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/psutil-7.2.2-py314h0f05182_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.14.4-habeac84_100_cp314.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.3-py314h67df5f8_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pyzmq-27.1.0-py312hda471dd_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.3-h853b02a_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/rpds-py-0.30.0-py314h2e6c369_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h366c992_103.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tornado-6.5.5-py314h5bd0f2a_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h280c20c_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zeromq-4.3.5-h41580af_10.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.13.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/appnope-0.1.4-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/argon2-cffi-25.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/argon2-cffi-bindings-25.1.0-py314h0612a62_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/arrow-1.4.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.18.0-pyhcf101f3_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/backports.zstd-1.3.0-py314h680f03e_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/backports.zstd-1.5.0-py314h680f03e_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.14.3-pyha770c72_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.3.0-pyhcf101f3_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-with-css-6.3.0-hbca2aae_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-python-1.2.0-py314h3daef5d_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-hd037594_9.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/c-ares-1.34.6-hc919400_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.4.22-hbd8a1cb_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cached-property-1.5.2-hd8ed1ab_1.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/cached_property-1.5.2-pyha770c72_1.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2026.4.22-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cffi-2.0.0-py314h44086f9_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.7-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.3-pyhe01879c_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.14.4-py314hd8ed1ab_100.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/debugpy-1.8.20-py314he609de1_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/decorator-5.2.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/defusedxml-0.7.1-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/fqdn-1.5.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/gsl-2.8-h8d0574d_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/h11-0.16.0-pyhcf101f3_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/httpcore-1.0.9-pyh29332c3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/httpx-0.28.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/icu-78.3-hef89b57_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.13-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.8.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.2.0-pyh5552912_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.2.0-pyha191276_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.13.0-pyh53cf698_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ipython_pygments_lexers-1.1.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/isoduration-20.11.0-pyhd8ed1ab_1.conda @@ -2420,83 +2151,46 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.8.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.9.1-pyhc90fa1f_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_events-0.12.1-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.17.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.18.2-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server_terminals-0.5.4-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.5.7-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_pygments-0.3.0-pyhd8ed1ab_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_server-2.28.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/krb5-1.22.2-h385eeb1_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/lark-1.3.1-pyhd8ed1ab_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-6_hb0561ab_openblas.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-22.1.4-h55c6f16_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libedit-3.1.20250104-pl5321hafb1f1b_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libev-4.33-h93a5062_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.7.5-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.3-h8088a28_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libmpdec-4.0.0-h84a0fba_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libnghttp2-1.68.1-h8f3e76b_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopenblas-0.3.32-openmp_he657e61_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsodium-1.0.21-h1a92334_3.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.53.0-h1b79a29_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libuv-1.51.0-h6caf38d_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.2-h8088a28_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/llvm-openmp-22.1.4-hc7d1edf_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/markupsafe-3.0.3-py314h6e9b3f0_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.2.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mistune-3.2.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/msgspec-0.21.1-py314h6c2aa35_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.2.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mistune-3.2.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/nbclient-0.10.4-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/nbconvert-core-7.17.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/nbformat-5.10.4-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.6-h1d4f5a5_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/nodejs-25.8.2-h7039424_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/notebook-shim-0.2.4-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.6.2-hd24854e_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/overrides-7.7.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.2-pyhc364b38_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pandocfilters-1.5.0-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.6-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.7-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pexpect-4.9.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pixi-kernel-0.7.1-pyhbbac1ac_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.9.6-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/prometheus_client-0.25.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.52-pyha770c72_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/psutil-7.2.2-py314ha14b1ff_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ptyprocess-0.7.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyobjc-core-12.1-py314h3a4d195_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyobjc-framework-cocoa-12.1-py314h36abed7_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.14.4-h4c637c5_100_cp314.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.21.2-pyhe01879c_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.14.4-h4df99d1_100.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-json-logger-3.2.1-pyh332efcf_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2026.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.14-8_cp314.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyyaml-6.0.3-py314h6e9b3f0_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyzmq-27.1.0-py312h022ad19_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.3-h46df422_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.37.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.33.1-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.34.2-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/returns-0.27.0-pyhc364b38_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3339-validator-0.1.4-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3986-validator-0.1.1-pyh9f0ad1d_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3987-syntax-1.1.0-pyhe01879c_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/rpds-py-0.30.0-py314haad56a0_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/send2trash-2.1.0-pyh5552912_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/send2trash-2.1.0-pyha191276_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-82.0.1-pyh332efcf_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sniffio-1.3.1-pyhd8ed1ab_2.conda @@ -2504,231 +2198,225 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyhc90fa1f_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tinycss2-1.4.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h010d191_3.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.1-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tornado-6.5.5-py314h6c2aa35_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.15.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.15.0-h396c80c_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing_utils-0.1.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/uri-template-1.3.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.6.3-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.6.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.7.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.7.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/webcolors-25.10.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/webencodings-0.5.1-pyhd8ed1ab_3.conda - conda: https://conda.anaconda.org/conda-forge/noarch/websocket-client-1.9.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/yaml-0.2.5-h925e9cb_3.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zeromq-4.3.5-h4818236_10.conda - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.1-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hbf9d68e_6.conda + - pypi: . + - pypi: https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/04/5f/9ff93450ba96b09c7c2b3f81c94de31c89f92292f1380261bd7195bea4ea/contourpy-1.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/07/c7/deb8c5e604404dbf10a3808a858946ca3547692ff6316b698945bb72177e/python_socketio-5.16.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0b/2e/7eea398450457ecb54e18e9d10110993fa65561c4f3add5e8eccd2b9cd41/scipy-1.17.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/0b/f9/f15c95d6b200167cb22c5eca5eecfa9d28a8ee3f74095f1cd2345c71f2f9/pydoclint-0.8.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0d/12/bbce9472f489cb5c4c23b0d13e5c59c37c1aab11b7ac637dfe6bbdccebe7/copier-9.15.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0d/1f/d398de1612f7a611e22d743280339c9af4903675635e41be3370091c704b/arviz_stats-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0d/fe/6bea5c9162869c5beba5d9c8abbed835ec85bf1ec1fba05a3822325c45f3/build-1.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/9b/91/cc8cc78a111826c54743d88651e1687008133c37e5ee615fee9b57990fac/aiohttp-3.13.5-cp314-cp314-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/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/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/9e/43/53afb8ba17218f19b77c7834128566c5bbb100a0ad9ba2e8e89d089d7079/autopep8-2.3.2-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/3e/5c/fb93d3092640a24dfb7bd7727a24016d7c01774ca013e60efd3f683c8002/backrefs-7.0-py314-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/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/0d/fe/6bea5c9162869c5beba5d9c8abbed835ec85bf1ec1fba05a3822325c45f3/build-1.5.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/7b/67/b1944235474aac3f0b0e1b232ce49547f9f9461ca4b943df1b88da5d3f1d/bumps-1.0.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/51/ac/b9d68ebddfe1b02c77af5bf81120e12b036b4432dc6af7a303d90e2bc38b/chardet-7.4.3-cp314-cp314-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-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/fd/e1/3542a9cb596cadd76fcef413f19c79216e002623158befe6daa03dbfa88c/contourpy-1.3.3-cp314-cp314-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/98/ba/fc20faf5b2bb04615fa906f8daecfe896e6f7a56b34debd93c4a4d63e6e5/copier-9.15.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e2/3e/51dff36d99ae14639a133d9b164d63e628532e2974d8b1edb99dd1ebc733/coverage-7.13.5-cp314-cp314-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/d0/42/bf7fb4d923a15b99b678ecb3bdcc02d336ee34baa876c6f41c5c55038b9c/crysfml-0.6.1-cp314-cp314-macosx_14_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/96/5d/0c59079aa7ef34980a5925a06a90ad2b7c94e486c194b3527d557cabb042/cryspy-0.11.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/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/4a/f3/00bb1e867fba351e2d784170955713bee200c43ea306c59f30bd7e748192/dask-2026.3.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/c7/a0/5ff05d1919ca249508012cad89f08fdc6cfbdaa15b41651c5fe6dffaf1d3/dfo_ls-1.6.5-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/04/f1/58c14b37525dc075f3bdf149251f079723049a9f1c82eb48835a0e6b8db3/diffpy_pdffit2-1.6.0-cp314-cp314-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/b4/5e/bf11645aebb9af7d8d35927c40d3855816a0855c799e8156eeca8d632c90/diffpy_structure-3.4.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/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/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/85/d7/9b6ac05350ab7f7d3a730ff143ff3e2cada54514117c37be37e26dc91242/docstripy-0.7.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/fa/bc/8b8ec5a4bfc5b9cf3ce27a118339e994f88410be5677c96493e0ea28e76d/dunamai-1.26.1-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/c4/6d/82e65254354ba651dc966775270a9bbc02414a3eb3f1704e6c87dab2ea83/essdiffraction-26.5.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/8a/06/2c1bd1ee9eee3e65b0b6395dcf960e71b11576995eae3b4ab9ec63d89bea/essreduce-26.4.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/36/f0/2888cdac391807d68d90dcb16ef858ddc1b5309bfc6966195a459dd326e2/fonttools-4.62.1-cp314-cp314-macosx_10_15_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/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/d5/0c/043d5e551459da400957a1395e0febbf771446ff34291afcbe3d8be2a279/fsspec-2026.4.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d5/08/c2409cb01d5368dcfedcbaffa7d044cc8957d57a9d0855244a5eb4709d30/funcy-2.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/06/41/4e70dea1d0311016c0b0b1c53a24a266f9f8a34c6bc1af0f17cfca20aa1d/gemmi-0.7.5-cp314-cp314-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/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/fd/6f/b842bfa6f21d6f87c57f9abf7194225e55279d96d869775e19e9f7236fc5/gitpython-3.1.49-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/11/8c/c9138d881c79aa0ea9ed83cbd58d5ca75624378b38cee225dcf5c42cc91f/griffelib-2.0.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/6a/b7/9366ed44ced9b7ef357ab48c94205280276db9d7f064aa3012a97227e966/h5py-3.16.0-cp314-cp314-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/11/8d/d2532ad2a603ca2b93ad9f5135732124e57811d0168155852f37fbce2458/pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/12/aa/fb2a0649fdeef5ab7072d221e8f4df164098792c813af6c87e2581cfa860/mpltoolbox-26.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/12/c9/6869a1dcf4aaf309b9543ec070be3ec3adebee7c9bec9af8c230494134b9/interrogate-1.7.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/56/6d/0d9848617b9f753b87f214f1c682592f7ca42de085f564352f10f0843026/ipywidgets-8.1.8-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/f4/a4/61adb19f3c74b0dc0e411de4f06ebef564b1f179928f9dffcbd4b378f2ef/jupyter_notebook_parser-0.1.4-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ab/b5/36c712098e6191d1b4e349304ef73a8d06aed77e56ceaac8c0a306c7bda1/jupyterlab_widgets-3.0.16-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/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/49/b2/97980f3ad4fae37dd7fe31626e2bf75fbf8bdf5d303950ec1fab39a12da8/kiwisolver-1.5.0-cp314-cp314-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/8a/a1/8d812e53a5da1687abb10445275d41a8b13adb781bbf7196ddbcf8d88505/lazy_loader-0.5-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/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/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/41/86/86231232fff41c9f8e4a1a7d7a597d349a02527109c3af7d618366122139/matplotlib-3.10.9-cp314-cp314-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/fb/86/dd6e5db36df29e76c7a7699123569a4a18c1623ce68d826ed96c62643cae/mdit_py_plugins-0.5.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/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/76/8e/56ccb09c7232a55403a7637caa21922f3b65901a37f5e8bdb405d0de0946/mike-2.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/13/95/cf3f7fe4910cf0365fa8ea0c731f4b8a624d97cd76ea777913ac8d0868e2/mkdocs_jupyter-0.26.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/19/d4/225027a913621a879b429a043674aa35220e6ce67785acad4f7bd0c4ff33/xarray_einstats-0.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1c/59/964ecb8008722d27d8a835baea81f56a91cea8e097b3be992bc6ccde6367/versioningit-3.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1d/77/928ea2e70641ca177a11140062cc5840d421795f2e82749d408d0cce900a/narwhals-2.21.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/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/20/16/e777eadfa0c0305878c36fae1d5e6db474fbb15dae202b9ec378809dfb4d/nbstripout-0.9.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/20/7a/1c6e3562dfd8950adbb11ffbc65d21e7c89d01a6e4f137fa981056de25c5/gitpython-3.1.50-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-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/28/88/4789719fbbe166d12d345b3ac66b96105f10001b16e00a9765ba29261a21/nbqa-1.9.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/28/de/a3e710469772c6a89595fc52816da05c1e164b4c866a89e3cb82fb1b67c5/mkdocs_autorefs-1.4.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/13/95/cf3f7fe4910cf0365fa8ea0c731f4b8a624d97cd76ea777913ac8d0868e2/mkdocs_jupyter-0.26.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a8/4e/c09876f08fa9faaa5e1178f3d77b7af3f343258689bd6f3b72593b2f74e3/mkdocs_markdownextradata_plugin-0.2.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2a/2d/d4bf65e47cea8ff2c794a600c4fd1273a7902f268757c531e0ee9f18aa58/pooch-1.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2b/76/7f1bfd6afff4c5e38e36a3c6d68eb5f4b7311ea80baf693db78d95b603c4/propcache-0.5.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/2c/01/bc663630c510822c95c47a66af9fa7a443c295b47d5f041e5e6ae62ef659/mkdocs_material-9.7.6-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/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/6e/94/be70f8ee9c45f2f62b39a1f0e9303bc20e138a8f3b8e50ffd89498e177e1/mkdocstrings-1.0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2e/84/efc7c0bf3a1c5eef81d397f6fddac855becdbb11cb38ff957888603014a7/sqlalchemy-2.0.49-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/2f/40/dc34d1a8d5f1e51fc64640b62b191684da52ca469da9cd74e84936ffa4a6/msgpack-1.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/2f/c8/005d1de3af80f54411703d1263a0b9d31276411ec9f273d9432c59b17299/arviz_plots-1.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/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/32/5d/f7e914f7d9325abff4057cee62c0fa70263683189f774473cbfb534cd13b/matplotlib-3.10.9-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/33/b2/986d1220f6ee931e338d272bc1f3ec02cfe5f9b5fad84e95afdad57f1ebc/format_docstring-0.2.7-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/33/eb/f9f1ded8e4db9638f9530c3782eb01f5ab04945f4cb9e597a51c203fa4c5/diffpy_pdffit2-1.6.0.tar.gz + - 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/36/e1/a8933a72c45a87177fbde2696e0d0755c8c9062f8c077a961c6215fa27b1/fonttools-4.63.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/38/7e/7b91c89a4cf0f543a83be978657afb20c86af6d725253e319589dcc4ce52/lmfit-1.3.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/38/8b/7ec325b4e9e78beefc2d025b01ee8a2fde771ef7c957c3bff99b9e1fbffa/xraydb-4.5.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3a/eb/fea4d1d51c49832120f7f285d07306db3960f423a2612c6057caf3e8196f/pip-26.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3c/26/1062c7ec1b053db9e499b4d2d5bc231743201b74051c973dadeac80a8f43/questionary-2.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3e/17/1f31d8562e6f970d64911f1abc330d233bc0c0601411cf7e19c1292be6da/spdx_headers-1.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3e/5c/fb93d3092640a24dfb7bd7727a24016d7c01774ca013e60efd3f683c8002/backrefs-7.0-py314-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3f/0e/fa3b193432cfc60c93b42f3be03365f5f909d2b3ea410295cf36df739e31/widgetsnbextension-4.0.15-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3f/d0/7b958df957e4827837b590944008f0b28078f552b451f7407b4b3d54f574/asciichartpy-1.5.25-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/42/2b/fef67d616931055bf3d6764885990a3ac647d68734a2d6a9e1d13de437a2/yarl-1.23.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/42/d9/27b13bc9419bf5dae02905b348f16ca827646cd76244ddd326f1a8139a6a/cyclebane-24.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/43/80/1315439acedd8398319bac177d6de3d48ab39c62cc0c810f74f0a9a73996/numpy-2.4.5-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/1b/9f/38ff9e57a2eade7bf9dfee5eae17f39fc0e998658050279cbb14d97d36d9/msgpack-1.1.2-cp314-cp314-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/f1/4f/733c48f270565d78b4544f2baddc2fb2a245e5a8640254b12c36ac7ac68e/multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/d0/69/f24d3d1c38ad69e256138b4ec2452a8c7cf66be49dc214771ae99dd4f0a0/narwhals-2.20.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/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/20/16/e777eadfa0c0305878c36fae1d5e6db474fbb15dae202b9ec378809dfb4d/nbstripout-0.9.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/1a/66/8ce2af42feeba7a85b573e615a360ca1e204c75f6c8d4c5641140b1bfd17/ncrystal-4.3.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a0/0d/df49ae8af94215db241701f692786a2e85c3ac4557aafd829270c54fb1fa/ncrystal_core-4.3.4-py3-none-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/74/10/6434f57fa65651672534ce34a1f40ddbc7b880658ce50a8f9d7ab0830719/ncrystal_python-4.3.4-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/91/fb/287076b2614e1d1044235f50f03748f31fa287e3dbe6abeb35cdfa351eca/numpy-2.4.4-cp314-cp314-macosx_14_0_arm64.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/95/25/bdb9326c3b5455f8d4d3549fce7abcf967259de146fe2cf7a82368141948/pandas-3.0.2-cp314-cp314-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ba/8c/1a9e46228571de18f8e28f16fabdfc20212a5d019f3e3303452b3f0a580d/pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/70/7a/be4bd8bcbb24ea475856dd68159d78b03b2bb53dae369f69c9606b8888f5/pip-26.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e4/bc/daa30c02069eeac5b9198985ba42f5d65ca71bed6705b18329e51d352b7c/plopp-26.4.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/90/ad/cba91b3bcf04073e4d1655a5c1710ef3f457f56f7d1b79dcc3d72f4dd912/plotly-6.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/43/fe/ad0ecbe2393cb690a4b3100a8fea47ecfdb49f6e06f40cf2f626635adc0c/scipp-26.3.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/46/b4/0887c88ddfaba1d7140ea335144eb904af97550786ee58bdb295ff10d255/crysfml-0.6.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/4a/f3/00bb1e867fba351e2d784170955713bee200c43ea306c59f30bd7e748192/dask-2026.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4c/65/b4b86e5fa07543bfbbcdc6c9f7f9f561e66a5f3539992e3009973f2b1314/jupytext-1.19.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/52/59/0782e51887ac6b07ffd1570e0364cf901ebc36345fea669969d2084baebb/simple_websocket-1.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/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/2a/2d/d4bf65e47cea8ff2c794a600c4fd1273a7902f268757c531e0ee9f18aa58/pooch-1.9.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ee/8c/83087ebc47ab0396ce092363001fa37c17153119ee282700c0713a195853/prettytable-3.17.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b2/fa/89a8ef0468d5833a23fff277b143d0573897cf75bd56670a6d28126c7d68/propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.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/25/7d/cea3531f77df694ac7f169378250d85f19f69b09a5f4fa45f650837ae7cc/py3dmol-2.5.4-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e0/a9/023730ba63db1e494a271cb018dcd361bd2c917ba7004c3e49d5daf795a2/py_cpuinfo-9.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/56/6d/0d9848617b9f753b87f214f1c682592f7ca42de085f564352f10f0843026/ipywidgets-8.1.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/58/3b/1cdec6772bdbaf7b25dab360c59f03cadf05492dd724c6540af905389b07/pandas-3.0.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/58/e0/f1871f520c359e4e3a2eb7437c9e7e792bb6c356414e8617937561167caf/pycifrw-5.0.1.tar.gz + - pypi: https://files.pythonhosted.org/packages/5b/29/74eeb4d3f3ae61ca096b018ad486b3b3c74b17bec09ab4edab721cbefec3/typeguard-4.5.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5b/69/93b34728cc386efdde0c342f8c680b9187dea7beb7adaf6b58a0713be101/mpld3-0.5.12-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5c/01/6cb4d63c6b6933be4b7945b2f64638336420f04ea71ca5b9a7539c008bc5/scippnexus-26.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/61/2b/e260d50e64690d2a9e405d52ccd18a63c286c5088937dd0107cb23eb3195/diffpy_utils-3.7.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/63/9f/724f66a48309dd97a2ff58f491d6ffd925f35d1278a5e55dc9a5ac6a156b/scippneutron-26.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/64/a8/c09fbe44b12fa919c5bfe0afb71e60d1231a7dc93405e54c30496c57c9d3/arviz-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/69/d1/705e6c19b437a4105bf3b9ae7945fcfc3ad2abb73d14bae0a3f2d58b305b/arviz_base-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6c/25/4f103d1bedb3593718713b3f743df7b3ff3fc68d36d6666c30265ef59c8a/ase-3.28.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6e/94/be70f8ee9c45f2f62b39a1f0e9303bc20e138a8f3b8e50ffd89498e177e1/mkdocstrings-1.0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-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/76/8e/56ccb09c7232a55403a7637caa21922f3b65901a37f5e8bdb405d0de0946/mike-2.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/78/3c/2a612b95ddbb9a6bdcb47b7a93c4884f74c6ff22356b2f7b213b16e65c35/pycifstar-0.3.0.tar.gz - - pypi: https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f3/0a/fd7d723f8f8153418fb40cf9c940e82004fce7e987026b08a68a36dd3fe7/pydantic-2.13.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/24/35/e4066358a22e3e99519db370494c7528f5a2aa1367370e80e27e20283543/pydantic_core-2.46.3-cp314-cp314-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/f7/27/a2fc51a4a122dfd1015e921ae9d22fee3d20b0b8080d9a704578bf9deece/pymdown_extensions-10.21.2-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/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/33/29/e756e715a48959f1c0045342088d7ca9762a2f509b945f362a316e9412b7/pytest_benchmark-5.2.3-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/d8/db/795879cc3ddfe338599bddea6388cc5100b088db0a4caf6e6c1af1c27e04/python_discovery-1.2.2-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/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/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.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/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.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/7b/67/b1944235474aac3f0b0e1b232ce49547f9f9461ca4b943df1b88da5d3f1d/bumps-1.0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7b/e3/d81b065a2d866a33a541ac63a2a4cc5737e03ce2379ac3191c98bb8867e3/trove_classifiers-2026.5.7.17-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7e/85/545a951eecc270fcd688288c600017e2050a1aacb56c711d208586d3e470/pymdown_extensions-10.21.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/aa/a4/f828e9718d3dce1f5f11c39c4f65afd32783c8b2aebb2e3d259e492c47bd/ruff-0.15.12-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/0f/0e/0eb94e64f5badef67f11fe1e448dde2a44f00940d8949f4adf71d560552e/scipp-26.3.1-cp314-cp314-macosx_14_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/82/9b/cf7e6f157c53d2c8bc0165580350c53939aea3b2a8390ec8e07f0adb82de/scippneutron-26.4.1-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/db/7b/8624a203326675d7746a254083a187398090a179335b2e4a20e2ddc46e83/scipy-1.17.1-cp314-cp314-macosx_14_0_arm64.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/c1/d4/59e74daffcb57a07668852eeeb6035af9f32cbfd7a1d2511f17d2fe6a738/smmap-5.0.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/bf/34/1fe99124be59579ebd24316522e1da780979c856977b142c0dcd878b0a2d/spglib-2.6.0.tar.gz - - pypi: https://files.pythonhosted.org/packages/55/33/bf28f618c0a9597d14e0b9ee7d1e0622faff738d44fe986ee287cdf1b8d0/sqlalchemy-2.0.49-cp314-cp314-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.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/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/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/85/d7/9b6ac05350ab7f7d3a730ff143ff3e2cada54514117c37be37e26dc91242/docstripy-0.7.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/8a/06/2c1bd1ee9eee3e65b0b6395dcf960e71b11576995eae3b4ab9ec63d89bea/essreduce-26.4.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8a/a1/8d812e53a5da1687abb10445275d41a8b13adb781bbf7196ddbcf8d88505/lazy_loader-0.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8d/c0/fdf9d3ee103ce66a55f0532835ad5e154226c5222423c6636ba049dc42fc/traittypes-0.2.3-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d6/bb/fbdc4e57731efb86b4209ffdd2519520782bf27b3c961beac3e5c20d2b87/trove_classifiers-2026.4.28.13-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/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-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/8f/5e/f1e1dd319e35e962a4e00b33150a8868b6329cc1d19fd533436ba5488f09/uncertainties-3.2.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/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/90/ad/cba91b3bcf04073e4d1655a5c1710ef3f457f56f7d1b79dcc3d72f4dd912/plotly-6.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/91/4c/e0ce1ef95d4000ebc1c11801f9b944fa5910ecc15b5e351865763d8657f8/graphviz-0.21-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/93/f7/d00d9b4a0313a6be3a3e0818e6375e15da6d7076f4ae47d1324e7ca986a1/radon-6.0.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/96/5d/0c59079aa7ef34980a5925a06a90ad2b7c94e486c194b3527d557cabb042/cryspy-0.11.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/99/31/6cf181011dc738c33bf6ba7aea2e8e1d3c1f71b7dab1942f3054f66f6202/asteval-1.0.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/99/37/e8730c3587a65eb5645d4aba2d27aae48e8003614d6aaf15dda67f702f1f/bidict-0.23.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9e/43/53afb8ba17218f19b77c7834128566c5bbb100a0ad9ba2e8e89d089d7079/autopep8-2.3.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a3/58/35da89ee790598a0700ea49b2a66594140f44dec458c07e8e3d4979137fc/ply-3.11-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a3/59/1bd6d7428d6ed9106efbb8c52310c60fd04f6672490f452aeaa3829aa436/greenlet-3.5.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/a4/39/3a0ae5b0edb66e61bb0e8bc53a503495cba5892297ae21faf6ba0525e681/varname-1.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/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/4b/eb/03bfb1299d4c4510329e470f13f9a4ce793df7fcb5a2fd3510f911066f61/virtualenv-21.3.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz - - 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/24/f9/e8242b68362bffe6fb536c8db5076861466fc780f0f1b479fc4ffbebb128/yarl-1.23.0-cp314-cp314-macosx_11_0_arm64.whl - - pypi: ./ - win-64: - - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.13.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/argon2-cffi-25.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/argon2-cffi-bindings-25.1.0-py314h5a2d7ad_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/arrow-1.4.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.3.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.18.0-pyhcf101f3_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/backports.zstd-1.3.0-py314h680f03e_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.14.3-pyha770c72_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.3.0-pyhcf101f3_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-with-css-6.3.0-hbca2aae_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/brotli-python-1.2.0-py314he701e3d_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h0ad9c76_9.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.4.22-h4c7d964_0.conda + - pypi: https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/a8/4e/c09876f08fa9faaa5e1178f3d77b7af3f343258689bd6f3b72593b2f74e3/mkdocs_markdownextradata_plugin-0.2.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/aa/54/0cce26da03a981f949bb8449c9778537f75f5917c172e1d2992ff25cb57d/python_engineio-4.13.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ab/b5/36c712098e6191d1b4e349304ef73a8d06aed77e56ceaac8c0a306c7bda1/jupyterlab_widgets-3.0.16-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/af/46/661159ad844034ba8b3f4e0516215c41e4ee17db4213d13a82227670764f/sciline-25.11.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/52/bc858b1665d0dec3a2511f4e6f5c18ea85c0977563d624d597c95d6d0fd7/jupyterquiz-2.9.6.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b4/5e/bf11645aebb9af7d8d35927c40d3855816a0855c799e8156eeca8d632c90/diffpy_structure-3.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/b7/6f/a05a317a66fee0aad270011461f1a63a453ed12471249f172f7d2e2bc7b4/python_discovery-1.3.1-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/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bd/c9/989f4034fb46841208de7aeeac2c6d8300745ab4f28c42f629ba77c2d916/aiohttp-3.13.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/bf/34/1fe99124be59579ebd24316522e1da780979c856977b142c0dcd878b0a2d/spglib-2.6.0.tar.gz + - pypi: https://files.pythonhosted.org/packages/bf/50/98b146aea0f1cd7531d25f12bea69fa9ce8d1662124f93fb30dc4511b65e/docstring_parser_fork-0.0.14-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c1/d4/59e74daffcb57a07668852eeeb6035af9f32cbfd7a1d2511f17d2fe6a738/smmap-5.0.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c2/20/193faab46a68ea550587331a698c3dca8099f8901d10937c4443135c7ed9/chardet-7.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/c4/6d/82e65254354ba651dc966775270a9bbc02414a3eb3f1704e6c87dab2ea83/essdiffraction-26.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c6/3d/020a6b6248c3d4a37797db068256f0b3f15b01bc481327ba888c50309aa8/mkdocs_plugin_inline_svg-0.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c7/a0/5ff05d1919ca249508012cad89f08fdc6cfbdaa15b41651c5fe6dffaf1d3/dfo_ls-1.6.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-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/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-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/d5/0c/043d5e551459da400957a1395e0febbf771446ff34291afcbe3d8be2a279/fsspec-2026.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d7/51/ec641c26e6dca1b25a7d2035ba6ecb7c884ef1a100a9e42fbe4ce4405139/coverage-7.14.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/d8/8b/e2bbeb42068f0c48899e8eddd34902afc0f7429d4d2a152d2dc2670dc661/pythreejs-2.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/db/bc/83e112abc66cd466c6b83f99118035867cecd41802f8d044638aa78a106e/locket-1.0.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/dc/83/6d810a8a9ebc9c307989b418840c20e46907c74d707beb67ab566773e6fc/xarray-2026.4.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/dc/b7/901d837999a9350a7773289f7760cb473d4ba01fdc6ebae0ff2553d269ac/ncrystal_python-4.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e4/bc/daa30c02069eeac5b9198985ba42f5d65ca71bed6705b18329e51d352b7c/plopp-26.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e5/04/c5bb20d64417d20cba0105277235c51969444fa873000fbc26ac0a3fc5a8/gemmi-0.7.5-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e7/f9/b06c934a6aa8bc91f566bd2a214fd04c30506c2d9e2b6b171953216a65b6/kiwisolver-1.5.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/e8/31/bf1a0803d077e679cfeee5f2f67290a0fa79c7385b5d9a8c17b9db2c48f0/ruff-0.15.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/eb/be/b257e12f9710819fde40adc972578bee6b72c5992da1bc8369bef2597756/nbmake-1.5.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ee/8c/83087ebc47ab0396ce092363001fa37c17153119ee282700c0713a195853/prettytable-3.17.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f1/16/d905e7f53e661ce2c24686c38048d8e2b750ffc4350009d41c4e6c6c9826/h5py-3.16.0-cp314-cp314-manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/f1/2c/3850985d4c64048dec7b826f8a803e135b52b11b4c81c9cd4326b1ca15ab/ncrystal_core-4.4.2-py3-none-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/f1/5b/e63c877c4c94382b66de5045e08ec8cd960e8a4d22f0d62a4dfb1f9e5ac6/ipydatawidgets-4.3.5-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.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/f4/34/a9dbe051de88a63eb7408ea66630bac38e72f7f6077d4be58737106860d9/virtualenv-21.3.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f4/a4/61adb19f3c74b0dc0e411de4f06ebef564b1f179928f9dffcbd4b378f2ef/jupyter_notebook_parser-0.1.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f5/57/2a154a69d6642860300bf8eb205d13131104991f2b1065bbb9075ac5c32e/tof-26.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f6/f0/10642828a8dfb741e5f3fbaac830550a518a775c7fff6f04a007259b0548/py-1.11.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f7/00/bbca25f8a2372465cdf93138c1e1e38dff045fb0afef1488f395d0afcb3b/ncrystal-4.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fa/bc/8b8ec5a4bfc5b9cf3ce27a118339e994f88410be5677c96493e0ea28e76d/dunamai-1.26.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fe/3b/8ec5074bcfc450fe84273713b4b0a0dd47c0249358f5d82eb8104ffe2520/multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + osx-arm64: + - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.13.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/appnope-0.1.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/argon2-cffi-25.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/arrow-1.4.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.18.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/backports.zstd-1.5.0-py314h680f03e_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.14.3-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.3.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-with-css-6.3.0-hbca2aae_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.4.22-hbd8a1cb_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cached-property-1.5.2-hd8ed1ab_1.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/cached_property-1.5.2-pyha770c72_1.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2026.4.22-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/cffi-2.0.0-py314h5a2d7ad_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.7-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.3-pyhe01879c_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.14.4-py314hd8ed1ab_100.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/debugpy-1.8.20-py314hb98de8c_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/decorator-5.2.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/defusedxml-0.7.1-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/fqdn-1.5.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/gsl-2.8-h5b8d9c4_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/h11-0.16.0-pyhcf101f3_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda @@ -2737,8 +2425,8 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.13-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.8.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.2.0-pyh6dadd2b_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.13.0-pyhe2676ad_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.2.0-pyh5552912_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.13.0-pyh53cf698_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ipython_pygments_lexers-1.1.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/isoduration-20.11.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jedi-0.19.2-pyhd8ed1ab_1.conda @@ -2750,3400 +2438,4138 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-with-format-nongpl-4.26.0-hcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter-lsp-2.3.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.8.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.9.1-pyh6dadd2b_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.9.1-pyhc90fa1f_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_events-0.12.1-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.17.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.18.2-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server_terminals-0.5.4-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.5.7-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_pygments-0.3.0-pyhd8ed1ab_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_server-2.28.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/krb5-1.22.2-h0ea6238_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/lark-1.3.1-pyhd8ed1ab_0.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.5-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.3-hfd05255_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libmpdec-4.0.0-hfd05255_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libsodium-1.0.21-h6a83c73_3.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.53.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.3-h692994f_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libxml2-2.15.3-hbc0d294_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.4-h4fa8253_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/markupsafe-3.0.3-py314h2359020_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.2.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mistune-3.2.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/mkl-2025.3.1-hac47afa_12.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/msgspec-0.21.1-py314h5a2d7ad_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.2.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mistune-3.2.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/nbclient-0.10.4-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/nbconvert-core-7.17.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/nbformat-5.10.4-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/nodejs-25.8.2-h80d1838_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/notebook-shim-0.2.4-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/onemkl-license-2025.3.1-h57928b3_12.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.6.2-hf411b9b_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/overrides-7.7.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.2-pyhc364b38_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pandocfilters-1.5.0-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.6-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.7-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pexpect-4.9.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pixi-kernel-0.7.1-pyhbbac1ac_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.9.6-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/prometheus_client-0.25.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.52-pyha770c72_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/psutil-7.2.2-py314hc5dbbe4_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ptyprocess-0.7.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyh09c184e_7.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/python-3.14.4-h4b44e0e_100_cp314.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.21.2-pyhe01879c_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.14.4-h4df99d1_100.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-json-logger-3.2.1-pyh332efcf_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2026.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.14-8_cp314.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/pywin32-311-py314h8f8f202_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/pywinpty-2.0.15-py314h51f0985_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/pyyaml-6.0.3-py314h2359020_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/pyzmq-27.1.0-py312h343a6d4_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.37.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.33.1-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.34.2-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/returns-0.27.0-pyhc364b38_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3339-validator-0.1.4-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3986-validator-0.1.1-pyh9f0ad1d_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3987-syntax-1.1.0-pyhe01879c_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/rpds-py-0.30.0-py314h9f07db2_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/send2trash-2.1.0-pyh6dadd2b_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/send2trash-2.1.0-pyh5552912_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-82.0.1-pyh332efcf_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sniffio-1.3.1-pyhd8ed1ab_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.8.3-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/tbb-2022.3.0-h3155e25_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyh6dadd2b_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyhc90fa1f_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tinycss2-1.4.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h6ed50ae_3.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.1-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/tornado-6.5.5-py314h5a2d7ad_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.15.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.15.0-h396c80c_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing_utils-0.1.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.26100.0-h57928b3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/uri-template-1.3.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.6.3-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-h41ae7f8_34.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.44.35208-h818238b_34.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.44.35208-h818238b_34.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.6.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.7.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.7.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/webcolors-25.10.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/webencodings-0.5.1-pyhd8ed1ab_3.conda - conda: https://conda.anaconda.org/conda-forge/noarch/websocket-client-1.9.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/win_inet_pton-1.1.0-pyh7428d3b_8.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/winpty-0.4.3-4.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/win-64/yaml-0.2.5-h6a83c73_3.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/zeromq-4.3.5-h507cc87_10.conda - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.1-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_6.conda + - 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/argon2-cffi-bindings-25.1.0-py314h0612a62_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-python-1.2.0-py314h3daef5d_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-hd037594_9.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/c-ares-1.34.6-hc919400_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cffi-2.0.0-py314h44086f9_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/debugpy-1.8.20-py314he609de1_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-78.3-hef89b57_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/krb5-1.22.2-h385eeb1_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libabseil-20260107.1-cxx17_h2062a1b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libblas-3.11.0-7_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-7_hb0561ab_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-22.1.5-h55c6f16_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libedit-3.1.20250104-pl5321hafb1f1b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libev-4.33-h93a5062_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.8.0-hf6b4638_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.5.2-hcf2aa1b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgcc-15.2.0-hcbb3090_19.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran-15.2.0-h07b0088_19.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran5-15.2.0-hdae7583_19.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.3-h8088a28_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libmpdec-4.0.0-h84a0fba_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libnghttp2-1.68.1-h8f3e76b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopenblas-0.3.33-openmp_he657e61_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsodium-1.0.21-h1a92334_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.53.1-h1b79a29_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libuv-1.51.0-h6caf38d_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.2-h8088a28_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/llvm-openmp-22.1.5-hc7d1edf_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/markupsafe-3.0.3-py314h6e9b3f0_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/msgspec-0.21.1-py314h6c2aa35_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.6-h1d4f5a5_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/nodejs-25.8.2-h7039424_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.6.2-hd24854e_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/psutil-7.2.2-py314ha14b1ff_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyobjc-core-12.1-py314h3a4d195_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyobjc-framework-cocoa-12.1-py314h36abed7_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.14.4-h4c637c5_100_cp314.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyyaml-6.0.3-py314h6e9b3f0_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyzmq-27.1.0-py312h022ad19_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.3-h46df422_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/rpds-py-0.30.0-py314haad56a0_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h010d191_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tornado-6.5.5-py314h6c2aa35_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/yaml-0.2.5-h925e9cb_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zeromq-4.3.5-h4818236_10.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hbf9d68e_6.conda + - pypi: . + - pypi: https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/04/f1/58c14b37525dc075f3bdf149251f079723049a9f1c82eb48835a0e6b8db3/diffpy_pdffit2-1.6.0-cp314-cp314-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/06/41/4e70dea1d0311016c0b0b1c53a24a266f9f8a34c6bc1af0f17cfca20aa1d/gemmi-0.7.5-cp314-cp314-macosx_11_0_arm64.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/0b/f9/f15c95d6b200167cb22c5eca5eecfa9d28a8ee3f74095f1cd2345c71f2f9/pydoclint-0.8.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0d/12/bbce9472f489cb5c4c23b0d13e5c59c37c1aab11b7ac637dfe6bbdccebe7/copier-9.15.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0d/1f/d398de1612f7a611e22d743280339c9af4903675635e41be3370091c704b/arviz_stats-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0d/fe/6bea5c9162869c5beba5d9c8abbed835ec85bf1ec1fba05a3822325c45f3/build-1.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0f/0e/0eb94e64f5badef67f11fe1e448dde2a44f00940d8949f4adf71d560552e/scipp-26.3.1-cp314-cp314-macosx_14_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/22/4d/eaedff67fc805aeba4ba746aec891b4b24cebb1a7d078084b6300f79d063/aiohttp-3.13.5-cp314-cp314-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/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/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/9e/43/53afb8ba17218f19b77c7834128566c5bbb100a0ad9ba2e8e89d089d7079/autopep8-2.3.2-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/3e/5c/fb93d3092640a24dfb7bd7727a24016d7c01774ca013e60efd3f683c8002/backrefs-7.0-py314-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/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/0d/fe/6bea5c9162869c5beba5d9c8abbed835ec85bf1ec1fba05a3822325c45f3/build-1.5.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/7b/67/b1944235474aac3f0b0e1b232ce49547f9f9461ca4b943df1b88da5d3f1d/bumps-1.0.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b1/2c/cad8b5e3623a987f3c930b68e2bdd06cfc388cd91cd42ed05f1227701b73/chardet-7.4.3-cp314-cp314-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/7d/c2/57f54b03d0f22d4044b8afb9ca0e184f8b1afd57b4f735c2fa70883dc601/contourpy-1.3.3-cp314-cp314-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/98/ba/fc20faf5b2bb04615fa906f8daecfe896e6f7a56b34debd93c4a4d63e6e5/copier-9.15.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/8d/b7/4758d4f73fb536347cc5e4ad63662f9d60ba9118cb6785e9616b2ce5d7fa/coverage-7.13.5-cp314-cp314-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/d9/1a/8a81b3a66f36969c8456f8af3a12f7d601fdd9cfed2ad5b4e72a2fb7ea8d/crysfml-0.6.1-cp314-cp314-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/96/5d/0c59079aa7ef34980a5925a06a90ad2b7c94e486c194b3527d557cabb042/cryspy-0.11.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/4a/f3/00bb1e867fba351e2d784170955713bee200c43ea306c59f30bd7e748192/dask-2026.3.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/c7/a0/5ff05d1919ca249508012cad89f08fdc6cfbdaa15b41651c5fe6dffaf1d3/dfo_ls-1.6.5-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/82/d0/26c81ffbe588f936d05f395da34046c66322e8067c9fd331c788c4f682f2/diffpy_pdffit2-1.6.0-cp314-cp314-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/b4/5e/bf11645aebb9af7d8d35927c40d3855816a0855c799e8156eeca8d632c90/diffpy_structure-3.4.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/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/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/85/d7/9b6ac05350ab7f7d3a730ff143ff3e2cada54514117c37be37e26dc91242/docstripy-0.7.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/fa/bc/8b8ec5a4bfc5b9cf3ce27a118339e994f88410be5677c96493e0ea28e76d/dunamai-1.26.1-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/c4/6d/82e65254354ba651dc966775270a9bbc02414a3eb3f1704e6c87dab2ea83/essdiffraction-26.5.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/8a/06/2c1bd1ee9eee3e65b0b6395dcf960e71b11576995eae3b4ab9ec63d89bea/essreduce-26.4.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/6b/67/74b070029043186b5dd13462c958cb7c7f811be0d2e634309d9a1ffb1505/fonttools-4.62.1-cp314-cp314-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/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/d5/0c/043d5e551459da400957a1395e0febbf771446ff34291afcbe3d8be2a279/fsspec-2026.4.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d5/08/c2409cb01d5368dcfedcbaffa7d044cc8957d57a9d0855244a5eb4709d30/funcy-2.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ff/1c/a28b27effb13a381fe077ea3e3e78f6debd6315f2b3edff67bbb93d0ef51/gemmi-0.7.5-cp314-cp314-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/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/fd/6f/b842bfa6f21d6f87c57f9abf7194225e55279d96d869775e19e9f7236fc5/gitpython-3.1.49-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/20/6b165108058767ee643c55c5c4904d591a830ee2b3c7dbd359828fbc829f/greenlet-3.5.0-cp314-cp314-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/11/8c/c9138d881c79aa0ea9ed83cbd58d5ca75624378b38cee225dcf5c42cc91f/griffelib-2.0.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/3e/14/615a450205e1b56d16c6783f5ccd116cde05550faad70ae077c955654a75/h5py-3.16.0-cp314-cp314-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/12/aa/fb2a0649fdeef5ab7072d221e8f4df164098792c813af6c87e2581cfa860/mpltoolbox-26.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/12/c9/6869a1dcf4aaf309b9543ec070be3ec3adebee7c9bec9af8c230494134b9/interrogate-1.7.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/56/6d/0d9848617b9f753b87f214f1c682592f7ca42de085f564352f10f0843026/ipywidgets-8.1.8-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/f4/a4/61adb19f3c74b0dc0e411de4f06ebef564b1f179928f9dffcbd4b378f2ef/jupyter_notebook_parser-0.1.4-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ab/b5/36c712098e6191d1b4e349304ef73a8d06aed77e56ceaac8c0a306c7bda1/jupyterlab_widgets-3.0.16-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/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/a3/36/4e551e8aa55c9188bca9abb5096805edbf7431072b76e2298e34fd3a3008/kiwisolver-1.5.0-cp314-cp314-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/8a/a1/8d812e53a5da1687abb10445275d41a8b13adb781bbf7196ddbcf8d88505/lazy_loader-0.5-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/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/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/ab/1a/5a4f747a8b271cbb024946d2dd3c913ab5032ba430626f8c3528ada96b4b/matplotlib-3.10.9-cp314-cp314-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/fb/86/dd6e5db36df29e76c7a7699123569a4a18c1623ce68d826ed96c62643cae/mdit_py_plugins-0.5.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/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/76/8e/56ccb09c7232a55403a7637caa21922f3b65901a37f5e8bdb405d0de0946/mike-2.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/13/95/cf3f7fe4910cf0365fa8ea0c731f4b8a624d97cd76ea777913ac8d0868e2/mkdocs_jupyter-0.26.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/19/d4/225027a913621a879b429a043674aa35220e6ce67785acad4f7bd0c4ff33/xarray_einstats-0.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1b/9f/38ff9e57a2eade7bf9dfee5eae17f39fc0e998658050279cbb14d97d36d9/msgpack-1.1.2-cp314-cp314-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/1c/59/964ecb8008722d27d8a835baea81f56a91cea8e097b3be992bc6ccde6367/versioningit-3.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1d/77/928ea2e70641ca177a11140062cc5840d421795f2e82749d408d0cce900a/narwhals-2.21.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/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1f/7e/c2cfe0bdbec1f5ce2bd92e03311038e1c491dfd54824606f38a61167a3f0/crysfml-0.6.2-cp314-cp314-macosx_14_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/20/16/e777eadfa0c0305878c36fae1d5e6db474fbb15dae202b9ec378809dfb4d/nbstripout-0.9.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/20/7a/1c6e3562dfd8950adbb11ffbc65d21e7c89d01a6e4f137fa981056de25c5/gitpython-3.1.50-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/24/f9/e8242b68362bffe6fb536c8db5076861466fc780f0f1b479fc4ffbebb128/yarl-1.23.0-cp314-cp314-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/25/1c/196c610ff4c6782d697ba780ebdc1616be143213701bf22c1a270f3bf7dd/numpy-2.4.5-cp314-cp314-macosx_14_0_arm64.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/27/d2/23d25e3f247b328be58d04a4c9f894178a0d1eda7d42867cfb388adaf416/fonttools-4.63.0-cp314-cp314-macosx_10_15_universal2.whl + - pypi: https://files.pythonhosted.org/packages/28/88/4789719fbbe166d12d345b3ac66b96105f10001b16e00a9765ba29261a21/nbqa-1.9.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/28/de/a3e710469772c6a89595fc52816da05c1e164b4c866a89e3cb82fb1b67c5/mkdocs_autorefs-1.4.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/13/95/cf3f7fe4910cf0365fa8ea0c731f4b8a624d97cd76ea777913ac8d0868e2/mkdocs_jupyter-0.26.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a8/4e/c09876f08fa9faaa5e1178f3d77b7af3f343258689bd6f3b72593b2f74e3/mkdocs_markdownextradata_plugin-0.2.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2a/2d/d4bf65e47cea8ff2c794a600c4fd1273a7902f268757c531e0ee9f18aa58/pooch-1.9.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/01/bc663630c510822c95c47a66af9fa7a443c295b47d5f041e5e6ae62ef659/mkdocs_material-9.7.6-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/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/6e/94/be70f8ee9c45f2f62b39a1f0e9303bc20e138a8f3b8e50ffd89498e177e1/mkdocstrings-1.0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2f/c8/005d1de3af80f54411703d1263a0b9d31276411ec9f273d9432c59b17299/arviz_plots-1.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/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/6e/f1/abd09c2ae91228c5f3998dbd7f41353def9eac64253de3c8105efa2082f7/msgpack-1.1.2-cp314-cp314-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/e0/bf/52f25716bbe93745595800f36fb17b73711f14da59ed0bb2eba141bc9f0f/multidict-6.7.1-cp314-cp314-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/d0/69/f24d3d1c38ad69e256138b4ec2452a8c7cf66be49dc214771ae99dd4f0a0/narwhals-2.20.0-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/20/16/e777eadfa0c0305878c36fae1d5e6db474fbb15dae202b9ec378809dfb4d/nbstripout-0.9.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/1a/66/8ce2af42feeba7a85b573e615a360ca1e204c75f6c8d4c5641140b1bfd17/ncrystal-4.3.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ab/14/d708fb7c6bf7e7be586c625840cc078a45e64dd6bffe8d60a7b17a22b24e/ncrystal_core-4.3.4-py3-none-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/74/10/6434f57fa65651672534ce34a1f40ddbc7b880658ce50a8f9d7ab0830719/ncrystal_python-4.3.4-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/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/33/b2/986d1220f6ee931e338d272bc1f3ec02cfe5f9b5fad84e95afdad57f1ebc/format_docstring-0.2.7-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/33/f0/3fe8c6e69135a845f4106f2ff8b6805638d4e85c264e70114e8126689587/tokenize_rt-6.2.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/38/7e/7b91c89a4cf0f543a83be978657afb20c86af6d725253e319589dcc4ce52/lmfit-1.3.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/38/8b/7ec325b4e9e78beefc2d025b01ee8a2fde771ef7c957c3bff99b9e1fbffa/xraydb-4.5.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3a/eb/fea4d1d51c49832120f7f285d07306db3960f423a2612c6057caf3e8196f/pip-26.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3c/26/1062c7ec1b053db9e499b4d2d5bc231743201b74051c973dadeac80a8f43/questionary-2.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3e/17/1f31d8562e6f970d64911f1abc330d233bc0c0601411cf7e19c1292be6da/spdx_headers-1.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3e/5c/fb93d3092640a24dfb7bd7727a24016d7c01774ca013e60efd3f683c8002/backrefs-7.0-py314-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3f/0e/fa3b193432cfc60c93b42f3be03365f5f909d2b3ea410295cf36df739e31/widgetsnbextension-4.0.15-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3f/d0/7b958df957e4827837b590944008f0b28078f552b451f7407b4b3d54f574/asciichartpy-1.5.25-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/41/86/86231232fff41c9f8e4a1a7d7a597d349a02527109c3af7d618366122139/matplotlib-3.10.9-cp314-cp314-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/42/d9/27b13bc9419bf5dae02905b348f16ca827646cd76244ddd326f1a8139a6a/cyclebane-24.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/49/b2/97980f3ad4fae37dd7fe31626e2bf75fbf8bdf5d303950ec1fab39a12da8/kiwisolver-1.5.0-cp314-cp314-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/4a/f3/00bb1e867fba351e2d784170955713bee200c43ea306c59f30bd7e748192/dask-2026.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4c/65/b4b86e5fa07543bfbbcdc6c9f7f9f561e66a5f3539992e3009973f2b1314/jupytext-1.19.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/51/ac/b9d68ebddfe1b02c77af5bf81120e12b036b4432dc6af7a303d90e2bc38b/chardet-7.4.3-cp314-cp314-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/52/59/0782e51887ac6b07ffd1570e0364cf901ebc36345fea669969d2084baebb/simple_websocket-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/55/33/bf28f618c0a9597d14e0b9ee7d1e0622faff738d44fe986ee287cdf1b8d0/sqlalchemy-2.0.49-cp314-cp314-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/56/6d/0d9848617b9f753b87f214f1c682592f7ca42de085f564352f10f0843026/ipywidgets-8.1.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/58/e0/f1871f520c359e4e3a2eb7437c9e7e792bb6c356414e8617937561167caf/pycifrw-5.0.1.tar.gz + - pypi: https://files.pythonhosted.org/packages/5b/29/74eeb4d3f3ae61ca096b018ad486b3b3c74b17bec09ab4edab721cbefec3/typeguard-4.5.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5b/69/93b34728cc386efdde0c342f8c680b9187dea7beb7adaf6b58a0713be101/mpld3-0.5.12-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5c/01/6cb4d63c6b6933be4b7945b2f64638336420f04ea71ca5b9a7539c008bc5/scippnexus-26.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/61/2b/e260d50e64690d2a9e405d52ccd18a63c286c5088937dd0107cb23eb3195/diffpy_utils-3.7.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/63/9f/724f66a48309dd97a2ff58f491d6ffd925f35d1278a5e55dc9a5ac6a156b/scippneutron-26.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/63/b1/4260d67d6bd85e58a66b72d54ce15d5de789b6f3870cc6bedf8ff9667401/propcache-0.5.2-cp314-cp314-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/64/a8/c09fbe44b12fa919c5bfe0afb71e60d1231a7dc93405e54c30496c57c9d3/arviz-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/68/10/bf2d6738d72748b961a3751ab89522d58c54efc36a8e1a12161216cd45cf/pandas-3.0.3-cp314-cp314-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/69/d1/705e6c19b437a4105bf3b9ae7945fcfc3ad2abb73d14bae0a3f2d58b305b/arviz_base-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6a/b7/9366ed44ced9b7ef357ab48c94205280276db9d7f064aa3012a97227e966/h5py-3.16.0-cp314-cp314-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/6c/25/4f103d1bedb3593718713b3f743df7b3ff3fc68d36d6666c30265ef59c8a/ase-3.28.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6e/94/be70f8ee9c45f2f62b39a1f0e9303bc20e138a8f3b8e50ffd89498e177e1/mkdocstrings-1.0.4-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/72/b9/313e8f2f2e9517ae050a692ae7b3e4b3f17cc5e6dfea0db51fe14e586580/jinja2_ansible_filters-1.3.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/76/8e/56ccb09c7232a55403a7637caa21922f3b65901a37f5e8bdb405d0de0946/mike-2.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/78/3c/2a612b95ddbb9a6bdcb47b7a93c4884f74c6ff22356b2f7b213b16e65c35/pycifstar-0.3.0.tar.gz + - pypi: https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.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/7b/67/b1944235474aac3f0b0e1b232ce49547f9f9461ca4b943df1b88da5d3f1d/bumps-1.0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7b/e3/d81b065a2d866a33a541ac63a2a4cc5737e03ce2379ac3191c98bb8867e3/trove_classifiers-2026.5.7.17-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7e/85/545a951eecc270fcd688288c600017e2050a1aacb56c711d208586d3e470/pymdown_extensions-10.21.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/85/d7/9b6ac05350ab7f7d3a730ff143ff3e2cada54514117c37be37e26dc91242/docstripy-0.7.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/fa/22/4cc31a62a6c7b74a8730e31a4274c5dc80e005751e277a2ce38e675e4923/numpy-2.4.4-cp314-cp314-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/8a/06/2c1bd1ee9eee3e65b0b6395dcf960e71b11576995eae3b4ab9ec63d89bea/essreduce-26.4.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8a/a1/8d812e53a5da1687abb10445275d41a8b13adb781bbf7196ddbcf8d88505/lazy_loader-0.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8d/c0/fdf9d3ee103ce66a55f0532835ad5e154226c5222423c6636ba049dc42fc/traittypes-0.2.3-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8f/5e/f1e1dd319e35e962a4e00b33150a8868b6329cc1d19fd533436ba5488f09/uncertainties-3.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/db/60/aba6a38de456e7341285102bede27514795c1eaa353bc0e7638b6b785356/pandas-3.0.2-cp314-cp314-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/cd/8b/601e6566b957ca50e28725cb6c355c59c2c8609751efbecd980db44e0349/pillow-12.2.0-cp314-cp314-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/70/7a/be4bd8bcbb24ea475856dd68159d78b03b2bb53dae369f69c9606b8888f5/pip-26.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e4/bc/daa30c02069eeac5b9198985ba42f5d65ca71bed6705b18329e51d352b7c/plopp-26.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/90/ad/cba91b3bcf04073e4d1655a5c1710ef3f457f56f7d1b79dcc3d72f4dd912/plotly-6.7.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/91/4c/e0ce1ef95d4000ebc1c11801f9b944fa5910ecc15b5e351865763d8657f8/graphviz-0.21-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/93/f7/d00d9b4a0313a6be3a3e0818e6375e15da6d7076f4ae47d1324e7ca986a1/radon-6.0.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/96/5d/0c59079aa7ef34980a5925a06a90ad2b7c94e486c194b3527d557cabb042/cryspy-0.11.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/99/31/6cf181011dc738c33bf6ba7aea2e8e1d3c1f71b7dab1942f3054f66f6202/asteval-1.0.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/99/37/e8730c3587a65eb5645d4aba2d27aae48e8003614d6aaf15dda67f702f1f/bidict-0.23.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9b/91/cc8cc78a111826c54743d88651e1687008133c37e5ee615fee9b57990fac/aiohttp-3.13.5-cp314-cp314-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9e/43/53afb8ba17218f19b77c7834128566c5bbb100a0ad9ba2e8e89d089d7079/autopep8-2.3.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-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/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a3/58/35da89ee790598a0700ea49b2a66594140f44dec458c07e8e3d4979137fc/ply-3.11-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/2a/2d/d4bf65e47cea8ff2c794a600c4fd1273a7902f268757c531e0ee9f18aa58/pooch-1.9.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ee/8c/83087ebc47ab0396ce092363001fa37c17153119ee282700c0713a195853/prettytable-3.17.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/0c/2a/a758b47de253636e1b8aef181c0b4f4f204bf0dd964914fb2af90a95b49b/propcache-0.4.1-cp314-cp314-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/f6/f0/10642828a8dfb741e5f3fbaac830550a518a775c7fff6f04a007259b0548/py-1.11.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/25/7d/cea3531f77df694ac7f169378250d85f19f69b09a5f4fa45f650837ae7cc/py3dmol-2.5.4-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e0/a9/023730ba63db1e494a271cb018dcd361bd2c917ba7004c3e49d5daf795a2/py_cpuinfo-9.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/58/e0/f1871f520c359e4e3a2eb7437c9e7e792bb6c356414e8617937561167caf/pycifrw-5.0.1.tar.gz - - 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/f3/0a/fd7d723f8f8153418fb40cf9c940e82004fce7e987026b08a68a36dd3fe7/pydantic-2.13.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/20/eb/59980e5f1ae54a3b86372bd9f0fa373ea2d402e8cdcd3459334430f91e91/pydantic_core-2.46.3-cp314-cp314-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/f7/27/a2fc51a4a122dfd1015e921ae9d22fee3d20b0b8080d9a704578bf9deece/pymdown_extensions-10.21.2-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/a4/39/3a0ae5b0edb66e61bb0e8bc53a503495cba5892297ae21faf6ba0525e681/varname-1.0.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/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.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/aa/54/0cce26da03a981f949bb8449c9778537f75f5917c172e1d2992ff25cb57d/python_engineio-4.13.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ab/b5/36c712098e6191d1b4e349304ef73a8d06aed77e56ceaac8c0a306c7bda1/jupyterlab_widgets-3.0.16-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/af/46/661159ad844034ba8b3f4e0516215c41e4ee17db4213d13a82227670764f/sciline-25.11.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/52/bc858b1665d0dec3a2511f4e6f5c18ea85c0977563d624d597c95d6d0fd7/jupyterquiz-2.9.6.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b4/5e/bf11645aebb9af7d8d35927c40d3855816a0855c799e8156eeca8d632c90/diffpy_structure-3.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/6f/a05a317a66fee0aad270011461f1a63a453ed12471249f172f7d2e2bc7b4/python_discovery-1.3.1-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/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ba/8c/1a9e46228571de18f8e28f16fabdfc20212a5d019f3e3303452b3f0a580d/pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.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/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/33/29/e756e715a48959f1c0045342088d7ca9762a2f509b945f362a316e9412b7/pytest_benchmark-5.2.3-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/bf/34/1fe99124be59579ebd24316522e1da780979c856977b142c0dcd878b0a2d/spglib-2.6.0.tar.gz + - pypi: https://files.pythonhosted.org/packages/bf/50/98b146aea0f1cd7531d25f12bea69fa9ce8d1662124f93fb30dc4511b65e/docstring_parser_fork-0.0.14-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c1/d4/59e74daffcb57a07668852eeeb6035af9f32cbfd7a1d2511f17d2fe6a738/smmap-5.0.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c4/6d/82e65254354ba651dc966775270a9bbc02414a3eb3f1704e6c87dab2ea83/essdiffraction-26.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c6/3d/020a6b6248c3d4a37797db068256f0b3f15b01bc481327ba888c50309aa8/mkdocs_plugin_inline_svg-0.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c7/a0/5ff05d1919ca249508012cad89f08fdc6cfbdaa15b41651c5fe6dffaf1d3/dfo_ls-1.6.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c7/ea/7988934c8e3e3418aa043f70421817df28d06aef50bfd85f5ad3ec6e70f1/ncrystal_core-4.4.2-py3-none-macosx_11_0_arm64.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/d8/db/795879cc3ddfe338599bddea6388cc5100b088db0a4caf6e6c1af1c27e04/python_discovery-1.2.2-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/07/c7/deb8c5e604404dbf10a3808a858946ca3547692ff6316b698945bb72177e/python_socketio-5.16.1-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/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-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/d3/19/43f5f2e568dddde567fc41f8471f9432c09563e19d3e617a48cfa52f8f0a/ruff-0.15.13-py3-none-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d5/08/c2409cb01d5368dcfedcbaffa7d044cc8957d57a9d0855244a5eb4709d30/funcy-2.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d5/0c/043d5e551459da400957a1395e0febbf771446ff34291afcbe3d8be2a279/fsspec-2026.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.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/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.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/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/33/f1/9614e03e1cdcbf9437570b5400ced8a720b5db22b28d8e0f1bda429f660d/ruff-0.15.12-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/1f/28/3f8aa247d29d010547d52207395cb057ebd0a40b88f64bc1dbac9e17a729/scipp-26.3.1-cp314-cp314-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/82/9b/cf7e6f157c53d2c8bc0165580350c53939aea3b2a8390ec8e07f0adb82de/scippneutron-26.4.1-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/4b/39/f0e8ea762a764a9dc52aa7dabcfad51a354819de1f0d4652b6a1122424d6/scipy-1.17.1-cp314-cp314-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/db/7b/8624a203326675d7746a254083a187398090a179335b2e4a20e2ddc46e83/scipy-1.17.1-cp314-cp314-macosx_14_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz + - pypi: https://files.pythonhosted.org/packages/db/bc/83e112abc66cd466c6b83f99118035867cecd41802f8d044638aa78a106e/locket-1.0.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/dc/83/6d810a8a9ebc9c307989b418840c20e46907c74d707beb67ab566773e6fc/xarray-2026.4.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/dc/b7/901d837999a9350a7773289f7760cb473d4ba01fdc6ebae0ff2553d269ac/ncrystal_python-4.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-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/c1/d4/59e74daffcb57a07668852eeeb6035af9f32cbfd7a1d2511f17d2fe6a738/smmap-5.0.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/bf/34/1fe99124be59579ebd24316522e1da780979c856977b142c0dcd878b0a2d/spglib-2.6.0.tar.gz - - pypi: https://files.pythonhosted.org/packages/cf/4f/8297e4ed88e80baa1f5aa3c484a0ee29ef3c69c7582f206c916973b75057/sqlalchemy-2.0.49-cp314-cp314-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e4/bc/daa30c02069eeac5b9198985ba42f5d65ca71bed6705b18329e51d352b7c/plopp-26.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/eb/be/b257e12f9710819fde40adc972578bee6b72c5992da1bc8369bef2597756/nbmake-1.5.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ee/8c/83087ebc47ab0396ce092363001fa37c17153119ee282700c0713a195853/prettytable-3.17.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f1/4f/733c48f270565d78b4544f2baddc2fb2a245e5a8640254b12c36ac7ac68e/multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/f1/5b/e63c877c4c94382b66de5045e08ec8cd960e8a4d22f0d62a4dfb1f9e5ac6/ipydatawidgets-4.3.5-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.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/f3/9b/4165a1d56ddc302a0e2d518fd9d412a4fd0b57562618c78c5f21c57194f5/coverage-7.14.0-cp314-cp314-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/f4/34/a9dbe051de88a63eb7408ea66630bac38e72f7f6077d4be58737106860d9/virtualenv-21.3.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f4/a4/61adb19f3c74b0dc0e411de4f06ebef564b1f179928f9dffcbd4b378f2ef/jupyter_notebook_parser-0.1.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f5/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/f6/f0/10642828a8dfb741e5f3fbaac830550a518a775c7fff6f04a007259b0548/py-1.11.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f7/00/bbca25f8a2372465cdf93138c1e1e38dff045fb0afef1488f395d0afcb3b/ncrystal-4.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fa/bc/8b8ec5a4bfc5b9cf3ce27a118339e994f88410be5677c96493e0ea28e76d/dunamai-1.26.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/8d/c0/fdf9d3ee103ce66a55f0532835ad5e154226c5222423c6636ba049dc42fc/traittypes-0.2.3-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d6/bb/fbdc4e57731efb86b4209ffdd2519520782bf27b3c961beac3e5c20d2b87/trove_classifiers-2026.4.28.13-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/91/88/b55b3117287a8540b76dbdd87733808d4d01c8067a3b339408c250bb3600/typeguard-4.5.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-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/b7/ee/e9c95cda829131f71a8dff5ce0406059fd16e591c074414e31ada19ba7c3/validate_pyproject-0.25-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a4/39/3a0ae5b0edb66e61bb0e8bc53a503495cba5892297ae21faf6ba0525e681/varname-1.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/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/4b/eb/03bfb1299d4c4510329e470f13f9a4ce793df7fcb5a2fd3510f911066f61/virtualenv-21.3.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/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/a9/5b/9b92f54c784c26e2a422e55a8d2607ab15b7ea3349e28359282f84f01d43/yarl-1.23.0-cp314-cp314-win_amd64.whl - - pypi: ./ -packages: -- conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda - build_number: 20 - sha256: 1dd3fffd892081df9726d7eb7e0dea6198962ba775bd88842135a4ddb4deb3c9 - md5: a9f577daf3de00bca7c3c76c0ecbd1de - depends: - - __glibc >=2.17,<3.0.a0 - - libgomp >=7.5.0 - constrains: - - openmp_impl <0.0a0 - license: BSD-3-Clause - license_family: BSD - purls: [] - size: 28948 - timestamp: 1770939786096 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/_openmp_mutex-4.5-7_kmp_llvm.conda - build_number: 7 - sha256: 7acaa2e0782cad032bdaf756b536874346ac1375745fb250e9bdd6a48a7ab3cd - md5: a44032f282e7d2acdeb1c240308052dd - depends: - - llvm-openmp >=9.0.1 - license: BSD-3-Clause - license_family: BSD - purls: [] - size: 8325 - timestamp: 1764092507920 -- conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda - sha256: a3967b937b9abf0f2a99f3173fa4630293979bd1644709d89580e7c62a544661 - md5: aaa2a381ccc56eac91d63b6c1240312f - depends: - - cpython - - python-gil - license: MIT - license_family: MIT - purls: [] - size: 8191 - timestamp: 1744137672556 -- pypi: https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl - name: aiohappyeyeballs - version: 2.6.1 - sha256: f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8 - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/22/4d/eaedff67fc805aeba4ba746aec891b4b24cebb1a7d078084b6300f79d063/aiohttp-3.13.5-cp314-cp314-win_amd64.whl - name: aiohttp - version: 3.13.5 - sha256: f85c6f327bf0b8c29da7d93b1cabb6363fb5e4e160a32fa241ed2dce21b73162 - requires_dist: - - aiohappyeyeballs>=2.5.0 - - aiosignal>=1.4.0 - - async-timeout>=4.0,<6.0 ; python_full_version < '3.11' - - attrs>=17.3.0 - - frozenlist>=1.1.1 - - multidict>=4.5,<7.0 - - propcache>=0.2.0 - - yarl>=1.17.0,<2.0 - - aiodns>=3.3.0 ; extra == 'speedups' - - brotli>=1.2 ; platform_python_implementation == 'CPython' and extra == 'speedups' - - brotlicffi>=1.2 ; platform_python_implementation != 'CPython' and extra == 'speedups' - - backports-zstd ; python_full_version < '3.14' and platform_python_implementation == 'CPython' and extra == 'speedups' - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/29/47/7be41556bfbb6917069d6a6634bb7dd5e163ba445b783a90d40f5ac7e3a7/aiohttp-3.13.5-cp312-cp312-macosx_11_0_arm64.whl - name: aiohttp - version: 3.13.5 - sha256: ab2899f9fa2f9f741896ebb6fa07c4c883bfa5c7f2ddd8cf2aafa86fa981b2d2 - requires_dist: - - aiohappyeyeballs>=2.5.0 - - aiosignal>=1.4.0 - - async-timeout>=4.0,<6.0 ; python_full_version < '3.11' - - attrs>=17.3.0 - - frozenlist>=1.1.1 - - multidict>=4.5,<7.0 - - propcache>=0.2.0 - - yarl>=1.17.0,<2.0 - - aiodns>=3.3.0 ; extra == 'speedups' - - brotli>=1.2 ; platform_python_implementation == 'CPython' and extra == 'speedups' - - brotlicffi>=1.2 ; platform_python_implementation != 'CPython' and extra == 'speedups' - - backports-zstd ; python_full_version < '3.14' and platform_python_implementation == 'CPython' and extra == 'speedups' - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/57/d8/8d44036d7eb7b6a8ec4c5494ea0c8c8b94fbc0ed3991c1a7adf230df03bf/aiohttp-3.13.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - name: aiohttp - version: 3.13.5 - sha256: b18f31b80d5a33661e08c89e202edabf1986e9b49c42b4504371daeaa11b47c1 - requires_dist: - - aiohappyeyeballs>=2.5.0 - - aiosignal>=1.4.0 - - async-timeout>=4.0,<6.0 ; python_full_version < '3.11' - - attrs>=17.3.0 - - frozenlist>=1.1.1 - - multidict>=4.5,<7.0 - - propcache>=0.2.0 - - yarl>=1.17.0,<2.0 - - aiodns>=3.3.0 ; extra == 'speedups' - - brotli>=1.2 ; platform_python_implementation == 'CPython' and extra == 'speedups' - - brotlicffi>=1.2 ; platform_python_implementation != 'CPython' and extra == 'speedups' - - backports-zstd ; python_full_version < '3.14' and platform_python_implementation == 'CPython' and extra == 'speedups' - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/9b/91/cc8cc78a111826c54743d88651e1687008133c37e5ee615fee9b57990fac/aiohttp-3.13.5-cp314-cp314-macosx_11_0_arm64.whl - name: aiohttp - version: 3.13.5 - sha256: 756c3c304d394977519824449600adaf2be0ccee76d206ee339c5e76b70ded25 - requires_dist: - - aiohappyeyeballs>=2.5.0 - - aiosignal>=1.4.0 - - async-timeout>=4.0,<6.0 ; python_full_version < '3.11' - - attrs>=17.3.0 - - frozenlist>=1.1.1 - - multidict>=4.5,<7.0 - - propcache>=0.2.0 - - yarl>=1.17.0,<2.0 - - aiodns>=3.3.0 ; extra == 'speedups' - - brotli>=1.2 ; platform_python_implementation == 'CPython' and extra == 'speedups' - - brotlicffi>=1.2 ; platform_python_implementation != 'CPython' and extra == 'speedups' - - backports-zstd ; python_full_version < '3.14' and platform_python_implementation == 'CPython' and extra == 'speedups' - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/aa/ca/eadf6f9c8fa5e31d40993e3db153fb5ed0b11008ad5d9de98a95045bed84/aiohttp-3.13.5-cp312-cp312-win_amd64.whl - name: aiohttp - version: 3.13.5 - sha256: 110e448e02c729bcebb18c60b9214a87ba33bac4a9fa5e9a5f139938b56c6cb1 - requires_dist: - - aiohappyeyeballs>=2.5.0 - - aiosignal>=1.4.0 - - async-timeout>=4.0,<6.0 ; python_full_version < '3.11' - - attrs>=17.3.0 - - frozenlist>=1.1.1 - - multidict>=4.5,<7.0 - - propcache>=0.2.0 - - yarl>=1.17.0,<2.0 - - aiodns>=3.3.0 ; extra == 'speedups' - - brotli>=1.2 ; platform_python_implementation == 'CPython' and extra == 'speedups' - - brotlicffi>=1.2 ; platform_python_implementation != 'CPython' and extra == 'speedups' - - backports-zstd ; python_full_version < '3.14' and platform_python_implementation == 'CPython' and extra == 'speedups' - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/bd/c9/989f4034fb46841208de7aeeac2c6d8300745ab4f28c42f629ba77c2d916/aiohttp-3.13.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - name: aiohttp - version: 3.13.5 - sha256: 241a94f7de7c0c3b616627aaad530fe2cb620084a8b144d3be7b6ecfe95bae3b - requires_dist: - - aiohappyeyeballs>=2.5.0 - - aiosignal>=1.4.0 - - async-timeout>=4.0,<6.0 ; python_full_version < '3.11' - - attrs>=17.3.0 - - frozenlist>=1.1.1 - - multidict>=4.5,<7.0 - - propcache>=0.2.0 - - yarl>=1.17.0,<2.0 - - aiodns>=3.3.0 ; extra == 'speedups' - - brotli>=1.2 ; platform_python_implementation == 'CPython' and extra == 'speedups' - - brotlicffi>=1.2 ; platform_python_implementation != 'CPython' and extra == 'speedups' - - backports-zstd ; python_full_version < '3.14' and platform_python_implementation == 'CPython' and extra == 'speedups' - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl - name: aiosignal - version: 1.4.0 - sha256: 053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e - requires_dist: - - frozenlist>=1.1.0 - - typing-extensions>=4.2 ; python_full_version < '3.13' - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/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' -- conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.13.0-pyhcf101f3_0.conda - sha256: f09aed24661cd45ba54a43772504f05c0698248734f9ae8cd289d314ac89707e - md5: af2df4b9108808da3dc76710fe50eae2 - depends: - - exceptiongroup >=1.0.2 - - idna >=2.8 - - python >=3.10 - - typing_extensions >=4.5 - - python - constrains: - - trio >=0.32.0 - - uvloop >=0.22.1 - - winloop >=0.2.3 - license: MIT - license_family: MIT - purls: - - pkg:pypi/anyio?source=compressed-mapping - size: 146764 - timestamp: 1774359453364 -- conda: https://conda.anaconda.org/conda-forge/noarch/appnope-0.1.4-pyhd8ed1ab_1.conda - sha256: 8f032b140ea4159806e4969a68b4a3c0a7cab1ad936eb958a2b5ffe5335e19bf - md5: 54898d0f524c9dee622d44bbb081a8ab - depends: - - python >=3.9 - license: BSD-2-Clause - license_family: BSD - purls: - - pkg:pypi/appnope?source=hash-mapping - size: 10076 - timestamp: 1733332433806 -- conda: https://conda.anaconda.org/conda-forge/noarch/argon2-cffi-25.1.0-pyhd8ed1ab_0.conda - sha256: bea62005badcb98b1ae1796ec5d70ea0fc9539e7d59708ac4e7d41e2f4bb0bad - md5: 8ac12aff0860280ee0cff7fa2cf63f3b - depends: - - argon2-cffi-bindings - - python >=3.9 - - typing-extensions - constrains: - - argon2_cffi ==999 - license: MIT - license_family: MIT - purls: - - pkg:pypi/argon2-cffi?source=hash-mapping - size: 18715 - timestamp: 1749017288144 -- conda: https://conda.anaconda.org/conda-forge/linux-64/argon2-cffi-bindings-25.1.0-py312h4c3975b_2.conda - sha256: 7988c207b2b766dad5ebabf25a92b8d75cb8faed92f256fd7a4e0875c9ec6d58 - md5: 1567f06d717246abab170736af8bad1b - depends: - - __glibc >=2.17,<3.0.a0 - - cffi >=1.0.1 - - libgcc >=14 - - python >=3.12,<3.13.0a0 - - python_abi 3.12.* *_cp312 - license: MIT - license_family: MIT - purls: - - pkg:pypi/argon2-cffi-bindings?source=hash-mapping - size: 35646 - timestamp: 1762509443854 -- conda: https://conda.anaconda.org/conda-forge/linux-64/argon2-cffi-bindings-25.1.0-py314h5bd0f2a_2.conda - sha256: 39234a99df3d2e3065383808ed8bfda36760de5ef590c54c3692bb53571ef02b - md5: 3cca1b74b2752917b5b65b81f61f0553 - depends: - - __glibc >=2.17,<3.0.a0 - - cffi >=2.0.0b1 - - libgcc >=14 - - python >=3.14,<3.15.0a0 - - python_abi 3.14.* *_cp314 - license: MIT - license_family: MIT - purls: - - pkg:pypi/argon2-cffi-bindings?source=hash-mapping - size: 35598 - timestamp: 1762509505285 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/argon2-cffi-bindings-25.1.0-py312h4409184_2.conda - sha256: 24c475f6f7abf03ef3cc2ac572b7a6d713bede00ef984591be92cdc439b09fbc - md5: 0a2a07b42db3f92b8dccf0f60b5ebee8 - depends: - - __osx >=11.0 - - cffi >=1.0.1 - - python >=3.12,<3.13.0a0 - - python >=3.12,<3.13.0a0 *_cpython - - python_abi 3.12.* *_cp312 - license: MIT - license_family: MIT - purls: - - pkg:pypi/argon2-cffi-bindings?source=hash-mapping - size: 34224 - timestamp: 1762509989973 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/argon2-cffi-bindings-25.1.0-py314h0612a62_2.conda - sha256: aab60bbaea5cc49dff37438d1ad469d64025cda2ce58103cf68da61701ed2075 - md5: a240a79a49a95b388ef81ccda27a5e51 - depends: - - __osx >=11.0 - - cffi >=2.0.0b1 - - python >=3.14,<3.15.0a0 - - python >=3.14,<3.15.0a0 *_cp314 - - python_abi 3.14.* *_cp314 - license: MIT - license_family: MIT - purls: - - pkg:pypi/argon2-cffi-bindings?source=hash-mapping - size: 34218 - timestamp: 1762509977830 -- conda: https://conda.anaconda.org/conda-forge/win-64/argon2-cffi-bindings-25.1.0-py312he06e257_2.conda - sha256: 38c5e43d991b0c43713fa2ceba3063afa4ccad2dd4c8eb720143de54d461a338 - md5: 5dc3781bbc4ddce0bf250a04c1a192c2 - depends: - - cffi >=1.0.1 - - python >=3.12,<3.13.0a0 - - python_abi 3.12.* *_cp312 - - ucrt >=10.0.20348.0 - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 - license: MIT - license_family: MIT - purls: - - pkg:pypi/argon2-cffi-bindings?source=hash-mapping - size: 38535 - timestamp: 1762509763237 -- conda: https://conda.anaconda.org/conda-forge/win-64/argon2-cffi-bindings-25.1.0-py314h5a2d7ad_2.conda - sha256: a742e7cd0d5534bfff3fd550a0c1e430411fad60a24f88930d261056ab08096f - md5: ffa247e46f47e157851dc547f4c513e4 - depends: - - cffi >=2.0.0b1 - - python >=3.14,<3.15.0a0 - - python_abi 3.14.* *_cp314 - - ucrt >=10.0.20348.0 - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 - license: MIT - license_family: MIT - purls: - - pkg:pypi/argon2-cffi-bindings?source=hash-mapping - size: 38653 - timestamp: 1762509771011 -- conda: https://conda.anaconda.org/conda-forge/noarch/arrow-1.4.0-pyhcf101f3_0.conda - sha256: 792da8131b1b53ff667bd6fc617ea9087b570305ccb9913deb36b8e12b3b5141 - md5: 85c4f19f377424eafc4ed7911b291642 - depends: - - python >=3.10 - - python-dateutil >=2.7.0 - - python-tzdata - - python - license: Apache-2.0 - license_family: APACHE - purls: - - pkg:pypi/arrow?source=hash-mapping - size: 113854 - timestamp: 1760831179410 -- pypi: https://files.pythonhosted.org/packages/3f/d0/7b958df957e4827837b590944008f0b28078f552b451f7407b4b3d54f574/asciichartpy-1.5.25-py2.py3-none-any.whl - name: asciichartpy - version: 1.5.25 - sha256: 33c417a3c8ef7d0a11b98eb9ea6dd9b2c1b17559e539b207a17d26d4302d0258 - requires_dist: - - setuptools - - flake8 ; extra == 'qa' -- pypi: https://files.pythonhosted.org/packages/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 - sha256: 6c64385c6ff859a474953c124987c7ee8354d781c76509b2c598741c4d1d28e9 - requires_dist: - - build ; extra == 'dev' - - twine ; extra == 'dev' - - sphinx ; extra == 'doc' - - pytest ; extra == 'test' - - pytest-cov ; extra == 'test' - - coverage ; extra == 'test' - - asteval[dev,doc,test] ; extra == 'all' - requires_python: '>=3.10' -- conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.1-pyhd8ed1ab_0.conda - sha256: ee4da0f3fe9d59439798ee399ef3e482791e48784873d546e706d0935f9ff010 - md5: 9673a61a297b00016442e022d689faa6 - depends: - - python >=3.10 - constrains: - - astroid >=2,<5 - license: Apache-2.0 - license_family: Apache - purls: - - pkg:pypi/asttokens?source=hash-mapping - size: 28797 - timestamp: 1763410017955 -- conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.3.0-pyhcf101f3_0.conda - sha256: ea8486637cfb89dc26dc9559921640cd1d5fd37e5e02c33d85c94572139f2efe - md5: b85e84cb64c762569cc1a760c2327e0a - depends: - - python >=3.10 - - typing_extensions >=4.0.0 - - python - license: MIT - license_family: MIT - purls: - - pkg:pypi/async-lru?source=hash-mapping - size: 22949 - timestamp: 1773926359134 -- conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda - sha256: 1b6124230bb4e571b1b9401537ecff575b7b109cc3a21ee019f65e083b8399ab - md5: c6b0543676ecb1fb2d7643941fe375f2 - depends: - - python >=3.10 - - python - license: MIT - license_family: MIT - purls: - - pkg:pypi/attrs?source=hash-mapping - size: 64927 - timestamp: 1773935801332 -- pypi: https://files.pythonhosted.org/packages/9e/43/53afb8ba17218f19b77c7834128566c5bbb100a0ad9ba2e8e89d089d7079/autopep8-2.3.2-py2.py3-none-any.whl - name: autopep8 - version: 2.3.2 - sha256: ce8ad498672c845a0c3de2629c15b635ec2b05ef8177a6e7c91c74f3e9b51128 - requires_dist: - - pycodestyle>=2.12.0 - - tomli ; python_full_version < '3.11' - requires_python: '>=3.9' -- conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.18.0-pyhcf101f3_1.conda - sha256: a14a9ad02101aab25570543a59c5193043b73dc311a25650134ed9e6cb691770 - md5: f1976ce927373500cc19d3c0b2c85177 - depends: - - python >=3.10 - - python - constrains: - - pytz >=2015.7 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/babel?source=compressed-mapping - size: 7684321 - timestamp: 1772555330347 -- conda: https://conda.anaconda.org/conda-forge/linux-64/backports.zstd-1.3.0-py312h90b7ffd_0.conda - sha256: d77a24be15e283d83214121428290dbe55632a6e458378205b39c550afa008cf - md5: 5b8c55fed2e576dde4b0b33693a4fdb1 - depends: - - python - - libgcc >=14 - - __glibc >=2.17,<3.0.a0 - - python_abi 3.12.* *_cp312 - - zstd >=1.5.7,<1.6.0a0 - license: BSD-3-Clause AND MIT AND EPL-2.0 - purls: - - pkg:pypi/backports-zstd?source=hash-mapping - size: 237970 - timestamp: 1767045004512 -- conda: https://conda.anaconda.org/conda-forge/noarch/backports.zstd-1.3.0-py314h680f03e_0.conda - noarch: generic - sha256: c31ab719d256bc6f89926131e88ecd0f0c5d003fe8481852c6424f4ec6c7eb29 - md5: a2ac7763a9ac75055b68f325d3255265 - depends: - - python >=3.14 - license: BSD-3-Clause AND MIT AND EPL-2.0 - purls: [] - size: 7514 - timestamp: 1767044983590 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/backports.zstd-1.3.0-py312h44dc372_0.conda - sha256: aee745bfca32f7073d3298157bbb2273d6d83383cb266840cf0a7862b3cd8efc - md5: c2d5961bfd98504b930e704426d16572 - depends: - - python - - python 3.12.* *_cpython - - __osx >=11.0 - - zstd >=1.5.7,<1.6.0a0 - - python_abi 3.12.* *_cp312 - license: BSD-3-Clause AND MIT AND EPL-2.0 - purls: - - pkg:pypi/backports-zstd?source=hash-mapping - size: 241051 - timestamp: 1767045000787 -- conda: https://conda.anaconda.org/conda-forge/win-64/backports.zstd-1.3.0-py312h06d0912_0.conda - sha256: c9c97cd644faa6c4fb38017c5ecfd082f56a3126af5925d246364fa4a22b2a74 - md5: 2db2b356f08f19ce4309a79a9ee6b9d8 - depends: - - python - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 - - ucrt >=10.0.20348.0 - - python_abi 3.12.* *_cp312 - - zstd >=1.5.7,<1.6.0a0 - license: BSD-3-Clause AND MIT AND EPL-2.0 - purls: - - pkg:pypi/backports-zstd?source=hash-mapping - size: 236635 - timestamp: 1767045021157 -- pypi: https://files.pythonhosted.org/packages/36/da/87912ddec6e06feffbaa3d7aa18fc6352bee2e8f1fee185d7d1690f8f4e8/backrefs-7.0-py312-none-any.whl - name: backrefs - version: '7.0' - sha256: ca42ce6a49ace3d75684dfa9937f3373902a63284ecb385ce36d15e5dcb41c12 - requires_dist: - - regex ; extra == 'extras' - requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/3e/5c/fb93d3092640a24dfb7bd7727a24016d7c01774ca013e60efd3f683c8002/backrefs-7.0-py314-none-any.whl - name: backrefs - version: '7.0' - sha256: a6448b28180e3ca01134c9cf09dcebafad8531072e09903c5451748a05f24bc9 - requires_dist: - - regex ; extra == 'extras' - requires_python: '>=3.10' -- conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.14.3-pyha770c72_0.conda - sha256: bf1e71c3c0a5b024e44ff928225a0874fc3c3356ec1a0b6fe719108e6d1288f6 - md5: 5267bef8efea4127aacd1f4e1f149b6e - depends: - - python >=3.10 - - soupsieve >=1.2 - - typing-extensions - license: MIT - license_family: MIT - purls: - - pkg:pypi/beautifulsoup4?source=hash-mapping - size: 90399 - timestamp: 1764520638652 -- pypi: https://files.pythonhosted.org/packages/99/37/e8730c3587a65eb5645d4aba2d27aae48e8003614d6aaf15dda67f702f1f/bidict-0.23.1-py3-none-any.whl - name: bidict - version: 0.23.1 - sha256: 5dae8d4d79b552a71cbabc7deb25dfe8ce710b17ff41711e13010ead2abfc3e5 - requires_python: '>=3.8' -- conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.3.0-pyhcf101f3_1.conda - sha256: f8ff1f98423674278964a46c93a1766f9e91960d44efd91c6c3ed56a33813f46 - md5: 7c5ebdc286220e8021bf55e6384acd67 - depends: - - python >=3.10 - - webencodings - - python - constrains: - - tinycss2 >=1.1.0,<1.5 - license: Apache-2.0 AND MIT - purls: - - pkg:pypi/bleach?source=hash-mapping - size: 142008 - timestamp: 1770719370680 -- conda: https://conda.anaconda.org/conda-forge/noarch/bleach-with-css-6.3.0-hbca2aae_1.conda - sha256: 7c07a865e5e4cca233cc4e0eb3f0f5ff6c90776461687b4fb0b1764133e1fd61 - md5: f11a319b9700b203aa14c295858782b6 - depends: - - bleach ==6.3.0 pyhcf101f3_1 - - tinycss2 - license: Apache-2.0 AND MIT - purls: [] - size: 4409 - timestamp: 1770719370682 -- pypi: https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl - name: blinker - version: 1.9.0 - sha256: ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc - requires_python: '>=3.9' -- conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.2.0-py312hdb49522_1.conda - sha256: 49df13a1bb5e388ca0e4e87022260f9501ed4192656d23dc9d9a1b4bf3787918 - md5: 64088dffd7413a2dd557ce837b4cbbdb - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=14 - - libstdcxx >=14 - - python >=3.12,<3.13.0a0 - - python_abi 3.12.* *_cp312 - constrains: - - libbrotlicommon 1.2.0 hb03c661_1 - license: MIT - license_family: MIT - purls: - - pkg:pypi/brotli?source=hash-mapping - size: 368300 - timestamp: 1764017300621 -- conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.2.0-py314h3de4e8d_1.conda - sha256: 3ad3500bff54a781c29f16ce1b288b36606e2189d0b0ef2f67036554f47f12b0 - md5: 8910d2c46f7e7b519129f486e0fe927a - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=14 - - libstdcxx >=14 - - python >=3.14,<3.15.0a0 - - python_abi 3.14.* *_cp314 - constrains: - - libbrotlicommon 1.2.0 hb03c661_1 - license: MIT - license_family: MIT - purls: - - pkg:pypi/brotli?source=hash-mapping - size: 367376 - timestamp: 1764017265553 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-python-1.2.0-py312h0dfefe5_1.conda - sha256: 6178775a86579d5e8eec6a7ab316c24f1355f6c6ccbe84bb341f342f1eda2440 - md5: 311fcf3f6a8c4eb70f912798035edd35 - depends: - - __osx >=11.0 - - libcxx >=19 - - python >=3.12,<3.13.0a0 - - python >=3.12,<3.13.0a0 *_cpython - - python_abi 3.12.* *_cp312 - constrains: - - libbrotlicommon 1.2.0 hc919400_1 - license: MIT - license_family: MIT - purls: - - pkg:pypi/brotli?source=hash-mapping - size: 359503 - timestamp: 1764018572368 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-python-1.2.0-py314h3daef5d_1.conda - sha256: 5c2e471fd262fcc3c5a9d5ea4dae5917b885e0e9b02763dbd0f0d9635ed4cb99 - md5: f9501812fe7c66b6548c7fcaa1c1f252 - depends: - - __osx >=11.0 - - libcxx >=19 - - python >=3.14,<3.15.0a0 - - python >=3.14,<3.15.0a0 *_cp314 - - python_abi 3.14.* *_cp314 - constrains: - - libbrotlicommon 1.2.0 hc919400_1 - license: MIT - license_family: MIT - purls: - - pkg:pypi/brotli?source=hash-mapping - size: 359854 - timestamp: 1764018178608 -- conda: https://conda.anaconda.org/conda-forge/win-64/brotli-python-1.2.0-py312hc6d9e41_1.conda - sha256: 2bb6f384a51929ef2d5d6039fcf6c294874f20aaab2f63ca768cbe462ed4b379 - md5: e8e7a6346a9e50d19b4daf41f367366f - depends: - - python >=3.12,<3.13.0a0 - - python_abi 3.12.* *_cp312 - - ucrt >=10.0.20348.0 - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 - constrains: - - libbrotlicommon 1.2.0 hfd05255_1 - license: MIT - license_family: MIT - purls: - - pkg:pypi/brotli?source=hash-mapping - size: 335482 - timestamp: 1764018063640 -- conda: https://conda.anaconda.org/conda-forge/win-64/brotli-python-1.2.0-py314he701e3d_1.conda - sha256: 6854ee7675135c57c73a04849c29cbebc2fb6a3a3bfee1f308e64bf23074719b - md5: 1302b74b93c44791403cbeee6a0f62a3 - depends: - - python >=3.14,<3.15.0a0 - - python_abi 3.14.* *_cp314 - - ucrt >=10.0.20348.0 - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 - constrains: - - libbrotlicommon 1.2.0 hfd05255_1 - license: MIT - license_family: MIT - purls: - - pkg:pypi/brotli?source=hash-mapping - size: 335782 - timestamp: 1764018443683 -- pypi: https://files.pythonhosted.org/packages/0d/fe/6bea5c9162869c5beba5d9c8abbed835ec85bf1ec1fba05a3822325c45f3/build-1.5.0-py3-none-any.whl - name: build - version: 1.5.0 - sha256: 13f3eecb844759ab66efec90ca17639bbf14dc06cb2fdf37a9010322d9c50a6f - requires_dist: - - packaging>=24.0 - - pyproject-hooks - - colorama ; os_name == 'nt' - - importlib-metadata>=4.6 ; python_full_version < '3.10.2' - - tomli>=1.1.0 ; python_full_version < '3.11' - - keyring ; extra == 'keyring' - - uv>=0.1.18 ; extra == 'uv' - - virtualenv>=20.17 ; python_full_version >= '3.10' and python_full_version < '3.14' and extra == 'virtualenv' - - virtualenv>=20.31 ; python_full_version >= '3.14' and extra == 'virtualenv' - requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/7b/67/b1944235474aac3f0b0e1b232ce49547f9f9461ca4b943df1b88da5d3f1d/bumps-1.0.4-py3-none-any.whl - name: bumps - version: 1.0.4 - sha256: 78b8cfaf9fbcbf2fd77f6d4a2f8c906b0e03a794804ba6caf64d56d6f6cce4d4 - requires_dist: - - numpy - - scipy - - h5py - - dill - - cloudpickle - - matplotlib - - blinker - - aiohttp - - python-socketio - - plotly - - mpld3 - - msgpack - - uncertainties - - build ; extra == 'dev' - - pre-commit ; extra == 'dev' - - pytest ; extra == 'dev' - - pytest-cov ; extra == 'dev' - - ruff ; extra == 'dev' - - wheel ; extra == 'dev' - - setuptools ; extra == 'dev' - - sphinx ; extra == 'dev' - - versioningit ; extra == 'dev' - requires_python: '>=3.10' -- conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_9.conda - sha256: 0b75d45f0bba3e95dc693336fa51f40ea28c980131fec438afb7ce6118ed05f6 - md5: d2ffd7602c02f2b316fd921d39876885 - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=14 - license: bzip2-1.0.6 - license_family: BSD - purls: [] - size: 260182 - timestamp: 1771350215188 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-hd037594_9.conda - sha256: 540fe54be35fac0c17feefbdc3e29725cce05d7367ffedfaaa1bdda234b019df - md5: 620b85a3f45526a8bc4d23fd78fc22f0 - depends: - - __osx >=11.0 - license: bzip2-1.0.6 - license_family: BSD - purls: [] - size: 124834 - timestamp: 1771350416561 -- conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h0ad9c76_9.conda - sha256: 76dfb71df5e8d1c4eded2dbb5ba15bb8fb2e2b0fe42d94145d5eed4c75c35902 - md5: 4cb8e6b48f67de0b018719cdf1136306 - depends: - - ucrt >=10.0.20348.0 - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 - license: bzip2-1.0.6 - license_family: BSD - purls: [] - size: 56115 - timestamp: 1771350256444 -- conda: https://conda.anaconda.org/conda-forge/linux-64/c-ares-1.34.6-hb03c661_0.conda - sha256: cc9accf72fa028d31c2a038460787751127317dcfa991f8d1f1babf216bb454e - md5: 920bb03579f15389b9e512095ad995b7 - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=14 - license: MIT - license_family: MIT - purls: [] - size: 207882 - timestamp: 1765214722852 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/c-ares-1.34.6-hc919400_0.conda - sha256: 2995f2aed4e53725e5efbc28199b46bf311c3cab2648fc4f10c2227d6d5fa196 - md5: bcb3cba70cf1eec964a03b4ba7775f01 - depends: - - __osx >=11.0 - license: MIT - license_family: MIT - purls: [] - size: 180327 - timestamp: 1765215064054 -- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.4.22-h4c7d964_0.conda - sha256: 6f4ff81534c19e76acf52fcabf4a258088a932b8f1ac56e9a59e98f6051f8e46 - md5: 56fb2c6c73efc627b40c77d14caecfba - depends: - - __win - license: ISC - purls: [] - size: 131388 - timestamp: 1776865633471 -- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.4.22-hbd8a1cb_0.conda - sha256: c9dbcc8039a52023660d6d1bbf87594a93dd69c6ac5a2a44323af2c92976728d - md5: e18ad67cf881dcadee8b8d9e2f8e5f73 - depends: - - __unix - license: ISC - purls: [] - size: 131039 - timestamp: 1776865545798 -- conda: https://conda.anaconda.org/conda-forge/noarch/cached-property-1.5.2-hd8ed1ab_1.tar.bz2 - noarch: python - sha256: 561e6660f26c35d137ee150187d89767c988413c978e1b712d53f27ddf70ea17 - md5: 9b347a7ec10940d3f7941ff6c460b551 - depends: - - cached_property >=1.5.2,<1.5.3.0a0 - license: BSD-3-Clause - license_family: BSD - purls: [] - size: 4134 - timestamp: 1615209571450 -- conda: https://conda.anaconda.org/conda-forge/noarch/cached_property-1.5.2-pyha770c72_1.tar.bz2 - sha256: 6dbf7a5070cc43d90a1e4c2ec0c541c69d8e30a0e25f50ce9f6e4a432e42c5d7 - md5: 576d629e47797577ab0f1b351297ef4a - depends: - - python >=3.6 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/cached-property?source=hash-mapping - size: 11065 - timestamp: 1615209567874 -- conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2026.4.22-pyhd8ed1ab_0.conda - sha256: 989db6e5957c4b44fa600c68c681ec2f36a55e48f7c7f1c073d5e91caa8cd878 - md5: 929471569c93acefb30282a22060dcd5 - depends: - - python >=3.10 - license: ISC - purls: - - pkg:pypi/certifi?source=compressed-mapping - size: 135656 - timestamp: 1776866680878 -- conda: https://conda.anaconda.org/conda-forge/linux-64/cffi-2.0.0-py312h460c074_1.conda - sha256: 7dafe8173d5f94e46cf9cd597cc8ff476a8357fbbd4433a8b5697b2864845d9c - md5: 648ee28dcd4e07a1940a17da62eccd40 - depends: - - __glibc >=2.17,<3.0.a0 - - libffi >=3.5.2,<3.6.0a0 - - libgcc >=14 - - pycparser - - python >=3.12,<3.13.0a0 - - python_abi 3.12.* *_cp312 - license: MIT - license_family: MIT - purls: - - pkg:pypi/cffi?source=hash-mapping - size: 295716 - timestamp: 1761202958833 -- conda: https://conda.anaconda.org/conda-forge/linux-64/cffi-2.0.0-py314h4a8dc5f_1.conda - sha256: c6339858a0aaf5d939e00d345c98b99e4558f285942b27232ac098ad17ac7f8e - md5: cf45f4278afd6f4e6d03eda0f435d527 - depends: - - __glibc >=2.17,<3.0.a0 - - libffi >=3.5.2,<3.6.0a0 - - libgcc >=14 - - pycparser - - python >=3.14,<3.15.0a0 - - python_abi 3.14.* *_cp314 - license: MIT - license_family: MIT - purls: - - pkg:pypi/cffi?source=hash-mapping - size: 300271 - timestamp: 1761203085220 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/cffi-2.0.0-py312h1b4d9a2_1.conda - sha256: 597e986ac1a1bd1c9b29d6850e1cdea4a075ce8292af55568952ec670e7dd358 - md5: 503ac138ad3cfc09459738c0f5750705 - depends: - - __osx >=11.0 - - libffi >=3.5.2,<3.6.0a0 - - pycparser - - python >=3.12,<3.13.0a0 - - python >=3.12,<3.13.0a0 *_cpython - - python_abi 3.12.* *_cp312 - license: MIT - license_family: MIT - purls: - - pkg:pypi/cffi?source=hash-mapping - size: 288080 - timestamp: 1761203317419 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/cffi-2.0.0-py314h44086f9_1.conda - sha256: 5b5ee5de01eb4e4fd2576add5ec9edfc654fbaf9293e7b7ad2f893a67780aa98 - md5: 10dd19e4c797b8f8bdb1ec1fbb6821d7 - depends: - - __osx >=11.0 - - libffi >=3.5.2,<3.6.0a0 - - pycparser - - python >=3.14,<3.15.0a0 - - python >=3.14,<3.15.0a0 *_cp314 - - python_abi 3.14.* *_cp314 - license: MIT - license_family: MIT - purls: - - pkg:pypi/cffi?source=hash-mapping - size: 292983 - timestamp: 1761203354051 -- conda: https://conda.anaconda.org/conda-forge/win-64/cffi-2.0.0-py312he06e257_1.conda - sha256: 3e3bdcb85a2e79fe47d9c8ce64903c76f663b39cb63b8e761f6f884e76127f82 - md5: 46f7dccfee37a52a97c0ed6f33fcf0a3 - depends: - - pycparser - - python >=3.12,<3.13.0a0 - - python_abi 3.12.* *_cp312 - - ucrt >=10.0.20348.0 - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 - license: MIT - license_family: MIT - purls: - - pkg:pypi/cffi?source=hash-mapping - size: 291324 - timestamp: 1761203195397 -- conda: https://conda.anaconda.org/conda-forge/win-64/cffi-2.0.0-py314h5a2d7ad_1.conda - sha256: 924f2f01fa7a62401145ef35ab6fc95f323b7418b2644a87fea0ea68048880ed - md5: c360170be1c9183654a240aadbedad94 - depends: - - pycparser - - python >=3.14,<3.15.0a0 - - python_abi 3.14.* *_cp314 - - ucrt >=10.0.20348.0 - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 - license: MIT - license_family: MIT - purls: - - pkg:pypi/cffi?source=hash-mapping - size: 294731 - timestamp: 1761203441365 -- pypi: https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl - name: cfgv - version: 3.5.0 - sha256: a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0 - requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/32/60/fca69c534602a7ced04280c952a246ad1edde2a6ca3a164f65d32ac41fe7/chardet-7.4.3-cp312-cp312-win_amd64.whl - name: chardet - version: 7.4.3 - sha256: 4b2799bd58e7245cfa8d4ab2e8ad1d76a5c3a5b1f32318eb6acca4c69a3e7101 - requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/51/ac/b9d68ebddfe1b02c77af5bf81120e12b036b4432dc6af7a303d90e2bc38b/chardet-7.4.3-cp314-cp314-macosx_11_0_arm64.whl - name: chardet - version: 7.4.3 - sha256: acc46d1b8b7d5783216afe15db56d1c179b9a40e5a1558bc13164c4fd20674c4 - requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/9c/2f/4c5af01fd1a7506a1d5375403d68925eac70289229492db5aa68b58103d8/chardet-7.4.3-cp312-cp312-macosx_11_0_arm64.whl - name: chardet - version: 7.4.3 - sha256: 29af5999f654e8729d251f1724a62b538b1262d9292cccaefddf8a02aae1ef6a - requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/b1/2c/cad8b5e3623a987f3c930b68e2bdd06cfc388cd91cd42ed05f1227701b73/chardet-7.4.3-cp314-cp314-win_amd64.whl - name: chardet - version: 7.4.3 - sha256: b95c934b9ad59e2ba8abb9be49df70d3ad1b0d95d864b9fdb7588d4fa8bd921c - requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/c2/20/193faab46a68ea550587331a698c3dca8099f8901d10937c4443135c7ed9/chardet-7.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - name: chardet - version: 7.4.3 - sha256: 6e3bd9f936e04bae89c254262af08d9e5b98f805175ba1e29d454e6cba3107b7 - requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/e5/59/a32a241d861cf180853a11c8e5a67641cb1b2af13c3a5ccce83ec07e2c9f/chardet-7.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - name: chardet - version: 7.4.3 - sha256: 9a4904dd5f071b7a7d7f50b4a67a86db3c902d243bf31708f1d5cde2f68239cb - requires_python: '>=3.10' -- conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.7-pyhd8ed1ab_0.conda - sha256: 3f9483d62ce24ecd063f8a5a714448445dc8d9e201147c46699fc0033e824457 - md5: a9167b9571f3baa9d448faa2139d1089 - depends: - - python >=3.10 - license: MIT - license_family: MIT - purls: - - pkg:pypi/charset-normalizer?source=compressed-mapping - size: 58872 - timestamp: 1775127203018 -- pypi: https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl - name: click - version: 8.3.3 - sha256: a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613 - requires_dist: - - colorama ; sys_platform == 'win32' - requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl - name: cloudpickle - version: 3.1.2 - sha256: 9acb47f6afd73f60dc1df93bb801b472f05ff42fa6c84167d25cb206be1fbf4a - requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl - name: colorama - version: 0.4.6 - sha256: 4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 - 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/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda - sha256: ab29d57dc70786c1269633ba3dff20288b81664d3ff8d21af995742e2bb03287 - md5: 962b9857ee8e7018c22f2776ffa0b2d7 - depends: - - python >=3.9 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/colorama?source=hash-mapping - size: 27011 - timestamp: 1733218222191 -- conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.3-pyhe01879c_0.conda - sha256: 576a44729314ad9e4e5ebe055fbf48beb8116b60e58f9070278985b2b634f212 - md5: 2da13f2b299d8e1995bafbbe9689a2f7 - depends: - - python >=3.9 - - python - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/comm?source=hash-mapping - size: 14690 - timestamp: 1753453984907 -- pypi: https://files.pythonhosted.org/packages/04/5f/9ff93450ba96b09c7c2b3f81c94de31c89f92292f1380261bd7195bea4ea/contourpy-1.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - name: contourpy - version: 1.3.3 - sha256: f64836de09927cba6f79dcd00fdd7d5329f3fccc633468507079c829ca4db4e3 - requires_dist: - - numpy>=1.25 - - furo ; extra == 'docs' - - sphinx>=7.2 ; extra == 'docs' - - sphinx-copybutton ; extra == 'docs' - - bokeh ; extra == 'bokeh' - - selenium ; extra == 'bokeh' - - contourpy[bokeh,docs] ; extra == 'mypy' - - bokeh ; extra == 'mypy' - - docutils-stubs ; extra == 'mypy' - - mypy==1.17.0 ; extra == 'mypy' - - types-pillow ; extra == 'mypy' - - contourpy[test-no-images] ; extra == 'test' - - matplotlib ; extra == 'test' - - pillow ; extra == 'test' - - pytest ; extra == 'test-no-images' - - pytest-cov ; extra == 'test-no-images' - - pytest-rerunfailures ; extra == 'test-no-images' - - pytest-xdist ; extra == 'test-no-images' - - wurlitzer ; extra == 'test-no-images' - requires_python: '>=3.11' -- pypi: https://files.pythonhosted.org/packages/19/e8/6026ed58a64563186a9ee3f29f41261fd1828f527dd93d33b60feca63352/contourpy-1.3.3-cp312-cp312-win_amd64.whl - name: contourpy - version: 1.3.3 - sha256: 8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b - requires_dist: - - numpy>=1.25 - - furo ; extra == 'docs' - - sphinx>=7.2 ; extra == 'docs' - - sphinx-copybutton ; extra == 'docs' - - bokeh ; extra == 'bokeh' - - selenium ; extra == 'bokeh' - - contourpy[bokeh,docs] ; extra == 'mypy' - - bokeh ; extra == 'mypy' - - docutils-stubs ; extra == 'mypy' - - mypy==1.17.0 ; extra == 'mypy' - - types-pillow ; extra == 'mypy' - - contourpy[test-no-images] ; extra == 'test' - - matplotlib ; extra == 'test' - - pillow ; extra == 'test' - - pytest ; extra == 'test-no-images' - - pytest-cov ; extra == 'test-no-images' - - pytest-rerunfailures ; extra == 'test-no-images' - - pytest-xdist ; extra == 'test-no-images' - - wurlitzer ; extra == 'test-no-images' - requires_python: '>=3.11' -- pypi: https://files.pythonhosted.org/packages/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl - name: contourpy - version: 1.3.3 - sha256: 556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6 - requires_dist: - - numpy>=1.25 - - furo ; extra == 'docs' - - sphinx>=7.2 ; extra == 'docs' - - sphinx-copybutton ; extra == 'docs' - - bokeh ; extra == 'bokeh' - - selenium ; extra == 'bokeh' - - contourpy[bokeh,docs] ; extra == 'mypy' - - bokeh ; extra == 'mypy' - - docutils-stubs ; extra == 'mypy' - - mypy==1.17.0 ; extra == 'mypy' - - types-pillow ; extra == 'mypy' - - contourpy[test-no-images] ; extra == 'test' - - matplotlib ; extra == 'test' - - pillow ; extra == 'test' - - pytest ; extra == 'test-no-images' - - pytest-cov ; extra == 'test-no-images' - - pytest-rerunfailures ; extra == 'test-no-images' - - pytest-xdist ; extra == 'test-no-images' - - wurlitzer ; extra == 'test-no-images' - requires_python: '>=3.11' -- pypi: https://files.pythonhosted.org/packages/7d/c2/57f54b03d0f22d4044b8afb9ca0e184f8b1afd57b4f735c2fa70883dc601/contourpy-1.3.3-cp314-cp314-win_amd64.whl - name: contourpy - version: 1.3.3 - sha256: cf9022ef053f2694e31d630feaacb21ea24224be1c3ad0520b13d844274614fd - requires_dist: - - numpy>=1.25 - - furo ; extra == 'docs' - - sphinx>=7.2 ; extra == 'docs' - - sphinx-copybutton ; extra == 'docs' - - bokeh ; extra == 'bokeh' - - selenium ; extra == 'bokeh' - - contourpy[bokeh,docs] ; extra == 'mypy' - - bokeh ; extra == 'mypy' - - docutils-stubs ; extra == 'mypy' - - mypy==1.17.0 ; extra == 'mypy' - - types-pillow ; extra == 'mypy' - - contourpy[test-no-images] ; extra == 'test' - - matplotlib ; extra == 'test' - - pillow ; extra == 'test' - - pytest ; extra == 'test-no-images' - - pytest-cov ; extra == 'test-no-images' - - pytest-rerunfailures ; extra == 'test-no-images' - - pytest-xdist ; extra == 'test-no-images' - - wurlitzer ; extra == 'test-no-images' - requires_python: '>=3.11' -- pypi: https://files.pythonhosted.org/packages/cc/8f/ec6289987824b29529d0dfda0d74a07cec60e54b9c92f3c9da4c0ac732de/contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - name: contourpy - version: 1.3.3 - sha256: 4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1 - requires_dist: - - numpy>=1.25 - - furo ; extra == 'docs' - - sphinx>=7.2 ; extra == 'docs' - - sphinx-copybutton ; extra == 'docs' - - bokeh ; extra == 'bokeh' - - selenium ; extra == 'bokeh' - - contourpy[bokeh,docs] ; extra == 'mypy' - - bokeh ; extra == 'mypy' - - docutils-stubs ; extra == 'mypy' - - mypy==1.17.0 ; extra == 'mypy' - - types-pillow ; extra == 'mypy' - - contourpy[test-no-images] ; extra == 'test' - - matplotlib ; extra == 'test' - - pillow ; extra == 'test' - - pytest ; extra == 'test-no-images' - - pytest-cov ; extra == 'test-no-images' - - pytest-rerunfailures ; extra == 'test-no-images' - - pytest-xdist ; extra == 'test-no-images' - - wurlitzer ; extra == 'test-no-images' - requires_python: '>=3.11' -- pypi: https://files.pythonhosted.org/packages/fd/e1/3542a9cb596cadd76fcef413f19c79216e002623158befe6daa03dbfa88c/contourpy-1.3.3-cp314-cp314-macosx_11_0_arm64.whl - name: contourpy - version: 1.3.3 - sha256: cbedb772ed74ff5be440fa8eee9bd49f64f6e3fc09436d9c7d8f1c287b121d77 - requires_dist: - - numpy>=1.25 - - furo ; extra == 'docs' - - sphinx>=7.2 ; extra == 'docs' - - sphinx-copybutton ; extra == 'docs' - - bokeh ; extra == 'bokeh' - - selenium ; extra == 'bokeh' - - contourpy[bokeh,docs] ; extra == 'mypy' - - bokeh ; extra == 'mypy' - - docutils-stubs ; extra == 'mypy' - - mypy==1.17.0 ; extra == 'mypy' - - types-pillow ; extra == 'mypy' - - contourpy[test-no-images] ; extra == 'test' - - matplotlib ; extra == 'test' - - pillow ; extra == 'test' - - pytest ; extra == 'test-no-images' - - pytest-cov ; extra == 'test-no-images' - - pytest-rerunfailures ; extra == 'test-no-images' - - pytest-xdist ; extra == 'test-no-images' - - wurlitzer ; extra == 'test-no-images' - requires_python: '>=3.11' -- pypi: https://files.pythonhosted.org/packages/98/ba/fc20faf5b2bb04615fa906f8daecfe896e6f7a56b34debd93c4a4d63e6e5/copier-9.15.0-py3-none-any.whl - name: copier - version: 9.15.0 - sha256: 0f59c2ea36df42f3ded85c091c3f1e2c8d3814b537504f0abc8c2e508f7e013d - 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/22/e5/06b1f88f42a5a99df42ce61208bdec3bddb3d261412874280a19796fc09c/coverage-7.13.5-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl - name: coverage - version: 7.13.5 - sha256: 6c36ddb64ed9d7e496028d1d00dfec3e428e0aabf4006583bb1839958d280510 - requires_dist: - - tomli ; python_full_version <= '3.11' and extra == 'toml' - requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/29/3d/821a9a5799fac2556bcf0bd37a70d1d11fa9e49784b6d22e92e8b2f85f18/coverage-7.13.5-cp312-cp312-win_amd64.whl - name: coverage - version: 7.13.5 - sha256: d2c87e0c473a10bffe991502eac389220533024c8082ec1ce849f4218dded810 - requires_dist: - - tomli ; python_full_version <= '3.11' and extra == 'toml' - requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/8c/49/cd14b789536ac6a4778c453c6a2338bc0a2fb60c5a5a41b4008328b9acc1/coverage-7.13.5-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl - name: coverage - version: 7.13.5 - sha256: 03ccc709a17a1de074fb1d11f217342fb0d2b1582ed544f554fc9fc3f07e95f5 - requires_dist: - - tomli ; python_full_version <= '3.11' and extra == 'toml' - requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/8d/b7/4758d4f73fb536347cc5e4ad63662f9d60ba9118cb6785e9616b2ce5d7fa/coverage-7.13.5-cp314-cp314-win_amd64.whl - name: coverage - version: 7.13.5 - sha256: 2aa055ae1857258f9e0045be26a6d62bdb47a72448b62d7b55f4820f361a2633 - requires_dist: - - tomli ; python_full_version <= '3.11' and extra == 'toml' - requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/a6/16/a68a19e5384e93f811dccc51034b1fd0b865841c390e3c931dcc4699e035/coverage-7.13.5-cp312-cp312-macosx_11_0_arm64.whl - name: coverage - version: 7.13.5 - sha256: 0e223ce4b4ed47f065bfb123687686512e37629be25cc63728557ae7db261422 - requires_dist: - - tomli ; python_full_version <= '3.11' and extra == 'toml' - requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/e2/3e/51dff36d99ae14639a133d9b164d63e628532e2974d8b1edb99dd1ebc733/coverage-7.13.5-cp314-cp314-macosx_11_0_arm64.whl - name: coverage - version: 7.13.5 - sha256: 9bb2a28101a443669a423b665939381084412b81c3f8c0fcfbac57f4e30b5b8e - requires_dist: - - tomli ; python_full_version <= '3.11' and extra == 'toml' - requires_python: '>=3.10' -- conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.12.13-py312hd8ed1ab_0.conda - noarch: generic - sha256: d3e9bbd7340199527f28bbacf947702368f31de60c433a16446767d3c6aaf6fe - md5: f54c1ffb8ecedb85a8b7fcde3a187212 - depends: - - python >=3.12,<3.13.0a0 - - python_abi * *_cp312 - license: Python-2.0 - purls: [] - size: 46463 - timestamp: 1772728929620 -- conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.14.4-py314hd8ed1ab_100.conda - noarch: generic - sha256: 40dc224f2b718e5f034efd2332bc315a719063235f63673468d26a24770094ee - md5: f111d4cfaf1fe9496f386bc98ae94452 - depends: - - python >=3.14,<3.15.0a0 - - python_abi * *_cp314 - license: Python-2.0 - purls: [] - size: 49809 - timestamp: 1775614256655 -- pypi: https://files.pythonhosted.org/packages/06/fe/2a936f465588dc7ef28793c2917b60a1c0bd26b0b716f4e43b228763c74b/crysfml-0.6.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - name: crysfml - version: 0.6.1 - sha256: aaa0c38132f99976fa95c7cb728ee98113137a0023819a60893461943dcefd93 - requires_dist: - - numpy - requires_python: '>=3.11,<3.15' -- pypi: https://files.pythonhosted.org/packages/2c/58/d025d34259682d555db962eab098f5add29187443c31081cbaf5c7ec4bea/crysfml-0.6.1-cp312-cp312-win_amd64.whl - name: crysfml - version: 0.6.1 - sha256: 13c51e0021b70dd939cb6d38ac4e82dc11e173e7e43125d1cd4c55050371cf2f - requires_dist: - - numpy - requires_python: '>=3.11,<3.15' -- pypi: https://files.pythonhosted.org/packages/36/28/46c4b0dfc8eb52c4fe8c902601b5e0acfc6b943f97cf8c53447ef9e1c66c/crysfml-0.6.1-cp312-cp312-macosx_14_0_arm64.whl - name: crysfml - version: 0.6.1 - sha256: 6321b1d45e29968976e2e504fba51cc9c01165a9911cde86a6b5b0473653f29a - requires_dist: - - numpy - requires_python: '>=3.11,<3.15' -- pypi: https://files.pythonhosted.org/packages/b2/e6/65abe97bbd42eb6ef73d3a58566ce89b097ae049511b7d9708288714a798/crysfml-0.6.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - name: crysfml - version: 0.6.1 - sha256: 419f99e6fb4756185e00a2ca9f95377ae2ac1559fc0b965060bbec8a66a7eb4a - requires_dist: - - numpy - requires_python: '>=3.11,<3.15' -- pypi: https://files.pythonhosted.org/packages/d0/42/bf7fb4d923a15b99b678ecb3bdcc02d336ee34baa876c6f41c5c55038b9c/crysfml-0.6.1-cp314-cp314-macosx_14_0_arm64.whl - name: crysfml - version: 0.6.1 - sha256: 341ca456a1b4ee5a607df283e6a630db7e25585b560c166edc4fc35dbb4c27e3 - requires_dist: - - numpy - requires_python: '>=3.11,<3.15' -- pypi: https://files.pythonhosted.org/packages/d9/1a/8a81b3a66f36969c8456f8af3a12f7d601fdd9cfed2ad5b4e72a2fb7ea8d/crysfml-0.6.1-cp314-cp314-win_amd64.whl - name: crysfml - version: 0.6.1 - sha256: b4aa83292665847f0d9aafa44b69c7abb779d0e1e13b5eb1ed516bc4c811ca6f - requires_dist: - - numpy - requires_python: '>=3.11,<3.15' -- pypi: https://files.pythonhosted.org/packages/96/5d/0c59079aa7ef34980a5925a06a90ad2b7c94e486c194b3527d557cabb042/cryspy-0.11.0-py3-none-any.whl - name: cryspy - version: 0.11.0 - sha256: 0b650655a0fbdc3cfcb28826c2ab9fbc5f491e32e1ea9a47d9b75976cd43f26f - requires_dist: - - numpy - - scipy - - pycifstar - - matplotlib -- pypi: https://files.pythonhosted.org/packages/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 - sha256: 85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30 - requires_dist: - - ipython ; extra == 'docs' - - matplotlib ; extra == 'docs' - - numpydoc ; extra == 'docs' - - sphinx ; extra == 'docs' - - pytest ; extra == 'tests' - - pytest-cov ; extra == 'tests' - - pytest-xdist ; extra == 'tests' - requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/f2/f2/728f041460f1b9739b85ee23b45fa5a505962ea11fd85bdbe2a02b021373/darkdetect-0.8.0-py3-none-any.whl - name: darkdetect - version: 0.8.0 - sha256: a7509ccf517eaad92b31c214f593dbcf138ea8a43b2935406bbd565e15527a85 - requires_dist: - - pyobjc-framework-cocoa ; sys_platform == 'darwin' and extra == 'macos-listener' - requires_python: '>=3.6' -- pypi: https://files.pythonhosted.org/packages/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' -- conda: https://conda.anaconda.org/conda-forge/linux-64/debugpy-1.8.20-py312h8285ef7_0.conda - sha256: f20121b67149ff80bf951ccae7442756586d8789204cd08ade59397b22bfd098 - md5: ee1b48795ceb07311dd3e665dd4f5f33 - depends: - - python - - libgcc >=14 - - libstdcxx >=14 - - __glibc >=2.17,<3.0.a0 - - python_abi 3.12.* *_cp312 - license: MIT - license_family: MIT - purls: - - pkg:pypi/debugpy?source=hash-mapping - size: 2858582 - timestamp: 1769744978783 -- conda: https://conda.anaconda.org/conda-forge/linux-64/debugpy-1.8.20-py314h42812f9_0.conda - sha256: d9e89e351d7189c41615cfceca76b3bcacaa9c81d9945ac1caa6fb9e5184f610 - md5: 57e6fad901c05754d5256fe3ab9f277b - depends: - - python - - libgcc >=14 - - libstdcxx >=14 - - __glibc >=2.17,<3.0.a0 - - python_abi 3.14.* *_cp314 - license: MIT - license_family: MIT - purls: - - pkg:pypi/debugpy?source=hash-mapping - size: 2886804 - timestamp: 1769744977998 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/debugpy-1.8.20-py312h6510ced_0.conda - sha256: f0ca130b5ffd6949673d3c61d7b8562ab76ad8debafb83f8b3443d30c172f5eb - md5: da3b5efcb0caabcede61a6ce4e0a7669 - depends: - - python - - __osx >=11.0 - - python 3.12.* *_cpython - - libcxx >=19 - - python_abi 3.12.* *_cp312 - license: MIT - license_family: MIT - purls: - - pkg:pypi/debugpy?source=hash-mapping - size: 2752978 - timestamp: 1769744996462 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/debugpy-1.8.20-py314he609de1_0.conda - sha256: 7736a82ebe75c0f3ea6991298363d1f2edb34291f8616c1d3719862881c3a167 - md5: 407c74dc27356ba6bf3a0191070e3ac0 - depends: - - python - - python 3.14.* *_cp314 - - __osx >=11.0 - - libcxx >=19 - - python_abi 3.14.* *_cp314 - license: MIT - license_family: MIT - purls: - - pkg:pypi/debugpy?source=hash-mapping - size: 2778080 - timestamp: 1769745040206 -- conda: https://conda.anaconda.org/conda-forge/win-64/debugpy-1.8.20-py312ha1a9051_0.conda - sha256: 5a886b1af3c66bf58213c7f3d802ea60fe8218313d9072bc1c9e8f7840548ba0 - md5: 032746a0b0663920f0afb18cec61062b - depends: - - python - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 - - ucrt >=10.0.20348.0 - - python_abi 3.12.* *_cp312 - license: MIT - license_family: MIT - purls: - - pkg:pypi/debugpy?source=hash-mapping - size: 3996113 - timestamp: 1769745013982 -- conda: https://conda.anaconda.org/conda-forge/win-64/debugpy-1.8.20-py314hb98de8c_0.conda - sha256: ece1d8299ad081edaf1e5279f2a900bdedddb2c795ac029a06401543cd7610ad - md5: 48ae8370a4562f7049d587d017792a3a - depends: - - python - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 - - ucrt >=10.0.20348.0 - - python_abi 3.14.* *_cp314 - license: MIT - license_family: MIT - purls: - - pkg:pypi/debugpy?source=hash-mapping - size: 4026404 - timestamp: 1769745008861 -- conda: https://conda.anaconda.org/conda-forge/noarch/decorator-5.2.1-pyhd8ed1ab_0.conda - sha256: c17c6b9937c08ad63cb20a26f403a3234088e57d4455600974a0ce865cb14017 - md5: 9ce473d1d1be1cc3810856a48b3fab32 - depends: - - python >=3.9 - license: BSD-2-Clause - license_family: BSD - purls: - - pkg:pypi/decorator?source=hash-mapping - size: 14129 - timestamp: 1740385067843 -- conda: https://conda.anaconda.org/conda-forge/noarch/defusedxml-0.7.1-pyhd8ed1ab_0.tar.bz2 - sha256: 9717a059677553562a8f38ff07f3b9f61727bd614f505658b0a5ecbcf8df89be - md5: 961b3a227b437d82ad7054484cfa71b2 - depends: - - python >=3.6 - license: PSF-2.0 - license_family: PSF - purls: - - pkg:pypi/defusedxml?source=hash-mapping - size: 24062 - timestamp: 1615232388757 -- pypi: https://files.pythonhosted.org/packages/c7/a0/5ff05d1919ca249508012cad89f08fdc6cfbdaa15b41651c5fe6dffaf1d3/dfo_ls-1.6.5-py3-none-any.whl - name: dfo-ls - version: 1.6.5 - sha256: d147d42e471e240f9abf8bc38351a88f555ea6a8fcfd83119bbbf93c36f75ab2 - requires_dist: - - setuptools - - numpy - - scipy>=1.11 - - pandas - - pytest ; extra == 'dev' - - sphinx ; extra == 'dev' - - sphinx-rtd-theme ; extra == 'dev' - - trustregion>=1.1 ; extra == 'trustregion' - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/01/5c/87b5fefdd3c4b157c8a16833f2236723136806814584c4589610217252f0/diffpy_pdffit2-1.6.0-cp312-cp312-macosx_11_0_arm64.whl - name: diffpy-pdffit2 - version: 1.6.0 - sha256: 4c4418388b9ab4eaeb485a9950a455b3713d21319a98d61e9f69ca5b9a6b45e3 - requires_dist: - - diffpy-structure - requires_python: '>=3.12,<3.15' -- pypi: https://files.pythonhosted.org/packages/04/f1/58c14b37525dc075f3bdf149251f079723049a9f1c82eb48835a0e6b8db3/diffpy_pdffit2-1.6.0-cp314-cp314-macosx_11_0_arm64.whl - name: diffpy-pdffit2 - version: 1.6.0 - sha256: 0e178ff1d40e6b652dedb96b744a2eb04320f58b21012304b29d52167b62afa5 - requires_dist: - - diffpy-structure - requires_python: '>=3.12,<3.15' -- pypi: https://files.pythonhosted.org/packages/33/eb/f9f1ded8e4db9638f9530c3782eb01f5ab04945f4cb9e597a51c203fa4c5/diffpy_pdffit2-1.6.0.tar.gz - name: diffpy-pdffit2 - version: 1.6.0 - sha256: 11a65466f8790f5ac7ae45f2f3fc0d5d116d156d274bcfc079df653123d080e2 - requires_dist: - - diffpy-structure - requires_python: '>=3.12,<3.15' -- pypi: https://files.pythonhosted.org/packages/6f/0c/8297c8d978c919ad6318011631a6123082d5da940da5f8612e75a247d739/diffpy_pdffit2-1.6.0-cp312-cp312-win_amd64.whl - name: diffpy-pdffit2 - version: 1.6.0 - sha256: 6c7865218f78effeeb8374fb62a5aef2b084264da96e77c03160aa411d33c2a0 - requires_dist: - - diffpy-structure - requires_python: '>=3.12,<3.15' -- pypi: https://files.pythonhosted.org/packages/82/d0/26c81ffbe588f936d05f395da34046c66322e8067c9fd331c788c4f682f2/diffpy_pdffit2-1.6.0-cp314-cp314-win_amd64.whl - name: diffpy-pdffit2 - version: 1.6.0 - sha256: dc4b5c57c5bcdac4983ff3ec33a960b0f45b3d8d0e20f44347f533861b890c2a - requires_dist: - - diffpy-structure - requires_python: '>=3.12,<3.15' -- pypi: https://files.pythonhosted.org/packages/b4/5e/bf11645aebb9af7d8d35927c40d3855816a0855c799e8156eeca8d632c90/diffpy_structure-3.4.0-py3-none-any.whl - name: diffpy-structure - version: 3.4.0 - sha256: bd0f06a96635d80316f51ebc0a08003bdeb2cb48c9bb18cbed1455ac60645e48 - requires_dist: - - numpy - - pycifrw - - diffpy-utils - requires_python: '>=3.12,<3.15' -- pypi: https://files.pythonhosted.org/packages/61/2b/e260d50e64690d2a9e405d52ccd18a63c286c5088937dd0107cb23eb3195/diffpy_utils-3.7.2-py3-none-any.whl - name: diffpy-utils - version: 3.7.2 - sha256: 6100600736791a8e4638e3dd476704f4dabe3cab75bcb5c60c83c16a2032519a - requires_dist: - - numpy - - xraydb - - scipy - requires_python: '>=3.10,<3.15' -- pypi: https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl - name: dill - version: 0.4.1 - sha256: 1e1ce33e978ae97fcfcff5638477032b801c46c7c65cf717f95fbc2248f79a9d - requires_dist: - - objgraph>=1.7.2 ; extra == 'graph' - - gprof2dot>=2022.7.29 ; extra == 'profile' - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/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/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/85/d7/9b6ac05350ab7f7d3a730ff143ff3e2cada54514117c37be37e26dc91242/docstripy-0.7.2-py3-none-any.whl - name: docstripy - version: 0.7.2 - sha256: c4ba35de6c1b1c51f7afad4a46d8953aad55dce1a490d198f7e98c8c63efefda - requires_dist: - - nbformat - requires_python: '>=3.7' -- pypi: https://files.pythonhosted.org/packages/fa/bc/8b8ec5a4bfc5b9cf3ce27a118339e994f88410be5677c96493e0ea28e76d/dunamai-1.26.1-py3-none-any.whl - name: dunamai - version: 1.26.1 - sha256: 2727d939c5b4257cb01ea404372803b477f5176e5a347c43beaf89cd5072e853 - requires_dist: - - importlib-metadata>=1.6.0 ; python_full_version < '3.8' - - packaging>=20.9 - requires_python: '>=3.5' -- pypi: ./ - name: easydiffraction - version: 0.15.0+devdirty9 - sha256: ea566085dad9a9e2b2a0916bd7af16841d1af477107460060da3446971d2c2ad - requires_dist: - - asciichartpy - - asteval - - bumps - - colorama - - crysfml - - cryspy - - darkdetect - - dfo-ls - - diffpy-pdffit2 - - diffpy-utils - - essdiffraction - - gemmi - - lmfit - - numpy - - pandas - - plotly - - pooch - - py3dmol - - rich - - scipy - - sympy - - tabulate - - typeguard - - typer - - uncertainties - - varname - - build ; extra == 'dev' - - copier ; extra == 'dev' - - docstripy ; extra == 'dev' - - format-docstring ; extra == 'dev' - - gitpython ; extra == 'dev' - - interrogate ; extra == 'dev' - - jinja2 ; extra == 'dev' - - jupyterquiz ; extra == 'dev' - - jupytext ; extra == 'dev' - - mike ; extra == 'dev' - - mkdocs ; extra == 'dev' - - mkdocs-autorefs ; extra == 'dev' - - mkdocs-jupyter ; extra == 'dev' - - mkdocs-markdownextradata-plugin ; extra == 'dev' - - mkdocs-material ; extra == 'dev' - - mkdocs-plugin-inline-svg ; extra == 'dev' - - mkdocstrings-python ; extra == 'dev' - - nbmake ; extra == 'dev' - - nbqa ; extra == 'dev' - - nbstripout ; extra == 'dev' - - pre-commit ; extra == 'dev' - - pydoclint ; extra == 'dev' - - pytest ; extra == 'dev' - - pytest-benchmark ; extra == 'dev' - - pytest-cov ; extra == 'dev' - - pytest-xdist ; extra == 'dev' - - pyyaml ; extra == 'dev' - - radon ; extra == 'dev' - - ruff ; extra == 'dev' - - spdx-headers ; extra == 'dev' - - validate-pyproject[all] ; extra == 'dev' - - versioningit ; extra == 'dev' - requires_python: '>=3.12' -- pypi: https://files.pythonhosted.org/packages/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/c4/6d/82e65254354ba651dc966775270a9bbc02414a3eb3f1704e6c87dab2ea83/essdiffraction-26.5.1-py3-none-any.whl - name: essdiffraction - version: 26.5.1 - sha256: 8a6c779078c71be250714619214069221ab7968a69580d4e4d3f4b3e9a1a53ad - requires_dist: - - dask>=2022.1.0 - - essreduce>=26.4.0 - - graphviz - - numpy>=2 - - plopp>=26.2.0 - - pythreejs>=2.4.1 - - sciline>=25.4.1 - - scipp>=25.11.0 - - scippneutron>=26.3.0 - - scippnexus>=23.12.0 - - tof>=25.12.0 - - ncrystal[cif]>=4.1.0 - - spglib!=2.7 - - pandas>=2.1.2 ; extra == 'test' - - pooch>=1.5 ; extra == 'test' - - pytest>=7.0 ; extra == 'test' - - ipywidgets>=8.1.7 ; extra == 'test' - - autodoc-pydantic ; extra == 'docs' - - ipykernel ; extra == 'docs' - - ipympl ; extra == 'docs' - - ipython!=8.7.0 ; extra == 'docs' - - myst-parser ; extra == 'docs' - - nbsphinx ; extra == 'docs' - - pandas ; extra == 'docs' - - pooch ; extra == 'docs' - - pydata-sphinx-theme>=0.14 ; extra == 'docs' - - sphinx ; extra == 'docs' - - sphinx-autodoc-typehints ; extra == 'docs' - - sphinx-copybutton ; extra == 'docs' - - sphinx-design ; extra == 'docs' - - sphinxcontrib-bibtex ; extra == 'docs' - - pyarrow ; extra == 'docs' - requires_python: '>=3.11' -- pypi: https://files.pythonhosted.org/packages/8a/06/2c1bd1ee9eee3e65b0b6395dcf960e71b11576995eae3b4ab9ec63d89bea/essreduce-26.4.1-py3-none-any.whl - name: essreduce - version: 26.4.1 - sha256: 1758a18fffca9c7c2a6fa9547cf87bf45f9d52fc3ccbdffcf7524f71bc060424 - requires_dist: - - sciline>=25.11.0 - - scipp>=26.3.1 - - scippneutron>=25.11.1 - - scippnexus>=25.6.0 - - graphviz>=0.20 ; extra == 'test' - - ipywidgets>=8.1 ; extra == 'test' - - matplotlib>=3.10.7 ; extra == 'test' - - numba>=0.63 ; extra == 'test' - - pooch>=1.9.0 ; extra == 'test' - - pytest>=7.0 ; extra == 'test' - - scipy>=1.14 ; extra == 'test' - - tof>=25.12.0 ; extra == 'test' - - autodoc-pydantic ; extra == 'docs' - - graphviz>=0.20 ; extra == 'docs' - - ipykernel ; extra == 'docs' - - ipython!=8.7.0 ; extra == 'docs' - - ipywidgets>=8.1 ; extra == 'docs' - - myst-parser ; extra == 'docs' - - nbsphinx ; extra == 'docs' - - numba>=0.63 ; extra == 'docs' - - plopp ; extra == 'docs' - - pydata-sphinx-theme>=0.14 ; extra == 'docs' - - sphinx>=7 ; extra == 'docs' - - sphinx-autodoc-typehints ; extra == 'docs' - - sphinx-copybutton ; extra == 'docs' - - sphinx-design ; extra == 'docs' - - tof>=25.12.0 ; extra == 'docs' - requires_python: '>=3.11' -- conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda - sha256: ee6cf346d017d954255bbcbdb424cddea4d14e4ed7e9813e429db1d795d01144 - md5: 8e662bd460bda79b1ea39194e3c4c9ab - depends: - - python >=3.10 - - typing_extensions >=4.6.0 - license: MIT and PSF-2.0 - purls: - - pkg:pypi/exceptiongroup?source=hash-mapping - size: 21333 - timestamp: 1763918099466 -- pypi: https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl - name: execnet - version: 2.1.2 - sha256: 67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec - requires_dist: - - hatch ; extra == 'testing' - - pre-commit ; extra == 'testing' - - pytest ; extra == 'testing' - - tox ; extra == 'testing' - requires_python: '>=3.8' -- conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.1-pyhd8ed1ab_0.conda - sha256: 210c8165a58fdbf16e626aac93cc4c14dbd551a01d1516be5ecad795d2422cad - md5: ff9efb7f7469aed3c4a8106ffa29593c - depends: - - python >=3.10 - license: MIT - license_family: MIT - purls: - - pkg:pypi/executing?source=hash-mapping - size: 30753 - timestamp: 1756729456476 -- pypi: https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl - name: filelock - version: 3.29.0 - sha256: 96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258 - requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/28/63/cd0c3b26afe60995a5295f37c246a93d454023726c3261cfbb3559969bb9/fonttools-4.62.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - name: fonttools - version: 4.62.1 - sha256: 8d337fdd49a79b0d51c4da87bc38169d21c3abbf0c1aa9367eff5c6656fb6dae - requires_dist: - - lxml>=4.0 ; extra == 'lxml' - - brotli>=1.0.1 ; platform_python_implementation == 'CPython' and extra == 'woff' - - brotlicffi>=0.8.0 ; platform_python_implementation != 'CPython' and extra == 'woff' - - zopfli>=0.1.4 ; extra == 'woff' - - unicodedata2>=17.0.0 ; python_full_version < '3.15' and extra == 'unicode' - - lz4>=1.7.4.2 ; extra == 'graphite' - - scipy ; platform_python_implementation != 'PyPy' and extra == 'interpolatable' - - munkres ; platform_python_implementation == 'PyPy' and extra == 'interpolatable' - - pycairo ; extra == 'interpolatable' - - matplotlib ; extra == 'plot' - - sympy ; extra == 'symfont' - - xattr ; sys_platform == 'darwin' and extra == 'type1' - - skia-pathops>=0.5.0 ; extra == 'pathops' - - uharfbuzz>=0.45.0 ; extra == 'repacker' - - lxml>=4.0 ; extra == 'all' - - brotli>=1.0.1 ; platform_python_implementation == 'CPython' and extra == 'all' - - brotlicffi>=0.8.0 ; platform_python_implementation != 'CPython' and extra == 'all' - - zopfli>=0.1.4 ; extra == 'all' - - unicodedata2>=17.0.0 ; python_full_version < '3.15' and extra == 'all' - - lz4>=1.7.4.2 ; extra == 'all' - - scipy ; platform_python_implementation != 'PyPy' and extra == 'all' - - munkres ; platform_python_implementation == 'PyPy' and extra == 'all' - - pycairo ; extra == 'all' - - matplotlib ; extra == 'all' - - sympy ; extra == 'all' - - xattr ; sys_platform == 'darwin' and extra == 'all' - - skia-pathops>=0.5.0 ; extra == 'all' - - uharfbuzz>=0.45.0 ; extra == 'all' - requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/28/b1/0c2ab56a16f409c6c8a68816e6af707827ad5d629634691ff60a52879792/fonttools-4.62.1-cp312-cp312-win_amd64.whl - name: fonttools - version: 4.62.1 - sha256: 9e7863e10b3de72376280b515d35b14f5eeed639d1aa7824f4cf06779ec65e42 - requires_dist: - - lxml>=4.0 ; extra == 'lxml' - - brotli>=1.0.1 ; platform_python_implementation == 'CPython' and extra == 'woff' - - brotlicffi>=0.8.0 ; platform_python_implementation != 'CPython' and extra == 'woff' - - zopfli>=0.1.4 ; extra == 'woff' - - unicodedata2>=17.0.0 ; python_full_version < '3.15' and extra == 'unicode' - - lz4>=1.7.4.2 ; extra == 'graphite' - - scipy ; platform_python_implementation != 'PyPy' and extra == 'interpolatable' - - munkres ; platform_python_implementation == 'PyPy' and extra == 'interpolatable' - - pycairo ; extra == 'interpolatable' - - matplotlib ; extra == 'plot' - - sympy ; extra == 'symfont' - - xattr ; sys_platform == 'darwin' and extra == 'type1' - - skia-pathops>=0.5.0 ; extra == 'pathops' - - uharfbuzz>=0.45.0 ; extra == 'repacker' - - lxml>=4.0 ; extra == 'all' - - brotli>=1.0.1 ; platform_python_implementation == 'CPython' and extra == 'all' - - brotlicffi>=0.8.0 ; platform_python_implementation != 'CPython' and extra == 'all' - - zopfli>=0.1.4 ; extra == 'all' - - unicodedata2>=17.0.0 ; python_full_version < '3.15' and extra == 'all' - - lz4>=1.7.4.2 ; extra == 'all' - - scipy ; platform_python_implementation != 'PyPy' and extra == 'all' - - munkres ; platform_python_implementation == 'PyPy' and extra == 'all' - - pycairo ; extra == 'all' - - matplotlib ; extra == 'all' - - sympy ; extra == 'all' - - xattr ; sys_platform == 'darwin' and extra == 'all' - - skia-pathops>=0.5.0 ; extra == 'all' - - uharfbuzz>=0.45.0 ; extra == 'all' - requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/36/f0/2888cdac391807d68d90dcb16ef858ddc1b5309bfc6966195a459dd326e2/fonttools-4.62.1-cp314-cp314-macosx_10_15_universal2.whl - name: fonttools - version: 4.62.1 - sha256: fa1d16210b6b10a826d71bed68dd9ec24a9e218d5a5e2797f37c573e7ec215ca - requires_dist: - - lxml>=4.0 ; extra == 'lxml' - - brotli>=1.0.1 ; platform_python_implementation == 'CPython' and extra == 'woff' - - brotlicffi>=0.8.0 ; platform_python_implementation != 'CPython' and extra == 'woff' - - zopfli>=0.1.4 ; extra == 'woff' - - unicodedata2>=17.0.0 ; python_full_version < '3.15' and extra == 'unicode' - - lz4>=1.7.4.2 ; extra == 'graphite' - - scipy ; platform_python_implementation != 'PyPy' and extra == 'interpolatable' - - munkres ; platform_python_implementation == 'PyPy' and extra == 'interpolatable' - - pycairo ; extra == 'interpolatable' - - matplotlib ; extra == 'plot' - - sympy ; extra == 'symfont' - - xattr ; sys_platform == 'darwin' and extra == 'type1' - - skia-pathops>=0.5.0 ; extra == 'pathops' - - uharfbuzz>=0.45.0 ; extra == 'repacker' - - lxml>=4.0 ; extra == 'all' - - brotli>=1.0.1 ; platform_python_implementation == 'CPython' and extra == 'all' - - brotlicffi>=0.8.0 ; platform_python_implementation != 'CPython' and extra == 'all' - - zopfli>=0.1.4 ; extra == 'all' - - unicodedata2>=17.0.0 ; python_full_version < '3.15' and extra == 'all' - - lz4>=1.7.4.2 ; extra == 'all' - - scipy ; platform_python_implementation != 'PyPy' and extra == 'all' - - munkres ; platform_python_implementation == 'PyPy' and extra == 'all' - - pycairo ; extra == 'all' - - matplotlib ; extra == 'all' - - sympy ; extra == 'all' - - xattr ; sys_platform == 'darwin' and extra == 'all' - - skia-pathops>=0.5.0 ; extra == 'all' - - uharfbuzz>=0.45.0 ; extra == 'all' - requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/47/d4/dbacced3953544b9a93088cc10ef2b596d348c983d5c67a404fa41ec51ba/fonttools-4.62.1-cp312-cp312-macosx_10_13_universal2.whl - name: fonttools - version: 4.62.1 - sha256: 90365821debbd7db678809c7491ca4acd1e0779b9624cdc6ddaf1f31992bf974 - requires_dist: - - lxml>=4.0 ; extra == 'lxml' - - brotli>=1.0.1 ; platform_python_implementation == 'CPython' and extra == 'woff' - - brotlicffi>=0.8.0 ; platform_python_implementation != 'CPython' and extra == 'woff' - - zopfli>=0.1.4 ; extra == 'woff' - - unicodedata2>=17.0.0 ; python_full_version < '3.15' and extra == 'unicode' - - lz4>=1.7.4.2 ; extra == 'graphite' - - scipy ; platform_python_implementation != 'PyPy' and extra == 'interpolatable' - - munkres ; platform_python_implementation == 'PyPy' and extra == 'interpolatable' - - pycairo ; extra == 'interpolatable' - - matplotlib ; extra == 'plot' - - sympy ; extra == 'symfont' - - xattr ; sys_platform == 'darwin' and extra == 'type1' - - skia-pathops>=0.5.0 ; extra == 'pathops' - - uharfbuzz>=0.45.0 ; extra == 'repacker' - - lxml>=4.0 ; extra == 'all' - - brotli>=1.0.1 ; platform_python_implementation == 'CPython' and extra == 'all' - - brotlicffi>=0.8.0 ; platform_python_implementation != 'CPython' and extra == 'all' - - zopfli>=0.1.4 ; extra == 'all' - - unicodedata2>=17.0.0 ; python_full_version < '3.15' and extra == 'all' - - lz4>=1.7.4.2 ; extra == 'all' - - scipy ; platform_python_implementation != 'PyPy' and extra == 'all' - - munkres ; platform_python_implementation == 'PyPy' and extra == 'all' - - pycairo ; extra == 'all' - - matplotlib ; extra == 'all' - - sympy ; extra == 'all' - - xattr ; sys_platform == 'darwin' and extra == 'all' - - skia-pathops>=0.5.0 ; extra == 'all' - - uharfbuzz>=0.45.0 ; extra == 'all' - requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/6b/67/74b070029043186b5dd13462c958cb7c7f811be0d2e634309d9a1ffb1505/fonttools-4.62.1-cp314-cp314-win_amd64.whl - name: fonttools - version: 4.62.1 - sha256: 1eecc128c86c552fb963fe846ca4e011b1be053728f798185a1687502f6d398e - requires_dist: - - lxml>=4.0 ; extra == 'lxml' - - brotli>=1.0.1 ; platform_python_implementation == 'CPython' and extra == 'woff' - - brotlicffi>=0.8.0 ; platform_python_implementation != 'CPython' and extra == 'woff' - - zopfli>=0.1.4 ; extra == 'woff' - - unicodedata2>=17.0.0 ; python_full_version < '3.15' and extra == 'unicode' - - lz4>=1.7.4.2 ; extra == 'graphite' - - scipy ; platform_python_implementation != 'PyPy' and extra == 'interpolatable' - - munkres ; platform_python_implementation == 'PyPy' and extra == 'interpolatable' - - pycairo ; extra == 'interpolatable' - - matplotlib ; extra == 'plot' - - sympy ; extra == 'symfont' - - xattr ; sys_platform == 'darwin' and extra == 'type1' - - skia-pathops>=0.5.0 ; extra == 'pathops' - - uharfbuzz>=0.45.0 ; extra == 'repacker' - - lxml>=4.0 ; extra == 'all' - - brotli>=1.0.1 ; platform_python_implementation == 'CPython' and extra == 'all' - - brotlicffi>=0.8.0 ; platform_python_implementation != 'CPython' and extra == 'all' - - zopfli>=0.1.4 ; extra == 'all' - - unicodedata2>=17.0.0 ; python_full_version < '3.15' and extra == 'all' - - lz4>=1.7.4.2 ; extra == 'all' - - scipy ; platform_python_implementation != 'PyPy' and extra == 'all' - - munkres ; platform_python_implementation == 'PyPy' and extra == 'all' - - pycairo ; extra == 'all' - - matplotlib ; extra == 'all' - - sympy ; extra == 'all' - - xattr ; sys_platform == 'darwin' and extra == 'all' - - skia-pathops>=0.5.0 ; extra == 'all' - - uharfbuzz>=0.45.0 ; extra == 'all' - requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/9b/8a/99c8b3c3888c5c474c08dbfd7c8899786de9604b727fcefb055b42c84bba/fonttools-4.62.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - name: fonttools - version: 4.62.1 - sha256: 149f7d84afca659d1a97e39a4778794a2f83bf344c5ee5134e09995086cc2392 - requires_dist: - - lxml>=4.0 ; extra == 'lxml' - - brotli>=1.0.1 ; platform_python_implementation == 'CPython' and extra == 'woff' - - brotlicffi>=0.8.0 ; platform_python_implementation != 'CPython' and extra == 'woff' - - zopfli>=0.1.4 ; extra == 'woff' - - unicodedata2>=17.0.0 ; python_full_version < '3.15' and extra == 'unicode' - - lz4>=1.7.4.2 ; extra == 'graphite' - - scipy ; platform_python_implementation != 'PyPy' and extra == 'interpolatable' - - munkres ; platform_python_implementation == 'PyPy' and extra == 'interpolatable' - - pycairo ; extra == 'interpolatable' - - matplotlib ; extra == 'plot' - - sympy ; extra == 'symfont' - - xattr ; sys_platform == 'darwin' and extra == 'type1' - - skia-pathops>=0.5.0 ; extra == 'pathops' - - uharfbuzz>=0.45.0 ; extra == 'repacker' - - lxml>=4.0 ; extra == 'all' - - brotli>=1.0.1 ; platform_python_implementation == 'CPython' and extra == 'all' - - brotlicffi>=0.8.0 ; platform_python_implementation != 'CPython' and extra == 'all' - - zopfli>=0.1.4 ; extra == 'all' - - unicodedata2>=17.0.0 ; python_full_version < '3.15' and extra == 'all' - - lz4>=1.7.4.2 ; extra == 'all' - - scipy ; platform_python_implementation != 'PyPy' and extra == 'all' - - munkres ; platform_python_implementation == 'PyPy' and extra == 'all' - - pycairo ; extra == 'all' - - matplotlib ; extra == 'all' - - sympy ; extra == 'all' - - xattr ; sys_platform == 'darwin' and extra == 'all' - - skia-pathops>=0.5.0 ; extra == 'all' - - uharfbuzz>=0.45.0 ; extra == 'all' - requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/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' -- conda: https://conda.anaconda.org/conda-forge/noarch/fqdn-1.5.1-pyhd8ed1ab_1.conda - sha256: 2509992ec2fd38ab27c7cdb42cf6cadc566a1cc0d1021a2673475d9fa87c6276 - md5: d3549fd50d450b6d9e7dddff25dd2110 - depends: - - cached-property >=1.3.0 - - python >=3.9,<4 - license: MPL-2.0 - license_family: MOZILLA - purls: - - pkg:pypi/fqdn?source=hash-mapping - size: 16705 - timestamp: 1733327494780 -- pypi: https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl - name: frozenlist - version: 1.8.0 - sha256: f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4 - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl - name: frozenlist - version: 1.8.0 - sha256: 3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0 - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl - name: frozenlist - version: 1.8.0 - sha256: 494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383 - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl - name: frozenlist - version: 1.8.0 - sha256: 4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl - name: frozenlist - version: 1.8.0 - sha256: cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2 - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl - name: frozenlist - version: 1.8.0 - sha256: 34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746 - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/d5/0c/043d5e551459da400957a1395e0febbf771446ff34291afcbe3d8be2a279/fsspec-2026.4.0-py3-none-any.whl - name: fsspec - version: 2026.4.0 - sha256: 11ef7bb35dab8a394fde6e608221d5cf3e8499401c249bebaeaad760a1a8dec2 - requires_dist: - - adlfs ; extra == 'abfs' - - adlfs ; extra == 'adl' - - pyarrow>=1 ; extra == 'arrow' - - dask ; extra == 'dask' - - distributed ; extra == 'dask' - - pre-commit ; extra == 'dev' - - ruff>=0.5 ; extra == 'dev' - - numpydoc ; extra == 'doc' - - sphinx ; extra == 'doc' - - sphinx-design ; extra == 'doc' - - sphinx-rtd-theme ; extra == 'doc' - - yarl ; extra == 'doc' - - dropbox ; extra == 'dropbox' - - dropboxdrivefs ; extra == 'dropbox' - - requests ; extra == 'dropbox' - - adlfs ; extra == 'full' - - aiohttp!=4.0.0a0,!=4.0.0a1 ; extra == 'full' - - dask ; extra == 'full' - - distributed ; extra == 'full' - - dropbox ; extra == 'full' - - dropboxdrivefs ; extra == 'full' - - fusepy ; extra == 'full' - - gcsfs>2024.2.0 ; extra == 'full' - - libarchive-c ; extra == 'full' - - ocifs ; extra == 'full' - - panel ; extra == 'full' - - paramiko ; extra == 'full' - - pyarrow>=1 ; extra == 'full' - - pygit2 ; extra == 'full' - - requests ; extra == 'full' - - s3fs>2024.2.0 ; extra == 'full' - - smbprotocol ; extra == 'full' - - tqdm ; extra == 'full' - - fusepy ; extra == 'fuse' - - gcsfs>2024.2.0 ; extra == 'gcs' - - pygit2 ; extra == 'git' - - requests ; extra == 'github' - - gcsfs ; extra == 'gs' - - panel ; extra == 'gui' - - pyarrow>=1 ; extra == 'hdfs' - - aiohttp!=4.0.0a0,!=4.0.0a1 ; extra == 'http' - - libarchive-c ; extra == 'libarchive' - - ocifs ; extra == 'oci' - - s3fs>2024.2.0 ; extra == 's3' - - paramiko ; extra == 'sftp' - - smbprotocol ; extra == 'smb' - - paramiko ; extra == 'ssh' - - aiohttp!=4.0.0a0,!=4.0.0a1 ; extra == 'test' - - numpy ; extra == 'test' - - pytest ; extra == 'test' - - pytest-asyncio!=0.22.0 ; extra == 'test' - - pytest-benchmark ; extra == 'test' - - pytest-cov ; extra == 'test' - - pytest-mock ; extra == 'test' - - pytest-recording ; extra == 'test' - - pytest-rerunfailures ; extra == 'test' - - requests ; extra == 'test' - - aiobotocore>=2.5.4,<3.0.0 ; extra == 'test-downstream' - - dask[dataframe,test] ; extra == 'test-downstream' - - moto[server]>4,<5 ; extra == 'test-downstream' - - pytest-timeout ; extra == 'test-downstream' - - xarray ; extra == 'test-downstream' - - adlfs ; extra == 'test-full' - - aiohttp!=4.0.0a0,!=4.0.0a1 ; extra == 'test-full' - - backports-zstd ; python_full_version < '3.14' and extra == 'test-full' - - cloudpickle ; extra == 'test-full' - - dask ; extra == 'test-full' - - distributed ; extra == 'test-full' - - dropbox ; extra == 'test-full' - - dropboxdrivefs ; extra == 'test-full' - - fastparquet ; extra == 'test-full' - - fusepy ; extra == 'test-full' - - gcsfs ; extra == 'test-full' - - jinja2 ; extra == 'test-full' - - kerchunk ; extra == 'test-full' - - libarchive-c ; extra == 'test-full' - - lz4 ; extra == 'test-full' - - notebook ; extra == 'test-full' - - numpy ; extra == 'test-full' - - ocifs ; extra == 'test-full' - - pandas<3.0.0 ; extra == 'test-full' - - panel ; extra == 'test-full' - - paramiko ; extra == 'test-full' - - pyarrow ; extra == 'test-full' - - pyarrow>=1 ; extra == 'test-full' - - pyftpdlib ; extra == 'test-full' - - pygit2 ; extra == 'test-full' - - pytest ; extra == 'test-full' - - pytest-asyncio!=0.22.0 ; extra == 'test-full' - - pytest-benchmark ; extra == 'test-full' - - pytest-cov ; extra == 'test-full' - - pytest-mock ; extra == 'test-full' - - pytest-recording ; extra == 'test-full' - - pytest-rerunfailures ; extra == 'test-full' - - python-snappy ; extra == 'test-full' - - requests ; extra == 'test-full' - - smbprotocol ; extra == 'test-full' - - tqdm ; extra == 'test-full' - - urllib3 ; extra == 'test-full' - - zarr ; extra == 'test-full' - - zstandard ; python_full_version < '3.14' and extra == 'test-full' - - tqdm ; extra == 'tqdm' - requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/d5/08/c2409cb01d5368dcfedcbaffa7d044cc8957d57a9d0855244a5eb4709d30/funcy-2.0-py2.py3-none-any.whl - name: funcy - version: '2.0' - sha256: 53df23c8bb1651b12f095df764bfb057935d49537a56de211b098f4c79614bb0 -- pypi: https://files.pythonhosted.org/packages/06/41/4e70dea1d0311016c0b0b1c53a24a266f9f8a34c6bc1af0f17cfca20aa1d/gemmi-0.7.5-cp314-cp314-macosx_11_0_arm64.whl - name: gemmi - version: 0.7.5 - sha256: 5144f107f2bca479d1b8266a79649bd631ee92c5b1319b27b0279157331ebc89 - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/b0/3e/a6497e1c2c9bc6ed2b79e0f2d31a4ce509fd2a9eed4e4f7ac63eda8113cb/gemmi-0.7.5-cp312-cp312-macosx_11_0_arm64.whl - name: gemmi - version: 0.7.5 - sha256: 5682920985109c6a08616ae9aae080f8b46a9714534dc864b535e3e6d203d5b8 - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/e5/04/c5bb20d64417d20cba0105277235c51969444fa873000fbc26ac0a3fc5a8/gemmi-0.7.5-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - name: gemmi - version: 0.7.5 - sha256: bdc67ad4a7fc420974ab3102f7f6ad1517fa0c3d9f2f7561e42e5f7017635242 - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/e8/88/5a431cd1ea7587408a66947384b39beb2ab2bcc1c87b7c4082f05036719f/gemmi-0.7.5-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - name: gemmi - version: 0.7.5 - sha256: 217bb9ac9da7c90704026dacfc0a0652a38f4df1e318225d8f35c75f1f8c7ebf - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/eb/f2/53be7a4ba5816e13c39be0f728facac4bcb39cf4903ceeec54b006511c8f/gemmi-0.7.5-cp312-cp312-win_amd64.whl - name: gemmi - version: 0.7.5 - sha256: a1fdb6f72006495b5119e3a8bb5c3185efa708b785bd4a5ce4397ef7abb3fec7 - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/ff/1c/a28b27effb13a381fe077ea3e3e78f6debd6315f2b3edff67bbb93d0ef51/gemmi-0.7.5-cp314-cp314-win_amd64.whl - name: gemmi - version: 0.7.5 - sha256: 419c36d9ea0f28dda0ff0d6db17035170d0888ca78aff82a0f9f604613aec58f - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl - name: ghp-import - version: 2.1.0 - sha256: 8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619 - requires_dist: - - python-dateutil>=2.8.1 - - twine ; extra == 'dev' - - markdown ; extra == 'dev' - - flake8 ; extra == 'dev' - - wheel ; extra == 'dev' -- pypi: https://files.pythonhosted.org/packages/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/fd/6f/b842bfa6f21d6f87c57f9abf7194225e55279d96d869775e19e9f7236fc5/gitpython-3.1.49-py3-none-any.whl - name: gitpython - version: 3.1.49 - sha256: 024b0422d7f84d15cd794844e029ffebd4c5d42a7eb9b936b458697ef550a02c - requires_dist: - - gitdb>=4.0.1,<5 - - typing-extensions>=3.10.0.2 ; python_full_version < '3.10' - - coverage[toml] ; extra == 'test' - - ddt>=1.1.1,!=1.4.3 ; extra == 'test' - - mock ; python_full_version < '3.8' and extra == 'test' - - mypy==1.18.2 ; python_full_version >= '3.9' and extra == 'test' - - pre-commit ; extra == 'test' - - pytest>=7.3.1 ; extra == 'test' - - pytest-cov ; extra == 'test' - - pytest-instafail ; extra == 'test' - - pytest-mock ; extra == 'test' - - pytest-sugar ; extra == 'test' - - typing-extensions ; python_full_version < '3.11' and extra == 'test' - - sphinx>=7.4.7,<8 ; extra == 'doc' - - sphinx-rtd-theme ; extra == 'doc' - - sphinx-autodoc-typehints ; extra == 'doc' - requires_python: '>=3.7' -- pypi: https://files.pythonhosted.org/packages/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/2b/e0/2e13df68f367e2f9960616927d60857dd7e56aaadd59a47c644216b2f920/greenlet-3.5.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl - name: greenlet - version: 3.5.0 - sha256: 9d280a7f5c331622c69f97eb167f33577ff2d1df282c41cd15907fc0a3ca198c - requires_dist: - - sphinx ; extra == 'docs' - - furo ; extra == 'docs' - - objgraph ; extra == 'test' - - psutil ; extra == 'test' - - setuptools ; extra == 'test' - requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/4c/fe/4fb3a0805bd5165da5ebf858da7cc01cce8061674106d2cf5bdab32cbfde/greenlet-3.5.0-cp312-cp312-win_amd64.whl - name: greenlet - version: 3.5.0 - sha256: d4d9f0624c775f2dfc56ba54d515a8c771044346852a918b405914f6b19d7fd8 - requires_dist: - - sphinx ; extra == 'docs' - - furo ; extra == 'docs' - - objgraph ; extra == 'test' - - psutil ; extra == 'test' - - setuptools ; extra == 'test' - requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/91/20/6b165108058767ee643c55c5c4904d591a830ee2b3c7dbd359828fbc829f/greenlet-3.5.0-cp314-cp314-win_amd64.whl - name: greenlet - version: 3.5.0 - sha256: 3bc59be3945ae9750b9e7d45067d01ae3fe90ea5f9ade99239dabdd6e28a5033 - requires_dist: - - sphinx ; extra == 'docs' - - furo ; extra == 'docs' - - objgraph ; extra == 'test' - - psutil ; extra == 'test' - - setuptools ; extra == 'test' - requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/a3/59/1bd6d7428d6ed9106efbb8c52310c60fd04f6672490f452aeaa3829aa436/greenlet-3.5.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl - name: greenlet - version: 3.5.0 - sha256: 8f52a464e4ed91780bdfbbdd2b97197f3accaa629b98c200f4dffada759f3ae7 - requires_dist: - - sphinx ; extra == 'docs' - - furo ; extra == 'docs' - - objgraph ; extra == 'test' - - psutil ; extra == 'test' - - setuptools ; extra == 'test' - requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/11/8c/c9138d881c79aa0ea9ed83cbd58d5ca75624378b38cee225dcf5c42cc91f/griffelib-2.0.2-py3-none-any.whl - name: griffelib - version: 2.0.2 - sha256: 925c857658fb1ba40c0772c37acbc2ab650bd794d9c1b9726922e36ea4117ea1 - requires_dist: - - pip>=24.0 ; extra == 'pypi' - - platformdirs>=4.2 ; extra == 'pypi' - - wheel>=0.42 ; extra == 'pypi' - requires_python: '>=3.10' + - pypi: https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fd/e1/3542a9cb596cadd76fcef413f19c79216e002623158befe6daa03dbfa88c/contourpy-1.3.3-cp314-cp314-macosx_11_0_arm64.whl + win-64: + - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.13.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/argon2-cffi-25.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/arrow-1.4.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.18.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/backports.zstd-1.5.0-py314h680f03e_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.14.3-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.3.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-with-css-6.3.0-hbca2aae_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.4.22-h4c7d964_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cached-property-1.5.2-hd8ed1ab_1.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/cached_property-1.5.2-pyha770c72_1.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2026.4.22-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.7-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.3-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.14.4-py314hd8ed1ab_100.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/decorator-5.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/defusedxml-0.7.1-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fqdn-1.5.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h11-0.16.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/httpcore-1.0.9-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/httpx-0.28.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.13-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.8.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.2.0-pyh6dadd2b_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.13.0-pyhe2676ad_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython_pygments_lexers-1.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/isoduration-20.11.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jedi-0.19.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/json5-0.14.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonpointer-3.1.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.26.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.9.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-with-format-nongpl-4.26.0-hcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter-lsp-2.3.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.8.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.9.1-pyh6dadd2b_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_events-0.12.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.18.2-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server_terminals-0.5.4-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.5.7-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_pygments-0.3.0-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_server-2.28.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/lark-1.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.2.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mistune-3.2.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbclient-0.10.4-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbconvert-core-7.17.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbformat-5.10.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/notebook-shim-0.2.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/overrides-7.7.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.2-pyhc364b38_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pandocfilters-1.5.0-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.7-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pixi-kernel-0.7.1-pyhbbac1ac_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.9.6-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prometheus_client-0.25.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.52-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyh09c184e_7.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.21.2-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.14.4-h4df99d1_100.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-json-logger-3.2.1-pyh332efcf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2026.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.14-8_cp314.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.37.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.34.2-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/returns-0.27.0-pyhc364b38_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3339-validator-0.1.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3986-validator-0.1.1-pyh9f0ad1d_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3987-syntax-1.1.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/send2trash-2.1.0-pyh6dadd2b_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-82.0.1-pyh332efcf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sniffio-1.3.1-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.8.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyh6dadd2b_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tinycss2-1.4.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.15.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.15.0-h396c80c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_utils-0.1.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/uri-template-1.3.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.7.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.7.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/webcolors-25.10.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/webencodings-0.5.1-pyhd8ed1ab_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/websocket-client-1.9.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/win_inet_pton-1.1.0-pyh7428d3b_8.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/argon2-cffi-bindings-25.1.0-py314h5a2d7ad_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/brotli-python-1.2.0-py314he701e3d_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h0ad9c76_9.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/cffi-2.0.0-py314h5a2d7ad_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/debugpy-1.8.20-py314hb98de8c_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/krb5-1.22.2-h0ea6238_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libblas-3.11.0-7_h8455456_mkl.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libcblas-3.11.0-7_h2a3cdd5_mkl.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.8.0-hac47afa_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libffi-3.5.2-h3d046cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libhwloc-2.13.0-default_h049141e_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.3-hfd05255_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libmpdec-4.0.0-hfd05255_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libsodium-1.0.21-h6a83c73_3.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.53.1-hf5d6505_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libwinpthread-12.0.0.r4.gg4f2fc60ca-h57928b3_10.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libxml2-16-2.15.3-h692994f_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libxml2-2.15.3-hbc0d294_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.5-h4fa8253_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/markupsafe-3.0.3-py314h2359020_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/mkl-2026.0.0-hac47afa_906.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/msgspec-0.21.1-py314h5a2d7ad_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/nodejs-25.8.2-h80d1838_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/onemkl-license-2026.0.0-h57928b3_906.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.6.2-hf411b9b_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/psutil-7.2.2-py314hc5dbbe4_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/python-3.14.4-h4b44e0e_100_cp314.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pywin32-311-py314h8f8f202_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pywinpty-2.0.15-py314h51f0985_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pyyaml-6.0.3-py314h2359020_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pyzmq-27.1.0-py312h343a6d4_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/rpds-py-0.30.0-py314h9f07db2_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/tbb-2023.0.0-hd3d4ead_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/win-64/tornado-6.5.5-py314h5a2d7ad_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.26100.0-h57928b3_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-h41ae7f8_36.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.44.35208-h818238b_36.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.44.35208-h818238b_36.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/winpty-0.4.3-4.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/win-64/yaml-0.2.5-h6a83c73_3.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/zeromq-4.3.5-h507cc87_10.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_6.conda + - pypi: . + - pypi: https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/07/c7/deb8c5e604404dbf10a3808a858946ca3547692ff6316b698945bb72177e/python_socketio-5.16.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0b/f9/f15c95d6b200167cb22c5eca5eecfa9d28a8ee3f74095f1cd2345c71f2f9/pydoclint-0.8.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0c/53/b50773ecf1d1e4a5858ee13011e30317ba02639ae4a1411a34967951fc9b/crysfml-0.6.2-cp314-cp314-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/0d/12/bbce9472f489cb5c4c23b0d13e5c59c37c1aab11b7ac637dfe6bbdccebe7/copier-9.15.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0d/1f/d398de1612f7a611e22d743280339c9af4903675635e41be3370091c704b/arviz_stats-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0d/fe/6bea5c9162869c5beba5d9c8abbed835ec85bf1ec1fba05a3822325c45f3/build-1.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/11/8c/c9138d881c79aa0ea9ed83cbd58d5ca75624378b38cee225dcf5c42cc91f/griffelib-2.0.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/12/aa/fb2a0649fdeef5ab7072d221e8f4df164098792c813af6c87e2581cfa860/mpltoolbox-26.2.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/13/95/cf3f7fe4910cf0365fa8ea0c731f4b8a624d97cd76ea777913ac8d0868e2/mkdocs_jupyter-0.26.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/19/d4/225027a913621a879b429a043674aa35220e6ce67785acad4f7bd0c4ff33/xarray_einstats-0.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1c/59/964ecb8008722d27d8a835baea81f56a91cea8e097b3be992bc6ccde6367/versioningit-3.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1d/77/928ea2e70641ca177a11140062cc5840d421795f2e82749d408d0cce900a/narwhals-2.21.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/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1f/28/3f8aa247d29d010547d52207395cb057ebd0a40b88f64bc1dbac9e17a729/scipp-26.3.1-cp314-cp314-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/20/16/e777eadfa0c0305878c36fae1d5e6db474fbb15dae202b9ec378809dfb4d/nbstripout-0.9.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/20/7a/1c6e3562dfd8950adbb11ffbc65d21e7c89d01a6e4f137fa981056de25c5/gitpython-3.1.50-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/22/4d/eaedff67fc805aeba4ba746aec891b4b24cebb1a7d078084b6300f79d063/aiohttp-3.13.5-cp314-cp314-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-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/28/88/4789719fbbe166d12d345b3ac66b96105f10001b16e00a9765ba29261a21/nbqa-1.9.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/28/de/a3e710469772c6a89595fc52816da05c1e164b4c866a89e3cb82fb1b67c5/mkdocs_autorefs-1.4.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2a/2d/d4bf65e47cea8ff2c794a600c4fd1273a7902f268757c531e0ee9f18aa58/pooch-1.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2c/01/bc663630c510822c95c47a66af9fa7a443c295b47d5f041e5e6ae62ef659/mkdocs_material-9.7.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2f/c8/005d1de3af80f54411703d1263a0b9d31276411ec9f273d9432c59b17299/arviz_plots-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/32/28/79f0f8de97cce916d5ae88a7bee1ad724855e83e6019c0b4d5b3fabc80f3/mkdocstrings_python-2.0.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/33/b2/986d1220f6ee931e338d272bc1f3ec02cfe5f9b5fad84e95afdad57f1ebc/format_docstring-0.2.7-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/33/f0/3fe8c6e69135a845f4106f2ff8b6805638d4e85c264e70114e8126689587/tokenize_rt-6.2.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/38/55/792619469bab9882d8bbd5865d45a72f6478762d04a9af4bf0d08c503e95/pandas-3.0.3-cp314-cp314-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/38/7e/7b91c89a4cf0f543a83be978657afb20c86af6d725253e319589dcc4ce52/lmfit-1.3.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/38/8b/7ec325b4e9e78beefc2d025b01ee8a2fde771ef7c957c3bff99b9e1fbffa/xraydb-4.5.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3a/eb/fea4d1d51c49832120f7f285d07306db3960f423a2612c6057caf3e8196f/pip-26.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3c/26/1062c7ec1b053db9e499b4d2d5bc231743201b74051c973dadeac80a8f43/questionary-2.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3e/14/615a450205e1b56d16c6783f5ccd116cde05550faad70ae077c955654a75/h5py-3.16.0-cp314-cp314-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/3e/17/1f31d8562e6f970d64911f1abc330d233bc0c0601411cf7e19c1292be6da/spdx_headers-1.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3e/5c/fb93d3092640a24dfb7bd7727a24016d7c01774ca013e60efd3f683c8002/backrefs-7.0-py314-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3f/0e/fa3b193432cfc60c93b42f3be03365f5f909d2b3ea410295cf36df739e31/widgetsnbextension-4.0.15-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3f/d0/7b958df957e4827837b590944008f0b28078f552b451f7407b4b3d54f574/asciichartpy-1.5.25-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/42/d9/27b13bc9419bf5dae02905b348f16ca827646cd76244ddd326f1a8139a6a/cyclebane-24.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4a/f3/00bb1e867fba351e2d784170955713bee200c43ea306c59f30bd7e748192/dask-2026.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4b/39/f0e8ea762a764a9dc52aa7dabcfad51a354819de1f0d4652b6a1122424d6/scipy-1.17.1-cp314-cp314-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/4c/65/b4b86e5fa07543bfbbcdc6c9f7f9f561e66a5f3539992e3009973f2b1314/jupytext-1.19.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/52/59/0782e51887ac6b07ffd1570e0364cf901ebc36345fea669969d2084baebb/simple_websocket-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/56/6d/0d9848617b9f753b87f214f1c682592f7ca42de085f564352f10f0843026/ipywidgets-8.1.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/58/e0/f1871f520c359e4e3a2eb7437c9e7e792bb6c356414e8617937561167caf/pycifrw-5.0.1.tar.gz + - pypi: https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/5b/29/74eeb4d3f3ae61ca096b018ad486b3b3c74b17bec09ab4edab721cbefec3/typeguard-4.5.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5b/69/93b34728cc386efdde0c342f8c680b9187dea7beb7adaf6b58a0713be101/mpld3-0.5.12-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5c/01/6cb4d63c6b6933be4b7945b2f64638336420f04ea71ca5b9a7539c008bc5/scippnexus-26.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/61/2b/e260d50e64690d2a9e405d52ccd18a63c286c5088937dd0107cb23eb3195/diffpy_utils-3.7.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/61/d2/45c9defbaa1ea297035d9d4cce9e8f80daafbf19319c6007f157c6256ea9/propcache-0.5.2-cp314-cp314-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/63/9f/724f66a48309dd97a2ff58f491d6ffd925f35d1278a5e55dc9a5ac6a156b/scippneutron-26.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/64/a8/c09fbe44b12fa919c5bfe0afb71e60d1231a7dc93405e54c30496c57c9d3/arviz-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/69/d1/705e6c19b437a4105bf3b9ae7945fcfc3ad2abb73d14bae0a3f2d58b305b/arviz_base-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6c/25/4f103d1bedb3593718713b3f743df7b3ff3fc68d36d6666c30265ef59c8a/ase-3.28.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6e/94/be70f8ee9c45f2f62b39a1f0e9303bc20e138a8f3b8e50ffd89498e177e1/mkdocstrings-1.0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6e/f1/abd09c2ae91228c5f3998dbd7f41353def9eac64253de3c8105efa2082f7/msgpack-1.1.2-cp314-cp314-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-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/76/8e/56ccb09c7232a55403a7637caa21922f3b65901a37f5e8bdb405d0de0946/mike-2.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/78/3c/2a612b95ddbb9a6bdcb47b7a93c4884f74c6ff22356b2f7b213b16e65c35/pycifstar-0.3.0.tar.gz + - pypi: https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.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/7b/67/b1944235474aac3f0b0e1b232ce49547f9f9461ca4b943df1b88da5d3f1d/bumps-1.0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7b/e3/d81b065a2d866a33a541ac63a2a4cc5737e03ce2379ac3191c98bb8867e3/trove_classifiers-2026.5.7.17-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7d/c2/57f54b03d0f22d4044b8afb9ca0e184f8b1afd57b4f735c2fa70883dc601/contourpy-1.3.3-cp314-cp314-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/7e/85/545a951eecc270fcd688288c600017e2050a1aacb56c711d208586d3e470/pymdown_extensions-10.21.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/82/d0/26c81ffbe588f936d05f395da34046c66322e8067c9fd331c788c4f682f2/diffpy_pdffit2-1.6.0-cp314-cp314-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/85/d7/9b6ac05350ab7f7d3a730ff143ff3e2cada54514117c37be37e26dc91242/docstripy-0.7.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8a/06/2c1bd1ee9eee3e65b0b6395dcf960e71b11576995eae3b4ab9ec63d89bea/essreduce-26.4.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8a/a1/8d812e53a5da1687abb10445275d41a8b13adb781bbf7196ddbcf8d88505/lazy_loader-0.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8d/c0/fdf9d3ee103ce66a55f0532835ad5e154226c5222423c6636ba049dc42fc/traittypes-0.2.3-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8f/5e/f1e1dd319e35e962a4e00b33150a8868b6329cc1d19fd533436ba5488f09/uncertainties-3.2.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/90/ad/cba91b3bcf04073e4d1655a5c1710ef3f457f56f7d1b79dcc3d72f4dd912/plotly-6.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/91/20/6b165108058767ee643c55c5c4904d591a830ee2b3c7dbd359828fbc829f/greenlet-3.5.0-cp314-cp314-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/91/4c/e0ce1ef95d4000ebc1c11801f9b944fa5910ecc15b5e351865763d8657f8/graphviz-0.21-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/93/f7/d00d9b4a0313a6be3a3e0818e6375e15da6d7076f4ae47d1324e7ca986a1/radon-6.0.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/96/5d/0c59079aa7ef34980a5925a06a90ad2b7c94e486c194b3527d557cabb042/cryspy-0.11.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/99/31/6cf181011dc738c33bf6ba7aea2e8e1d3c1f71b7dab1942f3054f66f6202/asteval-1.0.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/99/37/e8730c3587a65eb5645d4aba2d27aae48e8003614d6aaf15dda67f702f1f/bidict-0.23.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9e/43/53afb8ba17218f19b77c7834128566c5bbb100a0ad9ba2e8e89d089d7079/autopep8-2.3.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a3/36/4e551e8aa55c9188bca9abb5096805edbf7431072b76e2298e34fd3a3008/kiwisolver-1.5.0-cp314-cp314-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/a3/58/35da89ee790598a0700ea49b2a66594140f44dec458c07e8e3d4979137fc/ply-3.11-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a4/39/3a0ae5b0edb66e61bb0e8bc53a503495cba5892297ae21faf6ba0525e681/varname-1.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a4/ce/3b6fee91c85626eaf769d617f1be9d2e15c1cca027bbdeb2e0d751469355/verspec-0.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.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/a9/5b/9b92f54c784c26e2a422e55a8d2607ab15b7ea3349e28359282f84f01d43/yarl-1.23.0-cp314-cp314-win_amd64.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/aa/92/d063df4d63d988b20d881856c74df76c0c1786229bb870f3a52af0981d4d/numpy-2.4.5-cp314-cp314-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/ab/1a/5a4f747a8b271cbb024946d2dd3c913ab5032ba430626f8c3528ada96b4b/matplotlib-3.10.9-cp314-cp314-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ab/a6/870a3e8a50590bb92be184ad928c2922f088b00d9dc5c5ec7b924ee08c22/ruff-0.15.13-py3-none-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/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/af/46/661159ad844034ba8b3f4e0516215c41e4ee17db4213d13a82227670764f/sciline-25.11.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b1/2c/cad8b5e3623a987f3c930b68e2bdd06cfc388cd91cd42ed05f1227701b73/chardet-7.4.3-cp314-cp314-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/52/bc858b1665d0dec3a2511f4e6f5c18ea85c0977563d624d597c95d6d0fd7/jupyterquiz-2.9.6.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b4/5e/bf11645aebb9af7d8d35927c40d3855816a0855c799e8156eeca8d632c90/diffpy_structure-3.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/6f/a05a317a66fee0aad270011461f1a63a453ed12471249f172f7d2e2bc7b4/python_discovery-1.3.1-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/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bf/34/1fe99124be59579ebd24316522e1da780979c856977b142c0dcd878b0a2d/spglib-2.6.0.tar.gz + - pypi: https://files.pythonhosted.org/packages/bf/50/98b146aea0f1cd7531d25f12bea69fa9ce8d1662124f93fb30dc4511b65e/docstring_parser_fork-0.0.14-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c1/d4/59e74daffcb57a07668852eeeb6035af9f32cbfd7a1d2511f17d2fe6a738/smmap-5.0.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c3/d4/98078064ccc76b45cb0f6c002452011e93c4bd26f6850344f0951cc1fe89/fonttools-4.63.0-cp314-cp314-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/c4/6d/82e65254354ba651dc966775270a9bbc02414a3eb3f1704e6c87dab2ea83/essdiffraction-26.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c6/3d/020a6b6248c3d4a37797db068256f0b3f15b01bc481327ba888c50309aa8/mkdocs_plugin_inline_svg-0.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c7/6b/6c02f55c2ce2f137ccca0986be7dd89bea31d5bee4346b4377fa3b8586df/ncrystal_core-4.4.2-py3-none-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/c7/a0/5ff05d1919ca249508012cad89f08fdc6cfbdaa15b41651c5fe6dffaf1d3/dfo_ls-1.6.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/cd/8b/601e6566b957ca50e28725cb6c355c59c2c8609751efbecd980db44e0349/pillow-12.2.0-cp314-cp314-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/cf/4f/8297e4ed88e80baa1f5aa3c484a0ee29ef3c69c7582f206c916973b75057/sqlalchemy-2.0.49-cp314-cp314-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/d2/f0/834e479e47e499b6478e807fb57b31cc2db696c4db30557bb6f5aea4a90b/mando-0.7.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d5/08/c2409cb01d5368dcfedcbaffa7d044cc8957d57a9d0855244a5eb4709d30/funcy-2.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d5/0c/043d5e551459da400957a1395e0febbf771446ff34291afcbe3d8be2a279/fsspec-2026.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d8/8b/e2bbeb42068f0c48899e8eddd34902afc0f7429d4d2a152d2dc2670dc661/pythreejs-2.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.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/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/dc/83/6d810a8a9ebc9c307989b418840c20e46907c74d707beb67ab566773e6fc/xarray-2026.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/dc/b7/901d837999a9350a7773289f7760cb473d4ba01fdc6ebae0ff2553d269ac/ncrystal_python-4.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e0/bf/52f25716bbe93745595800f36fb17b73711f14da59ed0bb2eba141bc9f0f/multidict-6.7.1-cp314-cp314-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e4/bc/daa30c02069eeac5b9198985ba42f5d65ca71bed6705b18329e51d352b7c/plopp-26.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/eb/be/b257e12f9710819fde40adc972578bee6b72c5992da1bc8369bef2597756/nbmake-1.5.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ee/8c/83087ebc47ab0396ce092363001fa37c17153119ee282700c0713a195853/prettytable-3.17.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f1/5b/e63c877c4c94382b66de5045e08ec8cd960e8a4d22f0d62a4dfb1f9e5ac6/ipydatawidgets-4.3.5-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.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/f4/34/a9dbe051de88a63eb7408ea66630bac38e72f7f6077d4be58737106860d9/virtualenv-21.3.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f4/a4/61adb19f3c74b0dc0e411de4f06ebef564b1f179928f9dffcbd4b378f2ef/jupyter_notebook_parser-0.1.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f5/57/2a154a69d6642860300bf8eb205d13131104991f2b1065bbb9075ac5c32e/tof-26.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f6/f0/10642828a8dfb741e5f3fbaac830550a518a775c7fff6f04a007259b0548/py-1.11.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f7/00/bbca25f8a2372465cdf93138c1e1e38dff045fb0afef1488f395d0afcb3b/ncrystal-4.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f9/58/6e1b8f52fdc3184b47dc5037f5070d83a3d11042db1594b02d2a44d786c8/coverage-7.14.0-cp314-cp314-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/fa/bc/8b8ec5a4bfc5b9cf3ce27a118339e994f88410be5677c96493e0ea28e76d/dunamai-1.26.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ff/1c/a28b27effb13a381fe077ea3e3e78f6debd6315f2b3edff67bbb93d0ef51/gemmi-0.7.5-cp314-cp314-win_amd64.whl + user: + channels: + - url: https://conda.anaconda.org/nodefaults/ + - url: https://conda.anaconda.org/conda-forge/ + indexes: + - https://pypi.org/simple + packages: + linux-64: + - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/argon2-cffi-bindings-25.1.0-py314h5bd0f2a_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.2.0-py314h3de4e8d_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_9.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cffi-2.0.0-py314h4a8dc5f_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/debugpy-1.8.20-py314h42812f9_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.3-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.22.2-ha1258a1_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45.1-default_hbd61a6d_102.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20250104-pl5321h7949ede_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.8.0-hecca717_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h3435931_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-he0feb66_19.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-he0feb66_19.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.3-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsodium-1.0.21-h280c20c_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.53.1-h0c1763c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h934c35e_19.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.42-h5347b49_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.2-h25fd6f3_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.3-py314h67df5f8_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/msgspec-0.21.1-py314h5bd0f2a_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.6-hdb14827_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.2-h35e630c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/psutil-7.2.2-py314h0f05182_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.14.4-habeac84_100_cp314.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.3-py314h67df5f8_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pyzmq-27.1.0-py312hda471dd_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.3-h853b02a_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/rpds-py-0.30.0-py314h2e6c369_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h366c992_103.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tornado-6.5.5-py314h5bd0f2a_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h280c20c_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zeromq-4.3.5-h41580af_10.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.13.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/argon2-cffi-25.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/arrow-1.4.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.18.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/backports.zstd-1.5.0-py314h680f03e_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.14.3-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.3.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-with-css-6.3.0-hbca2aae_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.4.22-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cached-property-1.5.2-hd8ed1ab_1.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/cached_property-1.5.2-pyha770c72_1.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2026.4.22-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.7-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.3-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.14.4-py314hd8ed1ab_100.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/decorator-5.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/defusedxml-0.7.1-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fqdn-1.5.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h11-0.16.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/httpcore-1.0.9-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/httpx-0.28.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.13-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.8.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.2.0-pyha191276_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.13.0-pyh53cf698_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython_pygments_lexers-1.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/isoduration-20.11.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jedi-0.19.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/json5-0.14.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonpointer-3.1.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.26.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.9.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-with-format-nongpl-4.26.0-hcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter-lsp-2.3.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.8.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.9.1-pyhc90fa1f_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_events-0.12.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.18.2-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server_terminals-0.5.4-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.5.7-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_pygments-0.3.0-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_server-2.28.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/lark-1.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.2.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mistune-3.2.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbclient-0.10.4-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbconvert-core-7.17.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbformat-5.10.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/notebook-shim-0.2.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/overrides-7.7.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.2-pyhc364b38_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pandocfilters-1.5.0-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.7-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pexpect-4.9.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pixi-kernel-0.7.1-pyhbbac1ac_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.9.6-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prometheus_client-0.25.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.52-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ptyprocess-0.7.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.21.2-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.14.4-h4df99d1_100.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-json-logger-3.2.1-pyh332efcf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2026.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.14-8_cp314.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.37.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.34.2-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/returns-0.27.0-pyhc364b38_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3339-validator-0.1.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3986-validator-0.1.1-pyh9f0ad1d_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3987-syntax-1.1.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/send2trash-2.1.0-pyha191276_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-82.0.1-pyh332efcf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sniffio-1.3.1-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.8.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyhc90fa1f_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tinycss2-1.4.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.15.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.15.0-h396c80c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_utils-0.1.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/uri-template-1.3.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.7.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.7.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/webcolors-25.10.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/webencodings-0.5.1-pyhd8ed1ab_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/websocket-client-1.9.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.1-pyhcf101f3_0.conda + - pypi: https://files.pythonhosted.org/packages/04/5f/9ff93450ba96b09c7c2b3f81c94de31c89f92292f1380261bd7195bea4ea/contourpy-1.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/07/c7/deb8c5e604404dbf10a3808a858946ca3547692ff6316b698945bb72177e/python_socketio-5.16.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0b/2e/7eea398450457ecb54e18e9d10110993fa65561c4f3add5e8eccd2b9cd41/scipy-1.17.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/11/8d/d2532ad2a603ca2b93ad9f5135732124e57811d0168155852f37fbce2458/pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/12/aa/fb2a0649fdeef5ab7072d221e8f4df164098792c813af6c87e2581cfa860/mpltoolbox-26.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1d/77/928ea2e70641ca177a11140062cc5840d421795f2e82749d408d0cce900a/narwhals-2.21.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/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/25/7d/cea3531f77df694ac7f169378250d85f19f69b09a5f4fa45f650837ae7cc/py3dmol-2.5.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2a/2d/d4bf65e47cea8ff2c794a600c4fd1273a7902f268757c531e0ee9f18aa58/pooch-1.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2b/76/7f1bfd6afff4c5e38e36a3c6d68eb5f4b7311ea80baf693db78d95b603c4/propcache-0.5.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/2e/84/efc7c0bf3a1c5eef81d397f6fddac855becdbb11cb38ff957888603014a7/sqlalchemy-2.0.49-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/2f/40/dc34d1a8d5f1e51fc64640b62b191684da52ca469da9cd74e84936ffa4a6/msgpack-1.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/32/5d/f7e914f7d9325abff4057cee62c0fa70263683189f774473cbfb534cd13b/matplotlib-3.10.9-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/33/eb/f9f1ded8e4db9638f9530c3782eb01f5ab04945f4cb9e597a51c203fa4c5/diffpy_pdffit2-1.6.0.tar.gz + - pypi: https://files.pythonhosted.org/packages/36/e1/a8933a72c45a87177fbde2696e0d0755c8c9062f8c077a961c6215fa27b1/fonttools-4.63.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/38/7e/7b91c89a4cf0f543a83be978657afb20c86af6d725253e319589dcc4ce52/lmfit-1.3.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/38/8b/7ec325b4e9e78beefc2d025b01ee8a2fde771ef7c957c3bff99b9e1fbffa/xraydb-4.5.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3a/eb/fea4d1d51c49832120f7f285d07306db3960f423a2612c6057caf3e8196f/pip-26.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3f/0e/fa3b193432cfc60c93b42f3be03365f5f909d2b3ea410295cf36df739e31/widgetsnbextension-4.0.15-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3f/d0/7b958df957e4827837b590944008f0b28078f552b451f7407b4b3d54f574/asciichartpy-1.5.25-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/42/2b/fef67d616931055bf3d6764885990a3ac647d68734a2d6a9e1d13de437a2/yarl-1.23.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/42/d9/27b13bc9419bf5dae02905b348f16ca827646cd76244ddd326f1a8139a6a/cyclebane-24.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/43/80/1315439acedd8398319bac177d6de3d48ab39c62cc0c810f74f0a9a73996/numpy-2.4.5-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/43/fe/ad0ecbe2393cb690a4b3100a8fea47ecfdb49f6e06f40cf2f626635adc0c/scipp-26.3.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/46/b4/0887c88ddfaba1d7140ea335144eb904af97550786ee58bdb295ff10d255/crysfml-0.6.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/4a/f3/00bb1e867fba351e2d784170955713bee200c43ea306c59f30bd7e748192/dask-2026.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/52/59/0782e51887ac6b07ffd1570e0364cf901ebc36345fea669969d2084baebb/simple_websocket-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/56/6d/0d9848617b9f753b87f214f1c682592f7ca42de085f564352f10f0843026/ipywidgets-8.1.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/58/3b/1cdec6772bdbaf7b25dab360c59f03cadf05492dd724c6540af905389b07/pandas-3.0.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/58/e0/f1871f520c359e4e3a2eb7437c9e7e792bb6c356414e8617937561167caf/pycifrw-5.0.1.tar.gz + - pypi: https://files.pythonhosted.org/packages/5b/29/74eeb4d3f3ae61ca096b018ad486b3b3c74b17bec09ab4edab721cbefec3/typeguard-4.5.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5b/69/93b34728cc386efdde0c342f8c680b9187dea7beb7adaf6b58a0713be101/mpld3-0.5.12-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5c/01/6cb4d63c6b6933be4b7945b2f64638336420f04ea71ca5b9a7539c008bc5/scippnexus-26.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/61/2b/e260d50e64690d2a9e405d52ccd18a63c286c5088937dd0107cb23eb3195/diffpy_utils-3.7.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/63/9f/724f66a48309dd97a2ff58f491d6ffd925f35d1278a5e55dc9a5ac6a156b/scippneutron-26.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6c/25/4f103d1bedb3593718713b3f743df7b3ff3fc68d36d6666c30265ef59c8a/ase-3.28.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/78/3c/2a612b95ddbb9a6bdcb47b7a93c4884f74c6ff22356b2f7b213b16e65c35/pycifstar-0.3.0.tar.gz + - pypi: https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7b/67/b1944235474aac3f0b0e1b232ce49547f9f9461ca4b943df1b88da5d3f1d/bumps-1.0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-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/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/8a/06/2c1bd1ee9eee3e65b0b6395dcf960e71b11576995eae3b4ab9ec63d89bea/essreduce-26.4.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8a/a1/8d812e53a5da1687abb10445275d41a8b13adb781bbf7196ddbcf8d88505/lazy_loader-0.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8d/c0/fdf9d3ee103ce66a55f0532835ad5e154226c5222423c6636ba049dc42fc/traittypes-0.2.3-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8f/5e/f1e1dd319e35e962a4e00b33150a8868b6329cc1d19fd533436ba5488f09/uncertainties-3.2.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/90/ad/cba91b3bcf04073e4d1655a5c1710ef3f457f56f7d1b79dcc3d72f4dd912/plotly-6.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/91/4c/e0ce1ef95d4000ebc1c11801f9b944fa5910ecc15b5e351865763d8657f8/graphviz-0.21-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/96/5d/0c59079aa7ef34980a5925a06a90ad2b7c94e486c194b3527d557cabb042/cryspy-0.11.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/99/31/6cf181011dc738c33bf6ba7aea2e8e1d3c1f71b7dab1942f3054f66f6202/asteval-1.0.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/99/37/e8730c3587a65eb5645d4aba2d27aae48e8003614d6aaf15dda67f702f1f/bidict-0.23.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a3/58/35da89ee790598a0700ea49b2a66594140f44dec458c07e8e3d4979137fc/ply-3.11-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a3/59/1bd6d7428d6ed9106efbb8c52310c60fd04f6672490f452aeaa3829aa436/greenlet-3.5.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/a4/39/3a0ae5b0edb66e61bb0e8bc53a503495cba5892297ae21faf6ba0525e681/varname-1.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/aa/54/0cce26da03a981f949bb8449c9778537f75f5917c172e1d2992ff25cb57d/python_engineio-4.13.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ab/b5/36c712098e6191d1b4e349304ef73a8d06aed77e56ceaac8c0a306c7bda1/jupyterlab_widgets-3.0.16-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/af/46/661159ad844034ba8b3f4e0516215c41e4ee17db4213d13a82227670764f/sciline-25.11.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b4/5e/bf11645aebb9af7d8d35927c40d3855816a0855c799e8156eeca8d632c90/diffpy_structure-3.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bd/c9/989f4034fb46841208de7aeeac2c6d8300745ab4f28c42f629ba77c2d916/aiohttp-3.13.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/bf/34/1fe99124be59579ebd24316522e1da780979c856977b142c0dcd878b0a2d/spglib-2.6.0.tar.gz + - pypi: https://files.pythonhosted.org/packages/bf/e1/9e8e09ab8fc5c77f228e3271188dc4f569d12692548c42bff81b1fbba5e1/easydiffraction-0.16.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c4/6d/82e65254354ba651dc966775270a9bbc02414a3eb3f1704e6c87dab2ea83/essdiffraction-26.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c7/a0/5ff05d1919ca249508012cad89f08fdc6cfbdaa15b41651c5fe6dffaf1d3/dfo_ls-1.6.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d5/0c/043d5e551459da400957a1395e0febbf771446ff34291afcbe3d8be2a279/fsspec-2026.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d8/8b/e2bbeb42068f0c48899e8eddd34902afc0f7429d4d2a152d2dc2670dc661/pythreejs-2.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/db/bc/83e112abc66cd466c6b83f99118035867cecd41802f8d044638aa78a106e/locket-1.0.0-py2.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/dc/b7/901d837999a9350a7773289f7760cb473d4ba01fdc6ebae0ff2553d269ac/ncrystal_python-4.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e4/bc/daa30c02069eeac5b9198985ba42f5d65ca71bed6705b18329e51d352b7c/plopp-26.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e5/04/c5bb20d64417d20cba0105277235c51969444fa873000fbc26ac0a3fc5a8/gemmi-0.7.5-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e7/f9/b06c934a6aa8bc91f566bd2a214fd04c30506c2d9e2b6b171953216a65b6/kiwisolver-1.5.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/ee/8c/83087ebc47ab0396ce092363001fa37c17153119ee282700c0713a195853/prettytable-3.17.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f1/16/d905e7f53e661ce2c24686c38048d8e2b750ffc4350009d41c4e6c6c9826/h5py-3.16.0-cp314-cp314-manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/f1/2c/3850985d4c64048dec7b826f8a803e135b52b11b4c81c9cd4326b1ca15ab/ncrystal_core-4.4.2-py3-none-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/f1/5b/e63c877c4c94382b66de5045e08ec8cd960e8a4d22f0d62a4dfb1f9e5ac6/ipydatawidgets-4.3.5-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f2/f2/728f041460f1b9739b85ee23b45fa5a505962ea11fd85bdbe2a02b021373/darkdetect-0.8.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f5/57/2a154a69d6642860300bf8eb205d13131104991f2b1065bbb9075ac5c32e/tof-26.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f7/00/bbca25f8a2372465cdf93138c1e1e38dff045fb0afef1488f395d0afcb3b/ncrystal-4.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fe/3b/8ec5074bcfc450fe84273713b4b0a0dd47c0249358f5d82eb8104ffe2520/multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + osx-arm64: + - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.13.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/appnope-0.1.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/argon2-cffi-25.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/arrow-1.4.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.18.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/backports.zstd-1.5.0-py314h680f03e_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.14.3-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.3.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-with-css-6.3.0-hbca2aae_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.4.22-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cached-property-1.5.2-hd8ed1ab_1.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/cached_property-1.5.2-pyha770c72_1.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2026.4.22-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.7-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.3-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.14.4-py314hd8ed1ab_100.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/decorator-5.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/defusedxml-0.7.1-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fqdn-1.5.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h11-0.16.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/httpcore-1.0.9-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/httpx-0.28.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.13-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.8.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.2.0-pyh5552912_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.13.0-pyh53cf698_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython_pygments_lexers-1.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/isoduration-20.11.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jedi-0.19.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/json5-0.14.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonpointer-3.1.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.26.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.9.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-with-format-nongpl-4.26.0-hcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter-lsp-2.3.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.8.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.9.1-pyhc90fa1f_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_events-0.12.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.18.2-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server_terminals-0.5.4-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.5.7-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_pygments-0.3.0-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_server-2.28.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/lark-1.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.2.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mistune-3.2.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbclient-0.10.4-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbconvert-core-7.17.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbformat-5.10.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/notebook-shim-0.2.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/overrides-7.7.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.2-pyhc364b38_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pandocfilters-1.5.0-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.7-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pexpect-4.9.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pixi-kernel-0.7.1-pyhbbac1ac_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.9.6-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prometheus_client-0.25.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.52-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ptyprocess-0.7.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.21.2-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.14.4-h4df99d1_100.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-json-logger-3.2.1-pyh332efcf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2026.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.14-8_cp314.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.37.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.34.2-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/returns-0.27.0-pyhc364b38_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3339-validator-0.1.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3986-validator-0.1.1-pyh9f0ad1d_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3987-syntax-1.1.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/send2trash-2.1.0-pyh5552912_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-82.0.1-pyh332efcf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sniffio-1.3.1-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.8.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyhc90fa1f_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tinycss2-1.4.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.15.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.15.0-h396c80c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_utils-0.1.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/uri-template-1.3.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.7.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.7.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/webcolors-25.10.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/webencodings-0.5.1-pyhd8ed1ab_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/websocket-client-1.9.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/argon2-cffi-bindings-25.1.0-py314h0612a62_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-python-1.2.0-py314h3daef5d_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-hd037594_9.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cffi-2.0.0-py314h44086f9_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/debugpy-1.8.20-py314he609de1_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/krb5-1.22.2-h385eeb1_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-22.1.5-h55c6f16_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libedit-3.1.20250104-pl5321hafb1f1b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.8.0-hf6b4638_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.5.2-hcf2aa1b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.3-h8088a28_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libmpdec-4.0.0-h84a0fba_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsodium-1.0.21-h1a92334_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.53.1-h1b79a29_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.2-h8088a28_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/markupsafe-3.0.3-py314h6e9b3f0_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/msgspec-0.21.1-py314h6c2aa35_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.6-h1d4f5a5_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.6.2-hd24854e_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/psutil-7.2.2-py314ha14b1ff_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyobjc-core-12.1-py314h3a4d195_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyobjc-framework-cocoa-12.1-py314h36abed7_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.14.4-h4c637c5_100_cp314.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyyaml-6.0.3-py314h6e9b3f0_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyzmq-27.1.0-py312h022ad19_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.3-h46df422_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/rpds-py-0.30.0-py314haad56a0_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h010d191_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tornado-6.5.5-py314h6c2aa35_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/yaml-0.2.5-h925e9cb_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zeromq-4.3.5-h4818236_10.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hbf9d68e_6.conda + - pypi: https://files.pythonhosted.org/packages/04/f1/58c14b37525dc075f3bdf149251f079723049a9f1c82eb48835a0e6b8db3/diffpy_pdffit2-1.6.0-cp314-cp314-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/06/41/4e70dea1d0311016c0b0b1c53a24a266f9f8a34c6bc1af0f17cfca20aa1d/gemmi-0.7.5-cp314-cp314-macosx_11_0_arm64.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/0f/0e/0eb94e64f5badef67f11fe1e448dde2a44f00940d8949f4adf71d560552e/scipp-26.3.1-cp314-cp314-macosx_14_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/12/aa/fb2a0649fdeef5ab7072d221e8f4df164098792c813af6c87e2581cfa860/mpltoolbox-26.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1b/9f/38ff9e57a2eade7bf9dfee5eae17f39fc0e998658050279cbb14d97d36d9/msgpack-1.1.2-cp314-cp314-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/1d/77/928ea2e70641ca177a11140062cc5840d421795f2e82749d408d0cce900a/narwhals-2.21.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/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1f/7e/c2cfe0bdbec1f5ce2bd92e03311038e1c491dfd54824606f38a61167a3f0/crysfml-0.6.2-cp314-cp314-macosx_14_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/24/f9/e8242b68362bffe6fb536c8db5076861466fc780f0f1b479fc4ffbebb128/yarl-1.23.0-cp314-cp314-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/25/1c/196c610ff4c6782d697ba780ebdc1616be143213701bf22c1a270f3bf7dd/numpy-2.4.5-cp314-cp314-macosx_14_0_arm64.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/27/d2/23d25e3f247b328be58d04a4c9f894178a0d1eda7d42867cfb388adaf416/fonttools-4.63.0-cp314-cp314-macosx_10_15_universal2.whl + - pypi: https://files.pythonhosted.org/packages/2a/2d/d4bf65e47cea8ff2c794a600c4fd1273a7902f268757c531e0ee9f18aa58/pooch-1.9.0-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/38/8b/7ec325b4e9e78beefc2d025b01ee8a2fde771ef7c957c3bff99b9e1fbffa/xraydb-4.5.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3a/eb/fea4d1d51c49832120f7f285d07306db3960f423a2612c6057caf3e8196f/pip-26.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3f/0e/fa3b193432cfc60c93b42f3be03365f5f909d2b3ea410295cf36df739e31/widgetsnbextension-4.0.15-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3f/d0/7b958df957e4827837b590944008f0b28078f552b451f7407b4b3d54f574/asciichartpy-1.5.25-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/41/86/86231232fff41c9f8e4a1a7d7a597d349a02527109c3af7d618366122139/matplotlib-3.10.9-cp314-cp314-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/42/d9/27b13bc9419bf5dae02905b348f16ca827646cd76244ddd326f1a8139a6a/cyclebane-24.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/49/b2/97980f3ad4fae37dd7fe31626e2bf75fbf8bdf5d303950ec1fab39a12da8/kiwisolver-1.5.0-cp314-cp314-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/4a/f3/00bb1e867fba351e2d784170955713bee200c43ea306c59f30bd7e748192/dask-2026.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/52/59/0782e51887ac6b07ffd1570e0364cf901ebc36345fea669969d2084baebb/simple_websocket-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/55/33/bf28f618c0a9597d14e0b9ee7d1e0622faff738d44fe986ee287cdf1b8d0/sqlalchemy-2.0.49-cp314-cp314-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/56/6d/0d9848617b9f753b87f214f1c682592f7ca42de085f564352f10f0843026/ipywidgets-8.1.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/58/e0/f1871f520c359e4e3a2eb7437c9e7e792bb6c356414e8617937561167caf/pycifrw-5.0.1.tar.gz + - pypi: https://files.pythonhosted.org/packages/5b/29/74eeb4d3f3ae61ca096b018ad486b3b3c74b17bec09ab4edab721cbefec3/typeguard-4.5.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5b/69/93b34728cc386efdde0c342f8c680b9187dea7beb7adaf6b58a0713be101/mpld3-0.5.12-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5c/01/6cb4d63c6b6933be4b7945b2f64638336420f04ea71ca5b9a7539c008bc5/scippnexus-26.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/61/2b/e260d50e64690d2a9e405d52ccd18a63c286c5088937dd0107cb23eb3195/diffpy_utils-3.7.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/63/9f/724f66a48309dd97a2ff58f491d6ffd925f35d1278a5e55dc9a5ac6a156b/scippneutron-26.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/63/b1/4260d67d6bd85e58a66b72d54ce15d5de789b6f3870cc6bedf8ff9667401/propcache-0.5.2-cp314-cp314-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/68/10/bf2d6738d72748b961a3751ab89522d58c54efc36a8e1a12161216cd45cf/pandas-3.0.3-cp314-cp314-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/6a/b7/9366ed44ced9b7ef357ab48c94205280276db9d7f064aa3012a97227e966/h5py-3.16.0-cp314-cp314-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/6c/25/4f103d1bedb3593718713b3f743df7b3ff3fc68d36d6666c30265ef59c8a/ase-3.28.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/78/3c/2a612b95ddbb9a6bdcb47b7a93c4884f74c6ff22356b2f7b213b16e65c35/pycifstar-0.3.0.tar.gz + - pypi: https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7b/67/b1944235474aac3f0b0e1b232ce49547f9f9461ca4b943df1b88da5d3f1d/bumps-1.0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8a/06/2c1bd1ee9eee3e65b0b6395dcf960e71b11576995eae3b4ab9ec63d89bea/essreduce-26.4.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8a/a1/8d812e53a5da1687abb10445275d41a8b13adb781bbf7196ddbcf8d88505/lazy_loader-0.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8d/c0/fdf9d3ee103ce66a55f0532835ad5e154226c5222423c6636ba049dc42fc/traittypes-0.2.3-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8f/5e/f1e1dd319e35e962a4e00b33150a8868b6329cc1d19fd533436ba5488f09/uncertainties-3.2.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/90/ad/cba91b3bcf04073e4d1655a5c1710ef3f457f56f7d1b79dcc3d72f4dd912/plotly-6.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/91/4c/e0ce1ef95d4000ebc1c11801f9b944fa5910ecc15b5e351865763d8657f8/graphviz-0.21-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/96/5d/0c59079aa7ef34980a5925a06a90ad2b7c94e486c194b3527d557cabb042/cryspy-0.11.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/99/31/6cf181011dc738c33bf6ba7aea2e8e1d3c1f71b7dab1942f3054f66f6202/asteval-1.0.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/99/37/e8730c3587a65eb5645d4aba2d27aae48e8003614d6aaf15dda67f702f1f/bidict-0.23.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9b/91/cc8cc78a111826c54743d88651e1687008133c37e5ee615fee9b57990fac/aiohttp-3.13.5-cp314-cp314-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a3/58/35da89ee790598a0700ea49b2a66594140f44dec458c07e8e3d4979137fc/ply-3.11-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a4/39/3a0ae5b0edb66e61bb0e8bc53a503495cba5892297ae21faf6ba0525e681/varname-1.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/aa/54/0cce26da03a981f949bb8449c9778537f75f5917c172e1d2992ff25cb57d/python_engineio-4.13.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ab/b5/36c712098e6191d1b4e349304ef73a8d06aed77e56ceaac8c0a306c7bda1/jupyterlab_widgets-3.0.16-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/af/46/661159ad844034ba8b3f4e0516215c41e4ee17db4213d13a82227670764f/sciline-25.11.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b4/5e/bf11645aebb9af7d8d35927c40d3855816a0855c799e8156eeca8d632c90/diffpy_structure-3.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ba/8c/1a9e46228571de18f8e28f16fabdfc20212a5d019f3e3303452b3f0a580d/pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/bf/34/1fe99124be59579ebd24316522e1da780979c856977b142c0dcd878b0a2d/spglib-2.6.0.tar.gz + - pypi: https://files.pythonhosted.org/packages/bf/e1/9e8e09ab8fc5c77f228e3271188dc4f569d12692548c42bff81b1fbba5e1/easydiffraction-0.16.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c4/6d/82e65254354ba651dc966775270a9bbc02414a3eb3f1704e6c87dab2ea83/essdiffraction-26.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c7/a0/5ff05d1919ca249508012cad89f08fdc6cfbdaa15b41651c5fe6dffaf1d3/dfo_ls-1.6.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c7/ea/7988934c8e3e3418aa043f70421817df28d06aef50bfd85f5ad3ec6e70f1/ncrystal_core-4.4.2-py3-none-macosx_11_0_arm64.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/d5/0c/043d5e551459da400957a1395e0febbf771446ff34291afcbe3d8be2a279/fsspec-2026.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d8/8b/e2bbeb42068f0c48899e8eddd34902afc0f7429d4d2a152d2dc2670dc661/pythreejs-2.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/db/7b/8624a203326675d7746a254083a187398090a179335b2e4a20e2ddc46e83/scipy-1.17.1-cp314-cp314-macosx_14_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/db/bc/83e112abc66cd466c6b83f99118035867cecd41802f8d044638aa78a106e/locket-1.0.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/dc/b7/901d837999a9350a7773289f7760cb473d4ba01fdc6ebae0ff2553d269ac/ncrystal_python-4.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e4/bc/daa30c02069eeac5b9198985ba42f5d65ca71bed6705b18329e51d352b7c/plopp-26.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ee/8c/83087ebc47ab0396ce092363001fa37c17153119ee282700c0713a195853/prettytable-3.17.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f1/4f/733c48f270565d78b4544f2baddc2fb2a245e5a8640254b12c36ac7ac68e/multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/f1/5b/e63c877c4c94382b66de5045e08ec8cd960e8a4d22f0d62a4dfb1f9e5ac6/ipydatawidgets-4.3.5-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f2/f2/728f041460f1b9739b85ee23b45fa5a505962ea11fd85bdbe2a02b021373/darkdetect-0.8.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f5/57/2a154a69d6642860300bf8eb205d13131104991f2b1065bbb9075ac5c32e/tof-26.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f7/00/bbca25f8a2372465cdf93138c1e1e38dff045fb0afef1488f395d0afcb3b/ncrystal-4.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fd/e1/3542a9cb596cadd76fcef413f19c79216e002623158befe6daa03dbfa88c/contourpy-1.3.3-cp314-cp314-macosx_11_0_arm64.whl + win-64: + - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.13.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/argon2-cffi-25.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/arrow-1.4.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.18.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/backports.zstd-1.5.0-py314h680f03e_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.14.3-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.3.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-with-css-6.3.0-hbca2aae_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.4.22-h4c7d964_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cached-property-1.5.2-hd8ed1ab_1.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/cached_property-1.5.2-pyha770c72_1.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2026.4.22-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.7-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.3-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.14.4-py314hd8ed1ab_100.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/decorator-5.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/defusedxml-0.7.1-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fqdn-1.5.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h11-0.16.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/httpcore-1.0.9-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/httpx-0.28.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.13-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.8.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.2.0-pyh6dadd2b_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.13.0-pyhe2676ad_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython_pygments_lexers-1.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/isoduration-20.11.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jedi-0.19.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/json5-0.14.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonpointer-3.1.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.26.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.9.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-with-format-nongpl-4.26.0-hcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter-lsp-2.3.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.8.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.9.1-pyh6dadd2b_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_events-0.12.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.18.2-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server_terminals-0.5.4-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.5.7-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_pygments-0.3.0-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_server-2.28.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/lark-1.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.2.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mistune-3.2.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbclient-0.10.4-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbconvert-core-7.17.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbformat-5.10.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/notebook-shim-0.2.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/overrides-7.7.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.2-pyhc364b38_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pandocfilters-1.5.0-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.7-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pixi-kernel-0.7.1-pyhbbac1ac_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.9.6-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prometheus_client-0.25.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.52-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyh09c184e_7.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.21.2-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.14.4-h4df99d1_100.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-json-logger-3.2.1-pyh332efcf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2026.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.14-8_cp314.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.37.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.34.2-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/returns-0.27.0-pyhc364b38_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3339-validator-0.1.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3986-validator-0.1.1-pyh9f0ad1d_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3987-syntax-1.1.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/send2trash-2.1.0-pyh6dadd2b_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-82.0.1-pyh332efcf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sniffio-1.3.1-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.8.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyh6dadd2b_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tinycss2-1.4.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.15.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.15.0-h396c80c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_utils-0.1.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/uri-template-1.3.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.7.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.7.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/webcolors-25.10.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/webencodings-0.5.1-pyhd8ed1ab_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/websocket-client-1.9.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/win_inet_pton-1.1.0-pyh7428d3b_8.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/argon2-cffi-bindings-25.1.0-py314h5a2d7ad_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/brotli-python-1.2.0-py314he701e3d_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h0ad9c76_9.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/cffi-2.0.0-py314h5a2d7ad_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/debugpy-1.8.20-py314hb98de8c_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/krb5-1.22.2-h0ea6238_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.8.0-hac47afa_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libffi-3.5.2-h3d046cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-5.8.3-hfd05255_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libmpdec-4.0.0-hfd05255_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libsodium-1.0.21-h6a83c73_3.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.53.1-hf5d6505_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libzlib-1.3.2-hfd05255_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/markupsafe-3.0.3-py314h2359020_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/msgspec-0.21.1-py314h5a2d7ad_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.6.2-hf411b9b_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/psutil-7.2.2-py314hc5dbbe4_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/python-3.14.4-h4b44e0e_100_cp314.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pywin32-311-py314h8f8f202_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pywinpty-2.0.15-py314h51f0985_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pyyaml-6.0.3-py314h2359020_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pyzmq-27.1.0-py312h343a6d4_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/rpds-py-0.30.0-py314h9f07db2_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h6ed50ae_3.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/tornado-6.5.5-py314h5a2d7ad_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.26100.0-h57928b3_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-h41ae7f8_36.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.44.35208-h818238b_36.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.44.35208-h818238b_36.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/winpty-0.4.3-4.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/win-64/yaml-0.2.5-h6a83c73_3.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/zeromq-4.3.5-h507cc87_10.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_6.conda + - pypi: https://files.pythonhosted.org/packages/07/c7/deb8c5e604404dbf10a3808a858946ca3547692ff6316b698945bb72177e/python_socketio-5.16.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0c/53/b50773ecf1d1e4a5858ee13011e30317ba02639ae4a1411a34967951fc9b/crysfml-0.6.2-cp314-cp314-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/12/aa/fb2a0649fdeef5ab7072d221e8f4df164098792c813af6c87e2581cfa860/mpltoolbox-26.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1d/77/928ea2e70641ca177a11140062cc5840d421795f2e82749d408d0cce900a/narwhals-2.21.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/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1f/28/3f8aa247d29d010547d52207395cb057ebd0a40b88f64bc1dbac9e17a729/scipp-26.3.1-cp314-cp314-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/22/4d/eaedff67fc805aeba4ba746aec891b4b24cebb1a7d078084b6300f79d063/aiohttp-3.13.5-cp314-cp314-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/25/7d/cea3531f77df694ac7f169378250d85f19f69b09a5f4fa45f650837ae7cc/py3dmol-2.5.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2a/2d/d4bf65e47cea8ff2c794a600c4fd1273a7902f268757c531e0ee9f18aa58/pooch-1.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/38/55/792619469bab9882d8bbd5865d45a72f6478762d04a9af4bf0d08c503e95/pandas-3.0.3-cp314-cp314-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/38/7e/7b91c89a4cf0f543a83be978657afb20c86af6d725253e319589dcc4ce52/lmfit-1.3.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/38/8b/7ec325b4e9e78beefc2d025b01ee8a2fde771ef7c957c3bff99b9e1fbffa/xraydb-4.5.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3a/eb/fea4d1d51c49832120f7f285d07306db3960f423a2612c6057caf3e8196f/pip-26.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3e/14/615a450205e1b56d16c6783f5ccd116cde05550faad70ae077c955654a75/h5py-3.16.0-cp314-cp314-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/3f/0e/fa3b193432cfc60c93b42f3be03365f5f909d2b3ea410295cf36df739e31/widgetsnbextension-4.0.15-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3f/d0/7b958df957e4827837b590944008f0b28078f552b451f7407b4b3d54f574/asciichartpy-1.5.25-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/42/d9/27b13bc9419bf5dae02905b348f16ca827646cd76244ddd326f1a8139a6a/cyclebane-24.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4a/f3/00bb1e867fba351e2d784170955713bee200c43ea306c59f30bd7e748192/dask-2026.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4b/39/f0e8ea762a764a9dc52aa7dabcfad51a354819de1f0d4652b6a1122424d6/scipy-1.17.1-cp314-cp314-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/52/59/0782e51887ac6b07ffd1570e0364cf901ebc36345fea669969d2084baebb/simple_websocket-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/56/6d/0d9848617b9f753b87f214f1c682592f7ca42de085f564352f10f0843026/ipywidgets-8.1.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/58/e0/f1871f520c359e4e3a2eb7437c9e7e792bb6c356414e8617937561167caf/pycifrw-5.0.1.tar.gz + - pypi: https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/5b/29/74eeb4d3f3ae61ca096b018ad486b3b3c74b17bec09ab4edab721cbefec3/typeguard-4.5.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5b/69/93b34728cc386efdde0c342f8c680b9187dea7beb7adaf6b58a0713be101/mpld3-0.5.12-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5c/01/6cb4d63c6b6933be4b7945b2f64638336420f04ea71ca5b9a7539c008bc5/scippnexus-26.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/61/2b/e260d50e64690d2a9e405d52ccd18a63c286c5088937dd0107cb23eb3195/diffpy_utils-3.7.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/61/d2/45c9defbaa1ea297035d9d4cce9e8f80daafbf19319c6007f157c6256ea9/propcache-0.5.2-cp314-cp314-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/63/9f/724f66a48309dd97a2ff58f491d6ffd925f35d1278a5e55dc9a5ac6a156b/scippneutron-26.5.0-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/6e/f1/abd09c2ae91228c5f3998dbd7f41353def9eac64253de3c8105efa2082f7/msgpack-1.1.2-cp314-cp314-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/78/3c/2a612b95ddbb9a6bdcb47b7a93c4884f74c6ff22356b2f7b213b16e65c35/pycifstar-0.3.0.tar.gz + - pypi: https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7b/67/b1944235474aac3f0b0e1b232ce49547f9f9461ca4b943df1b88da5d3f1d/bumps-1.0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7d/c2/57f54b03d0f22d4044b8afb9ca0e184f8b1afd57b4f735c2fa70883dc601/contourpy-1.3.3-cp314-cp314-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/82/d0/26c81ffbe588f936d05f395da34046c66322e8067c9fd331c788c4f682f2/diffpy_pdffit2-1.6.0-cp314-cp314-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8a/06/2c1bd1ee9eee3e65b0b6395dcf960e71b11576995eae3b4ab9ec63d89bea/essreduce-26.4.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8a/a1/8d812e53a5da1687abb10445275d41a8b13adb781bbf7196ddbcf8d88505/lazy_loader-0.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8d/c0/fdf9d3ee103ce66a55f0532835ad5e154226c5222423c6636ba049dc42fc/traittypes-0.2.3-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8f/5e/f1e1dd319e35e962a4e00b33150a8868b6329cc1d19fd533436ba5488f09/uncertainties-3.2.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/90/ad/cba91b3bcf04073e4d1655a5c1710ef3f457f56f7d1b79dcc3d72f4dd912/plotly-6.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/91/20/6b165108058767ee643c55c5c4904d591a830ee2b3c7dbd359828fbc829f/greenlet-3.5.0-cp314-cp314-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/91/4c/e0ce1ef95d4000ebc1c11801f9b944fa5910ecc15b5e351865763d8657f8/graphviz-0.21-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/96/5d/0c59079aa7ef34980a5925a06a90ad2b7c94e486c194b3527d557cabb042/cryspy-0.11.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/99/31/6cf181011dc738c33bf6ba7aea2e8e1d3c1f71b7dab1942f3054f66f6202/asteval-1.0.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/99/37/e8730c3587a65eb5645d4aba2d27aae48e8003614d6aaf15dda67f702f1f/bidict-0.23.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a3/36/4e551e8aa55c9188bca9abb5096805edbf7431072b76e2298e34fd3a3008/kiwisolver-1.5.0-cp314-cp314-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/a3/58/35da89ee790598a0700ea49b2a66594140f44dec458c07e8e3d4979137fc/ply-3.11-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a4/39/3a0ae5b0edb66e61bb0e8bc53a503495cba5892297ae21faf6ba0525e681/varname-1.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a9/5b/9b92f54c784c26e2a422e55a8d2607ab15b7ea3349e28359282f84f01d43/yarl-1.23.0-cp314-cp314-win_amd64.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/aa/92/d063df4d63d988b20d881856c74df76c0c1786229bb870f3a52af0981d4d/numpy-2.4.5-cp314-cp314-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/ab/1a/5a4f747a8b271cbb024946d2dd3c913ab5032ba430626f8c3528ada96b4b/matplotlib-3.10.9-cp314-cp314-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/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/af/46/661159ad844034ba8b3f4e0516215c41e4ee17db4213d13a82227670764f/sciline-25.11.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b4/5e/bf11645aebb9af7d8d35927c40d3855816a0855c799e8156eeca8d632c90/diffpy_structure-3.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bf/34/1fe99124be59579ebd24316522e1da780979c856977b142c0dcd878b0a2d/spglib-2.6.0.tar.gz + - pypi: https://files.pythonhosted.org/packages/bf/e1/9e8e09ab8fc5c77f228e3271188dc4f569d12692548c42bff81b1fbba5e1/easydiffraction-0.16.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c3/d4/98078064ccc76b45cb0f6c002452011e93c4bd26f6850344f0951cc1fe89/fonttools-4.63.0-cp314-cp314-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/c4/6d/82e65254354ba651dc966775270a9bbc02414a3eb3f1704e6c87dab2ea83/essdiffraction-26.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c7/6b/6c02f55c2ce2f137ccca0986be7dd89bea31d5bee4346b4377fa3b8586df/ncrystal_core-4.4.2-py3-none-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/c7/a0/5ff05d1919ca249508012cad89f08fdc6cfbdaa15b41651c5fe6dffaf1d3/dfo_ls-1.6.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/cd/8b/601e6566b957ca50e28725cb6c355c59c2c8609751efbecd980db44e0349/pillow-12.2.0-cp314-cp314-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/cf/4f/8297e4ed88e80baa1f5aa3c484a0ee29ef3c69c7582f206c916973b75057/sqlalchemy-2.0.49-cp314-cp314-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/d5/0c/043d5e551459da400957a1395e0febbf771446ff34291afcbe3d8be2a279/fsspec-2026.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d8/8b/e2bbeb42068f0c48899e8eddd34902afc0f7429d4d2a152d2dc2670dc661/pythreejs-2.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/db/bc/83e112abc66cd466c6b83f99118035867cecd41802f8d044638aa78a106e/locket-1.0.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/dc/b7/901d837999a9350a7773289f7760cb473d4ba01fdc6ebae0ff2553d269ac/ncrystal_python-4.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e0/bf/52f25716bbe93745595800f36fb17b73711f14da59ed0bb2eba141bc9f0f/multidict-6.7.1-cp314-cp314-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e4/bc/daa30c02069eeac5b9198985ba42f5d65ca71bed6705b18329e51d352b7c/plopp-26.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ee/8c/83087ebc47ab0396ce092363001fa37c17153119ee282700c0713a195853/prettytable-3.17.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f1/5b/e63c877c4c94382b66de5045e08ec8cd960e8a4d22f0d62a4dfb1f9e5ac6/ipydatawidgets-4.3.5-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f2/f2/728f041460f1b9739b85ee23b45fa5a505962ea11fd85bdbe2a02b021373/darkdetect-0.8.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f5/57/2a154a69d6642860300bf8eb205d13131104991f2b1065bbb9075ac5c32e/tof-26.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f7/00/bbca25f8a2372465cdf93138c1e1e38dff045fb0afef1488f395d0afcb3b/ncrystal-4.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ff/1c/a28b27effb13a381fe077ea3e3e78f6debd6315f2b3edff67bbb93d0ef51/gemmi-0.7.5-cp314-cp314-win_amd64.whl +packages: +- conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda + build_number: 20 + sha256: 1dd3fffd892081df9726d7eb7e0dea6198962ba775bd88842135a4ddb4deb3c9 + md5: a9f577daf3de00bca7c3c76c0ecbd1de + depends: + - __glibc >=2.17,<3.0.a0 + - libgomp >=7.5.0 + constrains: + - openmp_impl <0.0a0 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 28948 + timestamp: 1770939786096 +- conda: https://conda.anaconda.org/conda-forge/linux-64/argon2-cffi-bindings-25.1.0-py312h4c3975b_2.conda + sha256: 7988c207b2b766dad5ebabf25a92b8d75cb8faed92f256fd7a4e0875c9ec6d58 + md5: 1567f06d717246abab170736af8bad1b + depends: + - __glibc >=2.17,<3.0.a0 + - cffi >=1.0.1 + - libgcc >=14 + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 + license: MIT + license_family: MIT + purls: + - pkg:pypi/argon2-cffi-bindings?source=hash-mapping + size: 35646 + timestamp: 1762509443854 +- conda: https://conda.anaconda.org/conda-forge/linux-64/argon2-cffi-bindings-25.1.0-py314h5bd0f2a_2.conda + sha256: 39234a99df3d2e3065383808ed8bfda36760de5ef590c54c3692bb53571ef02b + md5: 3cca1b74b2752917b5b65b81f61f0553 + depends: + - __glibc >=2.17,<3.0.a0 + - cffi >=2.0.0b1 + - libgcc >=14 + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + license: MIT + license_family: MIT + purls: + - pkg:pypi/argon2-cffi-bindings?source=hash-mapping + size: 35598 + timestamp: 1762509505285 +- conda: https://conda.anaconda.org/conda-forge/linux-64/backports.zstd-1.5.0-py312h90b7ffd_0.conda + sha256: a2b08a4e5e549b5f67c38edffd175437e2208547a7e67b5fa5373b67ef419e50 + md5: b31dba71fe091e7201826e57e0f7b261 + depends: + - python + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - zstd >=1.5.7,<1.6.0a0 + - python_abi 3.12.* *_cp312 + license: BSD-3-Clause AND MIT AND EPL-2.0 + purls: + - pkg:pypi/backports-zstd?source=compressed-mapping + size: 239928 + timestamp: 1778594049826 +- conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.2.0-py312hdb49522_1.conda + sha256: 49df13a1bb5e388ca0e4e87022260f9501ed4192656d23dc9d9a1b4bf3787918 + md5: 64088dffd7413a2dd557ce837b4cbbdb + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libstdcxx >=14 + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 + constrains: + - libbrotlicommon 1.2.0 hb03c661_1 + license: MIT + license_family: MIT + purls: + - pkg:pypi/brotli?source=hash-mapping + size: 368300 + timestamp: 1764017300621 +- conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.2.0-py314h3de4e8d_1.conda + sha256: 3ad3500bff54a781c29f16ce1b288b36606e2189d0b0ef2f67036554f47f12b0 + md5: 8910d2c46f7e7b519129f486e0fe927a + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libstdcxx >=14 + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + constrains: + - libbrotlicommon 1.2.0 hb03c661_1 + license: MIT + license_family: MIT + purls: + - pkg:pypi/brotli?source=hash-mapping + size: 367376 + timestamp: 1764017265553 +- conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_9.conda + sha256: 0b75d45f0bba3e95dc693336fa51f40ea28c980131fec438afb7ce6118ed05f6 + md5: d2ffd7602c02f2b316fd921d39876885 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: bzip2-1.0.6 + license_family: BSD + purls: [] + size: 260182 + timestamp: 1771350215188 +- conda: https://conda.anaconda.org/conda-forge/linux-64/c-ares-1.34.6-hb03c661_0.conda + sha256: cc9accf72fa028d31c2a038460787751127317dcfa991f8d1f1babf216bb454e + md5: 920bb03579f15389b9e512095ad995b7 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: MIT + license_family: MIT + purls: [] + size: 207882 + timestamp: 1765214722852 +- conda: https://conda.anaconda.org/conda-forge/linux-64/cffi-2.0.0-py312h460c074_1.conda + sha256: 7dafe8173d5f94e46cf9cd597cc8ff476a8357fbbd4433a8b5697b2864845d9c + md5: 648ee28dcd4e07a1940a17da62eccd40 + depends: + - __glibc >=2.17,<3.0.a0 + - libffi >=3.5.2,<3.6.0a0 + - libgcc >=14 + - pycparser + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 + license: MIT + license_family: MIT + purls: + - pkg:pypi/cffi?source=hash-mapping + size: 295716 + timestamp: 1761202958833 +- conda: https://conda.anaconda.org/conda-forge/linux-64/cffi-2.0.0-py314h4a8dc5f_1.conda + sha256: c6339858a0aaf5d939e00d345c98b99e4558f285942b27232ac098ad17ac7f8e + md5: cf45f4278afd6f4e6d03eda0f435d527 + depends: + - __glibc >=2.17,<3.0.a0 + - libffi >=3.5.2,<3.6.0a0 + - libgcc >=14 + - pycparser + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + license: MIT + license_family: MIT + purls: + - pkg:pypi/cffi?source=hash-mapping + size: 300271 + timestamp: 1761203085220 +- conda: https://conda.anaconda.org/conda-forge/linux-64/debugpy-1.8.20-py312h8285ef7_0.conda + sha256: f20121b67149ff80bf951ccae7442756586d8789204cd08ade59397b22bfd098 + md5: ee1b48795ceb07311dd3e665dd4f5f33 + depends: + - python + - libgcc >=14 + - libstdcxx >=14 + - __glibc >=2.17,<3.0.a0 + - python_abi 3.12.* *_cp312 + license: MIT + license_family: MIT + purls: + - pkg:pypi/debugpy?source=hash-mapping + size: 2858582 + timestamp: 1769744978783 +- conda: https://conda.anaconda.org/conda-forge/linux-64/debugpy-1.8.20-py314h42812f9_0.conda + sha256: d9e89e351d7189c41615cfceca76b3bcacaa9c81d9945ac1caa6fb9e5184f610 + md5: 57e6fad901c05754d5256fe3ab9f277b + depends: + - python + - libgcc >=14 + - libstdcxx >=14 + - __glibc >=2.17,<3.0.a0 + - python_abi 3.14.* *_cp314 + license: MIT + license_family: MIT + purls: + - pkg:pypi/debugpy?source=hash-mapping + size: 2886804 + timestamp: 1769744977998 - conda: https://conda.anaconda.org/conda-forge/linux-64/gsl-2.8-hbf7d49c_1.conda sha256: f923af07c3a3db746d3be8efebdaa9c819a6007ee3cc12445cee059641611e05 md5: 04e128d2adafe3c844cde58f103c481b depends: - - __glibc >=2.17,<3.0.a0 - - libblas >=3.9.0,<4.0a0 - - libcblas >=3.9.0,<4.0a0 - - libgcc >=13 - license: GPL-3.0-or-later - license_family: GPL + - __glibc >=2.17,<3.0.a0 + - libblas >=3.9.0,<4.0a0 + - libcblas >=3.9.0,<4.0a0 + - libgcc >=13 + license: GPL-3.0-or-later + license_family: GPL + purls: [] + size: 2486744 + timestamp: 1737621160295 +- conda: https://conda.anaconda.org/conda-forge/linux-64/icu-78.3-h33c6efd_0.conda + sha256: fbf86c4a59c2ed05bbffb2ba25c7ed94f6185ec30ecb691615d42342baa1a16a + md5: c80d8a3b84358cb967fa81e7075fbc8a + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libstdcxx >=14 + license: MIT + license_family: MIT + purls: [] + size: 12723451 + timestamp: 1773822285671 +- conda: https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.3-hb9d3cd8_0.conda + sha256: 0960d06048a7185d3542d850986d807c6e37ca2e644342dd0c72feefcf26c2a4 + md5: b38117a3c920364aff79f870c984b4a3 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + license: LGPL-2.1-or-later + purls: [] + size: 134088 + timestamp: 1754905959823 +- conda: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.22.2-ha1258a1_0.conda + sha256: 3e307628ca3527448dd1cb14ad7bb9d04d1d28c7d4c5f97ba196ae984571dd25 + md5: fb53fb07ce46a575c5d004bbc96032c2 + depends: + - __glibc >=2.17,<3.0.a0 + - keyutils >=1.6.3,<2.0a0 + - libedit >=3.1.20250104,<3.2.0a0 + - libedit >=3.1.20250104,<4.0a0 + - libgcc >=14 + - libstdcxx >=14 + - openssl >=3.5.5,<4.0a0 + license: MIT + license_family: MIT + purls: [] + size: 1386730 + timestamp: 1769769569681 +- conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45.1-default_hbd61a6d_102.conda + sha256: 3d584956604909ff5df353767f3a2a2f60e07d070b328d109f30ac40cd62df6c + md5: 18335a698559cdbcd86150a48bf54ba6 + depends: + - __glibc >=2.17,<3.0.a0 + - zstd >=1.5.7,<1.6.0a0 + constrains: + - binutils_impl_linux-64 2.45.1 + license: GPL-3.0-only + license_family: GPL + purls: [] + size: 728002 + timestamp: 1774197446916 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libabseil-20260107.1-cxx17_h7b12aa8_0.conda + sha256: a7a4481a4d217a3eadea0ec489826a69070fcc3153f00443aa491ed21527d239 + md5: 6f7b4302263347698fd24565fbf11310 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libstdcxx >=14 + constrains: + - libabseil-static =20260107.1=cxx17* + - abseil-cpp =20260107.1 + license: Apache-2.0 + license_family: Apache + purls: [] + size: 1384817 + timestamp: 1770863194876 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libblas-3.11.0-7_h4a7cf45_openblas.conda + build_number: 7 + sha256: 081c850f99bc355821fac9c6e3727d40b3f8ce3beb50a5437cf03726b611ff39 + md5: 955b44e8b00b7f7ef4ce0130cef12394 + depends: + - libopenblas >=0.3.33,<0.3.34.0a0 + - libopenblas >=0.3.33,<1.0a0 + constrains: + - libcblas 3.11.0 7*_openblas + - blas 2.307 openblas + - liblapack 3.11.0 7*_openblas + - liblapacke 3.11.0 7*_openblas + - mkl <2027 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 18716 + timestamp: 1778489854108 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlicommon-1.2.0-hb03c661_1.conda + sha256: 318f36bd49ca8ad85e6478bd8506c88d82454cc008c1ac1c6bf00a3c42fa610e + md5: 72c8fd1af66bd67bf580645b426513ed + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: MIT + license_family: MIT + purls: [] + size: 79965 + timestamp: 1764017188531 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlidec-1.2.0-hb03c661_1.conda + sha256: 12fff21d38f98bc446d82baa890e01fd82e3b750378fedc720ff93522ffb752b + md5: 366b40a69f0ad6072561c1d09301c886 + depends: + - __glibc >=2.17,<3.0.a0 + - libbrotlicommon 1.2.0 hb03c661_1 + - libgcc >=14 + license: MIT + license_family: MIT + purls: [] + size: 34632 + timestamp: 1764017199083 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlienc-1.2.0-hb03c661_1.conda + sha256: a0c15c79997820bbd3fbc8ecf146f4fe0eca36cc60b62b63ac6cf78857f1dd0d + md5: 4ffbb341c8b616aa2494b6afb26a0c5f + depends: + - __glibc >=2.17,<3.0.a0 + - libbrotlicommon 1.2.0 hb03c661_1 + - libgcc >=14 + license: MIT + license_family: MIT + purls: [] + size: 298378 + timestamp: 1764017210931 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.11.0-7_h0358290_openblas.conda + build_number: 7 + sha256: 956ae0bb1ec8b0c3663d75b151aceb0521b54e513bf97f621a035f9c87037970 + md5: 0675639dc24cb0032f199e7ff68e4633 + depends: + - libblas 3.11.0 7_h4a7cf45_openblas + constrains: + - liblapacke 3.11.0 7*_openblas + - blas 2.307 openblas + - liblapack 3.11.0 7*_openblas + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 18675 + timestamp: 1778489861559 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20250104-pl5321h7949ede_0.conda + sha256: d789471216e7aba3c184cd054ed61ce3f6dac6f87a50ec69291b9297f8c18724 + md5: c277e0a4d549b03ac1e9d6cbbe3d017b + depends: + - ncurses + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - ncurses >=6.5,<7.0a0 + license: BSD-2-Clause + license_family: BSD + purls: [] + size: 134676 + timestamp: 1738479519902 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libev-4.33-hd590300_2.conda + sha256: 1cd6048169fa0395af74ed5d8f1716e22c19a81a8a36f934c110ca3ad4dd27b4 + md5: 172bf1cd1ff8629f2b1179945ed45055 + depends: + - libgcc-ng >=12 + license: BSD-2-Clause + license_family: BSD + purls: [] + size: 112766 + timestamp: 1702146165126 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.8.0-hecca717_0.conda + sha256: ea33c40977ea7a2c3658c522230058395bc2ee0d89d99f0711390b6a1ee80d12 + md5: a3b390520c563d78cc58974de95a03e5 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + constrains: + - expat 2.8.0.* + license: MIT + license_family: MIT + purls: [] + size: 77241 + timestamp: 1777846112704 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h3435931_0.conda + sha256: 31f19b6a88ce40ebc0d5a992c131f57d919f73c0b92cd1617a5bec83f6e961e6 + md5: a360c33a5abe61c07959e449fa1453eb + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: MIT + license_family: MIT + purls: [] + size: 58592 + timestamp: 1769456073053 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-he0feb66_19.conda + sha256: 8e0a3b5e41272e5678499b5dfc4cddb673f9e935de01eb0767ce857001229f46 + md5: 57736f29cc2b0ec0b6c2952d3f101b6a + depends: + - __glibc >=2.17,<3.0.a0 + - _openmp_mutex >=4.5 + constrains: + - libgcc-ng ==15.2.0=*_19 + - libgomp 15.2.0 he0feb66_19 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 1041084 + timestamp: 1778269013026 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.2.0-h69a702a_19.conda + sha256: 9dcf54adfaa5e861123c2da4f2f0451a685464ea7e5a41ad91cf67b31d658d98 + md5: 331ee9b72b9dff570d56b1302c5ab37d + depends: + - libgcc 15.2.0 he0feb66_19 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 27694 + timestamp: 1778269016987 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.2.0-h69a702a_19.conda + sha256: 561a42758ef25b9ce308c4e2cf56daee4f06138385a17e29a492cd928e00be6f + md5: 42bf7eca1a951735fa06c0e3c0d5c8e6 + depends: + - libgfortran5 15.2.0 h68bc16d_19 + constrains: + - libgfortran-ng ==15.2.0=*_19 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 27655 + timestamp: 1778269042954 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.2.0-h68bc16d_19.conda + sha256: 057978bb69fea29ed715a9b98adf71015c31baecc4aeb2bfc20d4fd5d83579d4 + md5: 85072b0ad177c966294f129b7c04a2d5 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=15.2.0 + constrains: + - libgfortran 15.2.0 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 2483673 + timestamp: 1778269025089 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-he0feb66_19.conda + sha256: 5abe4ab9d93f6c9757d654f1969ae2267d4505315c1f2f8fe705fd60af084f1b + md5: faac990cb7aedc7f3a2224f2c9b0c26c + depends: + - __glibc >=2.17,<3.0.a0 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 603817 + timestamp: 1778268942614 +- conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.3-hb03c661_0.conda + sha256: ec30e52a3c1bf7d0425380a189d209a52baa03f22fb66dd3eb587acaa765bd6d + md5: b88d90cad08e6bc8ad540cb310a761fb + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + constrains: + - xz 5.8.3.* + license: 0BSD + purls: [] + size: 113478 + timestamp: 1775825492909 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb03c661_1.conda + sha256: fe171ed5cf5959993d43ff72de7596e8ac2853e9021dec0344e583734f1e0843 + md5: 2c21e66f50753a083cbe6b80f38268fa + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: BSD-2-Clause + license_family: BSD + purls: [] + size: 92400 + timestamp: 1769482286018 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libnghttp2-1.68.1-h877daf1_0.conda + sha256: 663444d77a42f2265f54fb8b48c5450bfff4388d9c0f8253dd7855f0d993153f + md5: 2a45e7f8af083626f009645a6481f12d + depends: + - __glibc >=2.17,<3.0.a0 + - c-ares >=1.34.6,<2.0a0 + - libev >=4.33,<4.34.0a0 + - libev >=4.33,<5.0a0 + - libgcc >=14 + - libstdcxx >=14 + - libzlib >=1.3.1,<2.0a0 + - openssl >=3.5.5,<4.0a0 + license: MIT + license_family: MIT + purls: [] + size: 663344 + timestamp: 1773854035739 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hb9d3cd8_1.conda + sha256: 927fe72b054277cde6cb82597d0fcf6baf127dcbce2e0a9d8925a68f1265eef5 + md5: d864d34357c3b65a4b731f78c0801dc4 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + license: LGPL-2.1-only + license_family: GPL + purls: [] + size: 33731 + timestamp: 1750274110928 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.33-pthreads_h94d23a6_0.conda + sha256: 3d9aa85648e5e18a6d66db98b8c4317cc426721ad7a220aa86330d1ccedc8903 + md5: 2d3278b721e40468295ca755c3b84070 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libgfortran + - libgfortran5 >=14.3.0 + constrains: + - openblas >=0.3.33,<0.3.34.0a0 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 5931919 + timestamp: 1776993658641 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libsodium-1.0.21-h280c20c_3.conda + sha256: 64e5c80cbce4680a2d25179949739a6def695d72c40ca28f010711764e372d97 + md5: 7af961ef4aa2c1136e11dd43ded245ab + depends: + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + license: ISC + purls: [] + size: 277661 + timestamp: 1772479381288 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.53.1-h0c1763c_0.conda + sha256: 54cdcd3214313b62c2a8ee277e6f42150d9b748264c1b70d958bf735e420ef8d + md5: 7dc38adcbf71e6b38748e919e16e0dce + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libzlib >=1.3.2,<2.0a0 + license: blessing + purls: [] + size: 954962 + timestamp: 1777986471789 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h934c35e_19.conda + sha256: dff1058c76ec6b8759e41cefa2508162d00e4a5e6721aa68ec3fd10094e702dc + md5: 5794b3bdc38177caf969dabd3af08549 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc 15.2.0 he0feb66_19 + constrains: + - libstdcxx-ng ==15.2.0=*_19 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 5852044 + timestamp: 1778269036376 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.42-h5347b49_0.conda + sha256: bc1b08c92626c91500fd9f26f2c797f3eb153b627d53e9c13cd167f1e12b2829 + md5: 38ffe67b78c9d4de527be8315e5ada2c + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 40297 + timestamp: 1775052476770 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libuv-1.51.0-hb03c661_1.conda + sha256: c180f4124a889ac343fc59d15558e93667d894a966ec6fdb61da1604481be26b + md5: 0f03292cc56bf91a077a134ea8747118 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: MIT + license_family: MIT + purls: [] + size: 895108 + timestamp: 1753948278280 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda + sha256: 6ae68e0b86423ef188196fff6207ed0c8195dd84273cb5623b85aa08033a410c + md5: 5aa797f8787fe7a17d1b0821485b5adc + depends: + - libgcc-ng >=12 + license: LGPL-2.1-or-later + purls: [] + size: 100393 + timestamp: 1702724383534 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.2-h25fd6f3_2.conda + sha256: 55044c403570f0dc26e6364de4dc5368e5f3fc7ff103e867c487e2b5ab2bcda9 + md5: d87ff7921124eccd67248aa483c23fec + depends: + - __glibc >=2.17,<3.0.a0 + constrains: + - zlib 1.3.2 *_2 + license: Zlib + license_family: Other + purls: [] + size: 63629 + timestamp: 1774072609062 +- conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.3-py312h8a5da7c_1.conda + sha256: 5f3aad1f3a685ed0b591faad335957dbdb1b73abfd6fc731a0d42718e0653b33 + md5: 93a4752d42b12943a355b682ee43285b + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 + constrains: + - jinja2 >=3.0.0 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/markupsafe?source=hash-mapping + size: 26057 + timestamp: 1772445297924 +- conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.3-py314h67df5f8_1.conda + sha256: c279be85b59a62d5c52f5dd9a4cd43ebd08933809a8416c22c3131595607d4cf + md5: 9a17c4307d23318476d7fbf0fedc0cde + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + constrains: + - jinja2 >=3.0.0 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/markupsafe?source=hash-mapping + size: 27424 + timestamp: 1772445227915 +- conda: https://conda.anaconda.org/conda-forge/linux-64/msgspec-0.21.1-py312h4c3975b_0.conda + sha256: 25eb262c378a922eeed85c941ab7de2687ea842daed80521b861b7472b5a7f9a + md5: 5e07dc45b4458c19fdc085bd6c1aa51f + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/msgspec?source=hash-mapping + size: 218330 + timestamp: 1776337395109 +- conda: https://conda.anaconda.org/conda-forge/linux-64/msgspec-0.21.1-py314h5bd0f2a_0.conda + sha256: 52565ceea81e801c59dcaeaf5a9c77fba2fade445e67e0864fda50d4b944e15b + md5: 4a8ea416a56e58f012e445f7af2bbcc8 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/msgspec?source=hash-mapping + size: 220990 + timestamp: 1776337508167 +- conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.6-hdb14827_0.conda + sha256: fc89f74bbe362fb29fa3c037697a89bec140b346a2469a90f7936d1d7ea4d8a3 + md5: fc21868a1a5aacc937e7a18747acb8a5 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: X11 AND BSD-3-Clause + purls: [] + size: 918956 + timestamp: 1777422145199 +- conda: https://conda.anaconda.org/conda-forge/linux-64/nodejs-25.8.2-he4ff34a_0.conda + sha256: d1a673d1418d9e956b6e4e46c23e72a511c5c1d45dc5519c947457427036d5e2 + md5: baffb1570b3918c784d4490babc52fbf + depends: + - libgcc >=14 + - libstdcxx >=14 + - __glibc >=2.28,<3.0.a0 + - libnghttp2 >=1.68.1,<2.0a0 + - libuv >=1.51.0,<2.0a0 + - c-ares >=1.34.6,<2.0a0 + - openssl >=3.5.5,<4.0a0 + - libsqlite >=3.52.0,<4.0a0 + - icu >=78.3,<79.0a0 + - libzlib >=1.3.2,<2.0a0 + - libabseil >=20260107.1,<20260108.0a0 + - libabseil * cxx17* + - zstd >=1.5.7,<1.6.0a0 + - libbrotlicommon >=1.2.0,<1.3.0a0 + - libbrotlienc >=1.2.0,<1.3.0a0 + - libbrotlidec >=1.2.0,<1.3.0a0 + license: MIT + license_family: MIT + purls: [] + size: 18829340 + timestamp: 1774514313036 +- conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.2-h35e630c_0.conda + sha256: c0ef482280e38c71a08ad6d71448194b719630345b0c9c60744a2010e8a8e0cb + md5: da1b85b6a87e141f5140bb9924cecab0 + depends: + - __glibc >=2.17,<3.0.a0 + - ca-certificates + - libgcc >=14 + license: Apache-2.0 + license_family: Apache + purls: [] + size: 3167099 + timestamp: 1775587756857 +- conda: https://conda.anaconda.org/conda-forge/linux-64/psutil-7.2.2-py312h5253ce2_0.conda + sha256: d834fd656133c9e4eaf63ffe9a117c7d0917d86d89f7d64073f4e3a0020bd8a7 + md5: dd94c506b119130aef5a9382aed648e7 + depends: + - python + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - python_abi 3.12.* *_cp312 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/psutil?source=hash-mapping + size: 225545 + timestamp: 1769678155334 +- conda: https://conda.anaconda.org/conda-forge/linux-64/psutil-7.2.2-py314h0f05182_0.conda + sha256: f15574ed6c8c8ed8c15a0c5a00102b1efe8b867c0bd286b498cd98d95bd69ae5 + md5: 4f225a966cfee267a79c5cb6382bd121 + depends: + - python + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - python_abi 3.14.* *_cp314 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/psutil?source=hash-mapping + size: 231303 + timestamp: 1769678156552 +- conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.12.13-hd63d673_0_cpython.conda + sha256: a44655c1c3e1d43ed8704890a91e12afd68130414ea2c0872e154e5633a13d7e + md5: 7eccb41177e15cc672e1babe9056018e + depends: + - __glibc >=2.17,<3.0.a0 + - bzip2 >=1.0.8,<2.0a0 + - ld_impl_linux-64 >=2.36.1 + - libexpat >=2.7.4,<3.0a0 + - libffi >=3.5.2,<3.6.0a0 + - libgcc >=14 + - liblzma >=5.8.2,<6.0a0 + - libnsl >=2.0.1,<2.1.0a0 + - libsqlite >=3.51.2,<4.0a0 + - libuuid >=2.41.3,<3.0a0 + - libxcrypt >=4.4.36 + - libzlib >=1.3.1,<2.0a0 + - ncurses >=6.5,<7.0a0 + - openssl >=3.5.5,<4.0a0 + - readline >=8.3,<9.0a0 + - tk >=8.6.13,<8.7.0a0 + - tzdata + constrains: + - python_abi 3.12.* *_cp312 + license: Python-2.0 + purls: [] + size: 31608571 + timestamp: 1772730708989 +- conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.14.4-habeac84_100_cp314.conda + build_number: 100 + sha256: dec247c5badc811baa34d6085df9d0465535883cf745e22e8d79092ad54a3a7b + md5: a443f87920815d41bfe611296e507995 + depends: + - __glibc >=2.17,<3.0.a0 + - bzip2 >=1.0.8,<2.0a0 + - ld_impl_linux-64 >=2.36.1 + - libexpat >=2.7.5,<3.0a0 + - libffi >=3.5.2,<3.6.0a0 + - libgcc >=14 + - liblzma >=5.8.2,<6.0a0 + - libmpdec >=4.0.0,<5.0a0 + - libsqlite >=3.52.0,<4.0a0 + - libuuid >=2.42,<3.0a0 + - libzlib >=1.3.2,<2.0a0 + - ncurses >=6.5,<7.0a0 + - openssl >=3.5.6,<4.0a0 + - python_abi 3.14.* *_cp314 + - readline >=8.3,<9.0a0 + - tk >=8.6.13,<8.7.0a0 + - tzdata + - zstd >=1.5.7,<1.6.0a0 + license: Python-2.0 + purls: [] + size: 36705460 + timestamp: 1775614357822 + python_site_packages_path: lib/python3.14/site-packages +- conda: https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.3-py312h8a5da7c_1.conda + sha256: cb142bfd92f6e55749365ddc244294fa7b64db6d08c45b018ff1c658907bfcbf + md5: 15878599a87992e44c059731771591cb + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 + - yaml >=0.2.5,<0.3.0a0 + license: MIT + license_family: MIT + purls: + - pkg:pypi/pyyaml?source=hash-mapping + size: 198293 + timestamp: 1770223620706 +- conda: https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.3-py314h67df5f8_1.conda + sha256: b318fb070c7a1f89980ef124b80a0b5ccf3928143708a85e0053cde0169c699d + md5: 2035f68f96be30dc60a5dfd7452c7941 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + - yaml >=0.2.5,<0.3.0a0 + license: MIT + license_family: MIT + purls: + - pkg:pypi/pyyaml?source=hash-mapping + size: 202391 + timestamp: 1770223462836 +- conda: https://conda.anaconda.org/conda-forge/linux-64/pyzmq-27.1.0-py312hda471dd_2.conda + noarch: python + sha256: be66c1f85c3b48137200d62c12d918f4f8ad329423daef04fed292818efd3c28 + md5: 082985717303dab433c976986c674b35 + depends: + - python + - libgcc >=14 + - libstdcxx >=14 + - __glibc >=2.17,<3.0.a0 + - zeromq >=4.3.5,<4.4.0a0 + - _python_abi3_support 1.* + - cpython >=3.12 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/pyzmq?source=hash-mapping + size: 211567 + timestamp: 1771716961404 +- conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.3-h853b02a_0.conda + sha256: 12ffde5a6f958e285aa22c191ca01bbd3d6e710aa852e00618fa6ddc59149002 + md5: d7d95fc8287ea7bf33e0e7116d2b95ec + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - ncurses >=6.5,<7.0a0 + license: GPL-3.0-only + license_family: GPL + purls: [] + size: 345073 + timestamp: 1765813471974 +- conda: https://conda.anaconda.org/conda-forge/linux-64/rpds-py-0.30.0-py312h868fb18_0.conda + sha256: 62f46e85caaba30b459da7dfcf3e5488ca24fd11675c33ce4367163ab191a42c + md5: 3ffc5a3572db8751c2f15bacf6a0e937 + depends: + - python + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - python_abi 3.12.* *_cp312 + constrains: + - __glibc >=2.17 + license: MIT + license_family: MIT + purls: + - pkg:pypi/rpds-py?source=hash-mapping + size: 383750 + timestamp: 1764543174231 +- conda: https://conda.anaconda.org/conda-forge/linux-64/rpds-py-0.30.0-py314h2e6c369_0.conda + sha256: e53b0cbf3b324eaa03ca1fe1a688fdf4ab42cea9c25270b0a7307d8aaaa4f446 + md5: c1c368b5437b0d1a68f372ccf01cb133 + depends: + - python + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - python_abi 3.14.* *_cp314 + constrains: + - __glibc >=2.17 + license: MIT + license_family: MIT + purls: + - pkg:pypi/rpds-py?source=hash-mapping + size: 376121 + timestamp: 1764543122774 +- conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h366c992_103.conda + sha256: cafeec44494f842ffeca27e9c8b0c27ed714f93ac77ddadc6aaf726b5554ebac + md5: cffd3bdd58090148f4cfcd831f4b26ab + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libzlib >=1.3.1,<2.0a0 + constrains: + - xorg-libx11 >=1.8.12,<2.0a0 + license: TCL + license_family: BSD + purls: [] + size: 3301196 + timestamp: 1769460227866 +- conda: https://conda.anaconda.org/conda-forge/linux-64/tornado-6.5.5-py312h4c3975b_0.conda + sha256: 4629b1c9139858fb08bb357df917ffc12e4d284c57ff389806bb3ae476ef4e0a + md5: 2b37798adbc54fd9e591d24679d2133a + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 + license: Apache-2.0 + license_family: Apache + purls: + - pkg:pypi/tornado?source=hash-mapping + size: 859665 + timestamp: 1774358032165 +- conda: https://conda.anaconda.org/conda-forge/linux-64/tornado-6.5.5-py314h5bd0f2a_0.conda + sha256: ed8d06093ff530a2dae9ed1e51eb6f908fbfd171e8b62f4eae782d67b420be5a + md5: dc1ff1e915ab35a06b6fa61efae73ab5 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + license: Apache-2.0 + license_family: Apache + purls: + - pkg:pypi/tornado?source=hash-mapping + size: 912476 + timestamp: 1774358032579 +- conda: https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h280c20c_3.conda + sha256: 6d9ea2f731e284e9316d95fa61869fe7bbba33df7929f82693c121022810f4ad + md5: a77f85f77be52ff59391544bfe73390a + depends: + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + license: MIT + license_family: MIT + purls: [] + size: 85189 + timestamp: 1753484064210 +- conda: https://conda.anaconda.org/conda-forge/linux-64/zeromq-4.3.5-h41580af_10.conda + sha256: 325d370b28e2b9cc1f765c5b4cdb394c91a5d958fbd15da1a14607a28fee09f6 + md5: 755b096086851e1193f3b10347415d7c + depends: + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - libstdcxx >=14 + - krb5 >=1.22.2,<1.23.0a0 + - libsodium >=1.0.21,<1.0.22.0a0 + license: MPL-2.0 + license_family: MOZILLA + purls: [] + size: 311150 + timestamp: 1772476812121 +- conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda + sha256: 68f0206ca6e98fea941e5717cec780ed2873ffabc0e1ed34428c061e2c6268c7 + md5: 4a13eeac0b5c8e5b8ab496e6c4ddd829 + depends: + - __glibc >=2.17,<3.0.a0 + - libzlib >=1.3.1,<2.0a0 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 601375 + timestamp: 1764777111296 +- conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda + sha256: a3967b937b9abf0f2a99f3173fa4630293979bd1644709d89580e7c62a544661 + md5: aaa2a381ccc56eac91d63b6c1240312f + depends: + - cpython + - python-gil + license: MIT + license_family: MIT + purls: [] + size: 8191 + timestamp: 1744137672556 +- conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.13.0-pyhcf101f3_0.conda + sha256: f09aed24661cd45ba54a43772504f05c0698248734f9ae8cd289d314ac89707e + md5: af2df4b9108808da3dc76710fe50eae2 + depends: + - exceptiongroup >=1.0.2 + - idna >=2.8 + - python >=3.10 + - typing_extensions >=4.5 + - python + constrains: + - trio >=0.32.0 + - uvloop >=0.22.1 + - winloop >=0.2.3 + license: MIT + license_family: MIT + purls: + - pkg:pypi/anyio?source=hash-mapping + size: 146764 + timestamp: 1774359453364 +- conda: https://conda.anaconda.org/conda-forge/noarch/appnope-0.1.4-pyhd8ed1ab_1.conda + sha256: 8f032b140ea4159806e4969a68b4a3c0a7cab1ad936eb958a2b5ffe5335e19bf + md5: 54898d0f524c9dee622d44bbb081a8ab + depends: + - python >=3.9 + license: BSD-2-Clause + license_family: BSD + purls: + - pkg:pypi/appnope?source=hash-mapping + size: 10076 + timestamp: 1733332433806 +- conda: https://conda.anaconda.org/conda-forge/noarch/argon2-cffi-25.1.0-pyhd8ed1ab_0.conda + sha256: bea62005badcb98b1ae1796ec5d70ea0fc9539e7d59708ac4e7d41e2f4bb0bad + md5: 8ac12aff0860280ee0cff7fa2cf63f3b + depends: + - argon2-cffi-bindings + - python >=3.9 + - typing-extensions + constrains: + - argon2_cffi ==999 + license: MIT + license_family: MIT + purls: + - pkg:pypi/argon2-cffi?source=hash-mapping + size: 18715 + timestamp: 1749017288144 +- conda: https://conda.anaconda.org/conda-forge/noarch/arrow-1.4.0-pyhcf101f3_0.conda + sha256: 792da8131b1b53ff667bd6fc617ea9087b570305ccb9913deb36b8e12b3b5141 + md5: 85c4f19f377424eafc4ed7911b291642 + depends: + - python >=3.10 + - python-dateutil >=2.7.0 + - python-tzdata + - python + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/arrow?source=hash-mapping + size: 113854 + timestamp: 1760831179410 +- conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.1-pyhd8ed1ab_0.conda + sha256: ee4da0f3fe9d59439798ee399ef3e482791e48784873d546e706d0935f9ff010 + md5: 9673a61a297b00016442e022d689faa6 + depends: + - python >=3.10 + constrains: + - astroid >=2,<5 + license: Apache-2.0 + license_family: Apache + purls: + - pkg:pypi/asttokens?source=hash-mapping + size: 28797 + timestamp: 1763410017955 +- conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.3.0-pyhcf101f3_0.conda + sha256: ea8486637cfb89dc26dc9559921640cd1d5fd37e5e02c33d85c94572139f2efe + md5: b85e84cb64c762569cc1a760c2327e0a + depends: + - python >=3.10 + - typing_extensions >=4.0.0 + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/async-lru?source=hash-mapping + size: 22949 + timestamp: 1773926359134 +- conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda + sha256: 1b6124230bb4e571b1b9401537ecff575b7b109cc3a21ee019f65e083b8399ab + md5: c6b0543676ecb1fb2d7643941fe375f2 + depends: + - python >=3.10 + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/attrs?source=hash-mapping + size: 64927 + timestamp: 1773935801332 +- conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.18.0-pyhcf101f3_1.conda + sha256: a14a9ad02101aab25570543a59c5193043b73dc311a25650134ed9e6cb691770 + md5: f1976ce927373500cc19d3c0b2c85177 + depends: + - python >=3.10 + - python + constrains: + - pytz >=2015.7 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/babel?source=hash-mapping + size: 7684321 + timestamp: 1772555330347 +- conda: https://conda.anaconda.org/conda-forge/noarch/backports.zstd-1.5.0-py314h680f03e_0.conda + noarch: generic + sha256: a1c97297e867776760489537bc5ae36fa83a154be30e3b79385a39ca4cb058fe + md5: 1133126d840e75287d83947be3fc3e71 + depends: + - python >=3.14 + license: BSD-3-Clause AND MIT AND EPL-2.0 + purls: + - pkg:pypi/backports-zstd?source=compressed-mapping + size: 7533 + timestamp: 1778594057496 +- conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.14.3-pyha770c72_0.conda + sha256: bf1e71c3c0a5b024e44ff928225a0874fc3c3356ec1a0b6fe719108e6d1288f6 + md5: 5267bef8efea4127aacd1f4e1f149b6e + depends: + - python >=3.10 + - soupsieve >=1.2 + - typing-extensions + license: MIT + license_family: MIT + purls: + - pkg:pypi/beautifulsoup4?source=hash-mapping + size: 90399 + timestamp: 1764520638652 +- conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.3.0-pyhcf101f3_1.conda + sha256: f8ff1f98423674278964a46c93a1766f9e91960d44efd91c6c3ed56a33813f46 + md5: 7c5ebdc286220e8021bf55e6384acd67 + depends: + - python >=3.10 + - webencodings + - python + constrains: + - tinycss2 >=1.1.0,<1.5 + license: Apache-2.0 AND MIT + purls: + - pkg:pypi/bleach?source=hash-mapping + size: 142008 + timestamp: 1770719370680 +- conda: https://conda.anaconda.org/conda-forge/noarch/bleach-with-css-6.3.0-hbca2aae_1.conda + sha256: 7c07a865e5e4cca233cc4e0eb3f0f5ff6c90776461687b4fb0b1764133e1fd61 + md5: f11a319b9700b203aa14c295858782b6 + depends: + - bleach ==6.3.0 pyhcf101f3_1 + - tinycss2 + license: Apache-2.0 AND MIT + purls: [] + size: 4409 + timestamp: 1770719370682 +- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.4.22-h4c7d964_0.conda + sha256: 6f4ff81534c19e76acf52fcabf4a258088a932b8f1ac56e9a59e98f6051f8e46 + md5: 56fb2c6c73efc627b40c77d14caecfba + depends: + - __win + license: ISC + purls: [] + size: 131388 + timestamp: 1776865633471 +- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.4.22-hbd8a1cb_0.conda + sha256: c9dbcc8039a52023660d6d1bbf87594a93dd69c6ac5a2a44323af2c92976728d + md5: e18ad67cf881dcadee8b8d9e2f8e5f73 + depends: + - __unix + license: ISC + purls: [] + size: 131039 + timestamp: 1776865545798 +- conda: https://conda.anaconda.org/conda-forge/noarch/cached-property-1.5.2-hd8ed1ab_1.tar.bz2 + noarch: python + sha256: 561e6660f26c35d137ee150187d89767c988413c978e1b712d53f27ddf70ea17 + md5: 9b347a7ec10940d3f7941ff6c460b551 + depends: + - cached_property >=1.5.2,<1.5.3.0a0 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 4134 + timestamp: 1615209571450 +- conda: https://conda.anaconda.org/conda-forge/noarch/cached_property-1.5.2-pyha770c72_1.tar.bz2 + sha256: 6dbf7a5070cc43d90a1e4c2ec0c541c69d8e30a0e25f50ce9f6e4a432e42c5d7 + md5: 576d629e47797577ab0f1b351297ef4a + depends: + - python >=3.6 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/cached-property?source=hash-mapping + size: 11065 + timestamp: 1615209567874 +- conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2026.4.22-pyhd8ed1ab_0.conda + sha256: 989db6e5957c4b44fa600c68c681ec2f36a55e48f7c7f1c073d5e91caa8cd878 + md5: 929471569c93acefb30282a22060dcd5 + depends: + - python >=3.10 + license: ISC + purls: + - pkg:pypi/certifi?source=hash-mapping + size: 135656 + timestamp: 1776866680878 +- conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.7-pyhd8ed1ab_0.conda + sha256: 3f9483d62ce24ecd063f8a5a714448445dc8d9e201147c46699fc0033e824457 + md5: a9167b9571f3baa9d448faa2139d1089 + depends: + - python >=3.10 + license: MIT + license_family: MIT + purls: + - pkg:pypi/charset-normalizer?source=hash-mapping + size: 58872 + timestamp: 1775127203018 +- conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda + sha256: ab29d57dc70786c1269633ba3dff20288b81664d3ff8d21af995742e2bb03287 + md5: 962b9857ee8e7018c22f2776ffa0b2d7 + depends: + - python >=3.9 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/colorama?source=hash-mapping + size: 27011 + timestamp: 1733218222191 +- conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.3-pyhe01879c_0.conda + sha256: 576a44729314ad9e4e5ebe055fbf48beb8116b60e58f9070278985b2b634f212 + md5: 2da13f2b299d8e1995bafbbe9689a2f7 + depends: + - python >=3.9 + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/comm?source=hash-mapping + size: 14690 + timestamp: 1753453984907 +- conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.12.13-py312hd8ed1ab_0.conda + noarch: generic + sha256: d3e9bbd7340199527f28bbacf947702368f31de60c433a16446767d3c6aaf6fe + md5: f54c1ffb8ecedb85a8b7fcde3a187212 + depends: + - python >=3.12,<3.13.0a0 + - python_abi * *_cp312 + license: Python-2.0 + purls: [] + size: 46463 + timestamp: 1772728929620 +- conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.14.4-py314hd8ed1ab_100.conda + noarch: generic + sha256: 40dc224f2b718e5f034efd2332bc315a719063235f63673468d26a24770094ee + md5: f111d4cfaf1fe9496f386bc98ae94452 + depends: + - python >=3.14,<3.15.0a0 + - python_abi * *_cp314 + license: Python-2.0 + purls: [] + size: 49809 + timestamp: 1775614256655 +- conda: https://conda.anaconda.org/conda-forge/noarch/decorator-5.2.1-pyhd8ed1ab_0.conda + sha256: c17c6b9937c08ad63cb20a26f403a3234088e57d4455600974a0ce865cb14017 + md5: 9ce473d1d1be1cc3810856a48b3fab32 + depends: + - python >=3.9 + license: BSD-2-Clause + license_family: BSD + purls: + - pkg:pypi/decorator?source=hash-mapping + size: 14129 + timestamp: 1740385067843 +- conda: https://conda.anaconda.org/conda-forge/noarch/defusedxml-0.7.1-pyhd8ed1ab_0.tar.bz2 + sha256: 9717a059677553562a8f38ff07f3b9f61727bd614f505658b0a5ecbcf8df89be + md5: 961b3a227b437d82ad7054484cfa71b2 + depends: + - python >=3.6 + license: PSF-2.0 + license_family: PSF + purls: + - pkg:pypi/defusedxml?source=hash-mapping + size: 24062 + timestamp: 1615232388757 +- conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda + sha256: ee6cf346d017d954255bbcbdb424cddea4d14e4ed7e9813e429db1d795d01144 + md5: 8e662bd460bda79b1ea39194e3c4c9ab + depends: + - python >=3.10 + - typing_extensions >=4.6.0 + license: MIT and PSF-2.0 + purls: + - pkg:pypi/exceptiongroup?source=hash-mapping + size: 21333 + timestamp: 1763918099466 +- conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.1-pyhd8ed1ab_0.conda + sha256: 210c8165a58fdbf16e626aac93cc4c14dbd551a01d1516be5ecad795d2422cad + md5: ff9efb7f7469aed3c4a8106ffa29593c + depends: + - python >=3.10 + license: MIT + license_family: MIT + purls: + - pkg:pypi/executing?source=hash-mapping + size: 30753 + timestamp: 1756729456476 +- conda: https://conda.anaconda.org/conda-forge/noarch/fqdn-1.5.1-pyhd8ed1ab_1.conda + sha256: 2509992ec2fd38ab27c7cdb42cf6cadc566a1cc0d1021a2673475d9fa87c6276 + md5: d3549fd50d450b6d9e7dddff25dd2110 + depends: + - cached-property >=1.3.0 + - python >=3.9,<4 + license: MPL-2.0 + license_family: MOZILLA + purls: + - pkg:pypi/fqdn?source=hash-mapping + size: 16705 + timestamp: 1733327494780 +- conda: https://conda.anaconda.org/conda-forge/noarch/h11-0.16.0-pyhcf101f3_1.conda + sha256: 96cac6573fd35ae151f4d6979bab6fbc90cb6b1fb99054ba19eb075da9822fcb + md5: b8993c19b0c32a2f7b66cbb58ca27069 + depends: + - python >=3.10 + - typing_extensions + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/h11?source=hash-mapping + size: 39069 + timestamp: 1767729720872 +- conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda + sha256: 84c64443368f84b600bfecc529a1194a3b14c3656ee2e832d15a20e0329b6da3 + md5: 164fc43f0b53b6e3a7bc7dce5e4f1dc9 + depends: + - python >=3.10 + - hyperframe >=6.1,<7 + - hpack >=4.1,<5 + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/h2?source=hash-mapping + size: 95967 + timestamp: 1756364871835 +- conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda + sha256: 6ad78a180576c706aabeb5b4c8ceb97c0cb25f1e112d76495bff23e3779948ba + md5: 0a802cb9888dd14eeefc611f05c40b6e + depends: + - python >=3.9 + license: MIT + license_family: MIT + purls: + - pkg:pypi/hpack?source=hash-mapping + size: 30731 + timestamp: 1737618390337 +- conda: https://conda.anaconda.org/conda-forge/noarch/httpcore-1.0.9-pyh29332c3_0.conda + sha256: 04d49cb3c42714ce533a8553986e1642d0549a05dc5cc48e0d43ff5be6679a5b + md5: 4f14640d58e2cc0aa0819d9d8ba125bb + depends: + - python >=3.9 + - h11 >=0.16 + - h2 >=3,<5 + - sniffio 1.* + - anyio >=4.0,<5.0 + - certifi + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/httpcore?source=hash-mapping + size: 49483 + timestamp: 1745602916758 +- conda: https://conda.anaconda.org/conda-forge/noarch/httpx-0.28.1-pyhd8ed1ab_0.conda + sha256: cd0f1de3697b252df95f98383e9edb1d00386bfdd03fdf607fa42fe5fcb09950 + md5: d6989ead454181f4f9bc987d3dc4e285 + depends: + - anyio + - certifi + - httpcore 1.* + - idna + - python >=3.9 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/httpx?source=hash-mapping + size: 63082 + timestamp: 1733663449209 +- conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda + sha256: 77af6f5fe8b62ca07d09ac60127a30d9069fdc3c68d6b256754d0ffb1f7779f8 + md5: 8e6923fc12f1fe8f8c4e5c9f343256ac + depends: + - python >=3.9 + license: MIT + license_family: MIT + purls: + - pkg:pypi/hyperframe?source=hash-mapping + size: 17397 + timestamp: 1737618427549 +- conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.13-pyhcf101f3_0.conda + sha256: 9ab620e6f64bb67737bd7bc1ad6f480770124e304c6710617aba7fe60b089f48 + md5: fb7130c190f9b4ec91219840a05ba3ac + depends: + - python >=3.10 + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/idna?source=hash-mapping + size: 59038 + timestamp: 1776947141407 +- conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.8.0-pyhcf101f3_0.conda + sha256: 82ab2a0d91ca1e7e63ab6a4939356667ef683905dea631bc2121aa534d347b16 + md5: 080594bf4493e6bae2607e65390c520a + depends: + - python >=3.10 + - zipp >=3.20 + - python + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/importlib-metadata?source=hash-mapping + size: 34387 + timestamp: 1773931568510 +- conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.2.0-pyh5552912_1.conda + sha256: 5c1f3e874adaf603449f2b135d48f168c5d510088c78c229bda0431268b43b27 + md5: 4b53d436f3fbc02ce3eeaf8ae9bebe01 + depends: + - appnope + - __osx + - comm >=0.1.1 + - debugpy >=1.6.5 + - ipython >=7.23.1 + - jupyter_client >=8.8.0 + - jupyter_core >=5.1,!=6.0.* + - matplotlib-inline >=0.1 + - nest-asyncio >=1.4 + - packaging >=22 + - psutil >=5.7 + - python >=3.10 + - pyzmq >=25 + - tornado >=6.4.1 + - traitlets >=5.4.0 + - python + constrains: + - appnope >=0.1.2 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/ipykernel?source=hash-mapping + size: 132260 + timestamp: 1770566135697 +- conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.2.0-pyh6dadd2b_1.conda + sha256: 9cdadaeef5abadca4113f92f5589db19f8b7df5e1b81cb0225f7024a3aedefa3 + md5: b3a7d5842f857414d9ae831a799444dd + depends: + - __win + - comm >=0.1.1 + - debugpy >=1.6.5 + - ipython >=7.23.1 + - jupyter_client >=8.8.0 + - jupyter_core >=5.1,!=6.0.* + - matplotlib-inline >=0.1 + - nest-asyncio >=1.4 + - packaging >=22 + - psutil >=5.7 + - python >=3.10 + - pyzmq >=25 + - tornado >=6.4.1 + - traitlets >=5.4.0 + - python + constrains: + - appnope >=0.1.2 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/ipykernel?source=hash-mapping + size: 132382 + timestamp: 1770566174387 +- conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.2.0-pyha191276_1.conda + sha256: b77ed58eb235e5ad80e742b03caeed4bbc2a2ef064cb9a2deee3b75dfae91b2a + md5: 8b267f517b81c13594ed68d646fd5dcb + depends: + - __linux + - comm >=0.1.1 + - debugpy >=1.6.5 + - ipython >=7.23.1 + - jupyter_client >=8.8.0 + - jupyter_core >=5.1,!=6.0.* + - matplotlib-inline >=0.1 + - nest-asyncio >=1.4 + - packaging >=22 + - psutil >=5.7 + - python >=3.10 + - pyzmq >=25 + - tornado >=6.4.1 + - traitlets >=5.4.0 + - python + constrains: + - appnope >=0.1.2 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/ipykernel?source=hash-mapping + size: 133644 + timestamp: 1770566133040 +- conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.13.0-pyh53cf698_0.conda + sha256: a0af49948a1842dfd15a0b0b2fd56c94ddbd07e07a6c8b4bc70d43015eafaff0 + md5: 73e9657cd19605740d21efb14d8d0cb9 + depends: + - __unix + - decorator >=5.1.0 + - ipython_pygments_lexers >=1.0.0 + - jedi >=0.18.2 + - matplotlib-inline >=0.1.6 + - prompt-toolkit >=3.0.41,<3.1.0 + - psutil >=7 + - pygments >=2.14.0 + - python >=3.11 + - stack_data >=0.6.0 + - traitlets >=5.13.0 + - typing_extensions >=4.6 + - pexpect >4.6 + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/ipython?source=hash-mapping + size: 651632 + timestamp: 1777038396606 +- conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.13.0-pyhe2676ad_0.conda + sha256: f252ec33597115ff21cbb31051f6f9be34ca36cbbbf3d266b597660d8d8edde9 + md5: 5631ab99e902463d9dd4221e5b4eab6d + depends: + - __win + - decorator >=5.1.0 + - ipython_pygments_lexers >=1.0.0 + - jedi >=0.18.2 + - matplotlib-inline >=0.1.6 + - prompt-toolkit >=3.0.41,<3.1.0 + - psutil >=7 + - pygments >=2.14.0 + - python >=3.11 + - stack_data >=0.6.0 + - traitlets >=5.13.0 + - typing_extensions >=4.6 + - colorama >=0.4.4 + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/ipython?source=hash-mapping + size: 650593 + timestamp: 1777038425499 +- conda: https://conda.anaconda.org/conda-forge/noarch/ipython_pygments_lexers-1.1.1-pyhd8ed1ab_0.conda + sha256: 894682a42a7d659ae12878dbcb274516a7031bbea9104e92f8e88c1f2765a104 + md5: bd80ba060603cc228d9d81c257093119 + depends: + - pygments + - python >=3.9 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/ipython-pygments-lexers?source=hash-mapping + size: 13993 + timestamp: 1737123723464 +- conda: https://conda.anaconda.org/conda-forge/noarch/isoduration-20.11.0-pyhd8ed1ab_1.conda + sha256: 08e838d29c134a7684bca0468401d26840f41c92267c4126d7b43a6b533b0aed + md5: 0b0154421989637d424ccf0f104be51a + depends: + - arrow >=0.15.0 + - python >=3.9 + license: MIT + license_family: MIT + purls: + - pkg:pypi/isoduration?source=hash-mapping + size: 19832 + timestamp: 1733493720346 +- conda: https://conda.anaconda.org/conda-forge/noarch/jedi-0.19.2-pyhd8ed1ab_1.conda + sha256: 92c4d217e2dc68983f724aa983cca5464dcb929c566627b26a2511159667dba8 + md5: a4f4c5dc9b80bc50e0d3dc4e6e8f1bd9 + depends: + - parso >=0.8.3,<0.9.0 + - python >=3.9 + license: Apache-2.0 AND MIT + purls: + - pkg:pypi/jedi?source=hash-mapping + size: 843646 + timestamp: 1733300981994 +- conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda + sha256: fc9ca7348a4f25fed2079f2153ecdcf5f9cf2a0bc36c4172420ca09e1849df7b + md5: 04558c96691bed63104678757beb4f8d + depends: + - markupsafe >=2.0 + - python >=3.10 + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/jinja2?source=hash-mapping + size: 120685 + timestamp: 1764517220861 +- conda: https://conda.anaconda.org/conda-forge/noarch/json5-0.14.0-pyhd8ed1ab_0.conda + sha256: 9daa95bd164c8fa23b3ab196e906ef806141d749eddce2a08baa064f722d25fa + md5: 1269891272187518a0a75c286f7d0bbf + depends: + - python >=3.10 + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/json5?source=hash-mapping + size: 34731 + timestamp: 1774655440045 +- conda: https://conda.anaconda.org/conda-forge/noarch/jsonpointer-3.1.1-pyhcf101f3_0.conda + sha256: a3d10301b6ff399ba1f3d39e443664804a3d28315a4fb81e745b6817845f70ae + md5: 89bf346df77603055d3c8fe5811691e6 + depends: + - python >=3.10 + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/jsonpointer?source=hash-mapping + size: 14190 + timestamp: 1774311356147 +- conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.26.0-pyhcf101f3_0.conda + sha256: db973a37d75db8e19b5f44bbbdaead0c68dde745407f281e2a7fe4db74ec51d7 + md5: ada41c863af263cc4c5fcbaff7c3e4dc + depends: + - attrs >=22.2.0 + - jsonschema-specifications >=2023.3.6 + - python >=3.10 + - referencing >=0.28.4 + - rpds-py >=0.25.0 + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/jsonschema?source=hash-mapping + size: 82356 + timestamp: 1767839954256 +- conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.9.1-pyhcf101f3_0.conda + sha256: 0a4f3b132f0faca10c89fdf3b60e15abb62ded6fa80aebfc007d05965192aa04 + md5: 439cd0f567d697b20a8f45cb70a1005a + depends: + - python >=3.10 + - referencing >=0.31.0 + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/jsonschema-specifications?source=hash-mapping + size: 19236 + timestamp: 1757335715225 +- conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-with-format-nongpl-4.26.0-hcf101f3_0.conda + sha256: 6886fc61e4e4edd38fd38729976b134e8bd2143f7fce56cc80d7ac7bac99bce1 + md5: 8368d58342d0825f0843dc6acdd0c483 + depends: + - jsonschema >=4.26.0,<4.26.1.0a0 + - fqdn + - idna + - isoduration + - jsonpointer >1.13 + - rfc3339-validator + - rfc3986-validator >0.1.0 + - rfc3987-syntax >=1.1.0 + - uri-template + - webcolors >=24.6.0 + license: MIT + license_family: MIT + purls: [] + size: 4740 + timestamp: 1767839954258 +- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter-lsp-2.3.1-pyhcf101f3_0.conda + sha256: 3766e2ae59641c172cec8a821528bfa6bf9543ffaaeb8b358bfd5259dcf18e4e + md5: 0c3b465ceee138b9c39279cc02e5c4a0 + depends: + - importlib-metadata >=4.8.3 + - jupyter_server >=1.1.2 + - python >=3.10 + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/jupyter-lsp?source=hash-mapping + size: 61633 + timestamp: 1775136333147 +- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.8.0-pyhcf101f3_0.conda + sha256: e402bd119720862a33229624ec23645916a7d47f30e1711a4af9e005162b84f3 + md5: 8a3d6d0523f66cf004e563a50d9392b3 + depends: + - jupyter_core >=5.1 + - python >=3.10 + - python-dateutil >=2.8.2 + - pyzmq >=25.0 + - tornado >=6.4.1 + - traitlets >=5.3 + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/jupyter-client?source=hash-mapping + size: 112785 + timestamp: 1767954655912 +- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.9.1-pyh6dadd2b_0.conda + sha256: ed709a6c25b731e01563521ef338b93986cd14b5bc17f35e9382000864872ccc + md5: a8db462b01221e9f5135be466faeb3e0 + depends: + - __win + - pywin32 + - platformdirs >=2.5 + - python >=3.10 + - traitlets >=5.3 + - python + constrains: + - pywin32 >=300 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/jupyter-core?source=hash-mapping + size: 64679 + timestamp: 1760643889625 +- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.9.1-pyhc90fa1f_0.conda + sha256: 1d34b80e5bfcd5323f104dbf99a2aafc0e5d823019d626d0dce5d3d356a2a52a + md5: b38fe4e78ee75def7e599843ef4c1ab0 + depends: + - __unix + - python + - platformdirs >=2.5 + - python >=3.10 + - traitlets >=5.3 + - python + constrains: + - pywin32 >=300 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/jupyter-core?source=hash-mapping + size: 65503 + timestamp: 1760643864586 +- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_events-0.12.1-pyhcf101f3_0.conda + sha256: c7edb5682c6316a95ad781dccb1b6589cd2ec0bf94f23c21152974eb0363b5d7 + md5: bf42ee94c750c0b2e7e998b79ac299ea + depends: + - jsonschema-with-format-nongpl >=4.18.0 + - packaging + - python >=3.10 + - python-json-logger >=2.0.4 + - pyyaml >=5.3 + - referencing + - rfc3339-validator + - rfc3986-validator >=0.1.1 + - traitlets >=5.3 + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/jupyter-events?source=hash-mapping + size: 24002 + timestamp: 1776861872237 +- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.18.2-pyhcf101f3_0.conda + sha256: 04fb8ea7749f67abaf76df6257bf86688e1389ceed55eb4fb0176fd2e882dbd6 + md5: 5ee7945accf0f215ddd6055d25d7cd83 + depends: + - anyio >=3.1.0 + - argon2-cffi >=21.1 + - jinja2 >=3.0.3 + - jupyter_client >=7.4.4 + - jupyter_core >=4.12,!=5.0.* + - jupyter_events >=0.11.0 + - jupyter_server_terminals >=0.4.4 + - nbconvert-core >=6.4.4 + - nbformat >=5.3.0 + - overrides >=5.0 + - packaging >=22.0 + - prometheus_client >=0.9 + - python >=3.10 + - pyzmq >=24 + - send2trash >=1.8.2 + - terminado >=0.8.3 + - tornado >=6.2.0 + - traitlets >=5.6.0 + - websocket-client >=1.7 + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/jupyter-server?source=hash-mapping + size: 360522 + timestamp: 1778060967727 +- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server_terminals-0.5.4-pyhcf101f3_0.conda + sha256: 5eda79ed9f53f590031d29346abd183051263227dd9ee667b5ca1133ce297654 + md5: 7b8bace4943e0dc345fc45938826f2b8 + depends: + - python >=3.10 + - terminado >=0.8.3 + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/jupyter-server-terminals?source=hash-mapping + size: 22052 + timestamp: 1768574057200 +- conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.5.7-pyhd8ed1ab_0.conda + sha256: b85befad5ba1f50c0cc042a2ffb26441d13ffc2f18572dc20d3541476da0c7b9 + md5: 2ffe77234070324e763a6eddabb5f467 + depends: + - async-lru >=1.0.0 + - httpx >=0.25.0,<1 + - ipykernel >=6.5.0,!=6.30.0 + - jinja2 >=3.0.3 + - jupyter-lsp >=2.0.0 + - jupyter_core + - jupyter_server >=2.4.0,<3 + - jupyterlab_server >=2.28.0,<3 + - notebook-shim >=0.2 + - packaging + - python >=3.10 + - setuptools >=41.1.0 + - tomli >=1.2.2 + - tornado >=6.2.0 + - traitlets + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/jupyterlab?source=hash-mapping + size: 8861204 + timestamp: 1777483115382 +- conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_pygments-0.3.0-pyhd8ed1ab_2.conda + sha256: dc24b900742fdaf1e077d9a3458fd865711de80bca95fe3c6d46610c532c6ef0 + md5: fd312693df06da3578383232528c468d + depends: + - pygments >=2.4.1,<3 + - python >=3.9 + constrains: + - jupyterlab >=4.0.8,<5.0.0 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/jupyterlab-pygments?source=hash-mapping + size: 18711 + timestamp: 1733328194037 +- conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_server-2.28.0-pyhcf101f3_0.conda + sha256: 381d2d6a259a3be5f38a69463e0f6c5dcf1844ae113058007b51c3bef13a7cee + md5: a63877cb23de826b1620d3adfccc4014 + depends: + - babel >=2.10 + - jinja2 >=3.0.3 + - json5 >=0.9.0 + - jsonschema >=4.18 + - jupyter_server >=1.21,<3 + - packaging >=21.3 + - python >=3.10 + - requests >=2.31 + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/jupyterlab-server?source=hash-mapping + size: 51621 + timestamp: 1761145478692 +- conda: https://conda.anaconda.org/conda-forge/noarch/lark-1.3.1-pyhd8ed1ab_0.conda + sha256: 49570840fb15f5df5d4b4464db8ee43a6d643031a2bc70ef52120a52e3809699 + md5: 9b965c999135d43a3d0f7bd7d024e26a + depends: + - python >=3.10 + license: MIT + license_family: MIT + purls: + - pkg:pypi/lark?source=hash-mapping + size: 94312 + timestamp: 1761596921009 +- conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.2.2-pyhd8ed1ab_0.conda + sha256: 35b43d7343f74452307fd018a1cca92b8f68961ff8e2ab6a81ce0a703c9a3764 + md5: 9acc1c385be401d533ff70ef5b50dae6 + depends: + - python >=3.10 + - traitlets + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/matplotlib-inline?source=hash-mapping + size: 15725 + timestamp: 1778264403247 +- conda: https://conda.anaconda.org/conda-forge/noarch/mistune-3.2.1-pyhcf101f3_0.conda + sha256: b52dc6c78fbbe7a3008535cb8bfd87d70d8053e9250bbe16e387470a9df07070 + md5: b97e84d1553b4a1c765b87fff83453ad + depends: + - python >=3.10 + - typing_extensions + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/mistune?source=hash-mapping + size: 74567 + timestamp: 1777824616382 +- conda: https://conda.anaconda.org/conda-forge/noarch/nbclient-0.10.4-pyhd8ed1ab_0.conda + sha256: 1b66960ee06874ddceeebe375d5f17fb5f393d025a09e15b830ad0c4fffb585b + md5: 00f5b8dafa842e0c27c1cd7296aa4875 + depends: + - jupyter_client >=6.1.12 + - jupyter_core >=4.12,!=5.0.* + - nbformat >=5.1 + - python >=3.8 + - traitlets >=5.4 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/nbclient?source=hash-mapping + size: 28473 + timestamp: 1766485646962 +- conda: https://conda.anaconda.org/conda-forge/noarch/nbconvert-core-7.17.1-pyhcf101f3_0.conda + sha256: ab2ac79c5892c5434d50b3542d96645bdaa06d025b6e03734be29200de248ac2 + md5: 2bce0d047658a91b99441390b9b27045 + depends: + - beautifulsoup4 + - bleach-with-css !=5.0.0 + - defusedxml + - importlib-metadata >=3.6 + - jinja2 >=3.0 + - jupyter_core >=4.7 + - jupyterlab_pygments + - markupsafe >=2.0 + - mistune >=2.0.3,<4 + - nbclient >=0.5.0 + - nbformat >=5.7 + - packaging + - pandocfilters >=1.4.1 + - pygments >=2.4.1 + - python >=3.10 + - traitlets >=5.1 + - python + constrains: + - pandoc >=2.9.2,<4.0.0 + - nbconvert ==7.17.1 *_0 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/nbconvert?source=hash-mapping + size: 202229 + timestamp: 1775615493260 +- conda: https://conda.anaconda.org/conda-forge/noarch/nbformat-5.10.4-pyhd8ed1ab_1.conda + sha256: 7a5bd30a2e7ddd7b85031a5e2e14f290898098dc85bea5b3a5bf147c25122838 + md5: bbe1963f1e47f594070ffe87cdf612ea + depends: + - jsonschema >=2.6 + - jupyter_core >=4.12,!=5.0.* + - python >=3.9 + - python-fastjsonschema >=2.15 + - traitlets >=5.1 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/nbformat?source=hash-mapping + size: 100945 + timestamp: 1733402844974 +- conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_1.conda + sha256: bb7b21d7fd0445ddc0631f64e66d91a179de4ba920b8381f29b9d006a42788c0 + md5: 598fd7d4d0de2455fb74f56063969a97 + depends: + - python >=3.9 + license: BSD-2-Clause + license_family: BSD + purls: + - pkg:pypi/nest-asyncio?source=hash-mapping + size: 11543 + timestamp: 1733325673691 +- conda: https://conda.anaconda.org/conda-forge/noarch/notebook-shim-0.2.4-pyhd8ed1ab_1.conda + sha256: 7b920e46b9f7a2d2aa6434222e5c8d739021dbc5cc75f32d124a8191d86f9056 + md5: e7f89ea5f7ea9401642758ff50a2d9c1 + depends: + - jupyter_server >=1.8,<3 + - python >=3.9 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/notebook-shim?source=hash-mapping + size: 16817 + timestamp: 1733408419340 +- conda: https://conda.anaconda.org/conda-forge/noarch/overrides-7.7.0-pyhd8ed1ab_1.conda + sha256: 1840bd90d25d4930d60f57b4f38d4e0ae3f5b8db2819638709c36098c6ba770c + md5: e51f1e4089cad105b6cac64bd8166587 + depends: + - python >=3.9 + - typing_utils + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/overrides?source=hash-mapping + size: 30139 + timestamp: 1734587755455 +- conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.2-pyhc364b38_0.conda + sha256: 3906abfb6511a3bb309e39b9b1b7bc38f50a723971de2395489fd1f379255890 + md5: 4c06a92e74452cfa53623a81592e8934 + depends: + - python >=3.8 + - python + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/packaging?source=hash-mapping + size: 91574 + timestamp: 1777103621679 +- conda: https://conda.anaconda.org/conda-forge/noarch/pandocfilters-1.5.0-pyhd8ed1ab_0.tar.bz2 + sha256: 2bb9ba9857f4774b85900c2562f7e711d08dd48e2add9bee4e1612fbee27e16f + md5: 457c2c8c08e54905d6954e79cb5b5db9 + depends: + - python !=3.0,!=3.1,!=3.2,!=3.3 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/pandocfilters?source=hash-mapping + size: 11627 + timestamp: 1631603397334 +- conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.7-pyhcf101f3_0.conda + sha256: 611882f7944b467281c46644ffde6c5145d1a7730388bcde26e7e86819b0998e + md5: 39894c952938276405a1bd30e4ce2caf + depends: + - python >=3.10 + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/parso?source=hash-mapping + size: 82472 + timestamp: 1777722955579 +- conda: https://conda.anaconda.org/conda-forge/noarch/pexpect-4.9.0-pyhd8ed1ab_1.conda + sha256: 202af1de83b585d36445dc1fda94266697341994d1a3328fabde4989e1b3d07a + md5: d0d408b1f18883a944376da5cf8101ea + depends: + - ptyprocess >=0.5 + - python >=3.9 + license: ISC + purls: + - pkg:pypi/pexpect?source=hash-mapping + size: 53561 + timestamp: 1733302019362 +- conda: https://conda.anaconda.org/conda-forge/noarch/pixi-kernel-0.7.1-pyhbbac1ac_0.conda + sha256: 506c9330b8dc5ae98f4c32629fa59fa40e6bdd42a681c48d2f9554693dd01156 + md5: d57ef7cb7ad6b5d62cef8b9bdf1d400b + depends: + - ipykernel >=6 + - jupyter_client >=7 + - jupyter_server >=2.4 + - msgspec >=0.18 + - python >=3.10 + - returns >=0.23 + - tomli >=2 + license: MIT + license_family: MIT + purls: + - pkg:pypi/pixi-kernel?source=hash-mapping + size: 39509 + timestamp: 1764156429044 +- conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.9.6-pyhcf101f3_0.conda + sha256: 8f29915c172f1f7f4f7c9391cd5dac3ebf5d13745c8b7c8006032615246345a5 + md5: 89c0b6d1793601a2a3a3f7d2d3d8b937 + depends: + - python >=3.10 + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/platformdirs?source=hash-mapping + size: 25862 + timestamp: 1775741140609 +- conda: https://conda.anaconda.org/conda-forge/noarch/prometheus_client-0.25.0-pyhd8ed1ab_0.conda + sha256: 4d7ec90d4f9c1f3b4a50623fefe4ebba69f651b102b373f7c0e9dbbfa43d495c + md5: a11ab1f31af799dd93c3a39881528884 + depends: + - python >=3.10 + license: Apache-2.0 + license_family: Apache + purls: + - pkg:pypi/prometheus-client?source=hash-mapping + size: 57113 + timestamp: 1775771465170 +- conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.52-pyha770c72_0.conda + sha256: 4817651a276016f3838957bfdf963386438c70761e9faec7749d411635979bae + md5: edb16f14d920fb3faf17f5ce582942d6 + depends: + - python >=3.10 + - wcwidth + constrains: + - prompt_toolkit 3.0.52 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/prompt-toolkit?source=hash-mapping + size: 273927 + timestamp: 1756321848365 +- conda: https://conda.anaconda.org/conda-forge/noarch/ptyprocess-0.7.0-pyhd8ed1ab_1.conda + sha256: a7713dfe30faf17508ec359e0bc7e0983f5d94682492469bd462cdaae9c64d83 + md5: 7d9daffbb8d8e0af0f769dbbcd173a54 + depends: + - python >=3.9 + license: ISC + purls: + - pkg:pypi/ptyprocess?source=hash-mapping + size: 19457 + timestamp: 1733302371990 +- conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda + sha256: 71bd24600d14bb171a6321d523486f6a06f855e75e547fa0cb2a0953b02047f0 + md5: 3bfdfb8dbcdc4af1ae3f9a8eb3948f04 + depends: + - python >=3.9 + license: MIT + license_family: MIT + purls: + - pkg:pypi/pure-eval?source=hash-mapping + size: 16668 + timestamp: 1733569518868 +- conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda + sha256: 79db7928d13fab2d892592223d7570f5061c192f27b9febd1a418427b719acc6 + md5: 12c566707c80111f9799308d9e265aef + depends: + - python >=3.9 + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/pycparser?source=hash-mapping + size: 110100 + timestamp: 1733195786147 +- conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda + sha256: cf70b2f5ad9ae472b71235e5c8a736c9316df3705746de419b59d442e8348e86 + md5: 16c18772b340887160c79a6acc022db0 + depends: + - python >=3.10 + license: BSD-2-Clause + license_family: BSD + purls: + - pkg:pypi/pygments?source=hash-mapping + size: 893031 + timestamp: 1774796815820 +- conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyh09c184e_7.conda + sha256: d016e04b0e12063fbee4a2d5fbb9b39a8d191b5a0042f0b8459188aedeabb0ca + md5: e2fd202833c4a981ce8a65974fe4abd1 + depends: + - __win + - python >=3.9 + - win_inet_pton + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/pysocks?source=hash-mapping + size: 21784 + timestamp: 1733217448189 +- conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda + sha256: ba3b032fa52709ce0d9fd388f63d330a026754587a2f461117cac9ab73d8d0d8 + md5: 461219d1a5bd61342293efa2c0c90eac + depends: + - __unix + - python >=3.9 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/pysocks?source=hash-mapping + size: 21085 + timestamp: 1733217331982 +- conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda + sha256: d6a17ece93bbd5139e02d2bd7dbfa80bee1a4261dced63f65f679121686bf664 + md5: 5b8d21249ff20967101ffa321cab24e8 + depends: + - python >=3.9 + - six >=1.5 + - python + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/python-dateutil?source=hash-mapping + size: 233310 + timestamp: 1751104122689 +- conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.21.2-pyhe01879c_0.conda + sha256: df9aa74e9e28e8d1309274648aac08ec447a92512c33f61a8de0afa9ce32ebe8 + md5: 23029aae904a2ba587daba708208012f + depends: + - python >=3.9 + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/fastjsonschema?source=hash-mapping + size: 244628 + timestamp: 1755304154927 +- conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.12.13-hd8ed1ab_0.conda + sha256: 97327b9509ae3aae28d27217a5d7bd31aff0ab61a02041e9c6f98c11d8a53b29 + md5: 32780d6794b8056b78602103a04e90ef + depends: + - cpython 3.12.13.* + - python_abi * *_cp312 + license: Python-2.0 purls: [] - size: 2486744 - timestamp: 1737621160295 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/gsl-2.8-h8d0574d_1.conda - sha256: f11d8f2007f6591022afa958d8fe15afbe4211198d1603c0eb886bc21a9eb19e - md5: cc261442bead590d89ca9f96884a344f + size: 46449 + timestamp: 1772728979370 +- conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.14.4-h4df99d1_100.conda + sha256: 36ff7984e4565c85149e64f8206303d412a0652e55cf806dcb856903fa056314 + md5: e4e60721757979d01d3964122f674959 depends: - - __osx >=11.0 - - libblas >=3.9.0,<4.0a0 - - libcblas >=3.9.0,<4.0a0 - license: GPL-3.0-or-later - license_family: GPL + - cpython 3.14.4.* + - python_abi * *_cp314 + license: Python-2.0 purls: [] - size: 1862134 - timestamp: 1737621413640 -- conda: https://conda.anaconda.org/conda-forge/win-64/gsl-2.8-h5b8d9c4_1.conda - sha256: 87a3468e09cc1ee0268e8639debad6a5b440090ef8cb1d2ee5eed66c86085528 - md5: a47cf810b7c03955139a150b228b93ca + size: 49806 + timestamp: 1775614307464 +- conda: https://conda.anaconda.org/conda-forge/noarch/python-json-logger-3.2.1-pyh332efcf_0.conda + sha256: 1c55116c22512cef7b01d55ae49697707f2c1fd829407183c19817e2d300fd8d + md5: 1cd2f3e885162ee1366312bd1b1677fd depends: - - libblas >=3.9.0,<4.0a0 - - libcblas >=3.9.0,<4.0a0 - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - license: GPL-3.0-or-later - license_family: GPL + - python >=3.10 + - typing_extensions + license: BSD-2-Clause + license_family: BSD + purls: + - pkg:pypi/python-json-logger?source=hash-mapping + size: 18969 + timestamp: 1777318679482 +- conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2026.2-pyhd8ed1ab_0.conda + sha256: e943f9c15a6bdba2e1b9f423ab913b3f6b02197b0ef9f8e6b7464d78b59965b9 + md5: f6ad7450fc21e00ecc23812baed6d2e4 + depends: + - python >=3.10 + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/tzdata?source=hash-mapping + size: 146639 + timestamp: 1777068997932 +- conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.12-8_cp312.conda + build_number: 8 + sha256: 80677180dd3c22deb7426ca89d6203f1c7f1f256f2d5a94dc210f6e758229809 + md5: c3efd25ac4d74b1584d2f7a57195ddf1 + constrains: + - python 3.12.* *_cpython + license: BSD-3-Clause + license_family: BSD purls: [] - size: 1528970 - timestamp: 1737622367981 -- conda: https://conda.anaconda.org/conda-forge/noarch/h11-0.16.0-pyhcf101f3_1.conda - sha256: 96cac6573fd35ae151f4d6979bab6fbc90cb6b1fb99054ba19eb075da9822fcb - md5: b8993c19b0c32a2f7b66cbb58ca27069 + size: 6958 + timestamp: 1752805918820 +- conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.14-8_cp314.conda + build_number: 8 + sha256: ad6d2e9ac39751cc0529dd1566a26751a0bf2542adb0c232533d32e176e21db5 + md5: 0539938c55b6b1a59b560e843ad864a4 + constrains: + - python 3.14.* *_cp314 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 6989 + timestamp: 1752805904792 +- conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.37.0-pyhcf101f3_0.conda + sha256: 0577eedfb347ff94d0f2fa6c052c502989b028216996b45c7f21236f25864414 + md5: 870293df500ca7e18bedefa5838a22ab depends: + - attrs >=22.2.0 - python >=3.10 - - typing_extensions + - rpds-py >=0.7.0 + - typing_extensions >=4.4.0 - python license: MIT license_family: MIT purls: - - pkg:pypi/h11?source=compressed-mapping - size: 39069 - timestamp: 1767729720872 -- conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda - sha256: 84c64443368f84b600bfecc529a1194a3b14c3656ee2e832d15a20e0329b6da3 - md5: 164fc43f0b53b6e3a7bc7dce5e4f1dc9 + - pkg:pypi/referencing?source=hash-mapping + size: 51788 + timestamp: 1760379115194 +- conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.34.2-pyhcf101f3_0.conda + sha256: 1715246b19c9f85ee022933b4845f2fc14ac9184981b7b7d9b728bec8e9588da + md5: 4a85203c1d80c1059086ae860836ffb9 depends: - python >=3.10 - - hyperframe >=6.1,<7 - - hpack >=4.1,<5 + - certifi >=2023.5.7 + - charset-normalizer >=2,<4 + - idna >=2.5,<4 + - urllib3 >=1.26,<3 - python - license: MIT - license_family: MIT - purls: - - pkg:pypi/h2?source=hash-mapping - size: 95967 - timestamp: 1756364871835 -- pypi: https://files.pythonhosted.org/packages/03/c1/0976b235cf29ead553e22f2fb6385a8252b533715e00d0ae52ed7b900582/h5py-3.16.0-cp312-cp312-win_amd64.whl - name: h5py - version: 3.16.0 - sha256: 96b422019a1c8975c2d5dadcf61d4ba6f01c31f92bbde6e4649607885fe502d6 - requires_dist: - - numpy>=1.21.2 - requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/3e/14/615a450205e1b56d16c6783f5ccd116cde05550faad70ae077c955654a75/h5py-3.16.0-cp314-cp314-win_amd64.whl - name: h5py - version: 3.16.0 - sha256: fa48993a0b799737ba7fd21e2350fa0a60701e58180fae9f2de834bc39a147ab - requires_dist: - - numpy>=1.21.2 - requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/6a/b7/9366ed44ced9b7ef357ab48c94205280276db9d7f064aa3012a97227e966/h5py-3.16.0-cp314-cp314-macosx_11_0_arm64.whl - name: h5py - version: 3.16.0 - sha256: 8c1eff849cdd53cbc73c214c30ebdb6f1bb8b64790b4b4fc36acdb5e43570210 - requires_dist: - - numpy>=1.21.2 - requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/9e/e9/1a19e42cd43cc1365e127db6aae85e1c671da1d9a5d746f4d34a50edb577/h5py-3.16.0-cp312-cp312-manylinux_2_28_x86_64.whl - name: h5py - version: 3.16.0 - sha256: dfc21898ff025f1e8e67e194965a95a8d4754f452f83454538f98f8a3fcb207e - requires_dist: - - numpy>=1.21.2 - requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/b0/42/c84efcc1d4caebafb1ecd8be4643f39c85c47a80fe254d92b8b43b1eadaf/h5py-3.16.0-cp312-cp312-macosx_11_0_arm64.whl - name: h5py - version: 3.16.0 - sha256: 42b012933a83e1a558c673176676a10ce2fd3759976a0fedee1e672d1e04fc9d - requires_dist: - - numpy>=1.21.2 - requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/f1/16/d905e7f53e661ce2c24686c38048d8e2b750ffc4350009d41c4e6c6c9826/h5py-3.16.0-cp314-cp314-manylinux_2_28_x86_64.whl - name: h5py - version: 3.16.0 - sha256: e4360f15875a532bc7b98196c7592ed4fc92672a57c0a621355961cafb17a6dd - requires_dist: - - numpy>=1.21.2 - requires_python: '>=3.10' -- conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda - sha256: 6ad78a180576c706aabeb5b4c8ceb97c0cb25f1e112d76495bff23e3779948ba - md5: 0a802cb9888dd14eeefc611f05c40b6e - depends: - - python >=3.9 - license: MIT - license_family: MIT + constrains: + - chardet >=3.0.2,<8 + license: Apache-2.0 purls: - - pkg:pypi/hpack?source=hash-mapping - size: 30731 - timestamp: 1737618390337 -- conda: https://conda.anaconda.org/conda-forge/noarch/httpcore-1.0.9-pyh29332c3_0.conda - sha256: 04d49cb3c42714ce533a8553986e1642d0549a05dc5cc48e0d43ff5be6679a5b - md5: 4f14640d58e2cc0aa0819d9d8ba125bb + - pkg:pypi/requests?source=compressed-mapping + size: 68709 + timestamp: 1778851103479 +- conda: https://conda.anaconda.org/conda-forge/noarch/returns-0.27.0-pyhc364b38_0.conda + sha256: 3b45efeae771f1a20307b36ecdb3a8911a89c05382836b50c62b0a99d8d3dfd8 + md5: da94ff04d97ec5efc42cbe5da3c43a84 depends: - - python >=3.9 - - h11 >=0.16 - - h2 >=3,<5 - - sniffio 1.* - - anyio >=4.0,<5.0 - - certifi + - python >=3.11 + - typing_extensions >=4.0,<5.0 - python - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/httpcore?source=hash-mapping - size: 49483 - timestamp: 1745602916758 -- conda: https://conda.anaconda.org/conda-forge/noarch/httpx-0.28.1-pyhd8ed1ab_0.conda - sha256: cd0f1de3697b252df95f98383e9edb1d00386bfdd03fdf607fa42fe5fcb09950 - md5: d6989ead454181f4f9bc987d3dc4e285 - depends: - - anyio - - certifi - - httpcore 1.* - - idna - - python >=3.9 - license: BSD-3-Clause + license: BSD-2-Clause license_family: BSD purls: - - pkg:pypi/httpx?source=hash-mapping - size: 63082 - timestamp: 1733663449209 -- conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda - sha256: 77af6f5fe8b62ca07d09ac60127a30d9069fdc3c68d6b256754d0ffb1f7779f8 - md5: 8e6923fc12f1fe8f8c4e5c9f343256ac + - pkg:pypi/returns?source=hash-mapping + size: 100559 + timestamp: 1776176903101 +- conda: https://conda.anaconda.org/conda-forge/noarch/rfc3339-validator-0.1.4-pyhd8ed1ab_1.conda + sha256: 2e4372f600490a6e0b3bac60717278448e323cab1c0fecd5f43f7c56535a99c5 + md5: 36de09a8d3e5d5e6f4ee63af49e59706 depends: - python >=3.9 + - six license: MIT license_family: MIT purls: - - pkg:pypi/hyperframe?source=hash-mapping - size: 17397 - timestamp: 1737618427549 -- conda: https://conda.anaconda.org/conda-forge/linux-64/icu-78.3-h33c6efd_0.conda - sha256: fbf86c4a59c2ed05bbffb2ba25c7ed94f6185ec30ecb691615d42342baa1a16a - md5: c80d8a3b84358cb967fa81e7075fbc8a - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=14 - - libstdcxx >=14 - license: MIT - license_family: MIT - purls: [] - size: 12723451 - timestamp: 1773822285671 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/icu-78.3-hef89b57_0.conda - sha256: 3a7907a17e9937d3a46dfd41cffaf815abad59a569440d1e25177c15fd0684e5 - md5: f1182c91c0de31a7abd40cedf6a5ebef + - pkg:pypi/rfc3339-validator?source=hash-mapping + size: 10209 + timestamp: 1733600040800 +- conda: https://conda.anaconda.org/conda-forge/noarch/rfc3986-validator-0.1.1-pyh9f0ad1d_0.tar.bz2 + sha256: 2a5b495a1de0f60f24d8a74578ebc23b24aa53279b1ad583755f223097c41c37 + md5: 912a71cc01012ee38e6b90ddd561e36f depends: - - __osx >=11.0 + - python license: MIT license_family: MIT - purls: [] - size: 12361647 - timestamp: 1773822915649 -- pypi: https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl - name: identify - version: 2.6.19 - sha256: 20e6a87f786f768c092a721ad107fc9df0eb89347be9396cadf3f4abbd1fb78a - requires_dist: - - ukkonen ; extra == 'license' - requires_python: '>=3.10' -- conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.13-pyhcf101f3_0.conda - sha256: 9ab620e6f64bb67737bd7bc1ad6f480770124e304c6710617aba7fe60b089f48 - md5: fb7130c190f9b4ec91219840a05ba3ac - depends: - - python >=3.10 - - python - license: BSD-3-Clause - license_family: BSD purls: - - pkg:pypi/idna?source=compressed-mapping - size: 59038 - timestamp: 1776947141407 -- conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.8.0-pyhcf101f3_0.conda - sha256: 82ab2a0d91ca1e7e63ab6a4939356667ef683905dea631bc2121aa534d347b16 - md5: 080594bf4493e6bae2607e65390c520a + - pkg:pypi/rfc3986-validator?source=hash-mapping + size: 7818 + timestamp: 1598024297745 +- conda: https://conda.anaconda.org/conda-forge/noarch/rfc3987-syntax-1.1.0-pyhe01879c_1.conda + sha256: 70001ac24ee62058557783d9c5a7bbcfd97bd4911ef5440e3f7a576f9e43bc92 + md5: 7234f99325263a5af6d4cd195035e8f2 depends: - - python >=3.10 - - zipp >=3.20 + - python >=3.9 + - lark >=1.2.2 - python - license: Apache-2.0 - license_family: APACHE + license: MIT + license_family: MIT purls: - - pkg:pypi/importlib-metadata?source=compressed-mapping - size: 34387 - timestamp: 1773931568510 -- pypi: https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl - name: iniconfig - version: 2.3.0 - sha256: f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12 - requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/12/c9/6869a1dcf4aaf309b9543ec070be3ec3adebee7c9bec9af8c230494134b9/interrogate-1.7.0-py3-none-any.whl - name: interrogate - version: 1.7.0 - sha256: b13ff4dd8403369670e2efe684066de9fcb868ad9d7f2b4095d8112142dc9d12 - requires_dist: - - attrs - - click>=7.1 - - colorama - - py - - tabulate - - tomli ; python_full_version < '3.11' - - cairosvg ; extra == 'dev' - - sphinx ; extra == 'dev' - - sphinx-autobuild ; extra == 'dev' - - pytest ; extra == 'dev' - - pytest-cov ; extra == 'dev' - - pytest-mock ; extra == 'dev' - - coverage[toml] ; extra == 'dev' - - wheel ; extra == 'dev' - - pre-commit ; extra == 'dev' - - sphinx ; extra == 'docs' - - sphinx-autobuild ; extra == 'docs' - - cairosvg ; extra == 'png' - - pytest ; extra == 'tests' - - pytest-cov ; extra == 'tests' - - pytest-mock ; extra == 'tests' - - coverage[toml] ; extra == 'tests' - requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/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' -- conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.2.0-pyh5552912_1.conda - sha256: 5c1f3e874adaf603449f2b135d48f168c5d510088c78c229bda0431268b43b27 - md5: 4b53d436f3fbc02ce3eeaf8ae9bebe01 + - pkg:pypi/rfc3987-syntax?source=hash-mapping + size: 22913 + timestamp: 1752876729969 +- conda: https://conda.anaconda.org/conda-forge/noarch/send2trash-2.1.0-pyh5552912_1.conda + sha256: 8fc024bf1a7b99fc833b131ceef4bef8c235ad61ecb95a71a6108be2ccda63e8 + md5: b70e2d44e6aa2beb69ba64206a16e4c6 depends: - - appnope - - __osx - - comm >=0.1.1 - - debugpy >=1.6.5 - - ipython >=7.23.1 - - jupyter_client >=8.8.0 - - jupyter_core >=5.1,!=6.0.* - - matplotlib-inline >=0.1 - - nest-asyncio >=1.4 - - packaging >=22 - - psutil >=5.7 + - __osx + - pyobjc-framework-cocoa - python >=3.10 - - pyzmq >=25 - - tornado >=6.4.1 - - traitlets >=5.4.0 - python - constrains: - - appnope >=0.1.2 license: BSD-3-Clause license_family: BSD purls: - - pkg:pypi/ipykernel?source=hash-mapping - size: 132260 - timestamp: 1770566135697 -- conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.2.0-pyh6dadd2b_1.conda - sha256: 9cdadaeef5abadca4113f92f5589db19f8b7df5e1b81cb0225f7024a3aedefa3 - md5: b3a7d5842f857414d9ae831a799444dd + - pkg:pypi/send2trash?source=hash-mapping + size: 22519 + timestamp: 1770937603551 +- conda: https://conda.anaconda.org/conda-forge/noarch/send2trash-2.1.0-pyh6dadd2b_1.conda + sha256: 305446a0b018f285351300463653d3d3457687270e20eda37417b12ee386ef76 + md5: 6ac53f3fff2c416d63511843a04646fa depends: - __win - - comm >=0.1.1 - - debugpy >=1.6.5 - - ipython >=7.23.1 - - jupyter_client >=8.8.0 - - jupyter_core >=5.1,!=6.0.* - - matplotlib-inline >=0.1 - - nest-asyncio >=1.4 - - packaging >=22 - - psutil >=5.7 + - pywin32 - python >=3.10 - - pyzmq >=25 - - tornado >=6.4.1 - - traitlets >=5.4.0 - python - constrains: - - appnope >=0.1.2 license: BSD-3-Clause license_family: BSD purls: - - pkg:pypi/ipykernel?source=hash-mapping - size: 132382 - timestamp: 1770566174387 -- conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.2.0-pyha191276_1.conda - sha256: b77ed58eb235e5ad80e742b03caeed4bbc2a2ef064cb9a2deee3b75dfae91b2a - md5: 8b267f517b81c13594ed68d646fd5dcb + - pkg:pypi/send2trash?source=hash-mapping + size: 22864 + timestamp: 1770937641143 +- conda: https://conda.anaconda.org/conda-forge/noarch/send2trash-2.1.0-pyha191276_1.conda + sha256: 59656f6b2db07229351dfb3a859c35e57cc8e8bcbc86d4e501bff881a6f771f1 + md5: 28eb91468df04f655a57bcfbb35fc5c5 depends: - __linux - - comm >=0.1.1 - - debugpy >=1.6.5 - - ipython >=7.23.1 - - jupyter_client >=8.8.0 - - jupyter_core >=5.1,!=6.0.* - - matplotlib-inline >=0.1 - - nest-asyncio >=1.4 - - packaging >=22 - - psutil >=5.7 - python >=3.10 - - pyzmq >=25 - - tornado >=6.4.1 - - traitlets >=5.4.0 - python - constrains: - - appnope >=0.1.2 license: BSD-3-Clause license_family: BSD purls: - - pkg:pypi/ipykernel?source=hash-mapping - size: 133644 - timestamp: 1770566133040 -- conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.13.0-pyh53cf698_0.conda - sha256: a0af49948a1842dfd15a0b0b2fd56c94ddbd07e07a6c8b4bc70d43015eafaff0 - md5: 73e9657cd19605740d21efb14d8d0cb9 + - pkg:pypi/send2trash?source=hash-mapping + size: 24108 + timestamp: 1770937597662 +- conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-82.0.1-pyh332efcf_0.conda + sha256: 82088a6e4daa33329a30bc26dc19a98c7c1d3f05c0f73ce9845d4eab4924e9e1 + md5: 8e194e7b992f99a5015edbd4ebd38efd depends: - - __unix - - decorator >=5.1.0 - - ipython_pygments_lexers >=1.0.0 - - jedi >=0.18.2 - - matplotlib-inline >=0.1.6 - - prompt-toolkit >=3.0.41,<3.1.0 - - psutil >=7 - - pygments >=2.14.0 - - python >=3.11 - - stack_data >=0.6.0 - - traitlets >=5.13.0 - - typing_extensions >=4.6 - - pexpect >4.6 - - python - license: BSD-3-Clause - license_family: BSD + - python >=3.10 + license: MIT + license_family: MIT purls: - - pkg:pypi/ipython?source=compressed-mapping - size: 651632 - timestamp: 1777038396606 -- conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.13.0-pyhe2676ad_0.conda - sha256: f252ec33597115ff21cbb31051f6f9be34ca36cbbbf3d266b597660d8d8edde9 - md5: 5631ab99e902463d9dd4221e5b4eab6d + - pkg:pypi/setuptools?source=hash-mapping + size: 639697 + timestamp: 1773074868565 +- conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda + sha256: 458227f759d5e3fcec5d9b7acce54e10c9e1f4f4b7ec978f3bfd54ce4ee9853d + md5: 3339e3b65d58accf4ca4fb8748ab16b3 depends: - - __win - - decorator >=5.1.0 - - ipython_pygments_lexers >=1.0.0 - - jedi >=0.18.2 - - matplotlib-inline >=0.1.6 - - prompt-toolkit >=3.0.41,<3.1.0 - - psutil >=7 - - pygments >=2.14.0 - - python >=3.11 - - stack_data >=0.6.0 - - traitlets >=5.13.0 - - typing_extensions >=4.6 - - colorama >=0.4.4 + - python >=3.9 - python - license: BSD-3-Clause - license_family: BSD + license: MIT + license_family: MIT purls: - - pkg:pypi/ipython?source=compressed-mapping - size: 650593 - timestamp: 1777038425499 -- conda: https://conda.anaconda.org/conda-forge/noarch/ipython_pygments_lexers-1.1.1-pyhd8ed1ab_0.conda - sha256: 894682a42a7d659ae12878dbcb274516a7031bbea9104e92f8e88c1f2765a104 - md5: bd80ba060603cc228d9d81c257093119 + - pkg:pypi/six?source=hash-mapping + size: 18455 + timestamp: 1753199211006 +- conda: https://conda.anaconda.org/conda-forge/noarch/sniffio-1.3.1-pyhd8ed1ab_2.conda + sha256: dce518f45e24cd03f401cb0616917773159a210c19d601c5f2d4e0e5879d30ad + md5: 03fe290994c5e4ec17293cfb6bdce520 depends: - - pygments - - python >=3.9 - license: BSD-3-Clause - license_family: BSD + - python >=3.10 + license: Apache-2.0 + license_family: Apache purls: - - pkg:pypi/ipython-pygments-lexers?source=hash-mapping - size: 13993 - timestamp: 1737123723464 -- 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' -- conda: https://conda.anaconda.org/conda-forge/noarch/isoduration-20.11.0-pyhd8ed1ab_1.conda - sha256: 08e838d29c134a7684bca0468401d26840f41c92267c4126d7b43a6b533b0aed - md5: 0b0154421989637d424ccf0f104be51a + - pkg:pypi/sniffio?source=hash-mapping + size: 15698 + timestamp: 1762941572482 +- conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.8.3-pyhd8ed1ab_0.conda + sha256: 23b71ecf089967d2900126920e7f9ff18cdcef82dbff3e2f54ffa360243a17ac + md5: 18de09b20462742fe093ba39185d9bac depends: - - arrow >=0.15.0 - - python >=3.9 + - python >=3.10 license: MIT license_family: MIT purls: - - pkg:pypi/isoduration?source=hash-mapping - size: 19832 - timestamp: 1733493720346 -- conda: https://conda.anaconda.org/conda-forge/noarch/jedi-0.19.2-pyhd8ed1ab_1.conda - sha256: 92c4d217e2dc68983f724aa983cca5464dcb929c566627b26a2511159667dba8 - md5: a4f4c5dc9b80bc50e0d3dc4e6e8f1bd9 + - pkg:pypi/soupsieve?source=hash-mapping + size: 38187 + timestamp: 1769034509657 +- conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda + sha256: 570da295d421661af487f1595045760526964f41471021056e993e73089e9c41 + md5: b1b505328da7a6b246787df4b5a49fbc depends: - - parso >=0.8.3,<0.9.0 + - asttokens + - executing + - pure_eval - python >=3.9 - license: Apache-2.0 AND MIT + license: MIT + license_family: MIT purls: - - pkg:pypi/jedi?source=hash-mapping - size: 843646 - timestamp: 1733300981994 -- conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda - sha256: fc9ca7348a4f25fed2079f2153ecdcf5f9cf2a0bc36c4172420ca09e1849df7b - md5: 04558c96691bed63104678757beb4f8d + - pkg:pypi/stack-data?source=hash-mapping + size: 26988 + timestamp: 1733569565672 +- conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyh6dadd2b_1.conda + sha256: b375e8df0d5710717c31e7c8e93c025c37fa3504aea325c7a55509f64e5d4340 + md5: e43ca10d61e55d0a8ec5d8c62474ec9e depends: - - markupsafe >=2.0 + - __win + - pywinpty >=1.1.0 + - python >=3.10 + - tornado >=6.1.0 + - python + license: BSD-2-Clause + license_family: BSD + purls: + - pkg:pypi/terminado?source=hash-mapping + size: 23665 + timestamp: 1766513806974 +- conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyhc90fa1f_1.conda + sha256: 6b6727a13d1ca6a23de5e6686500d0669081a117736a87c8abf444d60c1e40eb + md5: 17b43cee5cc84969529d5d0b0309b2cb + depends: + - __unix + - ptyprocess - python >=3.10 + - tornado >=6.1.0 - python + license: BSD-2-Clause + license_family: BSD + purls: + - pkg:pypi/terminado?source=hash-mapping + size: 24749 + timestamp: 1766513766867 +- conda: https://conda.anaconda.org/conda-forge/noarch/tinycss2-1.4.0-pyhd8ed1ab_0.conda + sha256: cad582d6f978276522f84bd209a5ddac824742fe2d452af6acf900f8650a73a2 + md5: f1acf5fdefa8300de697982bcb1761c9 + depends: + - python >=3.5 + - webencodings >=0.4 license: BSD-3-Clause license_family: BSD purls: - - pkg:pypi/jinja2?source=compressed-mapping - size: 120685 - timestamp: 1764517220861 -- 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' -- conda: https://conda.anaconda.org/conda-forge/noarch/json5-0.14.0-pyhd8ed1ab_0.conda - sha256: 9daa95bd164c8fa23b3ab196e906ef806141d749eddce2a08baa064f722d25fa - md5: 1269891272187518a0a75c286f7d0bbf + - pkg:pypi/tinycss2?source=hash-mapping + size: 28285 + timestamp: 1729802975370 +- conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.1-pyhcf101f3_0.conda + sha256: 91cafdb64268e43e0e10d30bd1bef5af392e69f00edd34dfaf909f69ab2da6bd + md5: b5325cf06a000c5b14970462ff5e4d58 depends: - python >=3.10 - license: Apache-2.0 - license_family: APACHE + - python + license: MIT + license_family: MIT purls: - - pkg:pypi/json5?source=compressed-mapping - size: 34731 - timestamp: 1774655440045 -- conda: https://conda.anaconda.org/conda-forge/noarch/jsonpointer-3.1.1-pyhcf101f3_0.conda - sha256: a3d10301b6ff399ba1f3d39e443664804a3d28315a4fb81e745b6817845f70ae - md5: 89bf346df77603055d3c8fe5811691e6 + - pkg:pypi/tomli?source=hash-mapping + size: 21561 + timestamp: 1774492402955 +- conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.15.0-pyhcf101f3_0.conda + sha256: dfb681579be59c2e790c95f7f49b7529a9b0511d6385ad276e3c8988cbd54d2c + md5: 4bada6a6d908a27262af8ebddf4f7492 depends: - python >=3.10 - python license: BSD-3-Clause license_family: BSD purls: - - pkg:pypi/jsonpointer?source=hash-mapping - size: 14190 - timestamp: 1774311356147 -- conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.26.0-pyhcf101f3_0.conda - sha256: db973a37d75db8e19b5f44bbbdaead0c68dde745407f281e2a7fe4db74ec51d7 - md5: ada41c863af263cc4c5fcbaff7c3e4dc + - pkg:pypi/traitlets?source=hash-mapping + size: 115165 + timestamp: 1778074251714 +- conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.15.0-h396c80c_0.conda + sha256: 7c2df5721c742c2a47b2c8f960e718c930031663ac1174da67c1ed5999f7938c + md5: edd329d7d3a4ab45dcf905899a7a6115 + depends: + - typing_extensions ==4.15.0 pyhcf101f3_0 + license: PSF-2.0 + license_family: PSF + purls: [] + size: 91383 + timestamp: 1756220668932 +- conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda + sha256: 032271135bca55aeb156cee361c81350c6f3fb203f57d024d7e5a1fc9ef18731 + md5: 0caa1af407ecff61170c9437a808404d depends: - - attrs >=22.2.0 - - jsonschema-specifications >=2023.3.6 - python >=3.10 - - referencing >=0.28.4 - - rpds-py >=0.25.0 - python + license: PSF-2.0 + license_family: PSF + purls: + - pkg:pypi/typing-extensions?source=hash-mapping + size: 51692 + timestamp: 1756220668932 +- conda: https://conda.anaconda.org/conda-forge/noarch/typing_utils-0.1.0-pyhd8ed1ab_1.conda + sha256: 3088d5d873411a56bf988eee774559335749aed6f6c28e07bf933256afb9eb6c + md5: f6d7aa696c67756a650e91e15e88223c + depends: + - python >=3.9 + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/typing-utils?source=hash-mapping + size: 15183 + timestamp: 1733331395943 +- conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + sha256: 1d30098909076af33a35017eed6f2953af1c769e273a0626a04722ac4acaba3c + md5: ad659d0a2b3e47e38d829aa8cad2d610 + license: LicenseRef-Public-Domain + purls: [] + size: 119135 + timestamp: 1767016325805 +- conda: https://conda.anaconda.org/conda-forge/noarch/uri-template-1.3.0-pyhd8ed1ab_1.conda + sha256: e0eb6c8daf892b3056f08416a96d68b0a358b7c46b99c8a50481b22631a4dfc0 + md5: e7cb0f5745e4c5035a460248334af7eb + depends: + - python >=3.9 license: MIT license_family: MIT purls: - - pkg:pypi/jsonschema?source=hash-mapping - size: 82356 - timestamp: 1767839954256 -- conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.9.1-pyhcf101f3_0.conda - sha256: 0a4f3b132f0faca10c89fdf3b60e15abb62ded6fa80aebfc007d05965192aa04 - md5: 439cd0f567d697b20a8f45cb70a1005a + - pkg:pypi/uri-template?source=hash-mapping + size: 23990 + timestamp: 1733323714454 +- conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.7.0-pyhd8ed1ab_0.conda + sha256: feff959a816f7988a0893201aa9727bbb7ee1e9cec2c4f0428269b489eb93fb4 + md5: cbb88288f74dbe6ada1c6c7d0a97223e depends: + - backports.zstd >=1.0.0 + - brotli-python >=1.2.0 + - h2 >=4,<5 + - pysocks >=1.5.6,<2.0,!=1.5.7 - python >=3.10 - - referencing >=0.31.0 - - python license: MIT license_family: MIT purls: - - pkg:pypi/jsonschema-specifications?source=hash-mapping - size: 19236 - timestamp: 1757335715225 -- conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-with-format-nongpl-4.26.0-hcf101f3_0.conda - sha256: 6886fc61e4e4edd38fd38729976b134e8bd2143f7fce56cc80d7ac7bac99bce1 - md5: 8368d58342d0825f0843dc6acdd0c483 + - pkg:pypi/urllib3?source=hash-mapping + size: 103560 + timestamp: 1778188657149 +- conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.7.0-pyhd8ed1ab_0.conda + sha256: 1ee2d8384972ecbf8630ce8a3ea9d16858358ad3e8566675295e66996d5352da + md5: eb9538b8e55069434a18547f43b96059 depends: - - jsonschema >=4.26.0,<4.26.1.0a0 - - fqdn - - idna - - isoduration - - jsonpointer >1.13 - - rfc3339-validator - - rfc3986-validator >0.1.0 - - rfc3987-syntax >=1.1.0 - - uri-template - - webcolors >=24.6.0 + - python >=3.10 license: MIT license_family: MIT - purls: [] - size: 4740 - timestamp: 1767839954258 -- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter-lsp-2.3.1-pyhcf101f3_0.conda - sha256: 3766e2ae59641c172cec8a821528bfa6bf9543ffaaeb8b358bfd5259dcf18e4e - md5: 0c3b465ceee138b9c39279cc02e5c4a0 + purls: + - pkg:pypi/wcwidth?source=hash-mapping + size: 82917 + timestamp: 1777744489106 +- conda: https://conda.anaconda.org/conda-forge/noarch/webcolors-25.10.0-pyhd8ed1ab_0.conda + sha256: 21f6c8a20fe050d09bfda3fb0a9c3493936ce7d6e1b3b5f8b01319ee46d6c6f6 + md5: 6639b6b0d8b5a284f027a2003669aa65 depends: - - importlib-metadata >=4.8.3 - - jupyter_server >=1.1.2 - python >=3.10 - - python license: BSD-3-Clause license_family: BSD purls: - - pkg:pypi/jupyter-lsp?source=compressed-mapping - size: 61633 - timestamp: 1775136333147 -- 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' -- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.8.0-pyhcf101f3_0.conda - sha256: e402bd119720862a33229624ec23645916a7d47f30e1711a4af9e005162b84f3 - md5: 8a3d6d0523f66cf004e563a50d9392b3 + - pkg:pypi/webcolors?source=hash-mapping + size: 18987 + timestamp: 1761899393153 +- conda: https://conda.anaconda.org/conda-forge/noarch/webencodings-0.5.1-pyhd8ed1ab_3.conda + sha256: 19ff205e138bb056a46f9e3839935a2e60bd1cf01c8241a5e172a422fed4f9c6 + md5: 2841eb5bfc75ce15e9a0054b98dcd64d depends: - - jupyter_core >=5.1 - - python >=3.10 - - python-dateutil >=2.8.2 - - pyzmq >=25.0 - - tornado >=6.4.1 - - traitlets >=5.3 - - python + - python >=3.9 license: BSD-3-Clause license_family: BSD purls: - - pkg:pypi/jupyter-client?source=hash-mapping - size: 112785 - timestamp: 1767954655912 -- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.9.1-pyh6dadd2b_0.conda - sha256: ed709a6c25b731e01563521ef338b93986cd14b5bc17f35e9382000864872ccc - md5: a8db462b01221e9f5135be466faeb3e0 + - pkg:pypi/webencodings?source=hash-mapping + size: 15496 + timestamp: 1733236131358 +- conda: https://conda.anaconda.org/conda-forge/noarch/websocket-client-1.9.0-pyhd8ed1ab_0.conda + sha256: 42a2b61e393e61cdf75ced1f5f324a64af25f347d16c60b14117393a98656397 + md5: 2f1ed718fcd829c184a6d4f0f2e07409 depends: - - __win - - pywin32 - - platformdirs >=2.5 - python >=3.10 - - traitlets >=5.3 - - python - constrains: - - pywin32 >=300 - license: BSD-3-Clause - license_family: BSD + license: Apache-2.0 + license_family: APACHE purls: - - pkg:pypi/jupyter-core?source=hash-mapping - size: 64679 - timestamp: 1760643889625 -- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.9.1-pyhc90fa1f_0.conda - sha256: 1d34b80e5bfcd5323f104dbf99a2aafc0e5d823019d626d0dce5d3d356a2a52a - md5: b38fe4e78ee75def7e599843ef4c1ab0 + - pkg:pypi/websocket-client?source=hash-mapping + size: 61391 + timestamp: 1759928175142 +- conda: https://conda.anaconda.org/conda-forge/noarch/win_inet_pton-1.1.0-pyh7428d3b_8.conda + sha256: 93807369ab91f230cf9e6e2a237eaa812492fe00face5b38068735858fba954f + md5: 46e441ba871f524e2b067929da3051c2 depends: - - __unix - - python - - platformdirs >=2.5 - - python >=3.10 - - traitlets >=5.3 - - python - constrains: - - pywin32 >=300 - license: BSD-3-Clause - license_family: BSD + - __win + - python >=3.9 + license: LicenseRef-Public-Domain purls: - - pkg:pypi/jupyter-core?source=hash-mapping - size: 65503 - timestamp: 1760643864586 -- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_events-0.12.1-pyhcf101f3_0.conda - sha256: c7edb5682c6316a95ad781dccb1b6589cd2ec0bf94f23c21152974eb0363b5d7 - md5: bf42ee94c750c0b2e7e998b79ac299ea + - pkg:pypi/win-inet-pton?source=hash-mapping + size: 9555 + timestamp: 1733130678956 +- conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.1-pyhcf101f3_0.conda + sha256: 523616c0530d305d2216c2b4a8dfd3872628b60083255b89c5e0d8c42e738cca + md5: e1c36c6121a7c9c76f2f148f1e83b983 depends: - - jsonschema-with-format-nongpl >=4.18.0 - - packaging - python >=3.10 - - python-json-logger >=2.0.4 - - pyyaml >=5.3 - - referencing - - rfc3339-validator - - rfc3986-validator >=0.1.1 - - traitlets >=5.3 - python - license: BSD-3-Clause - license_family: BSD + license: MIT + license_family: MIT purls: - - pkg:pypi/jupyter-events?source=compressed-mapping - size: 24002 - timestamp: 1776861872237 -- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.17.0-pyhcf101f3_0.conda - sha256: 74c4e642be97c538dae1895f7052599dfd740d8bd251f727bce6453ce8d6cd9a - md5: d79a87dcfa726bcea8e61275feed6f83 + - pkg:pypi/zipp?source=hash-mapping + size: 24461 + timestamp: 1776131454755 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/_openmp_mutex-4.5-7_kmp_llvm.conda + build_number: 7 + sha256: 7acaa2e0782cad032bdaf756b536874346ac1375745fb250e9bdd6a48a7ab3cd + md5: a44032f282e7d2acdeb1c240308052dd depends: - - anyio >=3.1.0 - - argon2-cffi >=21.1 - - jinja2 >=3.0.3 - - jupyter_client >=7.4.4 - - jupyter_core >=4.12,!=5.0.* - - jupyter_events >=0.11.0 - - jupyter_server_terminals >=0.4.4 - - nbconvert-core >=6.4.4 - - nbformat >=5.3.0 - - overrides >=5.0 - - packaging >=22.0 - - prometheus_client >=0.9 - - python >=3.10 - - pyzmq >=24 - - send2trash >=1.8.2 - - terminado >=0.8.3 - - tornado >=6.2.0 - - traitlets >=5.6.0 - - websocket-client >=1.7 - - python + - llvm-openmp >=9.0.1 license: BSD-3-Clause license_family: BSD + purls: [] + size: 8325 + timestamp: 1764092507920 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/argon2-cffi-bindings-25.1.0-py312h4409184_2.conda + sha256: 24c475f6f7abf03ef3cc2ac572b7a6d713bede00ef984591be92cdc439b09fbc + md5: 0a2a07b42db3f92b8dccf0f60b5ebee8 + depends: + - __osx >=11.0 + - cffi >=1.0.1 + - python >=3.12,<3.13.0a0 + - python >=3.12,<3.13.0a0 *_cpython + - python_abi 3.12.* *_cp312 + license: MIT + license_family: MIT + purls: + - pkg:pypi/argon2-cffi-bindings?source=hash-mapping + size: 34224 + timestamp: 1762509989973 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/argon2-cffi-bindings-25.1.0-py314h0612a62_2.conda + sha256: aab60bbaea5cc49dff37438d1ad469d64025cda2ce58103cf68da61701ed2075 + md5: a240a79a49a95b388ef81ccda27a5e51 + depends: + - __osx >=11.0 + - cffi >=2.0.0b1 + - python >=3.14,<3.15.0a0 + - python >=3.14,<3.15.0a0 *_cp314 + - python_abi 3.14.* *_cp314 + license: MIT + license_family: MIT purls: - - pkg:pypi/jupyter-server?source=hash-mapping - size: 347094 - timestamp: 1755870522134 -- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server_terminals-0.5.4-pyhcf101f3_0.conda - sha256: 5eda79ed9f53f590031d29346abd183051263227dd9ee667b5ca1133ce297654 - md5: 7b8bace4943e0dc345fc45938826f2b8 + - pkg:pypi/argon2-cffi-bindings?source=hash-mapping + size: 34218 + timestamp: 1762509977830 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/backports.zstd-1.5.0-py312h87c4bb7_0.conda + sha256: a492dcf07b1c58797b3192f11aef7e3beb18ec91646d6a5acfe5c6e61e66118d + md5: 6ec306e02579965dc9c01092a5f4ce4c depends: - - python >=3.10 - - terminado >=0.8.3 - python - license: BSD-3-Clause - license_family: BSD + - __osx >=11.0 + - zstd >=1.5.7,<1.6.0a0 + - python_abi 3.12.* *_cp312 + license: BSD-3-Clause AND MIT AND EPL-2.0 purls: - - pkg:pypi/jupyter-server-terminals?source=hash-mapping - size: 22052 - timestamp: 1768574057200 -- conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.5.7-pyhd8ed1ab_0.conda - sha256: b85befad5ba1f50c0cc042a2ffb26441d13ffc2f18572dc20d3541476da0c7b9 - md5: 2ffe77234070324e763a6eddabb5f467 + - pkg:pypi/backports-zstd?source=compressed-mapping + size: 240840 + timestamp: 1778594074672 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-python-1.2.0-py312h0dfefe5_1.conda + sha256: 6178775a86579d5e8eec6a7ab316c24f1355f6c6ccbe84bb341f342f1eda2440 + md5: 311fcf3f6a8c4eb70f912798035edd35 depends: - - async-lru >=1.0.0 - - httpx >=0.25.0,<1 - - ipykernel >=6.5.0,!=6.30.0 - - jinja2 >=3.0.3 - - jupyter-lsp >=2.0.0 - - jupyter_core - - jupyter_server >=2.4.0,<3 - - jupyterlab_server >=2.28.0,<3 - - notebook-shim >=0.2 - - packaging - - python >=3.10 - - setuptools >=41.1.0 - - tomli >=1.2.2 - - tornado >=6.2.0 - - traitlets - license: BSD-3-Clause - license_family: BSD + - __osx >=11.0 + - libcxx >=19 + - python >=3.12,<3.13.0a0 + - python >=3.12,<3.13.0a0 *_cpython + - python_abi 3.12.* *_cp312 + constrains: + - libbrotlicommon 1.2.0 hc919400_1 + license: MIT + license_family: MIT purls: - - pkg:pypi/jupyterlab?source=compressed-mapping - size: 8861204 - timestamp: 1777483115382 -- 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' -- conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_pygments-0.3.0-pyhd8ed1ab_2.conda - sha256: dc24b900742fdaf1e077d9a3458fd865711de80bca95fe3c6d46610c532c6ef0 - md5: fd312693df06da3578383232528c468d + - pkg:pypi/brotli?source=hash-mapping + size: 359503 + timestamp: 1764018572368 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-python-1.2.0-py314h3daef5d_1.conda + sha256: 5c2e471fd262fcc3c5a9d5ea4dae5917b885e0e9b02763dbd0f0d9635ed4cb99 + md5: f9501812fe7c66b6548c7fcaa1c1f252 depends: - - pygments >=2.4.1,<3 - - python >=3.9 + - __osx >=11.0 + - libcxx >=19 + - python >=3.14,<3.15.0a0 + - python >=3.14,<3.15.0a0 *_cp314 + - python_abi 3.14.* *_cp314 constrains: - - jupyterlab >=4.0.8,<5.0.0 - license: BSD-3-Clause + - libbrotlicommon 1.2.0 hc919400_1 + license: MIT + license_family: MIT + purls: + - pkg:pypi/brotli?source=hash-mapping + size: 359854 + timestamp: 1764018178608 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-hd037594_9.conda + sha256: 540fe54be35fac0c17feefbdc3e29725cce05d7367ffedfaaa1bdda234b019df + md5: 620b85a3f45526a8bc4d23fd78fc22f0 + depends: + - __osx >=11.0 + license: bzip2-1.0.6 license_family: BSD + purls: [] + size: 124834 + timestamp: 1771350416561 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/c-ares-1.34.6-hc919400_0.conda + sha256: 2995f2aed4e53725e5efbc28199b46bf311c3cab2648fc4f10c2227d6d5fa196 + md5: bcb3cba70cf1eec964a03b4ba7775f01 + depends: + - __osx >=11.0 + license: MIT + license_family: MIT + purls: [] + size: 180327 + timestamp: 1765215064054 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/cffi-2.0.0-py312h1b4d9a2_1.conda + sha256: 597e986ac1a1bd1c9b29d6850e1cdea4a075ce8292af55568952ec670e7dd358 + md5: 503ac138ad3cfc09459738c0f5750705 + depends: + - __osx >=11.0 + - libffi >=3.5.2,<3.6.0a0 + - pycparser + - python >=3.12,<3.13.0a0 + - python >=3.12,<3.13.0a0 *_cpython + - python_abi 3.12.* *_cp312 + license: MIT + license_family: MIT purls: - - pkg:pypi/jupyterlab-pygments?source=hash-mapping - size: 18711 - timestamp: 1733328194037 -- conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_server-2.28.0-pyhcf101f3_0.conda - sha256: 381d2d6a259a3be5f38a69463e0f6c5dcf1844ae113058007b51c3bef13a7cee - md5: a63877cb23de826b1620d3adfccc4014 + - pkg:pypi/cffi?source=hash-mapping + size: 288080 + timestamp: 1761203317419 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/cffi-2.0.0-py314h44086f9_1.conda + sha256: 5b5ee5de01eb4e4fd2576add5ec9edfc654fbaf9293e7b7ad2f893a67780aa98 + md5: 10dd19e4c797b8f8bdb1ec1fbb6821d7 + depends: + - __osx >=11.0 + - libffi >=3.5.2,<3.6.0a0 + - pycparser + - python >=3.14,<3.15.0a0 + - python >=3.14,<3.15.0a0 *_cp314 + - python_abi 3.14.* *_cp314 + license: MIT + license_family: MIT + purls: + - pkg:pypi/cffi?source=hash-mapping + size: 292983 + timestamp: 1761203354051 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/debugpy-1.8.20-py312h6510ced_0.conda + sha256: f0ca130b5ffd6949673d3c61d7b8562ab76ad8debafb83f8b3443d30c172f5eb + md5: da3b5efcb0caabcede61a6ce4e0a7669 depends: - - babel >=2.10 - - jinja2 >=3.0.3 - - json5 >=0.9.0 - - jsonschema >=4.18 - - jupyter_server >=1.21,<3 - - packaging >=21.3 - - python >=3.10 - - requests >=2.31 - python - license: BSD-3-Clause - license_family: BSD + - __osx >=11.0 + - python 3.12.* *_cpython + - libcxx >=19 + - python_abi 3.12.* *_cp312 + license: MIT + license_family: MIT purls: - - pkg:pypi/jupyterlab-server?source=hash-mapping - size: 51621 - timestamp: 1761145478692 -- pypi: https://files.pythonhosted.org/packages/b3/52/bc858b1665d0dec3a2511f4e6f5c18ea85c0977563d624d597c95d6d0fd7/jupyterquiz-2.9.6.4-py2.py3-none-any.whl - name: jupyterquiz - version: 2.9.6.4 - sha256: f8c4418f6c827454523fc882a30d744b585cb58ac1ae277769c3059d04fc272b -- pypi: https://files.pythonhosted.org/packages/16/5a/736dd2f4535dbf3bf26523f9158c011389ef88dd06ec2eef67fd744f1c7b/jupytext-1.19.1-py3-none-any.whl - name: jupytext - version: 1.19.1 - sha256: d8975035155d034bdfde5c0c37891425314b7ea8d3a6c4b5d18c294348714cd9 - requires_dist: - - markdown-it-py>=1.0 - - mdit-py-plugins - - nbformat - - packaging - - pyyaml - - tomli ; python_full_version < '3.11' - - autopep8 ; extra == 'dev' - - black ; extra == 'dev' - - flake8 ; extra == 'dev' - - gitpython ; extra == 'dev' - - ipykernel ; extra == 'dev' - - isort ; extra == 'dev' - - jupyter-fs[fs]>=1.0 ; extra == 'dev' - - jupyter-server!=2.11 ; extra == 'dev' - - marimo>=0.17.6,<=0.19.4 ; extra == 'dev' - - nbconvert ; extra == 'dev' - - pre-commit ; extra == 'dev' - - pytest ; extra == 'dev' - - pytest-asyncio ; extra == 'dev' - - pytest-cov>=2.6.1 ; extra == 'dev' - - pytest-randomly ; extra == 'dev' - - pytest-xdist ; extra == 'dev' - - sphinx ; extra == 'dev' - - sphinx-gallery>=0.8 ; extra == 'dev' - - myst-parser ; extra == 'docs' - - sphinx ; extra == 'docs' - - sphinx-copybutton ; extra == 'docs' - - sphinx-rtd-theme ; extra == 'docs' - - pytest ; extra == 'test' - - pytest-asyncio ; extra == 'test' - - pytest-randomly ; extra == 'test' - - pytest-xdist ; extra == 'test' - - black ; extra == 'test-cov' - - ipykernel ; extra == 'test-cov' - - jupyter-server!=2.11 ; extra == 'test-cov' - - nbconvert ; extra == 'test-cov' - - pytest ; extra == 'test-cov' - - pytest-asyncio ; extra == 'test-cov' - - pytest-cov>=2.6.1 ; extra == 'test-cov' - - pytest-randomly ; extra == 'test-cov' - - pytest-xdist ; extra == 'test-cov' - - autopep8 ; extra == 'test-external' - - black ; extra == 'test-external' - - flake8 ; extra == 'test-external' - - gitpython ; extra == 'test-external' - - ipykernel ; extra == 'test-external' - - isort ; extra == 'test-external' - - jupyter-fs[fs]>=1.0 ; extra == 'test-external' - - jupyter-server!=2.11 ; extra == 'test-external' - - marimo>=0.17.6,<=0.19.4 ; extra == 'test-external' - - nbconvert ; extra == 'test-external' - - pre-commit ; extra == 'test-external' - - pytest ; extra == 'test-external' - - pytest-asyncio ; extra == 'test-external' - - pytest-randomly ; extra == 'test-external' - - pytest-xdist ; extra == 'test-external' - - sphinx ; extra == 'test-external' - - sphinx-gallery>=0.8 ; extra == 'test-external' - - black ; extra == 'test-functional' - - pytest ; extra == 'test-functional' - - pytest-asyncio ; extra == 'test-functional' - - pytest-randomly ; extra == 'test-functional' - - pytest-xdist ; extra == 'test-functional' - - black ; extra == 'test-integration' - - ipykernel ; extra == 'test-integration' - - jupyter-server!=2.11 ; extra == 'test-integration' - - nbconvert ; extra == 'test-integration' - - pytest ; extra == 'test-integration' - - pytest-asyncio ; extra == 'test-integration' - - pytest-randomly ; extra == 'test-integration' - - pytest-xdist ; extra == 'test-integration' - - bash-kernel ; extra == 'test-ui' - requires_python: '>=3.9' -- conda: https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.3-hb9d3cd8_0.conda - sha256: 0960d06048a7185d3542d850986d807c6e37ca2e644342dd0c72feefcf26c2a4 - md5: b38117a3c920364aff79f870c984b4a3 + - pkg:pypi/debugpy?source=hash-mapping + size: 2752978 + timestamp: 1769744996462 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/debugpy-1.8.20-py314he609de1_0.conda + sha256: 7736a82ebe75c0f3ea6991298363d1f2edb34291f8616c1d3719862881c3a167 + md5: 407c74dc27356ba6bf3a0191070e3ac0 + depends: + - python + - python 3.14.* *_cp314 + - __osx >=11.0 + - libcxx >=19 + - python_abi 3.14.* *_cp314 + license: MIT + license_family: MIT + purls: + - pkg:pypi/debugpy?source=hash-mapping + size: 2778080 + timestamp: 1769745040206 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/gsl-2.8-h8d0574d_1.conda + sha256: f11d8f2007f6591022afa958d8fe15afbe4211198d1603c0eb886bc21a9eb19e + md5: cc261442bead590d89ca9f96884a344f depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - license: LGPL-2.1-or-later + - __osx >=11.0 + - libblas >=3.9.0,<4.0a0 + - libcblas >=3.9.0,<4.0a0 + license: GPL-3.0-or-later + license_family: GPL purls: [] - size: 134088 - timestamp: 1754905959823 -- pypi: https://files.pythonhosted.org/packages/49/b2/97980f3ad4fae37dd7fe31626e2bf75fbf8bdf5d303950ec1fab39a12da8/kiwisolver-1.5.0-cp314-cp314-macosx_11_0_arm64.whl - name: kiwisolver - version: 1.5.0 - sha256: 0cbe94b69b819209a62cb27bdfa5dc2a8977d8de2f89dfd97ba4f53ed3af754e - requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/99/9f/795fedf35634f746151ca8839d05681ceb6287fbed6cc1c9bf235f7887c2/kiwisolver-1.5.0-cp312-cp312-macosx_11_0_arm64.whl - name: kiwisolver - version: 1.5.0 - sha256: ed3a984b31da7481b103f68776f7128a89ef26ed40f4dc41a2223cda7fb24819 - requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/a3/36/4e551e8aa55c9188bca9abb5096805edbf7431072b76e2298e34fd3a3008/kiwisolver-1.5.0-cp314-cp314-win_amd64.whl - name: kiwisolver - version: 1.5.0 - sha256: d76e2d8c75051d58177e762164d2e9ab92886534e3a12e795f103524f221dd8e - requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/ad/cf/0348374369ca588f8fe9c338fae49fa4e16eeb10ffb3d012f23a54578a9e/kiwisolver-1.5.0-cp312-cp312-win_amd64.whl - name: kiwisolver - version: 1.5.0 - sha256: f18c2d9782259a6dc132fdc7a63c168cbc74b35284b6d75c673958982a378384 - requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/c4/13/680c54afe3e65767bed7ec1a15571e1a2f1257128733851ade24abcefbcc/kiwisolver-1.5.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - name: kiwisolver - version: 1.5.0 - sha256: bb5136fb5352d3f422df33f0c879a1b0c204004324150cc3b5e3c4f310c9049f - requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/e7/f9/b06c934a6aa8bc91f566bd2a214fd04c30506c2d9e2b6b171953216a65b6/kiwisolver-1.5.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - name: kiwisolver - version: 1.5.0 - sha256: 80aa065ffd378ff784822a6d7c3212f2d5f5e9c3589614b5c228b311fd3063ac - requires_python: '>=3.10' -- conda: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.22.2-ha1258a1_0.conda - sha256: 3e307628ca3527448dd1cb14ad7bb9d04d1d28c7d4c5f97ba196ae984571dd25 - md5: fb53fb07ce46a575c5d004bbc96032c2 + size: 1862134 + timestamp: 1737621413640 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/icu-78.3-hef89b57_0.conda + sha256: 3a7907a17e9937d3a46dfd41cffaf815abad59a569440d1e25177c15fd0684e5 + md5: f1182c91c0de31a7abd40cedf6a5ebef depends: - - __glibc >=2.17,<3.0.a0 - - keyutils >=1.6.3,<2.0a0 - - libedit >=3.1.20250104,<3.2.0a0 - - libedit >=3.1.20250104,<4.0a0 - - libgcc >=14 - - libstdcxx >=14 - - openssl >=3.5.5,<4.0a0 + - __osx >=11.0 license: MIT license_family: MIT purls: [] - size: 1386730 - timestamp: 1769769569681 + size: 12361647 + timestamp: 1773822915649 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/krb5-1.22.2-h385eeb1_0.conda sha256: c0a0bf028fe7f3defcdcaa464e536cf1b202d07451e18ad83fdd169d15bef6ed md5: e446e1822f4da8e5080a9de93474184d @@ -6158,71 +6584,6 @@ packages: purls: [] size: 1160828 timestamp: 1769770119811 -- conda: https://conda.anaconda.org/conda-forge/win-64/krb5-1.22.2-h0ea6238_0.conda - sha256: eb60f1ad8b597bcf95dee11bc11fe71a8325bc1204cf51d2bb1f2120ffd77761 - md5: 4432f52dc0c8eb6a7a6abc00a037d93c - depends: - - openssl >=3.5.5,<4.0a0 - - ucrt >=10.0.20348.0 - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 - license: MIT - license_family: MIT - purls: [] - size: 751055 - timestamp: 1769769688841 -- conda: https://conda.anaconda.org/conda-forge/noarch/lark-1.3.1-pyhd8ed1ab_0.conda - sha256: 49570840fb15f5df5d4b4464db8ee43a6d643031a2bc70ef52120a52e3809699 - md5: 9b965c999135d43a3d0f7bd7d024e26a - depends: - - python >=3.10 - license: MIT - license_family: MIT - purls: - - pkg:pypi/lark?source=compressed-mapping - size: 94312 - timestamp: 1761596921009 -- 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.1 - license: GPL-3.0-only - license_family: GPL - purls: [] - size: 728002 - timestamp: 1774197446916 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libabseil-20260107.1-cxx17_h7b12aa8_0.conda - sha256: a7a4481a4d217a3eadea0ec489826a69070fcc3153f00443aa491ed21527d239 - md5: 6f7b4302263347698fd24565fbf11310 - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=14 - - libstdcxx >=14 - constrains: - - libabseil-static =20260107.1=cxx17* - - abseil-cpp =20260107.1 - license: Apache-2.0 - license_family: Apache - purls: [] - size: 1384817 - timestamp: 1770863194876 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libabseil-20260107.1-cxx17_h2062a1b_0.conda sha256: 756611fbb8d2957a5b4635d9772bd8432cb6ddac05580a6284cca6fdc9b07fca md5: bb65152e0d7c7178c0f1ee25692c9fd1 @@ -6237,1436 +6598,1110 @@ packages: purls: [] 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 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libblas-3.11.0-7_h51639a9_openblas.conda + build_number: 7 + sha256: 662935bfb93d2d097e26e273a3a2f504a9f833f64aa6f9e295b577655478c39b + md5: ab6670d099d19fe70cb9efb88a1b5f78 depends: - - libopenblas >=0.3.32,<0.3.33.0a0 - - libopenblas >=0.3.32,<1.0a0 + - libopenblas >=0.3.33,<0.3.34.0a0 + - libopenblas >=0.3.33,<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 + - libcblas 3.11.0 7*_openblas + - blas 2.307 openblas + - mkl <2027 + - liblapack 3.11.0 7*_openblas + - liblapacke 3.11.0 7*_openblas license: BSD-3-Clause license_family: BSD purls: [] - size: 18621 - timestamp: 1774503034895 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libblas-3.11.0-6_h51639a9_openblas.conda - build_number: 6 - sha256: 979227fc03628925037ab2dfda008eb7b5592644d9c2c21dd285cefe8c42553d - md5: e551103471911260488a02155cef9c94 + size: 18783 + timestamp: 1778489983152 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlicommon-1.2.0-hc919400_1.conda + sha256: a7cb9e660531cf6fbd4148cff608c85738d0b76f0975c5fc3e7d5e92840b7229 + md5: 006e7ddd8a110771134fcc4e1e3a6ffa + depends: + - __osx >=11.0 + license: MIT + license_family: MIT + purls: [] + size: 79443 + timestamp: 1764017945924 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlidec-1.2.0-hc919400_1.conda + sha256: 2eae444039826db0454b19b52a3390f63bfe24f6b3e63089778dd5a5bf48b6bf + md5: 079e88933963f3f149054eec2c487bc2 + depends: + - __osx >=11.0 + - libbrotlicommon 1.2.0 hc919400_1 + license: MIT + license_family: MIT + purls: [] + size: 29452 + timestamp: 1764017979099 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlienc-1.2.0-hc919400_1.conda + sha256: 01436c32bb41f9cb4bcf07dda647ce4e5deb8307abfc3abdc8da5317db8189d1 + md5: b2b7c8288ca1a2d71ff97a8e6a1e8883 + depends: + - __osx >=11.0 + - libbrotlicommon 1.2.0 hc919400_1 + license: MIT + license_family: MIT + purls: [] + size: 290754 + timestamp: 1764018009077 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcblas-3.11.0-7_hb0561ab_openblas.conda + build_number: 7 + sha256: 3ac3d27022b3ca8b1980c087e0ede250434f6ed90a4fdc78a8a5ed382bc75505 + md5: 189b373453ec3904095dcb16f502bace depends: - - libopenblas >=0.3.32,<0.3.33.0a0 - - libopenblas >=0.3.32,<1.0a0 + - libblas 3.11.0 7_h51639a9_openblas constrains: - - liblapacke 3.11.0 6*_openblas - - liblapack 3.11.0 6*_openblas - - blas 2.306 openblas - - libcblas 3.11.0 6*_openblas - - mkl <2026 + - blas 2.307 openblas + - liblapack 3.11.0 7*_openblas + - liblapacke 3.11.0 7*_openblas license: BSD-3-Clause license_family: BSD purls: [] - 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 + size: 18810 + timestamp: 1778489991330 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-22.1.5-h55c6f16_1.conda + sha256: dddd01bd6b338221342a89530a1caffe6051a70cc8f8b1d8bb591d5447a3c603 + md5: ff484b683fecf1e875dfc7aa01d19796 + depends: + - __osx >=11.0 + license: Apache-2.0 WITH LLVM-exception + license_family: Apache + purls: [] + size: 569359 + timestamp: 1778191546305 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libedit-3.1.20250104-pl5321hafb1f1b_0.conda + sha256: 66aa216a403de0bb0c1340a88d1a06adaff66bae2cfd196731aa24db9859d631 + md5: 44083d2d2c2025afca315c7a172eab2b + depends: + - ncurses + - __osx >=11.0 + - ncurses >=6.5,<7.0a0 + license: BSD-2-Clause + license_family: BSD + purls: [] + size: 107691 + timestamp: 1738479560845 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libev-4.33-h93a5062_2.conda + sha256: 95cecb3902fbe0399c3a7e67a5bed1db813e5ab0e22f4023a5e0f722f2cc214f + md5: 36d33e440c31857372a72137f78bacf5 + license: BSD-2-Clause + license_family: BSD + purls: [] + size: 107458 + timestamp: 1702146414478 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.8.0-hf6b4638_0.conda + sha256: f4b1cafc59afaede8fa0a2d9cf376840f1c553001acd72f6ead18bbc8ac8c49c + md5: 65466e82c09e888ca7560c11a97d5450 + depends: + - __osx >=11.0 + constrains: + - expat 2.8.0.* + license: MIT + license_family: MIT + purls: [] + size: 68789 + timestamp: 1777846180142 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.5.2-hcf2aa1b_0.conda + sha256: 6686a26466a527585e6a75cc2a242bf4a3d97d6d6c86424a441677917f28bec7 + md5: 43c04d9cb46ef176bb2a4c77e324d599 + depends: + - __osx >=11.0 + license: MIT + license_family: MIT + purls: [] + size: 40979 + timestamp: 1769456747661 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgcc-15.2.0-hcbb3090_19.conda + sha256: 06644fa4d34d57c9e48f4d84b1256f9e5f654fdb37f43acc8a58a396952d42b7 + md5: 644058123986582db33aebd4ae2ca184 + depends: + - _openmp_mutex + constrains: + - libgcc-ng ==15.2.0=*_19 + - libgomp 15.2.0 19 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 404080 + timestamp: 1778273064154 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran-15.2.0-h07b0088_19.conda + sha256: d4837b3b9b30af3132d260225e91ab9dde83be04c59513f500cc81050fb37486 + md5: 1ea03f87cdb1078fbc0e2b2deb63752c + depends: + - libgfortran5 15.2.0 hdae7583_19 + constrains: + - libgfortran-ng ==15.2.0=*_19 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 139675 + timestamp: 1778273280875 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran5-15.2.0-hdae7583_19.conda + sha256: d0a68b7a121d115b80c169e24d1265dcc25a3fe58d107df1bbc430797e226d88 + md5: ba36d8c606a6a53fe0b8c12d47267b3d + depends: + - libgcc >=15.2.0 + constrains: + - libgfortran 15.2.0 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 599691 + timestamp: 1778273075448 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.3-h8088a28_0.conda + sha256: 34878d87275c298f1a732c6806349125cebbf340d24c6c23727268184bba051e + md5: b1fd823b5ae54fbec272cea0811bd8a9 + depends: + - __osx >=11.0 + constrains: + - xz 5.8.3.* + license: 0BSD + purls: [] + size: 92472 + timestamp: 1775825802659 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libmpdec-4.0.0-h84a0fba_1.conda + sha256: 1089c7f15d5b62c622625ec6700732ece83be8b705da8c6607f4dabb0c4bd6d2 + md5: 57c4be259f5e0b99a5983799a228ae55 + depends: + - __osx >=11.0 + license: BSD-2-Clause + license_family: BSD + purls: [] + size: 73690 + timestamp: 1769482560514 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libnghttp2-1.68.1-h8f3e76b_0.conda + sha256: 2bc7bc3978066f2c274ebcbf711850cc9ab92e023e433b9631958a098d11e10a + md5: 6ea18834adbc3b33df9bd9fb45eaf95b + depends: + - __osx >=11.0 + - c-ares >=1.34.6,<2.0a0 + - libcxx >=19 + - libev >=4.33,<4.34.0a0 + - libev >=4.33,<5.0a0 + - libzlib >=1.3.1,<2.0a0 + - openssl >=3.5.5,<4.0a0 + license: MIT + license_family: MIT + purls: [] + size: 576526 + timestamp: 1773854624224 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopenblas-0.3.33-openmp_he657e61_0.conda + sha256: 9dd455b2d172aeedfa2058d324b5b5822b0bc1b7c1f32cd183d7078540d2f6eb + md5: 909e41855c29f0d52ae630198cd57135 depends: - - mkl >=2025.3.1,<2026.0a0 + - __osx >=11.0 + - libgfortran + - libgfortran5 >=14.3.0 + - llvm-openmp >=19.1.7 constrains: - - blas 2.306 mkl - - liblapacke 3.11.0 6*_mkl - - liblapack 3.11.0 6*_mkl - - libcblas 3.11.0 6*_mkl + - openblas >=0.3.33,<0.3.34.0a0 license: BSD-3-Clause license_family: BSD purls: [] - size: 68082 - timestamp: 1774503684284 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlicommon-1.2.0-hb03c661_1.conda - sha256: 318f36bd49ca8ad85e6478bd8506c88d82454cc008c1ac1c6bf00a3c42fa610e - md5: 72c8fd1af66bd67bf580645b426513ed - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=14 - license: MIT - license_family: MIT - purls: [] - size: 79965 - timestamp: 1764017188531 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlicommon-1.2.0-hc919400_1.conda - sha256: a7cb9e660531cf6fbd4148cff608c85738d0b76f0975c5fc3e7d5e92840b7229 - md5: 006e7ddd8a110771134fcc4e1e3a6ffa + size: 4304965 + timestamp: 1776995497368 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsodium-1.0.21-h1a92334_3.conda + sha256: df603472ea1ebd8e7d4fb71e4360fe48d10b11c240df51c129de1da2ff9e8227 + md5: 7cc5247987e6d115134ebab15186bc13 depends: - __osx >=11.0 - license: MIT - license_family: MIT + license: ISC purls: [] - size: 79443 - timestamp: 1764017945924 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlidec-1.2.0-hb03c661_1.conda - sha256: 12fff21d38f98bc446d82baa890e01fd82e3b750378fedc720ff93522ffb752b - md5: 366b40a69f0ad6072561c1d09301c886 + size: 248039 + timestamp: 1772479570912 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.53.1-h1b79a29_0.conda + sha256: 49daec7c83e70d4efc17b813547824bc2bcf2f7256d84061d24fbfe537da9f74 + md5: 6681822ea9d362953206352371b6a904 depends: - - __glibc >=2.17,<3.0.a0 - - libbrotlicommon 1.2.0 hb03c661_1 - - libgcc >=14 - license: MIT - license_family: MIT + - __osx >=11.0 + - libzlib >=1.3.2,<2.0a0 + license: blessing purls: [] - size: 34632 - timestamp: 1764017199083 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlidec-1.2.0-hc919400_1.conda - sha256: 2eae444039826db0454b19b52a3390f63bfe24f6b3e63089778dd5a5bf48b6bf - md5: 079e88933963f3f149054eec2c487bc2 + size: 920047 + timestamp: 1777987051643 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libuv-1.51.0-h6caf38d_1.conda + sha256: 042c7488ad97a5629ec0a991a8b2a3345599401ecc75ad6a5af73b60e6db9689 + md5: c0d87c3c8e075daf1daf6c31b53e8083 depends: - __osx >=11.0 - - libbrotlicommon 1.2.0 hc919400_1 license: MIT license_family: MIT purls: [] - size: 29452 - timestamp: 1764017979099 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlienc-1.2.0-hb03c661_1.conda - sha256: a0c15c79997820bbd3fbc8ecf146f4fe0eca36cc60b62b63ac6cf78857f1dd0d - md5: 4ffbb341c8b616aa2494b6afb26a0c5f + size: 421195 + timestamp: 1753948426421 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.2-h8088a28_2.conda + sha256: 361415a698514b19a852f5d1123c5da746d4642139904156ddfca7c922d23a05 + md5: bc5a5721b6439f2f62a84f2548136082 depends: - - __glibc >=2.17,<3.0.a0 - - libbrotlicommon 1.2.0 hb03c661_1 - - libgcc >=14 - license: MIT - license_family: MIT + - __osx >=11.0 + constrains: + - zlib 1.3.2 *_2 + license: Zlib + license_family: Other purls: [] - size: 298378 - timestamp: 1764017210931 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlienc-1.2.0-hc919400_1.conda - sha256: 01436c32bb41f9cb4bcf07dda647ce4e5deb8307abfc3abdc8da5317db8189d1 - md5: b2b7c8288ca1a2d71ff97a8e6a1e8883 + size: 47759 + timestamp: 1774072956767 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/llvm-openmp-22.1.5-hc7d1edf_1.conda + sha256: 2cd49562feda2bf324651050b2b035080fd635ed0f1c96c9ce7a59eff3cc0029 + md5: 8a4e2a54034b35bc6fa5bf9282913f45 depends: - __osx >=11.0 - - libbrotlicommon 1.2.0 hc919400_1 - license: MIT - license_family: MIT + constrains: + - openmp 22.1.5|22.1.5.* + - intel-openmp <0.0a0 + license: Apache-2.0 WITH LLVM-exception + license_family: APACHE purls: [] - size: 290754 - timestamp: 1764018009077 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.11.0-6_h0358290_openblas.conda - build_number: 6 - sha256: 57edafa7796f6fa3ebbd5367692dd4c7f552be42109c2dd1a7c89b55089bf374 - md5: 36ae340a916635b97ac8a0655ace2a35 + size: 285806 + timestamp: 1778447786965 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/markupsafe-3.0.3-py312h04c11ed_1.conda + sha256: 330394fb9140995b29ae215a19fad46fcc6691bdd1b7654513d55a19aaa091c1 + md5: 11d95ab83ef0a82cc2de12c1e0b47fe4 depends: - - libblas 3.11.0 6_h4a7cf45_openblas + - __osx >=11.0 + - python >=3.12,<3.13.0a0 + - python >=3.12,<3.13.0a0 *_cpython + - python_abi 3.12.* *_cp312 constrains: - - blas 2.306 openblas - - liblapack 3.11.0 6*_openblas - - liblapacke 3.11.0 6*_openblas + - jinja2 >=3.0.0 license: BSD-3-Clause license_family: BSD - purls: [] - size: 18622 - timestamp: 1774503050205 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcblas-3.11.0-6_hb0561ab_openblas.conda - build_number: 6 - sha256: 2e6b3e9b1ab672133b70fc6730e42290e952793f132cb5e72eee22835463eba0 - md5: 805c6d31c5621fd75e53dfcf21fb243a + purls: + - pkg:pypi/markupsafe?source=hash-mapping + size: 25564 + timestamp: 1772445846939 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/markupsafe-3.0.3-py314h6e9b3f0_1.conda + sha256: 411153d14ee0d98be6e3751cf5cc0502db17bce2deebebb8779e33d29d0e525f + md5: d33c0a15882b70255abdd54711b06a45 depends: - - libblas 3.11.0 6_h51639a9_openblas + - __osx >=11.0 + - python >=3.14,<3.15.0a0 + - python >=3.14,<3.15.0a0 *_cp314 + - python_abi 3.14.* *_cp314 constrains: - - liblapacke 3.11.0 6*_openblas - - blas 2.306 openblas - - liblapack 3.11.0 6*_openblas + - jinja2 >=3.0.0 license: BSD-3-Clause license_family: BSD - purls: [] - 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 + purls: + - pkg:pypi/markupsafe?source=hash-mapping + size: 27256 + timestamp: 1772445397216 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/msgspec-0.21.1-py312h2bbb03f_0.conda + sha256: 50e284832520f08ef1e37e0ca20459f5df2c048f59dfba1f2e3ee0ccfe7be317 + md5: ae340bdc5bdf5abd3183c5962517cbde depends: - - libblas 3.11.0 6_hf2e6a31_mkl - constrains: - - blas 2.306 mkl - - liblapacke 3.11.0 6*_mkl - - liblapack 3.11.0 6*_mkl + - __osx >=11.0 + - python >=3.12,<3.13.0a0 + - python >=3.12,<3.13.0a0 *_cpython + - python_abi 3.12.* *_cp312 license: BSD-3-Clause license_family: BSD - purls: [] - size: 68221 - timestamp: 1774503722413 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-22.1.4-h55c6f16_0.conda - sha256: 25a0d02148a39b665d9c2957676faf62a4d2a58494d53b201151199a197db4b0 - md5: 448a1af83a9205655ee1cf48d3875ca3 + purls: + - pkg:pypi/msgspec?source=hash-mapping + size: 212357 + timestamp: 1776338798628 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/msgspec-0.21.1-py314h6c2aa35_0.conda + sha256: 24a9105921e94fa526ffde1e956fa550c48ddb9ce4b0cf19ae22e79ed267261e + md5: 26fce586b13842a0f9f9a3aabae3e943 depends: - __osx >=11.0 - license: Apache-2.0 WITH LLVM-exception - license_family: Apache - purls: [] - size: 569927 - timestamp: 1776816293111 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20250104-pl5321h7949ede_0.conda - sha256: d789471216e7aba3c184cd054ed61ce3f6dac6f87a50ec69291b9297f8c18724 - md5: c277e0a4d549b03ac1e9d6cbbe3d017b - depends: - - ncurses - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - ncurses >=6.5,<7.0a0 - license: BSD-2-Clause + - python >=3.14,<3.15.0a0 + - python >=3.14,<3.15.0a0 *_cp314 + - python_abi 3.14.* *_cp314 + license: BSD-3-Clause license_family: BSD + purls: + - pkg:pypi/msgspec?source=hash-mapping + size: 216965 + timestamp: 1776338889692 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.6-h1d4f5a5_0.conda + sha256: 4ea6c620b87bd1d42bb2ccc2c87cd2483fa2d7f9e905b14c223f11ff3f4c455d + md5: 343d10ed5b44030a2f67193905aea159 + depends: + - __osx >=11.0 + license: X11 AND BSD-3-Clause purls: [] - size: 134676 - timestamp: 1738479519902 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libedit-3.1.20250104-pl5321hafb1f1b_0.conda - sha256: 66aa216a403de0bb0c1340a88d1a06adaff66bae2cfd196731aa24db9859d631 - md5: 44083d2d2c2025afca315c7a172eab2b + size: 805509 + timestamp: 1777423252320 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/nodejs-25.8.2-h7039424_0.conda + sha256: 4782b172b3b8a557b60bf5f591821cf100e2092ba7a5494ce047dfa41626de26 + md5: ca8277c52fdface8bb8ebff7cd9a6f56 depends: - - ncurses + - libcxx >=19 - __osx >=11.0 - - ncurses >=6.5,<7.0a0 - license: BSD-2-Clause - license_family: BSD + - icu >=78.3,<79.0a0 + - libbrotlicommon >=1.2.0,<1.3.0a0 + - libbrotlienc >=1.2.0,<1.3.0a0 + - libbrotlidec >=1.2.0,<1.3.0a0 + - libnghttp2 >=1.68.1,<2.0a0 + - libuv >=1.51.0,<2.0a0 + - libsqlite >=3.52.0,<4.0a0 + - libzlib >=1.3.2,<2.0a0 + - openssl >=3.5.5,<4.0a0 + - zstd >=1.5.7,<1.6.0a0 + - c-ares >=1.34.6,<2.0a0 + - libabseil >=20260107.1,<20260108.0a0 + - libabseil * cxx17* + license: MIT + license_family: MIT purls: [] - size: 107691 - timestamp: 1738479560845 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libev-4.33-hd590300_2.conda - sha256: 1cd6048169fa0395af74ed5d8f1716e22c19a81a8a36f934c110ca3ad4dd27b4 - md5: 172bf1cd1ff8629f2b1179945ed45055 + size: 17101803 + timestamp: 1774517834028 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.6.2-hd24854e_0.conda + sha256: c91bf510c130a1ea1b6ff023e28bac0ccaef869446acd805e2016f69ebdc49ea + md5: 25dcccd4f80f1638428613e0d7c9b4e1 depends: - - libgcc-ng >=12 - license: BSD-2-Clause - license_family: BSD + - __osx >=11.0 + - ca-certificates + license: Apache-2.0 + license_family: Apache purls: [] - size: 112766 - timestamp: 1702146165126 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libev-4.33-h93a5062_2.conda - sha256: 95cecb3902fbe0399c3a7e67a5bed1db813e5ab0e22f4023a5e0f722f2cc214f - md5: 36d33e440c31857372a72137f78bacf5 - license: BSD-2-Clause + size: 3106008 + timestamp: 1775587972483 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/psutil-7.2.2-py312hb3ab3e3_0.conda + sha256: 6d0e21c76436374635c074208cfeee62a94d3c37d0527ad67fd8a7615e546a05 + md5: fd856899666759403b3c16dcba2f56ff + depends: + - python + - __osx >=11.0 + - python 3.12.* *_cpython + - python_abi 3.12.* *_cp312 + license: BSD-3-Clause license_family: BSD - purls: [] - size: 107458 - timestamp: 1702146414478 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.5-hecca717_0.conda - sha256: e8c2b57f6aacabdf2f1b0924bd4831ce5071ba080baa4a9e8c0d720588b6794c - md5: 49f570f3bc4c874a06ea69b7225753af + purls: + - pkg:pypi/psutil?source=hash-mapping + size: 239031 + timestamp: 1769678393511 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/psutil-7.2.2-py314ha14b1ff_0.conda + sha256: e0f31c053eb11803d63860c213b2b1b57db36734f5f84a3833606f7c91fedff9 + md5: fc4c7ab223873eee32080d51600ce7e7 depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=14 - constrains: - - expat 2.7.5.* - license: MIT - license_family: MIT - purls: [] - size: 76624 - timestamp: 1774719175983 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.7.5-hf6b4638_0.conda - sha256: 06780dec91dd25770c8cf01e158e1062fbf7c576b1406427475ce69a8af75b7e - md5: a32123f93e168eaa4080d87b0fb5da8a + - python + - __osx >=11.0 + - python 3.14.* *_cp314 + - python_abi 3.14.* *_cp314 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/psutil?source=hash-mapping + size: 245502 + timestamp: 1769678303655 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyobjc-core-12.1-py312h19bbe71_0.conda + sha256: b015f430fe9ea2c53e14be13639f1b781f68deaa5ae74cd8c1d07720890cd02a + md5: c65d7abdc9e60fd3af0ed852591adf1b depends: - __osx >=11.0 - constrains: - - expat 2.7.5.* - license: MIT - license_family: MIT - purls: [] - size: 68192 - timestamp: 1774719211725 -- conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.7.5-hac47afa_0.conda - sha256: 6850c3a4d5dc215b86f58518cfb8752998533d6569b08da8df1da72e7c68e571 - md5: bfb43f52f13b7c56e7677aa7a8efdf0c - depends: - - ucrt >=10.0.20348.0 - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 - constrains: - - expat 2.7.5.* + - libffi >=3.5.2,<3.6.0a0 + - python >=3.12,<3.13.0a0 + - python >=3.12,<3.13.0a0 *_cpython + - python_abi 3.12.* *_cp312 + - setuptools license: MIT license_family: MIT - purls: [] - size: 70609 - timestamp: 1774719377850 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h3435931_0.conda - sha256: 31f19b6a88ce40ebc0d5a992c131f57d919f73c0b92cd1617a5bec83f6e961e6 - md5: a360c33a5abe61c07959e449fa1453eb + purls: + - pkg:pypi/pyobjc-core?source=hash-mapping + size: 476750 + timestamp: 1763151865523 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyobjc-core-12.1-py314h3a4d195_0.conda + sha256: df5af268c5a74b7160d772c263ece6f43257faff571783443e34b5f1d5a61cf2 + md5: 75a84fc8337557347252cc4fd3ba2a93 depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=14 + - __osx >=11.0 + - libffi >=3.5.2,<3.6.0a0 + - python >=3.14,<3.15.0a0 + - python >=3.14,<3.15.0a0 *_cp314 + - python_abi 3.14.* *_cp314 + - setuptools license: MIT license_family: MIT - purls: [] - size: 58592 - timestamp: 1769456073053 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.5.2-hcf2aa1b_0.conda - sha256: 6686a26466a527585e6a75cc2a242bf4a3d97d6d6c86424a441677917f28bec7 - md5: 43c04d9cb46ef176bb2a4c77e324d599 + purls: + - pkg:pypi/pyobjc-core?source=hash-mapping + size: 483374 + timestamp: 1763151489724 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyobjc-framework-cocoa-12.1-py312h1de3e18_0.conda + sha256: 3710f5ae09c2ea77ba4d82cc51e876d9fc009b878b197a40d3c6347c09ae7d7c + md5: f0bae1b67ece138378923e340b940051 depends: - __osx >=11.0 + - libffi >=3.5.2,<3.6.0a0 + - pyobjc-core 12.1.* + - python >=3.12,<3.13.0a0 + - python >=3.12,<3.13.0a0 *_cpython + - python_abi 3.12.* *_cp312 license: MIT license_family: MIT - purls: [] - size: 40979 - timestamp: 1769456747661 -- conda: https://conda.anaconda.org/conda-forge/win-64/libffi-3.5.2-h3d046cb_0.conda - sha256: 59d01f2dfa8b77491b5888a5ab88ff4e1574c9359f7e229da254cdfe27ddc190 - md5: 720b39f5ec0610457b725eb3f396219a + purls: + - pkg:pypi/pyobjc-framework-cocoa?source=hash-mapping + size: 377723 + timestamp: 1763160705325 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyobjc-framework-cocoa-12.1-py314h36abed7_0.conda + sha256: aa76ee4328d0514d7c1c455dcd2d3b547db1c59797e54ce0a3f27de5b970e508 + md5: 4219bb3408016e22316cf8b443b5ef93 depends: - - ucrt >=10.0.20348.0 - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 + - __osx >=11.0 + - libffi >=3.5.2,<3.6.0a0 + - pyobjc-core 12.1.* + - python >=3.14,<3.15.0a0 + - python >=3.14,<3.15.0a0 *_cp314 + - python_abi 3.14.* *_cp314 license: MIT license_family: MIT - purls: [] - 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=*_18 - - libgomp 15.2.0 he0feb66_18 - license: GPL-3.0-only WITH GCC-exception-3.1 - license_family: GPL - purls: [] - size: 1041788 - timestamp: 1771378212382 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgcc-15.2.0-hcbb3090_18.conda - sha256: 1d9c4f35586adb71bcd23e31b68b7f3e4c4ab89914c26bed5f2859290be5560e - md5: 92df6107310b1fff92c4cc84f0de247b - depends: - - _openmp_mutex - constrains: - - 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: 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_18 - license: GPL-3.0-only WITH GCC-exception-3.1 - license_family: GPL - purls: [] - size: 27526 - timestamp: 1771378224552 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.2.0-h69a702a_18.conda - sha256: d2c9fad338fd85e4487424865da8e74006ab2e2475bd788f624d7a39b2a72aee - md5: 9063115da5bc35fdc3e1002e69b9ef6e - depends: - - libgfortran5 15.2.0 h68bc16d_18 - constrains: - - libgfortran-ng ==15.2.0=*_18 - license: GPL-3.0-only WITH GCC-exception-3.1 - license_family: GPL - purls: [] - size: 27523 - timestamp: 1771378269450 -- 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_18 - constrains: - - libgfortran-ng ==15.2.0=*_18 - license: GPL-3.0-only WITH GCC-exception-3.1 - license_family: GPL - purls: [] - 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 - constrains: - - libgfortran 15.2.0 - license: GPL-3.0-only WITH GCC-exception-3.1 - license_family: GPL - purls: [] - size: 2482475 - timestamp: 1771378241063 -- 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: - - libgfortran 15.2.0 - license: GPL-3.0-only WITH GCC-exception-3.1 - license_family: GPL - purls: [] - 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: 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 - - libxml2-16 >=2.14.6 - - ucrt >=10.0.20348.0 - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 - license: BSD-3-Clause - license_family: BSD - purls: [] - size: 2411241 - timestamp: 1765104337762 -- conda: https://conda.anaconda.org/conda-forge/win-64/libiconv-1.18-hc1393d2_2.conda - sha256: 0dcdb1a5f01863ac4e8ba006a8b0dc1a02d2221ec3319b5915a1863254d7efa7 - md5: 64571d1dd6cdcfa25d0664a5950fdaa2 - depends: - - ucrt >=10.0.20348.0 - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 - license: LGPL-2.1-only - purls: [] - size: 696926 - timestamp: 1754909290005 -- conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.3-hb03c661_0.conda - sha256: ec30e52a3c1bf7d0425380a189d209a52baa03f22fb66dd3eb587acaa765bd6d - md5: b88d90cad08e6bc8ad540cb310a761fb - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=14 - constrains: - - xz 5.8.3.* - license: 0BSD - purls: [] - size: 113478 - timestamp: 1775825492909 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.3-h8088a28_0.conda - sha256: 34878d87275c298f1a732c6806349125cebbf340d24c6c23727268184bba051e - md5: b1fd823b5ae54fbec272cea0811bd8a9 + purls: + - pkg:pypi/pyobjc-framework-cocoa?source=hash-mapping + size: 374792 + timestamp: 1763160601898 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.12.13-h8561d8f_0_cpython.conda + sha256: e658e647a4a15981573d6018928dec2c448b10c77c557c29872043ff23c0eb6a + md5: 8e7608172fa4d1b90de9a745c2fd2b81 depends: - __osx >=11.0 + - bzip2 >=1.0.8,<2.0a0 + - libexpat >=2.7.4,<3.0a0 + - libffi >=3.5.2,<3.6.0a0 + - liblzma >=5.8.2,<6.0a0 + - libsqlite >=3.51.2,<4.0a0 + - libzlib >=1.3.1,<2.0a0 + - ncurses >=6.5,<7.0a0 + - openssl >=3.5.5,<4.0a0 + - readline >=8.3,<9.0a0 + - tk >=8.6.13,<8.7.0a0 + - tzdata constrains: - - xz 5.8.3.* - license: 0BSD - purls: [] - size: 92472 - timestamp: 1775825802659 -- conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-5.8.3-hfd05255_0.conda - sha256: d636d1a25234063642f9c531a7bb58d84c1c496411280a36ea000bd122f078f1 - md5: 8f83619ab1588b98dd99c90b0bfc5c6d - depends: - - ucrt >=10.0.20348.0 - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 - constrains: - - xz 5.8.3.* - license: 0BSD - purls: [] - size: 106486 - timestamp: 1775825663227 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb03c661_1.conda - sha256: fe171ed5cf5959993d43ff72de7596e8ac2853e9021dec0344e583734f1e0843 - md5: 2c21e66f50753a083cbe6b80f38268fa - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=14 - license: BSD-2-Clause - license_family: BSD + - python_abi 3.12.* *_cp312 + license: Python-2.0 purls: [] - size: 92400 - timestamp: 1769482286018 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libmpdec-4.0.0-h84a0fba_1.conda - sha256: 1089c7f15d5b62c622625ec6700732ece83be8b705da8c6607f4dabb0c4bd6d2 - md5: 57c4be259f5e0b99a5983799a228ae55 + size: 12127424 + timestamp: 1772730755512 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.14.4-h4c637c5_100_cp314.conda + build_number: 100 + sha256: 27e7d6cbe021f37244b643f06a98e46767255f7c2907108dd3736f042757ddad + md5: e1bc5a3015a4bbeb304706dba5a32b7f depends: - __osx >=11.0 - license: BSD-2-Clause - license_family: BSD - purls: [] - 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.3,<15 - - vc14_runtime >=14.44.35208 - license: BSD-2-Clause - license_family: BSD + - bzip2 >=1.0.8,<2.0a0 + - libexpat >=2.7.5,<3.0a0 + - libffi >=3.5.2,<3.6.0a0 + - liblzma >=5.8.2,<6.0a0 + - libmpdec >=4.0.0,<5.0a0 + - libsqlite >=3.52.0,<4.0a0 + - libzlib >=1.3.2,<2.0a0 + - ncurses >=6.5,<7.0a0 + - openssl >=3.5.6,<4.0a0 + - python_abi 3.14.* *_cp314 + - readline >=8.3,<9.0a0 + - tk >=8.6.13,<8.7.0a0 + - tzdata + - zstd >=1.5.7,<1.6.0a0 + license: Python-2.0 purls: [] - size: 89411 - timestamp: 1769482314283 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libnghttp2-1.68.1-h877daf1_0.conda - sha256: 663444d77a42f2265f54fb8b48c5450bfff4388d9c0f8253dd7855f0d993153f - md5: 2a45e7f8af083626f009645a6481f12d + size: 13533346 + timestamp: 1775616188373 + python_site_packages_path: lib/python3.14/site-packages +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyyaml-6.0.3-py312h04c11ed_1.conda + sha256: 737959262d03c9c305618f2d48c7f1691fb996f14ae420bfd05932635c99f873 + md5: 95a5f0831b5e0b1075bbd80fcffc52ac depends: - - __glibc >=2.17,<3.0.a0 - - c-ares >=1.34.6,<2.0a0 - - libev >=4.33,<4.34.0a0 - - libev >=4.33,<5.0a0 - - libgcc >=14 - - libstdcxx >=14 - - libzlib >=1.3.1,<2.0a0 - - openssl >=3.5.5,<4.0a0 + - __osx >=11.0 + - python >=3.12,<3.13.0a0 + - python >=3.12,<3.13.0a0 *_cpython + - python_abi 3.12.* *_cp312 + - yaml >=0.2.5,<0.3.0a0 license: MIT license_family: MIT - purls: [] - size: 663344 - timestamp: 1773854035739 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libnghttp2-1.68.1-h8f3e76b_0.conda - sha256: 2bc7bc3978066f2c274ebcbf711850cc9ab92e023e433b9631958a098d11e10a - md5: 6ea18834adbc3b33df9bd9fb45eaf95b + purls: + - pkg:pypi/pyyaml?source=hash-mapping + size: 187278 + timestamp: 1770223990452 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyyaml-6.0.3-py314h6e9b3f0_1.conda + sha256: 95f385f9606e30137cf0b5295f63855fd22223a4cf024d306cf9098ea1c4a252 + md5: dcf51e564317816cb8d546891019b3ab depends: - __osx >=11.0 - - c-ares >=1.34.6,<2.0a0 - - libcxx >=19 - - libev >=4.33,<4.34.0a0 - - libev >=4.33,<5.0a0 - - libzlib >=1.3.1,<2.0a0 - - openssl >=3.5.5,<4.0a0 + - python >=3.14,<3.15.0a0 + - python >=3.14,<3.15.0a0 *_cp314 + - python_abi 3.14.* *_cp314 + - yaml >=0.2.5,<0.3.0a0 license: MIT license_family: MIT - purls: [] - size: 576526 - timestamp: 1773854624224 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hb9d3cd8_1.conda - sha256: 927fe72b054277cde6cb82597d0fcf6baf127dcbce2e0a9d8925a68f1265eef5 - md5: d864d34357c3b65a4b731f78c0801dc4 + purls: + - pkg:pypi/pyyaml?source=hash-mapping + size: 189475 + timestamp: 1770223788648 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyzmq-27.1.0-py312h022ad19_2.conda + noarch: python + sha256: 2f31f799a46ed75518fae0be75ecc8a1b84360dbfd55096bc2fe8bd9c797e772 + md5: 2f6b79700452ef1e91f45a99ab8ffe5a depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - license: LGPL-2.1-only + - python + - libcxx >=19 + - __osx >=11.0 + - _python_abi3_support 1.* + - cpython >=3.12 + - zeromq >=4.3.5,<4.4.0a0 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/pyzmq?source=hash-mapping + size: 191641 + timestamp: 1771717073430 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.3-h46df422_0.conda + sha256: a77010528efb4b548ac2a4484eaf7e1c3907f2aec86123ed9c5212ae44502477 + md5: f8381319127120ce51e081dce4865cf4 + depends: + - __osx >=11.0 + - ncurses >=6.5,<7.0a0 + license: GPL-3.0-only license_family: GPL purls: [] - size: 33731 - timestamp: 1750274110928 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.32-pthreads_h94d23a6_0.conda - sha256: 6dc30b28f32737a1c52dada10c8f3a41bc9e021854215efca04a7f00487d09d9 - md5: 89d61bc91d3f39fda0ca10fcd3c68594 + size: 313930 + timestamp: 1765813902568 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/rpds-py-0.30.0-py312h6ef9ec0_0.conda + sha256: ea06f6f66b1bea97244c36fd2788ccd92fd1fb06eae98e469dd95ee80831b057 + md5: a7cfbbdeb93bb9a3f249bc4c3569cd4c depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=14 - - libgfortran - - libgfortran5 >=14.3.0 + - python + - __osx >=11.0 + - python 3.12.* *_cpython + - python_abi 3.12.* *_cp312 constrains: - - openblas >=0.3.32,<0.3.33.0a0 - license: BSD-3-Clause - license_family: BSD - purls: [] - size: 5928890 - timestamp: 1774471724897 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopenblas-0.3.32-openmp_he657e61_0.conda - sha256: 713e453bde3531c22a660577e59bf91ef578dcdfd5edb1253a399fa23514949a - md5: 3a1111a4b6626abebe8b978bb5a323bf + - __osx >=11.0 + license: MIT + license_family: MIT + purls: + - pkg:pypi/rpds-py?source=hash-mapping + size: 358853 + timestamp: 1764543161524 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/rpds-py-0.30.0-py314haad56a0_0.conda + sha256: e161dd97403b8b8a083d047369a5cf854557dba1204d29e2f0250f5ac4403925 + md5: 76a4f88d1b7748c477abf3c341edc64c depends: + - python - __osx >=11.0 - - libgfortran - - libgfortran5 >=14.3.0 - - llvm-openmp >=19.1.7 + - python 3.14.* *_cp314 + - python_abi 3.14.* *_cp314 constrains: - - openblas >=0.3.32,<0.3.33.0a0 - license: BSD-3-Clause + - __osx >=11.0 + license: MIT + license_family: MIT + purls: + - pkg:pypi/rpds-py?source=hash-mapping + size: 350976 + timestamp: 1764543169524 +- 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: 4308797 - timestamp: 1774472508546 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libsodium-1.0.21-h280c20c_3.conda - sha256: 64e5c80cbce4680a2d25179949739a6def695d72c40ca28f010711764e372d97 - md5: 7af961ef4aa2c1136e11dd43ded245ab + size: 3127137 + timestamp: 1769460817696 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/tornado-6.5.5-py312h2bbb03f_0.conda + sha256: 29edd36311b4a810a9e6208437bdbedb28c9ac15221caf812cb5c5cf48375dca + md5: 02cce5319b0f1317d9642dcb2e475379 depends: - - libgcc >=14 - - __glibc >=2.17,<3.0.a0 - license: ISC - purls: [] - size: 277661 - timestamp: 1772479381288 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsodium-1.0.21-h1a92334_3.conda - sha256: df603472ea1ebd8e7d4fb71e4360fe48d10b11c240df51c129de1da2ff9e8227 - md5: 7cc5247987e6d115134ebab15186bc13 + - __osx >=11.0 + - python >=3.12,<3.13.0a0 + - python >=3.12,<3.13.0a0 *_cpython + - python_abi 3.12.* *_cp312 + license: Apache-2.0 + license_family: Apache + purls: + - pkg:pypi/tornado?source=hash-mapping + size: 859155 + timestamp: 1774358568476 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/tornado-6.5.5-py314h6c2aa35_0.conda + sha256: 4ccc4a20d676c0ba85adee9c99015bec7f5b685df0cf8006e34573f1d6c2ce75 + md5: 3f81f8b2fe2c26a82c0abf57ab2b9610 depends: - __osx >=11.0 - license: ISC - purls: [] - size: 248039 - timestamp: 1772479570912 -- conda: https://conda.anaconda.org/conda-forge/win-64/libsodium-1.0.21-h6a83c73_3.conda - sha256: d915f4fa8ebbf237c7a6e511ed458f2cfdc7c76843a924740318a15d0dd33d6d - md5: da2aa614d16a795b3007b6f4a1318a81 + - python >=3.14,<3.15.0a0 + - python >=3.14,<3.15.0a0 *_cp314 + - python_abi 3.14.* *_cp314 + license: Apache-2.0 + license_family: Apache + purls: + - pkg:pypi/tornado?source=hash-mapping + size: 910845 + timestamp: 1774358965067 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/yaml-0.2.5-h925e9cb_3.conda + sha256: b03433b13d89f5567e828ea9f1a7d5c5d697bf374c28a4168d71e9464f5dafac + md5: 78a0fe9e9c50d2c381e8ee47e3ea437d depends: - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 - - ucrt >=10.0.20348.0 - license: ISC + - __osx >=11.0 + license: MIT + license_family: MIT purls: [] - size: 276860 - timestamp: 1772479407566 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.53.0-hf4e2dac_0.conda - sha256: ec37c79f737933bbac965f5dc0f08ef2790247129a84bb3114fad4900adce401 - md5: 810d83373448da85c3f673fbcb7ad3a3 + size: 83386 + timestamp: 1753484079473 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/zeromq-4.3.5-h4818236_10.conda + sha256: 2705360c72d4db8de34291493379ffd13b09fd594d0af20c9eefa8a3f060d868 + md5: e85dcd3bde2b10081cdcaeae15797506 depends: - - __glibc >=2.17,<3.0.a0 - - icu >=78.3,<79.0a0 - - libgcc >=14 - - libzlib >=1.3.2,<2.0a0 - license: blessing + - __osx >=11.0 + - libcxx >=19 + - krb5 >=1.22.2,<1.23.0a0 + - libsodium >=1.0.21,<1.0.22.0a0 + license: MPL-2.0 + license_family: MOZILLA purls: [] - size: 958864 - timestamp: 1775753750179 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.53.0-h1b79a29_0.conda - sha256: 1a9d1e3e18dbb0b87cff3b40c3e42703730d7ac7ee9b9322c2682196a81ba0c3 - md5: 8423c008105df35485e184066cad4566 + size: 245246 + timestamp: 1772476886668 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hbf9d68e_6.conda + sha256: 9485ba49e8f47d2b597dd399e88f4802e100851b27c21d7525625b0b4025a5d9 + md5: ab136e4c34e97f34fb621d2592a393d8 depends: - __osx >=11.0 - - libzlib >=1.3.2,<2.0a0 - license: blessing + - libzlib >=1.3.1,<2.0a0 + license: BSD-3-Clause + license_family: BSD purls: [] - size: 920039 - timestamp: 1775754485962 -- conda: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.53.0-hf5d6505_0.conda - sha256: 7a6256ea136936df4c4f3b227ba1e273b7d61152f9811b52157af497f07640b0 - md5: 4152b5a8d2513fd7ae9fb9f221a5595d + size: 433413 + timestamp: 1764777166076 +- conda: https://conda.anaconda.org/conda-forge/win-64/argon2-cffi-bindings-25.1.0-py312he06e257_2.conda + sha256: 38c5e43d991b0c43713fa2ceba3063afa4ccad2dd4c8eb720143de54d461a338 + md5: 5dc3781bbc4ddce0bf250a04c1a192c2 depends: + - cffi >=1.0.1 + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 - ucrt >=10.0.20348.0 - vc >=14.3,<15 - vc14_runtime >=14.44.35208 - license: blessing - purls: [] - size: 1301855 - timestamp: 1775753831574 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h934c35e_18.conda - sha256: 78668020064fdaa27e9ab65cd2997e2c837b564ab26ce3bf0e58a2ce1a525c6e - md5: 1b08cd684f34175e4514474793d44bcb - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc 15.2.0 he0feb66_18 - constrains: - - libstdcxx-ng ==15.2.0=*_18 - license: GPL-3.0-only WITH GCC-exception-3.1 - license_family: GPL - purls: [] - size: 5852330 - timestamp: 1771378262446 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.42-h5347b49_0.conda - sha256: bc1b08c92626c91500fd9f26f2c797f3eb153b627d53e9c13cd167f1e12b2829 - md5: 38ffe67b78c9d4de527be8315e5ada2c - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=14 - license: BSD-3-Clause - license_family: BSD - purls: [] - size: 40297 - timestamp: 1775052476770 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libuv-1.51.0-hb03c661_1.conda - sha256: c180f4124a889ac343fc59d15558e93667d894a966ec6fdb61da1604481be26b - md5: 0f03292cc56bf91a077a134ea8747118 - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=14 license: MIT license_family: MIT - purls: [] - size: 895108 - timestamp: 1753948278280 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libuv-1.51.0-h6caf38d_1.conda - sha256: 042c7488ad97a5629ec0a991a8b2a3345599401ecc75ad6a5af73b60e6db9689 - md5: c0d87c3c8e075daf1daf6c31b53e8083 + purls: + - pkg:pypi/argon2-cffi-bindings?source=hash-mapping + size: 38535 + timestamp: 1762509763237 +- conda: https://conda.anaconda.org/conda-forge/win-64/argon2-cffi-bindings-25.1.0-py314h5a2d7ad_2.conda + sha256: a742e7cd0d5534bfff3fd550a0c1e430411fad60a24f88930d261056ab08096f + md5: ffa247e46f47e157851dc547f4c513e4 depends: - - __osx >=11.0 + - cffi >=2.0.0b1 + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 license: MIT license_family: MIT - purls: [] - size: 421195 - timestamp: 1753948426421 -- conda: https://conda.anaconda.org/conda-forge/win-64/libwinpthread-12.0.0.r4.gg4f2fc60ca-h57928b3_10.conda - sha256: 0fccf2d17026255b6e10ace1f191d0a2a18f2d65088fd02430be17c701f8ffe0 - md5: 8a86073cf3b343b87d03f41790d8b4e5 - depends: - - ucrt - constrains: - - pthreads-win32 <0.0a0 - - msys2-conda-epoch <0.0a0 - license: MIT AND BSD-3-Clause-Clear - purls: [] - size: 36621 - timestamp: 1759768399557 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda - sha256: 6ae68e0b86423ef188196fff6207ed0c8195dd84273cb5623b85aa08033a410c - md5: 5aa797f8787fe7a17d1b0821485b5adc + purls: + - pkg:pypi/argon2-cffi-bindings?source=hash-mapping + size: 38653 + timestamp: 1762509771011 +- conda: https://conda.anaconda.org/conda-forge/win-64/backports.zstd-1.5.0-py312h06d0912_0.conda + sha256: 55173c22b24fd257851f2967d4b0256172be3455bd5246b6b7a5c21eb0863f98 + md5: 891112b1a79fc9800317c5d56e056a8b depends: - - libgcc-ng >=12 - license: LGPL-2.1-or-later - purls: [] - size: 100393 - timestamp: 1702724383534 -- conda: https://conda.anaconda.org/conda-forge/win-64/libxml2-2.15.3-hbc0d294_0.conda - sha256: da68af9d9d28d65a6916db1bef68f8a25c64c4fdcf759f32a2d2f2f143220adf - md5: e3b5acbb857a12f5d59e8d174bc536c0 + - python + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - zstd >=1.5.7,<1.6.0a0 + - python_abi 3.12.* *_cp312 + license: BSD-3-Clause AND MIT AND EPL-2.0 + purls: + - pkg:pypi/backports-zstd?source=compressed-mapping + size: 238601 + timestamp: 1778594083648 +- conda: https://conda.anaconda.org/conda-forge/win-64/brotli-python-1.2.0-py312hc6d9e41_1.conda + sha256: 2bb6f384a51929ef2d5d6039fcf6c294874f20aaab2f63ca768cbe462ed4b379 + md5: e8e7a6346a9e50d19b4daf41f367366f depends: - - libiconv >=1.18,<2.0a0 - - liblzma >=5.8.3,<6.0a0 - - libxml2-16 2.15.3 h692994f_0 - - libzlib >=1.3.2,<2.0a0 + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 - ucrt >=10.0.20348.0 - vc >=14.3,<15 - vc14_runtime >=14.44.35208 constrains: - - icu <0.0a0 + - libbrotlicommon 1.2.0 hfd05255_1 license: MIT license_family: MIT - purls: [] - size: 43916 - timestamp: 1776376994334 -- conda: https://conda.anaconda.org/conda-forge/win-64/libxml2-16-2.15.3-h692994f_0.conda - sha256: 8038084c60eda2006d0122d05e3364fe8db0a18935ca6ed0168b5ba5aa33f904 - md5: f7d6fcda29570e20851b78d92ea2154e + purls: + - pkg:pypi/brotli?source=hash-mapping + size: 335482 + timestamp: 1764018063640 +- conda: https://conda.anaconda.org/conda-forge/win-64/brotli-python-1.2.0-py314he701e3d_1.conda + sha256: 6854ee7675135c57c73a04849c29cbebc2fb6a3a3bfee1f308e64bf23074719b + md5: 1302b74b93c44791403cbeee6a0f62a3 depends: - - libiconv >=1.18,<2.0a0 - - liblzma >=5.8.3,<6.0a0 - - libzlib >=1.3.2,<2.0a0 + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 - ucrt >=10.0.20348.0 - vc >=14.3,<15 - vc14_runtime >=14.44.35208 constrains: - - libxml2 2.15.3 - - icu <0.0a0 + - libbrotlicommon 1.2.0 hfd05255_1 license: MIT license_family: MIT - purls: [] - size: 518869 - timestamp: 1776376971242 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.2-h25fd6f3_2.conda - sha256: 55044c403570f0dc26e6364de4dc5368e5f3fc7ff103e867c487e2b5ab2bcda9 - md5: d87ff7921124eccd67248aa483c23fec - depends: - - __glibc >=2.17,<3.0.a0 - constrains: - - zlib 1.3.2 *_2 - license: Zlib - license_family: Other - purls: [] - size: 63629 - timestamp: 1774072609062 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.2-h8088a28_2.conda - sha256: 361415a698514b19a852f5d1123c5da746d4642139904156ddfca7c922d23a05 - md5: bc5a5721b6439f2f62a84f2548136082 - depends: - - __osx >=11.0 - constrains: - - zlib 1.3.2 *_2 - license: Zlib - license_family: Other - purls: [] - size: 47759 - timestamp: 1774072956767 -- conda: https://conda.anaconda.org/conda-forge/win-64/libzlib-1.3.2-hfd05255_2.conda - sha256: 88609816e0cc7452bac637aaf65783e5edf4fee8a9f8e22bdc3a75882c536061 - md5: dbabbd6234dea34040e631f87676292f + purls: + - pkg:pypi/brotli?source=hash-mapping + size: 335782 + timestamp: 1764018443683 +- conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h0ad9c76_9.conda + sha256: 76dfb71df5e8d1c4eded2dbb5ba15bb8fb2e2b0fe42d94145d5eed4c75c35902 + md5: 4cb8e6b48f67de0b018719cdf1136306 depends: - ucrt >=10.0.20348.0 - vc >=14.3,<15 - vc14_runtime >=14.44.35208 - constrains: - - zlib 1.3.2 *_2 - license: Zlib - license_family: Other + license: bzip2-1.0.6 + license_family: BSD purls: [] - size: 58347 - timestamp: 1774072851498 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/llvm-openmp-22.1.4-hc7d1edf_0.conda - sha256: a269273ccf48be6ac582bb958713ba8373262b9157a0fc76b7e5475e8a1d2a78 - md5: 46d04a647df7a4525e487d88068d19ef + size: 56115 + timestamp: 1771350256444 +- conda: https://conda.anaconda.org/conda-forge/win-64/cffi-2.0.0-py312he06e257_1.conda + sha256: 3e3bdcb85a2e79fe47d9c8ce64903c76f663b39cb63b8e761f6f884e76127f82 + md5: 46f7dccfee37a52a97c0ed6f33fcf0a3 depends: - - __osx >=11.0 - constrains: - - openmp 22.1.4|22.1.4.* - - intel-openmp <0.0a0 - license: Apache-2.0 WITH LLVM-exception - license_family: APACHE - purls: [] - size: 286406 - timestamp: 1776846235007 -- conda: https://conda.anaconda.org/conda-forge/win-64/llvm-openmp-22.1.4-h4fa8253_0.conda - sha256: 7d827f8c125ac2fe3a9d5b47c1f95fc540bb8ef78685e4bcf941957257bb1eff - md5: 761757ab617e8bfef18cc422dd02bbad + - pycparser + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: MIT + license_family: MIT + purls: + - pkg:pypi/cffi?source=hash-mapping + size: 291324 + timestamp: 1761203195397 +- conda: https://conda.anaconda.org/conda-forge/win-64/cffi-2.0.0-py314h5a2d7ad_1.conda + sha256: 924f2f01fa7a62401145ef35ab6fc95f323b7418b2644a87fea0ea68048880ed + md5: c360170be1c9183654a240aadbedad94 depends: + - pycparser + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 - ucrt >=10.0.20348.0 - vc >=14.3,<15 - vc14_runtime >=14.44.35208 - constrains: - - intel-openmp <0.0a0 - - openmp 22.1.4|22.1.4.* - license: Apache-2.0 WITH LLVM-exception - license_family: APACHE - purls: [] - size: 347999 - timestamp: 1776846360348 -- pypi: https://files.pythonhosted.org/packages/38/7e/7b91c89a4cf0f543a83be978657afb20c86af6d725253e319589dcc4ce52/lmfit-1.3.4-py3-none-any.whl - name: lmfit - version: 1.3.4 - sha256: afce1593b42324d37ae2908249b0c55445e2f4c1a0474ff706a8e2f7b5d949fa - requires_dist: - - asteval>=1.0 - - numpy>=1.24 - - scipy>=1.10.0 - - uncertainties>=3.2.2 - - dill>=0.3.4 - - build ; extra == 'dev' - - check-wheel-contents ; extra == 'dev' - - flake8-pyproject ; extra == 'dev' - - pre-commit ; extra == 'dev' - - twine ; extra == 'dev' - - cairosvg ; extra == 'doc' - - corner ; extra == 'doc' - - emcee>=3.0.0 ; extra == 'doc' - - ipykernel ; extra == 'doc' - - jupyter-sphinx>=0.2.4 ; extra == 'doc' - - matplotlib ; extra == 'doc' - - numdifftools ; extra == 'doc' - - pandas ; extra == 'doc' - - pillow ; extra == 'doc' - - pycairo ; sys_platform == 'win32' and extra == 'doc' - - sphinx ; extra == 'doc' - - sphinx-gallery>=0.10 ; extra == 'doc' - - sphinxcontrib-svg2pdfconverter ; extra == 'doc' - - sympy ; extra == 'doc' - - coverage ; extra == 'test' - - flaky ; extra == 'test' - - pytest ; extra == 'test' - - pytest-cov ; extra == 'test' - - lmfit[dev,doc,test] ; extra == 'all' - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/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 - sha256: 26ef1d70928b6057ee3ca12583d73c63e05c49de8972d620c278a7b206581a8a - requires_dist: - - six - - argparse ; python_full_version < '2.7' - - funcsigs ; python_full_version < '3.3' - - rst2ansi ; extra == 'restructuredtext' -- pypi: https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl - name: markdown - version: 3.10.2 - sha256: e91464b71ae3ee7afd3017d9f358ef0baf158fd9a298db92f1d4761133824c36 - requires_dist: - - coverage ; extra == 'testing' - - pyyaml ; extra == 'testing' - - mkdocs>=1.6 ; extra == 'docs' - - mkdocs-nature>=0.6 ; extra == 'docs' - - mdx-gh-links>=0.2 ; extra == 'docs' - - mkdocstrings[python]>=0.28.3 ; extra == 'docs' - - mkdocs-gen-files ; extra == 'docs' - - mkdocs-section-index ; extra == 'docs' - - mkdocs-literate-nav ; extra == 'docs' - requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl - name: markdown-it-py - version: 4.0.0 - sha256: 87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147 - requires_dist: - - mdurl~=0.1 - - psutil ; extra == 'benchmarking' - - pytest ; extra == 'benchmarking' - - pytest-benchmark ; extra == 'benchmarking' - - commonmark~=0.9 ; extra == 'compare' - - markdown~=3.4 ; extra == 'compare' - - mistletoe~=1.0 ; extra == 'compare' - - mistune~=3.0 ; extra == 'compare' - - panflute~=2.3 ; extra == 'compare' - - markdown-it-pyrs ; extra == 'compare' - - linkify-it-py>=1,<3 ; extra == 'linkify' - - mdit-py-plugins>=0.5.0 ; extra == 'plugins' - - gprof2dot ; extra == 'profiling' - - mdit-py-plugins>=0.5.0 ; extra == 'rtd' - - myst-parser ; extra == 'rtd' - - pyyaml ; extra == 'rtd' - - sphinx ; extra == 'rtd' - - sphinx-copybutton ; extra == 'rtd' - - sphinx-design ; extra == 'rtd' - - sphinx-book-theme~=1.0 ; extra == 'rtd' - - jupyter-sphinx ; extra == 'rtd' - - ipykernel ; extra == 'rtd' - - coverage ; extra == 'testing' - - pytest ; extra == 'testing' - - pytest-cov ; extra == 'testing' - - pytest-regressions ; extra == 'testing' - - requests ; extra == 'testing' - requires_python: '>=3.10' -- conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.3-py312h8a5da7c_1.conda - sha256: 5f3aad1f3a685ed0b591faad335957dbdb1b73abfd6fc731a0d42718e0653b33 - md5: 93a4752d42b12943a355b682ee43285b + license: MIT + license_family: MIT + purls: + - pkg:pypi/cffi?source=hash-mapping + size: 294731 + timestamp: 1761203441365 +- conda: https://conda.anaconda.org/conda-forge/win-64/debugpy-1.8.20-py312ha1a9051_0.conda + sha256: 5a886b1af3c66bf58213c7f3d802ea60fe8218313d9072bc1c9e8f7840548ba0 + md5: 032746a0b0663920f0afb18cec61062b depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=14 - - python >=3.12,<3.13.0a0 + - python + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 - python_abi 3.12.* *_cp312 - constrains: - - jinja2 >=3.0.0 - license: BSD-3-Clause - license_family: BSD + license: MIT + license_family: MIT purls: - - pkg:pypi/markupsafe?source=hash-mapping - size: 26057 - timestamp: 1772445297924 -- conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.3-py314h67df5f8_1.conda - sha256: c279be85b59a62d5c52f5dd9a4cd43ebd08933809a8416c22c3131595607d4cf - md5: 9a17c4307d23318476d7fbf0fedc0cde + - pkg:pypi/debugpy?source=hash-mapping + size: 3996113 + timestamp: 1769745013982 +- conda: https://conda.anaconda.org/conda-forge/win-64/debugpy-1.8.20-py314hb98de8c_0.conda + sha256: ece1d8299ad081edaf1e5279f2a900bdedddb2c795ac029a06401543cd7610ad + md5: 48ae8370a4562f7049d587d017792a3a depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=14 - - python >=3.14,<3.15.0a0 + - python + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 - python_abi 3.14.* *_cp314 - constrains: - - jinja2 >=3.0.0 - license: BSD-3-Clause - license_family: BSD + license: MIT + license_family: MIT purls: - - pkg:pypi/markupsafe?source=hash-mapping - size: 27424 - timestamp: 1772445227915 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/markupsafe-3.0.3-py312h04c11ed_1.conda - sha256: 330394fb9140995b29ae215a19fad46fcc6691bdd1b7654513d55a19aaa091c1 - md5: 11d95ab83ef0a82cc2de12c1e0b47fe4 + - pkg:pypi/debugpy?source=hash-mapping + size: 4026404 + timestamp: 1769745008861 +- conda: https://conda.anaconda.org/conda-forge/win-64/gsl-2.8-h5b8d9c4_1.conda + sha256: 87a3468e09cc1ee0268e8639debad6a5b440090ef8cb1d2ee5eed66c86085528 + md5: a47cf810b7c03955139a150b228b93ca depends: - - __osx >=11.0 - - python >=3.12,<3.13.0a0 - - python >=3.12,<3.13.0a0 *_cpython - - python_abi 3.12.* *_cp312 + - libblas >=3.9.0,<4.0a0 + - libcblas >=3.9.0,<4.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + license: GPL-3.0-or-later + license_family: GPL + purls: [] + size: 1528970 + timestamp: 1737622367981 +- conda: https://conda.anaconda.org/conda-forge/win-64/krb5-1.22.2-h0ea6238_0.conda + sha256: eb60f1ad8b597bcf95dee11bc11fe71a8325bc1204cf51d2bb1f2120ffd77761 + md5: 4432f52dc0c8eb6a7a6abc00a037d93c + depends: + - openssl >=3.5.5,<4.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: MIT + license_family: MIT + purls: [] + size: 751055 + timestamp: 1769769688841 +- conda: https://conda.anaconda.org/conda-forge/win-64/libblas-3.11.0-7_h8455456_mkl.conda + build_number: 7 + sha256: 9eec27eee4300284e62a61cb2298089c80d31f6f9e924eeabc06e9788a00cffe + md5: 269e54b44974ff48846b4c31d0397041 + depends: + - mkl >=2026.0.0,<2027.0a0 constrains: - - jinja2 >=3.0.0 + - blas 2.307 mkl + - libcblas 3.11.0 7*_mkl + - liblapacke 3.11.0 7*_mkl + - liblapack 3.11.0 7*_mkl license: BSD-3-Clause license_family: BSD - purls: - - pkg:pypi/markupsafe?source=hash-mapping - size: 25564 - timestamp: 1772445846939 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/markupsafe-3.0.3-py314h6e9b3f0_1.conda - sha256: 411153d14ee0d98be6e3751cf5cc0502db17bce2deebebb8779e33d29d0e525f - md5: d33c0a15882b70255abdd54711b06a45 + purls: [] + size: 68060 + timestamp: 1778490352569 +- conda: https://conda.anaconda.org/conda-forge/win-64/libcblas-3.11.0-7_h2a3cdd5_mkl.conda + build_number: 7 + sha256: 82da0f854831f783f9d3f1219c4255956e8167a474f3f526151128f02453871c + md5: 4700b7af6acefb26ff0127ba68230941 depends: - - __osx >=11.0 - - python >=3.14,<3.15.0a0 - - python >=3.14,<3.15.0a0 *_cp314 - - python_abi 3.14.* *_cp314 + - libblas 3.11.0 7_h8455456_mkl constrains: - - jinja2 >=3.0.0 + - blas 2.307 mkl + - liblapacke 3.11.0 7*_mkl + - liblapack 3.11.0 7*_mkl license: BSD-3-Clause license_family: BSD - purls: - - pkg:pypi/markupsafe?source=compressed-mapping - size: 27256 - timestamp: 1772445397216 -- conda: https://conda.anaconda.org/conda-forge/win-64/markupsafe-3.0.3-py312h05f76fc_1.conda - sha256: b744287a780211ac4595126ef96a44309c791f155d4724021ef99092bae4aace - md5: a73298d225c7852f97403ca105d10a13 + purls: [] + size: 68594 + timestamp: 1778490364980 +- conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.8.0-hac47afa_0.conda + sha256: 2d81d647c1f01108803457cac999b947456f44dd0a3c2325395677feacaeca67 + md5: 264e350e035092b5135a2147c238aec4 depends: - - python >=3.12,<3.13.0a0 - - python_abi 3.12.* *_cp312 - ucrt >=10.0.20348.0 - vc >=14.3,<15 - vc14_runtime >=14.44.35208 constrains: - - jinja2 >=3.0.0 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/markupsafe?source=compressed-mapping - size: 28510 - timestamp: 1772445175216 -- conda: https://conda.anaconda.org/conda-forge/win-64/markupsafe-3.0.3-py314h2359020_1.conda - sha256: 02805a0f3cd168dbf13afc5e4aed75cc00fe538ce143527a6471485b36f5887c - md5: 8de7b40f8b30a8fcaa423c2537fe4199 + - expat 2.8.0.* + license: MIT + license_family: MIT + purls: [] + size: 71094 + timestamp: 1777846223617 +- conda: https://conda.anaconda.org/conda-forge/win-64/libffi-3.5.2-h3d046cb_0.conda + sha256: 59d01f2dfa8b77491b5888a5ab88ff4e1574c9359f7e229da254cdfe27ddc190 + md5: 720b39f5ec0610457b725eb3f396219a depends: - - python >=3.14,<3.15.0a0 - - python_abi 3.14.* *_cp314 - ucrt >=10.0.20348.0 - vc >=14.3,<15 - vc14_runtime >=14.44.35208 - constrains: - - jinja2 >=3.0.0 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/markupsafe?source=hash-mapping - size: 30022 - timestamp: 1772445159549 -- pypi: https://files.pythonhosted.org/packages/04/a1/4571fc46e7702de8d0c2dc54ad1b2f8e29328dea3ee90831181f7353d93c/matplotlib-3.10.9-cp312-cp312-win_amd64.whl - name: matplotlib - version: 3.10.9 - sha256: d091f9d758b34aaaaa6331d13574bf01891d903b3dec59bfff458ef7551de5d6 - requires_dist: - - contourpy>=1.0.1 - - cycler>=0.10 - - fonttools>=4.22.0 - - kiwisolver>=1.3.1 - - numpy>=1.23 - - packaging>=20.0 - - pillow>=8 - - pyparsing>=3 - - python-dateutil>=2.7 - - meson-python>=0.13.1,<0.17.0 ; extra == 'dev' - - pybind11>=2.13.2,!=2.13.3 ; extra == 'dev' - - setuptools-scm>=7,<10 ; extra == 'dev' - - setuptools>=64 ; extra == 'dev' - requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/32/5d/f7e914f7d9325abff4057cee62c0fa70263683189f774473cbfb534cd13b/matplotlib-3.10.9-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - name: matplotlib - version: 3.10.9 - sha256: 34cf8167e023ad956c15f36302911d5406bd99a9862c1a8499ea6f7c0e015dc2 - requires_dist: - - contourpy>=1.0.1 - - cycler>=0.10 - - fonttools>=4.22.0 - - kiwisolver>=1.3.1 - - numpy>=1.23 - - packaging>=20.0 - - pillow>=8 - - pyparsing>=3 - - python-dateutil>=2.7 - - meson-python>=0.13.1,<0.17.0 ; extra == 'dev' - - pybind11>=2.13.2,!=2.13.3 ; extra == 'dev' - - setuptools-scm>=7,<10 ; extra == 'dev' - - setuptools>=64 ; extra == 'dev' - requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/32/91/d024616abdba99e83120e07a20658976f6a343646710760c4a51df126029/matplotlib-3.10.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - name: matplotlib - version: 3.10.9 - sha256: ae20801130378b82d647ff5047c07316295b68dc054ca6b3c13519d0ea624285 - requires_dist: - - contourpy>=1.0.1 - - cycler>=0.10 - - fonttools>=4.22.0 - - kiwisolver>=1.3.1 - - numpy>=1.23 - - packaging>=20.0 - - pillow>=8 - - pyparsing>=3 - - python-dateutil>=2.7 - - meson-python>=0.13.1,<0.17.0 ; extra == 'dev' - - pybind11>=2.13.2,!=2.13.3 ; extra == 'dev' - - setuptools-scm>=7,<10 ; extra == 'dev' - - setuptools>=64 ; extra == 'dev' - requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/41/86/86231232fff41c9f8e4a1a7d7a597d349a02527109c3af7d618366122139/matplotlib-3.10.9-cp314-cp314-macosx_11_0_arm64.whl - name: matplotlib - version: 3.10.9 - sha256: 97e35e8d39ccc85859095e01a53847432ba9a53ddf7986f7a54a11b73d0e143f - requires_dist: - - contourpy>=1.0.1 - - cycler>=0.10 - - fonttools>=4.22.0 - - kiwisolver>=1.3.1 - - numpy>=1.23 - - packaging>=20.0 - - pillow>=8 - - pyparsing>=3 - - python-dateutil>=2.7 - - meson-python>=0.13.1,<0.17.0 ; extra == 'dev' - - pybind11>=2.13.2,!=2.13.3 ; extra == 'dev' - - setuptools-scm>=7,<10 ; extra == 'dev' - - setuptools>=64 ; extra == 'dev' - requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/ab/1a/5a4f747a8b271cbb024946d2dd3c913ab5032ba430626f8c3528ada96b4b/matplotlib-3.10.9-cp314-cp314-win_amd64.whl - name: matplotlib - version: 3.10.9 - sha256: 336b9acc64d309063126edcdaca00db9373af3c476bb94388fe9c5a53ad13e6f - requires_dist: - - contourpy>=1.0.1 - - cycler>=0.10 - - fonttools>=4.22.0 - - kiwisolver>=1.3.1 - - numpy>=1.23 - - packaging>=20.0 - - pillow>=8 - - pyparsing>=3 - - python-dateutil>=2.7 - - meson-python>=0.13.1,<0.17.0 ; extra == 'dev' - - pybind11>=2.13.2,!=2.13.3 ; extra == 'dev' - - setuptools-scm>=7,<10 ; extra == 'dev' - - setuptools>=64 ; extra == 'dev' - requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/b7/18/4880dd762e40cd360c1bf06e890c5a97b997e91cb324602b1a19950ad5ce/matplotlib-3.10.9-cp312-cp312-macosx_11_0_arm64.whl - name: matplotlib - version: 3.10.9 - sha256: 41cb28c2bd769aa3e98322c6ab09854cbcc52ab69d2759d681bba3e327b2b320 - requires_dist: - - contourpy>=1.0.1 - - cycler>=0.10 - - fonttools>=4.22.0 - - kiwisolver>=1.3.1 - - numpy>=1.23 - - packaging>=20.0 - - pillow>=8 - - pyparsing>=3 - - python-dateutil>=2.7 - - meson-python>=0.13.1,<0.17.0 ; extra == 'dev' - - pybind11>=2.13.2,!=2.13.3 ; extra == 'dev' - - setuptools-scm>=7,<10 ; extra == 'dev' - - setuptools>=64 ; extra == 'dev' - requires_python: '>=3.10' -- conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.2.1-pyhd8ed1ab_0.conda - sha256: 9d690334de0cd1d22c51bc28420663f4277cfa60d34fa5cad1ce284a13f1d603 - md5: 00e120ce3e40bad7bfc78861ce3c4a25 + license: MIT + license_family: MIT + purls: [] + size: 45831 + timestamp: 1769456418774 +- conda: https://conda.anaconda.org/conda-forge/win-64/libhwloc-2.13.0-default_h049141e_1000.conda + sha256: 2ee12e37223dfcd0acd050c80a91150c482b6e2899198521e1800dce66662467 + md5: 6a01c986e30292c715038d2788aa1385 depends: - - python >=3.10 - - traitlets + - libwinpthread >=12.0.0.r4.gg4f2fc60ca + - libxml2 + - libxml2-16 >=2.14.6 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 license: BSD-3-Clause license_family: BSD - purls: - - pkg:pypi/matplotlib-inline?source=hash-mapping - size: 15175 - timestamp: 1761214578417 -- pypi: https://files.pythonhosted.org/packages/fb/86/dd6e5db36df29e76c7a7699123569a4a18c1623ce68d826ed96c62643cae/mdit_py_plugins-0.5.0-py3-none-any.whl - name: mdit-py-plugins - version: 0.5.0 - sha256: 07a08422fc1936a5d26d146759e9155ea466e842f5ab2f7d2266dd084c8dab1f - requires_dist: - - markdown-it-py>=2.0.0,<5.0.0 - - pre-commit ; extra == 'code-style' - - myst-parser ; extra == 'rtd' - - sphinx-book-theme ; extra == 'rtd' - - coverage ; extra == 'testing' - - pytest ; extra == 'testing' - - pytest-cov ; extra == 'testing' - - pytest-regressions ; extra == 'testing' - requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl - name: mdurl - version: 0.1.2 - sha256: 84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 - requires_python: '>=3.7' -- pypi: https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl - name: mergedeep - version: 1.3.4 - sha256: 70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307 - requires_python: '>=3.6' -- pypi: https://files.pythonhosted.org/packages/76/8e/56ccb09c7232a55403a7637caa21922f3b65901a37f5e8bdb405d0de0946/mike-2.2.0-py3-none-any.whl - name: mike - version: 2.2.0 - sha256: e1f4981c1152eec7c2490a3401142292cc47d686194188416db2648fdfe1d040 - requires_dist: - - jinja2>=2.7 - - mkdocs~=1.0 - - pyparsing>=3.0 - - pyyaml>=5.1 - - pyyaml-env-tag - - verspec - - importlib-metadata ; python_full_version < '3.10' - - importlib-resources ; python_full_version < '3.10' - - coverage ; extra == 'dev' - - flake8-quotes ; extra == 'dev' - - flake8>=3.0 ; extra == 'dev' - - shtab ; extra == 'dev' - - coverage ; extra == 'test' - - flake8-quotes ; extra == 'test' - - flake8>=3.0 ; extra == 'test' - - shtab ; extra == 'test' -- conda: https://conda.anaconda.org/conda-forge/noarch/mistune-3.2.0-pyhcf101f3_0.conda - sha256: d3fb4beb5e0a52b6cc33852c558e077e1bfe44df1159eb98332d69a264b14bae - md5: b11e360fc4de2b0035fc8aaa74f17fd6 + purls: [] + size: 2396128 + timestamp: 1770954127918 +- conda: https://conda.anaconda.org/conda-forge/win-64/libiconv-1.18-hc1393d2_2.conda + sha256: 0dcdb1a5f01863ac4e8ba006a8b0dc1a02d2221ec3319b5915a1863254d7efa7 + md5: 64571d1dd6cdcfa25d0664a5950fdaa2 depends: - - python >=3.10 - - typing_extensions - - python - license: BSD-3-Clause + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: LGPL-2.1-only + purls: [] + size: 696926 + timestamp: 1754909290005 +- conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-5.8.3-hfd05255_0.conda + sha256: d636d1a25234063642f9c531a7bb58d84c1c496411280a36ea000bd122f078f1 + md5: 8f83619ab1588b98dd99c90b0bfc5c6d + depends: + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + constrains: + - xz 5.8.3.* + license: 0BSD + purls: [] + size: 106486 + timestamp: 1775825663227 +- conda: https://conda.anaconda.org/conda-forge/win-64/libmpdec-4.0.0-hfd05255_1.conda + sha256: 40dcd0b9522a6e0af72a9db0ced619176e7cfdb114855c7a64f278e73f8a7514 + md5: e4a9fc2bba3b022dad998c78856afe47 + depends: + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: BSD-2-Clause license_family: BSD - purls: - - pkg:pypi/mistune?source=hash-mapping - size: 74250 - timestamp: 1766504456031 -- pypi: https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl - name: mkdocs - version: 1.6.1 - sha256: db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e - requires_dist: - - click>=7.0 - - colorama>=0.4 ; sys_platform == 'win32' - - ghp-import>=1.0 - - importlib-metadata>=4.4 ; python_full_version < '3.10' - - jinja2>=2.11.1 - - markdown>=3.3.6 - - markupsafe>=2.0.1 - - mergedeep>=1.3.4 - - mkdocs-get-deps>=0.2.0 - - packaging>=20.5 - - pathspec>=0.11.1 - - pyyaml-env-tag>=0.1 - - pyyaml>=5.1 - - watchdog>=2.0 - - babel>=2.9.0 ; extra == 'i18n' - - babel==2.9.0 ; extra == 'min-versions' - - click==7.0 ; extra == 'min-versions' - - colorama==0.4 ; sys_platform == 'win32' and extra == 'min-versions' - - ghp-import==1.0 ; extra == 'min-versions' - - importlib-metadata==4.4 ; python_full_version < '3.10' and extra == 'min-versions' - - jinja2==2.11.1 ; extra == 'min-versions' - - markdown==3.3.6 ; extra == 'min-versions' - - markupsafe==2.0.1 ; extra == 'min-versions' - - mergedeep==1.3.4 ; extra == 'min-versions' - - mkdocs-get-deps==0.2.0 ; extra == 'min-versions' - - packaging==20.5 ; extra == 'min-versions' - - pathspec==0.11.1 ; extra == 'min-versions' - - pyyaml-env-tag==0.1 ; extra == 'min-versions' - - pyyaml==5.1 ; extra == 'min-versions' - - watchdog==2.0 ; extra == 'min-versions' - requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/28/de/a3e710469772c6a89595fc52816da05c1e164b4c866a89e3cb82fb1b67c5/mkdocs_autorefs-1.4.4-py3-none-any.whl - name: mkdocs-autorefs - version: 1.4.4 - sha256: 834ef5408d827071ad1bc69e0f39704fa34c7fc05bc8e1c72b227dfdc5c76089 - requires_dist: - - markdown>=3.3 - - markupsafe>=2.0.1 - - mkdocs>=1.1 - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl - name: mkdocs-get-deps - version: 0.2.2 - sha256: e7878cbeac04860b8b5e0ca31d3abad3df9411a75a32cde82f8e44b6c16ff650 - requires_dist: - - importlib-metadata>=4.3 ; python_full_version < '3.10' - - mergedeep>=1.3.4 - - platformdirs>=2.2.0 - - pyyaml>=5.1 - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/13/95/cf3f7fe4910cf0365fa8ea0c731f4b8a624d97cd76ea777913ac8d0868e2/mkdocs_jupyter-0.26.3-py3-none-any.whl - name: mkdocs-jupyter - version: 0.26.3 - sha256: cd6644fb578131157194d750fd4d10fc2fd8f1e84e00036ee62df3b5b4b84c82 - requires_dist: - - ipykernel>6.0.0,<8 - - jupytext>1.13.8,<2 - - mkdocs-material>9.0.0 - - mkdocs>=1.4.0,<2 - - nbconvert>=7.2.9,<8 - - pygments>2.12.0 - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/a8/4e/c09876f08fa9faaa5e1178f3d77b7af3f343258689bd6f3b72593b2f74e3/mkdocs_markdownextradata_plugin-0.2.6-py3-none-any.whl - name: mkdocs-markdownextradata-plugin - version: 0.2.6 - sha256: 34dd40870781784c75809596b2d8d879da783815b075336d541de1f150c94242 - requires_dist: - - mkdocs - - pyyaml - requires_python: '>=3.6' -- pypi: https://files.pythonhosted.org/packages/2c/01/bc663630c510822c95c47a66af9fa7a443c295b47d5f041e5e6ae62ef659/mkdocs_material-9.7.6-py3-none-any.whl - name: mkdocs-material - version: 9.7.6 - sha256: 71b84353921b8ea1ba84fe11c50912cc512da8fe0881038fcc9a0761c0e635ba - requires_dist: - - babel>=2.10 - - backrefs>=5.7.post1 - - colorama>=0.4 - - jinja2>=3.1 - - markdown>=3.2 - - mkdocs-material-extensions>=1.3 - - mkdocs>=1.6,<2 - - paginate>=0.5 - - pygments>=2.16 - - pymdown-extensions>=10.2 - - requests>=2.30 - - mkdocs-git-committers-plugin-2>=1.1 ; extra == 'git' - - mkdocs-git-revision-date-localized-plugin>=1.2.4 ; extra == 'git' - - cairosvg>=2.6 ; extra == 'imaging' - - pillow>=10.2 ; extra == 'imaging' - - mkdocs-minify-plugin>=0.7 ; extra == 'recommended' - - mkdocs-redirects>=1.2 ; extra == 'recommended' - - mkdocs-rss-plugin>=1.6 ; extra == 'recommended' - requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl - name: mkdocs-material-extensions - version: 1.3.1 - sha256: adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31 - requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/c6/3d/020a6b6248c3d4a37797db068256f0b3f15b01bc481327ba888c50309aa8/mkdocs_plugin_inline_svg-0.1.0-py3-none-any.whl - name: mkdocs-plugin-inline-svg - version: 0.1.0 - sha256: a5aab2d98a19b24019f8e650f54fc647c2f31e7d0e36fc5cf2d2161acc0ea49a - requires_dist: - - mkdocs - requires_python: '>=3.5' -- pypi: https://files.pythonhosted.org/packages/6e/94/be70f8ee9c45f2f62b39a1f0e9303bc20e138a8f3b8e50ffd89498e177e1/mkdocstrings-1.0.4-py3-none-any.whl - name: mkdocstrings - version: 1.0.4 - sha256: 63464b4b29053514f32a1dbbf604e52876d5e638111b0c295ab7ed3cac73ca9b - requires_dist: - - jinja2>=3.1 - - markdown>=3.6 - - markupsafe>=1.1 - - mkdocs>=1.6 - - mkdocs-autorefs>=1.4 - - pymdown-extensions>=6.3 - - mkdocstrings-crystal>=0.3.4 ; extra == 'crystal' - - mkdocstrings-python-legacy>=0.2.1 ; extra == 'python-legacy' - - mkdocstrings-python>=1.16.2 ; extra == 'python' - requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/32/28/79f0f8de97cce916d5ae88a7bee1ad724855e83e6019c0b4d5b3fabc80f3/mkdocstrings_python-2.0.3-py3-none-any.whl - name: mkdocstrings-python - version: 2.0.3 - sha256: 0b83513478bdfd803ff05aa43e9b1fca9dd22bcd9471f09ca6257f009bc5ee12 - requires_dist: - - mkdocstrings>=0.30 - - mkdocs-autorefs>=1.4 - - griffelib>=2.0 - - typing-extensions>=4.0 ; python_full_version < '3.11' - requires_python: '>=3.10' -- conda: https://conda.anaconda.org/conda-forge/win-64/mkl-2025.3.1-hac47afa_12.conda - sha256: d7b8343e10053c8527e2e20fd96787d368c97129ffa799e863069a36bd299457 - md5: a3b1ee571898432da7e13ecb7bfd76ae + purls: [] + size: 89411 + timestamp: 1769482314283 +- conda: https://conda.anaconda.org/conda-forge/win-64/libsodium-1.0.21-h6a83c73_3.conda + sha256: d915f4fa8ebbf237c7a6e511ed458f2cfdc7c76843a924740318a15d0dd33d6d + md5: da2aa614d16a795b3007b6f4a1318a81 + depends: + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + license: ISC + purls: [] + size: 276860 + timestamp: 1772479407566 +- conda: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.53.1-hf5d6505_0.conda + sha256: e70562450332ca8954bc16f3455468cca5ef3695c7d7187ecc87f8fc3c70e9eb + md5: 7fea434a17c323256acc510a041b80d7 + depends: + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: blessing + purls: [] + size: 1304178 + timestamp: 1777986510497 +- conda: https://conda.anaconda.org/conda-forge/win-64/libwinpthread-12.0.0.r4.gg4f2fc60ca-h57928b3_10.conda + sha256: 0fccf2d17026255b6e10ace1f191d0a2a18f2d65088fd02430be17c701f8ffe0 + md5: 8a86073cf3b343b87d03f41790d8b4e5 + depends: + - ucrt + constrains: + - pthreads-win32 <0.0a0 + - msys2-conda-epoch <0.0a0 + license: MIT AND BSD-3-Clause-Clear + purls: [] + size: 36621 + timestamp: 1759768399557 +- conda: https://conda.anaconda.org/conda-forge/win-64/libxml2-16-2.15.3-h692994f_0.conda + sha256: 8038084c60eda2006d0122d05e3364fe8db0a18935ca6ed0168b5ba5aa33f904 + md5: f7d6fcda29570e20851b78d92ea2154e + depends: + - libiconv >=1.18,<2.0a0 + - liblzma >=5.8.3,<6.0a0 + - libzlib >=1.3.2,<2.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + constrains: + - libxml2 2.15.3 + - icu <0.0a0 + license: MIT + license_family: MIT + purls: [] + size: 518869 + timestamp: 1776376971242 +- conda: https://conda.anaconda.org/conda-forge/win-64/libxml2-2.15.3-hbc0d294_0.conda + sha256: da68af9d9d28d65a6916db1bef68f8a25c64c4fdcf759f32a2d2f2f143220adf + md5: e3b5acbb857a12f5d59e8d174bc536c0 depends: - - llvm-openmp >=22.1.4 - - onemkl-license 2025.3.1 h57928b3_12 - - tbb >=2022.3.0 + - libiconv >=1.18,<2.0a0 + - liblzma >=5.8.3,<6.0a0 + - libxml2-16 2.15.3 h692994f_0 + - libzlib >=1.3.2,<2.0a0 - ucrt >=10.0.20348.0 - vc >=14.3,<15 - vc14_runtime >=14.44.35208 - license: LicenseRef-IntelSimplifiedSoftwareOct2022 - license_family: Proprietary + constrains: + - icu <0.0a0 + license: MIT + license_family: MIT purls: [] - size: 100112670 - timestamp: 1776904283842 -- pypi: https://files.pythonhosted.org/packages/5b/69/93b34728cc386efdde0c342f8c680b9187dea7beb7adaf6b58a0713be101/mpld3-0.5.12-py3-none-any.whl - name: mpld3 - version: 0.5.12 - sha256: bea31799a4041029a906f53f2662bbf1c49903e0c0bc712b412354158ec7cf54 - requires_dist: - - jinja2 - - matplotlib -- pypi: https://files.pythonhosted.org/packages/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 - sha256: a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c - requires_dist: - - pytest>=4.6 ; extra == 'develop' - - pycodestyle ; extra == 'develop' - - pytest-cov ; extra == 'develop' - - codecov ; extra == 'develop' - - wheel ; extra == 'develop' - - sphinx ; extra == 'docs' - - gmpy2>=2.1.0a4 ; platform_python_implementation != 'PyPy' and extra == 'gmpy' - - pytest>=4.6 ; extra == 'tests' -- pypi: https://files.pythonhosted.org/packages/1b/9f/38ff9e57a2eade7bf9dfee5eae17f39fc0e998658050279cbb14d97d36d9/msgpack-1.1.2-cp314-cp314-macosx_11_0_arm64.whl - name: msgpack - version: 1.1.2 - sha256: 6c15b7d74c939ebe620dd8e559384be806204d73b4f9356320632d783d1f7939 - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/2f/40/dc34d1a8d5f1e51fc64640b62b191684da52ca469da9cd74e84936ffa4a6/msgpack-1.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - name: msgpack - version: 1.1.2 - sha256: 180759d89a057eab503cf62eeec0aa61c4ea1200dee709f3a8e9397dbb3b6931 - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/34/68/ba4f155f793a74c1483d4bdef136e1023f7bcba557f0db4ef3db3c665cf1/msgpack-1.1.2-cp312-cp312-macosx_11_0_arm64.whl - name: msgpack - version: 1.1.2 - sha256: 446abdd8b94b55c800ac34b102dffd2f6aa0ce643c55dfc017ad89347db3dbdb - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/65/92/a5100f7185a800a5d29f8d14041f61475b9de465ffcc0f3b9fba606e4505/msgpack-1.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - name: msgpack - version: 1.1.2 - sha256: 372839311ccf6bdaf39b00b61288e0557916c3729529b301c52c2d88842add42 - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/6e/f1/abd09c2ae91228c5f3998dbd7f41353def9eac64253de3c8105efa2082f7/msgpack-1.1.2-cp314-cp314-win_amd64.whl - name: msgpack - version: 1.1.2 - sha256: 9ade919fac6a3e7260b7f64cea89df6bec59104987cbea34d34a2fa15d74310b - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/8c/ec/d431eb7941fb55a31dd6ca3404d41fbb52d99172df2e7707754488390910/msgpack-1.1.2-cp312-cp312-win_amd64.whl - name: msgpack - version: 1.1.2 - sha256: 1de460f0403172cff81169a30b9a92b260cb809c4cb7e2fc79ae8d0510c78b6b - requires_python: '>=3.9' -- conda: https://conda.anaconda.org/conda-forge/linux-64/msgspec-0.21.1-py312h4c3975b_0.conda - sha256: 25eb262c378a922eeed85c941ab7de2687ea842daed80521b861b7472b5a7f9a - md5: 5e07dc45b4458c19fdc085bd6c1aa51f + size: 43916 + timestamp: 1776376994334 +- conda: https://conda.anaconda.org/conda-forge/win-64/libzlib-1.3.2-hfd05255_2.conda + sha256: 88609816e0cc7452bac637aaf65783e5edf4fee8a9f8e22bdc3a75882c536061 + md5: dbabbd6234dea34040e631f87676292f depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=14 - - python >=3.12,<3.13.0a0 - - python_abi 3.12.* *_cp312 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/msgspec?source=hash-mapping - size: 218330 - timestamp: 1776337395109 -- conda: https://conda.anaconda.org/conda-forge/linux-64/msgspec-0.21.1-py314h5bd0f2a_0.conda - sha256: 52565ceea81e801c59dcaeaf5a9c77fba2fade445e67e0864fda50d4b944e15b - md5: 4a8ea416a56e58f012e445f7af2bbcc8 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + constrains: + - zlib 1.3.2 *_2 + license: Zlib + license_family: Other + purls: [] + size: 58347 + timestamp: 1774072851498 +- conda: https://conda.anaconda.org/conda-forge/win-64/llvm-openmp-22.1.5-h4fa8253_1.conda + sha256: 7179e0266125c3333a097b399d0383734ee6c55fbadf332b447237a596e9698f + md5: bffe599d0eb2e78a32872712178e639c depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=14 - - python >=3.14,<3.15.0a0 - - python_abi 3.14.* *_cp314 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/msgspec?source=hash-mapping - size: 220990 - timestamp: 1776337508167 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/msgspec-0.21.1-py312h2bbb03f_0.conda - sha256: 50e284832520f08ef1e37e0ca20459f5df2c048f59dfba1f2e3ee0ccfe7be317 - md5: ae340bdc5bdf5abd3183c5962517cbde + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + constrains: + - openmp 22.1.5|22.1.5.* + - intel-openmp <0.0a0 + license: Apache-2.0 WITH LLVM-exception + license_family: APACHE + purls: [] + size: 347493 + timestamp: 1778448334890 +- conda: https://conda.anaconda.org/conda-forge/win-64/markupsafe-3.0.3-py312h05f76fc_1.conda + sha256: b744287a780211ac4595126ef96a44309c791f155d4724021ef99092bae4aace + md5: a73298d225c7852f97403ca105d10a13 depends: - - __osx >=11.0 - python >=3.12,<3.13.0a0 - - python >=3.12,<3.13.0a0 *_cpython - python_abi 3.12.* *_cp312 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + constrains: + - jinja2 >=3.0.0 license: BSD-3-Clause license_family: BSD purls: - - pkg:pypi/msgspec?source=hash-mapping - size: 212357 - timestamp: 1776338798628 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/msgspec-0.21.1-py314h6c2aa35_0.conda - sha256: 24a9105921e94fa526ffde1e956fa550c48ddb9ce4b0cf19ae22e79ed267261e - md5: 26fce586b13842a0f9f9a3aabae3e943 + - pkg:pypi/markupsafe?source=hash-mapping + size: 28510 + timestamp: 1772445175216 +- conda: https://conda.anaconda.org/conda-forge/win-64/markupsafe-3.0.3-py314h2359020_1.conda + sha256: 02805a0f3cd168dbf13afc5e4aed75cc00fe538ce143527a6471485b36f5887c + md5: 8de7b40f8b30a8fcaa423c2537fe4199 depends: - - __osx >=11.0 - python >=3.14,<3.15.0a0 - - python >=3.14,<3.15.0a0 *_cp314 - python_abi 3.14.* *_cp314 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + constrains: + - jinja2 >=3.0.0 license: BSD-3-Clause license_family: BSD purls: - - pkg:pypi/msgspec?source=hash-mapping - size: 216965 - timestamp: 1776338889692 + - pkg:pypi/markupsafe?source=hash-mapping + size: 30022 + timestamp: 1772445159549 +- conda: https://conda.anaconda.org/conda-forge/win-64/mkl-2026.0.0-hac47afa_906.conda + sha256: 5d6c0c02588a655aaaced67f25d1967810830d4336865e319f32cfb41d08de06 + md5: fada5d30be6e95c74ffc528f70268f02 + depends: + - llvm-openmp >=22.1.5 + - onemkl-license 2026.0.0 h57928b3_906 + - tbb >=2023.0.0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: LicenseRef-IntelSimplifiedSoftwareOct2022 + license_family: Proprietary + purls: [] + size: 114608976 + timestamp: 1778776186500 - conda: https://conda.anaconda.org/conda-forge/win-64/msgspec-0.21.1-py312he06e257_0.conda sha256: 003de3343b481937b5eb500ecdbfc882e87cea608be3741dc1fb13d22f8ed95e md5: 1f32f4f6aa595377a7e651e67ba53d30 @@ -7689,1429 +7724,1319 @@ packages: - python >=3.14,<3.15.0a0 - python_abi 3.14.* *_cp314 - ucrt >=10.0.20348.0 - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/msgspec?source=hash-mapping - size: 201836 - timestamp: 1776337750218 -- pypi: https://files.pythonhosted.org/packages/80/31/0b2517913687895f5904325c2069d6a3b78f66cc641a86a2baf75a05dcbb/multidict-6.7.1-cp312-cp312-win_amd64.whl - name: multidict - version: 6.7.1 - sha256: fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19 - requires_dist: - - typing-extensions>=4.1.0 ; python_full_version < '3.11' - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/a9/65/1caac9d4cd32e8433908683446eebc953e82d22b03d10d41a5f0fefe991b/multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl - name: multidict - version: 6.7.1 - sha256: b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7 - requires_dist: - - typing-extensions>=4.1.0 ; python_full_version < '3.11' - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/e0/bf/52f25716bbe93745595800f36fb17b73711f14da59ed0bb2eba141bc9f0f/multidict-6.7.1-cp314-cp314-win_amd64.whl - name: multidict - version: 6.7.1 - sha256: 5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f - requires_dist: - - typing-extensions>=4.1.0 ; python_full_version < '3.11' - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/f1/4f/733c48f270565d78b4544f2baddc2fb2a245e5a8640254b12c36ac7ac68e/multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl - name: multidict - version: 6.7.1 - sha256: 0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1 - requires_dist: - - typing-extensions>=4.1.0 ; python_full_version < '3.11' - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/f3/8d/5e5be3ced1d12966fefb5c4ea3b2a5b480afcea36406559442c6e31d4a48/multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - name: multidict - version: 6.7.1 - sha256: bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961 - requires_dist: - - typing-extensions>=4.1.0 ; python_full_version < '3.11' - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/fe/3b/8ec5074bcfc450fe84273713b4b0a0dd47c0249358f5d82eb8104ffe2520/multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - name: multidict - version: 6.7.1 - sha256: 7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709 - requires_dist: - - typing-extensions>=4.1.0 ; python_full_version < '3.11' - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/d0/69/f24d3d1c38ad69e256138b4ec2452a8c7cf66be49dc214771ae99dd4f0a0/narwhals-2.20.0-py3-none-any.whl - name: narwhals - version: 2.20.0 - sha256: 16e750ea5507d4ba6e8d03455b5f93a535e0405976561baea235bca5dc9f475d - requires_dist: - - cudf-cu12>=24.10.0 ; extra == 'cudf' - - dask[dataframe]>=2024.8 ; extra == 'dask' - - duckdb>=1.1 ; extra == 'duckdb' - - ibis-framework>=6.0.0 ; extra == 'ibis' - - packaging ; extra == 'ibis' - - pyarrow-hotfix ; extra == 'ibis' - - rich ; extra == 'ibis' - - modin ; extra == 'modin' - - pandas>=1.1.3 ; extra == 'pandas' - - polars>=0.20.4 ; extra == 'polars' - - pyarrow>=13.0.0 ; extra == 'pyarrow' - - pyspark>=3.5.0 ; extra == 'pyspark' - - pyspark[connect]>=3.5.0 ; extra == 'pyspark-connect' - - duckdb>=1.1 ; extra == 'sql' - - sqlparse ; extra == 'sql' - - sqlframe>=3.22.0,!=3.39.3 ; extra == 'sqlframe' - requires_python: '>=3.9' -- conda: https://conda.anaconda.org/conda-forge/noarch/nbclient-0.10.4-pyhd8ed1ab_0.conda - sha256: 1b66960ee06874ddceeebe375d5f17fb5f393d025a09e15b830ad0c4fffb585b - md5: 00f5b8dafa842e0c27c1cd7296aa4875 - depends: - - jupyter_client >=6.1.12 - - jupyter_core >=4.12,!=5.0.* - - nbformat >=5.1 - - python >=3.8 - - traitlets >=5.4 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 license: BSD-3-Clause license_family: BSD purls: - - pkg:pypi/nbclient?source=compressed-mapping - size: 28473 - timestamp: 1766485646962 -- conda: https://conda.anaconda.org/conda-forge/noarch/nbconvert-core-7.17.1-pyhcf101f3_0.conda - sha256: ab2ac79c5892c5434d50b3542d96645bdaa06d025b6e03734be29200de248ac2 - md5: 2bce0d047658a91b99441390b9b27045 + - pkg:pypi/msgspec?source=hash-mapping + size: 201836 + timestamp: 1776337750218 +- conda: https://conda.anaconda.org/conda-forge/win-64/nodejs-25.8.2-h80d1838_0.conda + sha256: 5e38e51da1aa4bc352db9b4cec1c3e25811de0f4408edaa24e009a64de6dbfdf + md5: e626ee7934e4b7cb21ce6b721cff8677 + license: MIT + license_family: MIT + purls: [] + size: 31271315 + timestamp: 1774517904472 +- conda: https://conda.anaconda.org/conda-forge/win-64/onemkl-license-2026.0.0-h57928b3_906.conda + sha256: 2c62b4b31da810043a47014a410c546015fcc17f39d8929ba989b2f0086dc71f + md5: 331614e966c27e5ec2a9715c9d17e9a0 + license: LicenseRef-IntelSimplifiedSoftwareOct2022 + license_family: Proprietary + purls: [] + size: 41154 + timestamp: 1778775952813 +- conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.6.2-hf411b9b_0.conda + sha256: feb5815125c60f2be4a411e532db1ed1cd2d7261a6a43c54cb6ae90724e2e154 + md5: 05c7d624cff49dbd8db1ad5ba537a8a3 + depends: + - ca-certificates + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: Apache-2.0 + license_family: Apache + purls: [] + size: 9410183 + timestamp: 1775589779763 +- conda: https://conda.anaconda.org/conda-forge/win-64/psutil-7.2.2-py312he5662c2_0.conda + sha256: edffc84c001a05b996b5f8607c8164432754e86ec9224e831cd00ebabdec04e7 + md5: a2724c93b745fc7861948eb8b9f6679a depends: - - beautifulsoup4 - - bleach-with-css !=5.0.0 - - defusedxml - - importlib-metadata >=3.6 - - jinja2 >=3.0 - - jupyter_core >=4.7 - - jupyterlab_pygments - - markupsafe >=2.0 - - mistune >=2.0.3,<4 - - nbclient >=0.5.0 - - nbformat >=5.7 - - packaging - - pandocfilters >=1.4.1 - - pygments >=2.4.1 - - python >=3.10 - - traitlets >=5.1 - python - constrains: - - pandoc >=2.9.2,<4.0.0 - - nbconvert ==7.17.1 *_0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - python_abi 3.12.* *_cp312 license: BSD-3-Clause license_family: BSD purls: - - pkg:pypi/nbconvert?source=compressed-mapping - size: 202229 - timestamp: 1775615493260 -- conda: https://conda.anaconda.org/conda-forge/noarch/nbformat-5.10.4-pyhd8ed1ab_1.conda - sha256: 7a5bd30a2e7ddd7b85031a5e2e14f290898098dc85bea5b3a5bf147c25122838 - md5: bbe1963f1e47f594070ffe87cdf612ea + - pkg:pypi/psutil?source=hash-mapping + size: 242769 + timestamp: 1769678170631 +- conda: https://conda.anaconda.org/conda-forge/win-64/psutil-7.2.2-py314hc5dbbe4_0.conda + sha256: 17c8274ce5a32c9793f73a5a0094bd6188f3a13026a93147655143d4df034214 + md5: fd539ac231820f64066839251aa9fa48 depends: - - jsonschema >=2.6 - - jupyter_core >=4.12,!=5.0.* - - python >=3.9 - - python-fastjsonschema >=2.15 - - traitlets >=5.1 + - python + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - python_abi 3.14.* *_cp314 license: BSD-3-Clause license_family: BSD purls: - - pkg:pypi/nbformat?source=hash-mapping - size: 100945 - timestamp: 1733402844974 -- pypi: https://files.pythonhosted.org/packages/eb/be/b257e12f9710819fde40adc972578bee6b72c5992da1bc8369bef2597756/nbmake-1.5.5-py3-none-any.whl - name: nbmake - version: 1.5.5 - sha256: c6fbe6e48b60cacac14af40b38bf338a3b88f47f085c54ac5b8639ff0babaf4b - requires_dist: - - ipykernel>=5.4.0 - - nbclient>=0.6.6 - - nbformat>=5.0.4 - - pygments>=2.7.3 - - pytest>=6.1.0 - requires_python: '>=3.8.0' -- pypi: https://files.pythonhosted.org/packages/28/88/4789719fbbe166d12d345b3ac66b96105f10001b16e00a9765ba29261a21/nbqa-1.9.1-py3-none-any.whl - name: nbqa - version: 1.9.1 - sha256: 95552d2f6c2c038136252a805aa78d85018aef922586270c3a074332737282e5 - requires_dist: - - autopep8>=1.5 - - ipython>=7.8.0 - - tokenize-rt>=3.2.0 - - tomli - - black ; extra == 'toolchain' - - blacken-docs ; extra == 'toolchain' - - flake8 ; extra == 'toolchain' - - isort ; extra == 'toolchain' - - jupytext ; extra == 'toolchain' - - mypy ; extra == 'toolchain' - - pylint ; extra == 'toolchain' - - pyupgrade ; extra == 'toolchain' - - ruff ; extra == 'toolchain' - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/20/16/e777eadfa0c0305878c36fae1d5e6db474fbb15dae202b9ec378809dfb4d/nbstripout-0.9.1-py3-none-any.whl - name: nbstripout - version: 0.9.1 - sha256: ca027ee45742ee77e4f8e9080254f9a707f1161ba11367b82fdf4a29892c759e - requires_dist: - - nbformat - requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/1a/66/8ce2af42feeba7a85b573e615a360ca1e204c75f6c8d4c5641140b1bfd17/ncrystal-4.3.4-py3-none-any.whl - name: ncrystal - version: 4.3.4 - sha256: 87ea53b4e6937e4df9b2e0c71dfd88ff77de6fe8c8b4d204405d24d12143aba0 - requires_dist: - - ncrystal-core==4.3.4 - - ncrystal-python==4.3.4 - - 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/0b/71/3b54e97c28cdf4993c8317d62ac1be655667455f129cf6162591d56aed89/ncrystal_core-4.3.4-py3-none-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - name: ncrystal-core - version: 4.3.4 - sha256: d3b94528c5d237d43c64c18a2347b967445c43c31ade5c64716e171838b53f9e - requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/a0/0d/df49ae8af94215db241701f692786a2e85c3ac4557aafd829270c54fb1fa/ncrystal_core-4.3.4-py3-none-macosx_11_0_arm64.whl - name: ncrystal-core - version: 4.3.4 - sha256: 47e4441b65170f63acc6c25ea02c7827dbf76a5813f4bf01f9404a060bee6063 - requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/ab/14/d708fb7c6bf7e7be586c625840cc078a45e64dd6bffe8d60a7b17a22b24e/ncrystal_core-4.3.4-py3-none-win_amd64.whl - name: ncrystal-core - version: 4.3.4 - sha256: 50642b491f1a9bbd4d37909dc11ef45ecf14c6272511ec5a1e1117ef7e51aa66 - requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/74/10/6434f57fa65651672534ce34a1f40ddbc7b880658ce50a8f9d7ab0830719/ncrystal_python-4.3.4-py3-none-any.whl - name: ncrystal-python - version: 4.3.4 - sha256: f7075904fa40c6a85ac9d792255ae0751b0d7059dd5297d54b1d7208e17be66e - requires_dist: - - numpy>=1.22 - requires_python: '>=3.8' -- conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.6-hdb14827_0.conda - sha256: fc89f74bbe362fb29fa3c037697a89bec140b346a2469a90f7936d1d7ea4d8a3 - md5: fc21868a1a5aacc937e7a18747acb8a5 + - pkg:pypi/psutil?source=hash-mapping + size: 249950 + timestamp: 1769678167309 +- conda: https://conda.anaconda.org/conda-forge/win-64/python-3.12.13-h0159041_0_cpython.conda + sha256: a02b446d8b7b167b61733a3de3be5de1342250403e72a63b18dac89e99e6180e + md5: 2956dff38eb9f8332ad4caeba941cfe7 + depends: + - bzip2 >=1.0.8,<2.0a0 + - libexpat >=2.7.4,<3.0a0 + - libffi >=3.5.2,<3.6.0a0 + - liblzma >=5.8.2,<6.0a0 + - libsqlite >=3.51.2,<4.0a0 + - libzlib >=1.3.1,<2.0a0 + - openssl >=3.5.5,<4.0a0 + - tk >=8.6.13,<8.7.0a0 + - tzdata + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + constrains: + - python_abi 3.12.* *_cp312 + license: Python-2.0 + purls: [] + size: 15840187 + timestamp: 1772728877265 +- conda: https://conda.anaconda.org/conda-forge/win-64/python-3.14.4-h4b44e0e_100_cp314.conda + build_number: 100 + sha256: e258d626b0ba778abb319f128de4c1211306fe86fe0803166817b1ce2514c920 + md5: 40b6a8f438afb5e7b314cc5c4a43cd84 + depends: + - bzip2 >=1.0.8,<2.0a0 + - libexpat >=2.7.5,<3.0a0 + - libffi >=3.5.2,<3.6.0a0 + - liblzma >=5.8.2,<6.0a0 + - libmpdec >=4.0.0,<5.0a0 + - libsqlite >=3.52.0,<4.0a0 + - libzlib >=1.3.2,<2.0a0 + - openssl >=3.5.6,<4.0a0 + - python_abi 3.14.* *_cp314 + - tk >=8.6.13,<8.7.0a0 + - tzdata + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - zstd >=1.5.7,<1.6.0a0 + license: Python-2.0 + purls: [] + size: 18055445 + timestamp: 1775615317758 + python_site_packages_path: Lib/site-packages +- conda: https://conda.anaconda.org/conda-forge/win-64/pywin32-311-py312h829343e_1.conda + sha256: a7505522048dad63940d06623f07eb357b9b65510a8d23ff32b99add05aac3a1 + md5: 64cbe4ecbebe185a2261d3f298a60cde + depends: + - python + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - python_abi 3.12.* *_cp312 + license: PSF-2.0 + license_family: PSF + purls: + - pkg:pypi/pywin32?source=hash-mapping + size: 6684490 + timestamp: 1756487136116 +- conda: https://conda.anaconda.org/conda-forge/win-64/pywin32-311-py314h8f8f202_1.conda + sha256: 6918a8067f296f3c65d43e84558170c9e6c3f4dd735cfe041af41a7fdba7b171 + md5: 2d7b7ba21e8a8ced0eca553d4d53f773 + depends: + - python + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - python_abi 3.14.* *_cp314 + license: PSF-2.0 + license_family: PSF + purls: + - pkg:pypi/pywin32?source=hash-mapping + size: 6713155 + timestamp: 1756487145487 +- conda: https://conda.anaconda.org/conda-forge/win-64/pywinpty-2.0.15-py312h275cf98_1.conda + sha256: 61cc6c2c712ab4d2b8e7a73d884ef8d3262cb80cc93a4aa074e8b08aa7ddd648 + md5: 66255d136bd0daa41713a334db41d9f0 + depends: + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + - winpty + license: MIT + license_family: MIT + purls: + - pkg:pypi/pywinpty?source=hash-mapping + size: 215371 + timestamp: 1759557609855 +- conda: https://conda.anaconda.org/conda-forge/win-64/pywinpty-2.0.15-py314h51f0985_1.conda + sha256: 048e20641da680aedaab285640a2aca56b7b5baf7a18f8f164f2796e13628c1f + md5: dd84e8748bd3c85a5c751b0576488080 depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=14 - license: X11 AND BSD-3-Clause - purls: [] - size: 918956 - timestamp: 1777422145199 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.6-h1d4f5a5_0.conda - sha256: 4ea6c620b87bd1d42bb2ccc2c87cd2483fa2d7f9e905b14c223f11ff3f4c455d - md5: 343d10ed5b44030a2f67193905aea159 + - python >=3.14.0rc3,<3.15.0a0 + - python_abi 3.14.* *_cp314 + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + - winpty + license: MIT + license_family: MIT + purls: + - pkg:pypi/pywinpty?source=hash-mapping + size: 216325 + timestamp: 1759557436167 +- conda: https://conda.anaconda.org/conda-forge/win-64/pyyaml-6.0.3-py312h05f76fc_1.conda + sha256: 1cab6cbd6042b2a1d8ee4d6b4ec7f36637a41f57d2f5c5cf0c12b7c4ce6a62f6 + md5: 9f6ebef672522cb9d9a6257215ca5743 depends: - - __osx >=11.0 - license: X11 AND BSD-3-Clause - purls: [] - size: 805509 - timestamp: 1777423252320 -- conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_1.conda - sha256: bb7b21d7fd0445ddc0631f64e66d91a179de4ba920b8381f29b9d006a42788c0 - md5: 598fd7d4d0de2455fb74f56063969a97 + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - yaml >=0.2.5,<0.3.0a0 + license: MIT + license_family: MIT + purls: + - pkg:pypi/pyyaml?source=hash-mapping + size: 179738 + timestamp: 1770223468771 +- conda: https://conda.anaconda.org/conda-forge/win-64/pyyaml-6.0.3-py314h2359020_1.conda + sha256: a2aff34027aa810ff36a190b75002d2ff6f9fbef71ec66e567616ac3a679d997 + md5: 0cd9b88826d0f8db142071eb830bce56 depends: - - python >=3.9 - license: BSD-2-Clause + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - yaml >=0.2.5,<0.3.0a0 + license: MIT + license_family: MIT + purls: + - pkg:pypi/pyyaml?source=hash-mapping + size: 181257 + timestamp: 1770223460931 +- conda: https://conda.anaconda.org/conda-forge/win-64/pyzmq-27.1.0-py312h343a6d4_2.conda + noarch: python + sha256: d84bcc19a945ca03d1fd794be3e9896ab6afc9f691d58d9c2da514abe584d4df + md5: eb1ec67a70b4d479f7dd76e6c8fe7575 + depends: + - python + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - zeromq >=4.3.5,<4.3.6.0a0 + - _python_abi3_support 1.* + - cpython >=3.12 + license: BSD-3-Clause license_family: BSD purls: - - pkg:pypi/nest-asyncio?source=hash-mapping - size: 11543 - timestamp: 1733325673691 -- 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.8.2-he4ff34a_0.conda - sha256: d1a673d1418d9e956b6e4e46c23e72a511c5c1d45dc5519c947457427036d5e2 - md5: baffb1570b3918c784d4490babc52fbf + - pkg:pypi/pyzmq?source=hash-mapping + size: 183235 + timestamp: 1771716967192 +- conda: https://conda.anaconda.org/conda-forge/win-64/rpds-py-0.30.0-py312hdabe01f_0.conda + sha256: faad05e6df2fc15e3ae06fdd71a36e17ff25364777aa4c40f2ec588740d64091 + md5: 2c51baeda0a355b0a5e7b6acb28cf02d depends: - - libgcc >=14 - - libstdcxx >=14 - - __glibc >=2.28,<3.0.a0 - - libnghttp2 >=1.68.1,<2.0a0 - - libuv >=1.51.0,<2.0a0 - - c-ares >=1.34.6,<2.0a0 - - openssl >=3.5.5,<4.0a0 - - libsqlite >=3.52.0,<4.0a0 - - icu >=78.3,<79.0a0 - - libzlib >=1.3.2,<2.0a0 - - libabseil >=20260107.1,<20260108.0a0 - - libabseil * cxx17* - - zstd >=1.5.7,<1.6.0a0 - - libbrotlicommon >=1.2.0,<1.3.0a0 - - libbrotlienc >=1.2.0,<1.3.0a0 - - libbrotlidec >=1.2.0,<1.3.0a0 + - python + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - python_abi 3.12.* *_cp312 license: MIT license_family: MIT - purls: [] - size: 18829340 - timestamp: 1774514313036 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/nodejs-25.8.2-h7039424_0.conda - sha256: 4782b172b3b8a557b60bf5f591821cf100e2092ba7a5494ce047dfa41626de26 - md5: ca8277c52fdface8bb8ebff7cd9a6f56 + purls: + - pkg:pypi/rpds-py?source=hash-mapping + size: 243577 + timestamp: 1764543069837 +- conda: https://conda.anaconda.org/conda-forge/win-64/rpds-py-0.30.0-py314h9f07db2_0.conda + sha256: e4435368c5c25076dc0f5918ba531c5a92caee8e0e2f9912ef6810049cf00db2 + md5: e86531e278ad304438e530953cd55d14 depends: - - libcxx >=19 - - __osx >=11.0 - - icu >=78.3,<79.0a0 - - libbrotlicommon >=1.2.0,<1.3.0a0 - - libbrotlienc >=1.2.0,<1.3.0a0 - - libbrotlidec >=1.2.0,<1.3.0a0 - - libnghttp2 >=1.68.1,<2.0a0 - - libuv >=1.51.0,<2.0a0 - - libsqlite >=3.52.0,<4.0a0 - - libzlib >=1.3.2,<2.0a0 - - openssl >=3.5.5,<4.0a0 - - zstd >=1.5.7,<1.6.0a0 - - c-ares >=1.34.6,<2.0a0 - - libabseil >=20260107.1,<20260108.0a0 - - libabseil * cxx17* + - python + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - python_abi 3.14.* *_cp314 license: MIT license_family: MIT + purls: + - pkg:pypi/rpds-py?source=hash-mapping + size: 235780 + timestamp: 1764543046065 +- conda: https://conda.anaconda.org/conda-forge/win-64/tbb-2023.0.0-hd3d4ead_2.conda + sha256: 8a4053839b8e997a5965e2dff7d6cf3c77be62d82c0e48c8a04a5ed2d2e73035 + md5: 8ee01a693aecff5432069eaaf1183c45 + depends: + - libhwloc >=2.13.0,<2.13.1.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: Apache-2.0 purls: [] - 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 + size: 156515 + timestamp: 1778673901757 +- conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h6ed50ae_3.conda + sha256: 0e79810fae28f3b69fe7391b0d43f5474d6bd91d451d5f2bde02f55ae481d5e3 + md5: 0481bfd9814bf525bd4b3ee4b51494c4 + depends: + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: TCL + license_family: BSD + purls: [] + size: 3526350 + timestamp: 1769460339384 +- conda: https://conda.anaconda.org/conda-forge/win-64/tornado-6.5.5-py312he06e257_0.conda + sha256: 1220c986664e9e8662e660dc64dd97ed823926b1ba05175771408cf1d6a46dd2 + md5: c6c66a64da3d2953c83ed2789a7f4bdb + depends: + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: Apache-2.0 + license_family: Apache + purls: + - pkg:pypi/tornado?source=hash-mapping + size: 859726 + timestamp: 1774358173994 +- conda: https://conda.anaconda.org/conda-forge/win-64/tornado-6.5.5-py314h5a2d7ad_0.conda + sha256: 49d64837dd02475903479ca47b82669bd6c9f7e6afde61860c6f3f2bd57d8a03 + md5: 87b1215adf7f0ba1fb9250af9fc668e1 + depends: + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: Apache-2.0 + license_family: Apache + purls: + - pkg:pypi/tornado?source=hash-mapping + size: 914835 + timestamp: 1774358183098 +- conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.26100.0-h57928b3_0.conda + sha256: 3005729dce6f3d3f5ec91dfc49fc75a0095f9cd23bab49efb899657297ac91a5 + md5: 71b24316859acd00bdb8b38f5e2ce328 + constrains: + - vc14_runtime >=14.29.30037 + - vs2015_runtime >=14.29.30037 + license: LicenseRef-MicrosoftWindowsSDK10 purls: [] - size: 31271315 - timestamp: 1774517904472 -- conda: https://conda.anaconda.org/conda-forge/noarch/notebook-shim-0.2.4-pyhd8ed1ab_1.conda - sha256: 7b920e46b9f7a2d2aa6434222e5c8d739021dbc5cc75f32d124a8191d86f9056 - md5: e7f89ea5f7ea9401642758ff50a2d9c1 + size: 694692 + timestamp: 1756385147981 +- conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-h41ae7f8_36.conda + sha256: 7c86d8ed3ac473c3e4dde0dd05aeb1f3189a26ad66c0e250f6cf4018e73358f2 + md5: 3466ff4a8753003eeb173f508d3d5a49 depends: - - jupyter_server >=1.8,<3 - - python >=3.9 + - vc14_runtime >=14.44.35208 + track_features: + - vc14 license: BSD-3-Clause license_family: BSD - purls: - - pkg:pypi/notebook-shim?source=hash-mapping - size: 16817 - timestamp: 1733408419340 -- pypi: https://files.pythonhosted.org/packages/0a/0d/0e3ecece05b7a7e87ab9fb587855548da437a061326fff64a223b6dcb78a/numpy-2.4.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - name: numpy - version: 2.4.4 - sha256: 81f4a14bee47aec54f883e0cad2d73986640c1590eb9bfaaba7ad17394481e6e - requires_python: '>=3.11' -- pypi: https://files.pythonhosted.org/packages/2d/5b/e1deebf88ff431b01b7406ca3583ab2bbb90972bbe1c568732e49c844f7e/numpy-2.4.4-cp312-cp312-win_amd64.whl - name: numpy - version: 2.4.4 - sha256: b5f0362dc928a6ecd9db58868fca5e48485205e3855957bdedea308f8672ea4a - requires_python: '>=3.11' -- pypi: https://files.pythonhosted.org/packages/91/fb/287076b2614e1d1044235f50f03748f31fa287e3dbe6abeb35cdfa351eca/numpy-2.4.4-cp314-cp314-macosx_14_0_arm64.whl - name: numpy - version: 2.4.4 - sha256: 2d19e6e2095506d1736b7d80595e0f252d76b89f5e715c35e06e937679ea7d7a - requires_python: '>=3.11' -- pypi: https://files.pythonhosted.org/packages/98/7c/21252050676612625449b4807d6b695b9ce8a7c9e1c197ee6216c8a65c7c/numpy-2.4.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - name: numpy - version: 2.4.4 - sha256: 27a8d92cd10f1382a67d7cf4db7ce18341b66438bdd9f691d7b0e48d104c2a9d - requires_python: '>=3.11' -- pypi: https://files.pythonhosted.org/packages/9b/fd/e5ecca1e78c05106d98028114f5c00d3eddb41207686b2b7de3e477b0e22/numpy-2.4.4-cp312-cp312-macosx_14_0_arm64.whl - name: numpy - version: 2.4.4 - sha256: 8b3b60bb7cba2c8c81837661c488637eee696f59a877788a396d33150c35d842 - requires_python: '>=3.11' -- pypi: https://files.pythonhosted.org/packages/fa/22/4cc31a62a6c7b74a8730e31a4274c5dc80e005751e277a2ce38e675e4923/numpy-2.4.4-cp314-cp314-win_amd64.whl - name: numpy - version: 2.4.4 - sha256: 715d1c092715954784bc79e1174fc2a90093dc4dc84ea15eb14dad8abdcdeb74 - requires_python: '>=3.11' -- conda: https://conda.anaconda.org/conda-forge/win-64/onemkl-license-2025.3.1-h57928b3_12.conda - sha256: 03eac4174077397a5bc480021e62412e73e80f34072d81053899f65dfe1045c7 - md5: 29ad104e60faa7ed1dc549ec029764bb - license: LicenseRef-IntelSimplifiedSoftwareOct2022 - license_family: Proprietary purls: [] - size: 40890 - timestamp: 1776904134221 -- conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.2-h35e630c_0.conda - sha256: c0ef482280e38c71a08ad6d71448194b719630345b0c9c60744a2010e8a8e0cb - md5: da1b85b6a87e141f5140bb9924cecab0 + size: 19989 + timestamp: 1778688080106 +- conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.44.35208-h818238b_36.conda + sha256: 902984f2282859a76d764d80d74f873df7c7749117cfac15c5106e086fb2b772 + md5: 65f5c81f2796961fcfd808eee8e73596 depends: - - __glibc >=2.17,<3.0.a0 - - ca-certificates - - libgcc >=14 - license: Apache-2.0 - license_family: Apache + - ucrt >=10.0.20348.0 + - vcomp14 14.44.35208 h818238b_36 + constrains: + - vs2015_runtime 14.44.35208.* *_36 + license: LicenseRef-MicrosoftVisualCpp2015-2022Runtime + license_family: Proprietary purls: [] - size: 3167099 - timestamp: 1775587756857 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.6.2-hd24854e_0.conda - sha256: c91bf510c130a1ea1b6ff023e28bac0ccaef869446acd805e2016f69ebdc49ea - md5: 25dcccd4f80f1638428613e0d7c9b4e1 + size: 683790 + timestamp: 1778688078434 +- conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.44.35208-h818238b_36.conda + sha256: 0cd5b905ab2b5e9fcb170fe8801b64917effef8e3a73ffd9b2cc4c3ee387f09c + md5: 4aa1884260877bd57d16070d20271e2d depends: - - __osx >=11.0 - - ca-certificates - license: Apache-2.0 - license_family: Apache + - ucrt >=10.0.20348.0 + constrains: + - vs2015_runtime 14.44.35208.* *_36 + license: LicenseRef-MicrosoftVisualCpp2015-2022Runtime + license_family: Proprietary purls: [] - size: 3106008 - timestamp: 1775587972483 -- conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.6.2-hf411b9b_0.conda - sha256: feb5815125c60f2be4a411e532db1ed1cd2d7261a6a43c54cb6ae90724e2e154 - md5: 05c7d624cff49dbd8db1ad5ba537a8a3 + size: 115995 + timestamp: 1778688058077 +- conda: https://conda.anaconda.org/conda-forge/win-64/winpty-0.4.3-4.tar.bz2 + sha256: 9df10c5b607dd30e05ba08cbd940009305c75db242476f4e845ea06008b0a283 + md5: 1cee351bf20b830d991dbe0bc8cd7dfe + license: MIT + license_family: MIT + purls: [] + size: 1176306 +- conda: https://conda.anaconda.org/conda-forge/win-64/yaml-0.2.5-h6a83c73_3.conda + sha256: 80ee68c1e7683a35295232ea79bcc87279d31ffeda04a1665efdb43cbd50a309 + md5: 433699cba6602098ae8957a323da2664 depends: - - ca-certificates + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 - ucrt >=10.0.20348.0 - vc >=14.3,<15 - vc14_runtime >=14.44.35208 - license: Apache-2.0 - license_family: Apache + - ucrt >=10.0.20348.0 + license: MIT + license_family: MIT purls: [] - size: 9410183 - timestamp: 1775589779763 -- conda: https://conda.anaconda.org/conda-forge/noarch/overrides-7.7.0-pyhd8ed1ab_1.conda - sha256: 1840bd90d25d4930d60f57b4f38d4e0ae3f5b8db2819638709c36098c6ba770c - md5: e51f1e4089cad105b6cac64bd8166587 + size: 63944 + timestamp: 1753484092156 +- conda: https://conda.anaconda.org/conda-forge/win-64/zeromq-4.3.5-h507cc87_10.conda + sha256: b8568dfde46edf3455458912ea6ffb760e4456db8230a0cf34ecbc557d3c275f + md5: 1ab0237036bfb14e923d6107473b0021 depends: - - python >=3.9 - - typing_utils - license: Apache-2.0 - license_family: APACHE - purls: - - pkg:pypi/overrides?source=hash-mapping - size: 30139 - timestamp: 1734587755455 -- conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.2-pyhc364b38_0.conda - sha256: 3906abfb6511a3bb309e39b9b1b7bc38f50a723971de2395489fd1f379255890 - md5: 4c06a92e74452cfa53623a81592e8934 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - libsodium >=1.0.21,<1.0.22.0a0 + - krb5 >=1.22.2,<1.23.0a0 + license: MPL-2.0 + license_family: MOZILLA + purls: [] + size: 265665 + timestamp: 1772476832995 +- conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_6.conda + sha256: 368d8628424966fd8f9c8018326a9c779e06913dd39e646cf331226acc90e5b2 + md5: 053b84beec00b71ea8ff7a4f84b55207 depends: - - python >=3.8 - - python - license: Apache-2.0 - license_family: APACHE - purls: - - pkg:pypi/packaging?source=compressed-mapping - size: 91574 - timestamp: 1777103621679 -- pypi: https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl - name: paginate - version: 0.5.7 - sha256: b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - libzlib >=1.3.1,<2.0a0 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 388453 + timestamp: 1764777142545 +- pypi: . + name: easydiffraction + requires_dist: + - arviz + - asciichartpy + - asteval + - bumps + - crysfml + - cryspy + - darkdetect + - dfo-ls + - diffpy-pdffit2 + - diffpy-utils + - gemmi + - h5py + - lmfit + - numpy + - pandas + - plotly + - pooch + - py3dmol + - rich + - scipy + - sympy + - typeguard + - typer + - uncertainties + - varname + - build ; extra == 'dev' + - copier ; extra == 'dev' + - docstripy ; extra == 'dev' + - essdiffraction ; 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' + requires_python: '>=3.12' +- pypi: https://files.pythonhosted.org/packages/01/5c/87b5fefdd3c4b157c8a16833f2236723136806814584c4589610217252f0/diffpy_pdffit2-1.6.0-cp312-cp312-macosx_11_0_arm64.whl + name: diffpy-pdffit2 + version: 1.6.0 + sha256: 4c4418388b9ab4eaeb485a9950a455b3713d21319a98d61e9f69ca5b9a6b45e3 + requires_dist: + - diffpy-structure + requires_python: '>=3.12,<3.15' +- pypi: https://files.pythonhosted.org/packages/01/8e/1e35281b8ab6d5d72ebe9911edcdffa3f36b04ed9d51dec6dd140396e220/scipy-1.17.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + name: scipy + version: 1.17.1 + sha256: 02ae3b274fde71c5e92ac4d54bc06c42d80e399fec704383dcd99b301df37458 + requires_dist: + - numpy>=1.26.4,<2.7 + - pytest>=8.0.0 ; extra == 'test' + - pytest-cov ; extra == 'test' + - pytest-timeout ; extra == 'test' + - pytest-xdist ; extra == 'test' + - asv ; extra == 'test' + - mpmath ; extra == 'test' + - gmpy2 ; extra == 'test' + - threadpoolctl ; extra == 'test' + - scikit-umfpack ; extra == 'test' + - pooch ; extra == 'test' + - hypothesis>=6.30 ; extra == 'test' + - array-api-strict>=2.3.1 ; extra == 'test' + - cython ; extra == 'test' + - meson ; extra == 'test' + - ninja ; sys_platform != 'emscripten' and extra == 'test' + - sphinx>=5.0.0,<8.2.0 ; extra == 'doc' + - intersphinx-registry ; extra == 'doc' + - pydata-sphinx-theme>=0.15.2 ; extra == 'doc' + - sphinx-copybutton ; extra == 'doc' + - sphinx-design>=0.4.0 ; extra == 'doc' + - matplotlib>=3.5 ; extra == 'doc' + - numpydoc ; extra == 'doc' + - jupytext ; extra == 'doc' + - myst-nb>=1.2.0 ; extra == 'doc' + - pooch ; extra == 'doc' + - jupyterlite-sphinx>=0.19.1 ; extra == 'doc' + - jupyterlite-pyodide-kernel ; extra == 'doc' + - linkify-it-py ; extra == 'doc' + - tabulate ; extra == 'doc' + - click<8.3.0 ; extra == 'dev' + - spin ; extra == 'dev' + - mypy==1.10.0 ; extra == 'dev' + - typing-extensions ; extra == 'dev' + - types-psutil ; extra == 'dev' + - pycodestyle ; extra == 'dev' + - ruff>=0.12.0 ; extra == 'dev' + - cython-lint>=0.12.2 ; extra == 'dev' + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/03/c1/0976b235cf29ead553e22f2fb6385a8252b533715e00d0ae52ed7b900582/h5py-3.16.0-cp312-cp312-win_amd64.whl + name: h5py + version: 3.16.0 + sha256: 96b422019a1c8975c2d5dadcf61d4ba6f01c31f92bbde6e4649607885fe502d6 + requires_dist: + - numpy>=1.21.2 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl + name: pyyaml-env-tag + version: '1.1' + sha256: 17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04 requires_dist: - - pytest ; extra == 'dev' - - tox ; extra == 'dev' - - black ; extra == 'lint' -- pypi: https://files.pythonhosted.org/packages/15/88/3cdd54fa279341afa10acf8d2b503556b1375245dccc9315659f795dd2e9/pandas-3.0.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl - name: pandas - version: 3.0.2 - sha256: deeca1b5a931fdf0c2212c8a659ade6d3b1edc21f0914ce71ef24456ca7a6535 + - pyyaml + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/04/5f/9ff93450ba96b09c7c2b3f81c94de31c89f92292f1380261bd7195bea4ea/contourpy-1.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + name: contourpy + version: 1.3.3 + sha256: f64836de09927cba6f79dcd00fdd7d5329f3fccc633468507079c829ca4db4e3 requires_dist: - - numpy>=1.26.0 ; python_full_version < '3.14' - - numpy>=2.3.3 ; python_full_version >= '3.14' - - python-dateutil>=2.8.2 - - tzdata ; sys_platform == 'win32' - - tzdata ; sys_platform == 'emscripten' - - hypothesis>=6.116.0 ; extra == 'test' - - pytest>=8.3.4 ; extra == 'test' - - pytest-xdist>=3.6.1 ; extra == 'test' - - pyarrow>=13.0.0 ; extra == 'pyarrow' - - bottleneck>=1.4.2 ; extra == 'performance' - - numba>=0.60.0 ; extra == 'performance' - - numexpr>=2.10.2 ; extra == 'performance' - - scipy>=1.14.1 ; extra == 'computation' - - xarray>=2024.10.0 ; extra == 'computation' - - fsspec>=2024.10.0 ; extra == 'fss' - - s3fs>=2024.10.0 ; extra == 'aws' - - gcsfs>=2024.10.0 ; extra == 'gcp' - - odfpy>=1.4.1 ; extra == 'excel' - - openpyxl>=3.1.5 ; extra == 'excel' - - python-calamine>=0.3.0 ; extra == 'excel' - - pyxlsb>=1.0.10 ; extra == 'excel' - - xlrd>=2.0.1 ; extra == 'excel' - - xlsxwriter>=3.2.0 ; extra == 'excel' - - pyarrow>=13.0.0 ; extra == 'parquet' - - pyarrow>=13.0.0 ; extra == 'feather' - - pyiceberg>=0.8.1 ; extra == 'iceberg' - - tables>=3.10.1 ; extra == 'hdf5' - - pyreadstat>=1.2.8 ; extra == 'spss' - - sqlalchemy>=2.0.36 ; extra == 'postgresql' - - psycopg2>=2.9.10 ; extra == 'postgresql' - - adbc-driver-postgresql>=1.2.0 ; extra == 'postgresql' - - sqlalchemy>=2.0.36 ; extra == 'mysql' - - pymysql>=1.1.1 ; extra == 'mysql' - - sqlalchemy>=2.0.36 ; extra == 'sql-other' - - adbc-driver-postgresql>=1.2.0 ; extra == 'sql-other' - - adbc-driver-sqlite>=1.2.0 ; extra == 'sql-other' - - beautifulsoup4>=4.12.3 ; extra == 'html' - - html5lib>=1.1 ; extra == 'html' - - lxml>=5.3.0 ; extra == 'html' - - lxml>=5.3.0 ; extra == 'xml' - - matplotlib>=3.9.3 ; extra == 'plot' - - jinja2>=3.1.5 ; extra == 'output-formatting' - - tabulate>=0.9.0 ; extra == 'output-formatting' - - pyqt5>=5.15.9 ; extra == 'clipboard' - - qtpy>=2.4.2 ; extra == 'clipboard' - - zstandard>=0.23.0 ; extra == 'compression' - - pytz>=2024.2 ; extra == 'timezone' - - adbc-driver-postgresql>=1.2.0 ; extra == 'all' - - adbc-driver-sqlite>=1.2.0 ; extra == 'all' - - beautifulsoup4>=4.12.3 ; extra == 'all' - - bottleneck>=1.4.2 ; extra == 'all' - - fastparquet>=2024.11.0 ; extra == 'all' - - fsspec>=2024.10.0 ; extra == 'all' - - gcsfs>=2024.10.0 ; extra == 'all' - - html5lib>=1.1 ; extra == 'all' - - hypothesis>=6.116.0 ; extra == 'all' - - jinja2>=3.1.5 ; extra == 'all' - - lxml>=5.3.0 ; extra == 'all' - - matplotlib>=3.9.3 ; extra == 'all' - - numba>=0.60.0 ; extra == 'all' - - numexpr>=2.10.2 ; extra == 'all' - - odfpy>=1.4.1 ; extra == 'all' - - openpyxl>=3.1.5 ; extra == 'all' - - psycopg2>=2.9.10 ; extra == 'all' - - pyarrow>=13.0.0 ; extra == 'all' - - pyiceberg>=0.8.1 ; extra == 'all' - - pymysql>=1.1.1 ; extra == 'all' - - pyqt5>=5.15.9 ; extra == 'all' - - pyreadstat>=1.2.8 ; extra == 'all' - - pytest>=8.3.4 ; extra == 'all' - - pytest-xdist>=3.6.1 ; extra == 'all' - - python-calamine>=0.3.0 ; extra == 'all' - - pytz>=2024.2 ; extra == 'all' - - pyxlsb>=1.0.10 ; extra == 'all' - - qtpy>=2.4.2 ; extra == 'all' - - scipy>=1.14.1 ; extra == 'all' - - s3fs>=2024.10.0 ; extra == 'all' - - sqlalchemy>=2.0.36 ; extra == 'all' - - tables>=3.10.1 ; extra == 'all' - - tabulate>=0.9.0 ; extra == 'all' - - xarray>=2024.10.0 ; extra == 'all' - - xlrd>=2.0.1 ; extra == 'all' - - xlsxwriter>=3.2.0 ; extra == 'all' - - zstandard>=0.23.0 ; extra == 'all' + - numpy>=1.25 + - furo ; extra == 'docs' + - sphinx>=7.2 ; extra == 'docs' + - sphinx-copybutton ; extra == 'docs' + - bokeh ; extra == 'bokeh' + - selenium ; extra == 'bokeh' + - contourpy[bokeh,docs] ; extra == 'mypy' + - bokeh ; extra == 'mypy' + - docutils-stubs ; extra == 'mypy' + - mypy==1.17.0 ; extra == 'mypy' + - types-pillow ; extra == 'mypy' + - contourpy[test-no-images] ; extra == 'test' + - matplotlib ; extra == 'test' + - pillow ; extra == 'test' + - pytest ; extra == 'test-no-images' + - pytest-cov ; extra == 'test-no-images' + - pytest-rerunfailures ; extra == 'test-no-images' + - pytest-xdist ; extra == 'test-no-images' + - wurlitzer ; extra == 'test-no-images' requires_python: '>=3.11' -- pypi: https://files.pythonhosted.org/packages/35/d0/4831af68ce30cc2d03c697bea8450e3225a835ef497d0d70f31b8cdde965/pandas-3.0.2-cp312-cp312-macosx_11_0_arm64.whl - name: pandas - version: 3.0.2 - sha256: 970762605cff1ca0d3f71ed4f3a769ea8f85fc8e6348f6e110b8fea7e6eb5a14 +- pypi: https://files.pythonhosted.org/packages/04/a1/4571fc46e7702de8d0c2dc54ad1b2f8e29328dea3ee90831181f7353d93c/matplotlib-3.10.9-cp312-cp312-win_amd64.whl + name: matplotlib + version: 3.10.9 + sha256: d091f9d758b34aaaaa6331d13574bf01891d903b3dec59bfff458ef7551de5d6 requires_dist: - - numpy>=1.26.0 ; python_full_version < '3.14' - - numpy>=2.3.3 ; python_full_version >= '3.14' - - python-dateutil>=2.8.2 - - tzdata ; sys_platform == 'win32' - - tzdata ; sys_platform == 'emscripten' - - hypothesis>=6.116.0 ; extra == 'test' - - pytest>=8.3.4 ; extra == 'test' - - pytest-xdist>=3.6.1 ; extra == 'test' - - pyarrow>=13.0.0 ; extra == 'pyarrow' - - bottleneck>=1.4.2 ; extra == 'performance' - - numba>=0.60.0 ; extra == 'performance' - - numexpr>=2.10.2 ; extra == 'performance' - - scipy>=1.14.1 ; extra == 'computation' - - xarray>=2024.10.0 ; extra == 'computation' - - fsspec>=2024.10.0 ; extra == 'fss' - - s3fs>=2024.10.0 ; extra == 'aws' - - gcsfs>=2024.10.0 ; extra == 'gcp' - - odfpy>=1.4.1 ; extra == 'excel' - - openpyxl>=3.1.5 ; extra == 'excel' - - python-calamine>=0.3.0 ; extra == 'excel' - - pyxlsb>=1.0.10 ; extra == 'excel' - - xlrd>=2.0.1 ; extra == 'excel' - - xlsxwriter>=3.2.0 ; extra == 'excel' - - pyarrow>=13.0.0 ; extra == 'parquet' - - pyarrow>=13.0.0 ; extra == 'feather' - - pyiceberg>=0.8.1 ; extra == 'iceberg' - - tables>=3.10.1 ; extra == 'hdf5' - - pyreadstat>=1.2.8 ; extra == 'spss' - - sqlalchemy>=2.0.36 ; extra == 'postgresql' - - psycopg2>=2.9.10 ; extra == 'postgresql' - - adbc-driver-postgresql>=1.2.0 ; extra == 'postgresql' - - sqlalchemy>=2.0.36 ; extra == 'mysql' - - pymysql>=1.1.1 ; extra == 'mysql' - - sqlalchemy>=2.0.36 ; extra == 'sql-other' - - adbc-driver-postgresql>=1.2.0 ; extra == 'sql-other' - - adbc-driver-sqlite>=1.2.0 ; extra == 'sql-other' - - beautifulsoup4>=4.12.3 ; extra == 'html' - - html5lib>=1.1 ; extra == 'html' - - lxml>=5.3.0 ; extra == 'html' - - lxml>=5.3.0 ; extra == 'xml' - - matplotlib>=3.9.3 ; extra == 'plot' - - jinja2>=3.1.5 ; extra == 'output-formatting' - - tabulate>=0.9.0 ; extra == 'output-formatting' - - pyqt5>=5.15.9 ; extra == 'clipboard' - - qtpy>=2.4.2 ; extra == 'clipboard' - - zstandard>=0.23.0 ; extra == 'compression' - - pytz>=2024.2 ; extra == 'timezone' - - adbc-driver-postgresql>=1.2.0 ; extra == 'all' - - adbc-driver-sqlite>=1.2.0 ; extra == 'all' - - beautifulsoup4>=4.12.3 ; extra == 'all' - - bottleneck>=1.4.2 ; extra == 'all' - - fastparquet>=2024.11.0 ; extra == 'all' - - fsspec>=2024.10.0 ; extra == 'all' - - gcsfs>=2024.10.0 ; extra == 'all' - - html5lib>=1.1 ; extra == 'all' - - hypothesis>=6.116.0 ; extra == 'all' - - jinja2>=3.1.5 ; extra == 'all' - - lxml>=5.3.0 ; extra == 'all' - - matplotlib>=3.9.3 ; extra == 'all' - - numba>=0.60.0 ; extra == 'all' - - numexpr>=2.10.2 ; extra == 'all' - - odfpy>=1.4.1 ; extra == 'all' - - openpyxl>=3.1.5 ; extra == 'all' - - psycopg2>=2.9.10 ; extra == 'all' - - pyarrow>=13.0.0 ; extra == 'all' - - pyiceberg>=0.8.1 ; extra == 'all' - - pymysql>=1.1.1 ; extra == 'all' - - pyqt5>=5.15.9 ; extra == 'all' - - pyreadstat>=1.2.8 ; extra == 'all' - - pytest>=8.3.4 ; extra == 'all' - - pytest-xdist>=3.6.1 ; extra == 'all' - - python-calamine>=0.3.0 ; extra == 'all' - - pytz>=2024.2 ; extra == 'all' - - pyxlsb>=1.0.10 ; extra == 'all' - - qtpy>=2.4.2 ; extra == 'all' - - scipy>=1.14.1 ; extra == 'all' - - s3fs>=2024.10.0 ; extra == 'all' - - sqlalchemy>=2.0.36 ; extra == 'all' - - tables>=3.10.1 ; extra == 'all' - - tabulate>=0.9.0 ; extra == 'all' - - xarray>=2024.10.0 ; extra == 'all' - - xlrd>=2.0.1 ; extra == 'all' - - xlsxwriter>=3.2.0 ; extra == 'all' - - zstandard>=0.23.0 ; extra == 'all' + - contourpy>=1.0.1 + - cycler>=0.10 + - fonttools>=4.22.0 + - kiwisolver>=1.3.1 + - numpy>=1.23 + - packaging>=20.0 + - pillow>=8 + - pyparsing>=3 + - python-dateutil>=2.7 + - meson-python>=0.13.1,<0.17.0 ; extra == 'dev' + - pybind11>=2.13.2,!=2.13.3 ; extra == 'dev' + - setuptools-scm>=7,<10 ; extra == 'dev' + - setuptools>=64 ; extra == 'dev' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/04/f1/58c14b37525dc075f3bdf149251f079723049a9f1c82eb48835a0e6b8db3/diffpy_pdffit2-1.6.0-cp314-cp314-macosx_11_0_arm64.whl + name: diffpy-pdffit2 + version: 1.6.0 + sha256: 0e178ff1d40e6b652dedb96b744a2eb04320f58b21012304b29d52167b62afa5 + requires_dist: + - diffpy-structure + requires_python: '>=3.12,<3.15' +- pypi: https://files.pythonhosted.org/packages/06/41/4e70dea1d0311016c0b0b1c53a24a266f9f8a34c6bc1af0f17cfca20aa1d/gemmi-0.7.5-cp314-cp314-macosx_11_0_arm64.whl + name: gemmi + version: 0.7.5 + sha256: 5144f107f2bca479d1b8266a79649bd631ee92c5b1319b27b0279157331ebc89 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/07/c7/deb8c5e604404dbf10a3808a858946ca3547692ff6316b698945bb72177e/python_socketio-5.16.1-py3-none-any.whl + name: python-socketio + version: 5.16.1 + sha256: a3eb1702e92aa2f2b5d3ba00261b61f062cce51f1cfb6900bf3ab4d1934d2d35 + requires_dist: + - bidict>=0.21.0 + - python-engineio>=4.11.0 + - requests>=2.21.0 ; extra == 'client' + - websocket-client>=0.54.0 ; extra == 'client' + - aiohttp>=3.4 ; extra == 'asyncio-client' + - tox ; extra == 'dev' + - sphinx ; extra == 'docs' + - furo ; extra == 'docs' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/08/ef/b3c6b9b5be2f82416d73fe2ed2e96e2793cd80e7510bd6a17ca79cdd88ec/fonttools-4.63.0-cp312-cp312-macosx_10_13_universal2.whl + name: fonttools + version: 4.63.0 + sha256: 37dd23e621e3b0aef1baa70a303b80aaf38449632cfc8fd2a55fb285bbccfc02 + requires_dist: + - lxml>=4.0 ; extra == 'lxml' + - brotli>=1.0.1 ; platform_python_implementation == 'CPython' and extra == 'woff' + - brotlicffi>=0.8.0 ; platform_python_implementation != 'CPython' and extra == 'woff' + - zopfli>=0.1.4 ; extra == 'woff' + - unicodedata2>=17.0.0 ; python_full_version < '3.15' and extra == 'unicode' + - lz4>=1.7.4.2 ; extra == 'graphite' + - scipy ; platform_python_implementation != 'PyPy' and extra == 'interpolatable' + - munkres ; platform_python_implementation == 'PyPy' and extra == 'interpolatable' + - pycairo ; extra == 'interpolatable' + - matplotlib ; extra == 'plot' + - sympy ; extra == 'symfont' + - xattr ; sys_platform == 'darwin' and extra == 'type1' + - skia-pathops>=0.5.0 ; extra == 'pathops' + - uharfbuzz>=0.45.0 ; extra == 'repacker' + - lxml>=4.0 ; extra == 'all' + - brotli>=1.0.1 ; platform_python_implementation == 'CPython' and extra == 'all' + - brotlicffi>=0.8.0 ; platform_python_implementation != 'CPython' and extra == 'all' + - zopfli>=0.1.4 ; extra == 'all' + - unicodedata2>=17.0.0 ; python_full_version < '3.15' and extra == 'all' + - lz4>=1.7.4.2 ; extra == 'all' + - scipy ; platform_python_implementation != 'PyPy' and extra == 'all' + - munkres ; platform_python_implementation == 'PyPy' and extra == 'all' + - pycairo ; extra == 'all' + - matplotlib ; extra == 'all' + - sympy ; extra == 'all' + - xattr ; sys_platform == 'darwin' and extra == 'all' + - skia-pathops>=0.5.0 ; extra == 'all' + - uharfbuzz>=0.45.0 ; extra == 'all' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/0b/2e/7eea398450457ecb54e18e9d10110993fa65561c4f3add5e8eccd2b9cd41/scipy-1.17.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + name: scipy + version: 1.17.1 + sha256: eee2cfda04c00a857206a4330f0c5e3e56535494e30ca445eb19ec624ae75118 + requires_dist: + - numpy>=1.26.4,<2.7 + - pytest>=8.0.0 ; extra == 'test' + - pytest-cov ; extra == 'test' + - pytest-timeout ; extra == 'test' + - pytest-xdist ; extra == 'test' + - asv ; extra == 'test' + - mpmath ; extra == 'test' + - gmpy2 ; extra == 'test' + - threadpoolctl ; extra == 'test' + - scikit-umfpack ; extra == 'test' + - pooch ; extra == 'test' + - hypothesis>=6.30 ; extra == 'test' + - array-api-strict>=2.3.1 ; extra == 'test' + - cython ; extra == 'test' + - meson ; extra == 'test' + - ninja ; sys_platform != 'emscripten' and extra == 'test' + - sphinx>=5.0.0,<8.2.0 ; extra == 'doc' + - intersphinx-registry ; extra == 'doc' + - pydata-sphinx-theme>=0.15.2 ; extra == 'doc' + - sphinx-copybutton ; extra == 'doc' + - sphinx-design>=0.4.0 ; extra == 'doc' + - matplotlib>=3.5 ; extra == 'doc' + - numpydoc ; extra == 'doc' + - jupytext ; extra == 'doc' + - myst-nb>=1.2.0 ; extra == 'doc' + - pooch ; extra == 'doc' + - jupyterlite-sphinx>=0.19.1 ; extra == 'doc' + - jupyterlite-pyodide-kernel ; extra == 'doc' + - linkify-it-py ; extra == 'doc' + - tabulate ; extra == 'doc' + - click<8.3.0 ; extra == 'dev' + - spin ; extra == 'dev' + - mypy==1.10.0 ; extra == 'dev' + - typing-extensions ; extra == 'dev' + - types-psutil ; extra == 'dev' + - pycodestyle ; extra == 'dev' + - ruff>=0.12.0 ; extra == 'dev' + - cython-lint>=0.12.2 ; extra == 'dev' requires_python: '>=3.11' -- pypi: https://files.pythonhosted.org/packages/7b/8b/721a9cff6fa6a91b162eb51019c6243b82b3226c71bb6c8ef4a9bd65cbc6/pandas-3.0.2-cp312-cp312-win_amd64.whl - name: pandas - version: 3.0.2 - sha256: a4785e1d6547d8427c5208b748ae2efb64659a21bd82bf440d4262d02bfa02a4 +- pypi: https://files.pythonhosted.org/packages/0b/f9/f15c95d6b200167cb22c5eca5eecfa9d28a8ee3f74095f1cd2345c71f2f9/pydoclint-0.8.4-py3-none-any.whl + name: pydoclint + version: 0.8.4 + sha256: 5e0f94f785d0e902faacebb117aadf84d6e30c5f781e0fdd0ee03c3b80ea2098 requires_dist: - - numpy>=1.26.0 ; python_full_version < '3.14' - - numpy>=2.3.3 ; python_full_version >= '3.14' - - python-dateutil>=2.8.2 - - tzdata ; sys_platform == 'win32' - - tzdata ; sys_platform == 'emscripten' - - hypothesis>=6.116.0 ; extra == 'test' - - pytest>=8.3.4 ; extra == 'test' - - pytest-xdist>=3.6.1 ; extra == 'test' - - pyarrow>=13.0.0 ; extra == 'pyarrow' - - bottleneck>=1.4.2 ; extra == 'performance' - - numba>=0.60.0 ; extra == 'performance' - - numexpr>=2.10.2 ; extra == 'performance' - - scipy>=1.14.1 ; extra == 'computation' - - xarray>=2024.10.0 ; extra == 'computation' - - fsspec>=2024.10.0 ; extra == 'fss' - - s3fs>=2024.10.0 ; extra == 'aws' - - gcsfs>=2024.10.0 ; extra == 'gcp' - - odfpy>=1.4.1 ; extra == 'excel' - - openpyxl>=3.1.5 ; extra == 'excel' - - python-calamine>=0.3.0 ; extra == 'excel' - - pyxlsb>=1.0.10 ; extra == 'excel' - - xlrd>=2.0.1 ; extra == 'excel' - - xlsxwriter>=3.2.0 ; extra == 'excel' - - pyarrow>=13.0.0 ; extra == 'parquet' - - pyarrow>=13.0.0 ; extra == 'feather' - - pyiceberg>=0.8.1 ; extra == 'iceberg' - - tables>=3.10.1 ; extra == 'hdf5' - - pyreadstat>=1.2.8 ; extra == 'spss' - - sqlalchemy>=2.0.36 ; extra == 'postgresql' - - psycopg2>=2.9.10 ; extra == 'postgresql' - - adbc-driver-postgresql>=1.2.0 ; extra == 'postgresql' - - sqlalchemy>=2.0.36 ; extra == 'mysql' - - pymysql>=1.1.1 ; extra == 'mysql' - - sqlalchemy>=2.0.36 ; extra == 'sql-other' - - adbc-driver-postgresql>=1.2.0 ; extra == 'sql-other' - - adbc-driver-sqlite>=1.2.0 ; extra == 'sql-other' - - beautifulsoup4>=4.12.3 ; extra == 'html' - - html5lib>=1.1 ; extra == 'html' - - lxml>=5.3.0 ; extra == 'html' - - lxml>=5.3.0 ; extra == 'xml' - - matplotlib>=3.9.3 ; extra == 'plot' - - jinja2>=3.1.5 ; extra == 'output-formatting' - - tabulate>=0.9.0 ; extra == 'output-formatting' - - pyqt5>=5.15.9 ; extra == 'clipboard' - - qtpy>=2.4.2 ; extra == 'clipboard' - - zstandard>=0.23.0 ; extra == 'compression' - - pytz>=2024.2 ; extra == 'timezone' - - adbc-driver-postgresql>=1.2.0 ; extra == 'all' - - adbc-driver-sqlite>=1.2.0 ; extra == 'all' - - beautifulsoup4>=4.12.3 ; extra == 'all' - - bottleneck>=1.4.2 ; extra == 'all' - - fastparquet>=2024.11.0 ; extra == 'all' - - fsspec>=2024.10.0 ; extra == 'all' - - gcsfs>=2024.10.0 ; extra == 'all' - - html5lib>=1.1 ; extra == 'all' - - hypothesis>=6.116.0 ; extra == 'all' - - jinja2>=3.1.5 ; extra == 'all' - - lxml>=5.3.0 ; extra == 'all' - - matplotlib>=3.9.3 ; extra == 'all' - - numba>=0.60.0 ; extra == 'all' - - numexpr>=2.10.2 ; extra == 'all' - - odfpy>=1.4.1 ; extra == 'all' - - openpyxl>=3.1.5 ; extra == 'all' - - psycopg2>=2.9.10 ; extra == 'all' - - pyarrow>=13.0.0 ; extra == 'all' - - pyiceberg>=0.8.1 ; extra == 'all' - - pymysql>=1.1.1 ; extra == 'all' - - pyqt5>=5.15.9 ; extra == 'all' - - pyreadstat>=1.2.8 ; extra == 'all' - - pytest>=8.3.4 ; extra == 'all' - - pytest-xdist>=3.6.1 ; extra == 'all' - - python-calamine>=0.3.0 ; extra == 'all' - - pytz>=2024.2 ; extra == 'all' - - pyxlsb>=1.0.10 ; extra == 'all' - - qtpy>=2.4.2 ; extra == 'all' - - scipy>=1.14.1 ; extra == 'all' - - s3fs>=2024.10.0 ; extra == 'all' - - sqlalchemy>=2.0.36 ; extra == 'all' - - tables>=3.10.1 ; extra == 'all' - - tabulate>=0.9.0 ; extra == 'all' - - xarray>=2024.10.0 ; extra == 'all' - - xlrd>=2.0.1 ; extra == 'all' - - xlsxwriter>=3.2.0 ; extra == 'all' - - zstandard>=0.23.0 ; extra == 'all' + - 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/0c/53/b50773ecf1d1e4a5858ee13011e30317ba02639ae4a1411a34967951fc9b/crysfml-0.6.2-cp314-cp314-win_amd64.whl + name: crysfml + version: 0.6.2 + sha256: 4278178f2028360f489f2cdfda7f2f7f26e4f1674b50eb934f403bb443a8f00a + requires_dist: + - numpy + requires_python: '>=3.11,<3.15' +- pypi: https://files.pythonhosted.org/packages/0d/12/bbce9472f489cb5c4c23b0d13e5c59c37c1aab11b7ac637dfe6bbdccebe7/copier-9.15.1-py3-none-any.whl + name: copier + version: 9.15.1 + sha256: 040164686e45e7a841dcd4ae39b01e27093ff91242be3563cae883c4e24c55cc + 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/0d/1f/d398de1612f7a611e22d743280339c9af4903675635e41be3370091c704b/arviz_stats-1.1.0-py3-none-any.whl + name: arviz-stats + version: 1.1.0 + sha256: ed47334ccff8670a0b90a50e1a37e7257268084eb3436e6b7b15e623f1001947 + requires_dist: + - numpy>=2 + - scipy>=1.13 + - sphinx-book-theme ; extra == 'doc' + - myst-parser[linkify] ; extra == 'doc' + - myst-nb ; extra == 'doc' + - sphinx-copybutton ; extra == 'doc' + - numpydoc ; extra == 'doc' + - sphinx<9 ; extra == 'doc' + - sphinx-design ; extra == 'doc' + - jupyter-sphinx ; extra == 'doc' + - h5netcdf[h5py] ; extra == 'doc' + - sphinx-autosummary-accessors ; extra == 'doc' + - numba ; extra == 'numba' + - xarray-einstats[einops,numba] ; extra == 'numba' + - pytest ; extra == 'test' + - pytest-cov ; extra == 'test' + - pytest ; extra == 'test-xarray' + - pytest-cov ; extra == 'test-xarray' + - h5netcdf[h5py] ; extra == 'test-xarray' + - arviz-base>=1.1,<1.2 ; extra == 'xarray' + - xarray-einstats ; extra == 'xarray' + - xarray>=2024.11.0 ; extra == 'xarray' + requires_python: '>=3.12' +- pypi: https://files.pythonhosted.org/packages/0d/fe/6bea5c9162869c5beba5d9c8abbed835ec85bf1ec1fba05a3822325c45f3/build-1.5.0-py3-none-any.whl + name: build + version: 1.5.0 + sha256: 13f3eecb844759ab66efec90ca17639bbf14dc06cb2fdf37a9010322d9c50a6f + requires_dist: + - packaging>=24.0 + - pyproject-hooks + - colorama ; os_name == 'nt' + - importlib-metadata>=4.6 ; python_full_version < '3.10.2' + - tomli>=1.1.0 ; python_full_version < '3.11' + - keyring ; extra == 'keyring' + - uv>=0.1.18 ; extra == 'uv' + - virtualenv>=20.17 ; python_full_version >= '3.10' and python_full_version < '3.14' and extra == 'virtualenv' + - virtualenv>=20.31 ; python_full_version >= '3.14' and extra == 'virtualenv' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/0f/0e/0eb94e64f5badef67f11fe1e448dde2a44f00940d8949f4adf71d560552e/scipp-26.3.1-cp314-cp314-macosx_14_0_arm64.whl + name: scipp + version: 26.3.1 + sha256: 1f103f6c5a33b08773206c613fe2dd9c02585f5c4e44b77311c54b7828a758ed + 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/95/25/bdb9326c3b5455f8d4d3549fce7abcf967259de146fe2cf7a82368141948/pandas-3.0.2-cp314-cp314-macosx_11_0_arm64.whl - name: pandas - version: 3.0.2 - sha256: 0555c5882688a39317179ab4a0ed41d3ebc8812ab14c69364bbee8fb7a3f6288 +- pypi: https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl + name: aiohappyeyeballs + version: 2.6.1 + sha256: f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl + name: pyparsing + version: 3.3.2 + sha256: 850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d + requires_dist: + - railroad-diagrams ; extra == 'diagrams' + - jinja2 ; extra == 'diagrams' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl + name: blinker + version: 1.9.0 + sha256: ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/11/8c/c9138d881c79aa0ea9ed83cbd58d5ca75624378b38cee225dcf5c42cc91f/griffelib-2.0.2-py3-none-any.whl + name: griffelib + version: 2.0.2 + sha256: 925c857658fb1ba40c0772c37acbc2ab650bd794d9c1b9726922e36ea4117ea1 + requires_dist: + - pip>=24.0 ; extra == 'pypi' + - platformdirs>=4.2 ; extra == 'pypi' + - wheel>=0.42 ; extra == 'pypi' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/11/8d/d2532ad2a603ca2b93ad9f5135732124e57811d0168155852f37fbce2458/pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + name: pillow + version: 12.2.0 + sha256: 4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286 + requires_dist: + - furo ; extra == 'docs' + - olefile ; extra == 'docs' + - sphinx>=8.2 ; extra == 'docs' + - sphinx-autobuild ; extra == 'docs' + - sphinx-copybutton ; extra == 'docs' + - sphinx-inline-tabs ; extra == 'docs' + - sphinxext-opengraph ; extra == 'docs' + - olefile ; extra == 'fpx' + - olefile ; extra == 'mic' + - arro3-compute ; extra == 'test-arrow' + - arro3-core ; extra == 'test-arrow' + - nanoarrow ; extra == 'test-arrow' + - pyarrow ; extra == 'test-arrow' + - check-manifest ; extra == 'tests' + - coverage>=7.4.2 ; extra == 'tests' + - defusedxml ; extra == 'tests' + - markdown2 ; extra == 'tests' + - olefile ; extra == 'tests' + - packaging ; extra == 'tests' + - pyroma>=5 ; extra == 'tests' + - pytest ; extra == 'tests' + - pytest-cov ; extra == 'tests' + - pytest-timeout ; extra == 'tests' + - pytest-xdist ; extra == 'tests' + - trove-classifiers>=2024.10.12 ; extra == 'tests' + - defusedxml ; extra == 'xmp' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/12/aa/fb2a0649fdeef5ab7072d221e8f4df164098792c813af6c87e2581cfa860/mpltoolbox-26.2.0-py3-none-any.whl + name: mpltoolbox + version: 26.2.0 + sha256: cd2668db4216fc4d7c2ba37974961aa61445f1517527b645b6082930e35ba7f0 requires_dist: - - numpy>=1.26.0 ; python_full_version < '3.14' - - numpy>=2.3.3 ; python_full_version >= '3.14' - - python-dateutil>=2.8.2 - - tzdata ; sys_platform == 'win32' - - tzdata ; sys_platform == 'emscripten' - - hypothesis>=6.116.0 ; extra == 'test' - - pytest>=8.3.4 ; extra == 'test' - - pytest-xdist>=3.6.1 ; extra == 'test' - - pyarrow>=13.0.0 ; extra == 'pyarrow' - - bottleneck>=1.4.2 ; extra == 'performance' - - numba>=0.60.0 ; extra == 'performance' - - numexpr>=2.10.2 ; extra == 'performance' - - scipy>=1.14.1 ; extra == 'computation' - - xarray>=2024.10.0 ; extra == 'computation' - - fsspec>=2024.10.0 ; extra == 'fss' - - s3fs>=2024.10.0 ; extra == 'aws' - - gcsfs>=2024.10.0 ; extra == 'gcp' - - odfpy>=1.4.1 ; extra == 'excel' - - openpyxl>=3.1.5 ; extra == 'excel' - - python-calamine>=0.3.0 ; extra == 'excel' - - pyxlsb>=1.0.10 ; extra == 'excel' - - xlrd>=2.0.1 ; extra == 'excel' - - xlsxwriter>=3.2.0 ; extra == 'excel' - - pyarrow>=13.0.0 ; extra == 'parquet' - - pyarrow>=13.0.0 ; extra == 'feather' - - pyiceberg>=0.8.1 ; extra == 'iceberg' - - tables>=3.10.1 ; extra == 'hdf5' - - pyreadstat>=1.2.8 ; extra == 'spss' - - sqlalchemy>=2.0.36 ; extra == 'postgresql' - - psycopg2>=2.9.10 ; extra == 'postgresql' - - adbc-driver-postgresql>=1.2.0 ; extra == 'postgresql' - - sqlalchemy>=2.0.36 ; extra == 'mysql' - - pymysql>=1.1.1 ; extra == 'mysql' - - sqlalchemy>=2.0.36 ; extra == 'sql-other' - - adbc-driver-postgresql>=1.2.0 ; extra == 'sql-other' - - adbc-driver-sqlite>=1.2.0 ; extra == 'sql-other' - - beautifulsoup4>=4.12.3 ; extra == 'html' - - html5lib>=1.1 ; extra == 'html' - - lxml>=5.3.0 ; extra == 'html' - - lxml>=5.3.0 ; extra == 'xml' - - matplotlib>=3.9.3 ; extra == 'plot' - - jinja2>=3.1.5 ; extra == 'output-formatting' - - tabulate>=0.9.0 ; extra == 'output-formatting' - - pyqt5>=5.15.9 ; extra == 'clipboard' - - qtpy>=2.4.2 ; extra == 'clipboard' - - zstandard>=0.23.0 ; extra == 'compression' - - pytz>=2024.2 ; extra == 'timezone' - - adbc-driver-postgresql>=1.2.0 ; extra == 'all' - - adbc-driver-sqlite>=1.2.0 ; extra == 'all' - - beautifulsoup4>=4.12.3 ; extra == 'all' - - bottleneck>=1.4.2 ; extra == 'all' - - fastparquet>=2024.11.0 ; extra == 'all' - - fsspec>=2024.10.0 ; extra == 'all' - - gcsfs>=2024.10.0 ; extra == 'all' - - html5lib>=1.1 ; extra == 'all' - - hypothesis>=6.116.0 ; extra == 'all' - - jinja2>=3.1.5 ; extra == 'all' - - lxml>=5.3.0 ; extra == 'all' - - matplotlib>=3.9.3 ; extra == 'all' - - numba>=0.60.0 ; extra == 'all' - - numexpr>=2.10.2 ; extra == 'all' - - odfpy>=1.4.1 ; extra == 'all' - - openpyxl>=3.1.5 ; extra == 'all' - - psycopg2>=2.9.10 ; extra == 'all' - - pyarrow>=13.0.0 ; extra == 'all' - - pyiceberg>=0.8.1 ; extra == 'all' - - pymysql>=1.1.1 ; extra == 'all' - - pyqt5>=5.15.9 ; extra == 'all' - - pyreadstat>=1.2.8 ; extra == 'all' - - pytest>=8.3.4 ; extra == 'all' - - pytest-xdist>=3.6.1 ; extra == 'all' - - python-calamine>=0.3.0 ; extra == 'all' - - pytz>=2024.2 ; extra == 'all' - - pyxlsb>=1.0.10 ; extra == 'all' - - qtpy>=2.4.2 ; extra == 'all' - - scipy>=1.14.1 ; extra == 'all' - - s3fs>=2024.10.0 ; extra == 'all' - - sqlalchemy>=2.0.36 ; extra == 'all' - - tables>=3.10.1 ; extra == 'all' - - tabulate>=0.9.0 ; extra == 'all' - - xarray>=2024.10.0 ; extra == 'all' - - xlrd>=2.0.1 ; extra == 'all' - - xlsxwriter>=3.2.0 ; extra == 'all' - - zstandard>=0.23.0 ; extra == 'all' + - matplotlib + - ipympl ; extra == 'test' + - pytest>=8.0 ; extra == 'test' requires_python: '>=3.11' -- pypi: https://files.pythonhosted.org/packages/c4/a8/3a61a721472959ab0ce865ef05d10b0d6bfe27ce8801c99f33d4fa996e65/pandas-3.0.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl - name: pandas - version: 3.0.2 - sha256: ef8b27695c3d3dc78403c9a7d5e59a62d5464a7e1123b4e0042763f7104dc74f +- pypi: https://files.pythonhosted.org/packages/12/c9/6869a1dcf4aaf309b9543ec070be3ec3adebee7c9bec9af8c230494134b9/interrogate-1.7.0-py3-none-any.whl + name: interrogate + version: 1.7.0 + sha256: b13ff4dd8403369670e2efe684066de9fcb868ad9d7f2b4095d8112142dc9d12 requires_dist: - - numpy>=1.26.0 ; python_full_version < '3.14' - - numpy>=2.3.3 ; python_full_version >= '3.14' - - python-dateutil>=2.8.2 - - tzdata ; sys_platform == 'win32' - - tzdata ; sys_platform == 'emscripten' - - hypothesis>=6.116.0 ; extra == 'test' - - pytest>=8.3.4 ; extra == 'test' - - pytest-xdist>=3.6.1 ; extra == 'test' - - pyarrow>=13.0.0 ; extra == 'pyarrow' - - bottleneck>=1.4.2 ; extra == 'performance' - - numba>=0.60.0 ; extra == 'performance' - - numexpr>=2.10.2 ; extra == 'performance' - - scipy>=1.14.1 ; extra == 'computation' - - xarray>=2024.10.0 ; extra == 'computation' - - fsspec>=2024.10.0 ; extra == 'fss' - - s3fs>=2024.10.0 ; extra == 'aws' - - gcsfs>=2024.10.0 ; extra == 'gcp' - - odfpy>=1.4.1 ; extra == 'excel' - - openpyxl>=3.1.5 ; extra == 'excel' - - python-calamine>=0.3.0 ; extra == 'excel' - - pyxlsb>=1.0.10 ; extra == 'excel' - - xlrd>=2.0.1 ; extra == 'excel' - - xlsxwriter>=3.2.0 ; extra == 'excel' - - pyarrow>=13.0.0 ; extra == 'parquet' - - pyarrow>=13.0.0 ; extra == 'feather' - - pyiceberg>=0.8.1 ; extra == 'iceberg' - - tables>=3.10.1 ; extra == 'hdf5' - - pyreadstat>=1.2.8 ; extra == 'spss' - - sqlalchemy>=2.0.36 ; extra == 'postgresql' - - psycopg2>=2.9.10 ; extra == 'postgresql' - - adbc-driver-postgresql>=1.2.0 ; extra == 'postgresql' - - sqlalchemy>=2.0.36 ; extra == 'mysql' - - pymysql>=1.1.1 ; extra == 'mysql' - - sqlalchemy>=2.0.36 ; extra == 'sql-other' - - adbc-driver-postgresql>=1.2.0 ; extra == 'sql-other' - - adbc-driver-sqlite>=1.2.0 ; extra == 'sql-other' - - beautifulsoup4>=4.12.3 ; extra == 'html' - - html5lib>=1.1 ; extra == 'html' - - lxml>=5.3.0 ; extra == 'html' - - lxml>=5.3.0 ; extra == 'xml' - - matplotlib>=3.9.3 ; extra == 'plot' - - jinja2>=3.1.5 ; extra == 'output-formatting' - - tabulate>=0.9.0 ; extra == 'output-formatting' - - pyqt5>=5.15.9 ; extra == 'clipboard' - - qtpy>=2.4.2 ; extra == 'clipboard' - - zstandard>=0.23.0 ; extra == 'compression' - - pytz>=2024.2 ; extra == 'timezone' - - adbc-driver-postgresql>=1.2.0 ; extra == 'all' - - adbc-driver-sqlite>=1.2.0 ; extra == 'all' - - beautifulsoup4>=4.12.3 ; extra == 'all' - - bottleneck>=1.4.2 ; extra == 'all' - - fastparquet>=2024.11.0 ; extra == 'all' - - fsspec>=2024.10.0 ; extra == 'all' - - gcsfs>=2024.10.0 ; extra == 'all' - - html5lib>=1.1 ; extra == 'all' - - hypothesis>=6.116.0 ; extra == 'all' - - jinja2>=3.1.5 ; extra == 'all' - - lxml>=5.3.0 ; extra == 'all' - - matplotlib>=3.9.3 ; extra == 'all' - - numba>=0.60.0 ; extra == 'all' - - numexpr>=2.10.2 ; extra == 'all' - - odfpy>=1.4.1 ; extra == 'all' - - openpyxl>=3.1.5 ; extra == 'all' - - psycopg2>=2.9.10 ; extra == 'all' - - pyarrow>=13.0.0 ; extra == 'all' - - pyiceberg>=0.8.1 ; extra == 'all' - - pymysql>=1.1.1 ; extra == 'all' - - pyqt5>=5.15.9 ; extra == 'all' - - pyreadstat>=1.2.8 ; extra == 'all' - - pytest>=8.3.4 ; extra == 'all' - - pytest-xdist>=3.6.1 ; extra == 'all' - - python-calamine>=0.3.0 ; extra == 'all' - - pytz>=2024.2 ; extra == 'all' - - pyxlsb>=1.0.10 ; extra == 'all' - - qtpy>=2.4.2 ; extra == 'all' - - scipy>=1.14.1 ; extra == 'all' - - s3fs>=2024.10.0 ; extra == 'all' - - sqlalchemy>=2.0.36 ; extra == 'all' - - tables>=3.10.1 ; extra == 'all' - - tabulate>=0.9.0 ; extra == 'all' - - xarray>=2024.10.0 ; extra == 'all' - - xlrd>=2.0.1 ; extra == 'all' - - xlsxwriter>=3.2.0 ; extra == 'all' - - zstandard>=0.23.0 ; extra == 'all' + - attrs + - click>=7.1 + - colorama + - py + - tabulate + - tomli ; python_full_version < '3.11' + - cairosvg ; extra == 'dev' + - sphinx ; extra == 'dev' + - sphinx-autobuild ; extra == 'dev' + - pytest ; extra == 'dev' + - pytest-cov ; extra == 'dev' + - pytest-mock ; extra == 'dev' + - coverage[toml] ; extra == 'dev' + - wheel ; extra == 'dev' + - pre-commit ; extra == 'dev' + - sphinx ; extra == 'docs' + - sphinx-autobuild ; extra == 'docs' + - cairosvg ; extra == 'png' + - pytest ; extra == 'tests' + - pytest-cov ; extra == 'tests' + - pytest-mock ; extra == 'tests' + - coverage[toml] ; extra == 'tests' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/13/95/cf3f7fe4910cf0365fa8ea0c731f4b8a624d97cd76ea777913ac8d0868e2/mkdocs_jupyter-0.26.3-py3-none-any.whl + name: mkdocs-jupyter + version: 0.26.3 + sha256: cd6644fb578131157194d750fd4d10fc2fd8f1e84e00036ee62df3b5b4b84c82 + requires_dist: + - ipykernel>6.0.0,<8 + - jupytext>1.13.8,<2 + - mkdocs-material>9.0.0 + - mkdocs>=1.4.0,<2 + - nbconvert>=7.2.9,<8 + - pygments>2.12.0 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/14/15/5574111ae50dd6e879456888c0eadd4c5a869959775854e18e18a6b345f3/propcache-0.5.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + name: propcache + version: 0.5.2 + sha256: 6f328175a2cde1f0ff2c4ed8ce968b9dcfb55f3a7153f39e2957ed994da13476 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/15/1d/9f9e30d76300b0150afaa8b37fab9a0194d44fd4f6b1e5038aca4a1440ed/crysfml-0.6.2-cp312-cp312-macosx_14_0_arm64.whl + name: crysfml + version: 0.6.2 + sha256: 75bba671d2237f6fbbb1284c473543eb143b5bd3ab69f40a2d2cf343dbe0977f + requires_dist: + - numpy + requires_python: '>=3.11,<3.15' +- pypi: https://files.pythonhosted.org/packages/19/2a/725ecc166d53438bc88f76822ed4b1e3b10756e790bafd7b523fe97c322d/yarl-1.23.0-cp312-cp312-macosx_11_0_arm64.whl + name: yarl + version: 1.23.0 + sha256: 13a563739ae600a631c36ce096615fe307f131344588b0bc0daec108cdb47b25 + requires_dist: + - idna>=2.0 + - multidict>=4.0 + - propcache>=0.2.1 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl + name: pydantic-core + version: 2.46.4 + sha256: 962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f + requires_dist: + - typing-extensions>=4.14.1 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/19/d4/225027a913621a879b429a043674aa35220e6ce67785acad4f7bd0c4ff33/xarray_einstats-0.10.0-py3-none-any.whl + name: xarray-einstats + version: 0.10.0 + sha256: fa3169b46cee29092db820d8bbc203148bada4fc970ee75e62cbf3dd7c5a8945 + requires_dist: + - numpy>=2.0 + - scipy>=1.13 + - xarray>=2024.2.0 + - furo ; extra == 'doc' + - myst-parser[linkify] ; extra == 'doc' + - myst-nb ; extra == 'doc' + - sphinx-copybutton ; extra == 'doc' + - numpydoc ; extra == 'doc' + - sphinx>=5 ; extra == 'doc' + - jupyter-sphinx ; extra == 'doc' + - sphinx-design ; extra == 'doc' + - watermark ; extra == 'doc' + - matplotlib ; extra == 'doc' + - sphinx-togglebutton ; extra == 'doc' + - einops ; extra == 'einops' + - numba>=0.55 ; extra == 'numba' + - hypothesis ; extra == 'test' + - pytest ; extra == 'test' + - pytest-cov ; extra == 'test' + - packaging ; extra == 'test' + - scipy>=1.15 ; extra == 'test' + - preliz>=0.19 ; extra == 'test' + requires_python: '>=3.12' +- pypi: https://files.pythonhosted.org/packages/19/e8/6026ed58a64563186a9ee3f29f41261fd1828f527dd93d33b60feca63352/contourpy-1.3.3-cp312-cp312-win_amd64.whl + name: contourpy + version: 1.3.3 + sha256: 8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b + requires_dist: + - numpy>=1.25 + - furo ; extra == 'docs' + - sphinx>=7.2 ; extra == 'docs' + - sphinx-copybutton ; extra == 'docs' + - bokeh ; extra == 'bokeh' + - selenium ; extra == 'bokeh' + - contourpy[bokeh,docs] ; extra == 'mypy' + - bokeh ; extra == 'mypy' + - docutils-stubs ; extra == 'mypy' + - mypy==1.17.0 ; extra == 'mypy' + - types-pillow ; extra == 'mypy' + - contourpy[test-no-images] ; extra == 'test' + - matplotlib ; extra == 'test' + - pillow ; extra == 'test' + - pytest ; extra == 'test-no-images' + - pytest-cov ; extra == 'test-no-images' + - pytest-rerunfailures ; extra == 'test-no-images' + - pytest-xdist ; extra == 'test-no-images' + - wurlitzer ; extra == 'test-no-images' requires_python: '>=3.11' -- pypi: https://files.pythonhosted.org/packages/db/60/aba6a38de456e7341285102bede27514795c1eaa353bc0e7638b6b785356/pandas-3.0.2-cp314-cp314-win_amd64.whl - name: pandas - version: 3.0.2 - sha256: b35d14bb5d8285d9494fe93815a9e9307c0876e10f1e8e89ac5b88f728ec8dcf +- pypi: https://files.pythonhosted.org/packages/1a/1f/86b4d15221096cb5500bcd73bf350745749e3ba056cdd7a7f75f126f154e/scipp-26.3.1-cp312-cp312-win_amd64.whl + name: scipp + version: 26.3.1 + sha256: 8b036876edf7895d17644f59711037d2d7d9ad048b1a503200646d8229fb1ad7 requires_dist: - - numpy>=1.26.0 ; python_full_version < '3.14' - - numpy>=2.3.3 ; python_full_version >= '3.14' - - python-dateutil>=2.8.2 - - tzdata ; sys_platform == 'win32' - - tzdata ; sys_platform == 'emscripten' - - hypothesis>=6.116.0 ; extra == 'test' - - pytest>=8.3.4 ; extra == 'test' - - pytest-xdist>=3.6.1 ; extra == 'test' - - pyarrow>=13.0.0 ; extra == 'pyarrow' - - bottleneck>=1.4.2 ; extra == 'performance' - - numba>=0.60.0 ; extra == 'performance' - - numexpr>=2.10.2 ; extra == 'performance' - - scipy>=1.14.1 ; extra == 'computation' - - xarray>=2024.10.0 ; extra == 'computation' - - fsspec>=2024.10.0 ; extra == 'fss' - - s3fs>=2024.10.0 ; extra == 'aws' - - gcsfs>=2024.10.0 ; extra == 'gcp' - - odfpy>=1.4.1 ; extra == 'excel' - - openpyxl>=3.1.5 ; extra == 'excel' - - python-calamine>=0.3.0 ; extra == 'excel' - - pyxlsb>=1.0.10 ; extra == 'excel' - - xlrd>=2.0.1 ; extra == 'excel' - - xlsxwriter>=3.2.0 ; extra == 'excel' - - pyarrow>=13.0.0 ; extra == 'parquet' - - pyarrow>=13.0.0 ; extra == 'feather' - - pyiceberg>=0.8.1 ; extra == 'iceberg' - - tables>=3.10.1 ; extra == 'hdf5' - - pyreadstat>=1.2.8 ; extra == 'spss' - - sqlalchemy>=2.0.36 ; extra == 'postgresql' - - psycopg2>=2.9.10 ; extra == 'postgresql' - - adbc-driver-postgresql>=1.2.0 ; extra == 'postgresql' - - sqlalchemy>=2.0.36 ; extra == 'mysql' - - pymysql>=1.1.1 ; extra == 'mysql' - - sqlalchemy>=2.0.36 ; extra == 'sql-other' - - adbc-driver-postgresql>=1.2.0 ; extra == 'sql-other' - - adbc-driver-sqlite>=1.2.0 ; extra == 'sql-other' - - beautifulsoup4>=4.12.3 ; extra == 'html' - - html5lib>=1.1 ; extra == 'html' - - lxml>=5.3.0 ; extra == 'html' - - lxml>=5.3.0 ; extra == 'xml' - - matplotlib>=3.9.3 ; extra == 'plot' - - jinja2>=3.1.5 ; extra == 'output-formatting' - - tabulate>=0.9.0 ; extra == 'output-formatting' - - pyqt5>=5.15.9 ; extra == 'clipboard' - - qtpy>=2.4.2 ; extra == 'clipboard' - - zstandard>=0.23.0 ; extra == 'compression' - - pytz>=2024.2 ; extra == 'timezone' - - adbc-driver-postgresql>=1.2.0 ; extra == 'all' - - adbc-driver-sqlite>=1.2.0 ; extra == 'all' - - beautifulsoup4>=4.12.3 ; extra == 'all' - - bottleneck>=1.4.2 ; extra == 'all' - - fastparquet>=2024.11.0 ; extra == 'all' - - fsspec>=2024.10.0 ; extra == 'all' - - gcsfs>=2024.10.0 ; extra == 'all' - - html5lib>=1.1 ; extra == 'all' - - hypothesis>=6.116.0 ; extra == 'all' - - jinja2>=3.1.5 ; extra == 'all' - - lxml>=5.3.0 ; extra == 'all' - - matplotlib>=3.9.3 ; extra == 'all' - - numba>=0.60.0 ; extra == 'all' - - numexpr>=2.10.2 ; extra == 'all' - - odfpy>=1.4.1 ; extra == 'all' - - openpyxl>=3.1.5 ; extra == 'all' - - psycopg2>=2.9.10 ; extra == 'all' - - pyarrow>=13.0.0 ; extra == 'all' - - pyiceberg>=0.8.1 ; extra == 'all' - - pymysql>=1.1.1 ; extra == 'all' - - pyqt5>=5.15.9 ; extra == 'all' - - pyreadstat>=1.2.8 ; extra == 'all' - - pytest>=8.3.4 ; extra == 'all' - - pytest-xdist>=3.6.1 ; extra == 'all' - - python-calamine>=0.3.0 ; extra == 'all' - - pytz>=2024.2 ; extra == 'all' - - pyxlsb>=1.0.10 ; extra == 'all' - - qtpy>=2.4.2 ; extra == 'all' - - scipy>=1.14.1 ; extra == 'all' - - s3fs>=2024.10.0 ; extra == 'all' - - sqlalchemy>=2.0.36 ; extra == 'all' - - tables>=3.10.1 ; extra == 'all' - - tabulate>=0.9.0 ; extra == 'all' - - xarray>=2024.10.0 ; extra == 'all' - - xlrd>=2.0.1 ; extra == 'all' - - xlsxwriter>=3.2.0 ; extra == 'all' - - zstandard>=0.23.0 ; extra == 'all' + - 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' -- conda: https://conda.anaconda.org/conda-forge/noarch/pandocfilters-1.5.0-pyhd8ed1ab_0.tar.bz2 - sha256: 2bb9ba9857f4774b85900c2562f7e711d08dd48e2add9bee4e1612fbee27e16f - md5: 457c2c8c08e54905d6954e79cb5b5db9 - depends: - - python !=3.0,!=3.1,!=3.2,!=3.3 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/pandocfilters?source=hash-mapping - size: 11627 - timestamp: 1631603397334 -- conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.6-pyhcf101f3_0.conda - sha256: 42b2d77ccea60752f3aa929a6413a7835aaacdbbde679f2f5870a744fa836b94 - md5: 97c1ce2fffa1209e7afb432810ec6e12 - depends: - - python >=3.10 - - python - license: MIT - license_family: MIT - purls: - - pkg:pypi/parso?source=hash-mapping - size: 82287 - timestamp: 1770676243987 -- 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 +- pypi: https://files.pythonhosted.org/packages/1a/c7/78200c18404ded028758b28b588aa1f4f3acd851271a74156a2a3db9eadf/crysfml-0.6.2-cp312-cp312-win_amd64.whl + name: crysfml + version: 0.6.2 + sha256: cd2027d98252a138bd7260b57f77c8d3c69e0da95454a44a9b80551198e8a327 requires_dist: - - locket - - toolz - - numpy>=1.20.0 ; extra == 'complete' - - pandas>=1.3 ; extra == 'complete' - - pyzmq ; extra == 'complete' - - blosc ; extra == 'complete' + - numpy + requires_python: '>=3.11,<3.15' +- pypi: https://files.pythonhosted.org/packages/1b/9f/38ff9e57a2eade7bf9dfee5eae17f39fc0e998658050279cbb14d97d36d9/msgpack-1.1.2-cp314-cp314-macosx_11_0_arm64.whl + name: msgpack + version: 1.1.2 + sha256: 6c15b7d74c939ebe620dd8e559384be806204d73b4f9356320632d783d1f7939 requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl - name: pathspec - version: 1.1.1 - sha256: a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189 +- pypi: https://files.pythonhosted.org/packages/1c/59/964ecb8008722d27d8a835baea81f56a91cea8e097b3be992bc6ccde6367/versioningit-3.3.0-py3-none-any.whl + name: versioningit + version: 3.3.0 + sha256: 23b1db3c4756cded9bd6b0ddec6643c261e3d0c471707da3e0b230b81ce53e4b requires_dist: - - hyperscan>=0.7 ; extra == 'hyperscan' - - typing-extensions>=4 ; extra == 'optional' - - google-re2>=1.1 ; extra == 're2' + - importlib-metadata>=3.6 ; python_full_version < '3.10' + - packaging>=17.1 + - tomli>=1.2,<3.0 ; python_full_version < '3.11' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/1d/77/928ea2e70641ca177a11140062cc5840d421795f2e82749d408d0cce900a/narwhals-2.21.2-py3-none-any.whl + name: narwhals + version: 2.21.2 + sha256: 7bb57c3700486039215455b9bf2d64261915cc0fd845cc30272d631df696b251 + requires_dist: + - cudf-cu12>=24.10.0 ; extra == 'cudf' + - dask[dataframe]>=2024.8 ; extra == 'dask' + - duckdb>=1.1 ; extra == 'duckdb' + - ibis-framework>=6.0.0 ; extra == 'ibis' + - packaging ; extra == 'ibis' + - pyarrow-hotfix ; extra == 'ibis' + - rich ; extra == 'ibis' + - modin ; extra == 'modin' + - pandas>=1.1.3 ; extra == 'pandas' + - polars>=0.20.4 ; extra == 'polars' + - pyarrow>=13.0.0 ; extra == 'pyarrow' + - pyspark>=3.5.0 ; extra == 'pyspark' + - pyspark[connect]>=3.5.0 ; extra == 'pyspark-connect' + - duckdb>=1.1 ; extra == 'sql' + - sqlparse ; extra == 'sql' + - sqlframe>=3.22.0,!=3.39.3 ; extra == 'sqlframe' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl + name: dill + version: 0.4.1 + sha256: 1e1ce33e978ae97fcfcff5638477032b801c46c7c65cf717f95fbc2248f79a9d + requires_dist: + - objgraph>=1.7.2 ; extra == 'graph' + - gprof2dot>=2022.7.29 ; extra == 'profile' requires_python: '>=3.9' -- conda: https://conda.anaconda.org/conda-forge/noarch/pexpect-4.9.0-pyhd8ed1ab_1.conda - sha256: 202af1de83b585d36445dc1fda94266697341994d1a3328fabde4989e1b3d07a - md5: d0d408b1f18883a944376da5cf8101ea - depends: - - ptyprocess >=0.5 - - python >=3.9 - license: ISC - purls: - - pkg:pypi/pexpect?source=hash-mapping - size: 53561 - timestamp: 1733302019362 -- pypi: https://files.pythonhosted.org/packages/11/8d/d2532ad2a603ca2b93ad9f5135732124e57811d0168155852f37fbce2458/pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - name: pillow - version: 12.2.0 - sha256: 4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286 +- 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/1e/e7/cd78635d0ece7e4d3393f2c1d2ebabf6ff4bd615da142891b1d42ad58abf/scipp-26.3.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + name: scipp + version: 26.3.1 + sha256: 7525c843f673ef5461d229095054a701aeb3233db29af137fdf4bbf0884ad9d4 + 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/1f/28/3f8aa247d29d010547d52207395cb057ebd0a40b88f64bc1dbac9e17a729/scipp-26.3.1-cp314-cp314-win_amd64.whl + name: scipp + version: 26.3.1 + sha256: 26291c0a882b9d5aac868c6d6f2508b79baa821ed30060a22c50620dbcce9e75 requires_dist: - - furo ; extra == 'docs' - - olefile ; extra == 'docs' - - sphinx>=8.2 ; extra == 'docs' - - sphinx-autobuild ; extra == 'docs' - - sphinx-copybutton ; extra == 'docs' - - sphinx-inline-tabs ; extra == 'docs' - - sphinxext-opengraph ; extra == 'docs' - - olefile ; extra == 'fpx' - - olefile ; extra == 'mic' - - arro3-compute ; extra == 'test-arrow' - - arro3-core ; extra == 'test-arrow' - - nanoarrow ; extra == 'test-arrow' - - pyarrow ; extra == 'test-arrow' - - check-manifest ; extra == 'tests' - - coverage>=7.4.2 ; extra == 'tests' - - defusedxml ; extra == 'tests' - - markdown2 ; extra == 'tests' - - olefile ; extra == 'tests' - - packaging ; extra == 'tests' - - pyroma>=5 ; extra == 'tests' - - pytest ; extra == 'tests' - - pytest-cov ; extra == 'tests' - - pytest-timeout ; extra == 'tests' - - pytest-xdist ; extra == 'tests' - - trove-classifiers>=2024.10.12 ; extra == 'tests' - - defusedxml ; extra == 'xmp' - requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/43/e3/fdc657359e919462369869f1c9f0e973f353f9a9ee295a39b1fea8ee1a77/pillow-12.2.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - name: pillow - version: 12.2.0 - sha256: 62f5409336adb0663b7caa0da5c7d9e7bdbaae9ce761d34669420c2a801b2780 + - 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/1f/7e/c2cfe0bdbec1f5ce2bd92e03311038e1c491dfd54824606f38a61167a3f0/crysfml-0.6.2-cp314-cp314-macosx_14_0_arm64.whl + name: crysfml + version: 0.6.2 + sha256: 2ca0cb14298c8db170d897e7744007a0e2f29762151ac458a0b38a1a1a9c2967 requires_dist: - - furo ; extra == 'docs' - - olefile ; extra == 'docs' - - sphinx>=8.2 ; extra == 'docs' - - sphinx-autobuild ; extra == 'docs' - - sphinx-copybutton ; extra == 'docs' - - sphinx-inline-tabs ; extra == 'docs' - - sphinxext-opengraph ; extra == 'docs' - - olefile ; extra == 'fpx' - - olefile ; extra == 'mic' - - arro3-compute ; extra == 'test-arrow' - - arro3-core ; extra == 'test-arrow' - - nanoarrow ; extra == 'test-arrow' - - pyarrow ; extra == 'test-arrow' - - check-manifest ; extra == 'tests' - - coverage>=7.4.2 ; extra == 'tests' - - defusedxml ; extra == 'tests' - - markdown2 ; extra == 'tests' - - olefile ; extra == 'tests' - - packaging ; extra == 'tests' - - pyroma>=5 ; extra == 'tests' - - pytest ; extra == 'tests' - - pytest-cov ; extra == 'tests' - - pytest-timeout ; extra == 'tests' - - pytest-xdist ; extra == 'tests' - - trove-classifiers>=2024.10.12 ; extra == 'tests' - - defusedxml ; extra == 'xmp' - requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/5d/7b/25a221d2c761c6a8ae21bfa3874988ff2583e19cf8a27bf2fee358df7942/pillow-12.2.0-cp312-cp312-win_amd64.whl - name: pillow - version: 12.2.0 - sha256: 7f84204dee22a783350679a0333981df803dac21a0190d706a50475e361c93f5 + - numpy + requires_python: '>=3.11,<3.15' +- pypi: https://files.pythonhosted.org/packages/20/16/e777eadfa0c0305878c36fae1d5e6db474fbb15dae202b9ec378809dfb4d/nbstripout-0.9.1-py3-none-any.whl + name: nbstripout + version: 0.9.1 + sha256: ca027ee45742ee77e4f8e9080254f9a707f1161ba11367b82fdf4a29892c759e requires_dist: - - furo ; extra == 'docs' - - olefile ; extra == 'docs' - - sphinx>=8.2 ; extra == 'docs' - - sphinx-autobuild ; extra == 'docs' - - sphinx-copybutton ; extra == 'docs' - - sphinx-inline-tabs ; extra == 'docs' - - sphinxext-opengraph ; extra == 'docs' - - olefile ; extra == 'fpx' - - olefile ; extra == 'mic' - - arro3-compute ; extra == 'test-arrow' - - arro3-core ; extra == 'test-arrow' - - nanoarrow ; extra == 'test-arrow' - - pyarrow ; extra == 'test-arrow' - - check-manifest ; extra == 'tests' - - coverage>=7.4.2 ; extra == 'tests' - - defusedxml ; extra == 'tests' - - markdown2 ; extra == 'tests' - - olefile ; extra == 'tests' - - packaging ; extra == 'tests' - - pyroma>=5 ; extra == 'tests' - - pytest ; extra == 'tests' - - pytest-cov ; extra == 'tests' - - pytest-timeout ; extra == 'tests' - - pytest-xdist ; extra == 'tests' - - trove-classifiers>=2024.10.12 ; extra == 'tests' - - defusedxml ; extra == 'xmp' + - nbformat requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/ba/8c/1a9e46228571de18f8e28f16fabdfc20212a5d019f3e3303452b3f0a580d/pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.whl - name: pillow - version: 12.2.0 - sha256: 80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae +- pypi: https://files.pythonhosted.org/packages/20/7a/1c6e3562dfd8950adbb11ffbc65d21e7c89d01a6e4f137fa981056de25c5/gitpython-3.1.50-py3-none-any.whl + name: gitpython + version: 3.1.50 + sha256: d352abe2908d07355014abdd21ddf798c2a961469239afec4962e9da884858f9 requires_dist: - - furo ; extra == 'docs' - - olefile ; extra == 'docs' - - sphinx>=8.2 ; extra == 'docs' - - sphinx-autobuild ; extra == 'docs' - - sphinx-copybutton ; extra == 'docs' - - sphinx-inline-tabs ; extra == 'docs' - - sphinxext-opengraph ; extra == 'docs' - - olefile ; extra == 'fpx' - - olefile ; extra == 'mic' - - arro3-compute ; extra == 'test-arrow' - - arro3-core ; extra == 'test-arrow' - - nanoarrow ; extra == 'test-arrow' - - pyarrow ; extra == 'test-arrow' - - check-manifest ; extra == 'tests' - - coverage>=7.4.2 ; extra == 'tests' - - defusedxml ; extra == 'tests' - - markdown2 ; extra == 'tests' - - olefile ; extra == 'tests' - - packaging ; extra == 'tests' - - pyroma>=5 ; extra == 'tests' - - pytest ; extra == 'tests' - - pytest-cov ; extra == 'tests' - - pytest-timeout ; extra == 'tests' - - pytest-xdist ; extra == 'tests' - - trove-classifiers>=2024.10.12 ; extra == 'tests' - - defusedxml ; extra == 'xmp' - requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/cd/8b/601e6566b957ca50e28725cb6c355c59c2c8609751efbecd980db44e0349/pillow-12.2.0-cp314-cp314-win_amd64.whl - name: pillow - version: 12.2.0 - sha256: 4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150 + - gitdb>=4.0.1,<5 + - typing-extensions>=3.10.0.2 ; python_full_version < '3.10' + - coverage[toml] ; extra == 'test' + - ddt>=1.1.1,!=1.4.3 ; extra == 'test' + - mock ; python_full_version < '3.8' and extra == 'test' + - mypy==1.18.2 ; python_full_version >= '3.9' and extra == 'test' + - pre-commit ; extra == 'test' + - pytest>=7.3.1 ; extra == 'test' + - pytest-cov ; extra == 'test' + - pytest-instafail ; extra == 'test' + - pytest-mock ; extra == 'test' + - pytest-sugar ; extra == 'test' + - typing-extensions ; python_full_version < '3.11' and extra == 'test' + - sphinx>=7.4.7,<8 ; extra == 'doc' + - sphinx-rtd-theme ; extra == 'doc' + - sphinx-autodoc-typehints ; extra == 'doc' + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/22/4d/eaedff67fc805aeba4ba746aec891b4b24cebb1a7d078084b6300f79d063/aiohttp-3.13.5-cp314-cp314-win_amd64.whl + name: aiohttp + version: 3.13.5 + sha256: f85c6f327bf0b8c29da7d93b1cabb6363fb5e4e160a32fa241ed2dce21b73162 requires_dist: - - furo ; extra == 'docs' - - olefile ; extra == 'docs' - - sphinx>=8.2 ; extra == 'docs' - - sphinx-autobuild ; extra == 'docs' - - sphinx-copybutton ; extra == 'docs' - - sphinx-inline-tabs ; extra == 'docs' - - sphinxext-opengraph ; extra == 'docs' - - olefile ; extra == 'fpx' - - olefile ; extra == 'mic' - - arro3-compute ; extra == 'test-arrow' - - arro3-core ; extra == 'test-arrow' - - nanoarrow ; extra == 'test-arrow' - - pyarrow ; extra == 'test-arrow' - - check-manifest ; extra == 'tests' - - coverage>=7.4.2 ; extra == 'tests' - - defusedxml ; extra == 'tests' - - markdown2 ; extra == 'tests' - - olefile ; extra == 'tests' - - packaging ; extra == 'tests' - - pyroma>=5 ; extra == 'tests' - - pytest ; extra == 'tests' - - pytest-cov ; extra == 'tests' - - pytest-timeout ; extra == 'tests' - - pytest-xdist ; extra == 'tests' - - trove-classifiers>=2024.10.12 ; extra == 'tests' - - defusedxml ; extra == 'xmp' - requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/d8/95/0a351b9289c2b5cbde0bacd4a83ebc44023e835490a727b2a3bd60ddc0f4/pillow-12.2.0-cp312-cp312-macosx_11_0_arm64.whl - name: pillow - version: 12.2.0 - sha256: f3f40b3c5a968281fd507d519e444c35f0ff171237f4fdde090dd60699458421 + - aiohappyeyeballs>=2.5.0 + - aiosignal>=1.4.0 + - async-timeout>=4.0,<6.0 ; python_full_version < '3.11' + - attrs>=17.3.0 + - frozenlist>=1.1.1 + - multidict>=4.5,<7.0 + - propcache>=0.2.0 + - yarl>=1.17.0,<2.0 + - aiodns>=3.3.0 ; extra == 'speedups' + - brotli>=1.2 ; platform_python_implementation == 'CPython' and extra == 'speedups' + - brotlicffi>=1.2 ; platform_python_implementation != 'CPython' and extra == 'speedups' + - backports-zstd ; python_full_version < '3.14' and platform_python_implementation == 'CPython' and extra == 'speedups' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl + name: mkdocs + version: 1.6.1 + sha256: db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e + requires_dist: + - click>=7.0 + - colorama>=0.4 ; sys_platform == 'win32' + - ghp-import>=1.0 + - importlib-metadata>=4.4 ; python_full_version < '3.10' + - jinja2>=2.11.1 + - markdown>=3.3.6 + - markupsafe>=2.0.1 + - mergedeep>=1.3.4 + - mkdocs-get-deps>=0.2.0 + - packaging>=20.5 + - pathspec>=0.11.1 + - pyyaml-env-tag>=0.1 + - pyyaml>=5.1 + - watchdog>=2.0 + - babel>=2.9.0 ; extra == 'i18n' + - babel==2.9.0 ; extra == 'min-versions' + - click==7.0 ; extra == 'min-versions' + - colorama==0.4 ; sys_platform == 'win32' and extra == 'min-versions' + - ghp-import==1.0 ; extra == 'min-versions' + - importlib-metadata==4.4 ; python_full_version < '3.10' and extra == 'min-versions' + - jinja2==2.11.1 ; extra == 'min-versions' + - markdown==3.3.6 ; extra == 'min-versions' + - markupsafe==2.0.1 ; extra == 'min-versions' + - mergedeep==1.3.4 ; extra == 'min-versions' + - mkdocs-get-deps==0.2.0 ; extra == 'min-versions' + - packaging==20.5 ; extra == 'min-versions' + - pathspec==0.11.1 ; extra == 'min-versions' + - pyyaml-env-tag==0.1 ; extra == 'min-versions' + - pyyaml==5.1 ; extra == 'min-versions' + - watchdog==2.0 ; extra == 'min-versions' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/24/f9/e8242b68362bffe6fb536c8db5076861466fc780f0f1b479fc4ffbebb128/yarl-1.23.0-cp314-cp314-macosx_11_0_arm64.whl + name: yarl + version: 1.23.0 + sha256: 23f371bd662cf44a7630d4d113101eafc0cfa7518a2760d20760b26021454719 requires_dist: - - furo ; extra == 'docs' - - olefile ; extra == 'docs' - - sphinx>=8.2 ; extra == 'docs' - - sphinx-autobuild ; extra == 'docs' - - sphinx-copybutton ; extra == 'docs' - - sphinx-inline-tabs ; extra == 'docs' - - sphinxext-opengraph ; extra == 'docs' - - olefile ; extra == 'fpx' - - olefile ; extra == 'mic' - - arro3-compute ; extra == 'test-arrow' - - arro3-core ; extra == 'test-arrow' - - nanoarrow ; extra == 'test-arrow' - - pyarrow ; extra == 'test-arrow' - - check-manifest ; extra == 'tests' - - coverage>=7.4.2 ; extra == 'tests' - - defusedxml ; extra == 'tests' - - markdown2 ; extra == 'tests' - - olefile ; extra == 'tests' - - packaging ; extra == 'tests' - - pyroma>=5 ; extra == 'tests' - - pytest ; extra == 'tests' - - pytest-cov ; extra == 'tests' - - pytest-timeout ; extra == 'tests' - - pytest-xdist ; extra == 'tests' - - trove-classifiers>=2024.10.12 ; extra == 'tests' - - defusedxml ; extra == 'xmp' - requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/70/7a/be4bd8bcbb24ea475856dd68159d78b03b2bb53dae369f69c9606b8888f5/pip-26.1-py3-none-any.whl - name: pip - version: '26.1' - sha256: 4e8486d821d814b77319acb7b9e8bf5a4ee7590a643e7cb21029f209be8573c1 + - idna>=2.0 + - multidict>=4.0 + - propcache>=0.2.1 requires_python: '>=3.10' -- conda: https://conda.anaconda.org/conda-forge/noarch/pixi-kernel-0.7.1-pyhbbac1ac_0.conda - sha256: 506c9330b8dc5ae98f4c32629fa59fa40e6bdd42a681c48d2f9554693dd01156 - md5: d57ef7cb7ad6b5d62cef8b9bdf1d400b - depends: - - ipykernel >=6 - - jupyter_client >=7 - - jupyter_server >=2.4 - - msgspec >=0.18 - - python >=3.10 - - returns >=0.23 - - tomli >=2 - license: MIT - license_family: MIT - purls: - - pkg:pypi/pixi-kernel?source=hash-mapping - size: 39509 - timestamp: 1764156429044 -- conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.9.6-pyhcf101f3_0.conda - sha256: 8f29915c172f1f7f4f7c9391cd5dac3ebf5d13745c8b7c8006032615246345a5 - md5: 89c0b6d1793601a2a3a3f7d2d3d8b937 - depends: - - python >=3.10 - - python - license: MIT - license_family: MIT - purls: - - pkg:pypi/platformdirs?source=compressed-mapping - size: 25862 - timestamp: 1775741140609 -- pypi: https://files.pythonhosted.org/packages/e4/bc/daa30c02069eeac5b9198985ba42f5d65ca71bed6705b18329e51d352b7c/plopp-26.4.2-py3-none-any.whl - name: plopp - version: 26.4.2 - sha256: 5cab99bb0905ce08a1d1d7d82f0f64cee7d594269ec1bd01a8a361bd14ab7bff - requires_dist: - - lazy-loader>=0.4 - - matplotlib>=3.8 - - scipp>=25.8.0 ; extra == 'scipp' - - plopp[scipp] ; extra == 'all' - - ipympl>0.8.4 ; extra == 'all' - - pythreejs>=2.4.1 ; extra == 'all' - - mpltoolbox>=24.6.0 ; extra == 'all' - - ipywidgets>=8.1.0 ; extra == 'all' - - graphviz>=0.20.3 ; extra == 'all' - - plopp[scipp] ; extra == 'test' - - graphviz>=0.20.3 ; extra == 'test' - - h5py>=3.12 ; extra == 'test' - - ipympl>=0.8.4 ; extra == 'test' - - ipywidgets>=8.1.0 ; extra == 'test' - - ipykernel>=6.26,<7 ; extra == 'test' - - mpltoolbox>=24.6.0 ; extra == 'test' - - pandas>=2.2.2 ; extra == 'test' - - plotly>=5.15.0 ; extra == 'test' - - pooch>=1.5 ; extra == 'test' - - pyarrow>=13.0.0 ; extra == 'test' - - pytest>=8.0 ; extra == 'test' - - pythreejs>=2.4.1 ; extra == 'test' - - scipy>=1.10.0 ; extra == 'test' - - xarray>=2024.5.0 ; extra == 'test' - - anywidget>=0.9.0 ; extra == 'test' +- pypi: https://files.pythonhosted.org/packages/25/1c/196c610ff4c6782d697ba780ebdc1616be143213701bf22c1a270f3bf7dd/numpy-2.4.5-cp314-cp314-macosx_14_0_arm64.whl + name: numpy + version: 2.4.5 + sha256: 144fcc5a3a17679b2b82543b4a2d8dd29937230a7af13232b5f753872feb6361 requires_python: '>=3.11' -- pypi: https://files.pythonhosted.org/packages/90/ad/cba91b3bcf04073e4d1655a5c1710ef3f457f56f7d1b79dcc3d72f4dd912/plotly-6.7.0-py3-none-any.whl - name: plotly - version: 6.7.0 - sha256: ac8aca1c25c663a59b5b9140a549264a5badde2e057d79b8c772ae2920e32ff0 +- pypi: https://files.pythonhosted.org/packages/25/7d/cea3531f77df694ac7f169378250d85f19f69b09a5f4fa45f650837ae7cc/py3dmol-2.5.4-py2.py3-none-any.whl + name: py3dmol + version: 2.5.4 + sha256: 32806726b5310524a2b5bfee320737f7feef635cafc945c991062806daa9e43a requires_dist: - - narwhals>=1.15.1 - - packaging - - anywidget ; extra == 'dev' - - build ; extra == 'dev' - - colorcet ; extra == 'dev' - - fiona<=1.9.6 ; python_full_version < '3.9' and extra == 'dev' - - geopandas ; extra == 'dev' - - inflect ; extra == 'dev' - - jupyterlab ; extra == 'dev' - - kaleido>=1.1.0 ; extra == 'dev' - - numpy>=1.22 ; extra == 'dev' - - orjson ; extra == 'dev' - - pandas ; extra == 'dev' - - pdfrw ; extra == 'dev' - - pillow ; extra == 'dev' - - plotly-geo ; extra == 'dev' - - polars[timezone] ; extra == 'dev' - - pyarrow ; extra == 'dev' - - pyshp ; extra == 'dev' - - pytest ; extra == 'dev' - - pytz ; extra == 'dev' - - requests ; extra == 'dev' - - ruff==0.11.12 ; extra == 'dev' - - scikit-image ; extra == 'dev' - - scipy ; extra == 'dev' - - shapely ; extra == 'dev' - - statsmodels ; extra == 'dev' - - vaex ; python_full_version < '3.10' and extra == 'dev' - - xarray ; extra == 'dev' - - build ; extra == 'dev-build' - - jupyterlab ; extra == 'dev-build' - - pytest ; extra == 'dev-build' - - requests ; extra == 'dev-build' - - ruff==0.11.12 ; extra == 'dev-build' - - pytest ; extra == 'dev-core' - - requests ; extra == 'dev-core' - - ruff==0.11.12 ; extra == 'dev-core' - - anywidget ; extra == 'dev-optional' - - build ; extra == 'dev-optional' - - colorcet ; extra == 'dev-optional' - - fiona<=1.9.6 ; python_full_version < '3.9' and extra == 'dev-optional' - - geopandas ; extra == 'dev-optional' - - inflect ; extra == 'dev-optional' - - jupyterlab ; extra == 'dev-optional' - - kaleido>=1.1.0 ; extra == 'dev-optional' - - numpy>=1.22 ; extra == 'dev-optional' - - orjson ; extra == 'dev-optional' - - pandas ; extra == 'dev-optional' - - pdfrw ; extra == 'dev-optional' - - pillow ; extra == 'dev-optional' - - plotly-geo ; extra == 'dev-optional' - - polars[timezone] ; extra == 'dev-optional' - - pyarrow ; extra == 'dev-optional' - - pyshp ; extra == 'dev-optional' - - pytest ; extra == 'dev-optional' - - pytz ; extra == 'dev-optional' - - requests ; extra == 'dev-optional' - - ruff==0.11.12 ; extra == 'dev-optional' - - scikit-image ; extra == 'dev-optional' - - scipy ; extra == 'dev-optional' - - shapely ; extra == 'dev-optional' - - statsmodels ; extra == 'dev-optional' - - vaex ; python_full_version < '3.10' and extra == 'dev-optional' - - xarray ; extra == 'dev-optional' - - numpy>=1.22 ; extra == 'express' - - kaleido>=1.1.0 ; extra == 'kaleido' - requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl - name: pluggy - version: 1.6.0 - sha256: e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746 + - ipython ; extra == 'ipython' +- pypi: https://files.pythonhosted.org/packages/27/d2/23d25e3f247b328be58d04a4c9f894178a0d1eda7d42867cfb388adaf416/fonttools-4.63.0-cp314-cp314-macosx_10_15_universal2.whl + name: fonttools + version: 4.63.0 + sha256: fd1e3094f42d806d3d7c79162fc59e5910fcbe3a7360c385b8da969bc4493745 requires_dist: - - pre-commit ; extra == 'dev' - - tox ; extra == 'dev' - - pytest ; extra == 'testing' - - pytest-benchmark ; extra == 'testing' - - coverage ; extra == 'testing' + - lxml>=4.0 ; extra == 'lxml' + - brotli>=1.0.1 ; platform_python_implementation == 'CPython' and extra == 'woff' + - brotlicffi>=0.8.0 ; platform_python_implementation != 'CPython' and extra == 'woff' + - zopfli>=0.1.4 ; extra == 'woff' + - unicodedata2>=17.0.0 ; python_full_version < '3.15' and extra == 'unicode' + - lz4>=1.7.4.2 ; extra == 'graphite' + - scipy ; platform_python_implementation != 'PyPy' and extra == 'interpolatable' + - munkres ; platform_python_implementation == 'PyPy' and extra == 'interpolatable' + - pycairo ; extra == 'interpolatable' + - matplotlib ; extra == 'plot' + - sympy ; extra == 'symfont' + - xattr ; sys_platform == 'darwin' and extra == 'type1' + - skia-pathops>=0.5.0 ; extra == 'pathops' + - uharfbuzz>=0.45.0 ; extra == 'repacker' + - lxml>=4.0 ; extra == 'all' + - brotli>=1.0.1 ; platform_python_implementation == 'CPython' and extra == 'all' + - brotlicffi>=0.8.0 ; platform_python_implementation != 'CPython' and extra == 'all' + - zopfli>=0.1.4 ; extra == 'all' + - unicodedata2>=17.0.0 ; python_full_version < '3.15' and extra == 'all' + - lz4>=1.7.4.2 ; extra == 'all' + - scipy ; platform_python_implementation != 'PyPy' and extra == 'all' + - munkres ; platform_python_implementation == 'PyPy' and extra == 'all' + - pycairo ; extra == 'all' + - matplotlib ; extra == 'all' + - sympy ; extra == 'all' + - xattr ; sys_platform == 'darwin' and extra == 'all' + - skia-pathops>=0.5.0 ; extra == 'all' + - uharfbuzz>=0.45.0 ; extra == 'all' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/28/55/5733807f4af131ea6194309ac0f43eb5b05463c676d036ef948f3143c1f2/pycifrw-5.0.1-cp312-cp312-win_amd64.whl + name: pycifrw + version: 5.0.1 + sha256: 9d2939cce3bded805f02beda5a6aea62eb95951d59a1b99d73aa3463052fe4fe + requires_dist: + - prettytable + - ply + - numpy +- pypi: https://files.pythonhosted.org/packages/28/88/4789719fbbe166d12d345b3ac66b96105f10001b16e00a9765ba29261a21/nbqa-1.9.1-py3-none-any.whl + name: nbqa + version: 1.9.1 + sha256: 95552d2f6c2c038136252a805aa78d85018aef922586270c3a074332737282e5 + requires_dist: + - autopep8>=1.5 + - ipython>=7.8.0 + - tokenize-rt>=3.2.0 + - tomli + - black ; extra == 'toolchain' + - blacken-docs ; extra == 'toolchain' + - flake8 ; extra == 'toolchain' + - isort ; extra == 'toolchain' + - jupytext ; extra == 'toolchain' + - mypy ; extra == 'toolchain' + - pylint ; extra == 'toolchain' + - pyupgrade ; extra == 'toolchain' + - ruff ; extra == 'toolchain' requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/79/ad/45312df6b63ba64ea35b8d8f5f0c577aac16e6b416eafe8e1cb34e03f9a7/plumbum-1.10.0-py3-none-any.whl - name: plumbum - version: 1.10.0 - sha256: 9583d737ac901c474d99d030e4d5eec4c4e6d2d7417b1cf49728cf3be34f6dc8 +- pypi: https://files.pythonhosted.org/packages/28/de/a3e710469772c6a89595fc52816da05c1e164b4c866a89e3cb82fb1b67c5/mkdocs_autorefs-1.4.4-py3-none-any.whl + name: mkdocs-autorefs + version: 1.4.4 + sha256: 834ef5408d827071ad1bc69e0f39704fa34c7fc05bc8e1c72b227dfdc5c76089 requires_dist: - - pywin32 ; platform_python_implementation != 'PyPy' and sys_platform == 'win32' - - paramiko ; extra == 'ssh' + - markdown>=3.3 + - markupsafe>=2.0.1 + - mkdocs>=1.1 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/29/47/7be41556bfbb6917069d6a6634bb7dd5e163ba445b783a90d40f5ac7e3a7/aiohttp-3.13.5-cp312-cp312-macosx_11_0_arm64.whl + name: aiohttp + version: 3.13.5 + sha256: ab2899f9fa2f9f741896ebb6fa07c4c883bfa5c7f2ddd8cf2aafa86fa981b2d2 + requires_dist: + - aiohappyeyeballs>=2.5.0 + - aiosignal>=1.4.0 + - async-timeout>=4.0,<6.0 ; python_full_version < '3.11' + - attrs>=17.3.0 + - frozenlist>=1.1.1 + - multidict>=4.5,<7.0 + - propcache>=0.2.0 + - yarl>=1.17.0,<2.0 + - aiodns>=3.3.0 ; extra == 'speedups' + - brotli>=1.2 ; platform_python_implementation == 'CPython' and extra == 'speedups' + - brotlicffi>=1.2 ; platform_python_implementation != 'CPython' and extra == 'speedups' + - backports-zstd ; python_full_version < '3.14' and platform_python_implementation == 'CPython' and extra == 'speedups' requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/a3/58/35da89ee790598a0700ea49b2a66594140f44dec458c07e8e3d4979137fc/ply-3.11-py2.py3-none-any.whl - name: ply - version: '3.11' - sha256: 096f9b8350b65ebd2fd1346b12452efe5b9607f7482813ffca50c22722a807ce - pypi: https://files.pythonhosted.org/packages/2a/2d/d4bf65e47cea8ff2c794a600c4fd1273a7902f268757c531e0ee9f18aa58/pooch-1.9.0-py3-none-any.whl name: pooch version: 1.9.0 @@ -9126,1698 +9051,2303 @@ packages: - pytest-httpserver ; extra == 'test' - pytest-localftpserver ; extra == 'test' requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl - name: pre-commit - version: 4.6.0 - sha256: e2cf246f7299edcabcf15f9b0571fdce06058527f0a06535068a86d38089f29b - requires_dist: - - cfgv>=2.0.0 - - identify>=1.0.0 - - nodeenv>=0.11.1 - - pyyaml>=5.1 - - virtualenv>=20.10.0 +- pypi: https://files.pythonhosted.org/packages/2b/76/7f1bfd6afff4c5e38e36a3c6d68eb5f4b7311ea80baf693db78d95b603c4/propcache-0.5.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + name: propcache + version: 0.5.2 + sha256: e00820e192c8dbebcafb383ebbf99030895f09905e7a0eb2e0340a0bcc2bc825 requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/ee/8c/83087ebc47ab0396ce092363001fa37c17153119ee282700c0713a195853/prettytable-3.17.0-py3-none-any.whl - name: prettytable - version: 3.17.0 - sha256: aad69b294ddbe3e1f95ef8886a060ed1666a0b83018bbf56295f6f226c43d287 +- pypi: https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl + name: frozenlist + version: 1.8.0 + sha256: f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/2b/e0/2e13df68f367e2f9960616927d60857dd7e56aaadd59a47c644216b2f920/greenlet-3.5.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl + name: greenlet + version: 3.5.0 + sha256: 9d280a7f5c331622c69f97eb167f33577ff2d1df282c41cd15907fc0a3ca198c requires_dist: - - wcwidth - - pytest ; extra == 'tests' - - pytest-cov ; extra == 'tests' - - pytest-lazy-fixtures ; extra == 'tests' + - sphinx ; extra == 'docs' + - furo ; extra == 'docs' + - objgraph ; extra == 'test' + - psutil ; extra == 'test' + - setuptools ; extra == 'test' requires_python: '>=3.10' -- conda: https://conda.anaconda.org/conda-forge/noarch/prometheus_client-0.25.0-pyhd8ed1ab_0.conda - sha256: 4d7ec90d4f9c1f3b4a50623fefe4ebba69f651b102b373f7c0e9dbbfa43d495c - md5: a11ab1f31af799dd93c3a39881528884 - depends: - - python >=3.10 - license: Apache-2.0 - license_family: Apache - purls: - - pkg:pypi/prometheus-client?source=compressed-mapping - size: 57113 - timestamp: 1775771465170 -- conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.52-pyha770c72_0.conda - sha256: 4817651a276016f3838957bfdf963386438c70761e9faec7749d411635979bae - md5: edb16f14d920fb3faf17f5ce582942d6 - depends: - - python >=3.10 - - wcwidth - constrains: - - prompt_toolkit 3.0.52 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/prompt-toolkit?source=hash-mapping - size: 273927 - timestamp: 1756321848365 -- pypi: https://files.pythonhosted.org/packages/0a/b6/5c9a0e42df4d00bfb4a3cbbe5cf9f54260300c88a0e9af1f47ca5ce17ac0/propcache-0.4.1-cp312-cp312-macosx_11_0_arm64.whl - name: propcache - version: 0.4.1 - sha256: f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207 - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/0c/2a/a758b47de253636e1b8aef181c0b4f4f204bf0dd964914fb2af90a95b49b/propcache-0.4.1-cp314-cp314-win_amd64.whl - name: propcache - version: 0.4.1 - sha256: 5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153 - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/46/4b/3aae6835b8e5f44ea6a68348ad90f78134047b503765087be2f9912140ea/propcache-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - name: propcache - version: 0.4.1 - sha256: 15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/54/09/d19cff2a5aaac632ec8fc03737b223597b1e347416934c1b3a7df079784c/propcache-0.4.1-cp312-cp312-win_amd64.whl - name: propcache - version: 0.4.1 - sha256: cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/b2/f2/889ad4b2408f72fe1a4f6a19491177b30ea7bf1a0fd5f17050ca08cfc882/propcache-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - name: propcache - version: 0.4.1 - sha256: 8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/b2/fa/89a8ef0468d5833a23fff277b143d0573897cf75bd56670a6d28126c7d68/propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.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.6 + sha256: 71b84353921b8ea1ba84fe11c50912cc512da8fe0881038fcc9a0761c0e635ba + requires_dist: + - babel>=2.10 + - backrefs>=5.7.post1 + - colorama>=0.4 + - jinja2>=3.1 + - markdown>=3.2 + - mkdocs-material-extensions>=1.3 + - mkdocs>=1.6,<2 + - paginate>=0.5 + - pygments>=2.16 + - pymdown-extensions>=10.2 + - requests>=2.30 + - mkdocs-git-committers-plugin-2>=1.1 ; extra == 'git' + - mkdocs-git-revision-date-localized-plugin>=1.2.4 ; extra == 'git' + - cairosvg>=2.6 ; extra == 'imaging' + - pillow>=10.2 ; extra == 'imaging' + - mkdocs-minify-plugin>=0.7 ; extra == 'recommended' + - mkdocs-redirects>=1.2 ; extra == 'recommended' + - mkdocs-rss-plugin>=1.6 ; extra == 'recommended' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl + name: mergedeep + version: 1.3.4 + sha256: 70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307 + requires_python: '>=3.6' +- pypi: https://files.pythonhosted.org/packages/2c/7d/49777a3e20b55863d4794384a38acd460c04157b0a00f8602b0d508b8431/propcache-0.5.2-cp312-cp312-macosx_11_0_arm64.whl name: propcache - version: 0.4.1 - sha256: 9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded - requires_python: '>=3.9' -- conda: https://conda.anaconda.org/conda-forge/linux-64/psutil-7.2.2-py312h5253ce2_0.conda - sha256: d834fd656133c9e4eaf63ffe9a117c7d0917d86d89f7d64073f4e3a0020bd8a7 - md5: dd94c506b119130aef5a9382aed648e7 - depends: - - python - - libgcc >=14 - - __glibc >=2.17,<3.0.a0 - - python_abi 3.12.* *_cp312 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/psutil?source=compressed-mapping - size: 225545 - timestamp: 1769678155334 -- conda: https://conda.anaconda.org/conda-forge/linux-64/psutil-7.2.2-py314h0f05182_0.conda - sha256: f15574ed6c8c8ed8c15a0c5a00102b1efe8b867c0bd286b498cd98d95bd69ae5 - md5: 4f225a966cfee267a79c5cb6382bd121 - depends: - - python - - libgcc >=14 - - __glibc >=2.17,<3.0.a0 - - python_abi 3.14.* *_cp314 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/psutil?source=hash-mapping - size: 231303 - timestamp: 1769678156552 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/psutil-7.2.2-py312hb3ab3e3_0.conda - sha256: 6d0e21c76436374635c074208cfeee62a94d3c37d0527ad67fd8a7615e546a05 - md5: fd856899666759403b3c16dcba2f56ff - depends: - - python - - __osx >=11.0 - - python 3.12.* *_cpython - - python_abi 3.12.* *_cp312 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/psutil?source=hash-mapping - size: 239031 - timestamp: 1769678393511 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/psutil-7.2.2-py314ha14b1ff_0.conda - sha256: e0f31c053eb11803d63860c213b2b1b57db36734f5f84a3833606f7c91fedff9 - md5: fc4c7ab223873eee32080d51600ce7e7 - depends: - - python - - __osx >=11.0 - - python 3.14.* *_cp314 - - python_abi 3.14.* *_cp314 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/psutil?source=hash-mapping - size: 245502 - timestamp: 1769678303655 -- conda: https://conda.anaconda.org/conda-forge/win-64/psutil-7.2.2-py312he5662c2_0.conda - sha256: edffc84c001a05b996b5f8607c8164432754e86ec9224e831cd00ebabdec04e7 - md5: a2724c93b745fc7861948eb8b9f6679a - depends: - - python - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 - - ucrt >=10.0.20348.0 - - python_abi 3.12.* *_cp312 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/psutil?source=hash-mapping - size: 242769 - timestamp: 1769678170631 -- conda: https://conda.anaconda.org/conda-forge/win-64/psutil-7.2.2-py314hc5dbbe4_0.conda - sha256: 17c8274ce5a32c9793f73a5a0094bd6188f3a13026a93147655143d4df034214 - md5: fd539ac231820f64066839251aa9fa48 - depends: - - python - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 - - ucrt >=10.0.20348.0 - - python_abi 3.14.* *_cp314 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/psutil?source=hash-mapping - size: 249950 - timestamp: 1769678167309 -- conda: https://conda.anaconda.org/conda-forge/noarch/ptyprocess-0.7.0-pyhd8ed1ab_1.conda - sha256: a7713dfe30faf17508ec359e0bc7e0983f5d94682492469bd462cdaae9c64d83 - md5: 7d9daffbb8d8e0af0f769dbbcd173a54 - depends: - - python >=3.9 - license: ISC - purls: - - pkg:pypi/ptyprocess?source=hash-mapping - size: 19457 - timestamp: 1733302371990 -- conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda - sha256: 71bd24600d14bb171a6321d523486f6a06f855e75e547fa0cb2a0953b02047f0 - md5: 3bfdfb8dbcdc4af1ae3f9a8eb3948f04 - depends: - - python >=3.9 - license: MIT - license_family: MIT - purls: - - pkg:pypi/pure-eval?source=hash-mapping - size: 16668 - timestamp: 1733569518868 -- pypi: https://files.pythonhosted.org/packages/f6/f0/10642828a8dfb741e5f3fbaac830550a518a775c7fff6f04a007259b0548/py-1.11.0-py2.py3-none-any.whl - name: py - version: 1.11.0 - sha256: 607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378 - requires_python: '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*' -- pypi: https://files.pythonhosted.org/packages/e0/a9/023730ba63db1e494a271cb018dcd361bd2c917ba7004c3e49d5daf795a2/py_cpuinfo-9.0.0-py3-none-any.whl - name: py-cpuinfo - version: 9.0.0 - sha256: 859625bc251f64e21f077d099d4162689c762b5d6a4c3c97553d56241c9674d5 -- pypi: https://files.pythonhosted.org/packages/25/7d/cea3531f77df694ac7f169378250d85f19f69b09a5f4fa45f650837ae7cc/py3dmol-2.5.4-py2.py3-none-any.whl - name: py3dmol - version: 2.5.4 - sha256: 32806726b5310524a2b5bfee320737f7feef635cafc945c991062806daa9e43a + version: 0.5.2 + sha256: e5cbfac9f61484f7e9f3597775500cd3ebe8274e9b050c38f9525c77c97520bf + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/2c/fa/65fcae2ed62f84ab72cf89536c7c3217a156e71a2c111b1305ab6f0690e2/sqlalchemy-2.0.49-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + name: sqlalchemy + version: 2.0.49 + sha256: 3bb9ec6436a820a4c006aad1ac351f12de2f2dbdaad171692ee457a02429b672 + 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/2e/84/efc7c0bf3a1c5eef81d397f6fddac855becdbb11cb38ff957888603014a7/sqlalchemy-2.0.49-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + name: sqlalchemy + version: 2.0.49 + sha256: 685e93e9c8f399b0c96a624799820176312f5ceef958c0f88215af4013d29066 requires_dist: - - ipython ; extra == 'ipython' -- pypi: https://files.pythonhosted.org/packages/28/55/5733807f4af131ea6194309ac0f43eb5b05463c676d036ef948f3143c1f2/pycifrw-5.0.1-cp312-cp312-win_amd64.whl - name: pycifrw - version: 5.0.1 - sha256: 9d2939cce3bded805f02beda5a6aea62eb95951d59a1b99d73aa3463052fe4fe + - 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/2f/40/dc34d1a8d5f1e51fc64640b62b191684da52ca469da9cd74e84936ffa4a6/msgpack-1.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + name: msgpack + version: 1.1.2 + sha256: 180759d89a057eab503cf62eeec0aa61c4ea1200dee709f3a8e9397dbb3b6931 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/2f/c8/005d1de3af80f54411703d1263a0b9d31276411ec9f273d9432c59b17299/arviz_plots-1.1.0-py3-none-any.whl + name: arviz-plots + version: 1.1.0 + sha256: 5c7ab5b0c7c29cda6ddb5e04c699c70285fe68a76d2b1b42302c69a85742adde + requires_dist: + - arviz-base>=1.1,<1.2 + - arviz-stats[xarray]>=1.1,<1.2 + - bokeh>=3.4 ; extra == 'bokeh' + - sphinx-book-theme ; extra == 'doc' + - myst-parser[linkify] ; extra == 'doc' + - myst-nb ; extra == 'doc' + - sphinx-copybutton ; extra == 'doc' + - numpydoc ; extra == 'doc' + - sphinx>=6 ; extra == 'doc' + - sphinx-design ; extra == 'doc' + - jupyter-sphinx ; extra == 'doc' + - h5netcdf[h5py] ; extra == 'doc' + - plotly<6 ; extra == 'doc' + - matplotlib>=3.9 ; extra == 'matplotlib' + - plotly>=5.19 ; extra == 'plotly' + - webcolors ; extra == 'plotly' + - hypothesis ; extra == 'test' + - pytest ; extra == 'test' + - pytest-cov ; extra == 'test' + - h5netcdf[h5py] ; extra == 'test' + - kaleido ; extra == 'test' + requires_python: '>=3.12' +- pypi: https://files.pythonhosted.org/packages/32/28/79f0f8de97cce916d5ae88a7bee1ad724855e83e6019c0b4d5b3fabc80f3/mkdocstrings_python-2.0.3-py3-none-any.whl + name: mkdocstrings-python + version: 2.0.3 + sha256: 0b83513478bdfd803ff05aa43e9b1fca9dd22bcd9471f09ca6257f009bc5ee12 requires_dist: - - prettytable - - ply - - numpy -- pypi: https://files.pythonhosted.org/packages/32/a0/37fb236da6040e337381dd656cafb97d09eacb998c5db3057547f5ffddd9/pycifrw-5.0.1-cp312-cp312-macosx_11_0_arm64.whl - name: pycifrw - version: 5.0.1 - sha256: 2d0464e10abda9890347a95c8c385654c2741fca186df371a5c47c3b4b819866 + - mkdocstrings>=0.30 + - mkdocs-autorefs>=1.4 + - griffelib>=2.0 + - typing-extensions>=4.0 ; python_full_version < '3.11' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/32/5d/f7e914f7d9325abff4057cee62c0fa70263683189f774473cbfb534cd13b/matplotlib-3.10.9-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + name: matplotlib + version: 3.10.9 + sha256: 34cf8167e023ad956c15f36302911d5406bd99a9862c1a8499ea6f7c0e015dc2 requires_dist: - - prettytable - - ply - - numpy -- pypi: https://files.pythonhosted.org/packages/58/e0/f1871f520c359e4e3a2eb7437c9e7e792bb6c356414e8617937561167caf/pycifrw-5.0.1.tar.gz - name: pycifrw - version: 5.0.1 - sha256: e636b80be6a2be15b215e69ecec0c0a784ebcbfed8b1e3bac4bcc6e6ba9a75e0 + - contourpy>=1.0.1 + - cycler>=0.10 + - fonttools>=4.22.0 + - kiwisolver>=1.3.1 + - numpy>=1.23 + - packaging>=20.0 + - pillow>=8 + - pyparsing>=3 + - python-dateutil>=2.7 + - meson-python>=0.13.1,<0.17.0 ; extra == 'dev' + - pybind11>=2.13.2,!=2.13.3 ; extra == 'dev' + - setuptools-scm>=7,<10 ; extra == 'dev' + - setuptools>=64 ; extra == 'dev' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/32/60/fca69c534602a7ced04280c952a246ad1edde2a6ca3a164f65d32ac41fe7/chardet-7.4.3-cp312-cp312-win_amd64.whl + name: chardet + version: 7.4.3 + sha256: 4b2799bd58e7245cfa8d4ab2e8ad1d76a5c3a5b1f32318eb6acca4c69a3e7101 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/32/91/d024616abdba99e83120e07a20658976f6a343646710760c4a51df126029/matplotlib-3.10.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + name: matplotlib + version: 3.10.9 + sha256: ae20801130378b82d647ff5047c07316295b68dc054ca6b3c13519d0ea624285 requires_dist: - - prettytable - - ply - - numpy -- pypi: https://files.pythonhosted.org/packages/ae/61/3c1ea8c10bf4f6bf83c33a7f5b4a3143f4cc1f979859dec5498b6cc31900/pycifrw-5.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - contourpy>=1.0.1 + - cycler>=0.10 + - fonttools>=4.22.0 + - kiwisolver>=1.3.1 + - numpy>=1.23 + - packaging>=20.0 + - pillow>=8 + - pyparsing>=3 + - python-dateutil>=2.7 + - meson-python>=0.13.1,<0.17.0 ; extra == 'dev' + - pybind11>=2.13.2,!=2.13.3 ; extra == 'dev' + - setuptools-scm>=7,<10 ; extra == 'dev' + - setuptools>=64 ; extra == 'dev' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/32/a0/37fb236da6040e337381dd656cafb97d09eacb998c5db3057547f5ffddd9/pycifrw-5.0.1-cp312-cp312-macosx_11_0_arm64.whl name: pycifrw version: 5.0.1 - sha256: 379801e71509d0f9c59b56edc5ceb6600796eaf2b84ee5e0f5a256c76542047d + sha256: 2d0464e10abda9890347a95c8c385654c2741fca186df371a5c47c3b4b819866 requires_dist: - prettytable - ply - numpy -- pypi: https://files.pythonhosted.org/packages/78/3c/2a612b95ddbb9a6bdcb47b7a93c4884f74c6ff22356b2f7b213b16e65c35/pycifstar-0.3.0.tar.gz - name: pycifstar - version: 0.3.0 - sha256: 5892fdf16c83372ee5f32557127d5f36e14b0bbe520883a4e2e70365382f70ed -- pypi: https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl - name: pycodestyle - version: 2.14.0 - sha256: dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d - requires_python: '>=3.9' -- conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda - sha256: 79db7928d13fab2d892592223d7570f5061c192f27b9febd1a418427b719acc6 - md5: 12c566707c80111f9799308d9e265aef - depends: - - python >=3.9 - - python - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/pycparser?source=hash-mapping - size: 110100 - timestamp: 1733195786147 -- pypi: https://files.pythonhosted.org/packages/f3/0a/fd7d723f8f8153418fb40cf9c940e82004fce7e987026b08a68a36dd3fe7/pydantic-2.13.3-py3-none-any.whl - name: pydantic - version: 2.13.3 - sha256: 6db14ac8dfc9a1e57f87ea2c0de670c251240f43cb0c30a5130e9720dc612927 - requires_dist: - - annotated-types>=0.6.0 - - pydantic-core==2.46.3 - - 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/20/eb/59980e5f1ae54a3b86372bd9f0fa373ea2d402e8cdcd3459334430f91e91/pydantic_core-2.46.3-cp314-cp314-win_amd64.whl - name: pydantic-core - version: 2.46.3 - sha256: 8940562319bc621da30714617e6a7eaa6b98c84e8c685bcdc02d7ed5e7c7c44e - requires_dist: - - typing-extensions>=4.14.1 - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/24/35/e4066358a22e3e99519db370494c7528f5a2aa1367370e80e27e20283543/pydantic_core-2.46.3-cp314-cp314-macosx_11_0_arm64.whl - name: pydantic-core - version: 2.46.3 - sha256: ced3310e51aa425f7f77da8bbbb5212616655bedbe82c70944320bc1dbe5e018 - requires_dist: - - typing-extensions>=4.14.1 - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/57/be/6b5e757b859013ebfbd7adba02f23b428f37c86dcbf78b5bb0b4ffd36e99/pydantic_core-2.46.3-cp312-cp312-win_amd64.whl - name: pydantic-core - version: 2.46.3 - sha256: c3212fda0ee959c1dd04c60b601ec31097aaa893573a3a1abd0a47bcac2968c1 - requires_dist: - - typing-extensions>=4.14.1 - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/5e/d5/e3d9717c9eba10855325650afd2a9cba8e607321697f18953af9d562da2f/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - name: pydantic-core - version: 2.46.3 - sha256: fb528e295ed31570ac3dcc9bfdd6e0150bc11ce6168ac87a8082055cf1a67395 - requires_dist: - - typing-extensions>=4.14.1 - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/a1/4f/2fb62c2267cae99b815bbf4a7b9283812c88ca3153ef29f7707200f1d4e5/pydantic_core-2.46.3-cp312-cp312-macosx_11_0_arm64.whl - name: pydantic-core - version: 2.46.3 - sha256: af8653713055ea18a3abc1537fe2ebc42f5b0bbb768d1eb79fd74eb47c0ac089 - requires_dist: - - typing-extensions>=4.14.1 - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/fd/86/ef96a4c6e79e7a2d0410826a68fbc0eccc0fd44aa733be199d5fcac3bb87/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - name: pydantic-core - version: 2.46.3 - sha256: ed42e6cc8e1b0e2b9b96e2276bad70ae625d10d6d524aed0c93de974ae029f9f +- pypi: https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl + name: distlib + version: 0.4.0 + sha256: 9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16 +- pypi: https://files.pythonhosted.org/packages/33/75/98a7eb100dc5cfd20b019046452f08d5e67dfbacc71d8f28763d32426fd3/spglib-2.6.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + name: spglib + version: 2.6.0 + sha256: a8e9c34da1e2428c3a8bd4e209e5356d12d454d8ac54120d5ba4a437d3abe7ba requires_dist: - - typing-extensions>=4.14.1 + - 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/87/6f/cc2b231dc78d8c3aaa674a676db190b8f8071c50134af8f8cf39b9b8e8df/pydoclint-0.8.3-py3-none-any.whl - name: pydoclint - version: 0.8.3 - sha256: 5fc9b82d0d515afce0908cb70e8ff695a68b19042785c248c4f227ad66b4a164 +- 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.1.0 - - docstring-parser-fork>=0.0.12 - - tomli>=2.0.1 ; python_full_version < '3.11' - - flake8>=4 ; extra == 'flake8' + - click>=8.0 + - jupyter-notebook-parser>=0.1.4 + - tomli>=1.1.0 ; python_full_version < '3.11' requires_python: '>=3.10' -- conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda - sha256: cf70b2f5ad9ae472b71235e5c8a736c9316df3705746de419b59d442e8348e86 - md5: 16c18772b340887160c79a6acc022db0 - depends: - - python >=3.10 - license: BSD-2-Clause - license_family: BSD - purls: - - pkg:pypi/pygments?source=compressed-mapping - size: 893031 - timestamp: 1774796815820 -- pypi: https://files.pythonhosted.org/packages/f7/27/a2fc51a4a122dfd1015e921ae9d22fee3d20b0b8080d9a704578bf9deece/pymdown_extensions-10.21.2-py3-none-any.whl - name: pymdown-extensions - version: 10.21.2 - sha256: 5c0fd2a2bea14eb39af8ff284f1066d898ab2187d81b889b75d46d4348c01638 +- pypi: https://files.pythonhosted.org/packages/33/eb/f9f1ded8e4db9638f9530c3782eb01f5ab04945f4cb9e597a51c203fa4c5/diffpy_pdffit2-1.6.0.tar.gz + name: diffpy-pdffit2 + version: 1.6.0 + sha256: 11a65466f8790f5ac7ae45f2f3fc0d5d116d156d274bcfc079df653123d080e2 requires_dist: - - markdown>=3.6 - - pyyaml - - pygments>=2.19.1 ; extra == 'extra' + - diffpy-structure + requires_python: '>=3.12,<3.15' +- pypi: https://files.pythonhosted.org/packages/33/f0/3fe8c6e69135a845f4106f2ff8b6805638d4e85c264e70114e8126689587/tokenize_rt-6.2.0-py2.py3-none-any.whl + name: tokenize-rt + version: 6.2.0 + sha256: a152bf4f249c847a66497a4a95f63376ed68ac6abf092a2f7cfb29d044ecff44 requires_python: '>=3.9' -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyobjc-core-12.1-py312h19bbe71_0.conda - sha256: b015f430fe9ea2c53e14be13639f1b781f68deaa5ae74cd8c1d07720890cd02a - md5: c65d7abdc9e60fd3af0ed852591adf1b - depends: - - __osx >=11.0 - - libffi >=3.5.2,<3.6.0a0 - - python >=3.12,<3.13.0a0 - - python >=3.12,<3.13.0a0 *_cpython - - python_abi 3.12.* *_cp312 - - setuptools - license: MIT - license_family: MIT - purls: - - pkg:pypi/pyobjc-core?source=hash-mapping - size: 476750 - timestamp: 1763151865523 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyobjc-core-12.1-py314h3a4d195_0.conda - sha256: df5af268c5a74b7160d772c263ece6f43257faff571783443e34b5f1d5a61cf2 - md5: 75a84fc8337557347252cc4fd3ba2a93 - depends: - - __osx >=11.0 - - libffi >=3.5.2,<3.6.0a0 - - python >=3.14,<3.15.0a0 - - python >=3.14,<3.15.0a0 *_cp314 - - python_abi 3.14.* *_cp314 - - setuptools - license: MIT - license_family: MIT - purls: - - pkg:pypi/pyobjc-core?source=hash-mapping - size: 483374 - timestamp: 1763151489724 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyobjc-framework-cocoa-12.1-py312h1de3e18_0.conda - sha256: 3710f5ae09c2ea77ba4d82cc51e876d9fc009b878b197a40d3c6347c09ae7d7c - md5: f0bae1b67ece138378923e340b940051 - depends: - - __osx >=11.0 - - libffi >=3.5.2,<3.6.0a0 - - pyobjc-core 12.1.* - - python >=3.12,<3.13.0a0 - - python >=3.12,<3.13.0a0 *_cpython - - python_abi 3.12.* *_cp312 - license: MIT - license_family: MIT - purls: - - pkg:pypi/pyobjc-framework-cocoa?source=hash-mapping - size: 377723 - timestamp: 1763160705325 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyobjc-framework-cocoa-12.1-py314h36abed7_0.conda - sha256: aa76ee4328d0514d7c1c455dcd2d3b547db1c59797e54ce0a3f27de5b970e508 - md5: 4219bb3408016e22316cf8b443b5ef93 - depends: - - __osx >=11.0 - - libffi >=3.5.2,<3.6.0a0 - - pyobjc-core 12.1.* - - python >=3.14,<3.15.0a0 - - python >=3.14,<3.15.0a0 *_cp314 - - python_abi 3.14.* *_cp314 - license: MIT - license_family: MIT - purls: - - pkg:pypi/pyobjc-framework-cocoa?source=hash-mapping - size: 374792 - timestamp: 1763160601898 -- pypi: https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl - name: pyparsing - version: 3.3.2 - sha256: 850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d +- pypi: https://files.pythonhosted.org/packages/34/23/35c7aea1274aef7525bdd2dc92f710bdde6d11652239d71d1ec450067939/coverage-7.14.0-cp312-cp312-macosx_11_0_arm64.whl + name: coverage + version: 7.14.0 + sha256: 829994cfe1aeb773ca27bf246d4badc1e764893e3bfb98fff820fcecd1ca4662 requires_dist: - - railroad-diagrams ; extra == 'diagrams' - - jinja2 ; extra == 'diagrams' + - tomli ; python_full_version <= '3.11' and extra == 'toml' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/34/68/ba4f155f793a74c1483d4bdef136e1023f7bcba557f0db4ef3db3c665cf1/msgpack-1.1.2-cp312-cp312-macosx_11_0_arm64.whl + name: msgpack + version: 1.1.2 + sha256: 446abdd8b94b55c800ac34b102dffd2f6aa0ce643c55dfc017ad89347db3dbdb requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl - name: pyproject-hooks - version: 1.2.0 - sha256: 9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913 - requires_python: '>=3.7' -- conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyh09c184e_7.conda - sha256: d016e04b0e12063fbee4a2d5fbb9b39a8d191b5a0042f0b8459188aedeabb0ca - md5: e2fd202833c4a981ce8a65974fe4abd1 - depends: - - __win - - python >=3.9 - - win_inet_pton - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/pysocks?source=hash-mapping - size: 21784 - timestamp: 1733217448189 -- conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda - sha256: ba3b032fa52709ce0d9fd388f63d330a026754587a2f461117cac9ab73d8d0d8 - md5: 461219d1a5bd61342293efa2c0c90eac - depends: - - __unix - - python >=3.9 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/pysocks?source=hash-mapping - size: 21085 - timestamp: 1733217331982 -- pypi: https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl - name: pytest - version: 9.0.3 - sha256: 2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9 +- pypi: https://files.pythonhosted.org/packages/36/da/87912ddec6e06feffbaa3d7aa18fc6352bee2e8f1fee185d7d1690f8f4e8/backrefs-7.0-py312-none-any.whl + name: backrefs + version: '7.0' + sha256: ca42ce6a49ace3d75684dfa9937f3373902a63284ecb385ce36d15e5dcb41c12 requires_dist: - - colorama>=0.4 ; sys_platform == 'win32' - - exceptiongroup>=1 ; python_full_version < '3.11' - - iniconfig>=1.0.1 - - packaging>=22 - - pluggy>=1.5,<2 - - pygments>=2.7.2 - - tomli>=1 ; python_full_version < '3.11' - - argcomplete ; extra == 'dev' - - attrs>=19.2 ; extra == 'dev' - - hypothesis>=3.56 ; extra == 'dev' - - mock ; extra == 'dev' - - requests ; extra == 'dev' - - setuptools ; extra == 'dev' - - xmlschema ; extra == 'dev' + - regex ; extra == 'extras' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/36/e1/a8933a72c45a87177fbde2696e0d0755c8c9062f8c077a961c6215fa27b1/fonttools-4.63.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + name: fonttools + version: 4.63.0 + sha256: 308f957cdeaf8abe4e5f2f124902ef405448af92c90f80e302a3b771c2e6116b + requires_dist: + - lxml>=4.0 ; extra == 'lxml' + - brotli>=1.0.1 ; platform_python_implementation == 'CPython' and extra == 'woff' + - brotlicffi>=0.8.0 ; platform_python_implementation != 'CPython' and extra == 'woff' + - zopfli>=0.1.4 ; extra == 'woff' + - unicodedata2>=17.0.0 ; python_full_version < '3.15' and extra == 'unicode' + - lz4>=1.7.4.2 ; extra == 'graphite' + - scipy ; platform_python_implementation != 'PyPy' and extra == 'interpolatable' + - munkres ; platform_python_implementation == 'PyPy' and extra == 'interpolatable' + - pycairo ; extra == 'interpolatable' + - matplotlib ; extra == 'plot' + - sympy ; extra == 'symfont' + - xattr ; sys_platform == 'darwin' and extra == 'type1' + - skia-pathops>=0.5.0 ; extra == 'pathops' + - uharfbuzz>=0.45.0 ; extra == 'repacker' + - lxml>=4.0 ; extra == 'all' + - brotli>=1.0.1 ; platform_python_implementation == 'CPython' and extra == 'all' + - brotlicffi>=0.8.0 ; platform_python_implementation != 'CPython' and extra == 'all' + - zopfli>=0.1.4 ; extra == 'all' + - unicodedata2>=17.0.0 ; python_full_version < '3.15' and extra == 'all' + - lz4>=1.7.4.2 ; extra == 'all' + - scipy ; platform_python_implementation != 'PyPy' and extra == 'all' + - munkres ; platform_python_implementation == 'PyPy' and extra == 'all' + - pycairo ; extra == 'all' + - matplotlib ; extra == 'all' + - sympy ; extra == 'all' + - xattr ; sys_platform == 'darwin' and extra == 'all' + - skia-pathops>=0.5.0 ; extra == 'all' + - uharfbuzz>=0.45.0 ; extra == 'all' requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/33/29/e756e715a48959f1c0045342088d7ca9762a2f509b945f362a316e9412b7/pytest_benchmark-5.2.3-py3-none-any.whl - name: pytest-benchmark - version: 5.2.3 - sha256: bc839726ad20e99aaa0d11a127445457b4219bdb9e80a1afc4b51da7f96b0803 - requires_dist: - - pytest>=8.1 - - py-cpuinfo - - aspectlib ; extra == 'aspect' - - pygal ; extra == 'histogram' - - pygaljs ; extra == 'histogram' - - setuptools ; extra == 'histogram' - - elasticsearch ; extra == 'elasticsearch' +- pypi: https://files.pythonhosted.org/packages/38/55/792619469bab9882d8bbd5865d45a72f6478762d04a9af4bf0d08c503e95/pandas-3.0.3-cp314-cp314-win_amd64.whl + name: pandas + version: 3.0.3 + sha256: 3c20a521bbb85902f79f7270c80a59e1b5452d96d170c034f207181870f97ac5 + requires_dist: + - numpy>=1.26.0 ; python_full_version < '3.14' + - numpy>=2.3.3 ; python_full_version >= '3.14' + - python-dateutil>=2.8.2 + - tzdata ; sys_platform == 'win32' + - tzdata ; sys_platform == 'emscripten' + - hypothesis>=6.116.0 ; extra == 'test' + - pytest>=8.3.4 ; extra == 'test' + - pytest-xdist>=3.6.1 ; extra == 'test' + - pyarrow>=13.0.0 ; extra == 'pyarrow' + - bottleneck>=1.4.2 ; extra == 'performance' + - numba>=0.60.0 ; extra == 'performance' + - numexpr>=2.10.2 ; extra == 'performance' + - scipy>=1.14.1 ; extra == 'computation' + - xarray>=2024.10.0 ; extra == 'computation' + - fsspec>=2024.10.0 ; extra == 'fss' + - s3fs>=2024.10.0 ; extra == 'aws' + - gcsfs>=2024.10.0 ; extra == 'gcp' + - odfpy>=1.4.1 ; extra == 'excel' + - openpyxl>=3.1.5 ; extra == 'excel' + - python-calamine>=0.3.0 ; extra == 'excel' + - pyxlsb>=1.0.10 ; extra == 'excel' + - xlrd>=2.0.1 ; extra == 'excel' + - xlsxwriter>=3.2.0 ; extra == 'excel' + - pyarrow>=13.0.0 ; extra == 'parquet' + - pyarrow>=13.0.0 ; extra == 'feather' + - pyiceberg>=0.8.1 ; extra == 'iceberg' + - tables>=3.10.1 ; extra == 'hdf5' + - pyreadstat>=1.2.8 ; extra == 'spss' + - sqlalchemy>=2.0.36 ; extra == 'postgresql' + - psycopg2>=2.9.10 ; extra == 'postgresql' + - adbc-driver-postgresql>=1.2.0 ; extra == 'postgresql' + - sqlalchemy>=2.0.36 ; extra == 'mysql' + - pymysql>=1.1.1 ; extra == 'mysql' + - sqlalchemy>=2.0.36 ; extra == 'sql-other' + - adbc-driver-postgresql>=1.2.0 ; extra == 'sql-other' + - adbc-driver-sqlite>=1.2.0 ; extra == 'sql-other' + - beautifulsoup4>=4.12.3 ; extra == 'html' + - html5lib>=1.1 ; extra == 'html' + - lxml>=5.3.0 ; extra == 'html' + - lxml>=5.3.0 ; extra == 'xml' + - matplotlib>=3.9.3 ; extra == 'plot' + - jinja2>=3.1.5 ; extra == 'output-formatting' + - tabulate>=0.9.0 ; extra == 'output-formatting' + - pyqt5>=5.15.9 ; extra == 'clipboard' + - qtpy>=2.4.2 ; extra == 'clipboard' + - zstandard>=0.23.0 ; extra == 'compression' + - pytz>=2020.1 ; extra == 'timezone' + - adbc-driver-postgresql>=1.2.0 ; extra == 'all' + - adbc-driver-sqlite>=1.2.0 ; extra == 'all' + - beautifulsoup4>=4.12.3 ; extra == 'all' + - bottleneck>=1.4.2 ; extra == 'all' + - fastparquet>=2024.11.0 ; extra == 'all' + - fsspec>=2024.10.0 ; extra == 'all' + - gcsfs>=2024.10.0 ; extra == 'all' + - html5lib>=1.1 ; extra == 'all' + - hypothesis>=6.116.0 ; extra == 'all' + - jinja2>=3.1.5 ; extra == 'all' + - lxml>=5.3.0 ; extra == 'all' + - matplotlib>=3.9.3 ; extra == 'all' + - numba>=0.60.0 ; extra == 'all' + - numexpr>=2.10.2 ; extra == 'all' + - odfpy>=1.4.1 ; extra == 'all' + - openpyxl>=3.1.5 ; extra == 'all' + - psycopg2>=2.9.10 ; extra == 'all' + - pyarrow>=13.0.0 ; extra == 'all' + - pyiceberg>=0.8.1 ; extra == 'all' + - pymysql>=1.1.1 ; extra == 'all' + - pyqt5>=5.15.9 ; extra == 'all' + - pyreadstat>=1.2.8 ; extra == 'all' + - pytest>=8.3.4 ; extra == 'all' + - pytest-xdist>=3.6.1 ; extra == 'all' + - python-calamine>=0.3.0 ; extra == 'all' + - pytz>=2020.1 ; extra == 'all' + - pyxlsb>=1.0.10 ; extra == 'all' + - qtpy>=2.4.2 ; extra == 'all' + - scipy>=1.14.1 ; extra == 'all' + - s3fs>=2024.10.0 ; extra == 'all' + - sqlalchemy>=2.0.36 ; extra == 'all' + - tables>=3.10.1 ; extra == 'all' + - tabulate>=0.9.0 ; extra == 'all' + - xarray>=2024.10.0 ; extra == 'all' + - xlrd>=2.0.1 ; extra == 'all' + - xlsxwriter>=3.2.0 ; extra == 'all' + - zstandard>=0.23.0 ; extra == 'all' + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/38/7e/7b91c89a4cf0f543a83be978657afb20c86af6d725253e319589dcc4ce52/lmfit-1.3.4-py3-none-any.whl + name: lmfit + version: 1.3.4 + sha256: afce1593b42324d37ae2908249b0c55445e2f4c1a0474ff706a8e2f7b5d949fa + requires_dist: + - asteval>=1.0 + - numpy>=1.24 + - scipy>=1.10.0 + - uncertainties>=3.2.2 + - dill>=0.3.4 + - build ; extra == 'dev' + - check-wheel-contents ; extra == 'dev' + - flake8-pyproject ; extra == 'dev' + - pre-commit ; extra == 'dev' + - twine ; extra == 'dev' + - cairosvg ; extra == 'doc' + - corner ; extra == 'doc' + - emcee>=3.0.0 ; extra == 'doc' + - ipykernel ; extra == 'doc' + - jupyter-sphinx>=0.2.4 ; extra == 'doc' + - matplotlib ; extra == 'doc' + - numdifftools ; extra == 'doc' + - pandas ; extra == 'doc' + - pillow ; extra == 'doc' + - pycairo ; sys_platform == 'win32' and extra == 'doc' + - sphinx ; extra == 'doc' + - sphinx-gallery>=0.10 ; extra == 'doc' + - sphinxcontrib-svg2pdfconverter ; extra == 'doc' + - sympy ; extra == 'doc' + - coverage ; extra == 'test' + - flaky ; extra == 'test' + - pytest ; extra == 'test' + - pytest-cov ; extra == 'test' + - lmfit[dev,doc,test] ; extra == 'all' requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl - name: pytest-cov - version: 7.1.0 - sha256: a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678 +- pypi: https://files.pythonhosted.org/packages/38/8b/7ec325b4e9e78beefc2d025b01ee8a2fde771ef7c957c3bff99b9e1fbffa/xraydb-4.5.8-py3-none-any.whl + name: xraydb + version: 4.5.8 + sha256: 2215baafa6a03d00d0254a94525aafc6493c8c285e4ac4477fbd6271b25e6a51 requires_dist: - - coverage[toml]>=7.10.6 - - pluggy>=1.2 - - pytest>=7 - - process-tests ; extra == 'testing' - - pytest-xdist ; extra == 'testing' - - virtualenv ; extra == 'testing' + - numpy>=1.19 + - scipy>=1.6 + - sqlalchemy>=2.0.1 + - platformdirs + - build ; extra == 'dev' + - twine ; extra == 'dev' + - sphinx ; extra == 'doc' + - pytest ; extra == 'test' + - pytest-cov ; extra == 'test' + - coverage ; extra == 'test' + - xraydb[dev,doc,test] ; extra == 'all' requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl - name: pytest-xdist - version: 3.8.0 - sha256: 202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88 +- pypi: https://files.pythonhosted.org/packages/3a/eb/fea4d1d51c49832120f7f285d07306db3960f423a2612c6057caf3e8196f/pip-26.1.1-py3-none-any.whl + name: pip + version: 26.1.1 + sha256: 99cb1c2899893b075ff56e4ed0af55669a955b49ad7fb8d8603ecdaf4ed653fb + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/3c/26/1062c7ec1b053db9e499b4d2d5bc231743201b74051c973dadeac80a8f43/questionary-2.1.1-py3-none-any.whl + name: questionary + version: 2.1.1 + sha256: a51af13f345f1cdea62347589fbb6df3b290306ab8930713bfae4d475a7d4a59 requires_dist: - - execnet>=2.1 - - pytest>=7.0.0 - - filelock ; extra == 'testing' - - psutil>=3.0 ; extra == 'psutil' - - setproctitle ; extra == 'setproctitle' + - prompt-toolkit>=2.0,<4.0 requires_python: '>=3.9' -- conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.12.13-hd63d673_0_cpython.conda - sha256: a44655c1c3e1d43ed8704890a91e12afd68130414ea2c0872e154e5633a13d7e - md5: 7eccb41177e15cc672e1babe9056018e - depends: - - __glibc >=2.17,<3.0.a0 - - bzip2 >=1.0.8,<2.0a0 - - ld_impl_linux-64 >=2.36.1 - - libexpat >=2.7.4,<3.0a0 - - libffi >=3.5.2,<3.6.0a0 - - libgcc >=14 - - liblzma >=5.8.2,<6.0a0 - - libnsl >=2.0.1,<2.1.0a0 - - libsqlite >=3.51.2,<4.0a0 - - libuuid >=2.41.3,<3.0a0 - - libxcrypt >=4.4.36 - - libzlib >=1.3.1,<2.0a0 - - ncurses >=6.5,<7.0a0 - - openssl >=3.5.5,<4.0a0 - - readline >=8.3,<9.0a0 - - tk >=8.6.13,<8.7.0a0 - - tzdata - constrains: - - python_abi 3.12.* *_cp312 - license: Python-2.0 - purls: [] - size: 31608571 - timestamp: 1772730708989 -- conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.14.4-habeac84_100_cp314.conda - build_number: 100 - sha256: dec247c5badc811baa34d6085df9d0465535883cf745e22e8d79092ad54a3a7b - md5: a443f87920815d41bfe611296e507995 - depends: - - __glibc >=2.17,<3.0.a0 - - bzip2 >=1.0.8,<2.0a0 - - ld_impl_linux-64 >=2.36.1 - - libexpat >=2.7.5,<3.0a0 - - libffi >=3.5.2,<3.6.0a0 - - libgcc >=14 - - liblzma >=5.8.2,<6.0a0 - - libmpdec >=4.0.0,<5.0a0 - - libsqlite >=3.52.0,<4.0a0 - - libuuid >=2.42,<3.0a0 - - libzlib >=1.3.2,<2.0a0 - - ncurses >=6.5,<7.0a0 - - openssl >=3.5.6,<4.0a0 - - python_abi 3.14.* *_cp314 - - readline >=8.3,<9.0a0 - - tk >=8.6.13,<8.7.0a0 - - tzdata - - zstd >=1.5.7,<1.6.0a0 - license: Python-2.0 - purls: [] - size: 36705460 - timestamp: 1775614357822 - python_site_packages_path: lib/python3.14/site-packages -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.12.13-h8561d8f_0_cpython.conda - sha256: e658e647a4a15981573d6018928dec2c448b10c77c557c29872043ff23c0eb6a - md5: 8e7608172fa4d1b90de9a745c2fd2b81 - depends: - - __osx >=11.0 - - bzip2 >=1.0.8,<2.0a0 - - libexpat >=2.7.4,<3.0a0 - - libffi >=3.5.2,<3.6.0a0 - - liblzma >=5.8.2,<6.0a0 - - libsqlite >=3.51.2,<4.0a0 - - libzlib >=1.3.1,<2.0a0 - - ncurses >=6.5,<7.0a0 - - openssl >=3.5.5,<4.0a0 - - readline >=8.3,<9.0a0 - - tk >=8.6.13,<8.7.0a0 - - tzdata - constrains: - - python_abi 3.12.* *_cp312 - license: Python-2.0 - purls: [] - size: 12127424 - timestamp: 1772730755512 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.14.4-h4c637c5_100_cp314.conda - build_number: 100 - sha256: 27e7d6cbe021f37244b643f06a98e46767255f7c2907108dd3736f042757ddad - md5: e1bc5a3015a4bbeb304706dba5a32b7f - depends: - - __osx >=11.0 - - bzip2 >=1.0.8,<2.0a0 - - libexpat >=2.7.5,<3.0a0 - - libffi >=3.5.2,<3.6.0a0 - - liblzma >=5.8.2,<6.0a0 - - libmpdec >=4.0.0,<5.0a0 - - libsqlite >=3.52.0,<4.0a0 - - libzlib >=1.3.2,<2.0a0 - - ncurses >=6.5,<7.0a0 - - openssl >=3.5.6,<4.0a0 - - python_abi 3.14.* *_cp314 - - readline >=8.3,<9.0a0 - - tk >=8.6.13,<8.7.0a0 - - tzdata - - zstd >=1.5.7,<1.6.0a0 - license: Python-2.0 - purls: [] - size: 13533346 - timestamp: 1775616188373 - python_site_packages_path: lib/python3.14/site-packages -- conda: https://conda.anaconda.org/conda-forge/win-64/python-3.12.13-h0159041_0_cpython.conda - sha256: a02b446d8b7b167b61733a3de3be5de1342250403e72a63b18dac89e99e6180e - md5: 2956dff38eb9f8332ad4caeba941cfe7 - depends: - - bzip2 >=1.0.8,<2.0a0 - - libexpat >=2.7.4,<3.0a0 - - libffi >=3.5.2,<3.6.0a0 - - liblzma >=5.8.2,<6.0a0 - - libsqlite >=3.51.2,<4.0a0 - - libzlib >=1.3.1,<2.0a0 - - openssl >=3.5.5,<4.0a0 - - tk >=8.6.13,<8.7.0a0 - - tzdata - - ucrt >=10.0.20348.0 - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 - constrains: - - python_abi 3.12.* *_cp312 - license: Python-2.0 - purls: [] - size: 15840187 - timestamp: 1772728877265 -- conda: https://conda.anaconda.org/conda-forge/win-64/python-3.14.4-h4b44e0e_100_cp314.conda - build_number: 100 - sha256: e258d626b0ba778abb319f128de4c1211306fe86fe0803166817b1ce2514c920 - md5: 40b6a8f438afb5e7b314cc5c4a43cd84 - depends: - - bzip2 >=1.0.8,<2.0a0 - - libexpat >=2.7.5,<3.0a0 - - libffi >=3.5.2,<3.6.0a0 - - liblzma >=5.8.2,<6.0a0 - - libmpdec >=4.0.0,<5.0a0 - - libsqlite >=3.52.0,<4.0a0 - - libzlib >=1.3.2,<2.0a0 - - openssl >=3.5.6,<4.0a0 - - python_abi 3.14.* *_cp314 - - tk >=8.6.13,<8.7.0a0 - - tzdata - - ucrt >=10.0.20348.0 - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 - - zstd >=1.5.7,<1.6.0a0 - license: Python-2.0 - purls: [] - size: 18055445 - timestamp: 1775615317758 - python_site_packages_path: Lib/site-packages -- conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda - sha256: d6a17ece93bbd5139e02d2bd7dbfa80bee1a4261dced63f65f679121686bf664 - md5: 5b8d21249ff20967101ffa321cab24e8 - depends: - - python >=3.9 - - six >=1.5 - - python - license: Apache-2.0 - license_family: APACHE - purls: - - pkg:pypi/python-dateutil?source=hash-mapping - size: 233310 - timestamp: 1751104122689 -- pypi: https://files.pythonhosted.org/packages/d8/db/795879cc3ddfe338599bddea6388cc5100b088db0a4caf6e6c1af1c27e04/python_discovery-1.2.2-py3-none-any.whl - name: python-discovery - version: 1.2.2 - sha256: e1ae95d9af875e78f15e19aed0c6137ab1bb49c200f21f5061786490c9585c7a +- pypi: https://files.pythonhosted.org/packages/3c/f0/cbf5d391b0b3a5e8cad264603e2fae256b0bde8ce43566b13b78faedc659/numpy-2.4.5-cp312-cp312-win_amd64.whl + name: numpy + version: 2.4.5 + sha256: 3333dba6a4e611d666f69e177ba8fe4140366ff681a5feb2374d3fd4fff3acb6 + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/3e/14/615a450205e1b56d16c6783f5ccd116cde05550faad70ae077c955654a75/h5py-3.16.0-cp314-cp314-win_amd64.whl + name: h5py + version: 3.16.0 + sha256: fa48993a0b799737ba7fd21e2350fa0a60701e58180fae9f2de834bc39a147ab + requires_dist: + - numpy>=1.21.2 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/3e/17/1f31d8562e6f970d64911f1abc330d233bc0c0601411cf7e19c1292be6da/spdx_headers-1.5.1-py3-none-any.whl + name: spdx-headers + version: 1.5.1 + sha256: 73bcb1ed087824b55ccaa497d03d8f0f0b0eaf30e5f0f7d5bbd29d2c4fe78fcf + requires_dist: + - chardet>=5.2.0 + - requests>=2.32.3 + - black>=23.0.0 ; extra == 'dev' + - build>=0.10.0 ; extra == 'dev' + - hatch>=1.9.0 ; extra == 'dev' + - isort>=5.12.0 ; extra == 'dev' + - mypy>=1.0.0 ; extra == 'dev' + - pre-commit>=4.3.0 ; extra == 'dev' + - pytest-cov>=4.0.0 ; extra == 'dev' + - pytest>=7.0.0 ; extra == 'dev' + - ruff>=0.5.0 ; extra == 'dev' + - twine>=4.0.0 ; extra == 'dev' + - types-requests>=2.31.0.6 ; extra == 'dev' + - pytest-cov>=4.0.0 ; extra == 'test' + - pytest-mock>=3.10.0 ; extra == 'test' + - pytest>=7.0.0 ; extra == 'test' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/3e/5c/fb93d3092640a24dfb7bd7727a24016d7c01774ca013e60efd3f683c8002/backrefs-7.0-py314-none-any.whl + name: backrefs + version: '7.0' + sha256: a6448b28180e3ca01134c9cf09dcebafad8531072e09903c5451748a05f24bc9 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.1 - sha256: f32ad10589859c11053ad7d9bb3c9695cdf862113bfb0d20bc4d890198287399 + - regex ; extra == 'extras' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/3f/0e/fa3b193432cfc60c93b42f3be03365f5f909d2b3ea410295cf36df739e31/widgetsnbextension-4.0.15-py3-none-any.whl + name: widgetsnbextension + version: 4.0.15 + sha256: 8156704e4346a571d9ce73b84bee86a29906c9abfd7223b7228a28899ccf3366 + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/3f/d0/7b958df957e4827837b590944008f0b28078f552b451f7407b4b3d54f574/asciichartpy-1.5.25-py2.py3-none-any.whl + name: asciichartpy + version: 1.5.25 + sha256: 33c417a3c8ef7d0a11b98eb9ea6dd9b2c1b17559e539b207a17d26d4302d0258 requires_dist: - - simple-websocket>=0.10.0 - - requests>=2.21.0 ; extra == 'client' - - websocket-client>=0.54.0 ; extra == 'client' - - aiohttp>=3.11 ; extra == 'asyncio-client' - - tox ; extra == 'dev' - - sphinx ; extra == 'docs' - - furo ; extra == 'docs' - requires_python: '>=3.8' -- conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.21.2-pyhe01879c_0.conda - sha256: df9aa74e9e28e8d1309274648aac08ec447a92512c33f61a8de0afa9ce32ebe8 - md5: 23029aae904a2ba587daba708208012f - depends: - - python >=3.9 - - python - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/fastjsonschema?source=hash-mapping - size: 244628 - timestamp: 1755304154927 -- conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.12.13-hd8ed1ab_0.conda - sha256: 97327b9509ae3aae28d27217a5d7bd31aff0ab61a02041e9c6f98c11d8a53b29 - md5: 32780d6794b8056b78602103a04e90ef - depends: - - cpython 3.12.13.* - - python_abi * *_cp312 - license: Python-2.0 - purls: [] - size: 46449 - timestamp: 1772728979370 -- conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.14.4-h4df99d1_100.conda - sha256: 36ff7984e4565c85149e64f8206303d412a0652e55cf806dcb856903fa056314 - md5: e4e60721757979d01d3964122f674959 - depends: - - cpython 3.14.4.* - - python_abi * *_cp314 - license: Python-2.0 - purls: [] - size: 49806 - timestamp: 1775614307464 -- conda: https://conda.anaconda.org/conda-forge/noarch/python-json-logger-3.2.1-pyh332efcf_0.conda - sha256: 1c55116c22512cef7b01d55ae49697707f2c1fd829407183c19817e2d300fd8d - md5: 1cd2f3e885162ee1366312bd1b1677fd - depends: - - python >=3.10 - - typing_extensions - license: BSD-2-Clause - license_family: BSD - purls: - - pkg:pypi/python-json-logger?source=compressed-mapping - size: 18969 - timestamp: 1777318679482 -- pypi: https://files.pythonhosted.org/packages/07/c7/deb8c5e604404dbf10a3808a858946ca3547692ff6316b698945bb72177e/python_socketio-5.16.1-py3-none-any.whl - name: python-socketio - version: 5.16.1 - sha256: a3eb1702e92aa2f2b5d3ba00261b61f062cce51f1cfb6900bf3ab4d1934d2d35 + - setuptools + - flake8 ; extra == 'qa' +- pypi: https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl + name: typer + version: 0.25.1 + sha256: 75caa44ed46a03fb2dab8808753ffacdbfea88495e74c85a28c5eefcf5f39c89 requires_dist: - - bidict>=0.21.0 - - python-engineio>=4.11.0 - - requests>=2.21.0 ; extra == 'client' - - websocket-client>=0.54.0 ; extra == 'client' - - aiohttp>=3.4 ; extra == 'asyncio-client' - - tox ; extra == 'dev' + - click>=8.2.1 + - shellingham>=1.3.0 + - rich>=13.8.0 + - annotated-doc>=0.0.2 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl + name: pydantic-core + version: 2.46.4 + sha256: e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89 + requires_dist: + - typing-extensions>=4.14.1 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/41/86/86231232fff41c9f8e4a1a7d7a597d349a02527109c3af7d618366122139/matplotlib-3.10.9-cp314-cp314-macosx_11_0_arm64.whl + name: matplotlib + version: 3.10.9 + sha256: 97e35e8d39ccc85859095e01a53847432ba9a53ddf7986f7a54a11b73d0e143f + requires_dist: + - contourpy>=1.0.1 + - cycler>=0.10 + - fonttools>=4.22.0 + - kiwisolver>=1.3.1 + - numpy>=1.23 + - packaging>=20.0 + - pillow>=8 + - pyparsing>=3 + - python-dateutil>=2.7 + - meson-python>=0.13.1,<0.17.0 ; extra == 'dev' + - pybind11>=2.13.2,!=2.13.3 ; extra == 'dev' + - setuptools-scm>=7,<10 ; extra == 'dev' + - setuptools>=64 ; extra == 'dev' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/42/2b/fef67d616931055bf3d6764885990a3ac647d68734a2d6a9e1d13de437a2/yarl-1.23.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + name: yarl + version: 1.23.0 + sha256: 1c57676bdedc94cd3bc37724cf6f8cd2779f02f6aba48de45feca073e714fe52 + requires_dist: + - idna>=2.0 + - multidict>=4.0 + - propcache>=0.2.1 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/42/d9/27b13bc9419bf5dae02905b348f16ca827646cd76244ddd326f1a8139a6a/cyclebane-24.10.0-py3-none-any.whl + name: cyclebane + version: 24.10.0 + sha256: 902dd318667e4a222afc270cc5bc72c67d5d6047d2e0e1c36018885fb80f5e5d + requires_dist: + - networkx + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/43/80/1315439acedd8398319bac177d6de3d48ab39c62cc0c810f74f0a9a73996/numpy-2.4.5-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + name: numpy + version: 2.4.5 + sha256: 7341b08ff8124d7353939778e2707b8732d03c78c1c30e0815aba2dacbe1245a + 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 + sha256: a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c + requires_dist: + - pytest>=4.6 ; extra == 'develop' + - pycodestyle ; extra == 'develop' + - pytest-cov ; extra == 'develop' + - codecov ; extra == 'develop' + - wheel ; extra == 'develop' - sphinx ; extra == 'docs' + - gmpy2>=2.1.0a4 ; platform_python_implementation != 'PyPy' and extra == 'gmpy' + - pytest>=4.6 ; extra == 'tests' +- pypi: https://files.pythonhosted.org/packages/43/e3/fdc657359e919462369869f1c9f0e973f353f9a9ee295a39b1fea8ee1a77/pillow-12.2.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + name: pillow + version: 12.2.0 + sha256: 62f5409336adb0663b7caa0da5c7d9e7bdbaae9ce761d34669420c2a801b2780 + requires_dist: - furo ; extra == 'docs' - requires_python: '>=3.8' -- conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2026.2-pyhd8ed1ab_0.conda - sha256: e943f9c15a6bdba2e1b9f423ab913b3f6b02197b0ef9f8e6b7464d78b59965b9 - md5: f6ad7450fc21e00ecc23812baed6d2e4 - depends: - - python >=3.10 - license: Apache-2.0 - license_family: APACHE - purls: - - pkg:pypi/tzdata?source=compressed-mapping - size: 146639 - timestamp: 1777068997932 -- conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.12-8_cp312.conda - build_number: 8 - sha256: 80677180dd3c22deb7426ca89d6203f1c7f1f256f2d5a94dc210f6e758229809 - md5: c3efd25ac4d74b1584d2f7a57195ddf1 - constrains: - - python 3.12.* *_cpython - license: BSD-3-Clause - license_family: BSD - purls: [] - size: 6958 - timestamp: 1752805918820 -- conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.14-8_cp314.conda - build_number: 8 - sha256: ad6d2e9ac39751cc0529dd1566a26751a0bf2542adb0c232533d32e176e21db5 - md5: 0539938c55b6b1a59b560e843ad864a4 - constrains: - - python 3.14.* *_cp314 - license: BSD-3-Clause - license_family: BSD - purls: [] - size: 6989 - timestamp: 1752805904792 -- 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 + - olefile ; extra == 'docs' + - sphinx>=8.2 ; extra == 'docs' + - sphinx-autobuild ; extra == 'docs' + - sphinx-copybutton ; extra == 'docs' + - sphinx-inline-tabs ; extra == 'docs' + - sphinxext-opengraph ; extra == 'docs' + - olefile ; extra == 'fpx' + - olefile ; extra == 'mic' + - arro3-compute ; extra == 'test-arrow' + - arro3-core ; extra == 'test-arrow' + - nanoarrow ; extra == 'test-arrow' + - pyarrow ; extra == 'test-arrow' + - check-manifest ; extra == 'tests' + - coverage>=7.4.2 ; extra == 'tests' + - defusedxml ; extra == 'tests' + - markdown2 ; extra == 'tests' + - olefile ; extra == 'tests' + - packaging ; extra == 'tests' + - pyroma>=5 ; extra == 'tests' + - pytest ; extra == 'tests' + - pytest-cov ; extra == 'tests' + - pytest-timeout ; extra == 'tests' + - pytest-xdist ; extra == 'tests' + - trove-classifiers>=2024.10.12 ; extra == 'tests' + - defusedxml ; extra == 'xmp' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/43/fe/ad0ecbe2393cb690a4b3100a8fea47ecfdb49f6e06f40cf2f626635adc0c/scipp-26.3.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + name: scipp + version: 26.3.1 + sha256: 2ef08ba8d83542807f9f9833ba8f01583215c1629693bfadb1d6508cbdeb335c + 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/44/7b/537a61906eac58d94131273084d21d4eb219f5453f0ed30de3aca580a2b4/scipp-26.3.1-cp312-cp312-macosx_14_0_arm64.whl + name: scipp + version: 26.3.1 + sha256: 2608ba21e2c550abe864598e8cfffe22d7e7be70ff9f9b03d44868e353b241c9 + 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/46/b4/0887c88ddfaba1d7140ea335144eb904af97550786ee58bdb295ff10d255/crysfml-0.6.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + name: crysfml + version: 0.6.2 + sha256: 8274d3c1ac37444d779b7819e752cba03ba3029953fed61479e4537225b3ee99 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.11,<3.15' +- pypi: https://files.pythonhosted.org/packages/47/9e/fd90114059175cac64e4fafa9bf3ac20584384d66de40793ae2e2f26f3bb/sqlalchemy-2.0.49-cp312-cp312-win_amd64.whl + name: sqlalchemy + version: 2.0.49 + sha256: 618a308215b6cececb6240b9abde545e3acdabac7ae3e1d4e666896bf5ba44b4 + 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' -- conda: https://conda.anaconda.org/conda-forge/win-64/pywin32-311-py312h829343e_1.conda - sha256: a7505522048dad63940d06623f07eb357b9b65510a8d23ff32b99add05aac3a1 - md5: 64cbe4ecbebe185a2261d3f298a60cde - depends: - - python - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 - - ucrt >=10.0.20348.0 - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 - - ucrt >=10.0.20348.0 - - python_abi 3.12.* *_cp312 - license: PSF-2.0 - license_family: PSF - purls: - - pkg:pypi/pywin32?source=hash-mapping - size: 6684490 - timestamp: 1756487136116 -- conda: https://conda.anaconda.org/conda-forge/win-64/pywin32-311-py314h8f8f202_1.conda - sha256: 6918a8067f296f3c65d43e84558170c9e6c3f4dd735cfe041af41a7fdba7b171 - md5: 2d7b7ba21e8a8ced0eca553d4d53f773 - depends: - - python - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 - - ucrt >=10.0.20348.0 - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 - - ucrt >=10.0.20348.0 - - python_abi 3.14.* *_cp314 - license: PSF-2.0 - license_family: PSF - purls: - - pkg:pypi/pywin32?source=hash-mapping - size: 6713155 - timestamp: 1756487145487 -- conda: https://conda.anaconda.org/conda-forge/win-64/pywinpty-2.0.15-py312h275cf98_1.conda - sha256: 61cc6c2c712ab4d2b8e7a73d884ef8d3262cb80cc93a4aa074e8b08aa7ddd648 - md5: 66255d136bd0daa41713a334db41d9f0 - depends: - - python >=3.12,<3.13.0a0 - - python_abi 3.12.* *_cp312 - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - - winpty - license: MIT - license_family: MIT - purls: - - pkg:pypi/pywinpty?source=hash-mapping - size: 215371 - timestamp: 1759557609855 -- conda: https://conda.anaconda.org/conda-forge/win-64/pywinpty-2.0.15-py314h51f0985_1.conda - sha256: 048e20641da680aedaab285640a2aca56b7b5baf7a18f8f164f2796e13628c1f - md5: dd84e8748bd3c85a5c751b0576488080 - depends: - - python >=3.14.0rc3,<3.15.0a0 - - python_abi 3.14.* *_cp314 - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - - winpty - license: MIT - license_family: MIT - purls: - - pkg:pypi/pywinpty?source=hash-mapping - size: 216325 - timestamp: 1759557436167 -- conda: https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.3-py312h8a5da7c_1.conda - sha256: cb142bfd92f6e55749365ddc244294fa7b64db6d08c45b018ff1c658907bfcbf - md5: 15878599a87992e44c059731771591cb - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=14 - - python >=3.12,<3.13.0a0 - - python_abi 3.12.* *_cp312 - - yaml >=0.2.5,<0.3.0a0 - license: MIT - license_family: MIT - purls: - - pkg:pypi/pyyaml?source=hash-mapping - size: 198293 - timestamp: 1770223620706 -- conda: https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.3-py314h67df5f8_1.conda - sha256: b318fb070c7a1f89980ef124b80a0b5ccf3928143708a85e0053cde0169c699d - md5: 2035f68f96be30dc60a5dfd7452c7941 - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=14 - - python >=3.14,<3.15.0a0 - - python_abi 3.14.* *_cp314 - - yaml >=0.2.5,<0.3.0a0 - license: MIT - license_family: MIT - purls: - - pkg:pypi/pyyaml?source=compressed-mapping - size: 202391 - timestamp: 1770223462836 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyyaml-6.0.3-py312h04c11ed_1.conda - sha256: 737959262d03c9c305618f2d48c7f1691fb996f14ae420bfd05932635c99f873 - md5: 95a5f0831b5e0b1075bbd80fcffc52ac - depends: - - __osx >=11.0 - - python >=3.12,<3.13.0a0 - - python >=3.12,<3.13.0a0 *_cpython - - python_abi 3.12.* *_cp312 - - yaml >=0.2.5,<0.3.0a0 - license: MIT - license_family: MIT - purls: - - pkg:pypi/pyyaml?source=hash-mapping - size: 187278 - timestamp: 1770223990452 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyyaml-6.0.3-py314h6e9b3f0_1.conda - sha256: 95f385f9606e30137cf0b5295f63855fd22223a4cf024d306cf9098ea1c4a252 - md5: dcf51e564317816cb8d546891019b3ab - depends: - - __osx >=11.0 - - python >=3.14,<3.15.0a0 - - python >=3.14,<3.15.0a0 *_cp314 - - python_abi 3.14.* *_cp314 - - yaml >=0.2.5,<0.3.0a0 - license: MIT - license_family: MIT - purls: - - pkg:pypi/pyyaml?source=hash-mapping - size: 189475 - timestamp: 1770223788648 -- conda: https://conda.anaconda.org/conda-forge/win-64/pyyaml-6.0.3-py312h05f76fc_1.conda - sha256: 1cab6cbd6042b2a1d8ee4d6b4ec7f36637a41f57d2f5c5cf0c12b7c4ce6a62f6 - md5: 9f6ebef672522cb9d9a6257215ca5743 - depends: - - python >=3.12,<3.13.0a0 - - python_abi 3.12.* *_cp312 - - ucrt >=10.0.20348.0 - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 - - yaml >=0.2.5,<0.3.0a0 - license: MIT - license_family: MIT - purls: - - pkg:pypi/pyyaml?source=hash-mapping - size: 179738 - timestamp: 1770223468771 -- conda: https://conda.anaconda.org/conda-forge/win-64/pyyaml-6.0.3-py314h2359020_1.conda - sha256: a2aff34027aa810ff36a190b75002d2ff6f9fbef71ec66e567616ac3a679d997 - md5: 0cd9b88826d0f8db142071eb830bce56 - depends: - - python >=3.14,<3.15.0a0 - - python_abi 3.14.* *_cp314 - - ucrt >=10.0.20348.0 - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 - - yaml >=0.2.5,<0.3.0a0 - license: MIT - license_family: MIT - purls: - - pkg:pypi/pyyaml?source=hash-mapping - size: 181257 - timestamp: 1770223460931 -- pypi: https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl - name: pyyaml-env-tag - version: '1.1' - sha256: 17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04 +- pypi: https://files.pythonhosted.org/packages/47/cc/ddaf3af9c46966fef5be879256f213d85a0c56c75d07a3b7defec7cf6b4c/numpy-2.4.5-cp312-cp312-macosx_14_0_arm64.whl + name: numpy + version: 2.4.5 + sha256: 4f5bc96d35d94e4ceab8b38a92241b4611e95dc44e63b9f1fa2a331858ee3507 + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/49/b2/97980f3ad4fae37dd7fe31626e2bf75fbf8bdf5d303950ec1fab39a12da8/kiwisolver-1.5.0-cp314-cp314-macosx_11_0_arm64.whl + name: kiwisolver + version: 1.5.0 + sha256: 0cbe94b69b819209a62cb27bdfa5dc2a8977d8de2f89dfd97ba4f53ed3af754e + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/49/b3/2de412451330756aaaa72d27131db6dde23995efe62c941184e15242a5fa/sqlalchemy-2.0.49-cp312-cp312-macosx_11_0_arm64.whl + name: sqlalchemy + version: 2.0.49 + sha256: 4bbccb45260e4ff1b7db0be80a9025bb1e6698bdb808b83fff0000f7a90b2c0b + 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/4a/f3/00bb1e867fba351e2d784170955713bee200c43ea306c59f30bd7e748192/dask-2026.3.0-py3-none-any.whl + name: dask + version: 2026.3.0 + sha256: be614b9242b0b38288060fb2d7696125946469c98a1c30e174883fd199e0428d + requires_dist: + - click>=8.1 + - cloudpickle>=3.0.0 + - fsspec>=2021.9.0 + - packaging>=20.0 + - partd>=1.4.0 + - pyyaml>=5.3.1 + - toolz>=0.12.0 + - importlib-metadata>=4.13.0 ; python_full_version < '3.12' + - numpy>=1.24 ; extra == 'array' + - dask[array] ; extra == 'dataframe' + - pandas>=2.0 ; extra == 'dataframe' + - pyarrow>=16.0 ; extra == 'dataframe' + - distributed>=2026.3.0,<2026.3.1 ; extra == 'distributed' + - bokeh>=3.1.0 ; extra == 'diagnostics' + - jinja2>=2.10.3 ; extra == 'diagnostics' + - dask[array,dataframe,diagnostics,distributed] ; extra == 'complete' + - pyarrow>=16.0 ; extra == 'complete' + - lz4>=4.3.2 ; extra == 'complete' + - pandas[test] ; extra == 'test' + - pytest ; extra == 'test' + - pytest-cov ; extra == 'test' + - pytest-mock ; extra == 'test' + - pytest-rerunfailures ; extra == 'test' + - pytest-timeout ; extra == 'test' + - pytest-xdist ; extra == 'test' + - pre-commit ; extra == 'test' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/4b/39/f0e8ea762a764a9dc52aa7dabcfad51a354819de1f0d4652b6a1122424d6/scipy-1.17.1-cp314-cp314-win_amd64.whl + name: scipy + version: 1.17.1 + sha256: 3877ac408e14da24a6196de0ddcace62092bfc12a83823e92e49e40747e52c19 + requires_dist: + - numpy>=1.26.4,<2.7 + - pytest>=8.0.0 ; extra == 'test' + - pytest-cov ; extra == 'test' + - pytest-timeout ; extra == 'test' + - pytest-xdist ; extra == 'test' + - asv ; extra == 'test' + - mpmath ; extra == 'test' + - gmpy2 ; extra == 'test' + - threadpoolctl ; extra == 'test' + - scikit-umfpack ; extra == 'test' + - pooch ; extra == 'test' + - hypothesis>=6.30 ; extra == 'test' + - array-api-strict>=2.3.1 ; extra == 'test' + - cython ; extra == 'test' + - meson ; extra == 'test' + - ninja ; sys_platform != 'emscripten' and extra == 'test' + - sphinx>=5.0.0,<8.2.0 ; extra == 'doc' + - intersphinx-registry ; extra == 'doc' + - pydata-sphinx-theme>=0.15.2 ; extra == 'doc' + - sphinx-copybutton ; extra == 'doc' + - sphinx-design>=0.4.0 ; extra == 'doc' + - matplotlib>=3.5 ; extra == 'doc' + - numpydoc ; extra == 'doc' + - jupytext ; extra == 'doc' + - myst-nb>=1.2.0 ; extra == 'doc' + - pooch ; extra == 'doc' + - jupyterlite-sphinx>=0.19.1 ; extra == 'doc' + - jupyterlite-pyodide-kernel ; extra == 'doc' + - linkify-it-py ; extra == 'doc' + - tabulate ; extra == 'doc' + - click<8.3.0 ; extra == 'dev' + - spin ; extra == 'dev' + - mypy==1.10.0 ; extra == 'dev' + - typing-extensions ; extra == 'dev' + - types-psutil ; extra == 'dev' + - pycodestyle ; extra == 'dev' + - ruff>=0.12.0 ; extra == 'dev' + - cython-lint>=0.12.2 ; extra == 'dev' + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/4c/65/b4b86e5fa07543bfbbcdc6c9f7f9f561e66a5f3539992e3009973f2b1314/jupytext-1.19.2-py3-none-any.whl + name: jupytext + version: 1.19.2 + sha256: 8a31e896c7e9215841783aade24336e945543057e1c2d7f00b22f9e870348688 requires_dist: + - markdown-it-py>=1.0 + - mdit-py-plugins + - nbformat + - packaging - pyyaml + - tomli ; python_full_version < '3.11' + - autopep8 ; extra == 'dev' + - black ; extra == 'dev' + - flake8 ; extra == 'dev' + - gitpython ; extra == 'dev' + - ipykernel ; extra == 'dev' + - isort ; extra == 'dev' + - jupyter-fs[fs]>=1.0 ; extra == 'dev' + - jupyter-server!=2.11 ; extra == 'dev' + - marimo>=0.17.6,<=0.19.4 ; extra == 'dev' + - nbconvert ; extra == 'dev' + - pre-commit ; extra == 'dev' + - pytest ; extra == 'dev' + - pytest-asyncio ; extra == 'dev' + - pytest-cov>=2.6.1 ; extra == 'dev' + - pytest-randomly ; extra == 'dev' + - pytest-xdist ; extra == 'dev' + - sphinx ; extra == 'dev' + - sphinx-gallery>=0.8 ; extra == 'dev' + - myst-parser ; extra == 'docs' + - sphinx ; extra == 'docs' + - sphinx-copybutton ; extra == 'docs' + - sphinx-rtd-theme ; extra == 'docs' + - pytest ; extra == 'test' + - pytest-asyncio ; extra == 'test' + - pytest-randomly ; extra == 'test' + - pytest-xdist ; extra == 'test' + - black ; extra == 'test-cov' + - ipykernel ; extra == 'test-cov' + - jupyter-server!=2.11 ; extra == 'test-cov' + - nbconvert ; extra == 'test-cov' + - pytest ; extra == 'test-cov' + - pytest-asyncio ; extra == 'test-cov' + - pytest-cov>=2.6.1 ; extra == 'test-cov' + - pytest-randomly ; extra == 'test-cov' + - pytest-xdist ; extra == 'test-cov' + - autopep8 ; extra == 'test-external' + - black ; extra == 'test-external' + - flake8 ; extra == 'test-external' + - gitpython ; extra == 'test-external' + - ipykernel ; extra == 'test-external' + - isort ; extra == 'test-external' + - jupyter-fs[fs]>=1.0 ; extra == 'test-external' + - jupyter-server!=2.11 ; extra == 'test-external' + - marimo>=0.17.6,<=0.19.4 ; extra == 'test-external' + - nbconvert ; extra == 'test-external' + - pre-commit ; extra == 'test-external' + - pytest ; extra == 'test-external' + - pytest-asyncio ; extra == 'test-external' + - pytest-randomly ; extra == 'test-external' + - pytest-xdist ; extra == 'test-external' + - sphinx ; extra == 'test-external' + - sphinx-gallery>=0.8 ; extra == 'test-external' + - black ; extra == 'test-functional' + - pytest ; extra == 'test-functional' + - pytest-asyncio ; extra == 'test-functional' + - pytest-randomly ; extra == 'test-functional' + - pytest-xdist ; extra == 'test-functional' + - black ; extra == 'test-integration' + - ipykernel ; extra == 'test-integration' + - jupyter-server!=2.11 ; extra == 'test-integration' + - nbconvert ; extra == 'test-integration' + - pytest ; extra == 'test-integration' + - pytest-asyncio ; extra == 'test-integration' + - pytest-randomly ; extra == 'test-integration' + - pytest-xdist ; extra == 'test-integration' + - bash-kernel ; extra == 'test-ui' requires_python: '>=3.9' -- conda: https://conda.anaconda.org/conda-forge/linux-64/pyzmq-27.1.0-py312hda471dd_2.conda - noarch: python - sha256: be66c1f85c3b48137200d62c12d918f4f8ad329423daef04fed292818efd3c28 - md5: 082985717303dab433c976986c674b35 - depends: - - python - - libgcc >=14 - - libstdcxx >=14 - - __glibc >=2.17,<3.0.a0 - - zeromq >=4.3.5,<4.4.0a0 - - _python_abi3_support 1.* - - cpython >=3.12 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/pyzmq?source=hash-mapping - size: 211567 - timestamp: 1771716961404 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyzmq-27.1.0-py312h022ad19_2.conda - noarch: python - sha256: 2f31f799a46ed75518fae0be75ecc8a1b84360dbfd55096bc2fe8bd9c797e772 - md5: 2f6b79700452ef1e91f45a99ab8ffe5a - depends: - - python - - libcxx >=19 - - __osx >=11.0 - - _python_abi3_support 1.* - - cpython >=3.12 - - zeromq >=4.3.5,<4.4.0a0 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/pyzmq?source=hash-mapping - size: 191641 - timestamp: 1771717073430 -- conda: https://conda.anaconda.org/conda-forge/win-64/pyzmq-27.1.0-py312h343a6d4_2.conda - noarch: python - sha256: d84bcc19a945ca03d1fd794be3e9896ab6afc9f691d58d9c2da514abe584d4df - md5: eb1ec67a70b4d479f7dd76e6c8fe7575 - depends: - - python - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 - - ucrt >=10.0.20348.0 - - zeromq >=4.3.5,<4.3.6.0a0 - - _python_abi3_support 1.* - - cpython >=3.12 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/pyzmq?source=hash-mapping - size: 183235 - timestamp: 1771716967192 -- 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 +- pypi: https://files.pythonhosted.org/packages/4c/fe/4fb3a0805bd5165da5ebf858da7cc01cce8061674106d2cf5bdab32cbfde/greenlet-3.5.0-cp312-cp312-win_amd64.whl + name: greenlet + version: 3.5.0 + sha256: d4d9f0624c775f2dfc56ba54d515a8c771044346852a918b405914f6b19d7fd8 + requires_dist: + - sphinx ; extra == 'docs' + - furo ; extra == 'docs' + - objgraph ; extra == 'test' + - psutil ; extra == 'test' + - setuptools ; extra == 'test' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/51/ac/b9d68ebddfe1b02c77af5bf81120e12b036b4432dc6af7a303d90e2bc38b/chardet-7.4.3-cp314-cp314-macosx_11_0_arm64.whl + name: chardet + version: 7.4.3 + sha256: acc46d1b8b7d5783216afe15db56d1c179b9a40e5a1558bc13164c4fd20674c4 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/52/59/0782e51887ac6b07ffd1570e0364cf901ebc36345fea669969d2084baebb/simple_websocket-1.1.0-py3-none-any.whl + name: simple-websocket + version: 1.1.0 + sha256: 4af6069630a38ed6c561010f0e11a5bc0d4ca569b36306eb257cd9a192497c8c requires_dist: - - 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 - sha256: 632cc032364a6f8bb1010a2f6a12d0f14bc7e5ede76585ef29dc0cecf4cd8859 + - wsproto + - tox ; extra == 'dev' + - flake8 ; extra == 'dev' + - pytest ; extra == 'dev' + - pytest-cov ; extra == 'dev' + - sphinx ; extra == 'docs' + requires_python: '>=3.6' +- pypi: https://files.pythonhosted.org/packages/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl + name: contourpy + version: 1.3.3 + sha256: 556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6 requires_dist: - - mando>=0.6,<0.8 - - colorama==0.4.1 ; python_full_version < '3.5' - - colorama>=0.4.1 ; python_full_version >= '3.5' - - tomli>=2.0.1 ; extra == 'toml' -- conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.3-h853b02a_0.conda - sha256: 12ffde5a6f958e285aa22c191ca01bbd3d6e710aa852e00618fa6ddc59149002 - md5: d7d95fc8287ea7bf33e0e7116d2b95ec - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=14 - - ncurses >=6.5,<7.0a0 - license: GPL-3.0-only - license_family: GPL - purls: [] - size: 345073 - timestamp: 1765813471974 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.3-h46df422_0.conda - sha256: a77010528efb4b548ac2a4484eaf7e1c3907f2aec86123ed9c5212ae44502477 - md5: f8381319127120ce51e081dce4865cf4 - depends: - - __osx >=11.0 - - ncurses >=6.5,<7.0a0 - license: GPL-3.0-only - license_family: GPL - purls: [] - size: 313930 - timestamp: 1765813902568 -- conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.37.0-pyhcf101f3_0.conda - sha256: 0577eedfb347ff94d0f2fa6c052c502989b028216996b45c7f21236f25864414 - md5: 870293df500ca7e18bedefa5838a22ab - depends: - - attrs >=22.2.0 - - python >=3.10 - - rpds-py >=0.7.0 - - typing_extensions >=4.4.0 - - python - license: MIT - license_family: MIT - purls: - - pkg:pypi/referencing?source=hash-mapping - size: 51788 - timestamp: 1760379115194 -- conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.33.1-pyhcf101f3_1.conda - sha256: 7f2c24dd3bd3c104a1d2c9a10ead5ed6758b0976b74f972cfe9c19884ccc4241 - md5: 9659f587a8ceacc21864260acd02fc67 - depends: - - python >=3.10 - - certifi >=2023.5.7 - - charset-normalizer >=2,<4 - - idna >=2.5,<4 - - urllib3 >=1.26,<3 - - python - constrains: - - chardet >=3.0.2,<8 - license: Apache-2.0 - license_family: APACHE - purls: - - pkg:pypi/requests?source=compressed-mapping - size: 63728 - timestamp: 1777030058920 -- conda: https://conda.anaconda.org/conda-forge/noarch/returns-0.27.0-pyhc364b38_0.conda - sha256: 3b45efeae771f1a20307b36ecdb3a8911a89c05382836b50c62b0a99d8d3dfd8 - md5: da94ff04d97ec5efc42cbe5da3c43a84 - depends: - - python >=3.11 - - typing_extensions >=4.0,<5.0 - - python - license: BSD-2-Clause - license_family: BSD - purls: - - pkg:pypi/returns?source=hash-mapping - size: 100559 - timestamp: 1776176903101 -- conda: https://conda.anaconda.org/conda-forge/noarch/rfc3339-validator-0.1.4-pyhd8ed1ab_1.conda - sha256: 2e4372f600490a6e0b3bac60717278448e323cab1c0fecd5f43f7c56535a99c5 - md5: 36de09a8d3e5d5e6f4ee63af49e59706 - depends: - - python >=3.9 - - six - license: MIT - license_family: MIT - purls: - - pkg:pypi/rfc3339-validator?source=hash-mapping - size: 10209 - timestamp: 1733600040800 -- conda: https://conda.anaconda.org/conda-forge/noarch/rfc3986-validator-0.1.1-pyh9f0ad1d_0.tar.bz2 - sha256: 2a5b495a1de0f60f24d8a74578ebc23b24aa53279b1ad583755f223097c41c37 - md5: 912a71cc01012ee38e6b90ddd561e36f - depends: - - python - license: MIT - license_family: MIT - purls: - - pkg:pypi/rfc3986-validator?source=hash-mapping - size: 7818 - timestamp: 1598024297745 -- conda: https://conda.anaconda.org/conda-forge/noarch/rfc3987-syntax-1.1.0-pyhe01879c_1.conda - sha256: 70001ac24ee62058557783d9c5a7bbcfd97bd4911ef5440e3f7a576f9e43bc92 - md5: 7234f99325263a5af6d4cd195035e8f2 - depends: - - python >=3.9 - - lark >=1.2.2 - - python - license: MIT - license_family: MIT - purls: - - pkg:pypi/rfc3987-syntax?source=hash-mapping - size: 22913 - timestamp: 1752876729969 -- pypi: https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl - name: rich - version: 15.0.0 - sha256: 33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb + - numpy>=1.25 + - furo ; extra == 'docs' + - sphinx>=7.2 ; extra == 'docs' + - sphinx-copybutton ; extra == 'docs' + - bokeh ; extra == 'bokeh' + - selenium ; extra == 'bokeh' + - contourpy[bokeh,docs] ; extra == 'mypy' + - bokeh ; extra == 'mypy' + - docutils-stubs ; extra == 'mypy' + - mypy==1.17.0 ; extra == 'mypy' + - types-pillow ; extra == 'mypy' + - contourpy[test-no-images] ; extra == 'test' + - matplotlib ; extra == 'test' + - pillow ; extra == 'test' + - pytest ; extra == 'test-no-images' + - pytest-cov ; extra == 'test-no-images' + - pytest-rerunfailures ; extra == 'test-no-images' + - pytest-xdist ; extra == 'test-no-images' + - wurlitzer ; extra == 'test-no-images' + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl + name: pluggy + version: 1.6.0 + sha256: e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746 requires_dist: - - ipywidgets>=7.5.1,<9 ; extra == 'jupyter' - - markdown-it-py>=2.2.0 - - pygments>=2.13.0,<3.0.0 - requires_python: '>=3.9.0' -- conda: https://conda.anaconda.org/conda-forge/linux-64/rpds-py-0.30.0-py312h868fb18_0.conda - sha256: 62f46e85caaba30b459da7dfcf3e5488ca24fd11675c33ce4367163ab191a42c - md5: 3ffc5a3572db8751c2f15bacf6a0e937 - depends: - - python - - __glibc >=2.17,<3.0.a0 - - libgcc >=14 - - python_abi 3.12.* *_cp312 - constrains: - - __glibc >=2.17 - license: MIT - license_family: MIT - purls: - - pkg:pypi/rpds-py?source=hash-mapping - size: 383750 - timestamp: 1764543174231 -- conda: https://conda.anaconda.org/conda-forge/linux-64/rpds-py-0.30.0-py314h2e6c369_0.conda - sha256: e53b0cbf3b324eaa03ca1fe1a688fdf4ab42cea9c25270b0a7307d8aaaa4f446 - md5: c1c368b5437b0d1a68f372ccf01cb133 - depends: - - python - - libgcc >=14 - - __glibc >=2.17,<3.0.a0 - - python_abi 3.14.* *_cp314 - constrains: - - __glibc >=2.17 - license: MIT - license_family: MIT - purls: - - pkg:pypi/rpds-py?source=hash-mapping - size: 376121 - timestamp: 1764543122774 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/rpds-py-0.30.0-py312h6ef9ec0_0.conda - sha256: ea06f6f66b1bea97244c36fd2788ccd92fd1fb06eae98e469dd95ee80831b057 - md5: a7cfbbdeb93bb9a3f249bc4c3569cd4c - depends: - - python - - __osx >=11.0 - - python 3.12.* *_cpython - - python_abi 3.12.* *_cp312 - constrains: - - __osx >=11.0 - license: MIT - license_family: MIT - purls: - - pkg:pypi/rpds-py?source=hash-mapping - size: 358853 - timestamp: 1764543161524 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/rpds-py-0.30.0-py314haad56a0_0.conda - sha256: e161dd97403b8b8a083d047369a5cf854557dba1204d29e2f0250f5ac4403925 - md5: 76a4f88d1b7748c477abf3c341edc64c - depends: - - python - - __osx >=11.0 - - python 3.14.* *_cp314 - - python_abi 3.14.* *_cp314 - constrains: - - __osx >=11.0 - license: MIT - license_family: MIT - purls: - - pkg:pypi/rpds-py?source=hash-mapping - size: 350976 - timestamp: 1764543169524 -- conda: https://conda.anaconda.org/conda-forge/win-64/rpds-py-0.30.0-py312hdabe01f_0.conda - sha256: faad05e6df2fc15e3ae06fdd71a36e17ff25364777aa4c40f2ec588740d64091 - md5: 2c51baeda0a355b0a5e7b6acb28cf02d - depends: - - python - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 - - ucrt >=10.0.20348.0 - - python_abi 3.12.* *_cp312 - license: MIT - license_family: MIT - purls: - - pkg:pypi/rpds-py?source=hash-mapping - size: 243577 - timestamp: 1764543069837 -- conda: https://conda.anaconda.org/conda-forge/win-64/rpds-py-0.30.0-py314h9f07db2_0.conda - sha256: e4435368c5c25076dc0f5918ba531c5a92caee8e0e2f9912ef6810049cf00db2 - md5: e86531e278ad304438e530953cd55d14 - depends: - - python - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 - - ucrt >=10.0.20348.0 - - python_abi 3.14.* *_cp314 - license: MIT - license_family: MIT - purls: - - pkg:pypi/rpds-py?source=hash-mapping - size: 235780 - timestamp: 1764543046065 -- pypi: https://files.pythonhosted.org/packages/33/f1/9614e03e1cdcbf9437570b5400ced8a720b5db22b28d8e0f1bda429f660d/ruff-0.15.12-py3-none-win_amd64.whl - name: ruff - version: 0.15.12 - sha256: c87a162d61ab3adca47c03f7f717c68672edec7d1b5499e652331780fe74950d - requires_python: '>=3.7' -- pypi: https://files.pythonhosted.org/packages/aa/a4/f828e9718d3dce1f5f11c39c4f65afd32783c8b2aebb2e3d259e492c47bd/ruff-0.15.12-py3-none-macosx_11_0_arm64.whl - name: ruff - version: 0.15.12 - sha256: fe87510d000220aa1ed530d4448a7c696a0cae1213e5ec30e5874287b66557b5 - requires_python: '>=3.7' -- pypi: https://files.pythonhosted.org/packages/e8/39/c61d193b8a1daaa8977f7dea9e8d8ba866e02ea7b65d32f6861693aa4c12/ruff-0.15.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - name: ruff - version: 0.15.12 - sha256: 83b2f4f2f3b1026b5fb449b467d9264bf22067b600f7b6f41fc5958909f449d0 + - pre-commit ; extra == 'dev' + - tox ; extra == 'dev' + - pytest ; extra == 'testing' + - pytest-benchmark ; extra == 'testing' + - coverage ; extra == 'testing' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/55/33/bf28f618c0a9597d14e0b9ee7d1e0622faff738d44fe986ee287cdf1b8d0/sqlalchemy-2.0.49-cp314-cp314-macosx_11_0_arm64.whl + name: sqlalchemy + version: 2.0.49 + sha256: 233088b4b99ebcbc5258c755a097aa52fbf90727a03a5a80781c4b9c54347a2e + requires_dist: + - importlib-metadata ; python_full_version < '3.8' + - greenlet>=1 ; platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64' + - typing-extensions>=4.6.0 + - greenlet>=1 ; extra == 'asyncio' + - mypy>=0.910 ; extra == 'mypy' + - pyodbc ; extra == 'mssql' + - pymssql ; extra == 'mssql-pymssql' + - pyodbc ; extra == 'mssql-pyodbc' + - mysqlclient>=1.4.0 ; extra == 'mysql' + - mysql-connector-python ; extra == 'mysql-connector' + - mariadb>=1.0.1,!=1.1.2,!=1.1.5,!=1.1.10 ; extra == 'mariadb-connector' + - cx-oracle>=8 ; extra == 'oracle' + - oracledb>=1.0.1 ; extra == 'oracle-oracledb' + - psycopg2>=2.7 ; extra == 'postgresql' + - pg8000>=1.29.1 ; extra == 'postgresql-pg8000' + - greenlet>=1 ; extra == 'postgresql-asyncpg' + - asyncpg ; extra == 'postgresql-asyncpg' + - psycopg2-binary ; extra == 'postgresql-psycopg2binary' + - psycopg2cffi ; extra == 'postgresql-psycopg2cffi' + - psycopg>=3.0.7 ; extra == 'postgresql-psycopg' + - psycopg[binary]>=3.0.7 ; extra == 'postgresql-psycopgbinary' + - pymysql ; extra == 'pymysql' + - greenlet>=1 ; extra == 'aiomysql' + - aiomysql>=0.2.0 ; extra == 'aiomysql' + - greenlet>=1 ; extra == 'aioodbc' + - aioodbc ; extra == 'aioodbc' + - greenlet>=1 ; extra == 'asyncmy' + - asyncmy>=0.2.3,!=0.2.4,!=0.2.6 ; extra == 'asyncmy' + - greenlet>=1 ; extra == 'aiosqlite' + - aiosqlite ; extra == 'aiosqlite' + - typing-extensions!=3.10.0.1 ; extra == 'aiosqlite' + - sqlcipher3-binary ; extra == 'sqlcipher' requires_python: '>=3.7' -- pypi: https://files.pythonhosted.org/packages/af/46/661159ad844034ba8b3f4e0516215c41e4ee17db4213d13a82227670764f/sciline-25.11.1-py3-none-any.whl - name: sciline - version: 25.11.1 - sha256: 13c378287b8157e819b9b67d7e973c65bc6bdc545a3602d18204c365b0c336f9 +- 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: - - cyclebane>=24.6.0 - - pytest ; extra == 'test' - - pytest-randomly>=3 ; extra == 'test' - - dask ; extra == 'test' - - graphviz ; extra == 'test' + - comm>=0.1.3 + - ipython>=6.1.0 + - traitlets>=4.3.1 + - widgetsnbextension~=4.0.14 + - jupyterlab-widgets~=3.0.15 - jsonschema ; extra == 'test' - - numpy ; extra == 'test' - - pandas ; extra == 'test' - - pydantic ; extra == 'test' - - rich ; extra == 'test' - - rich ; extra == 'progress' + - ipykernel ; extra == 'test' + - pytest>=3.6.0 ; extra == 'test' + - pytest-cov ; extra == 'test' + - pytz ; extra == 'test' + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/57/d8/8d44036d7eb7b6a8ec4c5494ea0c8c8b94fbc0ed3991c1a7adf230df03bf/aiohttp-3.13.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + name: aiohttp + version: 3.13.5 + sha256: b18f31b80d5a33661e08c89e202edabf1986e9b49c42b4504371daeaa11b47c1 + requires_dist: + - aiohappyeyeballs>=2.5.0 + - aiosignal>=1.4.0 + - async-timeout>=4.0,<6.0 ; python_full_version < '3.11' + - attrs>=17.3.0 + - frozenlist>=1.1.1 + - multidict>=4.5,<7.0 + - propcache>=0.2.0 + - yarl>=1.17.0,<2.0 + - aiodns>=3.3.0 ; extra == 'speedups' + - brotli>=1.2 ; platform_python_implementation == 'CPython' and extra == 'speedups' + - brotlicffi>=1.2 ; platform_python_implementation != 'CPython' and extra == 'speedups' + - backports-zstd ; python_full_version < '3.14' and platform_python_implementation == 'CPython' and extra == 'speedups' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/58/3b/1cdec6772bdbaf7b25dab360c59f03cadf05492dd724c6540af905389b07/pandas-3.0.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl + name: pandas + version: 3.0.3 + sha256: 9d71c63ae4ebdbf70209742096f1fc46a83a0613c99d4b23766cced9ff8cd62a + requires_dist: + - numpy>=1.26.0 ; python_full_version < '3.14' + - numpy>=2.3.3 ; python_full_version >= '3.14' + - python-dateutil>=2.8.2 + - tzdata ; sys_platform == 'win32' + - tzdata ; sys_platform == 'emscripten' + - hypothesis>=6.116.0 ; extra == 'test' + - pytest>=8.3.4 ; extra == 'test' + - pytest-xdist>=3.6.1 ; extra == 'test' + - pyarrow>=13.0.0 ; extra == 'pyarrow' + - bottleneck>=1.4.2 ; extra == 'performance' + - numba>=0.60.0 ; extra == 'performance' + - numexpr>=2.10.2 ; extra == 'performance' + - scipy>=1.14.1 ; extra == 'computation' + - xarray>=2024.10.0 ; extra == 'computation' + - fsspec>=2024.10.0 ; extra == 'fss' + - s3fs>=2024.10.0 ; extra == 'aws' + - gcsfs>=2024.10.0 ; extra == 'gcp' + - odfpy>=1.4.1 ; extra == 'excel' + - openpyxl>=3.1.5 ; extra == 'excel' + - python-calamine>=0.3.0 ; extra == 'excel' + - pyxlsb>=1.0.10 ; extra == 'excel' + - xlrd>=2.0.1 ; extra == 'excel' + - xlsxwriter>=3.2.0 ; extra == 'excel' + - pyarrow>=13.0.0 ; extra == 'parquet' + - pyarrow>=13.0.0 ; extra == 'feather' + - pyiceberg>=0.8.1 ; extra == 'iceberg' + - tables>=3.10.1 ; extra == 'hdf5' + - pyreadstat>=1.2.8 ; extra == 'spss' + - sqlalchemy>=2.0.36 ; extra == 'postgresql' + - psycopg2>=2.9.10 ; extra == 'postgresql' + - adbc-driver-postgresql>=1.2.0 ; extra == 'postgresql' + - sqlalchemy>=2.0.36 ; extra == 'mysql' + - pymysql>=1.1.1 ; extra == 'mysql' + - sqlalchemy>=2.0.36 ; extra == 'sql-other' + - adbc-driver-postgresql>=1.2.0 ; extra == 'sql-other' + - adbc-driver-sqlite>=1.2.0 ; extra == 'sql-other' + - beautifulsoup4>=4.12.3 ; extra == 'html' + - html5lib>=1.1 ; extra == 'html' + - lxml>=5.3.0 ; extra == 'html' + - lxml>=5.3.0 ; extra == 'xml' + - matplotlib>=3.9.3 ; extra == 'plot' + - jinja2>=3.1.5 ; extra == 'output-formatting' + - tabulate>=0.9.0 ; extra == 'output-formatting' + - pyqt5>=5.15.9 ; extra == 'clipboard' + - qtpy>=2.4.2 ; extra == 'clipboard' + - zstandard>=0.23.0 ; extra == 'compression' + - pytz>=2020.1 ; extra == 'timezone' + - adbc-driver-postgresql>=1.2.0 ; extra == 'all' + - adbc-driver-sqlite>=1.2.0 ; extra == 'all' + - beautifulsoup4>=4.12.3 ; extra == 'all' + - bottleneck>=1.4.2 ; extra == 'all' + - fastparquet>=2024.11.0 ; extra == 'all' + - fsspec>=2024.10.0 ; extra == 'all' + - gcsfs>=2024.10.0 ; extra == 'all' + - html5lib>=1.1 ; extra == 'all' + - hypothesis>=6.116.0 ; extra == 'all' + - jinja2>=3.1.5 ; extra == 'all' + - lxml>=5.3.0 ; extra == 'all' + - matplotlib>=3.9.3 ; extra == 'all' + - numba>=0.60.0 ; extra == 'all' + - numexpr>=2.10.2 ; extra == 'all' + - odfpy>=1.4.1 ; extra == 'all' + - openpyxl>=3.1.5 ; extra == 'all' + - psycopg2>=2.9.10 ; extra == 'all' + - pyarrow>=13.0.0 ; extra == 'all' + - pyiceberg>=0.8.1 ; extra == 'all' + - pymysql>=1.1.1 ; extra == 'all' + - pyqt5>=5.15.9 ; extra == 'all' + - pyreadstat>=1.2.8 ; extra == 'all' + - pytest>=8.3.4 ; extra == 'all' + - pytest-xdist>=3.6.1 ; extra == 'all' + - python-calamine>=0.3.0 ; extra == 'all' + - pytz>=2020.1 ; extra == 'all' + - pyxlsb>=1.0.10 ; extra == 'all' + - qtpy>=2.4.2 ; extra == 'all' + - scipy>=1.14.1 ; extra == 'all' + - s3fs>=2024.10.0 ; extra == 'all' + - sqlalchemy>=2.0.36 ; extra == 'all' + - tables>=3.10.1 ; extra == 'all' + - tabulate>=0.9.0 ; extra == 'all' + - xarray>=2024.10.0 ; extra == 'all' + - xlrd>=2.0.1 ; extra == 'all' + - xlsxwriter>=3.2.0 ; extra == 'all' + - zstandard>=0.23.0 ; extra == 'all' requires_python: '>=3.11' -- pypi: https://files.pythonhosted.org/packages/0f/0e/0eb94e64f5badef67f11fe1e448dde2a44f00940d8949f4adf71d560552e/scipp-26.3.1-cp314-cp314-macosx_14_0_arm64.whl - name: scipp - version: 26.3.1 - sha256: 1f103f6c5a33b08773206c613fe2dd9c02585f5c4e44b77311c54b7828a758ed +- pypi: https://files.pythonhosted.org/packages/58/e0/f1871f520c359e4e3a2eb7437c9e7e792bb6c356414e8617937561167caf/pycifrw-5.0.1.tar.gz + name: pycifrw + version: 5.0.1 + sha256: e636b80be6a2be15b215e69ecec0c0a784ebcbfed8b1e3bac4bcc6e6ba9a75e0 + requires_dist: + - prettytable + - ply + - numpy +- pypi: https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl + name: frozenlist + version: 1.8.0 + sha256: 3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/5b/29/74eeb4d3f3ae61ca096b018ad486b3b3c74b17bec09ab4edab721cbefec3/typeguard-4.5.2-py3-none-any.whl + name: typeguard + version: 4.5.2 + sha256: fcf9de18bd945cdb4c7b996e12b4c51ce83f92f191314a6d7cf1739586ec98cf + 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/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl + name: mkdocs-material-extensions + version: 1.3.1 + sha256: adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31 + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/5b/69/93b34728cc386efdde0c342f8c680b9187dea7beb7adaf6b58a0713be101/mpld3-0.5.12-py3-none-any.whl + name: mpld3 + version: 0.5.12 + sha256: bea31799a4041029a906f53f2662bbf1c49903e0c0bc712b412354158ec7cf54 + requires_dist: + - jinja2 + - matplotlib +- pypi: https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl + name: watchdog + version: 6.0.0 + sha256: 6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0 + requires_dist: + - pyyaml>=3.10 ; extra == 'watchmedo' + requires_python: '>=3.9' +- 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/5d/7b/25a221d2c761c6a8ae21bfa3874988ff2583e19cf8a27bf2fee358df7942/pillow-12.2.0-cp312-cp312-win_amd64.whl + name: pillow + version: 12.2.0 + sha256: 7f84204dee22a783350679a0333981df803dac21a0190d706a50475e361c93f5 + requires_dist: + - furo ; extra == 'docs' + - olefile ; extra == 'docs' + - sphinx>=8.2 ; extra == 'docs' + - sphinx-autobuild ; extra == 'docs' + - sphinx-copybutton ; extra == 'docs' + - sphinx-inline-tabs ; extra == 'docs' + - sphinxext-opengraph ; extra == 'docs' + - olefile ; extra == 'fpx' + - olefile ; extra == 'mic' + - arro3-compute ; extra == 'test-arrow' + - arro3-core ; extra == 'test-arrow' + - nanoarrow ; extra == 'test-arrow' + - pyarrow ; extra == 'test-arrow' + - check-manifest ; extra == 'tests' + - coverage>=7.4.2 ; extra == 'tests' + - defusedxml ; extra == 'tests' + - markdown2 ; extra == 'tests' + - olefile ; extra == 'tests' + - packaging ; extra == 'tests' + - pyroma>=5 ; extra == 'tests' + - pytest ; extra == 'tests' + - pytest-cov ; extra == 'tests' + - pytest-timeout ; extra == 'tests' + - pytest-xdist ; extra == 'tests' + - trove-classifiers>=2024.10.12 ; extra == 'tests' + - defusedxml ; extra == 'xmp' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + name: pydantic-core + version: 2.46.4 + sha256: 926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce + requires_dist: + - typing-extensions>=4.14.1 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/61/2b/e260d50e64690d2a9e405d52ccd18a63c286c5088937dd0107cb23eb3195/diffpy_utils-3.7.2-py3-none-any.whl + name: diffpy-utils + version: 3.7.2 + sha256: 6100600736791a8e4638e3dd476704f4dabe3cab75bcb5c60c83c16a2032519a requires_dist: - - numpy>=2 + - numpy + - xraydb + - scipy + requires_python: '>=3.10,<3.15' +- pypi: https://files.pythonhosted.org/packages/61/7c/5c0d34aa3024694d6dcb9271cdbdd08c4e47c1c0ad95ec7e7bc74cdea145/propcache-0.5.2-cp312-cp312-win_amd64.whl + name: propcache + version: 0.5.2 + sha256: d9ee8826a7d47863a08ac44e1a5f611a462eefc3a194b492da242128bec75b42 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/61/d2/45c9defbaa1ea297035d9d4cce9e8f80daafbf19319c6007f157c6256ea9/propcache-0.5.2-cp314-cp314-win_amd64.whl + name: propcache + version: 0.5.2 + sha256: 81e3a30b0bb60caa22033dd0f8a3618d1d67356212514f62c57db75cb0ef410c + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/63/9f/724f66a48309dd97a2ff58f491d6ffd925f35d1278a5e55dc9a5ac6a156b/scippneutron-26.5.0-py3-none-any.whl + name: scippneutron + version: 26.5.0 + sha256: e9cfad09b974867c6dc2a175cd2e575e06eaa951b2409e9ef863db237853bf99 + requires_dist: + - python-dateutil>=2.8 + - email-validator>=2 + - h5py>=3.12 + - lazy-loader>=0.4 + - mpltoolbox>=24.6.0 + - numpy>=1.20 + - plopp>=26.4.1 + - pydantic>=2 + - scipp>=25.8.0 + - scippnexus>=23.11.0 + - scipy>=1.7.0 + - scipp[all]>=25.8.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/63/b1/4260d67d6bd85e58a66b72d54ce15d5de789b6f3870cc6bedf8ff9667401/propcache-0.5.2-cp314-cp314-macosx_11_0_arm64.whl + name: propcache + version: 0.5.2 + sha256: 97797ebb098e670a2f92dd66f32897e30d7615b14e7f59711de23e30a9072539 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/64/a8/c09fbe44b12fa919c5bfe0afb71e60d1231a7dc93405e54c30496c57c9d3/arviz-1.1.0-py3-none-any.whl + name: arviz + version: 1.1.0 + sha256: 87ebd21ce052f30d21f932b4166fc31b91c0bc7443f8da7fed3518b342267010 + requires_dist: + - arviz-base>=1.1.0,<1.2.0 + - arviz-stats[xarray]>=1.1.0,<1.2.0 + - arviz-plots>=1.1.0,<1.2.0 + - arviz-plots[bokeh] ; extra == 'bokeh' + - build ; extra == 'check' + - pre-commit ; extra == 'check' + - h5netcdf ; extra == 'doc' + - h5py ; extra == 'doc' + - jupyter-sphinx ; extra == 'doc' + - matplotlib ; extra == 'doc' + - myst-parser[linkify] ; extra == 'doc' + - myst-nb ; extra == 'doc' + - pydata-sphinx-theme>=0.13 ; extra == 'doc' + - sphinx>=5 ; extra == 'doc' + - sphinx-copybutton ; extra == 'doc' + - sphinx-design ; extra == 'doc' + - sphinx-notfound-page ; extra == 'doc' + - sphinxcontrib-youtube ; extra == 'doc' + - sphinx-togglebutton ; extra == 'doc' + - sphobjinv ; extra == 'doc' + - arviz-base[h5netcdf] ; extra == 'h5netcdf' + - arviz-plots[matplotlib] ; extra == 'matplotlib' + - arviz-base[netcdf4] ; extra == 'netcdf4' + - arviz-plots[plotly] ; extra == 'plotly' - pytest ; extra == 'test' - - 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' + - arviz-base[zarr] ; extra == 'zarr' + requires_python: '>=3.12' +- pypi: https://files.pythonhosted.org/packages/65/92/a5100f7185a800a5d29f8d14041f61475b9de465ffcc0f3b9fba606e4505/msgpack-1.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + name: msgpack + version: 1.1.2 + sha256: 372839311ccf6bdaf39b00b61288e0557916c3729529b301c52c2d88842add42 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/65/b6/09b01cdbc15224e2850365192d17b7bdebb8bdbd8780ed221fcdf0d9a515/pandas-3.0.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl + name: pandas + version: 3.0.3 + sha256: 6dc0b3fd2169c9157deed50b4d519553a3655c8c6a96027136d654592be973a9 + requires_dist: + - numpy>=1.26.0 ; python_full_version < '3.14' + - numpy>=2.3.3 ; python_full_version >= '3.14' + - python-dateutil>=2.8.2 + - tzdata ; sys_platform == 'win32' + - tzdata ; sys_platform == 'emscripten' + - hypothesis>=6.116.0 ; extra == 'test' + - pytest>=8.3.4 ; extra == 'test' + - pytest-xdist>=3.6.1 ; extra == 'test' + - pyarrow>=13.0.0 ; extra == 'pyarrow' + - bottleneck>=1.4.2 ; extra == 'performance' + - numba>=0.60.0 ; extra == 'performance' + - numexpr>=2.10.2 ; extra == 'performance' + - scipy>=1.14.1 ; extra == 'computation' + - xarray>=2024.10.0 ; extra == 'computation' + - fsspec>=2024.10.0 ; extra == 'fss' + - s3fs>=2024.10.0 ; extra == 'aws' + - gcsfs>=2024.10.0 ; extra == 'gcp' + - odfpy>=1.4.1 ; extra == 'excel' + - openpyxl>=3.1.5 ; extra == 'excel' + - python-calamine>=0.3.0 ; extra == 'excel' + - pyxlsb>=1.0.10 ; extra == 'excel' + - xlrd>=2.0.1 ; extra == 'excel' + - xlsxwriter>=3.2.0 ; extra == 'excel' + - pyarrow>=13.0.0 ; extra == 'parquet' + - pyarrow>=13.0.0 ; extra == 'feather' + - pyiceberg>=0.8.1 ; extra == 'iceberg' + - tables>=3.10.1 ; extra == 'hdf5' + - pyreadstat>=1.2.8 ; extra == 'spss' + - sqlalchemy>=2.0.36 ; extra == 'postgresql' + - psycopg2>=2.9.10 ; extra == 'postgresql' + - adbc-driver-postgresql>=1.2.0 ; extra == 'postgresql' + - sqlalchemy>=2.0.36 ; extra == 'mysql' + - pymysql>=1.1.1 ; extra == 'mysql' + - sqlalchemy>=2.0.36 ; extra == 'sql-other' + - adbc-driver-postgresql>=1.2.0 ; extra == 'sql-other' + - adbc-driver-sqlite>=1.2.0 ; extra == 'sql-other' + - beautifulsoup4>=4.12.3 ; extra == 'html' + - html5lib>=1.1 ; extra == 'html' + - lxml>=5.3.0 ; extra == 'html' + - lxml>=5.3.0 ; extra == 'xml' + - matplotlib>=3.9.3 ; extra == 'plot' + - jinja2>=3.1.5 ; extra == 'output-formatting' + - tabulate>=0.9.0 ; extra == 'output-formatting' + - pyqt5>=5.15.9 ; extra == 'clipboard' + - qtpy>=2.4.2 ; extra == 'clipboard' + - zstandard>=0.23.0 ; extra == 'compression' + - pytz>=2020.1 ; extra == 'timezone' + - adbc-driver-postgresql>=1.2.0 ; extra == 'all' + - adbc-driver-sqlite>=1.2.0 ; extra == 'all' + - beautifulsoup4>=4.12.3 ; extra == 'all' + - bottleneck>=1.4.2 ; extra == 'all' + - fastparquet>=2024.11.0 ; extra == 'all' + - fsspec>=2024.10.0 ; extra == 'all' + - gcsfs>=2024.10.0 ; extra == 'all' + - html5lib>=1.1 ; extra == 'all' + - hypothesis>=6.116.0 ; extra == 'all' + - jinja2>=3.1.5 ; extra == 'all' + - lxml>=5.3.0 ; extra == 'all' + - matplotlib>=3.9.3 ; extra == 'all' + - numba>=0.60.0 ; extra == 'all' + - numexpr>=2.10.2 ; extra == 'all' + - odfpy>=1.4.1 ; extra == 'all' + - openpyxl>=3.1.5 ; extra == 'all' + - psycopg2>=2.9.10 ; extra == 'all' + - pyarrow>=13.0.0 ; extra == 'all' + - pyiceberg>=0.8.1 ; extra == 'all' + - pymysql>=1.1.1 ; extra == 'all' + - pyqt5>=5.15.9 ; extra == 'all' + - pyreadstat>=1.2.8 ; extra == 'all' + - pytest>=8.3.4 ; extra == 'all' + - pytest-xdist>=3.6.1 ; extra == 'all' + - python-calamine>=0.3.0 ; extra == 'all' + - pytz>=2020.1 ; extra == 'all' + - pyxlsb>=1.0.10 ; extra == 'all' + - qtpy>=2.4.2 ; extra == 'all' + - scipy>=1.14.1 ; extra == 'all' + - s3fs>=2024.10.0 ; extra == 'all' + - sqlalchemy>=2.0.36 ; extra == 'all' + - tables>=3.10.1 ; extra == 'all' + - tabulate>=0.9.0 ; extra == 'all' + - xarray>=2024.10.0 ; extra == 'all' + - xlrd>=2.0.1 ; extra == 'all' + - xlsxwriter>=3.2.0 ; extra == 'all' + - zstandard>=0.23.0 ; extra == 'all' requires_python: '>=3.11' -- pypi: https://files.pythonhosted.org/packages/1a/1f/86b4d15221096cb5500bcd73bf350745749e3ba056cdd7a7f75f126f154e/scipp-26.3.1-cp312-cp312-win_amd64.whl - name: scipp - version: 26.3.1 - sha256: 8b036876edf7895d17644f59711037d2d7d9ad048b1a503200646d8229fb1ad7 +- pypi: https://files.pythonhosted.org/packages/66/3e/868e5c3364b6cee19ff3e1a122194fa4ce51def02c61023970442162859e/yarl-1.23.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + name: yarl + version: 1.23.0 + sha256: a3d2bff8f37f8d0f96c7ec554d16945050d54462d6e95414babaa18bfafc7f51 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' + - idna>=2.0 + - multidict>=4.0 + - propcache>=0.2.1 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/68/10/bf2d6738d72748b961a3751ab89522d58c54efc36a8e1a12161216cd45cf/pandas-3.0.3-cp314-cp314-macosx_11_0_arm64.whl + name: pandas + version: 3.0.3 + sha256: bab900348131a7db1f69a7309ef141fd5680f1487094193bcbbb61791573bf8f + requires_dist: + - numpy>=1.26.0 ; python_full_version < '3.14' + - numpy>=2.3.3 ; python_full_version >= '3.14' + - python-dateutil>=2.8.2 + - tzdata ; sys_platform == 'win32' + - tzdata ; sys_platform == 'emscripten' + - hypothesis>=6.116.0 ; extra == 'test' + - pytest>=8.3.4 ; extra == 'test' + - pytest-xdist>=3.6.1 ; extra == 'test' + - pyarrow>=13.0.0 ; extra == 'pyarrow' + - bottleneck>=1.4.2 ; extra == 'performance' + - numba>=0.60.0 ; extra == 'performance' + - numexpr>=2.10.2 ; extra == 'performance' + - scipy>=1.14.1 ; extra == 'computation' + - xarray>=2024.10.0 ; extra == 'computation' + - fsspec>=2024.10.0 ; extra == 'fss' + - s3fs>=2024.10.0 ; extra == 'aws' + - gcsfs>=2024.10.0 ; extra == 'gcp' + - odfpy>=1.4.1 ; extra == 'excel' + - openpyxl>=3.1.5 ; extra == 'excel' + - python-calamine>=0.3.0 ; extra == 'excel' + - pyxlsb>=1.0.10 ; extra == 'excel' + - xlrd>=2.0.1 ; extra == 'excel' + - xlsxwriter>=3.2.0 ; extra == 'excel' + - pyarrow>=13.0.0 ; extra == 'parquet' + - pyarrow>=13.0.0 ; extra == 'feather' + - pyiceberg>=0.8.1 ; extra == 'iceberg' + - tables>=3.10.1 ; extra == 'hdf5' + - pyreadstat>=1.2.8 ; extra == 'spss' + - sqlalchemy>=2.0.36 ; extra == 'postgresql' + - psycopg2>=2.9.10 ; extra == 'postgresql' + - adbc-driver-postgresql>=1.2.0 ; extra == 'postgresql' + - sqlalchemy>=2.0.36 ; extra == 'mysql' + - pymysql>=1.1.1 ; extra == 'mysql' + - sqlalchemy>=2.0.36 ; extra == 'sql-other' + - adbc-driver-postgresql>=1.2.0 ; extra == 'sql-other' + - adbc-driver-sqlite>=1.2.0 ; extra == 'sql-other' + - beautifulsoup4>=4.12.3 ; extra == 'html' + - html5lib>=1.1 ; extra == 'html' + - lxml>=5.3.0 ; extra == 'html' + - lxml>=5.3.0 ; extra == 'xml' + - matplotlib>=3.9.3 ; extra == 'plot' + - jinja2>=3.1.5 ; extra == 'output-formatting' + - tabulate>=0.9.0 ; extra == 'output-formatting' + - pyqt5>=5.15.9 ; extra == 'clipboard' + - qtpy>=2.4.2 ; extra == 'clipboard' + - zstandard>=0.23.0 ; extra == 'compression' + - pytz>=2020.1 ; extra == 'timezone' + - adbc-driver-postgresql>=1.2.0 ; extra == 'all' + - adbc-driver-sqlite>=1.2.0 ; extra == 'all' + - beautifulsoup4>=4.12.3 ; extra == 'all' + - bottleneck>=1.4.2 ; extra == 'all' + - fastparquet>=2024.11.0 ; extra == 'all' + - fsspec>=2024.10.0 ; extra == 'all' + - gcsfs>=2024.10.0 ; extra == 'all' + - html5lib>=1.1 ; extra == 'all' + - hypothesis>=6.116.0 ; extra == 'all' + - jinja2>=3.1.5 ; extra == 'all' + - lxml>=5.3.0 ; extra == 'all' + - matplotlib>=3.9.3 ; extra == 'all' + - numba>=0.60.0 ; extra == 'all' + - numexpr>=2.10.2 ; extra == 'all' + - odfpy>=1.4.1 ; extra == 'all' + - openpyxl>=3.1.5 ; extra == 'all' + - psycopg2>=2.9.10 ; extra == 'all' + - pyarrow>=13.0.0 ; extra == 'all' + - pyiceberg>=0.8.1 ; extra == 'all' + - pymysql>=1.1.1 ; extra == 'all' + - pyqt5>=5.15.9 ; extra == 'all' + - pyreadstat>=1.2.8 ; extra == 'all' + - pytest>=8.3.4 ; extra == 'all' + - pytest-xdist>=3.6.1 ; extra == 'all' + - python-calamine>=0.3.0 ; extra == 'all' + - pytz>=2020.1 ; extra == 'all' + - pyxlsb>=1.0.10 ; extra == 'all' + - qtpy>=2.4.2 ; extra == 'all' + - scipy>=1.14.1 ; extra == 'all' + - s3fs>=2024.10.0 ; extra == 'all' + - sqlalchemy>=2.0.36 ; extra == 'all' + - tables>=3.10.1 ; extra == 'all' + - tabulate>=0.9.0 ; extra == 'all' + - xarray>=2024.10.0 ; extra == 'all' + - xlrd>=2.0.1 ; extra == 'all' + - xlsxwriter>=3.2.0 ; extra == 'all' + - zstandard>=0.23.0 ; extra == 'all' requires_python: '>=3.11' -- pypi: https://files.pythonhosted.org/packages/1e/e7/cd78635d0ece7e4d3393f2c1d2ebabf6ff4bd615da142891b1d42ad58abf/scipp-26.3.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - name: scipp - version: 26.3.1 - sha256: 7525c843f673ef5461d229095054a701aeb3233db29af137fdf4bbf0884ad9d4 +- pypi: https://files.pythonhosted.org/packages/69/d1/705e6c19b437a4105bf3b9ae7945fcfc3ad2abb73d14bae0a3f2d58b305b/arviz_base-1.1.0-py3-none-any.whl + name: arviz-base + version: 1.1.0 + sha256: 4f97016f697751038f45d144331a1830c921f0ebc2739d5df343120fba453e83 requires_dist: - numpy>=2 + - xarray>=2024.11.0 + - typing-extensions>=3.10 + - lazy-loader>=0.4 + - build ; extra == 'check' + - pre-commit ; extra == 'check' + - docstub==0.4 ; extra == 'check' + - mypy ; extra == 'check' + - pre-commit ; extra == 'ci' + - cloudpickle ; extra == 'ci' + - sphinx-book-theme ; extra == 'doc' + - myst-parser[linkify] ; extra == 'doc' + - myst-nb ; extra == 'doc' + - sphinx-copybutton ; extra == 'doc' + - numpydoc ; extra == 'doc' + - sphinx>=5 ; extra == 'doc' + - sphinx-design ; extra == 'doc' + - jupyter-sphinx ; extra == 'doc' + - h5netcdf[h5py] ; extra == 'doc' + - h5netcdf[h5py] ; extra == 'h5netcdf' + - netcdf4 ; extra == 'netcdf4' + - xarray!=2025.8.0 ; extra == 'test' - pytest ; extra == 'test' - - 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/1f/28/3f8aa247d29d010547d52207395cb057ebd0a40b88f64bc1dbac9e17a729/scipp-26.3.1-cp314-cp314-win_amd64.whl - name: scipp - version: 26.3.1 - sha256: 26291c0a882b9d5aac868c6d6f2508b79baa821ed30060a22c50620dbcce9e75 + - pytest-cov ; extra == 'test' + - scipy ; extra == 'test' + - zarr ; extra == 'zarr' + requires_python: '>=3.12' +- pypi: https://files.pythonhosted.org/packages/69/ff/6699e7b71e60d3049eb2bdcbc95ee3f35707b2b0e48f32e9e63d3ce30c08/coverage-7.14.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl + name: coverage + version: 7.14.0 + sha256: ca3d9cf2c32b521bd9518385608787fa86f38daf993695307531822c3430ed67 requires_dist: - - numpy>=2 + - tomli ; python_full_version <= '3.11' and extra == 'toml' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/6a/b7/9366ed44ced9b7ef357ab48c94205280276db9d7f064aa3012a97227e966/h5py-3.16.0-cp314-cp314-macosx_11_0_arm64.whl + name: h5py + version: 3.16.0 + sha256: 8c1eff849cdd53cbc73c214c30ebdb6f1bb8b64790b4b4fc36acdb5e43570210 + requires_dist: + - numpy>=1.21.2 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl + name: frozenlist + version: 1.8.0 + sha256: 494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383 + requires_python: '>=3.9' +- 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/6e/94/be70f8ee9c45f2f62b39a1f0e9303bc20e138a8f3b8e50ffd89498e177e1/mkdocstrings-1.0.4-py3-none-any.whl + name: mkdocstrings + version: 1.0.4 + sha256: 63464b4b29053514f32a1dbbf604e52876d5e638111b0c295ab7ed3cac73ca9b + requires_dist: + - jinja2>=3.1 + - markdown>=3.6 + - markupsafe>=1.1 + - mkdocs>=1.6 + - mkdocs-autorefs>=1.4 + - pymdown-extensions>=6.3 + - mkdocstrings-crystal>=0.3.4 ; extra == 'crystal' + - mkdocstrings-python-legacy>=0.2.1 ; extra == 'python-legacy' + - mkdocstrings-python>=1.16.2 ; extra == 'python' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/6e/f1/abd09c2ae91228c5f3998dbd7f41353def9eac64253de3c8105efa2082f7/msgpack-1.1.2-cp314-cp314-win_amd64.whl + name: msgpack + version: 1.1.2 + sha256: 9ade919fac6a3e7260b7f64cea89df6bec59104987cbea34d34a2fa15d74310b + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/6f/0c/8297c8d978c919ad6318011631a6123082d5da940da5f8612e75a247d739/diffpy_pdffit2-1.6.0-cp312-cp312-win_amd64.whl + name: diffpy-pdffit2 + version: 1.6.0 + sha256: 6c7865218f78effeeb8374fb62a5aef2b084264da96e77c03160aa411d33c2a0 + requires_dist: + - diffpy-structure + requires_python: '>=3.12,<3.15' +- 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/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' - - 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/43/fe/ad0ecbe2393cb690a4b3100a8fea47ecfdb49f6e06f40cf2f626635adc0c/scipp-26.3.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - name: scipp - version: 26.3.1 - sha256: 2ef08ba8d83542807f9f9833ba8f01583215c1629693bfadb1d6508cbdeb335c + - pytest-cov ; extra == 'test' +- pypi: https://files.pythonhosted.org/packages/76/8e/56ccb09c7232a55403a7637caa21922f3b65901a37f5e8bdb405d0de0946/mike-2.2.0-py3-none-any.whl + name: mike + version: 2.2.0 + sha256: e1f4981c1152eec7c2490a3401142292cc47d686194188416db2648fdfe1d040 + requires_dist: + - jinja2>=2.7 + - mkdocs~=1.0 + - pyparsing>=3.0 + - pyyaml>=5.1 + - pyyaml-env-tag + - verspec + - importlib-metadata ; python_full_version < '3.10' + - importlib-resources ; python_full_version < '3.10' + - coverage ; extra == 'dev' + - flake8-quotes ; extra == 'dev' + - flake8>=3.0 ; extra == 'dev' + - shtab ; extra == 'dev' + - coverage ; extra == 'test' + - flake8-quotes ; extra == 'test' + - flake8>=3.0 ; extra == 'test' + - shtab ; extra == 'test' +- pypi: https://files.pythonhosted.org/packages/77/c7/2342da9830e3e9d4870305ca5d2091d2a83284f2953079b7bdd3b5e029d8/fonttools-4.63.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + name: fonttools + version: 4.63.0 + sha256: 58dc6bb86a78d782f00f9190ca02c119cf5bbe2807536e361e18d42019f877d8 + requires_dist: + - lxml>=4.0 ; extra == 'lxml' + - brotli>=1.0.1 ; platform_python_implementation == 'CPython' and extra == 'woff' + - brotlicffi>=0.8.0 ; platform_python_implementation != 'CPython' and extra == 'woff' + - zopfli>=0.1.4 ; extra == 'woff' + - unicodedata2>=17.0.0 ; python_full_version < '3.15' and extra == 'unicode' + - lz4>=1.7.4.2 ; extra == 'graphite' + - scipy ; platform_python_implementation != 'PyPy' and extra == 'interpolatable' + - munkres ; platform_python_implementation == 'PyPy' and extra == 'interpolatable' + - pycairo ; extra == 'interpolatable' + - matplotlib ; extra == 'plot' + - sympy ; extra == 'symfont' + - xattr ; sys_platform == 'darwin' and extra == 'type1' + - skia-pathops>=0.5.0 ; extra == 'pathops' + - uharfbuzz>=0.45.0 ; extra == 'repacker' + - lxml>=4.0 ; extra == 'all' + - brotli>=1.0.1 ; platform_python_implementation == 'CPython' and extra == 'all' + - brotlicffi>=0.8.0 ; platform_python_implementation != 'CPython' and extra == 'all' + - zopfli>=0.1.4 ; extra == 'all' + - unicodedata2>=17.0.0 ; python_full_version < '3.15' and extra == 'all' + - lz4>=1.7.4.2 ; extra == 'all' + - scipy ; platform_python_implementation != 'PyPy' and extra == 'all' + - munkres ; platform_python_implementation == 'PyPy' and extra == 'all' + - pycairo ; extra == 'all' + - matplotlib ; extra == 'all' + - sympy ; extra == 'all' + - xattr ; sys_platform == 'darwin' and extra == 'all' + - skia-pathops>=0.5.0 ; extra == 'all' + - uharfbuzz>=0.45.0 ; extra == 'all' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/78/3c/2a612b95ddbb9a6bdcb47b7a93c4884f74c6ff22356b2f7b213b16e65c35/pycifstar-0.3.0.tar.gz + name: pycifstar + version: 0.3.0 + sha256: 5892fdf16c83372ee5f32557127d5f36e14b0bbe520883a4e2e70365382f70ed +- 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/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/7b/67/b1944235474aac3f0b0e1b232ce49547f9f9461ca4b943df1b88da5d3f1d/bumps-1.0.4-py3-none-any.whl + name: bumps + version: 1.0.4 + sha256: 78b8cfaf9fbcbf2fd77f6d4a2f8c906b0e03a794804ba6caf64d56d6f6cce4d4 + requires_dist: + - numpy + - scipy + - h5py + - dill + - cloudpickle + - matplotlib + - blinker + - aiohttp + - python-socketio + - plotly + - mpld3 + - msgpack + - uncertainties + - build ; extra == 'dev' + - pre-commit ; extra == 'dev' + - pytest ; extra == 'dev' + - pytest-cov ; extra == 'dev' + - ruff ; extra == 'dev' + - wheel ; extra == 'dev' + - setuptools ; extra == 'dev' + - sphinx ; extra == 'dev' + - versioningit ; extra == 'dev' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/7b/e3/d81b065a2d866a33a541ac63a2a4cc5737e03ce2379ac3191c98bb8867e3/trove_classifiers-2026.5.7.17-py3-none-any.whl + name: trove-classifiers + version: 2026.5.7.17 + sha256: 5ec0800de5e2ddbd7c663cb4c0c15328f132dc168813897c18866c5c7b93db33 +- pypi: https://files.pythonhosted.org/packages/7d/c2/57f54b03d0f22d4044b8afb9ca0e184f8b1afd57b4f735c2fa70883dc601/contourpy-1.3.3-cp314-cp314-win_amd64.whl + name: contourpy + version: 1.3.3 + sha256: cf9022ef053f2694e31d630feaacb21ea24224be1c3ad0520b13d844274614fd requires_dist: - - numpy>=2 - - pytest ; extra == 'test' + - numpy>=1.25 + - furo ; extra == 'docs' + - sphinx>=7.2 ; extra == 'docs' + - sphinx-copybutton ; extra == 'docs' + - bokeh ; extra == 'bokeh' + - selenium ; extra == 'bokeh' + - contourpy[bokeh,docs] ; extra == 'mypy' + - bokeh ; extra == 'mypy' + - docutils-stubs ; extra == 'mypy' + - mypy==1.17.0 ; extra == 'mypy' + - types-pillow ; extra == 'mypy' + - contourpy[test-no-images] ; extra == 'test' - matplotlib ; extra == 'test' - - 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' + - pillow ; extra == 'test' + - pytest ; extra == 'test-no-images' + - pytest-cov ; extra == 'test-no-images' + - pytest-rerunfailures ; extra == 'test-no-images' + - pytest-xdist ; extra == 'test-no-images' + - wurlitzer ; extra == 'test-no-images' requires_python: '>=3.11' -- pypi: https://files.pythonhosted.org/packages/44/7b/537a61906eac58d94131273084d21d4eb219f5453f0ed30de3aca580a2b4/scipp-26.3.1-cp312-cp312-macosx_14_0_arm64.whl - name: scipp - version: 26.3.1 - sha256: 2608ba21e2c550abe864598e8cfffe22d7e7be70ff9f9b03d44868e353b241c9 +- pypi: https://files.pythonhosted.org/packages/7e/85/545a951eecc270fcd688288c600017e2050a1aacb56c711d208586d3e470/pymdown_extensions-10.21.3-py3-none-any.whl + name: pymdown-extensions + version: 10.21.3 + sha256: d7a5d08014fc571e80ca21dd6f854e31f94c489800350564d55d15b3c41e76b6 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/82/9b/cf7e6f157c53d2c8bc0165580350c53939aea3b2a8390ec8e07f0adb82de/scippneutron-26.4.1-py3-none-any.whl - name: scippneutron - version: 26.4.1 - sha256: 3b9865ebdb7923eb25739b7cda624555d45275341000f099c1dcf371b1dd7e35 + - markdown>=3.6 + - pyyaml + - pygments>=2.19.1 ; extra == 'extra' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/80/31/0b2517913687895f5904325c2069d6a3b78f66cc641a86a2baf75a05dcbb/multidict-6.7.1-cp312-cp312-win_amd64.whl + name: multidict + version: 6.7.1 + sha256: fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19 requires_dist: - - python-dateutil>=2.8 - - email-validator>=2 - - h5py>=3.12 - - lazy-loader>=0.4 - - mpltoolbox>=24.6.0 - - numpy>=1.20 - - plopp>=26.4.1 - - pydantic>=2 - - scipp>=24.7.0 - - scippnexus>=23.11.0 - - scipy>=1.7.0 - - scipp[all]>=23.7.0 ; extra == 'all' - - pooch>=1.5 ; extra == 'all' - - hypothesis>=6.100 ; extra == 'test' - - ipympl>0.9.0 ; extra == 'test' - - ipykernel>6.30 ; extra == 'test' - - pace-neutrons>=0.3 ; extra == 'test' - - pooch>=1.5 ; extra == 'test' - - psutil>=5.0 ; extra == 'test' - - pytest>=7.0 ; extra == 'test' - - pytest-xdist>=3.0 ; extra == 'test' - - pythreejs>=2.4.1 ; extra == 'test' - - sciline>=25.1.0 ; extra == 'test' - requires_python: '>=3.11' -- pypi: https://files.pythonhosted.org/packages/5c/01/6cb4d63c6b6933be4b7945b2f64638336420f04ea71ca5b9a7539c008bc5/scippnexus-26.1.1-py3-none-any.whl - name: scippnexus - version: 26.1.1 - sha256: 899a0a5e71291b7809d902c17b6c74addf5a805397eabcec557491ff74eead12 + - typing-extensions>=4.1.0 ; python_full_version < '3.11' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl + name: pre-commit + version: 4.6.0 + sha256: e2cf246f7299edcabcf15f9b0571fdce06058527f0a06535068a86d38089f29b requires_dist: - - 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/01/8e/1e35281b8ab6d5d72ebe9911edcdffa3f36b04ed9d51dec6dd140396e220/scipy-1.17.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - name: scipy - version: 1.17.1 - sha256: 02ae3b274fde71c5e92ac4d54bc06c42d80e399fec704383dcd99b301df37458 + - cfgv>=2.0.0 + - identify>=1.0.0 + - nodeenv>=0.11.1 + - pyyaml>=5.1 + - virtualenv>=20.10.0 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl + name: filelock + version: 3.29.0 + sha256: 96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl + name: rich + version: 15.0.0 + sha256: 33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb requires_dist: - - numpy>=1.26.4,<2.7 - - pytest>=8.0.0 ; extra == 'test' - - pytest-cov ; extra == 'test' - - pytest-timeout ; extra == 'test' - - pytest-xdist ; extra == 'test' - - asv ; extra == 'test' - - mpmath ; extra == 'test' - - gmpy2 ; extra == 'test' - - threadpoolctl ; extra == 'test' - - scikit-umfpack ; extra == 'test' - - pooch ; extra == 'test' - - hypothesis>=6.30 ; extra == 'test' - - array-api-strict>=2.3.1 ; extra == 'test' - - cython ; extra == 'test' - - meson ; extra == 'test' - - ninja ; sys_platform != 'emscripten' and extra == 'test' - - sphinx>=5.0.0,<8.2.0 ; extra == 'doc' - - intersphinx-registry ; extra == 'doc' - - pydata-sphinx-theme>=0.15.2 ; extra == 'doc' - - sphinx-copybutton ; extra == 'doc' - - sphinx-design>=0.4.0 ; extra == 'doc' - - matplotlib>=3.5 ; extra == 'doc' - - numpydoc ; extra == 'doc' - - jupytext ; extra == 'doc' - - myst-nb>=1.2.0 ; extra == 'doc' - - pooch ; extra == 'doc' - - jupyterlite-sphinx>=0.19.1 ; extra == 'doc' - - jupyterlite-pyodide-kernel ; extra == 'doc' - - linkify-it-py ; extra == 'doc' - - tabulate ; extra == 'doc' - - click<8.3.0 ; extra == 'dev' - - spin ; extra == 'dev' - - mypy==1.10.0 ; extra == 'dev' - - typing-extensions ; extra == 'dev' - - types-psutil ; extra == 'dev' - - pycodestyle ; extra == 'dev' - - ruff>=0.12.0 ; extra == 'dev' - - cython-lint>=0.12.2 ; extra == 'dev' + - ipywidgets>=7.5.1,<9 ; extra == 'jupyter' + - markdown-it-py>=2.2.0 + - pygments>=2.13.0,<3.0.0 + requires_python: '>=3.9.0' +- pypi: https://files.pythonhosted.org/packages/82/d0/26c81ffbe588f936d05f395da34046c66322e8067c9fd331c788c4f682f2/diffpy_pdffit2-1.6.0-cp314-cp314-win_amd64.whl + name: diffpy-pdffit2 + version: 1.6.0 + sha256: dc4b5c57c5bcdac4983ff3ec33a960b0f45b3d8d0e20f44347f533861b890c2a + requires_dist: + - diffpy-structure + requires_python: '>=3.12,<3.15' +- pypi: https://files.pythonhosted.org/packages/85/19/93853133df2cb371083285ef6a93982a0173e7a233b0f61373ba9fd30eb2/coverage-7.14.0-cp312-cp312-win_amd64.whl + name: coverage + version: 7.14.0 + sha256: 70390b0da32cb90b501953716302906e8bcce087cb283e70d8c97729f22e92b2 + requires_dist: + - tomli ; python_full_version <= '3.11' and extra == 'toml' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/85/d7/9b6ac05350ab7f7d3a730ff143ff3e2cada54514117c37be37e26dc91242/docstripy-0.7.2-py3-none-any.whl + name: docstripy + version: 0.7.2 + sha256: c4ba35de6c1b1c51f7afad4a46d8953aad55dce1a490d198f7e98c8c63efefda + requires_dist: + - nbformat + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/87/36/cccb9bc2a6ab63d1b2980374f0dca72ce95ae267c9b4cfe77455bb70d0d4/fonttools-4.63.0-cp312-cp312-win_amd64.whl + name: fonttools + version: 4.63.0 + sha256: 59ac449f8cca9b4ffa08d2e7bbadad87ce710d69d1eda5c3c1ce579baa987272 + requires_dist: + - lxml>=4.0 ; extra == 'lxml' + - brotli>=1.0.1 ; platform_python_implementation == 'CPython' and extra == 'woff' + - brotlicffi>=0.8.0 ; platform_python_implementation != 'CPython' and extra == 'woff' + - zopfli>=0.1.4 ; extra == 'woff' + - unicodedata2>=17.0.0 ; python_full_version < '3.15' and extra == 'unicode' + - lz4>=1.7.4.2 ; extra == 'graphite' + - scipy ; platform_python_implementation != 'PyPy' and extra == 'interpolatable' + - munkres ; platform_python_implementation == 'PyPy' and extra == 'interpolatable' + - pycairo ; extra == 'interpolatable' + - matplotlib ; extra == 'plot' + - sympy ; extra == 'symfont' + - xattr ; sys_platform == 'darwin' and extra == 'type1' + - skia-pathops>=0.5.0 ; extra == 'pathops' + - uharfbuzz>=0.45.0 ; extra == 'repacker' + - lxml>=4.0 ; extra == 'all' + - brotli>=1.0.1 ; platform_python_implementation == 'CPython' and extra == 'all' + - brotlicffi>=0.8.0 ; platform_python_implementation != 'CPython' and extra == 'all' + - zopfli>=0.1.4 ; extra == 'all' + - unicodedata2>=17.0.0 ; python_full_version < '3.15' and extra == 'all' + - lz4>=1.7.4.2 ; extra == 'all' + - scipy ; platform_python_implementation != 'PyPy' and extra == 'all' + - munkres ; platform_python_implementation == 'PyPy' and extra == 'all' + - pycairo ; extra == 'all' + - matplotlib ; extra == 'all' + - sympy ; extra == 'all' + - xattr ; sys_platform == 'darwin' and extra == 'all' + - skia-pathops>=0.5.0 ; extra == 'all' + - uharfbuzz>=0.45.0 ; extra == 'all' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl + name: mkdocs-get-deps + version: 0.2.2 + sha256: e7878cbeac04860b8b5e0ca31d3abad3df9411a75a32cde82f8e44b6c16ff650 + requires_dist: + - importlib-metadata>=4.3 ; python_full_version < '3.10' + - mergedeep>=1.3.4 + - platformdirs>=2.2.0 + - pyyaml>=5.1 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl + name: cloudpickle + version: 3.1.2 + sha256: 9acb47f6afd73f60dc1df93bb801b472f05ff42fa6c84167d25cb206be1fbf4a + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl + name: nodeenv + version: 1.10.0 + sha256: 5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827 + requires_python: '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*' +- pypi: https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + name: pydantic-core + version: 2.46.4 + sha256: 7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b + requires_dist: + - typing-extensions>=4.14.1 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/8a/06/2c1bd1ee9eee3e65b0b6395dcf960e71b11576995eae3b4ab9ec63d89bea/essreduce-26.4.1-py3-none-any.whl + name: essreduce + version: 26.4.1 + sha256: 1758a18fffca9c7c2a6fa9547cf87bf45f9d52fc3ccbdffcf7524f71bc060424 + requires_dist: + - sciline>=25.11.0 + - scipp>=26.3.1 + - scippneutron>=25.11.1 + - scippnexus>=25.6.0 + - graphviz>=0.20 ; extra == 'test' + - ipywidgets>=8.1 ; extra == 'test' + - matplotlib>=3.10.7 ; extra == 'test' + - numba>=0.63 ; extra == 'test' + - pooch>=1.9.0 ; extra == 'test' + - pytest>=7.0 ; extra == 'test' + - scipy>=1.14 ; extra == 'test' + - tof>=25.12.0 ; extra == 'test' + - autodoc-pydantic ; extra == 'docs' + - graphviz>=0.20 ; extra == 'docs' + - ipykernel ; extra == 'docs' + - ipython!=8.7.0 ; extra == 'docs' + - ipywidgets>=8.1 ; extra == 'docs' + - myst-parser ; extra == 'docs' + - nbsphinx ; extra == 'docs' + - numba>=0.63 ; extra == 'docs' + - plopp ; extra == 'docs' + - pydata-sphinx-theme>=0.14 ; extra == 'docs' + - sphinx>=7 ; extra == 'docs' + - sphinx-autodoc-typehints ; extra == 'docs' + - sphinx-copybutton ; extra == 'docs' + - sphinx-design ; extra == 'docs' + - tof>=25.12.0 ; extra == 'docs' requires_python: '>=3.11' -- pypi: https://files.pythonhosted.org/packages/0b/2e/7eea398450457ecb54e18e9d10110993fa65561c4f3add5e8eccd2b9cd41/scipy-1.17.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - name: scipy - version: 1.17.1 - sha256: eee2cfda04c00a857206a4330f0c5e3e56535494e30ca445eb19ec624ae75118 +- 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: - - numpy>=1.26.4,<2.7 - - pytest>=8.0.0 ; extra == 'test' - - pytest-cov ; extra == 'test' - - pytest-timeout ; extra == 'test' - - pytest-xdist ; extra == 'test' - - asv ; extra == 'test' - - mpmath ; extra == 'test' - - gmpy2 ; extra == 'test' - - threadpoolctl ; extra == 'test' - - scikit-umfpack ; extra == 'test' - - pooch ; extra == 'test' - - hypothesis>=6.30 ; extra == 'test' - - array-api-strict>=2.3.1 ; extra == 'test' - - cython ; extra == 'test' - - meson ; extra == 'test' - - ninja ; sys_platform != 'emscripten' and extra == 'test' - - sphinx>=5.0.0,<8.2.0 ; extra == 'doc' - - intersphinx-registry ; extra == 'doc' - - pydata-sphinx-theme>=0.15.2 ; extra == 'doc' - - sphinx-copybutton ; extra == 'doc' - - sphinx-design>=0.4.0 ; extra == 'doc' - - matplotlib>=3.5 ; extra == 'doc' - - numpydoc ; extra == 'doc' - - jupytext ; extra == 'doc' - - myst-nb>=1.2.0 ; extra == 'doc' - - pooch ; extra == 'doc' - - jupyterlite-sphinx>=0.19.1 ; extra == 'doc' - - jupyterlite-pyodide-kernel ; extra == 'doc' - - linkify-it-py ; extra == 'doc' - - tabulate ; extra == 'doc' - - click<8.3.0 ; extra == 'dev' - - spin ; extra == 'dev' - - mypy==1.10.0 ; extra == 'dev' - - typing-extensions ; extra == 'dev' - - types-psutil ; extra == 'dev' - - pycodestyle ; extra == 'dev' - - ruff>=0.12.0 ; extra == 'dev' - - cython-lint>=0.12.2 ; extra == 'dev' - requires_python: '>=3.11' -- pypi: https://files.pythonhosted.org/packages/4b/39/f0e8ea762a764a9dc52aa7dabcfad51a354819de1f0d4652b6a1122424d6/scipy-1.17.1-cp314-cp314-win_amd64.whl - name: scipy - version: 1.17.1 - sha256: 3877ac408e14da24a6196de0ddcace62092bfc12a83823e92e49e40747e52c19 + - packaging + - pytest>=8.0 ; extra == 'test' + - pytest-cov>=5.0 ; extra == 'test' + - coverage[toml]>=7.2 ; extra == 'test' + - pre-commit==4.3.0 ; extra == 'lint' + - changelist==0.5 ; extra == 'dev' + - spin==0.15 ; extra == 'dev' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/8c/ec/d431eb7941fb55a31dd6ca3404d41fbb52d99172df2e7707754488390910/msgpack-1.1.2-cp312-cp312-win_amd64.whl + name: msgpack + version: 1.1.2 + sha256: 1de460f0403172cff81169a30b9a92b260cb809c4cb7e2fc79ae8d0510c78b6b + requires_python: '>=3.9' +- 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: - - numpy>=1.26.4,<2.7 - - pytest>=8.0.0 ; extra == 'test' + - traitlets>=4.2.2 + - numpy ; extra == 'test' + - pandas ; extra == 'test' + - xarray ; extra == 'test' + - pytest ; extra == 'test' +- pypi: https://files.pythonhosted.org/packages/8f/5e/f1e1dd319e35e962a4e00b33150a8868b6329cc1d19fd533436ba5488f09/uncertainties-3.2.3-py3-none-any.whl + name: uncertainties + version: 3.2.3 + sha256: 313353900d8f88b283c9bad81e7d2b2d3d4bcc330cbace35403faaed7e78890a + requires_dist: + - numpy ; extra == 'arrays' + - pytest ; extra == 'test' + - pytest-codspeed ; extra == 'test' - pytest-cov ; extra == 'test' - - pytest-timeout ; extra == 'test' - - pytest-xdist ; extra == 'test' - - asv ; extra == 'test' - - mpmath ; extra == 'test' - - gmpy2 ; extra == 'test' - - threadpoolctl ; extra == 'test' - - scikit-umfpack ; extra == 'test' - - pooch ; extra == 'test' - - hypothesis>=6.30 ; extra == 'test' - - array-api-strict>=2.3.1 ; extra == 'test' - - cython ; extra == 'test' - - meson ; extra == 'test' - - ninja ; sys_platform != 'emscripten' and extra == 'test' - - sphinx>=5.0.0,<8.2.0 ; extra == 'doc' - - intersphinx-registry ; extra == 'doc' - - pydata-sphinx-theme>=0.15.2 ; extra == 'doc' + - scipy ; extra == 'test' + - sphinx ; extra == 'doc' - sphinx-copybutton ; extra == 'doc' - - sphinx-design>=0.4.0 ; extra == 'doc' - - matplotlib>=3.5 ; extra == 'doc' - - numpydoc ; extra == 'doc' - - jupytext ; extra == 'doc' - - myst-nb>=1.2.0 ; extra == 'doc' - - pooch ; extra == 'doc' - - jupyterlite-sphinx>=0.19.1 ; extra == 'doc' - - jupyterlite-pyodide-kernel ; extra == 'doc' - - linkify-it-py ; extra == 'doc' - - tabulate ; extra == 'doc' - - click<8.3.0 ; extra == 'dev' - - spin ; extra == 'dev' - - mypy==1.10.0 ; extra == 'dev' - - typing-extensions ; extra == 'dev' - - types-psutil ; extra == 'dev' - - pycodestyle ; extra == 'dev' - - ruff>=0.12.0 ; extra == 'dev' - - cython-lint>=0.12.2 ; extra == 'dev' - requires_python: '>=3.11' -- pypi: https://files.pythonhosted.org/packages/a2/84/dc08d77fbf3d87d3ee27f6a0c6dcce1de5829a64f2eae85a0ecc1f0daa73/scipy-1.17.1-cp312-cp312-win_amd64.whl - name: scipy - version: 1.17.1 - sha256: 41b71f4a3a4cab9d366cd9065b288efc4d4f3c0b37a91a8e0947fb5bd7f31d87 + - python-docs-theme ; extra == 'doc' + - uncertainties[arrays,doc,test] ; extra == 'all' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/8f/82/b54e646be7b938fcbdda10030c6533bd2bb1a59930a1381cc83d6050a49c/spglib-2.6.0-cp312-cp312-win_amd64.whl + name: spglib + version: 2.6.0 + sha256: 86d0fd355689e58becd2cda609b03c3a0d9ad9d6f761cefd08b970db6f314eae + 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/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl + name: paginate + version: 0.5.7 + sha256: b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591 + requires_dist: + - pytest ; extra == 'dev' + - tox ; extra == 'dev' + - black ; extra == 'lint' +- pypi: https://files.pythonhosted.org/packages/90/ad/cba91b3bcf04073e4d1655a5c1710ef3f457f56f7d1b79dcc3d72f4dd912/plotly-6.7.0-py3-none-any.whl + name: plotly + version: 6.7.0 + sha256: ac8aca1c25c663a59b5b9140a549264a5badde2e057d79b8c772ae2920e32ff0 + requires_dist: + - narwhals>=1.15.1 + - packaging + - anywidget ; extra == 'dev' + - build ; extra == 'dev' + - colorcet ; extra == 'dev' + - fiona<=1.9.6 ; python_full_version < '3.9' and extra == 'dev' + - geopandas ; extra == 'dev' + - inflect ; extra == 'dev' + - jupyterlab ; extra == 'dev' + - kaleido>=1.1.0 ; extra == 'dev' + - numpy>=1.22 ; extra == 'dev' + - orjson ; extra == 'dev' + - pandas ; extra == 'dev' + - pdfrw ; extra == 'dev' + - pillow ; extra == 'dev' + - plotly-geo ; extra == 'dev' + - polars[timezone] ; extra == 'dev' + - pyarrow ; extra == 'dev' + - pyshp ; extra == 'dev' + - pytest ; extra == 'dev' + - pytz ; extra == 'dev' + - requests ; extra == 'dev' + - ruff==0.11.12 ; extra == 'dev' + - scikit-image ; extra == 'dev' + - scipy ; extra == 'dev' + - shapely ; extra == 'dev' + - statsmodels ; extra == 'dev' + - vaex ; python_full_version < '3.10' and extra == 'dev' + - xarray ; extra == 'dev' + - build ; extra == 'dev-build' + - jupyterlab ; extra == 'dev-build' + - pytest ; extra == 'dev-build' + - requests ; extra == 'dev-build' + - ruff==0.11.12 ; extra == 'dev-build' + - pytest ; extra == 'dev-core' + - requests ; extra == 'dev-core' + - ruff==0.11.12 ; extra == 'dev-core' + - anywidget ; extra == 'dev-optional' + - build ; extra == 'dev-optional' + - colorcet ; extra == 'dev-optional' + - fiona<=1.9.6 ; python_full_version < '3.9' and extra == 'dev-optional' + - geopandas ; extra == 'dev-optional' + - inflect ; extra == 'dev-optional' + - jupyterlab ; extra == 'dev-optional' + - kaleido>=1.1.0 ; extra == 'dev-optional' + - numpy>=1.22 ; extra == 'dev-optional' + - orjson ; extra == 'dev-optional' + - pandas ; extra == 'dev-optional' + - pdfrw ; extra == 'dev-optional' + - pillow ; extra == 'dev-optional' + - plotly-geo ; extra == 'dev-optional' + - polars[timezone] ; extra == 'dev-optional' + - pyarrow ; extra == 'dev-optional' + - pyshp ; extra == 'dev-optional' + - pytest ; extra == 'dev-optional' + - pytz ; extra == 'dev-optional' + - requests ; extra == 'dev-optional' + - ruff==0.11.12 ; extra == 'dev-optional' + - scikit-image ; extra == 'dev-optional' + - scipy ; extra == 'dev-optional' + - shapely ; extra == 'dev-optional' + - statsmodels ; extra == 'dev-optional' + - vaex ; python_full_version < '3.10' and extra == 'dev-optional' + - xarray ; extra == 'dev-optional' + - numpy>=1.22 ; extra == 'express' + - kaleido>=1.1.0 ; extra == 'kaleido' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/91/20/6b165108058767ee643c55c5c4904d591a830ee2b3c7dbd359828fbc829f/greenlet-3.5.0-cp314-cp314-win_amd64.whl + name: greenlet + version: 3.5.0 + sha256: 3bc59be3945ae9750b9e7d45067d01ae3fe90ea5f9ade99239dabdd6e28a5033 requires_dist: - - numpy>=1.26.4,<2.7 - - pytest>=8.0.0 ; extra == 'test' + - sphinx ; extra == 'docs' + - furo ; extra == 'docs' + - objgraph ; extra == 'test' + - psutil ; extra == 'test' + - setuptools ; extra == 'test' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/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' - - pytest-timeout ; extra == 'test' - - pytest-xdist ; extra == 'test' - - asv ; extra == 'test' - - mpmath ; extra == 'test' - - gmpy2 ; extra == 'test' - - threadpoolctl ; extra == 'test' - - scikit-umfpack ; extra == 'test' - - pooch ; extra == 'test' - - hypothesis>=6.30 ; extra == 'test' - - array-api-strict>=2.3.1 ; extra == 'test' - - cython ; extra == 'test' - - meson ; extra == 'test' - - ninja ; sys_platform != 'emscripten' and extra == 'test' - - sphinx>=5.0.0,<8.2.0 ; extra == 'doc' - - intersphinx-registry ; extra == 'doc' - - pydata-sphinx-theme>=0.15.2 ; extra == 'doc' - - sphinx-copybutton ; extra == 'doc' - - sphinx-design>=0.4.0 ; extra == 'doc' - - matplotlib>=3.5 ; extra == 'doc' - - numpydoc ; extra == 'doc' - - jupytext ; extra == 'doc' - - myst-nb>=1.2.0 ; extra == 'doc' - - pooch ; extra == 'doc' - - jupyterlite-sphinx>=0.19.1 ; extra == 'doc' - - jupyterlite-pyodide-kernel ; extra == 'doc' - - linkify-it-py ; extra == 'doc' - - tabulate ; extra == 'doc' - - click<8.3.0 ; extra == 'dev' - - spin ; extra == 'dev' - - mypy==1.10.0 ; extra == 'dev' - - typing-extensions ; extra == 'dev' - - types-psutil ; extra == 'dev' - - pycodestyle ; extra == 'dev' - - ruff>=0.12.0 ; extra == 'dev' - - cython-lint>=0.12.2 ; extra == 'dev' - requires_python: '>=3.11' -- pypi: https://files.pythonhosted.org/packages/cf/a9/599c28631bad314d219cf9ffd40e985b24d603fc8a2f4ccc5ae8419a535b/scipy-1.17.1-cp312-cp312-macosx_14_0_arm64.whl - name: scipy - version: 1.17.1 - sha256: cc90d2e9c7e5c7f1a482c9875007c095c3194b1cfedca3c2f3291cdc2bc7c086 + - 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/93/f7/d00d9b4a0313a6be3a3e0818e6375e15da6d7076f4ae47d1324e7ca986a1/radon-6.0.1-py2.py3-none-any.whl + name: radon + version: 6.0.1 + sha256: 632cc032364a6f8bb1010a2f6a12d0f14bc7e5ede76585ef29dc0cecf4cd8859 requires_dist: - - numpy>=1.26.4,<2.7 - - pytest>=8.0.0 ; extra == 'test' + - mando>=0.6,<0.8 + - colorama==0.4.1 ; python_full_version < '3.5' + - colorama>=0.4.1 ; python_full_version >= '3.5' + - tomli>=2.0.1 ; extra == 'toml' +- pypi: https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl + name: identify + version: 2.6.19 + sha256: 20e6a87f786f768c092a721ad107fc9df0eb89347be9396cadf3f4abbd1fb78a + requires_dist: + - ukkonen ; extra == 'license' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/96/5d/0c59079aa7ef34980a5925a06a90ad2b7c94e486c194b3527d557cabb042/cryspy-0.11.0-py3-none-any.whl + name: cryspy + version: 0.11.0 + sha256: 0b650655a0fbdc3cfcb28826c2ab9fbc5f491e32e1ea9a47d9b75976cd43f26f + requires_dist: + - numpy + - scipy + - pycifstar + - matplotlib +- pypi: https://files.pythonhosted.org/packages/97/37/ce5c3ef2595dac2be35039f7b91a0691ef643aa3d954815b3b51e026e0ab/crysfml-0.6.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + name: crysfml + version: 0.6.2 + sha256: e18ff9ea04b0b823dbc1558afb974bd5b66f3ce13f9e18b25adedfcfde1a59a4 + requires_dist: + - numpy + requires_python: '>=3.11,<3.15' +- pypi: https://files.pythonhosted.org/packages/99/31/6cf181011dc738c33bf6ba7aea2e8e1d3c1f71b7dab1942f3054f66f6202/asteval-1.0.8-py3-none-any.whl + name: asteval + version: 1.0.8 + sha256: 6c64385c6ff859a474953c124987c7ee8354d781c76509b2c598741c4d1d28e9 + requires_dist: + - build ; extra == 'dev' + - twine ; extra == 'dev' + - sphinx ; extra == 'doc' + - pytest ; extra == 'test' - pytest-cov ; extra == 'test' - - pytest-timeout ; extra == 'test' - - pytest-xdist ; extra == 'test' - - asv ; extra == 'test' - - mpmath ; extra == 'test' - - gmpy2 ; extra == 'test' - - threadpoolctl ; extra == 'test' - - scikit-umfpack ; extra == 'test' - - pooch ; extra == 'test' - - hypothesis>=6.30 ; extra == 'test' - - array-api-strict>=2.3.1 ; extra == 'test' - - cython ; extra == 'test' - - meson ; extra == 'test' - - ninja ; sys_platform != 'emscripten' and extra == 'test' - - sphinx>=5.0.0,<8.2.0 ; extra == 'doc' + - coverage ; extra == 'test' + - asteval[dev,doc,test] ; extra == 'all' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/99/37/e8730c3587a65eb5645d4aba2d27aae48e8003614d6aaf15dda67f702f1f/bidict-0.23.1-py3-none-any.whl + name: bidict + version: 0.23.1 + sha256: 5dae8d4d79b552a71cbabc7deb25dfe8ce710b17ff41711e13010ead2abfc3e5 + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl + name: tabulate + version: 0.10.0 + sha256: f0b0622e567335c8fabaaa659f1b33bcb6ddfe2e496071b743aa113f8774f2d3 + requires_dist: + - wcwidth ; extra == 'widechars' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/99/9f/795fedf35634f746151ca8839d05681ceb6287fbed6cc1c9bf235f7887c2/kiwisolver-1.5.0-cp312-cp312-macosx_11_0_arm64.whl + name: kiwisolver + version: 1.5.0 + sha256: ed3a984b31da7481b103f68776f7128a89ef26ed40f4dc41a2223cda7fb24819 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/9b/91/cc8cc78a111826c54743d88651e1687008133c37e5ee615fee9b57990fac/aiohttp-3.13.5-cp314-cp314-macosx_11_0_arm64.whl + name: aiohttp + version: 3.13.5 + sha256: 756c3c304d394977519824449600adaf2be0ccee76d206ee339c5e76b70ded25 + requires_dist: + - aiohappyeyeballs>=2.5.0 + - aiosignal>=1.4.0 + - async-timeout>=4.0,<6.0 ; python_full_version < '3.11' + - attrs>=17.3.0 + - frozenlist>=1.1.1 + - multidict>=4.5,<7.0 + - propcache>=0.2.0 + - yarl>=1.17.0,<2.0 + - aiodns>=3.3.0 ; extra == 'speedups' + - brotli>=1.2 ; platform_python_implementation == 'CPython' and extra == 'speedups' + - brotlicffi>=1.2 ; platform_python_implementation != 'CPython' and extra == 'speedups' + - backports-zstd ; python_full_version < '3.14' and platform_python_implementation == 'CPython' and extra == 'speedups' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/9c/2f/4c5af01fd1a7506a1d5375403d68925eac70289229492db5aa68b58103d8/chardet-7.4.3-cp312-cp312-macosx_11_0_arm64.whl + name: chardet + version: 7.4.3 + sha256: 29af5999f654e8729d251f1724a62b538b1262d9292cccaefddf8a02aae1ef6a + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl + name: pytest-cov + version: 7.1.0 + sha256: a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678 + requires_dist: + - coverage[toml]>=7.10.6 + - pluggy>=1.2 + - pytest>=7 + - process-tests ; extra == 'testing' + - pytest-xdist ; extra == 'testing' + - virtualenv ; extra == 'testing' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/9e/43/53afb8ba17218f19b77c7834128566c5bbb100a0ad9ba2e8e89d089d7079/autopep8-2.3.2-py2.py3-none-any.whl + name: autopep8 + version: 2.3.2 + sha256: ce8ad498672c845a0c3de2629c15b635ec2b05ef8177a6e7c91c74f3e9b51128 + requires_dist: + - pycodestyle>=2.12.0 + - tomli ; python_full_version < '3.11' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl + name: networkx + version: 3.6.1 + sha256: d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762 + requires_dist: + - asv ; extra == 'benchmarking' + - virtualenv ; extra == 'benchmarking' + - numpy>=1.25 ; extra == 'default' + - scipy>=1.11.2 ; extra == 'default' + - matplotlib>=3.8 ; extra == 'default' + - pandas>=2.0 ; extra == 'default' + - pre-commit>=4.1 ; extra == 'developer' + - mypy>=1.15 ; extra == 'developer' + - sphinx>=8.0 ; extra == 'doc' + - pydata-sphinx-theme>=0.16 ; extra == 'doc' + - sphinx-gallery>=0.18 ; extra == 'doc' + - numpydoc>=1.8.0 ; extra == 'doc' + - pillow>=10 ; extra == 'doc' + - texext>=0.6.7 ; extra == 'doc' + - myst-nb>=1.1 ; extra == 'doc' - intersphinx-registry ; extra == 'doc' - - pydata-sphinx-theme>=0.15.2 ; extra == 'doc' - - sphinx-copybutton ; extra == 'doc' - - sphinx-design>=0.4.0 ; extra == 'doc' - - matplotlib>=3.5 ; extra == 'doc' - - numpydoc ; extra == 'doc' - - jupytext ; extra == 'doc' - - myst-nb>=1.2.0 ; extra == 'doc' - - pooch ; extra == 'doc' - - jupyterlite-sphinx>=0.19.1 ; extra == 'doc' - - jupyterlite-pyodide-kernel ; extra == 'doc' - - linkify-it-py ; extra == 'doc' - - tabulate ; extra == 'doc' - - click<8.3.0 ; extra == 'dev' - - spin ; extra == 'dev' - - mypy==1.10.0 ; extra == 'dev' - - typing-extensions ; extra == 'dev' - - types-psutil ; extra == 'dev' - - pycodestyle ; extra == 'dev' - - ruff>=0.12.0 ; extra == 'dev' - - cython-lint>=0.12.2 ; extra == 'dev' - requires_python: '>=3.11' -- pypi: https://files.pythonhosted.org/packages/db/7b/8624a203326675d7746a254083a187398090a179335b2e4a20e2ddc46e83/scipy-1.17.1-cp314-cp314-macosx_14_0_arm64.whl + - 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/9e/e9/1a19e42cd43cc1365e127db6aae85e1c671da1d9a5d746f4d34a50edb577/h5py-3.16.0-cp312-cp312-manylinux_2_28_x86_64.whl + name: h5py + version: 3.16.0 + sha256: dfc21898ff025f1e8e67e194965a95a8d4754f452f83454538f98f8a3fcb207e + requires_dist: + - numpy>=1.21.2 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl + name: gitdb + version: 4.0.12 + sha256: 67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf + requires_dist: + - smmap>=3.0.1,<6 + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl + name: frozenlist + version: 1.8.0 + sha256: 4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl + name: sympy + version: 1.14.0 + sha256: e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5 + requires_dist: + - mpmath>=1.1.0,<1.4 + - pytest>=7.1.0 ; extra == 'dev' + - hypothesis>=6.70.0 ; extra == 'dev' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/a2/84/dc08d77fbf3d87d3ee27f6a0c6dcce1de5829a64f2eae85a0ecc1f0daa73/scipy-1.17.1-cp312-cp312-win_amd64.whl name: scipy version: 1.17.1 - sha256: 3fd1fcdab3ea951b610dc4cef356d416d5802991e7e32b5254828d342f7b7e0b + sha256: 41b71f4a3a4cab9d366cd9065b288efc4d4f3c0b37a91a8e0947fb5bd7f31d87 requires_dist: - numpy>=1.26.4,<2.7 - pytest>=8.0.0 ; extra == 'test' @@ -10858,188 +11388,500 @@ packages: - ruff>=0.12.0 ; extra == 'dev' - cython-lint>=0.12.2 ; extra == 'dev' requires_python: '>=3.11' -- conda: https://conda.anaconda.org/conda-forge/noarch/send2trash-2.1.0-pyh5552912_1.conda - sha256: 8fc024bf1a7b99fc833b131ceef4bef8c235ad61ecb95a71a6108be2ccda63e8 - md5: b70e2d44e6aa2beb69ba64206a16e4c6 - depends: - - __osx - - pyobjc-framework-cocoa - - python >=3.10 - - python - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/send2trash?source=hash-mapping - size: 22519 - timestamp: 1770937603551 -- conda: https://conda.anaconda.org/conda-forge/noarch/send2trash-2.1.0-pyh6dadd2b_1.conda - sha256: 305446a0b018f285351300463653d3d3457687270e20eda37417b12ee386ef76 - md5: 6ac53f3fff2c416d63511843a04646fa - depends: - - __win - - pywin32 - - python >=3.10 - - python - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/send2trash?source=hash-mapping - size: 22864 - timestamp: 1770937641143 -- conda: https://conda.anaconda.org/conda-forge/noarch/send2trash-2.1.0-pyha191276_1.conda - sha256: 59656f6b2db07229351dfb3a859c35e57cc8e8bcbc86d4e501bff881a6f771f1 - md5: 28eb91468df04f655a57bcfbb35fc5c5 - depends: - - __linux - - python >=3.10 - - python - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/send2trash?source=hash-mapping - size: 24108 - timestamp: 1770937597662 -- conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-82.0.1-pyh332efcf_0.conda - sha256: 82088a6e4daa33329a30bc26dc19a98c7c1d3f05c0f73ce9845d4eab4924e9e1 - md5: 8e194e7b992f99a5015edbd4ebd38efd - depends: - - python >=3.10 - license: MIT - license_family: MIT - purls: - - pkg:pypi/setuptools?source=hash-mapping - size: 639697 - timestamp: 1773074868565 -- pypi: https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl - name: shellingham - version: 1.5.4 - sha256: 7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686 +- pypi: https://files.pythonhosted.org/packages/a3/36/4e551e8aa55c9188bca9abb5096805edbf7431072b76e2298e34fd3a3008/kiwisolver-1.5.0-cp314-cp314-win_amd64.whl + name: kiwisolver + version: 1.5.0 + sha256: d76e2d8c75051d58177e762164d2e9ab92886534e3a12e795f103524f221dd8e + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/a3/58/35da89ee790598a0700ea49b2a66594140f44dec458c07e8e3d4979137fc/ply-3.11-py2.py3-none-any.whl + name: ply + version: '3.11' + sha256: 096f9b8350b65ebd2fd1346b12452efe5b9607f7482813ffca50c22722a807ce +- pypi: https://files.pythonhosted.org/packages/a3/59/1bd6d7428d6ed9106efbb8c52310c60fd04f6672490f452aeaa3829aa436/greenlet-3.5.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl + name: greenlet + version: 3.5.0 + sha256: 8f52a464e4ed91780bdfbbdd2b97197f3accaa629b98c200f4dffada759f3ae7 + requires_dist: + - sphinx ; extra == 'docs' + - furo ; extra == 'docs' + - objgraph ; extra == 'test' + - psutil ; extra == 'test' + - setuptools ; extra == 'test' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/a4/39/3a0ae5b0edb66e61bb0e8bc53a503495cba5892297ae21faf6ba0525e681/varname-1.0.0-py3-none-any.whl + name: varname + version: 1.0.0 + sha256: 1125bfe981c3bbbe56988f5cb85fdcd7cad923b153283c2d464aea8b4c833d51 + requires_dist: + - executing>=2.1 + - typing-extensions>=4.13 ; python_full_version < '3.10' + - asttokens==3.* ; extra == 'all' + - pure-eval==0.* ; extra == 'all' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/a4/ce/3b6fee91c85626eaf769d617f1be9d2e15c1cca027bbdeb2e0d751469355/verspec-0.1.0-py3-none-any.whl + name: verspec + version: 0.1.0 + sha256: 741877d5633cc9464c45a469ae2a31e801e6dbbaa85b9675d481cda100f11c31 + requires_dist: + - coverage ; extra == 'test' + - flake8>=3.7 ; extra == 'test' + - mypy ; extra == 'test' + - pretend ; extra == 'test' + - pytest ; extra == 'test' +- pypi: https://files.pythonhosted.org/packages/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-py3-none-any.whl + name: wsproto + version: 1.3.2 + sha256: 61eea322cdf56e8cc904bd3ad7573359a242ba65688716b0710a5eb12beab584 + requires_dist: + - h11>=0.16.0,<1 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl + name: mdit-py-plugins + version: 0.6.1 + sha256: 214c82fb2ac524472ab6a5bcab1de80f73b50443e187f401bfd77efbc7c6481d + requires_dist: + - markdown-it-py>=2.0.0,<5.0.0 + - pre-commit ; extra == 'code-style' + - myst-parser ; extra == 'rtd' + - sphinx-book-theme ; extra == 'rtd' + - coverage ; extra == 'testing' + - pytest ; extra == 'testing' + - pytest-cov ; extra == 'testing' + - pytest-regressions ; extra == 'testing' + - pytest-timeout ; extra == 'testing' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl + name: frozenlist + version: 1.8.0 + sha256: cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/a8/4e/c09876f08fa9faaa5e1178f3d77b7af3f343258689bd6f3b72593b2f74e3/mkdocs_markdownextradata_plugin-0.2.6-py3-none-any.whl + name: mkdocs-markdownextradata-plugin + version: 0.2.6 + sha256: 34dd40870781784c75809596b2d8d879da783815b075336d541de1f150c94242 + requires_dist: + - mkdocs + - pyyaml + requires_python: '>=3.6' +- pypi: https://files.pythonhosted.org/packages/a9/5b/9b92f54c784c26e2a422e55a8d2607ab15b7ea3349e28359282f84f01d43/yarl-1.23.0-cp314-cp314-win_amd64.whl + name: yarl + version: 1.23.0 + sha256: 63e92247f383c85ab00dd0091e8c3fa331a96e865459f5ee80353c70a4a42d70 + requires_dist: + - idna>=2.0 + - multidict>=4.0 + - propcache>=0.2.1 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/a9/65/1caac9d4cd32e8433908683446eebc953e82d22b03d10d41a5f0fefe991b/multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl + name: multidict + version: 6.7.1 + sha256: b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7 + requires_dist: + - typing-extensions>=4.1.0 ; python_full_version < '3.11' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/aa/54/0cce26da03a981f949bb8449c9778537f75f5917c172e1d2992ff25cb57d/python_engineio-4.13.1-py3-none-any.whl + name: python-engineio + version: 4.13.1 + sha256: f32ad10589859c11053ad7d9bb3c9695cdf862113bfb0d20bc4d890198287399 + requires_dist: + - simple-websocket>=0.10.0 + - requests>=2.21.0 ; extra == 'client' + - websocket-client>=0.54.0 ; extra == 'client' + - aiohttp>=3.11 ; extra == 'asyncio-client' + - tox ; extra == 'dev' + - sphinx ; extra == 'docs' + - furo ; extra == 'docs' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/aa/92/d063df4d63d988b20d881856c74df76c0c1786229bb870f3a52af0981d4d/numpy-2.4.5-cp314-cp314-win_amd64.whl + name: numpy + version: 2.4.5 + sha256: 4bd2cd4ef9c0afa87de73723c0a33c0edff62143e1432917458e26d3d195d87f + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/aa/ca/eadf6f9c8fa5e31d40993e3db153fb5ed0b11008ad5d9de98a95045bed84/aiohttp-3.13.5-cp312-cp312-win_amd64.whl + name: aiohttp + version: 3.13.5 + sha256: 110e448e02c729bcebb18c60b9214a87ba33bac4a9fa5e9a5f139938b56c6cb1 + requires_dist: + - aiohappyeyeballs>=2.5.0 + - aiosignal>=1.4.0 + - async-timeout>=4.0,<6.0 ; python_full_version < '3.11' + - attrs>=17.3.0 + - frozenlist>=1.1.1 + - multidict>=4.5,<7.0 + - propcache>=0.2.0 + - yarl>=1.17.0,<2.0 + - aiodns>=3.3.0 ; extra == 'speedups' + - brotli>=1.2 ; platform_python_implementation == 'CPython' and extra == 'speedups' + - brotlicffi>=1.2 ; platform_python_implementation != 'CPython' and extra == 'speedups' + - backports-zstd ; python_full_version < '3.14' and platform_python_implementation == 'CPython' and extra == 'speedups' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/ab/1a/5a4f747a8b271cbb024946d2dd3c913ab5032ba430626f8c3528ada96b4b/matplotlib-3.10.9-cp314-cp314-win_amd64.whl + name: matplotlib + version: 3.10.9 + sha256: 336b9acc64d309063126edcdaca00db9373af3c476bb94388fe9c5a53ad13e6f + requires_dist: + - contourpy>=1.0.1 + - cycler>=0.10 + - fonttools>=4.22.0 + - kiwisolver>=1.3.1 + - numpy>=1.23 + - packaging>=20.0 + - pillow>=8 + - pyparsing>=3 + - python-dateutil>=2.7 + - meson-python>=0.13.1,<0.17.0 ; extra == 'dev' + - pybind11>=2.13.2,!=2.13.3 ; extra == 'dev' + - setuptools-scm>=7,<10 ; extra == 'dev' + - setuptools>=64 ; extra == 'dev' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl + name: execnet + version: 2.1.2 + sha256: 67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec + requires_dist: + - hatch ; extra == 'testing' + - pre-commit ; extra == 'testing' + - pytest ; extra == 'testing' + - tox ; extra == 'testing' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/ab/a6/870a3e8a50590bb92be184ad928c2922f088b00d9dc5c5ec7b924ee08c22/ruff-0.15.13-py3-none-win_amd64.whl + name: ruff + version: 0.15.13 + sha256: 7064884d442b7d477b4e7473d12da7f08851d2b1982763c5d3f388a19468a1a4 requires_python: '>=3.7' -- pypi: https://files.pythonhosted.org/packages/52/59/0782e51887ac6b07ffd1570e0364cf901ebc36345fea669969d2084baebb/simple_websocket-1.1.0-py3-none-any.whl - name: simple-websocket - version: 1.1.0 - sha256: 4af6069630a38ed6c561010f0e11a5bc0d4ca569b36306eb257cd9a192497c8c +- 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/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl + name: pydantic-core + version: 2.46.4 + sha256: 23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462 + requires_dist: + - typing-extensions>=4.14.1 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/ad/cf/0348374369ca588f8fe9c338fae49fa4e16eeb10ffb3d012f23a54578a9e/kiwisolver-1.5.0-cp312-cp312-win_amd64.whl + name: kiwisolver + version: 1.5.0 + sha256: f18c2d9782259a6dc132fdc7a63c168cbc74b35284b6d75c673958982a378384 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl + name: click + version: 8.3.3 + sha256: a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613 + requires_dist: + - colorama ; sys_platform == 'win32' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/ae/61/3c1ea8c10bf4f6bf83c33a7f5b4a3143f4cc1f979859dec5498b6cc31900/pycifrw-5.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl + name: pycifrw + version: 5.0.1 + sha256: 379801e71509d0f9c59b56edc5ceb6600796eaf2b84ee5e0f5a256c76542047d requires_dist: - - wsproto - - tox ; extra == 'dev' - - flake8 ; extra == 'dev' - - pytest ; extra == 'dev' - - pytest-cov ; extra == 'dev' - - sphinx ; extra == 'docs' - requires_python: '>=3.6' -- conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda - sha256: 458227f759d5e3fcec5d9b7acce54e10c9e1f4f4b7ec978f3bfd54ce4ee9853d - md5: 3339e3b65d58accf4ca4fb8748ab16b3 - depends: - - python >=3.9 - - python - license: MIT - license_family: MIT - purls: - - pkg:pypi/six?source=hash-mapping - size: 18455 - timestamp: 1753199211006 -- 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 + - prettytable + - ply + - numpy +- 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/b0/3e/a6497e1c2c9bc6ed2b79e0f2d31a4ce509fd2a9eed4e4f7ac63eda8113cb/gemmi-0.7.5-cp312-cp312-macosx_11_0_arm64.whl + name: gemmi + version: 0.7.5 + sha256: 5682920985109c6a08616ae9aae080f8b46a9714534dc864b535e3e6d203d5b8 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/b0/42/c84efcc1d4caebafb1ecd8be4643f39c85c47a80fe254d92b8b43b1eadaf/h5py-3.16.0-cp312-cp312-macosx_11_0_arm64.whl + name: h5py + version: 3.16.0 + sha256: 42b012933a83e1a558c673176676a10ce2fd3759976a0fedee1e672d1e04fc9d + requires_dist: + - numpy>=1.21.2 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/b1/2c/cad8b5e3623a987f3c930b68e2bdd06cfc388cd91cd42ed05f1227701b73/chardet-7.4.3-cp314-cp314-win_amd64.whl + name: chardet + version: 7.4.3 + sha256: b95c934b9ad59e2ba8abb9be49df70d3ad1b0d95d864b9fdb7588d4fa8bd921c + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl + name: mdurl + version: 0.1.2 + sha256: 84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 requires_python: '>=3.7' -- conda: https://conda.anaconda.org/conda-forge/noarch/sniffio-1.3.1-pyhd8ed1ab_2.conda - sha256: dce518f45e24cd03f401cb0616917773159a210c19d601c5f2d4e0e5879d30ad - md5: 03fe290994c5e4ec17293cfb6bdce520 - depends: - - python >=3.10 - license: Apache-2.0 - license_family: Apache - purls: - - pkg:pypi/sniffio?source=hash-mapping - size: 15698 - timestamp: 1762941572482 -- conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.8.3-pyhd8ed1ab_0.conda - sha256: 23b71ecf089967d2900126920e7f9ff18cdcef82dbff3e2f54ffa360243a17ac - md5: 18de09b20462742fe093ba39185d9bac - depends: - - python >=3.10 - license: MIT - license_family: MIT - purls: - - pkg:pypi/soupsieve?source=hash-mapping - size: 38187 - timestamp: 1769034509657 -- 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 +- pypi: https://files.pythonhosted.org/packages/b3/52/bc858b1665d0dec3a2511f4e6f5c18ea85c0977563d624d597c95d6d0fd7/jupyterquiz-2.9.6.4-py2.py3-none-any.whl + name: jupyterquiz + version: 2.9.6.4 + sha256: f8c4418f6c827454523fc882a30d744b585cb58ac1ae277769c3059d04fc272b +- pypi: https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl + name: markdown-it-py + version: 4.2.0 + sha256: 9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a requires_dist: - - 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' + - mdurl~=0.1 + - psutil ; extra == 'benchmarking' + - pytest ; extra == 'benchmarking' + - pytest-benchmark ; extra == 'benchmarking' + - commonmark~=0.9 ; extra == 'compare' + - markdown~=3.4 ; extra == 'compare' + - mistletoe~=1.0 ; extra == 'compare' + - mistune~=3.0 ; extra == 'compare' + - panflute~=2.3 ; extra == 'compare' + - markdown-it-pyrs ; extra == 'compare' + - linkify-it-py>=1,<3 ; extra == 'linkify' + - mdit-py-plugins>=0.5.0 ; extra == 'plugins' + - gprof2dot ; extra == 'profiling' + - mdit-py-plugins>=0.5.0 ; extra == 'rtd' + - myst-parser ; extra == 'rtd' + - pyyaml ; extra == 'rtd' + - sphinx ; extra == 'rtd' + - sphinx-copybutton ; extra == 'rtd' + - sphinx-design ; extra == 'rtd' + - sphinx-book-theme~=1.0 ; extra == 'rtd' + - jupyter-sphinx ; extra == 'rtd' + - ipykernel ; extra == 'rtd' + - coverage ; extra == 'testing' + - pytest ; extra == 'testing' + - pytest-cov ; extra == 'testing' + - pytest-regressions ; extra == 'testing' + - pytest-timeout ; extra == 'testing' + - requests ; extra == 'testing' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/b4/5e/bf11645aebb9af7d8d35927c40d3855816a0855c799e8156eeca8d632c90/diffpy_structure-3.4.0-py3-none-any.whl + name: diffpy-structure + version: 3.4.0 + sha256: bd0f06a96635d80316f51ebc0a08003bdeb2cb48c9bb18cbed1455ac60645e48 + requires_dist: + - numpy + - pycifrw + - diffpy-utils + requires_python: '>=3.12,<3.15' +- pypi: https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl + name: watchdog + version: 6.0.0 + sha256: 20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2 + requires_dist: + - pyyaml>=3.10 ; extra == 'watchmedo' requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/33/75/98a7eb100dc5cfd20b019046452f08d5e67dfbacc71d8f28763d32426fd3/spglib-2.6.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - name: spglib - version: 2.6.0 - sha256: a8e9c34da1e2428c3a8bd4e209e5356d12d454d8ac54120d5ba4a437d3abe7ba +- pypi: https://files.pythonhosted.org/packages/b7/18/4880dd762e40cd360c1bf06e890c5a97b997e91cb324602b1a19950ad5ce/matplotlib-3.10.9-cp312-cp312-macosx_11_0_arm64.whl + name: matplotlib + version: 3.10.9 + sha256: 41cb28c2bd769aa3e98322c6ab09854cbcc52ab69d2759d681bba3e327b2b320 + requires_dist: + - contourpy>=1.0.1 + - cycler>=0.10 + - fonttools>=4.22.0 + - kiwisolver>=1.3.1 + - numpy>=1.23 + - packaging>=20.0 + - pillow>=8 + - pyparsing>=3 + - python-dateutil>=2.7 + - meson-python>=0.13.1,<0.17.0 ; extra == 'dev' + - pybind11>=2.13.2,!=2.13.3 ; extra == 'dev' + - setuptools-scm>=7,<10 ; extra == 'dev' + - setuptools>=64 ; extra == 'dev' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/b7/6f/a05a317a66fee0aad270011461f1a63a453ed12471249f172f7d2e2bc7b4/python_discovery-1.3.1-py3-none-any.whl + name: python-discovery + version: 1.3.1 + sha256: ed188687ebb3b82c01a17cd5ac62fc94d9f6487a7f1a0f9dfe89753fec91039c + 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' + - sphinxcontrib-towncrier>=0.4 ; extra == 'docs' + - towncrier>=25.8 ; 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/b7/ee/e9c95cda829131f71a8dff5ce0406059fd16e591c074414e31ada19ba7c3/validate_pyproject-0.25-py3-none-any.whl + name: validate-pyproject + version: '0.25' + sha256: f9d05e2686beff82f9ea954f582306b036ced3d3feb258c1110f2c2a495b1981 + requires_dist: + - fastjsonschema>=2.16.2,<=3 + - packaging>=24.2 ; extra == 'all' + - trove-classifiers>=2021.10.20 ; extra == 'all' + - tomli>=1.2.1 ; python_full_version < '3.11' and extra == 'all' + - validate-pyproject-schema-store ; extra == 'store' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl + name: frozenlist + version: 1.8.0 + sha256: 34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/b9/c5/fc1b368f303087d20e8c9bf3d6ceb186263cfac0ade735cd938538bea839/pandas-3.0.3-cp312-cp312-win_amd64.whl + name: pandas + version: 3.0.3 + sha256: c7be265b62cef88e253a941e4698604973736dcfe242fdb5198f0f7bc473cdcc + requires_dist: + - numpy>=1.26.0 ; python_full_version < '3.14' + - numpy>=2.3.3 ; python_full_version >= '3.14' + - python-dateutil>=2.8.2 + - tzdata ; sys_platform == 'win32' + - tzdata ; sys_platform == 'emscripten' + - hypothesis>=6.116.0 ; extra == 'test' + - pytest>=8.3.4 ; extra == 'test' + - pytest-xdist>=3.6.1 ; extra == 'test' + - pyarrow>=13.0.0 ; extra == 'pyarrow' + - bottleneck>=1.4.2 ; extra == 'performance' + - numba>=0.60.0 ; extra == 'performance' + - numexpr>=2.10.2 ; extra == 'performance' + - scipy>=1.14.1 ; extra == 'computation' + - xarray>=2024.10.0 ; extra == 'computation' + - fsspec>=2024.10.0 ; extra == 'fss' + - s3fs>=2024.10.0 ; extra == 'aws' + - gcsfs>=2024.10.0 ; extra == 'gcp' + - odfpy>=1.4.1 ; extra == 'excel' + - openpyxl>=3.1.5 ; extra == 'excel' + - python-calamine>=0.3.0 ; extra == 'excel' + - pyxlsb>=1.0.10 ; extra == 'excel' + - xlrd>=2.0.1 ; extra == 'excel' + - xlsxwriter>=3.2.0 ; extra == 'excel' + - pyarrow>=13.0.0 ; extra == 'parquet' + - pyarrow>=13.0.0 ; extra == 'feather' + - pyiceberg>=0.8.1 ; extra == 'iceberg' + - tables>=3.10.1 ; extra == 'hdf5' + - pyreadstat>=1.2.8 ; extra == 'spss' + - sqlalchemy>=2.0.36 ; extra == 'postgresql' + - psycopg2>=2.9.10 ; extra == 'postgresql' + - adbc-driver-postgresql>=1.2.0 ; extra == 'postgresql' + - sqlalchemy>=2.0.36 ; extra == 'mysql' + - pymysql>=1.1.1 ; extra == 'mysql' + - sqlalchemy>=2.0.36 ; extra == 'sql-other' + - adbc-driver-postgresql>=1.2.0 ; extra == 'sql-other' + - adbc-driver-sqlite>=1.2.0 ; extra == 'sql-other' + - beautifulsoup4>=4.12.3 ; extra == 'html' + - html5lib>=1.1 ; extra == 'html' + - lxml>=5.3.0 ; extra == 'html' + - lxml>=5.3.0 ; extra == 'xml' + - matplotlib>=3.9.3 ; extra == 'plot' + - jinja2>=3.1.5 ; extra == 'output-formatting' + - tabulate>=0.9.0 ; extra == 'output-formatting' + - pyqt5>=5.15.9 ; extra == 'clipboard' + - qtpy>=2.4.2 ; extra == 'clipboard' + - zstandard>=0.23.0 ; extra == 'compression' + - pytz>=2020.1 ; extra == 'timezone' + - adbc-driver-postgresql>=1.2.0 ; extra == 'all' + - adbc-driver-sqlite>=1.2.0 ; extra == 'all' + - beautifulsoup4>=4.12.3 ; extra == 'all' + - bottleneck>=1.4.2 ; extra == 'all' + - fastparquet>=2024.11.0 ; extra == 'all' + - fsspec>=2024.10.0 ; extra == 'all' + - gcsfs>=2024.10.0 ; extra == 'all' + - html5lib>=1.1 ; extra == 'all' + - hypothesis>=6.116.0 ; extra == 'all' + - jinja2>=3.1.5 ; extra == 'all' + - lxml>=5.3.0 ; extra == 'all' + - matplotlib>=3.9.3 ; extra == 'all' + - numba>=0.60.0 ; extra == 'all' + - numexpr>=2.10.2 ; extra == 'all' + - odfpy>=1.4.1 ; extra == 'all' + - openpyxl>=3.1.5 ; extra == 'all' + - psycopg2>=2.9.10 ; extra == 'all' + - pyarrow>=13.0.0 ; extra == 'all' + - pyiceberg>=0.8.1 ; extra == 'all' + - pymysql>=1.1.1 ; extra == 'all' + - pyqt5>=5.15.9 ; extra == 'all' + - pyreadstat>=1.2.8 ; extra == 'all' + - pytest>=8.3.4 ; extra == 'all' + - pytest-xdist>=3.6.1 ; extra == 'all' + - python-calamine>=0.3.0 ; extra == 'all' + - pytz>=2020.1 ; extra == 'all' + - pyxlsb>=1.0.10 ; extra == 'all' + - qtpy>=2.4.2 ; extra == 'all' + - scipy>=1.14.1 ; extra == 'all' + - s3fs>=2024.10.0 ; extra == 'all' + - sqlalchemy>=2.0.36 ; extra == 'all' + - tables>=3.10.1 ; extra == 'all' + - tabulate>=0.9.0 ; extra == 'all' + - xarray>=2024.10.0 ; extra == 'all' + - xlrd>=2.0.1 ; extra == 'all' + - xlsxwriter>=3.2.0 ; extra == 'all' + - zstandard>=0.23.0 ; extra == 'all' + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl + name: dnspython + version: 2.8.0 + sha256: 01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af 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/8f/82/b54e646be7b938fcbdda10030c6533bd2bb1a59930a1381cc83d6050a49c/spglib-2.6.0-cp312-cp312-win_amd64.whl - name: spglib - version: 2.6.0 - sha256: 86d0fd355689e58becd2cda609b03c3a0d9ad9d6f761cefd08b970db6f314eae + - 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/ba/8c/1a9e46228571de18f8e28f16fabdfc20212a5d019f3e3303452b3f0a580d/pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.whl + name: pillow + version: 12.2.0 + sha256: 80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae 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' + - furo ; extra == 'docs' + - olefile ; extra == 'docs' + - sphinx>=8.2 ; extra == 'docs' + - sphinx-autobuild ; extra == 'docs' + - sphinx-copybutton ; extra == 'docs' + - sphinx-inline-tabs ; extra == 'docs' + - sphinxext-opengraph ; extra == 'docs' + - olefile ; extra == 'fpx' + - olefile ; extra == 'mic' + - arro3-compute ; extra == 'test-arrow' + - arro3-core ; extra == 'test-arrow' + - nanoarrow ; extra == 'test-arrow' + - pyarrow ; extra == 'test-arrow' + - check-manifest ; extra == 'tests' + - coverage>=7.4.2 ; extra == 'tests' + - defusedxml ; extra == 'tests' + - markdown2 ; extra == 'tests' + - olefile ; extra == 'tests' + - packaging ; extra == 'tests' + - pyroma>=5 ; extra == 'tests' + - pytest ; extra == 'tests' + - pytest-cov ; extra == 'tests' + - pytest-timeout ; extra == 'tests' + - pytest-xdist ; extra == 'tests' + - trove-classifiers>=2024.10.12 ; extra == 'tests' + - defusedxml ; extra == 'xmp' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl + name: pyproject-hooks + version: 1.2.0 + sha256: 9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913 + requires_python: '>=3.7' - pypi: https://files.pythonhosted.org/packages/bd/8c/d4907ad4f6bdc5bf79462d8767728713a7b316918a7444df372958a0e417/spglib-2.6.0-cp312-cp312-macosx_11_0_arm64.whl name: spglib version: 2.6.0 @@ -11066,6 +11908,24 @@ packages: - spglib[docs] ; extra == 'doc' - spglib[test] ; extra == 'testing' requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/bd/c9/989f4034fb46841208de7aeeac2c6d8300745ab4f28c42f629ba77c2d916/aiohttp-3.13.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + name: aiohttp + version: 3.13.5 + sha256: 241a94f7de7c0c3b616627aaad530fe2cb620084a8b144d3be7b6ecfe95bae3b + requires_dist: + - aiohappyeyeballs>=2.5.0 + - aiosignal>=1.4.0 + - async-timeout>=4.0,<6.0 ; python_full_version < '3.11' + - attrs>=17.3.0 + - frozenlist>=1.1.1 + - multidict>=4.5,<7.0 + - propcache>=0.2.0 + - yarl>=1.17.0,<2.0 + - aiodns>=3.3.0 ; extra == 'speedups' + - brotli>=1.2 ; platform_python_implementation == 'CPython' and extra == 'speedups' + - brotlicffi>=1.2 ; platform_python_implementation != 'CPython' and extra == 'speedups' + - backports-zstd ; python_full_version < '3.14' and platform_python_implementation == 'CPython' and extra == 'speedups' + requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/bf/34/1fe99124be59579ebd24316522e1da780979c856977b142c0dcd878b0a2d/spglib-2.6.0.tar.gz name: spglib version: 2.6.0 @@ -11092,529 +11952,781 @@ packages: - spglib[docs] ; extra == 'doc' - spglib[test] ; extra == 'testing' requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/2c/fa/65fcae2ed62f84ab72cf89536c7c3217a156e71a2c111b1305ab6f0690e2/sqlalchemy-2.0.49-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - name: sqlalchemy - version: 2.0.49 - sha256: 3bb9ec6436a820a4c006aad1ac351f12de2f2dbdaad171692ee457a02429b672 - 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/2e/84/efc7c0bf3a1c5eef81d397f6fddac855becdbb11cb38ff957888603014a7/sqlalchemy-2.0.49-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - name: sqlalchemy - version: 2.0.49 - sha256: 685e93e9c8f399b0c96a624799820176312f5ceef958c0f88215af4013d29066 +- 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: - - 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/47/9e/fd90114059175cac64e4fafa9bf3ac20584384d66de40793ae2e2f26f3bb/sqlalchemy-2.0.49-cp312-cp312-win_amd64.whl - name: sqlalchemy - version: 2.0.49 - sha256: 618a308215b6cececb6240b9abde545e3acdabac7ae3e1d4e666896bf5ba44b4 + - docstring-parser[docs] ; extra == 'dev' + - docstring-parser[test] ; extra == 'dev' + - pre-commit>=2.16.0 ; python_full_version >= '3.9' and extra == 'dev' + - pydoctor>=25.4.0 ; extra == 'docs' + - pytest ; extra == 'test' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/bf/e1/9e8e09ab8fc5c77f228e3271188dc4f569d12692548c42bff81b1fbba5e1/easydiffraction-0.16.0-py3-none-any.whl + name: easydiffraction + version: 0.16.0 + sha256: 2691a1e175974ca79e0ec3c219d92b77f277c38fb3b0b8d25f6f7e99696bf70f requires_dist: - - 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' + - asciichartpy + - asteval + - bumps + - colorama + - crysfml + - cryspy + - darkdetect + - dfo-ls + - diffpy-pdffit2 + - diffpy-utils + - essdiffraction + - gemmi + - lmfit + - numpy + - pandas + - plotly + - pooch + - py3dmol + - rich + - scipy + - sympy + - tabulate + - typeguard + - typer + - uncertainties + - varname + - build ; extra == 'dev' + - copier ; extra == 'dev' + - docstripy ; extra == 'dev' + - format-docstring ; extra == 'dev' + - gitpython ; extra == 'dev' + - interrogate ; extra == 'dev' + - jinja2 ; extra == 'dev' + - jupyterquiz ; extra == 'dev' + - jupytext ; extra == 'dev' + - mike ; extra == 'dev' + - mkdocs ; extra == 'dev' + - mkdocs-autorefs ; extra == 'dev' + - mkdocs-jupyter ; extra == 'dev' + - mkdocs-markdownextradata-plugin ; extra == 'dev' + - mkdocs-material ; extra == 'dev' + - mkdocs-plugin-inline-svg ; extra == 'dev' + - mkdocstrings-python ; extra == 'dev' + - nbmake ; extra == 'dev' + - nbqa ; extra == 'dev' + - nbstripout ; extra == 'dev' + - pre-commit ; extra == 'dev' + - pydoclint ; extra == 'dev' + - pytest ; extra == 'dev' + - pytest-benchmark ; extra == 'dev' + - pytest-cov ; extra == 'dev' + - pytest-xdist ; extra == 'dev' + - pyyaml ; extra == 'dev' + - radon ; extra == 'dev' + - ruff ; extra == 'dev' + - spdx-headers ; extra == 'dev' + - validate-pyproject[all] ; extra == 'dev' + - versioningit ; extra == 'dev' + requires_python: '>=3.12' +- pypi: https://files.pythonhosted.org/packages/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/49/b3/2de412451330756aaaa72d27131db6dde23995efe62c941184e15242a5fa/sqlalchemy-2.0.49-cp312-cp312-macosx_11_0_arm64.whl - name: sqlalchemy - version: 2.0.49 - sha256: 4bbccb45260e4ff1b7db0be80a9025bb1e6698bdb808b83fff0000f7a90b2c0b +- pypi: https://files.pythonhosted.org/packages/c2/20/193faab46a68ea550587331a698c3dca8099f8901d10937c4443135c7ed9/chardet-7.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + name: chardet + version: 7.4.3 + sha256: 6e3bd9f936e04bae89c254262af08d9e5b98f805175ba1e29d454e6cba3107b7 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/c3/d4/98078064ccc76b45cb0f6c002452011e93c4bd26f6850344f0951cc1fe89/fonttools-4.63.0-cp314-cp314-win_amd64.whl + name: fonttools + version: 4.63.0 + sha256: 7d782fac32985914c351556f68ac0855391572bcd87de50e05970d3cd4c96fc5 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/55/33/bf28f618c0a9597d14e0b9ee7d1e0622faff738d44fe986ee287cdf1b8d0/sqlalchemy-2.0.49-cp314-cp314-macosx_11_0_arm64.whl - name: sqlalchemy - version: 2.0.49 - sha256: 233088b4b99ebcbc5258c755a097aa52fbf90727a03a5a80781c4b9c54347a2e + - lxml>=4.0 ; extra == 'lxml' + - brotli>=1.0.1 ; platform_python_implementation == 'CPython' and extra == 'woff' + - brotlicffi>=0.8.0 ; platform_python_implementation != 'CPython' and extra == 'woff' + - zopfli>=0.1.4 ; extra == 'woff' + - unicodedata2>=17.0.0 ; python_full_version < '3.15' and extra == 'unicode' + - lz4>=1.7.4.2 ; extra == 'graphite' + - scipy ; platform_python_implementation != 'PyPy' and extra == 'interpolatable' + - munkres ; platform_python_implementation == 'PyPy' and extra == 'interpolatable' + - pycairo ; extra == 'interpolatable' + - matplotlib ; extra == 'plot' + - sympy ; extra == 'symfont' + - xattr ; sys_platform == 'darwin' and extra == 'type1' + - skia-pathops>=0.5.0 ; extra == 'pathops' + - uharfbuzz>=0.45.0 ; extra == 'repacker' + - lxml>=4.0 ; extra == 'all' + - brotli>=1.0.1 ; platform_python_implementation == 'CPython' and extra == 'all' + - brotlicffi>=0.8.0 ; platform_python_implementation != 'CPython' and extra == 'all' + - zopfli>=0.1.4 ; extra == 'all' + - unicodedata2>=17.0.0 ; python_full_version < '3.15' and extra == 'all' + - lz4>=1.7.4.2 ; extra == 'all' + - scipy ; platform_python_implementation != 'PyPy' and extra == 'all' + - munkres ; platform_python_implementation == 'PyPy' and extra == 'all' + - pycairo ; extra == 'all' + - matplotlib ; extra == 'all' + - sympy ; extra == 'all' + - xattr ; sys_platform == 'darwin' and extra == 'all' + - skia-pathops>=0.5.0 ; extra == 'all' + - uharfbuzz>=0.45.0 ; extra == 'all' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/c4/13/680c54afe3e65767bed7ec1a15571e1a2f1257128733851ade24abcefbcc/kiwisolver-1.5.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + name: kiwisolver + version: 1.5.0 + sha256: bb5136fb5352d3f422df33f0c879a1b0c204004324150cc3b5e3c4f310c9049f + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/c4/6d/82e65254354ba651dc966775270a9bbc02414a3eb3f1704e6c87dab2ea83/essdiffraction-26.5.1-py3-none-any.whl + name: essdiffraction + version: 26.5.1 + sha256: 8a6c779078c71be250714619214069221ab7968a69580d4e4d3f4b3e9a1a53ad requires_dist: - - importlib-metadata ; python_full_version < '3.8' - - greenlet>=1 ; platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64' - - typing-extensions>=4.6.0 - - greenlet>=1 ; extra == 'asyncio' - - mypy>=0.910 ; extra == 'mypy' - - pyodbc ; extra == 'mssql' - - pymssql ; extra == 'mssql-pymssql' - - pyodbc ; extra == 'mssql-pyodbc' - - mysqlclient>=1.4.0 ; extra == 'mysql' - - mysql-connector-python ; extra == 'mysql-connector' - - mariadb>=1.0.1,!=1.1.2,!=1.1.5,!=1.1.10 ; extra == 'mariadb-connector' - - cx-oracle>=8 ; extra == 'oracle' - - oracledb>=1.0.1 ; extra == 'oracle-oracledb' - - psycopg2>=2.7 ; extra == 'postgresql' - - pg8000>=1.29.1 ; extra == 'postgresql-pg8000' - - greenlet>=1 ; extra == 'postgresql-asyncpg' - - asyncpg ; extra == 'postgresql-asyncpg' - - psycopg2-binary ; extra == 'postgresql-psycopg2binary' - - psycopg2cffi ; extra == 'postgresql-psycopg2cffi' - - psycopg>=3.0.7 ; extra == 'postgresql-psycopg' - - psycopg[binary]>=3.0.7 ; extra == 'postgresql-psycopgbinary' - - pymysql ; extra == 'pymysql' - - greenlet>=1 ; extra == 'aiomysql' - - aiomysql>=0.2.0 ; extra == 'aiomysql' - - greenlet>=1 ; extra == 'aioodbc' - - aioodbc ; extra == 'aioodbc' - - greenlet>=1 ; extra == 'asyncmy' - - asyncmy>=0.2.3,!=0.2.4,!=0.2.6 ; extra == 'asyncmy' - - greenlet>=1 ; extra == 'aiosqlite' - - aiosqlite ; extra == 'aiosqlite' - - typing-extensions!=3.10.0.1 ; extra == 'aiosqlite' - - sqlcipher3-binary ; extra == 'sqlcipher' - requires_python: '>=3.7' -- pypi: https://files.pythonhosted.org/packages/cf/4f/8297e4ed88e80baa1f5aa3c484a0ee29ef3c69c7582f206c916973b75057/sqlalchemy-2.0.49-cp314-cp314-win_amd64.whl - name: sqlalchemy - version: 2.0.49 - sha256: 77641d299179c37b89cf2343ca9972c88bb6eef0d5fc504a2f86afd15cd5adf5 + - dask>=2022.1.0 + - essreduce>=26.4.0 + - graphviz + - numpy>=2 + - plopp>=26.2.0 + - pythreejs>=2.4.1 + - sciline>=25.4.1 + - scipp>=25.11.0 + - scippneutron>=26.3.0 + - scippnexus>=23.12.0 + - tof>=25.12.0 + - ncrystal[cif]>=4.1.0 + - spglib!=2.7 + - pandas>=2.1.2 ; extra == 'test' + - pooch>=1.5 ; extra == 'test' + - pytest>=7.0 ; extra == 'test' + - ipywidgets>=8.1.7 ; extra == 'test' + - autodoc-pydantic ; extra == 'docs' + - ipykernel ; extra == 'docs' + - ipympl ; extra == 'docs' + - ipython!=8.7.0 ; extra == 'docs' + - myst-parser ; extra == 'docs' + - nbsphinx ; extra == 'docs' + - pandas ; extra == 'docs' + - pooch ; extra == 'docs' + - pydata-sphinx-theme>=0.14 ; extra == 'docs' + - sphinx ; extra == 'docs' + - sphinx-autodoc-typehints ; extra == 'docs' + - sphinx-copybutton ; extra == 'docs' + - sphinx-design ; extra == 'docs' + - sphinxcontrib-bibtex ; extra == 'docs' + - pyarrow ; extra == 'docs' + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/c6/3d/020a6b6248c3d4a37797db068256f0b3f15b01bc481327ba888c50309aa8/mkdocs_plugin_inline_svg-0.1.0-py3-none-any.whl + name: mkdocs-plugin-inline-svg + version: 0.1.0 + sha256: a5aab2d98a19b24019f8e650f54fc647c2f31e7d0e36fc5cf2d2161acc0ea49a + requires_dist: + - mkdocs + requires_python: '>=3.5' +- pypi: https://files.pythonhosted.org/packages/c7/6b/6c02f55c2ce2f137ccca0986be7dd89bea31d5bee4346b4377fa3b8586df/ncrystal_core-4.4.2-py3-none-win_amd64.whl + name: ncrystal-core + version: 4.4.2 + sha256: 9b28a90b63849e6a3a807a0a59f7c2ee57e4c64f5643b2dcb6a798ac8ccf666a + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/c7/a0/5ff05d1919ca249508012cad89f08fdc6cfbdaa15b41651c5fe6dffaf1d3/dfo_ls-1.6.5-py3-none-any.whl + name: dfo-ls + version: 1.6.5 + sha256: d147d42e471e240f9abf8bc38351a88f555ea6a8fcfd83119bbbf93c36f75ab2 requires_dist: - - 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' -- conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda - sha256: 570da295d421661af487f1595045760526964f41471021056e993e73089e9c41 - md5: b1b505328da7a6b246787df4b5a49fbc - depends: - - asttokens - - executing - - pure_eval - - python >=3.9 - license: MIT - license_family: MIT - purls: - - pkg:pypi/stack-data?source=hash-mapping - size: 26988 - timestamp: 1733569565672 -- pypi: https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl - name: sympy - version: 1.14.0 - sha256: e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5 + - setuptools + - numpy + - scipy>=1.11 + - pandas + - pytest ; extra == 'dev' + - sphinx ; extra == 'dev' + - sphinx-rtd-theme ; extra == 'dev' + - trustregion>=1.1 ; extra == 'trustregion' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/c7/ea/7988934c8e3e3418aa043f70421817df28d06aef50bfd85f5ad3ec6e70f1/ncrystal_core-4.4.2-py3-none-macosx_11_0_arm64.whl + name: ncrystal-core + version: 4.4.2 + sha256: b7e6101a6850aa18cf441825214381614db444ffcba648de8266fe1c4d1024ce + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl + name: pytest-xdist + version: 3.8.0 + sha256: 202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88 requires_dist: - - mpmath>=1.1.0,<1.4 - - pytest>=7.1.0 ; extra == 'dev' - - hypothesis>=6.70.0 ; extra == 'dev' + - execnet>=2.1 + - pytest>=7.0.0 + - filelock ; extra == 'testing' + - psutil>=3.0 ; extra == 'psutil' + - setproctitle ; extra == 'setproctitle' requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl - name: tabulate - version: 0.10.0 - sha256: f0b0622e567335c8fabaaa659f1b33bcb6ddfe2e496071b743aa113f8774f2d3 +- pypi: https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl + name: iniconfig + version: 2.3.0 + sha256: f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/cc/8f/ec6289987824b29529d0dfda0d74a07cec60e54b9c92f3c9da4c0ac732de/contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + name: contourpy + version: 1.3.3 + sha256: 4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1 requires_dist: - - wcwidth ; extra == 'widechars' + - numpy>=1.25 + - furo ; extra == 'docs' + - sphinx>=7.2 ; extra == 'docs' + - sphinx-copybutton ; extra == 'docs' + - bokeh ; extra == 'bokeh' + - selenium ; extra == 'bokeh' + - contourpy[bokeh,docs] ; extra == 'mypy' + - bokeh ; extra == 'mypy' + - docutils-stubs ; extra == 'mypy' + - mypy==1.17.0 ; extra == 'mypy' + - types-pillow ; extra == 'mypy' + - contourpy[test-no-images] ; extra == 'test' + - matplotlib ; extra == 'test' + - pillow ; extra == 'test' + - pytest ; extra == 'test-no-images' + - pytest-cov ; extra == 'test-no-images' + - pytest-rerunfailures ; extra == 'test-no-images' + - pytest-xdist ; extra == 'test-no-images' + - wurlitzer ; extra == 'test-no-images' + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/cd/8b/601e6566b957ca50e28725cb6c355c59c2c8609751efbecd980db44e0349/pillow-12.2.0-cp314-cp314-win_amd64.whl + name: pillow + version: 12.2.0 + sha256: 4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150 + requires_dist: + - furo ; extra == 'docs' + - olefile ; extra == 'docs' + - sphinx>=8.2 ; extra == 'docs' + - sphinx-autobuild ; extra == 'docs' + - sphinx-copybutton ; extra == 'docs' + - sphinx-inline-tabs ; extra == 'docs' + - sphinxext-opengraph ; extra == 'docs' + - olefile ; extra == 'fpx' + - olefile ; extra == 'mic' + - arro3-compute ; extra == 'test-arrow' + - arro3-core ; extra == 'test-arrow' + - nanoarrow ; extra == 'test-arrow' + - pyarrow ; extra == 'test-arrow' + - check-manifest ; extra == 'tests' + - coverage>=7.4.2 ; extra == 'tests' + - defusedxml ; extra == 'tests' + - markdown2 ; extra == 'tests' + - olefile ; extra == 'tests' + - packaging ; extra == 'tests' + - pyroma>=5 ; extra == 'tests' + - pytest ; extra == 'tests' + - pytest-cov ; extra == 'tests' + - pytest-timeout ; extra == 'tests' + - pytest-xdist ; extra == 'tests' + - trove-classifiers>=2024.10.12 ; extra == 'tests' + - defusedxml ; extra == 'xmp' requires_python: '>=3.10' -- conda: https://conda.anaconda.org/conda-forge/win-64/tbb-2022.3.0-h3155e25_2.conda - sha256: abd9a489f059fba85c8ffa1abdaa4d515d6de6a3325238b8e81203b913cf65a9 - md5: 0f9817ffbe25f9e69ceba5ea70c52606 - depends: - - 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: 155869 - timestamp: 1767886839029 -- conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyh6dadd2b_1.conda - sha256: b375e8df0d5710717c31e7c8e93c025c37fa3504aea325c7a55509f64e5d4340 - md5: e43ca10d61e55d0a8ec5d8c62474ec9e - depends: - - __win - - pywinpty >=1.1.0 - - python >=3.10 - - tornado >=6.1.0 - - python - license: BSD-2-Clause - license_family: BSD - purls: - - pkg:pypi/terminado?source=hash-mapping - size: 23665 - timestamp: 1766513806974 -- conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyhc90fa1f_1.conda - sha256: 6b6727a13d1ca6a23de5e6686500d0669081a117736a87c8abf444d60c1e40eb - md5: 17b43cee5cc84969529d5d0b0309b2cb - depends: - - __unix - - ptyprocess - - python >=3.10 - - tornado >=6.1.0 - - python - license: BSD-2-Clause - license_family: BSD - purls: - - pkg:pypi/terminado?source=hash-mapping - size: 24749 - timestamp: 1766513766867 -- conda: https://conda.anaconda.org/conda-forge/noarch/tinycss2-1.4.0-pyhd8ed1ab_0.conda - sha256: cad582d6f978276522f84bd209a5ddac824742fe2d452af6acf900f8650a73a2 - md5: f1acf5fdefa8300de697982bcb1761c9 - depends: - - python >=3.5 - - webencodings >=0.4 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/tinycss2?source=hash-mapping - size: 28285 - timestamp: 1729802975370 -- conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h366c992_103.conda - sha256: cafeec44494f842ffeca27e9c8b0c27ed714f93ac77ddadc6aaf726b5554ebac - md5: cffd3bdd58090148f4cfcd831f4b26ab - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=14 - - libzlib >=1.3.1,<2.0a0 - constrains: - - xorg-libx11 >=1.8.12,<2.0a0 - license: TCL - license_family: BSD - purls: [] - size: 3301196 - timestamp: 1769460227866 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h010d191_3.conda - sha256: 799cab4b6cde62f91f750149995d149bc9db525ec12595e8a1d91b9317f038b3 - md5: a9d86bc62f39b94c4661716624eb21b0 - depends: - - __osx >=11.0 - - libzlib >=1.3.1,<2.0a0 - license: TCL - license_family: BSD - purls: [] - size: 3127137 - timestamp: 1769460817696 -- conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h6ed50ae_3.conda - sha256: 0e79810fae28f3b69fe7391b0d43f5474d6bd91d451d5f2bde02f55ae481d5e3 - md5: 0481bfd9814bf525bd4b3ee4b51494c4 - depends: - - ucrt >=10.0.20348.0 - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 - license: TCL - license_family: BSD - purls: [] - size: 3526350 - timestamp: 1769460339384 -- 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 +- pypi: https://files.pythonhosted.org/packages/cf/3d/b16412745651e855f357e5e66930248688378853a6e2698a214e331fba1f/pandas-3.0.3-cp312-cp312-macosx_11_0_arm64.whl + name: pandas + version: 3.0.3 + sha256: b168fc218fd80a6cbdbdbc1a97ddc7889ed057d7eb45f50d866ceab5f39904c4 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' + - numpy>=1.26.0 ; python_full_version < '3.14' + - numpy>=2.3.3 ; python_full_version >= '3.14' + - python-dateutil>=2.8.2 + - tzdata ; sys_platform == 'win32' + - tzdata ; sys_platform == 'emscripten' + - hypothesis>=6.116.0 ; extra == 'test' + - pytest>=8.3.4 ; extra == 'test' + - pytest-xdist>=3.6.1 ; extra == 'test' + - pyarrow>=13.0.0 ; extra == 'pyarrow' + - bottleneck>=1.4.2 ; extra == 'performance' + - numba>=0.60.0 ; extra == 'performance' + - numexpr>=2.10.2 ; extra == 'performance' + - scipy>=1.14.1 ; extra == 'computation' + - xarray>=2024.10.0 ; extra == 'computation' + - fsspec>=2024.10.0 ; extra == 'fss' + - s3fs>=2024.10.0 ; extra == 'aws' + - gcsfs>=2024.10.0 ; extra == 'gcp' + - odfpy>=1.4.1 ; extra == 'excel' + - openpyxl>=3.1.5 ; extra == 'excel' + - python-calamine>=0.3.0 ; extra == 'excel' + - pyxlsb>=1.0.10 ; extra == 'excel' + - xlrd>=2.0.1 ; extra == 'excel' + - xlsxwriter>=3.2.0 ; extra == 'excel' + - pyarrow>=13.0.0 ; extra == 'parquet' + - pyarrow>=13.0.0 ; extra == 'feather' + - pyiceberg>=0.8.1 ; extra == 'iceberg' + - tables>=3.10.1 ; extra == 'hdf5' + - pyreadstat>=1.2.8 ; extra == 'spss' + - sqlalchemy>=2.0.36 ; extra == 'postgresql' + - psycopg2>=2.9.10 ; extra == 'postgresql' + - adbc-driver-postgresql>=1.2.0 ; extra == 'postgresql' + - sqlalchemy>=2.0.36 ; extra == 'mysql' + - pymysql>=1.1.1 ; extra == 'mysql' + - sqlalchemy>=2.0.36 ; extra == 'sql-other' + - adbc-driver-postgresql>=1.2.0 ; extra == 'sql-other' + - adbc-driver-sqlite>=1.2.0 ; extra == 'sql-other' + - beautifulsoup4>=4.12.3 ; extra == 'html' + - html5lib>=1.1 ; extra == 'html' + - lxml>=5.3.0 ; extra == 'html' + - lxml>=5.3.0 ; extra == 'xml' + - matplotlib>=3.9.3 ; extra == 'plot' + - jinja2>=3.1.5 ; extra == 'output-formatting' + - tabulate>=0.9.0 ; extra == 'output-formatting' + - pyqt5>=5.15.9 ; extra == 'clipboard' + - qtpy>=2.4.2 ; extra == 'clipboard' + - zstandard>=0.23.0 ; extra == 'compression' + - pytz>=2020.1 ; extra == 'timezone' + - adbc-driver-postgresql>=1.2.0 ; extra == 'all' + - adbc-driver-sqlite>=1.2.0 ; extra == 'all' + - beautifulsoup4>=4.12.3 ; extra == 'all' + - bottleneck>=1.4.2 ; extra == 'all' + - fastparquet>=2024.11.0 ; extra == 'all' + - fsspec>=2024.10.0 ; extra == 'all' + - gcsfs>=2024.10.0 ; extra == 'all' + - html5lib>=1.1 ; extra == 'all' + - hypothesis>=6.116.0 ; extra == 'all' + - jinja2>=3.1.5 ; extra == 'all' + - lxml>=5.3.0 ; extra == 'all' + - matplotlib>=3.9.3 ; extra == 'all' + - numba>=0.60.0 ; extra == 'all' + - numexpr>=2.10.2 ; extra == 'all' + - odfpy>=1.4.1 ; extra == 'all' + - openpyxl>=3.1.5 ; extra == 'all' + - psycopg2>=2.9.10 ; extra == 'all' + - pyarrow>=13.0.0 ; extra == 'all' + - pyiceberg>=0.8.1 ; extra == 'all' + - pymysql>=1.1.1 ; extra == 'all' + - pyqt5>=5.15.9 ; extra == 'all' + - pyreadstat>=1.2.8 ; extra == 'all' + - pytest>=8.3.4 ; extra == 'all' + - pytest-xdist>=3.6.1 ; extra == 'all' + - python-calamine>=0.3.0 ; extra == 'all' + - pytz>=2020.1 ; extra == 'all' + - pyxlsb>=1.0.10 ; extra == 'all' + - qtpy>=2.4.2 ; extra == 'all' + - scipy>=1.14.1 ; extra == 'all' + - s3fs>=2024.10.0 ; extra == 'all' + - sqlalchemy>=2.0.36 ; extra == 'all' + - tables>=3.10.1 ; extra == 'all' + - tabulate>=0.9.0 ; extra == 'all' + - xarray>=2024.10.0 ; extra == 'all' + - xlrd>=2.0.1 ; extra == 'all' + - xlsxwriter>=3.2.0 ; extra == 'all' + - zstandard>=0.23.0 ; extra == 'all' requires_python: '>=3.11' -- pypi: https://files.pythonhosted.org/packages/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' -- conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.1-pyhcf101f3_0.conda - sha256: 91cafdb64268e43e0e10d30bd1bef5af392e69f00edd34dfaf909f69ab2da6bd - md5: b5325cf06a000c5b14970462ff5e4d58 - depends: - - python >=3.10 - - python - license: MIT - license_family: MIT - purls: - - pkg:pypi/tomli?source=hash-mapping - size: 21561 - timestamp: 1774492402955 -- 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' -- conda: https://conda.anaconda.org/conda-forge/linux-64/tornado-6.5.5-py312h4c3975b_0.conda - sha256: 4629b1c9139858fb08bb357df917ffc12e4d284c57ff389806bb3ae476ef4e0a - md5: 2b37798adbc54fd9e591d24679d2133a - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=14 - - python >=3.12,<3.13.0a0 - - python_abi 3.12.* *_cp312 - license: Apache-2.0 - license_family: Apache - purls: - - pkg:pypi/tornado?source=compressed-mapping - size: 859665 - timestamp: 1774358032165 -- conda: https://conda.anaconda.org/conda-forge/linux-64/tornado-6.5.5-py314h5bd0f2a_0.conda - sha256: ed8d06093ff530a2dae9ed1e51eb6f908fbfd171e8b62f4eae782d67b420be5a - md5: dc1ff1e915ab35a06b6fa61efae73ab5 - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=14 - - python >=3.14,<3.15.0a0 - - python_abi 3.14.* *_cp314 - license: Apache-2.0 - license_family: Apache - purls: - - pkg:pypi/tornado?source=hash-mapping - size: 912476 - timestamp: 1774358032579 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/tornado-6.5.5-py312h2bbb03f_0.conda - sha256: 29edd36311b4a810a9e6208437bdbedb28c9ac15221caf812cb5c5cf48375dca - md5: 02cce5319b0f1317d9642dcb2e475379 - depends: - - __osx >=11.0 - - python >=3.12,<3.13.0a0 - - python >=3.12,<3.13.0a0 *_cpython - - python_abi 3.12.* *_cp312 - license: Apache-2.0 - license_family: Apache - purls: - - pkg:pypi/tornado?source=compressed-mapping - size: 859155 - timestamp: 1774358568476 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/tornado-6.5.5-py314h6c2aa35_0.conda - sha256: 4ccc4a20d676c0ba85adee9c99015bec7f5b685df0cf8006e34573f1d6c2ce75 - md5: 3f81f8b2fe2c26a82c0abf57ab2b9610 - depends: - - __osx >=11.0 - - python >=3.14,<3.15.0a0 - - python >=3.14,<3.15.0a0 *_cp314 - - python_abi 3.14.* *_cp314 - license: Apache-2.0 - license_family: Apache - purls: - - pkg:pypi/tornado?source=hash-mapping - size: 910845 - timestamp: 1774358965067 -- conda: https://conda.anaconda.org/conda-forge/win-64/tornado-6.5.5-py312he06e257_0.conda - sha256: 1220c986664e9e8662e660dc64dd97ed823926b1ba05175771408cf1d6a46dd2 - md5: c6c66a64da3d2953c83ed2789a7f4bdb - depends: - - python >=3.12,<3.13.0a0 - - python_abi 3.12.* *_cp312 - - ucrt >=10.0.20348.0 - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 - license: Apache-2.0 - license_family: Apache - purls: - - pkg:pypi/tornado?source=compressed-mapping - size: 859726 - timestamp: 1774358173994 -- conda: https://conda.anaconda.org/conda-forge/win-64/tornado-6.5.5-py314h5a2d7ad_0.conda - sha256: 49d64837dd02475903479ca47b82669bd6c9f7e6afde61860c6f3f2bd57d8a03 - md5: 87b1215adf7f0ba1fb9250af9fc668e1 - depends: - - python >=3.14,<3.15.0a0 - - python_abi 3.14.* *_cp314 - - ucrt >=10.0.20348.0 - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 - license: Apache-2.0 - license_family: Apache - purls: - - pkg:pypi/tornado?source=hash-mapping - size: 914835 - timestamp: 1774358183098 -- conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.3-pyhd8ed1ab_1.conda - sha256: f39a5620c6e8e9e98357507262a7869de2ae8cc07da8b7f84e517c9fd6c2b959 - md5: 019a7385be9af33791c989871317e1ed - depends: - - python >=3.9 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/traitlets?source=hash-mapping - size: 110051 - timestamp: 1733367480074 -- 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 +- pypi: https://files.pythonhosted.org/packages/cf/4f/8297e4ed88e80baa1f5aa3c484a0ee29ef3c69c7582f206c916973b75057/sqlalchemy-2.0.49-cp314-cp314-win_amd64.whl + name: sqlalchemy + version: 2.0.49 + sha256: 77641d299179c37b89cf2343ca9972c88bb6eef0d5fc504a2f86afd15cd5adf5 + requires_dist: + - importlib-metadata ; python_full_version < '3.8' + - greenlet>=1 ; platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64' + - typing-extensions>=4.6.0 + - greenlet>=1 ; extra == 'asyncio' + - mypy>=0.910 ; extra == 'mypy' + - pyodbc ; extra == 'mssql' + - pymssql ; extra == 'mssql-pymssql' + - pyodbc ; extra == 'mssql-pyodbc' + - mysqlclient>=1.4.0 ; extra == 'mysql' + - mysql-connector-python ; extra == 'mysql-connector' + - mariadb>=1.0.1,!=1.1.2,!=1.1.5,!=1.1.10 ; extra == 'mariadb-connector' + - cx-oracle>=8 ; extra == 'oracle' + - oracledb>=1.0.1 ; extra == 'oracle-oracledb' + - psycopg2>=2.7 ; extra == 'postgresql' + - pg8000>=1.29.1 ; extra == 'postgresql-pg8000' + - greenlet>=1 ; extra == 'postgresql-asyncpg' + - asyncpg ; extra == 'postgresql-asyncpg' + - psycopg2-binary ; extra == 'postgresql-psycopg2binary' + - psycopg2cffi ; extra == 'postgresql-psycopg2cffi' + - psycopg>=3.0.7 ; extra == 'postgresql-psycopg' + - psycopg[binary]>=3.0.7 ; extra == 'postgresql-psycopgbinary' + - pymysql ; extra == 'pymysql' + - greenlet>=1 ; extra == 'aiomysql' + - aiomysql>=0.2.0 ; extra == 'aiomysql' + - greenlet>=1 ; extra == 'aioodbc' + - aioodbc ; extra == 'aioodbc' + - greenlet>=1 ; extra == 'asyncmy' + - asyncmy>=0.2.3,!=0.2.4,!=0.2.6 ; extra == 'asyncmy' + - greenlet>=1 ; extra == 'aiosqlite' + - aiosqlite ; extra == 'aiosqlite' + - typing-extensions!=3.10.0.1 ; extra == 'aiosqlite' + - sqlcipher3-binary ; extra == 'sqlcipher' + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/cf/a9/599c28631bad314d219cf9ffd40e985b24d603fc8a2f4ccc5ae8419a535b/scipy-1.17.1-cp312-cp312-macosx_14_0_arm64.whl + name: scipy + version: 1.17.1 + sha256: cc90d2e9c7e5c7f1a482c9875007c095c3194b1cfedca3c2f3291cdc2bc7c086 + requires_dist: + - numpy>=1.26.4,<2.7 + - pytest>=8.0.0 ; extra == 'test' + - pytest-cov ; extra == 'test' + - pytest-timeout ; extra == 'test' + - pytest-xdist ; extra == 'test' + - asv ; extra == 'test' + - mpmath ; extra == 'test' + - gmpy2 ; extra == 'test' + - threadpoolctl ; extra == 'test' + - scikit-umfpack ; extra == 'test' + - pooch ; extra == 'test' + - hypothesis>=6.30 ; extra == 'test' + - array-api-strict>=2.3.1 ; extra == 'test' + - cython ; extra == 'test' + - meson ; extra == 'test' + - ninja ; sys_platform != 'emscripten' and extra == 'test' + - sphinx>=5.0.0,<8.2.0 ; extra == 'doc' + - intersphinx-registry ; extra == 'doc' + - pydata-sphinx-theme>=0.15.2 ; extra == 'doc' + - sphinx-copybutton ; extra == 'doc' + - sphinx-design>=0.4.0 ; extra == 'doc' + - matplotlib>=3.5 ; extra == 'doc' + - numpydoc ; extra == 'doc' + - jupytext ; extra == 'doc' + - myst-nb>=1.2.0 ; extra == 'doc' + - pooch ; extra == 'doc' + - jupyterlite-sphinx>=0.19.1 ; extra == 'doc' + - jupyterlite-pyodide-kernel ; extra == 'doc' + - linkify-it-py ; extra == 'doc' + - tabulate ; extra == 'doc' + - click<8.3.0 ; extra == 'dev' + - spin ; extra == 'dev' + - mypy==1.10.0 ; extra == 'dev' + - typing-extensions ; extra == 'dev' + - types-psutil ; extra == 'dev' + - pycodestyle ; extra == 'dev' + - ruff>=0.12.0 ; extra == 'dev' + - cython-lint>=0.12.2 ; extra == 'dev' + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/d1/0b/b905ae82d9419dc38123523862db64978ca2954b69609c3ae8fdaca1084c/numpy-2.4.5-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + name: numpy + version: 2.4.5 + sha256: 685681e956fc8dcb75adc6ff26694e1dfd738b24bd8d4696c51ca0110157f912 + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl + name: colorama + version: 0.4.6 + sha256: 4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 + requires_python: '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*' +- pypi: https://files.pythonhosted.org/packages/d2/f0/834e479e47e499b6478e807fb57b31cc2db696c4db30557bb6f5aea4a90b/mando-0.7.1-py2.py3-none-any.whl + name: mando + version: 0.7.1 + sha256: 26ef1d70928b6057ee3ca12583d73c63e05c49de8972d620c278a7b206581a8a requires_dist: - - traitlets>=4.2.2 + - six + - argparse ; python_full_version < '2.7' + - funcsigs ; python_full_version < '3.3' + - rst2ansi ; extra == 'restructuredtext' +- pypi: https://files.pythonhosted.org/packages/d3/19/43f5f2e568dddde567fc41f8471f9432c09563e19d3e617a48cfa52f8f0a/ruff-0.15.13-py3-none-macosx_11_0_arm64.whl + name: ruff + version: 0.15.13 + sha256: 1c26d2f66163deeb6e08d8b39fbbe983ce3c71cea06a6d7591cfd1421793c629 + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl + name: pytest + version: 9.0.3 + sha256: 2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9 + requires_dist: + - colorama>=0.4 ; sys_platform == 'win32' + - exceptiongroup>=1 ; python_full_version < '3.11' + - iniconfig>=1.0.1 + - packaging>=22 + - pluggy>=1.5,<2 + - pygments>=2.7.2 + - tomli>=1 ; python_full_version < '3.11' + - argcomplete ; extra == 'dev' + - attrs>=19.2 ; extra == 'dev' + - hypothesis>=3.56 ; extra == 'dev' + - mock ; extra == 'dev' + - requests ; extra == 'dev' + - setuptools ; extra == 'dev' + - xmlschema ; extra == 'dev' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/d5/08/c2409cb01d5368dcfedcbaffa7d044cc8957d57a9d0855244a5eb4709d30/funcy-2.0-py2.py3-none-any.whl + name: funcy + version: '2.0' + sha256: 53df23c8bb1651b12f095df764bfb057935d49537a56de211b098f4c79614bb0 +- pypi: https://files.pythonhosted.org/packages/d5/0c/043d5e551459da400957a1395e0febbf771446ff34291afcbe3d8be2a279/fsspec-2026.4.0-py3-none-any.whl + name: fsspec + version: 2026.4.0 + sha256: 11ef7bb35dab8a394fde6e608221d5cf3e8499401c249bebaeaad760a1a8dec2 + requires_dist: + - adlfs ; extra == 'abfs' + - adlfs ; extra == 'adl' + - pyarrow>=1 ; extra == 'arrow' + - dask ; extra == 'dask' + - distributed ; extra == 'dask' + - pre-commit ; extra == 'dev' + - ruff>=0.5 ; extra == 'dev' + - numpydoc ; extra == 'doc' + - sphinx ; extra == 'doc' + - sphinx-design ; extra == 'doc' + - sphinx-rtd-theme ; extra == 'doc' + - yarl ; extra == 'doc' + - dropbox ; extra == 'dropbox' + - dropboxdrivefs ; extra == 'dropbox' + - requests ; extra == 'dropbox' + - adlfs ; extra == 'full' + - aiohttp!=4.0.0a0,!=4.0.0a1 ; extra == 'full' + - dask ; extra == 'full' + - distributed ; extra == 'full' + - dropbox ; extra == 'full' + - dropboxdrivefs ; extra == 'full' + - fusepy ; extra == 'full' + - gcsfs>2024.2.0 ; extra == 'full' + - libarchive-c ; extra == 'full' + - ocifs ; extra == 'full' + - panel ; extra == 'full' + - paramiko ; extra == 'full' + - pyarrow>=1 ; extra == 'full' + - pygit2 ; extra == 'full' + - requests ; extra == 'full' + - s3fs>2024.2.0 ; extra == 'full' + - smbprotocol ; extra == 'full' + - tqdm ; extra == 'full' + - fusepy ; extra == 'fuse' + - gcsfs>2024.2.0 ; extra == 'gcs' + - pygit2 ; extra == 'git' + - requests ; extra == 'github' + - gcsfs ; extra == 'gs' + - panel ; extra == 'gui' + - pyarrow>=1 ; extra == 'hdfs' + - aiohttp!=4.0.0a0,!=4.0.0a1 ; extra == 'http' + - libarchive-c ; extra == 'libarchive' + - ocifs ; extra == 'oci' + - s3fs>2024.2.0 ; extra == 's3' + - paramiko ; extra == 'sftp' + - smbprotocol ; extra == 'smb' + - paramiko ; extra == 'ssh' + - aiohttp!=4.0.0a0,!=4.0.0a1 ; extra == 'test' - numpy ; extra == 'test' - - pandas ; extra == 'test' - - xarray ; extra == 'test' - pytest ; extra == 'test' -- pypi: https://files.pythonhosted.org/packages/d6/bb/fbdc4e57731efb86b4209ffdd2519520782bf27b3c961beac3e5c20d2b87/trove_classifiers-2026.4.28.13-py3-none-any.whl - name: trove-classifiers - version: 2026.4.28.13 - sha256: 8f4b1eb4e16296b57d612965444f87a83861cc989a0451ac97fe4265ddef03b8 -- pypi: https://files.pythonhosted.org/packages/91/88/b55b3117287a8540b76dbdd87733808d4d01c8067a3b339408c250bb3600/typeguard-4.5.1-py3-none-any.whl - name: typeguard - version: 4.5.1 - sha256: 44d2bf329d49a244110a090b55f5f91aa82d9a9834ebfd30bcc73651e4a8cc40 + - 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/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl + name: pycodestyle + version: 2.14.0 + sha256: dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/d7/51/ec641c26e6dca1b25a7d2035ba6ecb7c884ef1a100a9e42fbe4ce4405139/coverage-7.14.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl + name: coverage + version: 7.14.0 + sha256: 5ebb8f4614a3787d567e610bbfdf96a4798dd69a1afb1bd8ad228d4111fe6ff3 + requires_dist: + - tomli ; python_full_version <= '3.11' and extra == 'toml' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/d8/8b/e2bbeb42068f0c48899e8eddd34902afc0f7429d4d2a152d2dc2670dc661/pythreejs-2.4.2-py3-none-any.whl + name: pythreejs + version: 2.4.2 + sha256: 8418807163ad91f4df53b58c4e991b26214852a1236f28f1afeaadf99d095818 + requires_dist: + - ipywidgets>=7.2.1 + - ipydatawidgets>=1.1.1 + - numpy + - traitlets + - sphinx>=1.5 ; extra == 'docs' + - nbsphinx>=0.2.13 ; extra == 'docs' + - nbsphinx-link ; extra == 'docs' + - sphinx-rtd-theme ; extra == 'docs' + - scipy ; extra == 'examples' + - matplotlib ; extra == 'examples' + - scikit-image ; extra == 'examples' + - ipywebrtc ; extra == 'examples' + - nbval ; extra == 'test' + - pytest-check-links ; extra == 'test' + - numpy>=1.14 ; extra == 'test' + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/d8/95/0a351b9289c2b5cbde0bacd4a83ebc44023e835490a727b2a3bd60ddc0f4/pillow-12.2.0-cp312-cp312-macosx_11_0_arm64.whl + name: pillow + version: 12.2.0 + sha256: f3f40b3c5a968281fd507d519e444c35f0ff171237f4fdde090dd60699458421 + requires_dist: + - furo ; extra == 'docs' + - olefile ; extra == 'docs' + - sphinx>=8.2 ; extra == 'docs' + - sphinx-autobuild ; extra == 'docs' + - sphinx-copybutton ; extra == 'docs' + - sphinx-inline-tabs ; extra == 'docs' + - sphinxext-opengraph ; extra == 'docs' + - olefile ; extra == 'fpx' + - olefile ; extra == 'mic' + - arro3-compute ; extra == 'test-arrow' + - arro3-core ; extra == 'test-arrow' + - nanoarrow ; extra == 'test-arrow' + - pyarrow ; extra == 'test-arrow' + - check-manifest ; extra == 'tests' + - coverage>=7.4.2 ; extra == 'tests' + - defusedxml ; extra == 'tests' + - markdown2 ; extra == 'tests' + - olefile ; extra == 'tests' + - packaging ; extra == 'tests' + - pyroma>=5 ; extra == 'tests' + - pytest ; extra == 'tests' + - pytest-cov ; extra == 'tests' + - pytest-timeout ; extra == 'tests' + - pytest-xdist ; extra == 'tests' + - trove-classifiers>=2024.10.12 ; extra == 'tests' + - defusedxml ; extra == 'xmp' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl + name: cfgv + version: 3.5.0 + sha256: a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/db/7b/8624a203326675d7746a254083a187398090a179335b2e4a20e2ddc46e83/scipy-1.17.1-cp314-cp314-macosx_14_0_arm64.whl + name: scipy + version: 1.17.1 + sha256: 3fd1fcdab3ea951b610dc4cef356d416d5802991e7e32b5254828d342f7b7e0b + requires_dist: + - numpy>=1.26.4,<2.7 + - pytest>=8.0.0 ; extra == 'test' + - pytest-cov ; extra == 'test' + - pytest-timeout ; extra == 'test' + - pytest-xdist ; extra == 'test' + - asv ; extra == 'test' + - mpmath ; extra == 'test' + - gmpy2 ; extra == 'test' + - threadpoolctl ; extra == 'test' + - scikit-umfpack ; extra == 'test' + - pooch ; extra == 'test' + - hypothesis>=6.30 ; extra == 'test' + - array-api-strict>=2.3.1 ; extra == 'test' + - cython ; extra == 'test' + - meson ; extra == 'test' + - ninja ; sys_platform != 'emscripten' and extra == 'test' + - sphinx>=5.0.0,<8.2.0 ; extra == 'doc' + - intersphinx-registry ; extra == 'doc' + - pydata-sphinx-theme>=0.15.2 ; extra == 'doc' + - sphinx-copybutton ; extra == 'doc' + - sphinx-design>=0.4.0 ; extra == 'doc' + - matplotlib>=3.5 ; extra == 'doc' + - numpydoc ; extra == 'doc' + - jupytext ; extra == 'doc' + - myst-nb>=1.2.0 ; extra == 'doc' + - pooch ; extra == 'doc' + - jupyterlite-sphinx>=0.19.1 ; extra == 'doc' + - jupyterlite-pyodide-kernel ; extra == 'doc' + - linkify-it-py ; extra == 'doc' + - tabulate ; extra == 'doc' + - click<8.3.0 ; extra == 'dev' + - spin ; extra == 'dev' + - mypy==1.10.0 ; extra == 'dev' + - typing-extensions ; extra == 'dev' + - types-psutil ; extra == 'dev' + - pycodestyle ; extra == 'dev' + - ruff>=0.12.0 ; extra == 'dev' + - cython-lint>=0.12.2 ; extra == 'dev' + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz + name: watchdog + version: 6.0.0 + sha256: 9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282 requires_dist: - - importlib-metadata>=3.6 ; python_full_version < '3.10' - - typing-extensions>=4.14.0 + - pyyaml>=3.10 ; extra == 'watchmedo' requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl - name: typer - version: 0.25.1 - sha256: 75caa44ed46a03fb2dab8808753ffacdbfea88495e74c85a28c5eefcf5f39c89 +- pypi: https://files.pythonhosted.org/packages/db/bc/83e112abc66cd466c6b83f99118035867cecd41802f8d044638aa78a106e/locket-1.0.0-py2.py3-none-any.whl + name: locket + version: 1.0.0 + sha256: b6c819a722f7b6bd955b80781788e4a66a55628b858d347536b7e81325a3a5e3 + requires_python: '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*' +- pypi: https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl + name: watchdog + version: 6.0.0 + sha256: cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680 requires_dist: - - click>=8.2.1 - - shellingham>=1.3.0 - - rich>=13.8.0 - - annotated-doc>=0.0.2 - requires_python: '>=3.10' -- conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.15.0-h396c80c_0.conda - sha256: 7c2df5721c742c2a47b2c8f960e718c930031663ac1174da67c1ed5999f7938c - md5: edd329d7d3a4ab45dcf905899a7a6115 - depends: - - typing_extensions ==4.15.0 pyhcf101f3_0 - license: PSF-2.0 - license_family: PSF - purls: [] - size: 91383 - timestamp: 1756220668932 + - pyyaml>=3.10 ; extra == 'watchmedo' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/dc/83/6d810a8a9ebc9c307989b418840c20e46907c74d707beb67ab566773e6fc/xarray-2026.4.0-py3-none-any.whl + name: xarray + version: 2026.4.0 + sha256: d43751d9fb4a90f9249c30431684f00c41bc874f1edccd862631a40cbc0edf08 + requires_dist: + - numpy>=1.26 + - packaging>=24.2 + - pandas>=2.2 + - scipy>=1.15 ; extra == 'accel' + - bottleneck ; extra == 'accel' + - numbagg>=0.9 ; extra == 'accel' + - numba>=0.62 ; extra == 'accel' + - flox>=0.10 ; extra == 'accel' + - opt-einsum ; extra == 'accel' + - xarray[accel,etc,io,parallel,viz] ; extra == 'complete' + - netcdf4>=1.6.0 ; extra == 'io' + - h5netcdf[h5py]>=1.5.0 ; extra == 'io' + - pydap ; extra == 'io' + - scipy>=1.15 ; extra == 'io' + - zarr>=3.0 ; extra == 'io' + - fsspec ; extra == 'io' + - cftime ; extra == 'io' + - pooch ; extra == 'io' + - sparse>=0.15 ; extra == 'etc' + - dask[complete] ; extra == 'parallel' + - cartopy>=0.24 ; extra == 'viz' + - matplotlib>=3.10 ; extra == 'viz' + - nc-time-axis ; extra == 'viz' + - seaborn ; extra == 'viz' + - pandas-stubs ; extra == 'types' + - scipy-stubs ; extra == 'types' + - types-colorama ; extra == 'types' + - types-decorator ; extra == 'types' + - types-defusedxml ; extra == 'types' + - types-docutils ; extra == 'types' + - types-networkx ; extra == 'types' + - types-openpyxl ; extra == 'types' + - types-pexpect ; extra == 'types' + - types-psutil ; extra == 'types' + - types-pycurl ; extra == 'types' + - types-pygments ; extra == 'types' + - types-python-dateutil ; extra == 'types' + - types-pytz ; extra == 'types' + - types-pyyaml ; extra == 'types' + - types-requests ; extra == 'types' + - types-setuptools ; extra == 'types' + - types-xlrd ; extra == 'types' + requires_python: '>=3.11' - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl name: typing-inspection version: 0.4.2 @@ -11622,377 +12734,237 @@ packages: requires_dist: - typing-extensions>=4.12.0 requires_python: '>=3.9' -- conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda - sha256: 032271135bca55aeb156cee361c81350c6f3fb203f57d024d7e5a1fc9ef18731 - md5: 0caa1af407ecff61170c9437a808404d - depends: - - python >=3.10 - - python - license: PSF-2.0 - license_family: PSF - purls: - - pkg:pypi/typing-extensions?source=hash-mapping - size: 51692 - timestamp: 1756220668932 -- conda: https://conda.anaconda.org/conda-forge/noarch/typing_utils-0.1.0-pyhd8ed1ab_1.conda - sha256: 3088d5d873411a56bf988eee774559335749aed6f6c28e07bf933256afb9eb6c - md5: f6d7aa696c67756a650e91e15e88223c - depends: - - python >=3.9 - license: Apache-2.0 - license_family: APACHE - purls: - - pkg:pypi/typing-utils?source=hash-mapping - size: 15183 - timestamp: 1733331395943 -- conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda - sha256: 1d30098909076af33a35017eed6f2953af1c769e273a0626a04722ac4acaba3c - md5: ad659d0a2b3e47e38d829aa8cad2d610 - license: LicenseRef-Public-Domain - purls: [] - size: 119135 - timestamp: 1767016325805 -- conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.26100.0-h57928b3_0.conda - sha256: 3005729dce6f3d3f5ec91dfc49fc75a0095f9cd23bab49efb899657297ac91a5 - md5: 71b24316859acd00bdb8b38f5e2ce328 - constrains: - - vc14_runtime >=14.29.30037 - - vs2015_runtime >=14.29.30037 - license: LicenseRef-MicrosoftWindowsSDK10 - purls: [] - size: 694692 - timestamp: 1756385147981 -- pypi: https://files.pythonhosted.org/packages/8f/5e/f1e1dd319e35e962a4e00b33150a8868b6329cc1d19fd533436ba5488f09/uncertainties-3.2.3-py3-none-any.whl - name: uncertainties - version: 3.2.3 - sha256: 313353900d8f88b283c9bad81e7d2b2d3d4bcc330cbace35403faaed7e78890a - requires_dist: - - numpy ; extra == 'arrays' - - pytest ; extra == 'test' - - pytest-codspeed ; extra == 'test' - - pytest-cov ; extra == 'test' - - scipy ; extra == 'test' - - sphinx ; extra == 'doc' - - sphinx-copybutton ; extra == 'doc' - - python-docs-theme ; extra == 'doc' - - uncertainties[arrays,doc,test] ; extra == 'all' - requires_python: '>=3.8' -- conda: https://conda.anaconda.org/conda-forge/noarch/uri-template-1.3.0-pyhd8ed1ab_1.conda - sha256: e0eb6c8daf892b3056f08416a96d68b0a358b7c46b99c8a50481b22631a4dfc0 - md5: e7cb0f5745e4c5035a460248334af7eb - depends: - - python >=3.9 - license: MIT - license_family: MIT - purls: - - pkg:pypi/uri-template?source=hash-mapping - size: 23990 - timestamp: 1733323714454 -- conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.6.3-pyhd8ed1ab_0.conda - sha256: af641ca7ab0c64525a96fd9ad3081b0f5bcf5d1cbb091afb3f6ed5a9eee6111a - md5: 9272daa869e03efe68833e3dc7a02130 - depends: - - backports.zstd >=1.0.0 - - brotli-python >=1.2.0 - - h2 >=4,<5 - - pysocks >=1.5.6,<2.0,!=1.5.7 - - python >=3.10 - license: MIT - license_family: MIT - purls: - - pkg:pypi/urllib3?source=hash-mapping - size: 103172 - timestamp: 1767817860341 -- pypi: https://files.pythonhosted.org/packages/b7/ee/e9c95cda829131f71a8dff5ce0406059fd16e591c074414e31ada19ba7c3/validate_pyproject-0.25-py3-none-any.whl - name: validate-pyproject - version: '0.25' - sha256: f9d05e2686beff82f9ea954f582306b036ced3d3feb258c1110f2c2a495b1981 +- pypi: https://files.pythonhosted.org/packages/dc/b7/901d837999a9350a7773289f7760cb473d4ba01fdc6ebae0ff2553d269ac/ncrystal_python-4.4.2-py3-none-any.whl + name: ncrystal-python + version: 4.4.2 + sha256: f419318d088fade6bcff1e39e15baf6fe69fcf5306dd681fca1106d1f63a89ce requires_dist: - - fastjsonschema>=2.16.2,<=3 - - packaging>=24.2 ; extra == 'all' - - trove-classifiers>=2021.10.20 ; extra == 'all' - - tomli>=1.2.1 ; python_full_version < '3.11' and extra == 'all' - - validate-pyproject-schema-store ; extra == 'store' + - numpy>=1.22 requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/a4/39/3a0ae5b0edb66e61bb0e8bc53a503495cba5892297ae21faf6ba0525e681/varname-1.0.0-py3-none-any.whl - name: varname - version: 1.0.0 - sha256: 1125bfe981c3bbbe56988f5cb85fdcd7cad923b153283c2d464aea8b4c833d51 - requires_dist: - - executing>=2.1 - - typing-extensions>=4.13 ; python_full_version < '3.10' - - asttokens==3.* ; extra == 'all' - - pure-eval==0.* ; extra == 'all' - requires_python: '>=3.9' -- conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-h41ae7f8_34.conda - sha256: 9dc40c2610a6e6727d635c62cced5ef30b7b30123f5ef67d6139e23d21744b3a - md5: 1e610f2416b6acdd231c5f573d754a0f - depends: - - vc14_runtime >=14.44.35208 - track_features: - - vc14 - license: BSD-3-Clause - license_family: BSD - purls: [] - size: 19356 - timestamp: 1767320221521 -- conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.44.35208-h818238b_34.conda - sha256: 02732f953292cce179de9b633e74928037fa3741eb5ef91c3f8bae4f761d32a5 - md5: 37eb311485d2d8b2c419449582046a42 - depends: - - ucrt >=10.0.20348.0 - - vcomp14 14.44.35208 h818238b_34 - constrains: - - vs2015_runtime 14.44.35208.* *_34 - license: LicenseRef-MicrosoftVisualCpp2015-2022Runtime - license_family: Proprietary - purls: [] - size: 683233 - timestamp: 1767320219644 -- conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.44.35208-h818238b_34.conda - sha256: 878d5d10318b119bd98ed3ed874bd467acbe21996e1d81597a1dbf8030ea0ce6 - md5: 242d9f25d2ae60c76b38a5e42858e51d - depends: - - ucrt >=10.0.20348.0 - constrains: - - vs2015_runtime 14.44.35208.* *_34 - license: LicenseRef-MicrosoftVisualCpp2015-2022Runtime - license_family: Proprietary - purls: [] - size: 115235 - timestamp: 1767320173250 -- pypi: https://files.pythonhosted.org/packages/1c/59/964ecb8008722d27d8a835baea81f56a91cea8e097b3be992bc6ccde6367/versioningit-3.3.0-py3-none-any.whl - name: versioningit - version: 3.3.0 - sha256: 23b1db3c4756cded9bd6b0ddec6643c261e3d0c471707da3e0b230b81ce53e4b +- 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: - - importlib-metadata>=3.6 ; python_full_version < '3.10' - - packaging>=17.1 - - tomli>=1.2,<3.0 ; python_full_version < '3.11' + - dnspython>=2.0.0 + - idna>=2.0.0 requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/a4/ce/3b6fee91c85626eaf769d617f1be9d2e15c1cca027bbdeb2e0d751469355/verspec-0.1.0-py3-none-any.whl - name: verspec - version: 0.1.0 - sha256: 741877d5633cc9464c45a469ae2a31e801e6dbbaa85b9675d481cda100f11c31 +- pypi: https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl + name: markdown + version: 3.10.2 + sha256: e91464b71ae3ee7afd3017d9f358ef0baf158fd9a298db92f1d4761133824c36 requires_dist: - - coverage ; extra == 'test' - - flake8>=3.7 ; extra == 'test' - - mypy ; extra == 'test' - - pretend ; extra == 'test' - - pytest ; extra == 'test' -- pypi: https://files.pythonhosted.org/packages/4b/eb/03bfb1299d4c4510329e470f13f9a4ce793df7fcb5a2fd3510f911066f61/virtualenv-21.3.0-py3-none-any.whl - name: virtualenv - version: 21.3.0 - sha256: 4d28ee41f6d9ec8f1f00cd472b9ffbcedda1b3d3b9a575b5c94a2d004fd51bd7 + - coverage ; extra == 'testing' + - pyyaml ; extra == 'testing' + - mkdocs>=1.6 ; extra == 'docs' + - mkdocs-nature>=0.6 ; extra == 'docs' + - mdx-gh-links>=0.2 ; extra == 'docs' + - mkdocstrings[python]>=0.28.3 ; extra == 'docs' + - mkdocs-gen-files ; extra == 'docs' + - mkdocs-section-index ; extra == 'docs' + - mkdocs-literate-nav ; extra == 'docs' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/e0/bf/52f25716bbe93745595800f36fb17b73711f14da59ed0bb2eba141bc9f0f/multidict-6.7.1-cp314-cp314-win_amd64.whl + name: multidict + version: 6.7.1 + sha256: 5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f requires_dist: - - distlib>=0.3.7,<1 - - filelock>=3.24.2,<4 ; python_full_version >= '3.10' - - filelock>=3.16.1,<=3.19.1 ; python_full_version < '3.10' - - importlib-metadata>=6.6 ; python_full_version < '3.8' - - platformdirs>=3.9.1,<5 - - python-discovery>=1.2.2 - - typing-extensions>=4.13.2 ; python_full_version < '3.11' - requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl - name: watchdog - version: 6.0.0 - sha256: 6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0 + - typing-extensions>=4.1.0 ; python_full_version < '3.11' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl + name: shellingham + version: 1.5.4 + sha256: 7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686 + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/e4/bc/daa30c02069eeac5b9198985ba42f5d65ca71bed6705b18329e51d352b7c/plopp-26.4.2-py3-none-any.whl + name: plopp + version: 26.4.2 + sha256: 5cab99bb0905ce08a1d1d7d82f0f64cee7d594269ec1bd01a8a361bd14ab7bff requires_dist: - - pyyaml>=3.10 ; extra == 'watchmedo' + - lazy-loader>=0.4 + - matplotlib>=3.8 + - scipp>=25.8.0 ; extra == 'scipp' + - plopp[scipp] ; extra == 'all' + - ipympl>0.8.4 ; extra == 'all' + - pythreejs>=2.4.1 ; extra == 'all' + - mpltoolbox>=24.6.0 ; extra == 'all' + - ipywidgets>=8.1.0 ; extra == 'all' + - graphviz>=0.20.3 ; extra == 'all' + - plopp[scipp] ; extra == 'test' + - graphviz>=0.20.3 ; extra == 'test' + - h5py>=3.12 ; extra == 'test' + - ipympl>=0.8.4 ; extra == 'test' + - ipywidgets>=8.1.0 ; extra == 'test' + - ipykernel>=6.26,<7 ; extra == 'test' + - mpltoolbox>=24.6.0 ; extra == 'test' + - pandas>=2.2.2 ; extra == 'test' + - plotly>=5.15.0 ; extra == 'test' + - pooch>=1.5 ; extra == 'test' + - pyarrow>=13.0.0 ; extra == 'test' + - pytest>=8.0 ; extra == 'test' + - pythreejs>=2.4.1 ; extra == 'test' + - scipy>=1.10.0 ; extra == 'test' + - xarray>=2024.5.0 ; extra == 'test' + - anywidget>=0.9.0 ; extra == 'test' + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/e5/04/c5bb20d64417d20cba0105277235c51969444fa873000fbc26ac0a3fc5a8/gemmi-0.7.5-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + name: gemmi + version: 0.7.5 + sha256: bdc67ad4a7fc420974ab3102f7f6ad1517fa0c3d9f2f7561e42e5f7017635242 requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl - name: watchdog - version: 6.0.0 - sha256: 20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2 +- pypi: https://files.pythonhosted.org/packages/e5/59/a32a241d861cf180853a11c8e5a67641cb1b2af13c3a5ccce83ec07e2c9f/chardet-7.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + name: chardet + version: 7.4.3 + sha256: 9a4904dd5f071b7a7d7f50b4a67a86db3c902d243bf31708f1d5cde2f68239cb + 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 + sha256: 85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30 requires_dist: - - pyyaml>=3.10 ; extra == 'watchmedo' + - ipython ; extra == 'docs' + - matplotlib ; extra == 'docs' + - numpydoc ; extra == 'docs' + - sphinx ; extra == 'docs' + - pytest ; extra == 'tests' + - pytest-cov ; extra == 'tests' + - pytest-xdist ; extra == 'tests' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/e7/f9/b06c934a6aa8bc91f566bd2a214fd04c30506c2d9e2b6b171953216a65b6/kiwisolver-1.5.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + name: kiwisolver + version: 1.5.0 + sha256: 80aa065ffd378ff784822a6d7c3212f2d5f5e9c3589614b5c228b311fd3063ac + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/e8/31/bf1a0803d077e679cfeee5f2f67290a0fa79c7385b5d9a8c17b9db2c48f0/ruff-0.15.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + name: ruff + version: 0.15.13 + sha256: cc411dfebe5eebe55ce041c6ae080eb7668955e866daa2fbb16692a784f1c4ca + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/e8/88/5a431cd1ea7587408a66947384b39beb2ab2bcc1c87b7c4082f05036719f/gemmi-0.7.5-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + name: gemmi + version: 0.7.5 + sha256: 217bb9ac9da7c90704026dacfc0a0652a38f4df1e318225d8f35c75f1f8c7ebf requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz - name: watchdog - version: 6.0.0 - sha256: 9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282 +- pypi: https://files.pythonhosted.org/packages/eb/be/b257e12f9710819fde40adc972578bee6b72c5992da1bc8369bef2597756/nbmake-1.5.5-py3-none-any.whl + name: nbmake + version: 1.5.5 + sha256: c6fbe6e48b60cacac14af40b38bf338a3b88f47f085c54ac5b8639ff0babaf4b requires_dist: - - pyyaml>=3.10 ; extra == 'watchmedo' + - ipykernel>=5.4.0 + - nbclient>=0.6.6 + - nbformat>=5.0.4 + - pygments>=2.7.3 + - pytest>=6.1.0 + requires_python: '>=3.8.0' +- pypi: https://files.pythonhosted.org/packages/eb/f2/53be7a4ba5816e13c39be0f728facac4bcb39cf4903ceeec54b006511c8f/gemmi-0.7.5-cp312-cp312-win_amd64.whl + name: gemmi + version: 0.7.5 + sha256: a1fdb6f72006495b5119e3a8bb5c3185efa708b785bd4a5ce4397ef7abb3fec7 requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl - name: watchdog - version: 6.0.0 - sha256: cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680 +- pypi: https://files.pythonhosted.org/packages/ee/8c/83087ebc47ab0396ce092363001fa37c17153119ee282700c0713a195853/prettytable-3.17.0-py3-none-any.whl + name: prettytable + version: 3.17.0 + sha256: aad69b294ddbe3e1f95ef8886a060ed1666a0b83018bbf56295f6f226c43d287 requires_dist: - - pyyaml>=3.10 ; extra == 'watchmedo' - requires_python: '>=3.9' -- conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.6.0-pyhd8ed1ab_0.conda - sha256: e298b508b2473c4227206800dfb14c39e4b14fd79d4636132e9e1e4244cdf4aa - md5: c3197f8c0d5b955c904616b716aca093 - depends: - - python >=3.10 - license: MIT - license_family: MIT - purls: - - pkg:pypi/wcwidth?source=hash-mapping - size: 71550 - timestamp: 1770634638503 -- conda: https://conda.anaconda.org/conda-forge/noarch/webcolors-25.10.0-pyhd8ed1ab_0.conda - sha256: 21f6c8a20fe050d09bfda3fb0a9c3493936ce7d6e1b3b5f8b01319ee46d6c6f6 - md5: 6639b6b0d8b5a284f027a2003669aa65 - depends: - - python >=3.10 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/webcolors?source=hash-mapping - size: 18987 - timestamp: 1761899393153 -- conda: https://conda.anaconda.org/conda-forge/noarch/webencodings-0.5.1-pyhd8ed1ab_3.conda - sha256: 19ff205e138bb056a46f9e3839935a2e60bd1cf01c8241a5e172a422fed4f9c6 - md5: 2841eb5bfc75ce15e9a0054b98dcd64d - depends: - - python >=3.9 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/webencodings?source=hash-mapping - size: 15496 - timestamp: 1733236131358 -- conda: https://conda.anaconda.org/conda-forge/noarch/websocket-client-1.9.0-pyhd8ed1ab_0.conda - sha256: 42a2b61e393e61cdf75ced1f5f324a64af25f347d16c60b14117393a98656397 - md5: 2f1ed718fcd829c184a6d4f0f2e07409 - depends: - - python >=3.10 - license: Apache-2.0 - license_family: APACHE - purls: - - pkg:pypi/websocket-client?source=hash-mapping - size: 61391 - timestamp: 1759928175142 -- 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' -- conda: https://conda.anaconda.org/conda-forge/noarch/win_inet_pton-1.1.0-pyh7428d3b_8.conda - sha256: 93807369ab91f230cf9e6e2a237eaa812492fe00face5b38068735858fba954f - md5: 46e441ba871f524e2b067929da3051c2 - depends: - - __win - - python >=3.9 - license: LicenseRef-Public-Domain - purls: - - pkg:pypi/win-inet-pton?source=hash-mapping - size: 9555 - timestamp: 1733130678956 -- conda: https://conda.anaconda.org/conda-forge/win-64/winpty-0.4.3-4.tar.bz2 - sha256: 9df10c5b607dd30e05ba08cbd940009305c75db242476f4e845ea06008b0a283 - md5: 1cee351bf20b830d991dbe0bc8cd7dfe - license: MIT - license_family: MIT - purls: [] - size: 1176306 -- pypi: https://files.pythonhosted.org/packages/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-py3-none-any.whl - name: wsproto - version: 1.3.2 - sha256: 61eea322cdf56e8cc904bd3ad7573359a242ba65688716b0710a5eb12beab584 + - wcwidth + - pytest ; extra == 'tests' + - pytest-cov ; extra == 'tests' + - pytest-lazy-fixtures ; extra == 'tests' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/f1/16/d905e7f53e661ce2c24686c38048d8e2b750ffc4350009d41c4e6c6c9826/h5py-3.16.0-cp314-cp314-manylinux_2_28_x86_64.whl + name: h5py + version: 3.16.0 + sha256: e4360f15875a532bc7b98196c7592ed4fc92672a57c0a621355961cafb17a6dd requires_dist: - - h11>=0.16.0,<1 + - numpy>=1.21.2 requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/38/8b/7ec325b4e9e78beefc2d025b01ee8a2fde771ef7c957c3bff99b9e1fbffa/xraydb-4.5.8-py3-none-any.whl - name: xraydb - version: 4.5.8 - sha256: 2215baafa6a03d00d0254a94525aafc6493c8c285e4ac4477fbd6271b25e6a51 +- pypi: https://files.pythonhosted.org/packages/f1/2c/3850985d4c64048dec7b826f8a803e135b52b11b4c81c9cd4326b1ca15ab/ncrystal_core-4.4.2-py3-none-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + name: ncrystal-core + version: 4.4.2 + sha256: d0d9c47cd017b7cefc52dde50546d7c151bfdd75d345e42e2b3e74ab5fe83c62 + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/f1/4f/733c48f270565d78b4544f2baddc2fb2a245e5a8640254b12c36ac7ac68e/multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl + name: multidict + version: 6.7.1 + sha256: 0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1 requires_dist: - - numpy>=1.19 - - scipy>=1.6 - - sqlalchemy>=2.0.1 - - platformdirs - - build ; extra == 'dev' - - twine ; extra == 'dev' - - sphinx ; extra == 'doc' - - pytest ; extra == 'test' - - pytest-cov ; extra == 'test' - - coverage ; extra == 'test' - - xraydb[dev,doc,test] ; extra == 'all' + - typing-extensions>=4.1.0 ; python_full_version < '3.11' requires_python: '>=3.9' -- conda: https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h280c20c_3.conda - sha256: 6d9ea2f731e284e9316d95fa61869fe7bbba33df7929f82693c121022810f4ad - md5: a77f85f77be52ff59391544bfe73390a - depends: - - libgcc >=14 - - __glibc >=2.17,<3.0.a0 - license: MIT - license_family: MIT - purls: [] - size: 85189 - timestamp: 1753484064210 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/yaml-0.2.5-h925e9cb_3.conda - sha256: b03433b13d89f5567e828ea9f1a7d5c5d697bf374c28a4168d71e9464f5dafac - md5: 78a0fe9e9c50d2c381e8ee47e3ea437d - depends: - - __osx >=11.0 - license: MIT - license_family: MIT - purls: [] - size: 83386 - timestamp: 1753484079473 -- conda: https://conda.anaconda.org/conda-forge/win-64/yaml-0.2.5-h6a83c73_3.conda - sha256: 80ee68c1e7683a35295232ea79bcc87279d31ffeda04a1665efdb43cbd50a309 - md5: 433699cba6602098ae8957a323da2664 - depends: - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 - - ucrt >=10.0.20348.0 - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 - - ucrt >=10.0.20348.0 - license: MIT - license_family: MIT - purls: [] - size: 63944 - timestamp: 1753484092156 -- pypi: https://files.pythonhosted.org/packages/19/2a/725ecc166d53438bc88f76822ed4b1e3b10756e790bafd7b523fe97c322d/yarl-1.23.0-cp312-cp312-macosx_11_0_arm64.whl - name: yarl - version: 1.23.0 - sha256: 13a563739ae600a631c36ce096615fe307f131344588b0bc0daec108cdb47b25 +- 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: - - idna>=2.0 - - multidict>=4.0 - - propcache>=0.2.1 - requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/24/f9/e8242b68362bffe6fb536c8db5076861466fc780f0f1b479fc4ffbebb128/yarl-1.23.0-cp314-cp314-macosx_11_0_arm64.whl - name: yarl - version: 1.23.0 - sha256: 23f371bd662cf44a7630d4d113101eafc0cfa7518a2760d20760b26021454719 + - ipywidgets>=7.0.0 + - numpy + - traittypes>=0.2.0 + - sphinx ; extra == 'docs' + - recommonmark ; extra == 'docs' + - sphinx-rtd-theme ; extra == 'docs' + - pytest>=4 ; extra == 'test' + - pytest-cov ; extra == 'test' + - nbval>=0.9.2 ; extra == 'test' + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl + name: pathspec + version: 1.1.1 + sha256: a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189 requires_dist: - - idna>=2.0 - - multidict>=4.0 - - propcache>=0.2.1 - requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/42/2b/fef67d616931055bf3d6764885990a3ac647d68734a2d6a9e1d13de437a2/yarl-1.23.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - name: yarl - version: 1.23.0 - sha256: 1c57676bdedc94cd3bc37724cf6f8cd2779f02f6aba48de45feca073e714fe52 + - hyperscan>=0.7 ; extra == 'hyperscan' + - typing-extensions>=4 ; extra == 'optional' + - google-re2>=1.1 ; extra == 're2' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/f2/f2/728f041460f1b9739b85ee23b45fa5a505962ea11fd85bdbe2a02b021373/darkdetect-0.8.0-py3-none-any.whl + name: darkdetect + version: 0.8.0 + sha256: a7509ccf517eaad92b31c214f593dbcf138ea8a43b2935406bbd565e15527a85 + requires_dist: + - pyobjc-framework-cocoa ; sys_platform == 'darwin' and extra == 'macos-listener' + requires_python: '>=3.6' +- pypi: https://files.pythonhosted.org/packages/f3/8d/5e5be3ced1d12966fefb5c4ea3b2a5b480afcea36406559442c6e31d4a48/multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + name: multidict + version: 6.7.1 + sha256: bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961 requires_dist: - - idna>=2.0 - - multidict>=4.0 - - propcache>=0.2.1 - requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/66/3e/868e5c3364b6cee19ff3e1a122194fa4ce51def02c61023970442162859e/yarl-1.23.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - name: yarl - version: 1.23.0 - sha256: a3d2bff8f37f8d0f96c7ec554d16945050d54462d6e95414babaa18bfafc7f51 + - typing-extensions>=4.1.0 ; python_full_version < '3.11' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/f3/9b/4165a1d56ddc302a0e2d518fd9d412a4fd0b57562618c78c5f21c57194f5/coverage-7.14.0-cp314-cp314-macosx_11_0_arm64.whl + name: coverage + version: 7.14.0 + sha256: ba3b8390db29296dbbf49e91b6fe08f990743a90c8f447ba4c2ffc29670dfa63 requires_dist: - - idna>=2.0 - - multidict>=4.0 - - propcache>=0.2.1 + - tomli ; python_full_version <= '3.11' and extra == 'toml' requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/a9/5b/9b92f54c784c26e2a422e55a8d2607ab15b7ea3349e28359282f84f01d43/yarl-1.23.0-cp314-cp314-win_amd64.whl - name: yarl - version: 1.23.0 - sha256: 63e92247f383c85ab00dd0091e8c3fa331a96e865459f5ee80353c70a4a42d70 +- pypi: https://files.pythonhosted.org/packages/f4/34/a9dbe051de88a63eb7408ea66630bac38e72f7f6077d4be58737106860d9/virtualenv-21.3.3-py3-none-any.whl + name: virtualenv + version: 21.3.3 + sha256: 7d5987d8369e098e41406efb780a3d4ca79280097293899e351a6407ee153ab3 requires_dist: - - idna>=2.0 - - multidict>=4.0 - - propcache>=0.2.1 - requires_python: '>=3.10' + - distlib>=0.3.7,<1 + - filelock>=3.24.2,<4 ; python_full_version >= '3.10' + - filelock>=3.16.1,<=3.19.1 ; python_full_version < '3.10' + - importlib-metadata>=6.6 ; python_full_version < '3.8' + - platformdirs>=3.9.1,<5 + - python-discovery>=1.3.1 + - typing-extensions>=4.13.2 ; python_full_version < '3.11' + 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/f5/57/2a154a69d6642860300bf8eb205d13131104991f2b1065bbb9075ac5c32e/tof-26.3.0-py3-none-any.whl + name: tof + version: 26.3.0 + sha256: e89783a072b05fdb53d9e76fbf919dc8935e75e118fdaf17ca5cc33727ef002b + requires_dist: + - plopp>=23.10.0 + - pooch>=1.5.0 + - scipp>=25.1.0 + - lazy-loader>=0.3 + - pytest>=8.0 ; extra == 'test' + - scippneutron>=24.12.0 ; extra == 'test' + requires_python: '>=3.11' - pypi: https://files.pythonhosted.org/packages/f5/be/25216a49daeeb7af2bec0db22d5e7df08ed1d7c9f65d78b14f3b74fd72fc/yarl-1.23.0-cp312-cp312-win_amd64.whl name: yarl version: 1.23.0 @@ -12002,91 +12974,134 @@ packages: - multidict>=4.0 - propcache>=0.2.1 requires_python: '>=3.10' -- conda: https://conda.anaconda.org/conda-forge/linux-64/zeromq-4.3.5-h41580af_10.conda - sha256: 325d370b28e2b9cc1f765c5b4cdb394c91a5d958fbd15da1a14607a28fee09f6 - md5: 755b096086851e1193f3b10347415d7c - depends: - - libgcc >=14 - - __glibc >=2.17,<3.0.a0 - - libstdcxx >=14 - - krb5 >=1.22.2,<1.23.0a0 - - libsodium >=1.0.21,<1.0.22.0a0 - license: MPL-2.0 - license_family: MOZILLA - purls: [] - size: 311150 - timestamp: 1772476812121 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/zeromq-4.3.5-h4818236_10.conda - sha256: 2705360c72d4db8de34291493379ffd13b09fd594d0af20c9eefa8a3f060d868 - md5: e85dcd3bde2b10081cdcaeae15797506 - depends: - - __osx >=11.0 - - libcxx >=19 - - krb5 >=1.22.2,<1.23.0a0 - - libsodium >=1.0.21,<1.0.22.0a0 - license: MPL-2.0 - license_family: MOZILLA - purls: [] - size: 245246 - timestamp: 1772476886668 -- conda: https://conda.anaconda.org/conda-forge/win-64/zeromq-4.3.5-h507cc87_10.conda - sha256: b8568dfde46edf3455458912ea6ffb760e4456db8230a0cf34ecbc557d3c275f - md5: 1ab0237036bfb14e923d6107473b0021 - depends: - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 - - ucrt >=10.0.20348.0 - - libsodium >=1.0.21,<1.0.22.0a0 - - krb5 >=1.22.2,<1.23.0a0 - license: MPL-2.0 - license_family: MOZILLA - purls: [] - size: 265665 - timestamp: 1772476832995 -- conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.1-pyhcf101f3_0.conda - sha256: 523616c0530d305d2216c2b4a8dfd3872628b60083255b89c5e0d8c42e738cca - md5: e1c36c6121a7c9c76f2f148f1e83b983 - depends: - - python >=3.10 - - python - license: MIT - license_family: MIT - purls: - - pkg:pypi/zipp?source=compressed-mapping - size: 24461 - timestamp: 1776131454755 -- conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda - sha256: 68f0206ca6e98fea941e5717cec780ed2873ffabc0e1ed34428c061e2c6268c7 - md5: 4a13eeac0b5c8e5b8ab496e6c4ddd829 - depends: - - __glibc >=2.17,<3.0.a0 - - libzlib >=1.3.1,<2.0a0 - license: BSD-3-Clause - license_family: BSD - purls: [] - size: 601375 - timestamp: 1764777111296 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hbf9d68e_6.conda - sha256: 9485ba49e8f47d2b597dd399e88f4802e100851b27c21d7525625b0b4025a5d9 - md5: ab136e4c34e97f34fb621d2592a393d8 - depends: - - __osx >=11.0 - - libzlib >=1.3.1,<2.0a0 - license: BSD-3-Clause - license_family: BSD - purls: [] - size: 433413 - timestamp: 1764777166076 -- conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_6.conda - sha256: 368d8628424966fd8f9c8018326a9c779e06913dd39e646cf331226acc90e5b2 - md5: 053b84beec00b71ea8ff7a4f84b55207 - depends: - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 - - ucrt >=10.0.20348.0 - - libzlib >=1.3.1,<2.0a0 - license: BSD-3-Clause - license_family: BSD - purls: [] - size: 388453 - timestamp: 1764777142545 +- pypi: https://files.pythonhosted.org/packages/f6/f0/10642828a8dfb741e5f3fbaac830550a518a775c7fff6f04a007259b0548/py-1.11.0-py2.py3-none-any.whl + name: py + version: 1.11.0 + sha256: 607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378 + requires_python: '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*' +- pypi: https://files.pythonhosted.org/packages/f7/00/bbca25f8a2372465cdf93138c1e1e38dff045fb0afef1488f395d0afcb3b/ncrystal-4.4.2-py3-none-any.whl + name: ncrystal + version: 4.4.2 + sha256: e02fa7d743addc3fbea23287737a88b8d01192450fdca51554d3f9032fe4617c + requires_dist: + - ncrystal-core==4.4.2 + - ncrystal-python==4.4.2 + - spglib>=2.1.0 ; extra == 'composer' + - ase>=3.23.0 ; extra == 'cif' + - gemmi>=0.6.1 ; extra == 'cif' + - spglib>=2.1.0 ; extra == 'cif' + - endf-parserpy>=0.14.3 ; extra == 'endf' + - matplotlib>=3.6.0 ; extra == 'plot' + - ase>=3.23.0 ; extra == 'all' + - endf-parserpy>=0.14.3 ; extra == 'all' + - gemmi>=0.6.1 ; extra == 'all' + - matplotlib>=3.6.0 ; extra == 'all' + - spglib>=2.1.0 ; extra == 'all' + - pyyaml>=6.0.0 ; extra == 'devel' + - ase>=3.23.0 ; extra == 'devel' + - cppcheck ; extra == 'devel' + - endf-parserpy>=0.14.3 ; extra == 'devel' + - gemmi>=0.6.1 ; extra == 'devel' + - matplotlib>=3.6.0 ; extra == 'devel' + - mpmath>=1.3.0 ; extra == 'devel' + - numpy>=1.22 ; extra == 'devel' + - pybind11>=2.11.0 ; extra == 'devel' + - ruff>=0.8.1 ; extra == 'devel' + - simple-build-system>=1.6.0 ; extra == 'devel' + - spglib>=2.1.0 ; extra == 'devel' + - tomli>=2.0.0 ; python_full_version < '3.11' and extra == 'devel' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl + name: ghp-import + version: 2.1.0 + sha256: 8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619 + requires_dist: + - python-dateutil>=2.8.1 + - twine ; extra == 'dev' + - markdown ; extra == 'dev' + - flake8 ; extra == 'dev' + - wheel ; extra == 'dev' +- pypi: https://files.pythonhosted.org/packages/f9/58/6e1b8f52fdc3184b47dc5037f5070d83a3d11042db1594b02d2a44d786c8/coverage-7.14.0-cp314-cp314-win_amd64.whl + name: coverage + version: 7.14.0 + sha256: 45e0f79d8351fa76e256716df91eab12890d32678b9590df7ae1042e4bd4cf5d + requires_dist: + - tomli ; python_full_version <= '3.11' and extra == 'toml' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/fa/bc/8b8ec5a4bfc5b9cf3ce27a118339e994f88410be5677c96493e0ea28e76d/dunamai-1.26.1-py3-none-any.whl + name: dunamai + version: 1.26.1 + sha256: 2727d939c5b4257cb01ea404372803b477f5176e5a347c43beaf89cd5072e853 + requires_dist: + - importlib-metadata>=1.6.0 ; python_full_version < '3.8' + - packaging>=20.9 + requires_python: '>=3.5' +- pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl + name: toolz + version: 1.1.0 + sha256: 15ccc861ac51c53696de0a5d6d4607f99c210739caf987b5d2054f3efed429d8 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl + name: aiosignal + version: 1.4.0 + sha256: 053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e + requires_dist: + - frozenlist>=1.1.0 + - typing-extensions>=4.2 ; python_full_version < '3.13' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl + name: pydantic-core + version: 2.46.4 + sha256: 811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac + requires_dist: + - typing-extensions>=4.14.1 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl + name: pydantic + version: 2.13.4 + sha256: 45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba + requires_dist: + - annotated-types>=0.6.0 + - pydantic-core==2.46.4 + - 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/fd/e1/3542a9cb596cadd76fcef413f19c79216e002623158befe6daa03dbfa88c/contourpy-1.3.3-cp314-cp314-macosx_11_0_arm64.whl + name: contourpy + version: 1.3.3 + sha256: cbedb772ed74ff5be440fa8eee9bd49f64f6e3fc09436d9c7d8f1c287b121d77 + requires_dist: + - numpy>=1.25 + - furo ; extra == 'docs' + - sphinx>=7.2 ; extra == 'docs' + - sphinx-copybutton ; extra == 'docs' + - bokeh ; extra == 'bokeh' + - selenium ; extra == 'bokeh' + - contourpy[bokeh,docs] ; extra == 'mypy' + - bokeh ; extra == 'mypy' + - docutils-stubs ; extra == 'mypy' + - mypy==1.17.0 ; extra == 'mypy' + - types-pillow ; extra == 'mypy' + - contourpy[test-no-images] ; extra == 'test' + - matplotlib ; extra == 'test' + - pillow ; extra == 'test' + - pytest ; extra == 'test-no-images' + - pytest-cov ; extra == 'test-no-images' + - pytest-rerunfailures ; extra == 'test-no-images' + - pytest-xdist ; extra == 'test-no-images' + - wurlitzer ; extra == 'test-no-images' + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/fe/3b/8ec5074bcfc450fe84273713b4b0a0dd47c0249358f5d82eb8104ffe2520/multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + name: multidict + version: 6.7.1 + sha256: 7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709 + requires_dist: + - typing-extensions>=4.1.0 ; python_full_version < '3.11' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/ff/1c/a28b27effb13a381fe077ea3e3e78f6debd6315f2b3edff67bbb93d0ef51/gemmi-0.7.5-cp314-cp314-win_amd64.whl + name: gemmi + version: 0.7.5 + sha256: 419c36d9ea0f28dda0ff0d6db17035170d0888ca78aff82a0f9f604613aec58f + requires_python: '>=3.9' diff --git a/pixi.toml b/pixi.toml index 546620d4a..70a8a310e 100644 --- a/pixi.toml +++ b/pixi.toml @@ -1,27 +1,3 @@ -####################### -# ENVIRONMENT VARIABLES -####################### - -# Platform-independent - -[activation.env] -PYTHONIOENCODING = 'utf-8' - -# Platform-specific - -# Ensures the main package is used from the source code during -# development, even if main package is installed in editable mode -# via uv. This is important because `pixi update` might replace -# the installed version with the latest released version. - -# Unix/macOS -[target.unix.activation.env] -PYTHONPATH = "${PIXI_PROJECT_ROOT}/src${PYTHONPATH:+:${PYTHONPATH}}" - -# Windows -[target.win.activation.env] -PYTHONPATH = "${PIXI_PROJECT_ROOT}/src;%PYTHONPATH%" - ########### # WORKSPACE ########### @@ -32,49 +8,65 @@ PYTHONPATH = "${PIXI_PROJECT_ROOT}/src;%PYTHONPATH%" platforms = ['win-64', 'linux-64', 'osx-arm64'] # Channels for fetching packages -channels = ['conda-forge'] +channels = ['nodefaults', 'conda-forge'] -##################### -# SYSTEM REQUIREMENTS -##################### +########## +# FEATURES +########## -[system-requirements] +# Default features: +[activation.env] +PYTHONIOENCODING = 'utf-8' + +[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) +# like `scipp` that only have wheels for macOS 14.0+ (macosx_14_0_arm64) # are used instead of building from source. This is a workaround for # Pixi, see https://github.com/prefix-dev/pixi/issues/5667 macos = '14.0' - # Set minimum supported version for glibc to be 2.35 to ensure packages # like `crysfml` that only have wheels for glibc 2.35+ # (manylinux_2_35_x86_64) are used. #libc = { family = 'glibc', version = '2.35' } libc = '2.35' -########## -# FEATURES -########## +# Non-default features: + +# Set specific Python versions to be used in CI testing. + +[feature.py-min.dependencies] +python = '3.12.*' + +[feature.py-max.dependencies] +python = '3.14.*' -# Default feature configuration +# Development dependencies for local development and testing with +# editable installations. -[dependencies] +[feature.dev.dependencies] nodejs = '*' # Required for Prettier (non-Python formatting) jupyterlab = '*' # Jupyter notebooks +ipython = '*' # Interactive Python shell pixi-kernel = '*' # Pixi Jupyter kernel gsl = '*' # GNU Scientific Library; required for diffpy.pdffit2 -[pypi-dependencies] # == [feature.default.pypi-dependencies] -pip = '*' # Native package installer +[feature.dev.pypi-dependencies] +pip = '*' #pycrysfml = { version = ">=0.4.0", index = "https://easyscience.github.io/pypi/" } easydiffraction = { path = '.', editable = true, extras = ['dev'] } -# Specific features: Set specific Python versions +# User-like behavior for testing with pip-installed dependencies instead +# of editable installations. -[feature.py-min.dependencies] -python = '3.12.*' -[feature.py-max.dependencies] -python = '3.14.*' +[feature.user.dependencies] +jupyterlab = '*' # Jupyter notebooks +ipython = '*' # Interactive Python shell +pixi-kernel = '*' # Pixi Jupyter kernel + +[feature.user.pypi-dependencies] +pip = '*' +easydiffraction = '*' ############## # ENVIRONMENTS @@ -84,12 +76,18 @@ python = '3.14.*' # The `default` feature is always included in all environments. # Additional features can be specified per environment. -py-312-env = { features = ['py-min'] } -py-314-env = { features = ['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 = ['py-max'] } +# Specific environments for CI testing with different Python versions. +py-312-env = { features = ['py-min', 'dev'] } +py-314-env = { features = ['py-max', 'dev'] } + +# The `default` environment is developer-oriented for local development +# and testing with editable installation of the current package. +default = { features = ['py-max', 'dev'] } + +# The `user` environment allows testing with pip-installed dependencies +# instead of editable installation of the current package. +user = { features = ['py-max', 'user'] } ####### # TASKS @@ -104,8 +102,8 @@ default = { features = ['py-max'] } unit-tests = 'python -m pytest tests/unit/ --color=yes -v' functional-tests = 'python -m pytest tests/functional/ --color=yes -v' integration-tests = 'python -m pytest tests/integration/ --color=yes -n auto -v' -script-tests = 'python -m pytest tools/test_scripts.py --color=yes -n auto -v' -notebook-tests = 'python -m pytest --nbmake docs/docs/tutorials/ --nbmake-timeout=1200 --color=yes -n auto -v' +script-tests = { cmd = 'python -m pytest tools/test_scripts.py --color=yes -n auto -v', env = { EASYDIFFRACTION_ARTIFACT_ROOT = 'tmp/tutorials' } } +notebook-tests = { cmd = 'python -m pytest --nbmake docs/docs/tutorials/ --nbmake-timeout=1200 --color=yes -n auto -v', env = { EASYDIFFRACTION_ARTIFACT_ROOT = 'tmp/tutorials' } } test = { depends-on = ['unit-tests', 'functional-tests'] } test-all = { depends-on = [ @@ -186,10 +184,16 @@ cov = { depends-on = [ # 📓 Notebook Management ######################## +python = { cmd = 'python', env = { EASYDIFFRACTION_ARTIFACT_ROOT = 'tmp/tutorials' } } +tutorial = { cmd = 'python', env = { EASYDIFFRACTION_ARTIFACT_ROOT = 'tmp/tutorials' } } +tutorial-benchmarks = { cmd = 'python tools/benchmark_tutorials.py', env = { EASYDIFFRACTION_ARTIFACT_ROOT = 'tmp/tutorials' } } +jupyter = { cmd = 'jupyter', env = { EASYDIFFRACTION_ARTIFACT_ROOT = 'tmp/tutorials' } } + notebook-convert = 'jupytext docs/docs/tutorials/*.py --from py:percent --to ipynb' notebook-strip = 'nbstripout docs/docs/tutorials/*.ipynb' notebook-tweak = 'python tools/tweak_notebooks.py docs/docs/tutorials/' -notebook-exec = 'python -m pytest --nbmake docs/docs/tutorials/ --nbmake-timeout=1200 --overwrite --color=yes -n auto -v' +notebook-exec = { cmd = 'python -m pytest --nbmake docs/docs/tutorials/ --overwrite --color=yes -n auto -v', env = { EASYDIFFRACTION_ARTIFACT_ROOT = 'tmp/tutorials' } } +notebook-exec-ci = { cmd = 'python -m pytest --nbmake docs/docs/tutorials/ --nbmake-timeout=1200 --overwrite --color=yes -n auto -v', env = { EASYDIFFRACTION_ARTIFACT_ROOT = '.' } } notebook-prepare = { depends-on = [ 'notebook-convert', diff --git a/pyproject.toml b/pyproject.toml index c0943384c..507a069d1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,10 +23,7 @@ classifiers = [ ] requires-python = '>=3.12' dependencies = [ - 'essdiffraction', # ESS-specific diffraction library 'numpy', # Numerical computing library - 'colorama', # Color terminal output - 'tabulate', # Pretty-print tabular data for terminal output 'asciichartpy', # ASCII charts for terminal output 'pooch', # Data downloader 'typer', # Command-line interface creation @@ -44,15 +41,18 @@ dependencies = [ 'diffpy.pdffit2', # Calculations of Pair Distribution Function (PDF) 'diffpy.utils', # Utilities for PDF calculations 'uncertainties', # Propagation of uncertainties + 'h5py', # HDF5 file handling 'typeguard', # Runtime type checking 'darkdetect', # Detecting dark mode (system-level) 'pandas', # Displaying tables in Jupyter notebooks 'plotly', # Interactive plots + 'arviz', # Bayesian analysis summaries and posterior plotting 'py3Dmol', # Visualisation of crystal structures ] [project.optional-dependencies] dev = [ + 'essdiffraction', # ESS-specific diffraction library 'GitPython', # Interact with Git repositories 'build', # Building the package 'pre-commit', # Pre-commit hooks @@ -63,7 +63,6 @@ dev = [ 'pytest', # Testing 'pytest-cov', # Test coverage 'pytest-xdist', # Enable parallel testing - 'pytest-benchmark', # Benchmarking tests 'ruff', # Linting and formatting code 'radon', # Code complexity and maintainability 'validate-pyproject[all]', # Validate pyproject.toml @@ -170,7 +169,7 @@ source = ['src'] # Limit coverage to the source code directory [tool.coverage.report] show_missing = true # Show missing lines skip_covered = false # Skip files with 100% coverage in the report -fail_under = 70 # Minimum coverage percentage to pass +fail_under = 65 # Minimum coverage percentage to pass ########################## # Configuration for pytest diff --git a/src/easydiffraction/__init__.py b/src/easydiffraction/__init__.py index 11ea117c8..0e69a9c7e 100644 --- a/src/easydiffraction/__init__.py +++ b/src/easydiffraction/__init__.py @@ -14,5 +14,6 @@ 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 list_data from easydiffraction.utils.utils import list_tutorials from easydiffraction.utils.utils import show_version diff --git a/src/easydiffraction/__main__.py b/src/easydiffraction/__main__.py index bd5ae9be5..a66d0d8f8 100644 --- a/src/easydiffraction/__main__.py +++ b/src/easydiffraction/__main__.py @@ -1,6 +1,8 @@ # SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +from __future__ import annotations + import sys # Ensure UTF-8 output on all platforms (e.g. Windows with cp1252) @@ -14,6 +16,90 @@ app = typer.Typer(add_completion=False) +_MIN_PROJECT_FIRST_ARG_COUNT = 2 +_PROJECT_COMMAND_NAMES = frozenset({'fit', 'display', 'undo'}) +_GLOBAL_COMMAND_NAMES = frozenset({ + 'list-data', + 'download-data', + 'list-tutorials', + 'download-tutorial', + 'download-all-tutorials', + *_PROJECT_COMMAND_NAMES, +}) + + +def _normalized_cli_args(args: list[str]) -> list[str]: + """Return CLI args rewritten to support project-first commands.""" + if len(args) < _MIN_PROJECT_FIRST_ARG_COUNT: + return args + + first_arg = args[0] + if first_arg.startswith('-') or first_arg in _GLOBAL_COMMAND_NAMES: + return args + + if args[1] not in _PROJECT_COMMAND_NAMES: + return args + + return [args[1], first_arg, *args[2:]] + + +def _load_project(project_dir: str) -> object: + """Load one saved project directory.""" + return ed.Project.load(project_dir) + + +def _display_project_patterns(project: object) -> None: + """Render default pattern views for all experiments.""" + for experiment in project.experiments: + project.display.pattern(expt_name=experiment.name) + + +def _project_fit_mode(project: object) -> str | None: + """Return the resolved fitting mode type for one project.""" + return getattr(project.analysis, 'fitting_mode_type', None) + + +def _project_result_kind(project: object) -> str | None: + """Return the resolved fit result kind for one project.""" + result_kind = getattr(getattr(project.analysis, 'fit_result', None), 'result_kind', None) + return getattr(result_kind, 'value', None) + + +def _display_fit_outputs(project: object) -> None: + """Render the standard post-fit CLI outputs.""" + if _project_fit_mode(project) != 'sequential': + project.display.fit.results() + project.display.fit.correlations() + _display_project_patterns(project) + + +def _display_project_outputs(project: object) -> None: + """Render the typical displays for the loaded project state.""" + if _project_fit_mode(project) == 'sequential': + project.display.fit.series() + _display_project_patterns(project) + return + + project.display.fit.results() + project.display.fit.correlations() + + if _project_result_kind(project) == 'bayesian': + if project.rendering.plotter.engine == 'plotly': + project.display.posterior.pairs() + project.display.posterior.distribution() + for experiment in project.experiments: + project.display.posterior.predictive(expt_name=experiment.name) + + _display_project_patterns(project) + + +def run_cli(args: list[str] | None = None) -> None: + """ + Run the EasyDiffraction CLI with project-first argument support. + """ + cli_args = list(sys.argv[1:] if args is None else args) + app(args=_normalized_cli_args(cli_args)) + @app.callback(invoke_without_command=True) def main( @@ -37,12 +123,38 @@ def main( # Otherwise, let the chosen subcommand execute. +@app.command('list-data') +def list_data() -> None: + """List available example data and project archives.""" + ed.list_data() + + @app.command('list-tutorials') def list_tutorials() -> None: """List available tutorial notebooks.""" ed.list_tutorials() +@app.command('download-data') +def download_data( + id: int = typer.Argument(..., help='Data ID to download.'), + destination: str = typer.Option( + 'data', + '--destination', + '-d', + help='Directory to save the data or extracted project into.', + ), + overwrite: bool = typer.Option( # noqa: FBT001 + False, # noqa: FBT003 + '--overwrite', + '-o', + help='Overwrite an existing file or extracted project if present.', + ), +) -> None: + """Download one example data record by ID.""" + ed.download_data(id=id, destination=destination, overwrite=overwrite) + + @app.command('download-tutorial') def download_tutorial( id: int = typer.Argument(..., help='Tutorial ID to download.'), @@ -82,6 +194,18 @@ def download_all_tutorials( ed.download_all_tutorials(destination=destination, overwrite=overwrite) +@app.command('display') +def display( + project_dir: str = typer.Argument( + ..., + help='Path to the project directory (must contain project.cif).', + ), +) -> None: + """Display the typical outputs for a saved project state.""" + project = _load_project(project_dir) + _display_project_outputs(project) + + @app.command('fit') def fit( project_dir: str = typer.Argument( @@ -94,17 +218,28 @@ def fit( help='Run fitting without saving results back to the project directory.', ), ) -> None: - """Fit a saved project: easydiffraction fit PROJECT_DIR [--dry].""" - project = ed.Project.load(project_dir) + """Fit a saved project: easydiffraction PROJECT_DIR fit [--dry].""" + project = _load_project(project_dir) if dry: project.info._path = None project.analysis.fit() - project.analysis.display.fit_results() - project.display.plotter.plot_param_correlations() - for expt in project.experiments: - project.display.plotter.plot_meas_vs_calc(expt_name=expt.name, show_residual=True) - # project.summary.show_report() + _display_fit_outputs(project) + + +@app.command('undo') +def undo( + project_dir: str = typer.Argument( + ..., + help='Path to the project directory (must contain project.cif).', + ), +) -> None: + """ + Undo the last fit when fit-history support exists (not implemented). + """ + _load_project(project_dir) + typer.echo('Undo is not yet implemented.') + raise typer.Exit(code=1) if __name__ == '__main__': - app() + run_cli() diff --git a/src/easydiffraction/analysis/__init__.py b/src/easydiffraction/analysis/__init__.py index 78150ea54..f9d5e94de 100644 --- a/src/easydiffraction/analysis/__init__.py +++ b/src/easydiffraction/analysis/__init__.py @@ -1,2 +1,68 @@ # SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause + +from easydiffraction.analysis.categories.bayesian_convergence import BayesianConvergence +from easydiffraction.analysis.categories.bayesian_convergence import BayesianConvergenceFactory +from easydiffraction.analysis.categories.bayesian_distribution_caches import ( + BayesianDistributionCacheItem, +) +from easydiffraction.analysis.categories.bayesian_distribution_caches import ( + BayesianDistributionCaches, +) +from easydiffraction.analysis.categories.bayesian_distribution_caches import ( + BayesianDistributionCachesFactory, +) +from easydiffraction.analysis.categories.bayesian_pair_caches import BayesianPairCacheItem +from easydiffraction.analysis.categories.bayesian_pair_caches import BayesianPairCaches +from easydiffraction.analysis.categories.bayesian_pair_caches import BayesianPairCachesFactory +from easydiffraction.analysis.categories.bayesian_parameter_posteriors import ( + BayesianParameterPosteriorItem, +) +from easydiffraction.analysis.categories.bayesian_parameter_posteriors import ( + BayesianParameterPosteriors, +) +from easydiffraction.analysis.categories.bayesian_parameter_posteriors import ( + BayesianParameterPosteriorsFactory, +) +from easydiffraction.analysis.categories.bayesian_predictive_datasets import ( + BayesianPredictiveDatasetItem, +) +from easydiffraction.analysis.categories.bayesian_predictive_datasets import ( + BayesianPredictiveDatasets, +) +from easydiffraction.analysis.categories.bayesian_predictive_datasets import ( + BayesianPredictiveDatasetsFactory, +) +from easydiffraction.analysis.categories.bayesian_result import BayesianResult +from easydiffraction.analysis.categories.bayesian_result import BayesianResultFactory +from easydiffraction.analysis.categories.bayesian_sampler import BayesianSampler +from easydiffraction.analysis.categories.bayesian_sampler import BayesianSamplerFactory +from easydiffraction.analysis.categories.deterministic_result import DeterministicResult +from easydiffraction.analysis.categories.deterministic_result import DeterministicResultFactory +from easydiffraction.analysis.categories.fit_parameter_correlations import ( + FitParameterCorrelationItem, +) +from easydiffraction.analysis.categories.fit_parameter_correlations import FitParameterCorrelations +from easydiffraction.analysis.categories.fit_parameter_correlations import ( + FitParameterCorrelationsFactory, +) +from easydiffraction.analysis.categories.fit_parameters import FitParameterItem +from easydiffraction.analysis.categories.fit_parameters import FitParameters +from easydiffraction.analysis.categories.fit_parameters import FitParametersFactory +from easydiffraction.analysis.categories.fit_result import FitResult +from easydiffraction.analysis.categories.fit_result import FitResultFactory +from easydiffraction.analysis.categories.fitting import Fitting +from easydiffraction.analysis.categories.fitting import FittingFactory +from easydiffraction.analysis.categories.joint_fit import JointFitCollection +from easydiffraction.analysis.categories.joint_fit import JointFitFactory +from easydiffraction.analysis.categories.joint_fit import JointFitItem +from easydiffraction.analysis.categories.sequential_fit import SequentialFit +from easydiffraction.analysis.categories.sequential_fit import SequentialFitFactory +from easydiffraction.analysis.categories.sequential_fit_extract import ( + SequentialFitExtractCollection, +) +from easydiffraction.analysis.categories.sequential_fit_extract import SequentialFitExtractFactory +from easydiffraction.analysis.categories.sequential_fit_extract import SequentialFitExtractItem +from easydiffraction.analysis.enums import FitCorrelationSourceEnum +from easydiffraction.analysis.enums import FitModeEnum +from easydiffraction.analysis.enums import FitResultKindEnum diff --git a/src/easydiffraction/analysis/analysis.py b/src/easydiffraction/analysis/analysis.py index c132e409c..eccbf28d8 100644 --- a/src/easydiffraction/analysis/analysis.py +++ b/src/easydiffraction/analysis/analysis.py @@ -1,98 +1,89 @@ # SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +from __future__ import annotations + from contextlib import suppress +from itertools import combinations +from pathlib import Path import numpy as np import pandas as pd from easydiffraction.analysis.categories.aliases.factory import AliasesFactory +from easydiffraction.analysis.categories.bayesian_convergence import BayesianConvergence +from easydiffraction.analysis.categories.bayesian_distribution_caches import ( + BayesianDistributionCaches, +) +from easydiffraction.analysis.categories.bayesian_pair_caches import BayesianPairCaches +from easydiffraction.analysis.categories.bayesian_pair_caches.default import BayesianPairCachePaths +from easydiffraction.analysis.categories.bayesian_parameter_posteriors import ( + BayesianParameterPosteriors, +) +from easydiffraction.analysis.categories.bayesian_predictive_datasets import ( + BayesianPredictiveDatasets, +) +from easydiffraction.analysis.categories.bayesian_predictive_datasets.default import ( + BayesianPredictiveDatasetPaths, +) +from easydiffraction.analysis.categories.bayesian_result import BayesianResult +from easydiffraction.analysis.categories.bayesian_sampler import BayesianSampler from easydiffraction.analysis.categories.constraints.factory import ConstraintsFactory -from easydiffraction.analysis.categories.fit import Fit -from easydiffraction.analysis.categories.fit import FitFactory -from easydiffraction.analysis.categories.fit import FitModeEnum -from easydiffraction.analysis.categories.joint_fit_experiments import JointFitExperiments -from easydiffraction.analysis.fit_helpers.tracking import _make_display_handle +from easydiffraction.analysis.categories.deterministic_result import DeterministicResult +from easydiffraction.analysis.categories.fit_parameter_correlations import FitParameterCorrelations +from easydiffraction.analysis.categories.fit_parameters import FitParameters +from easydiffraction.analysis.categories.fit_result import FitResult +from easydiffraction.analysis.categories.fitting import Fitting +from easydiffraction.analysis.categories.fitting import FittingFactory +from easydiffraction.analysis.categories.joint_fit import JointFitCollection +from easydiffraction.analysis.categories.sequential_fit import SequentialFit +from easydiffraction.analysis.categories.sequential_fit import SequentialFitFactory +from easydiffraction.analysis.categories.sequential_fit_extract import ( + SequentialFitExtractCollection, +) +from easydiffraction.analysis.enums import FitCorrelationSourceEnum +from easydiffraction.analysis.enums import FitModeEnum +from easydiffraction.analysis.enums import FitResultKindEnum +from easydiffraction.analysis.fit_helpers.bayesian import BayesianFitResults +from easydiffraction.analysis.fit_helpers.bayesian import PosteriorParameterSummary +from easydiffraction.analysis.fit_helpers.bayesian import PosteriorPredictiveSummary +from easydiffraction.analysis.fit_helpers.bayesian import PosteriorSamples +from easydiffraction.analysis.fit_helpers.reporting import FitResults from easydiffraction.analysis.fitting import Fitter -from easydiffraction.core.guard import GuardedBase +from easydiffraction.analysis.minimizers.enums import MinimizerTypeEnum +from easydiffraction.core.category_owner import CategoryOwner +from easydiffraction.core.guard import _apply_help_filter 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.item.base import intensity_category_for +from easydiffraction.display.progress import make_display_handle from easydiffraction.display.tables import TableRenderer from easydiffraction.io.cif.serialize import analysis_to_cif from easydiffraction.utils.enums import VerbosityEnum from easydiffraction.utils.logging import console from easydiffraction.utils.logging import log +from easydiffraction.utils.utils import _help_method_rows +from easydiffraction.utils.utils import _help_property_rows from easydiffraction.utils.utils import render_cif +from easydiffraction.utils.utils import render_object_help from easydiffraction.utils.utils import render_table +_SUMMARY_HIDDEN_PARAMETER_CATEGORIES = frozenset({'pd_data', 'total_data', 'refln'}) +_POSTERIOR_SAMPLE_NDIM = 3 +_FLATTENED_POSTERIOR_SAMPLE_NDIM = 2 +_CREDIBLE_INTERVAL_LEVEL_COUNT = 2 -def _discover_property_rows(cls: type) -> list[list[str]]: - """ - Discover public properties from the class MRO. - - Parameters - ---------- - cls : type - The class to inspect. - - Returns - ------- - list[list[str]] - Table rows with ``[index, name, writable, description]``. - """ - seen: 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: - seen[key] = attr - rows = [] - for i, key in enumerate(sorted(seen), 1): - prop = seen[key] - writable = '✓' if prop.fset else '✗' - doc = GuardedBase._first_sentence(prop.fget.__doc__ if prop.fget else None) - rows.append([str(i), key, writable, doc]) - return rows +def _discover_property_rows(cls: type) -> list[list[str]]: + """Return public property rows for analysis help tables.""" + return _help_property_rows(cls) def _discover_method_rows(cls: type) -> list[list[str]]: - """ - Discover public methods from the class MRO. - - Parameters - ---------- - cls : type - The class to inspect. - - Returns - ------- - list[list[str]] - Table rows with ``[index, name(), description]``. - """ - 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)) - - rows = [] - for i, (key, method) in enumerate(sorted(methods_list), 1): - doc = GuardedBase._first_sentence(getattr(method, '__doc__', None)) - rows.append([str(i), f'{key}()', doc]) - return rows + """Return public method rows for analysis help tables.""" + return _help_method_rows(cls) class AnalysisDisplay: @@ -102,9 +93,13 @@ class AnalysisDisplay: Accessed via ``analysis.display``. """ - def __init__(self, analysis: 'Analysis') -> None: + def __init__(self, analysis: Analysis) -> None: self._analysis = analysis + def help(self) -> None: + """Print available analysis-display methods.""" + render_object_help(self) + def _flush_structure_categories(self) -> None: """ Flush pending category updates so symmetry flags are fresh. @@ -114,12 +109,23 @@ def _flush_structure_categories(self) -> None: structure._need_categories_update = True structure._update_categories() + @staticmethod + def _summary_parameters( + params: list[StringDescriptor | NumericDescriptor | Parameter], + ) -> list[StringDescriptor | NumericDescriptor | Parameter]: + """Return parameters suitable for compact summary displays.""" + return [ + param + for param in params + if param._identity.category_code not in _SUMMARY_HIDDEN_PARAMETER_CATEGORIES + ] + def all_params(self) -> None: """Print all parameters for structures and experiments.""" project = self._analysis.project self._flush_structure_categories() - structures_params = project.structures.parameters - experiments_params = project.experiments.parameters + structures_params = self._summary_parameters(project.structures.parameters) + experiments_params = self._summary_parameters(project.experiments.parameters) if not structures_params and not experiments_params: log.warning('No parameters found.') @@ -136,15 +142,17 @@ def all_params(self) -> None: 'fittable', ] - console.paragraph('All parameters for all structures (🧩 data blocks)') - df = Analysis._get_params_as_dataframe(structures_params) - filtered_df = df[filtered_headers] - tabler.render(filtered_df) + if structures_params: + console.paragraph('All parameters for all structures (🧩 data blocks)') + df = Analysis._get_params_as_dataframe(structures_params) + filtered_df = df[filtered_headers] + tabler.render(filtered_df) - console.paragraph('All parameters for all experiments (🔬 data blocks)') - df = Analysis._get_params_as_dataframe(experiments_params) - filtered_df = df[filtered_headers] - tabler.render(filtered_df) + if experiments_params: + console.paragraph('All parameters for all experiments (🔬 data blocks)') + df = Analysis._get_params_as_dataframe(experiments_params) + filtered_df = df[filtered_headers] + tabler.render(filtered_df) def fittable_params(self) -> None: """Print all fittable parameters.""" @@ -170,23 +178,27 @@ def fittable_params(self) -> None: 'free', ] - console.paragraph('Fittable parameters for all structures (🧩 data blocks)') - df = Analysis._get_params_as_dataframe(structures_params) - filtered_df = df[filtered_headers] - tabler.render(filtered_df) + if structures_params: + console.paragraph('Fittable parameters for all structures (🧩 data blocks)') + df = Analysis._get_params_as_dataframe(structures_params) + filtered_df = df[filtered_headers] + tabler.render(filtered_df) - console.paragraph('Fittable parameters for all experiments (🔬 data blocks)') - df = Analysis._get_params_as_dataframe(experiments_params) - filtered_df = df[filtered_headers] - tabler.render(filtered_df) + if experiments_params: + console.paragraph('Fittable parameters for all experiments (🔬 data blocks)') + df = Analysis._get_params_as_dataframe(experiments_params) + filtered_df = df[filtered_headers] + tabler.render(filtered_df) def free_params(self) -> None: """Print only currently free (varying) parameters.""" project = self._analysis.project self._flush_structure_categories() - structures_params = project.structures.free_parameters - experiments_params = project.experiments.free_parameters - free_params = structures_params + experiments_params + free_params = getattr(project, 'free_parameters', None) + if free_params is None: + structures_params = project.structures.free_parameters + experiments_params = project.experiments.free_parameters + free_params = structures_params + experiments_params if not free_params: log.warning('No free parameters found.') @@ -221,14 +233,14 @@ def how_to_access_parameters(self) -> None: code. """ project = self._analysis.project - structures_params = project.structures.parameters - experiments_params = project.experiments.parameters + structures_params = self._summary_parameters(project.structures.parameters) + experiments_params = self._summary_parameters(project.experiments.parameters) all_params = { 'structures': structures_params, 'experiments': experiments_params, } - if not all_params: + if not structures_params and not experiments_params: log.warning('No parameters found.') return @@ -287,14 +299,14 @@ def parameter_cif_uids(self) -> None: creating CIF-based constraints. """ project = self._analysis.project - structures_params = project.structures.parameters - experiments_params = project.experiments.parameters + structures_params = self._summary_parameters(project.structures.parameters) + experiments_params = self._summary_parameters(project.experiments.parameters) all_params = { 'structures': structures_params, 'experiments': experiments_params, } - if not all_params: + if not structures_params and not experiments_params: log.warning('No parameters found.') return @@ -340,20 +352,7 @@ def parameter_cif_uids(self) -> None: def constraints(self) -> None: """Print a table of all user-defined symbolic constraints.""" - analysis = self._analysis - if not analysis.constraints._items: - log.warning('No constraints defined.') - return - - rows = [[constraint.expression.value] for constraint in analysis.constraints] - - console.paragraph('User defined constraints') - render_table( - columns_headers=['expression'], - columns_alignment=['left'], - columns_data=rows, - ) - console.print(f'Constraints enabled: {analysis.constraints.enabled}') + self._analysis.constraints.show() def fit_results(self) -> None: """ @@ -379,13 +378,114 @@ def fit_results(self) -> None: def as_cif(self) -> None: """Render the analysis section as CIF in console.""" - cif_text: str = self._analysis.as_cif() - paragraph_title: str = 'Analysis 🧮 info as cif' - console.paragraph(paragraph_title) - render_cif(cif_text) + self._analysis.show_as_cif() + + +class _AnalysisOwnerAccessorsMixin: + @property + def project(self) -> object: + """Project that owns this analysis section.""" + return self._project + + @property + def aliases(self) -> object: + """Alias mappings used by symbolic constraints and displays.""" + return self._aliases + + @property + def constraints(self) -> object: + """Symbolic constraints owned by this analysis section.""" + return self._constraints + + @property + def display(self) -> AnalysisDisplay: + """Display helper for parameter tables, CIF, and fit results.""" + return self._display + + @property + def fitter(self) -> Fitter: + """Fitting engine used by this analysis object.""" + return self._fitter + + @fitter.setter + def fitter(self, value: Fitter) -> None: + self._fitter = value + + @property + def fit_results(self) -> object | None: + """Results from the most recent fit, if any.""" + if self._fit_results is None and self._has_persisted_fit_state(): + self._restore_fit_results_from_projection() + return self._fit_results + @fit_results.setter + def fit_results(self, value: object | None) -> None: + self._fit_results = value + self._fitter.results = value + + +class _AnalysisPersistedCategoryAccessorsMixin: + @property + def fit_parameters(self) -> FitParameters: + """Persisted fit-parameter control snapshots.""" + return self._fit_parameters + + @property + def fit_result(self) -> FitResult: + """Persisted common fit-result status metadata.""" + return self._fit_result + + @property + def fit_parameter_correlations(self) -> FitParameterCorrelations: + """Persisted fit-parameter correlation summaries.""" + return self._fit_parameter_correlations + + @property + def deterministic_result(self) -> DeterministicResult: + """Persisted deterministic fit-result metadata.""" + return self._deterministic_result + + @property + def bayesian_result(self) -> BayesianResult: + """Persisted Bayesian fit-result metadata.""" + return self._bayesian_result + + @property + def bayesian_sampler(self) -> BayesianSampler: + """Persisted Bayesian sampler settings.""" + return self._bayesian_sampler + + @property + def bayesian_convergence(self) -> BayesianConvergence: + """Persisted Bayesian convergence diagnostics.""" + return self._bayesian_convergence + + @property + def bayesian_parameter_posteriors(self) -> BayesianParameterPosteriors: + """Persisted Bayesian parameter posterior summaries.""" + return self._bayesian_parameter_posteriors + + @property + def bayesian_distribution_caches(self) -> BayesianDistributionCaches: + """Persisted Bayesian distribution-cache manifests.""" + return self._bayesian_distribution_caches + + @property + def bayesian_pair_caches(self) -> BayesianPairCaches: + """Persisted Bayesian pair-cache manifests.""" + return self._bayesian_pair_caches -class Analysis: + @property + def bayesian_predictive_datasets(self) -> BayesianPredictiveDatasets: + """Persisted Bayesian predictive-dataset manifests.""" + return self._bayesian_predictive_datasets + + +class Analysis( + _AnalysisOwnerAccessorsMixin, + _AnalysisPersistedCategoryAccessorsMixin, + CategoryOwner, +): """ High-level orchestration of analysis tasks for a Project. @@ -403,49 +503,421 @@ def __init__(self, project: object) -> None: project : object The project that owns models and experiments. """ - self.project = project + super().__init__() + self._project = project self._aliases_type: str = AliasesFactory.default_tag() - self.aliases = AliasesFactory.create(self._aliases_type) + 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: Fit = FitFactory.create(FitFactory.default_tag()) - self._fit._parent = self - self._joint_fit_experiments = JointFitExperiments() - self.fitter = Fitter(self._fit.minimizer_type.value) - self.fit_results = None + self._constraints = ConstraintsFactory.create(self._constraints_type) + self._constraints_handler = ConstraintsHandler.get() + self._fitting: Fitting = FittingFactory.create(FittingFactory.default_tag()) + self._fitting_mode_type: FitModeEnum = FitModeEnum.default() + self._joint_fit: JointFitCollection = JointFitCollection() + self._sequential_fit: SequentialFit = SequentialFitFactory.create( + SequentialFitFactory.default_tag() + ) + self._sequential_fit_extract = SequentialFitExtractCollection() + self._fit_parameters = FitParameters() + self._fit_result = FitResult() + self._fit_parameter_correlations = FitParameterCorrelations() + self._deterministic_result = DeterministicResult() + self._bayesian_result = BayesianResult() + self._bayesian_sampler = BayesianSampler() + self._bayesian_convergence = BayesianConvergence() + self._bayesian_parameter_posteriors = BayesianParameterPosteriors() + self._bayesian_distribution_caches = BayesianDistributionCaches() + self._bayesian_pair_caches = BayesianPairCaches() + self._bayesian_predictive_datasets = BayesianPredictiveDatasets() + self._has_persisted_fit_state_data = False + self._persisted_fit_state_sidecar: dict[str, object] = {} + self._fitter = Fitter(self._fitting.minimizer_type.value) + self._fit_results = None self._parameter_snapshots: dict[str, dict[str, dict]] = {} self._display = AnalysisDisplay(self) - @property - def display(self) -> AnalysisDisplay: - """Display helper for parameter tables, CIF, and fit results.""" - return self._display + @staticmethod + def _predictive_cache_key( + experiment_name: str, + x_axis_name: str, + *, + include_draws: bool = True, + ) -> str: + """Return the runtime cache key for one predictive summary.""" + key_suffix = 'draws' if include_draws else 'band' + return f'{experiment_name}:{x_axis_name}:{key_suffix}' + + def _live_parameter_map(self) -> dict[str, Parameter]: + """Return live parameters keyed by unique name.""" + all_parameters = self.project.structures.parameters + self.project.experiments.parameters + return { + param.unique_name: param + for param in all_parameters + if isinstance(param, Parameter) and hasattr(param, 'unique_name') + } + + def _ordered_restored_parameter_names(self) -> list[str]: + """ + Return persisted parameter names in display and array order. + """ + if self.fit_result.result_kind.value == FitResultKindEnum.BAYESIAN.value: + posterior_rows = list(self.bayesian_parameter_posteriors) + if posterior_rows: + return [row.unique_name.value for row in posterior_rows] + + return [row.param_unique_name.value for row in self.fit_parameters] + + def _restore_live_parameter_state(self, param_map: dict[str, Parameter]) -> None: + """Restore saved fit metadata onto live parameter objects.""" + for row in self.fit_parameters: + parameter = param_map.get(row.param_unique_name.value) + if parameter is None: + log.warning( + 'Persisted fit-state references unknown parameter ' + f'{row.param_unique_name.value!r}.' + ) + continue + + parameter.fit_min = row.fit_min.value + parameter.fit_max = row.fit_max.value + parameter._set_fit_bounds_uncertainty_multiplier( + row.fit_bounds_uncertainty_multiplier.value + ) + parameter._fit_start_value = row.start_value.value + parameter._fit_start_uncertainty = row.start_uncertainty.value + + for row in self.bayesian_parameter_posteriors: + parameter = param_map.get(row.unique_name.value) + if parameter is None or row.uncertainty.value is None: + continue + parameter.uncertainty = float(row.uncertainty.value) + + def _sync_live_minimizer_from_persisted_fit_state(self) -> None: + """Apply saved sampler settings to the live minimizer.""" + if not self._has_persisted_fit_state(): + return + + if self.fit_result.result_kind.value != FitResultKindEnum.BAYESIAN.value: + return + + if self.fitting.minimizer_type.value != MinimizerTypeEnum.BUMPS_DREAM.value: + return + + minimizer = self.fitting.minimizer + if minimizer is None: + return + + steps = int(self.bayesian_sampler.steps.value) + if steps <= 0: + return + + minimizer.steps = steps + minimizer.burn = int(self.bayesian_sampler.burn.value) + + thin = int(self.bayesian_sampler.thin.value) + if thin > 0: + minimizer.thin = thin + + pop = int(self.bayesian_sampler.pop.value) + if pop > 0: + minimizer.pop = pop + + minimizer.parallel = int(self.bayesian_sampler.parallel.value) + + init_value = str(self.bayesian_sampler.init.value) + if init_value: + minimizer.init = init_value + + def _restored_fit_parameters(self, param_map: dict[str, Parameter]) -> list[Parameter]: + """Return live parameters in the persisted fit-result order.""" + restored_parameters: list[Parameter] = [] + for unique_name in self._ordered_restored_parameter_names(): + parameter = param_map.get(unique_name) + if parameter is not None: + restored_parameters.append(parameter) + return restored_parameters + + def _restored_posterior_samples(self) -> PosteriorSamples | None: + """Return restored posterior samples from the HDF5 sidecar.""" + if not self.bayesian_result.has_posterior_samples.value: + return None + + posterior_data = self._persisted_fit_state_sidecar.get('posterior', {}) + parameter_samples = posterior_data.get('parameter_samples') + if parameter_samples is None: + return None + + posterior_rows = list(self.bayesian_parameter_posteriors) + parameter_names = [row.unique_name.value for row in posterior_rows] + if not parameter_names: + parameter_names = [row.param_unique_name.value for row in self.fit_parameters] + + parameter_sample_array = np.asarray(parameter_samples, dtype=float) + if parameter_sample_array.ndim != _POSTERIOR_SAMPLE_NDIM: + log.warning('Persisted posterior samples have an invalid shape for restore.') + return None + if parameter_sample_array.shape[2] != len(parameter_names): + log.warning( + 'Persisted posterior samples do not match restored posterior parameter names.' + ) + return None + + log_posterior = posterior_data.get('log_posterior') + draw_index = posterior_data.get('draw_index') + return PosteriorSamples( + parameter_names=parameter_names, + parameter_samples=parameter_sample_array, + log_posterior=( + None if log_posterior is None else np.asarray(log_posterior, dtype=float) + ), + draw_index=None if draw_index is None else np.asarray(draw_index), + ) + + def _restored_posterior_summaries(self) -> list[PosteriorParameterSummary]: + """Return posterior summary rows as runtime summary objects.""" + return [ + PosteriorParameterSummary( + unique_name=row.unique_name.value, + display_name=row.display_name.value, + best_sample_value=float(row.best_sample_value.value), + median=float(row.median.value), + standard_deviation=float(row.uncertainty.value), + interval_68=( + float(row.interval_68_lower.value), + float(row.interval_68_upper.value), + ), + interval_95=( + float(row.interval_95_lower.value), + float(row.interval_95_upper.value), + ), + ess_bulk=row.ess_bulk.value, + r_hat=row.r_hat.value, + ) + for row in self.bayesian_parameter_posteriors + ] + + def _restored_predictive_summaries(self) -> dict[str, PosteriorPredictiveSummary]: + """Return restored predictive summaries for runtime reuse.""" + restored_predictive: dict[str, PosteriorPredictiveSummary] = {} + predictive_data = self._persisted_fit_state_sidecar.get('predictive_datasets', {}) + for row in self.bayesian_predictive_datasets: + experiment_name = str(row.experiment_name.value) + x_axis_name = str(row.x_axis_name.value) + dataset = predictive_data.get(experiment_name) + if dataset is None: + continue + + summary = PosteriorPredictiveSummary( + experiment_name=experiment_name, + x_axis_name=x_axis_name, + x=np.asarray(dataset['x'], dtype=float), + best_sample_prediction=np.asarray( + dataset['best_sample_prediction'], + dtype=float, + ), + lower_95=( + None + if dataset.get('lower_95') is None + else np.asarray(dataset['lower_95'], dtype=float) + ), + upper_95=( + None + if dataset.get('upper_95') is None + else np.asarray(dataset['upper_95'], dtype=float) + ), + lower_68=( + None + if dataset.get('lower_68') is None + else np.asarray(dataset['lower_68'], dtype=float) + ), + upper_68=( + None + if dataset.get('upper_68') is None + else np.asarray(dataset['upper_68'], dtype=float) + ), + draws=( + None + if dataset.get('draws') is None + else np.asarray(dataset['draws'], dtype=float) + ), + ) + restored_predictive[experiment_name] = summary + restored_predictive[ + self._predictive_cache_key( + experiment_name, + x_axis_name, + include_draws=False, + ) + ] = summary + if summary.draws is not None: + restored_predictive[ + self._predictive_cache_key( + experiment_name, + x_axis_name, + include_draws=True, + ) + ] = summary + return restored_predictive + + def _restore_fit_results_from_projection(self) -> object | None: + """Rebuild a runtime fit-result object from saved state.""" + if not self._has_persisted_fit_state(): + return None + + param_map = self._live_parameter_map() + self._restore_live_parameter_state(param_map) + restored_parameters = self._restored_fit_parameters(param_map) + fitting_time = self.fit_result.fitting_time.value + reduced_chi_square = self.fit_result.reduced_chi_square.value + + if self.fit_result.result_kind.value == FitResultKindEnum.BAYESIAN.value: + restored_results = BayesianFitResults( + success=bool(self.fit_result.success.value), + parameters=restored_parameters, + reduced_chi_square=reduced_chi_square, + starting_parameters=list(restored_parameters), + fitting_time=fitting_time, + sampler_name=self.bayesian_result.sampler_name.value, + point_estimate_name=self.bayesian_result.point_estimate_name.value, + posterior_samples=self._restored_posterior_samples(), + posterior_parameter_summaries=self._restored_posterior_summaries(), + posterior_predictive=self._restored_predictive_summaries(), + credible_interval_levels=( + float(self.bayesian_result.credible_interval_inner.value), + float(self.bayesian_result.credible_interval_outer.value), + ), + sampler_settings={ + 'steps': int(self.bayesian_sampler.steps.value), + 'burn': int(self.bayesian_sampler.burn.value), + 'thin': int(self.bayesian_sampler.thin.value), + 'pop': int(self.bayesian_sampler.pop.value), + 'parallel': int(self.bayesian_sampler.parallel.value), + 'init': self.bayesian_sampler.init.value, + 'random_seed': self.bayesian_sampler.random_seed.value, + }, + convergence_diagnostics={ + 'converged': bool(self.bayesian_convergence.converged.value), + 'max_r_hat': self.bayesian_convergence.max_r_hat.value, + 'min_ess_bulk': self.bayesian_convergence.min_ess_bulk.value, + 'n_draws': int(self.bayesian_convergence.n_draws.value), + 'n_chains': int(self.bayesian_convergence.n_chains.value), + 'n_parameters': int(self.bayesian_convergence.n_parameters.value), + }, + sampler_completed=bool(self.bayesian_result.sampler_completed.value), + best_log_posterior=self.bayesian_result.best_log_posterior.value, + ) + restored_results.message = self.fit_result.message.value + restored_results.iterations = int(self.fit_result.iterations.value) + self.fit_results = restored_results + return restored_results + + restored_results = FitResults( + success=bool(self.fit_result.success.value), + parameters=restored_parameters, + reduced_chi_square=reduced_chi_square, + starting_parameters=list(restored_parameters), + fitting_time=fitting_time, + optimizer_name=self.deterministic_result.optimizer_name.value, + method_name=self.deterministic_result.method_name.value, + objective_name=self.deterministic_result.objective_name.value, + objective_value=self.deterministic_result.objective_value.value, + n_data_points=int(self.deterministic_result.n_data_points.value), + n_parameters=int(self.deterministic_result.n_parameters.value), + n_free_parameters=int(self.deterministic_result.n_free_parameters.value), + degrees_of_freedom=int(self.deterministic_result.degrees_of_freedom.value), + covariance_available=bool(self.deterministic_result.covariance_available.value), + correlation_available=bool(self.deterministic_result.correlation_available.value), + ) + restored_results.message = self.fit_result.message.value + restored_results.iterations = int(self.fit_result.iterations.value) + restored_results.chi_square = self.deterministic_result.objective_value.value + self.fit_results = restored_results + return restored_results def help(self) -> None: """Print a summary of analysis properties and methods.""" - console.paragraph("Help for 'Analysis'") - cls = type(self) + console.paragraph(f"Help for '{cls.__name__}'") - prop_rows = _discover_property_rows(cls) - if prop_rows: + property_rows = _discover_property_rows(cls) + method_rows = _discover_method_rows(cls) + property_names = [row[1] for row in property_rows] + method_names = [row[1][:-2] for row in method_rows] + property_names, method_names = _apply_help_filter(self, property_names, method_names) + + filtered_property_names = set(property_names) + filtered_method_names = set(method_names) + filtered_property_rows = [] + for row in property_rows: + if row[1] in filtered_property_names: + filtered_property_rows.append([ + str(len(filtered_property_rows) + 1), + row[1], + row[2], + row[3], + ]) + + filtered_method_rows = [] + for row in method_rows: + method_name = row[1][:-2] + if method_name in filtered_method_names: + filtered_method_rows.append([str(len(filtered_method_rows) + 1), row[1], row[2]]) + + if filtered_property_rows: console.paragraph('Properties') render_table( columns_headers=['#', 'Name', 'Writable', 'Description'], columns_alignment=['right', 'left', 'center', 'left'], - columns_data=prop_rows, + columns_data=filtered_property_rows, ) - method_rows = _discover_method_rows(cls) - if method_rows: + if filtered_method_rows: console.paragraph('Methods') render_table( columns_headers=['#', 'Name', 'Description'], columns_alignment=['right', 'left', 'left'], - columns_data=method_rows, + columns_data=filtered_method_rows, ) + def _help_filter( + self, + properties: list[str], + methods: list[str], + ) -> tuple[list[str], list[str]]: + """Hide inactive mode-specific categories from analysis help.""" + hidden_properties: set[str] + if self._fitting_mode_type is FitModeEnum.SINGLE: + hidden_properties = {'joint_fit', 'sequential_fit', 'sequential_fit_extract'} + elif self._fitting_mode_type is FitModeEnum.JOINT: + hidden_properties = {'sequential_fit', 'sequential_fit_extract'} + elif self._fitting_mode_type is FitModeEnum.SEQUENTIAL: + hidden_properties = {'joint_fit'} + else: # pragma: no cover + hidden_properties = set() + + filtered_properties = [name for name in properties if name not in hidden_properties] + return filtered_properties, methods + + def _serializable_categories(self) -> list: + """Serializable analysis categories for the active fit mode.""" + categories = [ + self.fitting, + self.aliases, + self.constraints, + ] + + if self._fitting_mode_type is FitModeEnum.JOINT: + categories.append(self.joint_fit) + elif self._fitting_mode_type is FitModeEnum.SEQUENTIAL: + categories.extend([ + self.sequential_fit, + self.sequential_fit_extract, + ]) + + if self._has_persisted_fit_state(): + categories.extend(self._fit_state_categories()) + + return categories + # ------------------------------------------------------------------ # Parameter helpers # ------------------------------------------------------------------ @@ -487,7 +959,8 @@ def _get_params_as_dataframe( } if isinstance(param, Parameter): record |= { - ('fittable', 'left'): True, + ('fittable', 'left'): not param.user_constrained + and not param.symmetry_constrained, ('free', 'left'): param.free, ('min', 'right'): param.fit_min, ('max', 'right'): param.fit_max, @@ -499,82 +972,895 @@ def _get_params_as_dataframe( df.columns = pd.MultiIndex.from_tuples(df.columns) return df + def fit(self) -> None: + """Execute fitting for the currently selected fitting mode.""" + mode = self._fitting_mode_type + if mode is FitModeEnum.SINGLE: + self._run_single() + elif mode is FitModeEnum.JOINT: + self._prepare_joint_fit() + self._run_joint() + elif mode is FitModeEnum.SEQUENTIAL: + self._run_sequential() + else: # pragma: no cover + msg = f'Unknown fit mode: {mode!r}' + raise ValueError(msg) + + def _prepare_joint_fit(self) -> None: + """ + Auto-populate and validate joint-fit rows before execution. + """ + experiments = self.project.experiments + minimum_joint_experiments = 2 + if len(experiments) < minimum_joint_experiments: + msg = ( + 'Joint fitting requires at least ' + f'{minimum_joint_experiments} experiments, found {len(experiments)}.' + ) + raise ValueError(msg) + + experiment_names = list(experiments.names) + experiment_name_set = set(experiment_names) + existing_ids = [item.experiment_id.value for item in self._joint_fit] + + unexpected_ids = sorted({name for name in existing_ids if name not in experiment_name_set}) + if unexpected_ids: + msg = ( + 'joint_fit contains experiment_id values not present in the project: ' + f'{unexpected_ids}.' + ) + raise ValueError(msg) + + existing_id_set = set(existing_ids) + for experiment_id in experiment_names: + if experiment_id not in existing_id_set: + self._joint_fit.create(experiment_id=experiment_id, weight=1.0) + existing_id_set.add(experiment_id) + + missing_ids = [name for name in experiment_names if name not in existing_id_set] + if missing_ids: + msg = f'joint_fit is missing rows for project experiments: {missing_ids}.' + raise ValueError(msg) + @property - def fit(self) -> Fit: - """Fit configuration and execution entry-point.""" - return self._fit + def fitting(self) -> Fitting: + """Fitting configuration category.""" + return self._fitting + + @property + def fitting_mode_type(self) -> str: + """Currently selected fitting mode.""" + return self._fitting_mode_type.value + + @fitting_mode_type.setter + def fitting_mode_type(self, value: str) -> None: + supported = [mode.value for mode in FitModeEnum] + + try: + new_mode = FitModeEnum(value) + except ValueError: + log.warning( + f"Unsupported fitting mode '{value}'. " + f'Supported fitting modes: {supported}. ' + f"For more information, use 'show_fitting_mode_types()'", + ) + return + + self._fitting_mode_type = new_mode + console.paragraph('Fitting mode changed to') + console.print(self._fitting_mode_type.value) + + def show_fitting_mode_types(self) -> None: + """Print supported fitting modes and mark the current type.""" + columns_data = [ + [ + '*' if mode is self._fitting_mode_type else '', + mode.value, + mode.description(), + ] + for mode in FitModeEnum + ] + console.paragraph('Fitting mode types') + render_table( + columns_headers=['', 'Type', 'Description'], + columns_alignment=['left', 'left', 'left'], + columns_data=columns_data, + ) + + def _set_fitting_mode_type(self, value: str) -> None: + """Set the fitting mode without console output.""" + supported = [mode.value for mode in FitModeEnum] + + try: + self._fitting_mode_type = FitModeEnum(value) + except ValueError: + log.warning( + f"Unsupported fitting mode '{value}' in CIF. " + f'Supported: {supported}. Keeping default.', + ) # ------------------------------------------------------------------ - # Joint-fit experiments (category) + # Joint-fit weights (category) # ------------------------------------------------------------------ @property - def joint_fit_experiments(self) -> object: + def joint_fit(self) -> object: """Per-experiment weight collection for joint fitting.""" - return self._joint_fit_experiments + return self._joint_fit + + @property + def sequential_fit(self) -> SequentialFit: + """Persisted settings for sequential fitting.""" + return self._sequential_fit + + @property + def sequential_fit_extract(self) -> SequentialFitExtractCollection: + """Persisted extract rules for sequential fitting.""" + return self._sequential_fit_extract + + def _has_persisted_fit_state(self) -> bool: + """ + Return whether a persisted fit-state projection is present. + """ + return self._has_persisted_fit_state_data + + def _set_has_persisted_fit_state(self, *, value: bool) -> None: + """Set the persisted fit-state presence flag.""" + self._has_persisted_fit_state_data = value + + def _fit_state_categories(self) -> list[object]: + """Return fit-state categories for the current result kind.""" + categories: list[object] = [ + self.fit_parameters, + self.fit_result, + self.fit_parameter_correlations, + ] + + try: + result_kind = FitResultKindEnum(self.fit_result.result_kind.value) + except ValueError: + log.warning( + 'Unsupported fit_result.result_kind while serializing analysis CIF: ' + f'{self.fit_result.result_kind.value!r}. ' + 'Saving only common fit-state categories.', + ) + return categories + + if result_kind is FitResultKindEnum.DETERMINISTIC: + categories.append(self.deterministic_result) + return categories + + categories.extend([ + self.bayesian_result, + self.bayesian_sampler, + self.bayesian_convergence, + self.bayesian_parameter_posteriors, + self.bayesian_distribution_caches, + self.bayesian_pair_caches, + self.bayesian_predictive_datasets, + ]) + return categories + + def _clear_persisted_fit_state(self) -> None: + """Reset all persisted fit-state categories before a new fit.""" + self._fit_parameters = FitParameters() + self._fit_result = FitResult() + self._fit_parameter_correlations = FitParameterCorrelations() + self._deterministic_result = DeterministicResult() + self._bayesian_result = BayesianResult() + self._bayesian_sampler = BayesianSampler() + self._bayesian_convergence = BayesianConvergence() + self._bayesian_parameter_posteriors = BayesianParameterPosteriors() + self._bayesian_distribution_caches = BayesianDistributionCaches() + self._bayesian_pair_caches = BayesianPairCaches() + self._bayesian_predictive_datasets = BayesianPredictiveDatasets() + self._set_has_persisted_fit_state(value=False) + self._persisted_fit_state_sidecar = {} + + def _capture_fit_parameter_state(self, parameters: list[Parameter]) -> None: + """Capture pre-fit parameter state.""" + self._clear_persisted_fit_state() + + for param in parameters: + self.fit_parameters.create( + param_unique_name=param.unique_name, + fit_min=param.fit_min, + fit_max=param.fit_max, + fit_bounds_uncertainty_multiplier=param.fit_bounds_uncertainty_multiplier, + start_value=param.value, + start_uncertainty=param.uncertainty, + ) + + self._set_has_persisted_fit_state(value=True) - def _run_fit(self, verbosity: str | None = None, *, use_physical_limits: bool = False) -> None: + def _selected_parameters_for_fit(self, experiments: list[object]) -> list[Parameter]: + """ + Return unique live parameters involved in the current fit slice. """ - Execute fitting for all experiments. + selected_parameters: list[Parameter] = [] + seen_unique_names: set[str] = set() - This method performs the optimization but does not display - results automatically. Call :meth:`display.fit_results` after - fitting to see a summary of the fit quality and parameter - values. + for param in self.project.structures.parameters: + if not isinstance(param, Parameter): + continue + if param.unique_name in seen_unique_names: + continue + selected_parameters.append(param) + seen_unique_names.add(param.unique_name) - In 'single' mode, fits each experiment independently. In 'joint' - mode, performs a simultaneous fit across experiments with - weights. If mode is 'sequential', logs an error directing the - user to :meth:`fit_sequential` instead. + for experiment in experiments: + for param in experiment.parameters: + if not isinstance(param, Parameter): + continue + if param.unique_name in seen_unique_names: + continue + selected_parameters.append(param) + seen_unique_names.add(param.unique_name) - Sets :attr:`fit_results` on success, which can be accessed - programmatically (e.g., - ``analysis.fit_results.reduced_chi_square``). + return selected_parameters - 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``. - use_physical_limits : bool, default=False - When ``True``, fall back to physical limits from the value - spec for parameters whose ``fit_min``/``fit_max`` are - unbounded. - """ - verb = VerbosityEnum(verbosity if verbosity is not None else self.project.verbosity) + @staticmethod + def _fit_data_point_count(experiments: list[object]) -> int: + """Return observed data-point count for one fit slice.""" + total = 0 + for experiment in experiments: + intensity_category = intensity_category_for(experiment) + total += int(np.asarray(intensity_category.intensity_meas).size) + return total + @staticmethod + def _resolve_covariance_matrix(results: FitResults) -> np.ndarray | None: + """ + Return a covariance matrix when the raw fit result exposes one. + """ + raw_result = results.engine_result + for attribute_name in ('covar', 'covariance_matrix'): + covariance = getattr(raw_result, attribute_name, None) + if covariance is None: + continue + + covariance_array = np.asarray(covariance, dtype=float) + if covariance_array.ndim != _FLATTENED_POSTERIOR_SAMPLE_NDIM: + continue + if covariance_array.shape[0] != covariance_array.shape[1]: + continue + return covariance_array + + return None + + @staticmethod + def _correlation_matrix_from_covariance(covariance: np.ndarray) -> np.ndarray | None: + """ + Return a correlation matrix derived from a covariance matrix. + """ + diagonal = np.diag(covariance) + if np.any(diagonal <= 0): + return None + + scales = np.sqrt(diagonal) + denominator = np.outer(scales, scales) + with np.errstate(invalid='ignore', divide='ignore'): + correlation = covariance / denominator + + if not np.all(np.isfinite(correlation)): + return None + return correlation + + @staticmethod + def _resolve_objective_value(results: FitResults) -> float | None: + """Return the objective value stored for a fit result.""" + if results.chi_square is None: + return None + return float(results.chi_square) + + def _store_common_fit_result_projection( + self, + results: FitResults, + *, + result_kind: FitResultKindEnum, + ) -> None: + """ + Store fields shared by deterministic and Bayesian fit results. + """ + self.fit_result._set_result_kind(result_kind.value) + self.fit_result._set_success(value=results.success) + self.fit_result._set_message(results.message) + self.fit_result._set_iterations(results.iterations) + self.fit_result._set_fitting_time(results.fitting_time) + self.fit_result._set_reduced_chi_square(results.reduced_chi_square) + self._set_has_persisted_fit_state(value=True) + + def _store_correlation_projection( + self, + *, + unique_names: list[str], + correlation_matrix: np.ndarray, + source_kind: FitCorrelationSourceEnum, + ) -> None: + """Store upper-triangle correlations from one matrix.""" + if len(unique_names) <= 1: + return + if correlation_matrix.shape != (len(unique_names), len(unique_names)): + return + + for row_index, unique_name_i in enumerate(unique_names[:-1]): + for column_index in range(row_index + 1, len(unique_names)): + correlation = correlation_matrix[row_index, column_index] + if not np.isfinite(correlation): + continue + self.fit_parameter_correlations.create( + source_kind=source_kind.value, + param_unique_name_i=unique_name_i, + param_unique_name_j=unique_names[column_index], + correlation=float(np.clip(correlation, -1.0, 1.0)), + ) + + def _store_deterministic_result_projection( + self, + results: FitResults, + *, + experiments: list[object], + fitted_parameters: list[Parameter], + ) -> None: + """Store deterministic fit results in persisted categories.""" + selected_parameters = self._selected_parameters_for_fit(experiments) + n_parameters = len(selected_parameters) + n_free_parameters = len(fitted_parameters) + n_data_points = self._fit_data_point_count(experiments) + degrees_of_freedom = max(n_data_points - n_free_parameters, 0) + covariance = self._resolve_covariance_matrix(results) + correlation_matrix = ( + self._correlation_matrix_from_covariance(covariance) + if covariance is not None + else None + ) + + self.deterministic_result._set_optimizer_name( + str(self.fitter.minimizer.name or self.fitter.selection) + ) + self.deterministic_result._set_method_name(str(self.fitter.minimizer.method or '')) + self.deterministic_result._set_objective_name('chi_square') + self.deterministic_result._set_objective_value(self._resolve_objective_value(results)) + self.deterministic_result._set_n_data_points(n_data_points) + self.deterministic_result._set_n_parameters(n_parameters) + self.deterministic_result._set_n_free_parameters(n_free_parameters) + self.deterministic_result._set_degrees_of_freedom(degrees_of_freedom) + self.deterministic_result._set_covariance_available(value=covariance is not None) + self.deterministic_result._set_correlation_available(value=correlation_matrix is not None) + + if correlation_matrix is not None: + self._store_correlation_projection( + unique_names=[param.unique_name for param in fitted_parameters], + correlation_matrix=correlation_matrix, + source_kind=FitCorrelationSourceEnum.DETERMINISTIC, + ) + + def _store_bayesian_distribution_cache_projection( + self, + *, + plotter: object, + results: BayesianFitResults, + flattened_samples: np.ndarray, + parameter_names: list[str], + ) -> dict[str, dict[str, np.ndarray]]: + """ + Store cached posterior density curves into persisted manifests. + """ + payload: dict[str, dict[str, np.ndarray]] = {} + for parameter_index, parameter_name in enumerate(parameter_names): + lower_bound, upper_bound = plotter._posterior_parameter_bounds( + fit_results=results, + parameter_name=parameter_name, + ) + density_curve = plotter._posterior_density_curve( + flattened_samples[:, parameter_index], + lower_bound=lower_bound, + upper_bound=upper_bound, + ) + if density_curve is None: + continue + + x_values, density_values = density_curve + x_array = np.asarray(x_values, dtype=float) + density_array = np.asarray(density_values, dtype=float) + cache_index = len(payload) + self.bayesian_distribution_caches.create( + param_unique_name=parameter_name, + x_path=f'/posterior/distribution/{cache_index}/x', + density_path=f'/posterior/distribution/{cache_index}/density', + n_grid=float(x_array.size), + n_draws_cached=float(np.isfinite(flattened_samples[:, parameter_index]).sum()), + ) + payload[parameter_name] = { + 'x': x_array, + 'density': density_array, + } + return payload + + @staticmethod + def _posterior_pair_contour_levels(density: np.ndarray) -> np.ndarray: + """Return default contour levels for one cached pair.""" + density_max = float(np.max(density)) + if not np.isfinite(density_max) or density_max <= 0: + return np.asarray([], dtype=float) + return density_max * np.asarray([0.20, 0.35, 0.50, 0.65, 0.80, 0.95], dtype=float) + + @staticmethod + def _ordered_pair_metadata( + parameter_names: list[str], + first_index: int, + second_index: int, + ) -> tuple[int, int, str, str]: + """Return ordered pair indices and parameter names.""" + x_index = first_index + y_index = second_index + x_name = parameter_names[x_index] + y_name = parameter_names[y_index] + if x_name > y_name: + x_index, y_index = y_index, x_index + x_name, y_name = y_name, x_name + return x_index, y_index, x_name, y_name + + def _store_one_bayesian_pair_cache_projection( + self, + *, + plotter: object, + results: BayesianFitResults, + density_samples: np.ndarray, + pair_metadata: tuple[int, int, str, str], + contour_grid_size: int, + pair_id: str, + ) -> tuple[str, dict[str, np.ndarray]] | None: + """Store one cached pair surface and return its payload.""" + x_index, y_index, x_name, y_name = pair_metadata + + x_values = density_samples[:, x_index] + y_values = density_samples[:, y_index] + x_bounds, y_bounds = plotter._posterior_pair_bounds( + fit_results=results, + x_parameter_name=x_name, + y_parameter_name=y_name, + x_values=x_values, + y_values=y_values, + ) + density_surface = plotter._posterior_pair_density_surface( + x_values=x_values, + y_values=y_values, + x_bounds=x_bounds, + y_bounds=y_bounds, + grid_size=contour_grid_size, + ) + if density_surface is None: + return None + + x_grid_array = np.asarray(density_surface[0], dtype=float) + y_grid_array = np.asarray(density_surface[1], dtype=float) + density_array = np.asarray(density_surface[2], dtype=float) + contour_levels = self._posterior_pair_contour_levels(density_array) + self.bayesian_pair_caches.create( + id=pair_id, + parameter_names=(x_name, y_name), + paths=BayesianPairCachePaths( + x_path=f'/posterior/pairs/{pair_id}/x', + y_path=f'/posterior/pairs/{pair_id}/y', + density_path=f'/posterior/pairs/{pair_id}/density', + contour_level_path=f'/posterior/pairs/{pair_id}/contour_levels', + ), + grid_shape=(float(x_grid_array.size), float(y_grid_array.size)), + n_draws_cached=float(density_samples.shape[0]), + ) + return pair_id, { + 'x': x_grid_array, + 'y': y_grid_array, + 'density': density_array, + 'contour_levels': contour_levels, + } + + def _store_bayesian_pair_cache_projection( + self, + *, + plotter: object, + results: BayesianFitResults, + flattened_samples: np.ndarray, + parameter_names: list[str], + ) -> dict[str, dict[str, np.ndarray]]: + """Store cached pair-density surfaces in manifests.""" + n_parameters = len(parameter_names) + if n_parameters <= 1: + return {} + + density_samples = plotter._thin_posterior_samples( + flattened_samples, + max_points=plotter._posterior_pair_density_max_points(n_parameters), + ) + contour_grid_size = plotter._posterior_pair_contour_grid_size(n_parameters) + payload: dict[str, dict[str, np.ndarray]] = {} + for first_index, second_index in combinations(range(n_parameters), 2): + pair_id = str(len(payload) + 1) + cache_projection = self._store_one_bayesian_pair_cache_projection( + plotter=plotter, + results=results, + density_samples=density_samples, + pair_metadata=self._ordered_pair_metadata( + parameter_names, + first_index, + second_index, + ), + contour_grid_size=contour_grid_size, + pair_id=pair_id, + ) + if cache_projection is None: + continue + + pair_id, pair_payload = cache_projection + payload[pair_id] = pair_payload + return payload + + @staticmethod + def _predictive_dataset_payload( + summary: PosteriorPredictiveSummary, + ) -> dict[str, np.ndarray]: + """Return persisted predictive arrays for one summary.""" + payload: dict[str, np.ndarray] = { + 'x': np.asarray(summary.x, dtype=float), + 'best_sample_prediction': np.asarray(summary.best_sample_prediction, dtype=float), + } + if summary.lower_95 is not None: + payload['lower_95'] = np.asarray(summary.lower_95, dtype=float) + if summary.upper_95 is not None: + payload['upper_95'] = np.asarray(summary.upper_95, dtype=float) + if summary.lower_68 is not None: + payload['lower_68'] = np.asarray(summary.lower_68, dtype=float) + if summary.upper_68 is not None: + payload['upper_68'] = np.asarray(summary.upper_68, dtype=float) + if summary.draws is not None: + payload['draws'] = np.asarray(summary.draws, dtype=float) + return payload + + def _store_bayesian_predictive_projection( + self, + *, + plotter: object, + results: BayesianFitResults, + ) -> dict[str, dict[str, np.ndarray]]: + """ + Store posterior predictive summaries into persisted manifests. + """ + predictive_payload: dict[str, dict[str, np.ndarray]] = {} + for experiment_name in self.project.experiments.names: + experiment = self.project.experiments[experiment_name] + x_axis, x_axis_name, _, _, _ = plotter._resolve_x_axis(experiment.type, None) + summary = plotter._build_posterior_predictive_summary( + fit_results=results, + experiment=experiment, + expt_name=experiment_name, + x_axis=x_axis, + include_draws=False, + ) + if summary is None: + continue + + results.posterior_predictive[summary.experiment_name] = summary + results.posterior_predictive[ + self._predictive_cache_key( + summary.experiment_name, + str(x_axis_name), + include_draws=False, + ) + ] = summary + predictive_payload[summary.experiment_name] = self._predictive_dataset_payload( + summary, + ) + predictive_root = f'/predictive/{summary.experiment_name}' + self.bayesian_predictive_datasets.create( + experiment_name=summary.experiment_name, + x_axis_name=str(x_axis_name), + paths=BayesianPredictiveDatasetPaths( + x_path=f'{predictive_root}/x', + best_sample_prediction_path=(f'{predictive_root}/best_sample_prediction'), + lower_95_path=( + None if summary.lower_95 is None else f'{predictive_root}/lower_95' + ), + upper_95_path=( + None if summary.upper_95 is None else f'{predictive_root}/upper_95' + ), + lower_68_path=( + None if summary.lower_68 is None else f'{predictive_root}/lower_68' + ), + upper_68_path=( + None if summary.upper_68 is None else f'{predictive_root}/upper_68' + ), + draws_path=(None if summary.draws is None else f'{predictive_root}/draws'), + ), + n_x=float(np.asarray(summary.x).size), + n_draws_cached=( + 0.0 if summary.draws is None else float(np.asarray(summary.draws).shape[0]) + ), + ) + return predictive_payload + + def _store_bayesian_plot_cache_projection(self, results: BayesianFitResults) -> None: + """Populate persisted Bayesian plot caches.""" + posterior_samples = results.posterior_samples + if posterior_samples is None: + self._persisted_fit_state_sidecar['distribution_caches'] = {} + self._persisted_fit_state_sidecar['pair_caches'] = {} + self._persisted_fit_state_sidecar['predictive_datasets'] = {} + self.bayesian_result._set_has_distribution_cache(value=False) + self.bayesian_result._set_has_pair_cache(value=False) + self.bayesian_result._set_has_posterior_predictive(value=False) + return + + flattened_samples = np.asarray(posterior_samples.flattened(), dtype=float) + parameter_names = list(posterior_samples.parameter_names) + if ( + flattened_samples.ndim != _FLATTENED_POSTERIOR_SAMPLE_NDIM + or not parameter_names + or flattened_samples.shape[1] != len(parameter_names) + ): + self._persisted_fit_state_sidecar['distribution_caches'] = {} + self._persisted_fit_state_sidecar['pair_caches'] = {} + self._persisted_fit_state_sidecar['predictive_datasets'] = {} + self.bayesian_result._set_has_distribution_cache(value=False) + self.bayesian_result._set_has_pair_cache(value=False) + self.bayesian_result._set_has_posterior_predictive(value=False) + return + + plotter = self.project.rendering.plotter + distribution_payload = self._store_bayesian_distribution_cache_projection( + plotter=plotter, + results=results, + flattened_samples=flattened_samples, + parameter_names=parameter_names, + ) + pair_payload = self._store_bayesian_pair_cache_projection( + plotter=plotter, + results=results, + flattened_samples=flattened_samples, + parameter_names=parameter_names, + ) + predictive_payload = self._store_bayesian_predictive_projection( + plotter=plotter, + results=results, + ) + + self._persisted_fit_state_sidecar['distribution_caches'] = distribution_payload + self._persisted_fit_state_sidecar['pair_caches'] = pair_payload + self._persisted_fit_state_sidecar['predictive_datasets'] = predictive_payload + self.bayesian_result._set_has_distribution_cache(value=bool(distribution_payload)) + self.bayesian_result._set_has_pair_cache(value=bool(pair_payload)) + self.bayesian_result._set_has_posterior_predictive(value=bool(predictive_payload)) + + def _store_bayesian_posterior_sidecar_projection( + self, + results: BayesianFitResults, + ) -> None: + """Persist posterior arrays while live samples exist.""" + posterior_samples = results.posterior_samples + if posterior_samples is None: + self._persisted_fit_state_sidecar['posterior'] = {} + return + + self._persisted_fit_state_sidecar['posterior'] = { + 'parameter_samples': np.asarray( + posterior_samples.parameter_samples, + dtype=float, + ), + 'log_posterior': ( + None + if posterior_samples.log_posterior is None + else np.asarray(posterior_samples.log_posterior, dtype=float) + ), + 'draw_index': ( + None + if posterior_samples.draw_index is None + else np.asarray(posterior_samples.draw_index) + ), + } + + def _store_bayesian_result_projection(self, results: BayesianFitResults) -> None: + """ + Store Bayesian fit-result projections into persisted categories. + """ + credible_interval_inner = 0.68 + credible_interval_outer = 0.95 + if len(results.credible_interval_levels) >= _CREDIBLE_INTERVAL_LEVEL_COUNT: + credible_interval_inner = float(results.credible_interval_levels[0]) + credible_interval_outer = float(results.credible_interval_levels[1]) + + point_estimate_name = results.point_estimate_name or 'best_sample' + sampler_settings = results.sampler_settings + convergence = results.convergence_diagnostics + + self.bayesian_result._set_sampler_name(results.sampler_name) + self.bayesian_result._set_point_estimate_name(point_estimate_name) + self.bayesian_result._set_success(value=results.success) + self.bayesian_result._set_sampler_completed(value=results.sampler_completed) + self.bayesian_result._set_best_log_posterior(results.best_log_posterior) + self.bayesian_result._set_credible_interval_inner(credible_interval_inner) + self.bayesian_result._set_credible_interval_outer(credible_interval_outer) + self.bayesian_result._set_has_posterior_samples( + value=results.posterior_samples is not None + ) + self.bayesian_result._set_has_distribution_cache(value=False) + self.bayesian_result._set_has_pair_cache(value=False) + self.bayesian_result._set_has_posterior_predictive(value=False) + self.bayesian_result._set_sidecar_file('results.h5') + self._store_bayesian_posterior_sidecar_projection(results) + + self.bayesian_sampler._set_steps(int(sampler_settings.get('steps', 0))) + self.bayesian_sampler._set_burn(int(sampler_settings.get('burn', 0))) + self.bayesian_sampler._set_thin(int(sampler_settings.get('thin', 0))) + self.bayesian_sampler._set_pop(int(sampler_settings.get('pop', 0))) + self.bayesian_sampler._set_parallel(int(sampler_settings.get('parallel', 0))) + self.bayesian_sampler._set_init(str(sampler_settings.get('init', ''))) + random_seed = sampler_settings.get('random_seed') + self.bayesian_sampler._set_random_seed(None if random_seed is None else int(random_seed)) + + self.bayesian_convergence._set_converged(value=bool(convergence.get('converged', False))) + self.bayesian_convergence._set_max_r_hat(convergence.get('max_r_hat')) + self.bayesian_convergence._set_min_ess_bulk(convergence.get('min_ess_bulk')) + self.bayesian_convergence._set_n_draws(int(convergence.get('n_draws', 0))) + self.bayesian_convergence._set_n_chains(int(convergence.get('n_chains', 0))) + self.bayesian_convergence._set_n_parameters(int(convergence.get('n_parameters', 0))) + + for summary in results.posterior_parameter_summaries: + self.bayesian_parameter_posteriors.create(summary=summary) + + posterior_samples = results.posterior_samples + if posterior_samples is None: + return + + self._store_bayesian_plot_cache_projection(results) + if len(posterior_samples.parameter_names) <= 1: + return + + flattened = posterior_samples.flattened() + correlation_matrix = np.corrcoef(flattened, rowvar=False) + self._store_correlation_projection( + unique_names=list(posterior_samples.parameter_names), + correlation_matrix=correlation_matrix, + source_kind=FitCorrelationSourceEnum.POSTERIOR, + ) + + def _store_fit_result_projection( + self, + results: FitResults, + *, + experiments: list[object], + fitted_parameters: list[Parameter], + ) -> None: + """ + Store the latest fit result into persisted fit-state categories. + """ + if isinstance(results, BayesianFitResults): + self._store_common_fit_result_projection( + results, + result_kind=FitResultKindEnum.BAYESIAN, + ) + self._store_bayesian_result_projection(results) + return + + self._store_common_fit_result_projection( + results, + result_kind=FitResultKindEnum.DETERMINISTIC, + ) + self._store_deterministic_result_projection( + results, + experiments=experiments, + fitted_parameters=fitted_parameters, + ) + + def _resolve_sequential_data_dir(self) -> Path: + """ + Resolve the sequential-fit data directory to an absolute path. + """ + data_dir = Path(self._sequential_fit.data_dir.value) + if data_dir.is_absolute(): + return data_dir + + project_path = self.project.info.path + if project_path is None: + msg = ( + 'Project must be saved before resolving a relative ' + 'sequential_fit.data_dir. Call save_as() first.' + ) + raise ValueError(msg) + + return project_path / data_dir + + def _prepare_fit_run(self) -> tuple[VerbosityEnum, object, object] | None: + """Resolve common inputs for single and joint fitting.""" + verb = VerbosityEnum(self.project.verbosity.fit.value) structures = self.project.structures if not structures: log.warning('No structures found in the project. Cannot run fit.') - return + return None experiments = self.project.experiments if not experiments: log.warning('No experiments found in the project. Cannot run fit.') - return + return None - # Apply constraints before fitting so that constrained + # Apply constraints before fitting so that user-constrained # parameters are marked and excluded from the free parameter # list built by the fitter. + self._sync_live_minimizer_from_persisted_fit_state() self._update_categories() - # Run the fitting process - mode = FitModeEnum(self._fit.mode.value) - if mode is FitModeEnum.JOINT: - self._fit_joint(verb, structures, experiments, use_physical_limits=use_physical_limits) - elif mode is FitModeEnum.SINGLE: - self._fit_single( - verb, structures, experiments, use_physical_limits=use_physical_limits - ) - elif mode is FitModeEnum.SEQUENTIAL: - log.error( - "fit.mode is 'sequential'. Use fit_sequential(data_dir=...) instead of fit()." - ) + return verb, structures, experiments + + def _run_single(self) -> None: + """ + Execute single-mode fitting with current project verbosity. + """ + prepared = self._prepare_fit_run() + if prepared is None: + return + + verb, structures, experiments = prepared + self._fit_single( + verb, + structures, + experiments, + use_physical_limits=False, + random_seed=None, + ) + + if self.project.info.path is not None: + self.project.save() + + def _run_joint(self) -> None: + """Execute joint-mode fitting with current project verbosity.""" + prepared = self._prepare_fit_run() + if prepared is None: return - # After fitting, save the project + verb, structures, experiments = prepared + self._fit_joint( + verb, + structures, + experiments, + use_physical_limits=False, + random_seed=None, + ) + + if self.project.info.path is not None: + self.project.save() + + def _run_sequential(self) -> None: + """ + Execute sequential fitting from persisted sequential settings. + """ + from easydiffraction.analysis.sequential import fit_sequential as _fit_seq # noqa: PLC0415 + + self._set_fitting_mode_type(FitModeEnum.SEQUENTIAL.value) + self._update_categories() + self._clear_persisted_fit_state() + + max_workers_value = self._sequential_fit.max_workers.value + max_workers = max_workers_value if max_workers_value == 'auto' else int(max_workers_value) + + chunk_size_value = self._sequential_fit.chunk_size.value + chunk_size = None if chunk_size_value == '.' else int(chunk_size_value) + + self.fit_results = None + self.fitter.results = None + + try: + _fit_seq( + analysis=self, + data_dir=str(self._resolve_sequential_data_dir()), + max_workers=max_workers, + chunk_size=chunk_size, + file_pattern=self._sequential_fit.file_pattern.value, + reverse=self._sequential_fit.reverse.value, + ) + finally: + self.fit_results = None + self.fitter.results = None + self._clear_persisted_fit_state() + if self.project.info.path is not None: self.project.save() @@ -585,6 +1871,7 @@ def _fit_joint( experiments: object, *, use_physical_limits: bool, + random_seed: int | None, ) -> None: """ Run joint fitting across all experiments with weights. @@ -599,21 +1886,21 @@ def _fit_joint( Project experiments collection. use_physical_limits : bool Whether to use physical limits as fit bounds. + random_seed : int | None + Optional random seed passed to stochastic minimizers. """ mode = 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) + # Auto-populate joint_fit if empty + if not len(self._joint_fit): + for experiment_id in experiments.names: + self._joint_fit.create(experiment_id=experiment_id, weight=0.5) if verb is not VerbosityEnum.SILENT: console.paragraph( f"Using all experiments 🔬 {experiments.names} for '{mode.value}' fitting" ) # Resolve weights to a plain numpy array experiments_list = list(experiments.values()) - weights_list = [ - self._joint_fit_experiments[name].weight.value for name in experiments.names - ] + weights_list = [self._joint_fit[name].weight.value for name in experiments.names] weights_array = np.array(weights_list, dtype=np.float64) self.fitter.fit( structures, @@ -622,6 +1909,7 @@ def _fit_joint( analysis=self, verbosity=verb, use_physical_limits=use_physical_limits, + random_seed=random_seed, ) # After fitting, get the results @@ -634,6 +1922,7 @@ def _fit_single( experiments: object, *, use_physical_limits: bool, + random_seed: int | None, ) -> None: """ Run single-mode fitting for each experiment independently. @@ -648,42 +1937,51 @@ def _fit_single( Project experiments collection. use_physical_limits : bool Whether to use physical limits as fit bounds. + random_seed : int | None + Optional random seed passed to stochastic minimizers. """ mode = FitModeEnum.SINGLE expt_names = experiments.names short_display_handle = self._fit_single_print_header(verb, expt_names, mode) short_rows: list[list[str]] = [] + self.fitter.minimizer.tracker._set_shared_display_handle(short_display_handle) - for expt_name in expt_names: - if verb is VerbosityEnum.FULL: - console.print(f"📋 Using experiment 🔬 '{expt_name}' for '{mode.value}' fitting") + try: + for expt_name in expt_names: + if verb is VerbosityEnum.FULL: + console.print( + f"📋 Using experiment 🔬 '{expt_name}' for '{mode.value}' fitting" + ) - experiment = experiments[expt_name] - self.fitter.fit( - structures, - [experiment], - analysis=self, - verbosity=verb, - use_physical_limits=use_physical_limits, - ) + experiment = experiments[expt_name] + self.fitter.fit( + structures, + [experiment], + analysis=self, + verbosity=verb, + use_physical_limits=use_physical_limits, + random_seed=random_seed, + ) - # After fitting, snapshot parameter values before - # they get overwritten by the next experiment's fit - results = self.fitter.results - self._snapshot_params(expt_name, results) - self.fit_results = results + # After fitting, snapshot parameter values before + # they get overwritten by the next experiment's fit + results = self.fitter.results + self._snapshot_params(expt_name, results) + self.fit_results = results - # Short mode: append one summary row and update in-place - if verb is VerbosityEnum.SHORT: - self._fit_single_update_short_table( - short_rows, expt_name, results, short_display_handle - ) + # Short mode: append one summary row and update in-place + if verb is VerbosityEnum.SHORT: + self._fit_single_update_short_table( + short_rows, expt_name, results, short_display_handle + ) + finally: + self.fitter.minimizer.tracker._set_shared_display_handle(None) - # Short mode: close the display handle - if short_display_handle is not None and hasattr(short_display_handle, 'close'): - with suppress(Exception): - short_display_handle.close() + # Short mode: close the display handle + if short_display_handle is not None and hasattr(short_display_handle, 'close'): + with suppress(Exception): + short_display_handle.close() @staticmethod def _fit_single_print_header( @@ -719,7 +2017,7 @@ def _fit_single_print_header( ) console.print("🚀 Starting fit process with 'lmfit'...") console.print('📈 Goodness-of-fit (reduced χ²) per experiment:') - return _make_display_handle() + return make_display_handle() def _snapshot_params(self, expt_name: str, results: object) -> None: """ @@ -775,79 +2073,6 @@ def _fit_single_update_short_table( display_handle=display_handle, ) - def fit_sequential( - self, - data_dir: str, - max_workers: int | str = 1, - chunk_size: int | None = None, - file_pattern: str = '*', - extract_diffrn: object = None, - verbosity: str | None = None, - *, - reverse: bool = False, - ) -> None: - """ - Run sequential fitting over all data files in a directory. - - Fits each dataset independently using the current structure and - experiment as a template. Results are written incrementally to - ``analysis/results.csv`` in the project directory. - - The project must contain exactly one structure and one - experiment (the template), and must have been saved - (``save_as()``) before calling this method. - - Parameters - ---------- - data_dir : str - Path to directory containing data files. - max_workers : int | str, default=1 - Number of parallel worker processes. ``1`` = sequential. - ``'auto'`` = physical CPU count. Uses - ``ProcessPoolExecutor`` with ``spawn`` context when > 1. - chunk_size : int | None, default=None - Files per chunk. Default ``None`` uses *max_workers*. - file_pattern : str, default='*' - Glob pattern to filter files in *data_dir*. - extract_diffrn : object, default=None - User callback ``f(file_path) → {diffrn_field: value}``. - Called per file after fitting. ``None`` = no diffrn - metadata. - verbosity : str | None, default=None - ``'full'``, ``'short'``, or ``'silent'``. Default: project - verbosity. - reverse : bool, default=False - When ``True``, process data files in reverse order. Useful - when starting values are better matched to the last file - (e.g. highest-temperature dataset in a cooling scan). - """ - from easydiffraction.analysis.sequential import fit_sequential as _fit_seq # noqa: PLC0415 - - # Record the fit mode for CIF serialization - self._fit.mode = FitModeEnum.SEQUENTIAL.value - - # Apply constraints before building the template - self._update_categories() - - # Temporarily override project verbosity if caller provided one - original_verbosity = None - if verbosity is not None: - original_verbosity = self.project.verbosity - self.project.verbosity = verbosity - try: - _fit_seq( - analysis=self, - data_dir=data_dir, - max_workers=max_workers, - chunk_size=chunk_size, - file_pattern=file_pattern, - extract_diffrn=extract_diffrn, - reverse=reverse, - ) - finally: - if original_verbosity is not None: - self.project.verbosity = original_verbosity - def _update_categories( self, *, @@ -864,14 +2089,15 @@ def _update_categories( called_by_minimizer : bool, default=False Whether this is called during fitting. """ - del called_by_minimizer + super()._update_categories(called_by_minimizer=called_by_minimizer) # Apply constraints to sync dependent parameters if self.constraints.enabled and self.constraints._items: - self.constraints_handler.set_aliases(self.aliases) - self.constraints_handler.set_constraints(self.constraints) - self.constraints_handler.apply() + self._constraints_handler.set_aliases(self.aliases) + self._constraints_handler.set_constraints(self.constraints) + self._constraints_handler.apply() + @property def as_cif(self) -> str: """ Serialize the analysis section to a CIF string. @@ -883,3 +2109,8 @@ def as_cif(self) -> str: """ self._update_categories() return analysis_to_cif(self) + + def show_as_cif(self) -> None: + """Pretty-print the analysis section as CIF text.""" + console.paragraph('Analysis info as CIF') + render_cif(self.as_cif) diff --git a/src/easydiffraction/analysis/calculators/cryspy.py b/src/easydiffraction/analysis/calculators/cryspy.py index 3c2da252c..7bac27385 100644 --- a/src/easydiffraction/analysis/calculators/cryspy.py +++ b/src/easydiffraction/analysis/calculators/cryspy.py @@ -315,13 +315,19 @@ def _powder_refln_core_arrays( try: indices = np.asarray(phase_block['index_hkl'], dtype=int) sin_theta_over_lambda = np.asarray(phase_block['sthovl'], dtype=float) - f_nucl = np.asarray(phase_block['f_nucl']) except KeyError: return None + structure_factor = phase_block.get('f_nucl') + if structure_factor is None: + structure_factor = phase_block.get('f_charge') + if structure_factor is None: + return None + structure_factor = np.asarray(structure_factor) + if indices.shape[0] != EXPECTED_HKL_INDEX_ROWS: return None - return indices, sin_theta_over_lambda, f_nucl + return indices, sin_theta_over_lambda, structure_factor @staticmethod def _powder_refln_d_spacing( diff --git a/src/easydiffraction/analysis/categories/__init__.py b/src/easydiffraction/analysis/categories/__init__.py index 4e798e209..743e0ff02 100644 --- a/src/easydiffraction/analysis/categories/__init__.py +++ b/src/easydiffraction/analysis/categories/__init__.py @@ -1,2 +1,46 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause + +from easydiffraction.analysis.categories.aliases import Alias +from easydiffraction.analysis.categories.aliases import Aliases +from easydiffraction.analysis.categories.bayesian_convergence import BayesianConvergence +from easydiffraction.analysis.categories.bayesian_distribution_caches import ( + BayesianDistributionCacheItem, +) +from easydiffraction.analysis.categories.bayesian_distribution_caches import ( + BayesianDistributionCaches, +) +from easydiffraction.analysis.categories.bayesian_pair_caches import BayesianPairCacheItem +from easydiffraction.analysis.categories.bayesian_pair_caches import BayesianPairCaches +from easydiffraction.analysis.categories.bayesian_parameter_posteriors import ( + BayesianParameterPosteriorItem, +) +from easydiffraction.analysis.categories.bayesian_parameter_posteriors import ( + BayesianParameterPosteriors, +) +from easydiffraction.analysis.categories.bayesian_predictive_datasets import ( + BayesianPredictiveDatasetItem, +) +from easydiffraction.analysis.categories.bayesian_predictive_datasets import ( + BayesianPredictiveDatasets, +) +from easydiffraction.analysis.categories.bayesian_result import BayesianResult +from easydiffraction.analysis.categories.bayesian_sampler import BayesianSampler +from easydiffraction.analysis.categories.constraints import Constraint +from easydiffraction.analysis.categories.constraints import Constraints +from easydiffraction.analysis.categories.deterministic_result import DeterministicResult +from easydiffraction.analysis.categories.fit_parameter_correlations import ( + FitParameterCorrelationItem, +) +from easydiffraction.analysis.categories.fit_parameter_correlations import FitParameterCorrelations +from easydiffraction.analysis.categories.fit_parameters import FitParameterItem +from easydiffraction.analysis.categories.fit_parameters import FitParameters +from easydiffraction.analysis.categories.fit_result import FitResult +from easydiffraction.analysis.categories.fitting import Fitting +from easydiffraction.analysis.categories.joint_fit import JointFitCollection +from easydiffraction.analysis.categories.joint_fit import JointFitItem +from easydiffraction.analysis.categories.sequential_fit import SequentialFit +from easydiffraction.analysis.categories.sequential_fit_extract import ( + SequentialFitExtractCollection, +) +from easydiffraction.analysis.categories.sequential_fit_extract import SequentialFitExtractItem diff --git a/src/easydiffraction/analysis/categories/aliases/default.py b/src/easydiffraction/analysis/categories/aliases/default.py index eef6201a8..5dd764ce5 100644 --- a/src/easydiffraction/analysis/categories/aliases/default.py +++ b/src/easydiffraction/analysis/categories/aliases/default.py @@ -30,6 +30,9 @@ class Alias(CategoryItem): ``unique_name`` for CIF serialization. """ + _category_code = 'alias' + _category_entry_name = 'label' + def __init__(self) -> None: super().__init__() @@ -56,9 +59,6 @@ def __init__(self) -> None: # Stored via object.__setattr__ to avoid parent-chain mutation. object.__setattr__(self, '_param_ref', None) - self._identity.category_code = 'alias' - self._identity.category_entry_name = lambda: str(self.label.value) - # ------------------------------------------------------------------ # Public properties # ------------------------------------------------------------------ diff --git a/src/easydiffraction/analysis/categories/bayesian_convergence/__init__.py b/src/easydiffraction/analysis/categories/bayesian_convergence/__init__.py new file mode 100644 index 000000000..e9acbd717 --- /dev/null +++ b/src/easydiffraction/analysis/categories/bayesian_convergence/__init__.py @@ -0,0 +1,7 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + +from easydiffraction.analysis.categories.bayesian_convergence.default import BayesianConvergence +from easydiffraction.analysis.categories.bayesian_convergence.factory import ( + BayesianConvergenceFactory, +) diff --git a/src/easydiffraction/analysis/categories/bayesian_convergence/default.py b/src/easydiffraction/analysis/categories/bayesian_convergence/default.py new file mode 100644 index 000000000..48d00fa41 --- /dev/null +++ b/src/easydiffraction/analysis/categories/bayesian_convergence/default.py @@ -0,0 +1,121 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Bayesian convergence diagnostics category.""" + +from __future__ import annotations + +from easydiffraction.analysis.categories.bayesian_convergence.factory import ( + BayesianConvergenceFactory, +) +from easydiffraction.core.category import CategoryItem +from easydiffraction.core.metadata import TypeInfo +from easydiffraction.core.validation import AttributeSpec +from easydiffraction.core.variable import BoolDescriptor +from easydiffraction.core.variable import IntegerDescriptor +from easydiffraction.core.variable import NumericDescriptor +from easydiffraction.io.cif.handler import CifHandler + + +@BayesianConvergenceFactory.register +class BayesianConvergence(CategoryItem): + """Persisted Bayesian convergence diagnostics.""" + + _category_code = 'bayesian_convergence' + + type_info = TypeInfo( + tag='default', + description='Persisted Bayesian convergence diagnostics', + ) + + def __init__(self) -> None: + super().__init__() + self._converged = BoolDescriptor( + name='converged', + description='Whether the Bayesian fit met convergence criteria.', + value_spec=AttributeSpec(default=False), + cif_handler=CifHandler(names=['_bayesian_convergence.converged']), + ) + self._max_r_hat = NumericDescriptor( + name='max_r_hat', + description='Maximum rank-normalized split-R-hat across parameters.', + value_spec=AttributeSpec(default=None, allow_none=True), + cif_handler=CifHandler(names=['_bayesian_convergence.max_r_hat']), + ) + self._min_ess_bulk = NumericDescriptor( + name='min_ess_bulk', + description='Minimum bulk effective sample size across parameters.', + value_spec=AttributeSpec(default=None, allow_none=True), + cif_handler=CifHandler(names=['_bayesian_convergence.min_ess_bulk']), + ) + self._n_draws = IntegerDescriptor( + name='n_draws', + description='Number of stored posterior draws.', + value_spec=AttributeSpec(default=0), + cif_handler=CifHandler(names=['_bayesian_convergence.n_draws']), + ) + self._n_chains = IntegerDescriptor( + name='n_chains', + description='Number of stored posterior chains.', + value_spec=AttributeSpec(default=0), + cif_handler=CifHandler(names=['_bayesian_convergence.n_chains']), + ) + self._n_parameters = IntegerDescriptor( + name='n_parameters', + description='Number of sampled parameters.', + value_spec=AttributeSpec(default=0), + cif_handler=CifHandler(names=['_bayesian_convergence.n_parameters']), + ) + + @property + def converged(self) -> BoolDescriptor: + """Whether the Bayesian fit met convergence criteria.""" + return self._converged + + def _set_converged(self, *, value: bool) -> None: + """Set the convergence flag for internal callers.""" + self._converged.value = value + + @property + def max_r_hat(self) -> NumericDescriptor: + """Maximum rank-normalized split-R-hat across parameters.""" + return self._max_r_hat + + def _set_max_r_hat(self, value: float | None) -> None: + """Set the maximum R-hat for internal callers.""" + self._max_r_hat.value = value + + @property + def min_ess_bulk(self) -> NumericDescriptor: + """Minimum bulk effective sample size across parameters.""" + return self._min_ess_bulk + + def _set_min_ess_bulk(self, value: float | None) -> None: + """Set the minimum ESS bulk for internal callers.""" + self._min_ess_bulk.value = value + + @property + def n_draws(self) -> IntegerDescriptor: + """Number of stored posterior draws.""" + return self._n_draws + + def _set_n_draws(self, value: int) -> None: + """Set the draw count for internal callers.""" + self._n_draws.value = value + + @property + def n_chains(self) -> IntegerDescriptor: + """Number of stored posterior chains.""" + return self._n_chains + + def _set_n_chains(self, value: int) -> None: + """Set the chain count for internal callers.""" + self._n_chains.value = value + + @property + def n_parameters(self) -> IntegerDescriptor: + """Number of sampled parameters.""" + return self._n_parameters + + def _set_n_parameters(self, value: int) -> None: + """Set the sampled-parameter count for internal callers.""" + self._n_parameters.value = value diff --git a/src/easydiffraction/analysis/categories/joint_fit_experiments/factory.py b/src/easydiffraction/analysis/categories/bayesian_convergence/factory.py similarity index 65% rename from src/easydiffraction/analysis/categories/joint_fit_experiments/factory.py rename to src/easydiffraction/analysis/categories/bayesian_convergence/factory.py index 992af7270..fbe5da383 100644 --- a/src/easydiffraction/analysis/categories/joint_fit_experiments/factory.py +++ b/src/easydiffraction/analysis/categories/bayesian_convergence/factory.py @@ -1,6 +1,6 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -"""Joint-fit-experiments factory — delegates to ``FactoryBase``.""" +"""Bayesian-convergence factory.""" from __future__ import annotations @@ -9,8 +9,8 @@ from easydiffraction.core.factory import FactoryBase -class JointFitExperimentsFactory(FactoryBase): - """Create joint-fit experiment collections by tag.""" +class BayesianConvergenceFactory(FactoryBase): + """Create Bayesian-convergence categories by tag.""" _default_rules: ClassVar[dict] = { frozenset(): 'default', diff --git a/src/easydiffraction/analysis/categories/bayesian_distribution_caches/__init__.py b/src/easydiffraction/analysis/categories/bayesian_distribution_caches/__init__.py new file mode 100644 index 000000000..4ecf63f0b --- /dev/null +++ b/src/easydiffraction/analysis/categories/bayesian_distribution_caches/__init__.py @@ -0,0 +1,12 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + +from easydiffraction.analysis.categories.bayesian_distribution_caches.default import ( + BayesianDistributionCacheItem, +) +from easydiffraction.analysis.categories.bayesian_distribution_caches.default import ( + BayesianDistributionCaches, +) +from easydiffraction.analysis.categories.bayesian_distribution_caches.factory import ( + BayesianDistributionCachesFactory, +) diff --git a/src/easydiffraction/analysis/categories/bayesian_distribution_caches/default.py b/src/easydiffraction/analysis/categories/bayesian_distribution_caches/default.py new file mode 100644 index 000000000..41183dc39 --- /dev/null +++ b/src/easydiffraction/analysis/categories/bayesian_distribution_caches/default.py @@ -0,0 +1,151 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Bayesian distribution-cache manifest rows.""" + +from __future__ import annotations + +from easydiffraction.analysis.categories.bayesian_distribution_caches.factory import ( + BayesianDistributionCachesFactory, +) +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 NumericDescriptor +from easydiffraction.core.variable import StringDescriptor +from easydiffraction.io.cif.handler import CifHandler + + +class BayesianDistributionCacheItem(CategoryItem): + """Single persisted Bayesian distribution-cache manifest row.""" + + _category_code = 'bayesian_distribution_cache' + _category_entry_name = 'param_unique_name' + + def __init__(self) -> None: + super().__init__() + self._param_unique_name = StringDescriptor( + name='param_unique_name', + description='Unique parameter name for the cached distribution.', + value_spec=AttributeSpec( + default='_', + validator=RegexValidator(pattern=r'^[A-Za-z_][A-Za-z0-9_.]*$'), + ), + cif_handler=CifHandler(names=['_bayesian_distribution_cache.param_unique_name']), + ) + self._x_path = StringDescriptor( + name='x_path', + description='HDF5 dataset path for the distribution x-grid.', + value_spec=AttributeSpec(default=''), + cif_handler=CifHandler(names=['_bayesian_distribution_cache.x_path']), + ) + self._density_path = StringDescriptor( + name='density_path', + description='HDF5 dataset path for the cached density values.', + value_spec=AttributeSpec(default=''), + cif_handler=CifHandler(names=['_bayesian_distribution_cache.density_path']), + ) + self._n_grid = NumericDescriptor( + name='n_grid', + description='Number of grid points in the cached distribution.', + value_spec=AttributeSpec(default=0), + cif_handler=CifHandler(names=['_bayesian_distribution_cache.n_grid']), + ) + self._n_draws_cached = NumericDescriptor( + name='n_draws_cached', + description='Number of draws summarized into the cached distribution.', + value_spec=AttributeSpec(default=0), + cif_handler=CifHandler(names=['_bayesian_distribution_cache.n_draws_cached']), + ) + + @property + def param_unique_name(self) -> StringDescriptor: + """Unique parameter name for the cached distribution.""" + return self._param_unique_name + + def _set_param_unique_name(self, value: str) -> None: + """Set the unique parameter name for internal callers.""" + self._param_unique_name.value = value + + @property + def x_path(self) -> StringDescriptor: + """HDF5 dataset path for the distribution x-grid.""" + return self._x_path + + def _set_x_path(self, value: str) -> None: + """Set the x-grid dataset path for internal callers.""" + self._x_path.value = value + + @property + def density_path(self) -> StringDescriptor: + """HDF5 dataset path for the cached density values.""" + return self._density_path + + def _set_density_path(self, value: str) -> None: + """Set the density dataset path for internal callers.""" + self._density_path.value = value + + @property + def n_grid(self) -> NumericDescriptor: + """Number of grid points in the cached distribution.""" + return self._n_grid + + def _set_n_grid(self, value: float) -> None: + """Set the grid-size count for internal callers.""" + self._n_grid.value = value + + @property + def n_draws_cached(self) -> NumericDescriptor: + """Number of draws summarized into the cached distribution.""" + return self._n_draws_cached + + def _set_n_draws_cached(self, value: float) -> None: + """Set the cached-draw count for internal callers.""" + self._n_draws_cached.value = value + + +@BayesianDistributionCachesFactory.register +class BayesianDistributionCaches(CategoryCollection): + """Collection of persisted Bayesian distribution-cache manifests.""" + + type_info = TypeInfo( + tag='default', + description='Persisted Bayesian distribution-cache manifests', + ) + + def __init__(self) -> None: + super().__init__(item_type=BayesianDistributionCacheItem) + + def create( + self, + *, + param_unique_name: str, + x_path: str, + density_path: str, + n_grid: float, + n_draws_cached: float, + ) -> None: + """ + Create a persisted Bayesian distribution-cache manifest row. + + Parameters + ---------- + param_unique_name : str + Unique parameter name for the cached distribution. + x_path : str + HDF5 dataset path for the distribution x-grid. + density_path : str + HDF5 dataset path for the cached density values. + n_grid : float + Number of grid points in the cached distribution. + n_draws_cached : float + Number of draws summarized into the cached distribution. + """ + item = BayesianDistributionCacheItem() + item._set_param_unique_name(param_unique_name) + item._set_x_path(x_path) + item._set_density_path(density_path) + item._set_n_grid(n_grid) + item._set_n_draws_cached(n_draws_cached) + self.add(item) diff --git a/src/easydiffraction/analysis/categories/bayesian_distribution_caches/factory.py b/src/easydiffraction/analysis/categories/bayesian_distribution_caches/factory.py new file mode 100644 index 000000000..a45016f5b --- /dev/null +++ b/src/easydiffraction/analysis/categories/bayesian_distribution_caches/factory.py @@ -0,0 +1,17 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Bayesian-distribution-caches factory.""" + +from __future__ import annotations + +from typing import ClassVar + +from easydiffraction.core.factory import FactoryBase + + +class BayesianDistributionCachesFactory(FactoryBase): + """Create Bayesian-distribution-cache collections by tag.""" + + _default_rules: ClassVar[dict] = { + frozenset(): 'default', + } diff --git a/src/easydiffraction/analysis/categories/bayesian_pair_caches/__init__.py b/src/easydiffraction/analysis/categories/bayesian_pair_caches/__init__.py new file mode 100644 index 000000000..edc955fa8 --- /dev/null +++ b/src/easydiffraction/analysis/categories/bayesian_pair_caches/__init__.py @@ -0,0 +1,8 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + +from easydiffraction.analysis.categories.bayesian_pair_caches.default import BayesianPairCacheItem +from easydiffraction.analysis.categories.bayesian_pair_caches.default import BayesianPairCaches +from easydiffraction.analysis.categories.bayesian_pair_caches.factory import ( + BayesianPairCachesFactory, +) diff --git a/src/easydiffraction/analysis/categories/bayesian_pair_caches/default.py b/src/easydiffraction/analysis/categories/bayesian_pair_caches/default.py new file mode 100644 index 000000000..937c77a5e --- /dev/null +++ b/src/easydiffraction/analysis/categories/bayesian_pair_caches/default.py @@ -0,0 +1,267 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Bayesian pair-cache manifest rows.""" + +from __future__ import annotations + +from dataclasses import dataclass + +from easydiffraction.analysis.categories.bayesian_pair_caches.factory import ( + BayesianPairCachesFactory, +) +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 NumericDescriptor +from easydiffraction.core.variable import StringDescriptor +from easydiffraction.io.cif.handler import CifHandler + + +def _normalized_parameter_pair( + param_unique_name_x: str, + param_unique_name_y: str, +) -> tuple[str, str]: + """Return a stable ordering for a cached parameter pair.""" + if param_unique_name_x <= param_unique_name_y: + return param_unique_name_x, param_unique_name_y + return param_unique_name_y, param_unique_name_x + + +@dataclass(frozen=True, slots=True) +class BayesianPairCachePaths: + """HDF5 dataset paths for one persisted pair cache.""" + + x_path: str + y_path: str + density_path: str + contour_level_path: str + + +class BayesianPairCacheItem(CategoryItem): + """Single persisted Bayesian pair-cache manifest row.""" + + _category_code = 'bayesian_pair_cache' + _category_entry_name = 'id' + + def __init__(self) -> None: + super().__init__() + self._param_unique_name_x = StringDescriptor( + name='param_unique_name_x', + description='First unique parameter name in the cached pair.', + value_spec=AttributeSpec( + default='_', + validator=RegexValidator(pattern=r'^[A-Za-z_][A-Za-z0-9_.]*$'), + ), + cif_handler=CifHandler(names=['_bayesian_pair_cache.param_unique_name_x']), + ) + self._param_unique_name_y = StringDescriptor( + name='param_unique_name_y', + description='Second unique parameter name in the cached pair.', + value_spec=AttributeSpec( + default='_', + validator=RegexValidator(pattern=r'^[A-Za-z_][A-Za-z0-9_.]*$'), + ), + cif_handler=CifHandler(names=['_bayesian_pair_cache.param_unique_name_y']), + ) + self._id = StringDescriptor( + name='id', + description='Stable identifier for the cached parameter pair.', + value_spec=AttributeSpec( + default='_', + validator=RegexValidator(pattern=r'^[A-Za-z0-9_.:-]+$'), + ), + cif_handler=CifHandler(names=['_bayesian_pair_cache.id']), + ) + self._x_path = StringDescriptor( + name='x_path', + description='HDF5 dataset path for the pair-cache x-grid.', + value_spec=AttributeSpec(default=''), + cif_handler=CifHandler(names=['_bayesian_pair_cache.x_path']), + ) + self._y_path = StringDescriptor( + name='y_path', + description='HDF5 dataset path for the pair-cache y-grid.', + value_spec=AttributeSpec(default=''), + cif_handler=CifHandler(names=['_bayesian_pair_cache.y_path']), + ) + self._density_path = StringDescriptor( + name='density_path', + description='HDF5 dataset path for the pair-cache density grid.', + value_spec=AttributeSpec(default=''), + cif_handler=CifHandler(names=['_bayesian_pair_cache.density_path']), + ) + self._contour_level_path = StringDescriptor( + name='contour_level_path', + description='HDF5 dataset path for cached contour levels.', + value_spec=AttributeSpec(default=''), + cif_handler=CifHandler(names=['_bayesian_pair_cache.contour_level_path']), + ) + self._n_grid_x = NumericDescriptor( + name='n_grid_x', + description='Number of x-grid points in the cached pair.', + value_spec=AttributeSpec(default=0), + cif_handler=CifHandler(names=['_bayesian_pair_cache.n_grid_x']), + ) + self._n_grid_y = NumericDescriptor( + name='n_grid_y', + description='Number of y-grid points in the cached pair.', + value_spec=AttributeSpec(default=0), + cif_handler=CifHandler(names=['_bayesian_pair_cache.n_grid_y']), + ) + self._n_draws_cached = NumericDescriptor( + name='n_draws_cached', + description='Number of draws summarized into the cached pair.', + value_spec=AttributeSpec(default=0), + cif_handler=CifHandler(names=['_bayesian_pair_cache.n_draws_cached']), + ) + + @property + def param_unique_name_x(self) -> StringDescriptor: + """First unique parameter name in the cached pair.""" + return self._param_unique_name_x + + def _set_param_unique_name_x(self, value: str) -> None: + """Set the first unique parameter name for internal callers.""" + self._param_unique_name_x.value = value + + @property + def param_unique_name_y(self) -> StringDescriptor: + """Second unique parameter name in the cached pair.""" + return self._param_unique_name_y + + def _set_param_unique_name_y(self, value: str) -> None: + """Set the second unique parameter name for internal callers.""" + self._param_unique_name_y.value = value + + @property + def id(self) -> StringDescriptor: + """Stable identifier for the cached parameter pair.""" + return self._id + + def _set_id(self, value: str) -> None: + """Set the pair-cache id for internal callers.""" + self._id.value = value + + @property + def x_path(self) -> StringDescriptor: + """HDF5 dataset path for the pair-cache x-grid.""" + return self._x_path + + def _set_x_path(self, value: str) -> None: + """Set the pair-cache x-grid path for internal callers.""" + self._x_path.value = value + + @property + def y_path(self) -> StringDescriptor: + """HDF5 dataset path for the pair-cache y-grid.""" + return self._y_path + + def _set_y_path(self, value: str) -> None: + """Set the pair-cache y-grid path for internal callers.""" + self._y_path.value = value + + @property + def density_path(self) -> StringDescriptor: + """HDF5 dataset path for the pair-cache density grid.""" + return self._density_path + + def _set_density_path(self, value: str) -> None: + """Set the pair-cache density path for internal callers.""" + self._density_path.value = value + + @property + def contour_level_path(self) -> StringDescriptor: + """HDF5 dataset path for cached contour levels.""" + return self._contour_level_path + + def _set_contour_level_path(self, value: str) -> None: + """Set the contour-level path for internal callers.""" + self._contour_level_path.value = value + + @property + def n_grid_x(self) -> NumericDescriptor: + """Number of x-grid points in the cached pair.""" + return self._n_grid_x + + def _set_n_grid_x(self, value: float) -> None: + """Set the x-grid size for internal callers.""" + self._n_grid_x.value = value + + @property + def n_grid_y(self) -> NumericDescriptor: + """Number of y-grid points in the cached pair.""" + return self._n_grid_y + + def _set_n_grid_y(self, value: float) -> None: + """Set the y-grid size for internal callers.""" + self._n_grid_y.value = value + + @property + def n_draws_cached(self) -> NumericDescriptor: + """Number of draws summarized into the cached pair.""" + return self._n_draws_cached + + def _set_n_draws_cached(self, value: float) -> None: + """Set the cached-draw count for internal callers.""" + self._n_draws_cached.value = value + + +@BayesianPairCachesFactory.register +class BayesianPairCaches(CategoryCollection): + """Collection of persisted Bayesian pair-cache manifests.""" + + type_info = TypeInfo( + tag='default', + description='Persisted Bayesian pair-cache manifests', + ) + + def __init__(self) -> None: + super().__init__(item_type=BayesianPairCacheItem) + + def create( + self, + *, + parameter_names: tuple[str, str], + paths: BayesianPairCachePaths, + grid_shape: tuple[float, float], + n_draws_cached: float, + id: str | None = None, + ) -> None: + """ + Create a persisted Bayesian pair-cache manifest row. + + Parameters + ---------- + parameter_names : tuple[str, str] + Unique parameter names for the cached pair. + paths : BayesianPairCachePaths + HDF5 dataset paths for the cached pair payloads. + grid_shape : tuple[float, float] + Number of x-grid and y-grid points in the cached pair. + n_draws_cached : float + Number of draws summarized into the cached pair. + id : str | None, default=None + Explicit persisted row id. When omitted, a simple sequential + identifier is generated. + """ + param_unique_name_x, param_unique_name_y = parameter_names + normalized_x, normalized_y = _normalized_parameter_pair( + param_unique_name_x, + param_unique_name_y, + ) + n_grid_x, n_grid_y = grid_shape + item = BayesianPairCacheItem() + item._set_param_unique_name_x(normalized_x) + item._set_param_unique_name_y(normalized_y) + item._set_x_path(paths.x_path) + item._set_y_path(paths.y_path) + item._set_density_path(paths.density_path) + item._set_contour_level_path(paths.contour_level_path) + item._set_n_grid_x(n_grid_x) + item._set_n_grid_y(n_grid_y) + item._set_n_draws_cached(n_draws_cached) + resolved_id = id or str(len(self) + 1) + item._set_id(resolved_id) + self.add(item) diff --git a/src/easydiffraction/analysis/categories/bayesian_pair_caches/factory.py b/src/easydiffraction/analysis/categories/bayesian_pair_caches/factory.py new file mode 100644 index 000000000..5c6df89b1 --- /dev/null +++ b/src/easydiffraction/analysis/categories/bayesian_pair_caches/factory.py @@ -0,0 +1,17 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Bayesian-pair-caches factory.""" + +from __future__ import annotations + +from typing import ClassVar + +from easydiffraction.core.factory import FactoryBase + + +class BayesianPairCachesFactory(FactoryBase): + """Create Bayesian-pair-cache collections by tag.""" + + _default_rules: ClassVar[dict] = { + frozenset(): 'default', + } diff --git a/src/easydiffraction/analysis/categories/bayesian_parameter_posteriors/__init__.py b/src/easydiffraction/analysis/categories/bayesian_parameter_posteriors/__init__.py new file mode 100644 index 000000000..10ec76953 --- /dev/null +++ b/src/easydiffraction/analysis/categories/bayesian_parameter_posteriors/__init__.py @@ -0,0 +1,12 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + +from easydiffraction.analysis.categories.bayesian_parameter_posteriors.default import ( + BayesianParameterPosteriorItem, +) +from easydiffraction.analysis.categories.bayesian_parameter_posteriors.default import ( + BayesianParameterPosteriors, +) +from easydiffraction.analysis.categories.bayesian_parameter_posteriors.factory import ( + BayesianParameterPosteriorsFactory, +) diff --git a/src/easydiffraction/analysis/categories/bayesian_parameter_posteriors/default.py b/src/easydiffraction/analysis/categories/bayesian_parameter_posteriors/default.py new file mode 100644 index 000000000..ea234f0ec --- /dev/null +++ b/src/easydiffraction/analysis/categories/bayesian_parameter_posteriors/default.py @@ -0,0 +1,242 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Bayesian parameter posterior summary rows.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from easydiffraction.analysis.categories.bayesian_parameter_posteriors.factory import ( + BayesianParameterPosteriorsFactory, +) +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 NumericDescriptor +from easydiffraction.core.variable import StringDescriptor +from easydiffraction.io.cif.handler import CifHandler + +if TYPE_CHECKING: + from easydiffraction.analysis.fit_helpers.bayesian import PosteriorParameterSummary + + +class BayesianParameterPosteriorItem(CategoryItem): + """Single persisted Bayesian parameter posterior summary row.""" + + _category_code = 'bayesian_parameter_posterior' + _category_entry_name = 'unique_name' + + def __init__(self) -> None: + super().__init__() + self._unique_name = StringDescriptor( + name='unique_name', + description='Unique EasyDiffraction parameter name.', + value_spec=AttributeSpec( + default='_', + validator=RegexValidator(pattern=r'^[A-Za-z_][A-Za-z0-9_.]*$'), + ), + cif_handler=CifHandler(names=['_bayesian_parameter_posterior.unique_name']), + ) + self._display_name = StringDescriptor( + name='display_name', + description='Human-readable parameter label.', + value_spec=AttributeSpec(default=''), + cif_handler=CifHandler(names=['_bayesian_parameter_posterior.display_name']), + ) + self._best_sample_value = NumericDescriptor( + name='best_sample_value', + description='Committed sampled parameter value.', + value_spec=AttributeSpec(default=None, allow_none=True), + cif_handler=CifHandler(names=['_bayesian_parameter_posterior.best_sample_value']), + ) + self._median = NumericDescriptor( + name='median', + description='Posterior median value.', + value_spec=AttributeSpec(default=None, allow_none=True), + cif_handler=CifHandler(names=['_bayesian_parameter_posterior.median']), + ) + self._uncertainty = NumericDescriptor( + name='uncertainty', + description='Posterior standard deviation.', + value_spec=AttributeSpec(default=None, allow_none=True), + cif_handler=CifHandler(names=['_bayesian_parameter_posterior.uncertainty']), + ) + self._interval_68_lower = NumericDescriptor( + name='interval_68_lower', + description='Lower bound of the 68% credible interval.', + value_spec=AttributeSpec(default=None, allow_none=True), + cif_handler=CifHandler(names=['_bayesian_parameter_posterior.interval_68_lower']), + ) + self._interval_68_upper = NumericDescriptor( + name='interval_68_upper', + description='Upper bound of the 68% credible interval.', + value_spec=AttributeSpec(default=None, allow_none=True), + cif_handler=CifHandler(names=['_bayesian_parameter_posterior.interval_68_upper']), + ) + self._interval_95_lower = NumericDescriptor( + name='interval_95_lower', + description='Lower bound of the 95% credible interval.', + value_spec=AttributeSpec(default=None, allow_none=True), + cif_handler=CifHandler(names=['_bayesian_parameter_posterior.interval_95_lower']), + ) + self._interval_95_upper = NumericDescriptor( + name='interval_95_upper', + description='Upper bound of the 95% credible interval.', + value_spec=AttributeSpec(default=None, allow_none=True), + cif_handler=CifHandler(names=['_bayesian_parameter_posterior.interval_95_upper']), + ) + self._ess_bulk = NumericDescriptor( + name='ess_bulk', + description='Bulk effective sample size when available.', + value_spec=AttributeSpec(default=None, allow_none=True), + cif_handler=CifHandler(names=['_bayesian_parameter_posterior.ess_bulk']), + ) + self._r_hat = NumericDescriptor( + name='r_hat', + description='Rank-normalized split-R-hat when available.', + value_spec=AttributeSpec(default=None, allow_none=True), + cif_handler=CifHandler(names=['_bayesian_parameter_posterior.r_hat']), + ) + + @property + def unique_name(self) -> StringDescriptor: + """Unique EasyDiffraction parameter name.""" + return self._unique_name + + def _set_unique_name(self, value: str) -> None: + """Set the unique parameter name for internal callers.""" + self._unique_name.value = value + + @property + def display_name(self) -> StringDescriptor: + """Human-readable parameter label.""" + return self._display_name + + def _set_display_name(self, value: str) -> None: + """Set the display name for internal callers.""" + self._display_name.value = value + + @property + def best_sample_value(self) -> NumericDescriptor: + """Committed sampled parameter value.""" + return self._best_sample_value + + def _set_best_sample_value(self, value: float | None) -> None: + """Set the best sampled parameter value for internal callers.""" + self._best_sample_value.value = value + + @property + def median(self) -> NumericDescriptor: + """Posterior median value.""" + return self._median + + def _set_median(self, value: float | None) -> None: + """Set the posterior median for internal callers.""" + self._median.value = value + + @property + def uncertainty(self) -> NumericDescriptor: + """Posterior standard deviation.""" + return self._uncertainty + + def _set_uncertainty(self, value: float | None) -> None: + """Set the posterior uncertainty for internal callers.""" + self._uncertainty.value = value + + @property + def interval_68_lower(self) -> NumericDescriptor: + """Lower bound of the 68% credible interval.""" + return self._interval_68_lower + + def _set_interval_68_lower(self, value: float | None) -> None: + """Set the 68% interval lower bound for internal callers.""" + self._interval_68_lower.value = value + + @property + def interval_68_upper(self) -> NumericDescriptor: + """Upper bound of the 68% credible interval.""" + return self._interval_68_upper + + def _set_interval_68_upper(self, value: float | None) -> None: + """Set the 68% interval upper bound for internal callers.""" + self._interval_68_upper.value = value + + @property + def interval_95_lower(self) -> NumericDescriptor: + """Lower bound of the 95% credible interval.""" + return self._interval_95_lower + + def _set_interval_95_lower(self, value: float | None) -> None: + """Set the 95% interval lower bound for internal callers.""" + self._interval_95_lower.value = value + + @property + def interval_95_upper(self) -> NumericDescriptor: + """Upper bound of the 95% credible interval.""" + return self._interval_95_upper + + def _set_interval_95_upper(self, value: float | None) -> None: + """Set the 95% interval upper bound for internal callers.""" + self._interval_95_upper.value = value + + @property + def ess_bulk(self) -> NumericDescriptor: + """Bulk effective sample size when available.""" + return self._ess_bulk + + def _set_ess_bulk(self, value: float | None) -> None: + """Set the ESS bulk value for internal callers.""" + self._ess_bulk.value = value + + @property + def r_hat(self) -> NumericDescriptor: + """Rank-normalized split-R-hat when available.""" + return self._r_hat + + def _set_r_hat(self, value: float | None) -> None: + """Set the R-hat value for internal callers.""" + self._r_hat.value = value + + +@BayesianParameterPosteriorsFactory.register +class BayesianParameterPosteriors(CategoryCollection): + """ + Collection of persisted Bayesian parameter posterior summaries. + """ + + type_info = TypeInfo( + tag='default', + description='Persisted Bayesian parameter posterior summaries', + ) + + def __init__(self) -> None: + super().__init__(item_type=BayesianParameterPosteriorItem) + + def create( + self, + *, + summary: PosteriorParameterSummary, + ) -> None: + """ + Create a persisted Bayesian parameter posterior summary row. + + Parameters + ---------- + summary : PosteriorParameterSummary + Runtime posterior summary to persist. + """ + item = BayesianParameterPosteriorItem() + item._set_unique_name(summary.unique_name) + item._set_display_name(summary.display_name) + item._set_best_sample_value(summary.best_sample_value) + item._set_median(summary.median) + item._set_uncertainty(summary.standard_deviation) + item._set_interval_68_lower(summary.interval_68[0]) + item._set_interval_68_upper(summary.interval_68[1]) + item._set_interval_95_lower(summary.interval_95[0]) + item._set_interval_95_upper(summary.interval_95[1]) + item._set_ess_bulk(summary.ess_bulk) + item._set_r_hat(summary.r_hat) + self.add(item) diff --git a/src/easydiffraction/analysis/categories/bayesian_parameter_posteriors/factory.py b/src/easydiffraction/analysis/categories/bayesian_parameter_posteriors/factory.py new file mode 100644 index 000000000..54eef2b83 --- /dev/null +++ b/src/easydiffraction/analysis/categories/bayesian_parameter_posteriors/factory.py @@ -0,0 +1,17 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Bayesian-parameter-posteriors factory.""" + +from __future__ import annotations + +from typing import ClassVar + +from easydiffraction.core.factory import FactoryBase + + +class BayesianParameterPosteriorsFactory(FactoryBase): + """Create Bayesian-parameter-posterior collections by tag.""" + + _default_rules: ClassVar[dict] = { + frozenset(): 'default', + } diff --git a/src/easydiffraction/analysis/categories/bayesian_predictive_datasets/__init__.py b/src/easydiffraction/analysis/categories/bayesian_predictive_datasets/__init__.py new file mode 100644 index 000000000..f706de4fc --- /dev/null +++ b/src/easydiffraction/analysis/categories/bayesian_predictive_datasets/__init__.py @@ -0,0 +1,12 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + +from easydiffraction.analysis.categories.bayesian_predictive_datasets.default import ( + BayesianPredictiveDatasetItem, +) +from easydiffraction.analysis.categories.bayesian_predictive_datasets.default import ( + BayesianPredictiveDatasets, +) +from easydiffraction.analysis.categories.bayesian_predictive_datasets.factory import ( + BayesianPredictiveDatasetsFactory, +) diff --git a/src/easydiffraction/analysis/categories/bayesian_predictive_datasets/default.py b/src/easydiffraction/analysis/categories/bayesian_predictive_datasets/default.py new file mode 100644 index 000000000..9bb8acf00 --- /dev/null +++ b/src/easydiffraction/analysis/categories/bayesian_predictive_datasets/default.py @@ -0,0 +1,260 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Bayesian predictive-dataset manifest rows.""" + +from __future__ import annotations + +from dataclasses import dataclass + +from easydiffraction.analysis.categories.bayesian_predictive_datasets.factory import ( + BayesianPredictiveDatasetsFactory, +) +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.variable import NumericDescriptor +from easydiffraction.core.variable import StringDescriptor +from easydiffraction.io.cif.handler import CifHandler + + +@dataclass(frozen=True, slots=True) +class BayesianPredictiveDatasetPaths: + """HDF5 dataset paths for one predictive dataset.""" + + x_path: str + best_sample_prediction_path: str + lower_95_path: str | None = None + upper_95_path: str | None = None + lower_68_path: str | None = None + upper_68_path: str | None = None + draws_path: str | None = None + + +class BayesianPredictiveDatasetItem(CategoryItem): + """Single persisted Bayesian predictive-dataset manifest row.""" + + _category_code = 'bayesian_predictive_dataset' + _category_entry_name = 'experiment_name' + + def __init__(self) -> None: + super().__init__() + self._experiment_name = StringDescriptor( + name='experiment_name', + description='Experiment name for the cached predictive dataset.', + value_spec=AttributeSpec(default=''), + cif_handler=CifHandler(names=['_bayesian_predictive_dataset.experiment_name']), + ) + self._x_axis_name = StringDescriptor( + name='x_axis_name', + description='Name of the predictive dataset x-axis.', + value_spec=AttributeSpec(default=''), + cif_handler=CifHandler(names=['_bayesian_predictive_dataset.x_axis_name']), + ) + self._x_path = StringDescriptor( + name='x_path', + description='HDF5 dataset path for the predictive x-axis values.', + value_spec=AttributeSpec(default=''), + cif_handler=CifHandler(names=['_bayesian_predictive_dataset.x_path']), + ) + self._best_sample_prediction_path = StringDescriptor( + name='best_sample_prediction_path', + description='HDF5 dataset path for the committed predictive curve.', + value_spec=AttributeSpec(default=''), + cif_handler=CifHandler( + names=['_bayesian_predictive_dataset.best_sample_prediction_path'] + ), + ) + self._lower_95_path = StringDescriptor( + name='lower_95_path', + description='HDF5 dataset path for the lower 95% predictive band.', + value_spec=AttributeSpec(default=None, allow_none=True), + cif_handler=CifHandler(names=['_bayesian_predictive_dataset.lower_95_path']), + ) + self._upper_95_path = StringDescriptor( + name='upper_95_path', + description='HDF5 dataset path for the upper 95% predictive band.', + value_spec=AttributeSpec(default=None, allow_none=True), + cif_handler=CifHandler(names=['_bayesian_predictive_dataset.upper_95_path']), + ) + self._lower_68_path = StringDescriptor( + name='lower_68_path', + description='HDF5 dataset path for the lower 68% predictive band.', + value_spec=AttributeSpec(default=None, allow_none=True), + cif_handler=CifHandler(names=['_bayesian_predictive_dataset.lower_68_path']), + ) + self._upper_68_path = StringDescriptor( + name='upper_68_path', + description='HDF5 dataset path for the upper 68% predictive band.', + value_spec=AttributeSpec(default=None, allow_none=True), + cif_handler=CifHandler(names=['_bayesian_predictive_dataset.upper_68_path']), + ) + self._draws_path = StringDescriptor( + name='draws_path', + description='HDF5 dataset path for cached predictive draws.', + value_spec=AttributeSpec(default=None, allow_none=True), + cif_handler=CifHandler(names=['_bayesian_predictive_dataset.draws_path']), + ) + self._n_x = NumericDescriptor( + name='n_x', + description='Number of x-axis points in the cached predictive dataset.', + value_spec=AttributeSpec(default=0), + cif_handler=CifHandler(names=['_bayesian_predictive_dataset.n_x']), + ) + self._n_draws_cached = NumericDescriptor( + name='n_draws_cached', + description='Number of cached predictive draws.', + value_spec=AttributeSpec(default=0), + cif_handler=CifHandler(names=['_bayesian_predictive_dataset.n_draws_cached']), + ) + + @property + def experiment_name(self) -> StringDescriptor: + """Experiment name for the cached predictive dataset.""" + return self._experiment_name + + def _set_experiment_name(self, value: str) -> None: + """Set the experiment name for internal callers.""" + self._experiment_name.value = value + + @property + def x_axis_name(self) -> StringDescriptor: + """Name of the predictive dataset x-axis.""" + return self._x_axis_name + + def _set_x_axis_name(self, value: str) -> None: + """Set the x-axis name for internal callers.""" + self._x_axis_name.value = value + + @property + def x_path(self) -> StringDescriptor: + """HDF5 dataset path for the predictive x-axis values.""" + return self._x_path + + def _set_x_path(self, value: str) -> None: + """Set the predictive x-axis path for internal callers.""" + self._x_path.value = value + + @property + def best_sample_prediction_path(self) -> StringDescriptor: + """HDF5 dataset path for the committed predictive curve.""" + return self._best_sample_prediction_path + + def _set_best_sample_prediction_path(self, value: str) -> None: + """Set the best-sample prediction path for internal callers.""" + self._best_sample_prediction_path.value = value + + @property + def lower_95_path(self) -> StringDescriptor: + """HDF5 dataset path for the lower 95% predictive band.""" + return self._lower_95_path + + def _set_lower_95_path(self, value: str | None) -> None: + """Set the lower-95 path for internal callers.""" + self._lower_95_path.value = value + + @property + def upper_95_path(self) -> StringDescriptor: + """HDF5 dataset path for the upper 95% predictive band.""" + return self._upper_95_path + + def _set_upper_95_path(self, value: str | None) -> None: + """Set the upper-95 path for internal callers.""" + self._upper_95_path.value = value + + @property + def lower_68_path(self) -> StringDescriptor: + """HDF5 dataset path for the lower 68% predictive band.""" + return self._lower_68_path + + def _set_lower_68_path(self, value: str | None) -> None: + """Set the lower-68 path for internal callers.""" + self._lower_68_path.value = value + + @property + def upper_68_path(self) -> StringDescriptor: + """HDF5 dataset path for the upper 68% predictive band.""" + return self._upper_68_path + + def _set_upper_68_path(self, value: str | None) -> None: + """Set the upper-68 path for internal callers.""" + self._upper_68_path.value = value + + @property + def draws_path(self) -> StringDescriptor: + """HDF5 dataset path for cached predictive draws.""" + return self._draws_path + + def _set_draws_path(self, value: str | None) -> None: + """Set the predictive-draws path for internal callers.""" + self._draws_path.value = value + + @property + def n_x(self) -> NumericDescriptor: + """Number of x-axis points in the cached predictive dataset.""" + return self._n_x + + def _set_n_x(self, value: float) -> None: + """Set the predictive x-axis size for internal callers.""" + self._n_x.value = value + + @property + def n_draws_cached(self) -> NumericDescriptor: + """Number of cached predictive draws.""" + return self._n_draws_cached + + def _set_n_draws_cached(self, value: float) -> None: + """Set the cached predictive-draw count for internal callers.""" + self._n_draws_cached.value = value + + +@BayesianPredictiveDatasetsFactory.register +class BayesianPredictiveDatasets(CategoryCollection): + """Collection of persisted Bayesian predictive-dataset manifests.""" + + type_info = TypeInfo( + tag='default', + description='Persisted Bayesian predictive-dataset manifests', + ) + + def __init__(self) -> None: + super().__init__(item_type=BayesianPredictiveDatasetItem) + + def create( + self, + *, + experiment_name: str, + x_axis_name: str, + paths: BayesianPredictiveDatasetPaths, + n_x: float, + n_draws_cached: float, + ) -> None: + """ + Create a persisted Bayesian predictive-dataset manifest row. + + Parameters + ---------- + experiment_name : str + Experiment name for the cached predictive dataset. + x_axis_name : str + Name of the predictive dataset x-axis. + paths : BayesianPredictiveDatasetPaths + HDF5 dataset paths for the predictive dataset payloads. + n_x : float + Number of x-axis points in the cached predictive dataset. + n_draws_cached : float + Number of cached predictive draws. + """ + item = BayesianPredictiveDatasetItem() + item._set_experiment_name(experiment_name) + item._set_x_axis_name(x_axis_name) + item._set_x_path(paths.x_path) + item._set_best_sample_prediction_path(paths.best_sample_prediction_path) + item._set_lower_95_path(paths.lower_95_path) + item._set_upper_95_path(paths.upper_95_path) + item._set_lower_68_path(paths.lower_68_path) + item._set_upper_68_path(paths.upper_68_path) + item._set_draws_path(paths.draws_path) + item._set_n_x(n_x) + item._set_n_draws_cached(n_draws_cached) + self.add(item) diff --git a/src/easydiffraction/analysis/categories/bayesian_predictive_datasets/factory.py b/src/easydiffraction/analysis/categories/bayesian_predictive_datasets/factory.py new file mode 100644 index 000000000..a88449263 --- /dev/null +++ b/src/easydiffraction/analysis/categories/bayesian_predictive_datasets/factory.py @@ -0,0 +1,17 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Bayesian-predictive-datasets factory.""" + +from __future__ import annotations + +from typing import ClassVar + +from easydiffraction.core.factory import FactoryBase + + +class BayesianPredictiveDatasetsFactory(FactoryBase): + """Create Bayesian-predictive-dataset collections by tag.""" + + _default_rules: ClassVar[dict] = { + frozenset(): 'default', + } diff --git a/src/easydiffraction/analysis/categories/bayesian_result/__init__.py b/src/easydiffraction/analysis/categories/bayesian_result/__init__.py new file mode 100644 index 000000000..eab2a07f3 --- /dev/null +++ b/src/easydiffraction/analysis/categories/bayesian_result/__init__.py @@ -0,0 +1,5 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + +from easydiffraction.analysis.categories.bayesian_result.default import BayesianResult +from easydiffraction.analysis.categories.bayesian_result.factory import BayesianResultFactory diff --git a/src/easydiffraction/analysis/categories/bayesian_result/default.py b/src/easydiffraction/analysis/categories/bayesian_result/default.py new file mode 100644 index 000000000..dd4210711 --- /dev/null +++ b/src/easydiffraction/analysis/categories/bayesian_result/default.py @@ -0,0 +1,215 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Bayesian fit-result metadata category.""" + +from __future__ import annotations + +from easydiffraction.analysis.categories.bayesian_result.factory import BayesianResultFactory +from easydiffraction.core.category import CategoryItem +from easydiffraction.core.metadata import TypeInfo +from easydiffraction.core.validation import AttributeSpec +from easydiffraction.core.variable import BoolDescriptor +from easydiffraction.core.variable import NumericDescriptor +from easydiffraction.core.variable import StringDescriptor +from easydiffraction.io.cif.handler import CifHandler + + +@BayesianResultFactory.register +class BayesianResult(CategoryItem): + """Persisted Bayesian fit-result metadata.""" + + _category_code = 'bayesian_result' + + type_info = TypeInfo( + tag='default', + description='Persisted Bayesian fit-result metadata', + ) + + def __init__(self) -> None: + super().__init__() + self._sampler_name = StringDescriptor( + name='sampler_name', + description='Name of the persisted Bayesian sampler.', + value_spec=AttributeSpec(default='dream'), + cif_handler=CifHandler(names=['_bayesian_result.sampler_name']), + ) + self._point_estimate_name = StringDescriptor( + name='point_estimate_name', + description='Committed sampled point estimate name.', + value_spec=AttributeSpec(default='best_sample'), + cif_handler=CifHandler(names=['_bayesian_result.point_estimate_name']), + ) + self._success = BoolDescriptor( + name='success', + description='Whether the persisted Bayesian fit produced usable results.', + value_spec=AttributeSpec(default=False), + cif_handler=CifHandler(names=['_bayesian_result.success']), + ) + self._sampler_completed = BoolDescriptor( + name='sampler_completed', + description='Whether the sampler completed and returned posterior data.', + value_spec=AttributeSpec(default=False), + cif_handler=CifHandler(names=['_bayesian_result.sampler_completed']), + ) + self._best_log_posterior = NumericDescriptor( + name='best_log_posterior', + description='Best log-posterior value reported by the sampler.', + value_spec=AttributeSpec(default=None, allow_none=True), + cif_handler=CifHandler(names=['_bayesian_result.best_log_posterior']), + ) + self._credible_interval_inner = NumericDescriptor( + name='credible_interval_inner', + description='Inner credible-interval level used in summaries.', + value_spec=AttributeSpec(default=0.68), + cif_handler=CifHandler(names=['_bayesian_result.credible_interval_inner']), + ) + self._credible_interval_outer = NumericDescriptor( + name='credible_interval_outer', + description='Outer credible-interval level used in summaries.', + value_spec=AttributeSpec(default=0.95), + cif_handler=CifHandler(names=['_bayesian_result.credible_interval_outer']), + ) + self._has_posterior_samples = BoolDescriptor( + name='has_posterior_samples', + description='Whether posterior samples were persisted.', + value_spec=AttributeSpec(default=False), + cif_handler=CifHandler(names=['_bayesian_result.has_posterior_samples']), + ) + self._has_distribution_cache = BoolDescriptor( + name='has_distribution_cache', + description='Whether distribution-cache manifests were persisted.', + value_spec=AttributeSpec(default=False), + cif_handler=CifHandler(names=['_bayesian_result.has_distribution_cache']), + ) + self._has_pair_cache = BoolDescriptor( + name='has_pair_cache', + description='Whether pair-cache manifests were persisted.', + value_spec=AttributeSpec(default=False), + cif_handler=CifHandler(names=['_bayesian_result.has_pair_cache']), + ) + self._has_posterior_predictive = BoolDescriptor( + name='has_posterior_predictive', + description='Whether posterior predictive manifests were persisted.', + value_spec=AttributeSpec(default=False), + cif_handler=CifHandler(names=['_bayesian_result.has_posterior_predictive']), + ) + self._sidecar_file = StringDescriptor( + name='sidecar_file', + description='Relative path to the persisted Bayesian HDF5 sidecar.', + value_spec=AttributeSpec(default='results.h5'), + cif_handler=CifHandler(names=['_bayesian_result.sidecar_file']), + ) + + @property + def sampler_name(self) -> StringDescriptor: + """Name of the persisted Bayesian sampler.""" + return self._sampler_name + + def _set_sampler_name(self, value: str) -> None: + """Set the sampler name for internal callers.""" + self._sampler_name.value = value + + @property + def point_estimate_name(self) -> StringDescriptor: + """Committed sampled point estimate name.""" + return self._point_estimate_name + + def _set_point_estimate_name(self, value: str) -> None: + """Set the point-estimate name for internal callers.""" + self._point_estimate_name.value = value + + @property + def success(self) -> BoolDescriptor: + """ + Whether the persisted Bayesian fit produced usable results. + """ + return self._success + + def _set_success(self, *, value: bool) -> None: + """Set the success flag for internal callers.""" + self._success.value = value + + @property + def sampler_completed(self) -> BoolDescriptor: + """Whether the sampler completed and returned posterior data.""" + return self._sampler_completed + + def _set_sampler_completed(self, *, value: bool) -> None: + """Set the sampler-completed flag for internal callers.""" + self._sampler_completed.value = value + + @property + def best_log_posterior(self) -> NumericDescriptor: + """Best log-posterior value reported by the sampler.""" + return self._best_log_posterior + + def _set_best_log_posterior(self, value: float | None) -> None: + """Set the best log-posterior for internal callers.""" + self._best_log_posterior.value = value + + @property + def credible_interval_inner(self) -> NumericDescriptor: + """Inner credible-interval level used in summaries.""" + return self._credible_interval_inner + + def _set_credible_interval_inner(self, value: float) -> None: + """ + Set the inner credible-interval level for internal callers. + """ + self._credible_interval_inner.value = value + + @property + def credible_interval_outer(self) -> NumericDescriptor: + """Outer credible-interval level used in summaries.""" + return self._credible_interval_outer + + def _set_credible_interval_outer(self, value: float) -> None: + """ + Set the outer credible-interval level for internal callers. + """ + self._credible_interval_outer.value = value + + @property + def has_posterior_samples(self) -> BoolDescriptor: + """Whether posterior samples were persisted.""" + return self._has_posterior_samples + + def _set_has_posterior_samples(self, *, value: bool) -> None: + """Set the posterior-samples flag for internal callers.""" + self._has_posterior_samples.value = value + + @property + def has_distribution_cache(self) -> BoolDescriptor: + """Whether distribution-cache manifests were persisted.""" + return self._has_distribution_cache + + def _set_has_distribution_cache(self, *, value: bool) -> None: + """Set the distribution-cache flag for internal callers.""" + self._has_distribution_cache.value = value + + @property + def has_pair_cache(self) -> BoolDescriptor: + """Whether pair-cache manifests were persisted.""" + return self._has_pair_cache + + def _set_has_pair_cache(self, *, value: bool) -> None: + """Set the pair-cache flag for internal callers.""" + self._has_pair_cache.value = value + + @property + def has_posterior_predictive(self) -> BoolDescriptor: + """Whether posterior predictive manifests were persisted.""" + return self._has_posterior_predictive + + def _set_has_posterior_predictive(self, *, value: bool) -> None: + """Set the posterior-predictive flag for internal callers.""" + self._has_posterior_predictive.value = value + + @property + def sidecar_file(self) -> StringDescriptor: + """Relative path to the persisted Bayesian HDF5 sidecar.""" + return self._sidecar_file + + def _set_sidecar_file(self, value: str) -> None: + """Set the sidecar-file path for internal callers.""" + self._sidecar_file.value = value diff --git a/src/easydiffraction/project/categories/display/factory.py b/src/easydiffraction/analysis/categories/bayesian_result/factory.py similarity index 70% rename from src/easydiffraction/project/categories/display/factory.py rename to src/easydiffraction/analysis/categories/bayesian_result/factory.py index 0d1be80a9..3d437a0df 100644 --- a/src/easydiffraction/project/categories/display/factory.py +++ b/src/easydiffraction/analysis/categories/bayesian_result/factory.py @@ -1,6 +1,6 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -"""Factory for project display categories.""" +"""Bayesian-result factory.""" from __future__ import annotations @@ -9,8 +9,8 @@ from easydiffraction.core.factory import FactoryBase -class DisplayFactory(FactoryBase): - """Create project display category instances.""" +class BayesianResultFactory(FactoryBase): + """Create Bayesian-result categories by tag.""" _default_rules: ClassVar[dict] = { frozenset(): 'default', diff --git a/src/easydiffraction/analysis/categories/bayesian_sampler/__init__.py b/src/easydiffraction/analysis/categories/bayesian_sampler/__init__.py new file mode 100644 index 000000000..962e3fbae --- /dev/null +++ b/src/easydiffraction/analysis/categories/bayesian_sampler/__init__.py @@ -0,0 +1,5 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + +from easydiffraction.analysis.categories.bayesian_sampler.default import BayesianSampler +from easydiffraction.analysis.categories.bayesian_sampler.factory import BayesianSamplerFactory diff --git a/src/easydiffraction/analysis/categories/bayesian_sampler/default.py b/src/easydiffraction/analysis/categories/bayesian_sampler/default.py new file mode 100644 index 000000000..c75ffa156 --- /dev/null +++ b/src/easydiffraction/analysis/categories/bayesian_sampler/default.py @@ -0,0 +1,133 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Resolved Bayesian sampler settings category.""" + +from __future__ import annotations + +from easydiffraction.analysis.categories.bayesian_sampler.factory import BayesianSamplerFactory +from easydiffraction.core.category import CategoryItem +from easydiffraction.core.metadata import TypeInfo +from easydiffraction.core.validation import AttributeSpec +from easydiffraction.core.variable import IntegerDescriptor +from easydiffraction.core.variable import StringDescriptor +from easydiffraction.io.cif.handler import CifHandler + + +@BayesianSamplerFactory.register +class BayesianSampler(CategoryItem): + """Persisted resolved Bayesian sampler settings.""" + + _category_code = 'bayesian_sampler' + + type_info = TypeInfo( + tag='default', + description='Persisted resolved Bayesian sampler settings', + ) + + def __init__(self) -> None: + super().__init__() + self._steps = IntegerDescriptor( + name='steps', + description='Resolved number of sampler steps.', + value_spec=AttributeSpec(default=0), + cif_handler=CifHandler(names=['_bayesian_sampler.steps']), + ) + self._burn = IntegerDescriptor( + name='burn', + description='Resolved burn-in count.', + value_spec=AttributeSpec(default=0), + cif_handler=CifHandler(names=['_bayesian_sampler.burn']), + ) + self._thin = IntegerDescriptor( + name='thin', + description='Resolved thinning interval.', + value_spec=AttributeSpec(default=0), + cif_handler=CifHandler(names=['_bayesian_sampler.thin']), + ) + self._pop = IntegerDescriptor( + name='pop', + description='Resolved population size.', + value_spec=AttributeSpec(default=0), + cif_handler=CifHandler(names=['_bayesian_sampler.pop']), + ) + self._parallel = IntegerDescriptor( + name='parallel', + description='Resolved DREAM worker count; 0 uses all CPUs.', + value_spec=AttributeSpec(default=0), + cif_handler=CifHandler(names=['_bayesian_sampler.parallel']), + ) + self._init = StringDescriptor( + name='init', + description='Resolved DREAM initialization mode.', + value_spec=AttributeSpec(default=''), + cif_handler=CifHandler(names=['_bayesian_sampler.init']), + ) + self._random_seed = IntegerDescriptor( + name='random_seed', + description='Resolved random seed used by the sampler.', + value_spec=AttributeSpec(default=None, allow_none=True), + cif_handler=CifHandler(names=['_bayesian_sampler.random_seed']), + ) + + @property + def steps(self) -> IntegerDescriptor: + """Resolved number of sampler steps.""" + return self._steps + + def _set_steps(self, value: int) -> None: + """Set the step count for internal callers.""" + self._steps.value = value + + @property + def burn(self) -> IntegerDescriptor: + """Resolved burn-in count.""" + return self._burn + + def _set_burn(self, value: int) -> None: + """Set the burn-in count for internal callers.""" + self._burn.value = value + + @property + def thin(self) -> IntegerDescriptor: + """Resolved thinning interval.""" + return self._thin + + def _set_thin(self, value: int) -> None: + """Set the thinning interval for internal callers.""" + self._thin.value = value + + @property + def pop(self) -> IntegerDescriptor: + """Resolved population size.""" + return self._pop + + def _set_pop(self, value: int) -> None: + """Set the population size for internal callers.""" + self._pop.value = value + + @property + def parallel(self) -> IntegerDescriptor: + """Resolved DREAM worker count; 0 uses all CPUs.""" + return self._parallel + + def _set_parallel(self, value: int) -> None: + """Set the DREAM worker count for internal callers.""" + self._parallel.value = value + + @property + def init(self) -> StringDescriptor: + """Resolved DREAM initialization mode.""" + return self._init + + def _set_init(self, value: str) -> None: + """Set the initialization mode for internal callers.""" + self._init.value = value + + @property + def random_seed(self) -> IntegerDescriptor: + """Resolved random seed used by the sampler.""" + return self._random_seed + + def _set_random_seed(self, value: int | None) -> None: + """Set the random seed for internal callers.""" + self._random_seed.value = value diff --git a/src/easydiffraction/analysis/categories/fit/factory.py b/src/easydiffraction/analysis/categories/bayesian_sampler/factory.py similarity index 70% rename from src/easydiffraction/analysis/categories/fit/factory.py rename to src/easydiffraction/analysis/categories/bayesian_sampler/factory.py index 37bef2bbc..6f5d1033c 100644 --- a/src/easydiffraction/analysis/categories/fit/factory.py +++ b/src/easydiffraction/analysis/categories/bayesian_sampler/factory.py @@ -1,6 +1,6 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -"""Fit factory — delegates entirely to ``FactoryBase``.""" +"""Bayesian-sampler factory.""" from __future__ import annotations @@ -9,8 +9,8 @@ from easydiffraction.core.factory import FactoryBase -class FitFactory(FactoryBase): - """Create fit category items by tag.""" +class BayesianSamplerFactory(FactoryBase): + """Create Bayesian-sampler categories by tag.""" _default_rules: ClassVar[dict] = { frozenset(): 'default', diff --git a/src/easydiffraction/analysis/categories/constraints/default.py b/src/easydiffraction/analysis/categories/constraints/default.py index 63a4264d4..0b622e183 100644 --- a/src/easydiffraction/analysis/categories/constraints/default.py +++ b/src/easydiffraction/analysis/categories/constraints/default.py @@ -18,14 +18,29 @@ from easydiffraction.core.validation import RegexValidator from easydiffraction.core.variable import StringDescriptor from easydiffraction.io.cif.handler import CifHandler +from easydiffraction.utils.logging import console +from easydiffraction.utils.logging import log +from easydiffraction.utils.utils import render_table class Constraint(CategoryItem): """Single constraint item stored as ``lhs = rhs`` expression.""" + _category_code = 'constraint' + _category_entry_name = 'id' + def __init__(self) -> None: super().__init__() + self._id = StringDescriptor( + name='id', + description='Explicit identifier for this constraint row.', + value_spec=AttributeSpec( + default='_', + validator=RegexValidator(pattern=r'^[A-Za-z0-9_]*$'), + ), + cif_handler=CifHandler(names=['_constraint.id']), + ) self._expression = StringDescriptor( name='expression', description='Constraint equation, e.g. "occ_Ba = 1 - occ_La".', @@ -36,13 +51,19 @@ def __init__(self) -> None: cif_handler=CifHandler(names=['_constraint.expression']), ) - self._identity.category_code = 'constraint' - self._identity.category_entry_name = lambda: self.lhs_alias - # ------------------------------------------------------------------ # Public properties # ------------------------------------------------------------------ + @property + def id(self) -> StringDescriptor: + """Explicit identifier for this constraint row.""" + return self._id + + @id.setter + def id(self, value: str) -> None: + self._id.value = value + @property def expression(self) -> StringDescriptor: """ @@ -116,7 +137,7 @@ def disable(self) -> None: """Deactivate constraints without deleting them.""" self._enabled = False - def create(self, *, expression: str) -> None: + def create(self, *, expression: str, id: str | None = None) -> None: """ Create a constraint from an expression string. @@ -127,8 +148,41 @@ def create(self, *, expression: str) -> None: expression : str Constraint equation, e.g. ``'biso_Co2 = biso_Co1'`` or ``'occ_Ba = 1 - occ_La'``. + id : str | None, default=None + Explicit row identifier. When not ``None``, this value is + used as the collection key instead of the left-hand alias. """ item = Constraint() item.expression = expression + if id is not None: + item.id = id + elif item.lhs_alias: + item.id = item.lhs_alias self.add(item) self._enabled = True + + def _after_from_cif(self) -> None: + """ + Backfill explicit ids when loading older CIF constraint loops. + """ + for item in self: + constraint_id = item.id.value.strip() + if constraint_id not in {'', '_', '?'} or not item.lhs_alias: + continue + item.id = item.lhs_alias + + def show(self) -> None: + """Print a table of all user-defined symbolic constraints.""" + if not self._items: + log.warning('No constraints defined.') + return + + rows = [[constraint.id.value, constraint.expression.value] for constraint in self] + + console.paragraph('User defined constraints') + render_table( + columns_headers=['id', 'expression'], + columns_alignment=['left', 'left'], + columns_data=rows, + ) + console.print(f'Constraints enabled: {self.enabled}') diff --git a/src/easydiffraction/analysis/categories/deterministic_result/__init__.py b/src/easydiffraction/analysis/categories/deterministic_result/__init__.py new file mode 100644 index 000000000..18ed7af8f --- /dev/null +++ b/src/easydiffraction/analysis/categories/deterministic_result/__init__.py @@ -0,0 +1,7 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + +from easydiffraction.analysis.categories.deterministic_result.default import DeterministicResult +from easydiffraction.analysis.categories.deterministic_result.factory import ( + DeterministicResultFactory, +) diff --git a/src/easydiffraction/analysis/categories/deterministic_result/default.py b/src/easydiffraction/analysis/categories/deterministic_result/default.py new file mode 100644 index 000000000..f66f2018d --- /dev/null +++ b/src/easydiffraction/analysis/categories/deterministic_result/default.py @@ -0,0 +1,187 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Deterministic fit-result metadata category.""" + +from __future__ import annotations + +from easydiffraction.analysis.categories.deterministic_result.factory import ( + DeterministicResultFactory, +) +from easydiffraction.core.category import CategoryItem +from easydiffraction.core.metadata import TypeInfo +from easydiffraction.core.validation import AttributeSpec +from easydiffraction.core.variable import BoolDescriptor +from easydiffraction.core.variable import NumericDescriptor +from easydiffraction.core.variable import StringDescriptor +from easydiffraction.io.cif.handler import CifHandler + + +@DeterministicResultFactory.register +class DeterministicResult(CategoryItem): + """Persisted deterministic fit-result metadata.""" + + _category_code = 'deterministic_result' + + type_info = TypeInfo( + tag='default', + description='Persisted deterministic fit-result metadata', + ) + + def __init__(self) -> None: + super().__init__() + self._optimizer_name = StringDescriptor( + name='optimizer_name', + description='Name of the persisted deterministic optimizer.', + value_spec=AttributeSpec(default=''), + cif_handler=CifHandler(names=['_deterministic_result.optimizer_name']), + ) + self._method_name = StringDescriptor( + name='method_name', + description='Method name of the persisted deterministic optimizer.', + value_spec=AttributeSpec(default=''), + cif_handler=CifHandler(names=['_deterministic_result.method_name']), + ) + self._objective_name = StringDescriptor( + name='objective_name', + description='Objective function name for the persisted deterministic fit.', + value_spec=AttributeSpec(default=''), + cif_handler=CifHandler(names=['_deterministic_result.objective_name']), + ) + self._objective_value = NumericDescriptor( + name='objective_value', + description='Objective value for the persisted deterministic fit.', + value_spec=AttributeSpec(default=None, allow_none=True), + cif_handler=CifHandler(names=['_deterministic_result.objective_value']), + ) + self._n_data_points = NumericDescriptor( + name='n_data_points', + description='Number of data points used in the persisted deterministic fit.', + value_spec=AttributeSpec(default=0), + cif_handler=CifHandler(names=['_deterministic_result.n_data_points']), + ) + self._n_parameters = NumericDescriptor( + name='n_parameters', + description='Number of parameters considered in the persisted deterministic fit.', + value_spec=AttributeSpec(default=0), + cif_handler=CifHandler(names=['_deterministic_result.n_parameters']), + ) + self._n_free_parameters = NumericDescriptor( + name='n_free_parameters', + description='Number of free parameters in the persisted deterministic fit.', + value_spec=AttributeSpec(default=0), + cif_handler=CifHandler(names=['_deterministic_result.n_free_parameters']), + ) + self._degrees_of_freedom = NumericDescriptor( + name='degrees_of_freedom', + description='Degrees of freedom for the persisted deterministic fit.', + value_spec=AttributeSpec(default=0), + cif_handler=CifHandler(names=['_deterministic_result.degrees_of_freedom']), + ) + self._covariance_available = BoolDescriptor( + name='covariance_available', + description='Whether covariance was available for the persisted deterministic fit.', + value_spec=AttributeSpec(default=False), + cif_handler=CifHandler(names=['_deterministic_result.covariance_available']), + ) + self._correlation_available = BoolDescriptor( + name='correlation_available', + description='Whether correlations were available for the persisted deterministic fit.', + value_spec=AttributeSpec(default=False), + cif_handler=CifHandler(names=['_deterministic_result.correlation_available']), + ) + + @property + def optimizer_name(self) -> StringDescriptor: + """Name of the persisted deterministic optimizer.""" + return self._optimizer_name + + def _set_optimizer_name(self, value: str) -> None: + """Set the optimizer name for internal callers.""" + self._optimizer_name.value = value + + @property + def method_name(self) -> StringDescriptor: + """Method name of the persisted deterministic optimizer.""" + return self._method_name + + def _set_method_name(self, value: str) -> None: + """Set the method name for internal callers.""" + self._method_name.value = value + + @property + def objective_name(self) -> StringDescriptor: + """ + Objective function name for the persisted deterministic fit. + """ + return self._objective_name + + def _set_objective_name(self, value: str) -> None: + """Set the objective name for internal callers.""" + self._objective_name.value = value + + @property + def objective_value(self) -> NumericDescriptor: + """Objective value for the persisted deterministic fit.""" + return self._objective_value + + def _set_objective_value(self, value: float | None) -> None: + """Set the objective value for internal callers.""" + self._objective_value.value = value + + @property + def n_data_points(self) -> NumericDescriptor: + """ + Number of data points used in the persisted deterministic fit. + """ + return self._n_data_points + + def _set_n_data_points(self, value: float) -> None: + """Set the data-point count for internal callers.""" + self._n_data_points.value = value + + @property + def n_parameters(self) -> NumericDescriptor: + """Number of parameters considered in the persisted fit.""" + return self._n_parameters + + def _set_n_parameters(self, value: float) -> None: + """Set the parameter count for internal callers.""" + self._n_parameters.value = value + + @property + def n_free_parameters(self) -> NumericDescriptor: + """ + Number of free parameters in the persisted deterministic fit. + """ + return self._n_free_parameters + + def _set_n_free_parameters(self, value: float) -> None: + """Set the free-parameter count for internal callers.""" + self._n_free_parameters.value = value + + @property + def degrees_of_freedom(self) -> NumericDescriptor: + """Degrees of freedom for the persisted deterministic fit.""" + return self._degrees_of_freedom + + def _set_degrees_of_freedom(self, value: float) -> None: + """Set the degrees of freedom for internal callers.""" + self._degrees_of_freedom.value = value + + @property + def covariance_available(self) -> BoolDescriptor: + """Whether covariance was available for the persisted fit.""" + return self._covariance_available + + def _set_covariance_available(self, *, value: bool) -> None: + """Set the covariance-available flag for internal callers.""" + self._covariance_available.value = value + + @property + def correlation_available(self) -> BoolDescriptor: + """Whether correlations were available for the persisted fit.""" + return self._correlation_available + + def _set_correlation_available(self, *, value: bool) -> None: + """Set the correlation-available flag for internal callers.""" + self._correlation_available.value = value diff --git a/src/easydiffraction/analysis/categories/deterministic_result/factory.py b/src/easydiffraction/analysis/categories/deterministic_result/factory.py new file mode 100644 index 000000000..1ee96acf9 --- /dev/null +++ b/src/easydiffraction/analysis/categories/deterministic_result/factory.py @@ -0,0 +1,17 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Deterministic-result factory.""" + +from __future__ import annotations + +from typing import ClassVar + +from easydiffraction.core.factory import FactoryBase + + +class DeterministicResultFactory(FactoryBase): + """Create deterministic-result categories by tag.""" + + _default_rules: ClassVar[dict] = { + frozenset(): 'default', + } diff --git a/src/easydiffraction/analysis/categories/fit/__init__.py b/src/easydiffraction/analysis/categories/fit/__init__.py deleted file mode 100644 index 2be409961..000000000 --- a/src/easydiffraction/analysis/categories/fit/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# SPDX-FileCopyrightText: 2026 EasyScience contributors -# SPDX-License-Identifier: BSD-3-Clause - -from easydiffraction.analysis.categories.fit.default import Fit -from easydiffraction.analysis.categories.fit.enums import FitModeEnum -from easydiffraction.analysis.categories.fit.factory import FitFactory diff --git a/src/easydiffraction/analysis/categories/fit/enums.py b/src/easydiffraction/analysis/categories/fit/enums.py deleted file mode 100644 index a5b87054e..000000000 --- a/src/easydiffraction/analysis/categories/fit/enums.py +++ /dev/null @@ -1,30 +0,0 @@ -# SPDX-FileCopyrightText: 2026 EasyScience contributors -# SPDX-License-Identifier: BSD-3-Clause -"""Enumeration for fit-mode values.""" - -from __future__ import annotations - -from enum import StrEnum - - -class FitModeEnum(StrEnum): - """Fitting mode for the analysis.""" - - SINGLE = 'single' - JOINT = 'joint' - SEQUENTIAL = 'sequential' - - @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' - if self is FitModeEnum.JOINT: - return 'Simultaneous fitting of all experiments with weights' - if self is FitModeEnum.SEQUENTIAL: - return 'Sequential fitting over data files in a directory' - return '' diff --git a/src/easydiffraction/analysis/categories/fit_parameter_correlations/__init__.py b/src/easydiffraction/analysis/categories/fit_parameter_correlations/__init__.py new file mode 100644 index 000000000..bbb74736d --- /dev/null +++ b/src/easydiffraction/analysis/categories/fit_parameter_correlations/__init__.py @@ -0,0 +1,12 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + +from easydiffraction.analysis.categories.fit_parameter_correlations.default import ( + FitParameterCorrelationItem, +) +from easydiffraction.analysis.categories.fit_parameter_correlations.default import ( + FitParameterCorrelations, +) +from easydiffraction.analysis.categories.fit_parameter_correlations.factory import ( + FitParameterCorrelationsFactory, +) diff --git a/src/easydiffraction/analysis/categories/fit_parameter_correlations/default.py b/src/easydiffraction/analysis/categories/fit_parameter_correlations/default.py new file mode 100644 index 000000000..4b08057e3 --- /dev/null +++ b/src/easydiffraction/analysis/categories/fit_parameter_correlations/default.py @@ -0,0 +1,184 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Persisted fit-parameter correlation summaries.""" + +from __future__ import annotations + +from easydiffraction.analysis.categories.fit_parameter_correlations.factory import ( + FitParameterCorrelationsFactory, +) +from easydiffraction.analysis.enums import FitCorrelationSourceEnum +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 NumericDescriptor +from easydiffraction.core.variable import StringDescriptor +from easydiffraction.io.cif.handler import CifHandler + + +def _normalized_parameter_pair( + param_unique_name_i: str, + param_unique_name_j: str, +) -> tuple[str, str]: + """Return a stable ordering for a parameter pair.""" + if param_unique_name_i <= param_unique_name_j: + return param_unique_name_i, param_unique_name_j + return param_unique_name_j, param_unique_name_i + + +class FitParameterCorrelationItem(CategoryItem): + """Single persisted fit-parameter correlation row.""" + + _category_code = 'fit_parameter_correlation' + _category_entry_name = 'id' + + def __init__(self) -> None: + super().__init__() + self._id = StringDescriptor( + name='id', + description='Stable identifier for the persisted correlation row.', + value_spec=AttributeSpec( + default='_', + validator=RegexValidator(pattern=r'^[A-Za-z0-9_.:-]+$'), + ), + cif_handler=CifHandler(names=['_fit_parameter_correlation.id']), + ) + self._source_kind = StringDescriptor( + name='source_kind', + description='Origin of the persisted correlation summary.', + value_spec=AttributeSpec( + default=FitCorrelationSourceEnum.default().value, + validator=MembershipValidator( + allowed=[member.value for member in FitCorrelationSourceEnum] + ), + ), + cif_handler=CifHandler(names=['_fit_parameter_correlation.source_kind']), + ) + self._param_unique_name_i = StringDescriptor( + name='param_unique_name_i', + description='First unique parameter name in the persisted pair.', + value_spec=AttributeSpec( + default='_', + validator=RegexValidator(pattern=r'^[A-Za-z_][A-Za-z0-9_.]*$'), + ), + cif_handler=CifHandler(names=['_fit_parameter_correlation.param_unique_name_i']), + ) + self._param_unique_name_j = StringDescriptor( + name='param_unique_name_j', + description='Second unique parameter name in the persisted pair.', + value_spec=AttributeSpec( + default='_', + validator=RegexValidator(pattern=r'^[A-Za-z_][A-Za-z0-9_.]*$'), + ), + cif_handler=CifHandler(names=['_fit_parameter_correlation.param_unique_name_j']), + ) + self._correlation = NumericDescriptor( + name='correlation', + description='Persisted correlation coefficient for the parameter pair.', + value_spec=AttributeSpec( + default=0.0, + validator=RangeValidator(ge=-1.0, le=1.0), + ), + cif_handler=CifHandler(names=['_fit_parameter_correlation.correlation']), + ) + + @property + def id(self) -> StringDescriptor: + """Stable identifier for the persisted correlation row.""" + return self._id + + def _set_id(self, value: str) -> None: + """Set the persisted correlation-row id for internal callers.""" + self._id.value = value + + @property + def source_kind(self) -> StringDescriptor: + """Origin of the persisted correlation summary.""" + return self._source_kind + + def _set_source_kind(self, value: str) -> None: + """Set the correlation source kind for internal callers.""" + self._source_kind.value = value + + @property + def param_unique_name_i(self) -> StringDescriptor: + """First unique parameter name in the persisted pair.""" + return self._param_unique_name_i + + def _set_param_unique_name_i(self, value: str) -> None: + """Set the first parameter name for internal callers.""" + self._param_unique_name_i.value = value + + @property + def param_unique_name_j(self) -> StringDescriptor: + """Second unique parameter name in the persisted pair.""" + return self._param_unique_name_j + + def _set_param_unique_name_j(self, value: str) -> None: + """Set the second parameter name for internal callers.""" + self._param_unique_name_j.value = value + + @property + def correlation(self) -> NumericDescriptor: + """Persisted correlation coefficient for the parameter pair.""" + return self._correlation + + def _set_correlation(self, value: float) -> None: + """Set the correlation coefficient for internal callers.""" + self._correlation.value = value + + +@FitParameterCorrelationsFactory.register +class FitParameterCorrelations(CategoryCollection): + """Collection of persisted fit-parameter correlation summaries.""" + + type_info = TypeInfo( + tag='default', + description='Persisted fit-parameter correlation summaries', + ) + + def __init__(self) -> None: + super().__init__(item_type=FitParameterCorrelationItem) + + def create( + self, + *, + source_kind: str, + param_unique_name_i: str, + param_unique_name_j: str, + correlation: float, + id: str | None = None, + ) -> None: + """ + Create a persisted fit-parameter correlation row. + + Parameters + ---------- + source_kind : str + Origin of the persisted correlation summary. + param_unique_name_i : str + First unique parameter name in the pair. + param_unique_name_j : str + Second unique parameter name in the pair. + correlation : float + Correlation coefficient for the parameter pair. + id : str | None, default=None + Explicit persisted row identifier. When omitted, a simple + sequential identifier is generated. + """ + normalized_i, normalized_j = _normalized_parameter_pair( + param_unique_name_i, + param_unique_name_j, + ) + item = FitParameterCorrelationItem() + item._set_source_kind(source_kind) + item._set_param_unique_name_i(normalized_i) + item._set_param_unique_name_j(normalized_j) + item._set_correlation(correlation) + resolved_id = id or str(len(self) + 1) + item._set_id(resolved_id) + self.add(item) diff --git a/src/easydiffraction/analysis/categories/fit_parameter_correlations/factory.py b/src/easydiffraction/analysis/categories/fit_parameter_correlations/factory.py new file mode 100644 index 000000000..095411086 --- /dev/null +++ b/src/easydiffraction/analysis/categories/fit_parameter_correlations/factory.py @@ -0,0 +1,17 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Fit-parameter-correlation factory.""" + +from __future__ import annotations + +from typing import ClassVar + +from easydiffraction.core.factory import FactoryBase + + +class FitParameterCorrelationsFactory(FactoryBase): + """Create fit-parameter correlation collections by tag.""" + + _default_rules: ClassVar[dict] = { + frozenset(): 'default', + } diff --git a/src/easydiffraction/analysis/categories/fit_parameters/__init__.py b/src/easydiffraction/analysis/categories/fit_parameters/__init__.py new file mode 100644 index 000000000..64e72b416 --- /dev/null +++ b/src/easydiffraction/analysis/categories/fit_parameters/__init__.py @@ -0,0 +1,6 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + +from easydiffraction.analysis.categories.fit_parameters.default import FitParameterItem +from easydiffraction.analysis.categories.fit_parameters.default import FitParameters +from easydiffraction.analysis.categories.fit_parameters.factory import FitParametersFactory diff --git a/src/easydiffraction/analysis/categories/fit_parameters/default.py b/src/easydiffraction/analysis/categories/fit_parameters/default.py new file mode 100644 index 000000000..a5e1f94f5 --- /dev/null +++ b/src/easydiffraction/analysis/categories/fit_parameters/default.py @@ -0,0 +1,177 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Fit-parameter control snapshots.""" + +from __future__ import annotations + +import numpy as np + +from easydiffraction.analysis.categories.fit_parameters.factory import FitParametersFactory +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 NumericDescriptor +from easydiffraction.core.variable import StringDescriptor +from easydiffraction.io.cif.handler import CifHandler + + +class FitParameterItem(CategoryItem): + """Single persisted fit-parameter control row.""" + + _category_code = 'fit_parameter' + _category_entry_name = 'param_unique_name' + + def __init__(self) -> None: + super().__init__() + self._param_unique_name = StringDescriptor( + name='param_unique_name', + description='Unique name of the referenced live parameter.', + value_spec=AttributeSpec( + default='_', + validator=RegexValidator(pattern=r'^[A-Za-z_][A-Za-z0-9_.]*$'), + ), + cif_handler=CifHandler(names=['_fit_parameter.param_unique_name']), + ) + self._fit_min = NumericDescriptor( + name='fit_min', + description='Persisted lower fit bound.', + value_spec=AttributeSpec(default=-np.inf), + cif_handler=CifHandler(names=['_fit_parameter.fit_min']), + ) + self._fit_max = NumericDescriptor( + name='fit_max', + description='Persisted upper fit bound.', + value_spec=AttributeSpec(default=np.inf), + cif_handler=CifHandler(names=['_fit_parameter.fit_max']), + ) + self._fit_bounds_uncertainty_multiplier = NumericDescriptor( + name='fit_bounds_uncertainty_multiplier', + description='Multiplier used to derive fit bounds from uncertainty.', + value_spec=AttributeSpec(default=None, allow_none=True), + cif_handler=CifHandler(names=['_fit_parameter.fit_bounds_uncertainty_multiplier']), + ) + self._start_value = NumericDescriptor( + name='start_value', + description='Persisted pre-fit value snapshot.', + value_spec=AttributeSpec(default=None, allow_none=True), + cif_handler=CifHandler(names=['_fit_parameter.start_value']), + ) + self._start_uncertainty = NumericDescriptor( + name='start_uncertainty', + description='Persisted pre-fit uncertainty snapshot.', + value_spec=AttributeSpec(default=None, allow_none=True), + cif_handler=CifHandler(names=['_fit_parameter.start_uncertainty']), + ) + + @property + def param_unique_name(self) -> StringDescriptor: + """Unique name of the referenced live parameter.""" + return self._param_unique_name + + def _set_param_unique_name(self, value: str) -> None: + """ + Set the referenced parameter unique name for internal callers. + """ + self._param_unique_name.value = value + + @property + def fit_min(self) -> NumericDescriptor: + """Persisted lower fit bound.""" + return self._fit_min + + def _set_fit_min(self, value: float) -> None: + """Set the persisted lower fit bound for internal callers.""" + self._fit_min.value = value + + @property + def fit_max(self) -> NumericDescriptor: + """Persisted upper fit bound.""" + return self._fit_max + + def _set_fit_max(self, value: float) -> None: + """Set the persisted upper fit bound for internal callers.""" + self._fit_max.value = value + + @property + def fit_bounds_uncertainty_multiplier(self) -> NumericDescriptor: + """Multiplier used to derive fit bounds from uncertainty.""" + return self._fit_bounds_uncertainty_multiplier + + def _set_fit_bounds_uncertainty_multiplier( + self, + value: float | None, + ) -> None: + """ + Set the fit-bounds uncertainty multiplier for internal callers. + """ + self._fit_bounds_uncertainty_multiplier.value = value + + @property + def start_value(self) -> NumericDescriptor: + """Persisted pre-fit value snapshot.""" + return self._start_value + + def _set_start_value(self, value: float | None) -> None: + """Set the pre-fit value snapshot for internal callers.""" + self._start_value.value = value + + @property + def start_uncertainty(self) -> NumericDescriptor: + """Persisted pre-fit uncertainty snapshot.""" + return self._start_uncertainty + + def _set_start_uncertainty(self, value: float | None) -> None: + """Set the pre-fit uncertainty snapshot for internal callers.""" + self._start_uncertainty.value = value + + +@FitParametersFactory.register +class FitParameters(CategoryCollection): + """Collection of persisted fit-parameter control snapshots.""" + + type_info = TypeInfo( + tag='default', + description='Persisted fit-parameter control snapshots', + ) + + def __init__(self) -> None: + super().__init__(item_type=FitParameterItem) + + def create( + self, + *, + param_unique_name: str, + fit_min: float, + fit_max: float, + fit_bounds_uncertainty_multiplier: float | None = None, + start_value: float | None = None, + start_uncertainty: float | None = None, + ) -> None: + """ + Create a persisted fit-parameter control snapshot row. + + Parameters + ---------- + param_unique_name : str + Unique name of the referenced live parameter. + fit_min : float + Persisted lower fit bound. + fit_max : float + Persisted upper fit bound. + fit_bounds_uncertainty_multiplier : float | None, default=None + Multiplier used to derive fit bounds from uncertainty. + start_value : float | None, default=None + Persisted pre-fit value snapshot. + start_uncertainty : float | None, default=None + Persisted pre-fit uncertainty snapshot. + """ + item = FitParameterItem() + item._set_param_unique_name(param_unique_name) + item._set_fit_min(fit_min) + item._set_fit_max(fit_max) + item._set_fit_bounds_uncertainty_multiplier(fit_bounds_uncertainty_multiplier) + item._set_start_value(start_value) + item._set_start_uncertainty(start_uncertainty) + self.add(item) diff --git a/src/easydiffraction/analysis/categories/fit_parameters/factory.py b/src/easydiffraction/analysis/categories/fit_parameters/factory.py new file mode 100644 index 000000000..4b73be48a --- /dev/null +++ b/src/easydiffraction/analysis/categories/fit_parameters/factory.py @@ -0,0 +1,17 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Fit-parameter factory.""" + +from __future__ import annotations + +from typing import ClassVar + +from easydiffraction.core.factory import FactoryBase + + +class FitParametersFactory(FactoryBase): + """Create fit-parameter collections by tag.""" + + _default_rules: ClassVar[dict] = { + frozenset(): 'default', + } diff --git a/src/easydiffraction/analysis/categories/fit_result/__init__.py b/src/easydiffraction/analysis/categories/fit_result/__init__.py new file mode 100644 index 000000000..8578ab47b --- /dev/null +++ b/src/easydiffraction/analysis/categories/fit_result/__init__.py @@ -0,0 +1,5 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + +from easydiffraction.analysis.categories.fit_result.default import FitResult +from easydiffraction.analysis.categories.fit_result.factory import FitResultFactory diff --git a/src/easydiffraction/analysis/categories/fit_result/default.py b/src/easydiffraction/analysis/categories/fit_result/default.py new file mode 100644 index 000000000..564df008e --- /dev/null +++ b/src/easydiffraction/analysis/categories/fit_result/default.py @@ -0,0 +1,135 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Common fit-result status category.""" + +from __future__ import annotations + +from easydiffraction.analysis.categories.fit_result.factory import FitResultFactory +from easydiffraction.analysis.enums import FitResultKindEnum +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 BoolDescriptor +from easydiffraction.core.variable import IntegerDescriptor +from easydiffraction.core.variable import NumericDescriptor +from easydiffraction.core.variable import StringDescriptor +from easydiffraction.io.cif.handler import CifHandler + + +@FitResultFactory.register +class FitResult(CategoryItem): + """Common persisted fit-result status metadata.""" + + _category_code = 'fit_result' + + type_info = TypeInfo( + tag='default', + description='Common persisted fit-result status metadata', + ) + + def __init__(self) -> None: + super().__init__() + self._result_kind = StringDescriptor( + name='result_kind', + description='Kind of the latest persisted fit-result projection.', + value_spec=AttributeSpec( + default=FitResultKindEnum.default().value, + validator=MembershipValidator( + allowed=[member.value for member in FitResultKindEnum] + ), + ), + cif_handler=CifHandler(names=['_fit_result.result_kind']), + ) + self._success = BoolDescriptor( + name='success', + description='Whether the latest persisted fit-result projection succeeded.', + value_spec=AttributeSpec(default=False), + cif_handler=CifHandler(names=['_fit_result.success']), + ) + self._message = StringDescriptor( + name='message', + description='Status message for the latest persisted fit-result projection.', + value_spec=AttributeSpec(default=''), + cif_handler=CifHandler(names=['_fit_result.message']), + ) + self._iterations = IntegerDescriptor( + name='iterations', + description='Iteration count for the latest persisted fit-result projection.', + value_spec=AttributeSpec(default=0), + cif_handler=CifHandler(names=['_fit_result.iterations']), + ) + self._fitting_time = NumericDescriptor( + name='fitting_time', + description='Fitting time in seconds for the latest persisted projection.', + value_spec=AttributeSpec(default=None, allow_none=True), + cif_handler=CifHandler(names=['_fit_result.fitting_time']), + ) + self._reduced_chi_square = NumericDescriptor( + name='reduced_chi_square', + description='Reduced chi-square for the latest persisted projection.', + value_spec=AttributeSpec(default=None, allow_none=True), + cif_handler=CifHandler(names=['_fit_result.reduced_chi_square']), + ) + + @property + def result_kind(self) -> StringDescriptor: + """Kind of the latest persisted fit-result projection.""" + return self._result_kind + + def _set_result_kind(self, value: str) -> None: + """Set the result kind for internal callers.""" + self._result_kind.value = value + + @property + def success(self) -> BoolDescriptor: + """ + Whether the latest persisted fit-result projection succeeded. + """ + return self._success + + def _set_success(self, *, value: bool) -> None: + """Set the success flag for internal callers.""" + self._success.value = value + + @property + def message(self) -> StringDescriptor: + """ + Status message for the latest persisted fit-result projection. + """ + return self._message + + def _set_message(self, value: str) -> None: + """Set the fit-result message for internal callers.""" + self._message.value = value + + @property + def iterations(self) -> IntegerDescriptor: + """ + Iteration count for the latest persisted fit-result projection. + """ + return self._iterations + + def _set_iterations(self, value: int) -> None: + """Set the iteration count for internal callers.""" + self._iterations.value = value + + @property + def fitting_time(self) -> NumericDescriptor: + """ + Fitting time in seconds for the latest persisted projection. + """ + return self._fitting_time + + def _set_fitting_time(self, value: float | None) -> None: + """Set the fitting time for internal callers.""" + self._fitting_time.value = value + + @property + def reduced_chi_square(self) -> NumericDescriptor: + """Reduced chi-square for the latest persisted projection.""" + return self._reduced_chi_square + + def _set_reduced_chi_square(self, value: float | None) -> None: + """Set the reduced chi-square for internal callers.""" + self._reduced_chi_square.value = value diff --git a/src/easydiffraction/analysis/categories/fit_result/factory.py b/src/easydiffraction/analysis/categories/fit_result/factory.py new file mode 100644 index 000000000..66cbd32bc --- /dev/null +++ b/src/easydiffraction/analysis/categories/fit_result/factory.py @@ -0,0 +1,17 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Fit-result factory.""" + +from __future__ import annotations + +from typing import ClassVar + +from easydiffraction.core.factory import FactoryBase + + +class FitResultFactory(FactoryBase): + """Create fit-result categories by tag.""" + + _default_rules: ClassVar[dict] = { + frozenset(): 'default', + } diff --git a/src/easydiffraction/analysis/categories/fitting/__init__.py b/src/easydiffraction/analysis/categories/fitting/__init__.py new file mode 100644 index 000000000..07fba76f3 --- /dev/null +++ b/src/easydiffraction/analysis/categories/fitting/__init__.py @@ -0,0 +1,5 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + +from easydiffraction.analysis.categories.fitting.default import Fitting +from easydiffraction.analysis.categories.fitting.factory import FittingFactory diff --git a/src/easydiffraction/analysis/categories/fit/default.py b/src/easydiffraction/analysis/categories/fitting/default.py similarity index 52% rename from src/easydiffraction/analysis/categories/fit/default.py rename to src/easydiffraction/analysis/categories/fitting/default.py index c002fe399..141e22365 100644 --- a/src/easydiffraction/analysis/categories/fit/default.py +++ b/src/easydiffraction/analysis/categories/fitting/default.py @@ -1,16 +1,14 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause """ -Fit category item. +Fitting category item. -Stores the active minimizer and fitting mode as CIF-serializable -descriptors and provides the public entry-point for running fits. +Stores the active minimizer as a CIF-serializable descriptor. """ from __future__ import annotations -from easydiffraction.analysis.categories.fit.enums import FitModeEnum -from easydiffraction.analysis.categories.fit.factory import FitFactory +from easydiffraction.analysis.categories.fitting.factory import FittingFactory from easydiffraction.analysis.fitting import Fitter from easydiffraction.analysis.minimizers.enums import MinimizerTypeEnum from easydiffraction.analysis.minimizers.factory import MinimizerFactory @@ -25,17 +23,19 @@ from easydiffraction.utils.utils import render_table -@FitFactory.register -class Fit(CategoryItem): +@FittingFactory.register +class Fitting(CategoryItem): """ - Analysis fitting configuration and execution entry-point. + Analysis fitting configuration category. - Holds the active minimizer backend tag and fit mode value. + Holds the active minimizer backend tag. """ + _category_code = 'fitting' + type_info = TypeInfo( tag='default', - description='Fit configuration category', + description='Fitting configuration category', ) def __init__(self) -> None: @@ -50,20 +50,9 @@ def __init__(self) -> None: allowed=[member.value for member in MinimizerTypeEnum] ), ), - cif_handler=CifHandler(names=['_fit.minimizer_type']), - ) - self._mode: StringDescriptor = StringDescriptor( - name='mode', - description='Fitting mode', - value_spec=AttributeSpec( - default=FitModeEnum.default().value, - validator=MembershipValidator(allowed=[member.value for member in FitModeEnum]), - ), - cif_handler=CifHandler(names=['_fit.mode']), + cif_handler=CifHandler(names=['_fitting.minimizer_type']), ) - self._identity.category_code = 'fit' - @property def minimizer_type(self) -> StringDescriptor: """Fitting minimizer backend type.""" @@ -88,15 +77,6 @@ def minimizer(self) -> object | None: return None return parent.fitter.minimizer - @property - def mode(self) -> StringDescriptor: - """Fitting mode.""" - return self._mode - - @mode.setter - def mode(self, value: str) -> None: - self._mode.value = value - def show_minimizer_types(self) -> None: """Print supported minimizers and mark the current selection.""" current = self.minimizer_type.value @@ -119,69 +99,14 @@ def show_available_minimizers() -> None: """Print available minimizer drivers on this system.""" MinimizerFactory.show_supported() - def show_modes(self) -> None: - """Print supported fit modes and mark the current selection.""" - parent = getattr(self, '_parent', None) - if parent is None or not getattr(parent, 'project', None): - modes = [FitModeEnum.SINGLE, FitModeEnum.JOINT, FitModeEnum.SEQUENTIAL] - else: - num_expts = len(parent.project.experiments) if parent.project.experiments else 0 - if num_expts <= 1: - modes = [FitModeEnum.SINGLE] - else: - modes = [FitModeEnum.SINGLE, FitModeEnum.JOINT, FitModeEnum.SEQUENTIAL] - - current = self.mode.value - columns_data = [ - ['*' if mode.value == current else '', mode.value, mode.description()] - for mode in modes - ] - console.paragraph('Fit modes') - render_table( - columns_headers=['', 'Type', 'Description'], - columns_alignment=['left', 'left', 'left'], - columns_data=columns_data, - ) - - def run( - self, - verbosity: str | None = None, - *, - use_physical_limits: bool = False, - ) -> None: - """ - Execute fitting for the owning analysis. - - Parameters - ---------- - verbosity : str | None, default=None - Console output verbosity override. - use_physical_limits : bool, default=False - Whether to fall back to physical limits as fit bounds. - - Raises - ------ - RuntimeError - If this category is not attached to an Analysis object. - """ - parent = getattr(self, '_parent', None) - if parent is None: - msg = 'Fit category is not attached to an Analysis object.' - raise RuntimeError(msg) - parent._run_fit(verbosity=verbosity, use_physical_limits=use_physical_limits) - - def __call__( - self, - verbosity: str | None = None, - *, - use_physical_limits: bool = False, - ) -> None: - """Execute :meth:`run` for convenience.""" - self.run(verbosity=verbosity, use_physical_limits=use_physical_limits) + @property + def as_cif(self) -> str: + """Return CIF representation of this fitting category.""" + return super().as_cif def from_cif(self, block: object, idx: int = 0) -> None: """ - Populate this fit configuration from a CIF block. + Populate this fitting configuration from a CIF block. Parameters ---------- diff --git a/src/easydiffraction/analysis/categories/fitting/factory.py b/src/easydiffraction/analysis/categories/fitting/factory.py new file mode 100644 index 000000000..b88bf9178 --- /dev/null +++ b/src/easydiffraction/analysis/categories/fitting/factory.py @@ -0,0 +1,17 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Fitting factory - delegates entirely to ``FactoryBase``.""" + +from __future__ import annotations + +from typing import ClassVar + +from easydiffraction.core.factory import FactoryBase + + +class FittingFactory(FactoryBase): + """Create fitting category items by tag.""" + + _default_rules: ClassVar[dict] = { + frozenset(): 'default', + } diff --git a/src/easydiffraction/analysis/categories/joint_fit/__init__.py b/src/easydiffraction/analysis/categories/joint_fit/__init__.py new file mode 100644 index 000000000..9c7c49f98 --- /dev/null +++ b/src/easydiffraction/analysis/categories/joint_fit/__init__.py @@ -0,0 +1,8 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + +from __future__ import annotations + +from easydiffraction.analysis.categories.joint_fit.default import JointFitCollection +from easydiffraction.analysis.categories.joint_fit.default import JointFitItem +from easydiffraction.analysis.categories.joint_fit.factory import JointFitFactory diff --git a/src/easydiffraction/analysis/categories/joint_fit_experiments/default.py b/src/easydiffraction/analysis/categories/joint_fit/default.py similarity index 64% rename from src/easydiffraction/analysis/categories/joint_fit_experiments/default.py rename to src/easydiffraction/analysis/categories/joint_fit/default.py index b94bd7de8..2821f2e8f 100644 --- a/src/easydiffraction/analysis/categories/joint_fit_experiments/default.py +++ b/src/easydiffraction/analysis/categories/joint_fit/default.py @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause """ -Joint-fit experiment weighting configuration. +Joint-fit weighting configuration. Stores per-experiment weights to be used when multiple experiments are fitted simultaneously. @@ -9,9 +9,7 @@ from __future__ import annotations -from easydiffraction.analysis.categories.joint_fit_experiments.factory import ( - JointFitExperimentsFactory, -) +from easydiffraction.analysis.categories.joint_fit.factory import JointFitFactory from easydiffraction.core.category import CategoryCollection from easydiffraction.core.category import CategoryItem from easydiffraction.core.metadata import TypeInfo @@ -23,20 +21,23 @@ from easydiffraction.io.cif.handler import CifHandler -class JointFitExperiment(CategoryItem): +class JointFitItem(CategoryItem): """A single joint-fit entry.""" + _category_code = 'joint_fit' + _category_entry_name = 'experiment_id' + def __init__(self) -> None: super().__init__() - self._id: StringDescriptor = StringDescriptor( - name='id', # TODO: need new name instead of id + self._experiment_id: StringDescriptor = StringDescriptor( + name='experiment_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']), + cif_handler=CifHandler(names=['_joint_fit.experiment_id']), ) self._weight: NumericDescriptor = NumericDescriptor( name='weight', @@ -45,18 +46,11 @@ def __init__(self) -> None: default=0.0, validator=RangeValidator(), ), - cif_handler=CifHandler(names=['_joint_fit_experiment.weight']), + cif_handler=CifHandler(names=['_joint_fit.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: + def experiment_id(self) -> StringDescriptor: """ Experiment identifier. @@ -64,11 +58,11 @@ def id(self) -> StringDescriptor: ``StringDescriptor`` object. Assigning to it updates the parameter value. """ - return self._id + return self._experiment_id - @id.setter - def id(self, value: str) -> None: - self._id.value = value + @experiment_id.setter + def experiment_id(self, value: str) -> None: + self._experiment_id.value = value @property def weight(self) -> NumericDescriptor: @@ -86,9 +80,9 @@ def weight(self, value: float) -> None: self._weight.value = value -@JointFitExperimentsFactory.register -class JointFitExperiments(CategoryCollection): - """Collection of :class:`JointFitExperiment` items.""" +@JointFitFactory.register +class JointFitCollection(CategoryCollection): + """Collection of :class:`JointFitItem` items.""" type_info = TypeInfo( tag='default', @@ -96,5 +90,5 @@ class JointFitExperiments(CategoryCollection): ) def __init__(self) -> None: - """Create an empty joint-fit experiments collection.""" - super().__init__(item_type=JointFitExperiment) + """Create an empty joint-fit collection.""" + super().__init__(item_type=JointFitItem) diff --git a/src/easydiffraction/analysis/categories/joint_fit/factory.py b/src/easydiffraction/analysis/categories/joint_fit/factory.py new file mode 100644 index 000000000..fa15edc00 --- /dev/null +++ b/src/easydiffraction/analysis/categories/joint_fit/factory.py @@ -0,0 +1,17 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Joint-fit factory - delegates to ``FactoryBase``.""" + +from __future__ import annotations + +from typing import ClassVar + +from easydiffraction.core.factory import FactoryBase + + +class JointFitFactory(FactoryBase): + """Create joint-fit collections by tag.""" + + _default_rules: ClassVar[dict] = { + frozenset(): 'default', + } diff --git a/src/easydiffraction/analysis/categories/joint_fit_experiments/__init__.py b/src/easydiffraction/analysis/categories/joint_fit_experiments/__init__.py deleted file mode 100644 index 1aa8f0ae3..000000000 --- a/src/easydiffraction/analysis/categories/joint_fit_experiments/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -# 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/sequential_fit/__init__.py b/src/easydiffraction/analysis/categories/sequential_fit/__init__.py new file mode 100644 index 000000000..5381b7e1e --- /dev/null +++ b/src/easydiffraction/analysis/categories/sequential_fit/__init__.py @@ -0,0 +1,7 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + +from __future__ import annotations + +from easydiffraction.analysis.categories.sequential_fit.default import SequentialFit +from easydiffraction.analysis.categories.sequential_fit.factory import SequentialFitFactory diff --git a/src/easydiffraction/analysis/categories/sequential_fit/default.py b/src/easydiffraction/analysis/categories/sequential_fit/default.py new file mode 100644 index 000000000..52519f56d --- /dev/null +++ b/src/easydiffraction/analysis/categories/sequential_fit/default.py @@ -0,0 +1,124 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +""" +Sequential-fit configuration category. + +Stores persisted settings for directory-based sequential fitting. +""" + +from __future__ import annotations + +from easydiffraction.analysis.categories.sequential_fit.factory import SequentialFitFactory +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 BoolDescriptor +from easydiffraction.core.variable import StringDescriptor +from easydiffraction.io.cif.handler import CifHandler + + +@SequentialFitFactory.register +class SequentialFit(CategoryItem): + """Persisted settings for sequential fitting.""" + + _category_code = 'sequential_fit' + + type_info = TypeInfo( + tag='default', + description='Sequential fitting settings', + ) + + def __init__(self) -> None: + super().__init__() + + self._data_dir = StringDescriptor( + name='data_dir', + description='Directory containing sequential-fit data files.', + value_spec=AttributeSpec(default=''), + cif_handler=CifHandler(names=['_sequential_fit.data_dir']), + ) + self._file_pattern = StringDescriptor( + name='file_pattern', + description='Glob pattern selecting sequential-fit files.', + value_spec=AttributeSpec(default='*'), + cif_handler=CifHandler(names=['_sequential_fit.file_pattern']), + ) + self._max_workers = StringDescriptor( + name='max_workers', + description='Worker-count token for sequential fitting.', + value_spec=AttributeSpec( + default='1', + validator=RegexValidator(pattern=r'^(auto|[1-9]\d*)$'), + ), + cif_handler=CifHandler(names=['_sequential_fit.max_workers']), + ) + self._chunk_size = StringDescriptor( + name='chunk_size', + description='Chunk-size token for sequential fitting.', + value_spec=AttributeSpec( + default='.', + validator=RegexValidator(pattern=r'^([1-9]\d*|\.)$'), + ), + cif_handler=CifHandler(names=['_sequential_fit.chunk_size']), + ) + self._reverse = BoolDescriptor( + name='reverse', + description='Whether to process sequential-fit files in reverse.', + value_spec=AttributeSpec(default=False), + cif_handler=CifHandler(names=['_sequential_fit.reverse']), + ) + + @property + def data_dir(self) -> StringDescriptor: + """Directory containing sequential-fit data files.""" + return self._data_dir + + @data_dir.setter + def data_dir(self, value: str) -> None: + self._data_dir.value = value + + @property + def file_pattern(self) -> StringDescriptor: + """Glob pattern selecting sequential-fit files.""" + return self._file_pattern + + @file_pattern.setter + def file_pattern(self, value: str) -> None: + self._file_pattern.value = value + + @property + def max_workers(self) -> StringDescriptor: + """Worker-count token for sequential fitting.""" + return self._max_workers + + @max_workers.setter + def max_workers(self, value: str) -> None: + self._max_workers.value = value + + @property + def chunk_size(self) -> StringDescriptor: + """Chunk-size token for sequential fitting.""" + return self._chunk_size + + @chunk_size.setter + def chunk_size(self, value: str) -> None: + self._chunk_size.value = value + + @property + def reverse(self) -> BoolDescriptor: + """Whether to process sequential-fit files in reverse.""" + return self._reverse + + @reverse.setter + def reverse(self, value: bool) -> None: + self._reverse.value = value + + @property + def as_cif(self) -> str: + """Return CIF representation of this sequential-fit category.""" + return super().as_cif + + def from_cif(self, block: object, idx: int = 0) -> None: + """Populate this sequential-fit category from a CIF block.""" + super().from_cif(block, idx) diff --git a/src/easydiffraction/analysis/categories/sequential_fit/factory.py b/src/easydiffraction/analysis/categories/sequential_fit/factory.py new file mode 100644 index 000000000..43c96f1ee --- /dev/null +++ b/src/easydiffraction/analysis/categories/sequential_fit/factory.py @@ -0,0 +1,17 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Sequential-fit factory - delegates to ``FactoryBase``.""" + +from __future__ import annotations + +from typing import ClassVar + +from easydiffraction.core.factory import FactoryBase + + +class SequentialFitFactory(FactoryBase): + """Create sequential-fit category items by tag.""" + + _default_rules: ClassVar[dict] = { + frozenset(): 'default', + } diff --git a/src/easydiffraction/analysis/categories/sequential_fit_extract/__init__.py b/src/easydiffraction/analysis/categories/sequential_fit_extract/__init__.py new file mode 100644 index 000000000..ff6ba0563 --- /dev/null +++ b/src/easydiffraction/analysis/categories/sequential_fit_extract/__init__.py @@ -0,0 +1,14 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + +from __future__ import annotations + +from easydiffraction.analysis.categories.sequential_fit_extract.default import ( + SequentialFitExtractCollection, +) +from easydiffraction.analysis.categories.sequential_fit_extract.default import ( + SequentialFitExtractItem, +) +from easydiffraction.analysis.categories.sequential_fit_extract.factory import ( + SequentialFitExtractFactory, +) diff --git a/src/easydiffraction/analysis/categories/sequential_fit_extract/default.py b/src/easydiffraction/analysis/categories/sequential_fit_extract/default.py new file mode 100644 index 000000000..a724692c3 --- /dev/null +++ b/src/easydiffraction/analysis/categories/sequential_fit_extract/default.py @@ -0,0 +1,162 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +""" +Sequential-fit extract-rule configuration. + +Stores persisted rules for extracting diffrn metadata from sequential +fit input files. +""" + +from __future__ import annotations + +import re + +from easydiffraction.analysis.categories.sequential_fit_extract.factory import ( + SequentialFitExtractFactory, +) +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 BoolDescriptor +from easydiffraction.core.variable import StringDescriptor +from easydiffraction.io.cif.handler import CifHandler + +_TARGET_SEGMENT_PATTERN = re.compile(r'^[A-Za-z_][A-Za-z0-9_]*$') +_EXTRACT_TARGET_SEGMENTS = 2 + + +def _validate_extract_target_shape(value: str) -> None: + """Validate the supported two-segment extract target form.""" + parts = value.split('.') + if ( + len(parts) != _EXTRACT_TARGET_SEGMENTS + or parts[0] != 'diffrn' + or not _TARGET_SEGMENT_PATTERN.fullmatch(parts[1]) + ): + msg = ( + 'sequential_fit_extract.target must use the form ' + "'diffrn.' with exactly two segments." + ) + raise ValueError(msg) + + +def _validate_extract_pattern(value: str) -> None: + """Validate that an extract pattern compiles and captures once.""" + try: + compiled = re.compile(value) + except re.error as error: + msg = f'Invalid sequential_fit_extract.pattern {value!r}: {error}.' + raise ValueError(msg) from error + + if compiled.groups != 1: + msg = 'sequential_fit_extract.pattern must define exactly one capture group.' + raise ValueError(msg) + + +class SequentialFitExtractItem(CategoryItem): + """A single sequential-fit extract rule.""" + + _category_code = 'sequential_fit_extract' + _category_entry_name = 'id' + + def __init__(self) -> None: + super().__init__() + + self._id = StringDescriptor( + name='id', + description='Identifier for this extract rule.', + value_spec=AttributeSpec( + default='_', + validator=RegexValidator(pattern=r'^[A-Za-z_][A-Za-z0-9_]*$'), + ), + cif_handler=CifHandler(names=['_sequential_fit_extract.id']), + ) + self._target = StringDescriptor( + name='target', + description='diffrn attribute updated by this extract rule.', + value_spec=AttributeSpec(default='diffrn._'), + cif_handler=CifHandler(names=['_sequential_fit_extract.target']), + ) + self._pattern = StringDescriptor( + name='pattern', + description='Regex used to extract one numeric capture group.', + value_spec=AttributeSpec(default='(.*)'), + cif_handler=CifHandler(names=['_sequential_fit_extract.pattern']), + ) + self._required = BoolDescriptor( + name='required', + description='Whether this extract rule must match every file.', + value_spec=AttributeSpec(default=False), + cif_handler=CifHandler(names=['_sequential_fit_extract.required']), + ) + + @property + def id(self) -> StringDescriptor: + """Identifier for this extract rule.""" + return self._id + + @id.setter + def id(self, value: str) -> None: + self._id.value = value + + @property + def target(self) -> StringDescriptor: + """Diffrn attribute updated by this extract rule.""" + return self._target + + @target.setter + def target(self, value: str) -> None: + self._target.value = value + + @property + def pattern(self) -> StringDescriptor: + """Regex used to extract one numeric capture group.""" + return self._pattern + + @pattern.setter + def pattern(self, value: str) -> None: + self._pattern.value = value + + @property + def required(self) -> BoolDescriptor: + """Whether this extract rule must match every file.""" + return self._required + + @required.setter + def required(self, value: bool) -> None: + self._required.value = value + + +@SequentialFitExtractFactory.register +class SequentialFitExtractCollection(CategoryCollection): + """Collection of :class:`SequentialFitExtractItem` items.""" + + type_info = TypeInfo( + tag='default', + description='Sequential-fit metadata extraction rules', + ) + + def __init__(self) -> None: + """Create an empty collection of extract rules.""" + super().__init__(item_type=SequentialFitExtractItem) + + def create( + self, + *, + id: str, + target: str, + pattern: str, + required: bool = False, + ) -> None: + """Create a validated sequential-fit extract rule.""" + _validate_extract_target_shape(target) + _validate_extract_pattern(pattern) + + item = SequentialFitExtractItem() + item.id = id + item.target = target + item.pattern = pattern + item.required = required + self.add(item) diff --git a/src/easydiffraction/analysis/categories/sequential_fit_extract/factory.py b/src/easydiffraction/analysis/categories/sequential_fit_extract/factory.py new file mode 100644 index 000000000..6fe176d60 --- /dev/null +++ b/src/easydiffraction/analysis/categories/sequential_fit_extract/factory.py @@ -0,0 +1,17 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Sequential-fit-extract factory - delegates to ``FactoryBase``.""" + +from __future__ import annotations + +from typing import ClassVar + +from easydiffraction.core.factory import FactoryBase + + +class SequentialFitExtractFactory(FactoryBase): + """Create sequential-fit-extract collections by tag.""" + + _default_rules: ClassVar[dict] = { + frozenset(): 'default', + } diff --git a/src/easydiffraction/analysis/enums.py b/src/easydiffraction/analysis/enums.py new file mode 100644 index 000000000..94d78cc64 --- /dev/null +++ b/src/easydiffraction/analysis/enums.py @@ -0,0 +1,54 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Enumeration types used by analysis components.""" + +from __future__ import annotations + +from enum import StrEnum + + +class FitModeEnum(StrEnum): + """Fitting mode for the analysis.""" + + SINGLE = 'single' + JOINT = 'joint' + SEQUENTIAL = 'sequential' + + @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 'Fit one experiment at a time.' + if self is FitModeEnum.JOINT: + return 'Fit several experiments together with shared parameters.' + if self is FitModeEnum.SEQUENTIAL: + return 'Fit one experiment against a series of data files.' + return '' + + +class FitResultKindEnum(StrEnum): + """Persisted kind of the latest fit-result projection.""" + + DETERMINISTIC = 'deterministic' + BAYESIAN = 'bayesian' + + @classmethod + def default(cls) -> FitResultKindEnum: + """Return the default persisted fit-result kind.""" + return cls.DETERMINISTIC + + +class FitCorrelationSourceEnum(StrEnum): + """Source of a persisted fit-parameter correlation summary.""" + + DETERMINISTIC = 'deterministic' + POSTERIOR = 'posterior' + + @classmethod + def default(cls) -> FitCorrelationSourceEnum: + """Return the default persisted correlation source.""" + return cls.DETERMINISTIC diff --git a/src/easydiffraction/analysis/fit_helpers/__init__.py b/src/easydiffraction/analysis/fit_helpers/__init__.py index 4e798e209..c7c53862a 100644 --- a/src/easydiffraction/analysis/fit_helpers/__init__.py +++ b/src/easydiffraction/analysis/fit_helpers/__init__.py @@ -1,2 +1,8 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause + +from easydiffraction.analysis.fit_helpers.bayesian import BayesianFitResults +from easydiffraction.analysis.fit_helpers.bayesian import PosteriorParameterSummary +from easydiffraction.analysis.fit_helpers.bayesian import PosteriorPredictiveSummary +from easydiffraction.analysis.fit_helpers.bayesian import PosteriorSamples +from easydiffraction.analysis.fit_helpers.reporting import FitResults diff --git a/src/easydiffraction/analysis/fit_helpers/bayesian.py b/src/easydiffraction/analysis/fit_helpers/bayesian.py new file mode 100644 index 000000000..ca191c835 --- /dev/null +++ b/src/easydiffraction/analysis/fit_helpers/bayesian.py @@ -0,0 +1,777 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Bayesian fit result models and posterior data containers.""" + +from __future__ import annotations + +from dataclasses import dataclass + +import arviz as az +import numpy as np +from rich.text import Text + +from easydiffraction.analysis.fit_helpers.metrics import calculate_r_factor +from easydiffraction.analysis.fit_helpers.metrics import calculate_r_factor_squared +from easydiffraction.analysis.fit_helpers.metrics import calculate_rb_factor +from easydiffraction.analysis.fit_helpers.metrics import calculate_weighted_r_factor +from easydiffraction.analysis.fit_helpers.reporting import FitResults +from easydiffraction.analysis.fit_helpers.reporting import _build_parameter_row +from easydiffraction.analysis.fit_helpers.reporting import _format_optional_float +from easydiffraction.utils.logging import console +from easydiffraction.utils.logging import log +from easydiffraction.utils.utils import render_table + +R_HAT_CONVERGENCE_THRESHOLD = 1.01 +ESS_BULK_CONVERGENCE_THRESHOLD = 400.0 +POSTERIOR_SAMPLE_NDIM = 3 +DEFAULT_CI_LEVELS = (0.68, 0.95) +DEFAULT_CREDIBLE_INTERVAL_LEVELS = DEFAULT_CI_LEVELS +IntervalLevels = tuple[float, ...] +SettingsMap = dict[str, object] | None +DiagnosticsMap = dict[str, object] | None + + +@dataclass(slots=True) +class PosteriorParameterSummary: + r""" + Posterior summary statistics for one fitted parameter. + + Attributes + ---------- + unique_name : str + Unique parameter name used across EasyDiffraction. + display_name : str + Human-readable label used in plots and tables. + best_sample_value : float + Highest-posterior sampled parameter value. + median : float + Posterior median value. + standard_deviation : float + Posterior standard deviation. + interval_68 : tuple[float, float] + Central 68% interval. + interval_95 : tuple[float, float] + Central 95% interval. + ess_bulk : float | None, default=None + Bulk effective sample size when available. + r_hat : float | None, default=None + Rank-normalized split-$\hat{R}$ when available. + """ + + unique_name: str + display_name: str + best_sample_value: float + median: float + standard_deviation: float + interval_68: tuple[float, float] + interval_95: tuple[float, float] + ess_bulk: float | None = None + r_hat: float | None = None + + +@dataclass(slots=True) +class PosteriorPredictiveSummary: + """ + Posterior predictive summaries for one experiment. + + Attributes + ---------- + experiment_name : str + Experiment identifier. + x_axis_name : str + Name of the x-axis used for the predictive arrays. + x : np.ndarray + X-axis values for the predictive curves. + best_sample_prediction : np.ndarray + Prediction corresponding to the committed point estimate. + lower_95 : np.ndarray | None, default=None + Lower bound of the 95% credible interval. + upper_95 : np.ndarray | None, default=None + Upper bound of the 95% credible interval. + lower_68 : np.ndarray | None, default=None + Lower bound of the 68% credible interval. + upper_68 : np.ndarray | None, default=None + Upper bound of the 68% credible interval. + draws : np.ndarray | None, default=None + Optional capped predictive draws with shape ``(n_draws, n_x)``. + """ + + experiment_name: str + x_axis_name: str + x: np.ndarray + best_sample_prediction: np.ndarray + lower_95: np.ndarray | None = None + upper_95: np.ndarray | None = None + lower_68: np.ndarray | None = None + upper_68: np.ndarray | None = None + draws: np.ndarray | None = None + + +@dataclass(slots=True) +class PosteriorSamples: + """ + Posterior samples and sample statistics from a Bayesian fit. + + Attributes + ---------- + parameter_names : list[str] + Parameter names in the preserved EasyDiffraction order. + parameter_samples : np.ndarray + Sample array with shape ``(n_draws, n_chains, n_parameters)``. + log_posterior : np.ndarray | None, default=None + Log-posterior values with shape ``(n_draws, n_chains)`` when + available. + draw_index : np.ndarray | None, default=None + Optional draw or generation indices associated with the first + axis of ``parameter_samples``. + """ + + parameter_names: list[str] + parameter_samples: np.ndarray + log_posterior: np.ndarray | None = None + draw_index: np.ndarray | None = None + + def flattened(self) -> np.ndarray: + """ + Return flattened posterior samples by parameter. + + Returns + ------- + np.ndarray + Array with shape ``(n_draws * n_chains, n_parameters)``. + """ + return np.asarray(self.parameter_samples).reshape(-1, len(self.parameter_names)) + + def to_arviz(self) -> object: + """ + Convert posterior samples to an ArviZ ``InferenceData`` object. + + Returns + ------- + object + ArviZ ``InferenceData`` instance built from the stored + posterior samples. + + Raises + ------ + ValueError + If the stored arrays do not have the expected shapes. + """ + posterior_array = np.asarray(self.parameter_samples, dtype=float) + if posterior_array.ndim != POSTERIOR_SAMPLE_NDIM: + msg = 'Posterior sample array must have shape (n_draws, n_chains, n_parameters).' + raise ValueError(msg) + + n_draws, n_chains, n_parameters = posterior_array.shape + if n_parameters != len(self.parameter_names): + msg = 'Posterior sample array does not match the parameter name list length.' + raise ValueError(msg) + + posterior_dict = { + name: np.transpose(posterior_array[:, :, index], (1, 0)) + for index, name in enumerate(self.parameter_names) + } + + sample_stats: dict[str, np.ndarray] | None = None + if self.log_posterior is not None: + log_posterior = np.asarray(self.log_posterior, dtype=float) + if log_posterior.shape != (n_draws, n_chains): + msg = 'Log-posterior array must match the first two posterior sample axes.' + raise ValueError(msg) + sample_stats = {'lp': np.transpose(log_posterior, (1, 0))} + + data = {'posterior': posterior_dict} + if sample_stats is not None: + data['sample_stats'] = sample_stats + + return az.from_dict(data) + + +SummaryList = list[PosteriorParameterSummary] | None +PredictiveMap = dict[str, PosteriorPredictiveSummary] | None + + +@dataclass(kw_only=True) +class BayesianFitResults(FitResults): + """ + Container for Bayesian fit results and posterior summaries. + + Attributes + ---------- + success : bool, default=False + Whether the Bayesian fit produced usable posterior results. + parameters : list[object] | None, default=None + Final committed parameter objects. + reduced_chi_square : float | None, default=None + Reduced chi-square evaluated at the committed point estimate. + engine_result : object | None, default=None + Opaque backend result object. + starting_parameters : list[object] | None, default=None + Starting parameter objects or snapshots. + fitting_time : float | None, default=None + Total fitting time in seconds. + sampler_name : str, default='dream' + Sampler identifier. + point_estimate_name : str, default='best_sample' + Name of the point estimate committed back to the project. + posterior_samples : PosteriorSamples | None, default=None + Stored posterior samples. + posterior_parameter_summaries : SummaryList, default=None + Posterior summaries for each sampled parameter. + posterior_predictive : PredictiveMap, default=None + Posterior predictive summaries keyed by experiment name. + credible_interval_levels : IntervalLevels, default=DEFAULT_CI_LEVELS + Interval levels available in the summaries. + sampler_settings : SettingsMap, default=None + Sampler settings recorded for reproducibility. + convergence_diagnostics : DiagnosticsMap, default=None + Convergence diagnostics and status metadata. + sampler_completed : bool, default=False + Whether the sampler completed a run and returned posterior data. + best_log_posterior : float | None, default=None + Best log-posterior value reported by the sampler. + """ + + success: bool = False + parameters: list[object] | None = None + reduced_chi_square: float | None = None + engine_result: object | None = None + starting_parameters: list[object] | None = None + fitting_time: float | None = None + sampler_name: str = 'dream' + point_estimate_name: str = 'best_sample' + posterior_samples: PosteriorSamples | None = None + posterior_parameter_summaries: SummaryList = None + posterior_predictive: PredictiveMap = None + credible_interval_levels: IntervalLevels = DEFAULT_CI_LEVELS + sampler_settings: SettingsMap = None + convergence_diagnostics: DiagnosticsMap = None + sampler_completed: bool = False + best_log_posterior: float | None = None + + def __post_init__(self) -> None: + """ + Initialize inherited FitResults state and normalize containers. + """ + super().__init__( + success=self.success, + parameters=self.parameters, + reduced_chi_square=self.reduced_chi_square, + engine_result=self.engine_result, + starting_parameters=self.starting_parameters, + fitting_time=self.fitting_time, + ) + self.posterior_parameter_summaries = ( + list(self.posterior_parameter_summaries) + if self.posterior_parameter_summaries is not None + else [] + ) + self.posterior_predictive = ( + dict(self.posterior_predictive) if self.posterior_predictive is not None else {} + ) + self.sampler_settings = dict(self.sampler_settings) if self.sampler_settings else {} + self.convergence_diagnostics = ( + dict(self.convergence_diagnostics) if self.convergence_diagnostics is not None else {} + ) + + def display_results( + self, + y_obs: list[float] | None = None, + y_calc: list[float] | None = None, + y_err: list[float] | None = None, + f_obs: list[float] | None = None, + f_calc: list[float] | None = None, + ) -> None: + """ + Render a Bayesian fit summary with posterior diagnostics. + + Parameters + ---------- + y_obs : list[float] | None, default=None + Observed intensities for pattern R-factor metrics. + y_calc : list[float] | None, default=None + Calculated intensities for pattern R-factor metrics. + y_err : list[float] | None, default=None + Standard deviations of observed intensities for wR. + f_obs : list[float] | None, default=None + Observed structure-factor magnitudes for Bragg R. + f_calc : list[float] | None, default=None + Calculated structure-factor magnitudes for Bragg R. + """ + metrics = _calculate_fit_quality_metrics( + y_obs=y_obs, + y_calc=y_calc, + y_err=y_err, + f_obs=f_obs, + f_calc=f_calc, + ) + + self._display_summary_header() + _print_fit_quality_metrics(metrics) + + console.print('📈 Committed parameters:') + _render_committed_parameter_table(self.parameters) + + console.print('📊 Posterior parameter summaries:') + _render_posterior_summary_table( + parameters=self.parameters, + posterior_parameter_summaries=self.posterior_parameter_summaries, + ) + + self._print_table_notes() + + def _print_table_notes(self) -> None: + """ + Print parameter and posterior-diagnostic notes below tables. + """ + super()._print_table_notes() + for note in _posterior_table_notes(self.posterior_parameter_summaries): + log.warning(note) + + def _display_summary_header(self) -> None: + """Render the high-level Bayesian fit summary.""" + status_icon, overall_status = _format_bayesian_overall_status( + success=self.success, + sampler_completed=self.sampler_completed, + convergence_diagnostics=self.convergence_diagnostics, + ) + fitting_time = _format_optional_float(self.fitting_time, suffix=' seconds') + goodness_of_fit = _format_optional_float(self.reduced_chi_square) + + console.paragraph('Bayesian fit results') + console.print(f'{status_icon} Overall status: {overall_status}') + if self.message: + console.print(f'💬 Sampler status: {self.message}') + console.print(f'🧪 Sampler: {self.sampler_name}') + console.print( + f'🎯 Committed point estimate: {_format_point_estimate_name(self.point_estimate_name)}' + ) + sampler_completed = 'yes' if self.sampler_completed else 'no' + console.print(f'🔁 Sampler completed: {sampler_completed}') + console.print(f'⏱️ Fitting time: {fitting_time}') + console.print(f'📏 Goodness-of-fit (reduced χ²): {goodness_of_fit}') + if self.best_log_posterior is not None: + console.print(f'📉 Best log-posterior: {self.best_log_posterior:.2f}') + + sampler_settings = _format_sampler_settings(self.sampler_settings) + if sampler_settings is not None: + console.print(Text(f'⚙️ Sampler settings: {sampler_settings}')) + + convergence_summary = _format_convergence_summary(self.convergence_diagnostics) + if convergence_summary is not None: + console.print(Text.from_markup(f'📊 Convergence: {convergence_summary}')) + + +def compute_convergence_diagnostics(posterior_samples: PosteriorSamples) -> dict[str, object]: + """ + Compute convergence diagnostics from posterior samples. + + Parameters + ---------- + posterior_samples : PosteriorSamples + Posterior samples container. + + Returns + ------- + dict[str, object] + Convergence metrics keyed by diagnostic name. + """ + inference_data = posterior_samples.to_arviz() + rhat_dataset = az.rhat(inference_data) + ess_dataset = az.ess(inference_data, method='bulk') + + r_hat_by_parameter = _dataset_to_scalar_dict(rhat_dataset) + ess_bulk_by_parameter = _dataset_to_scalar_dict(ess_dataset) + + finite_r_hat = [value for value in r_hat_by_parameter.values() if value is not None] + finite_ess_bulk = [value for value in ess_bulk_by_parameter.values() if value is not None] + + max_r_hat = max(finite_r_hat, default=None) + min_ess_bulk = min(finite_ess_bulk, default=None) + + converged = len(finite_r_hat) == len(r_hat_by_parameter) and len(finite_ess_bulk) == len( + ess_bulk_by_parameter + ) + if max_r_hat is not None and max_r_hat > R_HAT_CONVERGENCE_THRESHOLD: + converged = False + if min_ess_bulk is not None and min_ess_bulk < ESS_BULK_CONVERGENCE_THRESHOLD: + converged = False + + return { + 'converged': converged, + 'r_hat_by_parameter': r_hat_by_parameter, + 'ess_bulk_by_parameter': ess_bulk_by_parameter, + 'max_r_hat': max_r_hat, + 'min_ess_bulk': min_ess_bulk, + 'n_draws': int(posterior_samples.parameter_samples.shape[0]), + 'n_chains': int(posterior_samples.parameter_samples.shape[1]), + 'n_parameters': len(posterior_samples.parameter_names), + } + + +def summarize_posterior_parameters( + parameter_names: list[str], + posterior_samples: PosteriorSamples, + best_sample_values: np.ndarray, + parameter_display_names: list[str] | None = None, + convergence_diagnostics: dict[str, object] | None = None, +) -> list[PosteriorParameterSummary]: + """ + Build posterior parameter summaries in EasyDiffraction order. + + Parameters + ---------- + parameter_names : list[str] + Sampled parameter names in EasyDiffraction order. + posterior_samples : PosteriorSamples + Posterior sample container. + best_sample_values : np.ndarray + Best posterior sample values in the same order. + parameter_display_names : list[str] | None, default=None + Human-readable parameter names in the same order. + convergence_diagnostics : dict[str, object] | None, default=None + Optional convergence diagnostics keyed by parameter name. + + Returns + ------- + list[PosteriorParameterSummary] + Summary rows matching the input parameter order. + + Raises + ------ + ValueError + If the posterior sample array is incompatible with the parameter + name list. + """ + flattened = posterior_samples.flattened() + if flattened.shape[1] != len(parameter_names): + msg = 'Posterior samples do not match the sampled parameter name list length.' + raise ValueError(msg) + if parameter_display_names is not None and len(parameter_display_names) != len( + parameter_names + ): + msg = 'Posterior display-name list must match the sampled parameter name list length.' + raise ValueError(msg) + + r_hat_by_parameter = {} + ess_bulk_by_parameter = {} + if convergence_diagnostics is not None: + r_hat_by_parameter = convergence_diagnostics.get('r_hat_by_parameter', {}) + ess_bulk_by_parameter = convergence_diagnostics.get('ess_bulk_by_parameter', {}) + + summaries: list[PosteriorParameterSummary] = [] + for index, parameter_name in enumerate(parameter_names): + values = flattened[:, index] + interval_68 = tuple(np.quantile(values, [0.16, 0.84]).tolist()) + interval_95 = tuple(np.quantile(values, [0.025, 0.975]).tolist()) + display_name = ( + parameter_display_names[index] + if parameter_display_names is not None + else parameter_name + ) + summaries.append( + PosteriorParameterSummary( + unique_name=parameter_name, + display_name=display_name, + best_sample_value=float(best_sample_values[index]), + median=float(np.median(values)), + standard_deviation=float(np.std(values, ddof=1)), + interval_68=(float(interval_68[0]), float(interval_68[1])), + interval_95=(float(interval_95[0]), float(interval_95[1])), + ess_bulk=_maybe_scalar(ess_bulk_by_parameter.get(parameter_name)), + r_hat=_maybe_scalar(r_hat_by_parameter.get(parameter_name)), + ) + ) + + return summaries + + +def standard_deviations_from_summaries( + summaries: list[PosteriorParameterSummary], +) -> np.ndarray: + """ + Return posterior standard deviations in summary order. + + Parameters + ---------- + summaries : list[PosteriorParameterSummary] + Posterior summaries in parameter order. + + Returns + ------- + np.ndarray + Standard deviations in the same order. + """ + return np.array([summary.standard_deviation for summary in summaries], dtype=float) + + +def _dataset_to_scalar_dict(dataset: object) -> dict[str, float | None]: + values: dict[str, float | None] = {} + for name, data_array in dataset.data_vars.items(): + values[name] = _maybe_scalar(np.asarray(data_array).reshape(-1)[0]) + return values + + +def _maybe_scalar(value: object) -> float | None: + if value is None: + return None + scalar = float(value) + if not np.isfinite(scalar): + return None + return scalar + + +def _format_sampler_settings(sampler_settings: dict[str, object]) -> str | None: + if not sampler_settings: + return None + + parts = [ + f'{key}={sampler_settings[key]}' + for key in ('steps', 'burn', 'thin', 'pop', 'init', 'samples') + if key in sampler_settings + ] + return ', '.join(parts) if parts else None + + +def _calculate_fit_quality_metrics( + *, + y_obs: list[float] | None, + y_calc: list[float] | None, + y_err: list[float] | None, + f_obs: list[float] | None, + f_calc: list[float] | None, +) -> dict[str, float | None]: + """Compute optional fit-quality metrics for summary rendering.""" + metrics: dict[str, float | None] = { + 'rf': None, + 'rf2': None, + 'wr': None, + 'br': None, + } + if y_obs is not None and y_calc is not None: + metrics['rf'] = calculate_r_factor(y_obs, y_calc) * 100 + metrics['rf2'] = calculate_r_factor_squared(y_obs, y_calc) * 100 + if y_obs is not None and y_calc is not None and y_err is not None: + metrics['wr'] = calculate_weighted_r_factor(y_obs, y_calc, y_err) * 100 + if f_obs is not None and f_calc is not None: + metrics['br'] = calculate_rb_factor(f_obs, f_calc) * 100 + return metrics + + +def _print_fit_quality_metrics(metrics: dict[str, float | None]) -> None: + """Render any available fit-quality metrics.""" + metric_labels = ( + ('📏 R-factor (Rf)', metrics['rf']), + ('📏 R-factor squared (Rf²)', metrics['rf2']), + ('📏 Weighted R-factor (wR)', metrics['wr']), + ('📏 Bragg R-factor (BR)', metrics['br']), + ) + for label, value in metric_labels: + if value is not None: + console.print(f'{label}: {value:.2f}%') + + +def _format_point_estimate_name(point_estimate_name: str) -> str: + """Return a user-facing label for the committed point estimate.""" + normalized_name = point_estimate_name.strip().lower().replace('_', ' ') + if normalized_name in {'best sample', 'map'}: + return 'Best posterior sample' + return point_estimate_name.replace('_', ' ').title() + + +def _format_bayesian_overall_status( + *, + success: bool, + sampler_completed: bool, + convergence_diagnostics: dict[str, object], +) -> tuple[str, str]: + """Return icon and text for Bayesian run status.""" + if not success: + return '❌', 'failed' + + converged = convergence_diagnostics.get('converged') + if converged is False: + return '⚠️', 'completed with warnings' + if sampler_completed: + return '✅', 'completed' + return '✅', 'posterior available' + + +def _format_convergence_summary(convergence_diagnostics: dict[str, object]) -> str | None: + if not convergence_diagnostics: + return None + + parts: list[str] = [] + converged = convergence_diagnostics.get('converged') + if converged is not None: + status = 'passed' if converged else '[red]failed[/red]' + parts.append(f'status={status}') + + max_r_hat = _maybe_scalar(convergence_diagnostics.get('max_r_hat')) + if max_r_hat is not None: + parts.append(f'max_r_hat={_format_r_hat(max_r_hat)}') + + min_ess_bulk = _maybe_scalar(convergence_diagnostics.get('min_ess_bulk')) + if min_ess_bulk is not None: + parts.append(f'min_ess_bulk={_format_ess_bulk(min_ess_bulk)}') + + n_draws = convergence_diagnostics.get('n_draws') + n_chains = convergence_diagnostics.get('n_chains') + if n_draws is not None and n_chains is not None: + parts.append(f'draws={n_draws}, chains={n_chains}') + + return ', '.join(parts) if parts else None + + +def _render_committed_parameter_table(parameters: list[object]) -> None: + headers = [ + 'datablock', + 'category', + 'entry', + 'parameter', + 'units', + 'start', + 'best posterior sample', + 'uncertainty', + 'change', + ] + alignments = [ + 'left', + 'left', + 'left', + 'left', + 'left', + 'right', + 'right', + 'right', + 'right', + ] + rows = [_build_parameter_row(parameter) for parameter in parameters] + render_table( + columns_headers=headers, + columns_alignment=alignments, + columns_data=rows, + ) + + +def _render_posterior_summary_table( + *, + parameters: list[object], + posterior_parameter_summaries: list[PosteriorParameterSummary], +) -> None: + if not posterior_parameter_summaries: + console.print('No posterior parameter summaries available.') + return + + parameters_by_name = {parameter.unique_name: parameter for parameter in parameters} + headers = [ + 'datablock', + 'category', + 'entry', + 'parameter', + 'units', + 'median', + '95% interval', + 'r-hat', + 'ess bulk', + ] + alignments = [ + 'left', + 'left', + 'left', + 'left', + 'left', + 'right', + 'right', + 'right', + 'right', + ] + rows = [ + _build_posterior_summary_row(summary, parameters_by_name) + for summary in posterior_parameter_summaries + ] + render_table( + columns_headers=headers, + columns_alignment=alignments, + columns_data=rows, + ) + + +def _build_posterior_summary_row( + summary: PosteriorParameterSummary, + parameters_by_name: dict[str, object], +) -> list[str]: + parameter = parameters_by_name.get(summary.unique_name) + identity = getattr(parameter, '_identity', None) + datablock = getattr(identity, 'datablock_entry_name', 'N/A') + category = getattr(identity, 'category_code', 'N/A') + entry = getattr(identity, 'category_entry_name', '') or '' + parameter_name = getattr(parameter, 'name', summary.display_name) + units = getattr(parameter, 'units', 'N/A') + + return [ + datablock, + category, + entry, + parameter_name, + units, + f'{summary.median:.4f}', + _format_interval(summary.interval_95), + _format_r_hat(summary.r_hat), + _format_ess_bulk(summary.ess_bulk), + ] + + +def _format_interval(interval: tuple[float, float]) -> str: + return f'[{interval[0]:.4f}, {interval[1]:.4f}]' + + +def _format_r_hat(value: float | None) -> str: + if value is None or not np.isfinite(value): + return 'N/A' + formatted = f'{value:.3f}' + if value > R_HAT_CONVERGENCE_THRESHOLD: + return f'[red]{formatted}[/red]' + return formatted + + +def _format_ess_bulk(value: float | None) -> str: + if value is None or not np.isfinite(value): + return 'N/A' + formatted = f'{value:.1f}' + if value < ESS_BULK_CONVERGENCE_THRESHOLD: + return f'[red]{formatted}[/red]' + return formatted + + +def _posterior_table_notes( + posterior_parameter_summaries: list[PosteriorParameterSummary], +) -> list[str]: + """Return warning notes for posterior summary diagnostics.""" + if not posterior_parameter_summaries: + return [] + + has_failed_r_hat = any( + summary.r_hat is not None and summary.r_hat > R_HAT_CONVERGENCE_THRESHOLD + for summary in posterior_parameter_summaries + ) + has_failed_ess_bulk = any( + summary.ess_bulk is not None and summary.ess_bulk < ESS_BULK_CONVERGENCE_THRESHOLD + for summary in posterior_parameter_summaries + ) + + if not has_failed_r_hat and not has_failed_ess_bulk: + return [] + + notes: list[str] = [] + if has_failed_r_hat: + notes.append( + f'[red]r-hat > {R_HAT_CONVERGENCE_THRESHOLD:.2f}[/red]: ' + 'Consider longer sampling, better initialization, or reparameterization.' + ) + if has_failed_ess_bulk: + notes.append( + f'[red]ess bulk < {ESS_BULK_CONVERGENCE_THRESHOLD:.0f}[/red]: ' + 'Consider longer sampling or reparameterization.' + ) + return notes diff --git a/src/easydiffraction/analysis/fit_helpers/reporting.py b/src/easydiffraction/analysis/fit_helpers/reporting.py index a83ad7bf5..aefad27b6 100644 --- a/src/easydiffraction/analysis/fit_helpers/reporting.py +++ b/src/easydiffraction/analysis/fit_helpers/reporting.py @@ -108,8 +108,10 @@ def display_results( console.paragraph('Fit results') console.print(f'{status_icon} Success: {self.success}') - console.print(f'⏱️ Fitting time: {self.fitting_time:.2f} seconds') - console.print(f'📏 Goodness-of-fit (reduced χ²): {self.reduced_chi_square:.2f}') + fitting_time = _format_optional_float(self.fitting_time, suffix=' seconds') + goodness_of_fit = _format_optional_float(self.reduced_chi_square) + console.print(f'⏱️ Fitting time: {fitting_time}') + console.print(f'📏 Goodness-of-fit (reduced χ²): {goodness_of_fit}') if rf is not None: console.print(f'📏 R-factor (Rf): {rf:.2f}%') if rf2 is not None: @@ -125,10 +127,10 @@ def display_results( 'category', 'entry', 'parameter', + 'units', 'start', 'fitted', 'uncertainty', - 'units', 'change', ] alignments = [ @@ -136,10 +138,10 @@ def display_results( 'left', 'left', 'left', + 'left', 'right', 'right', 'right', - 'left', 'right', ] @@ -221,10 +223,10 @@ def _build_parameter_row(param: object) -> list[str]: param._identity.category_code, param._identity.category_entry_name or '', name, + units, start, fitted, uncertainty, - units, relative_change, ] @@ -248,3 +250,29 @@ def _compute_relative_change(param: object) -> str: change = ((param.value - param._fit_start_value) / param._fit_start_value) * 100 arrow = '↑' if change > 0 else '↓' return f'{abs(change):.2f} % {arrow}' + + +def _format_optional_float( + value: float | None, + *, + suffix: str = '', +) -> str: + """ + Format an optional float for console output. + + Parameters + ---------- + value : float | None + Value to format. + suffix : str, default='' + Optional suffix appended to formatted numeric values. + + Returns + ------- + str + ``'N/A'`` when the value is ``None``; otherwise a formatted + string with two decimal places. + """ + if value is None: + return 'N/A' + return f'{value:.2f}{suffix}' diff --git a/src/easydiffraction/analysis/fit_helpers/tracking.py b/src/easydiffraction/analysis/fit_helpers/tracking.py index 99f9c8b68..c24f94359 100644 --- a/src/easydiffraction/analysis/fit_helpers/tracking.py +++ b/src/easydiffraction/analysis/fit_helpers/tracking.py @@ -1,86 +1,64 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -import time -from contextlib import suppress - -import numpy as np +from __future__ import annotations -from easydiffraction.utils.logging import console - -try: - from IPython.display import HTML - from IPython.display import DisplayHandle - from IPython.display import display -except ImportError: - display = None - clear_output = None +import sys +import time +from dataclasses import dataclass +from typing import TYPE_CHECKING from easydiffraction.analysis.fit_helpers.metrics import calculate_reduced_chi_square +from easydiffraction.display.progress import ACTIVITY_LABEL_BURN_IN +from easydiffraction.display.progress import ACTIVITY_LABEL_FITTING +from easydiffraction.display.progress import ACTIVITY_LABEL_POST_PROCESSING +from easydiffraction.display.progress import ACTIVITY_LABEL_PRE_PROCESSING +from easydiffraction.display.progress import ACTIVITY_LABEL_PROCESSING +from easydiffraction.display.progress import ACTIVITY_LABEL_SAMPLING +from easydiffraction.display.progress import ActivityIndicator +from easydiffraction.display.progress import _TerminalLiveHandle as _SharedTerminalLiveHandle +from easydiffraction.display.progress import make_display_handle from easydiffraction.utils.enums import VerbosityEnum -from easydiffraction.utils.environment import in_jupyter -from easydiffraction.utils.utils import render_table - -try: - from rich.live import Live -except ImportError: # pragma: no cover - rich always available in app env - Live = None # type: ignore[assignment] +from easydiffraction.utils.logging import console +from easydiffraction.utils.utils import build_table_renderable -from easydiffraction.utils.logging import ConsoleManager +if TYPE_CHECKING: + import numpy as np SIGNIFICANT_CHANGE_THRESHOLD = 0.01 # 1% threshold -DEFAULT_HEADERS = ['iteration', 'χ²', 'improvement [%]'] -DEFAULT_ALIGNMENTS = ['center', 'center', 'center'] - - -class _TerminalLiveHandle: - """ - Adapter that exposes update()/close() for terminal live updates. +FIT_PROGRESS_UPDATE_SECONDS = 5.0 +SAMPLER_PROGRESS_UPDATE_SECONDS = 5.0 +TRACKING_MODE_FIT = 'fit' +TRACKING_MODE_SAMPLER = 'sampling' +SAMPLER_PHASE_POST_PROCESSING = 'post-processing' +SAMPLER_PHASE_PRE_PROCESSING = 'pre-processing' +DEFAULT_HEADERS = ['iteration', 'time (s)', 'χ²', 'change / status'] +DEFAULT_ALIGNMENTS = ['center', 'center', 'center', 'center'] +SAMPLER_HEADERS = ['iteration', 'progress', 'time (s)', 'log posterior', 'phase'] +SAMPLER_ALIGNMENTS = ['center', 'center', 'center', 'center', 'center'] - Wraps a rich.live.Live instance but keeps the tracker decoupled from - the underlying UI mechanism. - """ +_TerminalLiveHandle = _SharedTerminalLiveHandle - def __init__(self, live: object) -> None: - self._live = live - 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() -> object | None: + """Return a backward-compatible generic live display handle.""" + return make_display_handle() -def _make_display_handle() -> object | None: +@dataclass(frozen=True, slots=True) +class SamplerProgressUpdate: """ - 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. + Normalized sampler progress payload forwarded by monitor hooks. """ - if in_jupyter() and display is not None and HTML is not None: - h = DisplayHandle() - # Create an empty placeholder area to update in place - h.display(HTML('')) - return h - if Live is not None: - # Reuse the shared Console to coordinate with logging output - # and keep consistent width - live = Live(console=ConsoleManager.get(), auto_refresh=True) - live.start() - return _TerminalLiveHandle(live) - return None + + iteration: int + total_iterations: int + phase: str + progress_percent: float + log_posterior: float + reduced_chi2: float + elapsed_time: float + force_report: bool = False class FitProgressTracker: @@ -97,24 +75,50 @@ def __init__(self) -> None: self._previous_chi2: float | None = None self._last_chi2: float | None = None self._last_iteration: int | None = None + self._last_reported_iteration: int | None = None self._best_chi2: float | None = None self._best_iteration: int | None = None self._fitting_time: float | None = None + self._start_time: float | None = None + self._end_time: float | None = None self._verbosity: VerbosityEnum = VerbosityEnum.FULL + self._last_progress_time: float | None = None + self._tracking_mode: str = TRACKING_MODE_FIT + self._sampler_total_iterations: int | None = None + self._last_sampler_phase: str | None = None + self._last_sampler_progress_percent: float | None = None + self._last_sampler_log_posterior: float | None = None + self._last_sampler_elapsed_time: float | None = None + self._sampler_pre_processing_pending: bool = False self._df_rows: list[list[str]] = [] - self._display_handle: object | None = None - self._live: object | None = None + self._activity_indicator: ActivityIndicator | None = None + self._activity_label: str = ACTIVITY_LABEL_FITTING + self._shared_display_handle: object | None = None def reset(self) -> None: """Reset internal state before a new optimization run.""" + self._stop_activity_indicator() self._iteration = 0 self._previous_chi2 = None self._last_chi2 = None self._last_iteration = None + self._last_reported_iteration = None self._best_chi2 = None self._best_iteration = None self._fitting_time = None + self._start_time = None + self._end_time = None + self._last_progress_time = None + self._tracking_mode = TRACKING_MODE_FIT + self._sampler_total_iterations = None + self._last_sampler_phase = None + self._last_sampler_progress_percent = None + self._last_sampler_log_posterior = None + self._last_sampler_elapsed_time = None + self._sampler_pre_processing_pending = False + self._df_rows = [] + self._activity_label = ACTIVITY_LABEL_FITTING def track( self, @@ -140,50 +144,153 @@ def track( reduced_chi2 = calculate_reduced_chi_square(residuals, len(parameters)) + if self._tracking_mode == TRACKING_MODE_SAMPLER: + if self._previous_chi2 is None: + self._previous_chi2 = reduced_chi2 + self._best_chi2 = reduced_chi2 + elif self._best_chi2 is None or reduced_chi2 < self._best_chi2: + self._best_chi2 = reduced_chi2 + + self._last_chi2 = reduced_chi2 + return residuals + + self.track_fit_progress( + iteration=self._iteration, + reduced_chi2=reduced_chi2, + elapsed_time=self._current_elapsed_time(), + ) + + return residuals + + def track_fit_progress( + self, + *, + iteration: int, + reduced_chi2: float, + elapsed_time: float, + ) -> None: + """Update fit progress from a backend iteration callback.""" + self._iteration = max(1, iteration) + row: list[str] = [] - # First iteration, initialize tracking if self._previous_chi2 is None: self._previous_chi2 = reduced_chi2 self._best_chi2 = reduced_chi2 self._best_iteration = self._iteration + self._last_progress_time = elapsed_time row = [ str(self._iteration), + self._format_elapsed_time(elapsed_time), f'{reduced_chi2:.2f}', '', ] - - # Subsequent iterations, check for significant changes else: change = (self._previous_chi2 - reduced_chi2) / self._previous_chi2 - # Improvement check if change > SIGNIFICANT_CHANGE_THRESHOLD: change_in_percent = change * 100 row = [ str(self._iteration), + self._format_elapsed_time(elapsed_time), f'{reduced_chi2:.2f}', f'{change_in_percent:.1f}% ↓', ] self._previous_chi2 = reduced_chi2 + self._last_progress_time = elapsed_time + elif self._should_render_fit_row(elapsed_time): + row = [ + str(self._iteration), + self._format_elapsed_time(elapsed_time), + f'{reduced_chi2:.2f}', + '', + ] + self._last_progress_time = elapsed_time - # Output if there is something new to display if row: self.add_tracking_info(row) - # Update best chi-square if better - if reduced_chi2 < self._best_chi2: + if self._best_chi2 is None or reduced_chi2 < self._best_chi2: self._best_chi2 = reduced_chi2 self._best_iteration = self._iteration - # Store last chi-square and iteration self._last_chi2 = reduced_chi2 self._last_iteration = self._iteration - return residuals + def track_sampler_progress(self, update: SamplerProgressUpdate) -> None: + """ + Update progress from a sampler monitor. + + Parameters + ---------- + update : SamplerProgressUpdate + Sampler iteration, phase, timing, and fit-quality payload. + """ + self._iteration = update.iteration + self._tracking_mode = TRACKING_MODE_SAMPLER + self._sampler_total_iterations = max(1, update.total_iterations) + + clamped_iteration = min(max(1, update.iteration), self._sampler_total_iterations) + clamped_progress = min(max(update.progress_percent, 0.0), 100.0) + previous_phase = self._last_sampler_phase + self._last_sampler_phase = update.phase + self._last_sampler_progress_percent = clamped_progress + self._last_sampler_log_posterior = update.log_posterior + self._last_sampler_elapsed_time = update.elapsed_time + self._set_activity_label(self._activity_label_for_sampler_phase(update.phase)) + + row = self._initial_sampler_progress_row( + update=update, + clamped_iteration=clamped_iteration, + clamped_progress=clamped_progress, + ) + if not row: + row = self._continued_sampler_progress_row( + update=update, + previous_phase=previous_phase, + clamped_iteration=clamped_iteration, + clamped_progress=clamped_progress, + ) + + if row: + self.add_tracking_info(row) + + self._last_chi2 = update.reduced_chi2 + self._last_iteration = update.iteration + + def start_sampler_pre_processing(self, *, total_iterations: int) -> None: + """Mark sampler setup so a status row appears on update.""" + self._tracking_mode = TRACKING_MODE_SAMPLER + self._sampler_total_iterations = max(1, total_iterations) + self._last_sampler_phase = SAMPLER_PHASE_PRE_PROCESSING + self._last_sampler_progress_percent = None + self._last_sampler_log_posterior = None + self._last_sampler_elapsed_time = None + self._sampler_pre_processing_pending = True + self._set_activity_label(ACTIVITY_LABEL_PRE_PROCESSING) + + def start_sampler_post_processing( + self, + *, + log_posterior: float | None = None, + ) -> None: + """Switch the activity indicator to post-processing.""" + if self._tracking_mode != TRACKING_MODE_SAMPLER: + return + + if self._sampler_total_iterations is None: + self._sampler_total_iterations = max(1, self._last_iteration or 1) + + elapsed_time = self._elapsed_since_start() + self._last_sampler_phase = SAMPLER_PHASE_POST_PROCESSING + self._last_sampler_progress_percent = 100.0 + if log_posterior is not None: + self._last_sampler_log_posterior = float(log_posterior) + self._last_sampler_elapsed_time = elapsed_time + self._set_activity_label(ACTIVITY_LABEL_POST_PROCESSING) @property def best_chi2(self) -> float | None: @@ -208,13 +315,27 @@ def fitting_time(self) -> float | None: def start_timer(self) -> None: """Begin timing of a fit run.""" self._start_time = time.perf_counter() + self._end_time = None def stop_timer(self) -> None: """Stop timing and store elapsed time for the run.""" + if self._start_time is None: + self._fitting_time = None + return self._end_time = time.perf_counter() self._fitting_time = self._end_time - self._start_time - def start_tracking(self, minimizer_name: str) -> None: + def _elapsed_since_start(self) -> float | None: + """ + Return elapsed wall time using the active timer when available. + """ + if self._start_time is None: + return None + if self._end_time is not None: + return self._end_time - self._start_time + return time.perf_counter() - self._start_time + + def start_tracking(self, minimizer_name: str, *, mode: str = TRACKING_MODE_FIT) -> None: """ Initialize display and headers and announce the minimizer. @@ -222,26 +343,26 @@ def start_tracking(self, minimizer_name: str) -> None: ---------- minimizer_name : str Name of the minimizer used for the run. + mode : str, default=TRACKING_MODE_FIT + Tracking mode for the run. """ + self._tracking_mode = ( + TRACKING_MODE_SAMPLER if mode == TRACKING_MODE_SAMPLER else TRACKING_MODE_FIT + ) + self._df_rows = [] + self._activity_label = self._default_activity_label() + 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:') + if self._verbosity is VerbosityEnum.FULL: + console.print(f"🚀 Starting fit process with '{minimizer_name}'...") + if self._tracking_mode == TRACKING_MODE_SAMPLER: + console.print('📈 Bayesian sampling progress:') + else: + console.print('📈 Goodness-of-fit progress:') - # Reset rows and create an environment-appropriate handle - self._df_rows = [] - self._display_handle = _make_display_handle() - - # Initial empty table; subsequent updates will reuse the handle - render_table( - columns_headers=DEFAULT_HEADERS, - columns_alignment=DEFAULT_ALIGNMENTS, - columns_data=self._df_rows, - display_handle=self._display_handle, - ) + self._start_activity_indicator() def add_tracking_info(self, row: list[str]) -> None: """ @@ -250,41 +371,386 @@ def add_tracking_info(self, row: list[str]) -> None: Parameters ---------- row : list[str] - Columns corresponding to DEFAULT_HEADERS. + Columns corresponding to the active tracking headers. """ + if row: + iteration_cell = row[0].split('/', maxsplit=1)[0] + if iteration_cell.isdigit(): + self._last_reported_iteration = int(iteration_cell) self._df_rows.append(row) - if self._verbosity is not VerbosityEnum.FULL: - return - # Append and update via the active handle (Jupyter or - # terminal live) - render_table( - columns_headers=DEFAULT_HEADERS, - columns_alignment=DEFAULT_ALIGNMENTS, - columns_data=self._df_rows, - display_handle=self._display_handle, - ) + if self._verbosity is VerbosityEnum.FULL: + self._refresh_activity_indicator() def finish_tracking(self) -> None: """Finalize progress display and print best result summary.""" - # Add last iteration as last row - row: list[str] = [ + if self._tracking_mode == TRACKING_MODE_SAMPLER: + self._finalize_sampler_tracking_row() + else: + self._finalize_fit_tracking_row() + + if self._verbosity is VerbosityEnum.SILENT: + return + + self._stop_activity_indicator() + if self._verbosity is VerbosityEnum.FULL and not self._cleanup_during_exception(): + self._print_completion_summary() + + @staticmethod + def _cleanup_during_exception() -> bool: + """ + Return whether cleanup runs during exception handling. + """ + return sys.exc_info()[0] is not None + + def _initial_sampler_progress_row( + self, + *, + update: SamplerProgressUpdate, + clamped_iteration: int, + clamped_progress: float, + ) -> list[str]: + if self._df_rows: + return [] + + self._previous_chi2 = update.reduced_chi2 + self._best_chi2 = update.reduced_chi2 + self._best_iteration = update.iteration + self._last_progress_time = update.elapsed_time + if self._sampler_pre_processing_pending: + self._sampler_pre_processing_pending = False + return self._sampler_status_row( + iteration_label=self._sampler_iteration_label(clamped_iteration), + phase=SAMPLER_PHASE_PRE_PROCESSING, + elapsed_time=update.elapsed_time, + log_posterior=update.log_posterior, + ) + return self._sampler_progress_row( + clamped_iteration=clamped_iteration, + clamped_progress=clamped_progress, + log_posterior=update.log_posterior, + phase=update.phase, + elapsed_time=update.elapsed_time, + ) + + def _continued_sampler_progress_row( + self, + *, + update: SamplerProgressUpdate, + previous_phase: str | None, + clamped_iteration: int, + clamped_progress: float, + ) -> list[str]: + if self._best_chi2 is not None and update.reduced_chi2 < self._best_chi2: + self._best_chi2 = update.reduced_chi2 + self._best_iteration = update.iteration + + if not self._should_render_sampler_row( + iteration=update.iteration, + previous_phase=previous_phase, + phase=update.phase, + elapsed_time=update.elapsed_time, + force_report=update.force_report, + clamped_iteration=clamped_iteration, + ): + return [] + + self._last_progress_time = update.elapsed_time + return self._sampler_progress_row( + clamped_iteration=clamped_iteration, + clamped_progress=clamped_progress, + log_posterior=update.log_posterior, + phase=update.phase, + elapsed_time=update.elapsed_time, + ) + + def _should_render_sampler_row( + self, + *, + iteration: int, + previous_phase: str | None, + phase: str, + elapsed_time: float, + force_report: bool, + clamped_iteration: int, + ) -> bool: + if iteration == self._last_reported_iteration: + return False + + return ( + force_report + or previous_phase != phase + or self._last_progress_time is None + or elapsed_time - self._last_progress_time >= SAMPLER_PROGRESS_UPDATE_SECONDS + or clamped_iteration >= self._sampler_total_iterations + ) + + def _sampler_progress_row( + self, + *, + clamped_iteration: int, + clamped_progress: float, + log_posterior: float, + phase: str, + elapsed_time: float, + ) -> list[str]: + return [ + self._sampler_iteration_label(clamped_iteration), + f'{clamped_progress:.1f}%', + self._format_elapsed_time(elapsed_time), + f'{log_posterior:.2f}', + phase, + ] + + def _sampler_status_row( + self, + *, + iteration_label: str = '', + phase: str, + elapsed_time: float | None, + log_posterior: float | None = None, + ) -> list[str]: + """ + Return a status-only sampler row without iteration metrics. + """ + return [ + iteration_label, + '', + self._format_elapsed_time(elapsed_time), + '' if log_posterior is None else f'{log_posterior:.2f}', + phase, + ] + + def _finalize_sampler_tracking_row(self) -> None: + row = self._final_sampler_tracking_row() + if row is None: + return + + if not self._df_rows: + self.add_tracking_info(row) + return + + if self._rows_match_on_columns(self._df_rows[-1], row, (0, 1, 3, 4)): + self._replace_last_tracking_row(row) + return + + if self._df_rows[-1] != row: + self.add_tracking_info(row) + + def _final_sampler_tracking_row(self) -> list[str] | None: + if self._last_iteration is None or self._sampler_total_iterations is None: + return None + + final_progress = self._resolved_final_sampler_progress() + elapsed_time = self._resolved_final_sampler_elapsed_time() + if self._last_sampler_phase == SAMPLER_PHASE_POST_PROCESSING: + return self._sampler_status_row( + phase=SAMPLER_PHASE_POST_PROCESSING, + elapsed_time=elapsed_time, + ) + + log_posterior = ( + f'{self._last_sampler_log_posterior:.2f}' + if self._last_sampler_log_posterior is not None + else '' + ) + return [ + self._sampler_iteration_label(self._last_iteration), + f'{final_progress:.1f}%', + self._format_elapsed_time(elapsed_time), + log_posterior, + self._last_sampler_phase or TRACKING_MODE_SAMPLER, + ] + + def _finalize_fit_tracking_row(self) -> None: + row = self._final_fit_tracking_row() + if row is None: + return + + if not self._df_rows: + self.add_tracking_info(row) + return + + if self._rows_match_on_columns(self._df_rows[-1], row, (0, 2)): + self._replace_last_tracking_row(row) + return + + if self._df_rows[-1][:3] != row[:3]: + self.add_tracking_info(row) + + def _final_fit_tracking_row(self) -> list[str] | None: + if self._last_iteration is None: + return None + + return [ str(self._last_iteration), + self._format_elapsed_time(self._fitting_time), f'{self._last_chi2:.2f}' if self._last_chi2 is not None else '', '', ] - self.add_tracking_info(row) - if self._verbosity is not VerbosityEnum.FULL: + def _resolved_final_sampler_progress(self) -> float: + if self._last_sampler_progress_percent is not None: + return self._last_sampler_progress_percent + + if self._last_iteration is None or self._sampler_total_iterations is None: + msg = 'Sampler progress is unavailable without final iteration counts.' + raise RuntimeError(msg) + return ( + 100.0 + * min(self._last_iteration, self._sampler_total_iterations) + / self._sampler_total_iterations + ) + + def _resolved_final_sampler_elapsed_time(self) -> float | None: + if self._fitting_time is not None: + return self._fitting_time + return self._last_sampler_elapsed_time + + def _sampler_iteration_label(self, iteration: int) -> str: + if self._sampler_total_iterations is None: + msg = 'Sampler iteration labels require a configured total iteration count.' + raise RuntimeError(msg) + clamped_iteration = min(iteration, self._sampler_total_iterations) + return f'{clamped_iteration}/{self._sampler_total_iterations}' + + def _print_completion_summary(self) -> None: + if self._tracking_mode == TRACKING_MODE_SAMPLER: + console.print('✅ Bayesian sampling complete.') return - # Close terminal live if used - if self._display_handle is not None and hasattr(self._display_handle, 'close'): - with suppress(Exception): - self._display_handle.close() + if self._best_chi2 is None or self._best_iteration is None: + return - # Print best result console.print( f'🏆 Best goodness-of-fit (reduced χ²) is {self._best_chi2:.2f} ' f'at iteration {self._best_iteration}' ) console.print('✅ Fitting complete.') + + def _headers(self) -> list[str]: + """Return column headers for the active tracking mode.""" + if self._tracking_mode == TRACKING_MODE_SAMPLER: + return SAMPLER_HEADERS + return DEFAULT_HEADERS + + def _alignments(self) -> list[str]: + """Return column alignments for the active tracking mode.""" + if self._tracking_mode == TRACKING_MODE_SAMPLER: + return SAMPLER_ALIGNMENTS + return DEFAULT_ALIGNMENTS + + def _current_elapsed_time(self) -> float | None: + """Return elapsed run time in seconds when timing is active.""" + if self._start_time is None: + return None + + end_time = self._end_time if self._end_time is not None else time.perf_counter() + return max(end_time - self._start_time, 0.0) + + def _format_elapsed_time(self, elapsed_time: float | None = None) -> str: + """Format elapsed time in seconds with two decimal places.""" + resolved_time = elapsed_time + if resolved_time is None: + resolved_time = self._current_elapsed_time() + if resolved_time is None: + return '' + return f'{resolved_time:.2f}' + + def _should_render_fit_row(self, elapsed_time: float | None) -> bool: + if elapsed_time is None or self._last_progress_time is None: + return False + return elapsed_time - self._last_progress_time >= FIT_PROGRESS_UPDATE_SECONDS + + @staticmethod + def _rows_match_on_columns( + current_row: list[str], + new_row: list[str], + column_indices: tuple[int, ...], + ) -> bool: + """ + Return whether two tracking rows match on selected columns. + """ + return all( + len(current_row) > index + and len(new_row) > index + and current_row[index] == new_row[index] + for index in column_indices + ) + + def _replace_last_tracking_row(self, row: list[str]) -> None: + """ + Replace the last rendered tracking row and refresh the view. + """ + if not self._df_rows: + self.add_tracking_info(row) + return + + self._df_rows[-1] = row + if self._verbosity is VerbosityEnum.FULL: + self._refresh_activity_indicator() + + def _default_activity_label(self) -> str: + if self._tracking_mode == TRACKING_MODE_SAMPLER: + return ACTIVITY_LABEL_PROCESSING + return ACTIVITY_LABEL_FITTING + + @staticmethod + def _activity_label_for_sampler_phase(phase: str) -> str: + normalized_phase = phase.strip().lower() + if normalized_phase == SAMPLER_PHASE_PRE_PROCESSING: + return ACTIVITY_LABEL_PRE_PROCESSING + if normalized_phase == 'burn-in': + return ACTIVITY_LABEL_BURN_IN + if normalized_phase == SAMPLER_PHASE_POST_PROCESSING: + return ACTIVITY_LABEL_POST_PROCESSING + if normalized_phase == 'sampling': + return ACTIVITY_LABEL_SAMPLING + if normalized_phase: + return normalized_phase + return ACTIVITY_LABEL_SAMPLING + + def _set_shared_display_handle(self, display_handle: object | None) -> None: + self._shared_display_handle = display_handle + + def _start_activity_indicator(self) -> None: + self._activity_indicator = ActivityIndicator( + self._activity_label, + verbosity=self._verbosity, + display_handle=self._shared_display_handle, + ) + self._activity_indicator.start() + self._refresh_activity_indicator() + + def _stop_activity_indicator(self) -> None: + if self._activity_indicator is None: + return + + self._activity_indicator.stop() + self._activity_indicator = None + + def _set_activity_label(self, label: str) -> None: + if label == self._activity_label: + return + + self._activity_label = label + self._refresh_activity_indicator() + + def _refresh_activity_indicator(self) -> None: + if self._activity_indicator is None: + return + + if self._verbosity is VerbosityEnum.FULL: + self._activity_indicator.update( + label=self._activity_label, + content=self._table_renderable(), + ) + return + + self._activity_indicator.update(label=self._activity_label) + + def _table_renderable(self) -> object: + return build_table_renderable( + columns_headers=self._headers(), + columns_alignment=self._alignments(), + columns_data=self._df_rows, + ) diff --git a/src/easydiffraction/analysis/fitting.py b/src/easydiffraction/analysis/fitting.py index 9b0360143..72d89a6ce 100644 --- a/src/easydiffraction/analysis/fitting.py +++ b/src/easydiffraction/analysis/fitting.py @@ -21,6 +21,50 @@ from easydiffraction.datablocks.structure.collection import Structures +def _resolve_fit_result_message(results: FitResults) -> str: + """Return a normalized fit-result message.""" + if results.message: + return results.message + + raw_result = results.engine_result + message = getattr(raw_result, 'message', '') + return str(message) if message is not None else '' + + +def _resolve_fit_result_iterations(results: FitResults) -> int: + """Return a normalized iteration or evaluation count.""" + if results.iterations: + return int(results.iterations) + + raw_result = results.engine_result + for attribute_name in ('nfev', 'nit', 'iterations', 'niter'): + value = getattr(raw_result, attribute_name, None) + if value is not None: + return int(value) + return 0 + + +def _resolve_fit_result_chi_square(results: FitResults) -> float | None: + """Return a normalized chi-square-like objective value.""" + if results.chi_square is not None: + return float(results.chi_square) + + raw_result = results.engine_result + chisqr = getattr(raw_result, 'chisqr', None) + if chisqr is not None: + return float(chisqr) + + fun = getattr(raw_result, 'fun', None) + if fun is None: + return None + + if np.isscalar(fun): + return float(fun) + + fun_array = np.asarray(fun, dtype=float) + return float(np.sum(fun_array**2)) + + class Fitter: """Handles the fitting workflow using a pluggable minimizer.""" @@ -30,6 +74,69 @@ def __init__(self, selection: str = MinimizerTypeEnum.default()) -> None: self.minimizer = MinimizerFactory.create(selection) self.results: FitResults | None = None + @staticmethod + def _collect_fit_parameters( + structures: Structures, + experiments: list[ExperimentBase], + ) -> list[Parameter]: + """Return free parameters from structures and experiments.""" + expt_free_params: list[Parameter] = [] + for expt in experiments: + expt_free_params.extend( + p + for p in expt.parameters + if isinstance(p, Parameter) and not p.user_constrained and p.free + ) + return structures.free_parameters + expt_free_params + + def _build_objective_function( + self, + *, + params: list[Parameter], + structures: Structures, + experiments: list[ExperimentBase], + weights: np.ndarray | None, + analysis: object, + ) -> object: + """Return the residual function for the current fit context.""" + + def objective_function(engine_params: dict[str, Any]) -> np.ndarray: + """Evaluate residuals for the current minimizer state.""" + return self._residual_function( + engine_params=engine_params, + parameters=params, + structures=structures, + experiments=experiments, + weights=weights, + analysis=analysis, + ) + + return objective_function + + def _postprocess_fit_results( + self, + *, + analysis: object, + experiments: list[ExperimentBase], + fitted_parameters: list[Parameter], + ) -> None: + """Populate result fields and persist fit projections.""" + if self.results is None: + return + + self.results.message = _resolve_fit_result_message(self.results) + self.results.iterations = _resolve_fit_result_iterations(self.results) + self.results.chi_square = _resolve_fit_result_chi_square(self.results) + + if analysis is None: + return + + analysis._store_fit_result_projection( + self.results, + experiments=experiments, + fitted_parameters=fitted_parameters, + ) + def fit( self, structures: Structures, @@ -39,6 +146,7 @@ def fit( verbosity: VerbosityEnum = VerbosityEnum.FULL, *, use_physical_limits: bool = False, + random_seed: int | None = None, ) -> None: """ Run the fitting process. @@ -65,6 +173,8 @@ def fit( When ``True``, fall back to physical limits from the value spec for parameters whose ``fit_min``/``fit_max`` are unbounded. + random_seed : int | None, default=None + Optional random seed passed to stochastic minimizers. """ # Enforce symmetry constraints (e.g. ADP) before collecting # free parameters so that components fixed by site symmetry are @@ -73,52 +183,48 @@ def fit( structure._need_categories_update = True structure._update_categories() - expt_free_params: list[Parameter] = [] - for expt in experiments: - expt_free_params.extend( - p - for p in expt.parameters - if isinstance(p, Parameter) and not p.constrained and p.free - ) - params = structures.free_parameters + expt_free_params + params = self._collect_fit_parameters(structures, experiments) if not params: + if analysis is not None: + analysis._clear_persisted_fit_state() + analysis.fit_results = None + self.results = None print('⚠️ No parameters selected for fitting.') return + if analysis is not None: + analysis._capture_fit_parameter_state(params) + for param in params: 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, - structures=structures, - experiments=experiments, - weights=weights, + objective_function = self._build_objective_function( + params=params, + structures=structures, + experiments=experiments, + weights=weights, + analysis=analysis, + ) + + try: + # Keep tracker finalization in this layer so post-processing + # can run before the live display is closed. + self.results = self.minimizer.fit( + params, + objective_function, + verbosity=verbosity, + finalize_tracking=False, + use_physical_limits=use_physical_limits, + random_seed=random_seed, + ) + self._postprocess_fit_results( analysis=analysis, + experiments=experiments, + fitted_parameters=params, ) - - # Perform fitting - self.results = self.minimizer.fit( - params, - objective_function, - verbosity=verbosity, - use_physical_limits=use_physical_limits, - ) + finally: + self.minimizer._stop_tracking() def _process_fit_results( self, @@ -239,4 +345,7 @@ def _residual_function( # Append the residuals for this experiment residuals.extend(diff) - return self.minimizer.tracker.track(np.array(residuals), parameters) + residual_array = np.array(residuals) + if getattr(self.minimizer, '_tracks_progress_via_solver_monitor', lambda: False)(): + return residual_array + return self.minimizer.tracker.track(residual_array, parameters) diff --git a/src/easydiffraction/analysis/minimizers/__init__.py b/src/easydiffraction/analysis/minimizers/__init__.py index 7dddb75ee..1006eefb7 100644 --- a/src/easydiffraction/analysis/minimizers/__init__.py +++ b/src/easydiffraction/analysis/minimizers/__init__.py @@ -4,8 +4,10 @@ from easydiffraction.analysis.minimizers.bumps import BumpsMinimizer from easydiffraction.analysis.minimizers.bumps_amoeba import BumpsAmoebaMinimizer from easydiffraction.analysis.minimizers.bumps_de import BumpsDEMinimizer +from easydiffraction.analysis.minimizers.bumps_dream import BumpsDreamMinimizer from easydiffraction.analysis.minimizers.bumps_lm import BumpsLmMinimizer from easydiffraction.analysis.minimizers.dfols import DfolsMinimizer +from easydiffraction.analysis.minimizers.enums import DreamPopulationInitializationEnum from easydiffraction.analysis.minimizers.lmfit import LmfitMinimizer from easydiffraction.analysis.minimizers.lmfit_least_squares import LmfitLeastSquaresMinimizer from easydiffraction.analysis.minimizers.lmfit_leastsq import LmfitLeastsqMinimizer diff --git a/src/easydiffraction/analysis/minimizers/base.py b/src/easydiffraction/analysis/minimizers/base.py index c0b39eb33..2d8bc83f9 100644 --- a/src/easydiffraction/analysis/minimizers/base.py +++ b/src/easydiffraction/analysis/minimizers/base.py @@ -34,15 +34,27 @@ def __init__( ) -> None: self.name: str | None = name self.method: str | None = method - self.max_iterations: int | None = max_iterations + self._max_iterations: int | None = max_iterations self.result: FitResults | None = None self._previous_chi2: float | None = None self._iteration: int | None = None self._best_chi2: float | None = None self._best_iteration: int | None = None self._fitting_time: float | None = None + self._resolved_random_seed: int | None = None + self._tracking_active: bool = False + self._deferred_warning_messages: list[str] = [] self.tracker: FitProgressTracker = FitProgressTracker() + @property + def max_iterations(self) -> int | None: + """User-facing iteration limit for the current minimizer.""" + return self._max_iterations + + @max_iterations.setter + def max_iterations(self, value: int | None) -> None: + self._max_iterations = value + def _start_tracking( self, minimizer_name: str, @@ -60,13 +72,46 @@ def _start_tracking( """ self.tracker.reset() self.tracker._verbosity = verbosity - self.tracker.start_tracking(minimizer_name) + self._tracking_active = True + self._deferred_warning_messages = [] + self.tracker.start_tracking(minimizer_name, mode=self._tracking_mode()) self.tracker.start_timer() def _stop_tracking(self) -> None: """Stop timer and finalize tracking.""" + if not self._tracking_active: + self._emit_deferred_warnings() + return + + self._tracking_active = False self.tracker.stop_timer() self.tracker.finish_tracking() + if self.result is not None: + self.result.fitting_time = self.tracker.fitting_time + self._emit_deferred_warnings() + + def _warn_after_tracking(self, message: str) -> None: + """Log immediately or defer a warning until tracking stops.""" + if self._tracking_active: + self._deferred_warning_messages.append(message) + return + + log.warning(message) + + def _emit_deferred_warnings(self) -> None: + """Flush warnings deferred during live progress display.""" + while self._deferred_warning_messages: + log.warning(self._deferred_warning_messages.pop(0)) + + @staticmethod + def _tracking_mode() -> str: + """Return the tracker mode for the current minimizer.""" + return 'fit' + + @staticmethod + def _tracks_progress_via_solver_monitor() -> bool: + """Return whether live progress comes from solver callbacks.""" + return False @abstractmethod def _prepare_solver_args(self, parameters: list[Any]) -> dict[str, Any]: @@ -124,7 +169,38 @@ def _finalize_fit( self._warn_boundary_parameters(parameters) self._warn_physical_limit_violations(parameters) success = self._check_success(raw_result) - self.result = FitResults( + self.result = self._build_fit_results( + parameters=parameters, + raw_result=raw_result, + success=success, + ) + return self.result + + def _build_fit_results( + self, + *, + parameters: list[object], + raw_result: object, + success: bool, + ) -> FitResults: + """ + Build the final fit-result object for this minimizer. + + Parameters + ---------- + parameters : list[object] + Parameters after the solver finished. + raw_result : object + Backend-specific solver output object. + success : bool + Whether the minimizer considers the run successful. + + Returns + ------- + FitResults + Aggregated outcome of the fit. + """ + return FitResults( success=success, parameters=parameters, reduced_chi_square=self.tracker.best_chi2, @@ -132,7 +208,6 @@ def _finalize_fit( starting_parameters=parameters, fitting_time=self.tracker.fitting_time, ) - return self.result @staticmethod def _warn_boundary_parameters(parameters: list[object]) -> None: @@ -231,13 +306,42 @@ def _warn_physical_limit_violations(parameters: list[object]) -> None: def _check_success(self, raw_result: object) -> bool: """Determine whether the fit was successful.""" + def _resolve_random_seed(self, random_seed: int | None) -> int | None: + """ + Validate or normalize the random seed for this minimizer. + + Parameters + ---------- + random_seed : int | None + User-provided random seed. + + Returns + ------- + int | None + Seed accepted by the minimizer, or ``None`` when not used. + + Raises + ------ + ValueError + If this minimizer does not support ``random_seed``. + """ + if random_seed is None: + self._resolved_random_seed = None + return None + + minimizer_name = self.name or self.__class__.__name__ + msg = f"Minimizer '{minimizer_name}' does not support random_seed." + raise ValueError(msg) + def fit( self, parameters: list[object], objective_function: Callable[..., object], verbosity: VerbosityEnum = VerbosityEnum.FULL, *, + finalize_tracking: bool = True, use_physical_limits: bool = False, + random_seed: int | None = None, ) -> FitResults: """ Run the full minimization workflow. @@ -251,10 +355,14 @@ def fit( arguments. verbosity : VerbosityEnum, default=VerbosityEnum.FULL Console output verbosity. + finalize_tracking : bool, default=True + Whether to stop and finalize live tracking before returning. use_physical_limits : bool, default=False When ``True``, fall back to physical limits from the value spec for parameters whose ``fit_min``/``fit_max`` are unbounded. + random_seed : int | None, default=None + Optional random seed passed to stochastic minimizers. Returns ------- @@ -264,18 +372,23 @@ def fit( if use_physical_limits: self._apply_physical_limits(parameters) + resolved_random_seed = self._resolve_random_seed(random_seed) + minimizer_name = self.name or 'Unnamed Minimizer' if self.method is not None and f'({self.method})' not in minimizer_name: minimizer_name += f' ({self.method})' self._start_tracking(minimizer_name, verbosity=verbosity) - solver_args = self._prepare_solver_args(parameters) - raw_result = self._run_solver(objective_function, **solver_args) - - self._stop_tracking() - - return self._finalize_fit(parameters, raw_result) + try: + solver_args = self._prepare_solver_args(parameters) + if resolved_random_seed is not None: + solver_args['random_seed'] = resolved_random_seed + raw_result = self._run_solver(objective_function, **solver_args) + return self._finalize_fit(parameters, raw_result) + finally: + if finalize_tracking: + self._stop_tracking() def _objective_function( self, diff --git a/src/easydiffraction/analysis/minimizers/bumps.py b/src/easydiffraction/analysis/minimizers/bumps.py index 109262343..f80446cfc 100644 --- a/src/easydiffraction/analysis/minimizers/bumps.py +++ b/src/easydiffraction/analysis/minimizers/bumps.py @@ -8,6 +8,7 @@ from bumps.fitproblem import FitProblem from bumps.fitters import FITTERS from bumps.fitters import FitDriver +from bumps.fitters import monitor as bumps_monitor from bumps.parameter import Parameter as BumpsParameter from scipy.optimize import OptimizeResult @@ -20,38 +21,74 @@ DEFAULT_MAX_ITERATIONS = 1000 +class _BumpsEvaluationLimitError(RuntimeError): + """Raised when the BUMPS residual-evaluation budget is exhausted.""" + + def __init__( + self, + *, + evaluation_count: int, + parameter_values: np.ndarray, + residuals: np.ndarray | None, + ) -> None: + super().__init__('maximum number of residual evaluations reached') + self.evaluation_count = evaluation_count + self.parameter_values = parameter_values + self.residuals = residuals + + class _EasyDiffractionFitness: - """ - Adaptor wrapping an EasyDiffraction objective into bumps Fitness. - """ + """Wrap an EasyDiffraction objective in the BUMPS fitness API.""" def __init__( self, bumps_params: list[BumpsParameter], objective_function: object, + max_evaluations: int | None = None, ) -> None: self._bumps_params = bumps_params self._objective_function = objective_function + self._max_evaluations = max_evaluations self._numpoints = 0 + self._evaluation_count = 0 + self._count_evaluations = True + self._last_parameter_values: np.ndarray | None = None + self._last_residuals: np.ndarray | None = None def parameters(self) -> dict[str, BumpsParameter]: - """Return bumps parameters as a name-keyed dictionary.""" + """Return BUMPS parameters as a name-keyed dictionary.""" return {p.name: p for p in self._bumps_params} def update(self) -> None: - """Signal that parameters have changed (no-op).""" + """Signal that parameters have changed.""" def residuals(self) -> np.ndarray: - """Compute residuals using current bumps parameter values.""" + """Compute residuals for the current BUMPS parameter values.""" + if ( + self._count_evaluations + and self._max_evaluations is not None + and self._evaluation_count >= self._max_evaluations + ): + last_parameter_values = self._last_parameter_values + if last_parameter_values is None: + last_parameter_values = np.array([p.value for p in self._bumps_params]) + raise _BumpsEvaluationLimitError( + evaluation_count=self._evaluation_count, + parameter_values=last_parameter_values, + residuals=self._last_residuals, + ) + values = np.array([p.value for p in self._bumps_params]) - r = self._objective_function(values) + r = np.asarray(self._objective_function(values), dtype=float) self._numpoints = len(r) + self._last_parameter_values = values.copy() + self._last_residuals = r.copy() + if self._count_evaluations: + self._evaluation_count += 1 return r def nllf(self) -> float: - """ - Negative log-likelihood as half the sum of squared residuals. - """ + """Return half the sum of squared residuals.""" r = self.residuals() return 0.5 * np.sum(r**2) @@ -59,10 +96,101 @@ def numpoints(self) -> int: """Return the number of data points.""" return self._numpoints + @property + def evaluation_count(self) -> int: + """Return the live residual-evaluation count.""" + return self._evaluation_count + + def reset_evaluation_count(self) -> None: + """Reset the residual-evaluation counter.""" + self._evaluation_count = 0 + + def stop_counting_evaluations(self) -> None: + """Freeze residual-evaluation counting.""" + self._count_evaluations = False + + @property + def last_residuals(self) -> np.ndarray | None: + """Return the last successful residual vector.""" + return self._last_residuals + + def last_reduced_chi_square(self, *, n_parameters: int) -> float | None: + """Return reduced chi-square for the last residual vector.""" + if self._last_residuals is None: + return None + + chi_square = float(np.sum(self._last_residuals**2)) + dof = len(self._last_residuals) - n_parameters + if dof <= 0: + return chi_square + return chi_square / dof + + +class _BumpsProgressMonitor(bumps_monitor.Monitor): + """Report live BUMPS fit evaluation counts.""" + + def __init__( + self, + *, + tracker: object, + fitness: _EasyDiffractionFitness, + n_points: int, + n_parameters: int, + ) -> None: + self._tracker = tracker + self._fitness = fitness + self._n_points = n_points + self._n_parameters = n_parameters + + @staticmethod + def config_history(history: object) -> None: + """ + Declare the history fields needed for deterministic progress. + """ + history.requires(time=1, step=1, value=1) + + def __call__(self, history: object) -> None: + """Forward deterministic BUMPS progress to the fit tracker.""" + if not history.time or not history.value: + return + + self._tracker.track_fit_progress( + iteration=self._reported_iteration(history), + reduced_chi2=self._reduced_chi_square_from_nllf(float(history.value[0])), + elapsed_time=float(history.time[0]), + ) + + def final(self, history: object, best: dict[str, object]) -> None: + """Record the final BUMPS state in the fit tracker.""" + if not history.time or best.get('value') is None: + return + + self._tracker.track_fit_progress( + iteration=self._reported_iteration(history), + reduced_chi2=self._reduced_chi_square_from_nllf(float(best['value'])), + elapsed_time=float(history.time[0]), + ) + + def _reported_iteration(self, history: object) -> int: + """Return the live fit evaluation count shown in progress.""" + if self._fitness.evaluation_count > 0: + return self._fitness.evaluation_count + + step = int(history.step[0]) if history.step else 0 + return max(1, step) + + def _reduced_chi_square_from_nllf(self, nllf: float) -> float: + """Convert negative log-likelihood to reduced chi-square.""" + dof = self._n_points - self._n_parameters + chi_square = 2.0 * nllf + if dof <= 0: + return chi_square + return chi_square / dof + @MinimizerFactory.register class BumpsMinimizer(MinimizerBase): - """Minimizer using the bumps package.""" + """Minimizer using the BUMPS package.""" type_info = TypeInfo( tag=MinimizerTypeEnum.BUMPS, @@ -81,12 +209,19 @@ def __init__( max_iterations=max_iterations, ) + @staticmethod + def _tracks_progress_via_solver_monitor() -> bool: + """ + Use BUMPS monitor callbacks for live deterministic progress. + """ + return True + def _prepare_solver_args( # noqa: PLR6301 self, parameters: list[object], ) -> dict[str, object]: """ - Prepare bumps parameters from EasyDiffraction parameters. + Prepare BUMPS parameters from EasyDiffraction parameters. Parameters ---------- @@ -116,7 +251,7 @@ def _run_solver( **kwargs: object, ) -> object: """ - Run the bumps solver. + Run the BUMPS solver. Uses FitDriver directly instead of bumps.fitters.fit() to skip the expensive post-fit stderr/Jacobian computation that would @@ -135,43 +270,123 @@ def _run_solver( A scipy OptimizeResult with the optimized values. """ bumps_params = kwargs.get('bumps_params') - fitness = _EasyDiffractionFitness(bumps_params, objective_function) + fitness = _EasyDiffractionFitness( + bumps_params, + objective_function, + max_evaluations=self.max_iterations, + ) fitness.nllf() # pre-compute so numpoints() is valid + fitness.reset_evaluation_count() problem = FitProblem(fitness) + progress_monitor = _BumpsProgressMonitor( + tracker=self.tracker, + fitness=fitness, + n_points=fitness.numpoints(), + n_parameters=len(bumps_params), + ) fitclass = next(cls for cls in FITTERS if cls.id == self.method) driver = FitDriver( fitclass=fitclass, problem=problem, - monitors=[], + monitors=[progress_monitor], steps=self.max_iterations, ) driver.clip() - x, fx = driver.fit() + try: + x, fx = driver.fit() + evaluation_limit_reached = False + evaluation_limit_message = 'successful termination' + except _BumpsEvaluationLimitError as exc: + x, fx, evaluation_limit_message = self._handle_evaluation_limit( + exc=exc, + fitness=fitness, + n_parameters=len(bumps_params), + ) + evaluation_limit_reached = True + finally: + fitness.stop_counting_evaluations() - success = x is not None + success = x is not None and not evaluation_limit_reached if success: problem.setp(x) + return self._build_optimize_result( + bumps_params=bumps_params, + fitness=fitness, + success=success, + evaluation_limit_reached=evaluation_limit_reached, + evaluation_limit_message=evaluation_limit_message, + function_value=fx, + ) + + def _handle_evaluation_limit( + self, + *, + exc: _BumpsEvaluationLimitError, + fitness: _EasyDiffractionFitness, + n_parameters: int, + ) -> tuple[np.ndarray, None, str]: + """ + Build a partial result when the evaluation budget is exhausted. + """ + reduced_chi2 = self._reduced_chi_square_from_limit( + exc=exc, + fitness=fitness, + n_parameters=n_parameters, + ) + if reduced_chi2 is not None: + elapsed_time = self.tracker._current_elapsed_time() + self.tracker.track_fit_progress( + iteration=exc.evaluation_count, + reduced_chi2=reduced_chi2, + elapsed_time=0.0 if elapsed_time is None else elapsed_time, + ) + return exc.parameter_values.copy(), None, str(exc) + + @staticmethod + def _reduced_chi_square_from_limit( + *, + exc: _BumpsEvaluationLimitError, + fitness: _EasyDiffractionFitness, + n_parameters: int, + ) -> float | None: + """Return reduced chi-square at the evaluation cutoff.""" + if exc.residuals is not None: + chi_square = float(np.sum(exc.residuals**2)) + dof = len(exc.residuals) - n_parameters + return chi_square if dof <= 0 else chi_square / dof + if fitness.last_residuals is None: + return None + return fitness.last_reduced_chi_square(n_parameters=n_parameters) + + def _build_optimize_result( + self, + *, + bumps_params: list[BumpsParameter], + fitness: _EasyDiffractionFitness, + success: bool, + evaluation_limit_reached: bool, + evaluation_limit_message: str, + function_value: float | None, + ) -> OptimizeResult: + """Convert the BUMPS solver outcome into an OptimizeResult.""" # Read values back from bumps Parameters in our original order. # FitProblem sorts parameters alphabetically, so x from # driver.fit() uses that sorted order — not ours. result_x = np.array([p.value for p in bumps_params]) - - covar, stderr = ( + covariance, stderr = ( self._compute_covariance(bumps_params, fitness) if success else (None, None) ) - var_names = [p.name for p in bumps_params] - return OptimizeResult( x=result_x, dx=stderr, - fun=fx, + fun=function_value, success=success, - status=0 if success else -1, - message='successful termination' if success else 'fit failed', - covar=covar, - var_names=var_names, + status=0 if success else 5 if evaluation_limit_reached else -1, + message='successful termination' if success else evaluation_limit_message, + covar=covariance, + var_names=[p.name for p in bumps_params], ) def _compute_covariance( # noqa: PLR6301 @@ -225,7 +440,7 @@ def _sync_result_to_parameters( # noqa: PLR6301 raw_result: object, ) -> None: """ - Synchronize the result from the solver to the parameters. + Synchronize the solver result back to the parameters. Parameters ---------- @@ -251,7 +466,7 @@ def _sync_result_to_parameters( # noqa: PLR6301 def _check_success(self, raw_result: object) -> bool: # noqa: PLR6301 """ - Determine success from bumps OptimizeResult. + Determine success from a BUMPS OptimizeResult. Parameters ---------- diff --git a/src/easydiffraction/analysis/minimizers/bumps_dream.py b/src/easydiffraction/analysis/minimizers/bumps_dream.py new file mode 100644 index 000000000..225c7ea0d --- /dev/null +++ b/src/easydiffraction/analysis/minimizers/bumps_dream.py @@ -0,0 +1,1025 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Bumps minimizer variant using the DREAM sampler.""" + +from __future__ import annotations + +import multiprocessing +import random +import sys +from dataclasses import dataclass + +import numpy as np +from bumps.fitproblem import FitProblem +from bumps.fitters import FITTERS +from bumps.fitters import FitDriver +from bumps.fitters import monitor as bumps_monitor +from bumps.mapper import MPMapper +from bumps.mapper import can_pickle +from scipy.optimize import OptimizeResult + +from easydiffraction.analysis.fit_helpers.bayesian import BayesianFitResults +from easydiffraction.analysis.fit_helpers.bayesian import PosteriorSamples +from easydiffraction.analysis.fit_helpers.bayesian import compute_convergence_diagnostics +from easydiffraction.analysis.fit_helpers.bayesian import standard_deviations_from_summaries +from easydiffraction.analysis.fit_helpers.bayesian import summarize_posterior_parameters +from easydiffraction.analysis.fit_helpers.tracking import SamplerProgressUpdate +from easydiffraction.analysis.minimizers.bumps import BumpsMinimizer +from easydiffraction.analysis.minimizers.bumps import _EasyDiffractionFitness +from easydiffraction.analysis.minimizers.enums import DreamPopulationInitializationEnum +from easydiffraction.analysis.minimizers.enums import MinimizerTypeEnum +from easydiffraction.analysis.minimizers.factory import MinimizerFactory +from easydiffraction.core.metadata import TypeInfo +from easydiffraction.utils.logging import log + +_BUMPS_DREAM_LOG = log + +DEFAULT_METHOD = 'dream' +DEFAULT_MAX_ITERATIONS = 3000 +DEFAULT_BURN_FRACTION = 0.2 +DEFAULT_MIN_BURN = 50 +DEFAULT_THIN = 1 +DEFAULT_POP = 4 +DEFAULT_PARALLEL = 0 +DEFAULT_INIT = DreamPopulationInitializationEnum.LHS +DEFAULT_ALPHA = 0.0 +DEFAULT_OUTLIER_TEST = 'none' +DEFAULT_TRIM = False +MAX_RANDOM_SEED = int(np.iinfo(np.uint32).max) +TOTAL_PROGRESS_POINTS = 25 +DREAM_SAMPLE_ARRAY_NDIM = 3 +DREAM_DRIVER_FAILURES = (ArithmeticError, RuntimeError, TypeError, ValueError) + + +@dataclass(slots=True) +class _DreamRunContext: + """Prepared driver state and metadata for one DREAM run.""" + + driver: FitDriver + parameter_names: list[str] + parameter_display_names: list[str] + parameter_uids: list[str] + sampler_settings: dict[str, object] + starting_values: np.ndarray + starting_uncertainties: list[float | None] + + +@dataclass(slots=True) +class _DreamDriverResult: + """ + Raw driver outcome captured before EasyDiffraction normalization. + """ + + best_values: object | None + best_nllf: float | None + raw_state: object | None + error: Exception | None = None + + +class _DreamProgressMonitor(bumps_monitor.Monitor): + """ + Progress monitor translating DREAM updates into chi-square rows. + """ + + def __init__( + self, + *, + tracker: object, + n_points: int, + n_parameters: int, + total_generations: int, + burn_steps: int, + ) -> None: + self._tracker = tracker + self._n_points = n_points + self._n_parameters = n_parameters + self._total_generations = max(1, total_generations) + self._burn_steps = max(0, burn_steps) + burn_target_count, sampling_target_count = self._phase_progress_point_counts( + total_generations=self._total_generations, + burn_steps=self._burn_steps, + ) + self._burn_targets = self._progress_targets( + start=1, + stop=self._burn_steps, + target_count=burn_target_count, + ) + self._sampling_targets = self._progress_targets( + start=self._burn_steps + 1, + stop=self._total_generations, + target_count=sampling_target_count, + ) + self._next_burn_target_index = 0 + self._next_sampling_target_index = 0 + + @staticmethod + def config_history(history: object) -> None: + """Declare the history fields needed for progress updates.""" + history.requires(time=1, step=1, value=1, population_values=1) + + def __call__(self, history: object) -> None: + """Forward sampler progress to the shared fit tracker.""" + step = int(history.step[0]) if history.step else 0 + generation = max(1, step) + if not self._should_report(generation): + return + nllf = float(history.value[0]) + reduced_chi2 = self._reduced_chi_square_from_nllf(nllf) + log_posterior = self._population_mean_log_posterior(history) + self._tracker.track_sampler_progress( + SamplerProgressUpdate( + iteration=generation, + total_iterations=self._total_generations, + phase=self._phase_name(generation), + progress_percent=self._progress_percent(generation), + log_posterior=log_posterior, + reduced_chi2=reduced_chi2, + elapsed_time=float(history.time[0]), + force_report=True, + ) + ) + + def final(self, history: object, best: dict[str, object]) -> None: + """Record the final DREAM state in the shared fit tracker.""" + if not history.time or best.get('value') is None: + return + step = int(history.step[0]) if history.step else 0 + generation = max(1, step) + best_nllf = float(best['value']) + reduced_chi2 = self._reduced_chi_square_from_nllf(best_nllf) + self._tracker.track_sampler_progress( + SamplerProgressUpdate( + iteration=generation, + total_iterations=self._total_generations, + phase=self._phase_name(generation), + progress_percent=self._progress_percent(generation), + log_posterior=self._population_mean_log_posterior(history), + reduced_chi2=reduced_chi2, + elapsed_time=float(history.time[0]), + force_report=True, + ) + ) + + @staticmethod + def _progress_targets( + *, + start: int, + stop: int, + target_count: int, + ) -> list[int]: + """ + Return monotonically increasing reporting targets for one phase. + """ + if target_count < 1 or stop < start: + return [] + + targets = np.linspace(start, stop, num=target_count) + rounded = np.rint(targets).astype(int) + unique_targets = sorted({int(value) for value in rounded if start <= value <= stop}) + if start not in unique_targets: + unique_targets.insert(0, start) + if stop not in unique_targets: + unique_targets.append(stop) + return unique_targets + + @staticmethod + def _phase_progress_point_counts( + *, + total_generations: int, + burn_steps: int, + ) -> tuple[int, int]: + """Return proportional burn and sampling progress counts.""" + total_points = min(TOTAL_PROGRESS_POINTS, max(1, total_generations)) + burn_generations = min(max(0, burn_steps), total_generations) + sampling_generations = max(total_generations - burn_generations, 0) + + if burn_generations == 0: + return 0, total_points + if sampling_generations == 0: + return total_points, 0 + + burn_target_count = round(total_points * burn_generations / total_generations) + burn_target_count = min( + max(burn_target_count, 1), + burn_generations, + total_points - 1, + ) + sampling_target_count = min( + max(total_points - burn_target_count, 1), + sampling_generations, + ) + return burn_target_count, sampling_target_count + + def _should_report(self, generation: int) -> bool: + """Return whether the current generation should be rendered.""" + clamped_generation = min(max(1, generation), self._total_generations) + if self._phase_name(clamped_generation) == 'burn-in': + return self._consume_progress_target( + clamped_generation, + phase_targets=self._burn_targets, + target_index_name='_next_burn_target_index', + ) + + return self._consume_progress_target( + clamped_generation, + phase_targets=self._sampling_targets, + target_index_name='_next_sampling_target_index', + ) + + def _consume_progress_target( + self, + generation: int, + *, + phase_targets: list[int], + target_index_name: str, + ) -> bool: + """ + Advance a phase target pointer when the generation reaches it. + """ + target_index = getattr(self, target_index_name) + should_report = False + while target_index < len(phase_targets) and generation >= phase_targets[target_index]: + target_index += 1 + should_report = True + setattr(self, target_index_name, target_index) + return should_report + + def _phase_name(self, generation: int) -> str: + """Return the current sampler phase name.""" + clamped_generation = min(generation, self._total_generations) + if clamped_generation <= self._burn_steps: + return 'burn-in' + return 'sampling' + + def _progress_percent(self, generation: int) -> float: + """Return DREAM progress as a percentage.""" + clamped_generation = min(generation, self._total_generations) + return 100.0 * clamped_generation / self._total_generations + + @staticmethod + def _population_mean_log_posterior(history: object) -> float: + """Return the mean log-posterior across the population.""" + population_values = history.population_values[0] if history.population_values else None + if population_values is None: + return -float(history.value[0]) + + nllf_values = np.asarray(population_values, dtype=float) + finite_mask = np.isfinite(nllf_values) + if not np.any(finite_mask): + return -float(history.value[0]) + return float(np.mean(-nllf_values[finite_mask])) + + def _reduced_chi_square_from_nllf(self, nllf: float) -> float: + """ + Convert DREAM's negative log-likelihood to reduced chi-square. + """ + dof = self._n_points - self._n_parameters + chi_square = 2.0 * nllf + if dof <= 0: + return chi_square + return chi_square / dof + + +@MinimizerFactory.register +class BumpsDreamMinimizer(BumpsMinimizer): + """Bumps minimizer using the DREAM Bayesian sampler.""" + + type_info = TypeInfo( + tag=MinimizerTypeEnum.BUMPS_DREAM, + description='Bumps library with DREAM Bayesian sampling', + ) + + def __init__( + self, + name: str = MinimizerTypeEnum.BUMPS_DREAM, + method: str = DEFAULT_METHOD, + max_iterations: int = DEFAULT_MAX_ITERATIONS, + ) -> None: + super().__init__( + name=name, + method=method, + max_iterations=max_iterations, + ) + self._burn: int | None = None + self._thin: int = DEFAULT_THIN + self._pop: int = DEFAULT_POP + self._parallel: int = DEFAULT_PARALLEL + self._init: DreamPopulationInitializationEnum = DEFAULT_INIT + + @property + def max_iterations(self) -> int: + """DREAM exposes sampler length through ``steps`` instead.""" + sampler_name = self.type_info.description.partition('with ')[2].split()[0] + msg = f"{sampler_name} sampler uses 'steps' instead of 'max_iterations'." + raise AttributeError(msg) + + @max_iterations.setter + def max_iterations(self, value: int) -> None: + del value + sampler_name = self.type_info.description.partition('with ')[2].split()[0] + msg = f"{sampler_name} sampler uses 'steps' instead of 'max_iterations'." + raise AttributeError(msg) + + @property + def steps(self) -> int: + """Number of DREAM generations retained after burn-in.""" + return self._validated_positive_integer('steps', self._max_iterations) + + @steps.setter + def steps(self, value: int) -> None: + self._max_iterations = self._validated_positive_integer('steps', value) + + @property + def burn(self) -> int | None: + """Explicit DREAM burn-in generations or ``None`` for auto.""" + return self._burn + + @burn.setter + def burn(self, value: int | None) -> None: + if value is None: + self._burn = None + return + self._burn = self._validated_non_negative_integer('burn', value) + + @property + def thin(self) -> int: + """DREAM thinning interval.""" + return self._thin + + @thin.setter + def thin(self, value: int) -> None: + self._thin = self._validated_positive_integer('thin', value) + + @property + def pop(self) -> int: + """DREAM population multiplier.""" + return self._pop + + @pop.setter + def pop(self, value: int) -> None: + self._pop = self._validated_positive_integer('pop', value) + + @property + def parallel(self) -> int: + """DREAM parallel worker count; ``0`` uses all CPUs.""" + return self._parallel + + @parallel.setter + def parallel(self, value: int) -> None: + self._parallel = self._validated_non_negative_integer('parallel', value) + + @property + def init(self) -> DreamPopulationInitializationEnum: + """DREAM population initializer.""" + return self._init + + @init.setter + def init(self, value: DreamPopulationInitializationEnum | str) -> None: + self._init = self._validated_init(value) + + def _resolve_random_seed(self, random_seed: int | None) -> int: + """ + Return a user-provided or generated random seed. + + Parameters + ---------- + random_seed : int | None + User-provided random seed. + + Returns + ------- + int + Seed to use for the DREAM run. + """ + if random_seed is None: + generator = np.random.default_rng() + random_seed = int(generator.integers(0, np.iinfo(np.int32).max)) + + integer_seed = self._validated_random_seed_value(random_seed) + + self._resolved_random_seed = integer_seed + return self._resolved_random_seed + + @staticmethod + def _validated_random_seed_value(random_seed: object) -> int: + """Validate and normalize a DREAM random seed.""" + if isinstance(random_seed, bool): + msg = f'DREAM random_seed must be an integer between 0 and {MAX_RANDOM_SEED}.' + raise TypeError(msg) + + integer_seed = int(random_seed) + if integer_seed != random_seed or integer_seed < 0 or integer_seed > MAX_RANDOM_SEED: + msg = f'DREAM random_seed must be an integer between 0 and {MAX_RANDOM_SEED}.' + raise ValueError(msg) + return integer_seed + + @staticmethod + def _tracking_mode() -> str: + """Use sampler-style progress reporting for DREAM runs.""" + return 'sampling' + + def _prepare_solver_args( + self, + parameters: list[object], + ) -> dict[str, object]: + """ + Prepare DREAM solver arguments in EasyDiffraction order. + + Parameters + ---------- + parameters : list[object] + List of parameters to be sampled. + + Returns + ------- + dict[str, object] + BUMPS parameters plus EasyDiffraction parameter metadata. + """ + self._validate_sampled_parameter_bounds(parameters) + solver_args = super()._prepare_solver_args(parameters) + solver_args['parameter_names'] = [parameter.unique_name for parameter in parameters] + solver_args['parameter_display_names'] = [ + getattr(parameter, 'name', parameter.unique_name) for parameter in parameters + ] + solver_args['parameter_uids'] = [parameter._minimizer_uid for parameter in parameters] + solver_args['starting_uncertainties'] = [parameter.uncertainty for parameter in parameters] + return solver_args + + @classmethod + def _validate_sampled_parameter_bounds( + cls, + parameters: list[object], + ) -> None: + """ + Validate finite ordered bounds for sampled DREAM parameters. + """ + issues: list[str] = [] + for parameter in parameters: + parameter_name = cls._parameter_name_for_bound_validation(parameter) + parameter_issues = cls._parameter_bound_issues(parameter) + if parameter_issues: + issues.append(f'- {parameter_name}: {"; ".join(parameter_issues)}') + + if not issues: + return + + message = 'DREAM requires finite valid bounds for every sampled parameter:\n' + '\n'.join( + issues + ) + raise ValueError(message) + + @staticmethod + def _parameter_name_for_bound_validation(parameter: object) -> str: + """Return the user-facing name for DREAM bound validation.""" + unique_name = getattr(parameter, 'unique_name', None) + if unique_name: + return str(unique_name) + + parameter_name = getattr(parameter, 'name', None) + if parameter_name: + return str(parameter_name) + return '' + + @classmethod + def _parameter_bound_issues( + cls, + parameter: object, + ) -> list[str]: + """Return bound-validation issues for one sampled parameter.""" + lower_bound = getattr(parameter, 'fit_min', None) + upper_bound = getattr(parameter, 'fit_max', None) + value = getattr(parameter, 'value', None) + issues: list[str] = [] + + lower_is_finite = cls._is_finite_bound_value(lower_bound) + upper_is_finite = cls._is_finite_bound_value(upper_bound) + value_is_finite = cls._is_finite_bound_value(value) + + if not lower_is_finite: + issues.append(f'fit_min must be finite (got {lower_bound!r})') + if not upper_is_finite: + issues.append(f'fit_max must be finite (got {upper_bound!r})') + + bounds_are_ordered = lower_is_finite and upper_is_finite and lower_bound < upper_bound + if lower_is_finite and upper_is_finite and not bounds_are_ordered: + issues.append(f'fit_min ({lower_bound}) must be smaller than fit_max ({upper_bound})') + + if not value_is_finite: + issues.append(f'starting value must be finite (got {value!r})') + elif bounds_are_ordered and not lower_bound <= value <= upper_bound: + issues.append(f'starting value {value} is outside [{lower_bound}, {upper_bound}]') + + return issues + + @staticmethod + def _is_finite_bound_value(value: object) -> bool: + """Return whether a bound-validation value is finite.""" + try: + return bool(np.isfinite(value)) + except TypeError: + return False + + @staticmethod + def _validated_positive_integer(name: str, value: float) -> int: + """Validate a DREAM setting that must be a positive integer.""" + if isinstance(value, bool): + msg = f"DREAM setting '{name}' must be a positive integer." + raise TypeError(msg) + + integer_value = int(value) + if integer_value != value or integer_value < 1: + msg = f"DREAM setting '{name}' must be a positive integer." + raise ValueError(msg) + return integer_value + + @staticmethod + def _validated_non_negative_integer(name: str, value: float) -> int: + """ + Validate a DREAM setting that must be a non-negative integer. + """ + if isinstance(value, bool): + msg = f"DREAM setting '{name}' must be a non-negative integer." + raise TypeError(msg) + + integer_value = int(value) + if integer_value != value or integer_value < 0: + msg = f"DREAM setting '{name}' must be a non-negative integer." + raise ValueError(msg) + return integer_value + + @staticmethod + def _validated_init( + value: DreamPopulationInitializationEnum | str, + ) -> DreamPopulationInitializationEnum: + """Validate a DREAM population initializer.""" + try: + return DreamPopulationInitializationEnum(value) + except ValueError: + valid_values = ', '.join( + initialization.value for initialization in DreamPopulationInitializationEnum + ) + msg = f"DREAM setting 'init' must be one of: {valid_values}." + raise ValueError(msg) from None + + def _resolved_burn(self, steps: int) -> int: + """Return the configured or automatic DREAM burn-in length.""" + if self.burn is None: + proposed_burn = max(DEFAULT_MIN_BURN, int(steps * DEFAULT_BURN_FRACTION)) + return min(proposed_burn, max(steps - 1, 0)) + + burn = self.burn + if burn >= steps: + msg = "DREAM setting 'burn' must be smaller than 'steps'." + raise ValueError(msg) + return burn + + def _sampler_settings( + self, + *, + random_seed: int, + steps: int, + burn: int, + n_parameters: int, + ) -> dict[str, object]: + """Build the sampler settings dictionary recorded in results.""" + samples = steps * self.pop * n_parameters + return { + 'random_seed': int(random_seed), + 'steps': int(steps), + 'burn': int(burn), + 'thin': int(self.thin), + 'pop': int(self.pop), + 'parallel': int(self.parallel), + 'init': self.init.value, + 'samples': int(samples), + 'alpha': float(DEFAULT_ALPHA), + 'outliers': DEFAULT_OUTLIER_TEST, + 'trim': DEFAULT_TRIM, + } + + def _run_solver( + self, + objective_function: object, + **kwargs: object, + ) -> object: + """ + Run the DREAM sampler and normalize its posterior outputs. + + Parameters + ---------- + objective_function : object + Objective function returning residuals. + **kwargs : object + Solver arguments including BUMPS parameters and random seed. + + Returns + ------- + object + Normalized DREAM result stored in an ``OptimizeResult``. + """ + total_iterations = int(self.steps + self._resolved_burn(self.steps) + 1) + self.tracker.start_sampler_pre_processing(total_iterations=total_iterations) + context = self._prepare_run_context(objective_function=objective_function, kwargs=kwargs) + driver_result = self._execute_driver( + driver=context.driver, + random_seed=int(context.sampler_settings['random_seed']), + ) + if driver_result.error is not None: + return self._failure_result( + context=context, + message=f'DREAM sampling failed: {driver_result.error}', + raw_state=driver_result.raw_state, + sampler_completed=False, + ) + if driver_result.best_values is None or driver_result.raw_state is None: + return self._failure_result( + context=context, + message='DREAM sampling did not produce usable posterior samples.', + raw_state=driver_result.raw_state, + sampler_completed=False, + ) + + self.tracker.start_sampler_post_processing() + + return self._build_success_result( + context=context, + raw_state=driver_result.raw_state, + best_nllf=driver_result.best_nllf, + ) + + def _prepare_run_context( + self, + *, + objective_function: object, + kwargs: dict[str, object], + ) -> _DreamRunContext: + """Prepare a driver and metadata for one DREAM solver run.""" + bumps_params = kwargs.get('bumps_params') + parameter_names = kwargs.get('parameter_names') + parameter_display_names = kwargs.get('parameter_display_names') + parameter_uids = kwargs.get('parameter_uids') + random_seed = int(kwargs.get('random_seed')) + starting_uncertainties = kwargs.get('starting_uncertainties') + + fitness = _EasyDiffractionFitness(bumps_params, objective_function) + fitness.nllf() + fitclass = next(cls for cls in FITTERS if cls.id == self.method) + steps = self.steps + burn = self._resolved_burn(steps) + init = self.init + sampler_settings = self._sampler_settings( + random_seed=random_seed, + steps=steps, + burn=burn, + n_parameters=len(bumps_params), + ) + driver = self._build_driver( + fitclass=fitclass, + fitness=fitness, + steps=steps, + burn=burn, + init=init, + sampler_settings=sampler_settings, + n_parameters=len(bumps_params), + ) + starting_values = np.array([parameter.value for parameter in bumps_params], dtype=float) + resolved_uncertainties = ( + list(starting_uncertainties) + if starting_uncertainties is not None + else [None] * len(bumps_params) + ) + return _DreamRunContext( + driver=driver, + parameter_names=parameter_names, + parameter_display_names=parameter_display_names, + parameter_uids=parameter_uids, + sampler_settings=sampler_settings, + starting_values=starting_values, + starting_uncertainties=resolved_uncertainties, + ) + + def _build_driver( + self, + *, + fitclass: object, + fitness: object, + steps: int, + burn: int, + init: DreamPopulationInitializationEnum, + sampler_settings: dict[str, object], + n_parameters: int, + ) -> FitDriver: + """Build and clip the BUMPS DREAM driver.""" + total_generations = int(steps + burn + 1) + problem = FitProblem(fitness) + progress_monitor = _DreamProgressMonitor( + tracker=self.tracker, + n_points=fitness.numpoints(), + n_parameters=n_parameters, + total_generations=total_generations, + burn_steps=int(burn), + ) + mapper = self._build_mapper(problem) + try: + driver = FitDriver( + fitclass=fitclass, + problem=problem, + monitors=[progress_monitor], + mapper=mapper, + steps=steps, + burn=burn, + thin=self.thin, + pop=self.pop, + init=init.value, + samples=sampler_settings['samples'], + alpha=DEFAULT_ALPHA, + outliers=DEFAULT_OUTLIER_TEST, + trim=DEFAULT_TRIM, + ) + driver.clip() + except Exception: + MPMapper.stop_mapper() + raise + else: + return driver + + def _build_mapper(self, problem: FitProblem) -> object | None: + """Return a DREAM mapper for the configured parallel setting.""" + if self.parallel == 1: + return None + + if self._requires_serial_mapper_for_spawn_main_module(): + self._warn_after_tracking( + 'DREAM parallel evaluation requires an import-safe main ' + 'module on spawn-based multiprocessing; falling back to ' + 'serial execution.' + ) + return None + + shared_display_handle = getattr(self.tracker, '_shared_display_handle', None) + activity_indicator = getattr(self.tracker, '_activity_indicator', None) + if shared_display_handle is not None: + self.tracker._set_shared_display_handle(None) + if activity_indicator is not None: + self.tracker._activity_indicator = None + + try: + if not can_pickle(problem): + self._warn_after_tracking( + 'DREAM parallel evaluation requires a picklable ' + 'problem; falling back to serial execution.' + ) + return None + + return MPMapper.start_mapper(problem, [], cpus=self.parallel) + except RuntimeError as error: + message = str(error) + if 'bootstrapping phase' not in message: + raise + self._warn_after_tracking( + 'DREAM parallel evaluation requires an import-safe main ' + 'module on spawn-based multiprocessing; falling back to ' + 'serial execution.' + ) + return None + finally: + if activity_indicator is not None: + self.tracker._activity_indicator = activity_indicator + if shared_display_handle is not None: + self.tracker._set_shared_display_handle(shared_display_handle) + + @staticmethod + def _requires_serial_mapper_for_spawn_main_module() -> bool: + """ + Return whether direct-script spawn startup should stay serial. + """ + start_method = multiprocessing.get_start_method(allow_none=True) + if start_method is None: + start_method = multiprocessing.get_start_method() + if start_method != 'spawn': + return False + + main_module = sys.modules.get('__main__') + if main_module is None: + return False + + return ( + getattr(main_module, '__file__', None) is not None + and getattr(main_module, '__spec__', None) is None + ) + + @staticmethod + def _execute_driver(*, driver: FitDriver, random_seed: int) -> _DreamDriverResult: + """ + Run the DREAM driver under a deterministic RNG-state guard. + """ + numpy_rng = np.random.mtrand._rand + numpy_state = numpy_rng.get_state() + python_state = random.getstate() + try: + validated_seed = BumpsDreamMinimizer._validated_random_seed_value(random_seed) + numpy_rng.seed(validated_seed) + random.seed(validated_seed) + best_values, best_nllf = driver.fit() + except DREAM_DRIVER_FAILURES as error: # pragma: no cover - backend-specific + return _DreamDriverResult( + best_values=None, + best_nllf=None, + raw_state=getattr(driver.fitter, 'state', None), + error=error, + ) + finally: + MPMapper.stop_mapper() + numpy_rng.set_state(numpy_state) + random.setstate(python_state) + + return _DreamDriverResult( + best_values=best_values, + best_nllf=float(best_nllf), + raw_state=getattr(driver.fitter, 'state', None), + ) + + @staticmethod + def _failure_result( + *, + context: _DreamRunContext, + message: str, + raw_state: object, + sampler_completed: bool, + ) -> OptimizeResult: + """ + Build a normalized failure result for an incomplete DREAM run. + """ + return OptimizeResult( + x=context.starting_values, + dx=None, + fun=None, + success=False, + status=-1, + message=message, + var_names=context.parameter_names, + posterior_samples=None, + posterior_parameter_summaries=[], + convergence_diagnostics={}, + sampler_settings=context.sampler_settings, + sampler_completed=sampler_completed, + raw_state=raw_state, + best_log_posterior=None, + starting_values=context.starting_values, + starting_uncertainties=context.starting_uncertainties, + ) + + def _build_success_result( + self, + *, + context: _DreamRunContext, + raw_state: object, + best_nllf: float | None, + ) -> OptimizeResult: + """Normalize a completed DREAM run into an OptimizeResult.""" + draw_index, parameter_samples_array, log_posterior = raw_state.chains() + if ( + parameter_samples_array.ndim != DREAM_SAMPLE_ARRAY_NDIM + or parameter_samples_array.size == 0 + ): + return self._failure_result( + context=context, + message='DREAM sampling did not return a usable posterior sample array.', + raw_state=raw_state, + sampler_completed=True, + ) + + state_best_values, best_log_posterior = raw_state.best() + best_by_name = dict(zip(raw_state.labels, state_best_values, strict=True)) + label_to_index = {label: index for index, label in enumerate(raw_state.labels)} + ordered_indices = [label_to_index[uid] for uid in context.parameter_uids] + ordered_samples = np.asarray(parameter_samples_array, dtype=float)[:, :, ordered_indices] + best_sample_values = np.array( + [best_by_name[uid] for uid in context.parameter_uids], + dtype=float, + ) + posterior_samples = PosteriorSamples( + parameter_names=context.parameter_names, + parameter_samples=ordered_samples, + log_posterior=np.asarray(log_posterior, dtype=float), + draw_index=np.asarray(draw_index, dtype=float), + ) + convergence_diagnostics = compute_convergence_diagnostics(posterior_samples) + if not convergence_diagnostics.get('converged', True): + self._warn_after_tracking( + 'Convergence diagnostics indicate the posterior may be poorly mixed.' + ) + posterior_parameter_summaries = summarize_posterior_parameters( + parameter_names=context.parameter_names, + posterior_samples=posterior_samples, + best_sample_values=best_sample_values, + parameter_display_names=context.parameter_display_names, + convergence_diagnostics=convergence_diagnostics, + ) + posterior_standard_deviations = standard_deviations_from_summaries( + posterior_parameter_summaries + ) + + return OptimizeResult( + x=best_sample_values, + dx=posterior_standard_deviations, + fun=float(best_nllf), + success=True, + status=0, + message='DREAM sampling completed', + var_names=context.parameter_names, + posterior_samples=posterior_samples, + posterior_parameter_summaries=posterior_parameter_summaries, + convergence_diagnostics=convergence_diagnostics, + sampler_settings=context.sampler_settings, + sampler_completed=True, + raw_state=raw_state, + best_log_posterior=float(best_log_posterior), + starting_values=context.starting_values, + starting_uncertainties=context.starting_uncertainties, + ) + + @staticmethod + def _sync_result_to_parameters( + parameters: list[object], + raw_result: object, + ) -> None: + """ + Sync best posterior values or restore starts. + + Parameters + ---------- + parameters : list[object] + Parameters being optimized. + raw_result : object + DREAM result object. + """ + if hasattr(raw_result, 'x'): + if getattr(raw_result, 'success', False): + values = raw_result.x + uncertainties = getattr(raw_result, 'dx', None) + else: + values = getattr(raw_result, 'starting_values', raw_result.x) + uncertainties = getattr(raw_result, 'starting_uncertainties', None) + else: + values = raw_result + uncertainties = None + + if values is None: + return + + for index, parameter in enumerate(parameters): + parameter._set_value_from_minimizer(float(values[index])) + if uncertainties is None: + parameter.uncertainty = None + continue + + uncertainty = uncertainties[index] + parameter.uncertainty = None if uncertainty is None else float(uncertainty) + + def _build_fit_results( + self, + *, + parameters: list[object], + raw_result: object, + success: bool, + ) -> BayesianFitResults: + """ + Build the Bayesian fit result container. + + Parameters + ---------- + parameters : list[object] + Parameters after the solver finished. + raw_result : object + Normalized DREAM solver output. + success : bool + Whether DREAM produced usable posterior samples. + + Returns + ------- + BayesianFitResults + Bayesian result object for the finished run. + """ + fit_results = BayesianFitResults( + success=success, + parameters=parameters, + reduced_chi_square=self.tracker.best_chi2, + engine_result=getattr(raw_result, 'raw_state', raw_result), + starting_parameters=parameters, + fitting_time=self.tracker.fitting_time, + sampler_name='dream', + point_estimate_name='best_sample', + posterior_samples=getattr(raw_result, 'posterior_samples', None), + posterior_parameter_summaries=getattr(raw_result, 'posterior_parameter_summaries', []), + posterior_predictive={}, + credible_interval_levels=(0.68, 0.95), + sampler_settings=getattr(raw_result, 'sampler_settings', {}), + convergence_diagnostics=getattr(raw_result, 'convergence_diagnostics', {}), + sampler_completed=getattr(raw_result, 'sampler_completed', False), + best_log_posterior=getattr(raw_result, 'best_log_posterior', None), + ) + fit_results.message = getattr(raw_result, 'message', '') + fit_results.iterations = int(fit_results.sampler_settings.get('steps', self.steps)) + fit_results.result = raw_result + return fit_results diff --git a/src/easydiffraction/analysis/minimizers/enums.py b/src/easydiffraction/analysis/minimizers/enums.py index cdfe4ecb8..0d5bb086d 100644 --- a/src/easydiffraction/analysis/minimizers/enums.py +++ b/src/easydiffraction/analysis/minimizers/enums.py @@ -16,6 +16,7 @@ class MinimizerTypeEnum(StrEnum): DFOLS = 'dfols' BUMPS = 'bumps' BUMPS_LM = 'bumps (lm)' + BUMPS_DREAM = 'bumps (dream)' BUMPS_AMOEBA = 'bumps (amoeba)' BUMPS_DE = 'bumps (de)' @@ -45,7 +46,17 @@ def description(self) -> str: 'BUMPS library using the default Levenberg-Marquardt method' ), MinimizerTypeEnum.BUMPS_LM: ('BUMPS library with Levenberg-Marquardt method'), + MinimizerTypeEnum.BUMPS_DREAM: ('BUMPS library with DREAM Bayesian sampling'), MinimizerTypeEnum.BUMPS_AMOEBA: ('BUMPS library with Nelder-Mead simplex method'), MinimizerTypeEnum.BUMPS_DE: ('BUMPS library with differential evolution method'), } return descriptions.get(self, '') + + +class DreamPopulationInitializationEnum(StrEnum): + """Supported DREAM population initializers.""" + + EPS = 'eps' + COV = 'cov' + LHS = 'lhs' + RANDOM = 'random' diff --git a/src/easydiffraction/analysis/sequential.py b/src/easydiffraction/analysis/sequential.py index 220179f89..a43b537a3 100644 --- a/src/easydiffraction/analysis/sequential.py +++ b/src/easydiffraction/analysis/sequential.py @@ -9,27 +9,39 @@ import contextlib import csv import multiprocessing as mp +import os +import re import sys +import time from concurrent.futures import ProcessPoolExecutor from dataclasses import dataclass from dataclasses import replace from pathlib import Path -from typing import TYPE_CHECKING from typing import Any +from easydiffraction.display.progress import ACTIVITY_LABEL_FITTING +from easydiffraction.display.progress import ActivityIndicator from easydiffraction.io.ascii import extract_data_paths_from_dir from easydiffraction.utils.enums import VerbosityEnum from easydiffraction.utils.logging import console from easydiffraction.utils.logging import log - -if TYPE_CHECKING: - from collections.abc import Callable +from easydiffraction.utils.utils import build_table_renderable # ------------------------------------------------------------------ # Template dataclass (picklable for ProcessPoolExecutor) # ------------------------------------------------------------------ +@dataclass(frozen=True) +class SequentialFitExtractRule: + """Picklable sequential-fit extract rule for worker execution.""" + + id: str + field_name: str + pattern: str + required: bool + + @dataclass(frozen=True) class SequentialFitTemplate: """ @@ -48,6 +60,7 @@ class SequentialFitTemplate: constraints_enabled: bool minimizer_tag: str calculator_tag: str + diffrn_extract_rules: list[SequentialFitExtractRule] diffrn_field_names: list[str] @@ -77,9 +90,9 @@ def _fit_worker( Returns ------- dict[str, Any] - Result dict with keys: ``file_path``, ``fit_success``, - ``chi_squared``, ``reduced_chi_squared``, ``n_iterations``, and - per-parameter ``{unique_name}`` / ``{unique_name}.uncertainty``. + Result dict with keys: ``file_path``, ``success``, + ``reduced_chi_square``, ``iterations``, and per-parameter + ``{unique_name}`` / ``{unique_name}.uncertainty``. """ # Lazy import to avoid circular dependencies and keep the module # importable without heavy imports at top level. @@ -106,13 +119,16 @@ def _fit_worker( # 4. Replace data from the new data path expt._load_ascii_data_to_experiment(data_path) - # 5. Override parameter values from propagated starting values + # 5. Extract diffrn metadata from the data file + result.update(_extract_diffrn_values(expt, data_path, template.diffrn_extract_rules)) + + # 6. Override parameter values from propagated starting values _apply_param_overrides(project, template.initial_params) - # 6. Set free flags + # 7. Set free flags _set_free_params(project, template.free_param_unique_names) - # 7. Apply constraints + # 8. Apply constraints if template.constraints_enabled and template.alias_defs: _apply_constraints( project, @@ -120,17 +136,22 @@ def _fit_worker( template.constraint_defs, ) - # 8. Set calculator and minimizer + # 9. Set calculator and minimizer # (internal, no console output) from easydiffraction.analysis.fitting import Fitter # noqa: PLC0415 expt._set_calculator_type(template.calculator_tag, announce=False) project.analysis.fitter = Fitter(template.minimizer_tag) - # 9. Fit - project.analysis.fit(verbosity='silent') + # 10. Fit + original_verbosity = project.verbosity.fit.value + project.verbosity.fit = 'silent' + try: + project.analysis.fit() + finally: + project.verbosity.fit = original_verbosity - # 10. Collect results + # 11. Collect results result.update(_collect_results(project, template)) except ( @@ -142,10 +163,9 @@ def _fit_worker( IndexError, OSError, ) as exc: - result['fit_success'] = False - result['chi_squared'] = None - result['reduced_chi_squared'] = None - result['n_iterations'] = 0 + result['success'] = False + result['reduced_chi_square'] = None + result['iterations'] = 0 result['error'] = str(exc) return result @@ -232,6 +252,82 @@ def _apply_constraints( project.analysis.constraints.create(expression=expr) +def _extract_diffrn_values( + experiment: object, + data_path: str, + extract_rules: list[SequentialFitExtractRule], +) -> dict[str, float]: + """ + Extract diffrn metadata from a single data file. + + Parameters + ---------- + experiment : object + The worker experiment whose diffrn descriptors are updated. + data_path : str + Path to the data file being fitted. + extract_rules : list[SequentialFitExtractRule] + Persisted extract rules resolved from analysis settings. + + Returns + ------- + dict[str, float] + Extracted ``diffrn.`` values for the CSV row. + + Raises + ------ + ValueError + If a required rule does not match or captures a non-numeric + value. + """ + if not extract_rules: + return {} + + compiled_rules = [(rule, re.compile(rule.pattern)) for rule in extract_rules] + matched_rule_ids: set[str] = set() + extracted_values: dict[str, float] = {} + + with Path(data_path).open(encoding='utf-8', errors='ignore') as handle: + for line in handle: + for rule, pattern in compiled_rules: + if rule.id in matched_rule_ids: + continue + + match = pattern.search(line) + if match is None: + continue + + try: + extracted_value = float(match.group(1)) + except (TypeError, ValueError) as error: + msg = ( + f"Sequential extract rule '{rule.id}' captured a non-numeric value " + f"for 'diffrn.{rule.field_name}' in {data_path!r}." + ) + raise ValueError(msg) from error + + descriptor = getattr(experiment.diffrn, rule.field_name) + descriptor.value = extracted_value + extracted_values[f'diffrn.{rule.field_name}'] = extracted_value + matched_rule_ids.add(rule.id) + + if len(matched_rule_ids) == len(extract_rules): + break + + missing_required = [ + f'{rule.id} (diffrn.{rule.field_name})' + for rule in extract_rules + if rule.required and rule.id not in matched_rule_ids + ] + if missing_required: + msg = ( + f'Sequential extract rules did not match {data_path!r}: {", ".join(missing_required)}.' + ) + raise ValueError(msg) + + return extracted_values + + def _collect_results( project: object, template: SequentialFitTemplate, @@ -255,17 +351,17 @@ def _collect_results( result: dict[str, Any] = {} fit_results = project.analysis.fit_results + tracker = getattr(project.analysis.fitter.minimizer, 'tracker', None) + best_iteration = getattr(tracker, 'best_iteration', None) if fit_results is not None: - result['fit_success'] = fit_results.success - result['chi_squared'] = fit_results.chi_square - result['reduced_chi_squared'] = fit_results.reduced_chi_square - result['n_iterations'] = project.analysis.fitter.minimizer.tracker.best_iteration or 0 + result['fit_result.success'] = fit_results.success + result['fit_result.reduced_chi_square'] = fit_results.reduced_chi_square + result['fit_result.iterations'] = fit_results.iterations or best_iteration or 0 else: - result['fit_success'] = False - result['chi_squared'] = None - result['reduced_chi_squared'] = None - result['n_iterations'] = 0 + result['fit_result.success'] = False + result['fit_result.reduced_chi_square'] = None + result['fit_result.iterations'] = best_iteration or 0 # Collect all free parameter values and uncertainties all_params = project.structures.parameters + project.experiments.parameters @@ -286,10 +382,9 @@ def _collect_results( _META_COLUMNS = [ 'file_path', - 'chi_squared', - 'reduced_chi_squared', - 'fit_success', - 'n_iterations', + 'fit_result.reduced_chi_square', + 'fit_result.success', + 'fit_result.iterations', ] @@ -355,7 +450,47 @@ def _append_to_csv( with csv_path.open('a', newline='', encoding='utf-8') as f: writer = csv.DictWriter(f, fieldnames=header, extrasaction='ignore') for result in results: - writer.writerow(result) + row = dict(result) + file_path = row.get('file_path') + if file_path: + row['file_path'] = _relative_file_path_for_csv(csv_path, str(file_path)) + writer.writerow(row) + + +def _relative_file_path_for_csv( + csv_path: Path, + file_path: str, +) -> str: + """Return *file_path* relative to the CSV-owning project.""" + project_path = csv_path.parent.parent.resolve() + resolved_path = _resolve_project_file_path(project_path, file_path) + relative_path = os.path.relpath(resolved_path, start=project_path) + return relative_path.replace('\\', '/') + + +def _resolve_csv_file_path( + csv_path: Path, + file_path: str, +) -> str: + """Resolve a stored CSV file path against the owning project.""" + project_path = csv_path.parent.parent.resolve() + return str(_resolve_project_file_path(project_path, file_path)) + + +def _resolve_project_file_path( + project_path: Path, + file_path: str, +) -> Path: + """Resolve a data file path to an absolute path near the project.""" + path = Path(file_path) + if path.is_absolute(): + return path.resolve() + + cwd_relative_path = path.resolve() + if cwd_relative_path.is_relative_to(project_path): + return cwd_relative_path + + return (project_path / path).resolve() def _extract_params_from_row(row: dict[str, str]) -> dict[str, float]: @@ -413,8 +548,9 @@ def _read_csv_for_recovery( for row in reader: file_path = row.get('file_path', '') if file_path: - fitted.add(file_path) - if row.get('fit_success', '').lower() == 'true': + fitted.add(_resolve_csv_file_path(csv_path, file_path)) + success_value = row.get('fit_result.success', row.get('success', '')) + if success_value.lower() == 'true': params = _extract_params_from_row(row) if params: last_params = params @@ -441,7 +577,14 @@ def _build_template(project: object) -> SequentialFitTemplate: ------- SequentialFitTemplate A frozen, picklable snapshot. + + Raises + ------ + TypeError + If a sequential extract target does not reference an existing + numeric ``diffrn`` descriptor on the template experiment. """ + from easydiffraction.core.variable import NumericDescriptor # noqa: PLC0415 from easydiffraction.core.variable import Parameter # noqa: PLC0415 structure = next(iter(project.structures.values())) @@ -452,7 +595,7 @@ def _build_template(project: object) -> SequentialFitTemplate: free_names: list[str] = [] initial_params: dict[str, float] = {} for p in all_params: - if isinstance(p, Parameter) and not p.constrained and p.free: + if isinstance(p, Parameter) and not p.user_constrained and p.free: free_names.append(p.unique_name) initial_params[p.unique_name] = p.value @@ -470,12 +613,31 @@ def _build_template(project: object) -> SequentialFitTemplate: constraint.expression.value for constraint in project.analysis.constraints ] - # Collect diffrn field names from the experiment + # Validate and collect sequential diffrn extract rules against the + # template experiment before worker execution starts. + diffrn_extract_rules: list[SequentialFitExtractRule] = [] diffrn_field_names: list[str] = [] - if hasattr(experiment, 'diffrn'): - diffrn_field_names.extend( - p.name for p in experiment.diffrn.parameters if hasattr(p, 'name') and p.name != 'type' + for extract_rule in project.analysis.sequential_fit_extract: + target = extract_rule.target.value + field_name = target.split('.', maxsplit=1)[1] + descriptor = getattr(experiment.diffrn, field_name, None) + if not isinstance(descriptor, NumericDescriptor): + msg = ( + f"Sequential extract target '{target}' must reference an existing numeric " + 'diffrn descriptor on the template experiment.' + ) + raise TypeError(msg) + + diffrn_extract_rules.append( + SequentialFitExtractRule( + id=extract_rule.id.value, + field_name=field_name, + pattern=extract_rule.pattern.value, + required=extract_rule.required.value, + ) ) + if field_name not in diffrn_field_names: + diffrn_field_names.append(field_name) return SequentialFitTemplate( structure_cif=structure.as_cif, @@ -485,8 +647,9 @@ def _build_template(project: object) -> SequentialFitTemplate: alias_defs=alias_defs, constraint_defs=constraint_defs, constraints_enabled=project.analysis.constraints.enabled, - minimizer_tag=project.analysis.fit.minimizer_type.value or 'lmfit', + minimizer_tag=project.analysis.fitting.minimizer_type.value or 'lmfit', calculator_tag=experiment.calculation.calculator_type.value, + diffrn_extract_rules=diffrn_extract_rules, diffrn_field_names=diffrn_field_names, ) @@ -496,11 +659,315 @@ def _build_template(project: object) -> SequentialFitTemplate: # ------------------------------------------------------------------ -def _report_chunk_progress( +_SEQUENTIAL_CHUNK_PROGRESS_HEADERS = [ + 'chunk', + 'progress', + 'time (s)', + 'files', + 'count', + 'average χ²', + 'status', +] +_SEQUENTIAL_CHUNK_PROGRESS_ALIGNMENTS = [ + 'right', + 'right', + 'right', + 'left', + 'right', + 'right', + 'center', +] +_SEQUENTIAL_FILE_PROGRESS_HEADERS = ['file', 'progress', 'time (s)', 'χ²', 'iterations', 'status'] +_SEQUENTIAL_FILE_PROGRESS_ALIGNMENTS = ['left', 'right', 'right', 'right', 'right', 'center'] + + +@dataclass +class SequentialProgressState: + """Mutable live progress rows for sequential fitting.""" + + chunk_rows: list[list[str]] + file_rows: list[list[str]] + + +@dataclass +class SequentialProgressContext: + """Mutable sequential-fit progress handles and state.""" + + verbosity: VerbosityEnum + state: SequentialProgressState | None + indicator: ActivityIndicator | None = None + + +@dataclass(frozen=True) +class _ChunkProgressMetrics: + """File counts and elapsed time for a completed chunk.""" + + completed_files_before: int + total_files: int + elapsed_time: float + + +@dataclass(frozen=True) +class SequentialRunPlan: + """Resolved sequential-fit inputs and bookkeeping.""" + + verbosity: VerbosityEnum + template: SequentialFitTemplate + csv_path: Path + header: list[str] + remaining: list[str] + chunks: list[list[str]] + max_workers: int + processed_count: int + + +def _summarize_chunk_results(results: list[dict[str, Any]]) -> tuple[str, str]: + """Return average reduced chi-square and status for a chunk.""" + num_files = len(results) + successful = [r for r in results if r.get('fit_result.success')] + if successful: + avg_chi2 = sum(r['fit_result.reduced_chi_square'] for r in successful) / len(successful) + chi2_str = f'{avg_chi2:.2f}' + else: + chi2_str = '—' + + if len(successful) == num_files: + status = '✅' + elif successful: + status = '⚠️' + else: + status = '❌' + + return chi2_str, status + + +def _chunk_file_range(chunk: list[str]) -> str: + """Return the inclusive file-name range for a chunk.""" + first_name = Path(chunk[0]).name + last_name = Path(chunk[-1]).name + if first_name == last_name: + return first_name + return f'{first_name} - {last_name}' + + +def _format_progress_percent(completed_items: int, total_items: int) -> str: + """Return overall progress as a percentage string.""" + if total_items < 1: + return '0.0%' + clamped_completed = min(max(completed_items, 0), total_items) + return f'{100.0 * clamped_completed / total_items:.1f}%' + + +def _format_elapsed_seconds(elapsed_time: float) -> str: + """Return elapsed time in seconds with two decimal places.""" + return f'{max(elapsed_time, 0.0):.2f}' + + +def _build_chunk_progress_row( chunk_idx: int, total_chunks: int, + chunk: list[str], results: list[dict[str, Any]], + completed_files: int, + total_files: int, + elapsed_time: float, +) -> list[str]: + """ + Return one sequential-progress table row for a completed chunk. + """ + chi2_str, status = _summarize_chunk_results(results) + return [ + f'{chunk_idx}/{total_chunks}', + _format_progress_percent(completed_files, total_files), + _format_elapsed_seconds(elapsed_time), + _chunk_file_range(chunk), + str(len(results)), + chi2_str, + status, + ] + + +def _build_file_progress_rows( + results: list[dict[str, Any]], + completed_files_before: int, + total_files: int, + elapsed_time: float, +) -> list[list[str]]: + """Return sequential-progress rows for individual file fits.""" + rows: list[list[str]] = [] + time_str = _format_elapsed_seconds(elapsed_time) + for index, result in enumerate(results, start=1): + reduced_chi2 = result.get('fit_result.reduced_chi_square') + chi2_str = f'{reduced_chi2:.2f}' if reduced_chi2 is not None else '—' + iterations = str(result.get('fit_result.iterations') or 0) + status = '✅' if result.get('fit_result.success') else '❌' + rows.append([ + Path(result['file_path']).name, + _format_progress_percent(completed_files_before + index, total_files), + time_str, + chi2_str, + iterations, + status, + ]) + return rows + + +def _build_progress_renderable( + verbosity: VerbosityEnum, + progress_state: SequentialProgressState, +) -> object: + """Build the sequential progress table renderable.""" + if verbosity is VerbosityEnum.FULL: + return build_table_renderable( + columns_headers=_SEQUENTIAL_FILE_PROGRESS_HEADERS, + columns_alignment=_SEQUENTIAL_FILE_PROGRESS_ALIGNMENTS, + columns_data=progress_state.file_rows, + ) + + return build_table_renderable( + columns_headers=_SEQUENTIAL_CHUNK_PROGRESS_HEADERS, + columns_alignment=_SEQUENTIAL_CHUNK_PROGRESS_ALIGNMENTS, + columns_data=progress_state.chunk_rows, + ) + + +def _create_progress_context( + verbosity: VerbosityEnum, +) -> SequentialProgressContext: + """Return a mutable progress context for the given verbosity.""" + if verbosity is VerbosityEnum.SILENT: + return SequentialProgressContext(verbosity=verbosity, state=None) + + return SequentialProgressContext( + verbosity=verbosity, + state=SequentialProgressState(chunk_rows=[], file_rows=[]), + ) + + +def _start_progress_display(progress: SequentialProgressContext) -> None: + """ + Start the live progress indicator with an empty bordered table. + """ + if progress.verbosity is VerbosityEnum.SILENT or progress.state is None: + return + + indicator = ActivityIndicator( + ACTIVITY_LABEL_FITTING, + verbosity=progress.verbosity, + ) + indicator.start() + indicator.update( + content=_build_progress_renderable(progress.verbosity, progress.state), + ) + progress.indicator = indicator + + +def _stop_progress_display(progress: SequentialProgressContext) -> None: + """Stop any active sequential-fit progress display.""" + if progress.indicator is not None: + progress.indicator.stop() + progress.indicator = None + + +def _print_sequential_header( + analysis: object, + verbosity: VerbosityEnum, + remaining: list[str], + chunks: list[list[str]], + max_workers: int, +) -> None: + """Print the user-facing sequential-fit header.""" + if verbosity is VerbosityEnum.SILENT: + return + + console.paragraph('Sequential fitting') + console.print(f"🚀 Starting fit process with '{analysis.fitter.selection}'...") + console.print(f'📋 {len(remaining)} files in {len(chunks)} chunks (max_workers={max_workers})') + console.print('📈 Goodness-of-fit progress:') + + +def _print_sequential_completion( verbosity: VerbosityEnum, + processed_count: int, + csv_path: Path, +) -> None: + """Print the final sequential-fit summary.""" + if verbosity is VerbosityEnum.SILENT: + return + + console.print(f'✅ Sequential fitting complete: {processed_count} files processed.') + console.print(f'📄 Results saved to:\n{csv_path}') + + +def _prepare_sequential_run( + analysis: object, + data_dir: str, + max_workers: int | str, + chunk_size: int | None, + file_pattern: str, + *, + reverse: bool, +) -> SequentialRunPlan | None: + """Resolve inputs and bookkeeping for one sequential-fit run.""" + verbosity = VerbosityEnum(analysis.project.verbosity.fit.value) + + _check_seq_preconditions(analysis.project) + + data_paths = extract_data_paths_from_dir(data_dir, file_pattern=file_pattern) + template = _build_template(analysis.project) + csv_path, header, already_fitted, template = _setup_csv_and_recovery( + analysis.project, + template, + verbosity, + ) + + remaining = [path for path in data_paths if path not in already_fitted] + if reverse: + remaining.reverse() + if not remaining: + if verbosity is not VerbosityEnum.SILENT: + console.print('✅ All files already fitted. Nothing to do.') + return None + + resolved_workers, resolved_chunk_size = _resolve_workers(max_workers, chunk_size) + chunks = [ + remaining[index : index + resolved_chunk_size] + for index in range(0, len(remaining), resolved_chunk_size) + ] + return SequentialRunPlan( + verbosity=verbosity, + template=template, + csv_path=csv_path, + header=header, + remaining=remaining, + chunks=chunks, + max_workers=resolved_workers, + processed_count=len(already_fitted) + len(remaining), + ) + + +def _run_fit_loop_with_pool( + max_workers: int, + chunks: list[list[str]], + template: SequentialFitTemplate, + csv_info: tuple[Path, list[str]], + progress: SequentialProgressContext, +) -> None: + """Execute the fit loop inside a worker-pool context.""" + pool_cm, main_mod, main_file_bak, main_spec_bak = _create_pool_context(max_workers) + try: + _run_fit_loop(pool_cm, chunks, template, csv_info, progress) + finally: + _restore_main_state(main_mod, main_file_bak, main_spec_bak) + + +def _report_chunk_progress( + chunk_idx: int, + total_chunks: int, + chunk: list[str], + results: list[dict[str, Any]], + progress: SequentialProgressContext, + metrics: _ChunkProgressMetrics, ) -> None: """ Report progress after a chunk completes. @@ -511,63 +978,46 @@ def _report_chunk_progress( 1-based index of the current chunk. total_chunks : int Total number of chunks. + chunk : list[str] + File paths in the current chunk. results : list[dict[str, Any]] Results from the chunk. - verbosity : VerbosityEnum - Output verbosity. + progress : SequentialProgressContext + Mutable progress handles and accumulated table rows. + metrics : _ChunkProgressMetrics + File counts and elapsed time for the completed chunk. """ - if verbosity is VerbosityEnum.SILENT: + if progress.verbosity is VerbosityEnum.SILENT or progress.state is None: return - num_files = len(results) - successful = [r for r in results if r.get('fit_success')] - if successful: - avg_chi2 = sum(r['reduced_chi_squared'] for r in successful) / len(successful) - chi2_str = f'{avg_chi2:.2f}' - else: - chi2_str = '—' + completed_files = metrics.completed_files_before + len(results) - if verbosity is VerbosityEnum.SHORT: - status = '✅' if successful else '❌' - print(f'{status} Chunk {chunk_idx}/{total_chunks}: {num_files} files, avg χ² = {chi2_str}') - elif verbosity is VerbosityEnum.FULL: - print( - f'Chunk {chunk_idx}/{total_chunks}: ' - f'{num_files} files, {len(successful)} succeeded, ' - f'avg reduced χ² = {chi2_str}' + if progress.verbosity is VerbosityEnum.FULL: + new_rows = _build_file_progress_rows( + results, + metrics.completed_files_before, + metrics.total_files, + metrics.elapsed_time, ) - for r in results: - status = '✅' if r.get('fit_success') else '❌' - rchi2 = r.get('reduced_chi_squared') - rchi2_str = f'{rchi2:.2f}' if rchi2 is not None else '—' - print(f' {status} {Path(r["file_path"]).name}: χ² = {rchi2_str}') - - -def _apply_diffrn_metadata( - results: list[dict[str, Any]], - extract_diffrn: Callable, -) -> None: - """ - Enrich result dicts with diffrn metadata from a user callback. - - Calls *extract_diffrn* for each result and merges the returned - key/value pairs into the result dict under ``diffrn.`` keys. - Failures are logged as warnings and do not interrupt processing. + progress.state.file_rows.extend(new_rows) + else: + new_rows = [ + _build_chunk_progress_row( + chunk_idx, + total_chunks, + chunk, + results, + completed_files, + metrics.total_files, + metrics.elapsed_time, + ) + ] + progress.state.chunk_rows.extend(new_rows) - Parameters - ---------- - results : list[dict[str, Any]] - Worker result dicts (mutated in place). - extract_diffrn : Callable - User callback: ``f(file_path) → {field: value}``. - """ - for result in results: - try: - diffrn_values = extract_diffrn(result['file_path']) - for key, val in diffrn_values.items(): - result[f'diffrn.{key}'] = val - except (RuntimeError, ValueError, TypeError, KeyError, AttributeError, OSError) as exc: - log.warning(f'extract_diffrn failed for {result["file_path"]}: {exc}') + if progress.indicator is not None: + progress.indicator.update( + content=_build_progress_renderable(progress.verbosity, progress.state), + ) # ------------------------------------------------------------------ @@ -612,7 +1062,9 @@ def _check_seq_preconditions(project: object) -> list[str]: from easydiffraction.core.variable import Parameter # noqa: PLC0415 free_params = [ - p for p in project.parameters if isinstance(p, Parameter) and not p.constrained and p.free + p + for p in project.parameters + if isinstance(p, Parameter) and not p.user_constrained and p.free ] if not free_params: msg = 'No free parameters found. Mark at least one parameter as free.' @@ -651,7 +1103,7 @@ def _setup_csv_and_recovery( num_skipped = len(already_fitted) log.info(f'Resuming: {num_skipped} files already fitted, skipping.') if verb is not VerbosityEnum.SILENT: - print(f'📂 Resuming from CSV: {num_skipped} files already fitted.') + console.print(f'📂 Resuming from CSV: {num_skipped} files already fitted.') if recovered_params is not None: template = replace(template, initial_params=recovered_params) else: @@ -751,8 +1203,7 @@ def _run_fit_loop( chunks: list[list[str]], template: SequentialFitTemplate, csv_info: tuple[Path, list[str]], - extract_diffrn: Callable | None, - verb: VerbosityEnum, + progress: SequentialProgressContext, ) -> None: """ Execute the chunk-based fitting loop. @@ -767,13 +1218,14 @@ def _run_fit_loop( Starting template (updated via propagation). csv_info : tuple[Path, list[str]] Tuple of ``(csv_path, header)``. - extract_diffrn : Callable | None - User callback for diffrn metadata. - verb : VerbosityEnum - Output verbosity. + progress : SequentialProgressContext + Mutable progress handles and accumulated table rows. """ csv_path, header = csv_info total_chunks = len(chunks) + total_files = sum(len(chunk) for chunk in chunks) + completed_files = 0 + started_at = time.perf_counter() with pool_cm as executor: for chunk_idx, chunk in enumerate(chunks, start=1): if executor is not None: @@ -782,11 +1234,21 @@ def _run_fit_loop( else: results = [_fit_worker(template, path) for path in chunk] - if extract_diffrn is not None: - _apply_diffrn_metadata(results, extract_diffrn) - _append_to_csv(csv_path, header, results) - _report_chunk_progress(chunk_idx, total_chunks, results, verb) + elapsed_time = time.perf_counter() - started_at + _report_chunk_progress( + chunk_idx, + total_chunks, + chunk, + results, + progress, + _ChunkProgressMetrics( + completed_files_before=completed_files, + total_files=total_files, + elapsed_time=elapsed_time, + ), + ) + completed_files += len(results) # Propagate last successful params last_ok = _find_last_successful(results) @@ -797,7 +1259,7 @@ def _run_fit_loop( def _find_last_successful(results: list[dict[str, Any]]) -> dict[str, Any] | None: """Return the last successful result dict, or None.""" for r in reversed(results): - if r.get('fit_success') and r.get('params'): + if r.get('fit_result.success') and r.get('params'): return r return None @@ -808,7 +1270,6 @@ def fit_sequential( max_workers: int | str = 1, chunk_size: int | None = None, file_pattern: str = '*', - extract_diffrn: Callable | None = None, *, reverse: bool = False, ) -> None: @@ -829,8 +1290,6 @@ def fit_sequential( Files per chunk. Default ``None`` uses ``max_workers``. file_pattern : str, default='*' Glob pattern to filter files in *data_dir*. - extract_diffrn : Callable | None, default=None - User callback: ``f(file_path) → {diffrn_field: value}``. reverse : bool, default=False When ``True``, process data files in reverse order. Useful when starting values are better matched to the last file (e.g. @@ -839,48 +1298,40 @@ def fit_sequential( if mp.parent_process() is not None: return - project = analysis.project - verb = VerbosityEnum(project.verbosity) - - _check_seq_preconditions(project) - - data_paths = extract_data_paths_from_dir(data_dir, file_pattern=file_pattern) - template = _build_template(project) - - csv_path, header, already_fitted, template = _setup_csv_and_recovery( - project, - template, - verb, + plan = _prepare_sequential_run( + analysis, + data_dir, + max_workers, + chunk_size, + file_pattern, + reverse=reverse, ) - - remaining = [p for p in data_paths if p not in already_fitted] - if reverse: - remaining.reverse() - if not remaining: - if verb is not VerbosityEnum.SILENT: - print('✅ All files already fitted. Nothing to do.') + if plan is None: return - max_workers, chunk_size = _resolve_workers(max_workers, chunk_size) - chunks = [remaining[i : i + chunk_size] for i in range(0, len(remaining), chunk_size)] - - if verb is not VerbosityEnum.SILENT: - console.paragraph('Sequential fitting') - console.print(f"🚀 Starting fit process with '{analysis.fitter.selection}'...") - console.print( - f'📋 {len(remaining)} files in {len(chunks)} chunks (max_workers={max_workers})' - ) - console.print('📈 Goodness-of-fit (reduced χ²):') + _print_sequential_header( + analysis, + plan.verbosity, + plan.remaining, + plan.chunks, + plan.max_workers, + ) - pool_cm, main_mod, main_file_bak, main_spec_bak = _create_pool_context(max_workers) + progress = _create_progress_context(plan.verbosity) + _start_progress_display(progress) try: - _run_fit_loop(pool_cm, chunks, template, (csv_path, header), extract_diffrn, verb) + _run_fit_loop_with_pool( + plan.max_workers, + plan.chunks, + plan.template, + (plan.csv_path, plan.header), + progress, + ) finally: - _restore_main_state(main_mod, main_file_bak, main_spec_bak) + _stop_progress_display(progress) - if verb is not VerbosityEnum.SILENT: - print( - f'✅ Sequential fitting complete: ' - f'{len(already_fitted) + len(remaining)} files processed.' - ) - print(f'📄 Results saved to: {csv_path}') + _print_sequential_completion( + plan.verbosity, + plan.processed_count, + plan.csv_path, + ) diff --git a/src/easydiffraction/core/__init__.py b/src/easydiffraction/core/__init__.py index 4e798e209..72bd7f4d6 100644 --- a/src/easydiffraction/core/__init__.py +++ b/src/easydiffraction/core/__init__.py @@ -1,2 +1,4 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause + +from easydiffraction.core.variable import BoolDescriptor diff --git a/src/easydiffraction/core/category.py b/src/easydiffraction/core/category.py index 0804d7d40..d2d95fb80 100644 --- a/src/easydiffraction/core/category.py +++ b/src/easydiffraction/core/category.py @@ -22,6 +22,13 @@ class CategoryItem(GuardedBase): # CategoryCollection and use them when serializing to CIF! # TODO: Common for all categories _update_priority = 10 # Default. Lower values run first. + _category_code: str | None = None + _category_entry_name: str | None = None + + def __init__(self) -> None: + super().__init__() + if self._category_code is not None: + self._identity.category_code = self._category_code def __str__(self) -> str: """Human-readable representation of this component.""" @@ -37,6 +44,17 @@ def _update( # noqa: PLR6301 ) -> None: del called_by_minimizer + def _resolve_category_entry_name(self) -> str | None: + """Resolve the declared category entry value for this item.""" + attr_name = self._category_entry_name + if attr_name is None: + return None + + value = getattr(self, attr_name) + if isinstance(value, GenericDescriptorBase): + value = value.value + return str(value) + @property def unique_name(self) -> str: """Fully qualified name: datablock, category, entry.""" diff --git a/src/easydiffraction/core/category_owner.py b/src/easydiffraction/core/category_owner.py new file mode 100644 index 000000000..09e0cf68e --- /dev/null +++ b/src/easydiffraction/core/category_owner.py @@ -0,0 +1,76 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + +from __future__ import annotations + +from easydiffraction.core.category import CategoryCollection +from easydiffraction.core.category import CategoryItem +from easydiffraction.core.guard import GuardedBase + + +class CategoryOwner(GuardedBase): + """Base class for objects that own flat CIF-like categories.""" + + def __init__(self) -> None: + super().__init__() + self._need_categories_update = True + + @property + def categories(self) -> list: + """ + All category objects owned by this object, sorted by priority. + """ + categories = [ + value + for value in vars(self).values() + if isinstance(value, (CategoryItem, CategoryCollection)) + ] + return sorted(categories, key=lambda category: type(category)._update_priority) + + def _serializable_categories(self) -> list: + """Categories that should be serialized for this owner.""" + return self.categories + + @property + def parameters(self) -> list: + """All parameters from all owned categories.""" + parameters = [] + for category in self.categories: + parameters.extend(category.parameters) + return parameters + + def _update_categories( + self, + *, + called_by_minimizer: bool = False, + ) -> None: + """Run update hooks on all owned categories.""" + 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) + + self._need_categories_update = False + + def help(self) -> None: + """Print a summary of public attributes and categories.""" + super().help() + + from easydiffraction.utils.logging import console # noqa: PLC0415 + from easydiffraction.utils.utils import render_table # noqa: PLC0415 + + categories = self.categories + if categories: + console.paragraph('Categories') + rows = [] + for category in categories: + code = category._identity.category_code or type(category).__name__ + type_name = type(category).__name__ + num_params = len(category.parameters) + rows.append([code, type_name, str(num_params)]) + render_table( + columns_headers=['Category', 'Type', '# Parameters'], + columns_alignment=['left', 'left', 'right'], + columns_data=rows, + ) diff --git a/src/easydiffraction/core/collection.py b/src/easydiffraction/core/collection.py index 520bf94f9..356469354 100644 --- a/src/easydiffraction/core/collection.py +++ b/src/easydiffraction/core/collection.py @@ -79,6 +79,19 @@ def __setitem__(self, name: str, item: GuardedBase) -> None: self._items.append(item) self._rebuild_index() + def _adopt_items(self, items: list[GuardedBase]) -> None: + """ + Replace items and link each child to this collection. + """ + for item in self._items: + item._parent = None + + for item in items: + item._parent = self + + self._items = items + self._rebuild_index() + def __delitem__(self, name: str) -> None: """Delete an item by key or raise ``KeyError`` if missing.""" for i, item in enumerate(self._items): diff --git a/src/easydiffraction/core/datablock.py b/src/easydiffraction/core/datablock.py index f6f7ae056..4eb18866a 100644 --- a/src/easydiffraction/core/datablock.py +++ b/src/easydiffraction/core/datablock.py @@ -3,20 +3,14 @@ from __future__ import annotations -from easydiffraction.core.category import CategoryCollection -from easydiffraction.core.category import CategoryItem +from easydiffraction.core.category_owner import CategoryOwner from easydiffraction.core.collection import CollectionBase -from easydiffraction.core.guard import GuardedBase from easydiffraction.core.variable import Parameter -class DatablockItem(GuardedBase): +class DatablockItem(CategoryOwner): """Base class for items in a datablock collection.""" - def __init__(self) -> None: - super().__init__() - self._need_categories_update = True - def __str__(self) -> str: """Human-readable representation of this component.""" name = self.unique_name @@ -31,59 +25,11 @@ def __repr__(self) -> str: num_categories = len(self.categories) return f'<{cls} datablock "{name}" ({num_categories} categories)>' - def _update_categories( - self, - *, - 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 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. - # Calling as_cif or data getter should first check this flag - # and call this method if True. - # 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? - # - # 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) - - self._need_categories_update = False - @property def unique_name(self) -> str | None: """Unique name of this datablock item (from identity).""" return self._identity.datablock_entry_name - @property - 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)) - ] - # Sort by _update_priority (lower values first) - return sorted(cats, key=lambda c: type(c)._update_priority) - - @property - def parameters(self) -> list: - """All parameters from all categories in this datablock.""" - params = [] - for v in self.categories: - params.extend(v.parameters) - return params - @property def as_cif(self) -> str: """Return CIF representation of this object.""" @@ -113,28 +59,6 @@ def _cif_for_display(self, max_loop_display: int = 20) -> str: self._update_categories() return datablock_item_to_cif(self, max_loop_display=max_loop_display) - def help(self) -> None: - """Print a summary of public attributes and categories.""" - super().help() - - from easydiffraction.utils.logging import console # noqa: PLC0415 - from easydiffraction.utils.utils import render_table # noqa: PLC0415 - - 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, - ) - # ====================================================================== @@ -187,8 +111,12 @@ def parameters(self) -> list: @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] + """All Parameters not blocked by constraints or symmetry.""" + return [ + p + for p in self.parameters + if isinstance(p, Parameter) and not p.user_constrained and not p.symmetry_constrained + ] @property def free_parameters(self) -> list: diff --git a/src/easydiffraction/core/guard.py b/src/easydiffraction/core/guard.py index 986b589b3..d1f8d4fd4 100644 --- a/src/easydiffraction/core/guard.py +++ b/src/easydiffraction/core/guard.py @@ -14,6 +14,33 @@ from collections.abc import Generator +def _apply_help_filter( + obj: object, + properties: list[str], + methods: list[str], +) -> tuple[list[str], list[str]]: + """ + Apply an optional instance help filter that may only hide members. + """ + help_filter = getattr(obj, '_help_filter', None) + if not callable(help_filter): + return properties, methods + + filtered_properties, filtered_methods = help_filter(list(properties), list(methods)) + invalid_properties = sorted(set(filtered_properties) - set(properties)) + invalid_methods = sorted(set(filtered_methods) - set(methods)) + if invalid_properties or invalid_methods: + owner_name = type(obj).__name__ + msg = f'{owner_name}._help_filter() may only hide discovered members.' + if invalid_properties: + msg += f' Invalid properties: {invalid_properties}.' + if invalid_methods: + msg += f' Invalid methods: {invalid_methods}.' + raise RuntimeError(msg) + + return filtered_properties, filtered_methods + + class GuardedBase(ABC): """Base class enforcing controlled attribute access and linkage.""" @@ -208,8 +235,14 @@ def help(self) -> None: if key not in seen: seen[key] = prop + property_names = sorted(seen) + + methods = dict(cls._iter_methods()) + method_names = sorted(methods) + property_names, method_names = _apply_help_filter(self, property_names, method_names) + prop_rows = [] - for i, key in enumerate(sorted(seen), 1): + for i, key in enumerate(property_names, 1): prop = seen[key] writable = '✓' if prop.fset else '✗' doc = self._first_sentence(prop.fget.__doc__ if prop.fget else None) @@ -223,9 +256,8 @@ def help(self) -> None: columns_data=prop_rows, ) - methods = dict(cls._iter_methods()) method_rows = [] - for i, key in enumerate(sorted(methods), 1): + for i, key in enumerate(method_names, 1): doc = self._first_sentence(getattr(methods[key], '__doc__', None)) method_rows.append([str(i), f'{key}()', doc]) diff --git a/src/easydiffraction/core/identity.py b/src/easydiffraction/core/identity.py index f6aca8cb2..b6b9e57a2 100644 --- a/src/easydiffraction/core/identity.py +++ b/src/easydiffraction/core/identity.py @@ -41,6 +41,13 @@ def _resolve_up(self, attr: str, visited: set[int] | None = None) -> str | None: if isinstance(value, str): return value + if attr == 'category_entry': + resolver = getattr(self._owner, '_resolve_category_entry_name', None) + if callable(resolver): + resolved = resolver() + if resolved is not None: + return resolved + # Climb to parent if available parent = getattr(self._owner, '__dict__', {}).get('_parent') if parent and hasattr(parent, '_identity'): diff --git a/src/easydiffraction/core/singleton.py b/src/easydiffraction/core/singleton.py index 59ade7c38..9f997e535 100644 --- a/src/easydiffraction/core/singleton.py +++ b/src/easydiffraction/core/singleton.py @@ -34,7 +34,7 @@ def get(cls) -> Self: # ====================================================================== -# TODO: Implement changing atrr '.constrained' back to False +# TODO: Implement changing attr '.user_constrained' back to False # when removing constraints class ConstraintsHandler(SingletonBase): """ @@ -95,7 +95,7 @@ def apply(self) -> None: For each constraint: - Evaluate RHS using current values of aliased parameters - Locate the dependent parameter via direct alias reference - - Update its value and mark it as constrained + - Update its value and mark it as user constrained """ if not self._parsed_constraints: return # Nothing to apply @@ -132,8 +132,8 @@ def apply(self) -> None: # Get the actual parameter object we want to update param = self._alias_to_param[lhs_alias].param - # Update its value and mark it as constrained - param._set_value_constrained(rhs_value) + # Update its value and mark it as user constrained + param._set_value_user_constrained(rhs_value) except (ValueError, TypeError, ArithmeticError, KeyError, AttributeError) 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 3fd882084..314d7e94c 100644 --- a/src/easydiffraction/core/validation.py +++ b/src/easydiffraction/core/validation.py @@ -27,6 +27,7 @@ class DataTypeHints: """Type hint aliases for numeric, string, and boolean types.""" Numeric = int | float | np.integer | np.floating + Integer = int | np.integer String = str Bool = bool @@ -38,6 +39,7 @@ class DataTypes(Enum): """Enumeration of supported data types for descriptors.""" NUMERIC = (int, float, np.integer, np.floating) + INTEGER = (int, np.integer) STRING = (str,) BOOL = (bool,) ANY = (object,) # fallback for unconstrained diff --git a/src/easydiffraction/core/variable.py b/src/easydiffraction/core/variable.py index 84bbd2f42..074afb714 100644 --- a/src/easydiffraction/core/variable.py +++ b/src/easydiffraction/core/variable.py @@ -22,6 +22,8 @@ # ====================================================================== +DEFAULT_FIT_BOUNDS_MULTIPLIER = 4.0 + class GenericDescriptorBase(GuardedBase): """ @@ -126,11 +128,11 @@ def _parent_of_type(self, cls: type) -> object | None: obj = getattr(obj, '_parent', None) return None - def _datablock_item(self) -> object | None: - """Return the DatablockItem ancestor, if any.""" - from easydiffraction.core.datablock import DatablockItem # noqa: PLC0415 + def _category_owner(self) -> object | None: + """Return the CategoryOwner ancestor, if any.""" + from easydiffraction.core.category_owner import CategoryOwner # noqa: PLC0415 - return self._parent_of_type(DatablockItem) + return self._parent_of_type(CategoryOwner) @property def value(self) -> object: @@ -151,18 +153,18 @@ def value(self, v: object) -> None: current=self._value, ) - # Mark parent datablock as needing categories update + # Mark the owning category owner as needing an update # TODO: Check if it is actually in use? - parent_datablock = self._datablock_item() - if parent_datablock is not None: - parent_datablock._need_categories_update = True + parent_owner = self._category_owner() + if parent_owner is not None: + parent_owner._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 + marks the owning category owner dirty so that ``_update_categories()`` knows work is needed. This exists because: @@ -173,9 +175,9 @@ def _set_value_from_minimizer(self, v: object) -> None: evaluations. """ self._value = v - parent_datablock = self._datablock_item() - if parent_datablock is not None: - parent_datablock._need_categories_update = True + parent_owner = self._category_owner() + if parent_owner is not None: + parent_owner._need_categories_update = True @property def description(self) -> str | None: @@ -221,6 +223,28 @@ def __init__( # ====================================================================== +class GenericBoolDescriptor(GenericDescriptorBase): + """Base descriptor that constrains values to booleans.""" + + _value_type = DataTypes.BOOL + + def __init__( + self, + *, + value_spec: AttributeSpec | None = None, + **kwargs: object, + ) -> None: + if value_spec is None: + value_spec = AttributeSpec( + data_type=DataTypes.BOOL, + default=False, + ) + super().__init__(value_spec=value_spec, **kwargs) + + +# ====================================================================== + + class GenericNumericDescriptor(GenericDescriptorBase): """Base descriptor that constrains values to numbers.""" @@ -252,6 +276,15 @@ def units(self) -> str: # ====================================================================== +class GenericIntegerDescriptor(GenericNumericDescriptor): + """Base descriptor that constrains values to integers.""" + + _value_type = DataTypes.INTEGER + + +# ====================================================================== + + class GenericParameter(GenericNumericDescriptor): """ Numeric descriptor extended with fitting-related attributes. @@ -280,12 +313,13 @@ def __init__( self._fit_min = self._fit_min_spec.default self._fit_max_spec = AttributeSpec(data_type=DataTypes.NUMERIC, default=np.inf) self._fit_max = self._fit_max_spec.default + self._fit_bounds_uncertainty_multiplier: float | None = None 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 - self._symmetry_fixed_spec = self._BOOL_SPEC_TEMPLATE - self._symmetry_fixed = self._symmetry_fixed_spec.default + self._user_constrained_spec = self._BOOL_SPEC_TEMPLATE + self._user_constrained = self._user_constrained_spec.default + self._symmetry_constrained_spec = self._BOOL_SPEC_TEMPLATE + self._symmetry_constrained = self._symmetry_constrained_spec.default def _physical_lower_bound(self) -> float: """ @@ -322,25 +356,25 @@ def _minimizer_uid(self) -> str: return self.unique_name.replace('.', '__') @property - def constrained(self) -> bool: + def user_constrained(self) -> bool: """Whether this parameter is part of a constraint expression.""" - return self._constrained + return self._user_constrained - def _set_value_constrained(self, v: object) -> None: + def _set_value_user_constrained(self, v: object) -> None: """ Set the value from a constraint expression. - Bypasses validation and marks the parent datablock dirty, like - ``_set_value_from_minimizer``, because constraints are applied - inside the minimizer loop where trial values may exceed - physical-range validators. Flags the parameter as constrained. - Used exclusively by ``ConstraintsHandler.apply()``. + Bypasses validation and marks the parent category owner dirty, + like ``_set_value_from_minimizer``, because constraints are + applied inside the minimizer loop where trial values may exceed + physical-range validators. Flags the parameter as user + constrained. Used exclusively by ``ConstraintsHandler.apply()``. """ self._value = v - self._constrained = True - parent_datablock = self._datablock_item() - if parent_datablock is not None: - parent_datablock._need_categories_update = True + self._user_constrained = True + parent_owner = self._category_owner() + if parent_owner is not None: + parent_owner._need_categories_update = True @property def free(self) -> bool: @@ -353,24 +387,24 @@ def free(self, v: bool) -> None: validated = self._free_spec.validated( v, name=f'{self.unique_name}.free', current=self._free ) - if validated and self._symmetry_fixed: + if validated and self._symmetry_constrained: log.warning( - f"Parameter '{self.unique_name}' is fixed by symmetry. Ignoring free=True." + f"Parameter '{self.unique_name}' is constrained by symmetry. Ignoring free=True." ) self._free = False return self._free = validated @property - def symmetry_fixed(self) -> bool: + def symmetry_constrained(self) -> bool: """ - Whether this parameter is fixed by crystallographic symmetry. + Return whether symmetry constrains this parameter. """ - return self._symmetry_fixed + return self._symmetry_constrained - def _set_symmetry_fixed(self, *, value: bool) -> None: + def _set_symmetry_constrained(self, *, value: bool) -> None: """ - Mark or unmark this parameter as fixed by symmetry. + Mark or unmark this parameter as constrained by symmetry. When set to True, ``free`` is forced to False and any subsequent attempt to set ``free = True`` is ignored with a warning. When @@ -380,14 +414,14 @@ def _set_symmetry_fixed(self, *, value: bool) -> None: Parameters ---------- value : bool - New symmetry-fixed state. + New symmetry-constrained state. """ - validated = self._symmetry_fixed_spec.validated( + validated = self._symmetry_constrained_spec.validated( value, - name=f'{self.unique_name}.symmetry_fixed', - current=self._symmetry_fixed, + name=f'{self.unique_name}.symmetry_constrained', + current=self._symmetry_constrained, ) - self._symmetry_fixed = validated + self._symmetry_constrained = validated if validated: self._free = False @@ -414,6 +448,7 @@ def fit_min(self, v: float) -> None: self._fit_min = self._fit_min_spec.validated( v, name=f'{self.unique_name}.fit_min', current=self._fit_min ) + self._fit_bounds_uncertainty_multiplier = None @property def fit_max(self) -> float: @@ -426,6 +461,84 @@ def fit_max(self, v: float) -> None: self._fit_max = self._fit_max_spec.validated( v, name=f'{self.unique_name}.fit_max', current=self._fit_max ) + self._fit_bounds_uncertainty_multiplier = None + + @property + def fit_bounds_uncertainty_multiplier(self) -> float | None: + """ + Multiplier used for uncertainty-derived fit bounds, if known. + """ + return self._fit_bounds_uncertainty_multiplier + + def _set_fit_bounds_uncertainty_multiplier(self, value: float | None) -> None: + """Set the cached uncertainty-derived fit-bounds multiplier.""" + self._fit_bounds_uncertainty_multiplier = value + + def set_fit_bounds_from_uncertainty( + self, + multiplier: float = DEFAULT_FIT_BOUNDS_MULTIPLIER, + *, + clip_to_limits: bool = True, + ) -> None: + """ + Set fit bounds from the current standard uncertainty. + + Parameters + ---------- + multiplier : float, default=DEFAULT_FIT_BOUNDS_MULTIPLIER + Positive finite factor applied symmetrically to the current + parameter uncertainty. + clip_to_limits : bool, default=True + Whether to clip the resolved fit bounds to the parameter's + physical lower and upper limits when those are finite. + + Raises + ------ + ValueError + If the current value, uncertainty, or multiplier is missing, + invalid, or produces non-increasing bounds. + """ + name = self.unique_name + value = self.value + uncertainty = self.uncertainty + + if value is None or not np.isfinite(float(value)): + msg = f'Cannot set fit bounds for {name}: current value is missing or invalid.' + raise ValueError(msg) + + resolved_multiplier = float(multiplier) + if isinstance(multiplier, bool) or not np.isfinite(resolved_multiplier): + msg = 'multiplier must be a positive finite number.' + raise ValueError(msg) + if resolved_multiplier <= 0: + msg = 'multiplier must be a positive finite number.' + raise ValueError(msg) + + if uncertainty is None or uncertainty <= 0 or not np.isfinite(float(uncertainty)): + msg = f'Cannot set fit bounds for {name}: uncertainty is missing or invalid.' + raise ValueError(msg) + + lower = float(value) - resolved_multiplier * float(uncertainty) + upper = float(value) + resolved_multiplier * float(uncertainty) + + if clip_to_limits: + physical_lower = float(self._physical_lower_bound()) + physical_upper = float(self._physical_upper_bound()) + if np.isfinite(physical_lower): + lower = max(lower, physical_lower) + if np.isfinite(physical_upper): + upper = min(upper, physical_upper) + + if lower >= upper: + msg = ( + f'Cannot set fit bounds for {name}: resolved lower bound {lower} ' + f'is not below upper bound {upper}.' + ) + raise ValueError(msg) + + self.fit_min = lower + self.fit_max = upper + self._fit_bounds_uncertainty_multiplier = resolved_multiplier # ====================================================================== @@ -458,6 +571,33 @@ def __init__( # ====================================================================== +class BoolDescriptor(GenericBoolDescriptor): + """Boolean descriptor bound to a CIF handler.""" + + def __init__( + self, + *, + cif_handler: CifHandler, + **kwargs: object, + ) -> None: + """ + Initialize a boolean descriptor bound to a CIF handler. + + Parameters + ---------- + cif_handler : CifHandler + Object that tracks CIF identifiers. + **kwargs : object + Forwarded to GenericBoolDescriptor. + """ + super().__init__(**kwargs) + self._cif_handler = cif_handler + self._cif_handler.attach(self) + + +# ====================================================================== + + class NumericDescriptor(GenericNumericDescriptor): """Numeric descriptor bound to a CIF handler.""" @@ -485,6 +625,33 @@ def __init__( # ====================================================================== +class IntegerDescriptor(GenericIntegerDescriptor): + """Integer descriptor bound to a CIF handler.""" + + def __init__( + self, + *, + cif_handler: CifHandler, + **kwargs: object, + ) -> None: + """ + Integer descriptor bound to a CIF handler. + + Parameters + ---------- + cif_handler : CifHandler + Object that tracks CIF identifiers. + **kwargs : object + Forwarded to GenericIntegerDescriptor. + """ + super().__init__(**kwargs) + self._cif_handler = cif_handler + self._cif_handler.attach(self) + + +# ====================================================================== + + class Parameter(GenericParameter): """Fittable parameter bound to a CIF handler.""" diff --git a/src/easydiffraction/crystallography/crystallography.py b/src/easydiffraction/crystallography/crystallography.py index 7e97be444..4bc0dba5c 100644 --- a/src/easydiffraction/crystallography/crystallography.py +++ b/src/easydiffraction/crystallography/crystallography.py @@ -118,7 +118,7 @@ def _crystal_system_from_name_hm(name_hm: str) -> str | None: return crystal_system -_CELL_FIXED_AXES_BY_SYSTEM: dict[str, set[str]] = { +_CELL_CONSTRAINED_AXES_BY_SYSTEM: dict[str, set[str]] = { 'cubic': {'lattice_b', 'lattice_c', 'angle_alpha', 'angle_beta', 'angle_gamma'}, 'tetragonal': {'lattice_b', 'angle_alpha', 'angle_beta', 'angle_gamma'}, 'orthorhombic': {'angle_alpha', 'angle_beta', 'angle_gamma'}, @@ -129,7 +129,7 @@ def _crystal_system_from_name_hm(name_hm: str) -> str | None: } -def _cell_fixed_axes(crystal_system: str) -> set[str]: +def _cell_constrained_axes(crystal_system: str) -> set[str]: """ Return cell keys that are dependent on others for a crystal system. @@ -145,14 +145,14 @@ def _cell_fixed_axes(crystal_system: str) -> set[str]: Returns ------- set[str] - Subset of cell keys that are fixed by symmetry. + Subset of cell keys that are constrained by symmetry. """ - return _CELL_FIXED_AXES_BY_SYSTEM.get(crystal_system, set()) + return _CELL_CONSTRAINED_AXES_BY_SYSTEM.get(crystal_system, set()) -def cell_symmetry_fixed_flags(name_hm: str) -> dict[str, bool]: +def cell_symmetry_constrained_flags(name_hm: str) -> dict[str, bool]: """ - Return per-key flags indicating which cell parameters are fixed. + Return cell-parameter symmetry-constraint flags. Parameters ---------- @@ -162,16 +162,16 @@ def cell_symmetry_fixed_flags(name_hm: str) -> dict[str, bool]: Returns ------- dict[str, bool] - Mapping of cell key to ``True`` when the parameter is fixed by - symmetry (dependent on another parameter or set to a fixed - angle), ``False`` when it is independent. Returns all keys - ``False`` when the space group cannot be resolved. + Mapping of cell key to ``True`` when the parameter is + constrained by symmetry (dependent on another parameter or set + to a fixed angle), ``False`` when it is independent. Returns all + keys ``False`` when the space group cannot be resolved. """ crystal_system = _crystal_system_from_name_hm(name_hm) if crystal_system is None: return dict.fromkeys(_CELL_KEYS, False) - fixed = _cell_fixed_axes(crystal_system) - return {key: key in fixed for key in _CELL_KEYS} + constrained = _cell_constrained_axes(crystal_system) + return {key: key in constrained for key in _CELL_KEYS} def _get_wyckoff_exprs( @@ -218,13 +218,13 @@ def _get_wyckoff_exprs( return [sympify(comp.strip()) for comp in components] -def _fract_fixed_flags(parsed_exprs: list[Expr]) -> dict[str, bool]: +def _fract_constrained_flags(parsed_exprs: list[Expr]) -> dict[str, bool]: """ - Return per-axis flags marking coordinates fixed by site symmetry. + Return fractional-coordinate symmetry-constraint flags. - For each axis (x, y, z), the coordinate is considered fixed when the - corresponding symbol does not appear as a free symbol in any of the - Wyckoff position expressions. + For each axis (x, y, z), the coordinate is considered constrained + when the corresponding symbol does not appear as a free symbol in + any of the Wyckoff position expressions. Parameters ---------- @@ -235,7 +235,7 @@ def _fract_fixed_flags(parsed_exprs: list[Expr]) -> dict[str, bool]: ------- dict[str, bool] Mapping ``'fract_x' / 'fract_y' / 'fract_z'`` to ``True`` if - that axis is fixed by symmetry. + that axis is constrained by symmetry. """ x, y, z = symbols('x y z') symbols_xyz = (x, y, z) @@ -271,10 +271,10 @@ def _apply_fract_constraints( 'y': sympify(atom_site['fract_y']), 'z': sympify(atom_site['fract_z']), } - fixed_flags = _fract_fixed_flags(parsed_exprs) + constrained_flags = _fract_constrained_flags(parsed_exprs) for i, axis in enumerate(axes): - if fixed_flags[f'fract_{axis}']: + if constrained_flags[f'fract_{axis}']: evaluated = simplify(parsed_exprs[i].subs(substitutions)) atom_site[f'fract_{axis}'] = float(evaluated) @@ -312,13 +312,13 @@ def apply_atom_site_symmetry_constraints( return atom_site -def atom_site_symmetry_fixed_flags( +def atom_site_symmetry_constrained_flags( name_hm: str, coord_code: int, wyckoff_letter: str, ) -> dict[str, bool]: """ - Return per-axis flags marking coordinates fixed by site symmetry. + Return atom-site symmetry-constraint flags. Parameters ---------- @@ -339,7 +339,7 @@ def atom_site_symmetry_fixed_flags( parsed_exprs = _get_wyckoff_exprs(name_hm, coord_code, wyckoff_letter) if parsed_exprs is None: return {'fract_x': False, 'fract_y': False, 'fract_z': False} - return _fract_fixed_flags(parsed_exprs) + return _fract_constrained_flags(parsed_exprs) # ------------------------------------------------------------------ diff --git a/src/easydiffraction/datablocks/experiment/categories/background/chebyshev.py b/src/easydiffraction/datablocks/experiment/categories/background/chebyshev.py index 5f90094dc..77e109c8d 100644 --- a/src/easydiffraction/datablocks/experiment/categories/background/chebyshev.py +++ b/src/easydiffraction/datablocks/experiment/categories/background/chebyshev.py @@ -41,6 +41,9 @@ class PolynomialTerm(CategoryItem): not break immediately. Tests should migrate to the short names. """ + _category_code = 'background' + _category_entry_name = 'id' + def __init__(self) -> None: super().__init__() @@ -75,9 +78,6 @@ def __init__(self) -> None: 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 # ------------------------------------------------------------------ diff --git a/src/easydiffraction/datablocks/experiment/categories/background/line_segment.py b/src/easydiffraction/datablocks/experiment/categories/background/line_segment.py index 83e202b43..431abb317 100644 --- a/src/easydiffraction/datablocks/experiment/categories/background/line_segment.py +++ b/src/easydiffraction/datablocks/experiment/categories/background/line_segment.py @@ -34,6 +34,9 @@ class LineSegment(CategoryItem): """Single background control point for interpolation.""" + _category_code = 'background' + _category_entry_name = 'id' + def __init__(self) -> None: super().__init__() @@ -78,9 +81,6 @@ def __init__(self) -> None: ), ) - self._identity.category_code = 'background' - self._identity.category_entry_name = lambda: str(self._id.value) - # ------------------------------------------------------------------ # Public properties # ------------------------------------------------------------------ diff --git a/src/easydiffraction/datablocks/experiment/categories/calculation/default.py b/src/easydiffraction/datablocks/experiment/categories/calculation/default.py index 1574c58b6..ae54663a1 100644 --- a/src/easydiffraction/datablocks/experiment/categories/calculation/default.py +++ b/src/easydiffraction/datablocks/experiment/categories/calculation/default.py @@ -21,6 +21,8 @@ class Calculation(CategoryItem): """Calculator selection and access for an experiment.""" + _category_code = 'calculation' + type_info = TypeInfo( tag='default', description='Experiment calculation category', @@ -45,8 +47,6 @@ def __init__( cif_handler=CifHandler(names=['_calculation.calculator_type']), ) - self._identity.category_code = 'calculation' - @property def calculator_type(self) -> StringDescriptor: """Calculation backend type.""" diff --git a/src/easydiffraction/datablocks/experiment/categories/data/bragg_pd.py b/src/easydiffraction/datablocks/experiment/categories/data/bragg_pd.py index 0f8633010..1d5c866b8 100644 --- a/src/easydiffraction/datablocks/experiment/categories/data/bragg_pd.py +++ b/src/easydiffraction/datablocks/experiment/categories/data/bragg_pd.py @@ -285,10 +285,11 @@ class PdCwlDataPoint( ): """Powder diffraction data point for CWL experiments.""" + _category_code = 'pd_data' + _category_entry_name = 'point_id' + 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( @@ -298,10 +299,11 @@ class PdTofDataPoint( ): """Powder diffraction data point for time-of-flight experiments.""" + _category_code = 'pd_data' + _category_entry_name = 'point_id' + 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): @@ -572,7 +574,7 @@ def _create_items_set_xcoord_and_id(self, values: object) -> None: # TODO: split into multiple methods # Create items - self._items = [self._item_type() for _ in range(values.size)] + self._adopt_items([self._item_type() for _ in range(values.size)]) # Set two-theta values for p, v in zip(self._items, values, strict=True): @@ -651,7 +653,7 @@ def _create_items_set_xcoord_and_id(self, values: object) -> None: # TODO: split into multiple methods # Create items - self._items = [self._item_type() for _ in range(values.size)] + self._adopt_items([self._item_type() for _ in range(values.size)]) # Set time-of-flight values for p, v in zip(self._items, values, strict=True): diff --git a/src/easydiffraction/datablocks/experiment/categories/data/total_pd.py b/src/easydiffraction/datablocks/experiment/categories/data/total_pd.py index 42b3ca8ab..58ef0c74b 100644 --- a/src/easydiffraction/datablocks/experiment/categories/data/total_pd.py +++ b/src/easydiffraction/datablocks/experiment/categories/data/total_pd.py @@ -33,6 +33,9 @@ class TotalDataPoint(CategoryItem): original measurement was CWL or TOF. """ + _category_code = 'total_data' + _category_entry_name = 'point_id' + def __init__(self) -> None: super().__init__() @@ -114,9 +117,6 @@ def __init__(self) -> None: ), ) - self._identity.category_code = 'total_data' - self._identity.category_entry_name = lambda: str(self.point_id.value) - # ------------------------------------------------------------------ # Public properties # ------------------------------------------------------------------ @@ -349,7 +349,7 @@ def _create_items_set_xcoord_and_id(self, values: object) -> None: # TODO: split into multiple methods # Create items - self._items = [self._item_type() for _ in range(values.size)] + self._adopt_items([self._item_type() for _ in range(values.size)]) # Set r values for p, v in zip(self._items, values, strict=True): diff --git a/src/easydiffraction/datablocks/experiment/categories/diffrn/default.py b/src/easydiffraction/datablocks/experiment/categories/diffrn/default.py index 9635fc2ef..92b4290de 100644 --- a/src/easydiffraction/datablocks/experiment/categories/diffrn/default.py +++ b/src/easydiffraction/datablocks/experiment/categories/diffrn/default.py @@ -17,6 +17,8 @@ class DefaultDiffrn(CategoryItem): """Ambient conditions recorded during diffraction measurement.""" + _category_code = 'diffrn' + type_info = TypeInfo( tag='default', description='Diffraction ambient conditions', @@ -73,8 +75,6 @@ def __init__(self) -> None: cif_handler=CifHandler(names=['_diffrn.ambient_electric_field']), ) - self._identity.category_code = 'diffrn' - # ------------------------------------------------------------------ # Public properties # ------------------------------------------------------------------ diff --git a/src/easydiffraction/datablocks/experiment/categories/excluded_regions/default.py b/src/easydiffraction/datablocks/experiment/categories/excluded_regions/default.py index 2f4170bbd..36df8e468 100644 --- a/src/easydiffraction/datablocks/experiment/categories/excluded_regions/default.py +++ b/src/easydiffraction/datablocks/experiment/categories/excluded_regions/default.py @@ -27,6 +27,9 @@ class ExcludedRegion(CategoryItem): """Closed interval [start, end] to be excluded.""" + _category_code = 'excluded_regions' + _category_entry_name = 'id' + def __init__(self) -> None: super().__init__() @@ -61,8 +64,6 @@ def __init__(self) -> None: ), cif_handler=CifHandler(names=['_excluded_region.end']), ) - self._identity.category_code = 'excluded_regions' - self._identity.category_entry_name = lambda: str(self._id.value) # ------------------------------------------------------------------ # Public properties diff --git a/src/easydiffraction/datablocks/experiment/categories/experiment_type/default.py b/src/easydiffraction/datablocks/experiment/categories/experiment_type/default.py index 1b9d38113..83348086b 100644 --- a/src/easydiffraction/datablocks/experiment/categories/experiment_type/default.py +++ b/src/easydiffraction/datablocks/experiment/categories/experiment_type/default.py @@ -29,6 +29,8 @@ class ExperimentType(CategoryItem): """Container of attributes defining the experiment type.""" + _category_code = 'expt_type' + type_info = TypeInfo( tag='default', description='Experiment type descriptor', @@ -79,8 +81,6 @@ def __init__(self) -> None: cif_handler=CifHandler(names=['_expt_type.scattering_type']), ) - self._identity.category_code = 'expt_type' - # ------------------------------------------------------------------ # Private setters (used by factories and loaders only) # ------------------------------------------------------------------ diff --git a/src/easydiffraction/datablocks/experiment/categories/extinction/becker_coppens.py b/src/easydiffraction/datablocks/experiment/categories/extinction/becker_coppens.py index 0555d7d02..d5ffa1578 100644 --- a/src/easydiffraction/datablocks/experiment/categories/extinction/becker_coppens.py +++ b/src/easydiffraction/datablocks/experiment/categories/extinction/becker_coppens.py @@ -36,6 +36,8 @@ class BeckerCoppensExtinction(CategoryItem): (in arc-minutes, as expected by CrysPy). """ + _category_code = 'extinction' + type_info = TypeInfo( tag='becker-coppens', description='Becker-Coppens isotropic extinction correction', @@ -83,8 +85,6 @@ def __init__(self) -> None: cif_handler=CifHandler(names=['_extinction.radius']), ) - self._identity.category_code = 'extinction' - # ------------------------------------------------------------------ # Public properties # ------------------------------------------------------------------ diff --git a/src/easydiffraction/datablocks/experiment/categories/instrument/base.py b/src/easydiffraction/datablocks/experiment/categories/instrument/base.py index a2568884b..c8e209744 100644 --- a/src/easydiffraction/datablocks/experiment/categories/instrument/base.py +++ b/src/easydiffraction/datablocks/experiment/categories/instrument/base.py @@ -20,7 +20,8 @@ class InstrumentBase(CategoryItem): for concrete CWL/TOF instrument definitions. """ + _category_code = 'instrument' + def __init__(self) -> None: - """Initialize instrument base and set category code.""" + """Initialize instrument base.""" super().__init__() - self._identity.category_code = 'instrument' diff --git a/src/easydiffraction/datablocks/experiment/categories/linked_crystal/default.py b/src/easydiffraction/datablocks/experiment/categories/linked_crystal/default.py index d441aa9ca..b1cbf2d85 100644 --- a/src/easydiffraction/datablocks/experiment/categories/linked_crystal/default.py +++ b/src/easydiffraction/datablocks/experiment/categories/linked_crystal/default.py @@ -23,6 +23,8 @@ class LinkedCrystal(CategoryItem): """Linked crystal reference for single-crystal diffraction.""" + _category_code = 'linked_crystal' + type_info = TypeInfo( tag='default', description='Crystal reference with id and scale factor', @@ -53,8 +55,6 @@ def __init__(self) -> None: cif_handler=CifHandler(names=['_sc_crystal_block.scale']), ) - self._identity.category_code = 'linked_crystal' - # ------------------------------------------------------------------ # Public properties # ------------------------------------------------------------------ diff --git a/src/easydiffraction/datablocks/experiment/categories/linked_phases/default.py b/src/easydiffraction/datablocks/experiment/categories/linked_phases/default.py index eb6047a79..fca0fb3b2 100644 --- a/src/easydiffraction/datablocks/experiment/categories/linked_phases/default.py +++ b/src/easydiffraction/datablocks/experiment/categories/linked_phases/default.py @@ -23,6 +23,9 @@ class LinkedPhase(CategoryItem): """Link to a phase by id with a scale factor.""" + _category_code = 'linked_phases' + _category_entry_name = 'id' + def __init__(self) -> None: super().__init__() @@ -45,9 +48,6 @@ def __init__(self) -> None: 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 # ------------------------------------------------------------------ diff --git a/src/easydiffraction/datablocks/experiment/categories/peak/base.py b/src/easydiffraction/datablocks/experiment/categories/peak/base.py index c8270ed1c..330d34624 100644 --- a/src/easydiffraction/datablocks/experiment/categories/peak/base.py +++ b/src/easydiffraction/datablocks/experiment/categories/peak/base.py @@ -13,9 +13,10 @@ class PeakBase(CategoryItem): """Base class for peak profile categories.""" + _category_code = 'peak' + def __init__(self) -> None: super().__init__() - self._identity.category_code = 'peak' type_info = getattr(type(self), 'type_info', None) default_tag = type_info.tag if type_info is not None else '' diff --git a/src/easydiffraction/datablocks/experiment/categories/refln/bragg_sc.py b/src/easydiffraction/datablocks/experiment/categories/refln/bragg_sc.py index 52329abb0..8b29900d4 100644 --- a/src/easydiffraction/datablocks/experiment/categories/refln/bragg_sc.py +++ b/src/easydiffraction/datablocks/experiment/categories/refln/bragg_sc.py @@ -28,6 +28,9 @@ class Refln(CategoryItem): """Single reflection for single-crystal diffraction data.""" + _category_code = 'refln' + _category_entry_name = 'id' + def __init__(self) -> None: super().__init__() @@ -128,9 +131,6 @@ def __init__(self) -> None: cif_handler=CifHandler(names=['_refln.wavelength']), ) - self._identity.category_code = 'refln' - self._identity.category_entry_name = lambda: str(self.id.value) - # ------------------------------------------------------------------ # Public properties # ------------------------------------------------------------------ @@ -271,7 +271,7 @@ def _create_items_set_hkl_and_id( # TODO: split into multiple methods # Create items - self._items = [self._item_type() for _ in range(indices_h.size)] + self._adopt_items([self._item_type() for _ in range(indices_h.size)]) # Set indices for item, index_h, index_k, index_l in zip( diff --git a/src/easydiffraction/datablocks/experiment/collection.py b/src/easydiffraction/datablocks/experiment/collection.py index 16bfe5a49..65646d506 100644 --- a/src/easydiffraction/datablocks/experiment/collection.py +++ b/src/easydiffraction/datablocks/experiment/collection.py @@ -125,7 +125,7 @@ def add_from_data_path( scattering_type : str | None, default=None Scattering type (e.g. ``'bragg'``). """ - verbosity = self._parent.verbosity if self._parent is not None else None + verbosity = self._parent.verbosity.fit.value if self._parent is not None else None verb = VerbosityEnum(verbosity) if verbosity is not None else VerbosityEnum.FULL experiment = ExperimentFactory.from_scratch( name=name, diff --git a/src/easydiffraction/datablocks/structure/categories/atom_site_aniso/default.py b/src/easydiffraction/datablocks/structure/categories/atom_site_aniso/default.py index fbb7dedf9..acc422b02 100644 --- a/src/easydiffraction/datablocks/structure/categories/atom_site_aniso/default.py +++ b/src/easydiffraction/datablocks/structure/categories/atom_site_aniso/default.py @@ -32,6 +32,9 @@ class AtomSiteAniso(CategoryItem): ``atom_site.adp_type``. """ + _category_code = 'atom_site_aniso' + _category_entry_name = 'label' + def __init__(self) -> None: """Initialise with default zero-valued tensor components.""" super().__init__() @@ -134,9 +137,6 @@ def __init__(self) -> None: ), ) - self._identity.category_code = 'atom_site_aniso' - self._identity.category_entry_name = lambda: str(self.label.value) - # ------------------------------------------------------------------ # Public properties # ------------------------------------------------------------------ diff --git a/src/easydiffraction/datablocks/structure/categories/atom_sites/default.py b/src/easydiffraction/datablocks/structure/categories/atom_sites/default.py index 562ad18d6..2ac4b1d3e 100644 --- a/src/easydiffraction/datablocks/structure/categories/atom_sites/default.py +++ b/src/easydiffraction/datablocks/structure/categories/atom_sites/default.py @@ -36,6 +36,9 @@ class AtomSite(CategoryItem): CIF serialization. """ + _category_code = 'atom_site' + _category_entry_name = 'label' + def __init__(self) -> None: """Initialise the atom site with default descriptor values.""" super().__init__() @@ -139,9 +142,6 @@ def __init__(self) -> None: 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 # ------------------------------------------------------------------ @@ -558,8 +558,8 @@ def _apply_atomic_coordinates_symmetry_constraints(self) -> None: 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. Coordinates fully - determined by site symmetry are flagged as ``symmetry_fixed`` so - they cannot be marked refinable. + determined by site symmetry are flagged as + ``symmetry_constrained`` so they cannot be marked refinable. """ structure = self._parent space_group_name = structure.space_group.name_h_m.value @@ -568,7 +568,7 @@ def _apply_atomic_coordinates_symmetry_constraints(self) -> None: wl = atom.wyckoff_letter.value if not wl: # TODO: Decide how to handle this case - self._clear_fract_symmetry_fixed(atom) + self._clear_fract_symmetry_constrained(atom) continue dummy_atom = { 'fract_x': atom.fract_x.value, @@ -581,7 +581,7 @@ def _apply_atomic_coordinates_symmetry_constraints(self) -> None: coord_code=space_group_coord_code, wyckoff_letter=wl, ) - fixed_flags = ecr.atom_site_symmetry_fixed_flags( + constrained_flags = ecr.atom_site_symmetry_constrained_flags( name_hm=space_group_name, coord_code=space_group_coord_code, wyckoff_letter=wl, @@ -589,17 +589,17 @@ def _apply_atomic_coordinates_symmetry_constraints(self) -> None: atom.fract_x.value = dummy_atom['fract_x'] atom.fract_y.value = dummy_atom['fract_y'] atom.fract_z.value = dummy_atom['fract_z'] - atom._fract_x._set_symmetry_fixed(value=fixed_flags['fract_x']) - atom._fract_y._set_symmetry_fixed(value=fixed_flags['fract_y']) - atom._fract_z._set_symmetry_fixed(value=fixed_flags['fract_z']) + atom._fract_x._set_symmetry_constrained(value=constrained_flags['fract_x']) + atom._fract_y._set_symmetry_constrained(value=constrained_flags['fract_y']) + atom._fract_z._set_symmetry_constrained(value=constrained_flags['fract_z']) @staticmethod - def _clear_fract_symmetry_fixed(atom: AtomSite) -> None: + def _clear_fract_symmetry_constrained(atom: AtomSite) -> None: """ - Reset the ``symmetry_fixed`` flag on all fract coordinates. + Clear fractional-coordinate symmetry constraints. """ for axis_param in (atom._fract_x, atom._fract_y, atom._fract_z): - axis_param._set_symmetry_fixed(value=False) + axis_param._set_symmetry_constrained(value=False) def _apply_adp_symmetry_constraints(self) -> None: """ @@ -607,9 +607,9 @@ def _apply_adp_symmetry_constraints(self) -> None: For each atom with an anisotropic ADP type and a Wyckoff letter, enforces the tensor constraints dictated by the site symmetry. - Tensor components fixed by symmetry are flagged as - ``symmetry_fixed`` (which also forces ``free = False``), and - ``adp_iso`` is flagged as fixed for all anisotropic atoms. + Tensor components constrained by symmetry are flagged as + ``symmetry_constrained`` (which also forces ``free = False``), + and ``adp_iso`` is flagged as fixed for all anisotropic atoms. """ structure = self._parent aniso_types = {AdpTypeEnum.BANI.value, AdpTypeEnum.UANI.value} @@ -620,7 +620,7 @@ def _apply_adp_symmetry_constraints(self) -> None: for atom in self._items: is_aniso = atom.adp_type.value in aniso_types # Isotropic ADP is not refinable for aniso atoms - atom._adp_iso._set_symmetry_fixed(value=is_aniso) + atom._adp_iso._set_symmetry_constrained(value=is_aniso) if not is_aniso: continue wl = atom.wyckoff_letter.value @@ -654,7 +654,7 @@ def _apply_adp_symmetry_constraints(self) -> None: for key, is_free in zip(adp_keys, ref_i, strict=False): param = getattr(aniso_entry, key) param.value = dummy[key] - param._set_symmetry_fixed(value=not is_free) + param._set_symmetry_constrained(value=not is_free) def _sync_iso_from_aniso(self) -> None: """ diff --git a/src/easydiffraction/datablocks/structure/categories/cell/default.py b/src/easydiffraction/datablocks/structure/categories/cell/default.py index 4c02ab713..9b2980082 100644 --- a/src/easydiffraction/datablocks/structure/categories/cell/default.py +++ b/src/easydiffraction/datablocks/structure/categories/cell/default.py @@ -23,6 +23,8 @@ class Cell(CategoryItem): descriptors supporting validation, fitting and CIF serialization. """ + _category_code = 'cell' + type_info = TypeInfo( tag='default', description='Unit cell parameters', @@ -93,8 +95,6 @@ def __init__(self) -> None: cif_handler=CifHandler(names=['_cell.angle_gamma']), ) - self._identity.category_code = 'cell' - # ------------------------------------------------------------------ # Private helper methods # ------------------------------------------------------------------ @@ -106,7 +106,7 @@ def _apply_cell_symmetry_constraints(self) -> None: Uses the parent structure's space-group symbol to determine which lattice parameters are dependent and sets them accordingly. Dependent parameters are also flagged as - ``symmetry_fixed`` so they cannot be marked refinable. + ``symmetry_constrained`` so they cannot be marked refinable. """ dummy_cell = { 'lattice_a': self.length_a.value, @@ -122,7 +122,7 @@ def _apply_cell_symmetry_constraints(self) -> None: cell=dummy_cell, name_hm=space_group_name, ) - fixed_flags = ecr.cell_symmetry_fixed_flags(name_hm=space_group_name) + constrained_flags = ecr.cell_symmetry_constrained_flags(name_hm=space_group_name) param_by_key = { 'lattice_a': self._length_a, @@ -134,7 +134,7 @@ def _apply_cell_symmetry_constraints(self) -> None: } for key, param in param_by_key.items(): param.value = dummy_cell[key] - param._set_symmetry_fixed(value=fixed_flags[key]) + param._set_symmetry_constrained(value=constrained_flags[key]) def _update( self, diff --git a/src/easydiffraction/datablocks/structure/categories/space_group/default.py b/src/easydiffraction/datablocks/structure/categories/space_group/default.py index e04015f24..bb727ac43 100644 --- a/src/easydiffraction/datablocks/structure/categories/space_group/default.py +++ b/src/easydiffraction/datablocks/structure/categories/space_group/default.py @@ -30,6 +30,8 @@ class SpaceGroup(CategoryItem): to the first allowed value for the new group. """ + _category_code = 'space_group' + type_info = TypeInfo( tag='default', description='Space group symmetry', @@ -77,8 +79,6 @@ def __init__(self) -> None: ), ) - self._identity.category_code = 'space_group' - # ------------------------------------------------------------------ # Private helper methods # ------------------------------------------------------------------ diff --git a/src/easydiffraction/display/plotters/ascii.py b/src/easydiffraction/display/plotters/ascii.py index a0c95fe11..625b2e2fd 100644 --- a/src/easydiffraction/display/plotters/ascii.py +++ b/src/easydiffraction/display/plotters/ascii.py @@ -10,6 +10,8 @@ from __future__ import annotations +import shutil + import asciichartpy import numpy as np @@ -22,13 +24,88 @@ DEFAULT_COLORS = { 'meas': asciichartpy.blue, 'calc': asciichartpy.red, + 'posterior': asciichartpy.red, + 'density': asciichartpy.green, 'resid': asciichartpy.green, } +ASCII_CHART_OFFSET = 3 +ASCII_CHART_LEFT_PADDING = 15 +ASCII_CHART_FALLBACK_POINT_COUNT = 80 +ASCII_CHART_MIN_POINT_COUNT = 2 +ASCII_CHART_CROP_TRIGGER_MULTIPLIER = 2 class AsciiPlotter(PlotterBase): """Terminal-based plotter using ASCII art.""" + @staticmethod + def _chart_point_count() -> int: + """Return the number of points that fit the current terminal.""" + fallback_columns = ( + ASCII_CHART_FALLBACK_POINT_COUNT + ASCII_CHART_OFFSET + ASCII_CHART_LEFT_PADDING + ) + columns = shutil.get_terminal_size(fallback=(fallback_columns, DEFAULT_HEIGHT)).columns + return max( + ASCII_CHART_MIN_POINT_COUNT, + columns - ASCII_CHART_OFFSET - ASCII_CHART_LEFT_PADDING, + ) + + @classmethod + def _resample_series_for_chart( + cls, + y_series: object, + ) -> list[list[float]]: + """Return y-series adapted to the available chart width.""" + target_point_count = cls._chart_point_count() + series_arrays = [np.ravel(np.asarray(series, dtype=float)) for series in y_series] + if not series_arrays: + return [] + + reference_array = series_arrays[0] + if cls._should_crop_to_peak_window(reference_array.size): + start, end = cls._peak_window_bounds(reference_array) + return [series_array[start:end].tolist() for series_array in series_arrays] + + resampled_series: list[list[float]] = [] + for series_array in series_arrays: + if series_array.size == 0: + resampled_series.append([0.0] * target_point_count) + continue + if series_array.size == 1: + resampled_series.append([float(series_array[0])] * target_point_count) + continue + + source_positions = np.linspace(0.0, 1.0, series_array.size) + target_positions = np.linspace(0.0, 1.0, target_point_count) + resampled_series.append( + np.interp(target_positions, source_positions, series_array).tolist() + ) + return resampled_series + + @classmethod + def _should_crop_to_peak_window( + cls, + point_count: int, + ) -> bool: + """Return whether a peak-centred viewport should be used.""" + return point_count > cls._chart_point_count() * ASCII_CHART_CROP_TRIGGER_MULTIPLIER + + @classmethod + def _peak_window_bounds( + cls, + y_array: np.ndarray, + ) -> tuple[int, int]: + """Return start/end indices for a peak-centred chart window.""" + target_point_count = cls._chart_point_count() + if y_array.size <= target_point_count: + return 0, y_array.size + + peak_index = int(np.argmax(np.nan_to_num(y_array, nan=float('-inf')))) + start = max(0, peak_index - target_point_count // 2) + end = min(y_array.size, start + target_point_count) + start = max(0, end - target_point_count) + return start, end + @staticmethod def _get_legend_item(label: str) -> str: """ @@ -61,6 +138,7 @@ def plot_powder( axes_labels: object, title: str, height: int | None = None, + excluded_ranges: tuple[tuple[float, float], ...] = (), ) -> None: """ Render a line plot for powder diffraction data. @@ -83,6 +161,8 @@ def plot_powder( Figure title printed above the chart. height : int | None, default=None Number of text rows to allocate for the chart. + excluded_ranges : tuple[tuple[float, float], ...], default=() + Excluded x-ranges to print below the selected x-range. """ # Intentionally unused; kept for a consistent display API del axes_labels @@ -91,8 +171,12 @@ def plot_powder( if height is None: height = DEFAULT_HEIGHT colors = [DEFAULT_COLORS[label] for label in labels] - config = {'height': height, 'colors': colors} - y_series = [y.tolist() for y in y_series] + config = { + 'height': height, + 'colors': colors, + 'offset': ASCII_CHART_OFFSET, + } + y_series = self._resample_series_for_chart(y_series) chart = asciichartpy.plot(y_series, config) @@ -100,6 +184,11 @@ def plot_powder( console.print( f'Displaying data for selected x-range from {x[0]} to {x[-1]} ({len(x)} points)' ) + if excluded_ranges: + formatted_ranges = ', '.join( + f'[{start:,.2f}, {end:,.2f}]' for start, end in excluded_ranges + ) + console.print(f'Excluded regions: {formatted_ranges}') console.print(f'Legend:\n{legend}') padded = '\n'.join(' ' + line for line in chart.splitlines()) @@ -130,12 +219,15 @@ def plot_powder_meas_vs_calc( axes_labels=plot_spec.axes_labels, title=plot_spec.title, height=plot_spec.height, + excluded_ranges=plot_spec.excluded_ranges, ) + if plot_spec.predictive_lower_95 is not None and plot_spec.predictive_upper_95 is not None: + console.print('Posterior predictive bands are available with the Plotly engine only.') if plot_spec.bragg_tick_sets: console.print('Bragg peak subplot rows are available with the Plotly engine only.') - @staticmethod def plot_single_crystal( + self, x_calc: object, y_meas: object, y_meas_su: object, @@ -170,7 +262,7 @@ def plot_single_crystal( if height is None: height = DEFAULT_HEIGHT - width = 60 # TODO: Make width configurable + width = self._chart_point_count() # Determine axis limits vmin = float(min(np.min(y_meas), np.min(x_calc))) @@ -213,8 +305,8 @@ def plot_single_crystal( print(f' {x_axis}') console.print(f'{" " * (width - 3)}{axes_labels[0]}') - @staticmethod def plot_scatter( + self, x: object, y: object, sy: object, @@ -223,13 +315,20 @@ def plot_scatter( 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 + _ = sy # ASCII backend does not use error bars if height is None: height = DEFAULT_HEIGHT + x_array = np.ravel(np.asarray(x, dtype=float)) + y_array = np.ravel(np.asarray(y, dtype=float)) + if x_array.size == y_array.size and x_array.size > 1: + order = np.argsort(x_array, kind='stable') + y_array = y_array[order] + + y_series = self._resample_series_for_chart([y_array]) config = {'height': height, 'colors': [asciichartpy.blue]} - chart = asciichartpy.plot([list(y)], config) + chart = asciichartpy.plot(y_series, config) console.paragraph(f'{title}') console.print(f'{axes_labels[1]} vs {axes_labels[0]}') diff --git a/src/easydiffraction/display/plotters/base.py b/src/easydiffraction/display/plotters/base.py index b1a19a274..a080ba8e7 100644 --- a/src/easydiffraction/display/plotters/base.py +++ b/src/easydiffraction/display/plotters/base.py @@ -60,6 +60,12 @@ class PowderMeasVsCalcSpec: bragg_peaks_height_fraction: float height: int | None = None y_bkg: np.ndarray | None = None + predictive_lower_95: np.ndarray | None = None + predictive_upper_95: np.ndarray | None = None + predictive_draws: np.ndarray | None = None + y_calc_name: str | None = None + y_calc_line_dash: str | None = None + excluded_ranges: tuple[tuple[float, float], ...] = () class XAxisType(StrEnum): @@ -185,6 +191,14 @@ class XAxisType(StrEnum): 'mode': 'lines', 'name': 'Total calculated (Icalc)', }, + 'posterior': { + 'mode': 'lines', + 'name': 'Best posterior sample', + }, + 'density': { + 'mode': 'lines', + 'name': 'Marginal density', + }, 'bkg': { 'mode': 'lines', 'name': 'Background (Ibkg)', @@ -224,6 +238,7 @@ def plot_powder( axes_labels: object, title: str, height: int | None, + excluded_ranges: tuple[tuple[float, float], ...] = (), ) -> None: """ Render a line plot for powder diffraction data. @@ -245,6 +260,8 @@ def plot_powder( Figure title. height : int | None Backend-specific height (text rows or pixels). + excluded_ranges : tuple[tuple[float, float], ...], default=() + Closed x-intervals to highlight as excluded regions. """ @abstractmethod diff --git a/src/easydiffraction/display/plotters/plotly.py b/src/easydiffraction/display/plotters/plotly.py index 887879dd3..afa99da90 100644 --- a/src/easydiffraction/display/plotters/plotly.py +++ b/src/easydiffraction/display/plotters/plotly.py @@ -38,6 +38,7 @@ 'meas': 'rgb(31, 119, 180)', 'bkg': 'rgb(140, 140, 140)', 'calc': 'rgb(214, 39, 40)', + 'posterior': 'rgb(214, 39, 40)', 'resid': 'rgb(44, 160, 44)', } @@ -65,6 +66,16 @@ COMPOSITE_MARGIN_RIGHT = 30 COMPOSITE_MARGIN_TOP = 40 COMPOSITE_MARGIN_BOTTOM = 45 +TITLE_FONT_SIZE = 14 +AXIS_TITLE_FONT_SIZE = 12 +PREDICTIVE_BAND_COLOR = 'rgba(214, 39, 40, 0.14)' +PREDICTIVE_BAND_EDGE_COLOR = 'rgba(214, 39, 40, 0.45)' +PREDICTIVE_DRAW_COLOR = 'rgba(140, 140, 140, 0.18)' +EXCLUDED_REGION_FILL_COLOR = 'rgba(120, 120, 120, 0.16)' +PREDICTIVE_DRAW_PLOT_CAP = 50 +PREDICTIVE_DRAW_ARRAY_NDIM = 2 +FIXED_ASPECT_WRAPPER_META_KEY = 'fixed_aspect_wrapper' +FIXED_ASPECT_WRAPPER_CLASS_NAME = 'ed-fixed-aspect-plotly-wrapper' @dataclass(frozen=True) @@ -157,6 +168,11 @@ def _correlation_grid_color(cls) -> str: return 'rgba(110, 145, 190, 0.35)' return 'rgba(120, 140, 160, 0.28)' + @classmethod + def _axis_frame_color(cls) -> str: + """Return the shared axis-frame color for Plotly figures.""" + return cls._correlation_grid_color() + @classmethod def _legend_background_color(cls) -> str: """Return a half-transparent legend background color.""" @@ -208,7 +224,7 @@ def plot_correlation_heatmap( 'yanchor': 'middle', }, hoverongaps=False, - hovertemplate=f'x: %{{x}}
y: %{{y}}
corr: %{{z:.{precision}f}}', + hovertemplate=f'%{{x}}
%{{y}}
correlation: %{{z:.{precision}f}}', ) label_trace = self._get_correlation_label_trace( corr_df, @@ -461,12 +477,13 @@ def _powder_meas_vs_calc_hover_template(plot_spec: PowderMeasVsCalcSpec) -> str: """ Return a shared hover template for composite powder traces. """ + calc_label = plot_spec.y_calc_name or 'Icalc' if plot_spec.y_bkg is None: return ( 'x: %{x:,.2f}
' 'Imeas: %{customdata[0]:,.2f}
' - 'Icalc: %{customdata[1]:,.2f}
' - 'Imeas - Icalc: %{customdata[2]:,.2f}' + f'{calc_label}: %{{customdata[1]:,.2f}}
' + f'Imeas - {calc_label}: %{{customdata[2]:,.2f}}' '' ) @@ -474,8 +491,8 @@ def _powder_meas_vs_calc_hover_template(plot_spec: PowderMeasVsCalcSpec) -> str: 'x: %{x:,.2f}
' 'Imeas: %{customdata[0]:,.2f}
' 'Ibkg: %{customdata[1]:,.2f}
' - 'Icalc: %{customdata[2]:,.2f}
' - 'Imeas - Icalc: %{customdata[3]:,.2f}' + f'{calc_label}: %{{customdata[2]:,.2f}}
' + f'Imeas - {calc_label}: %{{customdata[3]:,.2f}}' '' ) @@ -778,6 +795,90 @@ def _modebar_legend_toggle_post_script() -> str: window.requestAnimationFrame(installLegendToggleButton); """ + @classmethod + def _html_post_script(cls, fig: object) -> str | None: + """Return concatenated HTML post scripts for a Plotly figure.""" + scripts: list[str] = [] + if cls._has_visible_legend(fig): + scripts.append(cls._modebar_legend_toggle_post_script()) + if not scripts: + return None + return '\n'.join(cls._scoped_html_post_script(script) for script in scripts) + + @staticmethod + def _scoped_html_post_script(script: str) -> str: + """ + Return one HTML post script wrapped in its own block scope. + """ + return '{\n' + script.strip() + '\n}' + + @staticmethod + def _figure_meta(fig: object) -> dict[str, object] | None: + """Return figure layout metadata when available.""" + layout = getattr(fig, 'layout', None) + if layout is None: + return None + + meta = getattr(layout, 'meta', None) + if isinstance(meta, dict): + return meta + + layout_kwargs = getattr(layout, 'kwargs', None) + if isinstance(layout_kwargs, dict): + meta = layout_kwargs.get('meta') + if isinstance(meta, dict): + return meta + return None + + @classmethod + def _fixed_aspect_wrapper_aspect_ratio(cls, fig: object) -> str | None: + """Return the fixed aspect ratio requested for inline HTML.""" + meta = cls._figure_meta(fig) + if not isinstance(meta, dict): + return None + + wrapper = meta.get(FIXED_ASPECT_WRAPPER_META_KEY) + if not isinstance(wrapper, dict): + return None + + aspect_ratio = wrapper.get('aspect_ratio') + if not isinstance(aspect_ratio, str): + return None + + aspect_ratio = aspect_ratio.strip() + if not aspect_ratio: + return None + return aspect_ratio + + @classmethod + def _wrap_html_figure(cls, fig: object, html_fig: str) -> str: + """Wrap inline Plotly HTML in a fixed-aspect container.""" + aspect_ratio = cls._fixed_aspect_wrapper_aspect_ratio(fig) + if aspect_ratio is None: + return html_fig + + return ( + '\n\n' + f'
\n' + f'{html_fig}\n' + '
' + ) + @staticmethod def _get_figure( data: object, @@ -855,9 +956,7 @@ def _show_figure( if in_pycharm() or display is None or HTML is None: fig.show(config=config) else: - post_script = None - if self._has_visible_legend(fig): - post_script = self._modebar_legend_toggle_post_script() + post_script = self._html_post_script(fig) html_fig = pio.to_html( fig, include_plotlyjs='cdn', @@ -865,6 +964,7 @@ def _show_figure( config=config, post_script=post_script, ) + html_fig = self._wrap_html_figure(fig, html_fig) display(HTML(html_fig)) @classmethod @@ -900,6 +1000,7 @@ def _get_layout( }, title={ 'text': title, + 'font': {'size': TITLE_FONT_SIZE}, }, legend={ 'bgcolor': cls._legend_background_color(), @@ -909,14 +1010,22 @@ def _get_layout( 'y': 1.0, }, xaxis={ - 'title_text': axes_labels[0], + 'title': { + 'text': axes_labels[0], + 'font': {'size': AXIS_TITLE_FONT_SIZE}, + }, 'showline': True, + 'linecolor': cls._axis_frame_color(), 'mirror': True, 'zeroline': False, }, yaxis={ - 'title_text': axes_labels[1], + 'title': { + 'text': axes_labels[1], + 'font': {'size': AXIS_TITLE_FONT_SIZE}, + }, 'showline': True, + 'linecolor': cls._axis_frame_color(), 'mirror': True, 'zeroline': False, }, @@ -931,6 +1040,7 @@ def plot_powder( axes_labels: object, title: str, height: int | None = None, + excluded_ranges: tuple[tuple[float, float], ...] = (), ) -> None: """ Render a line plot for powder diffraction data. @@ -952,6 +1062,8 @@ def plot_powder( Figure title. height : int | None, default=None Ignored; Plotly auto-sizes based on renderer. + excluded_ranges : tuple[tuple[float, float], ...], default=() + Excluded x-ranges to shade on the figure. """ # Intentionally unused; accepted for API compatibility del height @@ -968,8 +1080,33 @@ def plot_powder( ) fig = self._get_figure(data, layout) + self._add_excluded_region_vrects(fig=fig, excluded_ranges=excluded_ranges) self._show_figure(fig) + @staticmethod + def _add_excluded_region_vrects( + *, + fig: object, + excluded_ranges: tuple[tuple[float, float], ...], + row: object | None = None, + col: int | None = None, + ) -> None: + """Shade excluded x-ranges on a Plotly figure.""" + for start, end in excluded_ranges: + add_kwargs = { + 'x0': start, + 'x1': end, + 'fillcolor': EXCLUDED_REGION_FILL_COLOR, + 'opacity': 1.0, + 'line_width': 0, + 'layer': 'below', + } + if row is not None: + add_kwargs['row'] = row + if col is not None: + add_kwargs['col'] = col + fig.add_vrect(**add_kwargs) + @staticmethod def _get_bragg_tick_trace( tick_set: BraggTickSet, @@ -1165,11 +1302,7 @@ def _get_main_intensity_range(cls, plot_spec: PowderMeasVsCalcSpec) -> tuple[flo if min(y_meas.size, y_calc.size) == 0: return 0.0, 1.0 - main_series = [y_meas, y_calc] - if plot_spec.y_bkg is not None: - y_bkg = np.asarray(plot_spec.y_bkg) - if y_bkg.size > 0: - main_series.append(y_bkg) + main_series = cls._main_intensity_series(plot_spec, y_meas=y_meas, y_calc=y_calc) main_y_min = float(min(np.min(series) for series in main_series)) main_y_max = float(max(np.max(series) for series in main_series)) @@ -1180,6 +1313,49 @@ def _get_main_intensity_range(cls, plot_spec: PowderMeasVsCalcSpec) -> tuple[flo return main_y_min - 1.0, main_y_max + 1.0 + @classmethod + def _main_intensity_series( + cls, + plot_spec: PowderMeasVsCalcSpec, + *, + y_meas: np.ndarray, + y_calc: np.ndarray, + ) -> list[np.ndarray]: + main_series = [y_meas, y_calc] + for values in ( + plot_spec.y_bkg, + plot_spec.predictive_lower_95, + plot_spec.predictive_upper_95, + ): + cls._append_non_empty_series(main_series, values) + + predictive_draws = cls._predictive_draw_array(plot_spec.predictive_draws) + if predictive_draws is not None: + main_series.extend(predictive_draws) + return main_series + + @staticmethod + def _append_non_empty_series( + main_series: list[np.ndarray], + values: np.ndarray | None, + ) -> None: + if values is None: + return + + array = np.asarray(values) + if array.size > 0: + main_series.append(array) + + @staticmethod + def _predictive_draw_array(values: object | None) -> np.ndarray | None: + if values is None: + return None + + predictive_draws = np.asarray(values) + if predictive_draws.ndim != PREDICTIVE_DRAW_ARRAY_NDIM or predictive_draws.size == 0: + return None + return predictive_draws + @classmethod def _get_residual_limit(cls, plot_spec: PowderMeasVsCalcSpec) -> float: """Return a symmetric residual limit matched to the main row.""" @@ -1221,11 +1397,46 @@ def plot_powder_meas_vs_calc( layout = self._get_powder_composite_rows(plot_spec) x_min, x_max = self._composite_x_range(np.asarray(plot_spec.x)) main_y_min, main_y_max = self._get_main_intensity_range(plot_spec) - residual_limit = None hover_data = self._powder_meas_vs_calc_hover_data(plot_spec) hover_template = self._powder_meas_vs_calc_hover_template(plot_spec) + fig = self._create_powder_composite_figure(layout) + self._add_predictive_band_traces(fig=fig, plot_spec=plot_spec) + self._add_main_intensity_traces( + fig=fig, + plot_spec=plot_spec, + hover_data=hover_data, + hover_template=hover_template, + ) + self._add_predictive_draw_traces(fig=fig, plot_spec=plot_spec) + self._add_bragg_tick_traces(fig=fig, plot_spec=plot_spec, layout=layout) + residual_limit = self._add_residual_trace( + fig=fig, + plot_spec=plot_spec, + layout=layout, + hover_data=hover_data, + hover_template=hover_template, + ) + self._add_excluded_region_vrects( + fig=fig, + excluded_ranges=plot_spec.excluded_ranges, + row='all', + col=1, + ) + self._configure_powder_composite_layout(fig=fig, plot_spec=plot_spec, layout=layout) + self._configure_powder_composite_axes( + fig=fig, + plot_spec=plot_spec, + layout=layout, + x_range=(x_min, x_max), + main_y_range=(main_y_min, main_y_max), + residual_limit=residual_limit, + ) + + self._show_figure(fig) - fig = make_subplots( + @staticmethod + def _create_powder_composite_figure(layout: PowderCompositeRows) -> object: + return make_subplots( rows=layout.row_count, cols=1, shared_xaxes=True, @@ -1233,58 +1444,146 @@ def plot_powder_meas_vs_calc( row_heights=layout.row_heights, ) - main_traces = ( - ( - ('meas', plot_spec.y_meas), - ('bkg', plot_spec.y_bkg), - ('calc', plot_spec.y_calc), - ) - if plot_spec.y_bkg is not None - else ( - ('meas', plot_spec.y_meas), - ('calc', plot_spec.y_calc), + def _add_predictive_band_traces( + self, + *, + fig: object, + plot_spec: PowderMeasVsCalcSpec, + ) -> None: + if plot_spec.predictive_lower_95 is None or plot_spec.predictive_upper_95 is None: + return + + lower_trace, upper_trace = self._get_predictive_band_traces( + x=plot_spec.x, + lower=plot_spec.predictive_lower_95, + upper=plot_spec.predictive_upper_95, + ) + fig.add_trace(lower_trace, row=1, col=1) + fig.add_trace(upper_trace, row=1, col=1) + + def _add_main_intensity_traces( + self, + *, + fig: object, + plot_spec: PowderMeasVsCalcSpec, + hover_data: object, + hover_template: str, + ) -> None: + meas_trace = self._get_powder_trace( + plot_spec.x, + plot_spec.y_meas, + 'meas', + customdata=hover_data, + hovertemplate=hover_template, + ) + fig.add_trace(meas_trace, row=1, col=1) + + if plot_spec.y_bkg is not None: + bkg_trace = self._get_powder_trace( + plot_spec.x, + plot_spec.y_bkg, + 'bkg', + customdata=hover_data, + hovertemplate=hover_template, ) + fig.add_trace(bkg_trace, row=1, col=1) + + calc_trace = self._get_powder_trace( + plot_spec.x, + plot_spec.y_calc, + 'calc', + customdata=hover_data, + hovertemplate=hover_template, ) - for label, y_values in main_traces: + if plot_spec.y_calc_name is not None: + calc_trace.name = plot_spec.y_calc_name + if plot_spec.y_calc_line_dash is not None: + calc_trace.line.dash = plot_spec.y_calc_line_dash + fig.add_trace(calc_trace, row=1, col=1) + + def _add_predictive_draw_traces( + self, + *, + fig: object, + plot_spec: PowderMeasVsCalcSpec, + ) -> None: + predictive_draws = self._predictive_draw_array(plot_spec.predictive_draws) + if predictive_draws is None: + return + + draw_cap = min(predictive_draws.shape[0], PREDICTIVE_DRAW_PLOT_CAP) + for index in range(draw_cap): fig.add_trace( - self._get_powder_trace( - plot_spec.x, - y_values, - label, - customdata=hover_data, - hovertemplate=hover_template, + go.Scatter( + x=plot_spec.x, + y=predictive_draws[index], + mode='lines', + line={'color': PREDICTIVE_DRAW_COLOR, 'width': 1}, + name='Posterior draw' if index == 0 else None, + showlegend=index == 0, + hovertemplate=( + 'Posterior draw
x: %{x:,.2f}
y: %{y:,.2f}' + ), ), row=1, col=1, ) - if layout.bragg_row is not None: - for idx, tick_set in enumerate(plot_spec.bragg_tick_sets): - color = BRAGG_TICK_COLORS[idx % len(BRAGG_TICK_COLORS)] - fig.add_trace( - self._get_bragg_tick_trace( - tick_set=tick_set, - row_y=float(idx + 1), - color=color, - ), - row=layout.bragg_row, - col=1, - ) + def _add_bragg_tick_traces( + self, + *, + fig: object, + plot_spec: PowderMeasVsCalcSpec, + layout: PowderCompositeRows, + ) -> None: + if layout.bragg_row is None: + return - if layout.residual_row is not None and plot_spec.y_resid is not None: - residual_limit = self._get_residual_limit(plot_spec) + for idx, tick_set in enumerate(plot_spec.bragg_tick_sets): + color = BRAGG_TICK_COLORS[idx % len(BRAGG_TICK_COLORS)] fig.add_trace( - self._get_powder_trace( - plot_spec.x, - plot_spec.y_resid, - 'resid', - customdata=hover_data, - hovertemplate=hover_template, + self._get_bragg_tick_trace( + tick_set=tick_set, + row_y=float(idx + 1), + color=color, ), - row=layout.residual_row, + row=layout.bragg_row, col=1, ) + def _add_residual_trace( + self, + *, + fig: object, + plot_spec: PowderMeasVsCalcSpec, + layout: PowderCompositeRows, + hover_data: object, + hover_template: str, + ) -> float | None: + if layout.residual_row is None or plot_spec.y_resid is None: + return None + + residual_limit = self._get_residual_limit(plot_spec) + fig.add_trace( + self._get_powder_trace( + plot_spec.x, + plot_spec.y_resid, + 'resid', + customdata=hover_data, + hovertemplate=hover_template, + ), + row=layout.residual_row, + col=1, + ) + return residual_limit + + def _configure_powder_composite_layout( + self, + *, + fig: object, + plot_spec: PowderMeasVsCalcSpec, + layout: PowderCompositeRows, + ) -> None: fig.update_layout( height=self._composite_figure_height(plot_spec, layout), margin={ @@ -1293,7 +1592,10 @@ def plot_powder_meas_vs_calc( 't': COMPOSITE_MARGIN_TOP, 'b': COMPOSITE_MARGIN_BOTTOM, }, - title={'text': plot_spec.title}, + title={ + 'text': plot_spec.title, + 'font': {'size': TITLE_FONT_SIZE}, + }, legend={ 'bgcolor': self._legend_background_color(), 'xanchor': 'right', @@ -1303,10 +1605,64 @@ def plot_powder_meas_vs_calc( }, ) - for row_idx in range(1, layout.row_count + 1): + def _configure_powder_composite_axes( + self, + *, + fig: object, + plot_spec: PowderMeasVsCalcSpec, + layout: PowderCompositeRows, + x_range: tuple[float | None, float | None], + main_y_range: tuple[float, float], + residual_limit: float | None, + ) -> None: + self._configure_shared_composite_axes( + fig=fig, + row_count=layout.row_count, + x_min=x_range[0], + x_max=x_range[1], + ) + fig.update_xaxes(showticklabels=(layout.row_count == 1), row=1, col=1) + fig.update_yaxes( + title_text=plot_spec.axes_labels[1], + title_font={'size': AXIS_TITLE_FONT_SIZE}, + range=list(main_y_range), + row=1, + col=1, + ) + + if layout.bragg_row is not None: + self._configure_bragg_axes(fig=fig, plot_spec=plot_spec, layout=layout) + if layout.residual_row is not None and residual_limit is not None: + self._configure_residual_axes( + fig=fig, + plot_spec=plot_spec, + layout=layout, + residual_limit=residual_limit, + ) + return + + terminal_row = layout.bragg_row if layout.bragg_row is not None else 1 + fig.update_xaxes( + title_text=plot_spec.axes_labels[0], + title_font={'size': AXIS_TITLE_FONT_SIZE}, + row=terminal_row, + col=1, + ) + + def _configure_shared_composite_axes( + self, + *, + fig: object, + row_count: int, + x_min: float | None, + x_max: float | None, + ) -> None: + axis_frame_color = self._axis_frame_color() + for row_idx in range(1, row_count + 1): x_axis_kwargs = { 'matches': 'x', 'showline': True, + 'linecolor': axis_frame_color, 'mirror': True, 'zeroline': False, 'tickformat': ',.6~g', @@ -1317,6 +1673,7 @@ def plot_powder_meas_vs_calc( fig.update_xaxes(row=row_idx, col=1, **x_axis_kwargs) fig.update_yaxes( showline=True, + linecolor=axis_frame_color, mirror=True, zeroline=False, tickformat=',.6~g', @@ -1325,50 +1682,86 @@ def plot_powder_meas_vs_calc( col=1, ) - fig.update_xaxes(showticklabels=(layout.row_count == 1), row=1, col=1) + @staticmethod + def _configure_bragg_axes( + *, + fig: object, + plot_spec: PowderMeasVsCalcSpec, + layout: PowderCompositeRows, + ) -> None: fig.update_yaxes( - title_text=plot_spec.axes_labels[1], - range=[main_y_min, main_y_max], - row=1, + tickmode='array', + tickvals=[float(idx + 1) for idx in range(len(plot_spec.bragg_tick_sets))], + ticktext=[tick_set.phase_id for tick_set in plot_spec.bragg_tick_sets], + range=[float(len(plot_spec.bragg_tick_sets)) + 0.5, 0.5], + showgrid=False, + row=layout.bragg_row, + col=1, + ) + fig.update_xaxes( + showticklabels=layout.residual_row is None, + row=layout.bragg_row, col=1, ) - if layout.bragg_row is not None: - fig.update_yaxes( - # title_text='Bragg peaks', - tickmode='array', - tickvals=[float(idx + 1) for idx in range(len(plot_spec.bragg_tick_sets))], - ticktext=[tick_set.phase_id for tick_set in plot_spec.bragg_tick_sets], - range=[float(len(plot_spec.bragg_tick_sets)) + 0.5, 0.5], - showgrid=False, - row=layout.bragg_row, - col=1, - ) - fig.update_xaxes( - showticklabels=layout.residual_row is None, - row=layout.bragg_row, - col=1, - ) - - if layout.residual_row is not None and plot_spec.y_resid is not None: - residual_tick_limit = self._get_display_tick_limit(residual_limit) - fig.update_yaxes( - # title_text='Residual', - range=[-residual_limit, residual_limit], - tickmode='array', - tickvals=[-residual_tick_limit, 0.0, residual_tick_limit], - scaleanchor='y', - scaleratio=1, - zeroline=False, - row=layout.residual_row, - col=1, - ) - fig.update_xaxes(title_text=plot_spec.axes_labels[0], row=layout.residual_row, col=1) - else: - terminal_row = layout.bragg_row if layout.bragg_row is not None else 1 - fig.update_xaxes(title_text=plot_spec.axes_labels[0], row=terminal_row, col=1) + def _configure_residual_axes( + self, + *, + fig: object, + plot_spec: PowderMeasVsCalcSpec, + layout: PowderCompositeRows, + residual_limit: float, + ) -> None: + residual_tick_limit = self._get_display_tick_limit(residual_limit) + fig.update_yaxes( + range=[-residual_limit, residual_limit], + tickmode='array', + tickvals=[-residual_tick_limit, 0.0, residual_tick_limit], + scaleanchor='y', + scaleratio=1, + zeroline=False, + row=layout.residual_row, + col=1, + ) + fig.update_xaxes( + title_text=plot_spec.axes_labels[0], + title_font={'size': AXIS_TITLE_FONT_SIZE}, + row=layout.residual_row, + col=1, + ) - self._show_figure(fig) + @staticmethod + def _get_predictive_band_traces( + *, + x: np.ndarray, + lower: np.ndarray, + upper: np.ndarray, + ) -> tuple[go.Scatter, go.Scatter]: + """ + Return Plotly traces for a filled predictive interval band. + """ + lower_trace = go.Scatter( + x=x, + y=lower, + mode='lines', + line={'color': PREDICTIVE_BAND_EDGE_COLOR, 'width': 1}, + hoverinfo='skip', + showlegend=False, + legendgroup='predictive_band', + ) + upper_trace = go.Scatter( + x=x, + y=upper, + mode='lines', + line={'color': PREDICTIVE_BAND_EDGE_COLOR, 'width': 1}, + fill='tonexty', + fillcolor=PREDICTIVE_BAND_COLOR, + name='95% credible interval', + hoverinfo='skip', + legendgroup='predictive_band', + legendrank=35, + ) + return lower_trace, upper_trace def plot_single_crystal( self, diff --git a/src/easydiffraction/display/plotting.py b/src/easydiffraction/display/plotting.py index 61e93f683..3bfbd5ec5 100644 --- a/src/easydiffraction/display/plotting.py +++ b/src/easydiffraction/display/plotting.py @@ -16,6 +16,9 @@ import numpy as np import pandas as pd +from easydiffraction.analysis.enums import FitCorrelationSourceEnum +from easydiffraction.analysis.enums import FitResultKindEnum +from easydiffraction.analysis.fit_helpers.bayesian import PosteriorPredictiveSummary from easydiffraction.datablocks.experiment.item.base import intensity_category_for from easydiffraction.datablocks.experiment.item.enums import SampleFormEnum from easydiffraction.datablocks.experiment.item.enums import ScatteringTypeEnum @@ -30,6 +33,10 @@ from easydiffraction.display.plotters.base import BraggTickSet from easydiffraction.display.plotters.base import PowderMeasVsCalcSpec from easydiffraction.display.plotters.base import XAxisType +from easydiffraction.display.plotters.plotly import ( + AXIS_TITLE_FONT_SIZE as PLOTLY_AXIS_TITLE_FONT_SIZE, +) +from easydiffraction.display.plotters.plotly import TITLE_FONT_SIZE as PLOTLY_TITLE_FONT_SIZE from easydiffraction.display.plotters.plotly import PlotlyPlotter from easydiffraction.display.tables import TableRenderer from easydiffraction.utils.environment import in_jupyter @@ -63,12 +70,108 @@ def description(self) -> str: return '' -DEFAULT_CORRELATION_THRESHOLD = 0.7 +class PosteriorPairPlotStyleEnum(StrEnum): + """Available posterior pair-plot rendering modes.""" + + AUTO = 'auto' + FAST = 'fast' + FULL = 'full' + + +DEFAULT_CORRELATION_THRESHOLD: float | None = None +DEFAULT_CORRELATION_MAX_PARAMETERS = 6 EXPECTED_COVAR_NDIM = 2 DEFAULT_RESIDUAL_HEIGHT_FRACTION = 0.25 DEFAULT_BRAGG_PEAKS_HEIGHT_FRACTION = 0.10 DEFAULT_RESID_HEIGHT = DEFAULT_RESIDUAL_HEIGHT_FRACTION DEFAULT_BRAGG_ROW = DEFAULT_BRAGG_PEAKS_HEIGHT_FRACTION +DEFAULT_POSTERIOR_PREDICTIVE_DRAWS = 200 +DEFAULT_POSTERIOR_PREDICTIVE_DRAW_PLOT_CAP = 50 +FULL_POSTERIOR_PAIR_COVARIANCE_RANK = 2 +POSTERIOR_FLATTENED_SAMPLE_NDIM = 2 +MIN_POSTERIOR_PARAMETER_COUNT = 2 +MIN_POSTERIOR_SAMPLE_COUNT = 2 +PAIR_DENSITY_SURFACE_NDIM = 2 +POSTERIOR_DENSITY_LINE_COLOR = 'rgb(99, 110, 250)' +POSTERIOR_DENSITY_FILL_COLOR = 'rgba(99, 110, 250, 0.22)' +POSTERIOR_PAIR_MARGINAL_DENSITY_LINE_COLOR = 'rgb(44, 160, 44)' +POSTERIOR_PAIR_MARGINAL_DENSITY_FILL_COLOR = 'rgba(44, 160, 44, 0.22)' +POSTERIOR_PAIR_MARGINAL_DENSITY_LINE_WIDTH = 1 +POSTERIOR_HISTOGRAM_FILL_COLOR = 'rgba(120, 120, 120, 0.38)' +POSTERIOR_HISTOGRAM_LINE_COLOR = 'rgba(120, 120, 120, 0.24)' +POSTERIOR_INTERVAL_95_FILL_COLOR = 'rgba(214, 39, 40, 0.14)' +POSTERIOR_MEDIAN_LINE_COLOR = 'rgb(80, 80, 80)' +POSTERIOR_POINT_ESTIMATE_LINE_COLOR = 'rgb(214, 39, 40)' +POSTERIOR_POINT_ESTIMATE_TRACE_NAME = 'Best posterior sample' +POSTERIOR_POINT_ESTIMATE_LINE_DASH = 'dot' +POSTERIOR_PREDICTIVE_INTERVAL_TRACE_NAME = '95% credible interval' +POSTERIOR_DRAW_LINE_COLOR = 'rgba(140, 140, 140, 0.18)' +POSTERIOR_SCATTER_MARKER_COLOR = 'rgba(140, 140, 140, 0.20)' +POSTERIOR_CONTOUR_FILL_COLORSCALE = [ + [0.0, 'rgba(224, 233, 255, 0.62)'], + [0.35, 'rgba(183, 203, 255, 0.70)'], + [0.60, 'rgba(138, 169, 252, 0.78)'], + [0.82, 'rgba(96, 131, 242, 0.84)'], + [1.0, 'rgba(58, 86, 224, 0.90)'], +] +POSTERIOR_NEGATIVE_CONTOUR_FILL_COLORSCALE = [ + [0.0, 'rgba(255, 224, 224, 0.62)'], + [0.35, 'rgba(250, 188, 188, 0.70)'], + [0.60, 'rgba(245, 148, 148, 0.78)'], + [0.82, 'rgba(237, 104, 104, 0.84)'], + [1.0, 'rgba(215, 48, 39, 0.90)'], +] +POSTERIOR_CONTOUR_LINE_COLORSCALE = [ + [0.0, 'rgba(183, 203, 255, 0.94)'], + [0.35, 'rgba(183, 203, 255, 0.94)'], + [0.35, 'rgba(138, 169, 252, 0.95)'], + [0.60, 'rgba(138, 169, 252, 0.95)'], + [0.60, 'rgba(96, 131, 242, 0.96)'], + [0.82, 'rgba(96, 131, 242, 0.96)'], + [0.82, 'rgba(58, 86, 224, 0.98)'], + [1.0, 'rgba(58, 86, 224, 0.98)'], +] +POSTERIOR_NEGATIVE_CONTOUR_LINE_COLORSCALE = [ + [0.0, 'rgba(250, 188, 188, 0.94)'], + [0.35, 'rgba(250, 188, 188, 0.94)'], + [0.35, 'rgba(245, 148, 148, 0.95)'], + [0.60, 'rgba(245, 148, 148, 0.95)'], + [0.60, 'rgba(237, 104, 104, 0.96)'], + [0.82, 'rgba(237, 104, 104, 0.96)'], + [0.82, 'rgba(215, 48, 39, 0.98)'], + [1.0, 'rgba(215, 48, 39, 0.98)'], +] +POSTERIOR_PAIR_SCATTER_MAX_POINTS = 1500 +POSTERIOR_PAIR_MAX_DENSITY_SAMPLES = 4000 +POSTERIOR_PAIR_MIN_DENSITY_SAMPLES = 800 +POSTERIOR_PAIR_TARGET_DENSITY_SAMPLE_BUDGET = 24000 +POSTERIOR_PAIR_MAX_CONTOUR_GRID_SIZE = 96 +POSTERIOR_PAIR_MIN_CONTOUR_GRID_SIZE = 56 +POSTERIOR_PAIR_TARGET_CONTOUR_GRID_POINT_BUDGET = 73728 +POSTERIOR_PAIR_AUTO_MAX_CONTOUR_PARAMETERS = 6 +PAIR_PLOT_CELL_SIZE_PIXELS = 190 +PAIR_PLOT_MIN_CELL_SIZE_PIXELS = 90 +PAIR_PLOT_MIN_SIZE_PIXELS = 680 +PAIR_PLOT_MARGIN_PIXELS = 120 +PAIR_PLOT_ESTIMATED_CONTAINER_WIDTH_PIXELS = 980 +PAIR_PLOT_SUBPLOT_SPACING = 0.01 +POSTERIOR_PAIR_AXIS_LINE_WIDTH = 1.2 +POSTERIOR_PAIR_AXIS_TITLE_FONT_SIZE = PLOTLY_AXIS_TITLE_FONT_SIZE +POSTERIOR_PAIR_TITLE_FONT_SIZE = PLOTLY_TITLE_FONT_SIZE +POSTERIOR_PAIR_Y_TITLE_XSHIFT_PIXELS = 16 +POSTERIOR_PAIR_X_TITLE_YSHIFT_PIXELS = 10 +SQUARE_MATRIX_TITLE_YSHIFT_PIXELS = 12 +POSTERIOR_PAIR_GUIDE_LINE_COLOR = 'rgba(125, 140, 173, 0.18)' +SQUARE_MATRIX_FIXED_ASPECT_RATIO = '1 / 1' +SQUARE_MATRIX_FIXED_ASPECT_META_KEY = 'fixed_aspect_wrapper' +SQUARE_MATRIX_LEFT_MARGIN_PIXELS = 40 +SQUARE_MATRIX_RIGHT_MARGIN_PIXELS = 24 +SQUARE_MATRIX_TOP_MARGIN_PIXELS = 40 +SQUARE_MATRIX_BOTTOM_MARGIN_PIXELS = 40 +SQUARE_MATRIX_AXIS_TITLE_LINE_HEIGHT_PIXELS = 18 +SQUARE_MATRIX_TITLE_LEFT_PADDING_PIXELS = 14 +POSTERIOR_PAIR_SAMPLE_MARKER_SIZE = 6 +POSTERIOR_PAIR_SAMPLE_HOVER_MARKER_SIZE = 6 @dataclass(frozen=True) @@ -78,6 +181,9 @@ class _MeasVsCalcPlotOptions: x_min: float | None = None x_max: float | None = None show_residual: bool | None = None + show_background: bool | None = None + show_bragg: bool | None = None + show_excluded: bool = False x: object | None = None @@ -90,6 +196,71 @@ class _PowderMeasVsCalcSeries: y_bkg: np.ndarray | None = None +@dataclass(frozen=True) +class _PosteriorDistributionContext: + """Inputs needed to build a posterior distribution plot.""" + + fit_results: object + parameter_name: str + values: np.ndarray + label: str + title: str + summary: object | None + + +@dataclass(frozen=True) +class _PosteriorPairsContext: + """Inputs needed to build a posterior pair plot.""" + + fit_results: object + parameter_names: list[str] + labels: list[str] + annotation_labels: list[str] + title: str + marginal_density_samples: np.ndarray + density_samples: np.ndarray + scatter_samples: np.ndarray + show_contours: bool + contour_grid_size: int + axis_frame_color: str + axis_ranges: list[tuple[float, float]] + + @property + def n_parameters(self) -> int: + """Return the number of plotted parameters.""" + return len(self.parameter_names) + + +@dataclass(frozen=True) +class _CorrelationHeatmapContext: + """Inputs needed to build a correlation matrix plot.""" + + corr_df: pd.DataFrame + row_labels: list[str] + col_labels: list[str] + threshold: float | None + precision: int + + @property + def n_rows(self) -> int: + """Return the number of displayed rows.""" + return self.corr_df.shape[0] + + @property + def n_cols(self) -> int: + """Return the number of displayed columns.""" + return self.corr_df.shape[1] + + +@dataclass(slots=True) +class _PosteriorPairsLegendState: + """Legend-visibility state for posterior pair plots.""" + + show_density: bool = True + show_scatter: bool = True + show_contour: bool = True + + class Plotter(RendererBase): """User-facing plotting facade backed by concrete plotters.""" @@ -162,11 +333,17 @@ def _auto_x_range_for_ascii( 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) + if ( + self._engine == 'asciichartpy' + and x_min is None + and x_max is None + and AsciiPlotter._should_crop_to_peak_window(len(x_array)) + ): + max_intensity_pos = int(np.argmax(pattern.intensity_meas)) + target_point_count = min(len(x_array), AsciiPlotter._chart_point_count()) + start = max(0, max_intensity_pos - target_point_count // 2) + end = min(len(x_array) - 1, start + target_point_count - 1) + start = max(0, end - target_point_count + 1) x_min = x_array[start] x_max = x_array[end] return x_min, x_max @@ -208,6 +385,18 @@ def _filtered_y_array( mask = (x_array >= lower_bound) & (x_array <= upper_bound) return y_array[mask] + def _filtered_optional_y_array( + self, + y_array: object | None, + x_array: object, + x_min: object, + x_max: object, + ) -> object | None: + """Filter an optional y-array by inclusive x-range limits.""" + if y_array is None: + return None + return self._filtered_y_array(y_array, x_array, x_min, x_max) + @staticmethod def _get_axes_labels( sample_form: object, @@ -409,6 +598,8 @@ def plot_meas( x_min: float | None = None, x_max: float | None = None, x: object | None = None, + *, + show_excluded: bool = False, ) -> None: """ Plot measured diffraction data for an experiment. @@ -423,16 +614,23 @@ def plot_meas( Upper bound for the x-axis range. x : object | None, default=None Optional explicit x-axis data to override stored values. + show_excluded : bool, default=False + Whether to show excluded fitting regions on supported plots. """ self._update_project_categories(expt_name) experiment = self._project.experiments[expt_name] + plot_options = _MeasVsCalcPlotOptions( + x_min=x_min, + x_max=x_max, + show_excluded=show_excluded, + x=x, + ) self._plot_meas_data( + experiment, intensity_category_for(experiment), expt_name, experiment.type, - x_min=x_min, - x_max=x_max, - x=x, + plot_options, ) def plot_calc( @@ -441,6 +639,8 @@ def plot_calc( x_min: float | None = None, x_max: float | None = None, x: object | None = None, + *, + show_excluded: bool = False, ) -> None: """ Plot calculated diffraction pattern for an experiment. @@ -455,16 +655,23 @@ def plot_calc( Upper bound for the x-axis range. x : object | None, default=None Optional explicit x-axis data to override stored values. + show_excluded : bool, default=False + Whether to show excluded fitting regions on supported plots. """ self._update_project_categories(expt_name) experiment = self._project.experiments[expt_name] + plot_options = _MeasVsCalcPlotOptions( + x_min=x_min, + x_max=x_max, + show_excluded=show_excluded, + x=x, + ) self._plot_calc_data( + experiment, intensity_category_for(experiment), expt_name, experiment.type, - x_min=x_min, - x_max=x_max, - x=x, + plot_options, ) def plot_meas_vs_calc( @@ -474,6 +681,7 @@ def plot_meas_vs_calc( x_max: float | None = None, *, show_residual: bool | None = None, + show_excluded: bool = False, x: object | None = None, ) -> None: """ @@ -491,17 +699,29 @@ def plot_meas_vs_calc( When ``None``, powder Bragg plots include the residual by default while other measured-vs-calculated plots keep the historical no-residual default. + show_excluded : bool, default=False + Whether to show excluded fitting regions on supported plots. x : object | None, default=None Optional explicit x-axis data to override stored values. """ - self._update_project_categories(expt_name) - experiment = self._project.experiments[expt_name] plot_options = _MeasVsCalcPlotOptions( x_min=x_min, x_max=x_max, show_residual=show_residual, + show_excluded=show_excluded, x=x, ) + self._plot_meas_vs_calc_request(expt_name=expt_name, plot_options=plot_options) + + def _plot_meas_vs_calc_request( + self, + *, + expt_name: str, + plot_options: _MeasVsCalcPlotOptions, + ) -> None: + """Render a measured-vs-calculated request from plot options.""" + self._update_project_categories(expt_name) + experiment = self._project.experiments[expt_name] self._plot_meas_vs_calc_data( experiment=experiment, expt_name=expt_name, @@ -511,7 +731,7 @@ def plot_meas_vs_calc( def plot_param_series( self, param: object, - versus: object | None = None, + versus: str | None = None, ) -> None: """ Plot a parameter's value across sequential fit results. @@ -524,15 +744,18 @@ def plot_param_series( Parameters ---------- param : object - Parameter descriptor whose ``unique_name`` identifies the + Descriptor whose ``unique_name`` or ``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. + versus : str | None, default=None + Persisted diffrn path (e.g. + ``'diffrn.ambient_temperature'``) whose sequential-results + column is used as the x-axis. When ``None``, the experiment + sequence number is used instead. """ - unique_name = param.unique_name + column_names = self._series_column_names(param) + if not column_names: + log.warning('Series plot target does not expose a CSV column name.') + return # Try CSV first (produced by fit_sequential or future fit) csv_path = None @@ -544,24 +767,172 @@ def plot_param_series( if csv_path is not None: self._plot_param_series_from_csv( csv_path=csv_path, - unique_name=unique_name, + column_names=column_names, param_descriptor=param, - versus_descriptor=versus, + versus_path=versus, ) else: # Fallback: in-memory snapshots from fit() single mode - versus_name = versus.name if versus is not None else None self.plot_param_series_from_snapshots( - unique_name, - versus_name, + column_names[0], + versus, self._project.experiments, self._project.analysis._parameter_snapshots, ) + @staticmethod + def _series_column_names(param: object) -> list[str]: + """Return candidate CSV column names for one plotted series.""" + names: list[str] = [] + + unique_name = getattr(param, 'unique_name', None) + if isinstance(unique_name, str) and unique_name: + names.append(unique_name) + + name = getattr(param, 'name', None) + if isinstance(name, str) and name and name not in names: + names.append(name) + + return names + + @staticmethod + def _numeric_series_values(values: object) -> list[float]: + """Return one CSV column normalized to numeric plot values.""" + series = pd.Series(values) + if series.dtype == bool: + return series.astype(float).tolist() + + normalized = series.replace({ + 'True': 1.0, + 'False': 0.0, + 'true': 1.0, + 'false': 0.0, + }) + return pd.to_numeric(normalized, errors='raise').tolist() + + def plot_all_param_series( + self, + versus: str | None = None, + ) -> None: + """ + Plot every fitted parameter across sequential fit results. + + Iterates the fitted parameters recorded in ``results.csv`` (or, + when absent, in the in-memory parameter snapshots) and emits one + ``plot_param_series`` plot per parameter. + + Parameters + ---------- + versus : str | None, default=None + Persisted diffrn path (e.g. + ``'diffrn.ambient_temperature'``) whose sequential-results + column is used as the x-axis. When ``None``, the experiment + sequence number is used instead. + """ + unique_names = self._collect_fitted_param_unique_names() + if not unique_names: + log.warning('No fitted parameters found to plot.') + return + + descriptors_by_name = self._fitted_param_descriptors_by_unique_name() + + for unique_name in unique_names: + descriptor = descriptors_by_name.get(unique_name) + if descriptor is None: + log.warning(f"Parameter '{unique_name}' not found in project; skipping plot.") + continue + self.plot_param_series(param=descriptor, versus=versus) + + def _collect_fitted_param_unique_names(self) -> list[str]: + """ + Return fitted parameter unique names from CSV or snapshots. + """ + from easydiffraction.analysis.sequential import _META_COLUMNS # noqa: PLC0415 + + meta = set(_META_COLUMNS) + + csv_path = None + if self._project.info.path is not None: + candidate = pathlib.Path(self._project.info.path) / 'analysis' / 'results.csv' + if candidate.is_file(): + csv_path = str(candidate) + + if csv_path is not None: + df = pd.read_csv(csv_path) + return [ + column + for column in df.columns + if column not in meta + and not column.startswith('diffrn.') + and not column.endswith('.uncertainty') + ] + + snapshots = self._project.analysis._parameter_snapshots + if not snapshots: + return [] + first_snapshot = next(iter(snapshots.values())) + return list(first_snapshot.keys()) + + def _fitted_param_descriptors_by_unique_name(self) -> dict[str, object]: + """Return descriptor map keyed by ``unique_name``.""" + all_params = self._project.structures.parameters + self._project.experiments.parameters + return {p.unique_name: p for p in all_params if hasattr(p, 'unique_name')} + + def _resolve_versus_descriptor_from_path( + self, + versus_path: str | None, + ) -> object | None: + """Return a template diffrn descriptor for a persisted path.""" + field_name = self._versus_field_name(versus_path) + if field_name is None: + return None + + project = getattr(self, '_project', None) + if project is None or getattr(project, 'experiments', None) is None: + return None + + experiment = next(iter(project.experiments.values()), None) + if experiment is None: + return None + + return self._resolve_diffrn_descriptor(experiment.diffrn, field_name) + + @staticmethod + def _versus_field_name(versus_path: str | None) -> str | None: + """Return the diffrn field name from a persisted path.""" + if versus_path is None: + return None + if versus_path.startswith('diffrn.'): + return versus_path.removeprefix('diffrn.') + return versus_path + + @classmethod + def _versus_axis_label( + cls, + versus_path: str | None, + descriptor: object | None, + ) -> str: + """Return the x-axis label for a persisted diffrn path.""" + if descriptor is not None: + label = getattr(descriptor, 'description', None) or getattr(descriptor, 'name', None) + units = getattr(descriptor, 'units', None) + if label is not None and units: + return f'{label} ({units})' + if label is not None: + return label + + field_name = cls._versus_field_name(versus_path) + if field_name is None: + return 'Experiment No.' + return field_name.replace('_', ' ') + def plot_param_correlations( self, threshold: float | None = DEFAULT_CORRELATION_THRESHOLD, precision: int = 2, + *, + max_parameters: int = DEFAULT_CORRELATION_MAX_PARAMETERS, + show_diagonal: bool = True, ) -> None: """ Plot the parameter correlation matrix from the latest fit. @@ -570,44 +941,57 @@ def plot_param_correlations( the active engine is Plotly, an interactive heatmap is shown. Otherwise, a rounded correlation table is rendered. - Only the lower triangle is shown (without the diagonal), since - the matrix is symmetric and diagonal values are always ``1``. + By default the lower triangle is shown with blank diagonal cells + so the grid stays square, like posterior pair plots. Set + ``show_diagonal=False`` to trim the empty outer row and column. Parameters ---------- threshold : float | None, default=DEFAULT_CORRELATION_THRESHOLD Minimum absolute off-diagonal correlation required for a - parameter to be shown. Parameters are kept only if they - participate in at least one pair with ``abs(correlation) >= - threshold``. Set to ``None`` or ``0`` to show the full - matrix. + parameter to be shown. When omitted, an automatic cutoff is + chosen so the displayed matrix stays at or below + ``max_parameters x max_parameters`` when possible. Set to + ``0`` to show the full matrix. precision : int, default=2 Number of decimal places to show in the table fallback. + max_parameters : int, default=DEFAULT_CORRELATION_MAX_PARAMETERS + Maximum number of parameters to display when ``threshold`` + is omitted. Ignored when ``threshold`` is provided. + show_diagonal : bool, default=True + Whether to retain blank diagonal cells in the displayed + lower-triangle matrix. """ corr_df = self._get_param_correlation_dataframe() if corr_df is None: return - corr_df = self._filter_correlation_dataframe(corr_df, threshold=threshold) + corr_df, resolved_threshold = self._resolve_correlation_filter( + corr_df, + threshold=threshold, + max_parameters=max_parameters, + ) if corr_df is None: return corr_df = self._mask_correlation_lower_triangle(corr_df) - title = 'Refined parameter correlation matrix' - if threshold is not None and threshold > 0: - title += f' with |correlation| >= {threshold:.2f}' + title = self._correlation_filtered_title( + 'Refined parameter correlation matrix', + resolved_threshold, + ) is_graphical = self._backend._supports_graphical_heatmap display_corr_df, row_numbers, col_numbers = self._trim_correlation_display_dataframe( corr_df, preserve_all_rows=not is_graphical, + show_diagonal=show_diagonal, ) if is_graphical: self._plot_correlation_heatmap( display_corr_df, title, - threshold=threshold, + threshold=resolved_threshold, precision=precision, ) return @@ -618,160 +1002,3527 @@ def plot_param_correlations( display_corr_df, row_numbers=row_numbers, col_numbers=col_numbers, - threshold=threshold, + threshold=resolved_threshold, precision=precision, ) ) - @staticmethod - def _filter_correlation_dataframe( + @classmethod + def _resolve_correlation_filter( + cls, corr_df: pd.DataFrame, + *, threshold: float | None, - ) -> pd.DataFrame | None: + max_parameters: int | None = DEFAULT_CORRELATION_MAX_PARAMETERS, + min_parameters: int = 1, + ) -> tuple[pd.DataFrame | None, float]: + """Return a filtered matrix and effective threshold.""" + if threshold is not None: + filtered_corr_df = cls._filter_correlation_dataframe(corr_df, threshold=threshold) + return filtered_corr_df, float(threshold) + if max_parameters is None: + return corr_df, 0.0 + validated_max_parameters = cls._validated_max_parameter_count( + max_parameters, + minimum=min_parameters, + ) + return cls._auto_filtered_correlation_dataframe( + corr_df, + max_parameters=validated_max_parameters, + min_parameters=min_parameters, + ) + + @staticmethod + def _validated_max_parameter_count( + max_parameters: int, + *, + minimum: int, + ) -> int: + """Return a validated parameter-count limit.""" + if not isinstance(max_parameters, int) or isinstance(max_parameters, bool): + msg = 'max_parameters must be an integer.' + raise TypeError(msg) + if max_parameters < minimum: + msg = f'max_parameters must be at least {minimum}.' + raise ValueError(msg) + return max_parameters + + @staticmethod + def _auto_filtered_correlation_dataframe( + corr_df: pd.DataFrame, + *, + max_parameters: int, + min_parameters: int = 1, + ) -> tuple[pd.DataFrame, float]: + """Return an auto-limited matrix for default display.""" + if corr_df.shape[0] <= max_parameters: + return corr_df, 0.0 + + abs_corr = np.abs(corr_df.to_numpy(copy=True)) + np.fill_diagonal(abs_corr, 0.0) + positive_values = np.unique(abs_corr[abs_corr > 0.0]) + for candidate in np.sort(positive_values): + keep_mask = (abs_corr >= candidate).any(axis=0) + if min_parameters <= int(keep_mask.sum()) <= max_parameters: + labels = corr_df.index[keep_mask] + return corr_df.loc[labels, labels], float(candidate) + + if positive_values.size == 0: + return corr_df.iloc[:max_parameters, :max_parameters], 0.0 + + parameter_strength = np.max(abs_corr, axis=0) + top_indices = np.argsort(-parameter_strength, kind='stable')[:max_parameters] + top_indices.sort() + labels = corr_df.index[top_indices] + return corr_df.loc[labels, labels], 0.0 + + @staticmethod + def _correlation_filtered_title(base_title: str, threshold: float) -> str: + """Return a plot title with a correlation cutoff.""" + if threshold <= 0: + return base_title + return f'{base_title} with |correlation| ≥ {threshold:.2f}' + + @staticmethod + def _posterior_pair_title(multiplier: float | None) -> str: """ - Filter a correlation matrix to only strongly correlated params. + Return the posterior pair title with its displayed bound scale. + """ + if multiplier is None: + return 'Posterior pair plot' + return f'Posterior pair plot in ±{multiplier:g} × uncertainty region' # noqa: RUF001 + + @staticmethod + def _posterior_pair_uncertainty_multiplier( + fit_results: object, + parameter_names: list[str], + ) -> float | None: + """ + Return a shared uncertainty-bound multiplier for a pair plot. + """ + parameters_by_name = { + getattr(parameter, 'unique_name', ''): parameter + for parameter in fit_results.parameters + } + multiplier: float | None = None + + for parameter_name in parameter_names: + parameter = parameters_by_name.get(parameter_name) + if parameter is None: + return None + + current = getattr(parameter, 'fit_bounds_uncertainty_multiplier', None) + if current is None or not np.isfinite(float(current)): + return None + + current_value = float(current) + if multiplier is None: + multiplier = current_value + continue + if not np.isclose(multiplier, current_value): + return None + + return multiplier + + def plot_posterior_pairs( + self, + parameters: list[object] | None = None, + style: PosteriorPairPlotStyleEnum | str = 'auto', + *, + threshold: float | None = DEFAULT_CORRELATION_THRESHOLD, + max_parameters: int = DEFAULT_CORRELATION_MAX_PARAMETERS, + ) -> None: + """ + Plot posterior pair relationships for sampled parameters. Parameters ---------- - corr_df : pd.DataFrame - Square correlation matrix. - threshold : float | None - Absolute-correlation cutoff. ``None`` or ``0`` keeps all + parameters : list[object] | None, default=None + Optional subset of sampled parameters to include. When + provided, ``threshold`` and ``max_parameters`` are ignored. + style : PosteriorPairPlotStyleEnum | str, default='auto' + Pair-plot rendering mode. Defaults to ``'auto'``. ``'auto'`` + keeps contours for compact plots and disables them for wide + grids. ``'fast'`` always skips contours. ``'full'`` always + renders contours. + threshold : float | None, default=DEFAULT_CORRELATION_THRESHOLD + Minimum absolute off-diagonal correlation required for a + parameter to be auto-selected. When omitted, an automatic + cutoff keeps the plot at or below ``max_parameters`` + parameters when possible. Set to ``0`` to show all sampled parameters. + max_parameters : int, default=DEFAULT_CORRELATION_MAX_PARAMETERS + Maximum number of parameters to auto-select when + ``parameters`` is omitted and ``threshold`` is ``None``. + Must be at least ``2``. + """ + if self.engine != PlotterEngineEnum.PLOTLY.value: + console.paragraph(self._posterior_pair_title(None)) - Returns - ------- - pd.DataFrame | None - Filtered square matrix, or ``None`` if no off-diagonal - correlations meet the cutoff. + plot = self._build_posterior_pairs_plot( + parameters=parameters, + style=style, + threshold=threshold, + max_parameters=max_parameters, + ) + if plot is None: + return + self._show_plot_figure(plot) + + def plot_param_distribution( + self, + param: object, + ) -> None: + """ + Plot the posterior distribution for one sampled parameter. + + Parameters + ---------- + param : object + Parameter descriptor or string identifier selecting the + posterior to plot. Strings may be unique names or + user-facing labels. + """ + if self.engine == PlotterEngineEnum.ASCII.value: + self._plot_ascii_param_distribution(param) + return + + plot = self._build_param_distribution_plot(param) + if plot is None: + return + self._show_plot_figure(plot) + + def plot_posterior_predictive( + self, + expt_name: str, + style: str = 'band', + x_min: float | None = None, + x_max: float | None = None, + *, + show_residual: bool | None = None, + show_excluded: bool = False, + x: object | None = None, + ) -> None: + """ + Plot posterior predictive checks for supported experiments. + + Parameters + ---------- + expt_name : str + Experiment name to plot. + style : str, default='band' + ``'band'`` shows the 95% credible interval, ``'draws'`` + shows sampled predictive curves, and ``'band+draws'`` shows + both together. ASCII powder plots fall back to measured and + max-posterior lines without uncertainty bands or draws. + Single-crystal plots currently render only the interval- + based reflection check. + 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 | None, default=None + Whether to include the residual row in supported powder + composite plots. + show_excluded : bool, default=False + Whether to show excluded fitting regions on supported plots. + x : object | None, default=None + Optional explicit x-axis data to override stored values. Raises ------ ValueError - If *threshold* is outside ``[0, 1]``. + If ``style`` is not one of ``'band'``, ``'draws'``, or + ``'band+draws'``. """ - if threshold is None or threshold <= 0: - return corr_df - if threshold > 1: - msg = 'Correlation threshold must be between 0 and 1.' + if style not in {'band', 'draws', 'band+draws'}: + msg = "style must be 'band', 'draws', or 'band+draws'." raise ValueError(msg) - abs_corr = np.abs(corr_df.to_numpy(copy=True)) - np.fill_diagonal(abs_corr, 0.0) - keep_mask = (abs_corr >= threshold).any(axis=0) + plot_options = _MeasVsCalcPlotOptions( + x_min=x_min, + x_max=x_max, + show_residual=show_residual, + show_excluded=show_excluded, + x=x, + ) - if not keep_mask.any(): - log.warning(f'No parameter pairs with |correlation| >= {threshold:.2f} were found.') - return None + self._plot_posterior_predictive_request( + expt_name=expt_name, + style=style, + plot_options=plot_options, + ) - labels = corr_df.index[keep_mask] - return corr_df.loc[labels, labels] + def _plot_posterior_predictive_request( + self, + *, + expt_name: str, + style: str, + plot_options: _MeasVsCalcPlotOptions, + ) -> None: + """Render a posterior predictive request from plot options.""" + if self._project is None: + log.warning('Plotter is not attached to a project.') + return - @staticmethod - def _mask_correlation_lower_triangle( - corr_df: pd.DataFrame, - ) -> pd.DataFrame: - """ - Mask the upper triangle and diagonal of a correlation matrix. + self._update_project_categories(expt_name) + experiment = self._project.experiments[expt_name] + x_axis, _, sample_form, scattering_type, _ = self._resolve_x_axis( + experiment.type, + plot_options.x, + ) - Only the lower triangle is kept, since the matrix is symmetric - and diagonal values are always ``1``. + if sample_form == SampleFormEnum.SINGLE_CRYSTAL: + if self.engine != PlotterEngineEnum.PLOTLY.value: + log.warning( + 'Single-crystal posterior predictive plots currently ' + 'require the Plotly backend.' + ) + return + self._plot_single_crystal_posterior_predictive( + experiment=experiment, + expt_name=expt_name, + x_axis=x_axis, + scattering_type=scattering_type, + plot_options=plot_options, + style=style, + ) + return - Parameters + if sample_form != SampleFormEnum.POWDER: + log.warning('Posterior predictive plots currently support powder experiments only.') + return + + if scattering_type == ScatteringTypeEnum.BRAGG: + self._plot_posterior_predictive_data( + experiment=experiment, + expt_name=expt_name, + plot_options=plot_options, + x_axis=x_axis, + style=style, + ) + return + + self._plot_non_bragg_posterior_predictive( + experiment=experiment, + expt_name=expt_name, + plot_options=plot_options, + x_axis=x_axis, + sample_form=sample_form, + scattering_type=scattering_type, + style=style, + ) + + def _plot_single_crystal_posterior_predictive( + self, + *, + experiment: object, + expt_name: str, + x_axis: object, + scattering_type: object, + plot_options: _MeasVsCalcPlotOptions, + style: str, + ) -> None: + """Render a single-crystal posterior predictive scatter plot.""" + if scattering_type != ScatteringTypeEnum.BRAGG: + log.warning( + 'Single-crystal posterior predictive plots currently support Bragg data only.' + ) + return + if x_axis not in {XAxisType.INTENSITY_CALC, 'intensity_calc'}: + log.warning( + 'Single-crystal posterior predictive plots currently support ' + "x='intensity_calc' only." + ) + return + if plot_options.show_residual: + log.warning( + 'Posterior predictive residuals are unavailable for ' + 'single-crystal plots; ignoring show_residual=True.' + ) + if style != 'band': + log.warning( + 'Single-crystal posterior predictive plots currently support ' + 'style="band" only; rendering the 95% credible interval.' + ) + + summary = self._get_or_build_posterior_predictive_summary( + experiment=experiment, + expt_name=expt_name, + x_axis=x_axis, + include_draws=False, + ) + if summary is None: + return + + pattern = intensity_category_for(experiment) + y_meas_raw = getattr(pattern, 'intensity_meas', None) + if y_meas_raw is None: + log.warning(f'No measured data available for experiment {expt_name}.') + return + y_meas = np.asarray(y_meas_raw, dtype=float) + if y_meas.shape != np.asarray(summary.best_sample_prediction).shape: + log.warning( + 'Single-crystal posterior predictive values do not match the ' + 'measured reflection array shape.' + ) + return + + y_meas_su_raw = getattr(pattern, 'intensity_meas_su', None) + if y_meas_su_raw is None: + log.warning(f'No measurement uncertainties for experiment {expt_name}') + y_meas_su = np.zeros_like(y_meas) + else: + y_meas_su = np.asarray(y_meas_su_raw, dtype=float) + if y_meas_su.shape != y_meas.shape: + log.warning( + 'Single-crystal posterior predictive uncertainties do not ' + 'match the measured reflection array shape.' + ) + return + + self._plot_single_crystal_posterior_predictive_summary( + expt_name=expt_name, + summary=summary, + y_meas=y_meas, + y_meas_su=y_meas_su, + axes_labels=self._get_axes_labels( + SampleFormEnum.SINGLE_CRYSTAL, + ScatteringTypeEnum.BRAGG, + XAxisType.INTENSITY_CALC, + ), + ) + + def _plot_non_bragg_posterior_predictive( + self, + *, + experiment: object, + expt_name: str, + plot_options: _MeasVsCalcPlotOptions, + x_axis: object, + sample_form: object, + scattering_type: object, + style: str, + ) -> None: + """Render non-Bragg posterior predictive summaries.""" + show_draws = self.engine == PlotterEngineEnum.PLOTLY.value and style in { + 'draws', + 'band+draws', + } + pattern = intensity_category_for(experiment) + y_meas = getattr(pattern, 'intensity_meas', None) + if y_meas is None: + log.warning(f'No measured data available for experiment {expt_name}.') + return + + ctx = self._prepare_powder_context( + pattern, + expt_name, + experiment.type, + plot_options.x_min, + plot_options.x_max, + plot_options.x, + ) + if ctx is None: + return + + if plot_options.show_residual: + log.warning( + 'Posterior predictive residuals are unavailable for non-Bragg ' + 'summary plots; ignoring show_residual=True.' + ) + + summary = self._get_or_build_posterior_predictive_summary( + experiment=experiment, + expt_name=expt_name, + x_axis=x_axis, + include_draws=show_draws, + ) + if summary is None: + return + + filtered_summary = self._filtered_posterior_predictive_summary( + summary=summary, + x_min=ctx['x_min'], + x_max=ctx['x_max'], + include_draws=show_draws, + ) + if filtered_summary is None: + log.warning( + f'No posterior predictive data available within the requested x-range ' + f'for experiment {expt_name}.' + ) + return + + filtered_y_meas = self._filtered_y_array( + y_meas, + ctx['x_array'], + ctx['x_min'], + ctx['x_max'], + ) + excluded_ranges = ( + self._excluded_ranges( + experiment=experiment, + x_min=ctx['x_min'], + x_max=ctx['x_max'], + ) + if plot_options.show_excluded + else () + ) + + axes_labels = self._get_axes_labels(sample_form, scattering_type, x_axis) + self._plot_posterior_predictive_summary( + expt_name=expt_name, + summary=filtered_summary, + y_meas=filtered_y_meas, + axes_labels=axes_labels, + show_band=style in {'band', 'band+draws'}, + show_draws=style in {'draws', 'band+draws'}, + excluded_ranges=excluded_ranges, + ) + + @staticmethod + def _filter_correlation_dataframe( + corr_df: pd.DataFrame, + threshold: float | None, + ) -> pd.DataFrame | None: + """ + Filter a correlation matrix to only strongly correlated params. + + Parameters + ---------- + corr_df : pd.DataFrame + Square correlation matrix. + threshold : float | None + Absolute-correlation cutoff. ``None`` or ``0`` keeps all + parameters. + + Returns + ------- + pd.DataFrame | None + Filtered square matrix, or ``None`` if no off-diagonal + correlations meet the cutoff. + + Raises + ------ + ValueError + If *threshold* is outside ``[0, 1]``. + """ + if threshold is None or threshold <= 0: + return corr_df + if threshold > 1: + msg = 'Correlation threshold must be between 0 and 1.' + raise ValueError(msg) + + abs_corr = np.abs(corr_df.to_numpy(copy=True)) + np.fill_diagonal(abs_corr, 0.0) + keep_mask = (abs_corr >= threshold).any(axis=0) + + if not keep_mask.any(): + log.warning(f'No parameter pairs with |correlation| >= {threshold:.2f} were found.') + return None + + labels = corr_df.index[keep_mask] + return corr_df.loc[labels, labels] + + @staticmethod + def _mask_correlation_lower_triangle( + corr_df: pd.DataFrame, + ) -> pd.DataFrame: + """ + Mask the upper triangle and diagonal of a correlation matrix. + + Only the lower triangle is kept, since the matrix is symmetric + and diagonal values are always ``1``. + + Parameters ---------- corr_df : pd.DataFrame Square correlation matrix. - Returns - ------- - pd.DataFrame - Correlation matrix with upper triangle and diagonal masked. - """ - masked_values = corr_df.to_numpy(copy=True) - mask = np.triu(np.ones_like(masked_values, dtype=bool), k=0) - masked_values[mask] = np.nan - return pd.DataFrame(masked_values, index=corr_df.index, columns=corr_df.columns) + Returns + ------- + pd.DataFrame + Correlation matrix with upper triangle and diagonal masked. + """ + masked_values = corr_df.to_numpy(copy=True) + mask = np.triu(np.ones_like(masked_values, dtype=bool), k=0) + masked_values[mask] = np.nan + return pd.DataFrame(masked_values, index=corr_df.index, columns=corr_df.columns) + + @staticmethod + def _trim_correlation_display_dataframe( + corr_df: pd.DataFrame, + *, + preserve_all_rows: bool, + show_diagonal: bool, + ) -> tuple[pd.DataFrame, list[int], list[int]]: + """ + Trim empty outer rows/columns from the lower-triangle view. + + For the lower triangle without diagonal, the last column and + first row are always empty and can be trimmed. + + Parameters + ---------- + corr_df : pd.DataFrame + Masked correlation matrix. + preserve_all_rows : bool + Whether to keep the full row list so row labels continue to + identify all numeric column headers in tabular output. + show_diagonal : bool + Whether blank diagonal cells should remain visible. + + Returns + ------- + tuple[pd.DataFrame, list[int], list[int]] + Display matrix plus 1-based parameter numbers for the kept + rows and columns. + """ + num_rows, num_cols = corr_df.shape + row_numbers = list(range(1, num_rows + 1)) + col_numbers = list(range(1, num_cols + 1)) + + if show_diagonal or min(num_rows, num_cols) <= 1: + return corr_df, row_numbers, col_numbers + + if preserve_all_rows: + return corr_df.iloc[:, :-1], row_numbers, col_numbers[:-1] + return corr_df.iloc[1:, :-1], row_numbers[1:], col_numbers[:-1] + + def _get_param_correlation_dataframe(self) -> pd.DataFrame | None: + """ + Return the correlation matrix for the latest fit. + + Returns + ------- + pd.DataFrame | None + Square correlation matrix labeled by parameter unique names, + or ``None`` if unavailable. + """ + fit_results = self._get_fit_result_for_correlation() + if fit_results is None: + return None + + corr_df = self._posterior_correlation_dataframe(fit_results) + if corr_df is not None: + return corr_df + + raw_result = self._raw_fit_result_for_correlation(fit_results) + if raw_result is not None: + corr_df = self._correlation_dataframe_from_engine_result( + raw_result=raw_result, + parameters=fit_results.parameters, + ) + if corr_df is not None: + return corr_df + + corr_df = self._correlation_dataframe_from_persisted_projection(fit_results) + if corr_df is not None: + return corr_df + + log.warning( + 'Correlation matrix is unavailable for this fit. ' + 'Use a minimizer that returns covariance information or posterior samples.' + ) + return None + + def _posterior_correlation_dataframe( + self, + fit_results: object, + ) -> pd.DataFrame | None: + """Return posterior-sample correlations when available.""" + posterior_samples = getattr(fit_results, 'posterior_samples', None) + if posterior_samples is None: + return None + return self._correlation_from_posterior_samples(posterior_samples) + + @staticmethod + def _raw_fit_result_for_correlation(fit_results: object) -> object | None: + """Return raw fit results for correlation fallback.""" + raw_result = getattr(fit_results, 'result', None) + if raw_result is None: + raw_result = getattr(fit_results, 'engine_result', None) + if raw_result is None: + return None + + var_names = getattr(raw_result, 'var_names', None) + if not var_names: + return None + return raw_result + + def _correlation_dataframe_from_persisted_projection( + self, + fit_results: object, + ) -> pd.DataFrame | None: + """ + Return correlations restored from persisted fit-state rows. + """ + if self._project is None: + return None + + analysis = self._project.analysis + source_kind = ( + FitCorrelationSourceEnum.POSTERIOR.value + if analysis.fit_result.result_kind.value == FitResultKindEnum.BAYESIAN.value + else FitCorrelationSourceEnum.DETERMINISTIC.value + ) + correlation_rows = [ + row + for row in analysis.fit_parameter_correlations + if row.source_kind.value == source_kind + ] + if not correlation_rows: + return None + + parameter_names = [ + getattr(parameter, 'unique_name', '') + for parameter in getattr(fit_results, 'parameters', []) + if getattr(parameter, 'unique_name', None) + ] + if not parameter_names: + parameter_names = [ + summary.unique_name + for summary in getattr(fit_results, 'posterior_parameter_summaries', []) + ] + + for row in correlation_rows: + parameter_names.extend([row.param_unique_name_i.value, row.param_unique_name_j.value]) + parameter_names = list(dict.fromkeys(parameter_names)) + if len(parameter_names) < MIN_POSTERIOR_PARAMETER_COUNT: + return None + + correlation_values = np.eye(len(parameter_names), dtype=float) + corr_df = pd.DataFrame( + correlation_values, + index=parameter_names, + columns=parameter_names, + ) + wrote_any = False + for row in correlation_rows: + i_name = row.param_unique_name_i.value + j_name = row.param_unique_name_j.value + if i_name not in corr_df.index or j_name not in corr_df.index: + continue + corr_df.loc[i_name, j_name] = float(row.correlation.value) + corr_df.loc[j_name, i_name] = float(row.correlation.value) + wrote_any = True + return corr_df if wrote_any else None + + def _correlation_dataframe_from_engine_result( + self, + *, + raw_result: object, + parameters: list[object], + ) -> pd.DataFrame | None: + """Return correlations derived from engine result fields.""" + covar = getattr(raw_result, 'covar', None) + if covar is not None: + return self._correlation_from_covariance( + covar, + getattr(raw_result, 'var_names', None), + parameters, + ) + return self._get_param_correlation_dataframe_from_engine_params( + raw_result=raw_result, + parameters=parameters, + ) + + def _build_posterior_pairs_plot( + self, + *, + parameters: list[object] | None, + style: PosteriorPairPlotStyleEnum | str = 'auto', + threshold: float | None = DEFAULT_CORRELATION_THRESHOLD, + max_parameters: int | None = None, + ) -> object | None: + """ + Build a Plotly posterior pair plot. + + Parameters + ---------- + parameters : list[object] | None + Optional subset of sampled parameters to include. + style : PosteriorPairPlotStyleEnum | str, default='auto' + Posterior pair-plot rendering mode. Defaults to ``'auto'``. + threshold : float | None, default=DEFAULT_CORRELATION_THRESHOLD + Absolute-correlation cutoff for auto-selected parameters. + max_parameters : int | None, default=None + Maximum number of auto-selected parameters. ``None`` keeps + the full posterior parameter set. + + Returns + ------- + object | None + Plotly figure, or ``None`` when posterior plotting is + unavailable. + """ + context = self._posterior_pairs_context( + parameters, + style=style, + threshold=threshold, + max_parameters=max_parameters, + ) + if context is None: + return None + + make_subplots = __import__('plotly.subplots', fromlist=['make_subplots']).make_subplots + subplot_title_annotations: list[dict[str, object]] = [] + subplot_border_shapes: list[dict[str, object]] = [] + legend_state = _PosteriorPairsLegendState() + fig = make_subplots( + rows=context.n_parameters, + cols=context.n_parameters, + shared_xaxes='columns', + horizontal_spacing=PAIR_PLOT_SUBPLOT_SPACING, + vertical_spacing=PAIR_PLOT_SUBPLOT_SPACING, + ) + + for row_index in range(context.n_parameters): + for col_index in range(context.n_parameters): + self._populate_posterior_pair_panel( + fig=fig, + context=context, + row_index=row_index, + col_index=col_index, + legend_state=legend_state, + subplot_title_annotations=subplot_title_annotations, + subplot_border_shapes=subplot_border_shapes, + ) + + self._finalize_posterior_pairs_figure( + fig=fig, + context=context, + subplot_title_annotations=subplot_title_annotations, + subplot_border_shapes=subplot_border_shapes, + ) + return fig + + def _posterior_pairs_context( + self, + parameters: list[object] | None, + *, + style: PosteriorPairPlotStyleEnum | str = 'auto', + threshold: float | None = DEFAULT_CORRELATION_THRESHOLD, + max_parameters: int | None = None, + ) -> _PosteriorPairsContext | None: + """Return the resolved inputs for a posterior pair plot.""" + posterior_samples, fit_results = self._get_posterior_samples_and_fit_results() + if posterior_samples is None or fit_results is None: + return None + + plot_style = self._validated_posterior_pair_plot_style(style) + + parameter_names, resolved_threshold = self._resolved_posterior_pair_parameter_names( + fit_results=fit_results, + parameters=parameters, + threshold=threshold, + max_parameters=max_parameters, + ) + if parameter_names is None: + return None + if len(parameter_names) < MIN_POSTERIOR_PARAMETER_COUNT: + log.warning('Posterior pair plots require at least two sampled parameters.') + return None + + selected_samples = self._selected_posterior_samples(posterior_samples, parameter_names) + if selected_samples is None: + return None + + n_parameters = len(parameter_names) + show_contours = self._posterior_pair_show_contours( + n_parameters=n_parameters, + style=plot_style, + ) + if plot_style is PosteriorPairPlotStyleEnum.AUTO and not show_contours: + log.warning( + 'Posterior pair plot auto mode disabled contours for ' + f'{n_parameters} parameters. Use style="full" to force ' + 'contours.' + ) + + density_samples = self._thin_posterior_samples( + selected_samples, + max_points=self._posterior_pair_density_max_points(n_parameters), + ) + scatter_samples = self._thin_posterior_samples( + selected_samples, + max_points=POSTERIOR_PAIR_SCATTER_MAX_POINTS, + ) + uncertainty_multiplier = self._posterior_pair_uncertainty_multiplier( + fit_results, + parameter_names, + ) + + return _PosteriorPairsContext( + fit_results=fit_results, + parameter_names=parameter_names, + labels=self._posterior_plot_labels(fit_results, parameter_names), + annotation_labels=self._square_matrix_axis_title_labels(parameter_names), + title=self._correlation_filtered_title( + self._posterior_pair_title(uncertainty_multiplier), + resolved_threshold, + ), + marginal_density_samples=selected_samples, + density_samples=density_samples, + scatter_samples=scatter_samples, + show_contours=show_contours, + contour_grid_size=self._posterior_pair_contour_grid_size(n_parameters), + axis_frame_color=self._plot_axis_frame_color(), + axis_ranges=self._posterior_pair_axis_ranges( + fit_results=fit_results, + parameter_names=parameter_names, + samples=selected_samples, + ), + ) + + def _resolved_posterior_pair_parameter_names( + self, + *, + fit_results: object, + parameters: list[object] | None, + threshold: float | None, + max_parameters: int | None, + ) -> tuple[list[str] | None, float]: + """Return pair-plot names and the effective cutoff.""" + parameter_names = self._resolve_posterior_parameter_names( + fit_results=fit_results, + parameters=parameters, + ) + if parameter_names is None: + return None, 0.0 + if parameters is not None: + return parameter_names, 0.0 + + corr_df = self._posterior_correlation_dataframe(fit_results) + if corr_df is None: + return parameter_names, 0.0 + + filtered_corr_df, resolved_threshold = self._resolve_correlation_filter( + corr_df.loc[parameter_names, parameter_names], + threshold=threshold, + max_parameters=max_parameters, + min_parameters=MIN_POSTERIOR_PARAMETER_COUNT, + ) + if filtered_corr_df is None: + return None, 0.0 + return list(filtered_corr_df.index), resolved_threshold + + def _posterior_pair_axis_ranges( + self, + *, + fit_results: object, + parameter_names: list[str], + samples: np.ndarray, + ) -> list[tuple[float, float]]: + """Return per-parameter axis ranges for a pair plot.""" + axis_ranges: list[tuple[float, float]] = [] + for index, parameter_name in enumerate(parameter_names): + lower_bound, upper_bound = self._posterior_parameter_bounds( + fit_results=fit_results, + parameter_name=parameter_name, + ) + axis_ranges.append( + self._posterior_axis_bounds( + samples[:, index], + lower_bound=lower_bound, + upper_bound=upper_bound, + ) + ) + return axis_ranges + + def _populate_posterior_pair_panel( + self, + *, + fig: object, + context: _PosteriorPairsContext, + row_index: int, + col_index: int, + legend_state: _PosteriorPairsLegendState, + subplot_title_annotations: list[dict[str, object]], + subplot_border_shapes: list[dict[str, object]], + ) -> None: + """Populate one panel in the posterior pair plot grid.""" + row = row_index + 1 + col = col_index + 1 + if col_index > row_index: + self._hide_posterior_pair_panel(fig=fig, row=row, col=col) + return + + if row_index == col_index: + self._add_posterior_pair_diagonal( + fig=fig, + context=context, + row=row, + col=col, + parameter_index=col_index, + legend_state=legend_state, + ) + else: + self._add_posterior_pair_off_diagonal( + fig=fig, + context=context, + row=row, + col=col, + row_index=row_index, + col_index=col_index, + legend_state=legend_state, + ) + + self._configure_posterior_pair_panel_axes( + fig=fig, + context=context, + row=row, + col=col, + row_index=row_index, + col_index=col_index, + ) + self._collect_posterior_pair_panel_decorations( + fig=fig, + context=context, + row_index=row_index, + col_index=col_index, + subplot_title_annotations=subplot_title_annotations, + subplot_border_shapes=subplot_border_shapes, + ) + + @staticmethod + def _hide_posterior_pair_panel( + *, + fig: object, + row: int, + col: int, + ) -> None: + """Hide an upper-triangle panel in the pair plot grid.""" + fig.update_xaxes(visible=False, row=row, col=col) + fig.update_yaxes(visible=False, row=row, col=col) + + def _add_posterior_pair_diagonal( + self, + *, + fig: object, + context: _PosteriorPairsContext, + row: int, + col: int, + parameter_index: int, + legend_state: _PosteriorPairsLegendState, + ) -> None: + """Add the diagonal marginal-density panel.""" + go = __import__('plotly.graph_objects', fromlist=['Histogram']) + density_values = context.marginal_density_samples[:, parameter_index] + density_trace = self._posterior_density_trace( + fit_results=context.fit_results, + parameter_name=context.parameter_names[parameter_index], + values=density_values, + trace_name=context.labels[parameter_index], + ) + if density_trace is None: + fig.add_trace( + go.Histogram( + x=density_values, + nbinsx=40, + histnorm='probability density', + marker={'color': POSTERIOR_PAIR_MARGINAL_DENSITY_LINE_COLOR}, + showlegend=False, + hovertemplate=self._posterior_pair_density_hovertemplate( + context.parameter_names[parameter_index] + ), + ), + row=row, + col=col, + ) + return + + self._style_posterior_pair_marginal_density_trace(density_trace) + density_trace.hovertemplate = self._posterior_pair_density_hovertemplate( + context.parameter_names[parameter_index] + ) + density_trace.name = 'Marginal density' + density_trace.legendgroup = 'posterior-marginal-density' + density_trace.showlegend = legend_state.show_density + fig.add_trace(density_trace, row=row, col=col) + legend_state.show_density = False + y_axis_range = self._posterior_density_axis_range(np.asarray(density_trace.y)) + if y_axis_range is not None: + fig.update_yaxes(range=list(y_axis_range), row=row, col=col) + + def _add_posterior_pair_off_diagonal( + self, + *, + fig: object, + context: _PosteriorPairsContext, + row: int, + col: int, + row_index: int, + col_index: int, + legend_state: _PosteriorPairsLegendState, + ) -> None: + """Add one off-diagonal pair-relationship panel.""" + go = __import__('plotly.graph_objects', fromlist=['Scatter']) + x_density_values = context.density_samples[:, col_index] + y_density_values = context.density_samples[:, row_index] + x_scatter_values = context.scatter_samples[:, col_index] + y_scatter_values = context.scatter_samples[:, row_index] + contour_traces = None + if context.show_contours: + contour_traces = self._posterior_contour_traces( + fit_results=context.fit_results, + x_parameter_name=context.parameter_names[col_index], + y_parameter_name=context.parameter_names[row_index], + x_values=x_density_values, + y_values=y_density_values, + grid_size=context.contour_grid_size, + ) + sample_hovertemplate = self._posterior_pair_scatter_hovertemplate( + x_parameter_name=context.parameter_names[col_index], + y_parameter_name=context.parameter_names[row_index], + ) + fig.add_trace( + go.Scatter( + x=x_scatter_values, + y=y_scatter_values, + mode='markers', + marker={ + 'color': POSTERIOR_SCATTER_MARKER_COLOR, + 'size': POSTERIOR_PAIR_SAMPLE_MARKER_SIZE, + }, + name='Posterior samples', + legendgroup='posterior-samples', + showlegend=legend_state.show_scatter, + hoverinfo='skip', + zorder=0, + ), + row=row, + col=col, + ) + legend_state.show_scatter = False + if contour_traces is not None: + contour_traces[0].name = 'Posterior contours' + contour_traces[0].legendgroup = 'posterior-contours' + contour_traces[0].showlegend = legend_state.show_contour + contour_traces[1].legendgroup = 'posterior-contours' + contour_traces[1].showlegend = False + fig.add_trace(contour_traces[0], row=row, col=col) + fig.add_trace(contour_traces[1], row=row, col=col) + legend_state.show_contour = False + fig.add_trace( + go.Scatter( + x=x_scatter_values, + y=y_scatter_values, + mode='markers', + marker={ + 'color': 'rgba(0, 0, 0, 0)', + 'size': POSTERIOR_PAIR_SAMPLE_HOVER_MARKER_SIZE, + }, + showlegend=False, + hovertemplate=sample_hovertemplate, + zorder=3, + ), + row=row, + col=col, + ) + + @staticmethod + def _configure_posterior_pair_panel_axes( + *, + fig: object, + context: _PosteriorPairsContext, + row: int, + col: int, + row_index: int, + col_index: int, + ) -> None: + """Apply axis styling and labels to one pair-plot panel.""" + is_diagonal = row_index == col_index + fig.update_xaxes( + showline=True, + mirror=True, + range=list(context.axis_ranges[col_index]), + zeroline=False, + showgrid=False, + layer='above traces', + linecolor=context.axis_frame_color, + linewidth=POSTERIOR_PAIR_AXIS_LINE_WIDTH, + ticks='', + ticklen=0, + tickwidth=0, + showticklabels=False, + row=row, + col=col, + ) + fig.update_yaxes( + showline=True, + mirror=True, + zeroline=False, + showgrid=False, + layer='above traces', + linecolor=context.axis_frame_color, + linewidth=POSTERIOR_PAIR_AXIS_LINE_WIDTH, + ticks='', + ticklen=0, + tickwidth=0, + showticklabels=False, + row=row, + col=col, + ) + if not is_diagonal: + fig.update_yaxes(range=list(context.axis_ranges[row_index]), row=row, col=col) + if is_diagonal: + fig.update_yaxes( + title_text=None, + row=row, + col=col, + ) + fig.update_xaxes(title_text=None, row=row, col=col) + + @staticmethod + def _collect_posterior_pair_panel_decorations( + *, + fig: object, + context: _PosteriorPairsContext, + row_index: int, + col_index: int, + subplot_title_annotations: list[dict[str, object]], + subplot_border_shapes: list[dict[str, object]], + ) -> None: + """Collect annotations and frame shapes for one pair panel.""" + row = row_index + 1 + col = col_index + 1 + subplot = fig.get_subplot(row, col) + x_mid = 0.5 * (subplot.xaxis.domain[0] + subplot.xaxis.domain[1]) + y_mid = 0.5 * (subplot.yaxis.domain[0] + subplot.yaxis.domain[1]) + if col_index == 0: + subplot_title_annotations.append({ + 'x': subplot.xaxis.domain[0], + 'xref': 'paper', + 'xanchor': 'right', + 'xshift': -POSTERIOR_PAIR_Y_TITLE_XSHIFT_PIXELS, + 'y': 0.5 * (subplot.yaxis.domain[0] + subplot.yaxis.domain[1]), + 'yref': 'paper', + 'yanchor': 'middle', + 'text': context.annotation_labels[row_index], + 'align': 'center', + 'font': {'size': POSTERIOR_PAIR_AXIS_TITLE_FONT_SIZE}, + 'textangle': -90, + 'showarrow': False, + }) + if row_index == context.n_parameters - 1: + subplot_title_annotations.append({ + 'x': x_mid, + 'xref': 'paper', + 'xanchor': 'center', + 'y': subplot.yaxis.domain[0], + 'yref': 'paper', + 'yanchor': 'top', + 'yshift': -POSTERIOR_PAIR_X_TITLE_YSHIFT_PIXELS, + 'text': context.annotation_labels[col_index], + 'align': 'center', + 'font': {'size': POSTERIOR_PAIR_AXIS_TITLE_FONT_SIZE}, + 'showarrow': False, + }) + subplot_border_shapes.extend([ + { + 'type': 'line', + 'xref': 'paper', + 'yref': 'paper', + 'x0': x_mid, + 'x1': x_mid, + 'y0': subplot.yaxis.domain[0], + 'y1': subplot.yaxis.domain[1], + 'line': { + 'color': POSTERIOR_PAIR_GUIDE_LINE_COLOR, + 'width': 1, + }, + 'layer': 'above', + }, + { + 'type': 'line', + 'xref': 'paper', + 'yref': 'paper', + 'x0': subplot.xaxis.domain[0], + 'x1': subplot.xaxis.domain[1], + 'y0': y_mid, + 'y1': y_mid, + 'line': { + 'color': POSTERIOR_PAIR_GUIDE_LINE_COLOR, + 'width': 1, + }, + 'layer': 'above', + }, + { + 'type': 'rect', + 'xref': 'paper', + 'yref': 'paper', + 'x0': subplot.xaxis.domain[0], + 'x1': subplot.xaxis.domain[1], + 'y0': subplot.yaxis.domain[0], + 'y1': subplot.yaxis.domain[1], + 'line': { + 'color': context.axis_frame_color, + 'width': POSTERIOR_PAIR_AXIS_LINE_WIDTH, + }, + 'fillcolor': 'rgba(0, 0, 0, 0)', + 'layer': 'above', + }, + ]) + + @staticmethod + def _square_matrix_title_annotation( + title: str, + annotation_labels: list[str], + ) -> dict[str, object]: + """Return a top-left title annotation for matrix plots.""" + return { + 'x': 0.0, + 'xref': 'paper', + 'xanchor': 'left', + 'xshift': -Plotter._square_matrix_title_left_shift(annotation_labels), + 'y': 1.0, + 'yref': 'paper', + 'yanchor': 'bottom', + 'yshift': SQUARE_MATRIX_TITLE_YSHIFT_PIXELS, + 'text': title, + 'font': {'size': POSTERIOR_PAIR_TITLE_FONT_SIZE}, + 'showarrow': False, + } + + @staticmethod + def _posterior_pair_title_annotation( + title: str, + annotation_labels: list[str], + ) -> dict[str, object]: + """Return the outer title annotation for the pair plot.""" + return Plotter._square_matrix_title_annotation( + title, + annotation_labels, + ) + + @staticmethod + def _square_matrix_title_left_shift(annotation_labels: list[str]) -> int: + """Return the title shift relative to the shared left margin.""" + extra_margin = Plotter._square_matrix_extra_axis_title_margin(annotation_labels) + return max( + 0, + SQUARE_MATRIX_LEFT_MARGIN_PIXELS + + extra_margin + - SQUARE_MATRIX_TITLE_LEFT_PADDING_PIXELS, + ) + + @staticmethod + def _square_matrix_gap_data_width(n_parameters: int) -> float: + """Return the gap width matching pair-plot spacing.""" + if n_parameters <= 1: + return 0.0 + + denominator = 1.0 - PAIR_PLOT_SUBPLOT_SPACING * (n_parameters - 1) + if denominator <= 0: + return 0.0 + return PAIR_PLOT_SUBPLOT_SPACING * n_parameters / denominator + + @classmethod + def _square_matrix_plot_extent(cls, n_parameters: int) -> float: + """Return the inner plot extent for one square matrix plot.""" + gap_width = cls._square_matrix_gap_data_width(n_parameters) + return float(n_parameters + max(0, n_parameters - 1) * gap_width) + + @classmethod + def _square_matrix_target_plot_size_pixels(cls, n_parameters: int) -> float: + """Return the target inner size for one square matrix plot.""" + cell_size = cls._posterior_pair_cell_size_pixels( + n_parameters, + available_width_pixels=PAIR_PLOT_ESTIMATED_CONTAINER_WIDTH_PIXELS, + ) + return cell_size * cls._square_matrix_plot_extent(n_parameters) + + @classmethod + def _square_matrix_layout_meta( + cls, + *, + n_parameters: int, + annotation_labels: list[str], + ) -> dict[str, object]: + """Return wrapper metadata for square matrix plots.""" + margins = cls._square_matrix_layout_margin(annotation_labels) + plot_size = cls._square_matrix_target_plot_size_pixels(n_parameters) + aspect_width = round(plot_size + int(margins['l']) + int(margins['r'])) + aspect_height = round(plot_size + int(margins['t']) + int(margins['b'])) + return { + SQUARE_MATRIX_FIXED_ASPECT_META_KEY: { + 'aspect_ratio': f'{aspect_width} / {aspect_height}', + } + } + + def _finalize_posterior_pairs_figure( + self, + *, + fig: object, + context: _PosteriorPairsContext, + subplot_title_annotations: list[dict[str, object]], + subplot_border_shapes: list[dict[str, object]], + ) -> None: + """Apply final layout settings to the posterior pair plot.""" + fig.update_layout( + autosize=True, + margin=self._square_matrix_layout_margin(context.annotation_labels), + bargap=0.05, + annotations=[ + self._posterior_pair_title_annotation( + context.title, + context.annotation_labels, + ), + *subplot_title_annotations, + ], + shapes=subplot_border_shapes, + meta=self._square_matrix_layout_meta( + n_parameters=context.n_parameters, + annotation_labels=context.annotation_labels, + ), + legend={ + 'bgcolor': 'rgba(0, 0, 0, 0)', + 'xanchor': 'right', + 'x': 0.995, + 'yanchor': 'top', + 'y': 0.995, + 'groupclick': 'togglegroup', + }, + ) + + @staticmethod + def _square_matrix_layout_margin(annotation_labels: list[str]) -> dict[str, int | bool]: + """Return outer margins sized for multiline matrix labels.""" + extra_margin = Plotter._square_matrix_extra_axis_title_margin(annotation_labels) + return { + 'autoexpand': False, + 'l': SQUARE_MATRIX_LEFT_MARGIN_PIXELS + extra_margin, + 'r': SQUARE_MATRIX_RIGHT_MARGIN_PIXELS, + 't': SQUARE_MATRIX_TOP_MARGIN_PIXELS, + 'b': SQUARE_MATRIX_BOTTOM_MARGIN_PIXELS + extra_margin, + } + + @staticmethod + def _square_matrix_extra_axis_title_margin(annotation_labels: list[str]) -> int: + """Return extra margin needed for multiline axis labels.""" + if not annotation_labels: + return 0 + + max_line_count = max( + Plotter._square_matrix_axis_title_line_count(label) for label in annotation_labels + ) + return max(0, max_line_count - 1) * SQUARE_MATRIX_AXIS_TITLE_LINE_HEIGHT_PIXELS + + @staticmethod + def _square_matrix_axis_title_line_count(label: str) -> int: + """Return the number of display lines in one axis title.""" + if not label: + return 1 + return label.count('
') + 1 + + @staticmethod + def _posterior_pair_cell_size_pixels( + n_parameters: int, + *, + available_width_pixels: float, + ) -> int: + """Return an estimated square cell size for a pair plot.""" + if n_parameters < 1: + return PAIR_PLOT_CELL_SIZE_PIXELS + + plot_width = max( + PAIR_PLOT_MIN_CELL_SIZE_PIXELS, + available_width_pixels - PAIR_PLOT_MARGIN_PIXELS, + ) + cell_size = plot_width / n_parameters + return round( + min( + PAIR_PLOT_CELL_SIZE_PIXELS, + max(PAIR_PLOT_MIN_CELL_SIZE_PIXELS, cell_size), + ) + ) + + @classmethod + def _posterior_pair_figure_height_pixels(cls, n_parameters: int) -> int: + """ + Return the initial figure height for a responsive pair plot. + """ + cell_size = cls._posterior_pair_cell_size_pixels( + n_parameters, + available_width_pixels=PAIR_PLOT_ESTIMATED_CONTAINER_WIDTH_PIXELS, + ) + return max( + PAIR_PLOT_MIN_SIZE_PIXELS, + cell_size * n_parameters + PAIR_PLOT_MARGIN_PIXELS, + ) + + @staticmethod + def _posterior_pair_contour_panel_count(n_parameters: int) -> int: + """Return the number of lower-triangle contour panels.""" + if n_parameters < MIN_POSTERIOR_PARAMETER_COUNT: + return 1 + return n_parameters * (n_parameters - 1) // 2 + + @classmethod + def _posterior_pair_density_max_points(cls, n_parameters: int) -> int: + """Return a KDE sample cap for interactive pair plots.""" + panel_count = cls._posterior_pair_contour_panel_count(n_parameters) + estimated_limit = round( + POSTERIOR_PAIR_TARGET_DENSITY_SAMPLE_BUDGET / panel_count, + ) + return max( + POSTERIOR_PAIR_MIN_DENSITY_SAMPLES, + min(POSTERIOR_PAIR_MAX_DENSITY_SAMPLES, estimated_limit), + ) + + @classmethod + def _posterior_pair_contour_grid_size(cls, n_parameters: int) -> int: + """Return contour grid size for current pair-plot width.""" + panel_count = cls._posterior_pair_contour_panel_count(n_parameters) + estimated_grid_size = round( + np.sqrt(POSTERIOR_PAIR_TARGET_CONTOUR_GRID_POINT_BUDGET / panel_count), + ) + return max( + POSTERIOR_PAIR_MIN_CONTOUR_GRID_SIZE, + min(POSTERIOR_PAIR_MAX_CONTOUR_GRID_SIZE, estimated_grid_size), + ) + + @staticmethod + def _validated_posterior_pair_plot_style( + style: PosteriorPairPlotStyleEnum | str, + ) -> PosteriorPairPlotStyleEnum: + """Return a validated posterior pair-plot rendering mode.""" + try: + return PosteriorPairPlotStyleEnum(style) + except ValueError as exc: + supported_styles = ', '.join(item.value for item in PosteriorPairPlotStyleEnum) + msg = f'style must be one of {supported_styles} for posterior pair plots.' + raise ValueError(msg) from exc + + @staticmethod + def _posterior_pair_show_contours( + *, + n_parameters: int, + style: PosteriorPairPlotStyleEnum, + ) -> bool: + """Return whether contours should be rendered.""" + if style is PosteriorPairPlotStyleEnum.FULL: + return True + if style is PosteriorPairPlotStyleEnum.FAST: + return False + return n_parameters <= POSTERIOR_PAIR_AUTO_MAX_CONTOUR_PARAMETERS + + def _plot_axis_frame_color(self) -> str: + """ + Return the shared axis-frame color for Plotly-backed plots. + """ + axis_frame_color = getattr(self._backend, '_axis_frame_color', None) + if callable(axis_frame_color): + return axis_frame_color() + return PlotlyPlotter._axis_frame_color() + + def _plot_legend_background_color(self) -> str: + """Return the shared legend background for Plotly plots.""" + legend_background_color = getattr(self._backend, '_legend_background_color', None) + if callable(legend_background_color): + return legend_background_color() + return PlotlyPlotter._legend_background_color() + + def _resolved_posterior_contour_surface( + self, + *, + fit_results: object, + x_parameter_name: str, + y_parameter_name: str, + x_values: np.ndarray, + y_values: np.ndarray, + grid_size: int, + ) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray | None] | None: + """Return cached or computed posterior contour surface data.""" + cached_surface = self._cached_posterior_pair_surface( + x_parameter_name=x_parameter_name, + y_parameter_name=y_parameter_name, + ) + if cached_surface is not None: + return cached_surface + + bounds = self._posterior_pair_bounds( + fit_results=fit_results, + x_parameter_name=x_parameter_name, + y_parameter_name=y_parameter_name, + x_values=x_values, + y_values=y_values, + ) + density_surface = self._posterior_pair_density_surface( + x_values=x_values, + y_values=y_values, + x_bounds=bounds[0], + y_bounds=bounds[1], + grid_size=grid_size, + ) + if density_surface is None: + return None + + x_grid, y_grid, density = density_surface + return x_grid, y_grid, density, None + + def _posterior_contour_traces( + self, + *, + fit_results: object, + x_parameter_name: str, + y_parameter_name: str, + x_values: np.ndarray, + y_values: np.ndarray, + grid_size: int, + ) -> tuple[object, object] | None: + """ + Return filled and line contour traces for posterior pair plots. + """ + go = __import__('plotly.graph_objects', fromlist=['Contour']) + + surface = self._resolved_posterior_contour_surface( + fit_results=fit_results, + x_parameter_name=x_parameter_name, + y_parameter_name=y_parameter_name, + x_values=x_values, + y_values=y_values, + grid_size=grid_size, + ) + if surface is None: + return None + + x_grid, y_grid, density, contour_levels = surface + + fill_colorscale, line_colorscale = self._posterior_pair_contour_colorscales( + x_values, + y_values, + ) + contour_start, contour_end, contour_size = self._posterior_contour_levels( + density=density, + contour_levels=contour_levels, + ) + fill_density = np.array(density, copy=True) + fill_density[fill_density < contour_start] = np.nan + fill_trace = go.Contour( + x=x_grid, + y=y_grid, + z=fill_density, + contours={ + 'coloring': 'fill', + 'showlabels': False, + 'showlines': False, + 'start': contour_start, + 'end': contour_end, + 'size': contour_size, + }, + colorscale=fill_colorscale, + zmin=contour_start, + zmax=contour_end, + connectgaps=False, + hoverinfo='skip', + showscale=False, + showlegend=False, + zorder=1, + ) + line_trace = go.Contour( + x=x_grid, + y=y_grid, + z=density, + contours={ + 'coloring': 'lines', + 'showlabels': False, + 'start': contour_start, + 'end': contour_end, + 'size': contour_size, + }, + colorscale=line_colorscale, + zmin=contour_start, + zmax=contour_end, + line={'width': 0.9}, + hoverinfo='skip', + showscale=False, + showlegend=False, + zorder=2, + ) + return fill_trace, line_trace + + def _cached_posterior_pair_surface( + self, + *, + x_parameter_name: str, + y_parameter_name: str, + ) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray | None] | None: + """ + Return a restored posterior pair-density surface when available. + """ + if self._project is None: + return None + + analysis = self._project.analysis + sidecar_data = getattr(analysis, '_persisted_fit_state_sidecar', {}) + pair_caches = sidecar_data.get('pair_caches', {}) + for cache in analysis.bayesian_pair_caches: + cache_x = cache.param_unique_name_x.value + cache_y = cache.param_unique_name_y.value + if {cache_x, cache_y} != {x_parameter_name, y_parameter_name}: + continue + + cache_data = pair_caches.get(cache.id.value) + if cache_data is None: + return None + + x_grid = np.asarray(cache_data.get('x'), dtype=float) + y_grid = np.asarray(cache_data.get('y'), dtype=float) + density = np.asarray(cache_data.get('density'), dtype=float) + contour_levels = cache_data.get('contour_levels') + if contour_levels is not None: + contour_levels = np.asarray(contour_levels, dtype=float) + + if x_parameter_name != cache_x or y_parameter_name != cache_y: + x_grid, y_grid = y_grid, x_grid + if density.ndim == PAIR_DENSITY_SURFACE_NDIM: + density = density.T + + expected_shape = (y_grid.size, x_grid.size) + if x_grid.ndim != 1 or y_grid.ndim != 1 or density.shape != expected_shape: + log.warning( + 'Persisted posterior pair cache is invalid for ' + f'{x_parameter_name!r} and {y_parameter_name!r}.' + ) + return None + return x_grid, y_grid, density, contour_levels + + return None + + @staticmethod + def _posterior_contour_levels( + *, + density: np.ndarray, + contour_levels: np.ndarray | None, + ) -> tuple[float, float, float]: + """Return contour start, end, and step for pair density.""" + if contour_levels is not None and contour_levels.ndim == 1 and contour_levels.size > 0: + finite_levels = contour_levels[np.isfinite(contour_levels)] + if finite_levels.size > 0: + start = float(finite_levels[0]) + end = float(finite_levels[-1]) + if finite_levels.size > 1: + size = float(np.min(np.diff(finite_levels))) + else: + size = max(end - start, abs(end) * 0.15, 1e-6) + if end > start and size > 0: + return start, end, size + + density_max = float(np.max(density)) + return ( + density_max * 0.20, + density_max * 0.95, + density_max * 0.15, + ) + + def _build_param_distribution_plot( + self, + param: object, + ) -> object | None: + """ + Build a Plotly posterior distribution plot for one parameter. + + Parameters + ---------- + param : object + Parameter descriptor to plot. + + Returns + ------- + object | None + Plotly figure, or ``None`` when posterior plotting is + unavailable. + """ + context = self._posterior_distribution_context(param) + if context is None: + return None + + go = __import__('plotly.graph_objects', fromlist=['Figure', 'Histogram']) + fig, layout_factory = self._posterior_distribution_figure( + go=go, + title=context.title, + label=context.label, + ) + histogram_bin_edges = self._posterior_distribution_histogram_bin_edges(context.values) + density_trace = self._posterior_density_trace( + fit_results=context.fit_results, + parameter_name=context.parameter_name, + values=context.values, + trace_name='Marginal density', + ) + if density_trace is not None: + self._style_posterior_pair_marginal_density_trace(density_trace) + density_trace.hovertemplate = self._posterior_pair_density_hovertemplate( + context.parameter_name + ) + x_axis_range = self._posterior_distribution_x_axis_range( + values=context.values, + density_trace=density_trace, + histogram_bin_edges=histogram_bin_edges, + ) + y_axis_range = self._posterior_distribution_y_axis_range( + values=context.values, + density_trace=density_trace, + histogram_bin_edges=histogram_bin_edges, + ) + + self._add_posterior_distribution_interval_traces( + fig=fig, + summary=context.summary, + y_axis_range=y_axis_range, + ) + self._add_posterior_distribution_histogram( + fig=fig, + go=go, + values=context.values, + histogram_bin_edges=histogram_bin_edges, + ) + self._add_posterior_distribution_density_trace(fig=fig, density_trace=density_trace) + self._add_posterior_distribution_reference_traces( + fig=fig, + summary=context.summary, + values=context.values, + y_axis_range=y_axis_range, + ) + self._apply_posterior_distribution_layout( + fig=fig, + layout_factory=layout_factory, + title=context.title, + label=context.label, + x_axis_range=x_axis_range, + y_axis_range=y_axis_range, + ) + return fig + + def _plot_ascii_param_distribution( + self, + param: object, + ) -> None: + """Render one posterior marginal on the ASCII backend.""" + context = self._posterior_distribution_context(param) + if context is None: + return + + lower_bound, upper_bound = self._posterior_parameter_bounds( + fit_results=context.fit_results, + parameter_name=context.parameter_name, + ) + density_curve = self._cached_posterior_density_curve(context.parameter_name) + if density_curve is None: + density_curve = self._posterior_density_curve( + context.values, + lower_bound=lower_bound, + upper_bound=upper_bound, + ) + if density_curve is None: + log.warning( + f'Posterior distribution is unavailable for parameter {context.parameter_name}.' + ) + return + + grid, density = density_curve + self._backend.plot_powder( + x=grid, + y_series=[density], + labels=['density'], + axes_labels=[context.label, 'Probability density'], + title=context.title, + height=self.height, + ) + + def _cached_posterior_density_curve( + self, + parameter_name: str, + ) -> tuple[np.ndarray, np.ndarray] | None: + """ + Return a restored posterior density curve for one parameter. + """ + if self._project is None: + return None + + sidecar_data = getattr(self._project.analysis, '_persisted_fit_state_sidecar', {}) + cache_data = sidecar_data.get('distribution_caches', {}).get(parameter_name) + if cache_data is None: + return None + + x_values = np.asarray(cache_data.get('x'), dtype=float) + density_values = np.asarray(cache_data.get('density'), dtype=float) + if x_values.ndim != 1 or density_values.shape != x_values.shape: + log.warning( + f'Persisted posterior distribution cache is invalid for {parameter_name!r}.' + ) + return None + return x_values, density_values + + def _posterior_distribution_context( + self, + param: object, + ) -> _PosteriorDistributionContext | None: + """Return the context for a posterior distribution plot.""" + fit_results = self._get_fit_result_for_correlation() + if fit_results is None: + return None + + posterior_samples = getattr(fit_results, 'posterior_samples', None) + if posterior_samples is None: + log.warning('Posterior samples are unavailable. Run a Bayesian fit first.') + return None + + parameter_names = self._resolve_posterior_parameter_names( + fit_results=fit_results, + parameters=[param], + ) + if parameter_names is None: + return None + + parameter_name = parameter_names[0] + samples = self._selected_posterior_samples(posterior_samples, [parameter_name]) + if samples is None: + return None + + label = self._posterior_plot_labels(fit_results, [parameter_name])[0] + return _PosteriorDistributionContext( + fit_results=fit_results, + parameter_name=parameter_name, + values=samples[:, 0], + label=label, + title=f'Posterior distribution: {parameter_name}', + summary=self._posterior_summary_by_name(fit_results).get(parameter_name), + ) + + def _posterior_distribution_figure( + self, + *, + go: object, + title: str, + label: str, + ) -> tuple[object, object | None]: + """Return the figure and optional backend layout factory.""" + layout_factory = getattr(self._backend, '_get_layout', None) + if callable(layout_factory): + figure = go.Figure(layout=layout_factory(title, [label, 'Probability density'])) + return figure, layout_factory + return go.Figure(), layout_factory + + def _posterior_distribution_y_axis_range( + self, + *, + values: np.ndarray, + density_trace: object | None, + histogram_bin_edges: np.ndarray | None, + ) -> tuple[float, float] | None: + """Return the y-axis range for a posterior distribution plot.""" + density_sources = [] + histogram_density = self._posterior_distribution_histogram_density( + values, + histogram_bin_edges, + ) + if histogram_density is not None: + density_sources.append(histogram_density) + if density_trace is not None: + density_sources.append(np.asarray(density_trace.y, dtype=float)) + if not density_sources: + return None + return self._posterior_density_axis_range(np.concatenate(density_sources)) + + @staticmethod + def _posterior_distribution_x_axis_range( + *, + values: np.ndarray, + density_trace: object | None, + histogram_bin_edges: np.ndarray | None, + ) -> tuple[float, float] | None: + """Return the x-axis range for a posterior distribution plot.""" + if density_trace is not None: + density_x = np.asarray(density_trace.x, dtype=float) + return float(density_x[0]), float(density_x[-1]) + + if histogram_bin_edges is not None: + return ( + float(histogram_bin_edges[0]), + float(histogram_bin_edges[-1]), + ) + + finite_values = np.asarray(values, dtype=float) + finite_values = finite_values[np.isfinite(finite_values)] + if finite_values.size == 0: + return None + return float(np.min(finite_values)), float(np.max(finite_values)) + + @staticmethod + def _posterior_distribution_histogram_bin_edges(values: np.ndarray) -> np.ndarray | None: + """Return histogram bin edges used by the distribution plot.""" + finite_values = np.asarray(values, dtype=float) + finite_values = finite_values[np.isfinite(finite_values)] + if finite_values.size == 0: + return None + + bin_edges = np.histogram_bin_edges(finite_values, bins='auto') + if bin_edges.size >= MIN_POSTERIOR_SAMPLE_COUNT: + return np.asarray(bin_edges, dtype=float) + return None + + @staticmethod + def _posterior_distribution_histogram_density( + values: np.ndarray, + histogram_bin_edges: np.ndarray | None, + ) -> np.ndarray | None: + """Return densities matching the rendered histogram bins.""" + if histogram_bin_edges is None: + return None + + histogram_density, _ = np.histogram( + np.asarray(values, dtype=float), + bins=histogram_bin_edges, + density=True, + ) + return np.asarray(histogram_density, dtype=float) + + def _add_posterior_distribution_interval_traces( + self, + *, + fig: object, + summary: object | None, + y_axis_range: tuple[float, float] | None, + ) -> None: + """Add credible-interval bands to the distribution plot.""" + if summary is None or y_axis_range is None: + return + + fig.add_trace( + self._posterior_interval_band_trace( + x0=summary.interval_95[0], + x1=summary.interval_95[1], + y_axis_range=y_axis_range, + trace_name='95% credible interval', + color=POSTERIOR_INTERVAL_95_FILL_COLOR, + ) + ) + + @staticmethod + def _add_posterior_distribution_histogram( + *, + fig: object, + go: object, + values: np.ndarray, + histogram_bin_edges: np.ndarray | None, + ) -> None: + """Add the histogram trace for a posterior distribution plot.""" + histogram_kwargs: dict[str, object] = {} + if ( + histogram_bin_edges is not None + and histogram_bin_edges.size >= MIN_POSTERIOR_SAMPLE_COUNT + ): + histogram_kwargs['xbins'] = { + 'start': float(histogram_bin_edges[0]), + 'end': float(histogram_bin_edges[-1]), + 'size': float(histogram_bin_edges[1] - histogram_bin_edges[0]), + } + + fig.add_trace( + go.Histogram( + x=values, + histnorm='probability density', + marker={ + 'color': POSTERIOR_HISTOGRAM_FILL_COLOR, + 'line': {'color': POSTERIOR_HISTOGRAM_LINE_COLOR, 'width': 1}, + }, + opacity=0.82, + name='Posterior histogram', + hovertemplate='sample=%{x:.4f}
density: %{y:.2f}', + **histogram_kwargs, + ) + ) + + @staticmethod + def _add_posterior_distribution_density_trace( + *, + fig: object, + density_trace: object | None, + ) -> None: + """Add the KDE trace for a posterior distribution plot.""" + if density_trace is None: + return + + density_trace.name = 'Marginal density' + density_trace.showlegend = True + fig.add_trace(density_trace) + + def _add_posterior_distribution_reference_traces( + self, + *, + fig: object, + summary: object | None, + values: np.ndarray, + y_axis_range: tuple[float, float] | None, + ) -> None: + """Add posterior median and MAP reference lines.""" + if y_axis_range is None: + return + + fig.add_trace( + self._posterior_reference_line_trace( + x_value=float(np.median(values)), + y_axis_range=y_axis_range, + trace_name='Median', + color=POSTERIOR_MEDIAN_LINE_COLOR, + dash='dash', + ) + ) + if summary is None: + return + + fig.add_trace( + self._posterior_reference_line_trace( + x_value=summary.best_sample_value, + y_axis_range=y_axis_range, + trace_name=POSTERIOR_POINT_ESTIMATE_TRACE_NAME, + color=POSTERIOR_POINT_ESTIMATE_LINE_COLOR, + dash=POSTERIOR_POINT_ESTIMATE_LINE_DASH, + ) + ) + + @staticmethod + def _apply_posterior_distribution_layout( + *, + fig: object, + layout_factory: object | None, + title: str, + label: str, + x_axis_range: tuple[float, float] | None, + y_axis_range: tuple[float, float] | None, + ) -> None: + """Apply layout settings to the distribution plot.""" + if callable(layout_factory): + fig.update_layout( + title={ + 'text': title, + 'font': {'size': POSTERIOR_PAIR_TITLE_FONT_SIZE}, + } + ) + else: + fig.update_layout( + title={ + 'text': title, + 'font': {'size': POSTERIOR_PAIR_TITLE_FONT_SIZE}, + }, + xaxis_title=label, + yaxis_title='Probability density', + legend={ + 'bgcolor': 'rgba(0, 0, 0, 0)', + 'xanchor': 'right', + 'x': 1.0, + 'yanchor': 'top', + 'y': 1.0, + }, + ) + fig.update_xaxes(title_font={'size': POSTERIOR_PAIR_AXIS_TITLE_FONT_SIZE}) + fig.update_yaxes(title_font={'size': POSTERIOR_PAIR_AXIS_TITLE_FONT_SIZE}) + if x_axis_range is not None: + fig.update_xaxes(range=list(x_axis_range)) + if y_axis_range is not None: + fig.update_yaxes(range=list(y_axis_range)) + + def _show_plot_figure(self, figure: object) -> None: + """Display a figure through the active backend when possible.""" + show_figure = getattr(self._backend, '_show_figure', None) + if callable(show_figure): + show_figure(figure) + return + figure.show() + + @staticmethod + def _style_posterior_pair_marginal_density_trace(density_trace: object) -> None: + """Apply pair-plot-specific styling to a marginal KDE trace.""" + density_trace.line = { + 'color': POSTERIOR_PAIR_MARGINAL_DENSITY_LINE_COLOR, + 'width': POSTERIOR_PAIR_MARGINAL_DENSITY_LINE_WIDTH, + } + density_trace.fillcolor = POSTERIOR_PAIR_MARGINAL_DENSITY_FILL_COLOR + + @staticmethod + def _posterior_pair_density_hovertemplate(parameter_name: str) -> str: + """Return the hover template for a marginal density trace.""" + return f'{parameter_name}: %{{x:.4f}}
density: %{{y:.4f}}' + + @staticmethod + def _posterior_pair_scatter_hovertemplate( + *, + x_parameter_name: str, + y_parameter_name: str, + ) -> str: + """Return the hover template for pair-plot sample points.""" + return f'{x_parameter_name}: %{{x:.4f}}
{y_parameter_name}: %{{y:.4f}}' + + @staticmethod + def _posterior_pair_correlation_value( + x_values: np.ndarray, + y_values: np.ndarray, + ) -> float | None: + """Return the sample correlation for one contour panel.""" + finite_mask = np.isfinite(x_values) & np.isfinite(y_values) + if np.count_nonzero(finite_mask) < MIN_POSTERIOR_SAMPLE_COUNT: + return None + + correlation_matrix = np.corrcoef(x_values[finite_mask], y_values[finite_mask]) + correlation_value = float(correlation_matrix[0, 1]) + if not np.isfinite(correlation_value): + return None + return correlation_value + + @staticmethod + def _posterior_pair_contour_colorscales( + x_values: np.ndarray, + y_values: np.ndarray, + ) -> tuple[list[list[float | str]], list[list[float | str]]]: + """Return sign-aware contour palettes for one panel.""" + correlation_value = Plotter._posterior_pair_correlation_value(x_values, y_values) + if correlation_value is not None and correlation_value < 0: + return ( + POSTERIOR_NEGATIVE_CONTOUR_FILL_COLORSCALE, + POSTERIOR_NEGATIVE_CONTOUR_LINE_COLORSCALE, + ) + return POSTERIOR_CONTOUR_FILL_COLORSCALE, POSTERIOR_CONTOUR_LINE_COLORSCALE + + def _posterior_density_trace( + self, + *, + fit_results: object, + parameter_name: str, + values: np.ndarray, + trace_name: str, + ) -> object | None: + """Return a filled KDE trace for one posterior marginal.""" + go = __import__('plotly.graph_objects', fromlist=['Scatter']) + + lower_bound, upper_bound = self._posterior_parameter_bounds( + fit_results=fit_results, + parameter_name=parameter_name, + ) + density_curve = self._cached_posterior_density_curve(parameter_name) + if density_curve is None: + density_curve = self._posterior_density_curve( + values, + lower_bound=lower_bound, + upper_bound=upper_bound, + ) + if density_curve is None: + return None + + grid, density = density_curve + return go.Scatter( + x=grid, + y=density, + mode='lines', + line={'color': POSTERIOR_DENSITY_LINE_COLOR, 'width': 2}, + fill='tozeroy', + fillcolor=POSTERIOR_DENSITY_FILL_COLOR, + name=trace_name, + showlegend=False, + hovertemplate='%{x:.4f}
density: %{y:.2f}', + ) + + @staticmethod + def _posterior_density_axis_range( + density_values: np.ndarray, + ) -> tuple[float, float] | None: + """Return a padded y-axis range for posterior density plots.""" + data = np.asarray(density_values, dtype=float) + data = data[np.isfinite(data)] + if data.size == 0: + return None + + data_min = float(np.min(data)) + data_max = float(np.max(data)) + data_range = data_max - data_min + upper_padding = 0.08 * data_range if data_range > 0 else max(abs(data_max), 1.0) * 0.05 + if upper_padding == 0: + upper_padding = 1e-6 + lower = min(0.0, data_min) + return lower, data_max + upper_padding + + @staticmethod + def _posterior_interval_band_trace( + *, + x0: float, + x1: float, + y_axis_range: tuple[float, float], + trace_name: str, + color: str, + ) -> object: + """Return a hideable credible-interval band trace.""" + go = __import__('plotly.graph_objects', fromlist=['Scatter']) + + return go.Scatter( + x=[x0, x1, x1, x0, x0], + y=[ + y_axis_range[0], + y_axis_range[0], + y_axis_range[1], + y_axis_range[1], + y_axis_range[0], + ], + mode='lines', + fill='toself', + fillcolor=color, + line={'color': color, 'width': 0}, + name=trace_name, + showlegend=True, + hoverinfo='skip', + ) + + @staticmethod + def _posterior_reference_line_trace( + *, + x_value: float, + y_axis_range: tuple[float, float], + trace_name: str, + color: str, + dash: str, + ) -> object: + """ + Return a named vertical reference line for posterior plots. + """ + go = __import__('plotly.graph_objects', fromlist=['Scatter']) + + return go.Scatter( + x=[x_value, x_value], + y=[y_axis_range[0], y_axis_range[1]], + mode='lines', + line={'color': color, 'width': 2, 'dash': dash}, + name=trace_name, + showlegend=True, + hovertemplate=f'{trace_name}: %{{x:.4f}}', + ) + + @staticmethod + def _posterior_parameter_bounds( + *, + fit_results: object, + parameter_name: str, + ) -> tuple[float | None, float | None]: + """Return finite fit bounds for a posterior parameter.""" + parameters_by_name = { + getattr(parameter, 'unique_name', ''): parameter + for parameter in fit_results.parameters + } + parameter = parameters_by_name.get(parameter_name) + if parameter is None: + return None, None + + lower_bound = getattr(parameter, 'fit_min', None) + upper_bound = getattr(parameter, 'fit_max', None) + lower = ( + float(lower_bound) + if lower_bound is not None and np.isfinite(float(lower_bound)) + else None + ) + upper = ( + float(upper_bound) + if upper_bound is not None and np.isfinite(float(upper_bound)) + else None + ) + return lower, upper + + @classmethod + def _posterior_pair_bounds( + cls, + *, + fit_results: object, + x_parameter_name: str, + y_parameter_name: str, + x_values: np.ndarray, + y_values: np.ndarray, + ) -> tuple[tuple[float, float], tuple[float, float]]: + """Return plotting bounds for a posterior pair panel.""" + x_lower, x_upper = cls._posterior_parameter_bounds( + fit_results=fit_results, + parameter_name=x_parameter_name, + ) + y_lower, y_upper = cls._posterior_parameter_bounds( + fit_results=fit_results, + parameter_name=y_parameter_name, + ) + return ( + cls._posterior_axis_bounds(x_values, lower_bound=x_lower, upper_bound=x_upper), + cls._posterior_axis_bounds(y_values, lower_bound=y_lower, upper_bound=y_upper), + ) + + @staticmethod + def _posterior_axis_bounds( + values: np.ndarray, + *, + lower_bound: float | None, + upper_bound: float | None, + ) -> tuple[float, float]: + """Return finite plotting bounds for one posterior axis.""" + data = np.asarray(values, dtype=float) + data = data[np.isfinite(data)] + data_min = float(np.min(data)) + data_max = float(np.max(data)) + data_range = data_max - data_min + padding = 0.05 * data_range if data_range > 0 else max(abs(data_min), 1.0) * 0.05 + if padding == 0: + padding = 1e-6 + + resolved_lower = lower_bound if lower_bound is not None else data_min - padding + resolved_upper = upper_bound if upper_bound is not None else data_max + padding + return float(resolved_lower), float(resolved_upper) + + @classmethod + def _posterior_density_curve( + cls, + values: np.ndarray, + *, + lower_bound: float | None, + upper_bound: float | None, + grid_size: int = 256, + ) -> tuple[np.ndarray, np.ndarray] | None: + """Estimate a boundary-aware posterior density curve.""" + gaussian_kde = __import__('scipy.stats', fromlist=['gaussian_kde']).gaussian_kde + + data = np.asarray(values, dtype=float) + data = data[np.isfinite(data)] + if data.size < MIN_POSTERIOR_SAMPLE_COUNT: + return None + + data_min = float(np.min(data)) + data_max = float(np.max(data)) + data_range = data_max - data_min + padding = 0.05 * data_range if data_range > 0 else max(abs(data_min), 1.0) * 0.05 + if padding == 0: + padding = 1e-6 + + grid_lower = lower_bound if lower_bound is not None else data_min - padding + grid_upper = upper_bound if upper_bound is not None else data_max + padding + if grid_upper <= grid_lower: + return None + + grid = np.linspace(grid_lower, grid_upper, num=grid_size) + if np.allclose(data, data[0]): + bandwidth = max(abs(data[0]) * 0.01, 1e-6) + density = np.exp(-0.5 * ((grid - data[0]) / bandwidth) ** 2) + density /= bandwidth * np.sqrt(2.0 * np.pi) + else: + kde = gaussian_kde(data) + density = cls._evaluate_reflected_1d_kde( + kde, + grid, + lower_bound=lower_bound, + upper_bound=upper_bound, + ) + + area = np.trapezoid(density, grid) + if area <= 0: + return None + return grid, density / area + + @classmethod + def _posterior_pair_density_surface( + cls, + *, + x_values: np.ndarray, + y_values: np.ndarray, + x_bounds: tuple[float, float], + y_bounds: tuple[float, float], + grid_size: int = 96, + ) -> tuple[np.ndarray, np.ndarray, np.ndarray] | None: + """Estimate a 2D KDE surface for a posterior pair panel.""" + gaussian_kde = __import__('scipy.stats', fromlist=['gaussian_kde']).gaussian_kde + + x_data = np.asarray(x_values, dtype=float) + y_data = np.asarray(y_values, dtype=float) + mask = np.isfinite(x_data) & np.isfinite(y_data) + x_data = x_data[mask] + y_data = y_data[mask] + if x_data.size < MIN_POSTERIOR_SAMPLE_COUNT or y_data.size < MIN_POSTERIOR_SAMPLE_COUNT: + return None + + if np.allclose(x_data, x_data[0]) and np.allclose(y_data, y_data[0]): + return None + + pair_data = np.vstack([x_data, y_data]) + covariance = np.cov(pair_data) + if np.linalg.matrix_rank(covariance) < FULL_POSTERIOR_PAIR_COVARIANCE_RANK: + return None + + x_grid = np.linspace(x_bounds[0], x_bounds[1], num=grid_size) + y_grid = np.linspace(y_bounds[0], y_bounds[1], num=grid_size) + mesh_x, mesh_y = np.meshgrid(x_grid, y_grid) + try: + density = cls._evaluate_reflected_2d_kde( + gaussian_kde(pair_data), + mesh_x=mesh_x, + mesh_y=mesh_y, + x_bounds=x_bounds, + y_bounds=y_bounds, + ) + except (np.linalg.LinAlgError, ValueError): + return None + if not np.any(np.isfinite(density)): + return None + return x_grid, y_grid, density + + @staticmethod + def _reflection_positions_1d( + values: np.ndarray, + *, + lower_bound: float | None, + upper_bound: float | None, + ) -> list[np.ndarray]: + """Return mirrored positions for boundary-corrected KDEs.""" + reflected = [values] + if lower_bound is not None: + reflected.append(2.0 * lower_bound - values) + if upper_bound is not None: + reflected.append(2.0 * upper_bound - values) + return reflected + + @classmethod + def _evaluate_reflected_1d_kde( + cls, + kde: object, + grid: np.ndarray, + *, + lower_bound: float | None, + upper_bound: float | None, + ) -> np.ndarray: + """Evaluate a 1D KDE using mirrored-boundary correction.""" + density = np.zeros_like(grid, dtype=float) + for reflected_grid in cls._reflection_positions_1d( + grid, + lower_bound=lower_bound, + upper_bound=upper_bound, + ): + density += np.asarray(kde(reflected_grid), dtype=float) + return density + + @classmethod + def _evaluate_reflected_2d_kde( + cls, + kde: object, + *, + mesh_x: np.ndarray, + mesh_y: np.ndarray, + x_bounds: tuple[float, float], + y_bounds: tuple[float, float], + ) -> np.ndarray: + """Evaluate a 2D KDE using mirrored-boundary correction.""" + x_positions = cls._reflection_positions_1d( + mesh_x.ravel(), + lower_bound=x_bounds[0], + upper_bound=x_bounds[1], + ) + y_positions = cls._reflection_positions_1d( + mesh_y.ravel(), + lower_bound=y_bounds[0], + upper_bound=y_bounds[1], + ) + density = np.zeros(mesh_x.size, dtype=float) + for reflected_x in x_positions: + for reflected_y in y_positions: + positions = np.vstack([reflected_x, reflected_y]) + density += np.asarray(kde(positions), dtype=float) + return density.reshape(mesh_x.shape) + + def _get_or_build_posterior_predictive_summary( + self, + *, + experiment: object, + expt_name: str, + x_axis: object, + include_draws: bool = True, + ) -> object | None: + """Return a cached or built predictive summary.""" + fit_results = self._get_fit_result_for_correlation() + if fit_results is None: + return None + + posterior_predictive = getattr(fit_results, 'posterior_predictive', None) + if posterior_predictive is None: + return None + + x_axis_name = getattr(x_axis, 'value', x_axis) + draw_cache_key = self._posterior_predictive_key( + expt_name, + str(x_axis_name), + include_draws=True, + ) + band_cache_key = self._posterior_predictive_key( + expt_name, + str(x_axis_name), + include_draws=False, + ) + cache_key = draw_cache_key if include_draws else band_cache_key + summary = posterior_predictive.get(cache_key) + if summary is None and not include_draws: + summary = posterior_predictive.get(draw_cache_key) + if summary is None: + summary = posterior_predictive.get(expt_name) + summary_x_axis = getattr(summary, 'x_axis_name', None) + if summary is not None and str(summary_x_axis) != str(x_axis_name): + summary = None + if summary is not None: + return summary + + posterior_samples = getattr(fit_results, 'posterior_samples', None) + if posterior_samples is None: + return None + + summary = self._build_posterior_predictive_summary( + fit_results=fit_results, + experiment=experiment, + expt_name=expt_name, + x_axis=x_axis, + include_draws=include_draws, + ) + if summary is None: + return None + + posterior_predictive[cache_key] = summary + return summary + + def _build_posterior_predictive_summary( + self, + *, + fit_results: object, + experiment: object, + expt_name: str, + x_axis: object, + include_draws: bool = True, + ) -> object | None: + """Build posterior predictive summaries from posterior draws.""" + sampling_inputs = self._posterior_predictive_sampling_inputs(fit_results) + if sampling_inputs is None: + return None + + flattened_samples, parameter_names = sampling_inputs + sampled_parameters = self._posterior_predictive_parameters( + fit_results=fit_results, + parameter_names=parameter_names, + ) + if sampled_parameters is None: + return None + + predictive_data = self._evaluate_posterior_predictive_draws( + flattened_samples=flattened_samples, + sampled_parameters=sampled_parameters, + experiment=experiment, + expt_name=expt_name, + x_axis=x_axis, + ) + if predictive_data is None: + return None + + best_sample_prediction, x_values, predictive_draw_array = predictive_data + lower_68, upper_68 = np.quantile(predictive_draw_array, [0.16, 0.84], axis=0) + lower_95, upper_95 = np.quantile(predictive_draw_array, [0.025, 0.975], axis=0) + x_axis_name = getattr(x_axis, 'value', x_axis) + + return PosteriorPredictiveSummary( + experiment_name=expt_name, + x_axis_name=str(x_axis_name), + x=np.asarray(x_values, dtype=float), + best_sample_prediction=np.asarray(best_sample_prediction, dtype=float), + lower_95=np.asarray(lower_95, dtype=float), + upper_95=np.asarray(upper_95, dtype=float), + lower_68=np.asarray(lower_68, dtype=float), + upper_68=np.asarray(upper_68, dtype=float), + draws=predictive_draw_array if include_draws else None, + ) + + @staticmethod + def _posterior_predictive_sampling_inputs( + fit_results: object, + ) -> tuple[np.ndarray, list[str]] | None: + """Return predictive-sampling arrays and parameter names.""" + posterior_samples = getattr(fit_results, 'posterior_samples', None) + if posterior_samples is None: + return None + + flattened_samples = np.asarray(posterior_samples.flattened(), dtype=float) + parameter_names = getattr(posterior_samples, 'parameter_names', None) + if flattened_samples.ndim != POSTERIOR_FLATTENED_SAMPLE_NDIM or not parameter_names: + log.warning('Posterior samples are unavailable for predictive summaries.') + return None + return flattened_samples, list(parameter_names) + + @staticmethod + def _posterior_predictive_parameters( + *, + fit_results: object, + parameter_names: list[str], + ) -> list[object] | None: + """Return fitted parameters in posterior sample order.""" + parameters_by_name = { + getattr(parameter, 'unique_name', ''): parameter + for parameter in fit_results.parameters + } + sampled_parameters: list[object] = [] + for name in parameter_names: + parameter = parameters_by_name.get(name) + if parameter is None: + log.warning( + 'Posterior predictive summaries require matching fitted parameters for ' + f"'{name}'." + ) + return None + sampled_parameters.append(parameter) + return sampled_parameters + + def _evaluate_posterior_predictive_draws( + self, + *, + flattened_samples: np.ndarray, + sampled_parameters: list[object], + experiment: object, + expt_name: str, + x_axis: object, + ) -> tuple[np.ndarray, np.ndarray, np.ndarray] | None: + """Return best-sample and sampled predictive curves.""" + original_values = np.array( + [parameter.value for parameter in sampled_parameters], + dtype=float, + ) + original_uncertainties = [parameter.uncertainty for parameter in sampled_parameters] + predictive_draws: list[np.ndarray] = [] + draw_indices = self._posterior_predictive_draw_indices(flattened_samples.shape[0]) + + try: + best_sample_prediction, x_values = self._evaluate_posterior_predictive_state( + sampled_parameters=sampled_parameters, + values=original_values, + experiment=experiment, + expt_name=expt_name, + x_axis=x_axis, + ) + if best_sample_prediction is None or x_values is None: + return None + + for index in draw_indices: + prediction, current_x = self._evaluate_posterior_predictive_state( + sampled_parameters=sampled_parameters, + values=flattened_samples[index], + experiment=experiment, + expt_name=expt_name, + x_axis=x_axis, + ) + if prediction is None or current_x is None: + return None + if ( + prediction.shape != best_sample_prediction.shape + or current_x.shape != x_values.shape + ): + log.warning('Posterior predictive draws returned inconsistent array shapes.') + return None + predictive_draws.append(prediction) + finally: + self._restore_posterior_predictive_parameters( + sampled_parameters=sampled_parameters, + original_values=original_values, + original_uncertainties=original_uncertainties, + expt_name=expt_name, + ) + + return ( + np.asarray(best_sample_prediction, dtype=float), + np.asarray(x_values, dtype=float), + np.asarray(predictive_draws, dtype=float), + ) + + def _restore_posterior_predictive_parameters( + self, + *, + sampled_parameters: list[object], + original_values: np.ndarray, + original_uncertainties: list[float | None], + expt_name: str, + ) -> None: + """Restore parameter state after predictive sampling.""" + for parameter, value, uncertainty in zip( + sampled_parameters, + original_values, + original_uncertainties, + strict=True, + ): + parameter._set_value_from_minimizer(float(value)) + parameter.uncertainty = uncertainty + self._update_project_categories(expt_name) + + def _evaluate_posterior_predictive_state( + self, + *, + sampled_parameters: list[object], + values: np.ndarray, + experiment: object, + expt_name: str, + x_axis: object, + ) -> tuple[np.ndarray | None, np.ndarray | None]: + """Evaluate one posterior predictive state for an experiment.""" + for parameter, value in zip(sampled_parameters, values, strict=True): + parameter._set_value_from_minimizer(float(value)) + + self._update_project_categories(expt_name) + pattern = intensity_category_for(experiment) + x_name = getattr(x_axis, 'value', x_axis) + x_values = getattr(pattern, x_name, None) + y_calc = getattr(pattern, 'intensity_calc', None) + if x_values is None or y_calc is None: + log.warning( + f'Posterior predictive data is unavailable for experiment {expt_name}. ' + 'Ensure calculated intensities and the selected x axis are available.' + ) + return None, None + + return np.asarray(y_calc, dtype=float), np.asarray(x_values, dtype=float) + + @staticmethod + def _posterior_predictive_draw_indices(n_draws: int) -> np.ndarray: + """ + Select evenly spaced posterior draws for predictive summaries. + """ + if n_draws <= DEFAULT_POSTERIOR_PREDICTIVE_DRAWS: + return np.arange(n_draws, dtype=int) + + return np.unique( + np.linspace( + 0, + n_draws - 1, + num=DEFAULT_POSTERIOR_PREDICTIVE_DRAWS, + dtype=int, + ) + ) + + @staticmethod + def _posterior_predictive_key( + expt_name: str, + x_axis_name: str, + *, + include_draws: bool = True, + ) -> str: + """Return the cache key for a posterior predictive summary.""" + key_suffix = 'draws' if include_draws else 'band' + return f'{expt_name}:{x_axis_name}:{key_suffix}' + + def _get_posterior_inference_data( + self, + ) -> tuple[object | None, object | None]: + """ + Return posterior inference data for the current Bayesian fit. + + Returns + ------- + tuple[object | None, object | None] + ``(inference_data, fit_results)`` when posterior samples are + available, otherwise ``(None, None)``. + """ + if self.engine != PlotterEngineEnum.PLOTLY.value: + log.warning('Posterior plots currently require the Plotly plotting backend.') + return None, None + + fit_results = self._get_fit_result_for_correlation() + if fit_results is None: + return None, None + + posterior_samples = getattr(fit_results, 'posterior_samples', None) + if posterior_samples is None: + log.warning('Posterior samples are unavailable. Run a Bayesian fit first.') + return None, None + + return posterior_samples.to_arviz(), fit_results + + def _get_posterior_samples_and_fit_results( + self, + ) -> tuple[object | None, object | None]: + """Return posterior samples and fit results for plotting.""" + if self.engine != PlotterEngineEnum.PLOTLY.value: + log.warning('Posterior plots currently require the Plotly plotting backend.') + return None, None + + fit_results = self._get_fit_result_for_correlation() + if fit_results is None: + return None, None + + posterior_samples = getattr(fit_results, 'posterior_samples', None) + if posterior_samples is None: + log.warning('Posterior samples are unavailable. Run a Bayesian fit first.') + return None, None + + return posterior_samples, fit_results + + def _plot_posterior_predictive_summary( + self, + *, + expt_name: str, + summary: object, + y_meas: np.ndarray, + axes_labels: list[str], + show_band: bool, + show_draws: bool, + excluded_ranges: tuple[tuple[float, float], ...] = (), + ) -> None: + """ + Render posterior predictive summaries using the active backend. + """ + if self.engine == PlotterEngineEnum.ASCII.value: + self._plot_ascii_posterior_predictive_lines( + expt_name=expt_name, + x=np.asarray(summary.x, dtype=float), + y_meas=np.asarray(y_meas, dtype=float), + y_calc=np.asarray(summary.best_sample_prediction, dtype=float), + axes_labels=axes_labels, + excluded_ranges=excluded_ranges, + ) + return + + go = __import__('plotly.graph_objects', fromlist=['Figure', 'Scatter']) + axis_frame_color = self._plot_axis_frame_color() + + fig = go.Figure() + if show_band: + fig.add_trace( + go.Scatter( + x=summary.x, + y=summary.lower_95, + mode='lines', + line={'color': 'rgba(0, 0, 0, 0)'}, + hoverinfo='skip', + showlegend=False, + ) + ) + fig.add_trace( + go.Scatter( + x=summary.x, + y=summary.upper_95, + mode='lines', + line={'color': 'rgba(0, 0, 0, 0)'}, + fill='tonexty', + fillcolor=POSTERIOR_INTERVAL_95_FILL_COLOR, + name=POSTERIOR_PREDICTIVE_INTERVAL_TRACE_NAME, + hoverinfo='skip', + legendrank=30, + ) + ) + + if show_draws: + draws = getattr(summary, 'draws', None) + if draws is None: + log.warning('Posterior predictive draws are unavailable for plotting.') + return + + draw_cap = min(len(draws), DEFAULT_POSTERIOR_PREDICTIVE_DRAW_PLOT_CAP) + for index in range(draw_cap): + fig.add_trace( + go.Scatter( + x=summary.x, + y=draws[index], + mode='lines', + line={'color': POSTERIOR_DRAW_LINE_COLOR, 'width': 1}, + name='Posterior draw' if index == 0 else None, + showlegend=index == 0, + legendrank=40, + ) + ) + + fig.add_trace( + go.Scatter( + x=summary.x, + y=y_meas, + mode='lines+markers', + line={'color': 'rgb(31, 119, 180)', 'width': 1.5}, + name='Measured', + legendrank=10, + ) + ) + fig.add_trace( + go.Scatter( + x=summary.x, + y=summary.best_sample_prediction, + mode='lines', + line={ + 'color': POSTERIOR_POINT_ESTIMATE_LINE_COLOR, + 'width': 2, + 'dash': POSTERIOR_POINT_ESTIMATE_LINE_DASH, + }, + name=POSTERIOR_POINT_ESTIMATE_TRACE_NAME, + legendrank=20, + ) + ) + for start, end in excluded_ranges: + fig.add_vrect( + x0=start, + x1=end, + fillcolor='rgba(120, 120, 120, 0.16)', + opacity=1.0, + line_width=0, + layer='below', + ) + fig.update_layout( + title={ + 'text': f"Posterior predictive for experiment 🔬 '{expt_name}'", + 'font': {'size': POSTERIOR_PAIR_TITLE_FONT_SIZE}, + }, + margin={ + 'autoexpand': True, + 'r': 30, + 't': 40, + 'b': 45, + }, + legend={ + 'bgcolor': self._plot_legend_background_color(), + 'xanchor': 'right', + 'x': 1.0, + 'yanchor': 'top', + 'y': 1.0, + }, + xaxis_title=axes_labels[0], + yaxis_title=axes_labels[1], + ) + fig.update_xaxes( + title_font={'size': POSTERIOR_PAIR_AXIS_TITLE_FONT_SIZE}, + showline=True, + linecolor=axis_frame_color, + mirror=True, + zeroline=False, + ) + fig.update_yaxes( + title_font={'size': POSTERIOR_PAIR_AXIS_TITLE_FONT_SIZE}, + showline=True, + linecolor=axis_frame_color, + mirror=True, + zeroline=False, + ) + self._show_plot_figure(fig) + + def _plot_ascii_posterior_predictive_lines( + self, + *, + expt_name: str, + x: np.ndarray, + y_meas: np.ndarray, + y_calc: np.ndarray, + axes_labels: list[str], + excluded_ranges: tuple[tuple[float, float], ...] = (), + ) -> None: + """Render posterior predictive lines on the ASCII backend.""" + self._backend.plot_powder( + x=x, + y_series=[y_meas, y_calc], + labels=['meas', 'posterior'], + axes_labels=axes_labels, + title=f"Posterior predictive for experiment 🔬 '{expt_name}'", + height=self.height, + excluded_ranges=excluded_ranges, + ) + + def _plot_single_crystal_posterior_predictive_summary( + self, + *, + expt_name: str, + summary: PosteriorPredictiveSummary, + y_meas: np.ndarray, + y_meas_su: np.ndarray, + axes_labels: list[str], + ) -> None: + """Render single-crystal posterior predictive checks.""" + if summary.lower_95 is None or summary.upper_95 is None: + log.warning( + 'Single-crystal posterior predictive plots require 95% predictive intervals.' + ) + return + + best_sample_prediction = np.asarray(summary.best_sample_prediction, dtype=float) + lower_95 = np.asarray(summary.lower_95, dtype=float) + upper_95 = np.asarray(summary.upper_95, dtype=float) + if ( + lower_95.shape != best_sample_prediction.shape + or upper_95.shape != best_sample_prediction.shape + ): + log.warning('Single-crystal posterior predictive interval arrays have invalid shapes.') + return + + go = __import__('plotly.graph_objects', fromlist=['Figure']) + trace = PlotlyPlotter._get_single_crystal_trace( + x_calc=best_sample_prediction, + y_meas=y_meas, + y_meas_su=y_meas_su, + ) + trace.error_x = { + 'type': 'data', + 'array': np.maximum(0.0, upper_95 - best_sample_prediction), + 'arrayminus': np.maximum(0.0, best_sample_prediction - lower_95), + 'visible': True, + } + trace.customdata = np.column_stack((lower_95, upper_95, y_meas_su)) + trace.hovertemplate = ( + 'Predicted I²: %{x:,.2f}
' + '95% credible interval: [%{customdata[0]:,.2f}, %{customdata[1]:,.2f}]
' + 'Measured I²: %{y:,.2f}
' + 'su(I²meas): %{customdata[2]:,.2f}' + ) + + fig = go.Figure( + data=[trace], + layout=PlotlyPlotter._get_layout( + f"Posterior predictive reflection check for experiment 🔬 '{expt_name}'", + axes_labels, + shapes=[PlotlyPlotter._get_diagonal_shape()], + ), + ) + self._show_plot_figure(fig) + + def _filtered_posterior_predictive_summary( + self, + *, + summary: PosteriorPredictiveSummary, + x_min: float, + x_max: float, + include_draws: bool, + ) -> PosteriorPredictiveSummary | None: + """Return a predictive summary filtered to an x-range.""" + x_filtered = self._filtered_y_array(summary.x, summary.x, x_min, x_max) + if np.asarray(x_filtered).size == 0: + return None + + draws = None + if include_draws and summary.draws is not None: + draws = np.asarray( + [self._filtered_y_array(draw, summary.x, x_min, x_max) for draw in summary.draws], + dtype=float, + ) + + return PosteriorPredictiveSummary( + experiment_name=summary.experiment_name, + x_axis_name=summary.x_axis_name, + x=x_filtered, + best_sample_prediction=self._filtered_y_array( + summary.best_sample_prediction, + summary.x, + x_min, + x_max, + ), + lower_95=( + None + if summary.lower_95 is None + else self._filtered_y_array(summary.lower_95, summary.x, x_min, x_max) + ), + upper_95=( + None + if summary.upper_95 is None + else self._filtered_y_array(summary.upper_95, summary.x, x_min, x_max) + ), + lower_68=( + None + if summary.lower_68 is None + else self._filtered_y_array(summary.lower_68, summary.x, x_min, x_max) + ), + upper_68=( + None + if summary.upper_68 is None + else self._filtered_y_array(summary.upper_68, summary.x, x_min, x_max) + ), + draws=draws, + ) + + def _plot_posterior_predictive_data( + self, + *, + experiment: object, + expt_name: str, + plot_options: _MeasVsCalcPlotOptions, + x_axis: object, + style: str, + ) -> None: + """Render posterior predictive curves on the powder layout.""" + show_draws = self.engine == PlotterEngineEnum.PLOTLY.value and style in { + 'draws', + 'band+draws', + } + pattern = intensity_category_for(experiment) + ctx = self._prepare_powder_context( + pattern, + expt_name, + experiment.type, + plot_options.x_min, + plot_options.x_max, + plot_options.x, + ) + if ctx is None: + return + + summary = self._get_or_build_posterior_predictive_summary( + experiment=experiment, + expt_name=expt_name, + x_axis=x_axis, + include_draws=show_draws, + ) + if summary is None: + return + + y_meas = self._filtered_y_array( + pattern.intensity_meas, + ctx['x_array'], + ctx['x_min'], + ctx['x_max'], + ) + y_bkg = self._filtered_optional_y_array( + getattr(pattern, 'intensity_bkg', None), + ctx['x_array'], + ctx['x_min'], + ctx['x_max'], + ) + if not self._show_background_enabled(plot_options, background_available=y_bkg is not None): + y_bkg = None + y_calc = self._filtered_y_array( + summary.best_sample_prediction, + summary.x, + ctx['x_min'], + ctx['x_max'], + ) + excluded_ranges = ( + self._excluded_ranges( + experiment=experiment, + x_min=ctx['x_min'], + x_max=ctx['x_max'], + ) + if plot_options.show_excluded + else () + ) + if self.engine == PlotterEngineEnum.ASCII.value: + if plot_options.show_residual: + log.warning( + 'Posterior predictive residuals are unavailable for ' + 'ASCII summary plots; ignoring show_residual=True.' + ) + self._plot_ascii_posterior_predictive_lines( + expt_name=expt_name, + x=np.asarray(ctx['x_filtered'], dtype=float), + y_meas=np.asarray(y_meas, dtype=float), + y_calc=np.asarray(y_calc, dtype=float), + axes_labels=list(ctx['axes_labels']), + excluded_ranges=excluded_ranges, + ) + return + + y_resid = y_meas - y_calc if plot_options.show_residual is not False else None + + predictive_lower_95 = None + predictive_upper_95 = None + if style in {'band', 'band+draws'}: + predictive_lower_95 = self._filtered_y_array( + summary.lower_95, + summary.x, + ctx['x_min'], + ctx['x_max'], + ) + predictive_upper_95 = self._filtered_y_array( + summary.upper_95, + summary.x, + ctx['x_min'], + ctx['x_max'], + ) + + predictive_draws = None + if show_draws: + if summary.draws is None: + log.warning('Posterior predictive draws are unavailable for plotting.') + return + predictive_draws = np.asarray( + [ + self._filtered_y_array(draw, summary.x, ctx['x_min'], ctx['x_max']) + for draw in summary.draws + ], + dtype=float, + ) + + if np.asarray(ctx['x_filtered']).size == 0 or not self._show_bragg_enabled(plot_options): + bragg_tick_sets = () + else: + bragg_tick_sets = self._extract_bragg_tick_sets( + experiment=experiment, + expt_name=expt_name, + x_axis=ctx['x_axis'], + x_min=ctx['x_min'], + x_max=ctx['x_max'], + ) + self._backend.plot_powder_meas_vs_calc( + plot_spec=PowderMeasVsCalcSpec( + x=ctx['x_filtered'], + y_meas=y_meas, + y_calc=y_calc, + y_resid=y_resid, + bragg_tick_sets=bragg_tick_sets, + axes_labels=ctx['axes_labels'], + title=f"Posterior predictive for experiment 🔬 '{expt_name}'", + residual_height_fraction=DEFAULT_RESID_HEIGHT, + bragg_peaks_height_fraction=DEFAULT_BRAGG_ROW, + height=self._composite_plot_height(), + y_bkg=y_bkg, + predictive_lower_95=predictive_lower_95, + predictive_upper_95=predictive_upper_95, + predictive_draws=predictive_draws, + y_calc_name=POSTERIOR_POINT_ESTIMATE_TRACE_NAME, + y_calc_line_dash=POSTERIOR_POINT_ESTIMATE_LINE_DASH, + excluded_ranges=excluded_ranges, + ) + ) @staticmethod - def _trim_correlation_display_dataframe( - corr_df: pd.DataFrame, + def _resolve_posterior_parameter_names( *, - preserve_all_rows: bool, - ) -> tuple[pd.DataFrame, list[int], list[int]]: + fit_results: object, + parameters: list[object] | None, + ) -> list[str] | None: """ - Trim empty outer rows/columns from the lower-triangle view. - - For the lower triangle without diagonal, the last column and - first row are always empty and can be trimmed. + Resolve posterior parameter names from descriptors. Parameters ---------- - corr_df : pd.DataFrame - Masked correlation matrix. - preserve_all_rows : bool - Whether to keep the full row list so row labels continue to - identify all numeric column headers in tabular output. + fit_results : object + Bayesian fit result exposing posterior samples. + parameters : list[object] | None + Optional parameter subset. Returns ------- - tuple[pd.DataFrame, list[int], list[int]] - Display matrix plus 1-based parameter numbers for the kept - rows and columns. + list[str] | None + Posterior parameter names in plotting order, or ``None`` if + the selection cannot be resolved. """ - num_rows, num_cols = corr_df.shape - row_numbers = list(range(1, num_rows + 1)) - col_numbers = list(range(1, num_cols + 1)) + posterior_samples = getattr(fit_results, 'posterior_samples', None) + available_names = getattr(posterior_samples, 'parameter_names', None) + if not available_names: + log.warning('Posterior samples do not expose parameter names.') + return None - if min(num_rows, num_cols) <= 1: - return corr_df, row_numbers, col_numbers + if parameters is None: + return list(available_names) - if preserve_all_rows: - return corr_df.iloc[:, :-1], row_numbers, col_numbers[:-1] - return corr_df.iloc[1:, :-1], row_numbers[1:], col_numbers[:-1] + resolved_names: list[str] = [] + for parameter in parameters: + resolved_name = Plotter._resolve_posterior_parameter_name( + fit_results=fit_results, + available_names=list(available_names), + parameter=parameter, + ) + if resolved_name is None: + return None + resolved_names.append(resolved_name) + return resolved_names - def _get_param_correlation_dataframe(self) -> pd.DataFrame | None: + @staticmethod + def _resolve_posterior_parameter_name( + *, + fit_results: object, + available_names: list[str], + parameter: object, + ) -> str | None: """ - Return the correlation matrix for the latest fit. - - Returns - ------- - pd.DataFrame | None - Square correlation matrix labeled by parameter unique names, - or ``None`` if unavailable. + Resolve one posterior parameter selection into a unique name. """ - result = self._get_fit_result_for_correlation() - if result is None: + if isinstance(parameter, str): + return Plotter._resolve_posterior_parameter_name_from_string( + fit_results=fit_results, + available_names=available_names, + selection=parameter, + ) + + unique_name = getattr(parameter, 'unique_name', None) + if unique_name is None: + log.warning( + 'Posterior parameter selection expects parameter objects ' + 'or strings matching a unique name or label.' + ) return None - raw_result, var_names, fit_results = result + if unique_name not in available_names: + log.warning(f'Posterior samples do not contain the selected parameter: {unique_name}') + return None + return str(unique_name) - covar = getattr(raw_result, 'covar', None) - if covar is not None: - return self._correlation_from_covariance(covar, var_names, fit_results.parameters) + @staticmethod + def _resolve_posterior_parameter_name_from_string( + *, + fit_results: object, + available_names: list[str], + selection: str, + ) -> str | None: + """Resolve a string posterior parameter selection.""" + stripped_selection = selection.strip() + if not stripped_selection: + log.warning('Posterior parameter selection cannot use an empty string.') + return None + if stripped_selection in available_names: + return stripped_selection - corr_df = self._get_param_correlation_dataframe_from_engine_params( - raw_result=raw_result, - parameters=fit_results.parameters, + selection_candidates = Plotter._posterior_parameter_selection_candidates( + fit_results=fit_results, + available_names=available_names, ) - if corr_df is not None: - return corr_df + matching_names = [ + unique_name + for unique_name, candidates in selection_candidates.items() + if stripped_selection in candidates + ] + if len(matching_names) == 1: + return matching_names[0] + if len(matching_names) > 1: + matches = ', '.join(matching_names) + log.warning( + f"Posterior parameter selection '{stripped_selection}' is ambiguous. " + f'Matches: {matches}' + ) + return None log.warning( - 'Correlation matrix is unavailable for this fit. ' - 'Use the lmfit minimizer and ensure covariance estimation succeeds.' + f'Posterior samples do not contain the selected parameter or label: ' + f'{stripped_selection}' ) return None + @staticmethod + def _posterior_parameter_selection_candidates( + *, + fit_results: object, + available_names: list[str], + ) -> dict[str, set[str]]: + """ + Return string identifiers accepted for posterior selection. + """ + parameters_by_name = { + getattr(parameter, 'unique_name', ''): parameter + for parameter in fit_results.parameters + } + summaries_by_name = Plotter._posterior_summary_by_name(fit_results) + plot_labels = dict( + zip( + available_names, + Plotter._posterior_plot_labels(fit_results, available_names), + strict=True, + ) + ) + candidates_by_name: dict[str, set[str]] = {} + for unique_name in available_names: + candidates = {unique_name} + parameter = parameters_by_name.get(unique_name) + if parameter is not None: + parameter_name = getattr(parameter, 'name', None) + if isinstance(parameter_name, str) and parameter_name.strip(): + candidates.add(parameter_name.strip()) + + summary = summaries_by_name.get(unique_name) + summary_label = getattr(summary, 'display_name', None) + if isinstance(summary_label, str) and summary_label.strip(): + candidates.add(summary_label.strip()) + + plot_label = plot_labels.get(unique_name) + if isinstance(plot_label, str) and plot_label.strip(): + candidates.add(plot_label.strip()) + + candidates_by_name[unique_name] = candidates + return candidates_by_name + + @staticmethod + def _selected_posterior_samples( + posterior_samples: object, + parameter_names: list[str], + ) -> np.ndarray | None: + """Return flattened posterior samples in the selected order.""" + available_names = getattr(posterior_samples, 'parameter_names', None) + if not available_names: + return None + + name_to_index = {name: index for index, name in enumerate(available_names)} + try: + indices = [name_to_index[name] for name in parameter_names] + except KeyError: + return None + + flattened = np.asarray(posterior_samples.flattened(), dtype=float) + if flattened.ndim != POSTERIOR_FLATTENED_SAMPLE_NDIM: + return None + return flattened[:, indices] + + @staticmethod + def _thin_posterior_samples( + samples: np.ndarray, + *, + max_points: int = 4000, + ) -> np.ndarray: + """Downsample posterior samples for interactive plotting.""" + if samples.shape[0] <= max_points: + return samples + + indices = np.linspace(0, samples.shape[0] - 1, num=max_points, dtype=int) + return samples[indices] + + @staticmethod + def _posterior_plot_labels( + fit_results: object, + parameter_names: list[str], + ) -> list[str]: + """ + Return readable posterior plot labels for selected parameters. + """ + parameters_by_name = { + getattr(parameter, 'unique_name', ''): parameter + for parameter in fit_results.parameters + } + labels: list[str] = [] + for parameter_name in parameter_names: + parameter = parameters_by_name.get(parameter_name) + if parameter is None: + labels.append(parameter_name) + continue + + entry_name = getattr(getattr(parameter, '_identity', None), 'category_entry_name', '') + short_name = getattr(parameter, 'name', parameter_name) + if entry_name: + labels.append(f'{entry_name} {short_name}') + else: + labels.append(short_name) + return labels + + @staticmethod + def _square_matrix_axis_title_labels( + parameter_names: list[str], + ) -> list[str]: + """Return compact multiline labels for square-matrix axes.""" + return [Plotter._square_matrix_axis_title_label(name) for name in parameter_names] + + @staticmethod + def _square_matrix_axis_title_label(unique_name: str) -> str: + """Return one compact multiline axis title.""" + normalized_name = unique_name.strip() + if not normalized_name or '.' not in normalized_name: + return normalized_name + + name_parts = [part.strip() for part in normalized_name.split('.') if part.strip()] + if not name_parts: + return normalized_name + + return '
'.join([*(f'{part}.' for part in name_parts[:-1]), name_parts[-1]]) + + @staticmethod + def _posterior_summary_by_name( + fit_results: object, + ) -> dict[str, object]: + """Return posterior summaries keyed by unique parameter name.""" + summaries = getattr(fit_results, 'posterior_parameter_summaries', []) + return {summary.unique_name: summary for summary in summaries} + def _get_fit_result_for_correlation( self, - ) -> tuple[object, list[str], object] | None: + ) -> object | None: """ - Validate and return the raw fit result for correlation. + Validate and return the fit result for correlation. Returns ------- - tuple[object, list[str], object] | None - A tuple of ``(raw_result, var_names, fit_results)`` when all - required data is present, or ``None`` otherwise. + object | None + Fit result object when available, or ``None`` otherwise. """ if self._project is None: log.warning('Plotter is not attached to a project.') @@ -781,18 +4532,46 @@ def _get_fit_result_for_correlation( if fit_results is None: log.warning('No fit results available. Run fit() first.') return None + return fit_results - raw_result = getattr(fit_results, 'engine_result', None) - if raw_result is None: - log.warning('No raw fit result available. Correlation matrix cannot be plotted.') + @staticmethod + def _correlation_from_posterior_samples( + posterior_samples: object, + ) -> pd.DataFrame | None: + """ + Convert posterior samples into a correlation DataFrame. + + Parameters + ---------- + posterior_samples : object + Posterior sample container exposing ``flattened()`` and + ``parameter_names``. + + Returns + ------- + pd.DataFrame | None + Correlation matrix labeled by posterior parameter names, or + ``None`` if the sample array is invalid. + """ + parameter_names = getattr(posterior_samples, 'parameter_names', None) + if not parameter_names: + log.warning('Posterior samples do not expose parameter names.') return None - var_names = getattr(raw_result, 'var_names', None) - if not var_names: - log.warning('Fit result does not expose variable names for a correlation matrix.') + flattened = np.asarray(posterior_samples.flattened(), dtype=float) + if flattened.ndim != POSTERIOR_FLATTENED_SAMPLE_NDIM or flattened.shape[1] != len( + parameter_names + ): + log.warning('Posterior sample array has an invalid shape for correlations.') + return None + if flattened.shape[0] < MIN_POSTERIOR_SAMPLE_COUNT: + log.warning('At least two posterior draws are required for correlations.') return None - return raw_result, var_names, fit_results + corr = np.corrcoef(flattened, rowvar=False) + corr = np.nan_to_num(corr, nan=0.0, posinf=0.0, neginf=0.0) + np.fill_diagonal(corr, 1.0) + return pd.DataFrame(corr, index=parameter_names, columns=parameter_names) @staticmethod def _correlation_from_covariance( @@ -938,12 +4717,437 @@ def _plot_correlation_heatmap( precision : int Number of decimals to show in plot labels and hover text. """ - self._backend.plot_correlation_heatmap( + figure = self._build_correlation_heatmap_plot( corr_df, title, threshold=threshold, precision=precision, ) + self._show_plot_figure(figure) + + def _build_correlation_heatmap_plot( + self, + corr_df: pd.DataFrame, + title: str, + *, + threshold: float | None, + precision: int, + ) -> object: + """Build a compact pair-plot-style correlation heatmap.""" + go = __import__('plotly.graph_objects', fromlist=['Figure', 'Heatmap']) + context = _CorrelationHeatmapContext( + corr_df=corr_df, + row_labels=self._square_matrix_axis_title_labels(corr_df.index.tolist()), + col_labels=self._square_matrix_axis_title_labels(corr_df.columns.tolist()), + threshold=threshold, + precision=precision, + ) + plot_extent = self._square_matrix_plot_extent(context.n_cols) + x_edges = self._correlation_heatmap_edges(context.n_cols) + y_edges = self._correlation_heatmap_edges(context.n_rows) + x_centers = self._correlation_heatmap_centers(context.n_cols) + y_centers = self._correlation_heatmap_centers(context.n_rows) + + heatmap = go.Heatmap( + z=self._correlation_heatmap_values(context.corr_df), + x=x_edges, + y=y_edges, + customdata=self._correlation_heatmap_customdata(context.corr_df), + zmin=-1.0, + zmax=1.0, + zmid=0.0, + colorscale=self._plot_correlation_colorscale(), + showscale=False, + hoverongaps=False, + hovertemplate=( + '%{customdata[0]}
' + '%{customdata[1]}
' + f'correlation: %{{z:.{context.precision}f}}' + ), + ) + label_trace = PlotlyPlotter._get_correlation_label_trace( + context.corr_df, + x_centers=x_centers, + y_centers=y_centers, + threshold=context.threshold, + precision=context.precision, + ) + traces = [heatmap] + if label_trace is not None: + traces.append(label_trace) + fig = go.Figure(data=traces) + + fig.update_layout( + autosize=True, + margin=self._square_matrix_layout_margin([ + *context.row_labels, + *context.col_labels, + ]), + annotations=self._correlation_heatmap_annotations( + title=title, + context=context, + plot_extent=plot_extent, + x_centers=x_centers, + y_centers=y_centers, + ), + shapes=self._correlation_heatmap_grid_shapes(context), + meta=self._square_matrix_layout_meta( + n_parameters=context.n_cols, + annotation_labels=[*context.row_labels, *context.col_labels], + ), + showlegend=False, + ) + fig.update_xaxes( + range=[0.0, plot_extent], + showline=False, + mirror=False, + zeroline=False, + showgrid=False, + ticks='', + ticklen=0, + tickwidth=0, + showticklabels=False, + title_text=None, + layer='above traces', + constrain='domain', + ) + fig.update_yaxes( + range=[plot_extent, 0.0], + showline=False, + mirror=False, + zeroline=False, + showgrid=False, + ticks='', + ticklen=0, + tickwidth=0, + showticklabels=False, + title_text=None, + layer='above traces', + constrain='domain', + scaleanchor='x', + scaleratio=1, + ) + return fig + + @classmethod + def _correlation_heatmap_edges(cls, n_parameters: int) -> np.ndarray: + """Return expanded heatmap edges with pair-plot-like gaps.""" + if n_parameters <= 0: + return np.asarray([0.0], dtype=float) + + gap_width = cls._square_matrix_gap_data_width(n_parameters) + if np.isclose(gap_width, 0.0): + return np.arange(n_parameters + 1, dtype=float) + + widths = [1.0 if index % 2 == 0 else gap_width for index in range(2 * n_parameters - 1)] + return np.concatenate(([0.0], np.cumsum(np.asarray(widths, dtype=float)))) + + @classmethod + def _correlation_heatmap_centers(cls, n_parameters: int) -> np.ndarray: + """Return visible-cell centers for a gapped heatmap.""" + gap_width = cls._square_matrix_gap_data_width(n_parameters) + return np.arange(n_parameters, dtype=float) * (1.0 + gap_width) + 0.5 + + @staticmethod + def _correlation_heatmap_values(corr_df: pd.DataFrame) -> np.ndarray: + """Return a gapped heatmap array for correlation cells.""" + n_rows, n_cols = corr_df.shape + expanded = np.full((2 * n_rows - 1, 2 * n_cols - 1), np.nan, dtype=float) + expanded[0::2, 0::2] = corr_df.to_numpy(dtype=float) + return expanded + + @staticmethod + def _correlation_heatmap_customdata( + corr_df: pd.DataFrame, + ) -> np.ndarray: + """Return hover labels for a correlation heatmap.""" + n_rows, n_cols = corr_df.shape + expanded = np.empty((2 * n_rows - 1, 2 * n_cols - 1, 2), dtype=object) + expanded[..., 0] = '' + expanded[..., 1] = '' + for row_index, row_label in enumerate(corr_df.index): + for col_index, col_label in enumerate(corr_df.columns): + expanded[2 * row_index, 2 * col_index, 0] = str(col_label) + expanded[2 * row_index, 2 * col_index, 1] = str(row_label) + return expanded + + def _correlation_heatmap_annotations( + self, + *, + title: str, + context: _CorrelationHeatmapContext, + plot_extent: float, + x_centers: np.ndarray, + y_centers: np.ndarray, + ) -> list[dict[str, object]]: + """Return pair-plot-like title and axis annotations.""" + annotations = [ + self._square_matrix_title_annotation( + title, + [*context.row_labels, *context.col_labels], + ) + ] + for row_index, row_label in enumerate(context.row_labels): + annotations.append({ + 'x': 0.0, + 'xref': 'paper', + 'xanchor': 'right', + 'xshift': -POSTERIOR_PAIR_Y_TITLE_XSHIFT_PIXELS, + 'y': 1.0 - (float(y_centers[row_index]) / plot_extent), + 'yref': 'paper', + 'yanchor': 'middle', + 'text': row_label, + 'align': 'center', + 'font': {'size': POSTERIOR_PAIR_AXIS_TITLE_FONT_SIZE}, + 'textangle': -90, + 'showarrow': False, + }) + for col_index, col_label in enumerate(context.col_labels): + annotations.append({ + 'x': float(x_centers[col_index]) / plot_extent, + 'xref': 'paper', + 'xanchor': 'center', + 'y': 0.0, + 'yref': 'paper', + 'yanchor': 'top', + 'yshift': -POSTERIOR_PAIR_X_TITLE_YSHIFT_PIXELS, + 'text': col_label, + 'align': 'center', + 'font': {'size': POSTERIOR_PAIR_AXIS_TITLE_FONT_SIZE}, + 'showarrow': False, + }) + return annotations + + def _correlation_heatmap_grid_shapes( + self, + context: _CorrelationHeatmapContext, + ) -> list[dict[str, object]]: + """Return per-cell borders for the visible lower triangle.""" + axis_frame_color = self._plot_axis_frame_color() + gap_width = self._square_matrix_gap_data_width(context.n_cols) + return [ + { + 'type': 'rect', + 'xref': 'x', + 'yref': 'y', + 'x0': float(col_index * (1.0 + gap_width)), + 'x1': float(col_index * (1.0 + gap_width) + 1.0), + 'y0': float(row_index * (1.0 + gap_width)), + 'y1': float(row_index * (1.0 + gap_width) + 1.0), + 'layer': 'above', + 'line': { + 'color': axis_frame_color, + 'width': POSTERIOR_PAIR_AXIS_LINE_WIDTH, + }, + 'fillcolor': 'rgba(0, 0, 0, 0)', + } + for row_index in range(context.n_rows) + for col_index in range(context.n_cols) + if col_index <= row_index + ] + + def _populate_correlation_heatmap_panel( + self, + *, + fig: object, + context: _CorrelationHeatmapContext, + row_index: int, + col_index: int, + subplot_title_annotations: list[dict[str, object]], + subplot_border_shapes: list[dict[str, object]], + ) -> None: + """Populate one panel in the correlation-matrix grid.""" + row = row_index + 1 + col = col_index + 1 + if col_index > row_index: + self._hide_posterior_pair_panel(fig=fig, row=row, col=col) + return + + value = context.corr_df.iat[row_index, col_index] + if not pd.isna(value): + self._add_correlation_heatmap_value_panel( + fig=fig, + context=context, + row_index=row_index, + col_index=col_index, + value=float(value), + ) + + self._configure_correlation_heatmap_panel_axes(fig=fig, row=row, col=col) + self._collect_correlation_heatmap_panel_decorations( + fig=fig, + context=context, + row_index=row_index, + col_index=col_index, + subplot_title_annotations=subplot_title_annotations, + subplot_border_shapes=subplot_border_shapes, + ) + + def _add_correlation_heatmap_value_panel( + self, + *, + fig: object, + context: _CorrelationHeatmapContext, + row_index: int, + col_index: int, + value: float, + ) -> None: + """Add one colored cell and optional text label.""" + go = __import__('plotly.graph_objects', fromlist=['Heatmap']) + row = row_index + 1 + col = col_index + 1 + hovertemplate = ( + f'{context.corr_df.columns[col_index]}
' + f'{context.corr_df.index[row_index]}
' + f'correlation: %{{z:.{context.precision}f}}' + ) + fig.add_trace( + go.Heatmap( + z=[[value]], + x=[0.0, 1.0], + y=[0.0, 1.0], + zmin=-1.0, + zmax=1.0, + zmid=0.0, + colorscale=self._plot_correlation_colorscale(), + showscale=False, + hoverongaps=False, + hovertemplate=hovertemplate, + ), + row=row, + col=col, + ) + + if ( + context.threshold is not None + and context.threshold > 0 + and abs(value) < context.threshold + ): + return + + fig.add_trace( + go.Scatter( + x=[0.5], + y=[0.5], + mode='text', + text=[f'{value:.{context.precision}f}'], + textposition='middle center', + textfont={'color': PlotlyPlotter._correlation_label_color()}, + hoverinfo='skip', + showlegend=False, + ), + row=row, + col=col, + ) + + @staticmethod + def _configure_correlation_heatmap_panel_axes( + *, + fig: object, + row: int, + col: int, + ) -> None: + """Hide ticks and titles for one correlation-matrix panel.""" + fig.update_xaxes( + range=[0.0, 1.0], + showline=False, + mirror=False, + zeroline=False, + showgrid=False, + ticks='', + ticklen=0, + tickwidth=0, + showticklabels=False, + title_text=None, + layer='above traces', + row=row, + col=col, + ) + fig.update_yaxes( + range=[1.0, 0.0], + showline=False, + mirror=False, + zeroline=False, + showgrid=False, + ticks='', + ticklen=0, + tickwidth=0, + showticklabels=False, + title_text=None, + layer='above traces', + row=row, + col=col, + ) + + def _collect_correlation_heatmap_panel_decorations( + self, + *, + fig: object, + context: _CorrelationHeatmapContext, + row_index: int, + col_index: int, + subplot_title_annotations: list[dict[str, object]], + subplot_border_shapes: list[dict[str, object]], + ) -> None: + """Collect labels and frames for one correlation cell.""" + row = row_index + 1 + col = col_index + 1 + subplot = fig.get_subplot(row, col) + x_mid = 0.5 * (subplot.xaxis.domain[0] + subplot.xaxis.domain[1]) + y_mid = 0.5 * (subplot.yaxis.domain[0] + subplot.yaxis.domain[1]) + + if col_index == 0: + subplot_title_annotations.append({ + 'x': subplot.xaxis.domain[0], + 'xref': 'paper', + 'xanchor': 'right', + 'xshift': -POSTERIOR_PAIR_Y_TITLE_XSHIFT_PIXELS, + 'y': y_mid, + 'yref': 'paper', + 'yanchor': 'middle', + 'text': context.row_labels[row_index], + 'align': 'center', + 'font': {'size': POSTERIOR_PAIR_AXIS_TITLE_FONT_SIZE}, + 'textangle': -90, + 'showarrow': False, + }) + if row_index == context.n_rows - 1: + subplot_title_annotations.append({ + 'x': x_mid, + 'xref': 'paper', + 'xanchor': 'center', + 'y': subplot.yaxis.domain[0], + 'yref': 'paper', + 'yanchor': 'top', + 'yshift': -POSTERIOR_PAIR_X_TITLE_YSHIFT_PIXELS, + 'text': context.col_labels[col_index], + 'align': 'center', + 'font': {'size': POSTERIOR_PAIR_AXIS_TITLE_FONT_SIZE}, + 'showarrow': False, + }) + + subplot_border_shapes.append({ + 'type': 'rect', + 'xref': 'paper', + 'yref': 'paper', + 'x0': subplot.xaxis.domain[0], + 'x1': subplot.xaxis.domain[1], + 'y0': subplot.yaxis.domain[0], + 'y1': subplot.yaxis.domain[1], + 'line': { + 'color': self._plot_axis_frame_color(), + 'width': POSTERIOR_PAIR_AXIS_LINE_WIDTH, + }, + 'fillcolor': 'rgba(0, 0, 0, 0)', + 'layer': 'above', + }) + + def _plot_correlation_colorscale(self) -> list[tuple[float, str]]: + """Return the active correlation colorscale.""" + correlation_colorscale = getattr(self._backend, '_correlation_colorscale', None) + if callable(correlation_colorscale): + return correlation_colorscale() + return PlotlyPlotter._correlation_colorscale() @staticmethod def _format_correlation_table_dataframe( @@ -1012,18 +5216,19 @@ def _format_correlation_table_dataframe( def _plot_meas_data( self, + experiment: object, pattern: object, expt_name: str, expt_type: object, - x_min: object = None, - x_max: object = None, - x: object = None, + plot_options: _MeasVsCalcPlotOptions, ) -> None: """ Plot measured pattern using the current engine. Parameters ---------- + experiment : object + Experiment object used for excluded-range extraction. pattern : object Object with x-axis arrays (``two_theta``, ``time_of_flight``, ``d_spacing``) and ``meas`` array. @@ -1031,20 +5236,16 @@ def _plot_meas_data( 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. If ``None``, auto-detected from beam mode. + plot_options : _MeasVsCalcPlotOptions + X-range, excluded-region, and x-axis selection options. """ ctx = self._prepare_powder_context( pattern, expt_name, expt_type, - x_min, - x_max, - x, + plot_options.x_min, + plot_options.x_max, + plot_options.x, ) if ctx is None: return @@ -1055,6 +5256,15 @@ def _plot_meas_data( y_meas = self._filtered_y_array( pattern.intensity_meas, ctx['x_array'], ctx['x_min'], ctx['x_max'] ) + excluded_ranges = ( + self._excluded_ranges( + experiment=experiment, + x_min=ctx['x_min'], + x_max=ctx['x_max'], + ) + if plot_options.show_excluded + else () + ) self._backend.plot_powder( x=ctx['x_filtered'], @@ -1063,22 +5273,24 @@ def _plot_meas_data( axes_labels=ctx['axes_labels'], title=f"Measured data for experiment 🔬 '{expt_name}'", height=self.height, + excluded_ranges=excluded_ranges, ) def _plot_calc_data( self, + experiment: object, pattern: object, expt_name: str, expt_type: object, - x_min: object = None, - x_max: object = None, - x: object = None, + plot_options: _MeasVsCalcPlotOptions, ) -> None: """ Plot calculated pattern using the current engine. Parameters ---------- + experiment : object + Experiment object used for excluded-range extraction. pattern : object Object with x-axis arrays (``two_theta``, ``time_of_flight``, ``d_spacing``) and ``calc`` array. @@ -1086,20 +5298,16 @@ def _plot_calc_data( 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. If ``None``, auto-detected from beam mode. + plot_options : _MeasVsCalcPlotOptions + X-range, excluded-region, and x-axis selection options. """ ctx = self._prepare_powder_context( pattern, expt_name, expt_type, - x_min, - x_max, - x, + plot_options.x_min, + plot_options.x_max, + plot_options.x, ) if ctx is None: return @@ -1110,6 +5318,15 @@ def _plot_calc_data( y_calc = self._filtered_y_array( pattern.intensity_calc, ctx['x_array'], ctx['x_min'], ctx['x_max'] ) + excluded_ranges = ( + self._excluded_ranges( + experiment=experiment, + x_min=ctx['x_min'], + x_max=ctx['x_max'], + ) + if plot_options.show_excluded + else () + ) self._backend.plot_powder( x=ctx['x_filtered'], @@ -1118,6 +5335,7 @@ def _plot_calc_data( axes_labels=ctx['axes_labels'], title=f"Calculated data for experiment 🔬 '{expt_name}'", height=self.height, + excluded_ranges=excluded_ranges, ) def _plot_meas_vs_calc_data( @@ -1203,12 +5421,23 @@ def _plot_meas_vs_calc_data( if y_bkg_raw is not None else None ) + if not self._show_background_enabled(plot_options, background_available=y_bkg is not None): + y_bkg = None powder_series = _PowderMeasVsCalcSeries( y_meas=y_meas, y_calc=y_calc, y_bkg=y_bkg, ) + excluded_ranges = ( + self._excluded_ranges( + experiment=experiment, + x_min=ctx['x_min'], + x_max=ctx['x_max'], + ) + if plot_options.show_excluded + else () + ) if sample_form == SampleFormEnum.POWDER and scattering_type == ScatteringTypeEnum.BRAGG: self._plot_powder_bragg_meas_vs_calc( @@ -1218,6 +5447,7 @@ def _plot_meas_vs_calc_data( series=powder_series, plot_options=plot_options, title=title, + excluded_ranges=excluded_ranges, ) return @@ -1229,6 +5459,7 @@ def _plot_meas_vs_calc_data( if plot_options.show_residual is None else plot_options.show_residual, title=title, + excluded_ranges=excluded_ranges, ) def _plot_single_crystal_meas_vs_calc( @@ -1267,13 +5498,14 @@ def _plot_powder_bragg_meas_vs_calc( series: _PowderMeasVsCalcSeries, plot_options: _MeasVsCalcPlotOptions, title: str, + excluded_ranges: tuple[tuple[float, float], ...], ) -> None: """ Render the composite powder Bragg measured-vs-calculated plot. """ show_residual = True if plot_options.show_residual is None else plot_options.show_residual y_resid = series.y_meas - series.y_calc if show_residual else None - if np.asarray(ctx['x_filtered']).size == 0: + if np.asarray(ctx['x_filtered']).size == 0 or not self._show_bragg_enabled(plot_options): bragg_tick_sets = () else: bragg_tick_sets = self._extract_bragg_tick_sets( @@ -1283,6 +5515,7 @@ def _plot_powder_bragg_meas_vs_calc( x_min=ctx['x_min'], x_max=ctx['x_max'], ) + plot_spec = PowderMeasVsCalcSpec( x=ctx['x_filtered'], y_meas=series.y_meas, @@ -1295,9 +5528,30 @@ def _plot_powder_bragg_meas_vs_calc( bragg_peaks_height_fraction=DEFAULT_BRAGG_ROW, height=self._composite_plot_height(), y_bkg=series.y_bkg, + excluded_ranges=excluded_ranges, ) self._backend.plot_powder_meas_vs_calc(plot_spec=plot_spec) + @staticmethod + def _show_background_enabled( + plot_options: object, + *, + background_available: bool, + ) -> bool: + """Return whether the background curve should be shown.""" + show_background = getattr(plot_options, 'show_background', None) + if show_background is None: + return background_available + return show_background and background_available + + @staticmethod + def _show_bragg_enabled(plot_options: object) -> bool: + """Return whether Bragg reflection rows should be shown.""" + show_bragg = getattr(plot_options, 'show_bragg', None) + if show_bragg is None: + return True + return show_bragg + def _plot_line_meas_vs_calc( self, ctx: dict[str, object], @@ -1306,6 +5560,7 @@ def _plot_line_meas_vs_calc( *, show_residual: bool, title: str, + excluded_ranges: tuple[tuple[float, float], ...] = (), ) -> None: """ Render the non-composite line version of measured-vs-calculated. @@ -1323,8 +5578,36 @@ def _plot_line_meas_vs_calc( axes_labels=ctx['axes_labels'], title=title, height=self.height, + excluded_ranges=excluded_ranges, ) + @staticmethod + def _excluded_ranges( + *, + experiment: object, + x_min: float | None, + x_max: float | None, + ) -> tuple[tuple[float, float], ...]: + """Return excluded x-ranges clipped to the current view.""" + excluded_regions = getattr(experiment, 'excluded_regions', None) + if excluded_regions is None: + return () + + clipped_ranges: list[tuple[float, float]] = [] + lower_bound = -np.inf if x_min is None else float(x_min) + upper_bound = np.inf if x_max is None else float(x_max) + + for region in excluded_regions: + start = float(region.start.value) + end = float(region.end.value) + clipped_start = max(start, lower_bound) + clipped_end = min(end, upper_bound) + if clipped_start > clipped_end: + continue + clipped_ranges.append((clipped_start, clipped_end)) + + return tuple(clipped_ranges) + @staticmethod def _extract_bragg_tick_sets( experiment: object, @@ -1492,57 +5775,62 @@ def _bragg_tick_d_spacing( def _plot_param_series_from_csv( self, csv_path: str, - unique_name: str, + column_names: str | list[str], param_descriptor: object, - versus_descriptor: object | None = None, + versus_path: str | None = None, ) -> None: """ Plot a parameter's value across sequential fit results. Reads data from the CSV file at *csv_path*. The y-axis values - come from the column named *unique_name*, uncertainties from - ``{unique_name}.uncertainty``. When *versus_descriptor* is - provided, the x-axis uses the corresponding ``diffrn.{name}`` - column; otherwise the row index is used. + come from the first matching column named in *column_names*, + with uncertainties from ``{column_name}.uncertainty``. When + *versus_path* is provided, the x-axis uses the corresponding + ``diffrn.*`` CSV column; otherwise the row index is used. - Axis labels are derived from the live descriptor objects - (*param_descriptor* and *versus_descriptor*), which carry - ``.description`` and ``.units`` attributes. + Axis labels use the live parameter descriptor and, when + available, a template diffrn descriptor resolved from + *versus_path*. Parameters ---------- csv_path : str Path to the ``results.csv`` file. - unique_name : str - Unique name of the parameter to plot (CSV column key). + column_names : str | list[str] + Candidate CSV column keys to plot. param_descriptor : object The live parameter descriptor (for axis label / units). - versus_descriptor : object | None, default=None - A diffrn descriptor whose ``.name`` maps to a - ``diffrn.{name}`` CSV column. ``None`` → use row index. + versus_path : str | None, default=None + Persisted diffrn path whose matching CSV column provides the + x-axis values. ``None`` uses row index. """ df = pd.read_csv(csv_path) - if unique_name not in df.columns: + column_candidates = [column_names] if isinstance(column_names, str) else column_names + + column_name = next((name for name in column_candidates if name in df.columns), None) + if column_name is None: log.warning( - f"Parameter '{unique_name}' not found in CSV columns. " + f"Parameter '{column_candidates[0]}' not found in CSV columns. " f'Available: {list(df.columns)}' ) return - y = df[unique_name].astype(float).tolist() - uncert_col = f'{unique_name}.uncertainty' - sy = df[uncert_col].astype(float).tolist() if uncert_col in df.columns else [0.0] * len(y) + y = self._numeric_series_values(df[column_name]) + uncert_col = f'{column_name}.uncertainty' + sy = ( + self._numeric_series_values(df[uncert_col]) + if uncert_col in df.columns + else [0.0] * len(y) + ) # X-axis: diffrn column or row index - versus_name = versus_descriptor.name if versus_descriptor is not None else None - diffrn_col = f'diffrn.{versus_name}' if versus_name else None + diffrn_col = versus_path + versus_descriptor = self._resolve_versus_descriptor_from_path(versus_path) if diffrn_col and diffrn_col in df.columns: x = pd.to_numeric(df[diffrn_col], errors='coerce').tolist() - x_label = getattr(versus_descriptor, 'description', None) or versus_name - if hasattr(versus_descriptor, 'units') and versus_descriptor.units: - x_label = f'{x_label} ({versus_descriptor.units})' + x_label = self._versus_axis_label(versus_path, versus_descriptor) else: x = list(range(1, len(y) + 1)) x_label = 'Experiment No.' @@ -1551,7 +5839,7 @@ def _plot_param_series_from_csv( param_units = getattr(param_descriptor, 'units', '') y_label = f'Parameter value ({param_units})' if param_units else 'Parameter value' - title = f"Parameter '{unique_name}' across fit results" + title = f"Parameter '{column_name}' across fit results" self._backend.plot_scatter( x=x, @@ -1565,7 +5853,7 @@ def _plot_param_series_from_csv( def plot_param_series_from_snapshots( self, unique_name: str, - versus_name: str | None, + versus_path: str | None, experiments: object, parameter_snapshots: dict[str, dict[str, dict]], ) -> None: @@ -1580,8 +5868,8 @@ def plot_param_series_from_snapshots( ---------- unique_name : str Unique name of the parameter to plot. - versus_name : str | None - Name of the diffrn descriptor for the x-axis. + versus_path : str | None + Persisted diffrn path for the x-axis. experiments : object Experiments collection for accessing diffrn conditions. parameter_snapshots : dict[str, dict[str, dict]] @@ -1597,7 +5885,10 @@ def plot_param_series_from_snapshots( experiment = experiments[expt_name] diffrn = experiment.diffrn - x_axis_param = self._resolve_diffrn_descriptor(diffrn, versus_name) + x_axis_param = self._resolve_diffrn_descriptor( + diffrn, + self._versus_field_name(versus_path), + ) if x_axis_param is not None and x_axis_param.value is not None: value = x_axis_param.value @@ -1611,7 +5902,7 @@ def plot_param_series_from_snapshots( if x_axis_param is not None: axes_labels = [ - x_axis_param.description or x_axis_param.name, + self._versus_axis_label(versus_path, x_axis_param), f'Parameter value ({param_data["units"]})', ] else: diff --git a/src/easydiffraction/display/progress.py b/src/easydiffraction/display/progress.py new file mode 100644 index 000000000..6123e626e --- /dev/null +++ b/src/easydiffraction/display/progress.py @@ -0,0 +1,491 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Environment-aware activity indicator for long-running tasks.""" + +from __future__ import annotations + +import html +from contextlib import AbstractContextManager +from contextlib import suppress +from time import monotonic +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from types import TracebackType + +try: + from IPython.display import HTML + from IPython.display import DisplayHandle +except ImportError: # pragma: no cover - optional dependency + HTML = None + DisplayHandle = None + +from rich.console import Group +from rich.live import Live +from rich.protocol import is_renderable +from rich.text import Text + +from easydiffraction.utils.enums import VerbosityEnum +from easydiffraction.utils.environment import in_jupyter +from easydiffraction.utils.logging import ConsoleManager + +ACTIVITY_LABEL_BURN_IN = 'Burn-in...' +ACTIVITY_LABEL_FITTING = 'Fitting...' +ACTIVITY_LABEL_POST_PROCESSING = 'Post-processing...' +ACTIVITY_LABEL_PRE_PROCESSING = 'Pre-processing...' +ACTIVITY_LABEL_PROCESSING = 'Processing...' +ACTIVITY_LABEL_SAMPLING = 'Sampling...' +ACTIVITY_ACCENT_COLOR = '#d97706' +ACTIVITY_TERMINAL_STYLE = ACTIVITY_ACCENT_COLOR +ACTIVITY_TERMINAL_FALLBACK_STYLE = 'bold yellow' + +SPINNER_FRAMES: tuple[str, ...] = ( + '⠋', + '⠙', + '⠹', + '⠸', + '⠼', + '⠴', + '⠦', + '⠧', + '⠇', + '⠏', +) +_SPINNER_FRAME_SECONDS = 0.1 +_JUPYTER_SPINNER_SECONDS = 1.0 + + +def resolve_activity_terminal_style(console: object | None = None) -> str: + """ + Return a terminal-safe activity indicator style. + + Parameters + ---------- + console : object | None, default=None + Console-like object whose ``color_system`` determines whether + the accent color can be rendered directly. + + Returns + ------- + str + The preferred terminal style for the current console. + """ + color_system = getattr(console, 'color_system', None) + if color_system in {'standard', 'windows'}: + return ACTIVITY_TERMINAL_FALLBACK_STYLE + return ACTIVITY_TERMINAL_STYLE + + +class _TerminalLiveHandle: + """ + Adapter exposing update()/close() for terminal live updates. + + Wraps a ``rich.live.Live`` instance so callers can treat terminal + and notebook handles through a single update-oriented interface. + """ + + def __init__(self, *, console: object, auto_refresh: bool = True) -> None: + self._renderable: object = Text('') + self._live = Live( + console=console, + auto_refresh=auto_refresh, + refresh_per_second=1 / _SPINNER_FRAME_SECONDS, + get_renderable=self._get_renderable, + vertical_overflow='visible', + ) + self._live.start() + + def _get_renderable(self) -> object: + renderable = self._renderable + if callable(renderable): + return renderable() + return renderable + + def update(self, renderable: object) -> None: + """ + Refresh the live display with a new renderable. + + Parameters + ---------- + renderable : object + A Rich-compatible renderable to display. + """ + self._renderable = renderable + self._live.refresh() + + def close(self) -> None: + """Stop the live display, suppressing any errors.""" + with suppress(Exception): + self._live.stop() + + +def make_display_handle(*, auto_refresh: bool = True) -> object | None: + """ + Create a generic in-place display handle for the active environment. + + Parameters + ---------- + auto_refresh : bool, default=True + Whether a terminal live handle should refresh continuously. + + Returns + ------- + object | None + An IPython ``DisplayHandle`` in notebooks, a terminal live + handle in the console, or ``None`` if neither is available. + """ + if in_jupyter() and DisplayHandle is not None and HTML is not None: + handle = DisplayHandle() + with suppress(Exception): + handle.display(HTML('')) + return handle + + return _TerminalLiveHandle(console=ConsoleManager.get(), auto_refresh=auto_refresh) + + +class ActivityIndicator: + """ + Render a live activity indicator for long-running work. + + Parameters + ---------- + label : str, default=ACTIVITY_LABEL_PROCESSING + User-facing activity label. + verbosity : VerbosityEnum + Output verbosity controlling whether live display is shown. + display_handle : object | None, default=None + Optional existing live display handle to reuse. + animated : bool, default=True + Whether to animate the spinner label continuously. + refresh_per_second : float | None, default=None + Optional override for the Rich Live refresh rate. When ``None``, + defaults to one refresh per spinner frame. Lower values reduce + terminal flicker for multi-line live regions. + """ + + def __init__( + self, + label: str = ACTIVITY_LABEL_PROCESSING, + *, + verbosity: VerbosityEnum, + display_handle: object | None = None, + animated: bool = True, + refresh_per_second: float | None = None, + ) -> None: + self._label = label + self._verbosity = verbosity + self._content: object | None = None + self._provided_display_handle = display_handle + self._refresh_per_second = ( + refresh_per_second if refresh_per_second is not None else 1 / _SPINNER_FRAME_SECONDS + ) + self._animated = animated + self._display_handle: object | None = None + self._live: object | None = None + self._running = False + self._keep_stopped_label = False + self._started_at = monotonic() + + def start(self) -> None: + """ + Start the live activity indicator. + + Returns + ------- + None + Starts live rendering unless verbosity is silent. + """ + if self._verbosity is VerbosityEnum.SILENT or self._running: + return + + self._running = True + self._keep_stopped_label = False + self._started_at = monotonic() + + if self._provided_display_handle is not None: + self._display_handle = self._provided_display_handle + self._refresh() + return + + if in_jupyter() and DisplayHandle is not None and HTML is not None: + handle = DisplayHandle() + self._display_handle = handle + with suppress(Exception): + handle.display(HTML(self._render_html())) + return + + live = Live( + console=ConsoleManager.get(), + auto_refresh=self._animated, + refresh_per_second=self._refresh_per_second, + get_renderable=self._terminal_renderable, + vertical_overflow='visible', + ) + live.start() + self._live = live + + def update( + self, + *, + label: str | None = None, + content: object | None = None, + ) -> None: + """ + Refresh the current label and optional rendered content. + + Parameters + ---------- + label : str | None, default=None + Replacement activity label. When ``None``, keep the current + label. + content : object | None, default=None + Optional content rendered above the indicator. When + ``None``, keep the current content. + """ + if label is not None: + self._label = label + if content is not None: + self._content = content + self._refresh() + + def stop(self, *, final_label: str | None = None) -> None: + """ + Stop live rendering and keep optional final content visible. + + Parameters + ---------- + final_label : str | None, default=None + Optional final label to leave in place after stopping. When + omitted, only the current content remains visible. + """ + self._running = False + self._keep_stopped_label = final_label is not None + if final_label is not None: + self._label = final_label + + self._refresh() + + if self._live is not None: + with suppress(Exception): + self._live.stop() + self._live = None + self._display_handle = None + + def _terminal_renderable(self) -> object: + """Return the terminal renderable for the current state.""" + renderables: list[object] = [] + content = self._terminal_content() + if content is not None: + renderables.append(content) + + indicator_line = self._terminal_indicator_line() + if indicator_line is not None: + renderables.append(indicator_line) + + if not renderables: + return Text('') + + if len(renderables) == 1: + return renderables[0] + + return Group(*renderables) + + def _refresh(self) -> None: + if self._verbosity is VerbosityEnum.SILENT: + return + + if self._display_handle is not None: + self._refresh_display_handle() + return + + if self._live is not None: + with suppress(Exception): + self._live.refresh() + + def _refresh_display_handle(self) -> None: + if self._display_handle is None: + return + + if ( + HTML is not None + and DisplayHandle is not None + and isinstance(self._display_handle, DisplayHandle) + ): + with suppress(Exception): + self._display_handle.update(HTML(self._render_html())) + return + + renderable: object = self._terminal_renderable() + if isinstance(self._display_handle, _TerminalLiveHandle): + renderable = self._terminal_renderable + with suppress(Exception): + self._display_handle.update(renderable) + + def _terminal_content(self) -> object | None: + if self._content is None: + return None + if is_renderable(self._content): + return self._content + return Text(str(self._content)) + + def _terminal_indicator_line(self) -> Text | None: + style = resolve_activity_terminal_style(ConsoleManager.get()) + if self._running: + if self._animated: + frame = self._current_frame() + return Text(f'{frame} {self._label}', style=style) + return Text(self._label, style=style) + if self._keep_stopped_label: + return Text(self._label, style=style) + return None + + def _current_frame(self) -> str: + elapsed = monotonic() - self._started_at + frame_index = int(elapsed / _SPINNER_FRAME_SECONDS) % len(SPINNER_FRAMES) + return SPINNER_FRAMES[frame_index] + + def _render_html(self) -> str: + content_html = self._html_content() + indicator_html = self._html_indicator() + + sections = [section for section in (content_html, indicator_html) if section] + if not sections: + return '' + + body = ''.join(sections) + return f'{self._html_style()}
{body}
' + + def _html_content(self) -> str: + if self._content is None: + return '' + if isinstance(self._content, str): + return self._content + + data = getattr(self._content, 'data', None) + if isinstance(data, str): + return data + + text = html.escape(str(self._content)) + return f'
{text}
' + + def _html_indicator(self) -> str: + safe_label = html.escape(self._label) + + if self._running: + if not self._animated: + return ( + '
' + f'{safe_label}' + '
' + ) + return ( + '
' + '' + f'{safe_label}' + '
' + ) + + if self._keep_stopped_label: + return ( + '
' + f'{safe_label}' + '
' + ) + + return '' + + @staticmethod + def _html_style() -> str: + keyframes = [] + total_frames = len(SPINNER_FRAMES) + for index, frame in enumerate(SPINNER_FRAMES): + percent = int(index * 100 / total_frames) + keyframes.append(f'{percent}% {{ content: "{frame}"; }}') + keyframes.append(f'100% {{ content: "{SPINNER_FRAMES[0]}"; }}') + keyframe_css = ' '.join(keyframes) + + return ( + '' + ) + + +class _ActivityIndicatorContext(AbstractContextManager[ActivityIndicator]): + """Context manager wrapper for ``ActivityIndicator``.""" + + def __init__(self, *, label: str, verbosity: VerbosityEnum) -> None: + self._indicator = ActivityIndicator(label, verbosity=verbosity) + + def __enter__(self) -> ActivityIndicator: + self._indicator.start() + return self._indicator + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, + ) -> None: + del exc_type + del exc_value + del traceback + self._indicator.stop() + + +def activity_indicator( + label: str = ACTIVITY_LABEL_PROCESSING, + *, + verbosity: VerbosityEnum, +) -> AbstractContextManager[ActivityIndicator]: + """ + Manage an activity indicator around a block of work. + + Parameters + ---------- + label : str, default=ACTIVITY_LABEL_PROCESSING + User-facing activity label. + verbosity : VerbosityEnum + Output verbosity controlling whether live display is shown. + + Returns + ------- + AbstractContextManager[ActivityIndicator] + Context manager that starts the indicator on entry and stops it + on exit. + """ + return _ActivityIndicatorContext(label=label, verbosity=verbosity) diff --git a/src/easydiffraction/display/tablers/base.py b/src/easydiffraction/display/tablers/base.py index 75922fce3..0bc2bddfb 100644 --- a/src/easydiffraction/display/tablers/base.py +++ b/src/easydiffraction/display/tablers/base.py @@ -98,6 +98,29 @@ def _rich_border_color(self) -> str: def _pandas_border_color(self) -> str: return self._rich_to_hex(self._rich_border_color) + @abstractmethod + def build_renderable( + self, + alignments: object, + df: object, + ) -> object: + """ + Build a backend-native table representation. + + 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. + + Returns + ------- + object + Backend-native renderable, such as a Rich table or HTML. + """ + @abstractmethod def render( self, diff --git a/src/easydiffraction/display/tablers/pandas.py b/src/easydiffraction/display/tablers/pandas.py index affa3ff8f..823c2cb09 100644 --- a/src/easydiffraction/display/tablers/pandas.py +++ b/src/easydiffraction/display/tablers/pandas.py @@ -241,6 +241,38 @@ def render( object Backend-defined return value (commonly ``None``). """ - color = self._pandas_border_color - styler = self._apply_styling(df, alignments, color) + styler = self._build_styler(alignments, df) self._update_display(styler, display_handle) + + def build_renderable( + self, + alignments: object, + df: object, + ) -> object: + """ + Build notebook HTML for the provided table. + + Parameters + ---------- + alignments : object + Iterable of column justifications (e.g. 'left'). + df : object + Index-aware DataFrame whose index is shown as the first + column. + + Returns + ------- + object + HTML string representation of the styled table. + """ + styler = self._build_styler(alignments, df) + return styler.to_html() + + def _build_styler( + self, + alignments: object, + df: object, + ) -> object: + """Return a configured pandas Styler for the provided table.""" + color = self._pandas_border_color + return self._apply_styling(df, alignments, color) diff --git a/src/easydiffraction/display/tablers/rich.py b/src/easydiffraction/display/tablers/rich.py index 8daafb6ba..e23334be3 100644 --- a/src/easydiffraction/display/tablers/rich.py +++ b/src/easydiffraction/display/tablers/rich.py @@ -66,24 +66,27 @@ def _to_html(table: Table) -> str: "
 Table:
+    def build_renderable(
+        self,
+        alignments: object,
+        df: object,
+    ) -> object:
         """
         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.
+        df : object
+            DataFrame-like object providing rows to render.
 
         Returns
         -------
-        Table
+        object
             A :class:`~rich.table.Table` configured for display.
         """
+        color = self._rich_border_color
         table = Table(
             title=None,
             box=RICH_TABLE_BOX,
@@ -172,6 +175,5 @@ def render(
         object
             Backend-defined return value (commonly ``None``).
         """
-        color = self._rich_border_color
-        table = self._build_table(df, alignments, color)
+        table = self.build_renderable(alignments, df)
         self._update_display(table, display_handle)
diff --git a/src/easydiffraction/display/tables.py b/src/easydiffraction/display/tables.py
index cda6f0d19..574d245a0 100644
--- a/src/easydiffraction/display/tables.py
+++ b/src/easydiffraction/display/tables.py
@@ -75,6 +75,47 @@ def show_config(self) -> None:
         console.paragraph('Current tabler configuration')
         TableRenderer.get().render(df)
 
+    @staticmethod
+    def _prepare_dataframe(df: object) -> tuple[object, object]:
+        """
+        Normalize input table data for backend consumption.
+
+        Parameters
+        ----------
+        df : object
+            DataFrame with a two-level column index where the second
+            level provides per-column alignment.
+
+        Returns
+        -------
+        tuple[object, object]
+            Normalized ``(alignments, dataframe)`` pair.
+        """
+        prepared_df = df.copy()
+        prepared_df.index += 1
+
+        alignments = prepared_df.columns.get_level_values(1)
+        prepared_df.columns = prepared_df.columns.get_level_values(0)
+        return alignments, prepared_df
+
+    def build_renderable(self, df: object) -> object:
+        """
+        Build a backend-native renderable without displaying it.
+
+        Parameters
+        ----------
+        df : object
+            DataFrame with a two-level column index where the second
+            level provides per-column alignment.
+
+        Returns
+        -------
+        object
+            Backend-native renderable, such as a Rich table or HTML.
+        """
+        alignments, prepared_df = self._prepare_dataframe(df)
+        return self._backend.build_renderable(alignments, prepared_df)
+
     def render(self, df: object, display_handle: object | None = None) -> object:
         """
         Render a DataFrame as a table using the active backend.
@@ -94,19 +135,8 @@ def render(self, df: object, display_handle: object | None = None) -> object:
         object
             Backend-specific return value (usually ``None``).
         """
-        # Work on a copy to avoid mutating the original DataFrame
-        df = df.copy()
-
-        # Force starting index from 1
-        df.index += 1
-
-        # Extract column alignments
-        alignments = df.columns.get_level_values(1)
-
-        # Remove alignments from df (Keep only the first index level)
-        df.columns = df.columns.get_level_values(0)
-
-        return self._backend.render(alignments, df, display_handle)
+        alignments, prepared_df = self._prepare_dataframe(df)
+        return self._backend.render(alignments, prepared_df, display_handle)
 
 
 class TableRendererFactory(RendererFactoryBase):
diff --git a/src/easydiffraction/io/ascii.py b/src/easydiffraction/io/ascii.py
index 189926a16..dcbea91b3 100644
--- a/src/easydiffraction/io/ascii.py
+++ b/src/easydiffraction/io/ascii.py
@@ -5,13 +5,29 @@
 from __future__ import annotations
 
 import re
-import tempfile
 import zipfile
 from io import StringIO
 from pathlib import Path
 
 import numpy as np
 
+from easydiffraction.utils.environment import create_artifact_temp_dir
+from easydiffraction.utils.environment import resolve_artifact_path
+
+
+def _resolve_extraction_destination(destination: str | Path | None) -> Path:
+    """Return an extraction directory for ZIP contents."""
+    if destination is None:
+        return create_artifact_temp_dir(prefix='ed_zip_')
+
+    extract_dir = resolve_artifact_path(destination)
+    if not extract_dir.is_absolute():
+        extract_dir = Path.cwd() / extract_dir
+
+    extract_dir = extract_dir.resolve()
+    extract_dir.mkdir(parents=True, exist_ok=True)
+    return extract_dir
+
 
 def extract_project_from_zip(
     zip_path: str | Path,
@@ -31,7 +47,9 @@ def extract_project_from_zip(
         Path to the ZIP archive containing the project.
     destination : str | Path | None, default=None
         Directory to extract into.  When ``None``, a temporary directory
-        is created.
+        is created. Relative destinations are resolved against the
+        configured artifact root when ``EASYDIFFRACTION_ARTIFACT_ROOT``
+        is set.
 
     Returns
     -------
@@ -51,11 +69,7 @@ def extract_project_from_zip(
         msg = f'ZIP file not found: {zip_path}'
         raise FileNotFoundError(msg)
 
-    if destination is not None:
-        extract_dir = Path(destination)
-        extract_dir.mkdir(parents=True, exist_ok=True)
-    else:
-        extract_dir = Path(tempfile.mkdtemp(prefix='ed_zip_'))
+    extract_dir = _resolve_extraction_destination(destination)
 
     with zipfile.ZipFile(zip_path, 'r') as zf:
         # Determine the project directory from the archive contents
@@ -92,7 +106,9 @@ def extract_data_paths_from_zip(
         Path to the ZIP archive.
     destination : str | Path | None, default=None
         Directory to extract files into.  When ``None``, a temporary
-        directory is created.
+        directory is created. Relative destinations are resolved against
+        the current working directory, or against the configured
+        artifact root when ``EASYDIFFRACTION_ARTIFACT_ROOT`` is set.
 
     Returns
     -------
@@ -111,12 +127,7 @@ def extract_data_paths_from_zip(
         msg = f'ZIP file not found: {zip_path}'
         raise FileNotFoundError(msg)
 
-    if destination is not None:
-        extract_dir = Path(destination)
-        extract_dir.mkdir(parents=True, exist_ok=True)
-    else:
-        # TODO: Unify mkdir with other uses in the code
-        extract_dir = Path(tempfile.mkdtemp(prefix='ed_zip_'))
+    extract_dir = _resolve_extraction_destination(destination)
 
     with zipfile.ZipFile(zip_path, 'r') as zf:
         zf.extractall(extract_dir)
@@ -163,7 +174,7 @@ def extract_data_paths_from_dir(
     ValueError
         If no matching data files are found.
     """
-    dir_path = Path(dir_path)
+    dir_path = Path(dir_path).resolve()
     if not dir_path.is_dir():
         msg = f'Directory not found: {dir_path}'
         raise FileNotFoundError(msg)
diff --git a/src/easydiffraction/io/cif/serialize.py b/src/easydiffraction/io/cif/serialize.py
index 0b868b6c1..ef9fa277d 100644
--- a/src/easydiffraction/io/cif/serialize.py
+++ b/src/easydiffraction/io/cif/serialize.py
@@ -3,6 +3,7 @@
 
 from __future__ import annotations
 
+import textwrap
 from typing import TYPE_CHECKING
 from typing import Any
 
@@ -21,15 +22,15 @@
     from easydiffraction.core.category import CategoryItem
     from easydiffraction.core.variable import GenericDescriptorBase
 
-# Maximum CIF description length before using semicolon-delimited block
-_CIF_DESCRIPTION_WRAP_LEN = 60
-
 # Minimum string length to check for surrounding quotes
 _MIN_QUOTED_LEN = 2
 
 # Number of significant digits kept for CIF uncertainty notation
 _CIF_UNCERTAINTY_SIG_DIGITS = 2
 
+# Maximum CIF description length before using semicolon-delimited block
+_CIF_DESCRIPTION_WRAP_LEN = 60
+
 
 def format_value(value: object) -> str:
     """
@@ -48,9 +49,12 @@ def format_value(value: object) -> str:
     # None → CIF unknown marker
     if value is None:
         value = '?'
-    # Convert ints to floats
-    elif isinstance(value, int):
-        value = float(value)
+    # Booleans use CIF true/false tokens
+    elif isinstance(value, bool):
+        value = 'true' if value else 'false'
+    # Preserve integers as integers in CIF output
+    elif isinstance(value, (int, np.integer)):
+        value = str(int(value))
     # Empty strings → CIF unknown marker
     elif isinstance(value, str) and not value.strip():
         value = '?'
@@ -70,6 +74,29 @@ def format_value(value: object) -> str:
     return str(value)
 
 
+def _strip_optional_quotes(raw: str) -> str:
+    """Return an unquoted CIF token when it is wrapped in quotes."""
+    is_quoted = len(raw) >= _MIN_QUOTED_LEN and raw[0] == raw[-1] and raw[0] in {"'", '"'}
+    return raw[1:-1] if is_quoted else raw
+
+
+def _strip_cif_text_field_delimiters(raw: str) -> str:
+    """Return CIF text-field content without delimiter lines."""
+    if raw.startswith(';\n') and raw.endswith('\n;'):
+        return raw[2:-2].strip()
+    return raw
+
+
+def _parse_bool_cif_value(raw: str) -> bool | str:
+    """Parse CIF boolean tokens, returning the raw token if invalid."""
+    normalized_value = _strip_optional_quotes(raw).lower()
+    if normalized_value == 'true':
+        return True
+    if normalized_value == 'false':
+        return False
+    return _strip_optional_quotes(raw)
+
+
 ##################
 # Serialize to CIF
 ##################
@@ -87,7 +114,7 @@ def format_param_value(param: object) -> str:
     - Free parameter with uncertainty: value with esd in brackets,
       e.g. ``3.89(20)``
 
-    Constrained (dependent) parameters are always written without
+    User-constrained (dependent) parameters are always written without
     brackets, even if their ``free`` flag is ``True``, because they are
     not independently varied by the minimizer.
 
@@ -98,7 +125,7 @@ def format_param_value(param: object) -> str:
     ----------
     param : object
         A descriptor or parameter exposing ``.value`` and optionally
-        ``.free``, ``.constrained``, and ``.uncertainty``.
+        ``.free``, ``.user_constrained``, and ``.uncertainty``.
 
     Returns
     -------
@@ -108,10 +135,10 @@ def format_param_value(param: object) -> str:
     from easydiffraction.core.variable import Parameter  # noqa: PLC0415
 
     is_free = param.free if isinstance(param, Parameter) else False
-    is_constrained = param.constrained if isinstance(param, Parameter) else False
+    is_user_constrained = param.user_constrained if isinstance(param, Parameter) else False
     value = param.value  # type: ignore[attr-defined]
 
-    if not is_free or is_constrained or not isinstance(value, (int, float)):
+    if not is_free or is_user_constrained or not isinstance(value, (int, float)):
         return format_value(value)
 
     precision = 8
@@ -249,6 +276,39 @@ def _row(item: object) -> list[str]:
     return '\n'.join(lines)
 
 
+def category_owner_to_cif(
+    owner: object,
+    max_loop_display: int | None = None,
+) -> str:
+    """Render a category-owning object without a ``data_`` header."""
+    from easydiffraction.core.category import CategoryCollection  # noqa: PLC0415
+    from easydiffraction.core.category import CategoryItem  # noqa: PLC0415
+
+    categories_getter = getattr(owner, '_serializable_categories', None)
+    if callable(categories_getter):
+        categories = categories_getter()
+    else:
+        categories = [
+            value
+            for value in vars(owner).values()
+            if isinstance(value, (CategoryItem, CategoryCollection))
+        ]
+
+    item_parts = [
+        category.as_cif
+        for category in categories
+        if isinstance(category, CategoryItem) and category.as_cif
+    ]
+
+    collection_parts = [
+        category_collection_to_cif(category, max_display=max_loop_display)
+        for category in categories
+        if isinstance(category, CategoryCollection)
+    ]
+
+    return '\n\n'.join([part for part in item_parts + collection_parts if part])
+
+
 def datablock_item_to_cif(
     datablock: object,
     max_loop_display: int | None = None,
@@ -271,24 +331,11 @@ def datablock_item_to_cif(
     str
         CIF text representing the datablock as a loop.
     """
-    # Local imports to avoid import-time cycles
-    from easydiffraction.core.category import CategoryCollection  # noqa: PLC0415
-    from easydiffraction.core.category import CategoryItem  # noqa: PLC0415
-
     header = f'data_{datablock._identity.datablock_entry_name}'
-    parts: list[str] = [header]
-
-    # First categories
-    parts.extend(v.as_cif for v in vars(datablock).values() if isinstance(v, CategoryItem))
-
-    # Then collections
-    parts.extend(
-        category_collection_to_cif(v, max_display=max_loop_display)
-        for v in vars(datablock).values()
-        if isinstance(v, CategoryCollection)
-    )
-
-    return '\n\n'.join(parts)
+    body = category_owner_to_cif(datablock, max_loop_display=max_loop_display)
+    if not body:
+        return header
+    return f'{header}\n\n{body}'
 
 
 def datablock_collection_to_cif(collection: object) -> str:
@@ -296,25 +343,38 @@ def datablock_collection_to_cif(collection: object) -> str:
     return '\n\n'.join([block.as_cif for block in collection.values()])
 
 
+def _format_project_description(description: str) -> str:
+    """Format project descriptions as CIF text."""
+    normalized_description = ' '.join(description.split())
+    if not normalized_description:
+        return '?'
+
+    if len(normalized_description) > _CIF_DESCRIPTION_WRAP_LEN:
+        wrapped_description = '\n'.join(
+            textwrap.wrap(
+                normalized_description,
+                width=_CIF_DESCRIPTION_WRAP_LEN,
+                break_long_words=False,
+                break_on_hyphens=False,
+            )
+        )
+        return f'\n;\n{wrapped_description}\n;'
+
+    return format_value(normalized_description)
+
+
 def project_info_to_cif(info: object) -> str:
     """Render ProjectInfo to CIF text (id, title, description)."""
     name = f'{info.name}'
 
     title = f'{info.title}'
     if ' ' in title:
-        title = f"'{title}'"
-
-    if len(info.description) > _CIF_DESCRIPTION_WRAP_LEN:
-        description = f'\n;\n{info.description}\n;'
-    elif info.description:
-        description = f'{info.description}'
-        if ' ' in description:
-            description = f"'{description}'"
-    else:
-        description = '?'
+        title = format_value(info.title)
+
+    description = _format_project_description(info.description)
 
-    created = f"'{info._created.strftime('%d %b %Y %H:%M:%S')}'"
-    last_modified = f"'{info._last_modified.strftime('%d %b %Y %H:%M:%S')}'"
+    created = format_value(info.created.strftime('%d %b %Y %H:%M:%S'))
+    last_modified = format_value(info.last_modified.strftime('%d %b %Y %H:%M:%S'))
 
     return (
         f'_project.id               {name}\n'
@@ -333,10 +393,14 @@ def _as_cif_text(section: object) -> str:
 
 def project_config_to_cif(project: object) -> str:
     """Render project-level configuration to ``project.cif`` text."""
+    config = getattr(project, '_config', None)
+    if config is not None:
+        return category_owner_to_cif(config)
+
     lines: list[str] = [_as_cif_text(project.info)]
-    display = getattr(project, 'display', None)
-    if display is not None:
-        lines.extend(('', _as_cif_text(display)))
+    rendering = getattr(project, 'rendering', None)
+    if rendering is not None:
+        lines.extend(('', _as_cif_text(rendering)))
     return '\n'.join(lines)
 
 
@@ -350,7 +414,7 @@ def project_to_cif(project: object) -> str:
     if getattr(project, 'experiments', None):
         parts.append(_as_cif_text(project.experiments))
     if getattr(project, 'analysis', None):
-        parts.append(project.analysis.as_cif())
+        parts.append(_as_cif_text(project.analysis))
     if getattr(project, 'summary', None):
         parts.append(project.summary.as_cif())
     return '\n\n'.join([p for p in parts if p])
@@ -363,18 +427,32 @@ def experiment_to_cif(experiment: object) -> str:
 
 def analysis_to_cif(analysis: object) -> str:
     """Render analysis metadata, aliases, and constraints to CIF."""
-    lines: list[str] = []
-    lines.extend((
-        analysis.fit.as_cif,
-        '',
-        analysis.aliases.as_cif,
-        '',
-        analysis.constraints.as_cif,
-    ))
-    jfe_cif = analysis.joint_fit_experiments.as_cif
-    if jfe_cif:
-        lines.extend(('', jfe_cif))
-    return '\n'.join(lines)
+    parts: list[str] = [f'_fitting.mode_type {format_value(analysis.fitting_mode_type)}']
+
+    body = category_owner_to_cif(analysis)
+    if not body:
+        fallback_sections = [
+            getattr(analysis, 'fitting', None),
+            getattr(analysis, 'aliases', None),
+            getattr(analysis, 'constraints', None),
+        ]
+
+        if analysis.fitting_mode_type == 'joint':
+            fallback_sections.append(getattr(analysis, 'joint_fit', None))
+        elif analysis.fitting_mode_type == 'sequential':
+            fallback_sections.extend([
+                getattr(analysis, 'sequential_fit', None),
+                getattr(analysis, 'sequential_fit_extract', None),
+            ])
+
+        body = '\n\n'.join([
+            _as_cif_text(section) for section in fallback_sections if section is not None
+        ])
+
+    if body:
+        parts.append(body)
+
+    return '\n\n'.join(parts)
 
 
 def summary_to_cif(_summary: object) -> str:
@@ -401,11 +479,23 @@ def _wrap_in_data_block(cif_text: str, block_name: str = '_') -> str:
     return f'data_{block_name}\n\n{cif_text}'
 
 
+def _project_block_from_cif_text(cif_text: str) -> gemmi.cif.Block:
+    """Parse project CIF text."""
+    import gemmi  # noqa: PLC0415
+
+    return gemmi.cif.read_string(_wrap_in_data_block(cif_text, 'project')).sole_block()
+
+
 def _populate_project_info_from_block(
     info: object,
     block: gemmi.cif.Block,
 ) -> None:
     """Populate ProjectInfo fields from a parsed CIF block."""
+    from_cif = getattr(info, 'from_cif', None)
+    if callable(from_cif):
+        from_cif(block)
+        return
+
     read_cif_string = _make_cif_string_reader(block)
 
     name = read_cif_string('_project.id')
@@ -425,9 +515,7 @@ def project_info_from_cif(info: object, cif_text: str) -> None:
     """
     Populate a ProjectInfo instance from CIF text.
 
-    Reads ``_project.id``, ``_project.title``, and
-    ``_project.description`` from the given CIF string and sets them on
-    the *info* object.
+    Reads the core project metadata fields from CIF text.
 
     Parameters
     ----------
@@ -436,10 +524,7 @@ def project_info_from_cif(info: object, cif_text: str) -> None:
     cif_text : str
         CIF text content of ``project.cif``.
     """
-    import gemmi  # noqa: PLC0415
-
-    doc = gemmi.cif.read_string(_wrap_in_data_block(cif_text, 'project'))
-    block = doc.sole_block()
+    block = _project_block_from_cif_text(cif_text)
 
     _populate_project_info_from_block(info, block)
 
@@ -448,16 +533,17 @@ def project_config_from_cif(project: object, cif_text: str) -> None:
     """
     Populate project-level configuration from ``project.cif`` text.
     """
-    import gemmi  # noqa: PLC0415
-
-    doc = gemmi.cif.read_string(_wrap_in_data_block(cif_text, 'project'))
-    block = doc.sole_block()
+    block = _project_block_from_cif_text(cif_text)
 
     _populate_project_info_from_block(project.info, block)
 
-    display = getattr(project, 'display', None)
-    if display is not None:
-        display.from_cif(block)
+    rendering = getattr(project, 'rendering', None)
+    if rendering is not None:
+        rendering.from_cif(block)
+
+    verbosity = getattr(project, 'verbosity', None)
+    if verbosity is not None:
+        verbosity.from_cif(block)
 
 
 def analysis_from_cif(analysis: object, cif_text: str) -> None:
@@ -479,8 +565,12 @@ def analysis_from_cif(analysis: object, cif_text: str) -> None:
     doc = gemmi.cif.read_string(_wrap_in_data_block(cif_text, 'analysis'))
     block = doc.sole_block()
 
+    _raise_for_legacy_analysis_tags(block)
+    analysis._set_fitting_mode_type(_analysis_mode_from_cif_block(block))
+
     # Restore fit configuration
-    analysis.fit.from_cif(block)
+    analysis.fitting.from_cif(block)
+    _restore_mode_specific_analysis_sections(analysis, block)
 
     # Restore aliases (loop)
     analysis.aliases.from_cif(block)
@@ -490,8 +580,187 @@ def analysis_from_cif(analysis: object, cif_text: str) -> None:
     if analysis.constraints._items:
         analysis.constraints.enable()
 
-    # Restore joint-fit experiment weights (loop)
-    analysis._joint_fit_experiments.from_cif(block)
+    if _has_persisted_fit_state_sections(block):
+        _restore_persisted_fit_state(analysis, block)
+
+
+def _has_persisted_fit_state_sections(block: object) -> bool:
+    """Return True when any persisted fit-state section is present."""
+    scalar_tags = (
+        '_fit_result.result_kind',
+        '_deterministic_result.optimizer_name',
+        '_bayesian_result.sampler_name',
+        '_bayesian_sampler.steps',
+        '_bayesian_convergence.converged',
+    )
+    loop_tags = (
+        '_fit_parameter.param_unique_name',
+        '_fit_parameter_correlation.param_unique_name_i',
+        '_bayesian_parameter_posterior.unique_name',
+        '_bayesian_distribution_cache.param_unique_name',
+        '_bayesian_pair_cache.param_unique_name_x',
+        '_bayesian_predictive_dataset.experiment_name',
+    )
+
+    return any(_has_cif_value(block, tag) for tag in scalar_tags) or any(
+        _has_cif_loop(block, tag) for tag in loop_tags
+    )
+
+
+def _restore_common_fit_state(analysis: object, block: object) -> None:
+    """Restore fit-state categories shared by both fit kinds."""
+    analysis.fit_parameters.from_cif(block)
+    analysis.fit_result.from_cif(block)
+    analysis.fit_parameter_correlations.from_cif(block)
+
+
+def _restore_deterministic_fit_state(analysis: object, block: object) -> None:
+    """Restore deterministic-only persisted fit-state categories."""
+    analysis.deterministic_result.from_cif(block)
+
+
+def _restore_bayesian_fit_state(analysis: object, block: object) -> None:
+    """Restore Bayesian-only persisted fit-state categories."""
+    analysis.bayesian_result.from_cif(block)
+    analysis.bayesian_sampler.from_cif(block)
+    analysis.bayesian_convergence.from_cif(block)
+    analysis.bayesian_parameter_posteriors.from_cif(block)
+    analysis.bayesian_distribution_caches.from_cif(block)
+    analysis.bayesian_pair_caches.from_cif(block)
+    analysis.bayesian_predictive_datasets.from_cif(block)
+    analysis._sync_live_minimizer_from_persisted_fit_state()
+
+
+def _restore_persisted_fit_state(analysis: object, block: object) -> None:
+    """
+    Restore persisted fit-state categories after analysis configuration.
+    """
+    from easydiffraction.analysis.enums import FitResultKindEnum  # noqa: PLC0415
+
+    analysis._set_has_persisted_fit_state(value=True)
+    _restore_common_fit_state(analysis, block)
+
+    result_kind_value = analysis.fit_result.result_kind.value
+    try:
+        result_kind = FitResultKindEnum(result_kind_value)
+    except ValueError:
+        log.warning(
+            'Unsupported _fit_result.result_kind in analysis CIF: '
+            f'{result_kind_value!r}. Skipping kind-specific fit-state categories.',
+        )
+        return
+
+    if result_kind is FitResultKindEnum.DETERMINISTIC:
+        _restore_deterministic_fit_state(analysis, block)
+        return
+
+    _restore_bayesian_fit_state(analysis, block)
+
+
+def _collect_legacy_analysis_tags(block: object) -> list[str]:
+    """Return deprecated analysis CIF tags present in a block."""
+    legacy_tags: list[str] = []
+    if _has_cif_value(block, '_fit.minimizer_type'):
+        legacy_tags.append('_fit.minimizer_type')
+    if _has_cif_value(block, '_fit.mode'):
+        legacy_tags.append('_fit.mode')
+    if _has_cif_loop(block, '_joint_fit_experiment.id'):
+        legacy_tags.append('_joint_fit_experiment.id')
+    if _has_cif_loop(block, '_joint_fit_experiment.weight'):
+        legacy_tags.append('_joint_fit_experiment.weight')
+    return legacy_tags
+
+
+def _raise_for_legacy_analysis_tags(block: object) -> None:
+    """Raise when deprecated analysis CIF tags are present."""
+    legacy_tags = _collect_legacy_analysis_tags(block)
+    if not legacy_tags:
+        return
+
+    msg = (
+        'Legacy analysis CIF tags are no longer supported: '
+        f'{legacy_tags}. Use _fitting.minimizer_type, _fitting.mode_type, '
+        '_joint_fit.experiment_id, and _joint_fit.weight.'
+    )
+    raise ValueError(msg)
+
+
+def _analysis_mode_from_cif_block(block: object) -> str:
+    """Return the fitting mode stored in an analysis CIF block."""
+    read_cif_string = _make_cif_string_reader(block)
+    mode_value = read_cif_string('_fitting.mode_type')
+    if mode_value is not None:
+        return mode_value
+
+    from easydiffraction.analysis.enums import FitModeEnum  # noqa: PLC0415
+
+    return FitModeEnum.default().value
+
+
+def _has_joint_fit_rows(block: object) -> bool:
+    """Return True when joint-fit rows are present."""
+    return _has_cif_loop(block, '_joint_fit.experiment_id') or _has_cif_loop(
+        block,
+        '_joint_fit.weight',
+    )
+
+
+def _has_sequential_fit_settings(block: object) -> bool:
+    """Return True when sequential-fit scalar settings are present."""
+    return any(
+        _has_cif_value(block, tag)
+        for tag in (
+            '_sequential_fit.data_dir',
+            '_sequential_fit.file_pattern',
+            '_sequential_fit.max_workers',
+            '_sequential_fit.chunk_size',
+            '_sequential_fit.reverse',
+        )
+    )
+
+
+def _warn_inactive_analysis_sections(
+    *,
+    has_joint_rows: bool,
+    has_sequential_settings: bool,
+    has_sequential_extract_rows: bool,
+) -> None:
+    """Warn when inactive analysis sections are skipped."""
+    skipped_sections: list[str] = []
+    if has_joint_rows:
+        skipped_sections.append('joint_fit')
+    if has_sequential_settings or has_sequential_extract_rows:
+        skipped_sections.append('sequential_fit')
+    log.warning(
+        'Skipping inactive analysis CIF sections while fitting_mode_type is single: '
+        f'{skipped_sections}.'
+    )
+
+
+def _restore_mode_specific_analysis_sections(analysis: object, block: object) -> None:
+    """Restore only the active mode-specific analysis sections."""
+    has_joint_rows = _has_joint_fit_rows(block)
+    has_sequential_settings = _has_sequential_fit_settings(block)
+    has_sequential_extract_rows = _has_cif_loop(block, '_sequential_fit_extract.id')
+
+    if analysis.fitting_mode_type == 'joint':
+        if has_joint_rows:
+            analysis.joint_fit.from_cif(block)
+        return
+
+    if analysis.fitting_mode_type == 'sequential':
+        if has_sequential_settings:
+            analysis.sequential_fit.from_cif(block)
+        if has_sequential_extract_rows:
+            analysis.sequential_fit_extract.from_cif(block)
+        return
+
+    if has_joint_rows or has_sequential_settings or has_sequential_extract_rows:
+        _warn_inactive_analysis_sections(
+            has_joint_rows=has_joint_rows,
+            has_sequential_settings=has_sequential_settings,
+            has_sequential_extract_rows=has_sequential_extract_rows,
+        )
 
 
 def _make_cif_string_reader(block: gemmi.cif.Block) -> object:
@@ -518,14 +787,26 @@ def _read(tag: str) -> str | None:
         # CIF unknown / inapplicable markers
         if raw in {'?', '.'}:
             return None
-        # Strip surrounding quotes
-        if len(raw) >= _MIN_QUOTED_LEN and raw[0] == raw[-1] and raw[0] in {"'", '"'}:
-            raw = raw[1:-1]
-        return raw
+        raw = _strip_cif_text_field_delimiters(raw)
+        return _strip_optional_quotes(raw)
 
     return _read
 
 
+def _has_cif_value(block: gemmi.cif.Block, tag: str) -> bool:
+    """Return True when a scalar CIF tag is present in the block."""
+    return block.find_value(tag) is not None
+
+
+def _has_cif_loop(block: gemmi.cif.Block, tag: str) -> bool:
+    """Return True when a CIF loop column is present in the block."""
+    loop_ref = block.find_loop(tag)
+    if loop_ref is None:
+        return False
+    loop = loop_ref.get_loop() if hasattr(loop_ref, 'get_loop') else loop_ref
+    return loop is not None
+
+
 # TODO: Check the following methods:
 
 ######################
@@ -564,33 +845,9 @@ def param_from_cif(
     if not found_values:
         return
 
-    # If found, pick the one at the given index
+    # If found, pick the one at the given index.
     raw = found_values[idx]
-
-    # CIF unknown / inapplicable markers → keep default
-    if raw in {'?', '.'}:
-        return
-
-    # If numeric, parse with uncertainty if present
-    if self._value_type == DataTypes.NUMERIC:
-        has_brackets = '(' in raw
-        u = str_to_ufloat(raw)
-        self.value = u.n
-        if has_brackets and hasattr(self, 'free'):
-            self.free = True  # type: ignore[attr-defined]
-            if not np.isnan(u.s) and hasattr(self, 'uncertainty'):
-                self.uncertainty = u.s  # type: ignore[attr-defined]
-
-    # If string, strip quotes if present
-    elif self._value_type == DataTypes.STRING:
-        if len(raw) >= _MIN_QUOTED_LEN and raw[0] == raw[-1] and raw[0] in {"'", '"'}:
-            self.value = raw[1:-1]
-        else:
-            self.value = raw
-
-    # Other types are not supported
-    else:
-        log.debug(f'Unrecognized type: {self._value_type}')
+    _set_param_from_raw_cif_value(self, raw)
 
 
 def category_item_from_cif(
@@ -620,11 +877,23 @@ def _set_param_from_raw_cif_value(
     raw : str
         The raw string from the CIF loop cell.
     """
+    raw = _strip_cif_text_field_delimiters(raw)
+
     # CIF unknown / inapplicable markers → keep default
     if raw in {'?', '.'}:
         return
 
-    if param._value_type == DataTypes.NUMERIC:
+    if param._value_type == DataTypes.INTEGER:
+        numeric_value = str_to_ufloat(raw).n
+        integer_value = round(numeric_value)
+        if not np.isclose(numeric_value, integer_value):
+            log.warning(
+                f'Ignoring non-integer CIF value {raw!r} for integer field {param.unique_name}.'
+            )
+            return
+        param.value = integer_value
+
+    elif param._value_type == DataTypes.NUMERIC:
         has_brackets = '(' in raw
         u = str_to_ufloat(raw)
         param.value = u.n
@@ -634,10 +903,11 @@ def _set_param_from_raw_cif_value(
                 param.uncertainty = u.s  # type: ignore[attr-defined]
 
     # If string, strip quotes if present
-    # TODO: Make a helper function for this
     elif param._value_type == DataTypes.STRING:
-        is_quoted = len(raw) >= _MIN_QUOTED_LEN and raw[0] == raw[-1] and raw[0] in {"'", '"'}
-        param.value = raw[1:-1] if is_quoted else raw
+        param.value = _strip_optional_quotes(raw)
+
+    elif param._value_type == DataTypes.BOOL:
+        param.value = _parse_bool_cif_value(raw)
 
     else:
         log.debug(f'Unrecognized type: {param._value_type}')
@@ -715,11 +985,7 @@ def category_collection_from_cif(
     array = np.array(loop.values, dtype=str).reshape(num_rows, num_cols)
 
     # Pre-create default items in the collection
-    self._items = [self._item_type() for _ in range(num_rows)]
-
-    # Set parent for each item to enable identity resolution
-    for item in self._items:
-        object.__setattr__(item, '_parent', self)  # noqa: PLC2801
+    self._adopt_items([self._item_type() for _ in range(num_rows)])
 
     # Set those items' parameters, which are present in the loop
     for row_idx in range(num_rows):
@@ -732,3 +998,9 @@ def category_collection_from_cif(
                     #  param_from_cif
                     _set_param_from_raw_cif_value(param, array[row_idx][col_idx])
                     break
+
+    after_from_cif = getattr(self, '_after_from_cif', None)
+    if callable(after_from_cif):
+        after_from_cif()
+
+    self._rebuild_index()
diff --git a/src/easydiffraction/io/results_sidecar.py b/src/easydiffraction/io/results_sidecar.py
new file mode 100644
index 000000000..36d442c77
--- /dev/null
+++ b/src/easydiffraction/io/results_sidecar.py
@@ -0,0 +1,652 @@
+# SPDX-FileCopyrightText: 2026 EasyScience contributors 
+# SPDX-License-Identifier: BSD-3-Clause
+"""Bayesian fit sidecar read/write helpers."""
+
+from __future__ import annotations
+
+from pathlib import Path
+from tempfile import NamedTemporaryFile
+
+import numpy as np
+
+from easydiffraction.utils.logging import log
+
+_DEFAULT_SIDECAR_FILE_NAME = 'results.h5'
+_POSTERIOR_PARAMETER_SAMPLES_PATH = '/posterior/parameter_samples'
+_POSTERIOR_LOG_POSTERIOR_PATH = '/posterior/log_posterior'
+_POSTERIOR_DRAW_INDEX_PATH = '/posterior/draw_index'
+_POSTERIOR_SAMPLE_NDIM = 3
+_PREDICTIVE_DRAWS_NDIM = 2
+
+
+def _normalized_hdf5_path(path: str) -> str:
+    """Return an HDF5 path without a leading slash."""
+    return path.lstrip('/')
+
+
+def _sidecar_file_name(analysis: object) -> str:
+    """Return the configured sidecar file name for an analysis."""
+    bayesian_result = getattr(analysis, 'bayesian_result', None)
+    if bayesian_result is None:
+        return _DEFAULT_SIDECAR_FILE_NAME
+
+    file_name = bayesian_result.sidecar_file.value
+    if not isinstance(file_name, str) or not file_name.strip():
+        return _DEFAULT_SIDECAR_FILE_NAME
+
+    normalized_name = file_name.strip()
+    normalized_path = Path(normalized_name)
+    if (
+        normalized_path.is_absolute()
+        or normalized_path.name in {'', '.', '..'}
+        or normalized_path.name != normalized_name
+    ):
+        log.warning(
+            'Ignoring Bayesian sidecar file path outside the analysis directory: '
+            f'{normalized_name!r}. Using {_DEFAULT_SIDECAR_FILE_NAME!r} instead.'
+        )
+        return _DEFAULT_SIDECAR_FILE_NAME
+
+    return normalized_path.name
+
+
+def _sidecar_path(*, analysis: object, analysis_dir: Path) -> Path:
+    """Return the results sidecar path inside the analysis directory."""
+    resolved_analysis_dir = analysis_dir.resolve()
+    sidecar_path = (resolved_analysis_dir / _sidecar_file_name(analysis)).resolve()
+    if sidecar_path.parent != resolved_analysis_dir:
+        log.warning(
+            'Resolved Bayesian sidecar file path escaped the analysis directory. '
+            f'Using {_DEFAULT_SIDECAR_FILE_NAME!r} instead.'
+        )
+        return resolved_analysis_dir / _DEFAULT_SIDECAR_FILE_NAME
+    return sidecar_path
+
+
+def _should_use_sidecar(analysis: object) -> bool:
+    """
+    Return whether the analysis currently expects a Bayesian sidecar.
+    """
+    has_fit_state = getattr(analysis, '_has_persisted_fit_state', None)
+    if not callable(has_fit_state) or not has_fit_state():
+        return False
+
+    if analysis.fit_result.result_kind.value != 'bayesian':
+        return False
+
+    return any((
+        analysis.bayesian_result.has_posterior_samples.value,
+        len(analysis.bayesian_distribution_caches) > 0,
+        len(analysis.bayesian_pair_caches) > 0,
+        len(analysis.bayesian_predictive_datasets) > 0,
+    ))
+
+
+def _delete_stale_sidecar(sidecar_path: Path) -> None:
+    """
+    Delete an existing sidecar when no persisted arrays should remain.
+    """
+    if sidecar_path.is_file():
+        sidecar_path.unlink()
+
+
+def _create_dataset(handle: object, path: str, data: np.ndarray) -> None:
+    """Create or replace one dataset in an open HDF5 file."""
+    normalized_path = _normalized_hdf5_path(path)
+    group_name, _, dataset_name = normalized_path.rpartition('/')
+    group = handle.require_group(group_name) if group_name else handle
+    if dataset_name in group:
+        del group[dataset_name]
+    group.create_dataset(dataset_name, data=data)
+
+
+def _read_dataset(handle: object, path: str) -> np.ndarray | None:
+    """Read one dataset from an open HDF5 file when it exists."""
+    normalized_path = _normalized_hdf5_path(path)
+    if normalized_path not in handle:
+        return None
+    return np.asarray(handle[normalized_path])
+
+
+def _posterior_payload_from_analysis(analysis: object) -> dict[str, np.ndarray | None]:
+    """Return posterior arrays from runtime or restored data."""
+    fit_results = getattr(analysis, 'fit_results', None)
+    posterior_samples = getattr(fit_results, 'posterior_samples', None)
+    if posterior_samples is not None:
+        return {
+            'parameter_samples': np.asarray(posterior_samples.parameter_samples, dtype=float),
+            'log_posterior': (
+                None
+                if posterior_samples.log_posterior is None
+                else np.asarray(posterior_samples.log_posterior, dtype=float)
+            ),
+            'draw_index': (
+                None
+                if posterior_samples.draw_index is None
+                else np.asarray(posterior_samples.draw_index)
+            ),
+        }
+
+    sidecar_data = getattr(analysis, '_persisted_fit_state_sidecar', {})
+    return dict(sidecar_data.get('posterior', {}))
+
+
+def _distribution_cache_payload(analysis: object) -> dict[str, dict[str, np.ndarray]]:
+    """Return persisted distribution caches keyed by parameter name."""
+    sidecar_data = getattr(analysis, '_persisted_fit_state_sidecar', {})
+    return dict(sidecar_data.get('distribution_caches', {}))
+
+
+def _pair_cache_payload(analysis: object) -> dict[str, dict[str, np.ndarray]]:
+    """Return persisted pair-cache arrays keyed by cache id."""
+    sidecar_data = getattr(analysis, '_persisted_fit_state_sidecar', {})
+    return dict(sidecar_data.get('pair_caches', {}))
+
+
+def _predictive_payload(analysis: object) -> dict[str, dict[str, np.ndarray]]:
+    """Return persisted predictive arrays keyed by experiment name."""
+    fit_results = getattr(analysis, 'fit_results', None)
+    posterior_predictive = getattr(fit_results, 'posterior_predictive', None)
+    if posterior_predictive:
+        payload: dict[str, dict[str, np.ndarray]] = {}
+        for runtime_key, summary in posterior_predictive.items():
+            experiment_name = getattr(summary, 'experiment_name', None)
+            if not isinstance(experiment_name, str) or not experiment_name.strip():
+                experiment_name = runtime_key
+
+            dataset_payload = payload.setdefault(experiment_name, {})
+            dataset_payload['x'] = np.asarray(summary.x, dtype=float)
+            dataset_payload['best_sample_prediction'] = np.asarray(
+                summary.best_sample_prediction,
+                dtype=float,
+            )
+            if summary.lower_95 is not None:
+                dataset_payload['lower_95'] = np.asarray(summary.lower_95, dtype=float)
+            if summary.upper_95 is not None:
+                dataset_payload['upper_95'] = np.asarray(summary.upper_95, dtype=float)
+            if summary.lower_68 is not None:
+                dataset_payload['lower_68'] = np.asarray(summary.lower_68, dtype=float)
+            if summary.upper_68 is not None:
+                dataset_payload['upper_68'] = np.asarray(summary.upper_68, dtype=float)
+            if summary.draws is not None:
+                dataset_payload['draws'] = np.asarray(summary.draws, dtype=float)
+        return payload
+
+    sidecar_data = getattr(analysis, '_persisted_fit_state_sidecar', {})
+    return dict(sidecar_data.get('predictive_datasets', {}))
+
+
+def _validate_posterior_payload(
+    analysis: object,
+    payload: dict[str, np.ndarray | None],
+) -> bool:
+    """Return whether posterior arrays match stored metadata."""
+    parameter_samples = payload.get('parameter_samples')
+    if parameter_samples is None:
+        if analysis.bayesian_result.has_posterior_samples.value:
+            log.warning('Bayesian fit-state expects posterior samples, but none are available.')
+        return False
+
+    parameter_samples = np.asarray(parameter_samples, dtype=float)
+    if parameter_samples.ndim != _POSTERIOR_SAMPLE_NDIM:
+        log.warning(
+            'Posterior parameter samples must have shape (n_draws, n_chains, n_parameters).'
+        )
+        return False
+
+    n_draws, n_chains, n_parameters = parameter_samples.shape
+    if not _posterior_manifest_counts_match(
+        analysis,
+        n_draws=n_draws,
+        n_chains=n_chains,
+        n_parameters=n_parameters,
+    ):
+        return False
+
+    return _posterior_aux_shapes_match(
+        payload,
+        n_draws=n_draws,
+        n_chains=n_chains,
+    )
+
+
+def _posterior_manifest_counts_match(
+    analysis: object,
+    *,
+    n_draws: int,
+    n_chains: int,
+    n_parameters: int,
+) -> bool:
+    """Return whether manifest counts match the sample shape."""
+    if analysis.bayesian_convergence.n_draws.value not in {0, n_draws}:
+        log.warning('Posterior sample draw count does not match bayesian_convergence.n_draws.')
+        return False
+    if analysis.bayesian_convergence.n_chains.value not in {0, n_chains}:
+        log.warning('Posterior sample chain count does not match bayesian_convergence.n_chains.')
+        return False
+    if analysis.bayesian_convergence.n_parameters.value not in {0, n_parameters}:
+        log.warning(
+            'Posterior sample parameter count does not match bayesian_convergence.n_parameters.'
+        )
+        return False
+
+    return True
+
+
+def _posterior_aux_shapes_match(
+    payload: dict[str, np.ndarray | None],
+    *,
+    n_draws: int,
+    n_chains: int,
+) -> bool:
+    """Return whether auxiliary arrays match the sample shape."""
+    log_posterior = payload.get('log_posterior')
+    if log_posterior is not None and np.asarray(log_posterior).shape != (n_draws, n_chains):
+        log.warning(
+            'Posterior log-posterior array does not match posterior sample draw and chain axes.'
+        )
+        return False
+
+    draw_index = payload.get('draw_index')
+    if draw_index is not None and np.asarray(draw_index).shape != (n_draws,):
+        log.warning('Posterior draw-index array does not match posterior sample draw count.')
+        return False
+
+    return True
+
+
+def _write_posterior_payload(handle: object, analysis: object) -> bool:
+    """Write canonical posterior arrays when they are available."""
+    payload = _posterior_payload_from_analysis(analysis)
+    if not _validate_posterior_payload(analysis, payload):
+        return False
+
+    parameter_samples = np.asarray(payload['parameter_samples'], dtype=float)
+    _create_dataset(handle, _POSTERIOR_PARAMETER_SAMPLES_PATH, parameter_samples)
+
+    log_posterior = payload.get('log_posterior')
+    if log_posterior is not None:
+        _create_dataset(handle, _POSTERIOR_LOG_POSTERIOR_PATH, np.asarray(log_posterior))
+
+    draw_index = payload.get('draw_index')
+    if draw_index is not None:
+        _create_dataset(handle, _POSTERIOR_DRAW_INDEX_PATH, np.asarray(draw_index))
+
+    return True
+
+
+def _write_distribution_caches(handle: object, analysis: object) -> bool:
+    """Write cached posterior distribution arrays for manifest rows."""
+    payload = _distribution_cache_payload(analysis)
+    wrote_any = False
+    for cache in analysis.bayesian_distribution_caches:
+        cache_data = payload.get(cache.param_unique_name.value)
+        if cache_data is None:
+            continue
+
+        x_values = np.asarray(cache_data.get('x'))
+        density_values = np.asarray(cache_data.get('density'))
+        n_grid = int(cache.n_grid.value)
+        if x_values.shape != (n_grid,) or density_values.shape != (n_grid,):
+            log.warning(
+                'Skipping Bayesian distribution cache with shape mismatch for '
+                f'{cache.param_unique_name.value!r}.'
+            )
+            continue
+
+        _create_dataset(handle, cache.x_path.value, x_values)
+        _create_dataset(handle, cache.density_path.value, density_values)
+        wrote_any = True
+
+    return wrote_any
+
+
+def _write_pair_caches(handle: object, analysis: object) -> bool:
+    """Write cached posterior pair-density arrays for manifest rows."""
+    payload = _pair_cache_payload(analysis)
+    wrote_any = False
+    for cache in analysis.bayesian_pair_caches:
+        cache_data = payload.get(cache.id.value)
+        if cache_data is None:
+            continue
+
+        x_values = np.asarray(cache_data.get('x'))
+        y_values = np.asarray(cache_data.get('y'))
+        density_values = np.asarray(cache_data.get('density'))
+        contour_levels = np.asarray(cache_data.get('contour_levels'))
+        n_grid_x = int(cache.n_grid_x.value)
+        n_grid_y = int(cache.n_grid_y.value)
+
+        valid_density_shape = density_values.shape in {
+            (n_grid_y, n_grid_x),
+            (n_grid_x, n_grid_y),
+        }
+        if (
+            x_values.shape != (n_grid_x,)
+            or y_values.shape != (n_grid_y,)
+            or not valid_density_shape
+        ):
+            log.warning(
+                f'Skipping Bayesian pair cache with shape mismatch for {cache.id.value!r}.'
+            )
+            continue
+
+        _create_dataset(handle, cache.x_path.value, x_values)
+        _create_dataset(handle, cache.y_path.value, y_values)
+        _create_dataset(handle, cache.density_path.value, density_values)
+        _create_dataset(handle, cache.contour_level_path.value, contour_levels)
+        wrote_any = True
+
+    return wrote_any
+
+
+def _write_predictive_datasets(handle: object, analysis: object) -> bool:
+    """Write cached posterior predictive arrays for manifest rows."""
+    payload = _predictive_payload(analysis)
+    wrote_any = False
+    for dataset in analysis.bayesian_predictive_datasets:
+        dataset_data = payload.get(dataset.experiment_name.value)
+        if dataset_data is None:
+            continue
+
+        x_values = np.asarray(dataset_data.get('x'))
+        best_sample_prediction = np.asarray(dataset_data.get('best_sample_prediction'))
+        n_x = int(dataset.n_x.value)
+        if x_values.shape != (n_x,) or best_sample_prediction.shape != (n_x,):
+            log.warning(
+                'Skipping Bayesian predictive dataset with shape mismatch for '
+                f'{dataset.experiment_name.value!r}.'
+            )
+            continue
+
+        _create_dataset(handle, dataset.x_path.value, x_values)
+        _create_dataset(
+            handle,
+            dataset.best_sample_prediction_path.value,
+            best_sample_prediction,
+        )
+
+        for field_name, path_value in (
+            ('lower_95', dataset.lower_95_path.value),
+            ('upper_95', dataset.upper_95_path.value),
+            ('lower_68', dataset.lower_68_path.value),
+            ('upper_68', dataset.upper_68_path.value),
+        ):
+            values = dataset_data.get(field_name)
+            if values is None or path_value is None:
+                continue
+            values_array = np.asarray(values)
+            if values_array.shape != (n_x,):
+                log.warning(
+                    'Skipping Bayesian predictive band with shape mismatch for '
+                    f'{dataset.experiment_name.value!r}:{field_name}.'
+                )
+                continue
+            _create_dataset(handle, path_value, values_array)
+
+        draws = dataset_data.get('draws')
+        if draws is not None and dataset.draws_path.value is not None:
+            draws_array = np.asarray(draws)
+            if draws_array.ndim != _PREDICTIVE_DRAWS_NDIM or draws_array.shape[1] != n_x:
+                log.warning(
+                    'Skipping Bayesian predictive draws with shape mismatch for '
+                    f'{dataset.experiment_name.value!r}.'
+                )
+            elif dataset.n_draws_cached.value not in {0, draws_array.shape[0]}:
+                log.warning(
+                    'Skipping Bayesian predictive draws whose draw count does not match '
+                    'the manifest metadata.'
+                )
+            else:
+                _create_dataset(handle, dataset.draws_path.value, draws_array)
+
+        wrote_any = True
+
+    return wrote_any
+
+
+def write_analysis_results_sidecar(
+    *,
+    analysis: object,
+    analysis_dir: Path,
+) -> None:
+    """
+    Write persisted Bayesian arrays to ``analysis/results.h5``.
+
+    Parameters
+    ----------
+    analysis : object
+        Analysis instance that owns fit-state categories and runtime fit
+        results.
+    analysis_dir : Path
+        The project ``analysis/`` directory.
+
+    Raises
+    ------
+    Exception
+        Propagated when sidecar writing fails after temporary-file
+        cleanup.
+    """
+    sidecar_path = _sidecar_path(analysis=analysis, analysis_dir=analysis_dir)
+    if not _should_use_sidecar(analysis):
+        _delete_stale_sidecar(sidecar_path)
+        return
+
+    import h5py  # noqa: PLC0415
+
+    analysis_dir.mkdir(parents=True, exist_ok=True)
+    with NamedTemporaryFile(
+        delete=False,
+        dir=analysis_dir,
+        prefix=f'{sidecar_path.stem}.',
+        suffix=sidecar_path.suffix,
+    ) as temporary_file:
+        temporary_path = Path(temporary_file.name)
+
+    try:
+        with h5py.File(temporary_path, 'w') as handle:
+            wrote_any = _write_posterior_payload(handle, analysis)
+            wrote_any = _write_distribution_caches(handle, analysis) or wrote_any
+            wrote_any = _write_pair_caches(handle, analysis) or wrote_any
+            wrote_any = _write_predictive_datasets(handle, analysis) or wrote_any
+    except Exception:
+        if temporary_path.exists():
+            temporary_path.unlink()
+        raise
+
+    if not wrote_any:
+        temporary_path.unlink()
+        _delete_stale_sidecar(sidecar_path)
+        return
+
+    temporary_path.replace(sidecar_path)
+
+
+def _read_posterior_payload(handle: object, analysis: object) -> dict[str, np.ndarray]:
+    """Read canonical posterior arrays from a sidecar file."""
+    parameter_samples = _read_dataset(handle, _POSTERIOR_PARAMETER_SAMPLES_PATH)
+    if parameter_samples is None:
+        return {}
+
+    payload: dict[str, np.ndarray] = {
+        'parameter_samples': np.asarray(parameter_samples, dtype=float),
+    }
+    log_posterior = _read_dataset(handle, _POSTERIOR_LOG_POSTERIOR_PATH)
+    if log_posterior is not None:
+        payload['log_posterior'] = np.asarray(log_posterior, dtype=float)
+    draw_index = _read_dataset(handle, _POSTERIOR_DRAW_INDEX_PATH)
+    if draw_index is not None:
+        payload['draw_index'] = np.asarray(draw_index)
+
+    if not _validate_posterior_payload(analysis, payload):
+        return {}
+    return payload
+
+
+def _read_distribution_caches(
+    handle: object, analysis: object
+) -> dict[str, dict[str, np.ndarray]]:
+    """Read cached posterior distribution arrays for manifest rows."""
+    payload: dict[str, dict[str, np.ndarray]] = {}
+    for cache in analysis.bayesian_distribution_caches:
+        x_values = _read_dataset(handle, cache.x_path.value)
+        density_values = _read_dataset(handle, cache.density_path.value)
+        if x_values is None or density_values is None:
+            continue
+        if x_values.shape != (int(cache.n_grid.value),) or density_values.shape != (
+            int(cache.n_grid.value),
+        ):
+            log.warning(
+                'Skipping restored Bayesian distribution cache with shape mismatch for '
+                f'{cache.param_unique_name.value!r}.'
+            )
+            continue
+        payload[cache.param_unique_name.value] = {
+            'x': np.asarray(x_values),
+            'density': np.asarray(density_values),
+        }
+    return payload
+
+
+def _read_pair_caches(handle: object, analysis: object) -> dict[str, dict[str, np.ndarray]]:
+    """Read cached posterior pair-density arrays for manifest rows."""
+    payload: dict[str, dict[str, np.ndarray]] = {}
+    for cache in analysis.bayesian_pair_caches:
+        x_values = _read_dataset(handle, cache.x_path.value)
+        y_values = _read_dataset(handle, cache.y_path.value)
+        density_values = _read_dataset(handle, cache.density_path.value)
+        contour_levels = _read_dataset(handle, cache.contour_level_path.value)
+        if any(value is None for value in (x_values, y_values, density_values, contour_levels)):
+            continue
+
+        n_grid_x = int(cache.n_grid_x.value)
+        n_grid_y = int(cache.n_grid_y.value)
+        valid_density_shape = density_values.shape in {
+            (n_grid_y, n_grid_x),
+            (n_grid_x, n_grid_y),
+        }
+        if (
+            x_values.shape != (n_grid_x,)
+            or y_values.shape != (n_grid_y,)
+            or not valid_density_shape
+        ):
+            log.warning(
+                'Skipping restored Bayesian pair cache with shape mismatch for '
+                f'{cache.id.value!r}.'
+            )
+            continue
+
+        payload[cache.id.value] = {
+            'x': np.asarray(x_values),
+            'y': np.asarray(y_values),
+            'density': np.asarray(density_values),
+            'contour_levels': np.asarray(contour_levels),
+        }
+    return payload
+
+
+def _read_predictive_datasets(
+    handle: object, analysis: object
+) -> dict[str, dict[str, np.ndarray]]:
+    """Read cached posterior predictive arrays for manifest rows."""
+    payload: dict[str, dict[str, np.ndarray]] = {}
+    for dataset in analysis.bayesian_predictive_datasets:
+        x_values = _read_dataset(handle, dataset.x_path.value)
+        best_sample_prediction = _read_dataset(handle, dataset.best_sample_prediction_path.value)
+        if x_values is None or best_sample_prediction is None:
+            continue
+
+        n_x = int(dataset.n_x.value)
+        if x_values.shape != (n_x,) or best_sample_prediction.shape != (n_x,):
+            log.warning(
+                'Skipping restored Bayesian predictive dataset with shape mismatch for '
+                f'{dataset.experiment_name.value!r}.'
+            )
+            continue
+
+        dataset_payload: dict[str, np.ndarray] = {
+            'x': np.asarray(x_values),
+            'best_sample_prediction': np.asarray(best_sample_prediction),
+        }
+
+        for field_name, path_value in (
+            ('lower_95', dataset.lower_95_path.value),
+            ('upper_95', dataset.upper_95_path.value),
+            ('lower_68', dataset.lower_68_path.value),
+            ('upper_68', dataset.upper_68_path.value),
+            ('draws', dataset.draws_path.value),
+        ):
+            if path_value is None:
+                continue
+            values = _read_dataset(handle, path_value)
+            if values is None:
+                continue
+            values_array = np.asarray(values)
+            if field_name == 'draws':
+                if values_array.ndim != _PREDICTIVE_DRAWS_NDIM or values_array.shape[1] != n_x:
+                    log.warning(
+                        'Skipping restored Bayesian predictive draws with shape mismatch for '
+                        f'{dataset.experiment_name.value!r}.'
+                    )
+                    continue
+            elif values_array.shape != (n_x,):
+                log.warning(
+                    'Skipping restored Bayesian predictive band with shape mismatch for '
+                    f'{dataset.experiment_name.value!r}:{field_name}.'
+                )
+                continue
+            dataset_payload[field_name] = values_array
+
+        payload[dataset.experiment_name.value] = dataset_payload
+    return payload
+
+
+def read_analysis_results_sidecar(
+    *,
+    analysis: object,
+    analysis_dir: Path,
+) -> None:
+    """
+    Read persisted Bayesian arrays from ``analysis/results.h5``.
+
+    Parameters
+    ----------
+    analysis : object
+        Analysis instance that owns fit-state categories.
+    analysis_dir : Path
+        The project ``analysis/`` directory.
+    """
+    analysis._persisted_fit_state_sidecar = {}
+    if not _should_use_sidecar(analysis):
+        return
+
+    sidecar_path = _sidecar_path(analysis=analysis, analysis_dir=analysis_dir)
+    if not sidecar_path.is_file():
+        log.warning(
+            'Expected Bayesian results sidecar is missing: '
+            f"'{sidecar_path}'. Restoring available CIF summaries only."
+        )
+        return
+
+    import h5py  # noqa: PLC0415
+
+    with h5py.File(sidecar_path, 'r') as handle:
+        sidecar_data: dict[str, object] = {}
+
+        posterior_payload = _read_posterior_payload(handle, analysis)
+        if posterior_payload:
+            sidecar_data['posterior'] = posterior_payload
+
+        distribution_caches = _read_distribution_caches(handle, analysis)
+        if distribution_caches:
+            sidecar_data['distribution_caches'] = distribution_caches
+
+        pair_caches = _read_pair_caches(handle, analysis)
+        if pair_caches:
+            sidecar_data['pair_caches'] = pair_caches
+
+        predictive_datasets = _read_predictive_datasets(handle, analysis)
+        if predictive_datasets:
+            sidecar_data['predictive_datasets'] = predictive_datasets
+
+    analysis._persisted_fit_state_sidecar = sidecar_data
diff --git a/src/easydiffraction/project/categories/display/__init__.py b/src/easydiffraction/project/categories/display/__init__.py
deleted file mode 100644
index d0862c2e0..000000000
--- a/src/easydiffraction/project/categories/display/__init__.py
+++ /dev/null
@@ -1,8 +0,0 @@
-# SPDX-FileCopyrightText: 2026 EasyScience contributors 
-# SPDX-License-Identifier: BSD-3-Clause
-"""Project display category exports."""
-
-from __future__ import annotations
-
-from easydiffraction.project.categories.display.default import Display
-from easydiffraction.project.categories.display.factory import DisplayFactory
diff --git a/src/easydiffraction/project/categories/display/default.py b/src/easydiffraction/project/categories/display/default.py
deleted file mode 100644
index d8287b09b..000000000
--- a/src/easydiffraction/project/categories/display/default.py
+++ /dev/null
@@ -1,117 +0,0 @@
-# SPDX-FileCopyrightText: 2026 EasyScience contributors 
-# SPDX-License-Identifier: BSD-3-Clause
-"""Project display 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 MembershipValidator
-from easydiffraction.core.variable import StringDescriptor
-from easydiffraction.display.plotting import Plotter
-from easydiffraction.display.plotting import PlotterEngineEnum
-from easydiffraction.display.tables import TableEngineEnum
-from easydiffraction.display.tables import TableRenderer
-from easydiffraction.io.cif.handler import CifHandler
-from easydiffraction.io.cif.parse import read_cif_str
-from easydiffraction.project.categories.display.factory import DisplayFactory
-
-
-@DisplayFactory.register
-class Display(CategoryItem):
-    """Display engine selection and access for a project."""
-
-    type_info = TypeInfo(
-        tag='default',
-        description='Project display category',
-    )
-
-    def __init__(self) -> None:
-        super().__init__()
-
-        self._plotter = Plotter()
-        self._tabler = TableRenderer.get()
-
-        self._plotter_type = StringDescriptor(
-            name='plotter_type',
-            description='Plot renderer backend type',
-            value_spec=AttributeSpec(
-                default=self._plotter.engine,
-                validator=MembershipValidator(
-                    allowed=[member.value for member in PlotterEngineEnum],
-                ),
-            ),
-            cif_handler=CifHandler(names=['_display.plotter_type']),
-        )
-        self._tabler_type = StringDescriptor(
-            name='tabler_type',
-            description='Table renderer backend type',
-            value_spec=AttributeSpec(
-                default=self._tabler.engine,
-                validator=MembershipValidator(
-                    allowed=[member.value for member in TableEngineEnum],
-                ),
-            ),
-            cif_handler=CifHandler(names=['_display.tabler_type']),
-        )
-
-        self._identity.category_code = 'display'
-
-    @property
-    def plotter_type(self) -> StringDescriptor:
-        """Plot renderer backend type."""
-        return self._plotter_type
-
-    @plotter_type.setter
-    def plotter_type(self, value: str) -> None:
-        self._plotter.engine = value
-        self._plotter_type.value = self._plotter.engine
-
-    @property
-    def tabler_type(self) -> StringDescriptor:
-        """Table renderer backend type."""
-        return self._tabler_type
-
-    @tabler_type.setter
-    def tabler_type(self, value: str) -> None:
-        self._tabler.engine = value
-        self._tabler_type.value = self._tabler.engine
-
-    @property
-    def plotter(self) -> Plotter:
-        """Live plotting facade bound to the owning project."""
-        parent = getattr(self, '_parent', None)
-        if parent is not None:
-            self._plotter._set_project(parent)
-        return self._plotter
-
-    @property
-    def tabler(self) -> TableRenderer:
-        """Live table-rendering facade."""
-        return self._tabler
-
-    def show_plotter_types(self) -> None:
-        """Print supported plot renderer backends."""
-        self.plotter.show_supported_engines()
-
-    def show_tabler_types(self) -> None:
-        """Print supported table renderer backends."""
-        self.tabler.show_supported_engines()
-
-    def from_cif(self, block: object, idx: int = 0) -> None:
-        """Populate this display category from a CIF block."""
-        del idx
-        plotter_type = read_cif_str(block, '_display.plotter_type')
-        if plotter_type is not None:
-            if plotter_type == self._plotter.engine:
-                self._plotter_type.value = plotter_type
-            else:
-                self.plotter_type = plotter_type
-
-        tabler_type = read_cif_str(block, '_display.tabler_type')
-        if tabler_type is not None:
-            if tabler_type == self._tabler.engine:
-                self._tabler_type.value = tabler_type
-            else:
-                self.tabler_type = tabler_type
diff --git a/src/easydiffraction/project/categories/info/__init__.py b/src/easydiffraction/project/categories/info/__init__.py
new file mode 100644
index 000000000..5b464da22
--- /dev/null
+++ b/src/easydiffraction/project/categories/info/__init__.py
@@ -0,0 +1,8 @@
+# SPDX-FileCopyrightText: 2026 EasyScience contributors 
+# SPDX-License-Identifier: BSD-3-Clause
+"""Project info category exports."""
+
+from __future__ import annotations
+
+from easydiffraction.project.categories.info.default import ProjectInfo
+from easydiffraction.project.categories.info.factory import ProjectInfoFactory
diff --git a/src/easydiffraction/project/categories/info/default.py b/src/easydiffraction/project/categories/info/default.py
new file mode 100644
index 000000000..ff730dcee
--- /dev/null
+++ b/src/easydiffraction/project/categories/info/default.py
@@ -0,0 +1,175 @@
+# SPDX-FileCopyrightText: 2026 EasyScience contributors 
+# SPDX-License-Identifier: BSD-3-Clause
+"""Project info category."""
+
+from __future__ import annotations
+
+import datetime
+import pathlib
+
+from easydiffraction.core.category import CategoryItem
+from easydiffraction.core.metadata import TypeInfo
+from easydiffraction.core.validation import AttributeSpec
+from easydiffraction.core.variable import StringDescriptor
+from easydiffraction.io.cif.handler import CifHandler
+from easydiffraction.io.cif.serialize import project_info_to_cif
+from easydiffraction.project.categories.info.factory import ProjectInfoFactory
+from easydiffraction.utils.logging import console
+from easydiffraction.utils.utils import render_cif
+
+_PROJECT_TIMESTAMP_FORMAT = '%d %b %Y %H:%M:%S'
+
+
+@ProjectInfoFactory.register
+class ProjectInfo(CategoryItem):
+    """Project metadata category."""
+
+    _category_code = 'project'
+
+    type_info = TypeInfo(
+        tag='default',
+        description='Project metadata category',
+    )
+
+    def __init__(
+        self,
+        name: str = 'untitled_project',
+        title: str = 'Untitled Project',
+        description: str = '',
+    ) -> None:
+        super().__init__()
+
+        created = datetime.datetime.now(tz=datetime.UTC)
+        last_modified = datetime.datetime.now(tz=datetime.UTC)
+
+        self._project_id = StringDescriptor(
+            name='id',
+            description='Project identifier',
+            value_spec=AttributeSpec(default=name),
+            cif_handler=CifHandler(names=['_project.id']),
+        )
+        self._title_descriptor = StringDescriptor(
+            name='title',
+            description='Project title',
+            value_spec=AttributeSpec(default=title),
+            cif_handler=CifHandler(names=['_project.title']),
+        )
+        self._description_descriptor = StringDescriptor(
+            name='description',
+            description='Project description',
+            value_spec=AttributeSpec(default=' '.join(description.split())),
+            cif_handler=CifHandler(names=['_project.description']),
+        )
+        self._created_descriptor = StringDescriptor(
+            name='created',
+            description='Project creation timestamp',
+            value_spec=AttributeSpec(default=created.strftime(_PROJECT_TIMESTAMP_FORMAT)),
+            cif_handler=CifHandler(names=['_project.created']),
+        )
+        self._last_modified_descriptor = StringDescriptor(
+            name='last_modified',
+            description='Project last-modified timestamp',
+            value_spec=AttributeSpec(default=last_modified.strftime(_PROJECT_TIMESTAMP_FORMAT)),
+            cif_handler=CifHandler(names=['_project.last_modified']),
+        )
+        self._path: pathlib.Path | None = None
+
+    @staticmethod
+    def _parse_timestamp(value: str) -> datetime.datetime:
+        """Parse project timestamp text from CIF storage format."""
+        return datetime.datetime.strptime(value, _PROJECT_TIMESTAMP_FORMAT).replace(
+            tzinfo=datetime.UTC,
+        )
+
+    @staticmethod
+    def _normalize_timestamp(value: datetime.datetime) -> datetime.datetime:
+        """Return timestamps as UTC-aware datetimes."""
+        if value.tzinfo is None:
+            return value.replace(tzinfo=datetime.UTC)
+        return value.astimezone(datetime.UTC)
+
+    @staticmethod
+    def _format_timestamp(value: datetime.datetime) -> str:
+        """Format a project timestamp for CIF storage."""
+        return ProjectInfo._normalize_timestamp(value).strftime(_PROJECT_TIMESTAMP_FORMAT)
+
+    @property
+    def unique_name(self) -> str:
+        """Unique name for GuardedBase diagnostics."""
+        return self.name
+
+    @property
+    def name(self) -> str:
+        """Return the project name."""
+        return self._project_id.value
+
+    @name.setter
+    def name(self, value: str) -> None:
+        self._project_id.value = value
+
+    @property
+    def title(self) -> str:
+        """Return the project title."""
+        return self._title_descriptor.value
+
+    @title.setter
+    def title(self, value: str) -> None:
+        self._title_descriptor.value = value
+
+    @property
+    def description(self) -> str:
+        """Return sanitized description with single spaces."""
+        return ' '.join(self._description_descriptor.value.split())
+
+    @description.setter
+    def description(self, value: str) -> None:
+        self._description_descriptor.value = ' '.join(value.split())
+
+    @property
+    def path(self) -> pathlib.Path | None:
+        """Return the project path as a Path object."""
+        return self._path
+
+    @path.setter
+    def path(self, value: object) -> None:
+        """Set the project directory path."""
+        self._path = pathlib.Path(value)
+
+    @property
+    def created(self) -> datetime.datetime:
+        """Return the creation timestamp."""
+        return self._parse_timestamp(self._created_descriptor.value)
+
+    def _set_created(self, value: datetime.datetime | str) -> None:
+        """Set the creation timestamp from runtime or CIF input."""
+        if isinstance(value, datetime.datetime):
+            self._created_descriptor.value = self._format_timestamp(value)
+            return
+        self._created_descriptor.value = value
+
+    @property
+    def last_modified(self) -> datetime.datetime:
+        """Return the last modified timestamp."""
+        return self._parse_timestamp(self._last_modified_descriptor.value)
+
+    def _set_last_modified(self, value: datetime.datetime | str) -> None:
+        """Set the last-modified timestamp from runtime or CIF input."""
+        if isinstance(value, datetime.datetime):
+            self._last_modified_descriptor.value = self._format_timestamp(value)
+            return
+        self._last_modified_descriptor.value = value
+
+    def update_last_modified(self) -> None:
+        """Update the last modified timestamp."""
+        self._set_last_modified(datetime.datetime.now())
+
+    @property
+    def as_cif(self) -> str:
+        """Export project metadata to CIF."""
+        return project_info_to_cif(self)
+
+    def show_as_cif(self) -> None:
+        """Pretty-print CIF via shared utilities."""
+        paragraph_title = f"Project 📦 '{self.name}' info as CIF"
+        console.paragraph(paragraph_title)
+        render_cif(self.as_cif)
diff --git a/src/easydiffraction/project/categories/info/factory.py b/src/easydiffraction/project/categories/info/factory.py
new file mode 100644
index 000000000..1a6bccb4f
--- /dev/null
+++ b/src/easydiffraction/project/categories/info/factory.py
@@ -0,0 +1,17 @@
+# SPDX-FileCopyrightText: 2026 EasyScience contributors 
+# SPDX-License-Identifier: BSD-3-Clause
+"""Factory for project info categories."""
+
+from __future__ import annotations
+
+from typing import ClassVar
+
+from easydiffraction.core.factory import FactoryBase
+
+
+class ProjectInfoFactory(FactoryBase):
+    """Create project info category instances."""
+
+    _default_rules: ClassVar[dict] = {
+        frozenset(): 'default',
+    }
diff --git a/src/easydiffraction/project/categories/rendering/__init__.py b/src/easydiffraction/project/categories/rendering/__init__.py
new file mode 100644
index 000000000..3e9889159
--- /dev/null
+++ b/src/easydiffraction/project/categories/rendering/__init__.py
@@ -0,0 +1,8 @@
+# SPDX-FileCopyrightText: 2026 EasyScience contributors 
+# SPDX-License-Identifier: BSD-3-Clause
+"""Project rendering category exports."""
+
+from __future__ import annotations
+
+from easydiffraction.project.categories.rendering.default import Rendering
+from easydiffraction.project.categories.rendering.factory import RenderingFactory
diff --git a/src/easydiffraction/project/categories/rendering/default.py b/src/easydiffraction/project/categories/rendering/default.py
new file mode 100644
index 000000000..179162a08
--- /dev/null
+++ b/src/easydiffraction/project/categories/rendering/default.py
@@ -0,0 +1,165 @@
+# SPDX-FileCopyrightText: 2026 EasyScience contributors 
+# SPDX-License-Identifier: BSD-3-Clause
+"""Project rendering 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 MembershipValidator
+from easydiffraction.core.variable import StringDescriptor
+from easydiffraction.display.plotting import Plotter
+from easydiffraction.display.plotting import PlotterEngineEnum
+from easydiffraction.display.tables import TableEngineEnum
+from easydiffraction.display.tables import TableRenderer
+from easydiffraction.io.cif.handler import CifHandler
+from easydiffraction.io.cif.parse import read_cif_str
+from easydiffraction.project.categories.rendering.factory import RenderingFactory
+from easydiffraction.utils.logging import console
+from easydiffraction.utils.utils import render_table
+
+AUTO_ENGINE = 'auto'
+CHART_ENGINE_OPTIONS = [AUTO_ENGINE, *[member.value for member in PlotterEngineEnum]]
+TABLE_ENGINE_OPTIONS = [AUTO_ENGINE, *[member.value for member in TableEngineEnum]]
+
+
+@RenderingFactory.register
+class Rendering(CategoryItem):
+    """Chart and table engine selection for a project."""
+
+    _category_code = 'rendering'
+
+    type_info = TypeInfo(
+        tag='default',
+        description='Project rendering category',
+    )
+
+    def __init__(self) -> None:
+        super().__init__()
+
+        self._plotter = Plotter()
+        self._tabler = TableRenderer.get()
+
+        # Persist symbolic "auto" so project.cif stays portable.
+        self._chart_engine = StringDescriptor(
+            name='chart_engine',
+            description='Chart renderer backend type',
+            value_spec=AttributeSpec(
+                default=AUTO_ENGINE,
+                validator=MembershipValidator(
+                    allowed=CHART_ENGINE_OPTIONS,
+                ),
+            ),
+            cif_handler=CifHandler(names=['_rendering.chart_engine']),
+        )
+        self._table_engine = StringDescriptor(
+            name='table_engine',
+            description='Table renderer backend type',
+            value_spec=AttributeSpec(
+                default=AUTO_ENGINE,
+                validator=MembershipValidator(
+                    allowed=TABLE_ENGINE_OPTIONS,
+                ),
+            ),
+            cif_handler=CifHandler(names=['_rendering.table_engine']),
+        )
+
+    @staticmethod
+    def _resolved_chart_engine(value: str) -> str:
+        if value == AUTO_ENGINE:
+            return PlotterEngineEnum.default().value
+        return value
+
+    @staticmethod
+    def _resolved_table_engine(value: str) -> str:
+        if value == AUTO_ENGINE:
+            return TableEngineEnum.default().value
+        return value
+
+    def _set_chart_engine(self, value: str) -> None:
+        if value not in CHART_ENGINE_OPTIONS:
+            self._plotter.engine = value
+            return
+
+        resolved_engine = self._resolved_chart_engine(value)
+        if self._plotter.engine != resolved_engine:
+            self._plotter.engine = resolved_engine
+        self._chart_engine.value = value
+
+    def _set_table_engine(self, value: str) -> None:
+        if value not in TABLE_ENGINE_OPTIONS:
+            self._tabler.engine = value
+            return
+
+        resolved_engine = self._resolved_table_engine(value)
+        if self._tabler.engine != resolved_engine:
+            self._tabler.engine = resolved_engine
+        self._table_engine.value = value
+
+    @property
+    def chart_engine(self) -> StringDescriptor:
+        """Chart renderer backend type."""
+        return self._chart_engine
+
+    @chart_engine.setter
+    def chart_engine(self, value: str) -> None:
+        self._set_chart_engine(value)
+
+    @property
+    def table_engine(self) -> StringDescriptor:
+        """Table renderer backend type."""
+        return self._table_engine
+
+    @table_engine.setter
+    def table_engine(self, value: str) -> None:
+        self._set_table_engine(value)
+
+    @property
+    def plotter(self) -> Plotter:
+        """Live plotting facade bound to the owning project."""
+        direct_parent = getattr(self, '_parent', None)
+        owner = direct_parent
+        while owner is not None and not hasattr(owner, 'structures'):
+            owner = getattr(owner, '_parent', None)
+        if owner is None:
+            owner = direct_parent
+        if owner is not None:
+            self._plotter._set_project(owner)
+        return self._plotter
+
+    @property
+    def tabler(self) -> TableRenderer:
+        """Live table-rendering facade."""
+        return self._tabler
+
+    def show_chart_engines(self) -> None:
+        """Print supported chart renderer backends."""
+        self.plotter.show_supported_engines()
+
+    def show_table_engines(self) -> None:
+        """Print supported table renderer backends."""
+        self.tabler.show_supported_engines()
+
+    def show_config(self) -> None:
+        """Print the current rendering configuration."""
+        console.paragraph('Current rendering configuration')
+        render_table(
+            columns_headers=['Setting', 'Value'],
+            columns_alignment=['left', 'left'],
+            columns_data=[
+                ['Chart engine', self.chart_engine.value],
+                ['Table engine', self.table_engine.value],
+            ],
+        )
+
+    def from_cif(self, block: object, idx: int = 0) -> None:
+        """Populate this rendering category from a CIF block."""
+        del idx
+        chart_engine = read_cif_str(block, '_rendering.chart_engine')
+        if chart_engine is not None:
+            self._set_chart_engine(chart_engine)
+
+        table_engine = read_cif_str(block, '_rendering.table_engine')
+        if table_engine is not None:
+            self._set_table_engine(table_engine)
diff --git a/src/easydiffraction/project/categories/rendering/factory.py b/src/easydiffraction/project/categories/rendering/factory.py
new file mode 100644
index 000000000..c2bdf7c5f
--- /dev/null
+++ b/src/easydiffraction/project/categories/rendering/factory.py
@@ -0,0 +1,17 @@
+# SPDX-FileCopyrightText: 2026 EasyScience contributors 
+# SPDX-License-Identifier: BSD-3-Clause
+"""Factory for project rendering categories."""
+
+from __future__ import annotations
+
+from typing import ClassVar
+
+from easydiffraction.core.factory import FactoryBase
+
+
+class RenderingFactory(FactoryBase):
+    """Create project rendering category instances."""
+
+    _default_rules: ClassVar[dict] = {
+        frozenset(): 'default',
+    }
diff --git a/src/easydiffraction/project/categories/verbosity/__init__.py b/src/easydiffraction/project/categories/verbosity/__init__.py
new file mode 100644
index 000000000..a7bfce5b3
--- /dev/null
+++ b/src/easydiffraction/project/categories/verbosity/__init__.py
@@ -0,0 +1,8 @@
+# SPDX-FileCopyrightText: 2026 EasyScience contributors 
+# SPDX-License-Identifier: BSD-3-Clause
+"""Project verbosity category exports."""
+
+from __future__ import annotations
+
+from easydiffraction.project.categories.verbosity.default import Verbosity
+from easydiffraction.project.categories.verbosity.factory import VerbosityFactory
diff --git a/src/easydiffraction/project/categories/verbosity/default.py b/src/easydiffraction/project/categories/verbosity/default.py
new file mode 100644
index 000000000..09b3821c6
--- /dev/null
+++ b/src/easydiffraction/project/categories/verbosity/default.py
@@ -0,0 +1,55 @@
+# SPDX-FileCopyrightText: 2026 EasyScience contributors 
+# SPDX-License-Identifier: BSD-3-Clause
+"""Project fit-output verbosity 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 MembershipValidator
+from easydiffraction.core.variable import StringDescriptor
+from easydiffraction.io.cif.handler import CifHandler
+from easydiffraction.project.categories.verbosity.factory import VerbosityFactory
+from easydiffraction.utils.enums import VerbosityEnum
+
+
+@VerbosityFactory.register
+class Verbosity(CategoryItem):
+    """Fit-output verbosity selection for a project."""
+
+    _category_code = 'verbosity'
+
+    type_info = TypeInfo(
+        tag='default',
+        description='Project verbosity category',
+    )
+
+    def __init__(self) -> None:
+        super().__init__()
+
+        self._fit = StringDescriptor(
+            name='fit',
+            description='Fitting process output verbosity',
+            value_spec=AttributeSpec(
+                default=VerbosityEnum.default().value,
+                validator=MembershipValidator(
+                    allowed=[member.value for member in VerbosityEnum],
+                ),
+            ),
+            cif_handler=CifHandler(names=['_verbosity.fit']),
+        )
+
+    @property
+    def fit(self) -> StringDescriptor:
+        """Fitting process output verbosity."""
+        return self._fit
+
+    @fit.setter
+    def fit(self, value: str) -> None:
+        self._fit.value = VerbosityEnum(value).value
+
+    @property
+    def as_cif(self) -> str:
+        """Return CIF representation of this verbosity category."""
+        return super().as_cif
diff --git a/src/easydiffraction/project/categories/verbosity/factory.py b/src/easydiffraction/project/categories/verbosity/factory.py
new file mode 100644
index 000000000..28b26bb97
--- /dev/null
+++ b/src/easydiffraction/project/categories/verbosity/factory.py
@@ -0,0 +1,17 @@
+# SPDX-FileCopyrightText: 2026 EasyScience contributors 
+# SPDX-License-Identifier: BSD-3-Clause
+"""Factory for project verbosity categories."""
+
+from __future__ import annotations
+
+from typing import ClassVar
+
+from easydiffraction.core.factory import FactoryBase
+
+
+class VerbosityFactory(FactoryBase):
+    """Create project verbosity category instances."""
+
+    _default_rules: ClassVar[dict] = {
+        frozenset(): 'default',
+    }
diff --git a/src/easydiffraction/project/display.py b/src/easydiffraction/project/display.py
new file mode 100644
index 000000000..5b760d997
--- /dev/null
+++ b/src/easydiffraction/project/display.py
@@ -0,0 +1,849 @@
+# SPDX-FileCopyrightText: 2026 EasyScience contributors 
+# SPDX-License-Identifier: BSD-3-Clause
+"""Project display facade grouping charts and reports."""
+
+from __future__ import annotations
+
+from contextlib import nullcontext
+from dataclasses import dataclass
+from typing import TYPE_CHECKING
+
+from easydiffraction.datablocks.experiment.item.base import intensity_category_for
+from easydiffraction.datablocks.experiment.item.enums import SampleFormEnum
+from easydiffraction.datablocks.experiment.item.enums import ScatteringTypeEnum
+from easydiffraction.display.plotting import PlotterEngineEnum
+from easydiffraction.display.plotting import PosteriorPairPlotStyleEnum
+from easydiffraction.display.plotting import _MeasVsCalcPlotOptions
+from easydiffraction.display.progress import ACTIVITY_LABEL_PROCESSING
+from easydiffraction.display.progress import activity_indicator
+from easydiffraction.utils.enums import VerbosityEnum
+from easydiffraction.utils.logging import log
+from easydiffraction.utils.utils import render_object_help
+from easydiffraction.utils.utils import render_table
+
+if TYPE_CHECKING:
+    from easydiffraction.project.project import Project
+
+
+_PATTERN_OPTION_DESCRIPTIONS: dict[str, str] = {
+    'auto': 'Show the most informative available pattern view.',
+    'measured': 'Measured diffraction intensities.',
+    'calculated': 'Calculated diffraction intensities.',
+    'background': 'Calculated background intensities when present.',
+    'residual': 'Measured minus calculated residuals when supported.',
+    'bragg': 'Bragg reflection tick marks when reflection data exists.',
+    'excluded': 'Excluded fitting regions when defined on the experiment.',
+    'uncertainty': 'Posterior predictive uncertainty bands when available.',
+}
+
+
+@dataclass(frozen=True, slots=True)
+class PatternOptionStatus:
+    """Availability metadata for one ``display.pattern`` option."""
+
+    name: str
+    description: str
+    available: bool
+    auto_included: bool
+    reason: str
+
+
+class ParameterDisplay:
+    """Parameter-table namespace under ``project.display``."""
+
+    def __init__(self, project: Project) -> None:
+        self._project = project
+
+    def all(self) -> None:
+        """Show all structure and experiment parameters."""
+        self._project.analysis.display.all_params()
+
+    def fittable(self) -> None:
+        """Show all currently fittable parameters."""
+        self._project.analysis.display.fittable_params()
+
+    def free(self) -> None:
+        """Show all currently free parameters."""
+        self._project.analysis.display.free_params()
+
+    def access(self) -> None:
+        """Show Python access paths for all parameters."""
+        self._project.analysis.display.how_to_access_parameters()
+
+    def cif_uids(self) -> None:
+        """Show CIF unique identifiers for all parameters."""
+        self._project.analysis.display.parameter_cif_uids()
+
+    def help(self) -> None:
+        """Print available parameter-display methods."""
+        render_object_help(self)
+
+
+class FitDisplay:
+    """Fit-report namespace under ``project.display``."""
+
+    def __init__(self, project: Project) -> None:
+        self._project = project
+
+    def results(self) -> None:
+        """Show the latest fit summary and fitted parameter table."""
+        self._project.analysis.display.fit_results()
+
+    def correlations(
+        self,
+        threshold: float | None = None,
+        precision: int = 2,
+        *,
+        max_parameters: int = 6,
+        show_diagonal: bool = True,
+    ) -> None:
+        """Show parameter correlations from the latest fit."""
+        self._project.rendering.plotter.plot_param_correlations(
+            threshold=threshold,
+            precision=precision,
+            max_parameters=max_parameters,
+            show_diagonal=show_diagonal,
+        )
+
+    def series(
+        self,
+        param: object | None = None,
+        versus: str | None = None,
+    ) -> None:
+        """
+        Plot fitted parameter(s) across sequential results.
+
+            Use a persisted diffrn path such as
+            ``'diffrn.ambient_temperature'`` for *versus*. When *param*
+            is provided, plot that single parameter. When *param* is
+            ``None`` (default), plot every fitted parameter, one after
+            another.
+        """
+        if param is None:
+            self._project.rendering.plotter.plot_all_param_series(versus=versus)
+        else:
+            self._project.rendering.plotter.plot_param_series(param=param, versus=versus)
+
+    def help(self) -> None:
+        """Print available fit-display methods."""
+        render_object_help(self)
+
+
+class PosteriorDisplay:
+    """Posterior-plot namespace under ``project.display``."""
+
+    def __init__(self, project: Project) -> None:
+        self._project = project
+
+    def _pairs_need_processing_indicator(
+        self,
+        *,
+        parameters: list[object] | None,
+    ) -> bool:
+        """
+        Return whether posterior pair plotting still needs processing.
+        """
+        if parameters is not None:
+            return True
+
+        analysis = self._project.analysis
+        sidecar_data = getattr(analysis, '_persisted_fit_state_sidecar', {})
+        pair_caches = sidecar_data.get('pair_caches', {})
+        return not (
+            analysis.bayesian_result.has_pair_cache.value
+            and len(analysis.bayesian_pair_caches) > 0
+            and bool(pair_caches)
+        )
+
+    def _predictive_needs_processing_indicator(
+        self,
+        *,
+        expt_name: str,
+        style: str,
+        x: object | None,
+    ) -> bool:
+        """Return whether predictive plotting still needs processing."""
+        analysis = self._project.analysis
+        sidecar_data = getattr(analysis, '_persisted_fit_state_sidecar', {})
+        predictive_datasets = sidecar_data.get('predictive_datasets', {})
+        if not (
+            analysis.bayesian_result.has_posterior_predictive.value
+            and bool(predictive_datasets)
+            and expt_name in predictive_datasets
+        ):
+            return True
+
+        experiment = self._project.experiments[expt_name]
+        plotter = self._project.rendering.plotter
+        _, x_axis_name, _, _, _ = plotter._resolve_x_axis(experiment.type, x)
+        require_draws = plotter.engine == PlotterEngineEnum.PLOTLY.value and style in {
+            'draws',
+            'band+draws',
+        }
+
+        matching_rows = [
+            row
+            for row in analysis.bayesian_predictive_datasets
+            if row.experiment_name.value == expt_name
+            and str(row.x_axis_name.value) == str(x_axis_name)
+        ]
+        if not matching_rows:
+            return True
+        if not require_draws:
+            return False
+        return not any(
+            row.draws_path.value is not None
+            and predictive_datasets[expt_name].get('draws') is not None
+            for row in matching_rows
+        )
+
+    def pairs(
+        self,
+        parameters: list[object] | None = None,
+        style: PosteriorPairPlotStyleEnum | str = PosteriorPairPlotStyleEnum.AUTO,
+        *,
+        threshold: float | None = None,
+        max_parameters: int = 6,
+    ) -> None:
+        """Plot posterior pair relationships for sampled parameters."""
+        indicator_context = (
+            activity_indicator(
+                ACTIVITY_LABEL_PROCESSING,
+                verbosity=VerbosityEnum(self._project.verbosity.fit.value),
+            )
+            if self._pairs_need_processing_indicator(parameters=parameters)
+            else nullcontext()
+        )
+        with indicator_context:
+            self._project.rendering.plotter.plot_posterior_pairs(
+                parameters=parameters,
+                style=style,
+                threshold=threshold,
+                max_parameters=max_parameters,
+            )
+
+    def distribution(self, param: object | None = None) -> None:
+        """
+        Plot posterior distributions for one or all free parameters.
+        """
+        plotter = self._project.rendering.plotter
+        if param is not None:
+            plotter.plot_param_distribution(param)
+            return
+
+        free_parameters = getattr(self._project, 'free_parameters', None)
+        if not free_parameters:
+            log.warning('No free parameters found.')
+            return
+
+        for free_parameter in free_parameters:
+            plotter.plot_param_distribution(free_parameter)
+
+    def predictive(
+        self,
+        expt_name: str,
+        style: str = 'band',
+        x_min: float | None = None,
+        x_max: float | None = None,
+        *,
+        show_residual: bool | None = None,
+        x: object | None = None,
+    ) -> None:
+        """Plot posterior predictive summaries for one experiment."""
+        indicator_context = (
+            activity_indicator(
+                ACTIVITY_LABEL_PROCESSING,
+                verbosity=VerbosityEnum(self._project.verbosity.fit.value),
+            )
+            if self._predictive_needs_processing_indicator(
+                expt_name=expt_name,
+                style=style,
+                x=x,
+            )
+            else nullcontext()
+        )
+        with indicator_context:
+            self._project.rendering.plotter.plot_posterior_predictive(
+                expt_name=expt_name,
+                style=style,
+                x_min=x_min,
+                x_max=x_max,
+                show_residual=show_residual,
+                x=x,
+            )
+
+    def help(self) -> None:
+        """Print available posterior-display methods."""
+        render_object_help(self)
+
+
+class ProjectDisplay:
+    """Grouped display facade exposed as ``project.display``."""
+
+    def __init__(self, project: Project) -> None:
+        self._project = project
+        self._parameters = ParameterDisplay(project)
+        self._fit = FitDisplay(project)
+        self._posterior = PosteriorDisplay(project)
+
+    @property
+    def parameters(self) -> ParameterDisplay:
+        """Parameter-table namespace."""
+        return self._parameters
+
+    @property
+    def fit(self) -> FitDisplay:
+        """Fit-report namespace."""
+        return self._fit
+
+    @property
+    def posterior(self) -> PosteriorDisplay:
+        """Posterior-plot namespace."""
+        return self._posterior
+
+    def help(self) -> None:
+        """Print display namespaces and methods."""
+        render_object_help(self)
+
+    def pattern(
+        self,
+        expt_name: str,
+        x_min: float | None = None,
+        x_max: float | None = None,
+        include: str | tuple[str, ...] = 'auto',
+        *,
+        x: object | None = None,
+    ) -> None:
+        """Show a pattern view for one experiment."""
+        normalized_include = self._normalize_include(include)
+        statuses = self._pattern_option_statuses(expt_name)
+
+        if normalized_include == ('auto',):
+            auto_include = self._auto_include(statuses)
+            if x is not None:
+                auto_include = tuple(option for option in auto_include if option != 'excluded')
+            if not auto_include:
+                msg = self._status_by_name(statuses, 'auto').reason
+                raise ValueError(msg)
+            if 'uncertainty' in auto_include:
+                indicator_context = (
+                    activity_indicator(
+                        ACTIVITY_LABEL_PROCESSING,
+                        verbosity=VerbosityEnum(self._project.verbosity.fit.value),
+                    )
+                    if self._posterior._predictive_needs_processing_indicator(
+                        expt_name=expt_name,
+                        style='band',
+                        x=x,
+                    )
+                    else nullcontext()
+                )
+                with indicator_context:
+                    self._project.rendering.plotter._plot_posterior_predictive_request(
+                        expt_name=expt_name,
+                        style='band',
+                        plot_options=_MeasVsCalcPlotOptions(
+                            x_min=x_min,
+                            x_max=x_max,
+                            show_residual=True if 'residual' in auto_include else None,
+                            show_background='background' in auto_include,
+                            show_bragg='bragg' in auto_include,
+                            show_excluded='excluded' in auto_include,
+                            x=x,
+                        ),
+                    )
+                return
+            self._show_point_estimate_pattern(
+                expt_name=expt_name,
+                x_min=x_min,
+                x_max=x_max,
+                include=auto_include,
+                statuses=statuses,
+                x=x,
+            )
+            return
+
+        self._validate_requested_include(statuses, normalized_include)
+        if x is not None and 'excluded' in normalized_include:
+            msg = "Excluded-region overlays currently require the experiment's default x-axis."
+            raise ValueError(msg)
+
+        if 'uncertainty' in normalized_include:
+            indicator_context = (
+                activity_indicator(
+                    ACTIVITY_LABEL_PROCESSING,
+                    verbosity=VerbosityEnum(self._project.verbosity.fit.value),
+                )
+                if self._posterior._predictive_needs_processing_indicator(
+                    expt_name=expt_name,
+                    style='band',
+                    x=x,
+                )
+                else nullcontext()
+            )
+            with indicator_context:
+                self._project.rendering.plotter._plot_posterior_predictive_request(
+                    expt_name=expt_name,
+                    style='band',
+                    plot_options=_MeasVsCalcPlotOptions(
+                        x_min=x_min,
+                        x_max=x_max,
+                        show_residual=True if 'residual' in normalized_include else None,
+                        show_background='background' in normalized_include,
+                        show_bragg='bragg' in normalized_include,
+                        show_excluded='excluded' in normalized_include,
+                        x=x,
+                    ),
+                )
+            return
+
+        self._show_point_estimate_pattern(
+            expt_name=expt_name,
+            x_min=x_min,
+            x_max=x_max,
+            include=normalized_include,
+            statuses=statuses,
+            x=x,
+        )
+
+    def show_pattern_options(self, expt_name: str) -> None:
+        """Show available ``pattern(include=...)`` options."""
+        statuses = self._pattern_option_statuses(expt_name)
+        render_table(
+            columns_headers=['Option', 'Description', 'Available', 'Auto', 'Reason'],
+            columns_alignment=['left', 'left', 'center', 'center', 'left'],
+            columns_data=[
+                [
+                    status.name,
+                    status.description,
+                    'yes' if status.available else 'no',
+                    'yes' if status.auto_included else 'no',
+                    status.reason or '-',
+                ]
+                for status in statuses
+            ],
+        )
+
+    @staticmethod
+    def _normalize_include(include: str | tuple[str, ...]) -> tuple[str, ...]:
+        """Validate and normalize a ``pattern(include=...)`` value."""
+        values = (include,) if isinstance(include, str) else include
+        if not values:
+            msg = 'include must contain at least one option.'
+            raise ValueError(msg)
+
+        normalized = tuple(dict.fromkeys(values))
+        unknown = [value for value in normalized if value not in _PATTERN_OPTION_DESCRIPTIONS]
+        if unknown:
+            msg = f'Unknown pattern include option(s): {unknown}.'
+            raise ValueError(msg)
+        if 'auto' in normalized and len(normalized) > 1:
+            msg = "include='auto' cannot be combined with other options."
+            raise ValueError(msg)
+        return normalized
+
+    @staticmethod
+    def _status_by_name(
+        statuses: list[PatternOptionStatus],
+        option_name: str,
+    ) -> PatternOptionStatus:
+        """Return one pattern option status by name."""
+        for status in statuses:
+            if status.name == option_name:
+                return status
+        msg = f'Unknown pattern option: {option_name}.'
+        raise ValueError(msg)
+
+    @staticmethod
+    def _with_available_options(
+        status_by_name: dict[str, PatternOptionStatus],
+        required: tuple[str, ...],
+        optional: tuple[str, ...],
+    ) -> tuple[str, ...]:
+        """Return required options plus available optional ones."""
+        include = list(required)
+        include.extend(
+            option_name for option_name in optional if status_by_name[option_name].available
+        )
+        return tuple(include)
+
+    @classmethod
+    def _auto_include(
+        cls,
+        statuses: list[PatternOptionStatus],
+    ) -> tuple[str, ...]:
+        """Return the effective include tuple for ``include='auto'``."""
+        status_by_name = {status.name: status for status in statuses}
+        optional_point_estimate = ('background', 'residual', 'bragg', 'excluded')
+
+        if status_by_name['uncertainty'].available:
+            return cls._with_available_options(
+                status_by_name,
+                ('measured', 'calculated', 'uncertainty'),
+                optional_point_estimate,
+            )
+        if status_by_name['measured'].available and status_by_name['calculated'].available:
+            return cls._with_available_options(
+                status_by_name,
+                ('measured', 'calculated'),
+                optional_point_estimate,
+            )
+        if status_by_name['measured'].available:
+            return cls._with_available_options(
+                status_by_name,
+                ('measured',),
+                ('excluded',),
+            )
+        if status_by_name['calculated'].available:
+            return cls._with_available_options(
+                status_by_name,
+                ('calculated',),
+                ('excluded',),
+            )
+        return ()
+
+    @classmethod
+    def _validate_requested_include(
+        cls,
+        statuses: list[PatternOptionStatus],
+        include: tuple[str, ...],
+    ) -> None:
+        """
+        Raise a clear error when a requested include is unavailable.
+        """
+        status_by_name = {status.name: status for status in statuses}
+        unavailable = [
+            option_name
+            for option_name in include
+            if option_name != 'auto' and not status_by_name[option_name].available
+        ]
+        if unavailable:
+            option_name = unavailable[0]
+            msg = status_by_name[option_name].reason
+            raise ValueError(msg)
+
+        include_set = set(include)
+        if 'background' in include_set and not {'measured', 'calculated'}.issubset(include_set):
+            msg = 'background requires both measured and calculated data in the same view.'
+            raise ValueError(msg)
+        if 'bragg' in include_set and not {'measured', 'calculated'}.issubset(include_set):
+            msg = 'bragg requires both measured and calculated data in the same view.'
+            raise ValueError(msg)
+        if 'residual' in include_set and not {'measured', 'calculated'}.issubset(include_set):
+            msg = 'residual requires both measured and calculated data in the same view.'
+            raise ValueError(msg)
+        if 'excluded' in include_set and not include_set.intersection({
+            'measured',
+            'calculated',
+            'uncertainty',
+        }):
+            msg = 'excluded requires measured, calculated, or uncertainty data in the same view.'
+            raise ValueError(msg)
+
+    def _show_point_estimate_pattern(
+        self,
+        *,
+        expt_name: str,
+        x_min: float | None,
+        x_max: float | None,
+        include: tuple[str, ...],
+        statuses: list[PatternOptionStatus],
+        x: object | None,
+    ) -> None:
+        """
+        Dispatch a point-estimate pattern view to the live plotter.
+        """
+        self._validate_requested_include(statuses, include)
+        include_set = set(include)
+        if include_set == {'measured'}:
+            self._project.rendering.plotter.plot_meas(
+                expt_name=expt_name,
+                x_min=x_min,
+                x_max=x_max,
+                x=x,
+                show_excluded=False,
+            )
+            return
+        if include_set == {'measured', 'excluded'}:
+            self._project.rendering.plotter.plot_meas(
+                expt_name=expt_name,
+                x_min=x_min,
+                x_max=x_max,
+                x=x,
+                show_excluded=True,
+            )
+            return
+        if include_set == {'calculated'}:
+            self._project.rendering.plotter.plot_calc(
+                expt_name=expt_name,
+                x_min=x_min,
+                x_max=x_max,
+                x=x,
+                show_excluded=False,
+            )
+            return
+        if include_set == {'calculated', 'excluded'}:
+            self._project.rendering.plotter.plot_calc(
+                expt_name=expt_name,
+                x_min=x_min,
+                x_max=x_max,
+                x=x,
+                show_excluded=True,
+            )
+            return
+        if {'measured', 'calculated'}.issubset(include_set):
+            self._project.rendering.plotter._plot_meas_vs_calc_request(
+                expt_name=expt_name,
+                plot_options=_MeasVsCalcPlotOptions(
+                    x_min=x_min,
+                    x_max=x_max,
+                    show_residual='residual' in include_set,
+                    show_background='background' in include_set,
+                    show_bragg='bragg' in include_set,
+                    show_excluded='excluded' in include_set,
+                    x=x,
+                ),
+            )
+            return
+
+        msg = (
+            'Point-estimate pattern views currently support include values '
+            "'measured', 'calculated', or combinations containing both."
+        )
+        raise ValueError(msg)
+
+    def _pattern_option_statuses(self, expt_name: str) -> list[PatternOptionStatus]:
+        """Return availability details for the requested experiment."""
+        self._project.rendering.plotter._update_project_categories(expt_name)
+        experiment = self._project.experiments[expt_name]
+        pattern = intensity_category_for(experiment)
+        sample_form = experiment.type.sample_form.value
+        scattering_type = experiment.type.scattering_type.value
+        has_linked_structure = self._has_linked_structure_for_calculation(experiment)
+
+        measured_available = self._has_nonempty_value(getattr(pattern, 'intensity_meas', None))
+        calculated_available = has_linked_structure and self._has_nonempty_value(
+            getattr(pattern, 'intensity_calc', None)
+        )
+        background_available = (
+            sample_form == SampleFormEnum.POWDER.value
+            and scattering_type == ScatteringTypeEnum.BRAGG.value
+            and measured_available
+            and calculated_available
+            and self._has_nonempty_value(getattr(experiment, 'background', None))
+            and self._has_nonempty_value(getattr(pattern, 'intensity_bkg', None))
+        )
+        bragg_available = (
+            measured_available
+            and calculated_available
+            and sample_form == SampleFormEnum.POWDER.value
+            and scattering_type == ScatteringTypeEnum.BRAGG.value
+            and self._has_nonempty_value(getattr(experiment, 'refln', None))
+        )
+        residual_available = (
+            sample_form == SampleFormEnum.POWDER.value
+            and measured_available
+            and calculated_available
+        )
+        has_excluded_regions = self._has_nonempty_value(
+            getattr(experiment, 'excluded_regions', None)
+        )
+        uncertainty_available, uncertainty_reason = self._uncertainty_status(
+            measured_available=measured_available,
+            sample_form=sample_form,
+            scattering_type=scattering_type,
+        )
+
+        auto_include = self._auto_include([
+            PatternOptionStatus(
+                name='measured',
+                description=_PATTERN_OPTION_DESCRIPTIONS['measured'],
+                available=measured_available,
+                auto_included=False,
+                reason='',
+            ),
+            PatternOptionStatus(
+                name='calculated',
+                description=_PATTERN_OPTION_DESCRIPTIONS['calculated'],
+                available=calculated_available,
+                auto_included=False,
+                reason='',
+            ),
+            PatternOptionStatus(
+                name='background',
+                description=_PATTERN_OPTION_DESCRIPTIONS['background'],
+                available=background_available,
+                auto_included=False,
+                reason='',
+            ),
+            PatternOptionStatus(
+                name='residual',
+                description=_PATTERN_OPTION_DESCRIPTIONS['residual'],
+                available=residual_available,
+                auto_included=False,
+                reason='',
+            ),
+            PatternOptionStatus(
+                name='bragg',
+                description=_PATTERN_OPTION_DESCRIPTIONS['bragg'],
+                available=bragg_available,
+                auto_included=False,
+                reason='',
+            ),
+            PatternOptionStatus(
+                name='excluded',
+                description=_PATTERN_OPTION_DESCRIPTIONS['excluded'],
+                available=has_excluded_regions,
+                auto_included=False,
+                reason='',
+            ),
+            PatternOptionStatus(
+                name='uncertainty',
+                description=_PATTERN_OPTION_DESCRIPTIONS['uncertainty'],
+                available=uncertainty_available,
+                auto_included=False,
+                reason='',
+            ),
+        ])
+
+        return [
+            PatternOptionStatus(
+                name='auto',
+                description=_PATTERN_OPTION_DESCRIPTIONS['auto'],
+                available=bool(auto_include),
+                auto_included=True,
+                reason='' if auto_include else 'No supported pattern content is available.',
+            ),
+            PatternOptionStatus(
+                name='measured',
+                description=_PATTERN_OPTION_DESCRIPTIONS['measured'],
+                available=measured_available,
+                auto_included='measured' in auto_include,
+                reason='' if measured_available else 'Measured intensities are unavailable.',
+            ),
+            PatternOptionStatus(
+                name='calculated',
+                description=_PATTERN_OPTION_DESCRIPTIONS['calculated'],
+                available=calculated_available,
+                auto_included='calculated' in auto_include,
+                reason='' if calculated_available else 'Calculated intensities are unavailable.',
+            ),
+            PatternOptionStatus(
+                name='background',
+                description=_PATTERN_OPTION_DESCRIPTIONS['background'],
+                available=background_available,
+                auto_included='background' in auto_include,
+                reason=''
+                if background_available
+                else (
+                    'Background display currently requires powder Bragg '
+                    'measured and calculated data plus defined '
+                    'background points.'
+                ),
+            ),
+            PatternOptionStatus(
+                name='residual',
+                description=_PATTERN_OPTION_DESCRIPTIONS['residual'],
+                available=residual_available,
+                auto_included='residual' in auto_include,
+                reason=''
+                if residual_available
+                else ('Residuals currently require powder measured and calculated data.'),
+            ),
+            PatternOptionStatus(
+                name='bragg',
+                description=_PATTERN_OPTION_DESCRIPTIONS['bragg'],
+                available=bragg_available,
+                auto_included='bragg' in auto_include,
+                reason=''
+                if bragg_available
+                else (
+                    'Bragg tick marks require powder Bragg measured and '
+                    'calculated data with reflection rows.'
+                ),
+            ),
+            PatternOptionStatus(
+                name='excluded',
+                description=_PATTERN_OPTION_DESCRIPTIONS['excluded'],
+                available=has_excluded_regions,
+                auto_included='excluded' in auto_include,
+                reason=''
+                if has_excluded_regions
+                else ('No excluded regions are defined for this experiment.'),
+            ),
+            PatternOptionStatus(
+                name='uncertainty',
+                description=_PATTERN_OPTION_DESCRIPTIONS['uncertainty'],
+                available=uncertainty_available,
+                auto_included='uncertainty' in auto_include,
+                reason=uncertainty_reason,
+            ),
+        ]
+
+    @staticmethod
+    def _has_nonempty_value(value: object | None) -> bool:
+        """Return whether a plotting input has content."""
+        if value is None:
+            return False
+
+        try:
+            return len(value) > 0
+        except TypeError:
+            return True
+
+    def _has_linked_structure_for_calculation(self, experiment: object) -> bool:
+        """Return whether the experiment links to a known structure."""
+        structure_names = set(getattr(self._project.structures, 'names', ()))
+
+        linked_phases = getattr(experiment, 'linked_phases', None)
+        if self._has_nonempty_value(linked_phases):
+            for linked_phase in linked_phases:
+                identity = getattr(linked_phase, '_identity', None)
+                category_entry_name = getattr(identity, 'category_entry_name', None)
+                if category_entry_name in structure_names:
+                    return True
+
+        linked_crystal = getattr(experiment, 'linked_crystal', None)
+        linked_crystal_id = getattr(getattr(linked_crystal, 'id', None), 'value', None)
+        return linked_crystal_id in structure_names
+
+    def _uncertainty_status(
+        self,
+        *,
+        measured_available: bool,
+        sample_form: str,
+        scattering_type: str,
+    ) -> tuple[bool, str]:
+        """
+        Return whether posterior predictive uncertainty is available.
+        """
+        if not measured_available:
+            return False, 'Uncertainty bands require measured data.'
+
+        supported_sample_form = sample_form == SampleFormEnum.POWDER.value or (
+            sample_form == SampleFormEnum.SINGLE_CRYSTAL.value
+            and scattering_type == ScatteringTypeEnum.BRAGG.value
+        )
+        if not supported_sample_form:
+            return (
+                False,
+                ('Posterior predictive pattern views are unavailable for this experiment type.'),
+            )
+
+        fit_results = getattr(self._project.analysis, 'fit_results', None)
+        if fit_results is None:
+            return False, 'No fit results are available.'
+
+        posterior_predictive = getattr(fit_results, 'posterior_predictive', None)
+        if not posterior_predictive:
+            return False, 'Posterior predictive data is unavailable.'
+
+        active_chart_engine = getattr(self._project.rendering.plotter, 'engine', None)
+        if active_chart_engine is None:
+            chart_engine = getattr(self._project.rendering, 'chart_engine', None)
+            active_chart_engine = getattr(chart_engine, 'value', None)
+
+        if active_chart_engine != PlotterEngineEnum.PLOTLY.value:
+            return False, 'Uncertainty bands currently require the Plotly chart engine.'
+
+        return True, ''
diff --git a/src/easydiffraction/project/project.py b/src/easydiffraction/project/project.py
index 7dc42cf6d..9b89eb500 100644
--- a/src/easydiffraction/project/project.py
+++ b/src/easydiffraction/project/project.py
@@ -5,7 +5,10 @@
 from __future__ import annotations
 
 import pathlib
+import shutil
 import tempfile
+from typing import TYPE_CHECKING
+from typing import ClassVar
 
 from typeguard import typechecked
 from varname import varname
@@ -14,16 +17,27 @@
 from easydiffraction.core.guard import GuardedBase
 from easydiffraction.datablocks.experiment.collection import Experiments
 from easydiffraction.datablocks.structure.collection import Structures
+from easydiffraction.io.cif.serialize import analysis_from_cif
+from easydiffraction.io.cif.serialize import project_config_from_cif
 from easydiffraction.io.cif.serialize import project_config_to_cif
 from easydiffraction.io.cif.serialize import project_to_cif
-from easydiffraction.project.categories.display import Display
-from easydiffraction.project.categories.display import DisplayFactory
-from easydiffraction.project.project_info import ProjectInfo
+from easydiffraction.io.results_sidecar import read_analysis_results_sidecar
+from easydiffraction.io.results_sidecar import write_analysis_results_sidecar
+from easydiffraction.project.display import ProjectDisplay
+from easydiffraction.project.project_config import ProjectConfig
 from easydiffraction.summary.summary import Summary
 from easydiffraction.utils.enums import VerbosityEnum
+from easydiffraction.utils.environment import resolve_artifact_path
 from easydiffraction.utils.logging import console
 from easydiffraction.utils.logging import log
 
+if TYPE_CHECKING:
+    from collections.abc import Callable
+
+    from easydiffraction.project.categories.rendering import Rendering
+    from easydiffraction.project.categories.verbosity import Verbosity
+    from easydiffraction.project.project_info import ProjectInfo
+
 
 def _apply_csv_row_to_params(
     row: object,
@@ -58,6 +72,108 @@ def _apply_csv_row_to_params(
             param_map[col_name].value = float(row[col_name])
 
 
+def _apply_csv_row_to_diffrn(
+    row: object,
+    columns: object,
+    experiment: object,
+) -> None:
+    """
+    Override ``experiment.diffrn`` values from a CSV row.
+
+    Parameters
+    ----------
+    row : object
+        A pandas Series representing one CSV row.
+    columns : object
+        The DataFrame column index.
+    experiment : object
+        Live experiment whose ``diffrn`` descriptors are updated.
+    """
+    import pandas as pd  # noqa: PLC0415
+
+    from easydiffraction.core.variable import NumericDescriptor  # noqa: PLC0415
+
+    for col_name in columns:
+        if not col_name.startswith('diffrn.') or pd.isna(row[col_name]):
+            continue
+
+        field_name = col_name.removeprefix('diffrn.')
+        descriptor = getattr(experiment.diffrn, field_name, None)
+        if isinstance(descriptor, NumericDescriptor):
+            descriptor.value = float(row[col_name])
+
+
+def _resolve_data_path_from_results_csv(
+    project_path: pathlib.Path,
+    file_path: object,
+) -> pathlib.Path | None:
+    """Resolve a CSV-stored data path against the project path."""
+    if not isinstance(file_path, str) or not file_path:
+        return None
+
+    path = pathlib.Path(file_path)
+    if path.is_absolute():
+        return path
+    return project_path / path
+
+
+def _load_cif_directory(
+    cif_dir: pathlib.Path,
+    add_from_cif_path: Callable[[str], None],
+) -> None:
+    """Load all CIF files from one directory using the given loader."""
+    if not cif_dir.is_dir():
+        return
+
+    for cif_file in sorted(cif_dir.glob('*.cif')):
+        add_from_cif_path(str(cif_file))
+
+
+def _create_loading_project(project_cls: type[Project]) -> Project:
+    """Create a project instance while suppressing varname lookup."""
+    project_cls._loading = True
+    try:
+        return project_cls()
+    finally:
+        project_cls._loading = False
+
+
+def _load_project_info(project: Project, project_path: pathlib.Path) -> None:
+    """
+    Restore project configuration from ``project.cif`` when present.
+    """
+    project_cif_path = project_path / 'project.cif'
+    if project_cif_path.is_file():
+        project_config_from_cif(project, project_cif_path.read_text())
+
+
+def _resolved_analysis_cif_path(project_path: pathlib.Path) -> pathlib.Path | None:
+    """Return the preferred analysis CIF path for a saved project."""
+    analysis_cif_path = project_path / 'analysis' / 'analysis.cif'
+    if analysis_cif_path.is_file():
+        return analysis_cif_path
+
+    analysis_cif_path = project_path / 'analysis.cif'
+    if analysis_cif_path.is_file():
+        return analysis_cif_path
+    return None
+
+
+def _load_project_analysis(project: Project, project_path: pathlib.Path) -> None:
+    """Restore analysis categories and sidecar state from disk."""
+    analysis_cif_path = _resolved_analysis_cif_path(project_path)
+    if analysis_cif_path is None:
+        return
+
+    analysis_from_cif(project._analysis, analysis_cif_path.read_text())
+    read_analysis_results_sidecar(
+        analysis=project._analysis,
+        analysis_dir=analysis_cif_path.parent,
+    )
+    if project._analysis._has_persisted_fit_state():
+        project._analysis._restore_live_parameter_state(project._build_parameter_map())
+
+
 class Project(GuardedBase):
     """
     Central API for managing a diffraction data analysis project.
@@ -70,6 +186,7 @@ class Project(GuardedBase):
     # ------------------------------------------------------------------
     # Class-level sentinel: True while load() is constructing a project.
     _loading: bool = False
+    _current_project: ClassVar[Project | None] = None
 
     def __init__(
         self,
@@ -79,16 +196,26 @@ def __init__(
     ) -> None:
         super().__init__()
 
-        self._info: ProjectInfo = ProjectInfo(name, title, description)
+        self._config = ProjectConfig(name, title, description)
+        object.__setattr__(self, '_info', self._config.info)
         self._structures = Structures()
         self._experiments = Experiments()
-        self._display = DisplayFactory.create('default')
-        self._display._parent = self
+        object.__setattr__(self, '_rendering', self._config.rendering)
+        object.__setattr__(self, '_verbosity', self._config.verbosity)
+        self._display = ProjectDisplay(self)
         self._analysis = Analysis(self)
         self._summary = Summary(self)
         self._saved = False
         self._varname = 'project' if type(self)._loading else varname()
-        self._verbosity: VerbosityEnum = VerbosityEnum.FULL
+        type(self)._current_project = self
+
+    @classmethod
+    def current_project_path(cls) -> pathlib.Path | None:
+        """Return the saved path of the current project, if any."""
+        current_project = cls._current_project
+        if current_project is None:
+            return None
+        return current_project.info.path
 
     # ------------------------------------------------------------------
     # Dunder methods
@@ -152,8 +279,13 @@ def experiments(self, experiments: Experiments) -> None:
         self._experiments = experiments
 
     @property
-    def display(self) -> Display:
-        """Display configuration and facades bound to the project."""
+    def rendering(self) -> Rendering:
+        """Rendering configuration bound to the project."""
+        return self._rendering
+
+    @property
+    def display(self) -> ProjectDisplay:
+        """Current display entry-point bound to the project."""
         return self._display
 
     @property
@@ -171,6 +303,11 @@ def parameters(self) -> list:
         """Return parameters from all structures and experiments."""
         return self.structures.parameters + self.experiments.parameters
 
+    @property
+    def free_parameters(self) -> list:
+        """Return free parameters from structures and experiments."""
+        return self.structures.free_parameters + self.experiments.free_parameters
+
     @property
     def as_cif(self) -> str:
         """Export whole project as CIF text."""
@@ -178,21 +315,14 @@ def as_cif(self) -> str:
         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
+    def verbosity(self) -> Verbosity:
+        """Verbosity configuration bound to the project."""
+        return self._verbosity
 
     @verbosity.setter
     def verbosity(self, value: str) -> None:
         """
-        Set project-wide console output verbosity.
+        Set fitting process output verbosity.
 
         Parameters
         ----------
@@ -200,7 +330,7 @@ def verbosity(self, value: str) -> None:
             ``'full'`` for multi-line output, ``'short'`` for one-line
             status messages, or ``'silent'`` for no output.
         """
-        self._verbosity = VerbosityEnum(value)
+        self._verbosity.fit = VerbosityEnum(value).value
 
     # ------------------------------------------
     #  Project File I/O
@@ -232,52 +362,19 @@ def load(cls, dir_path: str) -> Project:
         FileNotFoundError
             If *dir_path* does not exist.
         """
-        from easydiffraction.io.cif.serialize import analysis_from_cif  # noqa: PLC0415
-        from easydiffraction.io.cif.serialize import project_config_from_cif  # noqa: PLC0415
-
         project_path = pathlib.Path(dir_path)
         if not project_path.is_dir():
             msg = f"Project directory not found: '{dir_path}'"
             raise FileNotFoundError(msg)
 
-        # Create a minimal project.
-        # Use _loading sentinel to skip varname() inside __init__.
-        cls._loading = True
-        try:
-            project = cls()
-        finally:
-            cls._loading = False
+        project = _create_loading_project(cls)
         project._saved = True
 
-        # 1. Load project info
-        project_cif_path = project_path / 'project.cif'
-        if project_cif_path.is_file():
-            cif_text = project_cif_path.read_text()
-            project_config_from_cif(project, cif_text)
-
-        project._info.path = project_path
-
-        # 2. Load structures
-        structures_dir = project_path / 'structures'
-        if structures_dir.is_dir():
-            for cif_file in sorted(structures_dir.glob('*.cif')):
-                project._structures.add_from_cif_path(str(cif_file))
-
-        # 3. Load experiments
-        experiments_dir = project_path / 'experiments'
-        if experiments_dir.is_dir():
-            for cif_file in sorted(experiments_dir.glob('*.cif')):
-                project._experiments.add_from_cif_path(str(cif_file))
-
-        # 4. Load analysis
-        #    Check analysis/analysis.cif first (future layout), then
-        #    fall back to analysis.cif at root (current layout).
-        analysis_cif_path = project_path / 'analysis' / 'analysis.cif'
-        if not analysis_cif_path.is_file():
-            analysis_cif_path = project_path / 'analysis.cif'
-        if analysis_cif_path.is_file():
-            cif_text = analysis_cif_path.read_text()
-            analysis_from_cif(project._analysis, cif_text)
+        _load_project_info(project, project_path)
+        project.info.path = project_path
+        _load_cif_directory(project_path / 'structures', project._structures.add_from_cif_path)
+        _load_cif_directory(project_path / 'experiments', project._experiments.add_from_cif_path)
+        _load_project_analysis(project, project_path)
 
         # 5. Resolve alias param references
         project._resolve_alias_references()
@@ -302,13 +399,7 @@ def _resolve_alias_references(self) -> None:
         if not aliases._items:
             return
 
-        # Build unique_name → parameter map
-        all_params = self._structures.parameters + self._experiments.parameters
-        param_map: dict[str, object] = {}
-        for p in all_params:
-            uname = getattr(p, 'unique_name', None)
-            if uname is not None:
-                param_map[uname] = p
+        param_map = self._build_parameter_map()
 
         for alias in aliases:
             uname = alias.param_unique_name.value
@@ -320,9 +411,25 @@ def _resolve_alias_references(self) -> None:
                     f"parameter '{uname}'. Reference not resolved."
                 )
 
+    def _build_parameter_map(self) -> dict[str, object]:
+        """
+        Return a ``unique_name`` to live parameter mapping.
+
+        The map combines structure and experiment parameters and is
+        reused by CIF restore steps that need to reconnect persisted
+        names to live parameter objects.
+        """
+        all_params = self._structures.parameters + self._experiments.parameters
+        param_map: dict[str, object] = {}
+        for param in all_params:
+            unique_name = getattr(param, 'unique_name', None)
+            if unique_name is not None:
+                param_map[unique_name] = param
+        return param_map
+
     def save(self) -> None:
         """Save the project into the existing project directory."""
-        if self._info.path is None:
+        if self.info.path is None:
             log.error('Project path not specified. Use save_as() to define the path first.')
             return
 
@@ -330,20 +437,20 @@ def save(self) -> None:
         console.print(self.info.path.resolve())
 
         # Apply constraints so dependent parameters are flagged
-        # before serialization (constrained params are written
+        # before serialization (user-constrained params are written
         # without brackets).
         self._analysis._update_categories()
 
         # Ensure project directory exists
-        self._info.path.mkdir(parents=True, exist_ok=True)
+        self.info.path.mkdir(parents=True, exist_ok=True)
 
         # Save project-level configuration
-        with (self._info.path / 'project.cif').open('w') as f:
+        with (self.info.path / 'project.cif').open('w') as f:
             f.write(project_config_to_cif(self))
             console.print('├── 📄 project.cif')
 
         # Save structures
-        sm_dir = self._info.path / 'structures'
+        sm_dir = self.info.path / 'structures'
         sm_dir.mkdir(parents=True, exist_ok=True)
         console.print('├── 📁 structures/')
         for structure in self.structures.values():
@@ -354,7 +461,7 @@ def save(self) -> None:
                 console.print(f'│   └── 📄 {file_name}')
 
         # Save experiments
-        expt_dir = self._info.path / 'experiments'
+        expt_dir = self.info.path / 'experiments'
         expt_dir.mkdir(parents=True, exist_ok=True)
         console.print('├── 📁 experiments/')
         for experiment in self.experiments.values():
@@ -365,19 +472,29 @@ def save(self) -> None:
                 console.print(f'│   └── 📄 {file_name}')
 
         # Save analysis
-        analysis_dir = self._info.path / 'analysis'
+        analysis_dir = self.info.path / 'analysis'
         analysis_dir.mkdir(parents=True, exist_ok=True)
         with (analysis_dir / 'analysis.cif').open('w') as f:
-            f.write(self.analysis.as_cif())
+            f.write(self.analysis.as_cif)
             console.print('├── 📁 analysis/')
-            console.print('│   └── 📄 analysis.cif')
+        write_analysis_results_sidecar(
+            analysis=self.analysis,
+            analysis_dir=analysis_dir,
+        )
+
+        analysis_file_names = sorted(
+            path.name for path in analysis_dir.iterdir() if path.is_file()
+        )
+        for index, file_name in enumerate(analysis_file_names):
+            branch = '└──' if index == len(analysis_file_names) - 1 else '├──'
+            console.print(f'│   {branch} 📄 {file_name}')
 
         # Save summary
-        with (self._info.path / 'summary.cif').open('w') as f:
+        with (self.info.path / 'summary.cif').open('w') as f:
             f.write(self.summary.as_cif())
             console.print('└── 📄 summary.cif')
 
-        self._info.update_last_modified()
+        self.info.update_last_modified()
         self._saved = True
 
     def save_as(
@@ -385,12 +502,40 @@ def save_as(
         dir_path: str,
         *,
         temporary: bool = False,
+        overwrite: bool = True,
     ) -> None:
-        """Save the project into a new directory."""
+        """
+        Save the project into a directory.
+
+        Parameters
+        ----------
+        dir_path : str
+            Destination directory for the saved project.
+        temporary : bool, default=False
+            Whether to save beneath the system temporary directory.
+        overwrite : bool, default=True
+            Whether to remove an existing target directory before
+            saving.
+        """
         if temporary:
             tmp: str = tempfile.gettempdir()
-            dir_path = pathlib.Path(tmp) / dir_path
-        self._info.path = dir_path
+            project_dir = pathlib.Path(tmp) / dir_path
+        else:
+            project_dir = resolve_artifact_path(dir_path)
+
+        if overwrite and project_dir.is_dir():
+            current_working_directory = pathlib.Path.cwd().resolve()
+            resolved_project_dir = project_dir.resolve()
+            if resolved_project_dir == current_working_directory:
+                for child_path in resolved_project_dir.iterdir():
+                    if child_path.is_dir():
+                        shutil.rmtree(child_path)
+                    else:
+                        child_path.unlink()
+            else:
+                shutil.rmtree(project_dir)
+
+        self.info.path = project_dir
         self.save()
 
     def apply_params_from_csv(self, row_index: int) -> None:
@@ -446,13 +591,18 @@ def apply_params_from_csv(self, row_index: int) -> None:
 
         row = df.iloc[row_index]
 
+        experiment = next(iter(self.experiments.values()))
+
         # 1. Reload data if file_path points to a real file
         file_path = row.get('file_path', '')
-        if file_path and pathlib.Path(file_path).is_file():
-            experiment = next(iter(self.experiments.values()))
-            experiment._load_ascii_data_to_experiment(file_path)
+        data_path = _resolve_data_path_from_results_csv(self.info.path, file_path)
+        if data_path is not None and data_path.is_file():
+            experiment._load_ascii_data_to_experiment(str(data_path))
+
+        # 2. Restore extracted diffrn metadata from the CSV row.
+        _apply_csv_row_to_diffrn(row, df.columns, experiment)
 
-        # 2. Override parameter values and uncertainties
+        # 3. Override parameter values and uncertainties
         all_params = self.structures.parameters + self.experiments.parameters
         param_map = {
             p.unique_name: p
diff --git a/src/easydiffraction/project/project_config.py b/src/easydiffraction/project/project_config.py
new file mode 100644
index 000000000..dd92137ab
--- /dev/null
+++ b/src/easydiffraction/project/project_config.py
@@ -0,0 +1,55 @@
+# SPDX-FileCopyrightText: 2026 EasyScience contributors 
+# SPDX-License-Identifier: BSD-3-Clause
+"""Project configuration owner for singleton project categories."""
+
+from __future__ import annotations
+
+from easydiffraction.core.category_owner import CategoryOwner
+from easydiffraction.project.categories.info import ProjectInfo
+from easydiffraction.project.categories.info import ProjectInfoFactory
+from easydiffraction.project.categories.rendering import Rendering
+from easydiffraction.project.categories.rendering import RenderingFactory
+from easydiffraction.project.categories.verbosity import Verbosity
+from easydiffraction.project.categories.verbosity import VerbosityFactory
+
+
+class ProjectConfig(CategoryOwner):
+    """Own singleton project configuration categories."""
+
+    def __init__(
+        self,
+        name: str = 'untitled_project',
+        title: str = 'Untitled Project',
+        description: str = '',
+    ) -> None:
+        super().__init__()
+        self._info = ProjectInfoFactory.create(
+            ProjectInfoFactory.default_tag(),
+            name=name,
+            title=title,
+            description=description,
+        )
+        self._rendering = RenderingFactory.create(RenderingFactory.default_tag())
+        self._verbosity = VerbosityFactory.create(VerbosityFactory.default_tag())
+
+    @property
+    def info(self) -> ProjectInfo:
+        """Project metadata category."""
+        return self._info
+
+    @property
+    def rendering(self) -> Rendering:
+        """Rendering configuration category."""
+        return self._rendering
+
+    @property
+    def verbosity(self) -> Verbosity:
+        """Verbosity configuration category."""
+        return self._verbosity
+
+    @property
+    def as_cif(self) -> str:
+        """Serialize singleton project categories to CIF."""
+        from easydiffraction.io.cif.serialize import category_owner_to_cif  # noqa: PLC0415
+
+        return category_owner_to_cif(self)
diff --git a/src/easydiffraction/project/project_info.py b/src/easydiffraction/project/project_info.py
index 94247f331..215e8ed54 100644
--- a/src/easydiffraction/project/project_info.py
+++ b/src/easydiffraction/project/project_info.py
@@ -1,134 +1,9 @@
-# SPDX-FileCopyrightText: 2025 EasyScience contributors 
+# SPDX-FileCopyrightText: 2026 EasyScience contributors 
 # SPDX-License-Identifier: BSD-3-Clause
-"""Project metadata container used by Project."""
+"""Project metadata category export."""
 
-import datetime
-import pathlib
+from __future__ import annotations
 
-from easydiffraction.core.guard import GuardedBase
-from easydiffraction.io.cif.serialize import project_info_to_cif
-from easydiffraction.utils.logging import console
-from easydiffraction.utils.utils import render_cif
+from easydiffraction.project.categories.info.default import ProjectInfo as _ProjectInfo
 
-
-class ProjectInfo(GuardedBase):
-    """Store project metadata: name, title, description, paths."""
-
-    def __init__(
-        self,
-        name: str = 'untitled_project',
-        title: str = 'Untitled Project',
-        description: str = '',
-    ) -> None:
-        super().__init__()
-
-        self._name = name
-        self._title = title
-        self._description = description
-        self._path: pathlib.Path | None = None  # pathlib.Path.cwd()
-        self._created: datetime.datetime = datetime.datetime.now()
-        self._last_modified: datetime.datetime = datetime.datetime.now()
-
-    @property
-    def name(self) -> str:
-        """Return the project name."""
-        return self._name
-
-    @name.setter
-    def name(self, value: str) -> None:
-        """
-        Set the project name.
-
-        Parameters
-        ----------
-        value : str
-            New project name.
-        """
-        self._name = value
-
-    @property
-    def unique_name(self) -> str:
-        """Unique name for GuardedBase diagnostics."""
-        return self.name
-
-    @property
-    def title(self) -> str:
-        """Return the project title."""
-        return self._title
-
-    @title.setter
-    def title(self, value: str) -> None:
-        """
-        Set the project title.
-
-        Parameters
-        ----------
-        value : str
-            New project title.
-        """
-        self._title = value
-
-    @property
-    def description(self) -> str:
-        """Return sanitized description with single spaces."""
-        return ' '.join(self._description.split())
-
-    @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 | None:
-        """Return the project path as a Path object."""
-        return self._path
-
-    @path.setter
-    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)
-
-    @property
-    def created(self) -> datetime.datetime:
-        """Return the creation timestamp."""
-        return self._created
-
-    @property
-    def last_modified(self) -> datetime.datetime:
-        """Return the last modified timestamp."""
-        return self._last_modified
-
-    def update_last_modified(self) -> None:
-        """Update the last modified timestamp."""
-        self._last_modified = datetime.datetime.now()
-
-    def parameters(self) -> None:
-        """List parameters (not implemented)."""
-
-    # TODO: Consider moving to io.cif.serialize
-    def as_cif(self) -> str:
-        """Export project metadata to CIF."""
-        return project_info_to_cif(self)
-
-    # TODO: Consider moving to io.cif.serialize
-    def show_as_cif(self) -> None:
-        """Pretty-print CIF via shared utilities."""
-        paragraph_title: str = f"Project 📦 '{self.name}' info as CIF"
-        cif_text: str = self.as_cif()
-        console.paragraph(paragraph_title)
-        render_cif(cif_text)
+ProjectInfo = _ProjectInfo
diff --git a/src/easydiffraction/summary/summary.py b/src/easydiffraction/summary/summary.py
index 768abb4b2..25977bfa9 100644
--- a/src/easydiffraction/summary/summary.py
+++ b/src/easydiffraction/summary/summary.py
@@ -6,6 +6,7 @@
 from easydiffraction.core.variable import Parameter
 from easydiffraction.io.cif.serialize import summary_to_cif
 from easydiffraction.utils.logging import console
+from easydiffraction.utils.utils import render_object_help
 from easydiffraction.utils.utils import render_table
 
 
@@ -28,6 +29,10 @@ def __init__(self, project: object) -> None:
         """
         self.project = project
 
+    def help(self) -> None:
+        """Print available summary-report methods."""
+        render_object_help(self)
+
     @staticmethod
     def _fmt_row(
         pretty_name: str,
@@ -214,7 +219,7 @@ def show_fitting_details(self) -> None:
         console.section('Fitting')
 
         console.paragraph('Minimization engine')
-        console.print(self.project.analysis.fit.minimizer_type.value)
+        console.print(self.project.analysis.fitting.minimizer_type.value)
 
         console.paragraph('Fit quality')
         columns_headers = ['metric', 'value']
diff --git a/src/easydiffraction/utils/environment.py b/src/easydiffraction/utils/environment.py
index e2df97d48..0fd35ce0c 100644
--- a/src/easydiffraction/utils/environment.py
+++ b/src/easydiffraction/utils/environment.py
@@ -5,7 +5,55 @@
 
 import os
 import sys
+import tempfile
 from importlib.util import find_spec
+from pathlib import Path
+
+_ARTIFACT_ROOT_ENV_VAR = 'EASYDIFFRACTION_ARTIFACT_ROOT'
+_PIXI_PROJECT_ROOT_ENV_VAR = 'PIXI_PROJECT_ROOT'
+_TUTORIALS_DIR = Path('docs') / 'docs' / 'tutorials'
+_TUTORIAL_ARTIFACT_ROOT = Path('tmp') / 'tutorials'
+
+
+def _repo_root() -> Path | None:
+    project_root = os.environ.get(_PIXI_PROJECT_ROOT_ENV_VAR)
+    if project_root:
+        return Path(project_root).resolve()
+
+    for parent in Path(__file__).resolve().parents:
+        if (parent / 'pixi.toml').is_file() and (parent / _TUTORIALS_DIR).is_dir():
+            return parent
+
+    return None
+
+
+def _tutorial_artifact_root() -> Path | None:
+    repo_root = _repo_root()
+    if repo_root is None:
+        return None
+
+    tutorials_dir = (repo_root / _TUTORIALS_DIR).resolve()
+    cwd = Path.cwd().resolve()
+    if not cwd.is_relative_to(tutorials_dir):
+        return None
+
+    return (repo_root / _TUTORIAL_ARTIFACT_ROOT).resolve()
+
+
+def _artifact_root() -> Path | None:
+    artifact_root = os.environ.get(_ARTIFACT_ROOT_ENV_VAR)
+    if not artifact_root:
+        return _tutorial_artifact_root()
+
+    root = Path(artifact_root)
+    if root.is_absolute():
+        return root.resolve()
+
+    project_root = os.environ.get(_PIXI_PROJECT_ROOT_ENV_VAR)
+    if project_root:
+        return (Path(project_root) / root).resolve()
+
+    return (Path.cwd() / root).resolve()
 
 
 def in_pytest() -> bool:
@@ -107,6 +155,52 @@ def in_github_ci() -> bool:
     return os.environ.get('GITHUB_ACTIONS') is not None
 
 
+def resolve_artifact_path(path: str | Path) -> Path:
+    """
+    Resolve a path against the configured artifact root.
+
+    Parameters
+    ----------
+    path : str | Path
+        Path to resolve.
+
+    Returns
+    -------
+    Path
+        The original path when no artifact root is configured or when
+        *path* is absolute. Otherwise, the absolute path under the
+        configured artifact root.
+    """
+    resolved_path = Path(path)
+    artifact_root = _artifact_root()
+    if artifact_root is None or resolved_path.is_absolute():
+        return resolved_path
+
+    return (artifact_root / resolved_path).resolve()
+
+
+def create_artifact_temp_dir(prefix: str) -> Path:
+    """
+    Create a temporary directory under the artifact root when set.
+
+    Parameters
+    ----------
+    prefix : str
+        Prefix for the temporary directory name.
+
+    Returns
+    -------
+    Path
+        Path to the created temporary directory.
+    """
+    artifact_root = _artifact_root()
+    if artifact_root is None:
+        return Path(tempfile.mkdtemp(prefix=prefix))
+
+    artifact_root.mkdir(parents=True, exist_ok=True)
+    return Path(tempfile.mkdtemp(prefix=prefix, dir=artifact_root)).resolve()
+
+
 # ----------------------------------------------------------------------
 # IPython/Jupyter helpers
 # ----------------------------------------------------------------------
diff --git a/src/easydiffraction/utils/logging.py b/src/easydiffraction/utils/logging.py
index a94aaddef..3c4da8ee5 100644
--- a/src/easydiffraction/utils/logging.py
+++ b/src/easydiffraction/utils/logging.py
@@ -682,7 +682,7 @@ def paragraph(cls, title: str) -> None:
         ----------
         title : str
             Heading text; substrings enclosed in single quotes are
-            rendered without the bold-blue style.
+            rendered without the bold deep_sky_blue3 style.
         """
         parts = re.split(r"('.*?')", title)
         text = Text()
@@ -690,7 +690,7 @@ def paragraph(cls, title: str) -> None:
             if part.startswith("'") and part.endswith("'"):
                 text.append(part)
             else:
-                text.append(part, style='bold blue')
+                text.append(part, style='bold deep_sky_blue3')
         formatted = f'{text.markup}'
         if not in_jupyter():
             formatted = f'\n{formatted}'
diff --git a/src/easydiffraction/utils/utils.py b/src/easydiffraction/utils/utils.py
index 0e301e30c..6dba4301d 100644
--- a/src/easydiffraction/utils/utils.py
+++ b/src/easydiffraction/utils/utils.py
@@ -6,6 +6,7 @@
 import functools
 import json
 import pathlib
+import shutil
 import urllib.request
 from importlib.metadata import PackageNotFoundError
 from importlib.metadata import version
@@ -20,6 +21,8 @@
 from uncertainties import ufloat_fromstr
 
 from easydiffraction.display.tables import TableRenderer
+from easydiffraction.io.ascii import extract_project_from_zip
+from easydiffraction.utils.environment import resolve_artifact_path
 from easydiffraction.utils.logging import console
 from easydiffraction.utils.logging import log
 
@@ -28,9 +31,9 @@
 _DATA_REPO = 'easyscience/diffraction'
 _DATA_ROOT = 'data'
 # commit SHA preferred
-_DATA_INDEX_REF = 'd5a1fddd0d3e3e919c7e4a19e83b94b4231b99b6'
+_DATA_INDEX_REF = 'dbe92a87e0106c4742eee0ff9a8e32bdb8b483cb'
 # macOS: sha256sum index.json
-_DATA_INDEX_HASH = 'sha256:8305fd55d5b0c7c63ffa2641c082c623a107c32af09343ba901e196f68fd9f73'
+_DATA_INDEX_HASH = 'sha256:9e7bbaf2cb650f4126572e85157c63bc76f201408856fe4af566bee55dcdfbb4'
 
 
 def _build_data_url(path: str) -> str:
@@ -112,6 +115,42 @@ def _fetch_data_index() -> dict:
         return json.load(f)
 
 
+def _existing_project_dir(extraction_dir: pathlib.Path) -> pathlib.Path | None:
+    """Return one extracted project directory from a destination."""
+    project_files = sorted(extraction_dir.rglob('project.cif'))
+    if not project_files:
+        return None
+    return project_files[0].parent.resolve()
+
+
+def _download_data_message(data_id: int | str, record: dict) -> str:
+    """Return the console message for one downloadable data record."""
+    description = record.get('description', '')
+    message = f'Data #{data_id}'
+    if description:
+        message += f': {description}'
+    return message
+
+
+def _download_data_targets(
+    data_id: int | str,
+    destination: str,
+    record: dict,
+) -> tuple[str, bool, pathlib.Path, pathlib.Path, pathlib.Path, str]:
+    """Return URL and filesystem targets for one download request."""
+    record_path = _record_path(record)
+    url = _build_data_url(record_path)
+    _validate_url(url)
+
+    fname = _filename_for_id_from_path(data_id, record_path)
+    is_project_archive = record.get('kind') == 'project' and fname.endswith('.zip')
+    dest_path = resolve_artifact_path(destination)
+    dest_path.mkdir(parents=True, exist_ok=True)
+    file_path = dest_path / fname
+    extraction_dir = dest_path / pathlib.Path(fname).stem
+    return url, is_project_archive, dest_path, file_path, extraction_dir, fname
+
+
 @functools.lru_cache(maxsize=1)
 def _fetch_tutorials_index() -> dict:
     """
@@ -160,14 +199,18 @@ def download_data(
     id : int | str
         Numeric dataset id (e.g. 12).
     destination : str, default='data'
-        Directory to save the file into (created if missing).
+        Directory to save the downloaded file or extracted project into
+        (created if missing). Relative destinations are resolved against
+        the configured artifact root when
+        ``EASYDIFFRACTION_ARTIFACT_ROOT`` is set.
     overwrite : bool, default=False
         Whether to overwrite the file if it already exists.
 
     Returns
     -------
     str
-        Full path to the downloaded file as string.
+        Full path to the downloaded file, or to the extracted project
+        directory for project ZIP archives, as string.
 
     Raises
     ------
@@ -186,24 +229,29 @@ def download_data(
         raise KeyError(msg)
 
     record = index[key]
-    record_path = _record_path(record)
-    url = _build_data_url(record_path)
-    _validate_url(url)
-    fname = _filename_for_id_from_path(id, record_path)
-
-    dest_path = pathlib.Path(destination)
-    dest_path.mkdir(parents=True, exist_ok=True)
-    file_path = dest_path / fname
-
-    description = record.get('description', '')
-    message = f'Data #{id}'
-    if description:
-        message += f': {description}'
+    url, is_project_archive, dest_path, file_path, extraction_dir, fname = _download_data_targets(
+        id, destination, record
+    )
+    message = _download_data_message(id, record)
 
     console.paragraph('Getting data...')
     console.print(f'{message}')
 
+    if is_project_archive and extraction_dir.exists() and not overwrite:
+        existing_project_dir = _existing_project_dir(extraction_dir)
+        if existing_project_dir is not None:
+            console.print(
+                f"✅ Data #{id} already extracted at '{existing_project_dir}'. "
+                'Keeping existing project.'
+            )
+            return str(existing_project_dir)
+
     if file_path.exists():
+        if is_project_archive and not overwrite:
+            project_dir = extract_project_from_zip(file_path, destination=extraction_dir)
+            file_path.unlink()
+            console.print(f"✅ Data #{id} extracted to '{project_dir}'")
+            return str(project_dir)
         if not overwrite:
             console.print(
                 f"✅ Data #{id} already present at '{file_path}'. Keeping existing file."
@@ -214,6 +262,9 @@ def download_data(
 
     known_hash = _normalize_known_hash(record.get('hash'))
 
+    if is_project_archive and extraction_dir.exists() and overwrite:
+        shutil.rmtree(extraction_dir)
+
     # Pooch downloads to destination with our controlled filename.
     pooch.retrieve(
         url=url,
@@ -222,10 +273,45 @@ def download_data(
         path=str(dest_path),
     )
 
-    console.print(f"✅ Data #{id} downloaded to '{file_path}'")
+    if is_project_archive:
+        project_dir = extract_project_from_zip(file_path, destination=extraction_dir)
+        file_path.unlink()
+        console.print(f"✅ Data #{id} downloaded and extracted to\n'{project_dir}'")
+        return str(project_dir)
+
+    console.print(f"✅ Data #{id} downloaded to:\n'{file_path}'")
     return str(file_path)
 
 
+def list_data() -> None:
+    """Display a table of available example data records."""
+    index = _fetch_data_index()
+    if not index:
+        console.print('❌ No example data available.')
+        return
+
+    console.paragraph('Example data available for download:')
+
+    columns_headers = ['id', 'file', 'kind', 'description']
+    columns_alignment = ['right', 'left', 'left', 'left']
+    columns_data = []
+
+    for data_id in sorted(index, key=lambda value: int(value) if value.isdigit() else value):
+        record = index[data_id]
+        columns_data.append([
+            data_id,
+            pathlib.PurePosixPath(_record_path(record)).name,
+            record.get('kind', ''),
+            record.get('description', ''),
+        ])
+
+    render_table(
+        columns_headers=columns_headers,
+        columns_data=columns_data,
+        columns_alignment=columns_alignment,
+    )
+
+
 def package_version(package_name: str) -> str | None:
     """
     Get the installed version string of the specified package.
@@ -474,7 +560,7 @@ def download_tutorial(
     with _safe_urlopen(url) as resp:
         file_path.write_bytes(resp.read())
 
-    console.print(f"✅ Tutorial #{id} downloaded to '{file_path}'")
+    console.print(f"✅ Tutorial #{id} downloaded to:\n'{file_path}'")
     return str(file_path)
 
 
@@ -563,6 +649,123 @@ def render_table(
     tabler.render(df, display_handle=display_handle)
 
 
+def build_table_renderable(
+    columns_data: object,
+    columns_alignment: object,
+    columns_headers: object = None,
+) -> object:
+    """
+    Build a table renderable for 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.
+
+    Returns
+    -------
+    object
+        Backend-native renderable, such as a Rich table or HTML.
+    """
+    headers = [
+        (col, align) for col, align in zip(columns_headers, columns_alignment, strict=False)
+    ]
+    df = pd.DataFrame(columns_data, columns=pd.MultiIndex.from_tuples(headers))
+
+    tabler = TableRenderer.get()
+    return tabler.build_renderable(df)
+
+
+def _help_first_sentence(docstring: str | None) -> str:
+    """Return the first paragraph of a docstring on one line."""
+    if not docstring:
+        return ''
+    first_para = docstring.strip().split('\n\n')[0]
+    return ' '.join(line.strip() for line in first_para.splitlines())
+
+
+def _help_property_rows(cls: type) -> list[list[str]]:
+    """Return public property rows for object help tables."""
+    seen: dict[str, property] = {}
+    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:
+                seen[key] = attr
+
+    rows = []
+    for i, key in enumerate(sorted(seen), 1):
+        prop = seen[key]
+        writable = '✓' if prop.fset else '✗'
+        doc = _help_first_sentence(prop.fget.__doc__ if prop.fget else None)
+        rows.append([str(i), key, writable, doc])
+    return rows
+
+
+def _help_method_rows(cls: type) -> list[list[str]]:
+    """Return public method rows for object help tables."""
+    seen: set[str] = set()
+    methods = []
+    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)
+                methods.append((key, raw))
+
+    rows = []
+    for i, (key, method) in enumerate(sorted(methods), 1):
+        doc = _help_first_sentence(getattr(method, '__doc__', None))
+        rows.append([str(i), f'{key}()', doc])
+    return rows
+
+
+def render_object_help(obj: object, title: str | None = None) -> None:
+    """
+    Print public properties and methods for a plain helper object.
+
+    Parameters
+    ----------
+    obj : object
+        Object whose public API should be summarized.
+    title : str | None, default=None
+        Optional display name. Uses the class name when omitted.
+    """
+    cls = type(obj)
+    display_title = title or cls.__name__
+    console.paragraph(f"Help for '{display_title}'")
+
+    prop_rows = _help_property_rows(cls)
+    if prop_rows:
+        console.paragraph('Properties')
+        render_table(
+            columns_headers=['#', 'Name', 'Writable', 'Description'],
+            columns_alignment=['right', 'left', 'center', 'left'],
+            columns_data=prop_rows,
+        )
+
+    method_rows = _help_method_rows(cls)
+    if method_rows:
+        console.paragraph('Methods')
+        render_table(
+            columns_headers=['#', 'Name', 'Description'],
+            columns_alignment=['right', 'left', 'left'],
+            columns_data=method_rows,
+        )
+
+
 def render_cif(cif_text: str) -> None:
     """
     Display CIF text as a formatted table in Jupyter or terminal.
diff --git a/tests/functional/test_fitting_workflow.py b/tests/functional/test_fitting_workflow.py
index 3fde95dde..0901269c6 100644
--- a/tests/functional/test_fitting_workflow.py
+++ b/tests/functional/test_fitting_workflow.py
@@ -139,13 +139,15 @@ def test_create_constraint(self):
 class TestFitting:
     def test_fit_produces_results(self):
         project = _make_fit_ready_project()
-        project.analysis.fit(verbosity='silent')
+        project.verbosity = 'silent'
+        project.analysis.fit()
         assert project.analysis.fit_results is not None
         assert project.analysis.fit_results.success is True
 
     def test_fit_improves_chi_squared(self):
         project = _make_fit_ready_project()
-        project.analysis.fit(verbosity='silent')
+        project.verbosity = 'silent'
+        project.analysis.fit()
         results = project.analysis.fit_results
         assert results.reduced_chi_square is not None
         # A well-configured fit should get reasonable chi-squared
@@ -154,7 +156,8 @@ def test_fit_improves_chi_squared(self):
     def test_fit_updates_parameter_values(self):
         project = _make_fit_ready_project()
         initial_a = project.structures['lbco'].cell.length_a.value
-        project.analysis.fit(verbosity='silent')
+        project.verbosity = 'silent'
+        project.analysis.fit()
         fitted_a = project.structures['lbco'].cell.length_a.value
         # Fitting should have adjusted the cell parameter
         assert fitted_a != pytest.approx(initial_a, abs=1e-6)
@@ -177,7 +180,8 @@ def test_fit_with_constraints(self):
             expression='biso_Ba = biso_La',
         )
 
-        project.analysis.fit(verbosity='silent')
+        project.verbosity = 'silent'
+        project.analysis.fit()
         assert project.analysis.fit_results.success is True
         # Constrained params should be equal after fitting
         la_biso = s.atom_sites['La'].adp_iso.value
diff --git a/tests/functional/test_project_lifecycle.py b/tests/functional/test_project_lifecycle.py
index 7d9c5a190..850f040e9 100644
--- a/tests/functional/test_project_lifecycle.py
+++ b/tests/functional/test_project_lifecycle.py
@@ -88,7 +88,7 @@ def test_default_verbosity_is_full(self):
             project = Project()
         finally:
             Project._loading = False
-        assert project.verbosity == 'full'
+        assert project.verbosity.fit.value == 'full'
 
     def test_set_verbosity_short(self):
         Project._loading = True
@@ -97,7 +97,7 @@ def test_set_verbosity_short(self):
         finally:
             Project._loading = False
         project.verbosity = 'short'
-        assert project.verbosity == 'short'
+        assert project.verbosity.fit.value == 'short'
 
     def test_set_verbosity_silent(self):
         Project._loading = True
@@ -106,7 +106,7 @@ def test_set_verbosity_silent(self):
         finally:
             Project._loading = False
         project.verbosity = 'silent'
-        assert project.verbosity == 'silent'
+        assert project.verbosity.fit.value == 'silent'
 
     def test_invalid_verbosity_raises(self):
         Project._loading = True
diff --git a/tests/functional/test_structure_workflow.py b/tests/functional/test_structure_workflow.py
index a128e73d9..f2e4a9193 100644
--- a/tests/functional/test_structure_workflow.py
+++ b/tests/functional/test_structure_workflow.py
@@ -162,9 +162,9 @@ def test_special_position_fract_cannot_be_freed(self, monkeypatch):
         s._update_categories()
 
         atom = s.atom_sites['La']
-        assert atom.fract_x.symmetry_fixed is True
-        assert atom.fract_y.symmetry_fixed is True
-        assert atom.fract_z.symmetry_fixed is True
+        assert atom.fract_x.symmetry_constrained is True
+        assert atom.fract_y.symmetry_constrained is True
+        assert atom.fract_z.symmetry_constrained is True
 
         monkeypatch.setattr(Logger, '_reaction', Logger.Reaction.WARN, raising=True)
         for parameter in ('fract_x', 'fract_y', 'fract_z'):
@@ -183,12 +183,12 @@ def test_cubic_cell_has_only_a_free(self):
         s._need_categories_update = True
         s._update_categories()
 
-        assert s.cell.length_a.symmetry_fixed is False
-        assert s.cell.length_b.symmetry_fixed is True
-        assert s.cell.length_c.symmetry_fixed is True
-        assert s.cell.angle_alpha.symmetry_fixed is True
-        assert s.cell.angle_beta.symmetry_fixed is True
-        assert s.cell.angle_gamma.symmetry_fixed is True
+        assert s.cell.length_a.symmetry_constrained is False
+        assert s.cell.length_b.symmetry_constrained is True
+        assert s.cell.length_c.symmetry_constrained is True
+        assert s.cell.angle_alpha.symmetry_constrained is True
+        assert s.cell.angle_beta.symmetry_constrained is True
+        assert s.cell.angle_gamma.symmetry_constrained is True
 
     def test_general_position_remains_refinable(self):
         project = _make_project()
@@ -208,7 +208,7 @@ def test_general_position_remains_refinable(self):
         s._update_categories()
 
         atom = s.atom_sites['La']
-        assert atom.fract_x.symmetry_fixed is False
+        assert atom.fract_x.symmetry_constrained is False
         atom.fract_x.free = True
         assert atom.fract_x.free is True
 
@@ -222,14 +222,14 @@ def test_changing_space_group_updates_flags(self, monkeypatch):
         # Start in P 1: cell free
         s._need_categories_update = True
         s._update_categories()
-        assert s.cell.length_b.symmetry_fixed is False
+        assert s.cell.length_b.symmetry_constrained is False
         s.cell.length_b.free = True
         assert s.cell.length_b.free is True
 
         # Switch to cubic: length_b becomes fixed
         s.space_group.name_h_m = 'P m -3 m'
         s._update_categories()
-        assert s.cell.length_b.symmetry_fixed is True
+        assert s.cell.length_b.symmetry_constrained is True
         assert s.cell.length_b.free is False
 
         # Setting free=True is now ignored with a warning
diff --git a/tests/functional/test_switchable_categories.py b/tests/functional/test_switchable_categories.py
index 25cc6cf3c..52bc991bd 100644
--- a/tests/functional/test_switchable_categories.py
+++ b/tests/functional/test_switchable_categories.py
@@ -45,7 +45,7 @@ def test_fit_default(self):
 
     def test_minimizer_default(self):
         project = _make_project_with_experiment()
-        assert project.analysis.fit.minimizer_type is not None
+        assert project.analysis.fitting.minimizer_type is not None
 
 
 # ------------------------------------------------------------------
diff --git a/tests/integration/fitting/conftest.py b/tests/integration/fitting/conftest.py
index a4987ae7f..7c5113274 100644
--- a/tests/integration/fitting/conftest.py
+++ b/tests/integration/fitting/conftest.py
@@ -76,7 +76,7 @@ def lbco_fitted_project():
     project = Project()
     project.structures.add(model)
     project.experiments.add(expt)
-    project.analysis.fit.minimizer_type = 'lmfit'
+    project.analysis.fitting.minimizer_type = 'lmfit'
 
     model.cell.length_a.free = True
     expt.linked_phases['lbco'].scale.free = True
@@ -84,6 +84,7 @@ def lbco_fitted_project():
     expt.background['1'].y.free = True
     expt.background['2'].y.free = True
 
-    project.analysis.fit(verbosity='silent')
+    project.verbosity = 'silent'
+    project.analysis.fit()
 
     return project
diff --git a/tests/integration/fitting/test_analysis_and_fit_category_support.py b/tests/integration/fitting/test_analysis_and_fit_category_support.py
new file mode 100644
index 000000000..89c0fd931
--- /dev/null
+++ b/tests/integration/fitting/test_analysis_and_fit_category_support.py
@@ -0,0 +1,338 @@
+# SPDX-FileCopyrightText: 2026 EasyScience contributors 
+# SPDX-License-Identifier: BSD-3-Clause
+
+from __future__ import annotations
+
+import re
+
+import pytest
+
+ANSI_ESCAPE_RE = re.compile(r'\x1b\[[0-?]*[ -/]*[@-~]')
+
+
+def _unstyled_output(text: str) -> str:
+    return ANSI_ESCAPE_RE.sub('', text)
+
+
+def _make_project():
+    class ExpCol:
+        def __init__(self) -> None:
+            self._names: list[str] = []
+
+        @property
+        def names(self):
+            return self._names
+
+        @property
+        def parameters(self):
+            return []
+
+        @property
+        def fittable_parameters(self):
+            return []
+
+        @property
+        def free_parameters(self):
+            return []
+
+    class Project:
+        experiments = ExpCol()
+        structures = ExpCol()
+        _varname = 'proj'
+        verbosity = 'full'
+
+    return Project()
+
+
+def _make_project_with_names(names: list[str]):
+    class ExpCol:
+        def __init__(self, names):
+            self._names = names
+
+        def __len__(self):
+            return len(self._names)
+
+        @property
+        def names(self):
+            return self._names
+
+        @property
+        def parameters(self):
+            return []
+
+        @property
+        def fittable_parameters(self):
+            return []
+
+        @property
+        def free_parameters(self):
+            return []
+
+    class Project:
+        experiments = ExpCol(names)
+        structures = ExpCol([])
+        _varname = 'proj'
+        verbosity = 'full'
+
+    return Project()
+
+
+def test_fit_mode_enum_members_default_and_descriptions():
+    from easydiffraction.analysis.enums import FitModeEnum
+
+    assert FitModeEnum.SINGLE == 'single'
+    assert FitModeEnum.JOINT == 'joint'
+    assert FitModeEnum.SEQUENTIAL == 'sequential'
+    assert FitModeEnum.default() is FitModeEnum.SINGLE
+    assert all(member.description() for member in FitModeEnum)
+
+
+def test_fitting_instantiation_defaults_and_helpers():
+    from easydiffraction.analysis.categories.fitting.default import Fitting
+    import easydiffraction.analysis.categories.fitting.default as fitting_mod
+
+    fitting = Fitting()
+
+    assert fitting._identity.category_code == 'fitting'
+    assert fitting.minimizer_type.value == 'lmfit (leastsq)'
+    assert fitting.minimizer is None
+
+    class ParentWithMinimizer:
+        fitter = type('FitterHolder', (), {'minimizer': 'MIN'})()
+
+    fitting._parent = ParentWithMinimizer()
+    assert fitting.minimizer == 'MIN'
+
+    shown: list[str] = []
+    monkeypatch = pytest.MonkeyPatch()
+    monkeypatch.setattr(
+        fitting_mod.MinimizerFactory,
+        'show_supported',
+        lambda: shown.append('shown'),
+    )
+    Fitting.show_available_minimizers()
+    monkeypatch.undo()
+
+    assert shown == ['shown']
+
+
+def test_fit_from_cif_warns_on_invalid_minimizer(monkeypatch):
+    import easydiffraction.analysis.categories.fitting.default as fitting_mod
+    from easydiffraction.analysis.categories.fitting.default import Fitting
+
+    fitting = Fitting()
+    fitting._minimizer_type._value = 'bad-minimizer'
+
+    class Parent:
+        fitter = None
+
+    warnings: list[str] = []
+    fitting._parent = Parent()
+    monkeypatch.setattr(fitting_mod.CategoryItem, 'from_cif', lambda self, block, idx=0: None)
+    monkeypatch.setattr(
+        fitting_mod,
+        'Fitter',
+        lambda value: (_ for _ in ()).throw(ValueError('bad minimizer')),
+    )
+    monkeypatch.setattr(fitting_mod.log, 'warning', lambda message: warnings.append(message))
+
+    fitting.from_cif(object())
+
+    assert warnings == ['bad minimizer']
+
+
+def test_fitting_fallback_paths_without_parent(monkeypatch):
+    import easydiffraction.analysis.categories.fitting.default as fitting_mod
+    from easydiffraction.analysis.categories.fitting.default import Fitting
+
+    fitting = Fitting()
+
+    assert fitting.minimizer is None
+
+    monkeypatch.setattr(fitting_mod.CategoryItem, 'from_cif', lambda self, block, idx=0: None)
+    fitting.from_cif(object())
+
+
+def test_show_fitting_mode_types_for_single_and_multiple_experiments(capsys):
+    from easydiffraction.analysis.analysis import Analysis
+
+    single = Analysis(project=_make_project_with_names(['e1']))
+    single.show_fitting_mode_types()
+    out_single = capsys.readouterr().out
+    assert 'Fitting mode types' in out_single
+    assert 'single' in out_single
+    assert 'joint' in out_single
+
+    multi = Analysis(project=_make_project_with_names(['e1', 'e2']))
+    multi.show_fitting_mode_types()
+    out_multi = capsys.readouterr().out
+    assert 'joint' in out_multi
+    assert 'sequential' in out_multi
+
+
+def test_show_minimizer_types_prints(capsys):
+    from easydiffraction.analysis.analysis import Analysis
+
+    analysis = Analysis(project=_make_project_with_names([]))
+    analysis.fitting.show_minimizer_types()
+    out = capsys.readouterr().out
+    assert 'Minimizer types' in out
+    assert 'lmfit (leastsq)' in out
+
+
+def test_analysis_help_and_mode_switching(capsys):
+    from easydiffraction.analysis.analysis import Analysis
+
+    analysis = Analysis(project=_make_project_with_names(['e1', 'e2']))
+    assert analysis.fitting_mode_type == 'single'
+    analysis.fitting_mode_type = 'joint'
+    assert analysis.fitting_mode_type == 'joint'
+    assert len(analysis.joint_fit) == 0
+
+    analysis.help()
+    out = _unstyled_output(capsys.readouterr().out)
+    assert "Help for 'Analysis'" in out
+    assert 'fitting' in out
+    assert 'display' in out
+    assert 'Properties' in out
+    assert 'Methods' in out
+    assert 'fit()' in out
+
+
+def test_display_fit_results_warns_when_no_results(capsys):
+    from easydiffraction.analysis.analysis import Analysis
+
+    analysis = Analysis(project=_make_project_with_names([]))
+    analysis.display.fit_results()
+    out = capsys.readouterr().out
+    assert 'No fit results available' in out
+
+
+def test_display_fit_results_calls_process_fit_results(monkeypatch):
+    from easydiffraction.analysis.analysis import Analysis
+
+    process_called = {'called': False, 'args': None}
+
+    def fake_process_fit_results(structures, experiments):
+        process_called['called'] = True
+        process_called['args'] = (structures, experiments)
+
+    class Project:
+        structures = object()
+        _varname = 'proj'
+        verbosity = 'full'
+
+        class Experiments:
+            names: list[str] = []
+
+            @staticmethod
+            def values():
+                return []
+
+            @property
+            def parameters(self):
+                return []
+
+            @property
+            def fittable_parameters(self):
+                return []
+
+            @property
+            def free_parameters(self):
+                return []
+
+        experiments = Experiments()
+
+    analysis = Analysis(project=Project())
+    analysis.fit_results = object()
+    monkeypatch.setattr(analysis.fitter, '_process_fit_results', fake_process_fit_results)
+
+    analysis.display.fit_results()
+
+    assert process_called['called'] is True
+
+
+def test_analysis_display_as_cif_and_constraints(monkeypatch, capsys):
+    import easydiffraction.analysis.analysis as analysis_mod
+    import easydiffraction.analysis.categories.constraints.default as constraints_mod
+    from easydiffraction.analysis.analysis import Analysis
+
+    analysis = Analysis(project=_make_project())
+    rendered: dict[str, str] = {}
+
+    monkeypatch.setattr(analysis_mod, 'render_cif', lambda text: rendered.setdefault('text', text))
+    analysis.display.as_cif()
+    assert 'text' in rendered
+
+    analysis.display.constraints()
+    assert 'No constraints' in capsys.readouterr().out
+
+    class FakeId:
+        value = 'constraint_1'
+
+    class FakeExpr:
+        value = 'x = y + 1'
+
+    class FakeConstraint:
+        id = FakeId()
+        expression = FakeExpr()
+
+    analysis.constraints._items = [FakeConstraint()]
+    captured: dict[str, object] = {}
+    monkeypatch.setattr(constraints_mod, 'render_table', lambda **kwargs: captured.update(kwargs))
+    analysis.display.constraints()
+    out = capsys.readouterr().out
+    assert 'User defined constraints' in out
+    assert captured['columns_headers'] == ['id', 'expression']
+    assert captured['columns_data'][0] == ['constraint_1', 'x = y + 1']
+
+
+def test_discover_helpers_and_snapshot_params():
+    from easydiffraction.analysis.analysis import Analysis
+    from easydiffraction.analysis.analysis import _discover_method_rows
+    from easydiffraction.analysis.analysis import _discover_property_rows
+
+    class Demo:
+        @property
+        def alpha(self):
+            """Alpha property."""
+            return 1
+
+        @property
+        def beta(self):
+            """Beta property."""
+            return 2
+
+        @beta.setter
+        def beta(self, value):
+            return None
+
+        def do_thing(self):
+            """Do a thing."""
+
+        def _private(self):
+            return None
+
+    property_rows = _discover_property_rows(Demo)
+    method_rows = _discover_method_rows(Demo)
+
+    assert len(property_rows) == 2
+    assert 'alpha' in [row[1] for row in property_rows]
+    assert next(row for row in property_rows if row[1] == 'beta')[2] == '✓'
+    assert 'do_thing()' in [row[1] for row in method_rows]
+    assert '_private()' not in [row[1] for row in method_rows]
+
+    analysis = Analysis(project=_make_project())
+
+    class FakeParam:
+        unique_name = 'p1'
+        value = 1.23
+        uncertainty = 0.01
+        units = 'A'
+
+    class FakeResults:
+        parameters = [FakeParam()]
+
+    analysis._snapshot_params('expt1', FakeResults())
+    assert analysis._parameter_snapshots['expt1']['p1']['value'] == 1.23
+    assert analysis._parameter_snapshots['expt1']['p1']['uncertainty'] == 0.01
diff --git a/tests/integration/fitting/test_analysis_display.py b/tests/integration/fitting/test_analysis_display.py
index 2f5b0e450..3a139be26 100644
--- a/tests/integration/fitting/test_analysis_display.py
+++ b/tests/integration/fitting/test_analysis_display.py
@@ -1,53 +1,53 @@
 # SPDX-FileCopyrightText: 2026 EasyScience contributors 
 # SPDX-License-Identifier: BSD-3-Clause
 
-"""Integration tests for Analysis display methods and CIF serialization."""
+"""Integration tests for project display reports and analysis CIF helpers."""
 
 
 def test_display_all_params(lbco_fitted_project):
     project = lbco_fitted_project
-    project.analysis.display.all_params()
+    project.display.parameters.all()
 
 
 def test_display_fittable_params(lbco_fitted_project):
     project = lbco_fitted_project
-    project.analysis.display.fittable_params()
+    project.display.parameters.fittable()
 
 
 def test_display_free_params(lbco_fitted_project):
     project = lbco_fitted_project
-    project.analysis.display.free_params()
+    project.display.parameters.free()
 
 
 def test_display_how_to_access_parameters(lbco_fitted_project):
     project = lbco_fitted_project
-    project.analysis.display.how_to_access_parameters()
+    project.display.parameters.access()
 
 
 def test_display_parameter_cif_uids(lbco_fitted_project):
     project = lbco_fitted_project
-    project.analysis.display.parameter_cif_uids()
+    project.display.parameters.cif_uids()
 
 
 def test_display_constraints_empty(lbco_fitted_project):
     project = lbco_fitted_project
-    project.analysis.display.constraints()
+    project.analysis.constraints.show()
 
 
 def test_display_fit_results(lbco_fitted_project):
     project = lbco_fitted_project
     assert project.analysis.fit_results is not None
-    project.analysis.display.fit_results()
+    project.display.fit.results()
 
 
 def test_display_as_cif(lbco_fitted_project):
     project = lbco_fitted_project
-    project.analysis.display.as_cif()
+    project.analysis.show_as_cif()
 
 
 def test_analysis_as_cif(lbco_fitted_project):
     project = lbco_fitted_project
-    cif_text = project.analysis.as_cif()
+    cif_text = project.analysis.as_cif
     assert isinstance(cif_text, str)
     assert len(cif_text) > 0
 
@@ -59,12 +59,12 @@ def test_analysis_help(lbco_fitted_project):
 
 def test_show_minimizer_types_again(lbco_fitted_project):
     project = lbco_fitted_project
-    project.analysis.fit.show_minimizer_types()
+    project.analysis.fitting.show_minimizer_types()
 
 
 def test_show_minimizer_types(lbco_fitted_project):
     project = lbco_fitted_project
-    project.analysis.fit.show_minimizer_types()
+    project.analysis.fitting.show_minimizer_types()
 
 
 def test_fit_results_attributes(lbco_fitted_project):
diff --git a/tests/integration/fitting/test_aniso_adp_fitting.py b/tests/integration/fitting/test_aniso_adp_fitting.py
index 24fed45f9..ccfbef066 100644
--- a/tests/integration/fitting/test_aniso_adp_fitting.py
+++ b/tests/integration/fitting/test_aniso_adp_fitting.py
@@ -59,7 +59,8 @@ def test_iso_then_aniso_fit() -> None:
     e.extinction.radius.free = True
 
     # Fit isotropic
-    project.analysis.fit(verbosity='silent')
+    project.verbosity = 'silent'
+    project.analysis.fit()
     chi2_iso = project.analysis.fit_results.reduced_chi_square
     assert chi2_iso < 20.0
 
@@ -81,7 +82,7 @@ def test_iso_then_aniso_fit() -> None:
     s.atom_site_aniso['O1'].adp_23.free = True
 
     # Fit anisotropic
-    project.analysis.fit(verbosity='silent')
+    project.analysis.fit()
     chi2_aniso = project.analysis.fit_results.reduced_chi_square
 
     # Anisotropic fit should improve (or at least match) chi2
diff --git a/tests/integration/fitting/test_bayesian_dream.py b/tests/integration/fitting/test_bayesian_dream.py
new file mode 100644
index 000000000..ef52c1f1a
--- /dev/null
+++ b/tests/integration/fitting/test_bayesian_dream.py
@@ -0,0 +1,197 @@
+# SPDX-FileCopyrightText: 2026 EasyScience contributors 
+# SPDX-License-Identifier: BSD-3-Clause
+
+from __future__ import annotations
+
+import tempfile
+
+import numpy as np
+
+from easydiffraction import ExperimentFactory
+from easydiffraction import Project
+from easydiffraction import StructureFactory
+from easydiffraction import download_data
+
+TEMP_DIR = tempfile.gettempdir()
+
+
+def _create_lbco_project() -> Project:
+    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.create(
+        label='La',
+        type_symbol='La',
+        fract_x=0,
+        fract_y=0,
+        fract_z=0,
+        wyckoff_letter='a',
+        occupancy=0.5,
+        adp_iso=0.1,
+    )
+    model.atom_sites.create(
+        label='Ba',
+        type_symbol='Ba',
+        fract_x=0,
+        fract_y=0,
+        fract_z=0,
+        wyckoff_letter='a',
+        occupancy=0.5,
+        adp_iso=0.1,
+    )
+    model.atom_sites.create(
+        label='Co',
+        type_symbol='Co',
+        fract_x=0.5,
+        fract_y=0.5,
+        fract_z=0.5,
+        wyckoff_letter='b',
+        adp_iso=0.1,
+    )
+    model.atom_sites.create(
+        label='O',
+        type_symbol='O',
+        fract_x=0,
+        fract_y=0.5,
+        fract_z=0.5,
+        wyckoff_letter='c',
+        adp_iso=0.1,
+    )
+
+    data_path = download_data(id=3, destination=TEMP_DIR)
+    experiment = ExperimentFactory.from_data_path(name='hrpt', data_path=data_path)
+    experiment.instrument.setup_wavelength = 1.494
+    experiment.instrument.calib_twotheta_offset = 0.0
+    experiment.peak.broad_gauss_u = 0.1
+    experiment.peak.broad_gauss_v = -0.1
+    experiment.peak.broad_gauss_w = 0.2
+    experiment.peak.broad_lorentz_x = 0.0
+    experiment.peak.broad_lorentz_y = 0.0
+    experiment.linked_phases.create(id='lbco', scale=5.0)
+    experiment.background.create(id='1', x=10, y=170)
+    experiment.background.create(id='2', x=165, y=170)
+
+    project = Project(name='lbco_bayesian')
+    project.structures.add(model)
+    project.experiments.add(experiment)
+    return project
+
+
+def _dream_parameters(project: Project) -> tuple[object, object, object]:
+    structure = project.structures['lbco']
+    experiment = project.experiments['hrpt']
+    return (
+        structure.cell.length_a,
+        experiment.linked_phases['lbco'].scale,
+        experiment.instrument.calib_twotheta_offset,
+    )
+
+
+def _configure_small_dream(project: Project) -> None:
+    project.analysis.fitting.minimizer_type = 'bumps (dream)'
+    minimizer = project.analysis.fitting.minimizer
+    minimizer.steps = 20
+    minimizer.burn = 5
+    minimizer.thin = 1
+    minimizer.pop = 4
+    minimizer.init = 'lhs'
+
+
+def _run_single_fit(project: Project, *, random_seed: int | None = None) -> None:
+    project.verbosity = 'silent'
+    prepared = project.analysis._prepare_fit_run()
+    assert prepared is not None
+    verb, structures, experiments = prepared
+    project.analysis._fit_single(
+        verb,
+        structures,
+        experiments,
+        use_physical_limits=False,
+        random_seed=random_seed,
+    )
+
+
+def test_small_bounded_dream_refinement_produces_posterior_results():
+    project = _create_lbco_project()
+    length_a, scale, offset = _dream_parameters(project)
+    for parameter in (length_a, scale, offset):
+        parameter.free = True
+
+    length_a.fit_min = 3.84
+    length_a.fit_max = 3.92
+    scale.fit_min = 1.0
+    scale.fit_max = 12.0
+    offset.fit_min = -1.0
+    offset.fit_max = 1.0
+
+    _configure_small_dream(project)
+    _run_single_fit(project, random_seed=11)
+
+    results = project.analysis.fit_results
+    assert results.success is True
+    assert results.sampler_completed is True
+    assert results.sampler_name == 'dream'
+    assert results.posterior_samples is not None
+    assert results.posterior_samples.parameter_samples.ndim == 3
+    assert results.sampler_settings['random_seed'] == 11
+    assert results.sampler_settings['init'] == 'lhs'
+    assert len(results.posterior_parameter_summaries) == 3
+
+
+def test_lm_prefit_followed_by_dream_uses_uncertainty_based_bounds():
+    project = _create_lbco_project()
+    length_a, scale, offset = _dream_parameters(project)
+    for parameter in (length_a, scale, offset):
+        parameter.free = True
+
+    project.analysis.fitting.minimizer_type = 'bumps (lm)'
+    _run_single_fit(project)
+
+    for parameter in (length_a, scale, offset):
+        assert parameter.uncertainty is not None
+        parameter.set_fit_bounds_from_uncertainty(multiplier=4)
+        assert np.isfinite(parameter.fit_min)
+        assert np.isfinite(parameter.fit_max)
+
+    _configure_small_dream(project)
+    _run_single_fit(project, random_seed=13)
+
+    results = project.analysis.fit_results
+    assert results.success is True
+    assert results.posterior_samples is not None
+    assert results.sampler_settings['random_seed'] == 13
+    assert len(results.posterior_parameter_summaries) == 3
+
+
+def test_bayesian_fit_results_reload_from_persisted_fit_state(tmp_path):
+    project = _create_lbco_project()
+    length_a, scale, offset = _dream_parameters(project)
+    for parameter in (length_a, scale, offset):
+        parameter.free = True
+
+    length_a.fit_min = 3.84
+    length_a.fit_max = 3.92
+    scale.fit_min = 1.0
+    scale.fit_max = 12.0
+    offset.fit_min = -1.0
+    offset.fit_max = 1.0
+
+    _configure_small_dream(project)
+    _run_single_fit(project, random_seed=17)
+
+    assert project.analysis.fit_results.posterior_samples is not None
+
+    proj_dir = tmp_path / 'dream_project'
+    project.save_as(str(proj_dir))
+
+    analysis_cif = proj_dir / 'analysis' / 'analysis.cif'
+    results_sidecar = proj_dir / 'analysis' / 'results.h5'
+    assert analysis_cif.is_file()
+    assert results_sidecar.is_file()
+
+    loaded = Project.load(str(proj_dir))
+    loaded_results = loaded.analysis.fit_results
+    assert loaded_results is not None
+    assert loaded_results.sampler_completed is True
+    assert loaded_results.posterior_samples is not None
+    assert loaded_results.posterior_samples.parameter_samples.ndim == 3
diff --git a/tests/integration/fitting/test_bayesian_helper_support.py b/tests/integration/fitting/test_bayesian_helper_support.py
new file mode 100644
index 000000000..b67d5db02
--- /dev/null
+++ b/tests/integration/fitting/test_bayesian_helper_support.py
@@ -0,0 +1,670 @@
+# SPDX-FileCopyrightText: 2026 EasyScience contributors 
+# SPDX-License-Identifier: BSD-3-Clause
+
+from __future__ import annotations
+
+import re
+
+import numpy as np
+import pytest
+
+ANSI_ESCAPE_RE = re.compile(r'\x1b\[[0-?]*[ -/]*[@-~]')
+
+
+def _unstyled_output(text: str) -> str:
+    return ANSI_ESCAPE_RE.sub('', text)
+
+
+class Identity:
+    def __init__(self) -> None:
+        self.datablock_entry_name = 'db'
+        self.category_code = 'cat'
+        self.category_entry_name = 'entry'
+
+
+class Param:
+    def __init__(self, unique_name: str, start: float, value: float, uncertainty: float) -> None:
+        self._identity = Identity()
+        self._fit_start_value = start
+        self.unique_name = unique_name
+        self.name = unique_name
+        self.value = value
+        self.uncertainty = uncertainty
+        self.units = 'arb'
+
+
+def test_posterior_samples_flatten_and_to_arviz():
+    from easydiffraction.analysis.fit_helpers.bayesian import PosteriorSamples
+
+    posterior_samples = PosteriorSamples(
+        parameter_names=['a', 'b'],
+        parameter_samples=np.array(
+            [
+                [[1.0, 10.0], [2.0, 20.0]],
+                [[3.0, 30.0], [4.0, 40.0]],
+            ],
+            dtype=float,
+        ),
+        log_posterior=np.array([[0.1, 0.2], [0.3, 0.4]], dtype=float),
+    )
+
+    flattened = posterior_samples.flattened()
+    inference_data = posterior_samples.to_arviz()
+
+    assert flattened.shape == (4, 2)
+    np.testing.assert_allclose(flattened[:, 0], np.array([1.0, 2.0, 3.0, 4.0]))
+    np.testing.assert_allclose(flattened[:, 1], np.array([10.0, 20.0, 30.0, 40.0]))
+    assert set(inference_data.posterior.data_vars) == {'a', 'b'}
+    assert inference_data.posterior['a'].shape == (2, 2)
+    assert inference_data.sample_stats['lp'].shape == (2, 2)
+
+
+def test_posterior_samples_to_arviz_validates_shapes():
+    from easydiffraction.analysis.fit_helpers.bayesian import PosteriorSamples
+
+    posterior_samples = PosteriorSamples(
+        parameter_names=['a'],
+        parameter_samples=np.array([1.0, 2.0]),
+    )
+
+    with pytest.raises(
+        ValueError,
+        match=r'Posterior sample array must have shape \(n_draws, n_chains, n_parameters\)\.',
+    ):
+        posterior_samples.to_arviz()
+
+
+def test_posterior_samples_to_arviz_validates_name_and_log_posterior_lengths():
+    from easydiffraction.analysis.fit_helpers.bayesian import PosteriorSamples
+
+    wrong_names = PosteriorSamples(
+        parameter_names=['a'],
+        parameter_samples=np.ones((2, 2, 2), dtype=float),
+    )
+    with pytest.raises(
+        ValueError,
+        match=r'Posterior sample array does not match the parameter name list length\.',
+    ):
+        wrong_names.to_arviz()
+
+    wrong_log_posterior = PosteriorSamples(
+        parameter_names=['a'],
+        parameter_samples=np.ones((2, 2, 1), dtype=float),
+        log_posterior=np.ones((3, 2), dtype=float),
+    )
+    with pytest.raises(
+        ValueError,
+        match=r'Log-posterior array must match the first two posterior sample axes\.',
+    ):
+        wrong_log_posterior.to_arviz()
+
+
+def test_compute_convergence_diagnostics_treats_non_finite_values_as_not_converged(
+    monkeypatch,
+):
+    from easydiffraction.analysis.fit_helpers.bayesian import PosteriorSamples
+    from easydiffraction.analysis.fit_helpers.bayesian import compute_convergence_diagnostics
+
+    posterior_samples = PosteriorSamples(
+        parameter_names=['a'],
+        parameter_samples=np.ones((4, 2, 1), dtype=float),
+    )
+
+    fake_dataset = type('FakeDataset', (), {'data_vars': {'a': np.array([np.nan], dtype=float)}})
+
+    monkeypatch.setattr(
+        'easydiffraction.analysis.fit_helpers.bayesian.az.rhat',
+        lambda inference_data: fake_dataset,
+    )
+    monkeypatch.setattr(
+        'easydiffraction.analysis.fit_helpers.bayesian.az.ess',
+        lambda inference_data, method='bulk': type(
+            'FakeDataset', (), {'data_vars': {'a': np.array([4000.0], dtype=float)}}
+        ),
+    )
+
+    diagnostics = compute_convergence_diagnostics(posterior_samples)
+
+    assert diagnostics['converged'] is False
+    assert diagnostics['r_hat_by_parameter'] == {'a': None}
+    assert diagnostics['ess_bulk_by_parameter'] == {'a': 4000.0}
+    assert diagnostics['max_r_hat'] is None
+    assert diagnostics['min_ess_bulk'] == pytest.approx(4000.0)
+
+
+def test_summarize_posterior_parameters_preserves_order_and_display_names():
+    from easydiffraction.analysis.fit_helpers.bayesian import PosteriorSamples
+    from easydiffraction.analysis.fit_helpers.bayesian import summarize_posterior_parameters
+
+    posterior_samples = PosteriorSamples(
+        parameter_names=['beta', 'alpha'],
+        parameter_samples=np.array(
+            [
+                [[2.0, 1.0], [2.2, 1.2]],
+                [[1.8, 0.8], [2.1, 1.1]],
+            ],
+            dtype=float,
+        ),
+    )
+
+    summaries = summarize_posterior_parameters(
+        parameter_names=['beta', 'alpha'],
+        posterior_samples=posterior_samples,
+        best_sample_values=np.array([2.05, 1.05]),
+        parameter_display_names=['Beta width', 'Alpha shift'],
+        convergence_diagnostics={
+            'r_hat_by_parameter': {'beta': 1.02, 'alpha': 1.0},
+            'ess_bulk_by_parameter': {'beta': 120.0, 'alpha': 800.0},
+        },
+    )
+
+    assert [summary.unique_name for summary in summaries] == ['beta', 'alpha']
+    assert [summary.display_name for summary in summaries] == ['Beta width', 'Alpha shift']
+    assert summaries[0].r_hat == pytest.approx(1.02)
+    assert summaries[0].ess_bulk == pytest.approx(120.0)
+    assert summaries[1].r_hat == pytest.approx(1.0)
+    assert summaries[1].ess_bulk == pytest.approx(800.0)
+
+
+def test_summarize_posterior_parameters_validates_display_name_length():
+    from easydiffraction.analysis.fit_helpers.bayesian import PosteriorSamples
+    from easydiffraction.analysis.fit_helpers.bayesian import summarize_posterior_parameters
+
+    posterior_samples = PosteriorSamples(
+        parameter_names=['alpha'],
+        parameter_samples=np.ones((2, 2, 1), dtype=float),
+    )
+
+    with pytest.raises(
+        ValueError,
+        match=r'Posterior display-name list must match the sampled parameter name list length\.',
+    ):
+        summarize_posterior_parameters(
+            parameter_names=['alpha'],
+            posterior_samples=posterior_samples,
+            best_sample_values=np.array([1.0]),
+            parameter_display_names=['Alpha', 'Extra'],
+        )
+
+
+def test_standard_deviations_from_summaries_returns_float_array():
+    from easydiffraction.analysis.fit_helpers.bayesian import PosteriorParameterSummary
+    from easydiffraction.analysis.fit_helpers.bayesian import standard_deviations_from_summaries
+
+    values = standard_deviations_from_summaries([
+        PosteriorParameterSummary(
+            unique_name='a',
+            display_name='A',
+            best_sample_value=1.0,
+            median=1.0,
+            standard_deviation=0.2,
+            interval_68=(0.9, 1.1),
+            interval_95=(0.8, 1.2),
+        ),
+        PosteriorParameterSummary(
+            unique_name='b',
+            display_name='B',
+            best_sample_value=2.0,
+            median=2.0,
+            standard_deviation=0.3,
+            interval_68=(1.9, 2.1),
+            interval_95=(1.8, 2.2),
+        ),
+    ])
+
+    np.testing.assert_allclose(values, np.array([0.2, 0.3]))
+    assert values.dtype == float
+
+
+def test_bayesian_format_helpers_cover_edge_cases():
+    from easydiffraction.analysis.fit_helpers.bayesian import _calculate_fit_quality_metrics
+    from easydiffraction.analysis.fit_helpers.bayesian import _dataset_to_scalar_dict
+    from easydiffraction.analysis.fit_helpers.bayesian import _format_bayesian_overall_status
+    from easydiffraction.analysis.fit_helpers.bayesian import _format_convergence_summary
+    from easydiffraction.analysis.fit_helpers.bayesian import _format_point_estimate_name
+    from easydiffraction.analysis.fit_helpers.bayesian import _format_sampler_settings
+    from easydiffraction.analysis.fit_helpers.bayesian import _maybe_scalar
+
+    dataset = type(
+        'FakeDataset',
+        (),
+        {'data_vars': {'a': np.array([np.nan], dtype=float), 'b': np.array([3.0], dtype=float)}},
+    )
+
+    assert _maybe_scalar(None) is None
+    assert _maybe_scalar(float('inf')) is None
+    assert _maybe_scalar(3.0) == pytest.approx(3.0)
+    assert _dataset_to_scalar_dict(dataset) == {'a': None, 'b': 3.0}
+    assert _format_sampler_settings({}) is None
+    assert (
+        _format_sampler_settings({'steps': 10, 'burn': 2, 'samples': 40})
+        == 'steps=10, burn=2, samples=40'
+    )
+    assert _format_point_estimate_name('map') == 'Best posterior sample'
+    assert _format_point_estimate_name('best_sample') == 'Best posterior sample'
+    assert _format_bayesian_overall_status(
+        success=False,
+        sampler_completed=False,
+        convergence_diagnostics={},
+    ) == ('❌', 'failed')
+    assert _format_bayesian_overall_status(
+        success=True,
+        sampler_completed=False,
+        convergence_diagnostics={'converged': False},
+    ) == ('⚠️', 'completed with warnings')
+    assert _format_bayesian_overall_status(
+        success=True,
+        sampler_completed=True,
+        convergence_diagnostics={'converged': True},
+    ) == ('✅', 'completed')
+    assert _format_bayesian_overall_status(
+        success=True,
+        sampler_completed=False,
+        convergence_diagnostics={},
+    ) == ('✅', 'posterior available')
+    assert _format_convergence_summary({}) is None
+    assert _format_convergence_summary({
+        'converged': False,
+        'max_r_hat': 1.02,
+        'min_ess_bulk': 200.0,
+        'n_draws': 30,
+        'n_chains': 8,
+    }) == (
+        'status=[red]failed[/red], max_r_hat=[red]1.020[/red], '
+        'min_ess_bulk=[red]200.0[/red], draws=30, chains=8'
+    )
+
+    metrics = _calculate_fit_quality_metrics(
+        y_obs=[10.0, 20.0],
+        y_calc=[9.5, 19.5],
+        y_err=[1.0, 1.0],
+        f_obs=[5.0, 6.0],
+        f_calc=[5.1, 5.9],
+    )
+
+    assert metrics['rf'] is not None
+    assert metrics['rf2'] is not None
+    assert metrics['wr'] is not None
+    assert metrics['br'] is not None
+
+
+def test_bayesian_fit_results_display_results_prints_sampler_and_convergence(capsys, monkeypatch):
+    from easydiffraction.analysis.fit_helpers.bayesian import BayesianFitResults
+    from easydiffraction.analysis.fit_helpers.bayesian import PosteriorParameterSummary
+    from easydiffraction.utils.logging import Logger
+
+    monkeypatch.setattr(Logger, '_reaction', Logger.Reaction.WARN, raising=True)
+
+    results = BayesianFitResults(
+        success=True,
+        parameters=[Param(unique_name='a', start=1.0, value=1.2, uncertainty=0.05)],
+        reduced_chi_square=1.2345,
+        fitting_time=0.9876,
+        sampler_name='dream',
+        sampler_completed=True,
+        sampler_settings={
+            'random_seed': 1313900679,
+            'steps': 200,
+            'burn': 50,
+            'thin': 1,
+            'pop': 4,
+            'init': 'lhs',
+            'samples': 3200,
+        },
+        convergence_diagnostics={
+            'converged': False,
+            'max_r_hat': 1.107,
+            'min_ess_bulk': 125.9,
+            'n_draws': 200,
+            'n_chains': 16,
+        },
+        posterior_parameter_summaries=[
+            PosteriorParameterSummary(
+                unique_name='a',
+                display_name='a',
+                best_sample_value=1.2,
+                median=1.15,
+                standard_deviation=0.05,
+                interval_68=(1.1, 1.2),
+                interval_95=(1.0, 1.3),
+                r_hat=1.107,
+                ess_bulk=125.9,
+            )
+        ],
+        best_log_posterior=-12.34,
+    )
+    results.message = 'DREAM sampling completed'
+
+    results.display_results(y_obs=[10.0, 20.0], y_calc=[9.5, 19.5])
+
+    out = _unstyled_output(capsys.readouterr().out)
+    assert 'Bayesian fit results' in out
+    assert 'Overall status: completed with warnings' in out
+    assert 'Sampler status: DREAM sampling completed' in out
+    assert 'Sampler: dream' in out
+    assert 'Sampler completed: yes' in out
+    assert 'steps=200' in out
+    assert 'init=lhs' in out
+    assert 'random_seed=1313900679' not in out
+    assert 'status=failed' in out
+    assert 'max_r_hat=1.107' in out
+    assert 'min_ess_bulk=125.9' in out
+    assert 'Posterior parameter summaries:' in out
+    assert 'Success: True' not in out
+    assert 'datablock' in out
+    assert 'category' in out
+    assert 'entry' in out
+    assert '95% interval' in out
+    assert '68% interval' not in out
+    assert 'std' not in out
+
+    monkeypatch.setattr(Logger, '_reaction', Logger.Reaction.RAISE, raising=True)
+
+
+def test_render_posterior_summary_table_without_summaries_prints_notice(capsys):
+    from easydiffraction.analysis.fit_helpers import bayesian
+
+    bayesian._render_posterior_summary_table(parameters=[], posterior_parameter_summaries=[])
+
+    assert 'No posterior parameter summaries available.' in capsys.readouterr().out
+
+
+def test_build_posterior_summary_row_restores_identifier_columns():
+    from easydiffraction.analysis.fit_helpers.bayesian import PosteriorParameterSummary
+    from easydiffraction.analysis.fit_helpers.bayesian import _build_posterior_summary_row
+
+    parameter = Param(unique_name='a', start=1.0, value=1.2, uncertainty=0.05)
+    summary = PosteriorParameterSummary(
+        unique_name='a',
+        display_name='a',
+        best_sample_value=1.2,
+        median=1.15,
+        standard_deviation=0.05,
+        interval_68=(1.1, 1.2),
+        interval_95=(1.0, 1.3),
+        r_hat=1.107,
+        ess_bulk=125.9,
+    )
+
+    row = _build_posterior_summary_row(summary, {'a': parameter})
+
+    assert row == [
+        'db',
+        'cat',
+        'entry',
+        'a',
+        'arb',
+        '1.1500',
+        '[1.0000, 1.3000]',
+        '[red]1.107[/red]',
+        '[red]125.9[/red]',
+    ]
+
+
+def test_render_committed_parameter_table_places_units_after_parameter(monkeypatch):
+    from easydiffraction.analysis.fit_helpers import bayesian
+
+    captured: dict[str, object] = {}
+
+    def fake_render_table(*, columns_headers, columns_alignment, columns_data):
+        captured['columns_headers'] = columns_headers
+        captured['columns_alignment'] = columns_alignment
+        captured['columns_data'] = columns_data
+
+    monkeypatch.setattr(bayesian, 'render_table', fake_render_table)
+
+    bayesian._render_committed_parameter_table([
+        Param(unique_name='a', start=1.0, value=1.2, uncertainty=0.05)
+    ])
+
+    assert captured['columns_headers'] == [
+        'datablock',
+        'category',
+        'entry',
+        'parameter',
+        'units',
+        'start',
+        'best posterior sample',
+        'uncertainty',
+        'change',
+    ]
+    assert captured['columns_alignment'] == [
+        'left',
+        'left',
+        'left',
+        'left',
+        'left',
+        'right',
+        'right',
+        'right',
+        'right',
+    ]
+    assert captured['columns_data'] == [
+        [
+            'db',
+            'cat',
+            'entry',
+            'a',
+            'arb',
+            '1.0000',
+            '1.2000',
+            '0.0500',
+            '20.00 % ↑',
+        ]
+    ]
+
+
+def test_render_posterior_summary_table_places_units_after_parameter(monkeypatch):
+    from easydiffraction.analysis.fit_helpers import bayesian
+    from easydiffraction.analysis.fit_helpers.bayesian import PosteriorParameterSummary
+
+    captured: dict[str, object] = {}
+
+    def fake_render_table(*, columns_headers, columns_alignment, columns_data):
+        captured['columns_headers'] = columns_headers
+        captured['columns_alignment'] = columns_alignment
+        captured['columns_data'] = columns_data
+
+    monkeypatch.setattr(bayesian, 'render_table', fake_render_table)
+
+    bayesian._render_posterior_summary_table(
+        parameters=[Param(unique_name='a', start=1.0, value=1.2, uncertainty=0.05)],
+        posterior_parameter_summaries=[
+            PosteriorParameterSummary(
+                unique_name='a',
+                display_name='a',
+                best_sample_value=1.2,
+                median=1.15,
+                standard_deviation=0.05,
+                interval_68=(1.1, 1.2),
+                interval_95=(1.0, 1.3),
+                r_hat=1.107,
+                ess_bulk=125.9,
+            )
+        ],
+    )
+
+    assert captured['columns_headers'] == [
+        'datablock',
+        'category',
+        'entry',
+        'parameter',
+        'units',
+        'median',
+        '95% interval',
+        'r-hat',
+        'ess bulk',
+    ]
+    assert captured['columns_alignment'] == [
+        'left',
+        'left',
+        'left',
+        'left',
+        'left',
+        'right',
+        'right',
+        'right',
+        'right',
+    ]
+    assert captured['columns_data'] == [
+        [
+            'db',
+            'cat',
+            'entry',
+            'a',
+            'arb',
+            '1.1500',
+            '[1.0000, 1.3000]',
+            '[red]1.107[/red]',
+            '[red]125.9[/red]',
+        ]
+    ]
+
+
+def test_posterior_table_notes_split_failed_diagnostics():
+    from easydiffraction.analysis.fit_helpers.bayesian import PosteriorParameterSummary
+    from easydiffraction.analysis.fit_helpers.bayesian import _posterior_table_notes
+
+    notes = _posterior_table_notes([
+        PosteriorParameterSummary(
+            unique_name='a',
+            display_name='a',
+            best_sample_value=1.0,
+            median=1.0,
+            standard_deviation=0.1,
+            interval_68=(0.9, 1.1),
+            interval_95=(0.8, 1.2),
+            r_hat=1.02,
+            ess_bulk=100.0,
+        )
+    ])
+
+    assert len(notes) == 2
+    assert 'r-hat' in notes[0]
+    assert 'ess bulk' in notes[1]
+
+
+def test_bayesian_helpers_cover_non_warning_and_default_display_paths():
+    from easydiffraction.analysis.fit_helpers.bayesian import PosteriorParameterSummary
+    from easydiffraction.analysis.fit_helpers.bayesian import _build_posterior_summary_row
+    from easydiffraction.analysis.fit_helpers.bayesian import _format_ess_bulk
+    from easydiffraction.analysis.fit_helpers.bayesian import _format_r_hat
+    from easydiffraction.analysis.fit_helpers.bayesian import _posterior_table_notes
+
+    summary = PosteriorParameterSummary(
+        unique_name='missing',
+        display_name='Missing',
+        best_sample_value=1.0,
+        median=1.0,
+        standard_deviation=0.1,
+        interval_68=(0.9, 1.1),
+        interval_95=(0.8, 1.2),
+        r_hat=1.0,
+        ess_bulk=500.0,
+    )
+
+    row = _build_posterior_summary_row(summary, {})
+
+    assert row == [
+        'N/A',
+        'N/A',
+        '',
+        'Missing',
+        'N/A',
+        '1.0000',
+        '[0.8000, 1.2000]',
+        '1.000',
+        '500.0',
+    ]
+    assert _format_r_hat(None) == 'N/A'
+    assert _format_r_hat(1.0) == '1.000'
+    assert _format_ess_bulk(None) == 'N/A'
+    assert _format_ess_bulk(500.0) == '500.0'
+    assert _posterior_table_notes([]) == []
+    assert _posterior_table_notes([summary]) == []
+
+
+def test_fitresults_display_results_prints_and_table(capsys):
+    from easydiffraction.analysis.fit_helpers.reporting import FitResults
+
+    params = [Param(unique_name='a', start=1.0, value=1.2, uncertainty=0.05)]
+
+    results = FitResults(
+        success=True,
+        parameters=params,
+        reduced_chi_square=1.2345,
+        fitting_time=0.9876,
+    )
+
+    results.display_results(
+        y_obs=[10.0, 20.0],
+        y_calc=[9.5, 19.5],
+        y_err=[1.0, 1.0],
+        f_obs=[5.0, 6.0],
+        f_calc=[5.1, 5.9],
+    )
+
+    out = _unstyled_output(capsys.readouterr().out)
+    assert 'Fit results' in out
+    assert 'Success: True' in out
+    assert 'reduced χ²' in out
+    assert 'R-factor (Rf)' in out
+    assert 'R-factor squared (Rf²)' in out
+    assert 'Weighted R-factor (wR)' in out
+    assert 'Bragg R-factor (BR)' in out
+    assert 'Fitted parameters:' in out
+    assert any(char in out for char in ('╒', '┌', '+', '─'))
+
+
+def test_fitresults_display_results_places_units_after_parameter(monkeypatch):
+    from easydiffraction.analysis.fit_helpers import reporting
+
+    captured: dict[str, object] = {}
+
+    def fake_render_table(*, columns_headers, columns_alignment, columns_data):
+        captured['columns_headers'] = columns_headers
+        captured['columns_alignment'] = columns_alignment
+        captured['columns_data'] = columns_data
+
+    monkeypatch.setattr(reporting, 'render_table', fake_render_table)
+
+    reporting.FitResults(
+        success=True,
+        parameters=[Param(unique_name='a', start=1.0, value=1.2, uncertainty=0.05)],
+    ).display_results()
+
+    assert captured['columns_headers'] == [
+        'datablock',
+        'category',
+        'entry',
+        'parameter',
+        'units',
+        'start',
+        'fitted',
+        'uncertainty',
+        'change',
+    ]
+    assert captured['columns_alignment'] == [
+        'left',
+        'left',
+        'left',
+        'left',
+        'left',
+        'right',
+        'right',
+        'right',
+        'right',
+    ]
+    assert captured['columns_data'] == [
+        [
+            'db',
+            'cat',
+            'entry',
+            'a',
+            'arb',
+            '1.0000',
+            '1.2000',
+            '0.0500',
+            '20.00 % ↑',
+        ]
+    ]
diff --git a/tests/integration/fitting/test_bayesian_tracker_and_base.py b/tests/integration/fitting/test_bayesian_tracker_and_base.py
new file mode 100644
index 000000000..91ec0e572
--- /dev/null
+++ b/tests/integration/fitting/test_bayesian_tracker_and_base.py
@@ -0,0 +1,511 @@
+# SPDX-FileCopyrightText: 2026 EasyScience contributors 
+# SPDX-License-Identifier: BSD-3-Clause
+
+from __future__ import annotations
+
+from dataclasses import dataclass
+
+import numpy as np
+import pytest
+
+
+class DummyParam:
+    def __init__(self, value: float) -> None:
+        self.value = value
+        self.fit_min = -np.inf
+        self.fit_max = np.inf
+        self.unique_name = f'param_{value}'
+
+    def _physical_lower_bound(self) -> float:
+        return -np.inf
+
+    def _physical_upper_bound(self) -> float:
+        return np.inf
+
+
+def test_tracker_terminal_flow_prints_and_updates_best(monkeypatch, capsys):
+    import easydiffraction.analysis.fit_helpers.tracking as tracking_mod
+    from easydiffraction.analysis.fit_helpers.tracking import FitProgressTracker
+
+    events: list[tuple[str, object]] = []
+
+    class FakeIndicator:
+        def __init__(self, label, *, verbosity, display_handle=None):
+            del display_handle
+            events.append(('init', label, verbosity))
+
+        def start(self):
+            events.append(('start', None))
+
+        def update(self, *, label=None, content=None):
+            events.append(('update', label))
+            del content
+
+        def stop(self):
+            events.append(('stop', None))
+
+    monkeypatch.setattr(tracking_mod, 'ActivityIndicator', FakeIndicator)
+    monkeypatch.setattr(tracking_mod, 'build_table_renderable', lambda **kwargs: 'table')
+
+    tracker = FitProgressTracker()
+    tracker.start_tracking('dummy')
+    tracker.start_timer()
+
+    tracker.track(np.array([2.0, 1.0]), parameters=[1])
+    out = capsys.readouterr().out
+    assert 'Goodness-of-fit' in out
+
+    tracker.track(np.array([1.9, 1.0]), parameters=[1])
+    tracker.track(np.array([0.1, 0.1]), parameters=[1])
+
+    tracker.stop_timer()
+    tracker.finish_tracking()
+
+    out = capsys.readouterr().out
+    assert 'Best goodness-of-fit' in out
+    assert tracker.best_iteration is not None
+    assert ('init', tracking_mod.ACTIVITY_LABEL_FITTING, tracker._verbosity) in events
+
+
+def test_tracker_sampler_progress_renders_and_completes(monkeypatch, capsys):
+    import easydiffraction.analysis.fit_helpers.tracking as tracking_mod
+    from easydiffraction.analysis.fit_helpers.tracking import FitProgressTracker
+    from easydiffraction.analysis.fit_helpers.tracking import SamplerProgressUpdate
+
+    events: list[tuple[str, object]] = []
+
+    class FakeIndicator:
+        def __init__(self, label, *, verbosity, display_handle=None):
+            del display_handle
+            events.append(('init', label, verbosity))
+
+        def start(self):
+            events.append(('start', None))
+
+        def update(self, *, label=None, content=None):
+            events.append(('update', label))
+            del content
+
+        def stop(self):
+            events.append(('stop', None))
+
+    monkeypatch.setattr(tracking_mod, 'ActivityIndicator', FakeIndicator)
+    monkeypatch.setattr(tracking_mod, 'build_table_renderable', lambda **kwargs: 'table')
+
+    tracker = FitProgressTracker()
+    tracker.start_tracking('dream', mode='sampling')
+    tracker.start_timer()
+    tracker.track_sampler_progress(
+        SamplerProgressUpdate(
+            iteration=1,
+            total_iterations=10,
+            phase='burn-in',
+            progress_percent=10.0,
+            log_posterior=-12.0,
+            reduced_chi2=5.0,
+            elapsed_time=0.0,
+            force_report=True,
+        )
+    )
+    tracker.track_sampler_progress(
+        SamplerProgressUpdate(
+            iteration=10,
+            total_iterations=10,
+            phase='sampling',
+            progress_percent=100.0,
+            log_posterior=-3.0,
+            reduced_chi2=1.0,
+            elapsed_time=6.0,
+            force_report=False,
+        )
+    )
+    tracker.stop_timer()
+    tracker.finish_tracking()
+
+    out = capsys.readouterr().out
+    assert 'Bayesian sampling progress' in out
+    assert 'Bayesian sampling complete.' in out
+    assert tracker.best_chi2 == pytest.approx(1.0)
+    assert tracker.best_iteration == 10
+    assert ('init', tracking_mod.ACTIVITY_LABEL_PROCESSING, tracker._verbosity) in events
+    assert ('update', tracking_mod.ACTIVITY_LABEL_BURN_IN) in events
+    assert ('update', tracking_mod.ACTIVITY_LABEL_SAMPLING) in events
+
+
+def test_tracker_helper_error_paths_and_short_mode(monkeypatch):
+    import easydiffraction.analysis.fit_helpers.tracking as tracking_mod
+    from easydiffraction.analysis.fit_helpers.tracking import FitProgressTracker
+    from easydiffraction.utils.enums import VerbosityEnum
+
+    events: list[tuple[str, object]] = []
+
+    class FakeIndicator:
+        def __init__(self, label, *, verbosity, display_handle=None):
+            del display_handle
+            events.append(('init', label, verbosity))
+
+        def start(self):
+            events.append(('start', None))
+
+        def update(self, *, label=None, content=None):
+            events.append(('update', label))
+            del content
+
+        def stop(self):
+            events.append(('stop', None))
+
+    monkeypatch.setattr(tracking_mod, 'ActivityIndicator', FakeIndicator)
+
+    tracker = FitProgressTracker()
+    tracker._verbosity = VerbosityEnum.SHORT
+    tracker.stop_timer()
+    tracker.start_tracking('dream', mode='sampling')
+    tracker.finish_tracking()
+
+    with pytest.raises(RuntimeError, match='Sampler progress is unavailable'):
+        FitProgressTracker()._resolved_final_sampler_progress()
+    with pytest.raises(RuntimeError, match='Sampler iteration labels require'):
+        FitProgressTracker()._sampler_iteration_label(1)
+
+    assert FitProgressTracker._rows_match_on_columns(['1', 'a'], ['1', 'b'], (0,)) is True
+    assert events == [
+        ('init', tracking_mod.ACTIVITY_LABEL_PROCESSING, VerbosityEnum.SHORT),
+        ('start', None),
+        ('update', tracking_mod.ACTIVITY_LABEL_PROCESSING),
+        ('stop', None),
+    ]
+
+
+def test_tracker_final_sampler_row_replaces_last_row():
+    from easydiffraction.analysis.fit_helpers.tracking import FitProgressTracker
+
+    tracker = FitProgressTracker()
+    tracker._tracking_mode = 'sampling'
+    tracker._sampler_total_iterations = 10
+    tracker._last_iteration = 10
+    tracker._last_sampler_progress_percent = 100.0
+    tracker._last_sampler_log_posterior = -3.0
+    tracker._last_sampler_phase = 'sampling'
+    tracker._last_sampler_elapsed_time = 5.0
+    tracker._df_rows = [['10/10', '90.0%', '4.00', '-3.00', 'sampling']]
+
+    tracker.finish_tracking()
+
+    assert tracker._df_rows[-1] == ['10/10', '100.0%', '5.00', '-3.00', 'sampling']
+
+
+def test_make_display_handle_uses_terminal_live_when_available(monkeypatch):
+    import easydiffraction.analysis.fit_helpers.tracking as tracking_mod
+
+    sentinel = object()
+
+    monkeypatch.setattr(tracking_mod, 'make_display_handle', lambda: sentinel)
+
+    assert tracking_mod._make_display_handle() is sentinel
+
+
+def test_tracker_misc_helper_paths(monkeypatch):
+    import easydiffraction.analysis.fit_helpers.tracking as tracking_mod
+    from easydiffraction.analysis.fit_helpers.tracking import FitProgressTracker
+    from easydiffraction.utils.enums import VerbosityEnum
+
+    render_calls: list[dict[str, object]] = []
+    update_calls: list[dict[str, object]] = []
+    monkeypatch.setattr(
+        tracking_mod, 'calculate_reduced_chi_square', lambda residuals, n_params: 3.0
+    )
+    monkeypatch.setattr(
+        tracking_mod,
+        'build_table_renderable',
+        lambda **kwargs: render_calls.append(kwargs) or 'renderable',
+    )
+
+    class FakeIndicator:
+        def update(self, *, label=None, content=None):
+            update_calls.append({'label': label, 'content': content})
+
+    tracker = FitProgressTracker()
+    tracker._tracking_mode = tracking_mod.TRACKING_MODE_SAMPLER
+    tracker._previous_chi2 = 5.0
+    tracker._best_chi2 = 5.0
+
+    residuals = np.array([1.0, -1.0], dtype=float)
+
+    assert np.array_equal(tracker.track(residuals, [1.0]), residuals)
+    assert tracker.best_chi2 == pytest.approx(3.0)
+
+    tracker.start_timer()
+    tracker.stop_timer()
+    assert tracker.fitting_time is not None
+
+    tracker.reset()
+    tracker._tracking_mode = tracking_mod.TRACKING_MODE_SAMPLER
+    assert tracker._headers() == tracking_mod.SAMPLER_HEADERS
+    assert tracker._alignments() == tracking_mod.SAMPLER_ALIGNMENTS
+    assert tracker._current_elapsed_time() is None
+    assert tracker._format_elapsed_time() == ''
+
+    tracker._verbosity = VerbosityEnum.FULL
+    tracker._activity_indicator = FakeIndicator()
+    tracker._replace_last_tracking_row(['1'])
+
+    assert tracker._df_rows == [['1']]
+    assert len(render_calls) == 1
+    assert update_calls == [
+        {
+            'label': tracking_mod.ACTIVITY_LABEL_FITTING,
+            'content': 'renderable',
+        }
+    ]
+
+
+def test_tracker_final_rows_cover_fallbacks_and_activity_labels():
+    import easydiffraction.analysis.fit_helpers.tracking as tracking_mod
+    from easydiffraction.analysis.fit_helpers.tracking import FitProgressTracker
+
+    tracker = FitProgressTracker()
+    tracker._tracking_mode = tracking_mod.TRACKING_MODE_SAMPLER
+    tracker._sampler_total_iterations = 10
+    tracker._last_iteration = 8
+    tracker._last_sampler_elapsed_time = 2.5
+
+    assert tracker._final_sampler_tracking_row() == ['8/10', '80.0%', '2.50', '', 'sampling']
+
+    tracker._tracking_mode = tracking_mod.TRACKING_MODE_FIT
+    tracker._fitting_time = 1.5
+    assert tracker._final_fit_tracking_row() == ['8', '1.50', '', '']
+    tracker._tracking_mode = tracking_mod.TRACKING_MODE_SAMPLER
+    assert tracker._default_activity_label() == tracking_mod.ACTIVITY_LABEL_PROCESSING
+    tracker._tracking_mode = tracking_mod.TRACKING_MODE_FIT
+    assert tracker._default_activity_label() == tracking_mod.ACTIVITY_LABEL_FITTING
+    assert (
+        tracker._activity_label_for_sampler_phase('burn-in') == tracking_mod.ACTIVITY_LABEL_BURN_IN
+    )
+    assert (
+        tracker._activity_label_for_sampler_phase('sampling')
+        == tracking_mod.ACTIVITY_LABEL_SAMPLING
+    )
+    assert tracker._activity_label_for_sampler_phase('annealing') == 'annealing'
+    assert tracker._activity_label_for_sampler_phase('') == tracking_mod.ACTIVITY_LABEL_SAMPLING
+
+
+def test_minimizer_base_fit_flow_and_finalize():
+    from easydiffraction.analysis.minimizers.base import MinimizerBase
+
+    @dataclass
+    class DummyResult:
+        success: bool = True
+
+    class DummyMinimizer(MinimizerBase):
+        def __init__(self) -> None:
+            super().__init__(name='dummy', method='m', max_iterations=5)
+            self.synced = False
+
+        def _prepare_solver_args(self, parameters):
+            return {'engine_parameters': {'ok': True}}
+
+        def _run_solver(self, objective_function, **kwargs):
+            residuals = objective_function(kwargs.get('engine_parameters'))
+            self.tracker.track(residuals=np.array(residuals), parameters=[1])
+            return DummyResult(success=True)
+
+        def _sync_result_to_parameters(self, parameters, raw_result):
+            self.synced = True
+            if parameters:
+                parameters[0].value = 42
+
+        def _check_success(self, raw_result):
+            return getattr(raw_result, 'success', False)
+
+        def _compute_residuals(
+            self, engine_params, parameters, structures, experiments, calculator
+        ):
+            assert engine_params == {'ok': True}
+            return np.array([0.0, 0.0])
+
+    minimizer = DummyMinimizer()
+    params = [DummyParam(1.0), DummyParam(2.0)]
+    objective = minimizer._create_objective_function(
+        parameters=params,
+        structures=None,
+        experiments=None,
+        calculator=None,
+    )
+
+    result = minimizer.fit(parameters=params, objective_function=objective)
+
+    assert result.success is True
+    assert minimizer.synced is True
+    assert isinstance(result.parameters, list)
+    assert result.parameters[0].value == 42
+    assert minimizer.tracker.fitting_time is not None
+    assert minimizer.tracker.fitting_time >= 0.0
+
+
+def test_minimizer_base_create_objective_function_uses_compute_residuals():
+    from easydiffraction.analysis.minimizers.base import MinimizerBase
+
+    class Minimizer(MinimizerBase):
+        def _prepare_solver_args(self, parameters):
+            return {}
+
+        def _run_solver(self, objective_function, **kwargs):
+            return None
+
+        def _sync_result_to_parameters(self, parameters, raw_result):
+            return None
+
+        def _check_success(self, raw_result):
+            return True
+
+        def _compute_residuals(
+            self, engine_params, parameters, structures, experiments, calculator
+        ):
+            return np.array([1.0, 2.0, 3.0])
+
+    minimizer = Minimizer()
+    objective = minimizer._create_objective_function(
+        parameters=[],
+        structures=None,
+        experiments=None,
+        calculator=None,
+    )
+
+    np.testing.assert_allclose(objective({}), np.array([1.0, 2.0, 3.0]))
+
+
+def test_minimizer_base_fit_stops_tracking_when_solver_prep_fails():
+    from easydiffraction.analysis.minimizers.base import MinimizerBase
+
+    class Minimizer(MinimizerBase):
+        def __init__(self) -> None:
+            super().__init__(name='dummy', method='m', max_iterations=5)
+            self.started = False
+            self.stopped = False
+
+        def _start_tracking(self, minimizer_name, verbosity=None):
+            self.started = True
+
+        def _stop_tracking(self):
+            self.stopped = True
+
+        def _prepare_solver_args(self, parameters):
+            message = 'prep failed'
+            raise ValueError(message)
+
+        def _run_solver(self, objective_function, **kwargs):
+            message = 'should not run solver'
+            raise AssertionError(message)
+
+        def _sync_result_to_parameters(self, parameters, raw_result):
+            return None
+
+        def _check_success(self, raw_result):
+            return True
+
+    minimizer = Minimizer()
+
+    with pytest.raises(ValueError, match='prep failed'):
+        minimizer.fit(parameters=[DummyParam(1.0)], objective_function=lambda _: np.array([0.0]))
+
+    assert minimizer.started is True
+    assert minimizer.stopped is True
+
+
+def test_minimizer_base_applies_physical_limits_and_warns(monkeypatch):
+    from easydiffraction.analysis.minimizers.base import MinimizerBase
+
+    warnings: list[str] = []
+    monkeypatch.setattr(
+        'easydiffraction.analysis.minimizers.base.log.warning',
+        lambda message: warnings.append(message),
+    )
+
+    class BoundaryParam(DummyParam):
+        def __init__(self) -> None:
+            super().__init__(5.0)
+            self.unique_name = 'boundary'
+
+        def _physical_lower_bound(self) -> float:
+            return 0.0
+
+        def _physical_upper_bound(self) -> float:
+            return 10.0
+
+    class DummyResult:
+        success = True
+
+    class Minimizer(MinimizerBase):
+        def __init__(self) -> None:
+            super().__init__(name='dummy', method='m', max_iterations=5)
+
+        def _prepare_solver_args(self, parameters):
+            return {'engine_parameters': {}}
+
+        def _run_solver(self, objective_function, **kwargs):
+            residuals = objective_function(kwargs.get('engine_parameters'))
+            self.tracker.track(residuals=np.array(residuals), parameters=[1])
+            return DummyResult()
+
+        def _sync_result_to_parameters(self, parameters, raw_result):
+            parameters[0].value = 11.0
+
+        def _check_success(self, raw_result):
+            return True
+
+        def _compute_residuals(
+            self, engine_params, parameters, structures, experiments, calculator
+        ):
+            return np.array([0.0, 0.0])
+
+    minimizer = Minimizer()
+    parameter = BoundaryParam()
+    objective = minimizer._create_objective_function(
+        parameters=[parameter],
+        structures=None,
+        experiments=None,
+        calculator=None,
+    )
+
+    result = minimizer.fit(
+        parameters=[parameter],
+        objective_function=objective,
+        use_physical_limits=True,
+    )
+
+    assert result.success is True
+    assert parameter.fit_min == 0.0
+    assert parameter.fit_max == 10.0
+    assert parameter._outside_physical_limits is True
+    assert any('upper bound' in message for message in warnings)
+    assert any('physical upper limit' in message for message in warnings)
+
+
+def test_minimizer_base_rejects_random_seed_when_not_supported():
+    from easydiffraction.analysis.minimizers.base import MinimizerBase
+
+    class Minimizer(MinimizerBase):
+        def _prepare_solver_args(self, parameters):
+            return {'engine_parameters': {}}
+
+        def _run_solver(self, objective_function, **kwargs):
+            return object()
+
+        def _sync_result_to_parameters(self, parameters, raw_result):
+            return None
+
+        def _check_success(self, raw_result):
+            return True
+
+        def _compute_residuals(
+            self, engine_params, parameters, structures, experiments, calculator
+        ):
+            return np.array([0.0])
+
+    minimizer = Minimizer(name='dummy')
+
+    with pytest.raises(
+        ValueError,
+        match=r"Minimizer 'dummy' does not support random_seed\.",
+    ):
+        minimizer.fit(parameters=[], objective_function=lambda _: np.array([0.0]), random_seed=7)
diff --git a/tests/integration/fitting/test_bumps_dream_support.py b/tests/integration/fitting/test_bumps_dream_support.py
new file mode 100644
index 000000000..a824347e1
--- /dev/null
+++ b/tests/integration/fitting/test_bumps_dream_support.py
@@ -0,0 +1,711 @@
+# SPDX-FileCopyrightText: 2026 EasyScience contributors 
+# SPDX-License-Identifier: BSD-3-Clause
+
+from __future__ import annotations
+
+import threading
+from types import SimpleNamespace
+from unittest.mock import MagicMock
+from unittest.mock import patch
+
+import numpy as np
+import pytest
+
+
+class FakeParam:
+    def __init__(
+        self,
+        uid: str,
+        value: float,
+        uncertainty: float | None = None,
+        *,
+        fit_min: float | None = 0.0,
+        fit_max: float | None = 1.0,
+    ) -> None:
+        self._minimizer_uid = uid
+        self.unique_name = uid
+        self.name = uid.upper()
+        self.value = value
+        self.uncertainty = uncertainty
+        self.fit_min = fit_min
+        self.fit_max = fit_max
+
+    def _set_value_from_minimizer(self, value: float) -> None:
+        self.value = value
+
+
+def _simulate_import_safe_spawn_main_module(monkeypatch) -> None:
+    import sys
+
+    monkeypatch.setattr(
+        'easydiffraction.analysis.minimizers.bumps_dream.multiprocessing.get_start_method',
+        lambda allow_none=True: 'spawn',
+    )
+    monkeypatch.setitem(
+        sys.modules,
+        '__main__',
+        SimpleNamespace(__file__='pytest_runner.py', __spec__=object()),
+    )
+
+
+def test_type_info_and_default_init():
+    from easydiffraction.analysis.minimizers.bumps_dream import BumpsDreamMinimizer
+    from easydiffraction.analysis.minimizers.enums import DreamPopulationInitializationEnum
+    from easydiffraction.analysis.minimizers.enums import MinimizerTypeEnum
+
+    minimizer = BumpsDreamMinimizer()
+
+    assert minimizer.type_info.tag == MinimizerTypeEnum.BUMPS_DREAM
+    assert minimizer.init is DreamPopulationInitializationEnum.LHS
+    assert minimizer.steps == 3000
+
+
+def test_dream_progress_monitor_allocates_rows_by_phase_ratio():
+    from easydiffraction.analysis.minimizers.bumps_dream import _DreamProgressMonitor
+
+    monitor = _DreamProgressMonitor(
+        tracker=MagicMock(),
+        n_points=100,
+        n_parameters=3,
+        total_generations=100,
+        burn_steps=40,
+    )
+
+    assert len(monitor._burn_targets) == 10
+    assert len(monitor._sampling_targets) == 15
+
+
+def test_dream_progress_monitor_helper_edge_cases():
+    from easydiffraction.analysis.minimizers.bumps_dream import _DreamProgressMonitor
+
+    monitor = _DreamProgressMonitor(
+        tracker=MagicMock(),
+        n_points=2,
+        n_parameters=3,
+        total_generations=10,
+        burn_steps=2,
+    )
+
+    assert _DreamProgressMonitor._progress_targets(start=3, stop=2, target_count=2) == []
+    assert _DreamProgressMonitor._progress_targets(start=1, stop=5, target_count=0) == []
+    assert _DreamProgressMonitor._phase_progress_point_counts(
+        total_generations=10, burn_steps=0
+    ) == (0, 10)
+    assert _DreamProgressMonitor._phase_progress_point_counts(
+        total_generations=10, burn_steps=10
+    ) == (10, 0)
+    assert _DreamProgressMonitor._population_mean_log_posterior(
+        SimpleNamespace(population_values=[], value=[2.5])
+    ) == pytest.approx(-2.5)
+    assert _DreamProgressMonitor._population_mean_log_posterior(
+        SimpleNamespace(population_values=[np.array([np.nan, np.inf])], value=[1.5])
+    ) == pytest.approx(-1.5)
+    assert monitor._reduced_chi_square_from_nllf(4.0) == pytest.approx(8.0)
+
+
+def test_init_accepts_enum_or_string_and_rejects_invalid():
+    from easydiffraction.analysis.minimizers.bumps_dream import BumpsDreamMinimizer
+    from easydiffraction.analysis.minimizers.enums import DreamPopulationInitializationEnum
+
+    minimizer = BumpsDreamMinimizer()
+
+    minimizer.init = DreamPopulationInitializationEnum.LHS
+    assert minimizer.init is DreamPopulationInitializationEnum.LHS
+
+    minimizer.init = 'random'
+    assert minimizer.init is DreamPopulationInitializationEnum.RANDOM
+
+    with pytest.raises(
+        ValueError,
+        match=r"DREAM setting 'init' must be one of: eps, cov, lhs, random\.",
+    ):
+        minimizer.init = 'bad-init'
+
+
+def test_resolve_random_seed_returns_provided_or_generated(monkeypatch):
+    from easydiffraction.analysis.minimizers.bumps_dream import BumpsDreamMinimizer
+
+    minimizer = BumpsDreamMinimizer()
+
+    assert minimizer._resolve_random_seed(17) == 17
+    assert minimizer._resolved_random_seed == 17
+
+    generator = SimpleNamespace(integers=lambda *args, **kwargs: 123456)
+    monkeypatch.setattr(np.random, 'default_rng', lambda: generator)
+
+    assert minimizer._resolve_random_seed(None) == 123456
+    assert minimizer._resolved_random_seed == 123456
+
+
+def test_dream_numeric_validators_reject_boolean_inputs():
+    from easydiffraction.analysis.minimizers.bumps_dream import BumpsDreamMinimizer
+
+    minimizer = BumpsDreamMinimizer()
+
+    with pytest.raises(
+        TypeError,
+        match=r"DREAM setting 'steps' must be a positive integer\.",
+    ):
+        minimizer.steps = True
+    with pytest.raises(
+        TypeError,
+        match=r"DREAM setting 'parallel' must be a non-negative integer\.",
+    ):
+        minimizer.parallel = True
+    with pytest.raises(TypeError, match='DREAM random_seed must be an integer'):
+        minimizer._validated_random_seed_value(random_seed=True)
+
+
+@pytest.mark.parametrize('seed', [-1, np.iinfo(np.uint32).max + 1])
+def test_resolve_random_seed_rejects_out_of_range_values(seed):
+    from easydiffraction.analysis.minimizers.bumps_dream import BumpsDreamMinimizer
+
+    minimizer = BumpsDreamMinimizer()
+
+    with pytest.raises(
+        ValueError,
+        match=r'DREAM random_seed must be an integer between 0 and 4294967295\.',
+    ):
+        minimizer._resolve_random_seed(seed)
+
+
+def test_resolved_burn_uses_auto_or_explicit_and_validates():
+    from easydiffraction.analysis.minimizers.bumps_dream import BumpsDreamMinimizer
+
+    minimizer = BumpsDreamMinimizer()
+
+    assert minimizer._resolved_burn(steps=200) == 50
+
+    minimizer.burn = 20
+    assert minimizer._resolved_burn(steps=200) == 20
+
+    minimizer.burn = 200
+    with pytest.raises(
+        ValueError,
+        match=r"DREAM setting 'burn' must be smaller than 'steps'\.",
+    ):
+        minimizer._resolved_burn(steps=200)
+
+
+def test_sampler_settings_include_init_and_sample_count():
+    from easydiffraction.analysis.minimizers.bumps_dream import BumpsDreamMinimizer
+    from easydiffraction.analysis.minimizers.enums import DreamPopulationInitializationEnum
+
+    minimizer = BumpsDreamMinimizer()
+    minimizer.thin = 1
+    minimizer.pop = 4
+    minimizer.init = DreamPopulationInitializationEnum.LHS
+
+    settings = minimizer._sampler_settings(
+        random_seed=7,
+        steps=10,
+        burn=2,
+        n_parameters=3,
+    )
+
+    assert settings['random_seed'] == 7
+    assert settings['parallel'] == 0
+    assert settings['init'] == 'lhs'
+    assert settings['samples'] == 120
+
+
+@pytest.mark.parametrize(
+    ('fit_min', 'fit_max', 'value', 'message'),
+    [
+        (None, 1.0, 0.5, r'fit_min must be finite'),
+        (0.0, np.inf, 0.5, r'fit_max must be finite'),
+        (2.0, 1.0, 1.5, r'fit_min \(2\.0\) must be smaller than fit_max \(1\.0\)'),
+        (0.0, 1.0, 2.0, r'starting value 2\.0 is outside \[0\.0, 1\.0\]'),
+    ],
+)
+def test_prepare_solver_args_rejects_invalid_dream_bounds(fit_min, fit_max, value, message):
+    from easydiffraction.analysis.minimizers.bumps_dream import BumpsDreamMinimizer
+
+    minimizer = BumpsDreamMinimizer()
+    parameter = FakeParam('alpha', value, fit_min=fit_min, fit_max=fit_max)
+
+    with pytest.raises(ValueError, match=message):
+        minimizer._prepare_solver_args([parameter])
+
+
+def test_prepare_solver_args_lists_all_offending_dream_parameters():
+    from easydiffraction.analysis.minimizers.bumps_dream import BumpsDreamMinimizer
+
+    minimizer = BumpsDreamMinimizer()
+    parameters = [
+        FakeParam('alpha', 2.0, fit_min=0.0, fit_max=1.0),
+        FakeParam('beta', 0.5, fit_min=None, fit_max=1.0),
+    ]
+
+    with pytest.raises(
+        ValueError,
+        match=r'alpha: .*outside \[0\.0, 1\.0\][\s\S]*beta: .*fit_min',
+    ):
+        minimizer._prepare_solver_args(parameters)
+
+
+def test_sync_result_to_parameters_restores_starting_values_on_failure():
+    from easydiffraction.analysis.minimizers.bumps_dream import BumpsDreamMinimizer
+
+    minimizer = BumpsDreamMinimizer()
+    parameters = [FakeParam('a', 10.0, uncertainty=0.7), FakeParam('b', 20.0, uncertainty=0.8)]
+    raw_result = SimpleNamespace(
+        x=np.array([99.0, 88.0]),
+        success=False,
+        starting_values=np.array([1.5, 2.5]),
+        starting_uncertainties=[0.1, None],
+    )
+
+    minimizer._sync_result_to_parameters(parameters, raw_result)
+
+    assert parameters[0].value == 1.5
+    assert parameters[0].uncertainty == 0.1
+    assert parameters[1].value == 2.5
+    assert parameters[1].uncertainty is None
+
+
+def test_build_mapper_falls_back_for_serial_and_unpicklable(monkeypatch):
+    from easydiffraction.analysis.minimizers.bumps_dream import BumpsDreamMinimizer
+
+    minimizer = BumpsDreamMinimizer()
+    minimizer.parallel = 1
+    assert minimizer._build_mapper('problem') is None
+
+    warnings: list[str] = []
+    minimizer.parallel = 0
+    _simulate_import_safe_spawn_main_module(monkeypatch)
+    monkeypatch.setattr(
+        'easydiffraction.analysis.minimizers.bumps_dream.can_pickle', lambda problem: False
+    )
+    monkeypatch.setattr(minimizer, '_warn_after_tracking', warnings.append)
+
+    assert minimizer._build_mapper('problem') is None
+    assert warnings == [
+        'DREAM parallel evaluation requires a picklable problem; falling back to serial execution.'
+    ]
+
+
+def test_build_mapper_temporarily_clears_shared_display_handle(monkeypatch):
+    from easydiffraction.analysis.minimizers.bumps_dream import BumpsDreamMinimizer
+
+    minimizer = BumpsDreamMinimizer()
+    minimizer.parallel = 0
+    _simulate_import_safe_spawn_main_module(monkeypatch)
+    handle = object()
+    activity_indicator = object()
+    observed_tracker_state: list[tuple[object | None, object | None]] = []
+
+    minimizer.tracker._set_shared_display_handle(handle)
+    minimizer.tracker._activity_indicator = activity_indicator
+    monkeypatch.setattr(
+        'easydiffraction.analysis.minimizers.bumps_dream.can_pickle',
+        lambda problem: (
+            observed_tracker_state.append((
+                minimizer.tracker._shared_display_handle,
+                minimizer.tracker._activity_indicator,
+            ))
+            or True
+        ),
+    )
+    monkeypatch.setattr(
+        'easydiffraction.analysis.minimizers.bumps_dream.MPMapper.start_mapper',
+        lambda problem, args, cpus: ('mapper', cpus),
+    )
+
+    mapper = minimizer._build_mapper('problem')
+
+    assert mapper == ('mapper', 0)
+    assert observed_tracker_state == [(None, None)]
+    assert minimizer.tracker._shared_display_handle is handle
+    assert minimizer.tracker._activity_indicator is activity_indicator
+
+
+def test_build_mapper_allows_real_can_pickle_with_live_tracker_state(monkeypatch):
+    from bumps.fitproblem import FitProblem
+    from bumps.parameter import Parameter as BumpsParameter
+
+    from easydiffraction.analysis.minimizers.bumps import _EasyDiffractionFitness
+    from easydiffraction.analysis.minimizers.bumps_dream import BumpsDreamMinimizer
+
+    minimizer = BumpsDreamMinimizer()
+    minimizer.parallel = 0
+    _simulate_import_safe_spawn_main_module(monkeypatch)
+    bumps_params = [BumpsParameter(value=1.0, name='alpha')]
+
+    def objective(values):
+        _ = minimizer.tracker._activity_indicator
+        return np.array([float(values[0])], dtype=float)
+
+    problem = FitProblem(_EasyDiffractionFitness(bumps_params, objective))
+    minimizer.tracker._set_shared_display_handle(threading.RLock())
+    minimizer.tracker._activity_indicator = SimpleNamespace(lock=threading.RLock())
+    monkeypatch.setattr(
+        'easydiffraction.analysis.minimizers.bumps_dream.MPMapper.start_mapper',
+        lambda problem, args, cpus: ('mapper', cpus),
+    )
+
+    assert minimizer._build_mapper(problem) == ('mapper', 0)
+
+
+def test_build_mapper_falls_back_for_spawn_bootstrap_runtime_error(monkeypatch):
+    from easydiffraction.analysis.minimizers.bumps_dream import BumpsDreamMinimizer
+
+    minimizer = BumpsDreamMinimizer()
+    minimizer.parallel = 0
+    warnings: list[str] = []
+    _simulate_import_safe_spawn_main_module(monkeypatch)
+
+    monkeypatch.setattr(
+        'easydiffraction.analysis.minimizers.bumps_dream.can_pickle', lambda problem: True
+    )
+    monkeypatch.setattr(
+        'easydiffraction.analysis.minimizers.bumps_dream.MPMapper.start_mapper',
+        lambda problem, args, cpus: (_ for _ in ()).throw(
+            RuntimeError('current process has finished its bootstrapping phase')
+        ),
+    )
+    monkeypatch.setattr(
+        'easydiffraction.analysis.minimizers.bumps_dream.log.warning',
+        lambda message: warnings.append(message),
+    )
+
+    assert minimizer._build_mapper('problem') is None
+    assert warnings == [
+        (
+            'DREAM parallel evaluation requires an import-safe main '
+            'module on spawn-based multiprocessing; falling back to '
+            'serial execution.'
+        )
+    ]
+
+
+def test_build_mapper_falls_back_before_starting_spawn_for_direct_script(monkeypatch):
+    import sys
+
+    from easydiffraction.analysis.minimizers.bumps_dream import BumpsDreamMinimizer
+
+    minimizer = BumpsDreamMinimizer()
+    minimizer.parallel = 0
+    warnings: list[str] = []
+    pickle_checks: list[object] = []
+
+    monkeypatch.setattr(
+        'easydiffraction.analysis.minimizers.bumps_dream.multiprocessing.get_start_method',
+        lambda allow_none=True: 'spawn',
+    )
+    monkeypatch.setitem(
+        sys.modules,
+        '__main__',
+        SimpleNamespace(__file__='docs/docs/tutorials/ed-21.py', __spec__=None),
+    )
+    monkeypatch.setattr(
+        'easydiffraction.analysis.minimizers.bumps_dream.can_pickle',
+        lambda problem: pickle_checks.append(problem) or True,
+    )
+    monkeypatch.setattr(
+        'easydiffraction.analysis.minimizers.bumps_dream.MPMapper.start_mapper',
+        lambda problem, args, cpus: (_ for _ in ()).throw(AssertionError('unexpected mapper')),
+    )
+    monkeypatch.setattr(
+        'easydiffraction.analysis.minimizers.bumps_dream.log.warning',
+        lambda message: warnings.append(message),
+    )
+
+    assert minimizer._build_mapper('problem') is None
+    assert pickle_checks == []
+    assert warnings == [
+        (
+            'DREAM parallel evaluation requires an import-safe main '
+            'module on spawn-based multiprocessing; falling back to '
+            'serial execution.'
+        )
+    ]
+
+
+def test_run_solver_preserves_parameter_order_and_forwards_init():
+    from easydiffraction.analysis.fit_helpers.bayesian import PosteriorParameterSummary
+    from easydiffraction.analysis.minimizers.bumps_dream import BumpsDreamMinimizer
+
+    minimizer = BumpsDreamMinimizer()
+    minimizer.steps = 4
+    minimizer.burn = 1
+    minimizer.thin = 1
+    minimizer.pop = 2
+    minimizer.init = 'lhs'
+
+    draw_index = np.array([0.0, 1.0])
+    parameter_samples = np.array(
+        [
+            [[1.0, 10.0], [2.0, 20.0]],
+            [[3.0, 30.0], [4.0, 40.0]],
+        ],
+        dtype=float,
+    )
+    log_posterior = np.array([[0.1, 0.2], [0.3, 0.4]], dtype=float)
+
+    class FakeState:
+        labels = ['uid_a', 'uid_b']
+
+        def chains(self):
+            return draw_index, parameter_samples, log_posterior
+
+        def best(self):
+            return np.array([11.0, 22.0]), 3.5
+
+    fake_fitter = SimpleNamespace(id='dream')
+
+    with (
+        patch('easydiffraction.analysis.minimizers.bumps_dream.FitDriver') as mock_driver_cls,
+        patch('easydiffraction.analysis.minimizers.bumps_dream.FitProblem'),
+        patch('easydiffraction.analysis.minimizers.bumps_dream.FITTERS', [fake_fitter]),
+        patch(
+            'easydiffraction.analysis.minimizers.bumps_dream.compute_convergence_diagnostics',
+            return_value={'converged': True},
+        ),
+        patch(
+            'easydiffraction.analysis.minimizers.bumps_dream.summarize_posterior_parameters',
+            return_value=[
+                PosteriorParameterSummary(
+                    unique_name='beta',
+                    display_name='Beta',
+                    best_sample_value=22.0,
+                    median=21.0,
+                    standard_deviation=0.4,
+                    interval_68=(20.5, 21.5),
+                    interval_95=(20.0, 22.0),
+                ),
+                PosteriorParameterSummary(
+                    unique_name='alpha',
+                    display_name='Alpha',
+                    best_sample_value=11.0,
+                    median=10.5,
+                    standard_deviation=0.3,
+                    interval_68=(10.0, 11.0),
+                    interval_95=(9.5, 11.5),
+                ),
+            ],
+        ) as summarize_mock,
+        patch(
+            'easydiffraction.analysis.minimizers.bumps_dream.standard_deviations_from_summaries',
+            return_value=np.array([0.4, 0.3]),
+        ),
+    ):
+        driver_instance = mock_driver_cls.return_value
+        driver_instance.clip = MagicMock()
+        driver_instance.fit.return_value = (np.array([22.0, 11.0]), 0.25)
+        driver_instance.fitter = SimpleNamespace(state=FakeState())
+
+        result = minimizer._run_solver(
+            lambda values: np.array([0.0, 0.0]),
+            bumps_params=[
+                SimpleNamespace(name='uid_a', value=1.0),
+                SimpleNamespace(name='uid_b', value=2.0),
+            ],
+            parameter_names=['beta', 'alpha'],
+            parameter_display_names=['Beta', 'Alpha'],
+            parameter_uids=['uid_b', 'uid_a'],
+            random_seed=17,
+            starting_uncertainties=[0.01, 0.02],
+        )
+
+    assert mock_driver_cls.call_args.kwargs['init'] == 'lhs'
+    np.testing.assert_allclose(result.x, np.array([22.0, 11.0]))
+    np.testing.assert_allclose(result.dx, np.array([0.4, 0.3]))
+    assert result.posterior_samples.parameter_names == ['beta', 'alpha']
+    np.testing.assert_allclose(
+        result.posterior_samples.parameter_samples[:, :, 0], parameter_samples[:, :, 1]
+    )
+    np.testing.assert_allclose(
+        result.posterior_samples.parameter_samples[:, :, 1], parameter_samples[:, :, 0]
+    )
+    assert result.sampler_settings['init'] == 'lhs'
+    assert result.sampler_settings['random_seed'] == 17
+    assert summarize_mock.call_args.kwargs['parameter_names'] == ['beta', 'alpha']
+
+
+def test_build_driver_stops_mapper_when_driver_clip_fails():
+    from easydiffraction.analysis.minimizers.bumps_dream import BumpsDreamMinimizer
+
+    minimizer = BumpsDreamMinimizer()
+
+    with (
+        patch.object(minimizer, '_build_mapper', return_value='mapper'),
+        patch(
+            'easydiffraction.analysis.minimizers.bumps_dream.FitProblem', return_value='problem'
+        ),
+        patch('easydiffraction.analysis.minimizers.bumps_dream.FitDriver') as mock_driver_cls,
+        patch(
+            'easydiffraction.analysis.minimizers.bumps_dream.MPMapper.stop_mapper'
+        ) as stop_mapper,
+    ):
+        mock_driver_cls.return_value.clip.side_effect = RuntimeError('clip failed')
+
+        with pytest.raises(RuntimeError, match='clip failed'):
+            minimizer._build_driver(
+                fitclass=object(),
+                fitness=SimpleNamespace(numpoints=lambda: 10),
+                steps=10,
+                burn=2,
+                init=minimizer.init,
+                sampler_settings={'samples': 40},
+                n_parameters=1,
+            )
+
+    stop_mapper.assert_called_once()
+
+
+def test_execute_driver_stops_mapper_when_seed_is_invalid():
+    from easydiffraction.analysis.minimizers.bumps_dream import BumpsDreamMinimizer
+
+    driver = SimpleNamespace(fit=MagicMock(), fitter=SimpleNamespace(state=None))
+
+    with patch(
+        'easydiffraction.analysis.minimizers.bumps_dream.MPMapper.stop_mapper'
+    ) as stop_mapper:
+        result = BumpsDreamMinimizer._execute_driver(driver=driver, random_seed=-1)
+
+    assert isinstance(result.error, ValueError)
+    driver.fit.assert_not_called()
+    stop_mapper.assert_called_once()
+
+
+def test_run_solver_failure_paths_return_failure_results(monkeypatch):
+    from easydiffraction.analysis.minimizers.bumps_dream import BumpsDreamMinimizer
+    from easydiffraction.analysis.minimizers.bumps_dream import _DreamDriverResult
+    from easydiffraction.analysis.minimizers.bumps_dream import _DreamRunContext
+
+    minimizer = BumpsDreamMinimizer()
+    context = _DreamRunContext(
+        driver=object(),
+        parameter_names=['alpha'],
+        parameter_display_names=['Alpha'],
+        parameter_uids=['uid_alpha'],
+        sampler_settings={'random_seed': 7},
+        starting_values=np.array([1.0]),
+        starting_uncertainties=[0.1],
+    )
+
+    monkeypatch.setattr(
+        minimizer,
+        '_prepare_run_context',
+        lambda *, objective_function, kwargs: context,
+    )
+    monkeypatch.setattr(
+        minimizer,
+        '_execute_driver',
+        lambda *, driver, random_seed: _DreamDriverResult(
+            best_values=None,
+            best_nllf=None,
+            raw_state='state',
+            error=RuntimeError('boom'),
+        ),
+    )
+
+    failed = minimizer._run_solver(lambda _: np.array([0.0]))
+
+    assert failed.success is False
+    assert failed.message == 'DREAM sampling failed: boom'
+    assert failed.sampler_completed is False
+
+    monkeypatch.setattr(
+        minimizer,
+        '_execute_driver',
+        lambda *, driver, random_seed: _DreamDriverResult(
+            best_values=np.array([1.0]),
+            best_nllf=0.5,
+            raw_state=None,
+            error=None,
+        ),
+    )
+
+    unusable = minimizer._run_solver(lambda _: np.array([0.0]))
+
+    assert unusable.success is False
+    assert unusable.message == 'DREAM sampling did not produce usable posterior samples.'
+    assert unusable.sampler_completed is False
+
+
+def test_build_success_result_handles_invalid_samples_and_warns_when_not_converged(monkeypatch):
+    from easydiffraction.analysis.fit_helpers.bayesian import PosteriorParameterSummary
+    from easydiffraction.analysis.minimizers.bumps_dream import BumpsDreamMinimizer
+    from easydiffraction.analysis.minimizers.bumps_dream import _DreamRunContext
+
+    minimizer = BumpsDreamMinimizer()
+    context = _DreamRunContext(
+        driver=object(),
+        parameter_names=['alpha'],
+        parameter_display_names=['Alpha'],
+        parameter_uids=['uid_alpha'],
+        sampler_settings={'random_seed': 7},
+        starting_values=np.array([1.0]),
+        starting_uncertainties=[0.1],
+    )
+
+    class BadState:
+        labels = ['uid_alpha']
+
+        @staticmethod
+        def chains():
+            return np.array([0.0]), np.array([1.0]), np.array([0.0])
+
+        @staticmethod
+        def best():
+            return np.array([1.0]), 0.5
+
+    failed = minimizer._build_success_result(context=context, raw_state=BadState(), best_nllf=0.5)
+
+    assert failed.success is False
+    assert failed.sampler_completed is True
+
+    class GoodState:
+        labels = ['uid_alpha']
+
+        @staticmethod
+        def chains():
+            return (
+                np.array([0.0, 1.0]),
+                np.ones((2, 2, 1), dtype=float),
+                np.zeros((2, 2), dtype=float),
+            )
+
+        @staticmethod
+        def best():
+            return np.array([1.0]), 0.5
+
+    warnings: list[str] = []
+    monkeypatch.setattr(
+        'easydiffraction.analysis.minimizers.bumps_dream.compute_convergence_diagnostics',
+        lambda posterior_samples: {'converged': False},
+    )
+    monkeypatch.setattr(
+        'easydiffraction.analysis.minimizers.bumps_dream.summarize_posterior_parameters',
+        lambda **kwargs: [
+            PosteriorParameterSummary(
+                unique_name='alpha',
+                display_name='Alpha',
+                best_sample_value=1.0,
+                median=1.0,
+                standard_deviation=0.2,
+                interval_68=(0.9, 1.1),
+                interval_95=(0.8, 1.2),
+            )
+        ],
+    )
+    monkeypatch.setattr(
+        'easydiffraction.analysis.minimizers.bumps_dream.standard_deviations_from_summaries',
+        lambda summaries: np.array([0.2]),
+    )
+    monkeypatch.setattr(
+        'easydiffraction.analysis.minimizers.bumps_dream.log.warning',
+        lambda message: warnings.append(message),
+    )
+
+    successful = minimizer._build_success_result(
+        context=context,
+        raw_state=GoodState(),
+        best_nllf=0.5,
+    )
+
+    assert successful.success is True
+    assert successful.sampler_completed is True
+    assert any('poorly mixed' in message for message in warnings)
diff --git a/tests/integration/fitting/test_cli_entrypoints.py b/tests/integration/fitting/test_cli_entrypoints.py
new file mode 100644
index 000000000..34cc85ad8
--- /dev/null
+++ b/tests/integration/fitting/test_cli_entrypoints.py
@@ -0,0 +1,182 @@
+# SPDX-FileCopyrightText: 2026 EasyScience contributors 
+# SPDX-License-Identifier: BSD-3-Clause
+
+from __future__ import annotations
+
+from typer.testing import CliRunner
+
+runner = CliRunner()
+
+
+def test_cli_version_invokes_show_version(monkeypatch):
+    import easydiffraction as ed
+    import easydiffraction.__main__ as main_mod
+
+    called = {'ok': False}
+
+    def fake_show_version() -> None:
+        print('VERSION_OK')
+        called['ok'] = True
+
+    monkeypatch.setattr(ed, 'show_version', fake_show_version)
+
+    result = runner.invoke(main_mod.app, ['--version'])
+
+    assert result.exit_code == 0
+    assert called['ok'] is True
+    assert 'VERSION_OK' in result.stdout
+
+
+def test_cli_help_shows_and_exits_zero():
+    import easydiffraction.__main__ as main_mod
+
+    result = runner.invoke(main_mod.app, ['--help'])
+
+    assert result.exit_code == 0
+    assert 'EasyDiffraction command-line interface' in result.stdout
+
+
+def test_cli_subcommands_call_utils(monkeypatch):
+    import easydiffraction as ed
+    import easydiffraction.__main__ as main_mod
+
+    calls: list[str] = []
+    monkeypatch.setattr(ed, 'list_tutorials', lambda: calls.append('LIST'))
+    monkeypatch.setattr(
+        ed,
+        'download_all_tutorials',
+        lambda destination='tutorials', overwrite=False: calls.append('DOWNLOAD_ALL'),
+    )
+    monkeypatch.setattr(
+        ed,
+        'download_tutorial',
+        lambda id, destination='tutorials', overwrite=False: calls.append(f'DOWNLOAD_{id}'),
+    )
+
+    list_result = runner.invoke(main_mod.app, ['list-tutorials'])
+    download_all_result = runner.invoke(main_mod.app, ['download-all-tutorials'])
+    download_one_result = runner.invoke(main_mod.app, ['download-tutorial', '1'])
+
+    assert list_result.exit_code == 0
+    assert download_all_result.exit_code == 0
+    assert download_one_result.exit_code == 0
+    assert calls == ['LIST', 'DOWNLOAD_ALL', 'DOWNLOAD_1']
+
+
+def test_cli_fit_loads_and_fits(monkeypatch, tmp_path):
+    import easydiffraction.__main__ as main_mod
+    from easydiffraction.project.project import Project
+
+    calls: list[str] = []
+
+    class FakeInfo:
+        _path = '/some/path'
+
+    class FakeExperiment:
+        name = 'exp1'
+
+    class FakeProject:
+        info = FakeInfo()
+        experiments = [FakeExperiment()]
+
+        class _analysis:
+            @staticmethod
+            def fit() -> None:
+                calls.append('FIT')
+
+            class display:
+                @staticmethod
+                def fit_results() -> None:
+                    calls.append('DISPLAY')
+
+        analysis = _analysis()
+
+        class _display:
+            class _fit:
+                @staticmethod
+                def results() -> None:
+                    calls.append('DISPLAY')
+
+                @staticmethod
+                def correlations() -> None:
+                    calls.append('PLOT_CORR')
+
+            fit = _fit()
+
+            @staticmethod
+            def pattern(expt_name: str, **kwargs) -> None:
+                del kwargs
+                calls.append(f'PLOT_{expt_name}_False')
+
+        display = _display()
+
+    fake_project = FakeProject()
+
+    project_dir = tmp_path / 'proj'
+    project_dir.mkdir()
+    (project_dir / 'project.cif').write_text('_project.id test\n')
+
+    monkeypatch.setattr(Project, 'load', staticmethod(lambda path: fake_project))
+
+    result = runner.invoke(main_mod.app, ['fit', str(project_dir)])
+
+    assert result.exit_code == 0
+    assert calls == ['FIT', 'DISPLAY', 'PLOT_CORR', 'PLOT_exp1_False']
+
+
+def test_cli_fit_dry_clears_path(monkeypatch, tmp_path):
+    import easydiffraction.__main__ as main_mod
+    from easydiffraction.project.project import Project
+
+    class FakeInfo:
+        _path = '/some/path'
+
+    class FakeExperiment:
+        name = 'exp1'
+
+    class FakeProject:
+        info = FakeInfo()
+        experiments = [FakeExperiment()]
+
+        class _analysis:
+            @staticmethod
+            def fit() -> None:
+                return None
+
+            class display:
+                @staticmethod
+                def fit_results() -> None:
+                    return None
+
+        analysis = _analysis()
+
+        class _display:
+            class _fit:
+                @staticmethod
+                def results() -> None:
+                    return None
+
+                @staticmethod
+                def correlations() -> None:
+                    return None
+
+            fit = _fit()
+
+            @staticmethod
+            def pattern(expt_name: str, **kwargs) -> None:
+                del expt_name, kwargs
+
+        display = _display()
+
+    fake_project = FakeProject()
+
+    project_dir = tmp_path / 'proj'
+    project_dir.mkdir()
+    (project_dir / 'project.cif').write_text('_project.id test\n')
+
+    monkeypatch.setattr(Project, 'load', staticmethod(lambda path: fake_project))
+
+    result = runner.invoke(main_mod.app, ['fit', '--dry', str(project_dir)])
+
+    assert result.exit_code == 0
+    assert fake_project.info._path is None
diff --git a/tests/integration/fitting/test_exploration_help.py b/tests/integration/fitting/test_exploration_help.py
index d86004691..08e2bcf54 100644
--- a/tests/integration/fitting/test_exploration_help.py
+++ b/tests/integration/fitting/test_exploration_help.py
@@ -65,6 +65,27 @@ def test_experiment_show_as_cif(lbco_fitted_project):
     expt.show_as_cif()
 
 
+def test_experiment_show_as_cif_omits_empty_category_gaps(lbco_fitted_project, monkeypatch):
+    import re
+
+    import easydiffraction.datablocks.experiment.item.base as experiment_base
+
+    captured = {}
+
+    def fake_render_cif(cif_text):
+        captured['cif_text'] = cif_text
+
+    monkeypatch.setattr(experiment_base, 'render_cif', fake_render_cif)
+
+    project = lbco_fitted_project
+    expt = project.experiments['hrpt']
+    expt.show_as_cif()
+
+    cif_text = captured['cif_text']
+    assert re.search(r'_pd_phase_block\.scale\n[^\n]+\n\nloop_', cif_text) is not None
+    assert '\n\n\n' not in cif_text
+
+
 def test_experiment_as_cif(lbco_fitted_project):
     project = lbco_fitted_project
     expt = project.experiments['hrpt']
diff --git a/tests/integration/fitting/test_multi.py b/tests/integration/fitting/test_multi.py
index a955bf928..d75acf6c8 100644
--- a/tests/integration/fitting/test_multi.py
+++ b/tests/integration/fitting/test_multi.py
@@ -107,7 +107,7 @@ def test_single_fit_neutron_pd_tof_mcstas_lbco_si() -> None:
     project.experiments['mcstas'].excluded_regions.create(start=108000, end=200000)
 
     # Prepare for fitting
-    project.analysis.fit.minimizer_type = 'lmfit'
+    project.analysis.fitting.minimizer_type = 'lmfit'
 
     # Select fitting parameters
     model_1.cell.length_a.free = True
@@ -200,8 +200,8 @@ def _test_joint_fit_bragg_pdf_neutron_pd_tof_si() -> None:
     project.experiments.add(pdf_expt)
 
     # Prepare for fitting
-    project.analysis.fit.mode = 'joint'
-    project.analysis.fit.minimizer_type = 'lmfit'
+    project.analysis.fitting_mode_type = 'joint'
+    project.analysis.fitting.minimizer_type = 'lmfit'
 
     # Select fitting parameters — shared structure
     model.cell.length_a.free = True
diff --git a/tests/integration/fitting/test_plotting.py b/tests/integration/fitting/test_plotting.py
index 1cb06c41b..393e78c3d 100644
--- a/tests/integration/fitting/test_plotting.py
+++ b/tests/integration/fitting/test_plotting.py
@@ -1,29 +1,29 @@
 # SPDX-FileCopyrightText: 2026 EasyScience contributors 
 # SPDX-License-Identifier: BSD-3-Clause
 
-"""Integration tests for the Plotter facade on a fitted project."""
+"""Integration tests for ``project.display.pattern``."""
 
 
-def test_plot_meas(lbco_fitted_project):
+def test_pattern_auto(lbco_fitted_project):
     project = lbco_fitted_project
-    project.display.plotter.plot_meas(expt_name='hrpt')
+    project.display.pattern(expt_name='hrpt')
 
 
-def test_plot_calc(lbco_fitted_project):
+def test_pattern_measured(lbco_fitted_project):
     project = lbco_fitted_project
-    project.display.plotter.plot_calc(expt_name='hrpt')
+    project.display.pattern(expt_name='hrpt', include='measured')
 
 
-def test_plot_meas_vs_calc(lbco_fitted_project):
+def test_pattern_measured_vs_calculated(lbco_fitted_project):
     project = lbco_fitted_project
-    project.display.plotter.plot_meas_vs_calc(expt_name='hrpt')
+    project.display.pattern(expt_name='hrpt', include=('measured', 'calculated'))
 
 
-def test_plot_meas_with_range(lbco_fitted_project):
+def test_pattern_with_range(lbco_fitted_project):
     project = lbco_fitted_project
-    project.display.plotter.plot_meas(expt_name='hrpt', x_min=20, x_max=80)
+    project.display.pattern(expt_name='hrpt', x_min=20, x_max=80)
 
 
-def test_plot_meas_vs_calc_with_range(lbco_fitted_project):
+def test_show_pattern_options(lbco_fitted_project):
     project = lbco_fitted_project
-    project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', x_min=20, x_max=80)
+    project.display.show_pattern_options(expt_name='hrpt')
diff --git a/tests/integration/fitting/test_powder-diffraction_constant-wavelength.py b/tests/integration/fitting/test_powder-diffraction_constant-wavelength.py
index 9e65996ac..0ffa95f14 100644
--- a/tests/integration/fitting/test_powder-diffraction_constant-wavelength.py
+++ b/tests/integration/fitting/test_powder-diffraction_constant-wavelength.py
@@ -86,7 +86,7 @@ def test_single_fit_neutron_pd_cwl_lbco() -> None:
     project.experiments.add(expt)
 
     # Prepare for fitting
-    project.analysis.fit.minimizer_type = 'lmfit'
+    project.analysis.fitting.minimizer_type = 'lmfit'
 
     # ------------ 1st fitting ------------
 
@@ -234,7 +234,7 @@ def test_single_fit_neutron_pd_cwl_lbco_with_constraints() -> None:
     project.experiments.add(expt)
 
     # Prepare for fitting
-    project.analysis.fit.minimizer_type = 'lmfit'
+    project.analysis.fitting.minimizer_type = 'lmfit'
 
     # ------------ 1st fitting ------------
 
@@ -400,7 +400,7 @@ def test_fit_neutron_pd_cwl_hs() -> None:
     project.experiments.add(expt)
 
     # Prepare for fitting
-    project.analysis.fit.minimizer_type = 'lmfit'
+    project.analysis.fitting.minimizer_type = '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 ec39c1de0..0e7e11898 100644
--- a/tests/integration/fitting/test_powder-diffraction_joint-fit.py
+++ b/tests/integration/fitting/test_powder-diffraction_joint-fit.py
@@ -122,8 +122,8 @@ def test_joint_fit_split_dataset_neutron_pd_cwl_pbso4() -> None:
     project.experiments.add(expt2)
 
     # Prepare for fitting
-    project.analysis.fit.minimizer_type = 'lmfit'
-    project.analysis.fit.mode = 'joint'
+    project.analysis.fitting.minimizer_type = 'lmfit'
+    project.analysis.fitting_mode_type = 'joint'
 
     # Select fitting parameters
     model.cell.length_a.free = True
@@ -255,7 +255,7 @@ def test_joint_fit_neutron_xray_pd_cwl_pbso4() -> None:
     project.experiments.add(expt2)
 
     # Prepare for fitting
-    project.analysis.fit.minimizer_type = 'lmfit'
+    project.analysis.fitting.minimizer_type = 'lmfit'
 
     # Select fitting parameters
     model.cell.length_a.free = True
@@ -279,7 +279,7 @@ def test_joint_fit_neutron_xray_pd_cwl_pbso4() -> None:
     # ------------ 2nd fitting ------------
 
     # Perform fit
-    project.analysis.fit.mode = 'joint'
+    project.analysis.fitting_mode_type = 'joint'
     project.analysis.fit()
 
     # Compare fit quality
@@ -292,8 +292,8 @@ def test_joint_fit_neutron_xray_pd_cwl_pbso4() -> None:
     # ------------ 3rd fitting ------------
 
     # Perform fit
-    project.analysis.joint_fit_experiments['xrd'].weight = 0.5  # Default
-    project.analysis.joint_fit_experiments['npd'].weight = 0.5  # Default
+    project.analysis.joint_fit['xrd'].weight = 0.5  # Default
+    project.analysis.joint_fit['npd'].weight = 0.5  # Default
     project.analysis.fit()
 
     # Compare fit quality
@@ -306,8 +306,8 @@ def test_joint_fit_neutron_xray_pd_cwl_pbso4() -> None:
     # ------------ 4th fitting ------------
 
     # Perform fit
-    project.analysis.joint_fit_experiments['xrd'].weight = 0.3
-    project.analysis.joint_fit_experiments['npd'].weight = 0.7
+    project.analysis.joint_fit['xrd'].weight = 0.3
+    project.analysis.joint_fit['npd'].weight = 0.7
     project.analysis.fit()
 
     # Compare fit quality
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 1885fb80f..496852e1d 100644
--- a/tests/integration/fitting/test_powder-diffraction_time-of-flight.py
+++ b/tests/integration/fitting/test_powder-diffraction_time-of-flight.py
@@ -58,7 +58,7 @@ def test_single_fit_neutron_pd_tof_si() -> None:
     project.experiments.add(expt)
 
     # Prepare for fitting
-    project.analysis.fit.minimizer_type = 'lmfit'
+    project.analysis.fitting.minimizer_type = 'lmfit'
 
     # Select fitting parameters
     model.cell.length_a.free = True
@@ -200,7 +200,7 @@ def test_single_fit_neutron_pd_tof_ncaf() -> None:
     project.experiments.add(expt)
 
     # Prepare for fitting
-    project.analysis.fit.minimizer_type = 'lmfit'
+    project.analysis.fitting.minimizer_type = 'lmfit'
 
     # Select fitting parameters
     expt.linked_phases['ncaf'].scale.free = True
diff --git a/tests/integration/fitting/test_project_load.py b/tests/integration/fitting/test_project_load.py
index acbc9432a..f908789a6 100644
--- a/tests/integration/fitting/test_project_load.py
+++ b/tests/integration/fitting/test_project_load.py
@@ -205,12 +205,18 @@ def test_save_load_round_trip_preserves_parameters(tmp_path) -> None:
     # Compare constraints
     assert len(loaded.analysis.constraints) == len(original.analysis.constraints)
     for i, orig_c in enumerate(original.analysis.constraints):
+        loaded_constraint = loaded.analysis.constraints[orig_c.id.value]
+        assert loaded_constraint.id.value == orig_c.id.value
+        assert loaded_constraint.expression.value == orig_c.expression.value
         assert loaded.analysis.constraints[i].expression.value == orig_c.expression.value
     assert loaded.analysis.constraints.enabled is True
 
     # Compare analysis settings
-    assert loaded.analysis.fit.minimizer_type.value == original.analysis.fit.minimizer_type.value
-    assert loaded.analysis.fit.mode.value == original.analysis.fit.mode.value
+    assert (
+        loaded.analysis.fitting.minimizer_type.value
+        == original.analysis.fitting.minimizer_type.value
+    )
+    assert loaded.analysis.fitting_mode_type == original.analysis.fitting_mode_type
 
 
 # ------------------------------------------------------------------
@@ -227,7 +233,8 @@ def test_save_load_round_trip_preserves_fit_quality(tmp_path) -> None:
     """
     # Create and fit the original project
     original = _create_lbco_project()
-    original.analysis.fit(verbosity='silent')
+    original.verbosity = 'silent'
+    original.analysis.fit()
     original_chi2 = original.analysis.fit_results.reduced_chi_square
 
     # Save the fitted project
@@ -238,7 +245,8 @@ def test_save_load_round_trip_preserves_fit_quality(tmp_path) -> None:
     loaded = Project.load(proj_dir)
 
     # Fit the loaded project
-    loaded.analysis.fit(verbosity='silent')
+    loaded.verbosity = 'silent'
+    loaded.analysis.fit()
     loaded_chi2 = loaded.analysis.fit_results.reduced_chi_square
 
     # The χ² values should be very close (same starting point,
diff --git a/tests/integration/fitting/test_sequential.py b/tests/integration/fitting/test_sequential.py
index 2583b9e10..73c068b3b 100644
--- a/tests/integration/fitting/test_sequential.py
+++ b/tests/integration/fitting/test_sequential.py
@@ -1,11 +1,10 @@
 # SPDX-FileCopyrightText: 2026 EasyScience contributors 
 # SPDX-License-Identifier: BSD-3-Clause
-"""Integration tests for Analysis.fit_sequential()."""
+"""Integration tests for sequential fitting via Analysis.fit()."""
 
 from __future__ import annotations
 
 import csv
-import shutil
 import tempfile
 from pathlib import Path
 
@@ -20,7 +19,10 @@
 TEMP_DIR = tempfile.gettempdir()
 
 
-def _create_sequential_project(tmp_path: Path) -> tuple[Project, str]:
+def _create_sequential_project(
+    tmp_path: Path,
+    temperatures: dict[str, float] | None = None,
+) -> tuple[Project, str]:
     """
     Build a project for sequential fitting and save it.
 
@@ -100,7 +102,8 @@ def _create_sequential_project(tmp_path: Path) -> tuple[Project, str]:
     expt.background['2'].y.free = True
 
     # Initial fit on the template
-    project.analysis.fit(verbosity='silent')
+    project.verbosity = 'silent'
+    project.analysis.fit()
 
     # Save project
     proj_dir = str(tmp_path / 'seq_project')
@@ -109,12 +112,39 @@ def _create_sequential_project(tmp_path: Path) -> tuple[Project, str]:
     # Create a data directory with copies of the same data file
     data_dir = tmp_path / 'scan_data'
     data_dir.mkdir()
+    source_text = Path(data_path).read_text(encoding='utf-8')
     for i in range(3):
-        shutil.copy(data_path, data_dir / f'scan_{i + 1:03d}.xye')
+        file_name = f'scan_{i + 1:03d}.xye'
+        destination = data_dir / file_name
+        destination_text = source_text
+        if temperatures is not None:
+            temperature = temperatures[file_name]
+            destination_text = f'# ambient_temperature = {temperature}\n{source_text}'
+        destination.write_text(destination_text, encoding='utf-8')
 
     return project, str(data_dir)
 
 
+def _run_sequential_fit(
+    project: Project,
+    data_dir: str,
+    *,
+    max_workers: int | str = 1,
+    chunk_size: int | None = None,
+    file_pattern: str = '*',
+    reverse: bool = False,
+) -> None:
+    project.analysis.fitting_mode_type = 'sequential'
+    project.analysis.sequential_fit.data_dir = data_dir
+    project.analysis.sequential_fit.max_workers = (
+        'auto' if max_workers == 'auto' else str(max_workers)
+    )
+    project.analysis.sequential_fit.chunk_size = '.' if chunk_size is None else str(chunk_size)
+    project.analysis.sequential_fit.file_pattern = file_pattern
+    project.analysis.sequential_fit.reverse = reverse
+    project.analysis.fit()
+
+
 # ------------------------------------------------------------------
 #  Test 1: Basic sequential fit produces CSV
 # ------------------------------------------------------------------
@@ -124,10 +154,7 @@ def test_fit_sequential_produces_csv(tmp_path) -> None:
     """fit_sequential creates a results.csv with one row per file."""
     project, data_dir = _create_sequential_project(tmp_path)
 
-    project.analysis.fit_sequential(
-        data_dir=data_dir,
-        verbosity='silent',
-    )
+    _run_sequential_fit(project, data_dir)
 
     csv_path = project.info.path / 'analysis' / 'results.csv'
     assert csv_path.is_file(), 'results.csv was not created'
@@ -138,9 +165,18 @@ def test_fit_sequential_produces_csv(tmp_path) -> None:
 
     assert len(rows) == 3, f'Expected 3 rows, got {len(rows)}'
 
-    # Each row should have fit_success
+    # Each row should have fit_result.success
     for row in rows:
-        assert row['fit_success'] == 'True', f'Fit failed for {row["file_path"]}'
+        assert row['fit_result.success'] == 'True', f'Fit failed for {row["file_path"]}'
+        assert int(row['fit_result.iterations']) > 0, (
+            f'Expected non-zero iterations for {row["file_path"]}'
+        )
+
+    assert 'fit_result.reduced_chi_square' in rows[0]
+    assert 'fit_result.iterations' in rows[0]
+    assert 'success' not in rows[0]
+    assert 'reduced_chi_square' not in rows[0]
+    assert 'iterations' not in rows[0]
 
     # Each row should have parameter values
     assert 'lbco.cell.length_a' in rows[0]
@@ -157,10 +193,7 @@ def test_fit_sequential_crash_recovery(tmp_path) -> None:
     project, data_dir = _create_sequential_project(tmp_path)
 
     # First run: fit all 3 files
-    project.analysis.fit_sequential(
-        data_dir=data_dir,
-        verbosity='silent',
-    )
+    _run_sequential_fit(project, data_dir)
 
     csv_path = project.info.path / 'analysis' / 'results.csv'
     with csv_path.open() as f:
@@ -168,10 +201,7 @@ def test_fit_sequential_crash_recovery(tmp_path) -> None:
     assert len(rows_first) == 3
 
     # Second run: should skip all 3 files
-    project.analysis.fit_sequential(
-        data_dir=data_dir,
-        verbosity='silent',
-    )
+    _run_sequential_fit(project, data_dir)
 
     with csv_path.open() as f:
         rows_second = list(csv.DictReader(f))
@@ -188,10 +218,7 @@ def test_fit_sequential_parameter_propagation(tmp_path) -> None:
     """Parameters from one fit propagate to the next."""
     project, data_dir = _create_sequential_project(tmp_path)
 
-    project.analysis.fit_sequential(
-        data_dir=data_dir,
-        verbosity='silent',
-    )
+    _run_sequential_fit(project, data_dir)
 
     csv_path = project.info.path / 'analysis' / 'results.csv'
     with csv_path.open() as f:
@@ -204,26 +231,24 @@ def test_fit_sequential_parameter_propagation(tmp_path) -> None:
 
 
 # ------------------------------------------------------------------
-#  Test 4: extract_diffrn callback
+#  Test 4: extract metadata rules
 # ------------------------------------------------------------------
 
 
-def test_fit_sequential_with_diffrn_callback(tmp_path) -> None:
-    """extract_diffrn callback populates diffrn columns in CSV."""
-    project, data_dir = _create_sequential_project(tmp_path)
-
+def test_fit_sequential_with_diffrn_extract_rules(tmp_path) -> None:
+    """Sequential extract rules populate diffrn columns in the CSV."""
     temperatures = {'scan_001.xye': 300.0, 'scan_002.xye': 350.0, 'scan_003.xye': 400.0}
+    project, data_dir = _create_sequential_project(tmp_path, temperatures=temperatures)
 
-    def extract_diffrn(file_path: str) -> dict[str, float]:
-        name = Path(file_path).name
-        return {'ambient_temperature': temperatures.get(name, 0.0)}
-
-    project.analysis.fit_sequential(
-        data_dir=data_dir,
-        extract_diffrn=extract_diffrn,
-        verbosity='silent',
+    project.analysis.sequential_fit_extract.create(
+        id='temperature',
+        target='diffrn.ambient_temperature',
+        pattern=r'ambient_temperature\s*=\s*([0-9.]+)',
+        required=True,
     )
 
+    _run_sequential_fit(project, data_dir)
+
     csv_path = project.info.path / 'analysis' / 'results.csv'
     with csv_path.open() as f:
         rows = list(csv.DictReader(f))
@@ -231,9 +256,9 @@ def extract_diffrn(file_path: str) -> dict[str, float]:
     # Check that temperature column is present and populated
     for row in rows:
         name = Path(row['file_path']).name
-        if 'diffrn.ambient_temperature' in row:
-            expected = temperatures.get(name, 0.0)
-            assert_almost_equal(float(row['diffrn.ambient_temperature']), expected)
+        assert 'diffrn.ambient_temperature' in row
+        expected = temperatures[name]
+        assert_almost_equal(float(row['diffrn.ambient_temperature']), expected)
 
 
 # ------------------------------------------------------------------
@@ -256,7 +281,7 @@ def test_fit_sequential_requires_saved_project(tmp_path) -> None:
     project.experiments.add(expt)
 
     with pytest.raises(ValueError, match='must be saved'):
-        project.analysis.fit_sequential(data_dir=str(tmp_path))
+        _run_sequential_fit(project, str(tmp_path))
 
 
 def test_fit_sequential_requires_one_structure(tmp_path) -> None:
@@ -265,7 +290,7 @@ def test_fit_sequential_requires_one_structure(tmp_path) -> None:
     project.save_as(str(tmp_path / 'proj'))
 
     with pytest.raises(ValueError, match='exactly 1 structure'):
-        project.analysis.fit_sequential(data_dir=str(tmp_path))
+        _run_sequential_fit(project, str(tmp_path))
 
 
 def test_fit_sequential_requires_one_experiment(tmp_path) -> None:
@@ -276,7 +301,7 @@ def test_fit_sequential_requires_one_experiment(tmp_path) -> None:
     project.save_as(str(tmp_path / 'proj'))
 
     with pytest.raises(ValueError, match='exactly 1 experiment'):
-        project.analysis.fit_sequential(data_dir=str(tmp_path))
+        _run_sequential_fit(project, str(tmp_path))
 
 
 # ------------------------------------------------------------------
@@ -288,11 +313,7 @@ def test_fit_sequential_parallel(tmp_path) -> None:
     """fit_sequential with max_workers=2 produces correct CSV."""
     project, data_dir = _create_sequential_project(tmp_path)
 
-    project.analysis.fit_sequential(
-        data_dir=data_dir,
-        max_workers=2,
-        verbosity='silent',
-    )
+    _run_sequential_fit(project, data_dir, max_workers=2)
 
     csv_path = project.info.path / 'analysis' / 'results.csv'
     assert csv_path.is_file(), 'results.csv was not created'
@@ -304,7 +325,10 @@ def test_fit_sequential_parallel(tmp_path) -> None:
     assert len(rows) == 3, f'Expected 3 rows, got {len(rows)}'
 
     for row in rows:
-        assert row['fit_success'] == 'True', f'Fit failed for {row["file_path"]}'
+        assert row['fit_result.success'] == 'True', f'Fit failed for {row["file_path"]}'
+        assert int(row['fit_result.iterations']) > 0, (
+            f'Expected non-zero iterations for {row["file_path"]}'
+        )
 
     # Parameter values should be present and reasonable
     assert 'lbco.cell.length_a' in rows[0]
@@ -320,19 +344,27 @@ def test_fit_sequential_parallel(tmp_path) -> None:
 
 def test_apply_params_from_csv_loads_data_and_params(tmp_path) -> None:
     """apply_params_from_csv overrides params and reloads data."""
-    project, data_dir = _create_sequential_project(tmp_path)
-
-    project.analysis.fit_sequential(
-        data_dir=data_dir,
-        verbosity='silent',
+    temperatures = {'scan_001.xye': 300.0, 'scan_002.xye': 350.0, 'scan_003.xye': 400.0}
+    project, data_dir = _create_sequential_project(tmp_path, temperatures=temperatures)
+    project.analysis.sequential_fit_extract.create(
+        id='temperature',
+        target='diffrn.ambient_temperature',
+        pattern=r'ambient_temperature\s*=\s*([0-9.]+)',
+        required=True,
     )
 
+    _run_sequential_fit(project, data_dir)
+
     csv_path = project.info.path / 'analysis' / 'results.csv'
     with csv_path.open() as f:
         rows = list(csv.DictReader(f))
 
     # Read the expected cell_length_a from CSV row 1
     expected_a = float(rows[1]['lbco.cell.length_a'])
+    expected_temperature = float(rows[1]['diffrn.ambient_temperature'])
+
+    expt = next(iter(project.experiments.values()))
+    expt.diffrn.ambient_temperature.value = None
 
     # Apply params from row 1
     project.apply_params_from_csv(row_index=1)
@@ -343,8 +375,8 @@ def test_apply_params_from_csv_loads_data_and_params(tmp_path) -> None:
 
     # Verify that the experiment has measured data loaded
     # (from the file_path in that CSV row)
-    expt = next(iter(project.experiments.values()))
     assert expt.data.intensity_meas is not None
+    assert_almost_equal(expt.diffrn.ambient_temperature.value, expected_temperature)
 
 
 def test_apply_params_from_csv_raises_on_missing_csv(tmp_path) -> None:
@@ -360,10 +392,7 @@ def test_apply_params_from_csv_raises_on_bad_index(tmp_path) -> None:
     """apply_params_from_csv raises on out-of-range index."""
     project, data_dir = _create_sequential_project(tmp_path)
 
-    project.analysis.fit_sequential(
-        data_dir=data_dir,
-        verbosity='silent',
-    )
+    _run_sequential_fit(project, data_dir)
 
     with pytest.raises(IndexError, match='out of range'):
         project.apply_params_from_csv(row_index=99)
diff --git a/tests/integration/scipp-analysis/dream/test_package_import.py b/tests/integration/scipp-analysis/dream/test_package_import.py
index 05f80a068..45b5bc95b 100644
--- a/tests/integration/scipp-analysis/dream/test_package_import.py
+++ b/tests/integration/scipp-analysis/dream/test_package_import.py
@@ -51,28 +51,9 @@ def get_base_version(
 def test_package_import(
     package_name: str,
 ) -> None:
-    """Verify installed package is not older than PyPI latest version.
-
-    Uses >= comparison to support both:
-    - Real releases where installed == latest
-    - Dev builds where installed (e.g., 999.0.0) > latest
-    """
+    """Verify  that the package is installed and can be fetched from PyPI."""
     installed_version = get_installed_version(package_name)
     latest_version = get_latest_version(package_name)
 
     assert installed_version is not None, f'Package {package_name} is not installed.'
     assert latest_version is not None, f'Could not fetch latest version for {package_name}.'
-
-    # Compare only MAJOR.MINOR.PATCH, ignoring local version identifiers
-    installed_base = Version(get_base_version(installed_version))
-    latest_base = Version(get_base_version(latest_version))
-
-    if installed_base < latest_base:
-        pytest.skip(
-            f'Installed {package_name} is older than latest PyPI release: '
-            f'{installed_base} < {latest_base}',
-        )
-
-    assert installed_base >= latest_base, (
-        f'Package {package_name} is outdated: Installed={installed_base}, Latest={latest_base}'
-    )
diff --git a/tests/unit/easydiffraction/analysis/calculators/test_cryspy.py b/tests/unit/easydiffraction/analysis/calculators/test_cryspy.py
index 5fdda3ee4..6cebe4c33 100644
--- a/tests/unit/easydiffraction/analysis/calculators/test_cryspy.py
+++ b/tests/unit/easydiffraction/analysis/calculators/test_cryspy.py
@@ -173,3 +173,32 @@ def test_last_powder_refln_records_reads_tof_time_and_d_spacing():
     assert records[0].d_spacing == pytest.approx(3.21)
     assert records[0].f_calc == pytest.approx(6.0)
     assert records[0].f_squared_calc == pytest.approx(36.0)
+
+
+def test_last_powder_refln_records_reads_xray_charge_structure_factor():
+    from easydiffraction.analysis.calculators.cryspy import CryspyCalculator
+    from easydiffraction.datablocks.experiment.item.enums import BeamModeEnum
+
+    calculator = CryspyCalculator()
+    calculator._last_powder_phase_blocks = {
+        'phase_exp': {
+            'index_hkl': np.array([[1], [1], [1]]),
+            'sthovl': np.array([0.2]),
+            'ttheta_hkl': np.array([np.pi / 3]),
+            'f_charge': np.array([8.0 - 6.0j]),
+        }
+    }
+    structure = SimpleNamespace(name='phase')
+    experiment = SimpleNamespace(
+        name='exp',
+        type=SimpleNamespace(beam_mode=SimpleNamespace(value=BeamModeEnum.CONSTANT_WAVELENGTH)),
+    )
+
+    records = calculator.last_powder_refln_records(structure, experiment, phase_id='phase-x')
+
+    assert len(records) == 1
+    assert records[0].phase_id == 'phase-x'
+    assert records[0].two_theta == pytest.approx(60.0)
+    assert records[0].d_spacing == pytest.approx(2.5)
+    assert records[0].f_calc == pytest.approx(10.0)
+    assert records[0].f_squared_calc == pytest.approx(100.0)
diff --git a/tests/unit/easydiffraction/analysis/categories/fitting/test_default.py b/tests/unit/easydiffraction/analysis/categories/fitting/test_default.py
new file mode 100644
index 000000000..094a033a3
--- /dev/null
+++ b/tests/unit/easydiffraction/analysis/categories/fitting/test_default.py
@@ -0,0 +1,37 @@
+# SPDX-FileCopyrightText: 2026 EasyScience contributors 
+# SPDX-License-Identifier: BSD-3-Clause
+"""Tests for analysis/categories/fitting/default.py."""
+
+from types import SimpleNamespace
+
+
+def test_fitting_defaults():
+    from easydiffraction.analysis.categories.fitting.default import Fitting
+
+    fitting = Fitting()
+
+    assert fitting.minimizer_type.value == 'lmfit (leastsq)'
+    assert fitting._identity.category_code == 'fitting'
+    assert Fitting.type_info.tag == 'default'
+
+
+def test_fitting_setter_updates_parent_fitter():
+    from easydiffraction.analysis.categories.fitting.default import Fitting
+
+    fitting = Fitting()
+    parent = SimpleNamespace(fitter=None)
+    fitting._parent = parent
+
+    fitting.minimizer_type = 'lmfit'
+
+    assert fitting.minimizer_type.value == 'lmfit'
+    assert parent.fitter is not None
+
+
+def test_fitting_as_cif_uses_fitting_prefix():
+    from easydiffraction.analysis.categories.fitting.default import Fitting
+
+    fitting = Fitting()
+    fitting.minimizer_type = 'lmfit'
+
+    assert '_fitting.minimizer_type' in fitting.as_cif
diff --git a/tests/unit/easydiffraction/analysis/categories/fitting/test_factory.py b/tests/unit/easydiffraction/analysis/categories/fitting/test_factory.py
new file mode 100644
index 000000000..24d248f1e
--- /dev/null
+++ b/tests/unit/easydiffraction/analysis/categories/fitting/test_factory.py
@@ -0,0 +1,24 @@
+# SPDX-FileCopyrightText: 2026 EasyScience contributors 
+# SPDX-License-Identifier: BSD-3-Clause
+"""Tests for analysis/categories/fitting/factory.py."""
+
+
+def test_fitting_factory_supported_tags():
+    from easydiffraction.analysis.categories.fitting.factory import FittingFactory
+
+    assert 'default' in FittingFactory.supported_tags()
+
+
+def test_fitting_factory_default_tag():
+    from easydiffraction.analysis.categories.fitting.factory import FittingFactory
+
+    assert FittingFactory.default_tag() == 'default'
+
+
+def test_fitting_factory_create():
+    from easydiffraction.analysis.categories.fitting.default import Fitting
+    from easydiffraction.analysis.categories.fitting.factory import FittingFactory
+
+    fitting = FittingFactory.create('default')
+
+    assert isinstance(fitting, Fitting)
diff --git a/tests/unit/easydiffraction/analysis/categories/sequential_fit/test_default.py b/tests/unit/easydiffraction/analysis/categories/sequential_fit/test_default.py
new file mode 100644
index 000000000..79c1c1fea
--- /dev/null
+++ b/tests/unit/easydiffraction/analysis/categories/sequential_fit/test_default.py
@@ -0,0 +1,35 @@
+# SPDX-FileCopyrightText: 2026 EasyScience contributors 
+# SPDX-License-Identifier: BSD-3-Clause
+"""Tests for analysis/categories/sequential_fit/default.py."""
+
+
+def test_sequential_fit_defaults():
+    from easydiffraction.analysis.categories.sequential_fit.default import SequentialFit
+
+    sequential_fit = SequentialFit()
+
+    assert sequential_fit.data_dir.value == ''
+    assert sequential_fit.file_pattern.value == '*'
+    assert sequential_fit.max_workers.value == '1'
+    assert sequential_fit.chunk_size.value == '.'
+    assert sequential_fit.reverse.value is False
+    assert sequential_fit._identity.category_code == 'sequential_fit'
+
+
+def test_sequential_fit_as_cif_serializes_all_fields():
+    from easydiffraction.analysis.categories.sequential_fit.default import SequentialFit
+
+    sequential_fit = SequentialFit()
+    sequential_fit.data_dir = 'scans'
+    sequential_fit.file_pattern = '*.xye'
+    sequential_fit.max_workers = 'auto'
+    sequential_fit.chunk_size = '4'
+    sequential_fit.reverse = True
+
+    as_cif = sequential_fit.as_cif
+
+    assert '_sequential_fit.data_dir scans' in as_cif
+    assert '_sequential_fit.file_pattern *.xye' in as_cif
+    assert '_sequential_fit.max_workers auto' in as_cif
+    assert '_sequential_fit.chunk_size 4' in as_cif
+    assert '_sequential_fit.reverse true' in as_cif.lower()
diff --git a/tests/unit/easydiffraction/analysis/categories/sequential_fit/test_factory.py b/tests/unit/easydiffraction/analysis/categories/sequential_fit/test_factory.py
new file mode 100644
index 000000000..aa3e9c86f
--- /dev/null
+++ b/tests/unit/easydiffraction/analysis/categories/sequential_fit/test_factory.py
@@ -0,0 +1,24 @@
+# SPDX-FileCopyrightText: 2026 EasyScience contributors 
+# SPDX-License-Identifier: BSD-3-Clause
+"""Tests for analysis/categories/sequential_fit/factory.py."""
+
+
+def test_sequential_fit_factory_supported_tags():
+    from easydiffraction.analysis.categories.sequential_fit.factory import SequentialFitFactory
+
+    assert 'default' in SequentialFitFactory.supported_tags()
+
+
+def test_sequential_fit_factory_default_tag():
+    from easydiffraction.analysis.categories.sequential_fit.factory import SequentialFitFactory
+
+    assert SequentialFitFactory.default_tag() == 'default'
+
+
+def test_sequential_fit_factory_create():
+    from easydiffraction.analysis.categories.sequential_fit.default import SequentialFit
+    from easydiffraction.analysis.categories.sequential_fit.factory import SequentialFitFactory
+
+    sequential_fit = SequentialFitFactory.create('default')
+
+    assert isinstance(sequential_fit, SequentialFit)
diff --git a/tests/unit/easydiffraction/analysis/categories/sequential_fit_extract/test_default.py b/tests/unit/easydiffraction/analysis/categories/sequential_fit_extract/test_default.py
new file mode 100644
index 000000000..ccf8e2f96
--- /dev/null
+++ b/tests/unit/easydiffraction/analysis/categories/sequential_fit_extract/test_default.py
@@ -0,0 +1,66 @@
+# SPDX-FileCopyrightText: 2026 EasyScience contributors 
+# SPDX-License-Identifier: BSD-3-Clause
+"""Tests for analysis/categories/sequential_fit_extract/default.py."""
+
+import pytest
+
+
+def test_sequential_fit_extract_item_defaults():
+    from easydiffraction.analysis.categories.sequential_fit_extract.default import (
+        SequentialFitExtractItem,
+    )
+
+    item = SequentialFitExtractItem()
+
+    assert item.id.value == '_'
+    assert item.target.value == 'diffrn._'
+    assert item.pattern.value == '(.*)'
+    assert item.required.value is False
+
+
+def test_sequential_fit_extract_collection_create():
+    from easydiffraction.analysis.categories.sequential_fit_extract.default import (
+        SequentialFitExtractCollection,
+    )
+
+    collection = SequentialFitExtractCollection()
+    collection.create(
+        id='temperature',
+        target='diffrn.ambient_temperature',
+        pattern=r'temp_(\d+)',
+        required=True,
+    )
+
+    assert collection.names == ['temperature']
+    assert collection['temperature'].target.value == 'diffrn.ambient_temperature'
+    assert collection['temperature'].required.value is True
+
+
+def test_sequential_fit_extract_collection_rejects_invalid_target():
+    from easydiffraction.analysis.categories.sequential_fit_extract.default import (
+        SequentialFitExtractCollection,
+    )
+
+    collection = SequentialFitExtractCollection()
+
+    with pytest.raises(ValueError, match='must use the form'):
+        collection.create(
+            id='temperature',
+            target='experiment.ambient_temperature',
+            pattern=r'temp_(\d+)',
+        )
+
+
+def test_sequential_fit_extract_collection_rejects_invalid_pattern():
+    from easydiffraction.analysis.categories.sequential_fit_extract.default import (
+        SequentialFitExtractCollection,
+    )
+
+    collection = SequentialFitExtractCollection()
+
+    with pytest.raises(ValueError, match='must define exactly one capture group'):
+        collection.create(
+            id='temperature',
+            target='diffrn.ambient_temperature',
+            pattern=r'temp_\d+',
+        )
diff --git a/tests/unit/easydiffraction/analysis/categories/sequential_fit_extract/test_factory.py b/tests/unit/easydiffraction/analysis/categories/sequential_fit_extract/test_factory.py
new file mode 100644
index 000000000..61e626c74
--- /dev/null
+++ b/tests/unit/easydiffraction/analysis/categories/sequential_fit_extract/test_factory.py
@@ -0,0 +1,32 @@
+# SPDX-FileCopyrightText: 2026 EasyScience contributors 
+# SPDX-License-Identifier: BSD-3-Clause
+"""Tests for analysis/categories/sequential_fit_extract/factory.py."""
+
+
+def test_sequential_fit_extract_factory_supported_tags():
+    from easydiffraction.analysis.categories.sequential_fit_extract.factory import (
+        SequentialFitExtractFactory,
+    )
+
+    assert 'default' in SequentialFitExtractFactory.supported_tags()
+
+
+def test_sequential_fit_extract_factory_default_tag():
+    from easydiffraction.analysis.categories.sequential_fit_extract.factory import (
+        SequentialFitExtractFactory,
+    )
+
+    assert SequentialFitExtractFactory.default_tag() == 'default'
+
+
+def test_sequential_fit_extract_factory_create():
+    from easydiffraction.analysis.categories.sequential_fit_extract.default import (
+        SequentialFitExtractCollection,
+    )
+    from easydiffraction.analysis.categories.sequential_fit_extract.factory import (
+        SequentialFitExtractFactory,
+    )
+
+    collection = SequentialFitExtractFactory.create('default')
+
+    assert isinstance(collection, SequentialFitExtractCollection)
diff --git a/tests/unit/easydiffraction/analysis/categories/test_bayesian_convergence.py b/tests/unit/easydiffraction/analysis/categories/test_bayesian_convergence.py
new file mode 100644
index 000000000..dbcaf7299
--- /dev/null
+++ b/tests/unit/easydiffraction/analysis/categories/test_bayesian_convergence.py
@@ -0,0 +1,17 @@
+# SPDX-FileCopyrightText: 2026 EasyScience contributors 
+# SPDX-License-Identifier: BSD-3-Clause
+"""Tests for analysis/categories/bayesian_convergence/."""
+
+
+def test_bayesian_convergence_factory_create():
+    from easydiffraction.analysis.categories.bayesian_convergence.default import (
+        BayesianConvergence,
+    )
+    from easydiffraction.analysis.categories.bayesian_convergence.factory import (
+        BayesianConvergenceFactory,
+    )
+
+    convergence = BayesianConvergenceFactory.create('default')
+
+    assert BayesianConvergenceFactory.default_tag() == 'default'
+    assert isinstance(convergence, BayesianConvergence)
diff --git a/tests/unit/easydiffraction/analysis/categories/test_bayesian_distribution_caches.py b/tests/unit/easydiffraction/analysis/categories/test_bayesian_distribution_caches.py
new file mode 100644
index 000000000..26e91f732
--- /dev/null
+++ b/tests/unit/easydiffraction/analysis/categories/test_bayesian_distribution_caches.py
@@ -0,0 +1,17 @@
+# SPDX-FileCopyrightText: 2026 EasyScience contributors 
+# SPDX-License-Identifier: BSD-3-Clause
+"""Tests for analysis/categories/bayesian_distribution_caches/."""
+
+
+def test_bayesian_distribution_caches_factory_create():
+    from easydiffraction.analysis.categories.bayesian_distribution_caches.default import (
+        BayesianDistributionCaches,
+    )
+    from easydiffraction.analysis.categories.bayesian_distribution_caches.factory import (
+        BayesianDistributionCachesFactory,
+    )
+
+    caches = BayesianDistributionCachesFactory.create('default')
+
+    assert BayesianDistributionCachesFactory.default_tag() == 'default'
+    assert isinstance(caches, BayesianDistributionCaches)
diff --git a/tests/unit/easydiffraction/analysis/categories/test_bayesian_pair_caches.py b/tests/unit/easydiffraction/analysis/categories/test_bayesian_pair_caches.py
new file mode 100644
index 000000000..f4307f2c4
--- /dev/null
+++ b/tests/unit/easydiffraction/analysis/categories/test_bayesian_pair_caches.py
@@ -0,0 +1,15 @@
+# SPDX-FileCopyrightText: 2026 EasyScience contributors 
+# SPDX-License-Identifier: BSD-3-Clause
+"""Tests for analysis/categories/bayesian_pair_caches/."""
+
+
+def test_bayesian_pair_caches_factory_create():
+    from easydiffraction.analysis.categories.bayesian_pair_caches.default import BayesianPairCaches
+    from easydiffraction.analysis.categories.bayesian_pair_caches.factory import (
+        BayesianPairCachesFactory,
+    )
+
+    caches = BayesianPairCachesFactory.create('default')
+
+    assert BayesianPairCachesFactory.default_tag() == 'default'
+    assert isinstance(caches, BayesianPairCaches)
diff --git a/tests/unit/easydiffraction/analysis/categories/test_bayesian_parameter_posteriors.py b/tests/unit/easydiffraction/analysis/categories/test_bayesian_parameter_posteriors.py
new file mode 100644
index 000000000..18aa5f019
--- /dev/null
+++ b/tests/unit/easydiffraction/analysis/categories/test_bayesian_parameter_posteriors.py
@@ -0,0 +1,17 @@
+# SPDX-FileCopyrightText: 2026 EasyScience contributors 
+# SPDX-License-Identifier: BSD-3-Clause
+"""Tests for analysis/categories/bayesian_parameter_posteriors/."""
+
+
+def test_bayesian_parameter_posteriors_factory_create():
+    from easydiffraction.analysis.categories.bayesian_parameter_posteriors.default import (
+        BayesianParameterPosteriors,
+    )
+    from easydiffraction.analysis.categories.bayesian_parameter_posteriors.factory import (
+        BayesianParameterPosteriorsFactory,
+    )
+
+    posteriors = BayesianParameterPosteriorsFactory.create('default')
+
+    assert BayesianParameterPosteriorsFactory.default_tag() == 'default'
+    assert isinstance(posteriors, BayesianParameterPosteriors)
diff --git a/tests/unit/easydiffraction/analysis/categories/test_bayesian_predictive_datasets.py b/tests/unit/easydiffraction/analysis/categories/test_bayesian_predictive_datasets.py
new file mode 100644
index 000000000..93746fb41
--- /dev/null
+++ b/tests/unit/easydiffraction/analysis/categories/test_bayesian_predictive_datasets.py
@@ -0,0 +1,17 @@
+# SPDX-FileCopyrightText: 2026 EasyScience contributors 
+# SPDX-License-Identifier: BSD-3-Clause
+"""Tests for analysis/categories/bayesian_predictive_datasets/."""
+
+
+def test_bayesian_predictive_datasets_factory_create():
+    from easydiffraction.analysis.categories.bayesian_predictive_datasets.default import (
+        BayesianPredictiveDatasets,
+    )
+    from easydiffraction.analysis.categories.bayesian_predictive_datasets.factory import (
+        BayesianPredictiveDatasetsFactory,
+    )
+
+    datasets = BayesianPredictiveDatasetsFactory.create('default')
+
+    assert BayesianPredictiveDatasetsFactory.default_tag() == 'default'
+    assert isinstance(datasets, BayesianPredictiveDatasets)
diff --git a/tests/unit/easydiffraction/analysis/categories/test_bayesian_result.py b/tests/unit/easydiffraction/analysis/categories/test_bayesian_result.py
new file mode 100644
index 000000000..e1d985385
--- /dev/null
+++ b/tests/unit/easydiffraction/analysis/categories/test_bayesian_result.py
@@ -0,0 +1,13 @@
+# SPDX-FileCopyrightText: 2026 EasyScience contributors 
+# SPDX-License-Identifier: BSD-3-Clause
+"""Tests for analysis/categories/bayesian_result/."""
+
+
+def test_bayesian_result_factory_create():
+    from easydiffraction.analysis.categories.bayesian_result.default import BayesianResult
+    from easydiffraction.analysis.categories.bayesian_result.factory import BayesianResultFactory
+
+    result = BayesianResultFactory.create('default')
+
+    assert BayesianResultFactory.default_tag() == 'default'
+    assert isinstance(result, BayesianResult)
diff --git a/tests/unit/easydiffraction/analysis/categories/test_bayesian_sampler.py b/tests/unit/easydiffraction/analysis/categories/test_bayesian_sampler.py
new file mode 100644
index 000000000..976341765
--- /dev/null
+++ b/tests/unit/easydiffraction/analysis/categories/test_bayesian_sampler.py
@@ -0,0 +1,13 @@
+# SPDX-FileCopyrightText: 2026 EasyScience contributors 
+# SPDX-License-Identifier: BSD-3-Clause
+"""Tests for analysis/categories/bayesian_sampler/."""
+
+
+def test_bayesian_sampler_factory_create():
+    from easydiffraction.analysis.categories.bayesian_sampler.default import BayesianSampler
+    from easydiffraction.analysis.categories.bayesian_sampler.factory import BayesianSamplerFactory
+
+    sampler = BayesianSamplerFactory.create('default')
+
+    assert BayesianSamplerFactory.default_tag() == 'default'
+    assert isinstance(sampler, BayesianSampler)
diff --git a/tests/unit/easydiffraction/analysis/categories/test_constraints.py b/tests/unit/easydiffraction/analysis/categories/test_constraints.py
index 15dddc4f2..07016b298 100644
--- a/tests/unit/easydiffraction/analysis/categories/test_constraints.py
+++ b/tests/unit/easydiffraction/analysis/categories/test_constraints.py
@@ -1,16 +1,63 @@
 # SPDX-FileCopyrightText: 2025 EasyScience contributors 
 # SPDX-License-Identifier: BSD-3-Clause
 
+import gemmi
+
 from easydiffraction.analysis.categories.constraints import Constraint
 from easydiffraction.analysis.categories.constraints import Constraints
 
 
 def test_constraint_creation_and_collection():
     c = Constraint()
+    c.id = 'constraint_1'
     c.expression = 'a = b + c'
+    assert c.id.value == 'constraint_1'
     assert c.lhs_alias == 'a'
     assert c.rhs_expr == 'b + c'
+
     coll = Constraints()
     coll.create(expression='a = b + c')
     assert 'a' in coll.names
+    assert coll['a'].id.value == 'a'
     assert coll['a'].rhs_expr == 'b + c'
+
+
+def test_constraints_create_uses_explicit_id():
+    coll = Constraints()
+
+    coll.create(id='constraint_1', expression='a = b + c')
+
+    assert coll.names == ['constraint_1']
+    assert coll['constraint_1'].id.value == 'constraint_1'
+    assert coll['constraint_1'].lhs_alias == 'a'
+    assert coll['constraint_1'].rhs_expr == 'b + c'
+
+
+def test_constraints_from_cif_preserves_explicit_id_keys():
+    doc = gemmi.cif.read_string(
+        'data_constraints\n\n'
+        'loop_\n'
+        '_constraint.id\n'
+        '_constraint.expression\n'
+        'constraint_1 "a = b + c"\n',
+    )
+    coll = Constraints()
+
+    coll.from_cif(doc.sole_block())
+
+    assert coll.names == ['constraint_1']
+    assert coll['constraint_1'].id.value == 'constraint_1'
+    assert coll['constraint_1'].lhs_alias == 'a'
+
+
+def test_constraints_from_cif_backfills_missing_id_from_lhs_alias():
+    doc = gemmi.cif.read_string(
+        'data_constraints\n\nloop_\n_constraint.expression\n"a = b + c"\n',
+    )
+    coll = Constraints()
+
+    coll.from_cif(doc.sole_block())
+
+    assert coll.names == ['a']
+    assert coll['a'].id.value == 'a'
+    assert coll['a'].expression.value == 'a = b + c'
diff --git a/tests/unit/easydiffraction/analysis/categories/test_deterministic_result.py b/tests/unit/easydiffraction/analysis/categories/test_deterministic_result.py
new file mode 100644
index 000000000..ed764146d
--- /dev/null
+++ b/tests/unit/easydiffraction/analysis/categories/test_deterministic_result.py
@@ -0,0 +1,17 @@
+# SPDX-FileCopyrightText: 2026 EasyScience contributors 
+# SPDX-License-Identifier: BSD-3-Clause
+"""Tests for analysis/categories/deterministic_result/."""
+
+
+def test_deterministic_result_factory_create():
+    from easydiffraction.analysis.categories.deterministic_result.default import (
+        DeterministicResult,
+    )
+    from easydiffraction.analysis.categories.deterministic_result.factory import (
+        DeterministicResultFactory,
+    )
+
+    result = DeterministicResultFactory.create('default')
+
+    assert DeterministicResultFactory.default_tag() == 'default'
+    assert isinstance(result, DeterministicResult)
diff --git a/tests/unit/easydiffraction/analysis/categories/test_fit.py b/tests/unit/easydiffraction/analysis/categories/test_fit.py
index aeac67736..8cbc25e12 100644
--- a/tests/unit/easydiffraction/analysis/categories/test_fit.py
+++ b/tests/unit/easydiffraction/analysis/categories/test_fit.py
@@ -1,31 +1,31 @@
 # SPDX-FileCopyrightText: 2026 EasyScience contributors 
 # SPDX-License-Identifier: BSD-3-Clause
-"""Tests for the fit category."""
+"""Tests for the fitting category."""
 
 
 def test_module_import():
-    import easydiffraction.analysis.categories.fit as MUT
+    import easydiffraction.analysis.categories.fitting as MUT
 
-    expected_module_name = 'easydiffraction.analysis.categories.fit'
+    expected_module_name = 'easydiffraction.analysis.categories.fitting'
     actual_module_name = MUT.__name__
     assert expected_module_name == actual_module_name
 
 
 class TestFitModeEnum:
     def test_members(self):
-        from easydiffraction.analysis.categories.fit.enums import FitModeEnum
+        from easydiffraction.analysis.enums import FitModeEnum
 
         assert FitModeEnum.SINGLE == 'single'
         assert FitModeEnum.JOINT == 'joint'
         assert FitModeEnum.SEQUENTIAL == 'sequential'
 
     def test_default(self):
-        from easydiffraction.analysis.categories.fit.enums import FitModeEnum
+        from easydiffraction.analysis.enums import FitModeEnum
 
         assert FitModeEnum.default() is FitModeEnum.SINGLE
 
     def test_descriptions(self):
-        from easydiffraction.analysis.categories.fit.enums import FitModeEnum
+        from easydiffraction.analysis.enums import FitModeEnum
 
         for member in FitModeEnum:
             desc = member.description()
@@ -33,60 +33,53 @@ def test_descriptions(self):
             assert len(desc) > 0
 
 
-class TestFitFactory:
+class TestFittingFactory:
     def test_supported_tags(self):
-        from easydiffraction.analysis.categories.fit.factory import FitFactory
+        from easydiffraction.analysis.categories.fitting.factory import FittingFactory
 
-        tags = FitFactory.supported_tags()
+        tags = FittingFactory.supported_tags()
         assert 'default' in tags
 
     def test_default_tag(self):
-        from easydiffraction.analysis.categories.fit.factory import FitFactory
+        from easydiffraction.analysis.categories.fitting.factory import FittingFactory
 
-        assert FitFactory.default_tag() == 'default'
+        assert FittingFactory.default_tag() == 'default'
 
     def test_create(self):
-        from easydiffraction.analysis.categories.fit.default import Fit
-        from easydiffraction.analysis.categories.fit.factory import FitFactory
+        from easydiffraction.analysis.categories.fitting.default import Fitting
+        from easydiffraction.analysis.categories.fitting.factory import FittingFactory
 
-        obj = FitFactory.create('default')
-        assert isinstance(obj, Fit)
+        obj = FittingFactory.create('default')
+        assert isinstance(obj, Fitting)
 
 
-class TestFit:
+class TestFitting:
     def test_instantiation(self):
-        from easydiffraction.analysis.categories.fit.default import Fit
+        from easydiffraction.analysis.categories.fitting.default import Fitting
 
-        fit = Fit()
-        assert fit is not None
+        fitting = Fitting()
+        assert fitting is not None
 
     def test_type_info(self):
-        from easydiffraction.analysis.categories.fit.default import Fit
+        from easydiffraction.analysis.categories.fitting.default import Fitting
 
-        assert Fit.type_info.tag == 'default'
+        assert Fitting.type_info.tag == 'default'
 
     def test_identity_category_code(self):
-        from easydiffraction.analysis.categories.fit.default import Fit
+        from easydiffraction.analysis.categories.fitting.default import Fitting
 
-        fit = Fit()
-        assert fit._identity.category_code == 'fit'
+        fitting = Fitting()
+        assert fitting._identity.category_code == 'fitting'
 
-    def test_mode_default(self):
-        from easydiffraction.analysis.categories.fit.default import Fit
-        from easydiffraction.analysis.categories.fit.enums import FitModeEnum
-
-        fit = Fit()
-        assert fit.mode.value == FitModeEnum.default().value
-
-    def test_mode_setter(self):
-        from easydiffraction.analysis.categories.fit.default import Fit
+    def test_minimizer_default(self):
+        from easydiffraction.analysis.categories.fitting.default import Fitting
 
-        fit = Fit()
-        fit.mode = 'joint'
-        assert fit.mode.value == 'joint'
+        fitting = Fitting()
+        assert fitting.minimizer_type.value == 'lmfit (leastsq)'
 
-    def test_minimizer_default(self):
-        from easydiffraction.analysis.categories.fit.default import Fit
+    def test_minimizer_type_setter(self):
+        from easydiffraction.analysis.categories.fitting.default import Fitting
 
-        fit = Fit()
-        assert fit.minimizer_type.value == 'lmfit (leastsq)'
+        fitting = Fitting()
+        fitting.minimizer_type = 'lmfit'
+        assert fitting.minimizer_type.value == 'lmfit'
diff --git a/tests/unit/easydiffraction/analysis/categories/test_fit_parameter_correlations.py b/tests/unit/easydiffraction/analysis/categories/test_fit_parameter_correlations.py
new file mode 100644
index 000000000..8cb72d57b
--- /dev/null
+++ b/tests/unit/easydiffraction/analysis/categories/test_fit_parameter_correlations.py
@@ -0,0 +1,17 @@
+# SPDX-FileCopyrightText: 2026 EasyScience contributors 
+# SPDX-License-Identifier: BSD-3-Clause
+"""Tests for analysis/categories/fit_parameter_correlations/."""
+
+
+def test_fit_parameter_correlations_factory_create():
+    from easydiffraction.analysis.categories.fit_parameter_correlations.default import (
+        FitParameterCorrelations,
+    )
+    from easydiffraction.analysis.categories.fit_parameter_correlations.factory import (
+        FitParameterCorrelationsFactory,
+    )
+
+    correlations = FitParameterCorrelationsFactory.create('default')
+
+    assert FitParameterCorrelationsFactory.default_tag() == 'default'
+    assert isinstance(correlations, FitParameterCorrelations)
diff --git a/tests/unit/easydiffraction/analysis/categories/test_fit_parameters.py b/tests/unit/easydiffraction/analysis/categories/test_fit_parameters.py
new file mode 100644
index 000000000..e3d0cedde
--- /dev/null
+++ b/tests/unit/easydiffraction/analysis/categories/test_fit_parameters.py
@@ -0,0 +1,13 @@
+# SPDX-FileCopyrightText: 2026 EasyScience contributors 
+# SPDX-License-Identifier: BSD-3-Clause
+"""Tests for analysis/categories/fit_parameters/."""
+
+
+def test_fit_parameters_factory_create():
+    from easydiffraction.analysis.categories.fit_parameters.default import FitParameters
+    from easydiffraction.analysis.categories.fit_parameters.factory import FitParametersFactory
+
+    collection = FitParametersFactory.create('default')
+
+    assert FitParametersFactory.default_tag() == 'default'
+    assert isinstance(collection, FitParameters)
diff --git a/tests/unit/easydiffraction/analysis/categories/test_fit_result.py b/tests/unit/easydiffraction/analysis/categories/test_fit_result.py
new file mode 100644
index 000000000..4c9d869dc
--- /dev/null
+++ b/tests/unit/easydiffraction/analysis/categories/test_fit_result.py
@@ -0,0 +1,13 @@
+# SPDX-FileCopyrightText: 2026 EasyScience contributors 
+# SPDX-License-Identifier: BSD-3-Clause
+"""Tests for analysis/categories/fit_result/."""
+
+
+def test_fit_result_factory_create():
+    from easydiffraction.analysis.categories.fit_result.default import FitResult
+    from easydiffraction.analysis.categories.fit_result.factory import FitResultFactory
+
+    fit_result = FitResultFactory.create('default')
+
+    assert FitResultFactory.default_tag() == 'default'
+    assert isinstance(fit_result, FitResult)
diff --git a/tests/unit/easydiffraction/analysis/categories/test_fit_state.py b/tests/unit/easydiffraction/analysis/categories/test_fit_state.py
new file mode 100644
index 000000000..236c798a5
--- /dev/null
+++ b/tests/unit/easydiffraction/analysis/categories/test_fit_state.py
@@ -0,0 +1,207 @@
+# SPDX-FileCopyrightText: 2026 EasyScience contributors 
+# SPDX-License-Identifier: BSD-3-Clause
+"""Tests for persisted fit-state analysis categories."""
+
+from __future__ import annotations
+
+import gemmi
+
+
+def test_fit_parameter_collection_is_empty_until_rows_exist():
+    from easydiffraction.analysis.categories.fit_parameters.default import FitParameters
+
+    collection = FitParameters()
+
+    assert collection.as_cif == ''
+
+
+def test_fit_parameter_collection_serializes_expected_tags_and_values():
+    from easydiffraction.analysis.categories.fit_parameters.default import FitParameters
+
+    collection = FitParameters()
+    collection.create(
+        param_unique_name='lbco.cell.length_a',
+        fit_min=3.88,
+        fit_max=3.90,
+        fit_bounds_uncertainty_multiplier=4.0,
+        start_value=3.89,
+        start_uncertainty=0.01,
+    )
+
+    cif_text = collection.as_cif
+
+    assert '_fit_parameter.param_unique_name' in cif_text
+    assert '_fit_parameter.fit_bounds_uncertainty_multiplier' in cif_text
+    assert 'lbco.cell.length_a' in cif_text
+
+
+def test_fit_result_serializes_expected_tags_and_enum_value():
+    from easydiffraction.analysis.categories.fit_result.default import FitResult
+
+    fit_result = FitResult()
+    fit_result._set_result_kind('bayesian')
+    fit_result._set_success(value=True)
+    fit_result._set_message('Sampler completed')
+    fit_result._set_iterations(3000)
+    fit_result._set_fitting_time(82.4)
+    fit_result._set_reduced_chi_square(1.031)
+
+    cif_text = fit_result.as_cif
+
+    assert '_fit_result.result_kind bayesian' in cif_text
+    assert '_fit_result.iterations 3000' in cif_text
+    assert '_fit_result.reduced_chi_square' in cif_text
+
+
+def test_fit_parameter_correlations_normalize_pair_order_and_replace_duplicate_ids():
+    from easydiffraction.analysis.categories.fit_parameter_correlations.default import (
+        FitParameterCorrelations,
+    )
+
+    correlations = FitParameterCorrelations()
+    correlations.create(
+        source_kind='posterior',
+        param_unique_name_i='z.param',
+        param_unique_name_j='a.param',
+        correlation=0.87,
+        id='1',
+    )
+    correlations.create(
+        source_kind='posterior',
+        param_unique_name_i='b.param',
+        param_unique_name_j='c.param',
+        correlation=0.55,
+        id='1',
+    )
+
+    assert len(correlations) == 1
+    assert correlations['1'].param_unique_name_i.value == 'b.param'
+    assert correlations['1'].param_unique_name_j.value == 'c.param'
+
+
+def test_fit_parameter_correlations_rebuild_index_from_cif():
+    from easydiffraction.analysis.categories.fit_parameter_correlations.default import (
+        FitParameterCorrelations,
+    )
+
+    cif_text = """data_fit_state
+loop_
+_fit_parameter_correlation.id
+_fit_parameter_correlation.source_kind
+_fit_parameter_correlation.param_unique_name_i
+_fit_parameter_correlation.param_unique_name_j
+_fit_parameter_correlation.correlation
+2 posterior hrpt.scale lbco.cell.length_a 0.42
+"""
+    document = gemmi.cif.read_string(cif_text)
+
+    correlations = FitParameterCorrelations()
+    correlations.from_cif(document.sole_block())
+
+    assert correlations.names == ['2']
+    assert correlations['2'].correlation.value == 0.42
+
+
+def test_bayesian_cache_manifest_collections_serialize_expected_keys():
+    from easydiffraction.analysis.categories.bayesian_distribution_caches.default import (
+        BayesianDistributionCaches,
+    )
+    from easydiffraction.analysis.categories.bayesian_pair_caches.default import (
+        BayesianPairCachePaths,
+        BayesianPairCaches,
+    )
+    from easydiffraction.analysis.categories.bayesian_predictive_datasets.default import (
+        BayesianPredictiveDatasetPaths,
+        BayesianPredictiveDatasets,
+    )
+
+    distributions = BayesianDistributionCaches()
+    distributions.create(
+        param_unique_name='lbco.cell.length_a',
+        x_path='/posterior/distribution/0/x',
+        density_path='/posterior/distribution/0/density',
+        n_grid=256,
+        n_draws_cached=48000,
+    )
+    pairs = BayesianPairCaches()
+    pairs.create(
+        parameter_names=('z.param', 'a.param'),
+        paths=BayesianPairCachePaths(
+            x_path='/posterior/pairs/0/x',
+            y_path='/posterior/pairs/0/y',
+            density_path='/posterior/pairs/0/density',
+            contour_level_path='/posterior/pairs/0/contour_levels',
+        ),
+        grid_shape=(64, 64),
+        n_draws_cached=4000,
+        id='7',
+    )
+    predictive = BayesianPredictiveDatasets()
+    predictive.create(
+        experiment_name='hrpt',
+        x_axis_name='two_theta',
+        paths=BayesianPredictiveDatasetPaths(
+            x_path='/predictive/hrpt/x',
+            best_sample_prediction_path='/predictive/hrpt/best_sample_prediction',
+            lower_95_path='/predictive/hrpt/lower_95',
+            upper_95_path='/predictive/hrpt/upper_95',
+        ),
+        n_x=2500,
+        n_draws_cached=0,
+    )
+
+    assert '_bayesian_distribution_cache.param_unique_name' in distributions.as_cif
+    assert pairs['7'].param_unique_name_x.value == 'a.param'
+    assert pairs['7'].param_unique_name_y.value == 'z.param'
+    assert '_bayesian_predictive_dataset.experiment_name' in predictive.as_cif
+
+
+def test_bayesian_sampler_and_convergence_use_integer_fields_in_cif():
+    from easydiffraction.analysis.categories.bayesian_convergence.default import (
+        BayesianConvergence,
+    )
+    from easydiffraction.analysis.categories.bayesian_sampler.default import BayesianSampler
+
+    sampler = BayesianSampler()
+    sampler._set_steps(100)
+    sampler._set_burn(20)
+    sampler._set_parallel(0)
+    sampler._set_random_seed(123)
+
+    convergence = BayesianConvergence()
+    convergence._set_n_draws(80)
+    convergence._set_n_chains(4)
+    convergence._set_n_parameters(3)
+
+    assert '_bayesian_sampler.steps 100' in sampler.as_cif
+    assert '_bayesian_sampler.parallel 0' in sampler.as_cif
+    assert '_bayesian_convergence.n_draws 80' in convergence.as_cif
+
+
+def test_bayesian_parameter_posteriors_preserve_row_order_from_cif():
+    from easydiffraction.analysis.categories.bayesian_parameter_posteriors.default import (
+        BayesianParameterPosteriors,
+    )
+
+    cif_text = """data_fit_state
+loop_
+_bayesian_parameter_posterior.unique_name
+_bayesian_parameter_posterior.display_name
+_bayesian_parameter_posterior.best_sample_value
+_bayesian_parameter_posterior.median
+_bayesian_parameter_posterior.uncertainty
+_bayesian_parameter_posterior.interval_68_lower
+_bayesian_parameter_posterior.interval_68_upper
+_bayesian_parameter_posterior.interval_95_lower
+_bayesian_parameter_posterior.interval_95_upper
+_bayesian_parameter_posterior.ess_bulk
+_bayesian_parameter_posterior.r_hat
+second.param second 2.0 2.1 0.2 1.9 2.3 1.8 2.4 20 1.01
+first.param first 1.0 1.1 0.1 0.9 1.3 0.8 1.4 10 1.00
+"""
+    document = gemmi.cif.read_string(cif_text)
+
+    posteriors = BayesianParameterPosteriors()
+    posteriors.from_cif(document.sole_block())
+
+    assert [row.unique_name.value for row in posteriors] == ['second.param', 'first.param']
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 51777f118..587a17cc2 100644
--- a/tests/unit/easydiffraction/analysis/categories/test_joint_fit_experiments.py
+++ b/tests/unit/easydiffraction/analysis/categories/test_joint_fit_experiments.py
@@ -1,17 +1,17 @@
 # SPDX-FileCopyrightText: 2025 EasyScience contributors 
 # SPDX-License-Identifier: BSD-3-Clause
 
-from easydiffraction.analysis.categories.joint_fit_experiments import JointFitExperiment
-from easydiffraction.analysis.categories.joint_fit_experiments import JointFitExperiments
+from easydiffraction.analysis.categories.joint_fit import JointFitCollection
+from easydiffraction.analysis.categories.joint_fit import JointFitItem
 
 
-def test_joint_fit_experiment_and_collection():
-    j = JointFitExperiment()
-    j.id = 'ex1'
+def test_joint_fit_item_and_collection():
+    j = JointFitItem()
+    j.experiment_id = 'ex1'
     j.weight = 0.5
-    assert j.id.value == 'ex1'
+    assert j.experiment_id.value == 'ex1'
     assert j.weight.value == 0.5
-    coll = JointFitExperiments()
-    coll.create(id='ex1', weight=0.5)
+    coll = JointFitCollection()
+    coll.create(experiment_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_bayesian.py b/tests/unit/easydiffraction/analysis/fit_helpers/test_bayesian.py
new file mode 100644
index 000000000..348f380f3
--- /dev/null
+++ b/tests/unit/easydiffraction/analysis/fit_helpers/test_bayesian.py
@@ -0,0 +1,385 @@
+# SPDX-FileCopyrightText: 2026 EasyScience contributors 
+# SPDX-License-Identifier: BSD-3-Clause
+
+from __future__ import annotations
+
+import numpy as np
+import pytest
+
+
+class Identity:
+    def __init__(self) -> None:
+        self.datablock_entry_name = 'db'
+        self.category_code = 'cat'
+        self.category_entry_name = 'entry'
+
+
+class Param:
+    def __init__(self, unique_name: str, start: float, value: float, uncertainty: float) -> None:
+        self._identity = Identity()
+        self._fit_start_value = start
+        self.unique_name = unique_name
+        self.name = unique_name
+        self.value = value
+        self.uncertainty = uncertainty
+        self.units = 'arb'
+
+
+def test_module_import():
+    import easydiffraction.analysis.fit_helpers.bayesian as MUT
+
+    assert MUT.__name__ == 'easydiffraction.analysis.fit_helpers.bayesian'
+
+
+def test_posterior_samples_flatten_and_to_arviz():
+    from easydiffraction.analysis.fit_helpers.bayesian import PosteriorSamples
+
+    posterior_samples = PosteriorSamples(
+        parameter_names=['a', 'b'],
+        parameter_samples=np.array(
+            [
+                [[1.0, 10.0], [2.0, 20.0]],
+                [[3.0, 30.0], [4.0, 40.0]],
+            ],
+            dtype=float,
+        ),
+        log_posterior=np.array([[0.1, 0.2], [0.3, 0.4]], dtype=float),
+    )
+
+    flattened = posterior_samples.flattened()
+    inference_data = posterior_samples.to_arviz()
+
+    assert flattened.shape == (4, 2)
+    np.testing.assert_allclose(flattened[:, 0], np.array([1.0, 2.0, 3.0, 4.0]))
+    np.testing.assert_allclose(flattened[:, 1], np.array([10.0, 20.0, 30.0, 40.0]))
+    assert set(inference_data.posterior.data_vars) == {'a', 'b'}
+    assert inference_data.posterior['a'].shape == (2, 2)
+    assert inference_data.sample_stats['lp'].shape == (2, 2)
+
+
+def test_posterior_samples_to_arviz_validates_shapes():
+    from easydiffraction.analysis.fit_helpers.bayesian import PosteriorSamples
+
+    posterior_samples = PosteriorSamples(
+        parameter_names=['a'],
+        parameter_samples=np.array([1.0, 2.0]),
+    )
+
+    with pytest.raises(
+        ValueError,
+        match=r'Posterior sample array must have shape \(n_draws, n_chains, n_parameters\)\.',
+    ):
+        posterior_samples.to_arviz()
+
+
+def test_compute_convergence_diagnostics_treats_non_finite_values_as_not_converged(monkeypatch):
+    from easydiffraction.analysis.fit_helpers.bayesian import PosteriorSamples
+    from easydiffraction.analysis.fit_helpers.bayesian import compute_convergence_diagnostics
+
+    posterior_samples = PosteriorSamples(
+        parameter_names=['a'],
+        parameter_samples=np.ones((4, 2, 1), dtype=float),
+    )
+
+    fake_dataset = type('FakeDataset', (), {'data_vars': {'a': np.array([np.nan], dtype=float)}})
+
+    monkeypatch.setattr(
+        'easydiffraction.analysis.fit_helpers.bayesian.az.rhat',
+        lambda inference_data: fake_dataset,
+    )
+    monkeypatch.setattr(
+        'easydiffraction.analysis.fit_helpers.bayesian.az.ess',
+        lambda inference_data, method='bulk': type(
+            'FakeDataset', (), {'data_vars': {'a': np.array([4000.0], dtype=float)}}
+        ),
+    )
+
+    diagnostics = compute_convergence_diagnostics(posterior_samples)
+
+    assert diagnostics['converged'] is False
+    assert diagnostics['r_hat_by_parameter'] == {'a': None}
+    assert diagnostics['ess_bulk_by_parameter'] == {'a': 4000.0}
+    assert diagnostics['max_r_hat'] is None
+    assert diagnostics['min_ess_bulk'] == pytest.approx(4000.0)
+
+
+def test_summarize_posterior_parameters_preserves_order_and_display_names():
+    from easydiffraction.analysis.fit_helpers.bayesian import PosteriorSamples
+    from easydiffraction.analysis.fit_helpers.bayesian import summarize_posterior_parameters
+
+    posterior_samples = PosteriorSamples(
+        parameter_names=['beta', 'alpha'],
+        parameter_samples=np.array(
+            [
+                [[2.0, 1.0], [2.2, 1.2]],
+                [[1.8, 0.8], [2.1, 1.1]],
+            ],
+            dtype=float,
+        ),
+    )
+
+    summaries = summarize_posterior_parameters(
+        parameter_names=['beta', 'alpha'],
+        posterior_samples=posterior_samples,
+        best_sample_values=np.array([2.05, 1.05]),
+        parameter_display_names=['Beta width', 'Alpha shift'],
+        convergence_diagnostics={
+            'r_hat_by_parameter': {'beta': 1.02, 'alpha': 1.0},
+            'ess_bulk_by_parameter': {'beta': 120.0, 'alpha': 800.0},
+        },
+    )
+
+    assert [summary.unique_name for summary in summaries] == ['beta', 'alpha']
+    assert [summary.display_name for summary in summaries] == ['Beta width', 'Alpha shift']
+    assert summaries[0].r_hat == pytest.approx(1.02)
+    assert summaries[0].ess_bulk == pytest.approx(120.0)
+    assert summaries[1].r_hat == pytest.approx(1.0)
+    assert summaries[1].ess_bulk == pytest.approx(800.0)
+
+
+def test_bayesian_fit_results_display_results_prints_sampler_and_convergence(capsys, monkeypatch):
+    from easydiffraction.analysis.fit_helpers.bayesian import BayesianFitResults
+    from easydiffraction.analysis.fit_helpers.bayesian import PosteriorParameterSummary
+    from easydiffraction.utils.logging import Logger
+
+    monkeypatch.setattr(Logger, '_reaction', Logger.Reaction.WARN, raising=True)
+
+    results = BayesianFitResults(
+        success=True,
+        parameters=[Param(unique_name='a', start=1.0, value=1.2, uncertainty=0.05)],
+        reduced_chi_square=1.2345,
+        fitting_time=0.9876,
+        sampler_name='dream',
+        sampler_completed=True,
+        sampler_settings={
+            'random_seed': 1313900679,
+            'steps': 200,
+            'burn': 50,
+            'thin': 1,
+            'pop': 4,
+            'init': 'lhs',
+            'samples': 3200,
+        },
+        convergence_diagnostics={
+            'converged': False,
+            'max_r_hat': 1.107,
+            'min_ess_bulk': 125.9,
+            'n_draws': 200,
+            'n_chains': 16,
+        },
+        posterior_parameter_summaries=[
+            PosteriorParameterSummary(
+                unique_name='a',
+                display_name='a',
+                best_sample_value=1.2,
+                median=1.15,
+                standard_deviation=0.05,
+                interval_68=(1.1, 1.2),
+                interval_95=(1.0, 1.3),
+                r_hat=1.107,
+                ess_bulk=125.9,
+            )
+        ],
+        best_log_posterior=-12.34,
+    )
+    results.message = 'DREAM sampling completed'
+
+    results.display_results(y_obs=[10.0, 20.0], y_calc=[9.5, 19.5])
+
+    out = capsys.readouterr().out
+    assert 'Bayesian fit results' in out
+    assert 'Overall status: completed with warnings' in out
+    assert 'Sampler status: DREAM sampling completed' in out
+    assert 'Sampler: dream' in out
+    assert 'Sampler completed: yes' in out
+    assert 'steps=200' in out
+    assert 'init=lhs' in out
+    assert 'random_seed=1313900679' not in out
+    assert 'status=failed' in out
+    assert 'max_r_hat=1.107' in out
+    assert 'min_ess_bulk=125.9' in out
+    assert 'Posterior parameter summaries:' in out
+    assert 'Success: True' not in out
+    assert 'datablock' in out
+    assert 'category' in out
+    assert 'entry' in out
+    assert '95% interval' in out
+    assert '68% interval' not in out
+    assert 'std' not in out
+
+    monkeypatch.setattr(Logger, '_reaction', Logger.Reaction.RAISE, raising=True)
+
+
+def test_build_posterior_summary_row_restores_identifier_columns():
+    from easydiffraction.analysis.fit_helpers.bayesian import PosteriorParameterSummary
+    from easydiffraction.analysis.fit_helpers.bayesian import _build_posterior_summary_row
+
+    parameter = Param(unique_name='a', start=1.0, value=1.2, uncertainty=0.05)
+    summary = PosteriorParameterSummary(
+        unique_name='a',
+        display_name='a',
+        best_sample_value=1.2,
+        median=1.15,
+        standard_deviation=0.05,
+        interval_68=(1.1, 1.2),
+        interval_95=(1.0, 1.3),
+        r_hat=1.107,
+        ess_bulk=125.9,
+    )
+
+    row = _build_posterior_summary_row(summary, {'a': parameter})
+
+    assert row == [
+        'db',
+        'cat',
+        'entry',
+        'a',
+        'arb',
+        '1.1500',
+        '[1.0000, 1.3000]',
+        '[red]1.107[/red]',
+        '[red]125.9[/red]',
+    ]
+
+
+def test_render_committed_parameter_table_places_units_after_parameter(monkeypatch):
+    from easydiffraction.analysis.fit_helpers import bayesian
+
+    captured: dict[str, object] = {}
+
+    def fake_render_table(*, columns_headers, columns_alignment, columns_data):
+        captured['columns_headers'] = columns_headers
+        captured['columns_alignment'] = columns_alignment
+        captured['columns_data'] = columns_data
+
+    monkeypatch.setattr(bayesian, 'render_table', fake_render_table)
+
+    bayesian._render_committed_parameter_table([
+        Param(unique_name='a', start=1.0, value=1.2, uncertainty=0.05)
+    ])
+
+    assert captured['columns_headers'] == [
+        'datablock',
+        'category',
+        'entry',
+        'parameter',
+        'units',
+        'start',
+        'best posterior sample',
+        'uncertainty',
+        'change',
+    ]
+    assert captured['columns_alignment'] == [
+        'left',
+        'left',
+        'left',
+        'left',
+        'left',
+        'right',
+        'right',
+        'right',
+        'right',
+    ]
+    assert captured['columns_data'] == [
+        [
+            'db',
+            'cat',
+            'entry',
+            'a',
+            'arb',
+            '1.0000',
+            '1.2000',
+            '0.0500',
+            '20.00 % ↑',
+        ]
+    ]
+
+
+def test_render_posterior_summary_table_places_units_after_parameter(monkeypatch):
+    from easydiffraction.analysis.fit_helpers import bayesian
+    from easydiffraction.analysis.fit_helpers.bayesian import PosteriorParameterSummary
+
+    captured: dict[str, object] = {}
+
+    def fake_render_table(*, columns_headers, columns_alignment, columns_data):
+        captured['columns_headers'] = columns_headers
+        captured['columns_alignment'] = columns_alignment
+        captured['columns_data'] = columns_data
+
+    monkeypatch.setattr(bayesian, 'render_table', fake_render_table)
+
+    bayesian._render_posterior_summary_table(
+        parameters=[Param(unique_name='a', start=1.0, value=1.2, uncertainty=0.05)],
+        posterior_parameter_summaries=[
+            PosteriorParameterSummary(
+                unique_name='a',
+                display_name='a',
+                best_sample_value=1.2,
+                median=1.15,
+                standard_deviation=0.05,
+                interval_68=(1.1, 1.2),
+                interval_95=(1.0, 1.3),
+                r_hat=1.107,
+                ess_bulk=125.9,
+            )
+        ],
+    )
+
+    assert captured['columns_headers'] == [
+        'datablock',
+        'category',
+        'entry',
+        'parameter',
+        'units',
+        'median',
+        '95% interval',
+        'r-hat',
+        'ess bulk',
+    ]
+    assert captured['columns_alignment'] == [
+        'left',
+        'left',
+        'left',
+        'left',
+        'left',
+        'right',
+        'right',
+        'right',
+        'right',
+    ]
+    assert captured['columns_data'] == [
+        [
+            'db',
+            'cat',
+            'entry',
+            'a',
+            'arb',
+            '1.1500',
+            '[1.0000, 1.3000]',
+            '[red]1.107[/red]',
+            '[red]125.9[/red]',
+        ]
+    ]
+
+
+def test_posterior_table_notes_split_failed_diagnostics():
+    from easydiffraction.analysis.fit_helpers.bayesian import PosteriorParameterSummary
+    from easydiffraction.analysis.fit_helpers.bayesian import _posterior_table_notes
+
+    notes = _posterior_table_notes([
+        PosteriorParameterSummary(
+            unique_name='a',
+            display_name='a',
+            best_sample_value=1.0,
+            median=1.0,
+            standard_deviation=0.1,
+            interval_68=(0.9, 1.1),
+            interval_95=(0.8, 1.2),
+            r_hat=1.02,
+            ess_bulk=100.0,
+        )
+    ])
+
+    assert len(notes) == 2
+    assert 'r-hat' in notes[0]
+    assert 'ess bulk' in notes[1]
diff --git a/tests/unit/easydiffraction/analysis/fit_helpers/test_reporting.py b/tests/unit/easydiffraction/analysis/fit_helpers/test_reporting.py
index ee8014f23..9758f70f8 100644
--- a/tests/unit/easydiffraction/analysis/fit_helpers/test_reporting.py
+++ b/tests/unit/easydiffraction/analysis/fit_helpers/test_reporting.py
@@ -57,5 +57,71 @@ def __init__(self, start, value, uncertainty, name='p', units='u'):
     assert 'Weighted R-factor (wR)' in out
     assert 'Bragg R-factor (BR)' in out
     assert 'Fitted parameters:' in out
-    # Table border: accept common border glyphs from Rich/tabulate
+    # Table border: accept common border glyphs from Rich
     assert any(ch in out for ch in ('╒', '┌', '+', '─'))
+
+
+def test_fitresults_display_results_places_units_after_parameter(monkeypatch):
+    class Identity:
+        def __init__(self):
+            self.datablock_entry_name = 'db'
+            self.category_code = 'cat'
+            self.category_entry_name = 'entry'
+
+    class Param:
+        def __init__(self):
+            self._identity = Identity()
+            self._fit_start_value = 1.0
+            self.value = 1.2
+            self.uncertainty = 0.05
+            self.name = 'a'
+            self.units = 'arb'
+
+    from easydiffraction.analysis.fit_helpers import reporting
+
+    captured: dict[str, object] = {}
+
+    def fake_render_table(*, columns_headers, columns_alignment, columns_data):
+        captured['columns_headers'] = columns_headers
+        captured['columns_alignment'] = columns_alignment
+        captured['columns_data'] = columns_data
+
+    monkeypatch.setattr(reporting, 'render_table', fake_render_table)
+
+    reporting.FitResults(success=True, parameters=[Param()]).display_results()
+
+    assert captured['columns_headers'] == [
+        'datablock',
+        'category',
+        'entry',
+        'parameter',
+        'units',
+        'start',
+        'fitted',
+        'uncertainty',
+        'change',
+    ]
+    assert captured['columns_alignment'] == [
+        'left',
+        'left',
+        'left',
+        'left',
+        'left',
+        'right',
+        'right',
+        'right',
+        'right',
+    ]
+    assert captured['columns_data'] == [
+        [
+            'db',
+            'cat',
+            'entry',
+            'a',
+            'arb',
+            '1.0000',
+            '1.2000',
+            '0.0500',
+            '20.00 % ↑',
+        ]
+    ]
diff --git a/tests/unit/easydiffraction/analysis/fit_helpers/test_tracking.py b/tests/unit/easydiffraction/analysis/fit_helpers/test_tracking.py
index 561eb54c3..ff9df3288 100644
--- a/tests/unit/easydiffraction/analysis/fit_helpers/test_tracking.py
+++ b/tests/unit/easydiffraction/analysis/fit_helpers/test_tracking.py
@@ -13,11 +13,11 @@ def test_module_import():
 
 
 def test_tracker_terminal_flow_prints_and_updates_best(monkeypatch, capsys):
-    import easydiffraction.analysis.fit_helpers.tracking as tracking_mod
+    import easydiffraction.display.progress as progress_mod
     from easydiffraction.analysis.fit_helpers.tracking import FitProgressTracker
 
-    # Force terminal branch (not notebook): tracking imports in_jupyter directly
-    monkeypatch.setattr(tracking_mod, 'in_jupyter', lambda: False)
+    # Force terminal branch (not notebook) in the shared progress layer.
+    monkeypatch.setattr(progress_mod, 'in_jupyter', lambda: False)
 
     tracker = FitProgressTracker()
     tracker.start_tracking('dummy')
@@ -43,3 +43,84 @@ def test_tracker_terminal_flow_prints_and_updates_best(monkeypatch, capsys):
     out2 = capsys.readouterr().out
     assert 'Best goodness-of-fit' in out2
     assert tracker.best_iteration is not None
+
+
+def test_tracker_fit_adds_timed_rows_and_resets_counter(monkeypatch):
+    import easydiffraction.analysis.fit_helpers.tracking as tracking_mod
+    from easydiffraction.analysis.fit_helpers.tracking import FitProgressTracker
+
+    chi2_values = iter([5.0, 4.0, 3.97, 3.97, 3.97, 3.97])
+    perf_counter_values = iter([0.0, 0.0, 2.0, 6.9, 7.1, 11.9, 12.2])
+
+    monkeypatch.setattr(
+        tracking_mod,
+        'calculate_reduced_chi_square',
+        lambda residuals, n_parameters: next(chi2_values),
+    )
+    monkeypatch.setattr(
+        tracking_mod.time,
+        'perf_counter',
+        lambda: next(perf_counter_values),
+    )
+
+    tracker = FitProgressTracker()
+    tracker.start_timer()
+
+    for _ in range(6):
+        tracker.track(np.array([1.0]), parameters=[1.0])
+
+    assert tracker._df_rows == [
+        ['1', '0.00', '5.00', ''],
+        ['2', '2.00', '4.00', '20.0% ↓'],
+        ['4', '7.10', '3.97', ''],
+        ['6', '12.20', '3.97', ''],
+    ]
+    assert tracker._last_progress_time == 12.2
+    assert tracker._previous_chi2 == 4.0
+
+
+def test_tracker_fit_progress_uses_backend_iterations_for_display():
+    from easydiffraction.analysis.fit_helpers.tracking import FitProgressTracker
+
+    tracker = FitProgressTracker()
+
+    tracker.track_fit_progress(iteration=1, reduced_chi2=10.0, elapsed_time=0.1)
+    tracker.track_fit_progress(iteration=63, reduced_chi2=5.0, elapsed_time=1.0)
+    tracker.track_fit_progress(iteration=122, reduced_chi2=4.0, elapsed_time=2.0)
+
+    assert tracker._df_rows == [
+        ['1', '0.10', '10.00', ''],
+        ['63', '1.00', '5.00', '50.0% ↓'],
+        ['122', '2.00', '4.00', '20.0% ↓'],
+    ]
+    assert tracker.best_iteration == 122
+
+
+def test_tracker_sampler_post_processing_adds_final_status_row():
+    from easydiffraction.analysis.fit_helpers.tracking import FitProgressTracker
+    from easydiffraction.analysis.fit_helpers.tracking import SamplerProgressUpdate
+
+    tracker = FitProgressTracker()
+    tracker.start_tracking('dream', mode='sampling')
+    tracker.start_timer()
+    tracker.track_sampler_progress(
+        SamplerProgressUpdate(
+            iteration=10,
+            total_iterations=10,
+            phase='sampling',
+            progress_percent=100.0,
+            log_posterior=-3.0,
+            reduced_chi2=1.0,
+            elapsed_time=5.0,
+            force_report=True,
+        )
+    )
+
+    tracker.start_sampler_post_processing()
+    tracker.stop_timer()
+    tracker.finish_tracking()
+
+    assert tracker._df_rows[-1][0] == ''
+    assert tracker._df_rows[-1][1] == ''
+    assert tracker._df_rows[-1][3] == ''
+    assert tracker._df_rows[-1][4] == 'post-processing'
diff --git a/tests/unit/easydiffraction/analysis/minimizers/test_base.py b/tests/unit/easydiffraction/analysis/minimizers/test_base.py
index 23d385e7c..4ee99f1d1 100644
--- a/tests/unit/easydiffraction/analysis/minimizers/test_base.py
+++ b/tests/unit/easydiffraction/analysis/minimizers/test_base.py
@@ -2,6 +2,7 @@
 # SPDX-License-Identifier: BSD-3-Clause
 
 import numpy as np
+import pytest
 
 
 class _DummyParam:
@@ -86,7 +87,8 @@ def _compute_residuals(
     assert minim.synced is True
     assert isinstance(result.parameters, list)
     assert result.parameters[0].value == 42
-    # Fitting time should be a positive float
+    assert result.fitting_time is not None
+    assert result.fitting_time >= 0.0
     assert minim.tracker.fitting_time is not None
     assert minim.tracker.fitting_time >= 0.0
 
@@ -119,3 +121,171 @@ def _compute_residuals(
     )
     out = f({})
     assert np.allclose(out, np.array([1.0, 2.0, 3.0]))
+
+
+def test_max_iterations_property_updates_internal_value():
+    from easydiffraction.analysis.minimizers.base import MinimizerBase
+
+    class M(MinimizerBase):
+        def __init__(self):
+            super().__init__(name='dummy', method='m', max_iterations=5)
+
+        def _prepare_solver_args(self, parameters):
+            del parameters
+            return {}
+
+        def _run_solver(self, objective_function, **kwargs):
+            del objective_function, kwargs
+            return
+
+        def _sync_result_to_parameters(self, parameters, raw_result):
+            del parameters, raw_result
+
+        def _check_success(self, raw_result):
+            del raw_result
+            return True
+
+    minimizer = M()
+
+    assert minimizer.max_iterations == 5
+
+    minimizer.max_iterations = 200
+
+    assert minimizer.max_iterations == 200
+
+
+def test_minimizer_base_fit_stops_tracking_when_solver_prep_fails():
+    from easydiffraction.analysis.minimizers.base import MinimizerBase
+
+    class M(MinimizerBase):
+        def __init__(self):
+            super().__init__(name='dummy', method='m', max_iterations=5)
+            self.started = False
+            self.stopped = False
+
+        def _start_tracking(self, minimizer_name, verbosity=None):
+            self.started = True
+
+        def _stop_tracking(self):
+            self.stopped = True
+
+        def _prepare_solver_args(self, parameters):
+            msg = 'prep failed'
+            raise ValueError(msg)
+
+        def _run_solver(self, objective_function, **kwargs):
+            msg = 'should not run solver'
+            raise AssertionError(msg)
+
+        def _sync_result_to_parameters(self, parameters, raw_result):
+            pass
+
+        def _check_success(self, raw_result):
+            return True
+
+    minimizer = M()
+
+    with pytest.raises(ValueError, match='prep failed'):
+        minimizer.fit(parameters=[_DummyParam(1.0)], objective_function=lambda _: np.array([0.0]))
+
+    assert minimizer.started is True
+    assert minimizer.stopped is True
+
+
+def test_minimizer_base_fit_preserves_solver_prep_error_during_cleanup(monkeypatch):
+    import easydiffraction.analysis.fit_helpers.tracking as tracking_mod
+    from easydiffraction.analysis.minimizers.base import MinimizerBase
+
+    monkeypatch.setattr(
+        tracking_mod.FitProgressTracker,
+        '_start_activity_indicator',
+        lambda self: None,
+    )
+    monkeypatch.setattr(
+        tracking_mod.FitProgressTracker,
+        '_stop_activity_indicator',
+        lambda self: None,
+    )
+    monkeypatch.setattr(tracking_mod.console, 'print', lambda *args, **kwargs: None)
+
+    class M(MinimizerBase):
+        def __init__(self):
+            super().__init__(name='dummy', method='m', max_iterations=5)
+
+        def _prepare_solver_args(self, parameters):
+            del parameters
+            msg = 'prep failed'
+            raise ValueError(msg)
+
+        def _run_solver(self, objective_function, **kwargs):
+            del objective_function, kwargs
+            msg = 'should not run solver'
+            raise AssertionError(msg)
+
+        def _sync_result_to_parameters(self, parameters, raw_result):
+            del parameters, raw_result
+
+        def _check_success(self, raw_result):
+            del raw_result
+            return True
+
+    minimizer = M()
+
+    with pytest.raises(ValueError, match='prep failed'):
+        minimizer.fit(parameters=[_DummyParam(1.0)], objective_function=lambda _: np.array([0.0]))
+
+
+def test_minimizer_base_stop_tracking_backfills_result_fitting_time():
+    from easydiffraction.analysis.minimizers.base import MinimizerBase
+
+    class DummyResult:
+        success = True
+
+    class DummyMinimizer(MinimizerBase):
+        def __init__(self):
+            super().__init__(name='dummy', method='m', max_iterations=5)
+
+        def _prepare_solver_args(self, parameters):
+            del parameters
+            return {'engine_parameters': {'ok': True}}
+
+        def _run_solver(self, objective_function, **kwargs):
+            residuals = objective_function(kwargs.get('engine_parameters'))
+            self.tracker.track(residuals=np.array(residuals), parameters=[1])
+            return DummyResult()
+
+        def _sync_result_to_parameters(self, parameters, raw_result):
+            del parameters, raw_result
+
+        def _check_success(self, raw_result):
+            del raw_result
+            return True
+
+        def _compute_residuals(
+            self, engine_params, parameters, structures, experiments, calculator
+        ):
+            del parameters, structures, experiments, calculator
+            assert engine_params == {'ok': True}
+            return np.array([0.0])
+
+    minimizer = DummyMinimizer()
+    params = [_DummyParam(1.0)]
+    objective = minimizer._create_objective_function(
+        parameters=params,
+        structures=None,
+        experiments=None,
+        calculator=None,
+    )
+
+    result = minimizer.fit(
+        parameters=params,
+        objective_function=objective,
+        finalize_tracking=False,
+    )
+
+    assert result.fitting_time is None
+
+    minimizer._stop_tracking()
+
+    assert result.fitting_time is not None
+    assert result.fitting_time >= 0.0
diff --git a/tests/unit/easydiffraction/analysis/minimizers/test_bumps.py b/tests/unit/easydiffraction/analysis/minimizers/test_bumps.py
index 668524eaf..830a4f64f 100644
--- a/tests/unit/easydiffraction/analysis/minimizers/test_bumps.py
+++ b/tests/unit/easydiffraction/analysis/minimizers/test_bumps.py
@@ -38,6 +38,18 @@ def test_default_max_iterations():
     assert m.max_iterations == 1000
 
 
+def test_bumps_minimizer_rejects_random_seed():
+    from easydiffraction.analysis.minimizers.bumps import BumpsMinimizer
+
+    m = BumpsMinimizer()
+
+    with pytest.raises(
+        ValueError,
+        match=r"Minimizer 'bumps' does not support random_seed\.",
+    ):
+        m._resolve_random_seed(17)
+
+
 def test_is_subclass_of_base():
     from easydiffraction.analysis.minimizers.base import MinimizerBase
     from easydiffraction.analysis.minimizers.bumps import BumpsMinimizer
@@ -177,6 +189,80 @@ def test_fitness_numpoints_after_nllf():
     assert fitness.numpoints() == 3
 
 
+def test_fitness_evaluation_count_can_be_reset_and_stopped():
+    from bumps.parameter import Parameter as BumpsParameter
+
+    from easydiffraction.analysis.minimizers.bumps import _EasyDiffractionFitness
+
+    bp = BumpsParameter(value=0.0, name='a')
+    fitness = _EasyDiffractionFitness([bp], lambda values: np.array([values[0]]))
+
+    fitness.residuals()
+    fitness.residuals()
+    assert fitness.evaluation_count == 2
+
+    fitness.reset_evaluation_count()
+    assert fitness.evaluation_count == 0
+
+    fitness.stop_counting_evaluations()
+    fitness.residuals()
+    assert fitness.evaluation_count == 0
+
+
+def test_fitness_raises_when_max_evaluations_is_reached():
+    from bumps.parameter import Parameter as BumpsParameter
+
+    from easydiffraction.analysis.minimizers.bumps import _BumpsEvaluationLimitError
+    from easydiffraction.analysis.minimizers.bumps import _EasyDiffractionFitness
+
+    bp = BumpsParameter(value=2.0, name='a')
+    fitness = _EasyDiffractionFitness(
+        [bp],
+        lambda values: np.array([values[0]]),
+        max_evaluations=2,
+    )
+
+    fitness.residuals()
+    fitness.residuals()
+
+    with pytest.raises(_BumpsEvaluationLimitError) as exc_info:
+        fitness.residuals()
+
+    assert exc_info.value.evaluation_count == 2
+    np.testing.assert_array_equal(exc_info.value.parameter_values, np.array([2.0]))
+    np.testing.assert_array_equal(exc_info.value.residuals, np.array([2.0]))
+
+
+def test_bumps_progress_monitor_reports_evaluation_count():
+    from easydiffraction.analysis.minimizers.bumps import _EasyDiffractionFitness
+    from easydiffraction.analysis.minimizers.bumps import _BumpsProgressMonitor
+
+    tracker = MagicMock()
+    fitness = _EasyDiffractionFitness([], lambda values: np.array([]))
+    fitness.reset_evaluation_count()
+    fitness._evaluation_count = 63
+    monitor = _BumpsProgressMonitor(
+        tracker=tracker,
+        fitness=fitness,
+        n_points=20,
+        n_parameters=4,
+    )
+
+    monitor(
+        types.SimpleNamespace(
+            step=[7],
+            value=[8.0],
+            time=[1.5],
+        )
+    )
+
+    tracker.track_fit_progress.assert_called_once_with(
+        iteration=63,
+        reduced_chi2=pytest.approx(1.0),
+        elapsed_time=1.5,
+    )
+
+
 def test_fitness_update_is_noop():
     from easydiffraction.analysis.minimizers.bumps import _EasyDiffractionFitness
 
@@ -220,6 +306,8 @@ def test_run_solver_returns_optimize_result():
             bumps_params=[bp1, bp2],
         )
 
+    assert len(mock_driver_cls.call_args.kwargs['monitors']) == 1
+
     assert isinstance(res, OptimizeResult)
     assert res.success is True
     np.testing.assert_array_almost_equal(res.x, [1.5, 2.5])
@@ -253,6 +341,51 @@ def test_run_solver_failure():
     assert res.status == -1
 
 
+def test_run_solver_stops_at_max_evaluations():
+    from easydiffraction.analysis.minimizers.bumps import BumpsMinimizer
+    from easydiffraction.analysis.minimizers.bumps import _BumpsEvaluationLimitError
+
+    m = BumpsMinimizer(max_iterations=50)
+    m.tracker = MagicMock()
+    m.tracker._current_elapsed_time.return_value = 1.25
+
+    fake_fitter = types.SimpleNamespace(id='lm')
+    limit_error = _BumpsEvaluationLimitError(
+        evaluation_count=50,
+        parameter_values=np.array([1.5]),
+        residuals=np.array([2.0, 2.0]),
+    )
+
+    with (
+        patch('easydiffraction.analysis.minimizers.bumps.FitDriver') as mock_driver_cls,
+        patch('easydiffraction.analysis.minimizers.bumps.FitProblem'),
+        patch('easydiffraction.analysis.minimizers.bumps.FITTERS', [fake_fitter]),
+        patch.object(m, '_compute_covariance', return_value=(None, None)) as mock_covariance,
+    ):
+        driver_instance = mock_driver_cls.return_value
+        driver_instance.fit.side_effect = limit_error
+        driver_instance.clip = MagicMock()
+
+        from bumps.parameter import Parameter as BumpsParameter
+
+        bp = BumpsParameter(value=1.5, name='a')
+        result = m._run_solver(
+            lambda values: np.array([values[0], values[0]]),
+            bumps_params=[bp],
+        )
+
+    assert result.success is False
+    assert result.status == 5
+    assert result.message == 'maximum number of residual evaluations reached'
+    np.testing.assert_array_equal(result.x, np.array([1.5]))
+    mock_covariance.assert_not_called()
+    m.tracker.track_fit_progress.assert_called_once_with(
+        iteration=50,
+        reduced_chi2=pytest.approx(8.0),
+        elapsed_time=1.25,
+    )
+
+
 # -- _sync_result_to_parameters tests -----------------------------------------
 
 
diff --git a/tests/unit/easydiffraction/analysis/minimizers/test_bumps_dream.py b/tests/unit/easydiffraction/analysis/minimizers/test_bumps_dream.py
new file mode 100644
index 000000000..3a23f5577
--- /dev/null
+++ b/tests/unit/easydiffraction/analysis/minimizers/test_bumps_dream.py
@@ -0,0 +1,384 @@
+# SPDX-FileCopyrightText: 2026 EasyScience contributors 
+# SPDX-License-Identifier: BSD-3-Clause
+
+from __future__ import annotations
+
+from types import SimpleNamespace
+from unittest.mock import MagicMock
+from unittest.mock import patch
+
+import numpy as np
+import pytest
+
+
+class FakeParam:
+    """Minimal stand-in for an EasyDiffraction parameter."""
+
+    def __init__(
+        self,
+        uid: str,
+        value: float,
+        uncertainty: float | None = None,
+        *,
+        fit_min: float | None = 0.0,
+        fit_max: float | None = 1.0,
+    ) -> None:
+        self._minimizer_uid = uid
+        self.unique_name = uid
+        self.name = uid.upper()
+        self.value = value
+        self.uncertainty = uncertainty
+        self.fit_min = fit_min
+        self.fit_max = fit_max
+
+    def _set_value_from_minimizer(self, value: float) -> None:
+        self.value = value
+
+
+def test_module_import():
+    import easydiffraction.analysis.minimizers.bumps_dream as MUT
+
+    assert MUT.__name__ == 'easydiffraction.analysis.minimizers.bumps_dream'
+
+
+def test_type_info_and_default_init():
+    from easydiffraction.analysis.minimizers.bumps_dream import BumpsDreamMinimizer
+    from easydiffraction.analysis.minimizers.enums import DreamPopulationInitializationEnum
+    from easydiffraction.analysis.minimizers.enums import MinimizerTypeEnum
+
+    minimizer = BumpsDreamMinimizer()
+
+    assert minimizer.type_info.tag == MinimizerTypeEnum.BUMPS_DREAM
+    assert minimizer.init is DreamPopulationInitializationEnum.LHS
+    assert minimizer.steps == 3000
+
+
+def test_dream_uses_steps_instead_of_max_iterations():
+    from easydiffraction.analysis.minimizers.bumps_dream import BumpsDreamMinimizer
+
+    minimizer = BumpsDreamMinimizer()
+
+    with pytest.raises(
+        AttributeError,
+        match=r"DREAM sampler uses 'steps' instead of 'max_iterations'\.",
+    ):
+        _ = minimizer.max_iterations
+
+    with pytest.raises(
+        AttributeError,
+        match=r"DREAM sampler uses 'steps' instead of 'max_iterations'\.",
+    ):
+        minimizer.max_iterations = 300
+
+    minimizer.steps = 300
+
+    assert minimizer.steps == 300
+
+
+def test_dream_progress_monitor_allocates_rows_by_phase_ratio():
+    from easydiffraction.analysis.minimizers.bumps_dream import _DreamProgressMonitor
+
+    monitor = _DreamProgressMonitor(
+        tracker=MagicMock(),
+        n_points=100,
+        n_parameters=3,
+        total_generations=100,
+        burn_steps=40,
+    )
+
+    assert len(monitor._burn_targets) == 10
+    assert len(monitor._sampling_targets) == 15
+
+
+def test_init_accepts_enum_or_string_and_rejects_invalid():
+    from easydiffraction.analysis.minimizers.bumps_dream import BumpsDreamMinimizer
+    from easydiffraction.analysis.minimizers.enums import DreamPopulationInitializationEnum
+
+    minimizer = BumpsDreamMinimizer()
+
+    minimizer.init = DreamPopulationInitializationEnum.LHS
+    assert minimizer.init is DreamPopulationInitializationEnum.LHS
+
+    minimizer.init = 'random'
+    assert minimizer.init is DreamPopulationInitializationEnum.RANDOM
+
+    with pytest.raises(
+        ValueError,
+        match=r"DREAM setting 'init' must be one of: eps, cov, lhs, random\.",
+    ):
+        minimizer.init = 'bad-init'
+
+
+def test_resolve_random_seed_returns_provided_or_generated(monkeypatch):
+    from easydiffraction.analysis.minimizers.bumps_dream import BumpsDreamMinimizer
+
+    minimizer = BumpsDreamMinimizer()
+
+    assert minimizer._resolve_random_seed(17) == 17
+    assert minimizer._resolved_random_seed == 17
+
+    generator = SimpleNamespace(integers=lambda *args, **kwargs: 123456)
+    monkeypatch.setattr(np.random, 'default_rng', lambda: generator)
+
+    assert minimizer._resolve_random_seed(None) == 123456
+    assert minimizer._resolved_random_seed == 123456
+
+
+@pytest.mark.parametrize('seed', [-1, np.iinfo(np.uint32).max + 1])
+def test_resolve_random_seed_rejects_out_of_range_values(seed):
+    from easydiffraction.analysis.minimizers.bumps_dream import BumpsDreamMinimizer
+
+    minimizer = BumpsDreamMinimizer()
+
+    with pytest.raises(
+        ValueError,
+        match=r'DREAM random_seed must be an integer between 0 and 4294967295\.',
+    ):
+        minimizer._resolve_random_seed(seed)
+
+
+def test_resolved_burn_uses_auto_or_explicit_and_validates():
+    from easydiffraction.analysis.minimizers.bumps_dream import BumpsDreamMinimizer
+
+    minimizer = BumpsDreamMinimizer()
+
+    assert minimizer._resolved_burn(steps=200) == 50
+
+    minimizer.burn = 20
+    assert minimizer._resolved_burn(steps=200) == 20
+
+    minimizer.burn = 200
+    with pytest.raises(
+        ValueError,
+        match=r"DREAM setting 'burn' must be smaller than 'steps'\.",
+    ):
+        minimizer._resolved_burn(steps=200)
+
+
+def test_sampler_settings_include_init_and_sample_count():
+    from easydiffraction.analysis.minimizers.bumps_dream import BumpsDreamMinimizer
+    from easydiffraction.analysis.minimizers.enums import DreamPopulationInitializationEnum
+
+    minimizer = BumpsDreamMinimizer()
+    minimizer.thin = 1
+    minimizer.pop = 4
+    minimizer.init = DreamPopulationInitializationEnum.LHS
+
+    settings = minimizer._sampler_settings(
+        random_seed=7,
+        steps=10,
+        burn=2,
+        n_parameters=3,
+    )
+
+    assert settings['random_seed'] == 7
+    assert settings['parallel'] == 0
+    assert settings['init'] == 'lhs'
+    assert settings['samples'] == 120
+
+
+@pytest.mark.parametrize(
+    ('fit_min', 'fit_max', 'value', 'message'),
+    [
+        (None, 1.0, 0.5, r'fit_min must be finite'),
+        (0.0, np.inf, 0.5, r'fit_max must be finite'),
+        (2.0, 1.0, 1.5, r'fit_min \(2\.0\) must be smaller than fit_max \(1\.0\)'),
+        (0.0, 1.0, 2.0, r'starting value 2\.0 is outside \[0\.0, 1\.0\]'),
+    ],
+)
+def test_prepare_solver_args_rejects_invalid_dream_bounds(
+    fit_min,
+    fit_max,
+    value,
+    message,
+):
+    from easydiffraction.analysis.minimizers.bumps_dream import BumpsDreamMinimizer
+
+    minimizer = BumpsDreamMinimizer()
+    parameter = FakeParam('alpha', value, fit_min=fit_min, fit_max=fit_max)
+
+    with pytest.raises(ValueError, match=message):
+        minimizer._prepare_solver_args([parameter])
+
+
+def test_prepare_solver_args_lists_all_offending_dream_parameters():
+    from easydiffraction.analysis.minimizers.bumps_dream import BumpsDreamMinimizer
+
+    minimizer = BumpsDreamMinimizer()
+    parameters = [
+        FakeParam('alpha', 2.0, fit_min=0.0, fit_max=1.0),
+        FakeParam('beta', 0.5, fit_min=None, fit_max=1.0),
+    ]
+
+    with pytest.raises(
+        ValueError,
+        match=r'alpha: .*outside \[0\.0, 1\.0\][\s\S]*beta: .*fit_min',
+    ):
+        minimizer._prepare_solver_args(parameters)
+
+
+def test_sync_result_to_parameters_restores_starting_values_on_failure():
+    from easydiffraction.analysis.minimizers.bumps_dream import BumpsDreamMinimizer
+
+    minimizer = BumpsDreamMinimizer()
+    parameters = [FakeParam('a', 10.0, uncertainty=0.7), FakeParam('b', 20.0, uncertainty=0.8)]
+    raw_result = SimpleNamespace(
+        x=np.array([99.0, 88.0]),
+        success=False,
+        starting_values=np.array([1.5, 2.5]),
+        starting_uncertainties=[0.1, None],
+    )
+
+    minimizer._sync_result_to_parameters(parameters, raw_result)
+
+    assert parameters[0].value == 1.5
+    assert parameters[0].uncertainty == 0.1
+    assert parameters[1].value == 2.5
+    assert parameters[1].uncertainty is None
+
+
+def test_run_solver_preserves_parameter_order_and_forwards_init():
+    from easydiffraction.analysis.fit_helpers.bayesian import PosteriorParameterSummary
+    from easydiffraction.analysis.minimizers.bumps_dream import BumpsDreamMinimizer
+
+    minimizer = BumpsDreamMinimizer()
+    minimizer.steps = 4
+    minimizer.burn = 1
+    minimizer.thin = 1
+    minimizer.pop = 2
+    minimizer.init = 'lhs'
+
+    draw_index = np.array([0.0, 1.0])
+    parameter_samples = np.array(
+        [
+            [[1.0, 10.0], [2.0, 20.0]],
+            [[3.0, 30.0], [4.0, 40.0]],
+        ],
+        dtype=float,
+    )
+    log_posterior = np.array([[0.1, 0.2], [0.3, 0.4]], dtype=float)
+
+    class FakeState:
+        labels = ['uid_a', 'uid_b']
+
+        def chains(self):
+            return draw_index, parameter_samples, log_posterior
+
+        def best(self):
+            return np.array([11.0, 22.0]), 3.5
+
+    fake_fitter = SimpleNamespace(id='dream')
+
+    with (
+        patch('easydiffraction.analysis.minimizers.bumps_dream.FitDriver') as mock_driver_cls,
+        patch('easydiffraction.analysis.minimizers.bumps_dream.FitProblem'),
+        patch('easydiffraction.analysis.minimizers.bumps_dream.FITTERS', [fake_fitter]),
+        patch(
+            'easydiffraction.analysis.minimizers.bumps_dream.compute_convergence_diagnostics',
+            return_value={'converged': True},
+        ),
+        patch(
+            'easydiffraction.analysis.minimizers.bumps_dream.summarize_posterior_parameters',
+            return_value=[
+                PosteriorParameterSummary(
+                    unique_name='beta',
+                    display_name='Beta',
+                    best_sample_value=22.0,
+                    median=21.0,
+                    standard_deviation=0.4,
+                    interval_68=(20.5, 21.5),
+                    interval_95=(20.0, 22.0),
+                ),
+                PosteriorParameterSummary(
+                    unique_name='alpha',
+                    display_name='Alpha',
+                    best_sample_value=11.0,
+                    median=10.5,
+                    standard_deviation=0.3,
+                    interval_68=(10.0, 11.0),
+                    interval_95=(9.5, 11.5),
+                ),
+            ],
+        ) as summarize_mock,
+        patch(
+            'easydiffraction.analysis.minimizers.bumps_dream.standard_deviations_from_summaries',
+            return_value=np.array([0.4, 0.3]),
+        ),
+    ):
+        driver_instance = mock_driver_cls.return_value
+        driver_instance.clip = MagicMock()
+        driver_instance.fit.return_value = (np.array([22.0, 11.0]), 0.25)
+        driver_instance.fitter = SimpleNamespace(state=FakeState())
+
+        result = minimizer._run_solver(
+            lambda values: np.array([0.0, 0.0]),
+            bumps_params=[
+                SimpleNamespace(name='uid_a', value=1.0),
+                SimpleNamespace(name='uid_b', value=2.0),
+            ],
+            parameter_names=['beta', 'alpha'],
+            parameter_display_names=['Beta', 'Alpha'],
+            parameter_uids=['uid_b', 'uid_a'],
+            random_seed=17,
+            starting_uncertainties=[0.01, 0.02],
+        )
+
+    assert mock_driver_cls.call_args.kwargs['init'] == 'lhs'
+    np.testing.assert_allclose(result.x, np.array([22.0, 11.0]))
+    np.testing.assert_allclose(result.dx, np.array([0.4, 0.3]))
+    assert result.posterior_samples.parameter_names == ['beta', 'alpha']
+    np.testing.assert_allclose(
+        result.posterior_samples.parameter_samples[:, :, 0], parameter_samples[:, :, 1]
+    )
+    np.testing.assert_allclose(
+        result.posterior_samples.parameter_samples[:, :, 1], parameter_samples[:, :, 0]
+    )
+    assert result.sampler_settings['init'] == 'lhs'
+    assert result.sampler_settings['random_seed'] == 17
+    assert summarize_mock.call_args.kwargs['parameter_names'] == ['beta', 'alpha']
+
+
+def test_build_driver_stops_mapper_when_driver_clip_fails():
+    from easydiffraction.analysis.minimizers.bumps_dream import BumpsDreamMinimizer
+
+    minimizer = BumpsDreamMinimizer()
+
+    with (
+        patch.object(minimizer, '_build_mapper', return_value='mapper'),
+        patch(
+            'easydiffraction.analysis.minimizers.bumps_dream.FitProblem', return_value='problem'
+        ),
+        patch('easydiffraction.analysis.minimizers.bumps_dream.FitDriver') as mock_driver_cls,
+        patch(
+            'easydiffraction.analysis.minimizers.bumps_dream.MPMapper.stop_mapper'
+        ) as stop_mapper,
+    ):
+        mock_driver_cls.return_value.clip.side_effect = RuntimeError('clip failed')
+
+        with pytest.raises(RuntimeError, match='clip failed'):
+            minimizer._build_driver(
+                fitclass=object(),
+                fitness=SimpleNamespace(numpoints=lambda: 10),
+                steps=10,
+                burn=2,
+                init=minimizer.init,
+                sampler_settings={'samples': 40},
+                n_parameters=1,
+            )
+
+    stop_mapper.assert_called_once()
+
+
+def test_execute_driver_stops_mapper_when_seed_is_invalid():
+    from easydiffraction.analysis.minimizers.bumps_dream import BumpsDreamMinimizer
+
+    driver = SimpleNamespace(fit=MagicMock(), fitter=SimpleNamespace(state=None))
+
+    with patch(
+        'easydiffraction.analysis.minimizers.bumps_dream.MPMapper.stop_mapper'
+    ) as stop_mapper:
+        result = BumpsDreamMinimizer._execute_driver(driver=driver, random_seed=-1)
+
+    assert isinstance(result.error, ValueError)
+    driver.fit.assert_not_called()
+    stop_mapper.assert_called_once()
diff --git a/tests/unit/easydiffraction/analysis/minimizers/test_dfols.py b/tests/unit/easydiffraction/analysis/minimizers/test_dfols.py
index 7d94bd04e..68437dd2a 100644
--- a/tests/unit/easydiffraction/analysis/minimizers/test_dfols.py
+++ b/tests/unit/easydiffraction/analysis/minimizers/test_dfols.py
@@ -43,10 +43,12 @@ def __init__(self):
 
     def fake_solve(fun, x0, bounds, maxfun):
         # Verify we pass reasonable arguments
+        del fun
         assert isinstance(x0, np.ndarray)
         assert x0.shape[0] == 2
         assert isinstance(bounds, tuple)
         assert all(isinstance(b, np.ndarray) for b in bounds)
+        assert maxfun == 10
         return FakeRes()
 
     monkeypatch.setattr(mod, 'solve', fake_solve)
@@ -63,3 +65,5 @@ def fake_solve(fun, x0, bounds, maxfun):
     assert params[0].uncertainty is None
     assert params[1].uncertainty is None
     assert minim._check_success(res) is True
+    assert minim.max_iterations == 10
+    assert not hasattr(minim, 'steps')
diff --git a/tests/unit/easydiffraction/analysis/minimizers/test_enums.py b/tests/unit/easydiffraction/analysis/minimizers/test_enums.py
index 23e44e5bd..5551b9b7a 100644
--- a/tests/unit/easydiffraction/analysis/minimizers/test_enums.py
+++ b/tests/unit/easydiffraction/analysis/minimizers/test_enums.py
@@ -9,12 +9,19 @@ def test_module_import():
 
 
 def test_enum_members():
+    from easydiffraction.analysis.minimizers.enums import DreamPopulationInitializationEnum
     from easydiffraction.analysis.minimizers.enums import MinimizerTypeEnum
 
     assert MinimizerTypeEnum.LMFIT == 'lmfit'
     assert MinimizerTypeEnum.LMFIT_LEASTSQ == 'lmfit (leastsq)'
     assert MinimizerTypeEnum.LMFIT_LEAST_SQUARES == 'lmfit (least_squares)'
     assert MinimizerTypeEnum.DFOLS == 'dfols'
+    assert MinimizerTypeEnum.BUMPS_DREAM == 'bumps (dream)'
+
+    assert DreamPopulationInitializationEnum.EPS == 'eps'
+    assert DreamPopulationInitializationEnum.COV == 'cov'
+    assert DreamPopulationInitializationEnum.LHS == 'lhs'
+    assert DreamPopulationInitializationEnum.RANDOM == 'random'
 
 
 def test_enum_default():
diff --git a/tests/unit/easydiffraction/analysis/minimizers/test_factory.py b/tests/unit/easydiffraction/analysis/minimizers/test_factory.py
index 7351d84b2..406a5e093 100644
--- a/tests/unit/easydiffraction/analysis/minimizers/test_factory.py
+++ b/tests/unit/easydiffraction/analysis/minimizers/test_factory.py
@@ -9,11 +9,12 @@ def test_minimizer_factory_list_and_show(capsys):
 
     lst = MinimizerFactory.supported_tags()
     assert isinstance(lst, list)
-    assert len(lst) >= 4
+    assert len(lst) >= 5
     assert 'lmfit' in lst
     assert 'lmfit (leastsq)' in lst
     assert 'lmfit (least_squares)' in lst
     assert 'dfols' in lst
+    assert 'bumps (dream)' in lst
     MinimizerFactory.show_supported()
     out = capsys.readouterr().out
     assert 'Supported types' in out
@@ -75,3 +76,12 @@ def test_minimizer_factory_create_lmfit_least_squares():
     m = MinimizerFactory.create('lmfit (least_squares)')
     assert isinstance(m, LmfitLeastSquaresMinimizer)
     assert m.method == 'least_squares'
+
+
+def test_minimizer_factory_create_bumps_dream():
+    from easydiffraction.analysis.minimizers.bumps_dream import BumpsDreamMinimizer
+    from easydiffraction.analysis.minimizers.factory import MinimizerFactory
+
+    m = MinimizerFactory.create('bumps (dream)')
+    assert isinstance(m, BumpsDreamMinimizer)
+    assert m.method == 'dream'
diff --git a/tests/unit/easydiffraction/analysis/minimizers/test_lmfit.py b/tests/unit/easydiffraction/analysis/minimizers/test_lmfit.py
index f41e98965..7091fdcd4 100644
--- a/tests/unit/easydiffraction/analysis/minimizers/test_lmfit.py
+++ b/tests/unit/easydiffraction/analysis/minimizers/test_lmfit.py
@@ -77,3 +77,38 @@ def __init__(self):
     assert params[1].value == 20.0
     assert params[1].uncertainty == 1.0
     assert minim._check_success(res) is True
+
+
+def test_lmfit_max_iterations_is_user_facing_iteration_setting(monkeypatch):
+    from easydiffraction.analysis.minimizers.lmfit import LmfitMinimizer
+
+    import easydiffraction.analysis.minimizers.lmfit as lm
+
+    observed_max_nfev = {}
+
+    def fake_minimize(
+        objective_function,
+        *,
+        params,
+        method,
+        nan_policy,
+        max_nfev,
+    ):
+        del objective_function, params, method, nan_policy
+        observed_max_nfev['value'] = max_nfev
+        return types.SimpleNamespace(success=True, params={})
+
+    monkeypatch.setattr(
+        lm,
+        'lmfit',
+        types.SimpleNamespace(Parameters=lm.lmfit.Parameters, minimize=fake_minimize),
+    )
+
+    minimizer = LmfitMinimizer()
+
+    minimizer.max_iterations = 300
+    minimizer._run_solver(lambda *args, **kwargs: np.array([0.0]), engine_parameters=object())
+
+    assert minimizer.max_iterations == 300
+    assert observed_max_nfev['value'] == 300
+    assert not hasattr(minimizer, 'steps')
diff --git a/tests/unit/easydiffraction/analysis/test_analysis.py b/tests/unit/easydiffraction/analysis/test_analysis.py
index a4d2c7faa..2af863797 100644
--- a/tests/unit/easydiffraction/analysis/test_analysis.py
+++ b/tests/unit/easydiffraction/analysis/test_analysis.py
@@ -1,6 +1,8 @@
 # SPDX-FileCopyrightText: 2025 EasyScience contributors 
 # SPDX-License-Identifier: BSD-3-Clause
 
+from types import SimpleNamespace
+
 
 def test_module_import():
     import easydiffraction.analysis.analysis as MUT
@@ -31,26 +33,26 @@ def test_show_minimizer_types_prints(capsys):
     from easydiffraction.analysis.analysis import Analysis
 
     a = Analysis(project=_make_project_with_names([]))
-    a.fit.show_minimizer_types()
+    a.fitting.show_minimizer_types()
     out = capsys.readouterr().out
     assert 'Minimizer types' in out
     assert 'lmfit (leastsq)' in out
 
 
-def test_fit_mode_category_and_joint_fit_experiments(monkeypatch, capsys):
+def test_fit_mode_category_and_joint_fit(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.value == 'single'
+    assert a.fitting_mode_type == 'single'
 
     # Switch to joint
-    a.fit.mode = 'joint'
-    assert a.fit.mode.value == 'joint'
+    a.fitting_mode_type = 'joint'
+    assert a.fitting_mode_type == 'joint'
 
-    # joint_fit_experiments exists but is empty until fit() populates it
-    assert len(a.joint_fit_experiments) == 0
+    # joint_fit exists but is empty until fit() populates it
+    assert len(a.joint_fit) == 0
 
 
 def test_analysis_help(capsys):
@@ -64,7 +66,20 @@ def test_analysis_help(capsys):
     assert 'display' in out
     assert 'Properties' in out
     assert 'Methods' in out
-    assert 'fit_sequential()' in out
+    assert 'fit()' in out
+    assert 'show_fitting_mode_types()' in out
+
+
+def test_analysis_display_help(capsys):
+    from easydiffraction.analysis.analysis import Analysis
+
+    a = Analysis(project=_make_project_with_names([]))
+    a.display.help()
+    out = capsys.readouterr().out
+    assert "Help for 'AnalysisDisplay'" in out
+    assert 'all_params()' in out
+    assert 'fit_results()' in out
+    assert 'how_to_access_parameters()' in out
 
 
 def test_display_fit_results_warns_when_no_results(capsys):
@@ -120,3 +135,167 @@ def values(self):
     a.display.fit_results()
 
     assert process_called['called'], '_process_fit_results should be called'
+
+
+def test_fit_single_short_reuses_tracker_display_handle(monkeypatch):
+    from easydiffraction.analysis.analysis import Analysis
+    from easydiffraction.utils.enums import VerbosityEnum
+
+    class Handle:
+        def __init__(self) -> None:
+            self.closed = False
+
+        def close(self) -> None:
+            self.closed = True
+
+    class Experiments:
+        def __init__(self) -> None:
+            self.names = ['e1']
+
+        def __getitem__(self, name: str) -> object:
+            del name
+            return object()
+
+    class Tracker:
+        def __init__(self) -> None:
+            self.best_iteration = 7
+            self.display_handles: list[object | None] = []
+
+        def _set_shared_display_handle(self, display_handle: object | None) -> None:
+            self.display_handles.append(display_handle)
+
+    project = SimpleNamespace(
+        experiments=Experiments(),
+        structures=object(),
+        _varname='proj',
+    )
+    analysis = Analysis(project=project)
+    tracker = Tracker()
+    analysis.fitter.minimizer = SimpleNamespace(tracker=tracker)
+    analysis.fitter.results = None
+
+    handle = Handle()
+    short_display_handles: list[object | None] = []
+
+    def fake_make_display_handle() -> Handle:
+        return handle
+
+    def fake_fit(
+        structures: object,
+        experiments: list[object],
+        *,
+        analysis: object,
+        verbosity: object,
+        use_physical_limits: bool,
+        random_seed: int | None,
+    ) -> None:
+        del structures, experiments, analysis, verbosity, use_physical_limits, random_seed
+        analysis_obj = fake_fit.analysis_obj
+        analysis_obj.fitter.results = SimpleNamespace(
+            reduced_chi_square=1.23,
+            success=True,
+            parameters=[],
+        )
+
+    fake_fit.analysis_obj = analysis
+
+    def fake_update_short_table(
+        self,
+        short_rows: list[list[str]],
+        expt_name: str,
+        results: object,
+        display_handle: object | None,
+    ) -> None:
+        del self, expt_name, results
+        short_rows.append(['e1', '1.23', '7', '✅'])
+        short_display_handles.append(display_handle)
+
+    monkeypatch.setattr(
+        'easydiffraction.analysis.analysis.make_display_handle', fake_make_display_handle
+    )
+    monkeypatch.setattr(analysis, '_snapshot_params', lambda expt_name, results: None)
+    monkeypatch.setattr(analysis.fitter, 'fit', fake_fit)
+    monkeypatch.setattr(Analysis, '_fit_single_update_short_table', fake_update_short_table)
+
+    analysis._fit_single(
+        VerbosityEnum.SHORT,
+        project.structures,
+        project.experiments,
+        use_physical_limits=False,
+        random_seed=None,
+    )
+
+    assert tracker.display_handles == [handle, None]
+    assert short_display_handles == [handle]
+    assert handle.closed is True
+
+
+def test_run_sequential_sets_mode_and_saves_project(monkeypatch, tmp_path):
+    from easydiffraction.analysis.analysis import Analysis
+
+    project = SimpleNamespace(
+        info=SimpleNamespace(path=tmp_path),
+        save_calls=0,
+        _varname='proj',
+    )
+
+    def save() -> None:
+        project.save_calls += 1
+
+    project.save = save
+
+    analysis = Analysis(project=project)
+    analysis.sequential_fit.data_dir.value = 'scans'
+    analysis.sequential_fit.file_pattern.value = '*.xye'
+    analysis.sequential_fit.max_workers.value = 'auto'
+    analysis.sequential_fit.chunk_size.value = '.'
+    analysis.sequential_fit.reverse.value = True
+
+    calls: list[tuple[str, object]] = []
+
+    def fake_fit_sequential(
+        *,
+        analysis: object,
+        data_dir: str,
+        max_workers: int | str,
+        chunk_size: int | None,
+        file_pattern: str,
+        reverse: bool,
+    ) -> None:
+        calls.append(('analysis', analysis))
+        calls.append(('data_dir', data_dir))
+        calls.append(('max_workers', max_workers))
+        calls.append(('chunk_size', chunk_size))
+        calls.append(('file_pattern', file_pattern))
+        calls.append(('reverse', reverse))
+
+    monkeypatch.setattr('easydiffraction.analysis.sequential.fit_sequential', fake_fit_sequential)
+    monkeypatch.setattr(
+        analysis, '_update_categories', lambda: calls.append(('update_categories', None))
+    )
+    monkeypatch.setattr(
+        analysis, '_resolve_sequential_data_dir', lambda: tmp_path / 'resolved-scans'
+    )
+    analysis.fit_results = object()
+    analysis.fitter.results = object()
+
+    analysis._run_sequential()
+
+    assert analysis.fitting_mode_type == 'sequential'
+    analysis_cif = analysis.as_cif
+    assert '_fitting.mode_type sequential' in analysis_cif
+    assert '_sequential_fit.data_dir scans' in analysis_cif
+    assert '_sequential_fit.file_pattern *.xye' in analysis_cif
+    assert calls == [
+        ('update_categories', None),
+        ('analysis', analysis),
+        ('data_dir', str(tmp_path / 'resolved-scans')),
+        ('max_workers', 'auto'),
+        ('chunk_size', None),
+        ('file_pattern', '*.xye'),
+        ('reverse', True),
+        ('update_categories', None),
+    ]
+    assert project.save_calls == 1
+    assert analysis.fit_results is None
+    assert analysis.fitter.results is None
diff --git a/tests/unit/easydiffraction/analysis/test_analysis_access_params.py b/tests/unit/easydiffraction/analysis/test_analysis_access_params.py
index b7d9f8956..96667689b 100644
--- a/tests/unit/easydiffraction/analysis/test_analysis_access_params.py
+++ b/tests/unit/easydiffraction/analysis/test_analysis_access_params.py
@@ -2,32 +2,45 @@
 # 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
+def _make_param(
+    db,
+    cat,
+    entry,
+    name,
+    val,
+    *,
+    user_constrained=False,
+    symmetry_constrained=False,
+):
     from easydiffraction.core.validation import AttributeSpec
     from easydiffraction.core.variable import Parameter
     from easydiffraction.io.cif.handler import CifHandler
 
-    # Build two parameters with identity metadata set directly
-    def make_param(db, cat, entry, name, val):
-        p = Parameter(
-            name=name,
-            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
-        if entry:
-            p._identity.category_entry_name = lambda: entry
-        else:
-            p._identity.category_entry_name = lambda: ''
-        return p
-
-    p1 = make_param('db1', 'catA', '', 'alpha', 1.0)
-    p2 = make_param('db2', 'catB', 'row1', 'beta', 2.0)
+    param = Parameter(
+        name=name,
+        value_spec=AttributeSpec(default=0.0),
+        cif_handler=CifHandler(names=[f'_{cat}.{name}']),
+    )
+    param.value = val
+    param._identity.datablock_entry_name = lambda: db
+    param._identity.category_code = cat
+    if entry:
+        param._identity.category_entry_name = lambda: entry
+    else:
+        param._identity.category_entry_name = lambda: ''
+    if user_constrained:
+        param._user_constrained = True
+    if symmetry_constrained:
+        param._set_symmetry_constrained(value=True)
+    return param
+
+
+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
+
+    p1 = _make_param('db1', 'catA', '', 'alpha', 1.0)
+    p2 = _make_param('db2', 'catB', 'row1', 'beta', 2.0)
 
     class Coll:
         def __init__(self, params):
@@ -82,3 +95,203 @@ def fake_render_table2(**kwargs):
     # Unique names are datablock.category[.entry].parameter
     assert any('db1 catA  alpha' in r.replace('.', ' ') for r in flat_rows2)
     assert any('db2 catB row1 beta' in r.replace('.', ' ') for r in flat_rows2)
+
+
+def test_how_to_access_parameters_skips_large_loop_categories(capsys, monkeypatch):
+    import easydiffraction.analysis.analysis as analysis_mod
+    from easydiffraction.analysis.analysis import Analysis
+
+    visible = _make_param('db1', 'catA', '', 'alpha', 1.0)
+    data_param = _make_param('db2', 'pd_data', '1', 'intensity_meas', 2.0)
+    refln_param = _make_param('db2', 'refln', '1', 'f_calc', 3.0)
+
+    class Coll:
+        def __init__(self, params):
+            self.parameters = params
+
+    class Project:
+        _varname = 'proj'
+
+        def __init__(self):
+            self.structures = Coll([visible])
+            self.experiments = Coll([data_param, refln_param])
+
+    captured = {}
+
+    def fake_render_table(**kwargs):
+        captured.update(kwargs)
+
+    monkeypatch.setattr(analysis_mod, 'render_table', fake_render_table)
+    Analysis(Project()).display.how_to_access_parameters()
+
+    out = capsys.readouterr().out
+    assert 'How to access parameters' in out
+
+    flat_rows = [' '.join(map(str, row)) for row in captured.get('columns_data') or []]
+    assert any("proj.structures['db1'].catA.alpha" in row for row in flat_rows)
+    assert not any('pd_data' in row for row in flat_rows)
+    assert not any('refln' in row for row in flat_rows)
+
+
+def test_parameter_cif_uids_skips_large_loop_categories(monkeypatch):
+    import easydiffraction.analysis.analysis as analysis_mod
+    from easydiffraction.analysis.analysis import Analysis
+
+    visible = _make_param('db1', 'catA', '', 'alpha', 1.0)
+    data_param = _make_param('db2', 'pd_data', '1', 'intensity_meas', 2.0)
+    refln_param = _make_param('db2', 'refln', '1', 'f_calc', 3.0)
+
+    class Coll:
+        def __init__(self, params):
+            self.parameters = params
+
+    class Project:
+        _varname = 'proj'
+
+        def __init__(self):
+            self.structures = Coll([visible])
+            self.experiments = Coll([data_param, refln_param])
+
+    captured = {}
+
+    def fake_render_table(**kwargs):
+        captured.update(kwargs)
+
+    monkeypatch.setattr(analysis_mod, 'render_table', fake_render_table)
+    Analysis(Project()).display.parameter_cif_uids()
+
+    flat_rows = [' '.join(map(str, row)) for row in captured.get('columns_data') or []]
+    assert any('db1 catA  alpha' in row.replace('.', ' ') for row in flat_rows)
+    assert not any('pd_data' in row for row in flat_rows)
+    assert not any('refln' in row for row in flat_rows)
+
+
+def test_all_params_skips_large_loop_categories(monkeypatch):
+    import easydiffraction.analysis.analysis as analysis_mod
+    from easydiffraction.analysis.analysis import Analysis
+
+    structure_param = _make_param('s1', 'cell', '', 'length_a', 4.0)
+    visible_experiment_param = _make_param('e1', 'instrument', '', 'wavelength', 1.5)
+    data_param = _make_param('e1', 'pd_data', '1', 'intensity_meas', 10.0)
+    refln_param = _make_param('e1', 'refln', '1', 'f_calc', 12.0)
+
+    class Coll:
+        def __init__(self, params):
+            self.parameters = params
+
+        def __iter__(self):
+            return iter(())
+
+    class Project:
+        def __init__(self):
+            self.structures = Coll([structure_param])
+            self.experiments = Coll([visible_experiment_param, data_param, refln_param])
+
+    rendered = []
+
+    class FakeTableRenderer:
+        def render(self, df):
+            rendered.append(df)
+
+    monkeypatch.setattr(
+        analysis_mod.TableRenderer, 'get', staticmethod(lambda: FakeTableRenderer())
+    )
+    Analysis(Project()).display.all_params()
+
+    assert len(rendered) == 2
+    experiment_categories = rendered[1]['category', 'left'].tolist()
+    experiment_parameters = rendered[1]['parameter', 'left'].tolist()
+    assert experiment_categories == ['instrument']
+    assert experiment_parameters == ['wavelength']
+
+
+def test_all_params_marks_constrained_parameters_not_fittable(monkeypatch):
+    import easydiffraction.analysis.analysis as analysis_mod
+    from easydiffraction.analysis.analysis import Analysis
+
+    refinable_param = _make_param('s1', 'cell', '', 'length_a', 4.0)
+    user_constrained_param = _make_param('s1', 'cell', '', 'length_b', 4.0, user_constrained=True)
+    symmetry_constrained_param = _make_param(
+        's1',
+        'cell',
+        '',
+        'length_c',
+        4.0,
+        symmetry_constrained=True,
+    )
+
+    class Coll:
+        def __init__(self, params):
+            self.parameters = params
+
+        def __iter__(self):
+            return iter(())
+
+    class Project:
+        def __init__(self):
+            self.structures = Coll([
+                refinable_param,
+                user_constrained_param,
+                symmetry_constrained_param,
+            ])
+            self.experiments = Coll([])
+
+    rendered = []
+
+    class FakeTableRenderer:
+        def render(self, df):
+            rendered.append(df)
+
+    monkeypatch.setattr(
+        analysis_mod.TableRenderer, 'get', staticmethod(lambda: FakeTableRenderer())
+    )
+    Analysis(Project()).display.all_params()
+
+    structure_df = rendered[0]
+    assert structure_df['parameter', 'left'].tolist() == ['length_a', 'length_b', 'length_c']
+    assert structure_df['fittable', 'left'].tolist() == [True, False, False]
+
+
+def test_fittable_params_excludes_symmetry_constrained_parameters(monkeypatch):
+    import easydiffraction.analysis.analysis as analysis_mod
+    from easydiffraction.analysis.analysis import Analysis
+
+    visible_param = _make_param('s1', 'cell', '', 'length_a', 4.0)
+    symmetry_constrained_param = _make_param(
+        's1',
+        'cell',
+        '',
+        'length_c',
+        4.0,
+        symmetry_constrained=True,
+    )
+
+    class Coll:
+        def __init__(self, params, fittable_params):
+            self.parameters = params
+            self.fittable_parameters = fittable_params
+
+        def __iter__(self):
+            return iter(())
+
+    class Project:
+        def __init__(self):
+            self.structures = Coll(
+                [visible_param, symmetry_constrained_param],
+                [visible_param],
+            )
+            self.experiments = Coll([], [])
+
+    rendered = []
+
+    class FakeTableRenderer:
+        def render(self, df):
+            rendered.append(df)
+
+    monkeypatch.setattr(
+        analysis_mod.TableRenderer, 'get', staticmethod(lambda: FakeTableRenderer())
+    )
+    Analysis(Project()).display.fittable_params()
+
+    structure_df = rendered[0]
+    assert structure_df['parameter', 'left'].tolist() == ['length_a']
diff --git a/tests/unit/easydiffraction/analysis/test_analysis_coverage.py b/tests/unit/easydiffraction/analysis/test_analysis_coverage.py
index 089e1f6b8..8a741fef7 100644
--- a/tests/unit/easydiffraction/analysis/test_analysis_coverage.py
+++ b/tests/unit/easydiffraction/analysis/test_analysis_coverage.py
@@ -2,6 +2,10 @@
 # SPDX-License-Identifier: BSD-3-Clause
 """Additional unit tests for analysis.py to cover patch gaps."""
 
+from types import SimpleNamespace
+
+import numpy as np
+
 
 def _make_project():
     class ExpCol:
@@ -72,16 +76,20 @@ def test_empty_constraints_warns(self, capsys):
         assert 'No constraints' in out
 
     def test_constraints_with_items(self, capsys, monkeypatch):
-        import easydiffraction.analysis.analysis as mod
+        import easydiffraction.analysis.categories.constraints.default as constraints_mod
         from easydiffraction.analysis.analysis import Analysis
 
         a = Analysis(project=_make_project())
 
         # Create a fake constraint with expression
+        class FakeId:
+            value = 'constraint_1'
+
         class FakeExpr:
             value = 'x = y + 1'
 
         class FakeConstraint:
+            id = FakeId()
             expression = FakeExpr()
 
         a.constraints._items = [FakeConstraint()]
@@ -91,12 +99,13 @@ class FakeConstraint:
         def fake_render_table(**kwargs):
             captured.update(kwargs)
 
-        monkeypatch.setattr(mod, 'render_table', fake_render_table)
+        monkeypatch.setattr(constraints_mod, 'render_table', fake_render_table)
         a.display.constraints()
         out = capsys.readouterr().out
         assert 'User defined constraints' in out
+        assert captured['columns_headers'] == ['id', 'expression']
         assert 'columns_data' in captured
-        assert captured['columns_data'][0][0] == 'x = y + 1'
+        assert captured['columns_data'][0] == ['constraint_1', 'x = y + 1']
 
 
 # ------------------------------------------------------------------
@@ -164,8 +173,8 @@ def test_setter_changes_minimizer(self, capsys):
         from easydiffraction.analysis.analysis import Analysis
 
         a = Analysis(project=_make_project())
-        assert a.fit.minimizer_type.value == 'lmfit (leastsq)'
-        a.fit.minimizer_type = 'lmfit (leastsq)'
+        assert a.fitting.minimizer_type.value == 'lmfit (leastsq)'
+        a.fitting.minimizer_type = 'lmfit (leastsq)'
         out = capsys.readouterr().out
         assert 'Current minimizer changed to' in out
 
@@ -194,3 +203,104 @@ class FakeResults:
         assert 'expt1' in a._parameter_snapshots
         assert a._parameter_snapshots['expt1']['p1']['value'] == 1.23
         assert a._parameter_snapshots['expt1']['p1']['uncertainty'] == 0.01
+
+
+class TestBayesianProjection:
+    def test_single_parameter_projection_persists_distribution_and_predictive_caches(self):
+        from easydiffraction.analysis.analysis import Analysis
+        from easydiffraction.analysis.fit_helpers.bayesian import BayesianFitResults
+        from easydiffraction.analysis.fit_helpers.bayesian import PosteriorParameterSummary
+        from easydiffraction.analysis.fit_helpers.bayesian import PosteriorPredictiveSummary
+        from easydiffraction.analysis.fit_helpers.bayesian import PosteriorSamples
+
+        class Plotter:
+            @staticmethod
+            def _posterior_parameter_bounds(*, fit_results, parameter_name):
+                del fit_results, parameter_name
+                return 0.5, 1.5
+
+            @staticmethod
+            def _posterior_density_curve(values, *, lower_bound, upper_bound):
+                del values
+                return (
+                    np.asarray([lower_bound, upper_bound], dtype=float),
+                    np.asarray([0.25, 0.75], dtype=float),
+                )
+
+            @staticmethod
+            def _resolve_x_axis(experiment_type, _axis_name):
+                del experiment_type
+                return np.asarray([1.0, 2.0], dtype=float), 'two_theta', None, None, None
+
+            @staticmethod
+            def _build_posterior_predictive_summary(
+                *,
+                fit_results,
+                experiment,
+                expt_name,
+                x_axis,
+                include_draws,
+            ):
+                del fit_results, experiment, x_axis, include_draws
+                return PosteriorPredictiveSummary(
+                    experiment_name=expt_name,
+                    x_axis_name='two_theta',
+                    x=np.asarray([1.0, 2.0], dtype=float),
+                    best_sample_prediction=np.asarray([3.0, 4.0], dtype=float),
+                    lower_95=np.asarray([2.5, 3.5], dtype=float),
+                    upper_95=np.asarray([3.5, 4.5], dtype=float),
+                )
+
+        class Experiments:
+            names = ['hrpt']
+
+            def __getitem__(self, name):
+                del name
+                return SimpleNamespace(type='powder')
+
+        project = SimpleNamespace(
+            experiments=Experiments(),
+            structures=object(),
+            rendering=SimpleNamespace(plotter=Plotter()),
+            _varname='proj',
+        )
+        analysis = Analysis(project=project)
+
+        results = BayesianFitResults(
+            success=True,
+            parameters=[],
+            posterior_samples=PosteriorSamples(
+                parameter_names=['alpha'],
+                parameter_samples=np.asarray([[[1.0]], [[1.2]]], dtype=float),
+            ),
+            posterior_parameter_summaries=[
+                PosteriorParameterSummary(
+                    unique_name='alpha',
+                    display_name='Alpha',
+                    best_sample_value=1.2,
+                    median=1.1,
+                    standard_deviation=0.1,
+                    interval_68=(1.0, 1.2),
+                    interval_95=(0.9, 1.3),
+                )
+            ],
+            posterior_predictive={},
+            sampler_settings={},
+            convergence_diagnostics={},
+        )
+
+        analysis._store_bayesian_result_projection(results)
+
+        assert analysis.bayesian_result.has_distribution_cache.value is True
+        assert analysis.bayesian_result.has_pair_cache.value is False
+        assert analysis.bayesian_result.has_posterior_predictive.value is True
+        assert np.allclose(
+            analysis._persisted_fit_state_sidecar['distribution_caches']['alpha']['x'],
+            np.asarray([0.5, 1.5], dtype=float),
+        )
+        assert np.allclose(
+            analysis._persisted_fit_state_sidecar['predictive_datasets']['hrpt'][
+                'best_sample_prediction'
+            ],
+            np.asarray([3.0, 4.0], dtype=float),
+        )
diff --git a/tests/unit/easydiffraction/analysis/test_enums.py b/tests/unit/easydiffraction/analysis/test_enums.py
new file mode 100644
index 000000000..75e1ca3ec
--- /dev/null
+++ b/tests/unit/easydiffraction/analysis/test_enums.py
@@ -0,0 +1,36 @@
+# SPDX-FileCopyrightText: 2026 EasyScience contributors 
+# SPDX-License-Identifier: BSD-3-Clause
+"""Tests for analysis/enums.py."""
+
+from easydiffraction.analysis.enums import FitCorrelationSourceEnum
+from easydiffraction.analysis.enums import FitResultKindEnum
+from easydiffraction.analysis.enums import FitModeEnum
+
+
+def test_fit_mode_enum_members():
+    assert FitModeEnum.SINGLE == 'single'
+    assert FitModeEnum.JOINT == 'joint'
+    assert FitModeEnum.SEQUENTIAL == 'sequential'
+
+
+def test_fit_mode_enum_default():
+    assert FitModeEnum.default() is FitModeEnum.SINGLE
+
+
+def test_fit_mode_enum_descriptions():
+    for member in FitModeEnum:
+        description = member.description()
+        assert isinstance(description, str)
+        assert description
+
+
+def test_fit_result_kind_enum_members_and_default():
+    assert FitResultKindEnum.DETERMINISTIC == 'deterministic'
+    assert FitResultKindEnum.BAYESIAN == 'bayesian'
+    assert FitResultKindEnum.default() is FitResultKindEnum.DETERMINISTIC
+
+
+def test_fit_correlation_source_enum_members_and_default():
+    assert FitCorrelationSourceEnum.DETERMINISTIC == 'deterministic'
+    assert FitCorrelationSourceEnum.POSTERIOR == 'posterior'
+    assert FitCorrelationSourceEnum.default() is FitCorrelationSourceEnum.DETERMINISTIC
diff --git a/tests/unit/easydiffraction/analysis/test_fitting.py b/tests/unit/easydiffraction/analysis/test_fitting.py
index 0f7ef88b1..83272f9fa 100644
--- a/tests/unit/easydiffraction/analysis/test_fitting.py
+++ b/tests/unit/easydiffraction/analysis/test_fitting.py
@@ -1,6 +1,10 @@
 # SPDX-FileCopyrightText: 2025 EasyScience contributors 
 # SPDX-License-Identifier: BSD-3-Clause
 
+from types import SimpleNamespace
+
+from easydiffraction.utils.enums import VerbosityEnum
+
 
 def test_module_import():
     import easydiffraction.analysis.fitting as MUT
@@ -71,7 +75,11 @@ class DummyExperiment:
         parameters = []
 
     class MockFitResults:
-        pass
+        def __init__(self):
+            self.message = ''
+            self.iterations = 0
+            self.chi_square = None
+            self.engine_result = object()
 
     class DummyMin:
         tracker = type('T', (), {'track': staticmethod(lambda a, b: a)})()
@@ -82,6 +90,9 @@ def fit(self, params, obj, verbosity=None, **kwargs):
         def _sync_result_to_parameters(self, params, engine_params):
             pass
 
+        def _stop_tracking(self):
+            return None
+
     f = Fitter()
     f.minimizer = DummyMin()
 
@@ -101,3 +112,174 @@ def mock_process(*args, **kwargs):
         'Use Analysis.show_fit_results() instead.'
     )
     assert f.results is not None, 'Fitter.fit() should still set results'
+
+
+def test_fitter_fit_defers_minimizer_tracking_until_postprocessing(monkeypatch):
+    from easydiffraction.analysis.fit_helpers.bayesian import BayesianFitResults
+    from easydiffraction.analysis.fitting import Fitter
+
+    class DummyParam:
+        value = 1.0
+        uncertainty = 0.1
+        _fit_start_value = None
+
+    class DummyStructure:
+        _need_categories_update = False
+
+        def _update_categories(self):
+            return None
+
+    class DummyStructures:
+        def __iter__(self):
+            return iter([DummyStructure()])
+
+    class DummyExperiment:
+        parameters = []
+
+    class DummyMin:
+        def __init__(self):
+            self.fit_calls: list[dict[str, object]] = []
+            self.stop_calls = 0
+            self.tracker = SimpleNamespace(track=lambda residuals, parameters: residuals)
+
+        def fit(self, params, obj, verbosity=None, **kwargs):
+            del params, obj
+            self.fit_calls.append({'verbosity': verbosity, **kwargs})
+            return BayesianFitResults(
+                success=True,
+                reduced_chi_square=1.2,
+                convergence_diagnostics={'converged': False},
+                sampler_settings={'steps': 300},
+                best_log_posterior=-10.0,
+            )
+
+        def _stop_tracking(self):
+            self.stop_calls += 1
+
+    analysis_events: list[str] = []
+    analysis = SimpleNamespace(
+        _capture_fit_parameter_state=lambda params: analysis_events.append('capture'),
+        _store_fit_result_projection=lambda results, experiments, fitted_parameters: (
+            analysis_events.append('store')
+        ),
+    )
+
+    fitter = Fitter()
+    fitter.minimizer = DummyMin()
+    monkeypatch.setattr(
+        fitter,
+        '_collect_fit_parameters',
+        lambda structures, experiments: [DummyParam()],
+    )
+
+    fitter.fit(
+        structures=DummyStructures(),
+        experiments=[DummyExperiment()],
+        analysis=analysis,
+        verbosity=VerbosityEnum.FULL,
+    )
+
+    assert fitter.minimizer.fit_calls[0]['finalize_tracking'] is False
+    assert fitter.minimizer.stop_calls == 1
+    assert analysis_events == ['capture', 'store']
+
+
+def test_fitter_fit_stops_tracking_when_minimizer_fit_raises(monkeypatch):
+    import pytest
+
+    from easydiffraction.analysis.fitting import Fitter
+
+    class DummyParam:
+        value = 1.0
+        _fit_start_value = None
+
+    class DummyStructure:
+        _need_categories_update = False
+
+        def _update_categories(self):
+            return None
+
+    class DummyStructures:
+        def __iter__(self):
+            return iter([DummyStructure()])
+
+    class DummyExperiment:
+        parameters = []
+
+    class DummyMin:
+        def __init__(self):
+            self.stop_calls = 0
+            self.tracker = SimpleNamespace(track=lambda residuals, parameters: residuals)
+
+        def fit(self, params, obj, verbosity=None, **kwargs):
+            del params, obj, verbosity, kwargs
+            msg = 'fit failed'
+            raise RuntimeError(msg)
+
+        def _stop_tracking(self):
+            self.stop_calls += 1
+
+    fitter = Fitter()
+    fitter.minimizer = DummyMin()
+    monkeypatch.setattr(
+        fitter,
+        '_collect_fit_parameters',
+        lambda structures, experiments: [DummyParam()],
+    )
+
+    with pytest.raises(RuntimeError, match='fit failed'):
+        fitter.fit(
+            structures=DummyStructures(),
+            experiments=[DummyExperiment()],
+            verbosity=VerbosityEnum.FULL,
+        )
+
+    assert fitter.minimizer.stop_calls == 1
+
+
+def test_residual_function_skips_tracker_for_solver_monitored_minimizer(monkeypatch):
+    import numpy as np
+
+    from easydiffraction.analysis.fitting import Fitter
+
+    class DummyExperiment:
+        def _update_categories(self, *, called_by_minimizer=False):
+            del called_by_minimizer
+            return
+
+    class DummyMin:
+        def __init__(self):
+            self.tracker = SimpleNamespace(
+                track=lambda residuals, parameters: (_ for _ in ()).throw(
+                    AssertionError('tracker.track should not be called')
+                )
+            )
+
+        def _sync_result_to_parameters(self, parameters, engine_params):
+            del parameters, engine_params
+
+        def _tracks_progress_via_solver_monitor(self):
+            return True
+
+    fitter = Fitter()
+    fitter.minimizer = DummyMin()
+
+    monkeypatch.setattr(
+        'easydiffraction.analysis.fitting.intensity_category_for',
+        lambda experiment: SimpleNamespace(
+            intensity_calc=np.array([1.0]),
+            intensity_meas=np.array([2.0]),
+            intensity_meas_su=np.array([1.0]),
+        ),
+    )
+
+    residuals = fitter._residual_function(
+        engine_params={},
+        parameters=[],
+        structures=[],
+        experiments=[DummyExperiment()],
+        weights=None,
+        analysis=None,
+    )
+
+    np.testing.assert_allclose(residuals, np.array([1.0]))
diff --git a/tests/unit/easydiffraction/analysis/test_sequential.py b/tests/unit/easydiffraction/analysis/test_sequential.py
index 3179a0b13..2be4c19f3 100644
--- a/tests/unit/easydiffraction/analysis/test_sequential.py
+++ b/tests/unit/easydiffraction/analysis/test_sequential.py
@@ -4,7 +4,9 @@
 
 from __future__ import annotations
 
+import contextlib
 import csv
+from types import SimpleNamespace
 
 import pytest
 
@@ -12,8 +14,15 @@
 from easydiffraction.analysis.sequential import _META_COLUMNS
 from easydiffraction.analysis.sequential import _append_to_csv
 from easydiffraction.analysis.sequential import _build_csv_header
+from easydiffraction.analysis.sequential import _chunk_file_range
 from easydiffraction.analysis.sequential import _read_csv_for_recovery
+from easydiffraction.analysis.sequential import _relative_file_path_for_csv
 from easydiffraction.analysis.sequential import _write_csv_header
+from easydiffraction.display.progress import ACTIVITY_LABEL_FITTING
+from easydiffraction.utils.enums import VerbosityEnum
+
+_TEST_SCAN_001 = 'data/scan_001.xye'
+_TEST_SCAN_002 = 'data/scan_002.xye'
 
 
 # ------------------------------------------------------------------
@@ -39,10 +48,103 @@ def _minimal_template(
         constraints_enabled=False,
         minimizer_tag='lmfit',
         calculator_tag='cryspy',
+        diffrn_extract_rules=[],
         diffrn_field_names=diffrn_fields,
     )
 
 
+class _RecordingConsole:
+    def __init__(self, events):
+        self._events = events
+
+    def paragraph(self, text):
+        self._events.append(('paragraph', text))
+
+    def print(self, *args, **kwargs):
+        self._events.append(('console_print', args, kwargs))
+
+
+def _make_indicator(events):
+    class RecordingIndicator:
+        def __init__(self, label, *, verbosity, animated=True):
+            events.append(('init', label, verbosity, animated))
+
+        def start(self):
+            events.append(('start',))
+
+        def update(self, *, label=None, content=None):
+            events.append(('update', label, content))
+
+        def stop(self):
+            events.append(('stop',))
+
+    return RecordingIndicator
+
+
+def _make_run_fit_loop(events, template, verbosity):
+    def fake_run_fit_loop(pool_cm, chunks, template_arg, csv_info, progress):
+        del pool_cm, csv_info
+        assert chunks == [['scan_001.xye']]
+        assert template_arg == template
+        assert progress.verbosity is VerbosityEnum(verbosity)
+        assert progress.state is not None
+        assert progress.state.chunk_rows == []
+        assert progress.state.file_rows == []
+        events.append((
+            'run_loop',
+            progress.indicator is not None,
+        ))
+
+    return fake_run_fit_loop
+
+
+def _run_non_silent_fit(monkeypatch, tmp_path, *, verbosity, is_jupyter):
+    import easydiffraction.analysis.sequential as sequential_mod
+
+    events: list[tuple[object, ...]] = []
+    template = _minimal_template()
+
+    monkeypatch.setattr(sequential_mod.mp, 'parent_process', lambda: None)
+    monkeypatch.setattr(sequential_mod, '_check_seq_preconditions', lambda project: None)
+    monkeypatch.setattr(sequential_mod, 'console', _RecordingConsole(events))
+    monkeypatch.setattr(
+        sequential_mod,
+        'extract_data_paths_from_dir',
+        lambda data_dir, file_pattern='*': ['scan_001.xye'],
+    )
+    monkeypatch.setattr(sequential_mod, '_build_template', lambda project: template)
+    monkeypatch.setattr(
+        sequential_mod,
+        '_setup_csv_and_recovery',
+        lambda project, template_arg, verb: (
+            tmp_path / 'results.csv',
+            ['file_path'],
+            set(),
+            template_arg,
+        ),
+    )
+    monkeypatch.setattr(sequential_mod, '_resolve_workers', lambda max_workers, chunk_size: (1, 1))
+    monkeypatch.setattr(
+        sequential_mod,
+        '_run_fit_loop_with_pool',
+        lambda max_workers, chunks, template_arg, csv_info, progress: _make_run_fit_loop(
+            events,
+            template,
+            verbosity,
+        )(None, chunks, template_arg, csv_info, progress),
+    )
+    monkeypatch.setattr(sequential_mod, 'ActivityIndicator', _make_indicator(events))
+    del is_jupyter  # legacy parameter, no longer affects behavior
+
+    analysis = SimpleNamespace(
+        project=SimpleNamespace(verbosity=SimpleNamespace(fit=SimpleNamespace(value=verbosity))),
+        fitter=SimpleNamespace(selection='lmfit'),
+    )
+
+    sequential_mod.fit_sequential(analysis, data_dir=str(tmp_path))
+    return events
+
+
 # ------------------------------------------------------------------
 #  _build_csv_header
 # ------------------------------------------------------------------
@@ -96,7 +198,7 @@ def test_full_header_order(self):
 class TestCsvWriteAndAppend:
     def test_write_creates_file_with_header(self, tmp_path):
         csv_path = tmp_path / 'results.csv'
-        header = ['file_path', 'chi_squared', 'param_a']
+        header = ['file_path', 'fit_result.reduced_chi_square', 'param_a']
         _write_csv_header(csv_path, header)
 
         with csv_path.open() as f:
@@ -124,6 +226,73 @@ def test_append_adds_rows(self, tmp_path):
         assert rows[0]['file_path'] == 'a.dat'
         assert rows[1]['value'] == '2.0'
 
+    def test_append_stores_file_paths_relative_to_project(self, tmp_path):
+        project_dir = tmp_path / 'project'
+        csv_path = project_dir / 'analysis' / 'results.csv'
+        csv_path.parent.mkdir(parents=True)
+        data_dir = project_dir / 'experiments' / 'scan'
+        data_dir.mkdir(parents=True)
+        data_path = data_dir / 'scan_001.dat'
+        data_path.write_text('1 2 3\n')
+        header = ['file_path', 'value']
+        _write_csv_header(csv_path, header)
+
+        _append_to_csv(
+            csv_path,
+            header,
+            [
+                {'file_path': str(data_path), 'value': 1.0},
+            ],
+        )
+
+        with csv_path.open() as f:
+            rows = list(csv.DictReader(f))
+        assert rows[0]['file_path'] == 'experiments/scan/scan_001.dat'
+
+    def test_append_normalizes_repo_relative_project_paths(self, tmp_path, monkeypatch):
+        workspace_dir = tmp_path / 'workspace'
+        project_dir = workspace_dir / 'projects' / 'cosio'
+        csv_path = project_dir / 'analysis' / 'results.csv'
+        csv_path.parent.mkdir(parents=True)
+        monkeypatch.chdir(workspace_dir)
+        header = ['file_path', 'value']
+        _write_csv_header(csv_path, header)
+
+        _append_to_csv(
+            csv_path,
+            header,
+            [
+                {
+                    'file_path': 'projects/cosio/experiments/d20_scan/scan_001.dat',
+                    'value': 1.0,
+                },
+            ],
+        )
+
+        with csv_path.open() as f:
+            rows = list(csv.DictReader(f))
+        assert rows[0]['file_path'] == 'experiments/d20_scan/scan_001.dat'
+
+    def test_relative_file_paths_use_posix_separators(self, tmp_path, monkeypatch):
+        import easydiffraction.analysis.sequential as sequential_mod
+
+        project_dir = tmp_path / 'project'
+        csv_path = project_dir / 'analysis' / 'results.csv'
+        csv_path.parent.mkdir(parents=True)
+        data_dir = project_dir / 'experiments' / 'scan'
+        data_dir.mkdir(parents=True)
+        data_path = data_dir / 'scan_001.dat'
+        data_path.write_text('1 2 3\n')
+        monkeypatch.setattr(
+            sequential_mod.os.path,
+            'relpath',
+            lambda _path, start: 'experiments\\scan\\scan_001.dat',
+        )
+
+        relative_path = _relative_file_path_for_csv(csv_path, str(data_path))
+
+        assert relative_path == 'experiments/scan/scan_001.dat'
+
     def test_append_ignores_extra_keys(self, tmp_path):
         csv_path = tmp_path / 'results.csv'
         header = ['file_path']
@@ -156,7 +325,9 @@ def test_returns_empty_when_no_file(self, tmp_path):
         assert params is None
 
     def test_returns_fitted_file_paths(self, tmp_path):
-        csv_path = tmp_path / 'results.csv'
+        project_dir = tmp_path / 'project'
+        csv_path = project_dir / 'analysis' / 'results.csv'
+        csv_path.parent.mkdir(parents=True)
         header = [*_META_COLUMNS, 'cell.a', 'cell.a.uncertainty']
         _write_csv_header(csv_path, header)
         _append_to_csv(
@@ -164,20 +335,18 @@ def test_returns_fitted_file_paths(self, tmp_path):
             header,
             [
                 {
-                    'file_path': '/data/a.dat',
-                    'fit_success': 'True',
-                    'chi_squared': '5.0',
-                    'reduced_chi_squared': '2.5',
-                    'n_iterations': '10',
+                    'file_path': str(project_dir / 'experiments' / 'a.dat'),
+                    'fit_result.success': 'True',
+                    'fit_result.reduced_chi_square': '2.5',
+                    'fit_result.iterations': '10',
                     'cell.a': '3.89',
                     'cell.a.uncertainty': '0.01',
                 },
                 {
-                    'file_path': '/data/b.dat',
-                    'fit_success': 'False',
-                    'chi_squared': '',
-                    'reduced_chi_squared': '',
-                    'n_iterations': '0',
+                    'file_path': str(project_dir / 'experiments' / 'b.dat'),
+                    'fit_result.success': 'False',
+                    'fit_result.reduced_chi_square': '',
+                    'fit_result.iterations': '0',
                     'cell.a': '',
                     'cell.a.uncertainty': '',
                 },
@@ -185,7 +354,35 @@ def test_returns_fitted_file_paths(self, tmp_path):
         )
 
         fitted, _params = _read_csv_for_recovery(csv_path)
-        assert fitted == {'/data/a.dat', '/data/b.dat'}
+        assert fitted == {
+            str((project_dir / 'experiments' / 'a.dat').resolve()),
+            str((project_dir / 'experiments' / 'b.dat').resolve()),
+        }
+
+    def test_resolves_legacy_repo_relative_paths(self, tmp_path, monkeypatch):
+        workspace_dir = tmp_path / 'workspace'
+        project_dir = workspace_dir / 'projects' / 'cosio'
+        csv_path = project_dir / 'analysis' / 'results.csv'
+        csv_path.parent.mkdir(parents=True)
+        monkeypatch.chdir(workspace_dir)
+        header = [*_META_COLUMNS, 'cell.a', 'cell.a.uncertainty']
+
+        with csv_path.open('w', newline='', encoding='utf-8') as handle:
+            writer = csv.DictWriter(handle, fieldnames=header)
+            writer.writeheader()
+            writer.writerow({
+                'file_path': 'projects/cosio/experiments/d20_scan/scan_001.dat',
+                'fit_result.success': 'True',
+                'fit_result.reduced_chi_square': '2.5',
+                'fit_result.iterations': '10',
+                'cell.a': '3.89',
+                'cell.a.uncertainty': '0.01',
+            })
+
+        fitted, _params = _read_csv_for_recovery(csv_path)
+        assert fitted == {
+            str((project_dir / 'experiments' / 'd20_scan' / 'scan_001.dat').resolve())
+        }
 
     def test_returns_last_successful_params(self, tmp_path):
         csv_path = tmp_path / 'results.csv'
@@ -197,19 +394,17 @@ def test_returns_last_successful_params(self, tmp_path):
             [
                 {
                     'file_path': 'a.dat',
-                    'fit_success': 'True',
-                    'chi_squared': '5.0',
-                    'reduced_chi_squared': '2.5',
-                    'n_iterations': '10',
+                    'fit_result.success': 'True',
+                    'fit_result.reduced_chi_square': '2.5',
+                    'fit_result.iterations': '10',
                     'cell.a': '3.89',
                     'cell.a.uncertainty': '0.01',
                 },
                 {
                     'file_path': 'b.dat',
-                    'fit_success': 'True',
-                    'chi_squared': '4.0',
-                    'reduced_chi_squared': '2.0',
-                    'n_iterations': '8',
+                    'fit_result.success': 'True',
+                    'fit_result.reduced_chi_square': '2.0',
+                    'fit_result.iterations': '8',
                     'cell.a': '3.90',
                     'cell.a.uncertainty': '0.02',
                 },
@@ -236,10 +431,9 @@ def test_skips_meta_columns_and_diffrn_and_uncertainty(self, tmp_path):
             [
                 {
                     'file_path': 'a.dat',
-                    'fit_success': 'True',
-                    'chi_squared': '5.0',
-                    'reduced_chi_squared': '2.5',
-                    'n_iterations': '10',
+                    'fit_result.success': 'True',
+                    'fit_result.reduced_chi_square': '2.5',
+                    'fit_result.iterations': '10',
                     'diffrn.temp': '300',
                     'cell.a': '3.89',
                     'cell.a.uncertainty': '0.01',
@@ -252,7 +446,7 @@ def test_skips_meta_columns_and_diffrn_and_uncertainty(self, tmp_path):
         assert 'cell.a' in params
         # Meta columns, diffrn, and uncertainty should be excluded
         assert 'file_path' not in params
-        assert 'fit_success' not in params
+        assert 'fit_result.success' not in params
         assert 'diffrn.temp' not in params
         assert 'cell.a.uncertainty' not in params
 
@@ -266,10 +460,9 @@ def test_returns_none_params_when_no_successful_rows(self, tmp_path):
             [
                 {
                     'file_path': 'a.dat',
-                    'fit_success': 'False',
-                    'chi_squared': '',
-                    'reduced_chi_squared': '',
-                    'n_iterations': '0',
+                    'fit_result.success': 'False',
+                    'fit_result.reduced_chi_square': '',
+                    'fit_result.iterations': '0',
                     'cell.a': '',
                     'cell.a.uncertainty': '',
                 },
@@ -300,3 +493,236 @@ def test_fields_accessible(self):
         assert template.diffrn_field_names == ['temp']
         assert template.minimizer_tag == 'lmfit'
         assert template.calculator_tag == 'cryspy'
+
+
+class TestChunkFileRange:
+    def test_formats_inclusive_range_with_spaced_dash(self):
+        assert _chunk_file_range([_TEST_SCAN_001, _TEST_SCAN_002]) == (
+            'scan_001.xye - scan_002.xye'
+        )
+
+    def test_returns_single_name_for_single_file_chunk(self):
+        assert _chunk_file_range([_TEST_SCAN_001]) == 'scan_001.xye'
+
+
+@pytest.mark.parametrize('verbosity', [VerbosityEnum.SHORT, VerbosityEnum.FULL])
+def test_report_chunk_progress_updates_indicator_with_renderable(monkeypatch, verbosity):
+    import easydiffraction.analysis.sequential as sequential_mod
+
+    update_calls: list[object] = []
+
+    class RecordingIndicator:
+        def update(self, *, label=None, content=None):
+            del label
+            update_calls.append(content)
+
+    progress_state = sequential_mod.SequentialProgressState(chunk_rows=[], file_rows=[])
+    progress = sequential_mod.SequentialProgressContext(
+        verbosity=verbosity,
+        state=progress_state,
+        indicator=RecordingIndicator(),
+    )
+
+    sequential_mod._report_chunk_progress(
+        1,
+        3,
+        [_TEST_SCAN_001, _TEST_SCAN_002],
+        [
+            {
+                'file_path': _TEST_SCAN_001,
+                'fit_result.success': True,
+                'fit_result.reduced_chi_square': 4.0,
+                'fit_result.iterations': 11,
+            },
+            {
+                'file_path': _TEST_SCAN_002,
+                'fit_result.success': False,
+                'fit_result.reduced_chi_square': None,
+                'fit_result.iterations': 0,
+            },
+        ],
+        progress,
+        sequential_mod._ChunkProgressMetrics(
+            completed_files_before=0,
+            total_files=3,
+            elapsed_time=19.76,
+        ),
+    )
+
+    if verbosity is VerbosityEnum.SHORT:
+        expected_rows = [
+            ['1/3', '66.7%', '19.76', 'scan_001.xye - scan_002.xye', '2', '4.00', '⚠️']
+        ]
+        assert progress_state.chunk_rows == expected_rows
+        assert progress_state.file_rows == []
+    else:
+        expected_rows = [
+            ['scan_001.xye', '33.3%', '19.76', '4.00', '11', '✅'],
+            ['scan_002.xye', '66.7%', '19.76', '—', '0', '❌'],
+        ]
+        assert progress_state.chunk_rows == []
+        assert progress_state.file_rows == expected_rows
+
+    assert len(update_calls) == 1
+    assert update_calls[0] is not None
+
+
+@pytest.mark.parametrize(
+    'verbosity',
+    ['short', 'full'],
+)
+def test_fit_sequential_non_silent_starts_indicator_with_progress_table(
+    monkeypatch,
+    tmp_path,
+    verbosity,
+):
+    events = _run_non_silent_fit(
+        monkeypatch,
+        tmp_path,
+        verbosity=verbosity,
+        is_jupyter=False,
+    )
+
+    verb_enum = VerbosityEnum(verbosity)
+
+    assert events[:4] == [
+        ('paragraph', 'Sequential fitting'),
+        ('console_print', ("🚀 Starting fit process with 'lmfit'...",), {}),
+        ('console_print', ('📋 1 files in 1 chunks (max_workers=1)',), {}),
+        ('console_print', ('📈 Goodness-of-fit progress:',), {}),
+    ]
+    assert events[4] == ('init', ACTIVITY_LABEL_FITTING, verb_enum, True)
+    assert events[5] == ('start',)
+    # Initial empty table renderable is pushed before the fit loop runs.
+    assert events[6][0] == 'update'
+    assert events[6][1] is None
+    assert events[6][2] is not None
+    assert events[7] == ('run_loop', True)
+    assert events[8] == ('stop',)
+    assert events[9:] == [
+        ('console_print', ('✅ Sequential fitting complete: 1 files processed.',), {}),
+        ('console_print', (f'📄 Results saved to:\n{tmp_path / "results.csv"}',), {}),
+    ]
+
+
+def test_run_fit_loop_runs_chunks_sequentially_with_executor_map(monkeypatch, tmp_path):
+    import easydiffraction.analysis.sequential as sequential_mod
+
+    template = _minimal_template()
+    header = ['file_path']
+    events: list[tuple[object, ...]] = []
+    progress = sequential_mod.SequentialProgressContext(
+        verbosity=VerbosityEnum.SHORT,
+        state=sequential_mod.SequentialProgressState(chunk_rows=[], file_rows=[]),
+    )
+
+    class FakeExecutor:
+        def map(self, func, templates, paths):
+            assert func is sequential_mod._fit_worker
+            for template_arg in templates:
+                assert template_arg == template
+            for path in paths:
+                yield {
+                    'file_path': path,
+                    'fit_result.success': True,
+                    'fit_result.reduced_chi_square': 1.0,
+                    'fit_result.iterations': 5,
+                    'params': {'cell.a': 4.0},
+                }
+
+    class FakePool:
+        def __enter__(self):
+            return FakeExecutor()
+
+        def __exit__(self, exc_type, exc, tb):
+            del exc_type, exc, tb
+            return False
+
+    monkeypatch.setattr(
+        sequential_mod,
+        '_append_to_csv',
+        lambda csv_path, header_arg, results: events.append((
+            'append',
+            csv_path,
+            header_arg,
+            [result['file_path'] for result in results],
+        )),
+    )
+    monkeypatch.setattr(
+        sequential_mod,
+        '_report_chunk_progress',
+        lambda *args: events.append(('report', [result['file_path'] for result in args[3]])),
+    )
+
+    sequential_mod._run_fit_loop(
+        FakePool(),
+        [[_TEST_SCAN_001, _TEST_SCAN_002]],
+        template,
+        (tmp_path / 'results.csv', header),
+        progress,
+    )
+
+    assert events == [
+        ('append', tmp_path / 'results.csv', header, [_TEST_SCAN_001, _TEST_SCAN_002]),
+        ('report', [_TEST_SCAN_001, _TEST_SCAN_002]),
+    ]
+
+
+def test_fit_sequential_silent_does_not_start_indicator(monkeypatch, tmp_path):
+    import easydiffraction.analysis.sequential as sequential_mod
+
+    template = _minimal_template()
+
+    class FailingIndicator:
+        def __init__(self, *args, **kwargs):
+            message = 'silent mode should not create an activity indicator'
+            raise AssertionError(message)
+
+    def fake_run_fit_loop(
+        pool_cm,
+        chunks,
+        template_arg,
+        csv_info,
+        progress,
+    ):
+        del pool_cm, csv_info
+        assert chunks == [['scan_001.xye']]
+        assert template_arg == template
+        assert progress.verbosity is VerbosityEnum.SILENT
+        assert progress.state is None
+        assert progress.indicator is None
+
+    monkeypatch.setattr(sequential_mod, 'ActivityIndicator', FailingIndicator)
+    monkeypatch.setattr(sequential_mod.mp, 'parent_process', lambda: None)
+    monkeypatch.setattr(sequential_mod, '_check_seq_preconditions', lambda project: None)
+    monkeypatch.setattr(
+        sequential_mod,
+        'extract_data_paths_from_dir',
+        lambda data_dir, file_pattern='*': ['scan_001.xye'],
+    )
+    monkeypatch.setattr(sequential_mod, '_build_template', lambda project: template)
+    monkeypatch.setattr(
+        sequential_mod,
+        '_setup_csv_and_recovery',
+        lambda project, template_arg, verb: (
+            tmp_path / 'results.csv',
+            ['file_path'],
+            set(),
+            template_arg,
+        ),
+    )
+    monkeypatch.setattr(sequential_mod, '_resolve_workers', lambda max_workers, chunk_size: (1, 1))
+    monkeypatch.setattr(
+        sequential_mod,
+        '_create_pool_context',
+        lambda max_workers: (contextlib.nullcontext(None), None, None, None),
+    )
+    monkeypatch.setattr(sequential_mod, '_run_fit_loop', fake_run_fit_loop)
+    monkeypatch.setattr(sequential_mod, '_restore_main_state', lambda *args: None)
+
+    analysis = SimpleNamespace(
+        project=SimpleNamespace(verbosity=SimpleNamespace(fit=SimpleNamespace(value='silent'))),
+        fitter=SimpleNamespace(selection='lmfit'),
+    )
+
+    sequential_mod.fit_sequential(analysis, data_dir=str(tmp_path))
diff --git a/tests/unit/easydiffraction/core/test_category.py b/tests/unit/easydiffraction/core/test_category.py
index 3d27b5012..c1a4ffbe2 100644
--- a/tests/unit/easydiffraction/core/test_category.py
+++ b/tests/unit/easydiffraction/core/test_category.py
@@ -1,6 +1,10 @@
 # SPDX-FileCopyrightText: 2025 EasyScience contributors 
 # SPDX-License-Identifier: BSD-3-Clause
 
+import importlib
+
+import pytest
+
 from easydiffraction.core.category import CategoryCollection
 from easydiffraction.core.category import CategoryItem
 from easydiffraction.core.validation import AttributeSpec
@@ -9,9 +13,11 @@
 
 
 class SimpleItem(CategoryItem):
+    _category_code = 'simple'
+    _category_entry_name = 'a'
+
     def __init__(self):
         super().__init__()
-        self._identity.category_code = 'simple'
         object.__setattr__(
             self,
             '_a',
@@ -32,7 +38,6 @@ def __init__(self):
                 cif_handler=CifHandler(names=['_simple.b']),
             ),
         )
-        self._identity.category_entry_name = lambda: str(self._a.value)
 
     @property
     def a(self):
@@ -56,6 +61,154 @@ def __init__(self):
         super().__init__(item_type=SimpleItem)
 
 
+def test_category_item_uses_declared_identity_metadata():
+    it = SimpleItem()
+
+    assert type(it)._category_code == 'simple'
+    assert type(it)._category_entry_name == 'a'
+    assert it._identity.category_code == 'simple'
+
+    it.a = 'name1'
+
+    assert it._identity.category_entry_name == 'name1'
+
+
+@pytest.mark.parametrize(
+    ('module_name', 'class_name', 'attr_name', 'value', 'category_code'),
+    [
+        pytest.param(
+            'easydiffraction.analysis.categories.aliases.default',
+            'Alias',
+            'label',
+            'alias_1',
+            'alias',
+            id='alias',
+        ),
+        pytest.param(
+            'easydiffraction.analysis.categories.constraints.default',
+            'Constraint',
+            'id',
+            'constraint_1',
+            'constraint',
+            id='constraint',
+        ),
+        pytest.param(
+            'easydiffraction.analysis.categories.joint_fit.default',
+            'JointFitItem',
+            'experiment_id',
+            'exp_1',
+            'joint_fit',
+            id='joint_fit',
+        ),
+        pytest.param(
+            'easydiffraction.analysis.categories.sequential_fit_extract.default',
+            'SequentialFitExtractItem',
+            'id',
+            'rule_1',
+            'sequential_fit_extract',
+            id='sequential_fit_extract',
+        ),
+        pytest.param(
+            'easydiffraction.datablocks.structure.categories.atom_sites.default',
+            'AtomSite',
+            'label',
+            'Fe1',
+            'atom_site',
+            id='atom_site',
+        ),
+        pytest.param(
+            'easydiffraction.datablocks.structure.categories.atom_site_aniso.default',
+            'AtomSiteAniso',
+            'label',
+            'Fe1',
+            'atom_site_aniso',
+            id='atom_site_aniso',
+        ),
+        pytest.param(
+            'easydiffraction.datablocks.experiment.categories.linked_phases.default',
+            'LinkedPhase',
+            'id',
+            'phase_1',
+            'linked_phases',
+            id='linked_phases',
+        ),
+        pytest.param(
+            'easydiffraction.datablocks.experiment.categories.background.line_segment',
+            'LineSegment',
+            'id',
+            'bg_1',
+            'background',
+            id='background_line_segment',
+        ),
+        pytest.param(
+            'easydiffraction.datablocks.experiment.categories.background.chebyshev',
+            'PolynomialTerm',
+            'id',
+            'poly_1',
+            'background',
+            id='background_chebyshev',
+        ),
+        pytest.param(
+            'easydiffraction.datablocks.experiment.categories.excluded_regions.default',
+            'ExcludedRegion',
+            'id',
+            'mask_1',
+            'excluded_regions',
+            id='excluded_region',
+        ),
+        pytest.param(
+            'easydiffraction.datablocks.experiment.categories.refln.bragg_sc',
+            'Refln',
+            'id',
+            'refl_1',
+            'refln',
+            id='refln',
+        ),
+        pytest.param(
+            'easydiffraction.datablocks.experiment.categories.data.bragg_pd',
+            'PdCwlDataPoint',
+            'point_id',
+            '1',
+            'pd_data',
+            id='pd_cwl_data',
+        ),
+        pytest.param(
+            'easydiffraction.datablocks.experiment.categories.data.bragg_pd',
+            'PdTofDataPoint',
+            'point_id',
+            '2',
+            'pd_data',
+            id='pd_tof_data',
+        ),
+        pytest.param(
+            'easydiffraction.datablocks.experiment.categories.data.total_pd',
+            'TotalDataPoint',
+            'point_id',
+            '3',
+            'total_data',
+            id='total_data',
+        ),
+    ],
+)
+def test_loop_items_resolve_declared_category_identity(
+    module_name,
+    class_name,
+    attr_name,
+    value,
+    category_code,
+):
+    module = importlib.import_module(module_name)
+    item_cls = getattr(module, class_name)
+    item = item_cls()
+
+    getattr(item, attr_name).value = value
+
+    assert item_cls._category_code == category_code
+    assert item_cls._category_entry_name == attr_name
+    assert item._identity.category_code == category_code
+    assert item._identity.category_entry_name == value
+
+
 def test_category_item_str_and_properties():
     it = SimpleItem()
     it.a = 'name1'
diff --git a/tests/unit/easydiffraction/core/test_category_owner.py b/tests/unit/easydiffraction/core/test_category_owner.py
new file mode 100644
index 000000000..264f01bb4
--- /dev/null
+++ b/tests/unit/easydiffraction/core/test_category_owner.py
@@ -0,0 +1,134 @@
+# SPDX-FileCopyrightText: 2026 EasyScience contributors 
+# SPDX-License-Identifier: BSD-3-Clause
+
+from __future__ import annotations
+
+from easydiffraction.core.category import CategoryItem
+from easydiffraction.core.category_owner import CategoryOwner
+from easydiffraction.core.validation import AttributeSpec
+from easydiffraction.core.variable import Parameter
+from easydiffraction.io.cif.handler import CifHandler
+from easydiffraction.io.cif.serialize import category_owner_to_cif
+
+
+class _FastCategory(CategoryItem):
+    _update_priority = 1
+
+    def __init__(self, update_calls: list[tuple[str, bool]] | None = None) -> None:
+        super().__init__()
+        self._identity.category_code = 'fast'
+        self._update_calls = update_calls
+        self._param = Parameter(
+            name='param',
+            description='Fast category parameter',
+            value_spec=AttributeSpec(default=0.0),
+            units='',
+            cif_handler=CifHandler(names=['_fast.param']),
+        )
+
+    @property
+    def param(self) -> Parameter:
+        return self._param
+
+    def _update(self, *, called_by_minimizer: bool = False) -> None:
+        if self._update_calls is not None:
+            self._update_calls.append(('fast', called_by_minimizer))
+
+
+class _SlowCategory(CategoryItem):
+    _update_priority = 20
+
+    def __init__(self, update_calls: list[tuple[str, bool]] | None = None) -> None:
+        super().__init__()
+        self._identity.category_code = 'slow'
+        self._update_calls = update_calls
+        self._param = Parameter(
+            name='param',
+            description='Slow category parameter',
+            value_spec=AttributeSpec(default=0.0),
+            units='',
+            cif_handler=CifHandler(names=['_slow.param']),
+        )
+
+    @property
+    def param(self) -> Parameter:
+        return self._param
+
+    def _update(self, *, called_by_minimizer: bool = False) -> None:
+        if self._update_calls is not None:
+            self._update_calls.append(('slow', called_by_minimizer))
+
+
+class _Owner(CategoryOwner):
+    def __init__(self, update_calls: list[tuple[str, bool]] | None = None) -> None:
+        super().__init__()
+        self._slow = _SlowCategory(update_calls)
+        self._fast = _FastCategory(update_calls)
+
+    @property
+    def fast(self) -> _FastCategory:
+        return self._fast
+
+    @property
+    def slow(self) -> _SlowCategory:
+        return self._slow
+
+    @property
+    def as_cif(self) -> str:
+        return category_owner_to_cif(self)
+
+
+class _OwnerWithSerializableSubset(_Owner):
+    def _serializable_categories(self) -> list:
+        return [self.slow]
+
+
+def test_category_owner_sorts_categories_and_flattens_parameters():
+    owner = _Owner()
+
+    assert owner.fast._parent is owner
+    assert owner.slow._parent is owner
+    assert [category._identity.category_code for category in owner.categories] == ['fast', 'slow']
+    assert [parameter.unique_name for parameter in owner.parameters] == [
+        'fast.param',
+        'slow.param',
+    ]
+
+
+def test_category_owner_updates_only_when_needed_and_can_force_minimizer_updates():
+    update_calls: list[tuple[str, bool]] = []
+    owner = _Owner(update_calls)
+
+    owner._update_categories()
+
+    assert update_calls == [('fast', False), ('slow', False)]
+    assert owner._need_categories_update is False
+
+    update_calls.clear()
+    owner._update_categories()
+    assert update_calls == []
+
+    owner._update_categories(called_by_minimizer=True)
+    assert update_calls == [('fast', True), ('slow', True)]
+    assert owner._need_categories_update is False
+
+
+def test_category_owner_descriptor_changes_mark_owner_dirty():
+    owner = _Owner()
+    owner._need_categories_update = False
+
+    owner.fast.param.value = 1.5
+    assert owner._need_categories_update is True
+
+    owner._need_categories_update = False
+    owner.slow.param._set_value_from_minimizer(2.5)
+    assert owner._need_categories_update is True
+
+
+def test_category_owner_as_cif_respects_serializable_categories_override():
+    owner = _OwnerWithSerializableSubset()
+
+    cif_text = owner.as_cif
+
+    assert '_slow.param' in cif_text
+    assert '_fast.param' not in cif_text
diff --git a/tests/unit/easydiffraction/core/test_datablock.py b/tests/unit/easydiffraction/core/test_datablock.py
index b41f68b74..98d0dc252 100644
--- a/tests/unit/easydiffraction/core/test_datablock.py
+++ b/tests/unit/easydiffraction/core/test_datablock.py
@@ -33,8 +33,8 @@ def __init__(self):
             # 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
+            # Make p2 user constrained and not free
+            self._p2._user_constrained = True
             self._p2._free = False
             # Mark p1 free to be included in free_parameters
             self._p1.free = True
@@ -67,7 +67,7 @@ def cat(self):
     # parameters collection aggregates from both blocks (p1 & p2 each)
     params = coll.parameters
     assert len(params) == 4
-    # fittable excludes constrained parameters
+    # fittable excludes user-constrained parameters
     fittable = coll.fittable_parameters
     assert all(isinstance(p, Parameter) for p in fittable)
     assert len(fittable) == 2  # only p1 from each block
@@ -76,6 +76,65 @@ def cat(self):
     assert free_params == fittable
 
 
+def test_datablock_collection_fittable_excludes_symmetry_constrained_parameters():
+    from easydiffraction.core.category import CategoryItem
+    from easydiffraction.core.datablock import DatablockCollection
+    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._free_param = Parameter(
+                name='free_param',
+                description='',
+                value_spec=AttributeSpec(default=0.0),
+                units='',
+                cif_handler=CifHandler(names=['_cat.free_param']),
+            )
+            self._fixed_param = Parameter(
+                name='fixed_param',
+                description='',
+                value_spec=AttributeSpec(default=0.0),
+                units='',
+                cif_handler=CifHandler(names=['_cat.fixed_param']),
+            )
+            self._free_param.value = 1.0
+            self._fixed_param.value = 2.0
+            self._free_param.free = True
+            self._fixed_param._set_symmetry_constrained(value=True)
+
+        @property
+        def free_param(self):
+            return self._free_param
+
+        @property
+        def fixed_param(self):
+            return self._fixed_param
+
+    class Block(DatablockItem):
+        def __init__(self, name):
+            super().__init__()
+            self._identity.datablock_entry_name = lambda: name
+            self._cat = Cat()
+
+        @property
+        def cat(self):
+            return self._cat
+
+    coll = DatablockCollection(item_type=Block)
+    coll.add(Block('A'))
+
+    fittable = coll.fittable_parameters
+
+    assert all(isinstance(p, Parameter) for p in fittable)
+    assert [p.name for p in fittable] == ['free_param']
+
+
 def test_datablock_item_help(capsys):
     from easydiffraction.core.category import CategoryItem
     from easydiffraction.core.datablock import DatablockItem
diff --git a/tests/unit/easydiffraction/core/test_parameters.py b/tests/unit/easydiffraction/core/test_parameters.py
index e7d102d9d..f81e35e87 100644
--- a/tests/unit/easydiffraction/core/test_parameters.py
+++ b/tests/unit/easydiffraction/core/test_parameters.py
@@ -103,6 +103,90 @@ def test_parameter_fit_bounds_assign_and_read():
     assert np.isclose(p.fit_max, 10.0)
 
 
+def test_parameter_set_fit_bounds_from_uncertainty_sets_bounds_and_returns_none():
+    from easydiffraction.core.validation import AttributeSpec
+    from easydiffraction.core.variable import Parameter
+    from easydiffraction.io.cif.handler import CifHandler
+
+    p = Parameter(
+        name='d',
+        value_spec=AttributeSpec(default=0.0),
+        cif_handler=CifHandler(names=['_param.d']),
+    )
+    p.value = 2.0
+    p.uncertainty = 0.25
+
+    result = p.set_fit_bounds_from_uncertainty(multiplier=4)
+
+    assert result is None
+    assert np.isclose(p.fit_min, 1.0)
+    assert np.isclose(p.fit_max, 3.0)
+
+
+def test_parameter_set_fit_bounds_from_uncertainty_uses_default_multiplier():
+    from easydiffraction.core.validation import AttributeSpec
+    from easydiffraction.core.variable import Parameter
+    from easydiffraction.io.cif.handler import CifHandler
+
+    p = Parameter(
+        name='default_multiplier',
+        value_spec=AttributeSpec(default=0.0),
+        cif_handler=CifHandler(names=['_param.default_multiplier']),
+    )
+    p.value = 2.0
+    p.uncertainty = 0.25
+
+    p.set_fit_bounds_from_uncertainty()
+
+    assert np.isclose(p.fit_min, 1.0)
+    assert np.isclose(p.fit_max, 3.0)
+
+
+def test_parameter_set_fit_bounds_from_uncertainty_clips_to_physical_limits():
+    from easydiffraction.core.validation import AttributeSpec
+    from easydiffraction.core.validation import DataTypes
+    from easydiffraction.core.validation import RangeValidator
+    from easydiffraction.core.variable import Parameter
+    from easydiffraction.io.cif.handler import CifHandler
+
+    p = Parameter(
+        name='bounded',
+        value_spec=AttributeSpec(
+            data_type=DataTypes.NUMERIC,
+            default=1.0,
+            validator=RangeValidator(ge=0.5, le=1.5),
+        ),
+        cif_handler=CifHandler(names=['_param.bounded']),
+    )
+    p.value = 1.0
+    p.uncertainty = 0.3
+
+    p.set_fit_bounds_from_uncertainty(multiplier=4)
+
+    assert np.isclose(p.fit_min, 0.5)
+    assert np.isclose(p.fit_max, 1.5)
+
+
+def test_parameter_set_fit_bounds_from_uncertainty_requires_valid_uncertainty():
+    from easydiffraction.core.validation import AttributeSpec
+    from easydiffraction.core.variable import Parameter
+    from easydiffraction.io.cif.handler import CifHandler
+
+    p = Parameter(
+        name='invalid',
+        value_spec=AttributeSpec(default=0.0),
+        cif_handler=CifHandler(names=['_param.invalid']),
+    )
+    p.value = 2.0
+    p.uncertainty = None
+
+    with pytest.raises(
+        ValueError,
+        match=r'Cannot set fit bounds for invalid: uncertainty is missing or invalid\.',
+    ):
+        p.set_fit_bounds_from_uncertainty(multiplier=4)
+
+
 def _make_param() -> object:
     from easydiffraction.core.validation import AttributeSpec
     from easydiffraction.core.variable import Parameter
@@ -115,43 +199,43 @@ def _make_param() -> object:
     )
 
 
-def test_parameter_symmetry_fixed_default_is_false():
+def test_parameter_symmetry_constrained_default_is_false():
     p = _make_param()
-    assert p.symmetry_fixed is False
+    assert p.symmetry_constrained is False
 
 
-def test_parameter_set_symmetry_fixed_forces_free_false():
+def test_parameter_set_symmetry_constrained_forces_free_false():
     p = _make_param()
     p.free = True
     assert p.free is True
-    p._set_symmetry_fixed(value=True)
-    assert p.symmetry_fixed is True
+    p._set_symmetry_constrained(value=True)
+    assert p.symmetry_constrained is True
     assert p.free is False
 
 
-def test_parameter_free_true_ignored_when_symmetry_fixed(monkeypatch):
+def test_parameter_free_true_ignored_when_symmetry_constrained(monkeypatch):
     from easydiffraction.utils.logging import Logger
 
     p = _make_param()
-    p._set_symmetry_fixed(value=True)
+    p._set_symmetry_constrained(value=True)
     monkeypatch.setattr(Logger, '_reaction', Logger.Reaction.WARN, raising=True)
     p.free = True
     assert p.free is False
-    assert p.symmetry_fixed is True
+    assert p.symmetry_constrained is True
     monkeypatch.setattr(Logger, '_reaction', Logger.Reaction.RAISE, raising=True)
 
 
-def test_parameter_free_false_allowed_when_symmetry_fixed():
+def test_parameter_free_false_allowed_when_symmetry_constrained():
     p = _make_param()
-    p._set_symmetry_fixed(value=True)
+    p._set_symmetry_constrained(value=True)
     p.free = False  # should not warn or raise
     assert p.free is False
 
 
-def test_parameter_clearing_symmetry_fixed_allows_free_true():
+def test_parameter_clearing_symmetry_constrained_allows_free_true():
     p = _make_param()
-    p._set_symmetry_fixed(value=True)
-    p._set_symmetry_fixed(value=False)
+    p._set_symmetry_constrained(value=True)
+    p._set_symmetry_constrained(value=False)
     p.free = True
     assert p.free is True
-    assert p.symmetry_fixed is False
+    assert p.symmetry_constrained is False
diff --git a/tests/unit/easydiffraction/crystallography/test_crystallography_coverage.py b/tests/unit/easydiffraction/crystallography/test_crystallography_coverage.py
index 13590dbea..7b548aca0 100644
--- a/tests/unit/easydiffraction/crystallography/test_crystallography_coverage.py
+++ b/tests/unit/easydiffraction/crystallography/test_crystallography_coverage.py
@@ -91,25 +91,29 @@ def test_triclinic(self):
         assert result['angle_beta'] == 85.0
         assert result['angle_gamma'] == 75.0
 
-    def test_invalid_name_hm_returns_cell_unchanged(self):
+    def test_invalid_name_hm_returns_cell_unchanged(self, monkeypatch):
+        from easydiffraction.utils.logging import Logger
+
         cell = _make_cell()
         original = dict(cell)
+        monkeypatch.setattr(Logger, '_reaction', Logger.Reaction.WARN, raising=True)
         result = apply_cell_symmetry_constraints(cell, 'NOT A REAL SG')
         assert result == original
+        monkeypatch.setattr(Logger, '_reaction', Logger.Reaction.RAISE, raising=True)
 
 
 # ------------------------------------------------------------------
-# cell_symmetry_fixed_flags
+# cell_symmetry_constrained_flags
 # ------------------------------------------------------------------
 
 
-class TestCellSymmetryFixedFlags:
+class TestCellSymmetryConstrainedFlags:
     def test_cubic_only_a_is_free(self):
         from easydiffraction.crystallography.crystallography import (
-            cell_symmetry_fixed_flags,
+            cell_symmetry_constrained_flags,
         )
 
-        flags = cell_symmetry_fixed_flags('F m -3 m')
+        flags = cell_symmetry_constrained_flags('F m -3 m')
         assert flags == {
             'lattice_a': False,
             'lattice_b': True,
@@ -121,10 +125,10 @@ def test_cubic_only_a_is_free(self):
 
     def test_monoclinic_b_and_beta_free(self):
         from easydiffraction.crystallography.crystallography import (
-            cell_symmetry_fixed_flags,
+            cell_symmetry_constrained_flags,
         )
 
-        flags = cell_symmetry_fixed_flags('P 21/c')
+        flags = cell_symmetry_constrained_flags('P 21/c')
         assert flags['lattice_a'] is False
         assert flags['lattice_b'] is False
         assert flags['lattice_c'] is False
@@ -134,19 +138,19 @@ def test_monoclinic_b_and_beta_free(self):
 
     def test_triclinic_all_free(self):
         from easydiffraction.crystallography.crystallography import (
-            cell_symmetry_fixed_flags,
+            cell_symmetry_constrained_flags,
         )
 
-        flags = cell_symmetry_fixed_flags('P 1')
+        flags = cell_symmetry_constrained_flags('P 1')
         assert all(v is False for v in flags.values())
 
     def test_invalid_returns_all_false(self, monkeypatch):
         from easydiffraction.crystallography.crystallography import (
-            cell_symmetry_fixed_flags,
+            cell_symmetry_constrained_flags,
         )
         from easydiffraction.utils.logging import Logger
 
         monkeypatch.setattr(Logger, '_reaction', Logger.Reaction.WARN, raising=True)
-        flags = cell_symmetry_fixed_flags('NOT A REAL SG')
+        flags = cell_symmetry_constrained_flags('NOT A REAL SG')
         assert all(v is False for v in flags.values())
         monkeypatch.setattr(Logger, '_reaction', Logger.Reaction.RAISE, raising=True)
diff --git a/tests/unit/easydiffraction/crystallography/test_crystallography_wyckoff.py b/tests/unit/easydiffraction/crystallography/test_crystallography_wyckoff.py
index 952f3004c..377ed7f2f 100644
--- a/tests/unit/easydiffraction/crystallography/test_crystallography_wyckoff.py
+++ b/tests/unit/easydiffraction/crystallography/test_crystallography_wyckoff.py
@@ -55,32 +55,32 @@ def test_valid_applies_constraints(self):
         assert result is not None
 
 
-class TestAtomSiteSymmetryFixedFlags:
+class TestAtomSiteSymmetryConstrainedFlags:
     def test_special_position_all_fixed(self):
         from easydiffraction.crystallography.crystallography import (
-            atom_site_symmetry_fixed_flags,
+            atom_site_symmetry_constrained_flags,
         )
 
         # P m -3 m (IT 221), Wyckoff 'a' = (0,0,0): all three axes fixed
-        flags = atom_site_symmetry_fixed_flags('P m -3 m', '1', 'a')
+        flags = atom_site_symmetry_constrained_flags('P m -3 m', '1', 'a')
         assert flags == {'fract_x': True, 'fract_y': True, 'fract_z': True}
 
     def test_general_position_all_free(self):
         from easydiffraction.crystallography.crystallography import (
-            atom_site_symmetry_fixed_flags,
+            atom_site_symmetry_constrained_flags,
         )
 
         # P 1 (IT 1), Wyckoff 'a' is the general position
-        flags = atom_site_symmetry_fixed_flags('P 1', '1', 'a')
+        flags = atom_site_symmetry_constrained_flags('P 1', '1', 'a')
         assert flags == {'fract_x': False, 'fract_y': False, 'fract_z': False}
 
     def test_invalid_returns_all_false(self, monkeypatch):
         from easydiffraction.crystallography.crystallography import (
-            atom_site_symmetry_fixed_flags,
+            atom_site_symmetry_constrained_flags,
         )
         from easydiffraction.utils.logging import Logger
 
         monkeypatch.setattr(Logger, '_reaction', Logger.Reaction.WARN, raising=True)
-        flags = atom_site_symmetry_fixed_flags('NOT REAL', None, 'a')
+        flags = atom_site_symmetry_constrained_flags('NOT REAL', None, 'a')
         assert flags == {'fract_x': False, 'fract_y': False, 'fract_z': False}
         monkeypatch.setattr(Logger, '_reaction', Logger.Reaction.RAISE, raising=True)
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
index 1a3f233cb..c50a095e5 100644
--- a/tests/unit/easydiffraction/datablocks/experiment/categories/data/test_bragg_pd.py
+++ b/tests/unit/easydiffraction/datablocks/experiment/categories/data/test_bragg_pd.py
@@ -4,6 +4,18 @@
 import numpy as np
 
 
+def _experiment_stub(name='exp1'):
+    from easydiffraction.core.identity import Identity
+
+    class ExperimentStub:
+        def __init__(self):
+            self._parent = None
+            self._identity = Identity(owner=self)
+            self._identity.datablock_entry_name = lambda: name
+
+    return ExperimentStub()
+
+
 def test_pd_cwl_data_point_defaults():
     from easydiffraction.datablocks.experiment.categories.data.bragg_pd import PdCwlDataPoint
 
@@ -95,6 +107,19 @@ def test_pd_tof_data_collection_create_and_properties():
     assert coll._items[2].point_id.value == '3'
 
 
+def test_pd_data_items_resolve_experiment_datablock_name():
+    from easydiffraction.datablocks.experiment.categories.data.bragg_pd import PdCwlData
+
+    coll = PdCwlData()
+    coll._parent = _experiment_stub('hrpt')
+
+    coll._create_items_set_xcoord_and_id(np.array([10.0, 20.0]))
+
+    param = coll._items[0].intensity_meas
+    assert param._identity.datablock_entry_name == 'hrpt'
+    assert param.unique_name == 'hrpt.pd_data.1.intensity_meas'
+
+
 def test_pd_data_calc_status_exclusion():
     from easydiffraction.datablocks.experiment.categories.data.bragg_pd import PdCwlData
 
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
index 41e638e58..3198158e2 100644
--- a/tests/unit/easydiffraction/datablocks/experiment/categories/data/test_total_pd.py
+++ b/tests/unit/easydiffraction/datablocks/experiment/categories/data/test_total_pd.py
@@ -4,6 +4,18 @@
 import numpy as np
 
 
+def _experiment_stub(name='exp1'):
+    from easydiffraction.core.identity import Identity
+
+    class ExperimentStub:
+        def __init__(self):
+            self._parent = None
+            self._identity = Identity(owner=self)
+            self._identity.datablock_entry_name = lambda: name
+
+    return ExperimentStub()
+
+
 def test_total_data_point_defaults():
     from easydiffraction.datablocks.experiment.categories.data.total_pd import TotalDataPoint
 
@@ -89,3 +101,16 @@ def test_total_data_type_info():
 
     assert TotalData.type_info.tag == 'total-pd'
     assert TotalData.type_info.description == 'Total scattering (PDF) data'
+
+
+def test_total_data_items_resolve_experiment_datablock_name():
+    from easydiffraction.datablocks.experiment.categories.data.total_pd import TotalData
+
+    coll = TotalData()
+    coll._parent = _experiment_stub('pdf-exp')
+
+    coll._create_items_set_xcoord_and_id(np.array([1.0, 2.0]))
+
+    param = coll._items[0].g_r_meas
+    assert param._identity.datablock_entry_name == 'pdf-exp'
+    assert param.unique_name == 'pdf-exp.total_data.1.g_r_meas'
diff --git a/tests/unit/easydiffraction/datablocks/experiment/categories/refln/test_bragg_sc.py b/tests/unit/easydiffraction/datablocks/experiment/categories/refln/test_bragg_sc.py
index fc7175d4a..ffae191b7 100644
--- a/tests/unit/easydiffraction/datablocks/experiment/categories/refln/test_bragg_sc.py
+++ b/tests/unit/easydiffraction/datablocks/experiment/categories/refln/test_bragg_sc.py
@@ -4,6 +4,18 @@
 import numpy as np
 
 
+def _experiment_stub(name='exp1'):
+    from easydiffraction.core.identity import Identity
+
+    class ExperimentStub:
+        def __init__(self):
+            self._parent = None
+            self._identity = Identity(owner=self)
+            self._identity.datablock_entry_name = lambda: name
+
+    return ExperimentStub()
+
+
 def test_refln_data_point_defaults():
     from easydiffraction.datablocks.experiment.categories.refln.bragg_sc import Refln
 
@@ -74,6 +86,23 @@ def test_refln_data_d_spacing_and_stol():
     np.testing.assert_array_almost_equal(coll.sin_theta_over_lambda, stol)
 
 
+def test_refln_items_resolve_experiment_datablock_name():
+    from easydiffraction.datablocks.experiment.categories.refln.bragg_sc import ReflnData
+
+    coll = ReflnData()
+    coll._parent = _experiment_stub('sc-exp')
+
+    coll._create_items_set_hkl_and_id(
+        np.array([1.0, 2.0]),
+        np.array([0.0, 0.0]),
+        np.array([0.0, 1.0]),
+    )
+
+    param = coll._items[0].intensity_meas
+    assert param._identity.datablock_entry_name == 'sc-exp'
+    assert param.unique_name == 'sc-exp.refln.1.intensity_meas'
+
+
 def test_refln_data_type_info():
     from easydiffraction.datablocks.experiment.categories.refln.bragg_sc import ReflnData
 
diff --git a/tests/unit/easydiffraction/display/plotters/test_ascii.py b/tests/unit/easydiffraction/display/plotters/test_ascii.py
index 49dd76537..54e533ea0 100644
--- a/tests/unit/easydiffraction/display/plotters/test_ascii.py
+++ b/tests/unit/easydiffraction/display/plotters/test_ascii.py
@@ -1,6 +1,8 @@
 # SPDX-FileCopyrightText: 2026 EasyScience contributors 
 # SPDX-License-Identifier: BSD-3-Clause
 
+import os
+
 import numpy as np
 
 
@@ -23,6 +25,52 @@ def test_ascii_plotter_plot_minimal(capsys):
     assert 'Displaying data for selected x-range' in out
 
 
+def test_ascii_plotter_plot_supports_max_posterior_legend(capsys):
+    from easydiffraction.display.plotters.ascii import AsciiPlotter
+
+    x = np.array([0.0, 1.0, 2.0])
+    y_meas = np.array([1.0, 2.0, 3.0])
+    y_map = np.array([0.5, 1.5, 2.5])
+    plotter = AsciiPlotter()
+
+    plotter.plot_powder(
+        x=x,
+        y_series=[y_meas, y_map],
+        labels=['meas', 'posterior'],
+        axes_labels=['x', 'y'],
+        title='Posterior predictive',
+        height=5,
+    )
+
+    out = capsys.readouterr().out
+    assert 'Measured (Imeas)' in out
+    assert 'Best posterior sample' in out
+
+
+def test_ascii_plotter_plot_single_crystal_uses_detected_terminal_width(monkeypatch, capsys):
+    from easydiffraction.display.plotters import ascii as ascii_mod
+    from easydiffraction.display.plotters.ascii import AsciiPlotter
+
+    monkeypatch.setattr(
+        ascii_mod.shutil,
+        'get_terminal_size',
+        lambda fallback: os.terminal_size((44, 24)),
+    )
+
+    p = AsciiPlotter()
+    p.plot_single_crystal(
+        x_calc=np.array([1.0, 2.0, 3.0]),
+        y_meas=np.array([1.1, 1.9, 3.2]),
+        y_meas_su=np.array([0.1, 0.1, 0.1]),
+        axes_labels=['F²calc', 'F²meas'],
+        title='SC width test',
+        height=6,
+    )
+
+    out = capsys.readouterr().out
+    assert f'└{"─" * 26}' in out
+
+
 def test_ascii_plotter_plot_single_crystal(capsys):
     from easydiffraction.display.plotters.ascii import AsciiPlotter
 
@@ -85,3 +133,176 @@ def test_ascii_plotter_plot_powder_meas_vs_calc_announces_plotly_only_bragg_row(
     assert 'Legend:' in out
     assert 'Residual (Imeas - Icalc)' in out
     assert 'Bragg peak subplot rows are available with the Plotly engine only.' in out
+
+
+def test_ascii_plotter_plot_limits_oversized_series_to_detected_terminal_width(monkeypatch):
+    from easydiffraction.display.plotters import ascii as ascii_mod
+    from easydiffraction.display.plotters.ascii import ASCII_CHART_LEFT_PADDING
+    from easydiffraction.display.plotters.ascii import ASCII_CHART_MIN_POINT_COUNT
+    from easydiffraction.display.plotters.ascii import ASCII_CHART_OFFSET
+    from easydiffraction.display.plotters.ascii import AsciiPlotter
+
+    captured: dict[str, object] = {}
+
+    def fake_plot(series, config):
+        captured['call'] = (series, config)
+        return 'chart'
+
+    monkeypatch.setattr(
+        ascii_mod.shutil,
+        'get_terminal_size',
+        lambda fallback: os.terminal_size((44, 24)),
+    )
+    monkeypatch.setattr(ascii_mod.asciichartpy, 'plot', fake_plot)
+
+    AsciiPlotter().plot_powder(
+        x=np.arange(256, dtype=float),
+        y_series=[np.linspace(0.0, 1.0, 256)],
+        labels=['density'],
+        axes_labels=['x', 'y'],
+        title='Width test',
+        height=5,
+    )
+
+    series, config = captured['call']
+    assert len(series[0]) == max(
+        ASCII_CHART_MIN_POINT_COUNT,
+        44 - ASCII_CHART_OFFSET - ASCII_CHART_LEFT_PADDING,
+    )
+    assert series[0][0] > 0.0
+    assert config['offset'] == ASCII_CHART_OFFSET
+
+
+def test_ascii_plotter_plot_interpolates_smaller_series_to_detected_terminal_width(monkeypatch):
+    from easydiffraction.display.plotters import ascii as ascii_mod
+    from easydiffraction.display.plotters.ascii import ASCII_CHART_LEFT_PADDING
+    from easydiffraction.display.plotters.ascii import ASCII_CHART_MIN_POINT_COUNT
+    from easydiffraction.display.plotters.ascii import ASCII_CHART_OFFSET
+    from easydiffraction.display.plotters.ascii import AsciiPlotter
+
+    captured: dict[str, object] = {}
+
+    def fake_plot(series, config):
+        captured['call'] = (series, config)
+        return 'chart'
+
+    monkeypatch.setattr(
+        ascii_mod.shutil,
+        'get_terminal_size',
+        lambda fallback: os.terminal_size((44, 24)),
+    )
+    monkeypatch.setattr(ascii_mod.asciichartpy, 'plot', fake_plot)
+
+    AsciiPlotter().plot_powder(
+        x=np.arange(4, dtype=float),
+        y_series=[np.array([0.0, 1.0, 0.0, 1.0])],
+        labels=['density'],
+        axes_labels=['x', 'y'],
+        title='Interpolation test',
+        height=5,
+    )
+
+    series, config = captured['call']
+    assert len(series[0]) == max(
+        ASCII_CHART_MIN_POINT_COUNT,
+        44 - ASCII_CHART_OFFSET - ASCII_CHART_LEFT_PADDING,
+    )
+    assert series[0][0] == 0.0
+    assert series[0][-1] == 1.0
+    assert config['offset'] == ASCII_CHART_OFFSET
+
+
+def test_ascii_plotter_plot_uses_fallback_width_when_terminal_size_unavailable(monkeypatch):
+    from easydiffraction.display.plotters import ascii as ascii_mod
+    from easydiffraction.display.plotters.ascii import ASCII_CHART_FALLBACK_POINT_COUNT
+    from easydiffraction.display.plotters.ascii import ASCII_CHART_OFFSET
+    from easydiffraction.display.plotters.ascii import AsciiPlotter
+
+    captured: dict[str, object] = {}
+
+    def fake_plot(series, config):
+        captured['call'] = (series, config)
+        return 'chart'
+
+    monkeypatch.setattr(
+        ascii_mod.shutil,
+        'get_terminal_size',
+        lambda fallback: os.terminal_size(fallback),
+    )
+    monkeypatch.setattr(ascii_mod.asciichartpy, 'plot', fake_plot)
+
+    AsciiPlotter().plot_powder(
+        x=np.arange(256, dtype=float),
+        y_series=[np.linspace(0.0, 1.0, 256)],
+        labels=['density'],
+        axes_labels=['x', 'y'],
+        title='Fallback width test',
+        height=5,
+    )
+
+    series, config = captured['call']
+    assert len(series[0]) == ASCII_CHART_FALLBACK_POINT_COUNT
+    assert config['offset'] == ASCII_CHART_OFFSET
+
+
+def test_ascii_plotter_plot_scatter_uses_detected_terminal_width(monkeypatch):
+    from easydiffraction.display.plotters import ascii as ascii_mod
+    from easydiffraction.display.plotters.ascii import ASCII_CHART_LEFT_PADDING
+    from easydiffraction.display.plotters.ascii import ASCII_CHART_MIN_POINT_COUNT
+    from easydiffraction.display.plotters.ascii import ASCII_CHART_OFFSET
+    from easydiffraction.display.plotters.ascii import AsciiPlotter
+
+    captured: dict[str, object] = {}
+
+    def fake_plot(series, config):
+        captured['call'] = (series, config)
+        return 'chart'
+
+    monkeypatch.setattr(
+        ascii_mod.shutil,
+        'get_terminal_size',
+        lambda fallback: os.terminal_size((44, 24)),
+    )
+    monkeypatch.setattr(ascii_mod.asciichartpy, 'plot', fake_plot)
+
+    AsciiPlotter().plot_scatter(
+        x=np.arange(4, dtype=float),
+        y=np.array([0.0, 1.0, 0.0, 1.0]),
+        sy=np.array([0.1, 0.1, 0.1, 0.1]),
+        axes_labels=['x', 'y'],
+        title='Scatter width test',
+        height=5,
+    )
+
+    series, config = captured['call']
+    assert len(series[0]) == max(
+        ASCII_CHART_MIN_POINT_COUNT,
+        44 - ASCII_CHART_OFFSET - ASCII_CHART_LEFT_PADDING,
+    )
+    assert config['colors'] == [ascii_mod.asciichartpy.blue]
+
+
+def test_ascii_plotter_plot_scatter_sorts_by_x_before_resampling(monkeypatch):
+    from easydiffraction.display.plotters import ascii as ascii_mod
+    from easydiffraction.display.plotters.ascii import AsciiPlotter
+
+    captured: dict[str, object] = {}
+
+    def fake_plot(series, config):
+        captured['call'] = (series, config)
+        return 'chart'
+
+    monkeypatch.setattr(AsciiPlotter, '_chart_point_count', lambda: 4)
+    monkeypatch.setattr(ascii_mod.asciichartpy, 'plot', fake_plot)
+
+    AsciiPlotter().plot_scatter(
+        x=np.array([400.0, 300.0, 200.0, 100.0]),
+        y=np.array([4.0, 3.0, 2.0, 1.0]),
+        sy=np.array([0.1, 0.1, 0.1, 0.1]),
+        axes_labels=['Temperature', 'Parameter value'],
+        title='Scatter order test',
+        height=5,
+    )
+
+    series, _config = captured['call']
+    assert series[0] == [1.0, 2.0, 3.0, 4.0]
diff --git a/tests/unit/easydiffraction/display/plotters/test_plotly.py b/tests/unit/easydiffraction/display/plotters/test_plotly.py
index 3905ee278..097d4321f 100644
--- a/tests/unit/easydiffraction/display/plotters/test_plotly.py
+++ b/tests/unit/easydiffraction/display/plotters/test_plotly.py
@@ -13,62 +13,19 @@ def test_module_import():
     assert expected_module_name == actual_module_name
 
 
-def test_default_template_name_prefers_jupyter_theme(monkeypatch):
+def test_get_layout_sets_title_and_axis_title_font_sizes():
     import easydiffraction.display.plotters.plotly as pp
 
-    monkeypatch.setattr(pp, 'in_jupyter', lambda: True)
-    monkeypatch.setattr(pp, 'is_dark', lambda: True)
-    monkeypatch.setattr(pp.darkdetect, 'isDark', lambda: False)
+    layout = pp.PlotlyPlotter._get_layout('Title', ['x axis', 'y axis'])
 
-    assert pp.PlotlyPlotter._default_template_name() == 'plotly_dark'
-
-
-def test_correlation_colorscale_uses_black_center_in_dark_mode(monkeypatch):
-    import easydiffraction.display.plotters.plotly as pp
-
-    monkeypatch.setattr(pp.PlotlyPlotter, '_is_dark_mode', staticmethod(lambda: True))
-
-    assert pp.PlotlyPlotter._correlation_colorscale()[1] == (0.5, '#000000')
-
-
-def test_default_template_name_uses_system_theme_outside_jupyter(monkeypatch):
-    import easydiffraction.display.plotters.plotly as pp
-
-    monkeypatch.setattr(pp, 'in_jupyter', lambda: False)
-    monkeypatch.setattr(pp, 'is_dark', lambda: False)
-    monkeypatch.setattr(pp.darkdetect, 'isDark', lambda: False)
-
-    assert pp.PlotlyPlotter._default_template_name() == 'plotly_white'
-
-
-def test_correlation_colorscale_uses_white_center_in_light_mode(monkeypatch):
-    import easydiffraction.display.plotters.plotly as pp
-
-    monkeypatch.setattr(pp.PlotlyPlotter, '_is_dark_mode', staticmethod(lambda: False))
-
-    assert pp.PlotlyPlotter._correlation_colorscale()[1] == (0.5, '#f7f7f7')
-
-
-def test_legend_background_color_uses_light_overlay_in_light_mode(monkeypatch):
-    import easydiffraction.display.plotters.plotly as pp
-
-    monkeypatch.setattr(pp.PlotlyPlotter, '_is_dark_mode', staticmethod(lambda: False))
-
-    assert pp.PlotlyPlotter._legend_background_color() == 'rgba(255, 255, 255, 0.5)'
-
-
-def test_legend_background_color_uses_dark_overlay_in_dark_mode(monkeypatch):
-    import easydiffraction.display.plotters.plotly as pp
-
-    monkeypatch.setattr(pp.PlotlyPlotter, '_is_dark_mode', staticmethod(lambda: True))
-
-    assert pp.PlotlyPlotter._legend_background_color() == 'rgba(0, 0, 0, 0.5)'
+    assert layout.title.font.size == pp.TITLE_FONT_SIZE
+    assert layout.xaxis.title.font.size == pp.AXIS_TITLE_FONT_SIZE
+    assert layout.yaxis.title.font.size == pp.AXIS_TITLE_FONT_SIZE
 
 
 def test_get_trace_and_plot(monkeypatch):
     import easydiffraction.display.plotters.plotly as pp
 
-    # Arrange: force non-PyCharm branch and stub fig.show/HTML/display so nothing opens
     monkeypatch.setattr(pp, 'in_pycharm', lambda: False)
 
     shown = {'count': 0}
@@ -83,7 +40,6 @@ def update_yaxes(self, **kwargs):
         def show(self, **kwargs):
             shown['count'] += 1
 
-    # Patch go.Scatter and go.Figure to minimal dummies
     class DummyScatter:
         def __init__(self, **kwargs):
             self.kwargs = kwargs
@@ -122,7 +78,6 @@ def __init__(self, html):
 
     plotter = pp.PlotlyPlotter()
 
-    # Exercise _get_powder_trace
     x = [0, 1, 2]
     y = [1, 2, 3]
     trace = plotter._get_powder_trace(x, y, label='calc')
@@ -278,6 +233,57 @@ def __init__(self, html):
     assert captured['displayed_html'] == '
plot
' +def test_show_figure_wraps_fixed_aspect_html(monkeypatch): + import easydiffraction.display.plotters.plotly as pp + + monkeypatch.setattr(pp, 'in_pycharm', lambda: False) + + captured = {} + + class DummyLayout: + def __init__(self): + self.meta = { + 'fixed_aspect_wrapper': { + 'aspect_ratio': '1 / 1', + } + } + self.showlegend = False + + class DummyFig: + def __init__(self): + self.data = [] + self.layout = DummyLayout() + + def show(self, **kwargs): + captured['show_called'] = True + + class DummyPIO: + @staticmethod + def to_html(fig, include_plotlyjs=None, full_html=None, config=None, post_script=None): + captured['post_script'] = post_script + return '
plot
' + + def dummy_display(obj): + captured['displayed_html'] = obj.html + + class DummyHTML: + def __init__(self, html): + self.html = html + + monkeypatch.setattr(pp, 'pio', DummyPIO) + monkeypatch.setattr(pp, 'display', dummy_display) + monkeypatch.setattr(pp, 'HTML', DummyHTML) + + plotter = pp.PlotlyPlotter() + plotter._show_figure(DummyFig()) + + assert captured.get('show_called') is not True + assert captured['post_script'] is None + assert 'aspect-ratio: 1 / 1;' in captured['displayed_html'] + assert 'ed-fixed-aspect-plotly-wrapper' in captured['displayed_html'] + assert '
plot
' in captured['displayed_html'] + + def test_plotly_single_crystal_trace_and_plot(monkeypatch): import easydiffraction.display.plotters.plotly as pp @@ -779,6 +785,9 @@ def fake_show_figure(self, fig): assert fig.layout.xaxis2.matches == 'x' assert fig.layout.yaxis2.title.text is None assert fig.layout.xaxis2.title.text == '2θ (degree)' + assert fig.layout.title.font.size == pp.TITLE_FONT_SIZE + assert fig.layout.yaxis.title.font.size == pp.AXIS_TITLE_FONT_SIZE + assert fig.layout.xaxis2.title.font.size == pp.AXIS_TITLE_FONT_SIZE assert [trace.name for trace in fig.data] == [ 'Measured (Imeas)', 'Total calculated (Icalc)', @@ -786,6 +795,53 @@ def fake_show_figure(self, fig): ] +def test_plot_powder_meas_vs_calc_styles_predictive_max_posterior_and_band(monkeypatch): + import easydiffraction.display.plotters.plotly as pp + + from easydiffraction.display.plotters.base import PowderMeasVsCalcSpec + + captured = {} + + def fake_show_figure(self, fig): + captured['fig'] = fig + + monkeypatch.setattr(pp.PlotlyPlotter, '_show_figure', fake_show_figure) + + plotter = pp.PlotlyPlotter() + plotter.plot_powder_meas_vs_calc( + plot_spec=PowderMeasVsCalcSpec( + x=np.array([1.0, 2.0, 3.0]), + y_meas=np.array([10.0, 12.0, 11.0]), + y_calc=np.array([9.0, 11.0, 10.5]), + y_resid=np.array([1.0, 1.0, 0.5]), + bragg_tick_sets=(), + axes_labels=['2θ (degree)', 'Intensity (arb. units)'], + title='Powder', + residual_height_fraction=0.25, + bragg_peaks_height_fraction=0.15, + height=None, + predictive_lower_95=np.array([8.0, 9.0, 10.0]), + predictive_upper_95=np.array([10.0, 11.0, 12.0]), + y_calc_name='Best posterior sample', + y_calc_line_dash='dot', + ), + ) + + fig = captured['fig'] + predictive_band_trace = next( + trace for trace in fig.data if trace.name == '95% credible interval' + ) + max_posterior_trace = next( + trace for trace in fig.data if trace.name == 'Best posterior sample' + ) + residual_trace = next(trace for trace in fig.data if trace.name == 'Residual (Imeas - Icalc)') + + assert predictive_band_trace.fillcolor == pp.PREDICTIVE_BAND_COLOR + assert predictive_band_trace.legendrank == 35 + assert max_posterior_trace.line.dash == 'dot' + assert predictive_band_trace.legendrank < residual_trace.legendrank + + def test_plot_powder_meas_vs_calc_keeps_exact_residual_scale_match(monkeypatch): import easydiffraction.display.plotters.plotly as pp diff --git a/tests/unit/easydiffraction/display/tablers/test_pandas.py b/tests/unit/easydiffraction/display/tablers/test_pandas.py index 2a2828a9c..e4a77c5b6 100644 --- a/tests/unit/easydiffraction/display/tablers/test_pandas.py +++ b/tests/unit/easydiffraction/display/tablers/test_pandas.py @@ -33,3 +33,15 @@ def test_apply_styling_returns_styler(self): df = pd.DataFrame({'A': [1.0], 'B': [2.0]}) styler = backend._apply_styling(df, ['left', 'right'], '#aabbcc') assert hasattr(styler, 'to_html') + + def test_build_renderable_returns_html(self): + from easydiffraction.display.tablers.pandas import PandasTableBackend + + pytest.importorskip('jinja2') + backend = PandasTableBackend() + df = pd.DataFrame({'A': [1.0], 'B': [2.0]}) + + html = backend.build_renderable(['left', 'right'], df) + + assert isinstance(html, str) + assert ' # SPDX-License-Identifier: BSD-3-Clause +import csv import re +from types import MethodType +from types import SimpleNamespace + +import numpy as np import pytest @@ -62,6 +67,78 @@ def test_plotter_factory_supported_and_unsupported(): PlotterFactory.create('nope') +@pytest.mark.parametrize( + ('descriptor_name', 'column_values', 'expected_y'), + [ + ('reduced_chi_square', ['1.5', '2.5'], [1.5, 2.5]), + ('iterations', ['5', '8'], [5.0, 8.0]), + ('success', ['True', 'False'], [1.0, 0.0]), + ], +) +def test_plot_param_series_reads_fit_result_columns_from_csv( + monkeypatch, + tmp_path, + descriptor_name, + column_values, + expected_y, +): + from easydiffraction.display.plotting import Plotter + from easydiffraction.project.project import Project + + project = Project(name='series') + project.info.path = tmp_path + + analysis_dir = tmp_path / 'analysis' + analysis_dir.mkdir(parents=True) + csv_path = analysis_dir / 'results.csv' + with csv_path.open('w', newline='', encoding='utf-8') as handle: + writer = csv.DictWriter( + handle, + fieldnames=[ + 'file_path', + 'fit_result.reduced_chi_square', + 'fit_result.success', + 'fit_result.iterations', + ], + ) + writer.writeheader() + writer.writerow({ + 'file_path': 'a.dat', + 'fit_result.reduced_chi_square': column_values[0], + 'fit_result.success': column_values[0] if descriptor_name == 'success' else 'True', + 'fit_result.iterations': column_values[0] if descriptor_name == 'iterations' else '5', + }) + writer.writerow({ + 'file_path': 'b.dat', + 'fit_result.reduced_chi_square': column_values[1], + 'fit_result.success': column_values[1] if descriptor_name == 'success' else 'False', + 'fit_result.iterations': column_values[1] if descriptor_name == 'iterations' else '8', + }) + + captured: dict[str, object] = {} + + plotter = Plotter() + plotter._set_project(project) + + def fake_plot_scatter(*, x, y, sy, axes_labels, title, height): + captured['x'] = x + captured['y'] = y + captured['sy'] = sy + captured['axes_labels'] = axes_labels + captured['title'] = title + captured['height'] = height + + monkeypatch.setattr(plotter._backend, 'plot_scatter', fake_plot_scatter) + + plotter.plot_param_series(getattr(project.analysis.fit_result, descriptor_name)) + + assert captured['x'] == [1, 2] + assert captured['y'] == expected_y + assert captured['sy'] == [0.0, 0.0] + assert captured['axes_labels'] == ['Experiment No.', 'Parameter value'] + assert captured['title'] == f"Parameter 'fit_result.{descriptor_name}' across fit results" + + def test_plotter_error_paths_and_filtering(capsys, monkeypatch): from easydiffraction.datablocks.experiment.item.enums import BeamModeEnum from easydiffraction.datablocks.experiment.item.enums import SampleFormEnum @@ -90,19 +167,43 @@ def __init__(self): p = Plotter() # Error paths (now log errors via console; messages are printed) - p._plot_meas_data(Ptn(two_theta=None, intensity_meas=None), 'E', ExptType()) + p._plot_meas_data( + object(), + Ptn(two_theta=None, intensity_meas=None), + 'E', + ExptType(), + _MeasVsCalcPlotOptions(), + ) out = capsys.readouterr().out assert 'No two_theta data available for experiment E' in out - p._plot_meas_data(Ptn(two_theta=[1], intensity_meas=None), 'E', ExptType()) + p._plot_meas_data( + object(), + Ptn(two_theta=[1], intensity_meas=None), + 'E', + ExptType(), + _MeasVsCalcPlotOptions(), + ) out = capsys.readouterr().out assert 'No measured data available for experiment E' in out - p._plot_calc_data(Ptn(two_theta=None, intensity_calc=None), 'E', ExptType()) + p._plot_calc_data( + object(), + Ptn(two_theta=None, intensity_calc=None), + 'E', + ExptType(), + _MeasVsCalcPlotOptions(), + ) out = capsys.readouterr().out assert 'No two_theta data available for experiment E' in out - p._plot_calc_data(Ptn(two_theta=[1], intensity_calc=None), 'E', ExptType()) + p._plot_calc_data( + object(), + Ptn(two_theta=[1], intensity_calc=None), + 'E', + ExptType(), + _MeasVsCalcPlotOptions(), + ) out = capsys.readouterr().out assert 'No calculated data available for experiment E' in out @@ -150,13 +251,24 @@ def test_plotter_routes_to_ascii_plotter(monkeypatch): from easydiffraction.datablocks.experiment.item.enums import SampleFormEnum from easydiffraction.datablocks.experiment.item.enums import ScatteringTypeEnum from easydiffraction.display.plotting import Plotter + from easydiffraction.display.plotting import _MeasVsCalcPlotOptions called = {} - def fake_plot_powder(self, x, y_series, labels, axes_labels, title, height=None): + def fake_plot_powder( + self, + x, + y_series, + labels, + axes_labels, + title, + height=None, + excluded_ranges=(), + ): called['labels'] = tuple(labels) called['axes'] = tuple(axes_labels) called['title'] = title + called['excluded_ranges'] = excluded_ranges monkeypatch.setattr(ascii_mod.AsciiPlotter, 'plot_powder', fake_plot_powder) @@ -174,9 +286,16 @@ def __init__(self): p = Plotter() p.engine = 'asciichartpy' # ensure AsciiPlotter - p._plot_meas_data(Ptn(), 'E', ExptType()) + p._plot_meas_data( + object(), + Ptn(), + 'E', + ExptType(), + _MeasVsCalcPlotOptions(), + ) assert called['labels'] == ('meas',) assert 'Measured data' in called['title'] + assert called['excluded_ranges'] == () def test_extract_bragg_tick_sets_groups_and_filters(): @@ -233,6 +352,1241 @@ class Experiment: assert tick_sets == () +def _make_bayesian_plotter_fixture(): + from easydiffraction.analysis.fit_helpers.bayesian import PosteriorParameterSummary + from easydiffraction.analysis.fit_helpers.bayesian import PosteriorSamples + from easydiffraction.display.plotting import Plotter + + samples = np.array( + [ + [[3.8900, 0.0760, -0.1170, 0.6290], [3.8908, 0.0775, -0.1185, 0.6300]], + [[3.8912, 0.0785, -0.1190, 0.6310], [3.8916, 0.0790, -0.1200, 0.6320]], + ], + dtype=float, + ) + parameter_names = ['length_a', 'broad_gauss_u', 'broad_gauss_v', 'twotheta_offset'] + posterior_samples = PosteriorSamples( + parameter_names=parameter_names, + parameter_samples=samples, + log_posterior=np.array([[0.1, 0.2], [0.3, 0.4]], dtype=float), + ) + parameters = [ + SimpleNamespace(unique_name='length_a', name='length_a', fit_min=3.8895, fit_max=3.8920), + SimpleNamespace( + unique_name='broad_gauss_u', name='broad_gauss_u', fit_min=0.05, fit_max=0.11 + ), + SimpleNamespace( + unique_name='broad_gauss_v', name='broad_gauss_v', fit_min=-0.14, fit_max=-0.10 + ), + SimpleNamespace( + unique_name='twotheta_offset', name='twotheta_offset', fit_min=0.625, fit_max=0.64 + ), + ] + summaries = [ + PosteriorParameterSummary( + unique_name=name, + display_name=name, + best_sample_value=float(samples[-1, -1, index]), + median=float(np.median(samples[:, :, index])), + standard_deviation=float(np.std(samples[:, :, index], ddof=1)), + interval_68=tuple(np.quantile(samples[:, :, index], [0.16, 0.84]).tolist()), + interval_95=tuple(np.quantile(samples[:, :, index], [0.025, 0.975]).tolist()), + ) + for index, name in enumerate(parameter_names) + ] + fit_results = SimpleNamespace( + posterior_samples=posterior_samples, + posterior_parameter_summaries=summaries, + posterior_predictive={}, + parameters=parameters, + ) + plotter = Plotter() + plotter._get_posterior_samples_and_fit_results = MethodType( + lambda self: (posterior_samples, fit_results), + plotter, + ) + plotter._get_fit_result_for_correlation = MethodType(lambda self: fit_results, plotter) + return plotter, fit_results, posterior_samples + + +def test_correlation_from_posterior_samples_returns_labeled_dataframe(): + from easydiffraction.display.plotting import Plotter + + _, _, posterior_samples = _make_bayesian_plotter_fixture() + + corr_df = Plotter()._correlation_from_posterior_samples(posterior_samples) + + assert list(corr_df.index) == posterior_samples.parameter_names + assert list(corr_df.columns) == posterior_samples.parameter_names + np.testing.assert_allclose(np.diag(corr_df.to_numpy()), np.ones(len(corr_df))) + + +def test_build_posterior_pairs_plot_hides_diagonal_ticks_and_uses_annotations(): + from easydiffraction.display.plotting import POSTERIOR_PAIR_SAMPLE_HOVER_MARKER_SIZE + from easydiffraction.display.plotting import POSTERIOR_PAIR_SAMPLE_MARKER_SIZE + from easydiffraction.display.plotting import POSTERIOR_PAIR_TITLE_FONT_SIZE + from easydiffraction.display.plotting import SQUARE_MATRIX_BOTTOM_MARGIN_PIXELS + from easydiffraction.display.plotting import SQUARE_MATRIX_TITLE_YSHIFT_PIXELS + from easydiffraction.display.plotting import SQUARE_MATRIX_TOP_MARGIN_PIXELS + + plotter, _, _ = _make_bayesian_plotter_fixture() + + figure = plotter._build_posterior_pairs_plot(parameters=None) + + assert figure.layout.title.text is None + assert figure.layout.autosize is True + assert figure.layout.width is None + assert figure.layout.height is None + assert ( + figure.layout.meta['fixed_aspect_wrapper']['aspect_ratio'] + == plotter._square_matrix_layout_meta( + n_parameters=4, + annotation_labels=[ + 'length_a', + 'broad_gauss_u', + 'broad_gauss_v', + 'twotheta_offset', + ], + )['fixed_aspect_wrapper']['aspect_ratio'] + ) + assert [annotation.text for annotation in figure.layout.annotations] == [ + 'Posterior pair plot', + 'length_a', + 'broad_gauss_u', + 'broad_gauss_v', + 'twotheta_offset', + 'length_a', + 'broad_gauss_u', + 'broad_gauss_v', + 'twotheta_offset', + ] + assert figure.layout.annotations[0].font.size == POSTERIOR_PAIR_TITLE_FONT_SIZE + assert figure.layout.annotations[0].yshift == SQUARE_MATRIX_TITLE_YSHIFT_PIXELS + assert figure.layout.annotations[0].xshift == -plotter._square_matrix_title_left_shift([ + 'length_a', + 'broad_gauss_u', + 'broad_gauss_v', + 'twotheta_offset', + ]) + assert figure.layout.margin.t == SQUARE_MATRIX_TOP_MARGIN_PIXELS + assert figure.layout.margin.b == SQUARE_MATRIX_BOTTOM_MARGIN_PIXELS + subplot = figure.get_subplot(1, 1) + bottom_subplot = figure.get_subplot(4, 1) + assert subplot.yaxis.showticklabels is False + assert subplot.yaxis.ticks == '' + assert subplot.yaxis.ticklen == 0 + assert subplot.yaxis.title.text is None + assert bottom_subplot.xaxis.showticklabels is False + assert bottom_subplot.xaxis.title.text is None + assert figure.layout.paper_bgcolor is None + assert figure.layout.plot_bgcolor is None + assert len(figure.layout.shapes) == 30 + assert any(trace.name == 'Posterior contours' for trace in figure.data) + sample_trace = next(trace for trace in figure.data if trace.name == 'Posterior samples') + hover_trace = next( + trace + for trace in figure.data + if getattr(trace, 'mode', None) == 'markers' + and getattr(trace.marker, 'color', None) == 'rgba(0, 0, 0, 0)' + ) + assert sample_trace.marker.size == POSTERIOR_PAIR_SAMPLE_MARKER_SIZE + assert hover_trace.marker.size == POSTERIOR_PAIR_SAMPLE_HOVER_MARKER_SIZE + + +def test_build_posterior_pairs_plot_fast_mode_skips_contours(): + plotter, _, _ = _make_bayesian_plotter_fixture() + + figure = plotter._build_posterior_pairs_plot(parameters=None, style='fast') + + assert all(trace.name != 'Posterior contours' for trace in figure.data) + + +def test_build_posterior_pairs_plot_sign_colors_contours_and_marginals(): + from easydiffraction.display.plotting import POSTERIOR_CONTOUR_FILL_COLORSCALE + from easydiffraction.display.plotting import POSTERIOR_CONTOUR_LINE_COLORSCALE + from easydiffraction.display.plotting import POSTERIOR_NEGATIVE_CONTOUR_FILL_COLORSCALE + from easydiffraction.display.plotting import POSTERIOR_NEGATIVE_CONTOUR_LINE_COLORSCALE + from easydiffraction.display.plotting import POSTERIOR_PAIR_MARGINAL_DENSITY_FILL_COLOR + from easydiffraction.display.plotting import POSTERIOR_PAIR_MARGINAL_DENSITY_LINE_COLOR + from easydiffraction.display.plotting import POSTERIOR_PAIR_MARGINAL_DENSITY_LINE_WIDTH + + plotter, _, _ = _make_bayesian_plotter_fixture() + + figure = plotter._build_posterior_pairs_plot(parameters=None) + + marginal_traces = [trace for trace in figure.data if trace.name == 'Marginal density'] + assert marginal_traces + assert all( + trace.line.color == POSTERIOR_PAIR_MARGINAL_DENSITY_LINE_COLOR for trace in marginal_traces + ) + assert all( + trace.line.width == POSTERIOR_PAIR_MARGINAL_DENSITY_LINE_WIDTH for trace in marginal_traces + ) + assert all( + trace.fillcolor == POSTERIOR_PAIR_MARGINAL_DENSITY_FILL_COLOR for trace in marginal_traces + ) + + fill_contours = [ + trace + for trace in figure.data + if trace.type == 'contour' and trace.contours.coloring == 'fill' + ] + line_contours = [ + trace + for trace in figure.data + if trace.type == 'contour' and trace.contours.coloring == 'lines' + ] + fill_end_colors = {trace.colorscale[-1][1] for trace in fill_contours} + line_end_colors = {trace.colorscale[-1][1] for trace in line_contours} + + assert POSTERIOR_CONTOUR_FILL_COLORSCALE[-1][1] in fill_end_colors + assert POSTERIOR_NEGATIVE_CONTOUR_FILL_COLORSCALE[-1][1] in fill_end_colors + assert POSTERIOR_CONTOUR_LINE_COLORSCALE[-1][1] in line_end_colors + assert POSTERIOR_NEGATIVE_CONTOUR_LINE_COLORSCALE[-1][1] in line_end_colors + + +def test_build_posterior_pairs_plot_formats_dotted_axis_titles_multiline(): + from easydiffraction.display.plotting import SQUARE_MATRIX_BOTTOM_MARGIN_PIXELS + from easydiffraction.display.plotting import SQUARE_MATRIX_LEFT_MARGIN_PIXELS + + plotter, fit_results, _ = _make_bayesian_plotter_fixture() + dotted_parameter_names = [ + 'lbco.cell.length_a', + 'hrpt.peak.broad_gauss_u', + 'hrpt.peak.broad_gauss_v', + 'hrpt.instrument.twotheta_offset', + ] + fit_results.posterior_samples.parameter_names = dotted_parameter_names + for index, unique_name in enumerate(dotted_parameter_names): + fit_results.parameters[index].unique_name = unique_name + fit_results.posterior_parameter_summaries[index].unique_name = unique_name + + figure = plotter._build_posterior_pairs_plot(parameters=None) + + annotation_texts = [annotation.text for annotation in figure.layout.annotations] + assert 'hrpt.
peak.
broad_gauss_u' in annotation_texts + assert 'hrpt.
instrument.
twotheta_offset' in annotation_texts + assert all('None broad_gauss_u' not in text for text in annotation_texts) + assert figure.layout.margin.l > SQUARE_MATRIX_LEFT_MARGIN_PIXELS + assert figure.layout.margin.b > SQUARE_MATRIX_BOTTOM_MARGIN_PIXELS + + +def test_build_posterior_pairs_plot_uses_full_names_in_hovertemplates(): + plotter, fit_results, _ = _make_bayesian_plotter_fixture() + dotted_parameter_names = [ + 'lbco.cell.length_a', + 'hrpt.peak.broad_gauss_u', + 'hrpt.peak.broad_gauss_v', + 'hrpt.instrument.twotheta_offset', + ] + fit_results.posterior_samples.parameter_names = dotted_parameter_names + for index, unique_name in enumerate(dotted_parameter_names): + fit_results.parameters[index].unique_name = unique_name + fit_results.posterior_parameter_summaries[index].unique_name = unique_name + + figure = plotter._build_posterior_pairs_plot(parameters=None) + + hovertemplates = { + trace.hovertemplate + for trace in figure.data + if getattr(trace, 'hovertemplate', None) is not None + } + + assert ( + 'lbco.cell.length_a: %{x:.4f}
hrpt.peak.broad_gauss_u: %{y:.4f}' + ) in hovertemplates + assert ( + 'hrpt.instrument.twotheta_offset: %{x:.4f}
density: %{y:.4f}' + ) in hovertemplates + + +def test_posterior_pair_figure_height_shrinks_cells_for_many_parameters(): + from easydiffraction.display.plotting import PAIR_PLOT_CELL_SIZE_PIXELS + from easydiffraction.display.plotting import Plotter + + cell_size = Plotter._posterior_pair_cell_size_pixels(8, available_width_pixels=980) + + assert cell_size < PAIR_PLOT_CELL_SIZE_PIXELS + + +def test_posterior_pair_density_budget_scales_with_parameter_count(): + from easydiffraction.display.plotting import Plotter + + assert Plotter._posterior_pair_density_max_points( + 8 + ) < Plotter._posterior_pair_density_max_points(4) + assert Plotter._posterior_pair_contour_grid_size( + 8 + ) < Plotter._posterior_pair_contour_grid_size(4) + + +def test_posterior_pairs_context_thins_kde_samples_and_preserves_axis_ranges(): + from easydiffraction.analysis.fit_helpers.bayesian import PosteriorSamples + from easydiffraction.display.plotting import POSTERIOR_PAIR_SCATTER_MAX_POINTS + from easydiffraction.display.plotting import Plotter + + parameter_count = 8 + sample_count = 5000 + parameter_names = [f'param_{index}' for index in range(parameter_count)] + samples = np.zeros((1, sample_count, parameter_count), dtype=float) + samples[0, 1, 0] = 100.0 + samples[0, 2, 1] = -50.0 + for index in range(2, parameter_count): + samples[0, :, index] = np.linspace(index, index + 1, sample_count, dtype=float) + + posterior_samples = PosteriorSamples( + parameter_names=parameter_names, + parameter_samples=samples, + log_posterior=np.zeros((1, sample_count), dtype=float), + ) + parameters = [ + SimpleNamespace(unique_name=name, name=name, fit_min=None, fit_max=None) + for name in parameter_names + ] + fit_results = SimpleNamespace( + posterior_samples=posterior_samples, + posterior_parameter_summaries=[], + posterior_predictive={}, + parameters=parameters, + ) + plotter = Plotter() + plotter._get_posterior_samples_and_fit_results = MethodType( + lambda self: (posterior_samples, fit_results), + plotter, + ) + + context = plotter._posterior_pairs_context(parameters=None) + + assert context is not None + assert context.marginal_density_samples.shape == (sample_count, parameter_count) + assert context.density_samples.shape == ( + Plotter._posterior_pair_density_max_points(parameter_count), + parameter_count, + ) + assert context.scatter_samples.shape == (POSTERIOR_PAIR_SCATTER_MAX_POINTS, parameter_count) + assert context.show_contours is False + assert context.contour_grid_size == Plotter._posterior_pair_contour_grid_size(parameter_count) + assert context.axis_ranges[0][1] > 100.0 + assert context.axis_ranges[1][0] < -50.0 + + +def test_posterior_pair_diagonal_matches_standalone_distribution_when_thinned(): + from easydiffraction.analysis.fit_helpers.bayesian import PosteriorParameterSummary + from easydiffraction.analysis.fit_helpers.bayesian import PosteriorSamples + from easydiffraction.display.plotting import Plotter + + sample_count = 5001 + angle = np.linspace(0.0, 12.0 * np.pi, sample_count, dtype=float) + sample_axis = np.linspace(-1.0, 1.0, sample_count, dtype=float) + samples = np.empty((1, sample_count, 2), dtype=float) + samples[0, :, 0] = 3.8913 + 0.00016 * np.sin(angle) + 0.00003 * np.cos(2.0 * angle) + samples[0, :, 1] = 0.0780 + 0.0024 * np.cos(0.5 * angle) + 0.0005 * sample_axis**2 + parameter_names = ['length_a', 'broad_gauss_u'] + posterior_samples = PosteriorSamples( + parameter_names=parameter_names, + parameter_samples=samples, + log_posterior=np.zeros((1, sample_count), dtype=float), + ) + parameters = [ + SimpleNamespace(unique_name='length_a', name='length_a', fit_min=3.8909, fit_max=3.8917), + SimpleNamespace( + unique_name='broad_gauss_u', + name='broad_gauss_u', + fit_min=0.074, + fit_max=0.082, + ), + ] + summaries = [ + PosteriorParameterSummary( + unique_name=name, + display_name=name, + best_sample_value=float(samples[0, -1, index]), + median=float(np.median(samples[:, :, index])), + standard_deviation=float(np.std(samples[:, :, index], ddof=1)), + interval_68=tuple(np.quantile(samples[:, :, index], [0.16, 0.84]).tolist()), + interval_95=tuple(np.quantile(samples[:, :, index], [0.025, 0.975]).tolist()), + ) + for index, name in enumerate(parameter_names) + ] + fit_results = SimpleNamespace( + posterior_samples=posterior_samples, + posterior_parameter_summaries=summaries, + posterior_predictive={}, + parameters=parameters, + ) + plotter = Plotter() + plotter._get_posterior_samples_and_fit_results = MethodType( + lambda self: (posterior_samples, fit_results), + plotter, + ) + plotter._get_fit_result_for_correlation = MethodType(lambda self: fit_results, plotter) + + pair_figure = plotter._build_posterior_pairs_plot(parameters=parameters) + distribution_figure = plotter._build_param_distribution_plot(parameters[0]) + + pair_trace = next( + trace + for trace in pair_figure.data + if trace.name == 'Marginal density' + and trace.hovertemplate == 'length_a: %{x:.4f}
density: %{y:.4f}' + ) + distribution_trace = next( + trace for trace in distribution_figure.data if trace.name == 'Marginal density' + ) + + np.testing.assert_allclose(pair_trace.x, distribution_trace.x) + np.testing.assert_allclose(pair_trace.y, distribution_trace.y) + + +def test_build_posterior_pairs_plot_rejects_unknown_style(): + plotter, _, _ = _make_bayesian_plotter_fixture() + + with pytest.raises( + ValueError, + match=r'style must be one of auto, fast, full for posterior pair plots\.', + ): + plotter._build_posterior_pairs_plot(parameters=None, style='slow') + + +def test_build_param_distribution_plot_returns_plotly_figure(): + from easydiffraction.display.plotting import POSTERIOR_PAIR_MARGINAL_DENSITY_FILL_COLOR + from easydiffraction.display.plotting import POSTERIOR_PAIR_MARGINAL_DENSITY_LINE_COLOR + from easydiffraction.display.plotting import POSTERIOR_PAIR_MARGINAL_DENSITY_LINE_WIDTH + from easydiffraction.display.plotting import POSTERIOR_INTERVAL_95_FILL_COLOR + from easydiffraction.display.plotting import POSTERIOR_POINT_ESTIMATE_LINE_DASH + + plotter, fit_results, _ = _make_bayesian_plotter_fixture() + parameter = fit_results.parameters[0] + + figure = plotter._build_param_distribution_plot(parameter) + + assert figure.layout.title.text == 'Posterior distribution: length_a' + assert {trace.name for trace in figure.data} >= { + 'Posterior histogram', + 'Marginal density', + '95% credible interval', + 'Median', + 'Best posterior sample', + } + marginal_trace = next(trace for trace in figure.data if trace.name == 'Marginal density') + histogram_trace = next(trace for trace in figure.data if trace.name == 'Posterior histogram') + interval_trace = next(trace for trace in figure.data if trace.name == '95% credible interval') + max_posterior_trace = next( + trace for trace in figure.data if trace.name == 'Best posterior sample' + ) + assert marginal_trace.line.color == POSTERIOR_PAIR_MARGINAL_DENSITY_LINE_COLOR + assert marginal_trace.line.width == POSTERIOR_PAIR_MARGINAL_DENSITY_LINE_WIDTH + assert marginal_trace.fillcolor == POSTERIOR_PAIR_MARGINAL_DENSITY_FILL_COLOR + assert marginal_trace.hovertemplate == 'length_a: %{x:.4f}
density: %{y:.4f}' + assert histogram_trace.xbins.size is not None + assert '68% credible interval' not in {trace.name for trace in figure.data} + assert interval_trace.fillcolor == POSTERIOR_INTERVAL_95_FILL_COLOR + assert max_posterior_trace.line.dash == POSTERIOR_POINT_ESTIMATE_LINE_DASH + assert figure.layout.xaxis.range is not None + assert tuple(figure.layout.xaxis.range) == ( + float(marginal_trace.x[0]), + float(marginal_trace.x[-1]), + ) + assert figure.layout.yaxis.range is not None + + +def test_plot_param_distribution_routes_ascii_to_marginal_density(monkeypatch): + from types import SimpleNamespace + + plotter, fit_results, posterior_samples = _make_bayesian_plotter_fixture() + captured: dict[str, object] = {} + plotter.engine = 'asciichartpy' + plotter._backend = SimpleNamespace( + plot_powder=lambda **kwargs: captured.setdefault('powder', kwargs) + ) + + plotter.plot_param_distribution(fit_results.parameters[0]) + + values = posterior_samples.flattened()[:, 0] + density_curve = plotter._posterior_density_curve( + values, + lower_bound=fit_results.parameters[0].fit_min, + upper_bound=fit_results.parameters[0].fit_max, + ) + + assert density_curve is not None + assert captured['powder']['labels'] == ['density'] + assert captured['powder']['axes_labels'] == ['length_a', 'Probability density'] + assert captured['powder']['title'] == 'Posterior distribution: length_a' + assert captured['powder']['height'] == plotter.height + np.testing.assert_allclose(captured['powder']['x'], density_curve[0]) + np.testing.assert_allclose(captured['powder']['y_series'][0], density_curve[1]) + + +def test_plot_posterior_predictive_summary_uses_consistent_labels_and_styles(monkeypatch): + from types import SimpleNamespace + + from easydiffraction.display.plotting import POSTERIOR_INTERVAL_95_FILL_COLOR + from easydiffraction.display.plotting import POSTERIOR_POINT_ESTIMATE_LINE_DASH + from easydiffraction.display.plotting import Plotter + from easydiffraction.display.plotters.plotly import PlotlyPlotter + + captured: dict[str, object] = {} + + plotter = Plotter() + plotter.engine = 'plotly' + plotter._backend = SimpleNamespace( + _show_figure=lambda figure: captured.setdefault('fig', figure) + ) + + plotter._plot_posterior_predictive_summary( + expt_name='hrpt', + summary=SimpleNamespace( + x=np.array([1.0, 2.0, 3.0]), + lower_95=np.array([8.0, 9.0, 10.0]), + upper_95=np.array([10.0, 11.0, 12.0]), + best_sample_prediction=np.array([9.0, 10.0, 11.0]), + ), + y_meas=np.array([9.5, 10.5, 11.5]), + axes_labels=['2θ (degree)', 'Intensity (arb. units)'], + show_band=True, + show_draws=False, + ) + + fig = captured['fig'] + upper_band_trace = fig.data[1] + measured_trace = next(trace for trace in fig.data if trace.name == 'Measured') + max_posterior_trace = next( + trace for trace in fig.data if trace.name == 'Best posterior sample' + ) + + assert upper_band_trace.name == '95% credible interval' + assert upper_band_trace.fillcolor == POSTERIOR_INTERVAL_95_FILL_COLOR + assert upper_band_trace.legendrank == 30 + assert measured_trace.legendrank == 10 + assert max_posterior_trace.legendrank == 20 + assert max_posterior_trace.line.dash == POSTERIOR_POINT_ESTIMATE_LINE_DASH + assert fig.layout.legend.x == 1.0 + assert fig.layout.legend.y == 1.0 + assert fig.layout.legend.bgcolor == PlotlyPlotter._legend_background_color() + assert fig.layout.margin.r == 30 + assert fig.layout.margin.t == 40 + assert fig.layout.margin.b == 45 + assert fig.layout.xaxis.showline is True + assert fig.layout.xaxis.mirror is True + assert fig.layout.xaxis.zeroline is False + assert fig.layout.xaxis.linecolor == PlotlyPlotter._axis_frame_color() + assert fig.layout.yaxis.showline is True + assert fig.layout.yaxis.mirror is True + assert fig.layout.yaxis.zeroline is False + assert fig.layout.yaxis.linecolor == PlotlyPlotter._axis_frame_color() + + +@pytest.mark.parametrize( + ('x_values', 'y_values', 'x_bounds', 'y_bounds'), + [ + (np.ones(8), np.arange(8, dtype=float), (0.5, 1.5), (0.0, 7.0)), + ( + np.arange(8, dtype=float), + 2.0 * np.arange(8, dtype=float) + 1.0, + (0.0, 7.0), + (1.0, 15.0), + ), + ], +) +def test_posterior_pair_density_surface_returns_none_for_rank_deficient_samples( + x_values, + y_values, + x_bounds, + y_bounds, +): + from easydiffraction.display.plotting import Plotter + + surface = Plotter._posterior_pair_density_surface( + x_values=np.asarray(x_values, dtype=float), + y_values=np.asarray(y_values, dtype=float), + x_bounds=x_bounds, + y_bounds=y_bounds, + ) + + assert surface is None + + +def test_plot_posterior_predictive_data_uses_max_posterior_label_and_dash(monkeypatch): + from types import SimpleNamespace + + 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 + from easydiffraction.display.plotting import XAxisType + + captured: dict[str, object] = {} + + class ExptType: + sample_form = type('SF', (), {'value': SampleFormEnum.POWDER})() + scattering_type = type('S', (), {'value': ScatteringTypeEnum.BRAGG})() + beam_mode = type('B', (), {'value': BeamModeEnum.CONSTANT_WAVELENGTH})() + + class Pattern: + two_theta = np.array([1.0, 2.0, 3.0]) + intensity_meas = np.array([10.0, 12.0, 11.0]) + intensity_bkg = np.array([1.0, 1.0, 1.0]) + + class Experiment: + type = ExptType() + data = Pattern() + + plotter = Plotter() + plotter.engine = 'plotly' + plotter._backend = SimpleNamespace( + plot_powder_meas_vs_calc=lambda *, plot_spec: captured.setdefault('plot_spec', plot_spec) + ) + + monkeypatch.setattr( + Plotter, + '_get_or_build_posterior_predictive_summary', + lambda self, **kwargs: SimpleNamespace( + x=np.array([1.0, 2.0, 3.0]), + lower_95=np.array([8.0, 9.0, 10.0]), + upper_95=np.array([10.0, 11.0, 12.0]), + best_sample_prediction=np.array([9.0, 11.0, 10.5]), + draws=None, + ), + ) + monkeypatch.setattr(Plotter, '_extract_bragg_tick_sets', lambda self, **kwargs: ()) + + plotter._plot_posterior_predictive_data( + experiment=Experiment(), + expt_name='hrpt', + plot_options=SimpleNamespace( + x_min=None, + x_max=None, + show_residual=None, + show_background=None, + show_bragg=None, + show_excluded=False, + x=None, + ), + x_axis=XAxisType.TWO_THETA, + style='band', + ) + + plot_spec = captured['plot_spec'] + assert plot_spec.y_calc_name == 'Best posterior sample' + assert plot_spec.y_calc_line_dash == 'dot' + + +def test_plot_posterior_predictive_request_allows_ascii_for_powder_bragg(monkeypatch): + 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 + + captured: dict[str, object] = {} + + class ExptType: + sample_form = type('SF', (), {'value': SampleFormEnum.POWDER})() + scattering_type = type('S', (), {'value': ScatteringTypeEnum.BRAGG})() + beam_mode = type('B', (), {'value': BeamModeEnum.CONSTANT_WAVELENGTH})() + + class Experiment: + type = ExptType() + + class Project: + experiments = {'hrpt': Experiment()} + + plotter = Plotter() + plotter.engine = 'asciichartpy' + plotter._set_project(Project()) + + monkeypatch.setattr(Plotter, '_update_project_categories', lambda self, expt_name: None) + + def fake_plot_posterior_predictive_data( + self, + *, + experiment, + expt_name, + plot_options, + x_axis, + style, + ): + captured['experiment'] = experiment + captured['expt_name'] = expt_name + captured['style'] = style + captured['x_axis'] = x_axis + captured['show_residual'] = plot_options.show_residual + + monkeypatch.setattr( + Plotter, '_plot_posterior_predictive_data', fake_plot_posterior_predictive_data + ) + + plotter.plot_posterior_predictive('hrpt') + + assert captured['experiment'] is Project.experiments['hrpt'] + assert captured['expt_name'] == 'hrpt' + assert captured['style'] == 'band' + assert captured['show_residual'] is None + + +def test_plot_posterior_predictive_summary_routes_ascii_to_measured_and_map(monkeypatch): + from types import SimpleNamespace + + from easydiffraction.display.plotting import Plotter + + captured: dict[str, object] = {} + plotter = Plotter() + plotter.engine = 'asciichartpy' + plotter._backend = SimpleNamespace( + plot_powder=lambda **kwargs: captured.setdefault('powder', kwargs) + ) + + plotter._plot_posterior_predictive_summary( + expt_name='pdf', + summary=SimpleNamespace( + x=np.array([1.0, 2.0, 3.0]), + best_sample_prediction=np.array([9.0, 10.0, 11.0]), + lower_95=np.array([8.0, 9.0, 10.0]), + upper_95=np.array([10.0, 11.0, 12.0]), + draws=np.array([[8.5, 9.5, 10.5]]), + ), + y_meas=np.array([9.5, 10.5, 11.5]), + axes_labels=['2θ (degree)', 'Intensity (arb. units)'], + show_band=True, + show_draws=True, + excluded_ranges=((1.2, 1.4),), + ) + + assert captured['powder']['labels'] == ['meas', 'posterior'] + np.testing.assert_allclose(captured['powder']['x'], np.array([1.0, 2.0, 3.0])) + np.testing.assert_allclose( + captured['powder']['y_series'][0], + np.array([9.5, 10.5, 11.5]), + ) + np.testing.assert_allclose( + captured['powder']['y_series'][1], + np.array([9.0, 10.0, 11.0]), + ) + assert captured['powder']['excluded_ranges'] == ((1.2, 1.4),) + + +def test_plot_posterior_predictive_data_routes_ascii_to_line_plot_without_intervals(monkeypatch): + from types import SimpleNamespace + + 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 + from easydiffraction.display.plotting import XAxisType + + captured: dict[str, object] = {} + + class ExptType: + sample_form = type('SF', (), {'value': SampleFormEnum.POWDER})() + scattering_type = type('S', (), {'value': ScatteringTypeEnum.BRAGG})() + beam_mode = type('B', (), {'value': BeamModeEnum.CONSTANT_WAVELENGTH})() + + class Pattern: + two_theta = np.array([1.0, 2.0, 3.0]) + intensity_meas = np.array([10.0, 12.0, 11.0]) + intensity_bkg = np.array([1.0, 1.0, 1.0]) + + class Experiment: + type = ExptType() + data = Pattern() + + plotter = Plotter() + plotter.engine = 'asciichartpy' + plotter._backend = SimpleNamespace( + plot_powder=lambda **kwargs: captured.setdefault('powder', kwargs), + plot_powder_meas_vs_calc=lambda **kwargs: captured.setdefault('composite', kwargs), + ) + + monkeypatch.setattr( + Plotter, + '_get_or_build_posterior_predictive_summary', + lambda self, **kwargs: SimpleNamespace( + x=np.array([1.0, 2.0, 3.0]), + lower_95=np.array([8.0, 9.0, 10.0]), + upper_95=np.array([10.0, 11.0, 12.0]), + best_sample_prediction=np.array([9.0, 11.0, 10.5]), + draws=None, + ), + ) + + plotter._plot_posterior_predictive_data( + experiment=Experiment(), + expt_name='hrpt', + plot_options=SimpleNamespace( + x_min=None, + x_max=None, + show_residual=None, + show_background=None, + show_bragg=None, + show_excluded=False, + x=None, + ), + x_axis=XAxisType.TWO_THETA, + style='band+draws', + ) + + assert 'composite' not in captured + assert captured['powder']['labels'] == ['meas', 'posterior'] + np.testing.assert_allclose(captured['powder']['x'], np.array([1.0, 2.0, 3.0])) + np.testing.assert_allclose( + captured['powder']['y_series'][0], + np.array([10.0, 12.0, 11.0]), + ) + np.testing.assert_allclose( + captured['powder']['y_series'][1], + np.array([9.0, 11.0, 10.5]), + ) + + +def test_plot_meas_vs_calc_request_respects_background_and_bragg_flags(): + 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 + from easydiffraction.display.plotting import _MeasVsCalcPlotOptions + + captured: dict[str, object] = {} + + class FakeBackend: + def plot_powder_meas_vs_calc(self, *, plot_spec): + captured['plot_spec'] = plot_spec + + class Pattern: + two_theta = np.array([1.0, 2.0, 3.0]) + intensity_meas = np.array([10.0, 12.0, 11.0]) + intensity_calc = np.array([9.0, 11.0, 10.5]) + intensity_bkg = np.array([1.0, 1.0, 1.0]) + + class Refln: + phase_id = np.array(['phase-a']) + two_theta = np.array([2.0]) + index_h = np.array([1]) + index_k = np.array([0]) + index_l = np.array([1]) + f_squared_calc = np.array([50.0]) + f_calc = np.array([7.0]) + + class ExptType: + sample_form = type('SF', (), {'value': SampleFormEnum.POWDER})() + scattering_type = type('S', (), {'value': ScatteringTypeEnum.BRAGG})() + beam_mode = type('B', (), {'value': BeamModeEnum.CONSTANT_WAVELENGTH})() + + class Experiment: + data = Pattern() + type = ExptType() + refln = Refln() + + plotter = Plotter() + plotter._backend = FakeBackend() + plotter._plot_meas_vs_calc_data( + experiment=Experiment(), + expt_name='E1', + plot_options=_MeasVsCalcPlotOptions( + show_background=False, + show_bragg=False, + ), + ) + + plot_spec = captured['plot_spec'] + assert plot_spec.y_bkg is None + assert plot_spec.bragg_tick_sets == () + + plotter._plot_meas_vs_calc_data( + experiment=Experiment(), + expt_name='E1', + plot_options=_MeasVsCalcPlotOptions( + show_background=True, + show_bragg=True, + ), + ) + + plot_spec = captured['plot_spec'] + assert np.allclose(plot_spec.y_bkg, np.array([1.0, 1.0, 1.0])) + assert len(plot_spec.bragg_tick_sets) == 1 + + +def test_build_param_distribution_plot_accepts_unique_name_string(): + plotter, fit_results, posterior_samples = _make_bayesian_plotter_fixture() + unique_name = 'phase.cell.length_a' + posterior_samples.parameter_names[0] = unique_name + fit_results.parameters[0].unique_name = unique_name + fit_results.posterior_parameter_summaries[0].unique_name = unique_name + + figure = plotter._build_param_distribution_plot(unique_name) + + assert figure.layout.title.text == f'Posterior distribution: {unique_name}' + + +def test_build_param_distribution_plot_accepts_user_facing_label_string(): + plotter, fit_results, posterior_samples = _make_bayesian_plotter_fixture() + unique_name = 'phase.cell.length_a' + posterior_samples.parameter_names[0] = unique_name + fit_results.parameters[0].unique_name = unique_name + fit_results.posterior_parameter_summaries[0].unique_name = unique_name + fit_results.posterior_parameter_summaries[0].display_name = 'Cell a' + + figure = plotter._build_param_distribution_plot('Cell a') + + assert figure.layout.title.text == f'Posterior distribution: {unique_name}' + + +def test_resolve_posterior_parameter_names_warns_on_ambiguous_label(monkeypatch): + from easydiffraction.analysis.fit_helpers.bayesian import PosteriorParameterSummary + from easydiffraction.analysis.fit_helpers.bayesian import PosteriorSamples + from easydiffraction.display.plotting import Plotter + + posterior_samples = PosteriorSamples( + parameter_names=['phase_a.length_a', 'phase_b.length_a'], + parameter_samples=np.ones((2, 2, 2), dtype=float), + ) + fit_results = SimpleNamespace( + posterior_samples=posterior_samples, + posterior_parameter_summaries=[ + PosteriorParameterSummary( + unique_name='phase_a.length_a', + display_name='length_a', + best_sample_value=1.0, + median=1.0, + standard_deviation=0.1, + interval_68=(0.9, 1.1), + interval_95=(0.8, 1.2), + ), + PosteriorParameterSummary( + unique_name='phase_b.length_a', + display_name='length_a', + best_sample_value=2.0, + median=2.0, + standard_deviation=0.1, + interval_68=(1.9, 2.1), + interval_95=(1.8, 2.2), + ), + ], + parameters=[ + SimpleNamespace(unique_name='phase_a.length_a', name='length_a'), + SimpleNamespace(unique_name='phase_b.length_a', name='length_a'), + ], + ) + warning_messages: list[str] = [] + + monkeypatch.setattr( + 'easydiffraction.display.plotting.log.warning', + lambda message: warning_messages.append(message), + ) + + result = Plotter._resolve_posterior_parameter_names( + fit_results=fit_results, + parameters=['length_a'], + ) + + assert result is None + assert warning_messages + assert 'ambiguous' in warning_messages[0] + assert 'phase_a.length_a' in warning_messages[0] + assert 'phase_b.length_a' in warning_messages[0] + + +def test_build_posterior_predictive_summary_restores_parameter_state(monkeypatch): + from easydiffraction.analysis.fit_helpers.bayesian import PosteriorPredictiveSummary + from easydiffraction.display.plotting import Plotter + + class FakePredictiveParameter: + def __init__(self, unique_name, value, uncertainty): + self.unique_name = unique_name + self.value = value + self.uncertainty = uncertainty + + def _set_value_from_minimizer(self, value): + self.value = value + + sampled_parameters = [ + FakePredictiveParameter('a', 1.0, 0.1), + FakePredictiveParameter('b', 2.0, 0.2), + ] + posterior_samples = SimpleNamespace( + parameter_names=['a', 'b'], + flattened=lambda: np.array( + [ + [1.0, 2.0], + [1.1, 2.1], + [0.9, 1.9], + [1.2, 2.2], + ], + dtype=float, + ), + ) + fit_results = SimpleNamespace( + posterior_samples=posterior_samples, + parameters=sampled_parameters, + ) + plotter = Plotter() + + def fake_evaluate(self, *, sampled_parameters, values, experiment, expt_name, x_axis): + x = np.array([0.0, 1.0], dtype=float) + y = np.array([values[0] + values[1], values[0] - values[1]], dtype=float) + return y, x + + monkeypatch.setattr(Plotter, '_evaluate_posterior_predictive_state', fake_evaluate) + monkeypatch.setattr(Plotter, '_update_project_categories', lambda self, expt_name: None) + + summary = plotter._build_posterior_predictive_summary( + fit_results=fit_results, + experiment=object(), + expt_name='hrpt', + x_axis='two_theta', + ) + + assert isinstance(summary, PosteriorPredictiveSummary) + assert summary.experiment_name == 'hrpt' + assert summary.x_axis_name == 'two_theta' + assert summary.draws.shape == (4, 2) + np.testing.assert_allclose(summary.best_sample_prediction, np.array([3.0, -1.0])) + np.testing.assert_allclose([parameter.value for parameter in sampled_parameters], [1.0, 2.0]) + assert [parameter.uncertainty for parameter in sampled_parameters] == [0.1, 0.2] + + +def test_build_posterior_predictive_summary_omits_draws_when_not_requested(monkeypatch): + from easydiffraction.display.plotting import Plotter + + class FakePredictiveParameter: + def __init__(self, unique_name, value, uncertainty): + self.unique_name = unique_name + self.value = value + self.uncertainty = uncertainty + + def _set_value_from_minimizer(self, value): + self.value = value + + sampled_parameters = [ + FakePredictiveParameter('a', 1.0, 0.1), + FakePredictiveParameter('b', 2.0, 0.2), + ] + posterior_samples = SimpleNamespace( + parameter_names=['a', 'b'], + flattened=lambda: np.array( + [ + [1.0, 2.0], + [1.1, 2.1], + [0.9, 1.9], + [1.2, 2.2], + ], + dtype=float, + ), + ) + fit_results = SimpleNamespace( + posterior_samples=posterior_samples, + parameters=sampled_parameters, + ) + plotter = Plotter() + + def fake_evaluate(self, *, sampled_parameters, values, experiment, expt_name, x_axis): + x = np.array([0.0, 1.0], dtype=float) + y = np.array([values[0] + values[1], values[0] - values[1]], dtype=float) + return y, x + + monkeypatch.setattr(Plotter, '_evaluate_posterior_predictive_state', fake_evaluate) + monkeypatch.setattr(Plotter, '_update_project_categories', lambda self, expt_name: None) + + summary = plotter._build_posterior_predictive_summary( + fit_results=fit_results, + experiment=object(), + expt_name='hrpt', + x_axis='two_theta', + include_draws=False, + ) + + assert summary is not None + assert summary.draws is None + np.testing.assert_allclose(summary.lower_95.shape, (2,)) + np.testing.assert_allclose(summary.upper_95.shape, (2,)) + + +def test_get_or_build_posterior_predictive_summary_rebuilds_draws_after_band_cache( + monkeypatch, +): + from easydiffraction.display.plotting import Plotter + + fit_results = SimpleNamespace( + posterior_predictive={}, + posterior_samples=object(), + ) + band_summary = SimpleNamespace(draws=None) + draw_summary = SimpleNamespace(draws=np.ones((2, 2), dtype=float)) + build_calls: list[bool] = [] + plotter = Plotter() + + monkeypatch.setattr(Plotter, '_get_fit_result_for_correlation', lambda self: fit_results) + + def fake_build( + self, + *, + fit_results, + experiment, + expt_name, + x_axis, + include_draws=True, + ): + del fit_results, experiment, expt_name, x_axis + build_calls.append(include_draws) + return draw_summary if include_draws else band_summary + + monkeypatch.setattr(Plotter, '_build_posterior_predictive_summary', fake_build) + + summary_band = plotter._get_or_build_posterior_predictive_summary( + experiment=object(), + expt_name='hrpt', + x_axis='two_theta', + include_draws=False, + ) + summary_draws = plotter._get_or_build_posterior_predictive_summary( + experiment=object(), + expt_name='hrpt', + x_axis='two_theta', + include_draws=True, + ) + + assert summary_band is band_summary + assert summary_draws is draw_summary + assert build_calls == [False, True] + + +def test_plot_posterior_predictive_defaults_to_band_for_bragg(monkeypatch): + 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 + + captured: dict[str, object] = {} + + class ExptType: + sample_form = type('SF', (), {'value': SampleFormEnum.POWDER})() + scattering_type = type('S', (), {'value': ScatteringTypeEnum.BRAGG})() + beam_mode = type('B', (), {'value': BeamModeEnum.CONSTANT_WAVELENGTH})() + + class Experiment: + type = ExptType() + + class Project: + experiments = {'hrpt': Experiment()} + + plotter = Plotter() + plotter.engine = 'plotly' + plotter._set_project(Project()) + + monkeypatch.setattr(Plotter, '_update_project_categories', lambda self, expt_name: None) + + def fake_plot_posterior_predictive_data( + self, + *, + experiment, + expt_name, + plot_options, + x_axis, + style, + ): + captured['experiment'] = experiment + captured['expt_name'] = expt_name + captured['style'] = style + captured['x_axis'] = x_axis + captured['show_residual'] = plot_options.show_residual + + monkeypatch.setattr( + Plotter, '_plot_posterior_predictive_data', fake_plot_posterior_predictive_data + ) + + plotter.plot_posterior_predictive('hrpt') + + assert captured['experiment'] is Project.experiments['hrpt'] + assert captured['expt_name'] == 'hrpt' + assert captured['style'] == 'band' + assert captured['show_residual'] is None + + +def test_plot_posterior_predictive_non_bragg_filters_x_range_and_warns_for_residual( + monkeypatch, +): + from easydiffraction.analysis.fit_helpers.bayesian import PosteriorPredictiveSummary + 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 + + captured: dict[str, object] = {} + warnings: list[str] = [] + + class ExptType: + sample_form = type('SF', (), {'value': SampleFormEnum.POWDER})() + scattering_type = type('S', (), {'value': ScatteringTypeEnum.TOTAL})() + beam_mode = type('B', (), {'value': BeamModeEnum.CONSTANT_WAVELENGTH})() + + class Pattern: + x = np.array([1.0, 2.0, 3.0]) + two_theta = np.array([1.0, 2.0, 3.0]) + intensity_meas = np.array([10.0, 20.0, 30.0]) + + class Experiment: + type = ExptType() + data = Pattern() + + class Project: + experiments = {'pdf': Experiment()} + + plotter = Plotter() + plotter.engine = 'plotly' + plotter._set_project(Project()) + + monkeypatch.setattr(Plotter, '_update_project_categories', lambda self, expt_name: None) + monkeypatch.setattr( + Plotter, + '_get_or_build_posterior_predictive_summary', + lambda self, **kwargs: PosteriorPredictiveSummary( + experiment_name='pdf', + x_axis_name='two_theta', + x=np.array([1.0, 2.0, 3.0]), + best_sample_prediction=np.array([9.0, 19.0, 29.0]), + lower_95=np.array([8.0, 18.0, 28.0]), + upper_95=np.array([10.0, 20.0, 30.0]), + ), + ) + monkeypatch.setattr('easydiffraction.display.plotting.log.warning', warnings.append) + + def fake_plot_summary( + self, + *, + expt_name, + summary, + y_meas, + axes_labels, + show_band, + show_draws, + excluded_ranges, + ): + captured['expt_name'] = expt_name + captured['summary'] = summary + captured['y_meas'] = y_meas + captured['axes_labels'] = axes_labels + captured['show_band'] = show_band + captured['show_draws'] = show_draws + captured['excluded_ranges'] = excluded_ranges + + monkeypatch.setattr(Plotter, '_plot_posterior_predictive_summary', fake_plot_summary) + + plotter.plot_posterior_predictive('pdf', x_min=1.5, x_max=2.5, show_residual=True) + + assert captured['expt_name'] == 'pdf' + np.testing.assert_allclose(captured['summary'].x, np.array([2.0])) + np.testing.assert_allclose( + captured['summary'].best_sample_prediction, + np.array([19.0]), + ) + np.testing.assert_allclose(captured['summary'].lower_95, np.array([18.0])) + np.testing.assert_allclose(captured['summary'].upper_95, np.array([20.0])) + np.testing.assert_allclose(captured['y_meas'], np.array([20.0])) + assert captured['show_band'] is True + assert captured['show_draws'] is False + assert captured['excluded_ranges'] == () + assert any('ignoring show_residual=True' in warning for warning in warnings) + + def test_extract_bragg_tick_sets_uses_derived_d_spacing_for_cwl_ticks(): import numpy as np @@ -641,7 +1995,7 @@ class Project: p = Plotter() p.engine = 'asciichartpy' p._set_project(Project()) - p.plot_param_correlations(threshold=0.1, precision=3) + p.plot_param_correlations(threshold=0.1, precision=3, show_diagonal=False) df = captured['df'] assert [column.strip() for column in df.columns.get_level_values(0)] == [ @@ -660,7 +2014,12 @@ def test_plot_param_correlations_renders_plotly_heatmap(monkeypatch): import numpy as np import easydiffraction.display.plotters.plotly as plotly_mod + from easydiffraction.display.plotting import POSTERIOR_PAIR_TITLE_FONT_SIZE from easydiffraction.display.plotting import Plotter + from easydiffraction.display.plotting import SQUARE_MATRIX_AXIS_TITLE_LINE_HEIGHT_PIXELS + from easydiffraction.display.plotting import SQUARE_MATRIX_BOTTOM_MARGIN_PIXELS + from easydiffraction.display.plotting import SQUARE_MATRIX_TITLE_YSHIFT_PIXELS + from easydiffraction.display.plotting import SQUARE_MATRIX_TOP_MARGIN_PIXELS captured = {} @@ -694,44 +2053,70 @@ class Project: p = Plotter() p.engine = 'plotly' p._set_project(Project()) - p.plot_param_correlations(threshold=0.1) + p.plot_param_correlations() fig = captured['fig'] + heatmap = fig.data[0] + text_trace = fig.data[1] + gap_width = Plotter._square_matrix_gap_data_width(2) assert len(fig.data) == 2 - assert fig.data[0].type == 'heatmap' - assert list(fig.data[0].x) == [0.0, 1.0] - assert list(fig.data[0].y) == [0.0, 1.0] - assert fig.data[0].xgap in (None, 0) - assert fig.data[0].ygap in (None, 0) - assert fig.data[0].colorbar.lenmode == 'fraction' - assert fig.data[0].colorbar.len == 1.0 - assert fig.data[0].colorbar.title.text == '' - assert fig.data[0].hovertemplate == 'x: %{x}
y: %{y}
corr: %{z:.2f}' - assert pytest.approx(fig.data[0].z[0][0], rel=1e-9) == -0.5 - assert fig.data[1].type == 'scatter' - assert fig.data[1].mode == 'text' - assert list(fig.data[1].x) == [0.5] - assert list(fig.data[1].y) == [0.5] - assert list(fig.data[1].text) == ['-0.50'] - assert fig.data[1].textposition == 'middle center' - assert fig.data[1].hoverinfo == 'skip' - assert fig.layout.xaxis.side == 'bottom' - assert fig.layout.xaxis.tickangle < 0 - assert list(fig.layout.xaxis.tickvals) == [0.5] - assert list(fig.layout.xaxis.ticktext) == ['phase.scale'] + assert heatmap.type == 'heatmap' + assert list(heatmap.x) == pytest.approx([0.0, 1.0, 1.0 + gap_width, 2.0 + gap_width]) + assert list(heatmap.y) == pytest.approx([0.0, 1.0, 1.0 + gap_width, 2.0 + gap_width]) + assert heatmap.showscale is False + assert heatmap.hovertemplate == ( + '%{customdata[0]}
%{customdata[1]}
correlation: %{z:.2f}' + ) + assert list(heatmap.customdata[2][0]) == ['phase.scale', 'phase.cell.length_c'] + assert pytest.approx(np.nanmin(np.asarray(heatmap.z, dtype=float)), rel=1e-9) == -0.5 + assert text_trace.type == 'scatter' + assert text_trace.mode == 'text' + assert list(text_trace.x) == [0.5] + assert list(text_trace.y) == pytest.approx([1.5 + gap_width]) + assert list(text_trace.text) == ['-0.50'] + assert text_trace.textposition == 'middle center' + assert text_trace.hoverinfo == 'skip' + assert [annotation.text for annotation in fig.layout.annotations] == [ + 'Refined parameter correlation matrix', + 'phase.
scale', + 'phase.
cell.
length_c', + 'phase.
scale', + 'phase.
cell.
length_c', + ] + assert fig.layout.annotations[0].font.size == POSTERIOR_PAIR_TITLE_FONT_SIZE + assert fig.layout.annotations[0].yshift == SQUARE_MATRIX_TITLE_YSHIFT_PIXELS + assert fig.layout.annotations[0].xshift == -Plotter._square_matrix_title_left_shift([ + 'phase.
scale', + 'phase.
cell.
length_c', + ]) + assert fig.layout.margin.t == SQUARE_MATRIX_TOP_MARGIN_PIXELS + assert fig.layout.margin.b == ( + SQUARE_MATRIX_BOTTOM_MARGIN_PIXELS + 2 * SQUARE_MATRIX_AXIS_TITLE_LINE_HEIGHT_PIXELS + ) + assert ( + fig.layout.meta['fixed_aspect_wrapper']['aspect_ratio'] + == Plotter._square_matrix_layout_meta( + n_parameters=2, + annotation_labels=[ + 'phase.
scale', + 'phase.
cell.
length_c', + 'phase.
scale', + 'phase.
cell.
length_c', + ], + )['fixed_aspect_wrapper']['aspect_ratio'] + ) assert fig.layout.xaxis.showline is False assert fig.layout.xaxis.mirror is False - assert fig.layout.xaxis.layer == 'above traces' - assert list(fig.layout.yaxis.tickvals) == [0.5] - assert list(fig.layout.yaxis.ticktext) == ['phase.cell.length_c'] assert fig.layout.yaxis.showline is False assert fig.layout.yaxis.mirror is False - assert fig.layout.yaxis.layer == 'above traces' - assert fig.layout.yaxis.ticklabelstandoff == 8 - assert len(fig.layout.shapes) == 1 - assert fig.layout.shapes[-1].type == 'rect' - assert fig.layout.shapes[-1].xref == 'paper' - assert fig.layout.shapes[-1].yref == 'paper' + assert fig.layout.xaxis.showticklabels is False + assert fig.layout.yaxis.showticklabels is False + assert fig.layout.xaxis.title.text is None + assert fig.layout.yaxis.title.text is None + assert fig.layout.paper_bgcolor is None + assert fig.layout.plot_bgcolor is None + assert len(fig.layout.shapes) == 3 + assert all(shape.type == 'rect' for shape in fig.layout.shapes) def test_plot_param_correlations_plotly_labels_respect_threshold(monkeypatch): @@ -785,17 +2170,152 @@ class Project: p = Plotter() p.engine = 'plotly' p._set_project(Project()) - p.plot_param_correlations() + p.plot_param_correlations(threshold=0.7) fig = captured['fig'] - assert len(fig.data) == 2 - assert fig.data[0].type == 'heatmap' - assert fig.data[1].type == 'scatter' - assert fig.data[1].mode == 'text' - assert list(fig.data[1].text) == ['-0.91', '0.83', '-0.89', '0.82'] + heatmap_traces = [trace for trace in fig.data if trace.type == 'heatmap'] + text_traces = [trace for trace in fig.data if trace.type == 'scatter' and trace.mode == 'text'] + assert len(heatmap_traces) == 1 + assert len(text_traces) == 1 + assert list(text_traces[0].text) == ['-0.91', '0.83', '-0.89', '0.82'] + assert len(fig.layout.shapes) == 15 + + +def test_plot_param_correlations_limits_default_table_to_six_parameters(monkeypatch): + from easydiffraction.display.plotting import Plotter + from easydiffraction.display.tables import TableRenderer + + captured = {} + + class FakeTabler: + def render(self, df): + captured['df'] = df + + monkeypatch.setattr(TableRenderer, 'get', staticmethod(lambda: FakeTabler())) + + class Param: + def __init__(self, uid, unique_name): + self._minimizer_uid = uid + self.unique_name = unique_name + + class RawResult: + covar = None + var_names = ['p1', 'p2', 'p3', 'p4', 'p5', 'p6'] + + class ParamResult: + def __init__(self, correl): + self.correl = correl + params = { + 'p1': ParamResult({'p2': 0.95}), + 'p2': ParamResult({'p1': 0.95, 'p3': 0.94}), + 'p3': ParamResult({'p2': 0.94, 'p4': 0.93}), + 'p4': ParamResult({'p3': 0.93, 'p5': 0.92}), + 'p5': ParamResult({'p4': 0.92, 'p6': 0.91}), + 'p6': ParamResult({'p5': 0.91}), + } + + class FitResults: + engine_result = RawResult() + parameters = [ + Param('p1', 'phase.scale'), + Param('p2', 'phase.cell.length_a'), + Param('p3', 'phase.background'), + Param('p4', 'phase.profile.u'), + Param('p5', 'phase.profile.v'), + Param('p6', 'phase.profile.w'), + ] + + class Analysis: + fit_results = FitResults() + + class Project: + analysis = Analysis() + + p = Plotter() + p.engine = 'asciichartpy' + p._set_project(Project()) + p.plot_param_correlations() + + df = captured['df'] + assert [column.strip() for column in df.columns.get_level_values(0)] == [ + 'parameter', + '1', + '2', + '3', + '4', + '5', + '6', + ] + assert list(df.index) == [0, 1, 2, 3, 4, 5] + assert list(df.iloc[:, 0]) == [ + 'phase.scale', + 'phase.cell.length_a', + 'phase.background', + 'phase.profile.u', + 'phase.profile.v', + 'phase.profile.w', + ] + assert df.iloc[0, 1] == '' + assert _strip_markup(df.iloc[1, 1]).strip() == '0.95' + assert _strip_markup(df.iloc[2, 2]).strip() == '0.94' + assert _strip_markup(df.iloc[3, 3]).strip() == '0.93' + assert _strip_markup(df.iloc[4, 4]).strip() == '0.92' + assert _strip_markup(df.iloc[5, 5]).strip() == '0.91' + assert df.iloc[5, 6] == '' + + +def test_plot_posterior_pairs_uses_default_max_parameter_limit(monkeypatch): + from easydiffraction.display.plotting import DEFAULT_CORRELATION_MAX_PARAMETERS + from easydiffraction.display.plotting import Plotter + + captured: dict[str, object] = {} + + plotter = Plotter() + + def fake_build(self, *, parameters, style, threshold, max_parameters): + captured['parameters'] = parameters + captured['style'] = style + captured['threshold'] = threshold + captured['max_parameters'] = max_parameters + return object() + + monkeypatch.setattr(Plotter, '_build_posterior_pairs_plot', fake_build) + monkeypatch.setattr(Plotter, '_show_plot_figure', lambda self, figure: None) + + plotter.plot_posterior_pairs() + + assert captured['parameters'] is None + assert captured['style'] == 'auto' + assert captured['threshold'] is None + assert captured['max_parameters'] == DEFAULT_CORRELATION_MAX_PARAMETERS + + +def test_plot_posterior_pairs_prints_title_before_ascii_backend_warning(monkeypatch): + import easydiffraction.display.plotting as plotting_mod + + from easydiffraction.display.plotting import Plotter + + events: list[tuple[str, str]] = [] + plotter = Plotter() + plotter.engine = 'asciichartpy' + + monkeypatch.setattr( + plotting_mod.console, 'paragraph', lambda text: events.append(('title', text)) + ) + monkeypatch.setattr( + plotting_mod.log, 'warning', lambda message: events.append(('warning', message)) + ) -def test_plot_param_correlations_filters_by_default_threshold(monkeypatch): + plotter.plot_posterior_pairs() + + assert events == [ + ('title', 'Posterior pair plot'), + ('warning', 'Posterior plots currently require the Plotly plotting backend.'), + ] + + +def test_plot_param_correlations_shows_full_table_when_threshold_is_zero(monkeypatch): from easydiffraction.display.plotting import Plotter from easydiffraction.display.tables import TableRenderer @@ -843,18 +2363,28 @@ class Project: p = Plotter() p.engine = 'asciichartpy' p._set_project(Project()) - p.plot_param_correlations() + p.plot_param_correlations(threshold=0) df = captured['df'] assert [column.strip() for column in df.columns.get_level_values(0)] == [ 'parameter', '1', + '2', + '3', ] - assert list(df.index) == [0, 1] + assert list(df.index) == [0, 1, 2] assert df.iloc[0, 0] == 'phase.scale' assert df.iloc[0, 1] == '' + assert df.iloc[0, 2] == '' + assert df.iloc[0, 3] == '' assert df.iloc[1, 0] == 'phase.cell.length_a' assert _strip_markup(df.iloc[1, 1]).strip() == '0.82' + assert df.iloc[1, 2] == '' + assert df.iloc[1, 3] == '' + assert df.iloc[2, 0] == 'phase.background' + assert _strip_markup(df.iloc[2, 1]).strip() == '0.25' + assert _strip_markup(df.iloc[2, 2]).strip() == '0.00' + assert df.iloc[2, 3] == '' def test_plot_param_correlations_hides_subthreshold_table_values(monkeypatch): @@ -909,7 +2439,7 @@ class Project: p = Plotter() p.engine = 'asciichartpy' p._set_project(Project()) - p.plot_param_correlations() + p.plot_param_correlations(threshold=0.7, show_diagonal=False) df = captured['df'] assert [column.strip() for column in df.columns.get_level_values(0)] == [ diff --git a/tests/unit/easydiffraction/display/test_plotting_coverage.py b/tests/unit/easydiffraction/display/test_plotting_coverage.py index 806e68da2..008881bcf 100644 --- a/tests/unit/easydiffraction/display/test_plotting_coverage.py +++ b/tests/unit/easydiffraction/display/test_plotting_coverage.py @@ -254,11 +254,13 @@ def test_unknown_name_returns_none(self): class TestAutoXRangeForAscii: - def test_narrows_range_for_ascii(self): + def test_narrows_range_for_ascii(self, monkeypatch): + from easydiffraction.display.plotters.ascii import AsciiPlotter from easydiffraction.display.plotting import Plotter p = Plotter() p.engine = 'asciichartpy' + monkeypatch.setattr(AsciiPlotter, '_chart_point_count', lambda: 80) class Ptn: intensity_meas = np.zeros(200) @@ -266,8 +268,42 @@ class Ptn: Ptn.intensity_meas[100] = 10.0 # max at index 100 x_array = np.arange(200, dtype=float) x_min, x_max = p._auto_x_range_for_ascii(Ptn(), x_array, None, None) - assert x_min == 50.0 - assert x_max == 150.0 + assert x_min == 60.0 + assert x_max == 139.0 + + def test_keeps_full_range_when_series_is_within_crop_threshold(self, monkeypatch): + from easydiffraction.display.plotters.ascii import AsciiPlotter + from easydiffraction.display.plotting import Plotter + + p = Plotter() + p.engine = 'asciichartpy' + monkeypatch.setattr(AsciiPlotter, '_chart_point_count', lambda: 80) + + class Ptn: + intensity_meas = np.zeros(120) + + Ptn.intensity_meas[60] = 10.0 + x_array = np.arange(120, dtype=float) + x_min, x_max = p._auto_x_range_for_ascii(Ptn(), x_array, None, None) + assert x_min is None + assert x_max is None + + def test_keeps_explicit_partial_limit_for_ascii(self, monkeypatch): + from easydiffraction.display.plotters.ascii import AsciiPlotter + from easydiffraction.display.plotting import Plotter + + p = Plotter() + p.engine = 'asciichartpy' + monkeypatch.setattr(AsciiPlotter, '_chart_point_count', lambda: 80) + + class Ptn: + intensity_meas = np.zeros(200) + + Ptn.intensity_meas[100] = 10.0 + x_array = np.arange(200, dtype=float) + x_min, x_max = p._auto_x_range_for_ascii(Ptn(), x_array, 20.0, None) + assert x_min == 20.0 + assert x_max is None def test_no_narrowing_when_limits_provided(self): from easydiffraction.display.plotting import Plotter @@ -329,7 +365,7 @@ def test_csv_plots_with_versus_descriptor(self, tmp_path, monkeypatch): csv = tmp_path / 'results.csv' csv.write_text( - 'my_param,my_param.uncertainty,diffrn.temperature\n1.0,0.1,300\n2.0,0.2,400\n' + 'my_param,my_param.uncertainty,diffrn.ambient_temperature\n1.0,0.1,300\n2.0,0.2,400\n' ) plot_calls = [] @@ -346,12 +382,12 @@ class ParamDesc: description = 'A param' units = 'Å' - class VersusDesc: - name = 'temperature' - description = 'Temperature' - units = 'K' - - p._plot_param_series_from_csv(str(csv), 'my_param', ParamDesc(), VersusDesc()) + p._plot_param_series_from_csv( + str(csv), + 'my_param', + ParamDesc(), + 'diffrn.ambient_temperature', + ) assert len(plot_calls) == 1 assert plot_calls[0]['x'] == [300.0, 400.0] assert plot_calls[0]['y'] == [1.0, 2.0] @@ -414,7 +450,7 @@ class Expt: }, } p.plot_param_series_from_snapshots( - 'param_a', 'ambient_temperature', experiments, snapshots + 'param_a', 'diffrn.ambient_temperature', experiments, snapshots ) assert len(plot_calls) == 1 assert plot_calls[0]['y'] == [1.23] diff --git a/tests/unit/easydiffraction/display/test_progress.py b/tests/unit/easydiffraction/display/test_progress.py new file mode 100644 index 000000000..b12147fa2 --- /dev/null +++ b/tests/unit/easydiffraction/display/test_progress.py @@ -0,0 +1,315 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Tests for display/progress.py.""" + +from __future__ import annotations + +from easydiffraction.utils.enums import VerbosityEnum + + +def test_make_display_handle_uses_terminal_live_when_available(monkeypatch): + import easydiffraction.display.progress as progress_mod + + class FakeLive: + def __init__( + self, + renderable=None, + *, + console, + auto_refresh, + refresh_per_second, + get_renderable=None, + vertical_overflow=None, + ): + self.renderable = renderable + self.console = console + self.auto_refresh = auto_refresh + self.refresh_per_second = refresh_per_second + self.get_renderable = get_renderable + self.started = False + self.stopped = False + self.refresh_calls = 0 + + def start(self): + self.started = True + + def stop(self): + self.stopped = True + + def refresh(self): + self.refresh_calls += 1 + + monkeypatch.setattr(progress_mod, 'in_jupyter', lambda: False) + monkeypatch.setattr(progress_mod, 'Live', FakeLive) + monkeypatch.setattr(progress_mod.ConsoleManager, 'get', lambda: 'console') + + handle = progress_mod.make_display_handle() + + assert isinstance(handle, progress_mod._TerminalLiveHandle) + assert handle._live.console == 'console' + assert handle._live.auto_refresh is True + assert handle._live.get_renderable is not None + assert handle._live.started is True + + handle.update('content') + + assert handle._live.refresh_calls == 1 + assert handle._live.get_renderable() == 'content' + + handle.close() + + assert handle._live.stopped is True + + +def test_make_display_handle_passes_auto_refresh(monkeypatch): + import easydiffraction.display.progress as progress_mod + + class FakeLive: + def __init__( + self, + renderable=None, + *, + console, + auto_refresh, + refresh_per_second, + get_renderable=None, + vertical_overflow=None, + ): + self.auto_refresh = auto_refresh + self.started = False + + def start(self): + self.started = True + + def stop(self): + self.started = False + + def refresh(self): + pass + + monkeypatch.setattr(progress_mod, 'in_jupyter', lambda: False) + monkeypatch.setattr(progress_mod, 'Live', FakeLive) + monkeypatch.setattr(progress_mod.ConsoleManager, 'get', lambda: 'console') + + handle = progress_mod.make_display_handle(auto_refresh=False) + + assert isinstance(handle, progress_mod._TerminalLiveHandle) + assert handle._live.auto_refresh is False + + +def test_activity_indicator_silent_does_not_create_handles(): + from easydiffraction.display.progress import ActivityIndicator + + indicator = ActivityIndicator(verbosity=VerbosityEnum.SILENT) + indicator.start() + + assert indicator._live is None + assert indicator._display_handle is None + + +def test_activity_indicator_html_style_prevents_stretching(): + import easydiffraction.display.progress as progress_mod + + indicator = progress_mod.ActivityIndicator(verbosity=VerbosityEnum.FULL) + style = indicator._html_style() + + assert 'align-items: flex-start;' in style + assert progress_mod.ACTIVITY_ACCENT_COLOR in style + assert 'font-weight: 400;' in style + assert 'var(--jp-ui-font-family' in style + + +def test_activity_indicator_terminal_line_uses_accent_style(): + import easydiffraction.display.progress as progress_mod + + indicator = progress_mod.ActivityIndicator(label='Fitting...', verbosity=VerbosityEnum.FULL) + indicator._running = True + + line = indicator._terminal_indicator_line() + + assert line is not None + assert line.style == progress_mod.ACTIVITY_TERMINAL_STYLE + assert 'bold' not in str(line.style) + + +def test_activity_indicator_terminal_line_is_static_when_not_animated(): + import easydiffraction.display.progress as progress_mod + + indicator = progress_mod.ActivityIndicator( + label='Fitting...', + verbosity=VerbosityEnum.FULL, + animated=False, + ) + indicator._running = True + + line = indicator._terminal_indicator_line() + + assert line is not None + assert line.plain == 'Fitting...' + + +def test_activity_indicator_terminal_live_uses_dynamic_renderable(monkeypatch): + import easydiffraction.display.progress as progress_mod + + class FakeLive: + def __init__( + self, + renderable=None, + *, + console, + auto_refresh, + refresh_per_second, + get_renderable=None, + vertical_overflow=None, + ): + self.renderable = renderable + self.console = console + self.auto_refresh = auto_refresh + self.refresh_per_second = refresh_per_second + self.get_renderable = get_renderable + self.refresh_calls = 0 + self.started = False + + def start(self): + self.started = True + + def stop(self): + self.started = False + + def refresh(self): + self.refresh_calls += 1 + + monkeypatch.setattr(progress_mod, 'in_jupyter', lambda: False) + monkeypatch.setattr(progress_mod, 'Live', FakeLive) + monkeypatch.setattr(progress_mod.ConsoleManager, 'get', lambda: 'console') + + indicator = progress_mod.ActivityIndicator(label='Fitting...', verbosity=VerbosityEnum.FULL) + indicator.start() + indicator._current_frame = lambda: 'X' + + assert indicator._live is not None + assert indicator._live.get_renderable is not None + assert indicator._live.renderable is None + assert indicator._live.get_renderable().plain == 'X Fitting...' + + indicator.update(label='Sampling...') + + assert indicator._live.refresh_calls == 1 + assert indicator._live.get_renderable().plain == 'X Sampling...' + + +def test_activity_indicator_terminal_live_disables_auto_refresh_when_not_animated( + monkeypatch, +): + import easydiffraction.display.progress as progress_mod + + class FakeLive: + def __init__( + self, + renderable=None, + *, + console, + auto_refresh, + refresh_per_second, + get_renderable=None, + vertical_overflow=None, + ): + self.renderable = renderable + self.console = console + self.auto_refresh = auto_refresh + self.refresh_per_second = refresh_per_second + self.get_renderable = get_renderable + self.refresh_calls = 0 + self.started = False + + def start(self): + self.started = True + + def stop(self): + self.started = False + + def refresh(self): + self.refresh_calls += 1 + + monkeypatch.setattr(progress_mod, 'in_jupyter', lambda: False) + monkeypatch.setattr(progress_mod, 'Live', FakeLive) + monkeypatch.setattr(progress_mod.ConsoleManager, 'get', lambda: 'console') + + indicator = progress_mod.ActivityIndicator( + label='Fitting...', + verbosity=VerbosityEnum.FULL, + animated=False, + ) + indicator.start() + + assert indicator._live is not None + assert indicator._live.auto_refresh is False + assert indicator._live.get_renderable is not None + assert indicator._live.get_renderable().plain == 'Fitting...' + + +def test_activity_indicator_render_html_uses_current_label(): + from easydiffraction.display.progress import ActivityIndicator + + indicator = ActivityIndicator(label='Fitting...', verbosity=VerbosityEnum.FULL) + indicator._running = True + + html = indicator._render_html() + + assert 'Fitting...' in html + + +def test_activity_indicator_updates_shared_terminal_handle_without_ipython(monkeypatch): + import easydiffraction.display.progress as progress_mod + + class FakeLive: + def __init__( + self, + renderable=None, + *, + console, + auto_refresh, + refresh_per_second, + get_renderable=None, + vertical_overflow=None, + ): + self.renderable = renderable + self.console = console + self.auto_refresh = auto_refresh + self.refresh_per_second = refresh_per_second + self.get_renderable = get_renderable + self.refresh_calls = 0 + self.started = False + + def start(self): + self.started = True + + def stop(self): + self.started = False + + def refresh(self): + self.refresh_calls += 1 + + monkeypatch.setattr(progress_mod, 'in_jupyter', lambda: False) + monkeypatch.setattr(progress_mod, 'HTML', None) + monkeypatch.setattr(progress_mod, 'DisplayHandle', None) + monkeypatch.setattr(progress_mod, 'Live', FakeLive) + monkeypatch.setattr(progress_mod.ConsoleManager, 'get', lambda: 'console') + + handle = progress_mod.make_display_handle() + indicator = progress_mod.ActivityIndicator( + label='Fitting...', + verbosity=VerbosityEnum.FULL, + display_handle=handle, + ) + indicator._current_frame = lambda: 'X' + + indicator.start() + + assert handle._live.refresh_calls == 1 + assert handle._live.get_renderable().plain == 'X Fitting...' + + indicator.update(label='Sampling...') + + assert handle._live.refresh_calls == 2 + assert handle._live.get_renderable().plain == 'X Sampling...' diff --git a/tests/unit/easydiffraction/display/test_tables.py b/tests/unit/easydiffraction/display/test_tables.py index 2fb16fe13..9920e7e12 100644 --- a/tests/unit/easydiffraction/display/test_tables.py +++ b/tests/unit/easydiffraction/display/test_tables.py @@ -2,6 +2,8 @@ # SPDX-License-Identifier: BSD-3-Clause """Tests for display/tables.py (TableEngineEnum, TableRenderer, TableRendererFactory).""" +from types import SimpleNamespace + import pandas as pd @@ -59,3 +61,30 @@ def test_render(self, monkeypatch, capsys): # Reset singleton to not leak state monkeypatch.setattr(TableRenderer, '_instance', None) + + def test_build_renderable_normalizes_dataframe(self, monkeypatch): + from easydiffraction.display.tables import TableRenderer + + monkeypatch.setattr(TableRenderer, '_instance', None) + + headers = [('Col', 'left')] + df = pd.DataFrame([['val']], columns=pd.MultiIndex.from_tuples(headers)) + calls: dict[str, object] = {} + + def fake_build_renderable(alignments, prepared_df): + calls['alignments'] = list(alignments) + calls['columns'] = list(prepared_df.columns) + calls['index'] = list(prepared_df.index) + return 'renderable' + + renderer = TableRenderer.get() + renderer._backend = SimpleNamespace(build_renderable=fake_build_renderable) + + assert renderer.build_renderable(df) == 'renderable' + assert calls == { + 'alignments': ['left'], + 'columns': ['Col'], + 'index': [1], + } + + monkeypatch.setattr(TableRenderer, '_instance', None) diff --git a/tests/unit/easydiffraction/io/cif/test_serialize.py b/tests/unit/easydiffraction/io/cif/test_serialize.py index 3a24b6ce2..375e5aa86 100644 --- a/tests/unit/easydiffraction/io/cif/test_serialize.py +++ b/tests/unit/easydiffraction/io/cif/test_serialize.py @@ -27,7 +27,7 @@ def __init__(self): self.value = 3 p = P() - assert MUT.param_to_cif(p) == '_x.y 3.' + assert MUT.param_to_cif(p) == '_x.y 3' def test_format_param_value_with_uncertainty_uses_two_sig_digits(): diff --git a/tests/unit/easydiffraction/io/cif/test_serialize_category_owner_baseline.py b/tests/unit/easydiffraction/io/cif/test_serialize_category_owner_baseline.py new file mode 100644 index 000000000..7d45dfbd3 --- /dev/null +++ b/tests/unit/easydiffraction/io/cif/test_serialize_category_owner_baseline.py @@ -0,0 +1,98 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + +from __future__ import annotations + +from easydiffraction.datablocks.experiment.item.factory import ExperimentFactory +from easydiffraction.datablocks.structure.item.base import Structure +from easydiffraction.project.project import Project + + +def test_real_structure_as_cif_starts_with_data_header() -> None: + structure = Structure(name='nickelate') + + assert structure.as_cif.startswith('data_nickelate') + + +def test_real_experiment_as_cif_starts_with_data_header() -> None: + experiment = ExperimentFactory.from_scratch( + name='powder_scan', + sample_form='powder', + beam_mode='constant wavelength', + radiation_probe='neutron', + scattering_type='bragg', + ) + + assert experiment.as_cif.startswith('data_powder_scan') + + +def test_real_analysis_as_cif_is_singleton_section_without_data_header() -> None: + project = Project(name='proj') + + analysis_cif = project.analysis.as_cif + + assert analysis_cif.startswith('_fitting.mode_type single') + assert not analysis_cif.startswith('data_') + assert '_fitting.minimizer_type' in analysis_cif + assert '_joint_fit.experiment_id' not in analysis_cif + assert '_sequential_fit.data_dir' not in analysis_cif + assert '_sequential_fit_extract.id' not in analysis_cif + + +def test_real_analysis_as_cif_includes_aliases_and_constraints_when_present() -> None: + project = Project(name='proj') + project.structures.create(name='phase_1') + parameter = project.structures['phase_1'].cell.length_a + + analysis = project.analysis + analysis.aliases.create(label='a_param', param=parameter) + analysis.constraints.create(expression='a_param = a_param') + + analysis_cif = analysis.as_cif + + assert '_alias.label' in analysis_cif + assert '_alias.param_unique_name' in analysis_cif + assert '_constraint.id' in analysis_cif + assert '_constraint.expression' in analysis_cif + assert 'a_param = a_param' in analysis_cif + + +def test_real_analysis_as_cif_includes_joint_fit_only_in_joint_mode() -> None: + project = Project(name='proj') + analysis = project.analysis + analysis._set_fitting_mode_type('joint') + analysis.joint_fit.create(experiment_id='ex1', weight=0.5) + + analysis_cif = analysis.as_cif + + assert not analysis_cif.startswith('data_') + assert '_fitting.mode_type joint' in analysis_cif + assert '_joint_fit.experiment_id' in analysis_cif + assert '_joint_fit.weight' in analysis_cif + assert '_sequential_fit.data_dir' not in analysis_cif + assert '_sequential_fit_extract.id' not in analysis_cif + + +def test_real_analysis_as_cif_includes_sequential_sections_only_in_sequential_mode() -> None: + project = Project(name='proj') + analysis = project.analysis + analysis._set_fitting_mode_type('sequential') + analysis.sequential_fit.data_dir.value = 'scans' + analysis.sequential_fit.file_pattern.value = '*.xye' + analysis.sequential_fit_extract.create( + id='temperature', + target='diffrn.ambient_temperature', + pattern=r'temp_(\d+)', + required=True, + ) + + analysis_cif = analysis.as_cif + + assert not analysis_cif.startswith('data_') + assert '_fitting.mode_type sequential' in analysis_cif + assert '_sequential_fit.data_dir scans' in analysis_cif + assert '_sequential_fit.file_pattern *.xye' in analysis_cif + assert '_sequential_fit_extract.id' in analysis_cif + assert '_sequential_fit_extract.target' in analysis_cif + assert 'diffrn.ambient_temperature' in analysis_cif + assert '_joint_fit.experiment_id' not in analysis_cif diff --git a/tests/unit/easydiffraction/io/cif/test_serialize_more.py b/tests/unit/easydiffraction/io/cif/test_serialize_more.py index 0f98fe1a7..3c9db1bc1 100644 --- a/tests/unit/easydiffraction/io/cif/test_serialize_more.py +++ b/tests/unit/easydiffraction/io/cif/test_serialize_more.py @@ -34,12 +34,56 @@ def __init__(self): out = MUT.datablock_item_to_cif(DB()) assert out.startswith('data_block1') - assert '_aa 42.' in out + assert '_aa 42' in out assert 'loop_' in out assert '_aa' in out assert '7' in out +def test_datablock_item_to_cif_skips_empty_category_fragments(): + import easydiffraction.io.cif.serialize as MUT + from easydiffraction.core.category import CategoryCollection + from easydiffraction.core.category import CategoryItem + from easydiffraction.io.cif.handler import CifHandler + + class Item(CategoryItem): + def __init__(self, val): + super().__init__() + self._p = type('P', (), {})() + self._p._cif_handler = CifHandler(names=['_aa']) + self._p.value = val + + @property + def parameters(self): + return [self._p] + + @property + def as_cif(self) -> str: + return MUT.category_item_to_cif(self) + + class EmptyItem(CategoryItem): + @property + def parameters(self): + return [] + + @property + def as_cif(self) -> str: + return '' + + class DB: + def __init__(self): + self._identity = type('I', (), {'datablock_entry_name': 'block1'})() + self.item = Item(42) + self.empty_item = EmptyItem() + self.coll = CategoryCollection(item_type=Item) + self.coll['row1'] = Item(7) + self.empty_coll = CategoryCollection(item_type=Item) + + out = MUT.datablock_item_to_cif(DB()) + assert out == 'data_block1\n\n_aa 42\n\nloop_\n_aa\n7' + assert '\n\n\n' not in out + + def test_datablock_collection_to_cif_concatenates_blocks(): import easydiffraction.io.cif.serialize as MUT @@ -63,11 +107,24 @@ def test_project_info_to_cif_contains_core_fields(): info = ProjectInfo(name='p1', title='My Title', description='Some description text') out = MUT.project_info_to_cif(info) assert '_project.id p1' in out - assert '_project.title' in out - assert 'My Title' in out - assert '_project.description' in out - assert '_project.created' in out - assert '_project.last_modified' in out + assert '_project.title "My Title"' in out + assert '_project.description "Some description text"' in out + assert '_project.created "' in out + assert '_project.last_modified "' in out + + +def test_project_info_to_cif_wraps_long_description_as_text_field(): + import easydiffraction.io.cif.serialize as MUT + from easydiffraction.project.project_info import ProjectInfo + + description = ' '.join(['long'] * 20) + info = ProjectInfo(name='p1', title='My Title', description=description) + + out = MUT.project_info_to_cif(info) + + assert '_project.description ' in out + assert '\n;\n' in out + assert 'long long long long long long long long long long long long' in out def test_experiment_to_cif_with_and_without_data(): @@ -115,12 +172,11 @@ def as_cif(self): out_without = MUT.experiment_to_cif(Exp('')) assert out_without.startswith('data_expA') - assert out_without.endswith('1.') + assert out_without.endswith('1') def test_analysis_to_cif_renders_all_sections(): import easydiffraction.io.cif.serialize as MUT - from easydiffraction.analysis.categories.joint_fit_experiments import JointFitExperiments class Obj: def __init__(self, t): @@ -131,16 +187,16 @@ def as_cif(self): return self._t class A: - fit = Obj('_fit.minimizer_type lmfit\n_fit.mode single') - joint_fit_experiments = JointFitExperiments() + fitting_mode_type = 'single' + fitting = Obj('_fitting.minimizer_type lmfit') aliases = Obj('ALIASES') constraints = Obj('CONSTRAINTS') out = MUT.analysis_to_cif(A()) - lines = out.splitlines() - assert lines[0].startswith('_fit.minimizer_type') - assert 'lmfit' in lines[0] - assert lines[1].startswith('_fit.mode') - assert 'single' in lines[1] + lines = [line for line in out.splitlines() if line] + assert lines[0].startswith('_fitting.mode_type') + assert 'single' in lines[0] + assert lines[1].startswith('_fitting.minimizer_type') + assert 'lmfit' in lines[1] assert 'ALIASES' in out assert 'CONSTRAINTS' in out diff --git a/tests/unit/easydiffraction/io/test_ascii.py b/tests/unit/easydiffraction/io/test_ascii.py index 310542d15..c76e8fcf4 100644 --- a/tests/unit/easydiffraction/io/test_ascii.py +++ b/tests/unit/easydiffraction/io/test_ascii.py @@ -5,6 +5,7 @@ from __future__ import annotations import zipfile +from pathlib import Path import numpy as np import pytest @@ -13,6 +14,7 @@ from easydiffraction.io.ascii import extract_data_paths_from_zip from easydiffraction.io.ascii import extract_project_from_zip from easydiffraction.io.ascii import load_numeric_block +from easydiffraction.project.project import Project class TestLoadNumericBlock: @@ -183,6 +185,34 @@ def test_destination_creates_directory(self, tmp_path): assert len(paths) == 1 assert dest.is_dir() + def test_relative_destination_does_not_depend_on_current_project(self, tmp_path, monkeypatch): + """Relative destinations are resolved from cwd, not project state.""" + zip_path = tmp_path / 'test.zip' + with zipfile.ZipFile(zip_path, 'w') as zf: + zf.writestr('scan_001.dat', '1 2 3\n') + + workspace = tmp_path / 'workspace' + workspace.mkdir() + monkeypatch.chdir(workspace) + + original_current_project = Project._current_project + try: + Project._loading = True + project_one = Project() + project_two = Project() + finally: + Project._loading = False + + try: + project_one.save_as(str(tmp_path / 'project-one')) + project_two.save_as(str(tmp_path / 'project-two')) + paths = extract_data_paths_from_zip(zip_path, destination='data/d20_scan') + finally: + Project._current_project = original_current_project + + assert len(paths) == 1 + assert Path(paths[0]).parent == (workspace / 'data' / 'd20_scan').resolve() + def test_raises_file_not_found(self, tmp_path): """Raises FileNotFoundError for missing ZIP path.""" with pytest.raises(FileNotFoundError): @@ -239,6 +269,21 @@ def test_lists_files_in_directory(self, tmp_path): assert 'scan_001.dat' in paths[0] assert 'scan_002.dat' in paths[1] + def test_lists_absolute_paths_for_relative_directory(self, tmp_path, monkeypatch): + """Returns absolute paths even when the input directory is relative.""" + data_dir = tmp_path / 'scans' + data_dir.mkdir() + (data_dir / 'scan_002.dat').write_text('2\n') + (data_dir / 'scan_001.dat').write_text('1\n') + monkeypatch.chdir(tmp_path) + + paths = extract_data_paths_from_dir('scans') + + assert paths == [ + str((data_dir / 'scan_001.dat').resolve()), + str((data_dir / 'scan_002.dat').resolve()), + ] + def test_raises_for_missing_directory(self, tmp_path): """Raises FileNotFoundError for non-existent directory.""" with pytest.raises(FileNotFoundError): diff --git a/tests/unit/easydiffraction/io/test_results_sidecar.py b/tests/unit/easydiffraction/io/test_results_sidecar.py new file mode 100644 index 000000000..556e21e5a --- /dev/null +++ b/tests/unit/easydiffraction/io/test_results_sidecar.py @@ -0,0 +1,138 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Tests for persisted Bayesian results sidecars.""" + +from __future__ import annotations + +from pathlib import Path +from types import SimpleNamespace + +import numpy as np + + +def _analysis_with_predictive_sidecar() -> object: + from easydiffraction.analysis.categories.bayesian_predictive_datasets.default import ( + BayesianPredictiveDatasetPaths, + BayesianPredictiveDatasets, + ) + from easydiffraction.analysis.categories.bayesian_result.default import BayesianResult + from easydiffraction.analysis.categories.fit_result.default import FitResult + + fit_result = FitResult() + fit_result._set_result_kind('bayesian') + bayesian_result = BayesianResult() + bayesian_result._set_has_posterior_predictive(value=True) + predictive = BayesianPredictiveDatasets() + predictive.create( + experiment_name='hrpt', + x_axis_name='two_theta', + paths=BayesianPredictiveDatasetPaths( + x_path='/predictive/hrpt/x', + best_sample_prediction_path='/predictive/hrpt/best_sample_prediction', + lower_95_path='/predictive/hrpt/lower_95', + upper_95_path='/predictive/hrpt/upper_95', + ), + n_x=2, + n_draws_cached=0, + ) + return SimpleNamespace( + fit_result=fit_result, + bayesian_result=bayesian_result, + bayesian_convergence=SimpleNamespace( + n_draws=SimpleNamespace(value=0), + n_chains=SimpleNamespace(value=0), + n_parameters=SimpleNamespace(value=0), + ), + bayesian_distribution_caches=[], + bayesian_pair_caches=[], + bayesian_predictive_datasets=predictive, + fit_results=SimpleNamespace( + posterior_predictive={ + 'hrpt': SimpleNamespace( + experiment_name='hrpt', + x_axis_name='two_theta', + x=np.asarray([1.0, 2.0]), + best_sample_prediction=np.asarray([3.0, 4.0]), + lower_95=np.asarray([2.5, 3.5]), + upper_95=np.asarray([3.5, 4.5]), + lower_68=None, + upper_68=None, + draws=None, + ) + } + ), + _persisted_fit_state_sidecar={}, + _has_persisted_fit_state=lambda: True, + ) + + +def test_write_and_read_analysis_results_sidecar_round_trip_predictive(tmp_path): + from easydiffraction.io.results_sidecar import read_analysis_results_sidecar + from easydiffraction.io.results_sidecar import write_analysis_results_sidecar + + analysis_dir = Path(tmp_path) + analysis = _analysis_with_predictive_sidecar() + + write_analysis_results_sidecar(analysis=analysis, analysis_dir=analysis_dir) + + sidecar_path = analysis_dir / 'results.h5' + assert sidecar_path.is_file() + + restored = _analysis_with_predictive_sidecar() + restored.fit_results = None + read_analysis_results_sidecar(analysis=restored, analysis_dir=analysis_dir) + + assert 'predictive_datasets' in restored._persisted_fit_state_sidecar + dataset = restored._persisted_fit_state_sidecar['predictive_datasets']['hrpt'] + assert np.allclose(dataset['x'], np.asarray([1.0, 2.0])) + assert np.allclose(dataset['best_sample_prediction'], np.asarray([3.0, 4.0])) + + +def test_read_analysis_results_sidecar_warns_when_expected_file_is_missing(tmp_path, monkeypatch): + from easydiffraction.io import results_sidecar as results_sidecar_mod + + analysis = _analysis_with_predictive_sidecar() + warnings: list[str] = [] + monkeypatch.setattr(results_sidecar_mod.log, 'warning', warnings.append) + + results_sidecar_mod.read_analysis_results_sidecar( + analysis=analysis, + analysis_dir=Path(tmp_path), + ) + + assert analysis._persisted_fit_state_sidecar == {} + assert any('Expected Bayesian results sidecar is missing' in warning for warning in warnings) + + +def test_sidecar_path_traversal_falls_back_to_local_results_file(tmp_path, monkeypatch): + from easydiffraction.io import results_sidecar as results_sidecar_mod + + analysis_dir = Path(tmp_path) / 'analysis' + external_sidecar = Path(tmp_path) / 'outside.h5' + analysis = _analysis_with_predictive_sidecar() + analysis.bayesian_result._set_sidecar_file('../outside.h5') + + warnings: list[str] = [] + monkeypatch.setattr(results_sidecar_mod.log, 'warning', warnings.append) + + results_sidecar_mod.write_analysis_results_sidecar( + analysis=analysis, + analysis_dir=analysis_dir, + ) + + assert (analysis_dir / 'results.h5').is_file() + assert not external_sidecar.exists() + + restored = _analysis_with_predictive_sidecar() + restored.fit_results = None + restored.bayesian_result._set_sidecar_file('../outside.h5') + results_sidecar_mod.read_analysis_results_sidecar( + analysis=restored, + analysis_dir=analysis_dir, + ) + + assert 'predictive_datasets' in restored._persisted_fit_state_sidecar + assert any( + 'Ignoring Bayesian sidecar file path outside the analysis directory' in warning + for warning in warnings + ) diff --git a/tests/unit/easydiffraction/project/categories/display/test_default.py b/tests/unit/easydiffraction/project/categories/display/test_default.py deleted file mode 100644 index c43171e31..000000000 --- a/tests/unit/easydiffraction/project/categories/display/test_default.py +++ /dev/null @@ -1,55 +0,0 @@ -# SPDX-FileCopyrightText: 2026 EasyScience contributors -# SPDX-License-Identifier: BSD-3-Clause - -import gemmi - - -def test_display_defaults(): - from easydiffraction.project.categories.display.default import Display - - display = Display() - - assert display.type_info.tag == 'default' - assert display._identity.category_code == 'display' - assert display.plotter_type.value == display.plotter.engine - assert display.tabler_type.value == display.tabler.engine - - -def test_display_plotter_binds_parent(): - from easydiffraction.project.categories.display.default import Display - - display = Display() - parent = object() - display._parent = parent - - plotter = display.plotter - - assert plotter._project is parent - - -def test_display_setters_update_engines(): - from easydiffraction.project.categories.display.default import Display - - display = Display() - - display.plotter_type = 'plotly' - display.tabler_type = 'rich' - - assert display.plotter_type.value == 'plotly' - assert display.plotter.engine == 'plotly' - assert display.tabler_type.value == 'rich' - assert display.tabler.engine == 'rich' - - -def test_display_from_cif_restores_types(): - from easydiffraction.project.categories.display.default import Display - - display = Display() - block = gemmi.cif.read_string( - 'data_test\n_display.plotter_type plotly\n_display.tabler_type rich\n', - ).sole_block() - - display.from_cif(block) - - assert display.plotter_type.value == 'plotly' - assert display.tabler_type.value == 'rich' diff --git a/tests/unit/easydiffraction/project/categories/display/test_factory.py b/tests/unit/easydiffraction/project/categories/display/test_factory.py deleted file mode 100644 index b335a0530..000000000 --- a/tests/unit/easydiffraction/project/categories/display/test_factory.py +++ /dev/null @@ -1,23 +0,0 @@ -# SPDX-FileCopyrightText: 2026 EasyScience contributors -# SPDX-License-Identifier: BSD-3-Clause - -import pytest - - -def test_display_factory_default_and_create(): - from easydiffraction.project.categories.display.default import Display - from easydiffraction.project.categories.display.factory import DisplayFactory - - assert DisplayFactory.default_tag() == 'default' - assert 'default' in DisplayFactory.supported_tags() - - display = DisplayFactory.create('default') - - assert isinstance(display, Display) - - -def test_display_factory_rejects_unknown_tag(): - from easydiffraction.project.categories.display.factory import DisplayFactory - - with pytest.raises(ValueError, match=r"Unsupported type: 'missing'"): - DisplayFactory.create('missing') diff --git a/tests/unit/easydiffraction/project/categories/info/test_default.py b/tests/unit/easydiffraction/project/categories/info/test_default.py new file mode 100644 index 000000000..b480ca16d --- /dev/null +++ b/tests/unit/easydiffraction/project/categories/info/test_default.py @@ -0,0 +1,63 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + +from __future__ import annotations + +import datetime + +import gemmi + + +def test_project_info_defaults_and_identity(): + from easydiffraction.project.categories.info.default import ProjectInfo + + info = ProjectInfo(name='beer', title='Beer title', description='Some description') + + assert info.type_info.tag == 'default' + assert info._identity.category_code == 'project' + assert info.name == 'beer' + assert info.title == 'Beer title' + assert info.description == 'Some description' + assert info.path is None + assert isinstance(info.created, datetime.datetime) + assert isinstance(info.last_modified, datetime.datetime) + assert info.created.tzinfo is not None + assert info.last_modified.tzinfo is not None + + +def test_project_info_setters_and_from_cif_restore_fields(): + from easydiffraction.project.categories.info.default import ProjectInfo + + info = ProjectInfo() + info.description = 'Some spaced\n description' + info.path = 'project-dir' + + assert info.description == 'Some spaced description' + assert info.path is not None + assert info.path.name == 'project-dir' + + block = gemmi.cif.read_string( + """data_test +_project.id beer +_project.title 'Beer title' +_project.description 'Some description' +_project.created '17 May 2026 11:13:21' +_project.last_modified '17 May 2026 11:13:51' +""", + ).sole_block() + + info.from_cif(block) + + assert info.name == 'beer' + assert info.title == 'Beer title' + assert info.description == 'Some description' + assert info.created == datetime.datetime(2026, 5, 17, 11, 13, 21, tzinfo=datetime.UTC) + assert info.last_modified == datetime.datetime( + 2026, + 5, + 17, + 11, + 13, + 51, + tzinfo=datetime.UTC, + ) diff --git a/tests/unit/easydiffraction/project/categories/info/test_factory.py b/tests/unit/easydiffraction/project/categories/info/test_factory.py new file mode 100644 index 000000000..9010e2f85 --- /dev/null +++ b/tests/unit/easydiffraction/project/categories/info/test_factory.py @@ -0,0 +1,32 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + +from __future__ import annotations + +import pytest + + +def test_project_info_factory_default_and_create(): + from easydiffraction.project.categories.info.default import ProjectInfo + from easydiffraction.project.categories.info.factory import ProjectInfoFactory + + assert ProjectInfoFactory.default_tag() == 'default' + assert 'default' in ProjectInfoFactory.supported_tags() + + info = ProjectInfoFactory.create( + 'default', + name='beer', + title='Beer title', + description='Some description', + ) + + assert isinstance(info, ProjectInfo) + assert info.name == 'beer' + assert info.title == 'Beer title' + + +def test_project_info_factory_rejects_unknown_tag(): + from easydiffraction.project.categories.info.factory import ProjectInfoFactory + + with pytest.raises(ValueError, match=r"Unsupported type: 'missing'"): + ProjectInfoFactory.create('missing') diff --git a/tests/unit/easydiffraction/project/categories/rendering/test_default.py b/tests/unit/easydiffraction/project/categories/rendering/test_default.py new file mode 100644 index 000000000..07a168ae2 --- /dev/null +++ b/tests/unit/easydiffraction/project/categories/rendering/test_default.py @@ -0,0 +1,69 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + +import gemmi + + +def test_rendering_defaults(): + from easydiffraction.display.plotting import PlotterEngineEnum + from easydiffraction.display.tables import TableEngineEnum + from easydiffraction.project.categories.rendering.default import Rendering + + rendering = Rendering() + + assert rendering.type_info.tag == 'default' + assert rendering._identity.category_code == 'rendering' + assert rendering.chart_engine.value == 'auto' + assert rendering.table_engine.value == 'auto' + assert rendering.plotter.engine in [member.value for member in PlotterEngineEnum] + assert rendering.tabler.engine in [member.value for member in TableEngineEnum] + + +def test_rendering_plotter_binds_parent(): + from easydiffraction.project.categories.rendering.default import Rendering + + rendering = Rendering() + parent = object() + rendering._parent = parent + + plotter = rendering.plotter + + assert plotter._project is parent + + +def test_rendering_setters_update_engines(): + from easydiffraction.display.plotting import PlotterEngineEnum + from easydiffraction.display.tables import TableEngineEnum + from easydiffraction.project.categories.rendering.default import Rendering + + rendering = Rendering() + + rendering.chart_engine = 'plotly' + rendering.table_engine = 'rich' + + assert rendering.chart_engine.value == 'plotly' + assert rendering.plotter.engine == 'plotly' + assert rendering.table_engine.value == 'rich' + assert rendering.tabler.engine == 'rich' + + rendering.chart_engine = 'auto' + rendering.table_engine = 'auto' + + assert rendering.chart_engine.value == 'auto' + assert rendering.table_engine.value == 'auto' + assert rendering.plotter.engine == PlotterEngineEnum.default().value + assert rendering.tabler.engine == TableEngineEnum.default().value + + +def test_rendering_from_cif_restores_types(): + from easydiffraction.project.categories.rendering.default import Rendering + + rendering = Rendering() + block = gemmi.cif.read_string( + 'data_test\n_rendering.chart_engine plotly\n_rendering.table_engine rich\n', + ).sole_block() + + rendering.from_cif(block) + + assert rendering.chart_engine.value == 'plotly' + assert rendering.table_engine.value == 'rich' diff --git a/tests/unit/easydiffraction/project/categories/rendering/test_factory.py b/tests/unit/easydiffraction/project/categories/rendering/test_factory.py new file mode 100644 index 000000000..0f2812e5c --- /dev/null +++ b/tests/unit/easydiffraction/project/categories/rendering/test_factory.py @@ -0,0 +1,23 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + +import pytest + + +def test_rendering_factory_default_and_create(): + from easydiffraction.project.categories.rendering.default import Rendering + from easydiffraction.project.categories.rendering.factory import RenderingFactory + + assert RenderingFactory.default_tag() == 'default' + assert 'default' in RenderingFactory.supported_tags() + + rendering = RenderingFactory.create('default') + + assert isinstance(rendering, Rendering) + + +def test_rendering_factory_rejects_unknown_tag(): + from easydiffraction.project.categories.rendering.factory import RenderingFactory + + with pytest.raises(ValueError, match=r"Unsupported type: 'missing'"): + RenderingFactory.create('missing') diff --git a/tests/unit/easydiffraction/project/categories/verbosity/test_default.py b/tests/unit/easydiffraction/project/categories/verbosity/test_default.py new file mode 100644 index 000000000..782bdbbf8 --- /dev/null +++ b/tests/unit/easydiffraction/project/categories/verbosity/test_default.py @@ -0,0 +1,44 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + +from __future__ import annotations + +import gemmi +import pytest + + +def test_verbosity_defaults_and_cif_output(): + from easydiffraction.project.categories.verbosity.default import Verbosity + + verbosity = Verbosity() + + assert verbosity.type_info.tag == 'default' + assert verbosity._identity.category_code == 'verbosity' + assert verbosity.fit.value == 'full' + assert '_verbosity.fit full' in verbosity.as_cif + + +def test_verbosity_setter_validates_enum_values(): + from easydiffraction.project.categories.verbosity.default import Verbosity + + verbosity = Verbosity() + + verbosity.fit = 'short' + assert verbosity.fit.value == 'short' + + verbosity.fit = 'silent' + assert verbosity.fit.value == 'silent' + + with pytest.raises(ValueError, match="'verbose' is not a valid VerbosityEnum"): + verbosity.fit = 'verbose' + + +def test_verbosity_from_cif_restores_fit_value(): + from easydiffraction.project.categories.verbosity.default import Verbosity + + verbosity = Verbosity() + block = gemmi.cif.read_string('data_test\n_verbosity.fit short\n').sole_block() + + verbosity.from_cif(block) + + assert verbosity.fit.value == 'short' diff --git a/tests/unit/easydiffraction/project/categories/verbosity/test_factory.py b/tests/unit/easydiffraction/project/categories/verbosity/test_factory.py new file mode 100644 index 000000000..3d3d6941b --- /dev/null +++ b/tests/unit/easydiffraction/project/categories/verbosity/test_factory.py @@ -0,0 +1,25 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + +from __future__ import annotations + +import pytest + + +def test_verbosity_factory_default_and_create(): + from easydiffraction.project.categories.verbosity.default import Verbosity + from easydiffraction.project.categories.verbosity.factory import VerbosityFactory + + assert VerbosityFactory.default_tag() == 'default' + assert 'default' in VerbosityFactory.supported_tags() + + verbosity = VerbosityFactory.create('default') + + assert isinstance(verbosity, Verbosity) + + +def test_verbosity_factory_rejects_unknown_tag(): + from easydiffraction.project.categories.verbosity.factory import VerbosityFactory + + with pytest.raises(ValueError, match=r"Unsupported type: 'missing'"): + VerbosityFactory.create('missing') diff --git a/tests/unit/easydiffraction/project/test_display.py b/tests/unit/easydiffraction/project/test_display.py new file mode 100644 index 000000000..f534da493 --- /dev/null +++ b/tests/unit/easydiffraction/project/test_display.py @@ -0,0 +1,668 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Unit tests for project/display.py.""" + +from __future__ import annotations + +from contextlib import contextmanager +from types import SimpleNamespace + +import pytest + +from easydiffraction.datablocks.experiment.item.enums import SampleFormEnum +from easydiffraction.datablocks.experiment.item.enums import ScatteringTypeEnum +from easydiffraction.display.progress import ACTIVITY_LABEL_PROCESSING +from easydiffraction.display.plotting import _MeasVsCalcPlotOptions +from easydiffraction.project.display import PatternOptionStatus +from easydiffraction.project.display import ProjectDisplay +from easydiffraction.utils.enums import VerbosityEnum + + +def _make_project_stub() -> tuple[SimpleNamespace, list[tuple[str, tuple, dict]]]: + calls: list[tuple[str, tuple, dict]] = [] + + def record(name: str): + def _recorder(*args, **kwargs): + calls.append((name, args, kwargs)) + + return _recorder + + analysis_display = SimpleNamespace( + all_params=record('all_params'), + fittable_params=record('fittable_params'), + free_params=record('free_params'), + how_to_access_parameters=record('how_to_access_parameters'), + parameter_cif_uids=record('parameter_cif_uids'), + fit_results=record('fit_results'), + ) + plotter = SimpleNamespace( + plot_param_correlations=record('plot_param_correlations'), + plot_param_series=record('plot_param_series'), + plot_all_param_series=record('plot_all_param_series'), + plot_posterior_pairs=record('plot_posterior_pairs'), + plot_param_distribution=record('plot_param_distribution'), + plot_posterior_predictive=record('plot_posterior_predictive'), + _plot_posterior_predictive_request=record('_plot_posterior_predictive_request'), + plot_meas=record('plot_meas'), + plot_calc=record('plot_calc'), + plot_meas_vs_calc=record('plot_meas_vs_calc'), + _plot_meas_vs_calc_request=record('_plot_meas_vs_calc_request'), + engine='plotly', + _resolve_x_axis=lambda expt_type, x: ('two_theta', 'two_theta', None, None, None), + ) + project = SimpleNamespace( + analysis=SimpleNamespace( + display=analysis_display, + fit_results=SimpleNamespace(posterior_predictive={}), + bayesian_result=SimpleNamespace( + has_pair_cache=SimpleNamespace(value=False), + has_posterior_predictive=SimpleNamespace(value=False), + ), + bayesian_pair_caches=[], + bayesian_predictive_datasets=[], + _persisted_fit_state_sidecar={}, + ), + rendering=SimpleNamespace(plotter=plotter), + experiments={'hrpt': SimpleNamespace(type=SimpleNamespace())}, + free_parameters=[], + verbosity=SimpleNamespace(fit=SimpleNamespace(value='full')), + ) + return project, calls + + +def _make_statuses( + *, + measured: bool = False, + calculated: bool = False, + background: bool = False, + residual: bool = False, + bragg: bool = False, + excluded: bool = False, + uncertainty: bool = False, + uncertainty_reason: str = 'Posterior predictive data is unavailable.', +) -> list[PatternOptionStatus]: + return [ + PatternOptionStatus( + name='auto', + description='auto', + available=True, + auto_included=True, + reason='', + ), + PatternOptionStatus( + name='measured', + description='measured', + available=measured, + auto_included=False, + reason='' if measured else 'Measured unavailable', + ), + PatternOptionStatus( + name='calculated', + description='calculated', + available=calculated, + auto_included=False, + reason='' if calculated else 'Calculated unavailable', + ), + PatternOptionStatus( + name='background', + description='background', + available=background, + auto_included=False, + reason='' if background else 'Background unavailable', + ), + PatternOptionStatus( + name='residual', + description='residual', + available=residual, + auto_included=False, + reason='' if residual else 'Residual unavailable', + ), + PatternOptionStatus( + name='bragg', + description='bragg', + available=bragg, + auto_included=False, + reason='' if bragg else 'Bragg unavailable', + ), + PatternOptionStatus( + name='excluded', + description='excluded', + available=excluded, + auto_included=False, + reason='' if excluded else 'Excluded unavailable', + ), + PatternOptionStatus( + name='uncertainty', + description='uncertainty', + available=uncertainty, + auto_included=False, + reason='' if uncertainty else uncertainty_reason, + ), + ] + + +def test_parameter_display_delegates_to_analysis_display(): + project, calls = _make_project_stub() + display = ProjectDisplay(project) + + display.parameters.all() + display.parameters.fittable() + display.parameters.free() + display.parameters.access() + display.parameters.cif_uids() + + assert [name for name, _args, _kwargs in calls] == [ + 'all_params', + 'fittable_params', + 'free_params', + 'how_to_access_parameters', + 'parameter_cif_uids', + ] + + +def test_project_display_help_lists_namespaces_and_methods(capsys): + project, _calls = _make_project_stub() + display = ProjectDisplay(project) + + display.help() + out = capsys.readouterr().out + + assert "Help for 'ProjectDisplay'" in out + assert 'parameters' in out + assert 'fit' in out + assert 'posterior' in out + assert 'pattern()' in out + assert 'show_pattern_options()' in out + + +def test_nested_project_display_help_lists_methods(capsys): + project, _calls = _make_project_stub() + display = ProjectDisplay(project) + + display.parameters.help() + display.fit.help() + display.posterior.help() + out = capsys.readouterr().out + + assert "Help for 'ParameterDisplay'" in out + assert 'all()' in out + assert 'access()' in out + assert "Help for 'FitDisplay'" in out + assert 'results()' in out + assert 'correlations()' in out + assert "Help for 'PosteriorDisplay'" in out + assert 'pairs()' in out + assert 'predictive()' in out + + +def test_fit_display_delegates_to_analysis_and_rendering(): + project, calls = _make_project_stub() + display = ProjectDisplay(project) + + display.fit.results() + display.fit.correlations( + threshold=0.75, + precision=3, + max_parameters=4, + show_diagonal=False, + ) + display.fit.series(param='scale', versus='diffrn.ambient_temperature') + display.fit.series(versus='diffrn.ambient_temperature') + + assert calls[0] == ('fit_results', (), {}) + assert calls[1] == ( + 'plot_param_correlations', + (), + { + 'threshold': 0.75, + 'precision': 3, + 'max_parameters': 4, + 'show_diagonal': False, + }, + ) + assert calls[2] == ( + 'plot_param_series', + (), + {'param': 'scale', 'versus': 'diffrn.ambient_temperature'}, + ) + assert calls[3] == ( + 'plot_all_param_series', + (), + {'versus': 'diffrn.ambient_temperature'}, + ) + + +def test_posterior_display_delegates_to_rendering_plotter(monkeypatch): + import easydiffraction.project.display as display_mod + + project, calls = _make_project_stub() + display = ProjectDisplay(project) + indicator_calls: list[tuple[str, VerbosityEnum]] = [] + + @contextmanager + def fake_activity_indicator(label, *, verbosity): + indicator_calls.append((label, verbosity)) + yield object() + + monkeypatch.setattr(display_mod, 'activity_indicator', fake_activity_indicator) + + display.posterior.pairs(parameters=['a'], threshold=0.5, max_parameters=3) + display.posterior.distribution('a') + display.posterior.predictive( + 'hrpt', + style='draws', + x_min=1.0, + x_max=2.0, + show_residual=True, + x='d_spacing', + ) + + assert calls[0][0] == 'plot_posterior_pairs' + assert calls[0][2]['parameters'] == ['a'] + assert calls[0][2]['threshold'] == 0.5 + assert calls[0][2]['max_parameters'] == 3 + assert calls[1] == ('plot_param_distribution', ('a',), {}) + assert calls[2] == ( + 'plot_posterior_predictive', + (), + { + 'expt_name': 'hrpt', + 'style': 'draws', + 'x_min': 1.0, + 'x_max': 2.0, + 'show_residual': True, + 'x': 'd_spacing', + }, + ) + assert indicator_calls == [ + (ACTIVITY_LABEL_PROCESSING, VerbosityEnum.FULL), + (ACTIVITY_LABEL_PROCESSING, VerbosityEnum.FULL), + ] + + +def test_posterior_predictive_skips_processing_indicator_for_restored_cache(monkeypatch): + import easydiffraction.project.display as display_mod + + project, calls = _make_project_stub() + project.analysis = SimpleNamespace( + fit_results=object(), + bayesian_result=SimpleNamespace(has_posterior_predictive=SimpleNamespace(value=True)), + bayesian_predictive_datasets=[ + SimpleNamespace( + experiment_name=SimpleNamespace(value='hrpt'), + x_axis_name=SimpleNamespace(value='two_theta'), + draws_path=SimpleNamespace(value=None), + ) + ], + _persisted_fit_state_sidecar={ + 'predictive_datasets': { + 'hrpt': { + 'x': [1.0, 2.0], + 'best_sample_prediction': [3.0, 4.0], + } + } + }, + ) + project.experiments = {'hrpt': SimpleNamespace(type=SimpleNamespace())} + project.rendering.plotter.engine = 'plotly' + project.rendering.plotter._resolve_x_axis = lambda expt_type, x: ( + 'two_theta', + 'two_theta', + None, + None, + None, + ) + display = ProjectDisplay(project) + indicator_calls: list[tuple[str, VerbosityEnum]] = [] + + @contextmanager + def fake_activity_indicator(label, *, verbosity): + indicator_calls.append((label, verbosity)) + yield object() + + monkeypatch.setattr(display_mod, 'activity_indicator', fake_activity_indicator) + + display.posterior.predictive('hrpt') + + assert calls == [ + ( + 'plot_posterior_predictive', + (), + { + 'expt_name': 'hrpt', + 'style': 'band', + 'x_min': None, + 'x_max': None, + 'show_residual': None, + 'x': None, + }, + ) + ] + assert indicator_calls == [] + + +def test_posterior_distribution_without_param_plots_all_free_parameters(): + project, calls = _make_project_stub() + project.free_parameters = ['a', 'b'] + project.rendering.plotter.engine = 'plotly' + display = ProjectDisplay(project) + + display.posterior.distribution() + + assert calls == [ + ('plot_param_distribution', ('a',), {}), + ('plot_param_distribution', ('b',), {}), + ] + + +def test_posterior_distribution_without_param_plots_all_free_parameters_for_ascii(): + project, calls = _make_project_stub() + project.free_parameters = ['a', 'b'] + project.rendering.plotter.engine = 'asciichartpy' + display = ProjectDisplay(project) + + display.posterior.distribution() + + assert calls == [ + ('plot_param_distribution', ('a',), {}), + ('plot_param_distribution', ('b',), {}), + ] + + +def test_posterior_distribution_without_param_warns_when_no_free_parameters(monkeypatch): + import easydiffraction.project.display as display_mod + + project, calls = _make_project_stub() + display = ProjectDisplay(project) + warnings: list[str] = [] + + monkeypatch.setattr(display_mod.log, 'warning', warnings.append) + + display.posterior.distribution() + + assert calls == [] + assert warnings == ['No free parameters found.'] + + +def test_pattern_auto_routes_measured_and_excluded_to_plot_meas(): + project, calls = _make_project_stub() + display = ProjectDisplay(project) + display._pattern_option_statuses = lambda expt_name: _make_statuses( + measured=True, + excluded=True, + ) + + display.pattern('hrpt') + + assert calls == [ + ( + 'plot_meas', + (), + { + 'expt_name': 'hrpt', + 'x_min': None, + 'x_max': None, + 'x': None, + 'show_excluded': True, + }, + ) + ] + + +def test_pattern_uncertainty_routes_to_posterior_predictive(monkeypatch): + import easydiffraction.project.display as display_mod + + project, calls = _make_project_stub() + display = ProjectDisplay(project) + display._pattern_option_statuses = lambda expt_name: _make_statuses( + measured=True, + calculated=True, + residual=True, + excluded=True, + uncertainty=True, + ) + indicator_calls: list[tuple[str, VerbosityEnum]] = [] + + @contextmanager + def fake_activity_indicator(label, *, verbosity): + indicator_calls.append((label, verbosity)) + yield object() + + monkeypatch.setattr(display_mod, 'activity_indicator', fake_activity_indicator) + + display.pattern( + 'hrpt', + x_min=1.0, + x_max=2.0, + include=('measured', 'calculated', 'uncertainty', 'residual', 'excluded'), + ) + + assert calls == [ + ( + '_plot_posterior_predictive_request', + (), + { + 'expt_name': 'hrpt', + 'style': 'band', + 'plot_options': _MeasVsCalcPlotOptions( + x_min=1.0, + x_max=2.0, + show_residual=True, + show_background=False, + show_bragg=False, + show_excluded=True, + x=None, + ), + }, + ) + ] + assert indicator_calls == [(ACTIVITY_LABEL_PROCESSING, VerbosityEnum.FULL)] + + +def test_pattern_measured_and_calculated_suppresses_background_and_bragg(): + project, calls = _make_project_stub() + display = ProjectDisplay(project) + display._pattern_option_statuses = lambda expt_name: _make_statuses( + measured=True, + calculated=True, + background=True, + bragg=True, + ) + + display.pattern('hrpt', include=('measured', 'calculated')) + + assert calls == [ + ( + '_plot_meas_vs_calc_request', + (), + { + 'expt_name': 'hrpt', + 'plot_options': _MeasVsCalcPlotOptions( + x_min=None, + x_max=None, + show_residual=False, + show_background=False, + show_bragg=False, + show_excluded=False, + x=None, + ), + }, + ) + ] + + +def test_pattern_measured_and_calculated_can_enable_background_and_bragg(): + project, calls = _make_project_stub() + display = ProjectDisplay(project) + display._pattern_option_statuses = lambda expt_name: _make_statuses( + measured=True, + calculated=True, + background=True, + residual=True, + bragg=True, + excluded=True, + ) + + display.pattern( + 'hrpt', + include=('measured', 'calculated', 'background', 'residual', 'bragg', 'excluded'), + ) + + assert calls == [ + ( + '_plot_meas_vs_calc_request', + (), + { + 'expt_name': 'hrpt', + 'plot_options': _MeasVsCalcPlotOptions( + x_min=None, + x_max=None, + show_residual=True, + show_background=True, + show_bragg=True, + show_excluded=True, + x=None, + ), + }, + ) + ] + + +def test_pattern_option_statuses_ignore_placeholder_arrays_without_usable_state(monkeypatch): + pattern = SimpleNamespace( + intensity_meas=[1.0, 2.0], + intensity_calc=[0.0, 0.0], + intensity_bkg=[0.0, 0.0], + ) + + experiment = SimpleNamespace( + type=SimpleNamespace( + sample_form=SimpleNamespace(value=SampleFormEnum.POWDER.value), + scattering_type=SimpleNamespace(value=ScatteringTypeEnum.BRAGG.value), + ), + linked_phases=[], + background=[], + refln=[], + excluded_regions=[], + ) + project = SimpleNamespace( + experiments={'hrpt': experiment}, + structures=SimpleNamespace(names=['phase-a']), + analysis=SimpleNamespace(fit_results=None), + rendering=SimpleNamespace( + plotter=SimpleNamespace(_update_project_categories=lambda expt_name: None), + chart_engine=SimpleNamespace(value='plotly'), + ), + ) + display = ProjectDisplay(project) + + monkeypatch.setattr( + 'easydiffraction.project.display.intensity_category_for', lambda expt: pattern + ) + + statuses = {status.name: status for status in display._pattern_option_statuses('hrpt')} + + assert statuses['measured'].available is True + assert statuses['calculated'].available is False + assert statuses['background'].available is False + assert statuses['bragg'].available is False + assert statuses['measured'].auto_included is True + assert statuses['calculated'].auto_included is False + + +def test_pattern_auto_routes_single_crystal_with_calculated_data(monkeypatch): + calls: list[tuple[str, tuple, dict]] = [] + + def record(name: str): + def _recorder(*args, **kwargs): + calls.append((name, args, kwargs)) + + return _recorder + + pattern = SimpleNamespace( + intensity_meas=[10.0, 12.0], + intensity_calc=[9.5, 11.5], + ) + experiment = SimpleNamespace( + type=SimpleNamespace( + sample_form=SimpleNamespace(value=SampleFormEnum.SINGLE_CRYSTAL.value), + scattering_type=SimpleNamespace(value=ScatteringTypeEnum.BRAGG.value), + ), + linked_crystal=SimpleNamespace(id=SimpleNamespace(value='si')), + excluded_regions=[], + ) + project = SimpleNamespace( + experiments={'heidi': experiment}, + structures=SimpleNamespace(names=['si']), + analysis=SimpleNamespace(fit_results=None), + rendering=SimpleNamespace( + plotter=SimpleNamespace( + _update_project_categories=lambda expt_name: None, + _plot_meas_vs_calc_request=record('_plot_meas_vs_calc_request'), + ), + chart_engine=SimpleNamespace(value='plotly'), + ), + ) + display = ProjectDisplay(project) + + monkeypatch.setattr( + 'easydiffraction.project.display.intensity_category_for', + lambda expt: pattern, + ) + + display.pattern('heidi') + + assert calls == [ + ( + '_plot_meas_vs_calc_request', + (), + { + 'expt_name': 'heidi', + 'plot_options': _MeasVsCalcPlotOptions( + x_min=None, + x_max=None, + show_residual=False, + show_background=False, + show_bragg=False, + show_excluded=False, + x=None, + ), + }, + ) + ] + + +def test_pattern_rejects_excluded_with_custom_x(): + project, _calls = _make_project_stub() + display = ProjectDisplay(project) + display._pattern_option_statuses = lambda expt_name: _make_statuses( + measured=True, + excluded=True, + ) + + with pytest.raises(ValueError, match='default x-axis'): + display.pattern('hrpt', include=('measured', 'excluded'), x='d_spacing') + + +def test_show_pattern_options_renders_table(monkeypatch): + project, _calls = _make_project_stub() + display = ProjectDisplay(project) + display._pattern_option_statuses = lambda expt_name: _make_statuses( + measured=True, + calculated=True, + ) + captured: dict[str, object] = {} + + def fake_render_table(*, columns_headers, columns_alignment, columns_data): + captured['columns_headers'] = columns_headers + captured['columns_alignment'] = columns_alignment + captured['columns_data'] = columns_data + + monkeypatch.setattr('easydiffraction.project.display.render_table', fake_render_table) + + display.show_pattern_options('hrpt') + + assert captured['columns_headers'] == ['Option', 'Description', 'Available', 'Auto', 'Reason'] + assert captured['columns_alignment'] == ['left', 'left', 'center', 'center', 'left'] + assert captured['columns_data'][0][0] == 'auto' + assert captured['columns_data'][1][0] == 'measured' diff --git a/tests/unit/easydiffraction/project/test_project.py b/tests/unit/easydiffraction/project/test_project.py index 92907f66c..3afbd363e 100644 --- a/tests/unit/easydiffraction/project/test_project.py +++ b/tests/unit/easydiffraction/project/test_project.py @@ -1,6 +1,10 @@ # SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +from collections import UserList +import csv +from types import SimpleNamespace + def test_module_import(): import easydiffraction.project.project as MUT @@ -26,7 +30,7 @@ def test_project_verbosity_default(): from easydiffraction.project.project import Project p = Project() - assert p.verbosity == 'full' + assert p.verbosity.fit.value == 'full' def test_project_verbosity_setter(): @@ -34,11 +38,11 @@ def test_project_verbosity_setter(): p = Project() p.verbosity = 'short' - assert p.verbosity == 'short' + assert p.verbosity.fit.value == 'short' p.verbosity = 'silent' - assert p.verbosity == 'silent' + assert p.verbosity.fit.value == 'silent' p.verbosity = 'full' - assert p.verbosity == 'full' + assert p.verbosity.fit.value == 'full' def test_project_verbosity_invalid(): @@ -49,3 +53,71 @@ def test_project_verbosity_invalid(): p = Project() with pytest.raises(ValueError, match="'verbose' is not a valid VerbosityEnum"): p.verbosity = 'verbose' + + +def test_project_free_params_aggregate_structures_and_experiments(): + from easydiffraction.project.project import Project + + project = Project() + structure_param = object() + experiment_param = object() + project._structures = SimpleNamespace(free_parameters=[structure_param]) + project._experiments = SimpleNamespace(free_parameters=[experiment_param]) + + assert project.free_parameters == [structure_param, experiment_param] + + +def test_project_exposes_rendering_and_display_facades(): + from easydiffraction.project.categories.rendering import Rendering + from easydiffraction.project.display import ProjectDisplay + from easydiffraction.project.project import Project + + project = Project() + + assert isinstance(project.rendering, Rendering) + assert isinstance(project.display, ProjectDisplay) + + +def test_apply_params_from_csv_resolves_relative_file_paths(tmp_path): + from easydiffraction.project.project import Project + + project = Project() + project.info.path = tmp_path / 'project' + analysis_dir = project.info.path / 'analysis' + analysis_dir.mkdir(parents=True) + data_dir = project.info.path / 'experiments' / 'scan' + data_dir.mkdir(parents=True) + data_path = data_dir / 'scan_001.dat' + data_path.write_text('1 2 3\n') + + csv_path = analysis_dir / 'results.csv' + with csv_path.open('w', newline='', encoding='utf-8') as handle: + writer = csv.DictWriter(handle, fieldnames=['file_path']) + writer.writeheader() + writer.writerow({'file_path': 'experiments/scan/scan_001.dat'}) + + loaded_paths: list[str] = [] + + class Experiment: + diffrn = SimpleNamespace() + + def _load_ascii_data_to_experiment(self, file_path): + loaded_paths.append(file_path) + + class Structures(UserList): + parameters = [] + + class Experiments: + parameters = [] + + @staticmethod + def values(): + return [experiment] + + experiment = Experiment() + project._structures = Structures() + project._experiments = Experiments() + + project.apply_params_from_csv(0) + + assert loaded_paths == [str(data_path)] diff --git a/tests/unit/easydiffraction/project/test_project_config.py b/tests/unit/easydiffraction/project/test_project_config.py new file mode 100644 index 000000000..be6576e94 --- /dev/null +++ b/tests/unit/easydiffraction/project/test_project_config.py @@ -0,0 +1,129 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + +from __future__ import annotations + +import datetime + + +def test_project_config_exposes_project_info_and_rendering_categories(): + from easydiffraction.core.category_owner import CategoryOwner + from easydiffraction.project.project_config import ProjectConfig + from easydiffraction.project.project_info import ProjectInfo + + config = ProjectConfig(name='beer', title='Beer title', description='Some description') + + assert isinstance(config, CategoryOwner) + assert isinstance(config.info, ProjectInfo) + assert config.info._parent is config + assert config.rendering._parent is config + assert config.info.name == 'beer' + assert config.info.title == 'Beer title' + assert config.info.description == 'Some description' + assert config.info.path is None + assert isinstance(config.info.created, datetime.datetime) + assert isinstance(config.info.last_modified, datetime.datetime) + assert config.verbosity._parent is config + assert config.verbosity.fit.value == 'full' + assert config.categories == [config.info, config.rendering, config.verbosity] + assert config.parameters == ( + config.info.parameters + config.rendering.parameters + config.verbosity.parameters + ) + + +def test_project_config_as_cif_has_project_and_rendering_sections_without_data_header(): + from easydiffraction.project.project_config import ProjectConfig + + config = ProjectConfig(name='beer', title='Beer title', description='Some description') + + cif_text = config.as_cif + + assert not cif_text.startswith('data_') + assert '_project.id beer' in cif_text + assert '_project.title' in cif_text + assert '_project.description' in cif_text + assert '_project.created' in cif_text + assert '_project.last_modified' in cif_text + assert '_rendering.chart_engine' in cif_text + assert '_rendering.table_engine' in cif_text + assert '_rendering.chart_engine auto' in cif_text + assert '_rendering.table_engine auto' in cif_text + assert '_verbosity.fit full' in cif_text + + +def test_project_save_and_load_use_auto_rendering_defaults_when_unset(tmp_path): + from easydiffraction.project.project import Project + + project = Project(name='beer', title='Beer title', description='Some description') + project.save_as(str(tmp_path / 'proj')) + + project_cif = (tmp_path / 'proj' / 'project.cif').read_text() + + assert not project_cif.startswith('data_') + assert '_rendering.chart_engine auto' in project_cif + assert '_rendering.table_engine auto' in project_cif + assert '_verbosity.fit full' in project_cif + + loaded = Project.load(str(tmp_path / 'proj')) + + assert loaded.rendering.chart_engine.value == 'auto' + assert loaded.rendering.table_engine.value == 'auto' + assert loaded.verbosity.fit.value == 'full' + + +def test_project_save_and_load_keep_project_config_section_format(tmp_path): + from easydiffraction.project.project import Project + + project = Project(name='beer', title='Beer title', description='Some description') + project.rendering.chart_engine = 'asciichartpy' + project.rendering.table_engine = 'rich' + project.save_as(str(tmp_path / 'proj')) + + project_cif = (tmp_path / 'proj' / 'project.cif').read_text() + assert not project_cif.startswith('data_') + assert '_project.id beer' in project_cif + assert '_rendering.chart_engine asciichartpy' in project_cif + assert '_rendering.table_engine rich' in project_cif + assert '_verbosity.fit full' in project_cif + + loaded = Project.load(str(tmp_path / 'proj')) + assert loaded.info.name == 'beer' + assert loaded.info.title == 'Beer title' + assert loaded.info.description == 'Some description' + assert isinstance(loaded.info.created, datetime.datetime) + assert isinstance(loaded.info.last_modified, datetime.datetime) + assert loaded.rendering.chart_engine.value == 'asciichartpy' + assert loaded.rendering.table_engine.value == 'rich' + assert loaded.verbosity.fit.value == 'full' + + +def test_project_save_wraps_long_description_as_cif_text_field(tmp_path): + from easydiffraction.project.project import Project + + description = ( + 'This is the most minimal example of using EasyDiffraction. ' + 'It shows how to load a previously saved project from a directory ' + 'and run refinement in just a few lines of code.' + ) + project = Project(name='beer', title='Beer title', description=description) + project.save_as(str(tmp_path / 'proj')) + + project_cif = (tmp_path / 'proj' / 'project.cif').read_text() + + assert '_project.description' in project_cif + description_tail = project_cif.split('_project.description', maxsplit=1)[1].lstrip(' ') + assert description_tail.startswith('\n;\n') + assert '\n;\n_project.created' in project_cif + description_block = description_tail.split('\n;\n', maxsplit=1)[1] + description_block = description_block.split('\n;\n_project.created', maxsplit=1)[0] + description_lines = description_block.splitlines() + + assert len(description_lines) > 1 + assert all(not line.startswith(';') for line in description_lines) + assert all(not line.endswith(';') for line in description_lines) + assert description_lines[0].startswith('This is the most minimal example') + assert description_lines[-1].endswith('lines of code.') + + loaded = Project.load(str(tmp_path / 'proj')) + + assert loaded.info.description == description diff --git a/tests/unit/easydiffraction/project/test_project_load.py b/tests/unit/easydiffraction/project/test_project_load.py index c788f0a74..f19b4392d 100644 --- a/tests/unit/easydiffraction/project/test_project_load.py +++ b/tests/unit/easydiffraction/project/test_project_load.py @@ -70,27 +70,27 @@ def test_round_trips_minimizer(self, tmp_path): loaded = Project.load(str(tmp_path / 'proj')) - assert loaded.analysis.fit.minimizer_type.value == 'lmfit (leastsq)' + assert loaded.analysis.fitting.minimizer_type.value == 'lmfit (leastsq)' def test_round_trips_fit_mode(self, tmp_path): original = Project(name='a2') - original.analysis.fit.mode = 'joint' + original.analysis.fitting_mode_type = 'joint' original.save_as(str(tmp_path / 'proj')) loaded = Project.load(str(tmp_path / 'proj')) - assert loaded.analysis.fit.mode.value == 'joint' + assert loaded.analysis.fitting_mode_type == 'joint' - def test_round_trips_display_configuration(self, tmp_path): + def test_round_trips_rendering_configuration(self, tmp_path): original = Project(name='d1') - original.display.plotter_type = 'asciichartpy' - original.display.tabler_type = 'rich' + original.rendering.chart_engine = 'asciichartpy' + original.rendering.table_engine = 'rich' original.save_as(str(tmp_path / 'proj')) loaded = Project.load(str(tmp_path / 'proj')) - assert loaded.display.plotter_type.value == 'asciichartpy' - assert loaded.display.tabler_type.value == 'rich' + assert loaded.rendering.chart_engine.value == 'asciichartpy' + assert loaded.rendering.table_engine.value == 'rich' def test_round_trips_constraints(self, tmp_path): original = Project(name='c1') @@ -120,9 +120,174 @@ def test_round_trips_constraints(self, tmp_path): assert loaded.analysis.aliases['b_param'].param is not None assert len(loaded.analysis.constraints) == 1 + assert loaded.analysis.constraints['b_param'].id.value == 'b_param' + assert loaded.analysis.constraints['b_param'].expression.value == 'b_param = a_param' assert loaded.analysis.constraints[0].expression.value == 'b_param = a_param' assert loaded.analysis.constraints.enabled is True + def test_round_trips_deterministic_fit_state_and_keeps_live_parameter_values(self, tmp_path): + original = Project(name='fit_state') + original.structures.create(name='lbco') + structure = original.structures['lbco'] + structure.space_group.name_h_m = 'P m -3 m' + structure.cell.length_a = 3.88 + parameter = structure.cell.length_a + parameter.free = True + parameter.uncertainty = 0.07 + parameter.fit_min = 3.8 + parameter.fit_max = 3.9 + parameter._set_fit_bounds_uncertainty_multiplier(4.0) + parameter._fit_start_value = 3.87 + parameter._fit_start_uncertainty = 0.02 + + original.analysis.fit_parameters.create( + param_unique_name=parameter.unique_name, + fit_min=parameter.fit_min, + fit_max=parameter.fit_max, + fit_bounds_uncertainty_multiplier=4.0, + start_value=3.87, + start_uncertainty=0.02, + ) + original.analysis.fit_result._set_result_kind('deterministic') + original.analysis.fit_result._set_success(value=True) + original.analysis.fit_result._set_message('Fit converged') + original.analysis.fit_result._set_iterations(37) + original.analysis.fit_result._set_fitting_time(1.82) + original.analysis.fit_result._set_reduced_chi_square(1.031) + original.analysis.deterministic_result._set_optimizer_name('lmfit') + original.analysis.deterministic_result._set_method_name('leastsq') + original.analysis._set_has_persisted_fit_state(value=True) + original.save_as(str(tmp_path / 'proj')) + + loaded = Project.load(str(tmp_path / 'proj')) + loaded_parameter = loaded.structures['lbco'].cell.length_a + + assert loaded.analysis.fit_result.result_kind.value == 'deterministic' + assert loaded.analysis.fit_parameters[parameter.unique_name].fit_min.value == 3.8 + assert loaded_parameter.value == 3.88 + assert loaded_parameter.fit_min == 3.8 + assert loaded_parameter.fit_max == 3.9 + assert loaded_parameter.fit_bounds_uncertainty_multiplier == 4.0 + assert loaded_parameter._fit_start_value == 3.87 + assert loaded_parameter._fit_start_uncertainty == 0.02 + assert loaded_parameter.uncertainty == 0.07 + + def test_round_trips_persisted_deterministic_correlation_summary_for_reloaded_display( + self, + tmp_path, + ): + from easydiffraction.display.plotting import Plotter + + original = Project(name='fit_correlation_state') + original.structures.create(name='lbco') + structure = original.structures['lbco'] + structure.space_group.name_h_m = 'P m -3 m' + structure.cell.length_a = 3.88 + structure.cell.length_b = 3.89 + + parameter_a = structure.cell.length_a + parameter_b = structure.cell.length_b + for parameter, start_value in ( + (parameter_a, 3.87), + (parameter_b, 3.88), + ): + parameter.free = True + parameter.uncertainty = 0.05 + parameter.fit_min = 3.8 + parameter.fit_max = 3.9 + parameter._set_fit_bounds_uncertainty_multiplier(4.0) + parameter._fit_start_value = start_value + parameter._fit_start_uncertainty = 0.02 + original.analysis.fit_parameters.create( + param_unique_name=parameter.unique_name, + fit_min=parameter.fit_min, + fit_max=parameter.fit_max, + fit_bounds_uncertainty_multiplier=4.0, + start_value=start_value, + start_uncertainty=0.02, + ) + + original.analysis.fit_result._set_result_kind('deterministic') + original.analysis.fit_result._set_success(value=True) + original.analysis.fit_result._set_message('Fit converged') + original.analysis.fit_result._set_iterations(21) + original.analysis.fit_result._set_fitting_time(0.74) + original.analysis.fit_result._set_reduced_chi_square(1.031) + original.analysis.deterministic_result._set_optimizer_name('lmfit') + original.analysis.deterministic_result._set_method_name('leastsq') + original.analysis.deterministic_result._set_objective_name('chi-square') + original.analysis.deterministic_result._set_objective_value(1.031) + original.analysis.deterministic_result._set_n_data_points(120) + original.analysis.deterministic_result._set_n_parameters(2) + original.analysis.deterministic_result._set_n_free_parameters(2) + original.analysis.deterministic_result._set_degrees_of_freedom(118) + original.analysis.deterministic_result._set_covariance_available(value=False) + original.analysis.deterministic_result._set_correlation_available(value=True) + original.analysis.fit_parameter_correlations.create( + source_kind='deterministic', + param_unique_name_i=parameter_b.unique_name, + param_unique_name_j=parameter_a.unique_name, + correlation=0.42, + ) + original.analysis._set_has_persisted_fit_state(value=True) + original.save_as(str(tmp_path / 'proj')) + + loaded = Project.load(str(tmp_path / 'proj')) + plotter = Plotter() + plotter._set_project(loaded) + + corr_df = plotter._get_param_correlation_dataframe() + + assert corr_df is not None + assert list(corr_df.index) == [parameter_a.unique_name, parameter_b.unique_name] + assert list(corr_df.columns) == [parameter_a.unique_name, parameter_b.unique_name] + assert corr_df.loc[parameter_a.unique_name, parameter_a.unique_name] == pytest.approx(1.0) + assert corr_df.loc[parameter_b.unique_name, parameter_b.unique_name] == pytest.approx(1.0) + assert corr_df.loc[parameter_a.unique_name, parameter_b.unique_name] == pytest.approx(0.42) + assert corr_df.loc[parameter_b.unique_name, parameter_a.unique_name] == pytest.approx(0.42) + + def test_round_trips_bayesian_sampler_settings_to_live_dream_minimizer(self, tmp_path): + original = Project(name='bayes_state') + original.analysis.fitting.minimizer_type = 'bumps (dream)' + original.analysis.fit_result._set_result_kind('bayesian') + original.analysis.bayesian_sampler._set_steps(300) + original.analysis.bayesian_sampler._set_burn(60) + original.analysis.bayesian_sampler._set_thin(2) + original.analysis.bayesian_sampler._set_pop(8) + original.analysis.bayesian_sampler._set_parallel(0) + original.analysis.bayesian_sampler._set_init('lhs') + original.analysis._set_has_persisted_fit_state(value=True) + original.save_as(str(tmp_path / 'proj')) + + loaded = Project.load(str(tmp_path / 'proj')) + minimizer = loaded.analysis.fitting.minimizer + + assert minimizer is not None + assert minimizer.steps == 300 + assert minimizer.burn == 60 + assert minimizer.thin == 2 + assert minimizer.pop == 8 + assert minimizer.parallel == 0 + assert minimizer.init.value == 'lhs' + + def test_round_trips_legacy_bayesian_steps_and_burn_to_live_dream_minimizer(self, tmp_path): + original = Project(name='legacy_bayes_state') + original.analysis.fitting.minimizer_type = 'bumps (dream)' + original.analysis.fit_result._set_result_kind('bayesian') + original.analysis.bayesian_sampler._set_steps(300) + original.analysis.bayesian_sampler._set_burn(60) + original.analysis._set_has_persisted_fit_state(value=True) + original.save_as(str(tmp_path / 'proj')) + + loaded = Project.load(str(tmp_path / 'proj')) + minimizer = loaded.analysis.fitting.minimizer + + assert minimizer is not None + assert minimizer.steps == 300 + assert minimizer.burn == 60 + assert minimizer.thin == 1 + assert minimizer.pop == 4 + class TestLoadAnalysisCifFallback: """Load falls back from analysis/analysis.cif to analysis.cif at root.""" @@ -136,7 +301,7 @@ def test_loads_analysis_from_subdir(self, tmp_path): assert (tmp_path / 'proj' / 'analysis' / 'analysis.cif').is_file() loaded = Project.load(str(tmp_path / 'proj')) - assert loaded.analysis.fit.minimizer_type.value == 'lmfit (leastsq)' + assert loaded.analysis.fitting.minimizer_type.value == 'lmfit (leastsq)' def test_loads_analysis_from_root_fallback(self, tmp_path): """Old layout fallback: analysis.cif at project root.""" @@ -150,4 +315,4 @@ def test_loads_analysis_from_root_fallback(self, tmp_path): analysis_dir.rmdir() loaded = Project.load(str(proj_dir)) - assert loaded.analysis.fit.minimizer_type.value == 'lmfit (leastsq)' + assert loaded.analysis.fitting.minimizer_type.value == 'lmfit (leastsq)' diff --git a/tests/unit/easydiffraction/project/test_project_save.py b/tests/unit/easydiffraction/project/test_project_save.py index bf632e11c..ff8a9b9be 100644 --- a/tests/unit/easydiffraction/project/test_project_save.py +++ b/tests/unit/easydiffraction/project/test_project_save.py @@ -24,8 +24,8 @@ def test_project_save_as_writes_core_files(tmp_path, monkeypatch): from easydiffraction.summary.summary import Summary # Monkeypatch as_cif producers to avoid heavy internals - monkeypatch.setattr(ProjectInfo, 'as_cif', lambda self: 'info') - monkeypatch.setattr(Analysis, 'as_cif', lambda self: 'analysis') + monkeypatch.setattr(ProjectInfo, 'as_cif', property(lambda self: 'info')) + monkeypatch.setattr(Analysis, 'as_cif', property(lambda self: 'analysis')) monkeypatch.setattr(Summary, 'as_cif', lambda self: 'summary') p = Project(name='p1') @@ -38,3 +38,86 @@ def test_project_save_as_writes_core_files(tmp_path, monkeypatch): assert (target / 'summary.cif').is_file() assert (target / 'structures').is_dir() assert (target / 'experiments').is_dir() + + +def test_project_save_lists_existing_analysis_results_csv(tmp_path, monkeypatch, capsys): + from easydiffraction.analysis.analysis import Analysis + from easydiffraction.project.project import Project + from easydiffraction.project.project_info import ProjectInfo + from easydiffraction.summary.summary import Summary + + monkeypatch.setattr(ProjectInfo, 'as_cif', property(lambda self: 'info')) + monkeypatch.setattr(Analysis, 'as_cif', property(lambda self: 'analysis')) + monkeypatch.setattr(Summary, 'as_cif', lambda self: 'summary') + + target = tmp_path / 'proj_dir' + analysis_dir = target / 'analysis' + analysis_dir.mkdir(parents=True) + (analysis_dir / 'results.csv').write_text('file_path\nscan_001.xye\n') + + p = Project(name='p1') + p.info.path = target + p.save() + + out = capsys.readouterr().out + assert 'analysis.cif' in out + assert 'results.csv' in out + + +def test_project_save_as_overwrites_existing_directory_by_default(tmp_path, monkeypatch): + from easydiffraction.analysis.analysis import Analysis + from easydiffraction.project.project import Project + from easydiffraction.project.project_info import ProjectInfo + from easydiffraction.summary.summary import Summary + + monkeypatch.setattr(ProjectInfo, 'as_cif', property(lambda self: 'info')) + monkeypatch.setattr(Analysis, 'as_cif', property(lambda self: 'analysis')) + monkeypatch.setattr(Summary, 'as_cif', lambda self: 'summary') + + target = tmp_path / 'proj_dir' + stale_file = target / 'stale.txt' + target.mkdir() + stale_file.write_text('stale') + + project = Project(name='p1') + project.save_as(str(target)) + + assert not stale_file.exists() + assert (target / 'project.cif').is_file() + + +def test_project_save_as_preserves_existing_directory_when_disabled(tmp_path, monkeypatch): + from easydiffraction.analysis.analysis import Analysis + from easydiffraction.project.project import Project + from easydiffraction.project.project_info import ProjectInfo + from easydiffraction.summary.summary import Summary + + monkeypatch.setattr(ProjectInfo, 'as_cif', property(lambda self: 'info')) + monkeypatch.setattr(Analysis, 'as_cif', property(lambda self: 'analysis')) + monkeypatch.setattr(Summary, 'as_cif', lambda self: 'summary') + + target = tmp_path / 'proj_dir' + stale_file = target / 'stale.txt' + target.mkdir() + stale_file.write_text('stale') + + project = Project(name='p1') + project.save_as( + str(target), + overwrite=False, + ) + + assert stale_file.exists() + assert (target / 'project.cif').is_file() + + +def test_project_save_omits_empty_fit_state_sections(tmp_path): + from easydiffraction.project.project import Project + + project = Project(name='no_fit_state') + project.save_as(str(tmp_path / 'proj')) + + analysis_cif = (tmp_path / 'proj' / 'analysis' / 'analysis.cif').read_text() + + assert '_fit_parameter.param_unique_name' not in analysis_cif + assert '_fit_result.result_kind' not in analysis_cif diff --git a/tests/unit/easydiffraction/summary/test_summary.py b/tests/unit/easydiffraction/summary/test_summary.py index b8af02667..0619aaf1c 100644 --- a/tests/unit/easydiffraction/summary/test_summary.py +++ b/tests/unit/easydiffraction/summary/test_summary.py @@ -28,10 +28,10 @@ def __init__(self): self.experiments = {} # empty mapping to exercise loops safely class A: - class Fit: + class Fitting: minimizer_type = type('V', (), {'value': 'lmfit'})() - fit = Fit() + fitting = Fitting() class R: reduced_chi_square = 0.0 @@ -50,6 +50,21 @@ class R: assert 'FITTING' in out +def test_summary_help(capsys): + from easydiffraction.summary.summary import Summary + + class P: + pass + + s = Summary(P()) + s.help() + out = capsys.readouterr().out + assert "Help for 'Summary'" in out + assert 'show_report()' in out + assert 'show_project_info()' in out + assert 'show_fitting_details()' 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 9e62dba2d..69158b7d6 100644 --- a/tests/unit/easydiffraction/summary/test_summary_details.py +++ b/tests/unit/easydiffraction/summary/test_summary_details.py @@ -112,10 +112,10 @@ def __init__(self): self.experiments = {'exp1': _Expt()} class A: - class Fit: + class Fitting: minimizer_type = _Val('lmfit') - fit = Fit() + fitting = Fitting() class R: reduced_chi_square = 1.23 diff --git a/tests/unit/easydiffraction/test___main__.py b/tests/unit/easydiffraction/test___main__.py index 76d150c6d..a78710b35 100644 --- a/tests/unit/easydiffraction/test___main__.py +++ b/tests/unit/easydiffraction/test___main__.py @@ -44,6 +44,14 @@ def test_cli_subcommands_call_utils(monkeypatch): import easydiffraction.__main__ as main_mod logs = [] + monkeypatch.setattr(ed, 'list_data', lambda: logs.append('LIST_DATA')) + monkeypatch.setattr( + ed, + 'download_data', + lambda id, destination='data', overwrite=False: logs.append( + f'DATA_{id}_{destination}_{overwrite}' + ), + ) monkeypatch.setattr(ed, 'list_tutorials', lambda: logs.append('LIST')) monkeypatch.setattr( ed, @@ -56,14 +64,25 @@ def test_cli_subcommands_call_utils(monkeypatch): lambda id, destination='tutorials', overwrite=False: logs.append(f'DOWNLOAD_{id}'), ) - res1 = runner.invoke(main_mod.app, ['list-tutorials']) - res2 = runner.invoke(main_mod.app, ['download-all-tutorials']) - res3 = runner.invoke(main_mod.app, ['download-tutorial', '1']) + res0 = runner.invoke(main_mod.app, ['list-data']) + res1 = runner.invoke(main_mod.app, ['download-data', '30', '--destination', 'projects']) + res2 = runner.invoke(main_mod.app, ['list-tutorials']) + res3 = runner.invoke(main_mod.app, ['download-all-tutorials']) + res4 = runner.invoke(main_mod.app, ['download-tutorial', '1']) + assert res0.exit_code == 0 assert res1.exit_code == 0 assert res2.exit_code == 0 assert res3.exit_code == 0 - assert logs == ['LIST', 'DOWNLOAD_ALL', 'DOWNLOAD_1'] + assert res4.exit_code == 0 + assert logs == ['LIST_DATA', 'DATA_30_projects_False', 'LIST', 'DOWNLOAD_ALL', 'DOWNLOAD_1'] + + +def test_cli_project_first_argument_normalization_supports_global_data_commands(): + import easydiffraction.__main__ as main_mod + + assert main_mod._normalized_cli_args(['list-data']) == ['list-data'] + assert main_mod._normalized_cli_args(['download-data', '30']) == ['download-data', '30'] def test_cli_fit_loads_and_fits(monkeypatch, tmp_path): @@ -95,16 +114,21 @@ def fit_results(): analysis = _analysis() class _display: - class _plotter: + class _fit: @staticmethod - def plot_param_correlations(): - calls.append('PLOT_CORR') + def results(): + calls.append('DISPLAY') @staticmethod - def plot_meas_vs_calc(expt_name, *, show_residual=False): - calls.append(f'PLOT_{expt_name}_{show_residual}') + def correlations(): + calls.append('PLOT_CORR') - plotter = _plotter() + fit = _fit() + + @staticmethod + def pattern(expt_name, **kwargs): + del kwargs + calls.append(f'PLOT_{expt_name}_False') display = _display() @@ -119,7 +143,64 @@ def plot_meas_vs_calc(expt_name, *, show_residual=False): result = runner.invoke(main_mod.app, ['fit', str(proj_dir)]) assert result.exit_code == 0 - assert calls == ['FIT', 'DISPLAY', 'PLOT_CORR', 'PLOT_exp1_True'] + assert calls == ['FIT', 'DISPLAY', 'PLOT_CORR', 'PLOT_exp1_False'] + + +def test_cli_fit_skips_fit_reports_for_sequential_mode(monkeypatch, tmp_path): + import easydiffraction.__main__ as main_mod + from easydiffraction.project.project import Project + + calls = [] + + class FakeInfo: + _path = '/some/path' + + class FakeExperiment: + name = 'exp1' + + class FakeProject: + info = FakeInfo() + experiments = [FakeExperiment()] + + class _analysis: + fitting_mode_type = 'sequential' + + @staticmethod + def fit(): + calls.append('FIT') + + analysis = _analysis() + + class _display: + class _fit: + @staticmethod + def results(): + calls.append('DISPLAY') + + @staticmethod + def correlations(): + calls.append('PLOT_CORR') + + fit = _fit() + + @staticmethod + def pattern(expt_name, **kwargs): + del kwargs + calls.append(f'PLOT_{expt_name}_False') + + display = _display() + + fake_project = FakeProject() + + proj_dir = tmp_path / 'proj' + proj_dir.mkdir() + (proj_dir / 'project.cif').write_text('_project.id test\n') + + monkeypatch.setattr(Project, 'load', staticmethod(lambda dir_path: fake_project)) + + result = runner.invoke(main_mod.app, ['fit', str(proj_dir)]) + assert result.exit_code == 0 + assert calls == ['FIT', 'PLOT_exp1_False'] def test_cli_fit_dry_clears_path(monkeypatch, tmp_path): @@ -149,16 +230,20 @@ def fit_results(): analysis = _analysis() class _display: - class _plotter: + class _fit: @staticmethod - def plot_param_correlations(): + def results(): pass @staticmethod - def plot_meas_vs_calc(expt_name, *, show_residual=False): + def correlations(): pass - plotter = _plotter() + fit = _fit() + + @staticmethod + def pattern(expt_name, **kwargs): + del expt_name, kwargs display = _display() diff --git a/tests/unit/easydiffraction/utils/test_environment.py b/tests/unit/easydiffraction/utils/test_environment.py index 691b0a9c5..fab45e723 100644 --- a/tests/unit/easydiffraction/utils/test_environment.py +++ b/tests/unit/easydiffraction/utils/test_environment.py @@ -84,3 +84,44 @@ def test_can_use_ipython_display_with_none(self): from easydiffraction.utils.environment import can_use_ipython_display assert can_use_ipython_display(None) is False + + +class TestArtifactPaths: + def test_resolve_artifact_path_uses_env_root(self, monkeypatch, tmp_path): + from easydiffraction.utils.environment import resolve_artifact_path + + monkeypatch.setenv('EASYDIFFRACTION_ARTIFACT_ROOT', 'tmp/tutorials') + monkeypatch.setenv('PIXI_PROJECT_ROOT', str(tmp_path)) + + assert resolve_artifact_path('data') == tmp_path / 'tmp' / 'tutorials' / 'data' + + def test_resolve_artifact_path_uses_tutorial_fallback(self, monkeypatch, tmp_path): + import easydiffraction.utils.environment as env + + repo_root = tmp_path / 'repo' + tutorials_dir = repo_root / 'docs' / 'docs' / 'tutorials' + tutorials_dir.mkdir(parents=True) + + monkeypatch.delenv('EASYDIFFRACTION_ARTIFACT_ROOT', raising=False) + monkeypatch.delenv('PIXI_PROJECT_ROOT', raising=False) + monkeypatch.chdir(tutorials_dir) + monkeypatch.setattr(env, '_repo_root', lambda: repo_root) + + assert env.resolve_artifact_path('data') == repo_root / 'tmp' / 'tutorials' / 'data' + + def test_create_artifact_temp_dir_uses_tutorial_fallback(self, monkeypatch, tmp_path): + import easydiffraction.utils.environment as env + + repo_root = tmp_path / 'repo' + tutorials_dir = repo_root / 'docs' / 'docs' / 'tutorials' + tutorials_dir.mkdir(parents=True) + + monkeypatch.delenv('EASYDIFFRACTION_ARTIFACT_ROOT', raising=False) + monkeypatch.delenv('PIXI_PROJECT_ROOT', raising=False) + monkeypatch.chdir(tutorials_dir) + monkeypatch.setattr(env, '_repo_root', lambda: repo_root) + + created_dir = env.create_artifact_temp_dir('ed_zip_') + + assert created_dir.is_dir() + assert created_dir.parent == repo_root / 'tmp' / 'tutorials' diff --git a/tests/unit/easydiffraction/utils/test_utils.py b/tests/unit/easydiffraction/utils/test_utils.py index 13564ca44..8c3d2e760 100644 --- a/tests/unit/easydiffraction/utils/test_utils.py +++ b/tests/unit/easydiffraction/utils/test_utils.py @@ -142,6 +142,33 @@ def test_render_table_terminal_branch(capsys, monkeypatch): assert ('╒' in out and '╕' in out) or ('┌' in out and '┐' in out) +def test_render_object_help_prints_public_api(capsys): + import easydiffraction.utils.utils as MUT + + class Example: + @property + def value(self): + """Visible value.""" + return 1 + + def run(self): + """Run visible action.""" + + def _hidden(self): + """Hidden action.""" + + MUT.render_object_help(Example()) + out = capsys.readouterr().out + assert "Help for 'Example'" in out + assert 'Properties' in out + assert 'value' in out + assert 'Visible value.' in out + assert 'Methods' in out + assert 'run()' in out + assert 'Run visible action.' in out + assert '_hidden' not in out + + def test_is_dev_version_with_dev_suffix(monkeypatch): import easydiffraction.utils.utils as MUT diff --git a/tests/unit/easydiffraction/utils/test_utils_coverage.py b/tests/unit/easydiffraction/utils/test_utils_coverage.py index 9357e2725..d2d07159e 100644 --- a/tests/unit/easydiffraction/utils/test_utils_coverage.py +++ b/tests/unit/easydiffraction/utils/test_utils_coverage.py @@ -433,6 +433,42 @@ def test_download_data_no_description(monkeypatch, tmp_path, capsys): assert 'Data #1' in out +def test_download_data_uses_tutorial_artifact_root_fallback(monkeypatch, tmp_path): + import easydiffraction.utils.environment as env + import easydiffraction.utils.utils as MUT + + repo_root = tmp_path / 'repo' + tutorials_dir = repo_root / 'docs' / 'docs' / 'tutorials' + tutorials_dir.mkdir(parents=True) + + fake_index = { + '1': { + 'path': 'data.xye', + 'hash': None, + 'description': 'Test data', + } + } + monkeypatch.setattr(MUT, '_fetch_data_index', lambda: fake_index) + monkeypatch.setattr(env, '_repo_root', lambda: repo_root) + monkeypatch.delenv('EASYDIFFRACTION_ARTIFACT_ROOT', raising=False) + monkeypatch.delenv('PIXI_PROJECT_ROOT', raising=False) + monkeypatch.chdir(tutorials_dir) + + def fake_retrieve(url, known_hash, fname, path): + import pathlib + + pathlib.Path(path, fname).write_text('x y e') + return str(pathlib.Path(path, fname)) + + monkeypatch.setattr(MUT.pooch, 'retrieve', fake_retrieve) + + result = MUT.download_data(id=1, destination='data') + + expected_path = repo_root / 'tmp' / 'tutorials' / 'data' / 'ed-1.xye' + assert result == str(expected_path) + assert expected_path.exists() + + # --- download_tutorial with overwrite=True ------------------------------------ diff --git a/tests/unit/test_benchmark_tutorials.py b/tests/unit/test_benchmark_tutorials.py new file mode 100644 index 000000000..c0b390ef7 --- /dev/null +++ b/tests/unit/test_benchmark_tutorials.py @@ -0,0 +1,133 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Unit tests for tutorial benchmark CSV persistence.""" + +from __future__ import annotations + +import csv +import importlib.util +import sys +from argparse import Namespace +from pathlib import Path +from types import SimpleNamespace + + +def _load_module(): + module_path = Path(__file__).resolve().parents[2] / 'tools' / 'benchmark_tutorials.py' + spec = importlib.util.spec_from_file_location('benchmark_tutorials', module_path) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = module + spec.loader.exec_module(module) + return module + + +MUT = _load_module() + + +def test_append_result_writes_one_row(tmp_path): + output_path = tmp_path / 'benchmark.csv' + + MUT._write_csv_header(output_path) + MUT._append_result( + output_path, + MUT.TutorialBenchmarkResult( + tutorial_name='ed-21.py', + elapsed_seconds=12.3456, + status='ok', + return_code=0, + ), + ) + + with output_path.open(encoding='utf-8', newline='') as handle: + rows = list(csv.DictReader(handle)) + + assert rows == [ + { + 'tutorial_name': 'ed-21.py', + 'elapsed_seconds': '12.346', + 'status': 'ok', + 'return_code': '0', + } + ] + + +def test_main_appends_first_result_before_second_tutorial_starts(monkeypatch, tmp_path): + tutorial_dir = tmp_path / 'tutorials' + tutorial_dir.mkdir() + first_tutorial = tutorial_dir / 'ed-01.py' + second_tutorial = tutorial_dir / 'ed-02.py' + first_tutorial.write_text('print("first")\n', encoding='utf-8') + second_tutorial.write_text('print("second")\n', encoding='utf-8') + + output_path = tmp_path / 'benchmarking' / 'results.csv' + args = Namespace( + tutorial_dir=tutorial_dir, + output_dir=tmp_path / 'benchmarking', + pattern=[], + ) + + monkeypatch.setattr( + MUT, + 'build_parser', + lambda: SimpleNamespace(parse_args=lambda: args), + ) + monkeypatch.setattr(MUT, '_build_output_path', lambda output_dir: output_path) + monkeypatch.setattr(MUT, '_build_env', dict) + + def fake_run_tutorial( + script_path: Path, + tutorial_dir_path: Path, + env: dict[str, str], + ) -> MUT.TutorialBenchmarkResult: + del tutorial_dir_path, env + if script_path == first_tutorial: + with output_path.open(encoding='utf-8', newline='') as handle: + rows = list(csv.reader(handle)) + assert rows == [MUT.CSV_HEADER] + return MUT.TutorialBenchmarkResult( + tutorial_name='ed-01.py', + elapsed_seconds=1.0, + status='ok', + return_code=0, + ) + + with output_path.open(encoding='utf-8', newline='') as handle: + rows = list(csv.DictReader(handle)) + assert rows == [ + { + 'tutorial_name': 'ed-01.py', + 'elapsed_seconds': '1.000', + 'status': 'ok', + 'return_code': '0', + } + ] + return MUT.TutorialBenchmarkResult( + tutorial_name='ed-02.py', + elapsed_seconds=2.0, + status='ok', + return_code=0, + ) + + monkeypatch.setattr(MUT, '_run_tutorial', fake_run_tutorial) + + exit_code = MUT.main() + + assert exit_code == 0 + with output_path.open(encoding='utf-8', newline='') as handle: + rows = list(csv.DictReader(handle)) + assert rows == [ + { + 'tutorial_name': 'ed-01.py', + 'elapsed_seconds': '1.000', + 'status': 'ok', + 'return_code': '0', + }, + { + 'tutorial_name': 'ed-02.py', + 'elapsed_seconds': '2.000', + 'status': 'ok', + 'return_code': '0', + }, + ] diff --git a/tools/benchmark_tutorials.py b/tools/benchmark_tutorials.py new file mode 100644 index 000000000..e28d77e98 --- /dev/null +++ b/tools/benchmark_tutorials.py @@ -0,0 +1,216 @@ +"""Benchmark tutorial scripts and save timing results.""" + +from __future__ import annotations + +import argparse +import csv +import os +import platform +import subprocess # noqa: S404 +import sys +import time +from dataclasses import dataclass +from datetime import datetime +from pathlib import Path +from pathlib import PurePosixPath + +ROOT = Path(__file__).resolve().parents[1] +SRC_ROOT = ROOT / 'src' +DEFAULT_TUTORIAL_DIR = ROOT / 'docs' / 'docs' / 'tutorials' +DEFAULT_OUTPUT_DIR = ROOT / 'docs' / 'dev' / 'benchmarking' +CHECKPOINT_DIR_NAME = '.ipynb_checkpoints' +CSV_HEADER = ['tutorial_name', 'elapsed_seconds', 'status', 'return_code'] + + +@dataclass(frozen=True) +class TutorialBenchmarkResult: + """Store timing data for one tutorial run.""" + + tutorial_name: str + elapsed_seconds: float + status: str + return_code: int + + +def _relative_display_path(path: Path, start_path: Path) -> str: + try: + return path.relative_to(start_path).as_posix() + except ValueError: + return path.as_posix() + + +def _slugify(value: str) -> str: + return value.lower().replace(' ', '-').replace('/', '-') + + +def _build_env() -> dict[str, str]: + env = os.environ.copy() + if SRC_ROOT.exists(): + existing_pythonpath = env.get('PYTHONPATH', '') + env['PYTHONPATH'] = ( + str(SRC_ROOT) + if not existing_pythonpath + else str(SRC_ROOT) + os.pathsep + existing_pythonpath + ) + return env + + +def _discover_tutorials(tutorial_dir: Path) -> list[Path]: + return [ + path + for path in sorted(tutorial_dir.rglob('*.py')) + if CHECKPOINT_DIR_NAME not in path.parts + ] + + +def _matches_requested_patterns( + script_path: Path, + tutorial_dir: Path, + patterns: list[str], +) -> bool: + if not patterns: + return True + + rel_path = PurePosixPath(_relative_display_path(script_path, tutorial_dir)) + return any(rel_path.match(pattern) or script_path.name == pattern for pattern in patterns) + + +def _run_tutorial( + script_path: Path, + tutorial_dir: Path, + env: dict[str, str], +) -> TutorialBenchmarkResult: + tutorial_name = _relative_display_path(script_path, tutorial_dir) + start_time = time.perf_counter() + result = subprocess.run( # noqa: S603 + [sys.executable, str(script_path)], + cwd=str(ROOT), + env=env, + capture_output=True, + text=True, + encoding='utf-8', + ) + elapsed_seconds = time.perf_counter() - start_time + status = 'ok' if result.returncode == 0 else 'failed' + + if result.returncode == 0: + print(f' OK {elapsed_seconds:.1f}s') + else: + print(f' FAILED {elapsed_seconds:.1f}s', file=sys.stderr) + details = ((result.stdout or '') + (result.stderr or '')).strip() + if details: + print(details, file=sys.stderr) + + return TutorialBenchmarkResult( + tutorial_name=tutorial_name, + elapsed_seconds=elapsed_seconds, + status=status, + return_code=result.returncode, + ) + + +def _build_output_path(output_dir: Path) -> Path: + timestamp = datetime.now().astimezone().strftime('%Y%m%d-%H%M%S') + system_name = _slugify(platform.system()) + machine_name = _slugify(platform.machine()) + python_name = f'py{sys.version_info.major}{sys.version_info.minor}' + file_name = ( + f'{timestamp}_{system_name}-{machine_name}_' + f'{python_name}_tutorial-benchmarks.csv' + ) + return output_dir / file_name + + +def _write_csv_header(output_path: Path) -> None: + output_path.parent.mkdir(parents=True, exist_ok=True) + with output_path.open('w', encoding='utf-8', newline='') as handle: + writer = csv.writer(handle) + writer.writerow(CSV_HEADER) + + +def _append_result(output_path: Path, result: TutorialBenchmarkResult) -> None: + with output_path.open('a', encoding='utf-8', newline='') as handle: + writer = csv.writer(handle) + writer.writerow( + [ + result.tutorial_name, + f'{result.elapsed_seconds:.3f}', + result.status, + result.return_code, + ] + ) + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + description='Run tutorial scripts sequentially and record timings.', + ) + parser.add_argument( + '--tutorial-dir', + type=Path, + default=DEFAULT_TUTORIAL_DIR, + help='Directory containing tutorial scripts to benchmark.', + ) + parser.add_argument( + '--output-dir', + type=Path, + default=DEFAULT_OUTPUT_DIR, + help='Directory where the benchmark CSV should be written.', + ) + parser.add_argument( + '--pattern', + action='append', + default=[], + help=( + 'Glob for tutorial paths relative to the tutorial directory. ' + 'Pass multiple times to benchmark a subset.' + ), + ) + return parser + + +def main() -> int: + args = build_parser().parse_args() + tutorial_dir = args.tutorial_dir.resolve() + output_dir = args.output_dir.resolve() + + if not tutorial_dir.is_dir(): + print(f'Tutorial directory not found: {tutorial_dir}', file=sys.stderr) + return 1 + + tutorials = [ + path + for path in _discover_tutorials(tutorial_dir) + if _matches_requested_patterns(path, tutorial_dir, args.pattern) + ] + if not tutorials: + print('No tutorial scripts matched the requested pattern(s).', file=sys.stderr) + return 1 + + output_path = _build_output_path(output_dir) + _write_csv_header(output_path) + + env = _build_env() + results: list[TutorialBenchmarkResult] = [] + for index, tutorial_path in enumerate(tutorials, start=1): + tutorial_name = _relative_display_path(tutorial_path, tutorial_dir) + print(f'[{index:2}/{len(tutorials)}] Running {tutorial_name}') + result = _run_tutorial(tutorial_path, tutorial_dir, env) + results.append(result) + _append_result(output_path, result) + + total_elapsed = sum(result.elapsed_seconds for result in results) + failure_count = sum(result.status == 'failed' for result in results) + + print(f'Wrote benchmark results to {_relative_display_path(output_path, ROOT)}') + print(f'Total elapsed time: {total_elapsed:.3f}s') + + if failure_count: + print(f'Failed tutorials: {failure_count}', file=sys.stderr) + return 1 + + return 0 + + +if __name__ == '__main__': + raise SystemExit(main()) \ No newline at end of file diff --git a/tools/generate_package_docs.py b/tools/generate_package_docs.py index 671a4f0ec..78915c2f7 100644 --- a/tools/generate_package_docs.py +++ b/tools/generate_package_docs.py @@ -3,9 +3,9 @@ """Generate project package structure markdown files. -Outputs two docs under docs/architecture/: - - package-structure-short.md (folders/files only) - - package-structure-full.md (folders/files and classes) +Outputs two docs under docs/dev/package-structure/: + - short.md (folders/files only) + - full.md (folders/files and classes) Run (from repo root): pixi run python tools/generate_package_docs.py @@ -21,7 +21,7 @@ REPO_ROOT = Path(__file__).resolve().parents[1] SRC_ROOT = REPO_ROOT / 'src' / 'easydiffraction' -DOCS_OUT_DIR = REPO_ROOT / 'docs' / 'architecture' +DOCS_OUT_DIR = REPO_ROOT / 'docs' / 'dev' / 'package-structure' IGNORE_DIRS = { @@ -132,8 +132,8 @@ def _render(node: Node, prefix: str = '') -> None: def write_markdown(short_lines: List[str], full_lines: List[str]) -> None: DOCS_OUT_DIR.mkdir(parents=True, exist_ok=True) - short_md = DOCS_OUT_DIR / 'package-structure-short.md' - full_md = DOCS_OUT_DIR / 'package-structure-full.md' + short_md = DOCS_OUT_DIR / 'short.md' + full_md = DOCS_OUT_DIR / 'full.md' short_content = [ '# Package Structure (short)', diff --git a/tools/param_consistency.py b/tools/param_consistency.py index 1c459e055..a9d266bae 100644 --- a/tools/param_consistency.py +++ b/tools/param_consistency.py @@ -8,8 +8,8 @@ python param_consistency.py --fix python param_consistency.py src/mypackage/ --check -Template (see architecture.md §9.8 for the full spec) ------------------------------------------------------- +Template (see docs/dev/adrs/accepted/property-docstring-template.md) +----------------------------------------------------------------------- Given ``description='Length of the a axis of the unit cell.'``, ``units='Å'``, and type ``Parameter``: